import { css, html } from "lit"; import { BaseElement } from "../../core/base-element.ts"; import { initialsForName } from "../cf-avatar/index.ts"; import type { CfcLabelView } from "@commonfabric/runner/cfc"; export type CfcAuthorshipState = "verified" | "unverified" | "unknown"; type CfcLabelQueryableValue = { getCfcLabel(): Promise; }; type CfcLabelResolvableValue = { resolveAsCell(): Promise; }; type CfcLabelSubscribableValue = { subscribe( callback: (value: unknown, cfcLabel?: CfcLabelView | undefined) => void, options?: { includeCfcLabel?: boolean }, ): () => void; }; type CfcReadableClaimValue = { get?(): unknown; sync?(): Promise; resolveAsCell?(): Promise | unknown; }; const DEFAULT_AUTHORSHIP_KIND = "authored-by"; // Poll cadence for re-reading a label whose resolved cell wasn't loaded yet. // Mirrors cf-cfc-label's retry-on-undefined; bounded so a resolved cell that // genuinely never carries a label stops retrying. const LABEL_RETRY_INTERVAL_MS = 100; const MAX_LABEL_RETRY_COUNT = 100; const AUTHOR_FIELDS = [ "subject", "author", "authorId", "sender", "senderId", "user", "userId", "id", ] as const; const AUTHOR_DISPLAY_FIELDS = [ "name", "displayName", "fullName", "label", "username", ] as const; const hasLabelQuery = (value: unknown): value is CfcLabelQueryableValue => typeof value === "object" && value !== null && "getCfcLabel" in value && typeof (value as { getCfcLabel?: unknown }).getCfcLabel === "function"; const hasLabelSubscription = ( value: unknown, ): value is CfcLabelSubscribableValue => typeof value === "object" && value !== null && "subscribe" in value && typeof (value as { subscribe?: unknown }).subscribe === "function"; const hasLabelResolution = ( value: unknown, ): value is CfcLabelResolvableValue => typeof value === "object" && value !== null && "resolveAsCell" in value && typeof (value as { resolveAsCell?: unknown }).resolveAsCell === "function"; const hasReadableClaim = ( value: unknown, ): value is CfcReadableClaimValue => typeof value === "object" && value !== null && (typeof (value as { get?: unknown }).get === "function" || typeof (value as { sync?: unknown }).sync === "function"); const labelHasRootIntegrityKind = ( view: CfcLabelView, kind: string, ): boolean => view.entries.some((entry) => entry.path.length === 0 && (entry.label.integrity ?? []).some((atom) => { if (typeof atom === "string") { return atom.startsWith(`${kind}:`); } if ( typeof atom !== "object" || atom === null || Array.isArray(atom) ) { return false; } return (atom as Record).kind === kind; }) ); const mergeLabelViews = ( ...views: Array ): CfcLabelView | undefined => { const entries: CfcLabelView["entries"] = []; const seen = new Set(); for (const view of views) { if (view === undefined) { continue; } for (const entry of view.entries) { const key = JSON.stringify(entry); if (seen.has(key)) { continue; } seen.add(key); entries.push(entry); } } return entries.length === 0 ? undefined : { version: 1, entries }; }; const isConcreteAuthorClaim = (value: unknown): boolean => { if ( typeof value === "string" || typeof value === "number" || typeof value === "boolean" ) { return true; } if (typeof value !== "object" || value === null || Array.isArray(value)) { return false; } const record = value as Record; return AUTHOR_FIELDS.some((field) => { const fieldValue = record[field]; return typeof fieldValue === "string" || typeof fieldValue === "number" || typeof fieldValue === "boolean"; }); }; const readClaimValue = async ( value: CfcReadableClaimValue, ): Promise => { const readCandidate = async (candidate: unknown): Promise => { if (!hasReadableClaim(candidate)) { return isConcreteAuthorClaim(candidate) ? candidate : undefined; } const beforeSync = candidate.get?.(); if (beforeSync !== undefined) { return beforeSync; } const synced = typeof candidate.sync === "function" ? await candidate.sync() : undefined; if (synced !== undefined && synced !== candidate) { const syncedClaim = await readCandidate(synced); if (syncedClaim !== undefined) { return syncedClaim; } } return candidate.get?.(); }; const directClaim = await readCandidate(value); if (directClaim !== undefined) { return directClaim; } if (typeof value.resolveAsCell === "function") { const resolved = await value.resolveAsCell(); return await readCandidate(resolved); } return undefined; }; interface LabelViewResult { readonly view: CfcLabelView | undefined; /** * True when the fallback `resolveAsCell()` path read a resolved cell's label * and got nothing back. `getCfcLabel` is a pure, non-blocking store read, so * an empty result means the resolved cell's doc is not loaded yet. This * component subscribes to `value`/`author`, NOT to that internally-resolved * cell, so its load would not re-trigger this read — the caller retries until * it lands (same liveness contract cf-cfc-label gets from its undefined-retry). */ readonly pendingResolution: boolean; } const readLabelView = async ( value: unknown, requiredRootIntegrityKind?: string, ): Promise => { let direct: CfcLabelView | undefined; if (hasLabelQuery(value)) { direct = await value.getCfcLabel(); } if ( direct !== undefined && requiredRootIntegrityKind !== undefined && labelHasRootIntegrityKind(direct, requiredRootIntegrityKind) ) { return { view: direct, pendingResolution: false }; } let resolvedLabel: CfcLabelView | undefined; let pendingResolution = false; if (hasLabelResolution(value)) { const resolved = await value.resolveAsCell(); if (hasLabelQuery(resolved)) { resolvedLabel = await resolved.getCfcLabel(); pendingResolution = resolvedLabel === undefined; } } return { view: mergeLabelViews(direct, resolvedLabel), pendingResolution }; }; const primitiveToString = (value: unknown): string | undefined => { if (typeof value === "string") { return value; } if (typeof value === "number" || typeof value === "boolean") { return String(value); } return undefined; }; const objectField = ( value: Record, field: string, ): string | undefined => primitiveToString(value[field]); const objectStringFields = ( value: unknown, fields: readonly string[], ): string[] => { if (typeof value !== "object" || value === null || Array.isArray(value)) { return []; } const record = value as Record; return fields.flatMap((field) => { const fieldValue = objectField(record, field); return fieldValue === undefined ? [] : [fieldValue]; }); }; const uniqueStrings = (values: readonly string[]): string[] => [ ...new Set(values), ]; const authorIdsForClaim = (author: unknown): string[] => { const primitive = primitiveToString(author); if (primitive !== undefined) { return [primitive]; } return uniqueStrings(objectStringFields(author, AUTHOR_FIELDS)); }; const primaryAuthorId = (author: unknown): string | undefined => authorIdsForClaim(author)[0]; const authorDisplayName = (author: unknown): string | undefined => objectStringFields(author, AUTHOR_DISPLAY_FIELDS)[0]; const representsPrincipalSubjectForLabel = ( view: CfcLabelView | undefined, ): string | undefined => { if (!view) { return undefined; } for (const entry of rootEntries(view)) { for (const atom of entry.label.integrity ?? []) { if (typeof atom !== "object" || atom === null || Array.isArray(atom)) { continue; } const atomRecord = atom as Record; if (objectField(atomRecord, "kind") !== "represents-principal") { continue; } const subject = objectField(atomRecord, "subject"); if (subject !== undefined) { return subject; } } } return undefined; }; const principalAuthorClaim = ( subject: string | undefined, displayName: string | undefined, ): unknown | undefined => { if (subject === undefined) { return undefined; } return { subject, ...(displayName !== undefined ? { name: displayName } : {}), }; }; export const integrityAtomMatchesAuthor = ( atom: unknown, author: unknown, kind: string = DEFAULT_AUTHORSHIP_KIND, ): boolean => { const authorIds = authorIdsForClaim(author); if (authorIds.length === 0) { return false; } if (typeof atom === "string") { return authorIds.some((authorId) => atom === `${kind}:${authorId}`); } if (typeof atom !== "object" || atom === null || Array.isArray(atom)) { return false; } const atomRecord = atom as Record; if (objectField(atomRecord, "kind") !== kind) { return false; } return AUTHOR_FIELDS.some((field) => { const atomAuthor = objectField(atomRecord, field); return atomAuthor !== undefined && authorIds.includes(atomAuthor); }); }; const rootEntries = (view: CfcLabelView) => view.entries.filter((entry) => entry.path.length === 0); const hasAuthorshipIntegrity = ( entries: ReturnType, kind: string, ): boolean => entries.some((entry) => (entry.label.integrity ?? []).some((atom) => typeof atom === "string" ? atom.startsWith(`${kind}:`) : typeof atom === "object" && atom !== null && !Array.isArray(atom) && objectField(atom as Record, "kind") === kind ) ); export const authorshipStateForLabel = ( view: CfcLabelView | undefined, author: unknown, kind: string = DEFAULT_AUTHORSHIP_KIND, ): CfcAuthorshipState => { if (!view || authorIdsForClaim(author).length === 0) { return "unknown"; } const entries = rootEntries(view); for (const entry of entries) { const integrity = entry.label.integrity; if (!Array.isArray(integrity)) { continue; } if ( integrity.some((atom) => integrityAtomMatchesAuthor(atom, author, kind)) ) { return "verified"; } } return hasAuthorshipIntegrity(entries, kind) ? "unverified" : "unknown"; }; /** * Shows trusted authorship state for a bound CFC-labeled content cell. * * The component certifies a bound `value` against the bound author claim. It * cannot inspect arbitrary slotted DOM: callers should slot the UI block that * renders the same bound value and author claim so the badge and rendered * content remain adjacent. * * @element cf-cfc-authorship * * @prop {unknown} value - Usually supplied via `$value`; queried for CFC label IPC. * @prop {unknown} author - Claimed author id/object, or a `$author`-bound claim cell. * @prop {unknown} authorName - Optional untrusted display fallback. * @prop {unknown} avatar - Optional avatar image URL shown only when verified. * @prop {boolean} verifyTextIntegrity - Require visible descendant text to * match the authorship claim. * @prop {boolean} allowLiteralText - Allow literal descendant text under text * integrity verification. * @prop {"ok"|"blocked"} textIntegrityState - Renderer-reported descendant text * integrity state. * @attr {string} kind - Integrity object kind; defaults to `authored-by`. */ export class CFCFCAuthorship extends BaseElement { static override styles = [ BaseElement.baseStyles, css` :host { display: block; color: var(--cf-theme-color-text, hsl(220, 14%, 12%)); font-size: 0.875rem; } .authorship { display: grid; grid-template-columns: minmax(0, auto) minmax(0, 1fr); gap: 0.75rem; align-items: start; } :host([badge-placement="end"]) .authorship, :host([data-badge-placement="end"]) .authorship { grid-template-columns: minmax(0, 1fr) minmax(0, auto); } .badge { display: inline-grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.5rem; align-items: center; min-width: 10rem; padding: 0.5rem 0.625rem; border-radius: 999px; border: 1px solid var(--cf-theme-color-border, hsl(220, 14%, 86%)); background: var(--cf-theme-color-surface, hsl(220, 20%, 98%)); } :host([badge-placement="end"]) .badge, :host([data-badge-placement="end"]) .badge { grid-column: 2; grid-row: 1; grid-template-columns: minmax(0, 1fr) auto; } :host([badge-placement="end"]) .avatar, :host([badge-placement="end"]) .status-dot, :host([data-badge-placement="end"]) .avatar, :host([data-badge-placement="end"]) .status-dot { grid-column: 2; } :host([badge-placement="end"]) .label, :host([data-badge-placement="end"]) .label { grid-column: 1; grid-row: 1; text-align: right; } .authorship.verified .badge { border-color: var(--cf-authorship-verified-border, hsl(155, 48%, 58%)); background: var(--cf-authorship-verified-bg, hsl(151, 58%, 95%)); } .authorship.unverified .badge { border-color: var(--cf-authorship-unverified-border, hsl(24, 82%, 64%)); background: var(--cf-authorship-unverified-bg, hsl(34, 100%, 96%)); } .authorship.unknown .badge { border-color: var(--cf-theme-color-border, hsl(220, 14%, 86%)); background: var(--cf-theme-color-muted, hsl(220, 18%, 96%)); } .avatar, .status-dot { display: inline-grid; place-items: center; width: 2rem; height: 2rem; border-radius: 999px; overflow: hidden; } .avatar { color: var(--cf-authorship-avatar-text, hsl(155, 65%, 16%)); background: var(--cf-authorship-avatar-bg, hsl(155, 55%, 84%)); font-weight: 700; letter-spacing: 0.02em; } .avatar img { width: 100%; height: 100%; object-fit: cover; } .status-dot { border: 1px dashed var(--cf-theme-color-border, hsl(220, 14%, 72%)); color: var(--cf-theme-color-text-muted, hsl(220, 10%, 44%)); font-weight: 700; } .label { display: grid; min-width: 0; line-height: 1.25; } .state { font-weight: 700; } .author { color: var(--cf-theme-color-text-muted, hsl(220, 10%, 44%)); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .content { min-width: 0; } :host([badge-placement="end"]) .content, :host([data-badge-placement="end"]) .content { grid-column: 1; grid-row: 1; } `, ]; static override properties = { value: { attribute: false }, cfcLabel: { attribute: false }, author: { attribute: false }, authorName: { attribute: false }, avatar: { attribute: false }, badgePlacement: { type: String, attribute: "badge-placement", reflect: true, }, kind: { type: String }, verifyTextIntegrity: { type: Boolean, attribute: "verify-text-integrity", }, allowLiteralText: { type: Boolean, attribute: "allow-literal-text", }, textIntegrityState: { type: String, attribute: "text-integrity-state", reflect: true, }, }; declare cfcLabel: CfcLabelView | undefined; declare authorName: unknown; declare avatar: unknown; declare badgePlacement: "start" | "end"; declare kind: string | undefined; declare verifyTextIntegrity: boolean; declare allowLiteralText: boolean; declare textIntegrityState: "ok" | "blocked"; private _labelRequestId = 0; private _authorRequestId = 0; private _value: unknown = undefined; private _author: unknown = undefined; private _authorClaim: unknown = undefined; private _observedValue: unknown = undefined; private _observedAuthor: unknown = undefined; private _unsubscribeValue: (() => void) | undefined; private _unsubscribeAuthor: (() => void) | undefined; private _labelRetryTimeout: ReturnType | undefined; private _labelRetryCount = 0; private _valueResolutionPending = false; private _authorResolutionPending = false; constructor() { super(); this.cfcLabel = undefined; this.authorName = undefined; this.avatar = undefined; this.badgePlacement = "start"; this.kind = DEFAULT_AUTHORSHIP_KIND; this.verifyTextIntegrity = false; this.allowLiteralText = false; this.textIntegrityState = "ok"; } get value(): unknown { return this._value; } set value(next: unknown) { const previous = this._value; this._value = next; this.requestUpdate("value", previous); this.refreshForCurrentValue(); } get author(): unknown { return this._author; } set author(next: unknown) { const previous = this._author; this._author = next; this.requestUpdate("author", previous); this.refreshForCurrentAuthor(); } get authorshipState(): CfcAuthorshipState { const labelState = authorshipStateForLabel( this.cfcLabel, this.authorClaim, this.kind ?? DEFAULT_AUTHORSHIP_KIND, ); if ( labelState === "verified" && this.verifyTextIntegrity && this.textIntegrityState === "blocked" ) { return "unverified"; } return labelState; } get authorClaim(): unknown { return hasReadableClaim(this.author) || hasLabelQuery(this.author) || hasLabelResolution(this.author) ? this._authorClaim : this.author; } override connectedCallback() { super.connectedCallback(); this.refreshForCurrentValue(); this.refreshForCurrentAuthor(); } override disconnectedCallback() { this.clearValueSubscription(); this.clearAuthorSubscription(); this.clearLabelRetry(); super.disconnectedCallback(); } protected override firstUpdated( changedProperties: Map, ) { super.firstUpdated(changedProperties); this.refreshForCurrentValue(); this.refreshForCurrentAuthor(); } private refreshForCurrentValue(): void { const hasSubscription = this.observeValue(this.value); if (!hasSubscription) { void this.refreshLabel(); } } private refreshForCurrentAuthor(): void { this.observeAuthor(this.author); void this.refreshAuthorClaim(); } private observeValue(value: unknown): boolean { if (Object.is(value, this._observedValue)) { return this._unsubscribeValue !== undefined; } this.clearValueSubscription(); this._observedValue = value; // New value → fresh retry budget for its (possibly cold) resolved label. this._labelRetryCount = 0; if (!hasLabelSubscription(value)) { return false; } // includeCfcLabel makes the worker read this cell's label (and its // one-hop link target's) on the sink's tracked tx, so a label-only change // re-fires this subscription and refreshLabel re-reads the new label — the // resolved-cell label is now reactive, not just polled. this._unsubscribeValue = value.subscribe(() => { void this.refreshLabel(); }, { includeCfcLabel: true }); return true; } private clearValueSubscription(): void { this._unsubscribeValue?.(); this._unsubscribeValue = undefined; this._observedValue = undefined; } private observeAuthor(author: unknown): boolean { if (Object.is(author, this._observedAuthor)) { return this._unsubscribeAuthor !== undefined; } this.clearAuthorSubscription(); this._observedAuthor = author; // New author → fresh retry budget for its (possibly cold) resolved label. this._labelRetryCount = 0; if (!hasLabelSubscription(author)) { return false; } this._unsubscribeAuthor = author.subscribe((claim) => { if (hasLabelQuery(author) || hasLabelResolution(author)) { void this.refreshAuthorClaim(); return; } const previous = this._authorClaim; this._authorClaim = claim; this.requestUpdate("author", previous); }, { includeCfcLabel: true }); return true; } private clearAuthorSubscription(): void { this._unsubscribeAuthor?.(); this._unsubscribeAuthor = undefined; this._observedAuthor = undefined; } async refreshLabel(): Promise { const requestId = ++this._labelRequestId; let view: typeof this.cfcLabel; let pendingResolution: boolean; try { ({ view, pendingResolution } = await readLabelView( this.value, this.kind ?? DEFAULT_AUTHORSHIP_KIND, )); } catch { // This runs fire-and-forget (void this.refreshLabel()). A disposal race // (logout, runtime swap) cancels the read; leave the label as-is rather // than leaking an unhandled rejection — matching refreshAuthorClaim. return; } if (requestId === this._labelRequestId) { const previous = this.cfcLabel; this.cfcLabel = view; this.requestUpdate("cfcLabel", previous); this._valueResolutionPending = pendingResolution; this.reconcileLabelRetry(); } } async refreshAuthorClaim(): Promise { const requestId = ++this._authorRequestId; const author = this.author; const canReadAuthor = hasReadableClaim(author); if ( !canReadAuthor && !hasLabelQuery(author) && !hasLabelResolution(author) ) { const previous = this._authorClaim; this._authorClaim = undefined; this.requestUpdate("author", previous); this._authorResolutionPending = false; this.reconcileLabelRetry(); return; } let authorClaim: unknown; let pendingResolution = false; try { const valueClaim = canReadAuthor ? await readClaimValue(author) : undefined; const profile = await readLabelView(author, "represents-principal"); pendingResolution = profile.pendingResolution; const profileSubject = representsPrincipalSubjectForLabel(profile.view); authorClaim = principalAuthorClaim( profileSubject, authorDisplayName(valueClaim) ?? primitiveToString(this.authorName), ) ?? valueClaim; } catch { authorClaim = undefined; } if (requestId === this._authorRequestId) { const previous = this._authorClaim; this._authorClaim = authorClaim; this.requestUpdate("author", previous); this._authorResolutionPending = pendingResolution; this.reconcileLabelRetry(); } } // Re-read the label(s) while a resolved cell's doc is still loading. The // resolved cell is queried one-shot inside `readLabelView` and is not // subscribed to, so without this poll a cold linked/bound-prop author would // stay unverified until an unrelated `value`/`author` change happened to // re-run the read. Bounded by MAX_LABEL_RETRY_COUNT. private reconcileLabelRetry(): void { if (this._valueResolutionPending || this._authorResolutionPending) { this.scheduleLabelRetry(); } else { this.clearLabelRetry(); this._labelRetryCount = 0; } } private scheduleLabelRetry(): void { if ( !this.isConnected || this._labelRetryTimeout !== undefined || this._labelRetryCount >= MAX_LABEL_RETRY_COUNT ) { return; } this._labelRetryTimeout = setTimeout(() => { this._labelRetryTimeout = undefined; if (!this.isConnected) return; this._labelRetryCount += 1; void this.refreshLabel(); void this.refreshAuthorClaim(); }, LABEL_RETRY_INTERVAL_MS); } private clearLabelRetry(): void { if (this._labelRetryTimeout !== undefined) { clearTimeout(this._labelRetryTimeout); this._labelRetryTimeout = undefined; } } private renderAvatar(state: CfcAuthorshipState) { if (state !== "verified") { return html` ! `; } const authorName = authorDisplayName(this.authorClaim) ?? primaryAuthorId(this.authorClaim); const avatar = primitiveToString(this.avatar); return html` ${avatar ? html` ` : initialsForName(authorName)} `; } override render() { const state = this.authorshipState; const claimLabel = authorDisplayName(this.authorClaim) ?? primaryAuthorId(this.authorClaim); const authorLabel = state === "verified" ? claimLabel ?? "unknown author" : claimLabel ?? primitiveToString(this.authorName) ?? "unknown author"; const stateLabel = state === "verified" ? "Verified author" : state === "unverified" ? "Unverified author" : "Unknown author"; return html` ${this.renderAvatar(state)} ${stateLabel} ${authorLabel} `; } } declare global { interface HTMLElementTagNameMap { "cf-cfc-authorship": CFCFCAuthorship; } }