/** * The sealer, S3a + S3b (journal spec §2.5 phase B, §2.4, §1.6, §2.6, §4.3; * T12984, T12985). * * `sealPending` turns live `_sync_capture` rows into sealed transactions: * * 1. **Group (§2.4).** A validated frame (its `_sync_frame` row exists) is * one transaction. Every capture with no validated frame is a singleton * transaction flagged `unframed`, provenance `foreign` (ruling (c)): the * sealer never claims atomicity it did not observe. A batch ends on a * group boundary. Apply and rebase frames wait for S5's intent subtraction. * 2. **Draft ops (§2.6).** Each capture becomes one draft. Values are decoded * from the capture's `enc()` text to typed wire values; references become * uids (resolved from the local key when the capture saw none); secret * columns and the local columns a stored ref uid is derived from (`ac_id`) * are removed. Natural rows carry `k`, their key with references as uids. * An unreadable capture is quarantined (`_sync_quarantine`, its table * suspect) and never stalls the outbox (T13036). * 3. **Net (§2.4, {@link netTransaction}).** Per (table, uid) inside the * transaction. Across transactions, the dead incarnation of a row that * never got a uid (its captures up to its delete) is dropped whole * (T13036). A minted row with no uid or `birth_fp` keeps its group * pending, and nothing after it seals first (§2.9). A group held for a * secret phase A could not seal (an unreadable credential, or no master key) * is not such a stall (T13534): it stays pending and is retried on the next * pass, a later group touching or referencing one of its rows waits behind * it, and every unrelated group still seals. * 4. **HLC.** `tick(at_ms)` per op in order, at its last capture's time; the * transaction's HLC is its maximum. * 5. **Write.** `_sync_txn`, `_sync_op`; `_sync_row_meta` upserted per op * (version, origin, actor, per-field `fhlc`, tombstone on D, key on * natural rows) and MOVED on K (hlc, fhlc, version and chash kept); * `chash` recomputed from the live row once no live capture of that row * remains; `_sync_ledger` per table; the `sealer.local_seq` counter. * 6. **Consume.** Sealed, dropped and quarantined captures are deleted, and * frames no live capture references. * * S3c: timestamp columns are canonical on the wire and in `chash` * ({@link canonicalStoreTimestamp}; local rows are never rewritten here), and * a delete in an append-only table leaves no per-row tombstone (§1.7 R5-5). * The step-0 fill and the repair diff of suspect tables are S3d. `sync.seal` * stays unreleased until those land: the sealer refuses a persisted flag too. * * Phase B is synchronous: one `BEGIN IMMEDIATE` … `COMMIT` with no await, so * a caller on the accessor's transaction queue serializes it with every * writer on the handle. * * @module store/sync/sealer * @task T12984 * @epic T12323 */ import type { DatabaseSync } from 'node:sqlite'; import type { TableScope } from '@cleocode/contracts'; import { type CaptureTableDef } from './capture.js'; import { type WireValue } from './sealer-values.js'; export { sealBacklog } from './seal-backlog.js'; export { canonicalJson, decodeEnc, type WireValue } from './sealer-values.js'; /** One sealed op (§2.6 `LedgerOp`, without the transaction-level fields). */ export interface SealedOp { readonly t: string; /** Row uid; for K the OLD uid. */ readonly u: string; readonly o: 'I' | 'U' | 'D' | 'K'; readonly h: string; readonly nu?: string; readonly bfp?: string; readonly obfp?: string; readonly k?: Record; readonly a?: Record; readonly b?: Record; /** Field HLCs other than `h`: only a copy reconcile's rule-3 op pins them (§1.5 N7, T13335). */ readonly fh?: Record; } /** What one `sealPending` call did. */ export interface SealReport { readonly txns: number; readonly ops: number; readonly captures: number; readonly unframed: number; /** Groups left pending (a minted row without uid or birth_fp), by first seq. */ readonly pending: ReadonlyArray<{ readonly firstSeq: number; readonly reason: string; }>; /** Captures of dead incarnations dropped whole (I … D of a row with no uid; T13036). */ readonly dropped: number; /** Unreadable captures moved to `_sync_quarantine` (T13036). */ readonly quarantined: ReadonlyArray<{ readonly seq: number; readonly tbl: string; readonly reason: string; }>; /** Why nothing was sealed, when the preconditions refused. */ readonly refused: string | null; } /** Options for {@link sealPending}. */ export interface SealOptions { readonly scope: TableScope; /** * This store's replica id (the HLC's replica part, `origin`). * @defaultValue the store's active replica (`_sync_replica`) */ readonly replica?: string; /** Most captures to consume; the batch still ends on a group boundary. @defaultValue 5000 */ readonly budget?: number; /** Wall clock in ms. @defaultValue Date.now */ readonly now?: () => number; /** Environment for the kill switch. @defaultValue process.env */ readonly env?: NodeJS.ProcessEnv; /** * Stop starting new groups after this long in one transaction, so a batch * never holds the write lock for long (T13032). @defaultValue 200 */ readonly maxMs?: number; /** * Seal although `sync.seal` is unreleased (tests only; T13037). Never set * from user input. */ readonly allowUnreleased?: boolean; /** * Phase A's sealed secret values (§2.5, §2.7; T13467), keyed by * {@link secretSealKey}. Without one, a group writing a secret column of a * {@link SEALED_SECRET_TABLES} table stays pending: the open-time sealer never * sends a credential row without its secret, and only `cloud sync` (which * holds the master key) runs phase A. */ readonly secrets?: SecretSeals; } /** * Tables whose secret columns travel sealed on the home stream (§2.7, T13467; * owner decision `t13467-global-secrets-sync-design`). Every other secret * column is dropped at seal and never travels (an agent's API key is * per-machine). */ export declare const SEALED_SECRET_TABLES: ReadonlySet; /** One secret column value prepared by phase A (§2.5). */ export interface SecretSeal { /** Canonical JSON of the column's live wire value when phase A read it. */ readonly live: string; /** The sealed envelope (base64), or null when the live value was NULL. */ readonly sealed: string | null; } /** Phase A's output, keyed by {@link secretSealKey}. */ export type SecretSeals = ReadonlyMap; /** The key of one secret column value: its table, row uid and column (the seal's AAD). */ export declare function secretSealKey(table: string, uid: string, column: string): string; /** A row of a sealed-secret table with live captures waiting for phase A. */ export interface PendingSecretRow { readonly table: string; readonly uid: string; /** The row's natural key, as local values (the credential id is derived from it). */ readonly key: Readonly>; /** Every secret column's live wire value. */ readonly values: Readonly>; /** * Other uids the row's live captures still carry (T13603): a held insert * captured before a re-key keeps its old uid, so phase A seals under it too. */ readonly capturedUids: readonly string[]; } /** * The rows of {@link SEALED_SECRET_TABLES} with live captures, and each secret * column's live wire value: what phase A (§2.5) must seal before phase B can * send them (T13467). Read-only and synchronous; phase A itself (the machine- * key decrypt and the seal) is async and runs only inside `cloud sync`. * * @param db - The store. * @param scope - Its scope. * @returns One entry per live row, in capture order. */ export declare function pendingSecretRows(db: DatabaseSync, scope: TableScope): PendingSecretRow[]; /** * sha256 of a row's canonical WIRE image (§2.7, M6; T13031): the values an I * op of the row would carry, so every replica hashes identical bytes. Secret * columns, uid and birth_fp, and a minted table's local key (a display id or * an autoincrement id, which differ per replica) are left out; references are * their uids; NULL columns are omitted, as in an I image; and timestamp * columns hash their canonical form (§1.8), so a replica holding legacy text * hashes like one that received the canonical value. */ export declare function rowChash(db: DatabaseSync, scope: TableScope, def: CaptureTableDef, uid: string): string | null; /** * The facts of live rows the repair diff (§4.4, T12987) reads, from the same * code the sealer uses, so a repaired row hashes exactly as a sealed one. * Statements are prepared once per view. */ export interface SealerRowView { /** The row's content hash ({@link rowChash}). */ chash(def: CaptureTableDef, uid: string): string | null; /** A natural row's key with refs as uids (canonical JSON), from its local key `rk`. */ naturalKeyJson(def: CaptureTableDef, rk: string): string; /** The live row's birth_fp, or undefined. */ liveBirthFp(table: string, uid: string): string | undefined; /** A natural row's uid from its local key `rk` (null for a minted or symmetric table). */ naturalUid(def: CaptureTableDef, rk: string): string | null; } /** * A {@link SealerRowView} over `db`, valid while its schema does not change. * * @param db - The store. * @param scope - Its scope. */ export declare function sealerRowView(db: DatabaseSync, scope: TableScope): SealerRowView; /** * Why the sealer refuses to run, or `null` when it may. A persisted * `sync.seal` is refused while the flag is unreleased (T13037), whatever * wrote it. */ export declare function sealPreconditions(db: DatabaseSync, env: NodeJS.ProcessEnv, allowUnreleased?: boolean): string | null; /** Where the sealer's transaction counter is persisted (T13033). */ export declare const SEAL_COUNTER_KEY = "sealer.local_seq"; /** * Take the next transaction counter for a transaction written outside a * sealing pass (a `retire` control transaction, T13278), in the caller's * transaction. The counter only rises, so the sealer continues past it. * * @param db - The store, inside a transaction. * @param atIso - Now (ISO-8601), recorded with the counter. * @returns The reserved `local_seq`. */ export declare function reserveLocalSeq(db: DatabaseSync, atIso: string): number; /** * Seal up to `budget` live captures (ending on a group boundary) into * transactions, in one synchronous `BEGIN IMMEDIATE` transaction. * * Groups seal strictly in capture order (§2.9): the first group that cannot * seal yet (a row without uid or birth_fp, an unreadable capture) stops the * batch, and is reported in `pending` until it can. Nothing after it gets an * HLC first. * * @returns what was sealed, what stays pending, or why nothing ran. */ export declare function sealPending(db: DatabaseSync, opts: SealOptions): SealReport; /** Set once the sealed-but-unsent ops have been made group-whole (T13233). */ export declare const LEGACY_GROUPS_KEY = "sealer.group_whole_v1"; /** * Make sealed-but-unsent ops group-whole before they are packed (T13287): * the one-time T13233 pass otherwise runs only at the next seal, so a * segment built first would carry a pre-T13222 partial op off the device. * A no-op once {@link LEGACY_GROUPS_KEY} is set. Runs in the caller's * transaction (the segment's `BEGIN IMMEDIATE`). * * @param db - The store, inside a transaction. * @param scope - The store's scope. * @param atIso - When the pass ran (the marker's `updated_at`). * @returns The sealed transactions still carrying a partial group, which * must not be packed; empty once every op is whole. */ export declare function completeLegacyGroupsBeforePack(db: DatabaseSync, scope: TableScope, atIso: string): ReadonlySet; /** `_sync_meta` key of the sync-set version the row-meta `chash` baseline matches (§2.3a rule 3). */ export declare const CHASH_BASELINE_KEY = "sync.set_version"; /** * The sync set's version: a hash of every sync-set table's captured columns, * secret columns and references as this store's schema has them (§2.3a, * §2.9). A migration that changes what is captured changes it. * * @param db - The store. * @param scope - Its scope. */ export declare function syncSetVersion(db: DatabaseSync, scope: TableScope): string; /** * Which row-meta rows a migration pass may re-baseline: taken BEFORE the first * migration ({@link chashBaselineSnapshot}), under the old schema. */ export interface ChashSnapshot { /** `tbl \0 uid` of live rows whose `chash` equals their live hash. */ readonly inBaseline: ReadonlySet; /** Tables with a live row whose `chash` already differs (an uncaptured edit). */ readonly divergent: ReadonlySet; /** Tables already marked `suspect:` (their repair diff is pending). */ readonly suspect: ReadonlySet; /** `total_changes()` when the snapshot (or the last re-baseline) was taken. */ readonly changes: number; /** The sync-set version at that time. */ readonly version: string; } /** What {@link rebaselineChash} did. */ export interface ChashRebaseline { /** Row-meta rows whose `chash` moved to the live row's hash. */ readonly rows: number; /** The sync-set version the baseline now matches. */ readonly version: string; /** The snapshot to pass to the next migration's re-baseline. */ readonly snapshot: ChashSnapshot; } /** * Before a migration pass: which live rows are in baseline (`chash` equals the * live hash under the current schema), and which tables already diverge. Only * in-baseline rows may be re-baselined afterwards; a divergence is an * uncaptured edit the repair diff (S3d) must still see (§2.3a rule 3, * NEW-8). * * @param db - The store, before its first pending migration. * @param scope - Its scope. */ export declare function chashBaselineSnapshot(db: DatabaseSync, scope: TableScope): ChashSnapshot; /** * Re-baseline `_sync_row_meta.chash` after a migration, emitting nothing * (§2.3a rule 3; B, T12775). A migration's backfill is deterministic and * every replica runs it itself, so its changes must not travel; but the row * meta's content hash would otherwise make the repair diff read every migrated * row as an uncaptured edit. * * Only rows that were in baseline before the pass ({@link ChashSnapshot}) move * to the live hash. A row that already diverged keeps its `chash`, and its * table is marked `suspect:` so the repair diff still emits the edit. A table * already suspect, and a row with a live capture (the sealer will hash it), * are left alone. Live rows only: a tombstone keeps the hash it was deleted * with. When the migration changed no row and the sync-set version did not * move, nothing is re-hashed. Records {@link CHASH_BASELINE_KEY}. * * Runs in the caller's transaction when there is one: the migration bracket * calls it before its COMMIT, so a migration and its re-baseline commit or * roll back together. Otherwise it opens its own `BEGIN IMMEDIATE`. * * @param db - The store. * @param scope - Its scope. * @param snapshot - From {@link chashBaselineSnapshot} before the pass, or the * previous re-baseline's `snapshot`. */ export declare function rebaselineChash(db: DatabaseSync, scope: TableScope, snapshot: ChashSnapshot): ChashRebaseline; //# sourceMappingURL=sealer.d.ts.map