import { type Cell, cellRuntime, type IExtendedStorageTransaction, } from "@commonfabric/runner"; import { stampExternalIngest } from "@commonfabric/runner/cfc"; import { sha256 } from "@commonfabric/content-hash"; import { toUnpaddedBase64url } from "@commonfabric/utils/base64url"; import { cloneIfNecessary } from "@commonfabric/data-model"; import type { FabricValue } from "@commonfabric/api"; /** * The one durable-write path for the fabric's external-ingest edges. An outside * service with no DID deposits data into the fabric; a vouched ingest channel * marks every value it writes with an unforgeable `ExternalIngest` provenance * stamp. * * This replaces the per-route, hand-rolled durable-write blocks * (`oauth2-common.utils.ts` persistTokens/clearAuthData, the divergent * non-retry `plaid-oauth.utils.ts` saveAuthData) with a single governed write. * `custodyIngest.*` adds the provenance mark for the ingest edges; `durableSet` * / `durableUpdate` are the same governed write for operator actions that are * NOT ingest (clearing tokens, removing an item), which must not carry the mark. * * NOT yet migrated: the webhook path still hand-rolls its own fire-and-forget * `sendToStream` (a transient, unmarked overwrite — see `webhooks.utils.ts`). * Moving webhooks onto a marked, durable trail is the planned `stream`-sink * follow-on of the ingest-channel work, not something this file already does — * see docs/plans/ingest-channels-journal-sink.md. * * The split-mint runs here: the payload is written under the ordinary member * identity (so the runtime gate strips any provenance atom an attacker smuggled * into it), while `stampExternalIngest` hands the commit pipeline only the * *verified channel metadata* — channel, audience, receive time, and a digest * of the bytes we wrote — from which it mints the trusted mark. The two never * share an authoring identity. See * docs/features/vouched-ingest-channel-mint.md. * * Honest limit: v1 is operator-trusted. This runtime is `as: identity` and sees * the plaintext; the split-mint protects the *mark*, not the *bytes*. */ export type VouchedChannel = { /** The ingest channel — its dedicated space DID. Recorded on every mark. */ readonly channel: string; /** * The stable per-source identifier the grant was vouched to. Recorded for * audit/display; NOT enforced (audience-binding is the federation PR5 * dependency). * * Two conventions coexist at rest, and a new channel must follow one: a * minted, token-bearing ingest channel uses its per-install `installId` * (`ingest.utils.ts`); a token-less integration channel uses a fixed * `did:web:commonfabric.org#` URI (OAuth: * `oauth2-common.utils.ts` `oauthIngestChannel`; Plaid equivalent). */ readonly audience: string; }; // A digest of the payload bytes the mark binds to, computed with the canonical // shared SHA-256 (sync, so it can run inside the retry loop — see durableEdit). // v1 hashes the JSON serialization of the parsed value; binding to the exact // received bytes is a future hardening. const digestOf = (payload: unknown): string => `sha256:${ toUnpaddedBase64url(sha256(new TextEncoder().encode( JSON.stringify(payload ?? null), ))) }`; // A fresh, independent deep-mutable copy of a cell value, so an in-place // `mutate` callback can't touch the transaction's working copy before the // explicit `set`. Uses the canonical `FabricValue` clone (force-copy // everything, leave it mutable); never a JSON round-trip, which mangles a // `FabricPrimitive`. const cloneValue = (value: T | undefined): T | undefined => value === undefined ? undefined : cloneIfNecessary(value as FabricValue, { frozen: false, deep: true, force: true, }) as T; /** * The one governed write. `mutate` runs INSIDE the retrying transaction, so any * read-modify-write it does re-reads the current value on every retry (no * stale-snapshot lost update). It returns the value the mark's digest binds to. * When `channel` is set, the ExternalIngest mark is minted from that value, * unless `wrote` says the mutator left the cell as it was: a transaction that * wrote nothing commits nothing, and a mark on it would vouch for nothing. */ const durableEdit = async ( cell: Cell, mutate: (bound: Cell) => W, channel?: VouchedChannel, wrote: (written: W) => boolean = () => true, ): Promise => { const link = cell.getAsNormalizedFullLink(); // Operator wall-clock, captured BEFORE the write: retries must not re-stamp // the time, and it must never come from the payload. const receivedAt = new Date().toISOString(); const { ok, error } = await cellRuntime(cell).editWithRetry( (tx: IExtendedStorageTransaction): W => { const written = mutate(cell.withTx(tx)); if (channel !== undefined && wrote(written)) { stampExternalIngest(tx, { channel: channel.channel, audience: channel.audience, receivedAt, valueDigest: digestOf(written), target: { space: link.space, id: link.id, scope: link.scope, path: link.path, }, }); } return written; }, ); if (error) throw error; return ok as W; }; /** * Governed durable write with NO ingest mark — the factored replacement for the * hand-rolled per-route `editWithRetry` set blocks, for writes that are operator * actions rather than external ingest (clearing OAuth tokens). */ export const durableSet = (cell: Cell, value: T): Promise => durableEdit(cell, (bound) => { bound.set(value); return value; }); /** * Governed durable read-modify-write with NO ingest mark — for operator actions * that must re-read the current value on each retry (e.g. removing one item * from a list), but are not external ingest. */ export const durableUpdate = ( cell: Cell, mutate: (current: T | undefined) => T, ): Promise => durableEdit(cell, (bound) => { // Clone before handing the value to `mutate`: an in-place mutate must not // touch the transaction's working copy before the explicit `set`. const next = mutate(cloneValue(bound.get() as T | undefined)); bound.set(next); return next; }); /** * Durably write external data into the fabric under a vouched ingest channel, * minting the `ExternalIngest` provenance mark. */ export const custodyIngest = { /** Durably replace the cell's value (e.g. an OAuth token refresh). */ set(cell: Cell, value: T, channel: VouchedChannel): Promise { return durableEdit(cell, (bound) => { bound.set(value); return value; }, channel); }, /** * Durably append one external record to a list cell (e.g. a webhook event or * a location point). The read-append-write runs inside the retry, so * concurrent appends don't lose each other; the mark binds to the appended * element — the external bytes — not the whole accumulated array. */ append(cell: Cell, element: E, channel: VouchedChannel): Promise { return durableEdit(cell, (bound) => { const current = (bound.get() as E[] | undefined) ?? []; bound.set([...current, element]); return element; }, channel); }, /** * Durably appends `elements` to a list cell in one commit, as one ingest * (e.g. the batch of records one POST carries). The mark binds to * `elements`, the payload this ingest brought, not to the accumulated list. * * The elements already in the list are left as they are. A list of objects * holds a link to a document for each one, and writing the whole list back * from a copy would store every earlier element again under a new id on * each call. */ appendAll( cell: Cell, elements: E[], channel: VouchedChannel, ): Promise { return durableEdit(cell, (bound) => { bound.pushAll(elements); return elements; }, channel); }, /** * Durably read-modify-write a cell under a vouched channel (e.g. upsert one * item into an accumulated auth blob). The read-merge-write runs inside the * retry — each attempt re-reads the current value — so concurrent updates * don't overwrite each other with a stale snapshot. The mark binds to the * written result. */ update( cell: Cell, mutate: (current: T | undefined) => T, channel: VouchedChannel, ): Promise { return durableEdit(cell, (bound) => { // Clone before handing the value to `mutate` (see durableUpdate). const next = mutate(cloneValue(bound.get() as T | undefined)); bound.set(next); return next; }, channel); }, /** * Durably replaces the cell's value with `value`, minting the mark from it, * when the cell is empty or when `supersedes(current)` returns `true`, * meaning `value` is newer than the `current` value the cell holds. An * empty cell is written without consulting `supersedes`. Otherwise nothing * is written and no mark is minted, so a stale arrival neither changes the * cell nor wakes anything watching it. The comparison runs inside the * retry, against the value the transaction commits over. Returns whether * `value` was written. For a cell whose value is never `undefined`: the * mutator returns `undefined` to mean it wrote nothing, which a cell that * can hold `undefined` could not tell apart from a write. */ async replaceIfNewer( cell: Cell, value: T, supersedes: (current: T) => boolean, channel: VouchedChannel, ): Promise { const written = await durableEdit( cell, (bound) => { const current = cloneValue(bound.get() as T | undefined); if (current !== undefined && !supersedes(current)) return undefined; bound.set(value); return value; }, channel, (written) => written !== undefined, ); return written !== undefined; }, } as const;