/** Trusted owner predicate derived from runtime identity and origin attestation. */ import type { CfcLabelView } from "@commonfabric/runner/cfc"; import { cfcLabelViewsEqual } from "@commonfabric/runner/cfc/label-view-core"; import type { JSONSchema } from "@commonfabric/runner/shared"; import type { CellHandle, RuntimeClient } from "@commonfabric/runtime-client"; import { consume } from "@lit/context"; import { html, type PropertyValues } from "lit"; import { property } from "lit/decorators.js"; import { BaseElement } from "../../core/base-element.ts"; import { runtimeContext } from "../../runtime-context.ts"; import { attestedOwnerPrincipal } from "./owner-predicate.ts"; /** * The schema the element subscribes to its origin under. It accepts any JSON * value, and the element reads none: an object is read at its shape, an array * at its elements' shapes, and nothing an object holds is read or followed. It * has to accept every value, since a read that matched nothing would deliver * nothing, and the label rides the delivery. A schema of its own gives the * element a subscription of its own: the client shares one subscription among * the handles on one cell and schema, and the first of them decides whether it * carries labels, so a handle that subscribed first for the value alone would * keep label-only changes from this element. */ const ORIGIN_LABEL_SCHEMA = { description: "The origin whose label cf-owner-view follows.", $ref: "#/$defs/Shallow", $defs: { Shallow: { anyOf: [ { type: "object", properties: {} }, { type: "array", items: { $ref: "#/$defs/Shallow" } }, { type: ["string", "number", "integer", "boolean", "null"] }, ], }, }, } as const satisfies JSONSchema; /** Publishes a presentation predicate after checking a runtime-attested owner. */ export class CFOwnerView extends BaseElement { @consume({ context: runtimeContext, subscribe: true }) @property({ attribute: false }) accessor runtime: RuntimeClient | undefined; /** Persisted creator identity cell; its CFC label supplies the attestation. */ @property({ attribute: false }) accessor originator: CellHandle | undefined; /** Per-user presentation state; null means the owner is unverified. */ @property({ attribute: false }) accessor result: CellHandle | undefined; #generation = 0; /** Whether the current binding's reset has landed, so it may be decided. */ #reset = false; /** * The label the current binding last wrote a decision for, or undefined * when none stands. The reset stands for the decision no label makes. */ #decidedFrom: { label: CfcLabelView | undefined } | undefined; #followed: CellHandle | undefined; #stopFollowing: (() => void) | undefined; /** The label the followed origin's subscription last delivered. */ #label: CfcLabelView | undefined; /** Whether the element was disconnected and has not reconnected since. */ #disconnected = false; override willUpdate(changed: PropertyValues): void { super.willUpdate(changed); if ( changed.has("runtime") || changed.has("originator") || changed.has("result") ) { void this.refresh(); } } /** * Follows the origin again after a reconnect with no property change, as a * move to another parent makes, which runs no update. A decision already * published stands until the origin's current label decides otherwise. */ override connectedCallback(): void { super.connectedCallback(); if (!this.#disconnected) return; this.#disconnected = false; if (!this.#reset) { void this.refresh(); return; } this.#follow(this.originator); void this.#decideFrom(++this.#generation, this.#label); } override disconnectedCallback(): void { this.#generation++; this.#disconnected = true; this.#follow(undefined); super.disconnectedCallback(); } /** * Rechecks the origin when its binding or runtime changes: closes the * presentation, decides from the origin's label, and decides again each * time the origin's subscription delivers an update. The label is what the * store holds, which can lag the binding, as when the origin's document is * not loaded yet, or is rolled back while the piece's start is retried. */ async refresh(): Promise { const generation = ++this.#generation; const { result } = this; this.#reset = false; this.#decidedFrom = undefined; this.#follow(this.isConnected ? this.originator : undefined); if (!result) return; try { await result.setStrict(null); } catch { // A refused reset cannot establish a fresh owner decision. return; } if (generation !== this.#generation) return; this.#reset = true; this.#decidedFrom = { label: undefined }; await this.#decideFrom(generation, this.#label); } #follow(originator: CellHandle | undefined): void { if (originator === this.#followed) return; this.#stopFollowing?.(); this.#stopFollowing = undefined; this.#followed = originator; this.#label = undefined; if (typeof originator?.subscribe !== "function") return; const labelled = originator.asSchema(ORIGIN_LABEL_SCHEMA); this.#stopFollowing = labelled.subscribe((_value, cfcLabel) => { this.#label = cfcLabel; if (this.#reset) void this.#decideFrom(++this.#generation, cfcLabel); }, { includeCfcLabel: true, // An origin the worker will not show attests no owner: the // presentation closes, decided from no label, without reading one. onRefused: () => { this.#label = undefined; if (this.#reset) this.#decide(++this.#generation, undefined); }, }); } /** * Decides from `label`, the one an update of the origin delivered, or from * a read of the origin's label when it delivered none. A subscription can * deliver no label even when the cell has one, as when another handle on * the same cell subscribed first for its value alone. */ async #decideFrom( generation: number, label: CfcLabelView | undefined, ): Promise { if (label === undefined) { try { label = await this.originator?.getCfcLabel(); } catch { // Missing or unreadable attestation keeps the presentation closed. } } this.#decide(generation, label); } /** * Writes the decision `label` supports, once for each label this binding * decides from, whatever happens to `result` meanwhile: a label equal to * the one the standing decision came from writes nothing, and a changed one * writes its decision even when it is the same as before. A label from an * origin other than the bound one, as an update the old origin delivers * before the element follows a new one, decides nothing. */ #decide(generation: number, label: CfcLabelView | undefined): void { const { runtime, originator, result } = this; if ( !runtime || !originator || !result || !this.isConnected || generation !== this.#generation || originator !== this.#followed || (this.#decidedFrom !== undefined && cfcLabelViewsEqual(this.#decidedFrom.label, label)) ) return; const decidedFrom = { label }; this.#decidedFrom = decidedFrom; let decision: boolean | null = null; try { const owner = attestedOwnerPrincipal(label); const actor = runtime.actingPrincipalDid(); if (owner && actor) decision = owner === actor; } catch { // Missing or unreadable attestation keeps the presentation closed. } result.setStrict(decision).catch(() => { // A refused write stands for no decision, so the origin's next update // decides again. if (this.#decidedFrom === decidedFrom) this.#decidedFrom = undefined; }); } override render() { return html``; } }