@powerhousedao/reactor 6.2.3-dev.3 → 6.2.3-dev.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/{build-worker-executor-DnU3ZU4H.js → build-worker-executor-Bvoba-AV.js} +24 -8
  2. package/dist/build-worker-executor-Bvoba-AV.js.map +1 -0
  3. package/dist/{document-indexer-C5Gsa1B3.js → document-indexer-BVTGbEB0.js} +190 -40
  4. package/dist/document-indexer-BVTGbEB0.js.map +1 -0
  5. package/dist/entry.d.ts +2 -1
  6. package/dist/entry.js +10 -6
  7. package/dist/entry.js.map +1 -1
  8. package/dist/{errors-D3S6Eysd.js → errors-BM_ULVWd.js} +9 -1
  9. package/dist/{errors-D3S6Eysd.js.map → errors-BM_ULVWd.js.map} +1 -1
  10. package/dist/{drive-container-types-yZrksiJR.js → forwarding-logger-CnuVDVze.js} +1297 -241
  11. package/dist/forwarding-logger-CnuVDVze.js.map +1 -0
  12. package/dist/index.d.ts +2707 -2040
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1708 -648
  15. package/dist/index.js.map +1 -1
  16. package/dist/projection-entry.d.ts +2 -1
  17. package/dist/projection-entry.js +11 -17
  18. package/dist/projection-entry.js.map +1 -1
  19. package/dist/{projection-shard-manager-CPQc5XHL.js → projection-shard-manager-BpcqDtnQ.js} +4 -4
  20. package/dist/projection-shard-manager-BpcqDtnQ.js.map +1 -0
  21. package/dist/{projection-worker-wI4PwcV2.js → projection-worker-D8uNFdwX.js} +1 -1
  22. package/dist/{projection-worker-wI4PwcV2.js.map → projection-worker-D8uNFdwX.js.map} +1 -1
  23. package/dist/{transport-CuogVKN_.js → transport-B3G50KVd.js} +1 -1
  24. package/dist/{transport-CuogVKN_.js.map → transport-B3G50KVd.js.map} +1 -1
  25. package/dist/{transport-ByGviWdZ.js → transport-CZDHzfvy.js} +1 -1
  26. package/dist/{transport-ByGviWdZ.js.map → transport-CZDHzfvy.js.map} +1 -1
  27. package/dist/{worker-handle-CrERzl8s.js → worker-handle-C2rLUHwv.js} +22 -16
  28. package/dist/worker-handle-C2rLUHwv.js.map +1 -0
  29. package/package.json +19 -10
  30. package/dist/build-worker-executor-DnU3ZU4H.js.map +0 -1
  31. package/dist/document-indexer-C5Gsa1B3.js.map +0 -1
  32. package/dist/drive-container-types-yZrksiJR.js.map +0 -1
  33. package/dist/error-info-Cpu4OY3o.js +0 -62
  34. package/dist/error-info-Cpu4OY3o.js.map +0 -1
  35. package/dist/forwarding-logger-BBkMSxuJ.js +0 -85
  36. package/dist/forwarding-logger-BBkMSxuJ.js.map +0 -1
  37. package/dist/projection-shard-manager-CPQc5XHL.js.map +0 -1
  38. package/dist/types-DMKLa0Ok.js +0 -33
  39. package/dist/types-DMKLa0Ok.js.map +0 -1
  40. package/dist/worker-HPysBfhx.js +0 -22
  41. package/dist/worker-HPysBfhx.js.map +0 -1
  42. package/dist/worker-handle-CrERzl8s.js.map +0 -1
@@ -1,5 +1,4 @@
1
- import { n as ReactorEventTypes, t as EventBusAggregateError } from "./types-DMKLa0Ok.js";
2
- import { AUTH_ACTION_TYPES, AUTH_DENIED_BY_GRANT_REASON, AUTH_NO_GRANT_REASON, AUTH_VERSION_UNSUPPORTED_REASON, DOCUMENT_DELETED_REASON, DowngradeNotSupportedError as DowngradeNotSupportedError$1, appendWithoutApplying, applyAuthAction, applyDeleteDocumentAction, applyDeleteDocumentAction as applyDeleteDocumentAction$1, applyUpgradeDocumentAction, applyUpgradeDocumentAction as applyUpgradeDocumentAction$1, baseReducerVersion, createPresignedHeader, decide, defaultBaseState, deriveOperationId, evaluate, garbageCollect, groupDocumentType, groupMembershipActionTypes, hashDocumentStateForScope, isDenied, isUndoRedo, mentionedGroupIds, normalizeDocumentModelVersion, referencedGroupIds, sortOperations } from "@powerhousedao/shared/document-model";
1
+ import { AUTH_ACTION_TYPES, AUTH_DENIED_BY_GRANT_REASON, AUTH_NO_GRANT_REASON, AUTH_VERSION_UNSUPPORTED_REASON, DOCUMENT_DELETED_REASON, DOCUMENT_SCOPE_ACTION_TYPES, DowngradeNotSupportedError as DowngradeNotSupportedError$1, actionSignerIdentity, actionSigningTarget, appendWithoutApplying, applyAuthAction, applyDeleteDocumentAction, applyDeleteDocumentAction as applyDeleteDocumentAction$1, applyUpgradeDocumentAction, applyUpgradeDocumentAction as applyUpgradeDocumentAction$1, base58Decode, baseReducerVersion, buildOperationSignatureMessage, buildOperationSignatureParams, canonicalJson, createPresignedHeader, decide, defaultBaseState, deriveDocumentId, deriveOperationId, evaluate, garbageCollect, groupDocumentType, groupMembershipActionTypes, hashActionV2, hashBrowser, hashDocumentStateForScope, isDenied, isDerivedDocumentId, isUndoRedo, isV2ActionHash, mentionedGroupIds, normalizeDocumentModelVersion, operationOutcome, referencedGroupIds, signaturePolicyOf, sortOperations, targetDocumentId, v2TupleProblem, withProtocolVersions } from "@powerhousedao/shared/document-model";
3
2
  import { v4 } from "uuid";
4
3
  import { Migrator, sql } from "kysely";
5
4
  //#region \0rolldown/runtime.js
@@ -52,6 +51,37 @@ function parsePagingOptions(paging, defaultLimit) {
52
51
  };
53
52
  }
54
53
  //#endregion
54
+ //#region src/events/types.ts
55
+ /**
56
+ * Custom error class that aggregates multiple errors from event subscribers.
57
+ */
58
+ var EventBusAggregateError = class extends Error {
59
+ errors;
60
+ constructor(errors) {
61
+ const message = `EventBus emit failed with ${errors.length} error(s): ${errors.map((e) => {
62
+ if (e && typeof e === "object" && "message" in e) return e.message;
63
+ return String(e);
64
+ }).join("; ")}`;
65
+ super(message);
66
+ this.name = "EventBusAggregateError";
67
+ this.errors = errors;
68
+ }
69
+ };
70
+ /**
71
+ * Event types for reactor lifecycle events.
72
+ */
73
+ const ReactorEventTypes = {
74
+ JOB_PENDING: 10001,
75
+ JOB_RUNNING: 10002,
76
+ JOB_WRITE_READY: 10003,
77
+ JOB_READ_READY: 10004,
78
+ JOB_FAILED: 10005,
79
+ READMODEL_BATCH_COMPLETED: 10006,
80
+ READMODEL_INDEXED: 10007,
81
+ MODEL_LOADED: 10008,
82
+ SIGNATURE_REFUSED: 10009
83
+ };
84
+ //#endregion
55
85
  //#region src/shared/errors.ts
56
86
  /**
57
87
  * Error thrown when attempting to access a deleted document.
@@ -167,19 +197,22 @@ var ExcessiveReshuffleError = class ExcessiveReshuffleError extends Error {
167
197
  return Error.isError(error) && error.name === "ExcessiveReshuffleError";
168
198
  }
169
199
  };
170
- /**
171
- * Error thrown when an operation has an invalid signature.
172
- */
200
+ /** The message carries the code: only name and message cross the queue. */
173
201
  var InvalidSignatureError = class InvalidSignatureError extends Error {
174
202
  documentId;
203
+ code;
175
204
  reason;
176
- constructor(documentId, reason) {
177
- super(`Invalid signature in document ${documentId}: ${reason}`);
205
+ constructor(documentId, code, reason) {
206
+ super(`Invalid signature in document ${documentId} [${code}]: ${reason}`);
178
207
  this.name = "InvalidSignatureError";
179
208
  this.documentId = documentId;
209
+ this.code = code;
180
210
  this.reason = reason;
181
211
  Error.captureStackTrace(this, InvalidSignatureError);
182
212
  }
213
+ static isError(error) {
214
+ return Error.isError(error) && error.name === "InvalidSignatureError";
215
+ }
183
216
  };
184
217
  /**
185
218
  * An UPGRADE_DOCUMENT action's preconditions (fromVersion and the per-scope
@@ -250,6 +283,30 @@ var AuthEnforcementDisabledError = class AuthEnforcementDisabledError extends Er
250
283
  return Error.isError(error) && error.name === "AuthEnforcementDisabledError";
251
284
  }
252
285
  };
286
+ /**
287
+ * Error thrown when a relationship edge an operation names does not exist.
288
+ *
289
+ * Detection is by `name`, not `instanceof`: the SharedWorker RPC boundary
290
+ * rebuilds a thrown error from `{ name, message, stack, cause }` alone
291
+ * (`reactor-browser/src/rpc/error-info.ts`), so the class identity is lost in
292
+ * transit.
293
+ */
294
+ var RelationshipNotFoundError = class RelationshipNotFoundError extends Error {
295
+ sourceId;
296
+ targetId;
297
+ relationshipType;
298
+ constructor(sourceId, targetId, relationshipType) {
299
+ super(`No ${relationshipType} relationship from ${sourceId} to ${targetId}`);
300
+ this.name = "RelationshipNotFoundError";
301
+ this.sourceId = sourceId;
302
+ this.targetId = targetId;
303
+ this.relationshipType = relationshipType;
304
+ Error.captureStackTrace(this, RelationshipNotFoundError);
305
+ }
306
+ static isError(error) {
307
+ return Error.isError(error) && error.name === "RelationshipNotFoundError";
308
+ }
309
+ };
253
310
  //#endregion
254
311
  //#region src/storage/interfaces.ts
255
312
  /**
@@ -302,6 +359,7 @@ const APPEND_CONDITION_FAILED_PREFIX = "Append condition failed: ";
302
359
  * conflict, not a fault: the caller retries against the new stream heads.
303
360
  */
304
361
  var AppendConditionFailedError = class extends Error {
362
+ condition;
305
363
  constructor(condition) {
306
364
  const streams = condition.streams.map((s) => `${s.documentId}:${s.scope}:${s.branch}@${s.revision}`).join(", ");
307
365
  super(`${APPEND_CONDITION_FAILED_PREFIX}a read-set stream advanced [${streams}]`);
@@ -690,34 +748,13 @@ function selectDecisionModel(flags, registry) {
690
748
  //#endregion
691
749
  //#region src/executor/util.ts
692
750
  /** Actions the reactor reduces itself, onto the document scope. */
693
- const DOCUMENT_SCOPE_ACTIONS = new Set([
694
- "CREATE_DOCUMENT",
695
- "DELETE_DOCUMENT",
696
- "UPGRADE_DOCUMENT",
697
- "ADD_RELATIONSHIP",
698
- "REMOVE_RELATIONSHIP",
699
- "UPDATE_RELATIONSHIP"
700
- ]);
751
+ const DOCUMENT_SCOPE_ACTIONS = DOCUMENT_SCOPE_ACTION_TYPES;
701
752
  /**
702
753
  * `CREATE_DOCUMENT` is exempt by necessity: it runs before the document exists,
703
754
  * so building a decision model would throw and defer the job forever.
704
755
  */
705
756
  const GATED_DOCUMENT_ACTIONS = new Set([...DOCUMENT_SCOPE_ACTIONS].filter((type) => type !== "CREATE_DOCUMENT"));
706
757
  /**
707
- * The document a document-scope action writes to, which is not always the job's
708
- * own document: delete and upgrade name it in `input.documentId`, and the
709
- * relationship actions in `input.sourceId`. `execute` only checks that a batch
710
- * shares one scope, so a caller can submit an action whose target is a document
711
- * other than the one the job is keyed by. The policy gate has to follow the
712
- * action rather than the job, or it decides against a policy the caller may
713
- * control instead of the one guarding the write.
714
- */
715
- function targetDocumentId(action, fallback) {
716
- const input = action.input;
717
- if (action.type === "ADD_RELATIONSHIP" || action.type === "REMOVE_RELATIONSHIP" || action.type === "UPDATE_RELATIONSHIP") return typeof input?.sourceId === "string" && input.sourceId.length > 0 ? input.sourceId : fallback;
718
- return typeof input?.documentId === "string" && input.documentId.length > 0 ? input.documentId : fallback;
719
- }
720
- /**
721
758
  * Creates a PHDocument from a CREATE_DOCUMENT action input.
722
759
  * Reconstructs the document header and initializes the base state.
723
760
  *
@@ -896,6 +933,40 @@ var TouchedStreams = class {
896
933
  return this.streams.values();
897
934
  }
898
935
  };
936
+ /**
937
+ * The ids of the actions the caller handed to a job. Load and reevaluation
938
+ * jobs write operations nobody submitted, so they report none.
939
+ */
940
+ function submittedActionIds(job) {
941
+ return job.kind === "mutation" ? job.actions.map((action) => action.id) : [];
942
+ }
943
+ /**
944
+ * Reports what became of each submitted action.
945
+ *
946
+ * A job's operations can include ones it only moved to a new index, so only
947
+ * those carrying a submitted action are reported. Returns undefined when the
948
+ * job submitted nothing, which keeps `JobInfo.result` null for the jobs that
949
+ * have no caller to answer to.
950
+ */
951
+ function summarizeSubmittedActions(operations, submitted) {
952
+ if (!submitted || submitted.length === 0) return;
953
+ const ids = new Set(submitted);
954
+ const actions = [];
955
+ for (const { operation, context } of operations) {
956
+ if (!ids.has(operation.action.id)) continue;
957
+ actions.push({
958
+ actionId: operation.action.id,
959
+ scope: context.scope,
960
+ index: operation.index,
961
+ ...operationOutcome(operation)
962
+ });
963
+ }
964
+ if (actions.length === 0) return;
965
+ return {
966
+ actions,
967
+ allApplied: actions.every((action) => action.kind === "applied")
968
+ };
969
+ }
899
970
  //#endregion
900
971
  //#region src/registry/errors.ts
901
972
  /**
@@ -993,6 +1064,7 @@ const JobExecutorEventTypes = {
993
1064
  //#endregion
994
1065
  //#region src/cache/collection-membership-cache.ts
995
1066
  var CollectionMembershipCache = class CollectionMembershipCache {
1067
+ operationIndex;
996
1068
  cache = /* @__PURE__ */ new Map();
997
1069
  constructor(operationIndex) {
998
1070
  this.operationIndex = operationIndex;
@@ -1206,10 +1278,23 @@ var DocumentMetaCache = class DocumentMetaCache {
1206
1278
  return {
1207
1279
  state: document.state.document,
1208
1280
  documentType,
1281
+ protocolVersions: createAction.input.protocolVersions,
1209
1282
  documentScopeRevision: documentScopeRevision + 1
1210
1283
  };
1211
1284
  }
1212
1285
  };
1286
+ /** Bind slots per statement: 16-bit on the wire, read signed in-process. */
1287
+ const MAX_BIND_PARAMETERS = 32767;
1288
+ /** Splits rows into runs that each bind at most MAX_BIND_PARAMETERS. */
1289
+ function chunkRows(rows) {
1290
+ if (rows.length === 0) return [];
1291
+ const columnsPerRow = Math.max(1, Object.keys(rows[0]).length);
1292
+ const perChunk = Math.max(1, Math.floor(MAX_BIND_PARAMETERS / columnsPerRow));
1293
+ if (rows.length <= perChunk) return [rows];
1294
+ const chunks = [];
1295
+ for (let i = 0; i < rows.length; i += perChunk) chunks.push(rows.slice(i, i + perChunk));
1296
+ return chunks;
1297
+ }
1213
1298
  var KyselyOperationIndexTxn = class {
1214
1299
  collections = [];
1215
1300
  collectionMemberships = [];
@@ -1275,6 +1360,7 @@ var KyselyOperationIndexTxn = class {
1275
1360
  }
1276
1361
  };
1277
1362
  var KyselyOperationIndex = class KyselyOperationIndex {
1363
+ db;
1278
1364
  trx;
1279
1365
  constructor(db) {
1280
1366
  this.db = db;
@@ -1331,7 +1417,7 @@ var KyselyOperationIndex = class KyselyOperationIndex {
1331
1417
  leftOrdinal: null
1332
1418
  }));
1333
1419
  for (const collectionId of collections) kyselyTxn.recordMembershipInvalidation(collectionId);
1334
- await trx.insertInto("document_collections").values(collectionRows).onConflict((oc) => oc.doNothing()).execute();
1420
+ for (const chunk of chunkRows(collectionRows)) await trx.insertInto("document_collections").values(chunk).onConflict((oc) => oc.doNothing()).execute();
1335
1421
  }
1336
1422
  let operationOrdinals = [];
1337
1423
  if (operations.length > 0) {
@@ -1349,7 +1435,10 @@ var KyselyOperationIndex = class KyselyOperationIndex {
1349
1435
  deniedReason: op.deniedReason ?? null,
1350
1436
  sourceRemote: op.sourceRemote
1351
1437
  }));
1352
- operationOrdinals = (await trx.insertInto("operation_index_operations").values(operationRows).returning("ordinal").execute()).map((row) => row.ordinal);
1438
+ for (const chunk of chunkRows(operationRows)) {
1439
+ const insertedOps = await trx.insertInto("operation_index_operations").values(chunk).returning("ordinal").execute();
1440
+ operationOrdinals = operationOrdinals.concat(insertedOps.map((row) => row.ordinal));
1441
+ }
1353
1442
  }
1354
1443
  if (memberships.length > 0) for (const m of memberships) {
1355
1444
  const ordinal = operationOrdinals[m.operationIndex];
@@ -1386,6 +1475,12 @@ var KyselyOperationIndex = class KyselyOperationIndex {
1386
1475
  if (signal?.aborted) throw new Error("Operation aborted");
1387
1476
  return (await this.queryExecutor.selectFrom("group_references").select("documentId").where("groupId", "=", groupId).orderBy("documentId").execute()).map((row) => row.documentId);
1388
1477
  }
1478
+ async getOrdinalsByOpIds(documentId, scope, branch, opIds, signal) {
1479
+ signal?.throwIfAborted();
1480
+ if (opIds.length === 0) return /* @__PURE__ */ new Map();
1481
+ const rows = await this.queryExecutor.selectFrom("operation_index_operations").select("opId").select((eb) => eb.fn.max("ordinal").as("ordinal")).where("documentId", "=", documentId).where("branch", "=", branch).where("scope", "=", scope).where("opId", "in", opIds).groupBy("opId").execute();
1482
+ return new Map(rows.map((row) => [row.opId, Number(row.ordinal)]));
1483
+ }
1389
1484
  async find(collectionId, cursor, view, paging, signal) {
1390
1485
  if (signal?.aborted) throw new Error("Operation aborted");
1391
1486
  const outerCursor = cursor ?? -1;
@@ -1605,21 +1700,6 @@ var RingBuffer = class {
1605
1700
  }
1606
1701
  };
1607
1702
  //#endregion
1608
- //#region src/cache/write-cache-types.ts
1609
- /**
1610
- * Where a snapshot sits in its stream.
1611
- *
1612
- * - `Head`: the newest revision of the stream when it was stored. Only these
1613
- * can answer a read that asks for the head.
1614
- * - `Historical`: state at an earlier revision. Usable as a starting point to
1615
- * replay forward from, and as an answer to a read for that same revision.
1616
- */
1617
- let SnapshotPosition = /* @__PURE__ */ function(SnapshotPosition) {
1618
- SnapshotPosition["Head"] = "head";
1619
- SnapshotPosition["Historical"] = "historical";
1620
- return SnapshotPosition;
1621
- }({});
1622
- //#endregion
1623
1703
  //#region src/cache/kysely-write-cache.ts
1624
1704
  /**
1625
1705
  * The last operation index a keyframe's document reflects for the scope. A
@@ -1758,13 +1838,13 @@ var KyselyWriteCache = class KyselyWriteCache {
1758
1838
  const snapshots = stream.ringBuffer.getAll();
1759
1839
  if (targetRevision === void 0) {
1760
1840
  const newest = highestRevision(snapshots);
1761
- if (newest?.position === SnapshotPosition.Head) {
1841
+ if (newest?.position === "head") {
1762
1842
  this.lruTracker.touch(streamKey);
1763
1843
  return copyDocument(newest.document);
1764
1844
  }
1765
1845
  if (newest) {
1766
1846
  const document = await this.warmMissRebuild(newest.document, newest.revision, documentId, scope, branch, void 0, signal);
1767
- this.store(documentId, scope, branch, (document.header.revision[scope] ?? 0) - 1, document, SnapshotPosition.Head);
1847
+ this.store(documentId, scope, branch, (document.header.revision[scope] ?? 0) - 1, document, "head");
1768
1848
  this.lruTracker.touch(streamKey);
1769
1849
  return document;
1770
1850
  }
@@ -1777,7 +1857,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1777
1857
  const newestOlder = this.findNearestOlderSnapshot(snapshots, targetRevision);
1778
1858
  if (newestOlder) {
1779
1859
  const document = await this.warmMissRebuild(newestOlder.document, newestOlder.revision, documentId, scope, branch, targetRevision, signal);
1780
- this.store(documentId, scope, branch, targetRevision, document, SnapshotPosition.Historical);
1860
+ this.store(documentId, scope, branch, targetRevision, document, "historical");
1781
1861
  this.lruTracker.touch(streamKey);
1782
1862
  return document;
1783
1863
  }
@@ -1785,7 +1865,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1785
1865
  }
1786
1866
  const document = await this.coldMissRebuild(documentId, scope, branch, targetRevision, signal);
1787
1867
  const revision = targetRevision ?? (document.header.revision[scope] ?? 0) - 1;
1788
- this.store(documentId, scope, branch, revision, document, targetRevision === void 0 ? SnapshotPosition.Head : SnapshotPosition.Historical);
1868
+ this.store(documentId, scope, branch, revision, document, targetRevision === void 0 ? "head" : "historical");
1789
1869
  return document;
1790
1870
  }
1791
1871
  /**
@@ -1820,7 +1900,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1820
1900
  if (run.length === 0) return;
1821
1901
  for (const entry of run.slice(0, -1)) this.persistKeyframe(documentId, scope, branch, entry.revision, entry.document);
1822
1902
  const head = run[run.length - 1];
1823
- this.store(documentId, scope, branch, head.revision, head.document, SnapshotPosition.Head);
1903
+ this.store(documentId, scope, branch, head.revision, head.document, "head");
1824
1904
  }
1825
1905
  store(documentId, scope, branch, revision, document, position) {
1826
1906
  const streamKey = this.makeStreamKey(documentId, scope, branch);
@@ -2026,10 +2106,10 @@ var KyselyWriteCache = class KyselyWriteCache {
2026
2106
  for (const pending of pendingUpgrades) pending.subsequentDeletes.push(operation.action);
2027
2107
  } else {
2028
2108
  const protocolVersion = baseReducerVersion(document.header);
2029
- document = docModule.reducer(document, operation.action, void 0, {
2109
+ document = withProtocolVersions(docModule.reducer(document, operation.action, void 0, {
2030
2110
  skip: operation.skip,
2031
2111
  protocolVersion
2032
- });
2112
+ }), document.header.protocolVersions);
2033
2113
  }
2034
2114
  }
2035
2115
  }
@@ -2078,12 +2158,12 @@ var KyselyWriteCache = class KyselyWriteCache {
2078
2158
  if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
2079
2159
  else {
2080
2160
  const protocolVersion = baseReducerVersion(document.header);
2081
- document = getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
2161
+ document = withProtocolVersions(getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
2082
2162
  skip: operation.skip,
2083
2163
  protocolVersion,
2084
2164
  replayOptions: { operation },
2085
2165
  skipIndexValidation: true
2086
- });
2166
+ }), document.header.protocolVersions);
2087
2167
  }
2088
2168
  }
2089
2169
  const reachedTarget = targetRevision !== void 0 && result.results.some((op) => op.index >= targetRevision);
@@ -2207,12 +2287,12 @@ var KyselyWriteCache = class KyselyWriteCache {
2207
2287
  if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
2208
2288
  else {
2209
2289
  const protocolVersion = baseReducerVersion(document.header);
2210
- document = module.reducer(document, operation.action, void 0, {
2290
+ document = withProtocolVersions(module.reducer(document, operation.action, void 0, {
2211
2291
  skip: operation.skip,
2212
2292
  protocolVersion,
2213
2293
  replayOptions: { operation },
2214
2294
  skipIndexValidation: true
2215
- });
2295
+ }), document.header.protocolVersions);
2216
2296
  }
2217
2297
  if (targetRevision !== void 0 && operation.index === targetRevision) break;
2218
2298
  }
@@ -2339,6 +2419,11 @@ function validateFeatureFlags(flags, prerequisites) {
2339
2419
  //#endregion
2340
2420
  //#region src/executor/execution-scope.ts
2341
2421
  var DefaultExecutionScope = class {
2422
+ operationStore;
2423
+ operationIndex;
2424
+ writeCache;
2425
+ documentMetaCache;
2426
+ collectionMembershipCache;
2342
2427
  constructor(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache) {
2343
2428
  this.operationStore = operationStore;
2344
2429
  this.operationIndex = operationIndex;
@@ -2358,6 +2443,13 @@ var DefaultExecutionScope = class {
2358
2443
  }
2359
2444
  };
2360
2445
  var KyselyExecutionScope = class {
2446
+ db;
2447
+ operationStore;
2448
+ operationIndex;
2449
+ keyframeStore;
2450
+ writeCache;
2451
+ documentMetaCache;
2452
+ collectionMembershipCache;
2361
2453
  constructor(db, operationStore, operationIndex, keyframeStore, writeCache, documentMetaCache, collectionMembershipCache) {
2362
2454
  this.db = db;
2363
2455
  this.operationStore = operationStore;
@@ -2385,7 +2477,7 @@ var KyselyExecutionScope = class {
2385
2477
  };
2386
2478
  //#endregion
2387
2479
  //#region src/utils/reshuffle.ts
2388
- const STRICT_ORDER_ACTION_TYPES = new Set([
2480
+ const STRICT_ORDER_ACTION_TYPES = /* @__PURE__ */ new Set([
2389
2481
  "CREATE_DOCUMENT",
2390
2482
  "DELETE_DOCUMENT",
2391
2483
  "UPGRADE_DOCUMENT",
@@ -2716,6 +2808,8 @@ const DRIVE_COLLECTION_PREFIX = "drive.";
2716
2808
  * rows and persisted remotes remain valid without migration.
2717
2809
  */
2718
2810
  var DriveCollectionId = class DriveCollectionId {
2811
+ driveId;
2812
+ branch;
2719
2813
  constructor(driveId, branch) {
2720
2814
  this.driveId = driveId;
2721
2815
  this.branch = branch;
@@ -2748,6 +2842,11 @@ var DriveCollectionId = class DriveCollectionId {
2748
2842
  //#endregion
2749
2843
  //#region src/executor/document-action-handler.ts
2750
2844
  var DocumentActionHandler = class {
2845
+ registry;
2846
+ logger;
2847
+ driveContainerTypes;
2848
+ featureFlags;
2849
+ decisionModel;
2751
2850
  constructor(registry, logger, driveContainerTypes, featureFlags, decisionModel) {
2752
2851
  this.registry = registry;
2753
2852
  this.logger = logger;
@@ -2840,7 +2939,7 @@ var DocumentActionHandler = class {
2840
2939
  [job.scope]: [...standing.operations[job.scope] ?? [], operation]
2841
2940
  };
2842
2941
  executing.touchedStreams.add(job.documentId, job.scope, job.branch);
2843
- stores.writeCache.putState(job.documentId, job.scope, job.branch, operation.index, standing, SnapshotPosition.Head);
2942
+ stores.writeCache.putState(job.documentId, job.scope, job.branch, operation.index, standing, "head");
2844
2943
  indexTxn.write([{
2845
2944
  ...operation,
2846
2945
  documentId: job.documentId,
@@ -2852,6 +2951,7 @@ var DocumentActionHandler = class {
2852
2951
  stores.documentMetaCache.putDocumentMeta(job.documentId, job.branch, {
2853
2952
  state: standing.state.document,
2854
2953
  documentType: standing.header.documentType,
2954
+ protocolVersions: standing.header.protocolVersions,
2855
2955
  documentScopeRevision: operation.index + 1
2856
2956
  });
2857
2957
  return buildSuccessResult(job, operation, job.documentId, standing.header.documentType, JSON.stringify({
@@ -2875,8 +2975,8 @@ var DocumentActionHandler = class {
2875
2975
  branch: job.branch
2876
2976
  });
2877
2977
  const resultingStateObj = {
2878
- header: document.header,
2879
- ...document.state
2978
+ ...document.state,
2979
+ header: document.header
2880
2980
  };
2881
2981
  const resultingState = JSON.stringify(resultingStateObj);
2882
2982
  const writeResult = await this.writeOperationToStore({
@@ -2893,7 +2993,7 @@ var DocumentActionHandler = class {
2893
2993
  [job.scope]: [...document.operations[job.scope] ?? [], operation]
2894
2994
  };
2895
2995
  executing.touchedStreams.add(document.header.id, job.scope, job.branch);
2896
- stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document, SnapshotPosition.Head);
2996
+ stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document, "head");
2897
2997
  indexTxn.write([{
2898
2998
  ...operation,
2899
2999
  documentId: document.header.id,
@@ -2910,6 +3010,7 @@ var DocumentActionHandler = class {
2910
3010
  stores.documentMetaCache.putDocumentMeta(document.header.id, job.branch, {
2911
3011
  state: document.state.document,
2912
3012
  documentType: document.header.documentType,
3013
+ protocolVersions: document.header.protocolVersions,
2913
3014
  documentScopeRevision: 1
2914
3015
  });
2915
3016
  return buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
@@ -2953,7 +3054,7 @@ var DocumentActionHandler = class {
2953
3054
  [job.scope]: [...document.operations[job.scope] ?? [], operation]
2954
3055
  };
2955
3056
  executing.touchedStreams.add(documentId, job.scope, job.branch);
2956
- stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, SnapshotPosition.Head);
3057
+ stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, "head");
2957
3058
  indexTxn.write([{
2958
3059
  ...operation,
2959
3060
  documentId,
@@ -2965,6 +3066,7 @@ var DocumentActionHandler = class {
2965
3066
  stores.documentMetaCache.putDocumentMeta(documentId, job.branch, {
2966
3067
  state: document.state.document,
2967
3068
  documentType: document.header.documentType,
3069
+ protocolVersions: document.header.protocolVersions,
2968
3070
  documentScopeRevision: operation.index + 1
2969
3071
  });
2970
3072
  return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
@@ -3003,7 +3105,7 @@ var DocumentActionHandler = class {
3003
3105
  } catch (error) {
3004
3106
  return buildErrorResult(job, /* @__PURE__ */ new Error(`Failed to fetch revisions for upgrade: ${error instanceof Error ? error.message : String(error)}`), startTime);
3005
3107
  }
3006
- const revisionScopes = new Set([...Object.keys(input.revision), ...Object.keys(actualRevisions)]);
3108
+ const revisionScopes = /* @__PURE__ */ new Set([...Object.keys(input.revision), ...Object.keys(actualRevisions)]);
3007
3109
  for (const revisionScope of revisionScopes) {
3008
3110
  const snapshot = input.revision[revisionScope] ?? 0;
3009
3111
  const actual = actualRevisions[revisionScope] ?? 0;
@@ -3045,8 +3147,8 @@ var DocumentActionHandler = class {
3045
3147
  branch: job.branch
3046
3148
  });
3047
3149
  const resultingStateObj = {
3048
- header: document.header,
3049
- ...document.state
3150
+ ...document.state,
3151
+ header: document.header
3050
3152
  };
3051
3153
  if (fromVersion > 0) resultingStateObj.__migrated = true;
3052
3154
  const resultingState = JSON.stringify(resultingStateObj);
@@ -3064,7 +3166,7 @@ var DocumentActionHandler = class {
3064
3166
  [job.scope]: [...document.operations[job.scope] ?? [], operation]
3065
3167
  };
3066
3168
  executing.touchedStreams.add(documentId, job.scope, job.branch);
3067
- stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, SnapshotPosition.Head);
3169
+ stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, "head");
3068
3170
  for (const scope of otherScopes) executing.postCommitInvalidations.push({
3069
3171
  documentId,
3070
3172
  scope,
@@ -3081,6 +3183,7 @@ var DocumentActionHandler = class {
3081
3183
  stores.documentMetaCache.putDocumentMeta(documentId, job.branch, {
3082
3184
  state: document.state.document,
3083
3185
  documentType: document.header.documentType,
3186
+ protocolVersions: document.header.protocolVersions,
3084
3187
  documentScopeRevision: operation.index + 1
3085
3188
  });
3086
3189
  return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
@@ -3150,7 +3253,7 @@ var DocumentActionHandler = class {
3150
3253
  const resultingState = JSON.stringify(resultingStateObj);
3151
3254
  executing.touchedStreams.add(input.sourceId, job.scope, job.branch);
3152
3255
  executing.touchedStreams.add(input.targetId, job.scope, job.branch);
3153
- stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc, SnapshotPosition.Head);
3256
+ stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc, "head");
3154
3257
  indexTxn.write([{
3155
3258
  ...operation,
3156
3259
  documentId: input.sourceId,
@@ -3169,6 +3272,7 @@ var DocumentActionHandler = class {
3169
3272
  stores.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
3170
3273
  state: sourceDoc.state.document,
3171
3274
  documentType: sourceDoc.header.documentType,
3275
+ protocolVersions: sourceDoc.header.protocolVersions,
3172
3276
  documentScopeRevision: operation.index + 1
3173
3277
  });
3174
3278
  return buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
@@ -3197,55 +3301,655 @@ var DocumentActionHandler = class {
3197
3301
  }
3198
3302
  };
3199
3303
  //#endregion
3200
- //#region src/executor/signature-verifier.ts
3201
- var SignatureVerifier = class {
3202
- constructor(verifier) {
3203
- this.verifier = verifier;
3204
- }
3205
- async verifyActions(documentId, branch, actions) {
3206
- if (!this.verifier) return;
3207
- for (const action of actions) {
3208
- const signer = action.context?.signer;
3209
- if (!signer) continue;
3210
- if (signer.signatures.length === 0) throw new InvalidSignatureError(documentId, `Action ${action.id} has signer but no signatures`);
3211
- const publicKey = signer.app.key;
3212
- let isValid;
3213
- try {
3214
- const tempOperation = {
3215
- id: deriveOperationId(documentId, action.scope, branch, action.id),
3216
- index: 0,
3217
- timestampUtcMs: action.timestampUtcMs || (/* @__PURE__ */ new Date()).toISOString(),
3218
- hash: "",
3219
- skip: 0,
3220
- action
3221
- };
3222
- isValid = await this.verifier(tempOperation, publicKey);
3223
- } catch (error) {
3224
- const errorMessage = error instanceof Error ? error.message : String(error);
3225
- throw new InvalidSignatureError(documentId, `Action ${action.id} verification failed: ${errorMessage}`);
3226
- }
3227
- if (!isValid) throw new InvalidSignatureError(documentId, `Action ${action.id} signature verification returned false`);
3304
+ //#region src/signer/passthrough-signer.ts
3305
+ /**
3306
+ * A no-op signer that returns empty values for all methods.
3307
+ * Used when signing is not required.
3308
+ */
3309
+ var PassthroughSigner = class {
3310
+ publicKey = {};
3311
+ sign() {
3312
+ return Promise.resolve(/* @__PURE__ */ new Uint8Array(0));
3313
+ }
3314
+ verify() {
3315
+ return Promise.resolve();
3316
+ }
3317
+ signAction() {
3318
+ return Promise.resolve([
3319
+ "",
3320
+ "",
3321
+ "",
3322
+ "",
3323
+ ""
3324
+ ]);
3325
+ }
3326
+ };
3327
+ //#endregion
3328
+ //#region src/signer/trust-policy.ts
3329
+ /**
3330
+ * The policy admission asks: the reactor's own key signing as its own user is
3331
+ * accepted, then the host's policy decides, else the default does. `own` is
3332
+ * read on every ask, since its user changes on login.
3333
+ */
3334
+ function admissionTrustPolicy(own, authEnforcement, policy) {
3335
+ return { authorizeSigner(signer, key, documentId) {
3336
+ const identity = actionSignerIdentity(own);
3337
+ if (identity.app.key !== "" && key === identity.app.key) {
3338
+ if (sameUser(actionSignerIdentity(signer).user, identity.user)) return Promise.resolve(true);
3228
3339
  }
3340
+ if (policy) return policy.authorizeSigner(signer, key, documentId);
3341
+ return Promise.resolve(!authEnforcement);
3342
+ } };
3343
+ }
3344
+ function sameUser(a, b) {
3345
+ return a.address.toLowerCase() === b.address.toLowerCase() && a.networkId === b.networkId && a.chainId === b.chainId;
3346
+ }
3347
+ //#endregion
3348
+ //#region src/signer/did-key.ts
3349
+ const DID_KEY_PREFIX = "did:key:z";
3350
+ const P256_MULTICODEC = [128, 36];
3351
+ const MAX_CACHED_KEYS = 1024;
3352
+ const P = BigInt("0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff");
3353
+ const A = BigInt("0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc");
3354
+ const B = BigInt("0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b");
3355
+ /** A did:key that is not a compressed P-256 public key. */
3356
+ var MalformedDidKeyError = class extends Error {
3357
+ constructor(did, detail) {
3358
+ super(`Malformed did:key ${did}: ${detail}`);
3359
+ this.name = "MalformedDidKeyError";
3229
3360
  }
3230
- async verifyOperations(documentId, operations) {
3231
- if (!this.verifier) return;
3232
- for (let i = 0; i < operations.length; i++) {
3233
- const operation = operations[i];
3234
- const signer = operation.action.context?.signer;
3235
- if (!signer) continue;
3236
- if (signer.signatures.length === 0) throw new InvalidSignatureError(documentId, `Operation ${operation.id} at index ${operation.index} has signer but no signatures`);
3237
- const publicKey = signer.app.key;
3238
- let isValid;
3239
- try {
3240
- isValid = await this.verifier(operation, publicKey);
3241
- } catch (error) {
3242
- const errorMessage = error instanceof Error ? error.message : String(error);
3243
- throw new InvalidSignatureError(documentId, `Operation ${operation.id} at index ${operation.index} verification failed: ${errorMessage}`);
3361
+ };
3362
+ const keyCache = /* @__PURE__ */ new Map();
3363
+ /** Cached per process: decompressing the point costs a BigInt modPow. */
3364
+ async function importDidKey(did) {
3365
+ const cached = keyCache.get(did);
3366
+ if (cached) {
3367
+ keyCache.delete(did);
3368
+ keyCache.set(did, cached);
3369
+ return cached;
3370
+ }
3371
+ const raw = decompressP256(compressedPointFromDid(did), did);
3372
+ const key = await crypto.subtle.importKey("raw", raw.buffer, {
3373
+ name: "ECDSA",
3374
+ namedCurve: "P-256"
3375
+ }, false, ["verify"]);
3376
+ keyCache.set(did, key);
3377
+ if (keyCache.size > MAX_CACHED_KEYS) {
3378
+ const oldest = keyCache.keys().next().value;
3379
+ if (oldest !== void 0) keyCache.delete(oldest);
3380
+ }
3381
+ return key;
3382
+ }
3383
+ function compressedPointFromDid(did) {
3384
+ if (!did.startsWith(DID_KEY_PREFIX)) throw new MalformedDidKeyError(did, "not a base58btc did:key");
3385
+ const decoded = base58Decode(did.slice(9));
3386
+ if (!decoded || decoded.length !== 35) throw new MalformedDidKeyError(did, "not a compressed P-256 key");
3387
+ if (decoded[0] !== P256_MULTICODEC[0] || decoded[1] !== P256_MULTICODEC[1]) throw new MalformedDidKeyError(did, "not a P-256 multicodec");
3388
+ return decoded.subarray(2);
3389
+ }
3390
+ function decompressP256(compressed, did) {
3391
+ const prefix = compressed[0];
3392
+ if (prefix !== 2 && prefix !== 3) throw new MalformedDidKeyError(did, `compression prefix ${prefix}`);
3393
+ let x = BigInt(0);
3394
+ for (let i = 1; i < compressed.length; i++) x = x << BigInt(8) | BigInt(compressed[i]);
3395
+ if (x >= P) throw new MalformedDidKeyError(did, "x outside the field");
3396
+ const ySquared = (modPow(x, BigInt(3), P) + A * x + B) % P;
3397
+ let y = modPow(ySquared, (P + BigInt(1)) / BigInt(4), P);
3398
+ if (y * y % P !== ySquared) throw new MalformedDidKeyError(did, "x is not on the curve");
3399
+ if (y % BigInt(2) === BigInt(0) !== (prefix === 2)) y = P - y;
3400
+ const uncompressed = /* @__PURE__ */ new Uint8Array(65);
3401
+ uncompressed[0] = 4;
3402
+ uncompressed.set(bigIntToBytes(x), 1);
3403
+ uncompressed.set(bigIntToBytes(y), 33);
3404
+ return uncompressed;
3405
+ }
3406
+ function modPow(base, exp, mod) {
3407
+ let result = BigInt(1);
3408
+ let b = base % mod;
3409
+ let e = exp;
3410
+ while (e > BigInt(0)) {
3411
+ if (e & BigInt(1)) result = result * b % mod;
3412
+ e >>= BigInt(1);
3413
+ b = b * b % mod;
3414
+ }
3415
+ return result;
3416
+ }
3417
+ function bigIntToBytes(value) {
3418
+ const bytes = /* @__PURE__ */ new Uint8Array(32);
3419
+ let v = value;
3420
+ for (let i = 31; i >= 0; i--) {
3421
+ bytes[i] = Number(v & BigInt(255));
3422
+ v >>= BigInt(8);
3423
+ }
3424
+ return bytes;
3425
+ }
3426
+ //#endregion
3427
+ //#region src/signer/verify-action-signature.ts
3428
+ const RENOWN_HASH_LENGTH = 44;
3429
+ const SHARED_HASH_LENGTH = 28;
3430
+ const HEX_SIGNATURE = /^(0x)?([0-9a-fA-F]{2})+$/;
3431
+ /**
3432
+ * Integrity only: identity binding and the live-id check sit with the caller.
3433
+ * `operation` is the incoming operation at load admission.
3434
+ */
3435
+ async function verifyActionSignature(action, target, path, operation) {
3436
+ const policy = target.policy ?? "legacy";
3437
+ const refusal = policyRefusal(action, policy);
3438
+ if (refusal) return refusal;
3439
+ const shape = signerShape(action.context?.signer);
3440
+ if (shape.kind === "unsigned") return policy === "v2-required" ? refuse("unsigned", "UNSIGNED_REQUIRED", `action ${action.id} is unsigned but ${target.documentId} requires v2 signatures`) : {
3441
+ ok: true,
3442
+ scheme: "unsigned"
3443
+ };
3444
+ if (shape.kind === "malformed") return refuse("legacy-unknown", "MALFORMED_TUPLE", `action ${action.id} ${shape.reason}`);
3445
+ const { signer, tuple } = shape;
3446
+ const scheme = schemeOf(tuple[2]);
3447
+ if (tuple[1] !== signer.app.key) return refuse(scheme, "KEY_MISMATCH", `action ${action.id} tuple key does not match signer.app.key`);
3448
+ if (scheme !== "v2" && policy === "v2-required") return refuse(scheme, "SCHEME_BELOW_POLICY", `action ${action.id} carries a legacy tuple but ${target.documentId} requires v2 signatures`);
3449
+ if (scheme === "legacy-unknown" && path === "mutation") return refuse(scheme, "MALFORMED_TUPLE", `action ${action.id} carries a hash of unknown length ${tuple[2].length}`);
3450
+ if (scheme === "v2") {
3451
+ const refusal = await checkV2(action, tuple, target, operation);
3452
+ if (refusal) return refusal;
3453
+ } else if (path === "mutation") {
3454
+ if (await legacyHash(scheme, action, target) !== tuple[2]) return refuse(scheme, "HASH_MISMATCH", `action ${action.id} does not match the hash its signature covers`);
3455
+ }
3456
+ return verifyEcdsa(tuple, scheme, action.id);
3457
+ }
3458
+ /** Refusals a document's policy makes before any tuple is read. */
3459
+ function policyRefusal(action, policy) {
3460
+ if (action.type !== "CREATE_DOCUMENT") return;
3461
+ const scheme = schemeOfAction(action);
3462
+ const input = action.input;
3463
+ const documentId = input?.documentId ?? "";
3464
+ if (policy === "legacy") return isDerivedDocumentId(documentId) ? refuse(scheme, "ID_MISMATCH", `action ${action.id}: a legacy document cannot take the content-addressed id ${documentId}`) : void 0;
3465
+ const expected = derivedCreateId(input);
3466
+ if (expected === documentId) return;
3467
+ return refuse(scheme, "ID_MISMATCH", expected === void 0 ? `action ${action.id} creates v2-required ${documentId} without the header params its id is derived from` : `action ${action.id} creates v2-required ${documentId}, but its header params derive ${expected}`);
3468
+ }
3469
+ function derivedCreateId(input) {
3470
+ if (!input?.signing || !input.protocolVersions) return;
3471
+ try {
3472
+ return deriveDocumentId({
3473
+ documentType: input.model,
3474
+ createdAtUtcIso: input.signing.createdAtUtcIso,
3475
+ nonce: input.signing.nonce,
3476
+ protocolVersions: input.protocolVersions
3477
+ });
3478
+ } catch {
3479
+ return;
3480
+ }
3481
+ }
3482
+ function schemeOfAction(action) {
3483
+ const shape = signerShape(action.context?.signer);
3484
+ if (shape.kind === "unsigned") return "unsigned";
3485
+ return shape.kind === "signed" ? schemeOf(shape.tuple[2]) : "legacy-unknown";
3486
+ }
3487
+ /** Peers send arbitrary JSON, so nothing past `context` is assumed. */
3488
+ function signerShape(value) {
3489
+ if (value === void 0 || value === null) return { kind: "unsigned" };
3490
+ if (!isRecord(value)) return {
3491
+ kind: "malformed",
3492
+ reason: "has a signer that is not an object"
3493
+ };
3494
+ const { app, user, signatures } = value;
3495
+ if (app !== void 0 && app !== null && !isRecord(app)) return {
3496
+ kind: "malformed",
3497
+ reason: "has a signer.app that is not an object"
3498
+ };
3499
+ const key = isRecord(app) ? app.key : void 0;
3500
+ if (key !== void 0 && key !== null && typeof key !== "string") return {
3501
+ kind: "malformed",
3502
+ reason: "has a signer.app.key that is not a string"
3503
+ };
3504
+ if (!key) return { kind: "unsigned" };
3505
+ if (!isRecord(user) || typeof user.address !== "string") return {
3506
+ kind: "malformed",
3507
+ reason: "has a signer without a user address"
3508
+ };
3509
+ if (!Array.isArray(signatures)) return {
3510
+ kind: "malformed",
3511
+ reason: "has a signer whose signatures are not a list"
3512
+ };
3513
+ const tuple = signatures.at(-1);
3514
+ if (!isTuple(tuple)) return {
3515
+ kind: "malformed",
3516
+ reason: signatures.length === 0 ? "has a signer but no signatures" : "carries a malformed signature tuple"
3517
+ };
3518
+ return {
3519
+ kind: "signed",
3520
+ signer: value,
3521
+ tuple
3522
+ };
3523
+ }
3524
+ function isRecord(value) {
3525
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3526
+ }
3527
+ async function checkV2(action, tuple, target, operation) {
3528
+ const problem = v2TupleProblem(tuple);
3529
+ if (problem) return refuse("v2", "MALFORMED_TUPLE", `action ${action.id}: ${problem}`);
3530
+ if (operation && !sameInstant(operation.timestampUtcMs, action.timestampUtcMs)) return refuse("v2", "TIMESTAMP_MISMATCH", `action ${action.id} is stamped ${action.timestampUtcMs} but its operation ${operation.timestampUtcMs}`);
3531
+ let expected;
3532
+ try {
3533
+ expected = await hashActionV2(action, target, action.context.signer);
3534
+ } catch (error) {
3535
+ return refuse("v2", "HASH_MISMATCH", `action ${action.id} has no v2 hash: ${error instanceof Error ? error.message : String(error)}`);
3536
+ }
3537
+ if (expected !== tuple[2]) return refuse("v2", "HASH_MISMATCH", `action ${action.id} does not match the hash its signature covers`);
3538
+ }
3539
+ function sameInstant(a, b) {
3540
+ const left = Date.parse(a);
3541
+ return !Number.isNaN(left) && left === Date.parse(b);
3542
+ }
3543
+ function schemeOf(hash) {
3544
+ if (isV2ActionHash(hash)) return "v2";
3545
+ if (hash.length === RENOWN_HASH_LENGTH) return "legacy-renown";
3546
+ if (hash.length === SHARED_HASH_LENGTH) return "legacy-shared";
3547
+ return "legacy-unknown";
3548
+ }
3549
+ async function legacyHash(scheme, action, target) {
3550
+ if (scheme === "legacy-renown") {
3551
+ const payload = [
3552
+ action.scope,
3553
+ action.type,
3554
+ JSON.stringify(action.input)
3555
+ ].join("");
3556
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(payload));
3557
+ return bytesToBase64(new Uint8Array(digest));
3558
+ }
3559
+ const signer = action.context.signer;
3560
+ return buildOperationSignatureParams({
3561
+ documentId: target.documentId,
3562
+ signer,
3563
+ action,
3564
+ previousStateHash: ""
3565
+ })[2];
3566
+ }
3567
+ async function verifyEcdsa(tuple, scheme, actionId) {
3568
+ const [timestamp, key, hash, prevStateHash, signatureHex] = tuple;
3569
+ if (!HEX_SIGNATURE.test(signatureHex)) return refuse(scheme, "MALFORMED_TUPLE", `action ${actionId} signature is not hex`);
3570
+ let cryptoKey;
3571
+ try {
3572
+ cryptoKey = await importDidKey(key);
3573
+ } catch (error) {
3574
+ return refuse(scheme, "MALFORMED_TUPLE", `action ${actionId}: ${error instanceof Error ? error.message : String(error)}`);
3575
+ }
3576
+ const message = buildOperationSignatureMessage([
3577
+ timestamp,
3578
+ key,
3579
+ hash,
3580
+ prevStateHash
3581
+ ]);
3582
+ let valid;
3583
+ try {
3584
+ valid = await crypto.subtle.verify({
3585
+ name: "ECDSA",
3586
+ hash: "SHA-256"
3587
+ }, cryptoKey, hexToBytes(signatureHex).buffer, message.buffer);
3588
+ } catch {
3589
+ valid = false;
3590
+ }
3591
+ if (!valid) return refuse(scheme, "BAD_SIGNATURE", `action ${actionId} signature does not verify under its key`);
3592
+ return {
3593
+ ok: true,
3594
+ scheme
3595
+ };
3596
+ }
3597
+ function isTuple(value) {
3598
+ return Array.isArray(value) && value.length === 5 && value.every((element) => typeof element === "string");
3599
+ }
3600
+ function refuse(scheme, code, reason) {
3601
+ return {
3602
+ ok: false,
3603
+ scheme,
3604
+ code,
3605
+ reason
3606
+ };
3607
+ }
3608
+ function hexToBytes(hex) {
3609
+ const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
3610
+ const bytes = new Uint8Array(clean.length / 2);
3611
+ for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16);
3612
+ return bytes;
3613
+ }
3614
+ function bytesToBase64(bytes) {
3615
+ let binary = "";
3616
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
3617
+ return btoa(binary);
3618
+ }
3619
+ //#endregion
3620
+ //#region src/executor/synthesized-signing.ts
3621
+ /** Submitted types whose stored action the reducer writes in their place. */
3622
+ const SYNTHESIZING_TYPES = /* @__PURE__ */ new Set(["UNDO", "REDO"]);
3623
+ /**
3624
+ * The id of the action synthesized from `submittedId`. Derived, so the stored
3625
+ * operation answers for the submitted id at the live-id check.
3626
+ */
3627
+ function synthesizedActionId(submittedId) {
3628
+ return hashBrowser(`synthesized:${submittedId}`, "sha1", "hex").slice(0, 32);
3629
+ }
3630
+ /**
3631
+ * Whether `operation` stores an action the reducer made from `submitted`
3632
+ * rather than `submitted` itself or an operation already in `history`.
3633
+ */
3634
+ function isSynthesized(submitted, operation, history) {
3635
+ if (!SYNTHESIZING_TYPES.has(submitted.type)) return false;
3636
+ const id = operation.action.id;
3637
+ if (!id) return true;
3638
+ if (id === submitted.id) return false;
3639
+ return !history.some((existing) => existing.action.id === id);
3640
+ }
3641
+ /**
3642
+ * Signs the synthesized action of `operation` in place as `signer`, under the
3643
+ * id {@link synthesizedActionId} derives from the submitted one. A REDO's
3644
+ * rebuilt action has no timestamp, so it takes the submitted one; the
3645
+ * operation timestamp is set to the action's, which a peer requires of a v2
3646
+ * tuple.
3647
+ */
3648
+ async function signSynthesized(operation, submitted, target, signer, signal) {
3649
+ const synthesized = operation.action;
3650
+ const id = synthesizedActionId(submitted.id);
3651
+ const timestampUtcMs = operation.timestampUtcMs || synthesized.timestampUtcMs || submitted.timestampUtcMs;
3652
+ const action = {
3653
+ id,
3654
+ timestampUtcMs,
3655
+ type: synthesized.type,
3656
+ input: synthesized.input,
3657
+ scope: synthesized.scope
3658
+ };
3659
+ const signature = await signer.signAction(action, {
3660
+ documentId: target.documentId,
3661
+ branch: target.branch
3662
+ }, signal);
3663
+ operation.action = {
3664
+ ...action,
3665
+ context: { signer: {
3666
+ ...actionSignerIdentity(signer),
3667
+ signatures: [signature]
3668
+ } }
3669
+ };
3670
+ operation.id = deriveOperationId(target.documentId, target.scope, target.branch, id);
3671
+ operation.timestampUtcMs = timestampUtcMs;
3672
+ }
3673
+ //#endregion
3674
+ //#region src/executor/signature-admission.ts
3675
+ const DEFAULT_TRUST_TIMEOUT_MS = 1e4;
3676
+ /** Runs once per write, when this reactor first stores it; never on re-appends. */
3677
+ var SignatureAdmission = class {
3678
+ mode;
3679
+ logger;
3680
+ eventBus;
3681
+ policySource;
3682
+ trustPolicy;
3683
+ trustTimeoutMs;
3684
+ constructor(mode, logger, eventBus, policySource = "meta", trust = {
3685
+ authEnforcement: false,
3686
+ timeoutMs: DEFAULT_TRUST_TIMEOUT_MS
3687
+ }) {
3688
+ this.mode = mode;
3689
+ this.logger = logger;
3690
+ this.eventBus = eventBus;
3691
+ this.policySource = policySource;
3692
+ this.trustPolicy = admissionTrustPolicy(trust.signer ?? new PassthroughSigner(), trust.authEnforcement, trust.policy);
3693
+ this.trustTimeoutMs = trust.timeoutMs;
3694
+ }
3695
+ /** The first refusal of a mutation's submitted actions, when enforcing. */
3696
+ async admitMutation(job, stores, signal) {
3697
+ const { operationStore } = stores;
3698
+ const candidates = job.actions.map((action) => candidate(action, mutationStream(action, job)));
3699
+ const live = await this.liveOperationIds(candidates, operationStore, signal);
3700
+ const committed = await this.storedAsSubmitted(candidates, live, isRetry(job), operationStore, signal);
3701
+ await resolvePolicies(candidates, job.actions, (documentId, branch) => storedPolicy(stores, this.policySource, documentId, branch, signal));
3702
+ const submitted = /* @__PURE__ */ new Set();
3703
+ for (const entry of candidates) {
3704
+ if (committed.has(entry)) continue;
3705
+ const verdict = await this.verdict(entry, live, submitted, "mutation", signal);
3706
+ submitted.add(entry.opId);
3707
+ if (verdict.ok) continue;
3708
+ const refusal = this.record(job, entry, verdict, "mutation");
3709
+ if (refusal) return {
3710
+ kind: "refused",
3711
+ error: refusal
3712
+ };
3713
+ }
3714
+ return {
3715
+ kind: "admitted",
3716
+ committed: [...committed.values()]
3717
+ };
3718
+ }
3719
+ /** The incoming operations a load drops, empty unless enforcing. */
3720
+ async admitLoad(job, operations, stores, signal) {
3721
+ const { operationStore } = stores;
3722
+ const stream = {
3723
+ documentId: job.documentId,
3724
+ scope: job.scope,
3725
+ branch: job.branch
3726
+ };
3727
+ const candidates = operations.map((operation) => ({
3728
+ ...candidate(operation.action, stream),
3729
+ operation
3730
+ }));
3731
+ const live = await this.liveOperationIds(candidates, operationStore, signal);
3732
+ const redelivered = await this.redelivered(candidates, live, operationStore, signal);
3733
+ await resolvePolicies(candidates, job.operations.map((operation) => operation.action), (documentId, branch) => storedPolicy(stores, this.policySource, documentId, branch, signal));
3734
+ const dropped = /* @__PURE__ */ new Set();
3735
+ for (let i = 0; i < candidates.length; i++) {
3736
+ const verdict = await this.verdict(candidates[i], live, void 0, "load", signal, redelivered.has(candidates[i]));
3737
+ if (verdict.ok) continue;
3738
+ if (this.record(job, candidates[i], verdict, "load")) dropped.add(operations[i]);
3739
+ }
3740
+ return dropped;
3741
+ }
3742
+ async verdict(entry, live, submitted, path, signal, known = false) {
3743
+ const verdict = await verifyActionSignature(entry.action, {
3744
+ documentId: entry.stream.documentId,
3745
+ branch: entry.stream.branch,
3746
+ policy: entry.policy
3747
+ }, path, entry.operation);
3748
+ if (!verdict.ok) return verdict;
3749
+ if (!known && (isLive(entry, live) || submitted?.has(entry.opId))) return {
3750
+ ok: false,
3751
+ scheme: verdict.scheme,
3752
+ code: "DUPLICATE_ACTION",
3753
+ reason: `action ${entry.action.id} is already in the stream`
3754
+ };
3755
+ if (verdict.scheme === "unsigned") {
3756
+ const address = entry.action.context?.signer?.user?.address;
3757
+ return address ? {
3758
+ ok: false,
3759
+ scheme: "unsigned",
3760
+ code: "UNSIGNED_IDENTITY",
3761
+ reason: `action ${entry.action.id} is unsigned but claims to act as ${address}`
3762
+ } : verdict;
3763
+ }
3764
+ if (known) return verdict;
3765
+ const signer = entry.action.context.signer;
3766
+ if (!await this.authorize(signer, entry.stream.documentId, signal)) return {
3767
+ ok: false,
3768
+ scheme: verdict.scheme,
3769
+ code: "SIGNER_UNAUTHORIZED",
3770
+ reason: `action ${entry.action.id}: key ${signer.app.key} may not sign as ${signer.user.address}`
3771
+ };
3772
+ return verdict;
3773
+ }
3774
+ /** Throws when the policy does, times out or the job is aborted. */
3775
+ async authorize(signer, documentId, signal) {
3776
+ signal?.throwIfAborted();
3777
+ let timer;
3778
+ let onAbort;
3779
+ const stop = new Promise((_, reject) => {
3780
+ timer = setTimeout(() => {
3781
+ reject(/* @__PURE__ */ new Error(`signature trust policy did not answer for ${signer.app.key} within ${this.trustTimeoutMs}ms`));
3782
+ }, this.trustTimeoutMs);
3783
+ if (signal) {
3784
+ onAbort = () => reject(signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason)));
3785
+ signal.addEventListener("abort", onAbort, { once: true });
3244
3786
  }
3245
- if (!isValid) throw new InvalidSignatureError(documentId, `Operation ${operation.id} at index ${operation.index} signature verification returned false`);
3787
+ });
3788
+ try {
3789
+ return await Promise.race([this.trustPolicy.authorizeSigner(signer, signer.app.key, documentId), stop]);
3790
+ } finally {
3791
+ clearTimeout(timer);
3792
+ if (onAbort) signal?.removeEventListener("abort", onAbort);
3793
+ }
3794
+ }
3795
+ /**
3796
+ * The live candidates stored exactly as submitted and still standing: one
3797
+ * an undo or a reshuffle has since superseded is refused, not re-emitted with
3798
+ * the state it once produced. A synthesized operation
3799
+ * holds another action, so it cannot be compared; only a retry, whose first
3800
+ * attempt this job was, takes it as committed.
3801
+ */
3802
+ async storedAsSubmitted(candidates, live, retry, operationStore, signal) {
3803
+ const committed = /* @__PURE__ */ new Map();
3804
+ const toCompare = [];
3805
+ for (const entry of candidates) if (entry.synthesizedOpId && live.has(entry.synthesizedOpId)) {
3806
+ if (retry) committed.set(entry, committedWrite(entry, entry.synthesizedOpId));
3807
+ } else if (live.has(entry.opId)) toCompare.push(entry);
3808
+ for (const [stream, entries] of byStream(toCompare)) {
3809
+ const stored = await operationStore.getOperationsByIds(stream.documentId, stream.scope, stream.branch, entries.map((entry) => entry.opId), signal);
3810
+ const byId = new Map(stored.map((operation) => [operation.id, operation]));
3811
+ const matching = entries.filter((entry) => {
3812
+ const operation = byId.get(entry.opId);
3813
+ return operation && sameContent(operation.action, entry.action);
3814
+ });
3815
+ if (matching.length === 0) continue;
3816
+ const later = await operationStore.getSince(stream.documentId, stream.scope, stream.branch, Math.min(...matching.map((entry) => byId.get(entry.opId).index)), void 0, void 0, signal);
3817
+ for (const entry of matching) if (!isSuperseded(byId.get(entry.opId), later.results)) committed.set(entry, committedWrite(entry, entry.opId));
3818
+ }
3819
+ return committed;
3820
+ }
3821
+ /**
3822
+ * The load candidates stored exactly as they arrive, timestamp included: a
3823
+ * peer re-sending what this stream holds, not a replay of its action.
3824
+ */
3825
+ async redelivered(candidates, live, operationStore, signal) {
3826
+ const found = /* @__PURE__ */ new Set();
3827
+ const toCompare = candidates.filter((entry) => live.has(entry.opId) && !(entry.synthesizedOpId && live.has(entry.synthesizedOpId)));
3828
+ for (const [stream, entries] of byStream(toCompare)) {
3829
+ const stored = await operationStore.getOperationsByIds(stream.documentId, stream.scope, stream.branch, entries.map((entry) => entry.opId), signal);
3830
+ for (const entry of entries) if (stored.some((operation) => operation.id === entry.opId && Date.parse(operation.timestampUtcMs) === Date.parse(entry.operation?.timestampUtcMs ?? "") && sameContent(operation.action, entry.action))) found.add(entry);
3831
+ }
3832
+ return found;
3833
+ }
3834
+ async liveOperationIds(candidates, operationStore, signal) {
3835
+ const live = /* @__PURE__ */ new Set();
3836
+ for (const [stream, entries] of byStream(candidates)) {
3837
+ const found = await operationStore.findOperationIds(stream.documentId, stream.scope, stream.branch, entries.flatMap((entry) => entry.synthesizedOpId ? [entry.opId, entry.synthesizedOpId] : [entry.opId]), signal);
3838
+ for (const opId of found) live.add(opId);
3246
3839
  }
3840
+ return live;
3841
+ }
3842
+ /** Logs and counts a refusal; returns the error only when enforcing. */
3843
+ record(job, entry, refusal, path) {
3844
+ const enforced = this.mode === "enforce";
3845
+ const event = {
3846
+ jobId: job.id,
3847
+ documentId: entry.stream.documentId,
3848
+ scope: entry.stream.scope,
3849
+ branch: entry.stream.branch,
3850
+ actionId: entry.action.id,
3851
+ code: refusal.code,
3852
+ scheme: refusal.scheme,
3853
+ path,
3854
+ enforced,
3855
+ reason: refusal.reason
3856
+ };
3857
+ this.logger.warn(enforced ? "Signature refused: @Refusal" : "Signature refusal logged, write admitted: @Refusal", event);
3858
+ this.eventBus.emit(ReactorEventTypes.SIGNATURE_REFUSED, event).catch((error) => {
3859
+ this.logger.error("Failed to emit SIGNATURE_REFUSED: @Error", error);
3860
+ });
3861
+ return enforced ? new InvalidSignatureError(entry.stream.documentId, refusal.code, refusal.reason) : void 0;
3247
3862
  }
3248
3863
  };
3864
+ /**
3865
+ * A CREATE_DOCUMENT is verified under its own input. Anything else takes the
3866
+ * stored document's policy, or, for a document this job creates, that CREATE's.
3867
+ */
3868
+ async function resolvePolicies(candidates, jobActions, stored) {
3869
+ const resolved = /* @__PURE__ */ new Map();
3870
+ for (const entry of candidates) {
3871
+ if (entry.action.type === "CREATE_DOCUMENT") {
3872
+ entry.policy = createPolicy(entry.action);
3873
+ continue;
3874
+ }
3875
+ const { documentId, branch } = entry.stream;
3876
+ const key = `${documentId}\u0000${branch}`;
3877
+ let policy = resolved.get(key);
3878
+ if (policy === void 0) {
3879
+ policy = await stored(documentId, branch) ?? createdPolicy(jobActions, documentId) ?? "legacy";
3880
+ resolved.set(key, policy);
3881
+ }
3882
+ entry.policy = policy;
3883
+ }
3884
+ }
3885
+ async function storedPolicy(stores, source, documentId, branch, signal) {
3886
+ try {
3887
+ if (source === "write-cache") {
3888
+ const document = await stores.writeCache.getState(documentId, "document", branch, void 0, signal);
3889
+ return signaturePolicyOf(document.header);
3890
+ }
3891
+ const meta = await stores.documentMetaCache.getDocumentMeta(documentId, branch, signal);
3892
+ return signaturePolicyOf(meta.protocolVersions);
3893
+ } catch (error) {
3894
+ if (DocumentNotFoundError.isError(error)) return;
3895
+ throw error;
3896
+ }
3897
+ }
3898
+ function createdPolicy(actions, documentId) {
3899
+ const create = actions.find((action) => action.type === "CREATE_DOCUMENT" && action.input?.documentId === documentId);
3900
+ return create ? createPolicy(create) : void 0;
3901
+ }
3902
+ function createPolicy(action) {
3903
+ return signaturePolicyOf(action.input);
3904
+ }
3905
+ function candidate(action, stream) {
3906
+ const opId = (actionId) => deriveOperationId(stream.documentId, stream.scope, stream.branch, actionId);
3907
+ return {
3908
+ action,
3909
+ stream,
3910
+ opId: opId(action.id),
3911
+ synthesizedOpId: SYNTHESIZING_TYPES.has(action.type) ? opId(synthesizedActionId(action.id)) : void 0
3912
+ };
3913
+ }
3914
+ function isLive(entry, live) {
3915
+ return live.has(entry.opId) || entry.synthesizedOpId !== void 0 && live.has(entry.synthesizedOpId);
3916
+ }
3917
+ function mutationStream(action, job) {
3918
+ return {
3919
+ ...actionSigningTarget(action, job.documentId, job.branch),
3920
+ scope: job.scope
3921
+ };
3922
+ }
3923
+ function byStream(candidates) {
3924
+ const groups = /* @__PURE__ */ new Map();
3925
+ for (const entry of candidates) {
3926
+ const key = `${entry.stream.documentId}\u0000${entry.stream.scope}\u0000${entry.stream.branch}`;
3927
+ const group = groups.get(key) ?? [entry.stream, []];
3928
+ group[1].push(entry);
3929
+ groups.set(key, group);
3930
+ }
3931
+ return [...groups.values()];
3932
+ }
3933
+ function isRetry(job) {
3934
+ return job.errorHistory.length > 0 || (job.retryCount ?? 0) > 0;
3935
+ }
3936
+ function committedWrite(entry, opId) {
3937
+ return {
3938
+ ...entry.stream,
3939
+ actionId: entry.action.id,
3940
+ opId
3941
+ };
3942
+ }
3943
+ function isSuperseded(operation, later) {
3944
+ return later.some((next) => next.index > operation.index && next.skip > 0 && next.index - next.skip <= operation.index);
3945
+ }
3946
+ function sameContent(stored, submitted) {
3947
+ try {
3948
+ return canonicalJson(stored) === canonicalJson(submitted);
3949
+ } catch {
3950
+ return false;
3951
+ }
3952
+ }
3249
3953
  //#endregion
3250
3954
  //#region src/executor/simple-job-executor.ts
3251
3955
  const MAX_SKIP_THRESHOLD = 1e3;
@@ -3263,6 +3967,7 @@ function isValidISOTimestamp(value) {
3263
3967
  * every test expect a failed job to look like.
3264
3968
  */
3265
3969
  var JobRollbackSignal = class extends Error {
3970
+ result;
3266
3971
  constructor(result) {
3267
3972
  super("job rolled back");
3268
3973
  this.result = result;
@@ -3270,13 +3975,27 @@ var JobRollbackSignal = class extends Error {
3270
3975
  }
3271
3976
  };
3272
3977
  var SimpleJobExecutor = class {
3978
+ logger;
3979
+ registry;
3980
+ operationStore;
3981
+ eventBus;
3982
+ writeCache;
3983
+ operationIndex;
3984
+ documentMetaCache;
3985
+ collectionMembershipCache;
3986
+ driveContainerTypes;
3273
3987
  config;
3274
3988
  featureFlags;
3275
3989
  decisionModel;
3276
- signatureVerifierModule;
3990
+ signatureAdmission;
3277
3991
  documentActionHandler;
3278
3992
  executionScope;
3279
- constructor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, driveContainerTypes, config, signatureVerifier, executionScope) {
3993
+ signer;
3994
+ /**
3995
+ * `signer` signs the operations the reducer synthesizes; unsigned if omitted.
3996
+ * `trustPolicy` decides which keys may sign as which users at admission.
3997
+ */
3998
+ constructor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, driveContainerTypes, config, executionScope, signer, trustPolicy) {
3280
3999
  this.logger = logger;
3281
4000
  this.registry = registry;
3282
4001
  this.operationStore = operationStore;
@@ -3286,6 +4005,7 @@ var SimpleJobExecutor = class {
3286
4005
  this.documentMetaCache = documentMetaCache;
3287
4006
  this.collectionMembershipCache = collectionMembershipCache;
3288
4007
  this.driveContainerTypes = driveContainerTypes;
4008
+ this.signer = signer ?? new PassthroughSigner();
3289
4009
  this.config = {
3290
4010
  featureFlags: config.featureFlags ?? {},
3291
4011
  maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
@@ -3295,11 +4015,17 @@ var SimpleJobExecutor = class {
3295
4015
  retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
3296
4016
  retryMaxDelayMs: config.retryMaxDelayMs ?? 5e3,
3297
4017
  yieldDeadlineMs: config.yieldDeadlineMs ?? 50,
3298
- batchApplies: config.batchApplies ?? true
4018
+ batchApplies: config.batchApplies ?? true,
4019
+ signatureVerification: config.signatureVerification ?? "enforce"
3299
4020
  };
3300
4021
  this.featureFlags = resolveFeatureFlags(config.featureFlags);
3301
4022
  this.decisionModel = selectDecisionModel(this.featureFlags, registry);
3302
- this.signatureVerifierModule = new SignatureVerifier(signatureVerifier);
4023
+ this.signatureAdmission = new SignatureAdmission(this.config.signatureVerification, logger, eventBus, this.featureFlags.documentDecisions ? "write-cache" : "meta", {
4024
+ signer: this.signer,
4025
+ authEnforcement: this.featureFlags.authEnforcement,
4026
+ policy: trustPolicy,
4027
+ timeoutMs: Math.min(DEFAULT_TRUST_TIMEOUT_MS, this.config.jobTimeoutMs / 2)
4028
+ });
3303
4029
  this.documentActionHandler = new DocumentActionHandler(registry, logger, driveContainerTypes, this.featureFlags, this.decisionModel);
3304
4030
  this.executionScope = executionScope ?? new DefaultExecutionScope(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache);
3305
4031
  }
@@ -3429,10 +4155,29 @@ var SimpleJobExecutor = class {
3429
4155
  pendingEvent
3430
4156
  };
3431
4157
  }
3432
- const positioned = await this.positionByTimestamp(job, stores, signal);
4158
+ let admission;
4159
+ try {
4160
+ admission = await this.signatureAdmission.admitMutation(job, stores, signal);
4161
+ } catch (error) {
4162
+ return { result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) };
4163
+ }
4164
+ if (admission.kind === "refused") return { result: buildErrorResult(job, admission.error, startTime) };
4165
+ let committed;
4166
+ try {
4167
+ committed = await this.reloadCommitted(admission.committed, stores, signal);
4168
+ } catch (error) {
4169
+ return { result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) };
4170
+ }
4171
+ const committedIds = new Set(admission.committed.map((write) => write.actionId));
4172
+ const fresh = committedIds.size === 0 ? job : {
4173
+ ...job,
4174
+ actions: job.actions.filter((action) => !committedIds.has(action.id))
4175
+ };
4176
+ if (fresh.actions.length === 0) return this.committedOutcome(job, committed, [], [], stores, startTime);
4177
+ const positioned = await this.positionByTimestamp(fresh, stores, signal);
3433
4178
  if (positioned.error) return { result: buildErrorResult(job, positioned.error, startTime) };
3434
4179
  const executing = {
3435
- job,
4180
+ job: fresh,
3436
4181
  startTime,
3437
4182
  indexTxn,
3438
4183
  stores,
@@ -3462,13 +4207,23 @@ var SimpleJobExecutor = class {
3462
4207
  } };
3463
4208
  const ordinals = await stores.operationIndex.commit(indexTxn, signal);
3464
4209
  postCommitMembershipInvalidations.push(...indexTxn.getMembershipInvalidations());
3465
- if (actionResult.operationsWithContext.length > 0) {
3466
- for (let i = 0; i < actionResult.operationsWithContext.length; i++) actionResult.operationsWithContext[i].context.ordinal = ordinals[i];
3467
- const collectionMemberships = await this.getCollectionMembershipsForOperations(actionResult.operationsWithContext, stores);
4210
+ for (let i = 0; i < actionResult.operationsWithContext.length; i++) actionResult.operationsWithContext[i].context.ordinal = ordinals[i];
4211
+ return this.committedOutcome(job, committed, actionResult.operationsWithContext, actionResult.generatedOperations, stores, startTime);
4212
+ }
4213
+ /**
4214
+ * A successful mutation's result and the write-ready event it owes, carrying
4215
+ * what earlier attempts committed ahead of what this one wrote.
4216
+ */
4217
+ async committedOutcome(job, reloaded, written, generated, stores, startTime) {
4218
+ const operationsWithContext = [...reloaded, ...written];
4219
+ let pendingEvent;
4220
+ if (operationsWithContext.length > 0) {
4221
+ const collectionMemberships = await this.getCollectionMembershipsForOperations(operationsWithContext, stores);
3468
4222
  pendingEvent = {
3469
4223
  jobId: job.id,
3470
- operations: actionResult.operationsWithContext,
4224
+ operations: operationsWithContext,
3471
4225
  jobMeta: job.meta,
4226
+ submittedActionIds: submittedActionIds(job),
3472
4227
  collectionMemberships
3473
4228
  };
3474
4229
  }
@@ -3476,14 +4231,62 @@ var SimpleJobExecutor = class {
3476
4231
  result: {
3477
4232
  job,
3478
4233
  success: true,
3479
- operations: actionResult.generatedOperations,
3480
- operationsWithContext: actionResult.operationsWithContext,
4234
+ operations: [...reloaded.map((entry) => entry.operation), ...generated],
4235
+ operationsWithContext,
3481
4236
  duration: Date.now() - startTime
3482
4237
  },
3483
4238
  pendingEvent
3484
4239
  };
3485
4240
  }
3486
4241
  /**
4242
+ * The stored operations of writes an earlier attempt committed, as a fresh
4243
+ * write would report them. State is rebuilt from the write cache, and left
4244
+ * out when it cannot be.
4245
+ */
4246
+ async reloadCommitted(writes, stores, signal) {
4247
+ const streams = /* @__PURE__ */ new Map();
4248
+ for (const write of writes) {
4249
+ const key = `${write.documentId}\u0000${write.scope}\u0000${write.branch}`;
4250
+ streams.set(key, [...streams.get(key) ?? [], write]);
4251
+ }
4252
+ const reloaded = [];
4253
+ for (const group of streams.values()) {
4254
+ const { documentId, scope, branch } = group[0];
4255
+ const opIds = [...new Set(group.map((write) => write.opId))];
4256
+ const operations = await stores.operationStore.getOperationsByIds(documentId, scope, branch, opIds, signal);
4257
+ const ordinals = await stores.operationIndex.getOrdinalsByOpIds(documentId, scope, branch, opIds, signal);
4258
+ const { documentType } = await stores.documentMetaCache.getDocumentMeta(documentId, branch, signal);
4259
+ for (const operation of operations.sort((a, b) => a.index - b.index)) reloaded.push({
4260
+ operation,
4261
+ context: {
4262
+ documentId,
4263
+ scope,
4264
+ branch,
4265
+ documentType,
4266
+ resultingState: await this.rebuiltState(stores, {
4267
+ documentId,
4268
+ scope,
4269
+ branch
4270
+ }, operation, signal),
4271
+ ordinal: ordinals.get(operation.id) ?? 0
4272
+ }
4273
+ });
4274
+ }
4275
+ return reloaded;
4276
+ }
4277
+ async rebuiltState(stores, stream, operation, signal) {
4278
+ try {
4279
+ const document = await stores.writeCache.getState(stream.documentId, stream.scope, stream.branch, operation.index, signal);
4280
+ return JSON.stringify({
4281
+ ...document.state,
4282
+ header: document.header
4283
+ });
4284
+ } catch (error) {
4285
+ this.logger.warn("No state for committed operation @OperationId in @Stream: @Error", operation.id, stream, error);
4286
+ return;
4287
+ }
4288
+ }
4289
+ /**
3487
4290
  * Drops the cached state of every stream a job wrote, after its transaction
3488
4291
  * did not commit.
3489
4292
  *
@@ -3512,16 +4315,6 @@ var SimpleJobExecutor = class {
3512
4315
  const actions = writes.map((write) => write.action);
3513
4316
  const generatedOperations = [];
3514
4317
  const operationsWithContext = [];
3515
- try {
3516
- await this.signatureVerifierModule.verifyActions(job.documentId, job.branch, actions);
3517
- } catch (error) {
3518
- return {
3519
- success: false,
3520
- generatedOperations,
3521
- operationsWithContext,
3522
- error: error instanceof Error ? error : new Error(String(error))
3523
- };
3524
- }
3525
4318
  for (const action of actions) if (action.timestampUtcMs && !isValidISOTimestamp(action.timestampUtcMs)) return {
3526
4319
  success: false,
3527
4320
  generatedOperations,
@@ -3619,7 +4412,7 @@ var SimpleJobExecutor = class {
3619
4412
  if (docMeta.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
3620
4413
  documentVersion = docMeta.state.version;
3621
4414
  }
3622
- if (isUndoRedo(action) || action.type === "PRUNE" || skip > 0) stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
4415
+ if (isUndoRedo(action) || skip > 0) stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
3623
4416
  let document;
3624
4417
  if (baseDocument !== void 0) document = baseDocument;
3625
4418
  else try {
@@ -3679,7 +4472,7 @@ var SimpleJobExecutor = class {
3679
4472
  branch: job.branch,
3680
4473
  protocolVersion
3681
4474
  };
3682
- updatedDocument = module.reducer(document, action, void 0, reducerOptions);
4475
+ updatedDocument = withProtocolVersions(module.reducer(document, action, void 0, reducerOptions), document.header.protocolVersions);
3683
4476
  } catch (error) {
3684
4477
  const contextMessage = `Failed to apply action to document:\n Action type: ${action.type}\n Document ID: ${job.documentId}\n Document type: ${document.header.documentType}\n Scope: ${job.scope}\n Original error: ${error instanceof Error ? error.message : String(error)}`;
3685
4478
  const enhancedError = new Error(contextMessage);
@@ -3691,6 +4484,15 @@ var SimpleJobExecutor = class {
3691
4484
  if (operations.length === 0) return buildErrorResult(job, /* @__PURE__ */ new Error("No operation generated from action"), startTime);
3692
4485
  const newOperation = operations[operations.length - 1];
3693
4486
  if (!isUndoRedo(action)) newOperation.skip = skip;
4487
+ if (deniedReason === void 0 && sourceOperation === void 0 && !executing.replayingAcceptedHistory && isSynthesized(action, newOperation, document.operations[scope] ?? [])) try {
4488
+ await signSynthesized(newOperation, action, {
4489
+ documentId: job.documentId,
4490
+ scope,
4491
+ branch: job.branch
4492
+ }, this.signer, signal);
4493
+ } catch (error) {
4494
+ return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
4495
+ }
3694
4496
  const resultingState = JSON.stringify({
3695
4497
  ...updatedDocument.state,
3696
4498
  header: updatedDocument.header
@@ -3791,7 +4593,7 @@ var SimpleJobExecutor = class {
3791
4593
  * apply and its own reasons for it.
3792
4594
  * - A positional or replayed run carries skips and re-appended operations,
3793
4595
  * whose indices are not a simple ascending run from the head.
3794
- * - UNDO, REDO, PRUNE and NOOP-with-skip each invalidate the write cache to
4596
+ * - UNDO, REDO and NOOP-with-skip each invalidate the write cache to
3795
4597
  * force a full-history rebuild, so they cannot be reduced against state
3796
4598
  * threaded from the write before them.
3797
4599
  * - The auth scope decides later writes against the policy earlier ones
@@ -3809,7 +4611,7 @@ var SimpleJobExecutor = class {
3809
4611
  if (scope === "auth" || scope === "document") return false;
3810
4612
  return writes.every((write) => {
3811
4613
  const type = write.action.type;
3812
- return write.skip === 0 && write.deniedReason === void 0 && write.sourceOperation === void 0 && !DOCUMENT_SCOPE_ACTIONS.has(type) && !isUndoRedo(write.action) && type !== "PRUNE" && type !== "NOOP";
4614
+ return write.skip === 0 && write.deniedReason === void 0 && write.sourceOperation === void 0 && !DOCUMENT_SCOPE_ACTIONS.has(type) && !isUndoRedo(write.action) && type !== "NOOP";
3813
4615
  });
3814
4616
  }
3815
4617
  /**
@@ -3932,13 +4734,22 @@ var SimpleJobExecutor = class {
3932
4734
  }
3933
4735
  if (!backdated) return plain();
3934
4736
  const conflicting = (await stores.operationStore.getConflicting(job.documentId, job.scope, job.branch, earliest, void 0, signal)).results.filter((operation) => !isGenesisOperation(operation));
3935
- if (conflicting.length === 0) {
4737
+ const nothingToMove = async () => {
3936
4738
  if (!this.featureFlags.authEnforcement) return plain();
3937
4739
  return this.evaluatePositioned(job, stores, this.appendedOperations(job, revisions.revision[job.scope] ?? 0), signal);
3938
- }
4740
+ };
4741
+ if (conflicting.length === 0) return nothingToMove();
3939
4742
  const nextIndex = revisions.revision[job.scope] ?? 0;
3940
4743
  let firstConflicting = conflicting[0].index;
3941
4744
  for (const operation of conflicting) if (operation.index < firstConflicting) firstConflicting = operation.index;
4745
+ const stored = (await stores.operationStore.getSince(job.documentId, job.scope, job.branch, firstConflicting - 1, void 0, void 0, signal)).results;
4746
+ const conflictingIds = new Set(conflicting.map((operation) => operation.id));
4747
+ const effective = garbageCollect(sortOperations(stored)).filter((operation) => !isGenesisOperation(operation));
4748
+ const firstMoving = effective.findIndex((operation) => conflictingIds.has(operation.id));
4749
+ const moving = firstMoving === -1 ? [] : effective.slice(firstMoving);
4750
+ if (moving.length === 0) return nothingToMove();
4751
+ let firstRetracted = moving[0].index;
4752
+ for (const operation of moving) firstRetracted = Math.min(firstRetracted, operation.index - operation.skip);
3942
4753
  const incoming = job.actions.map((action, i) => ({
3943
4754
  id: action.id,
3944
4755
  index: nextIndex + i,
@@ -3949,8 +4760,8 @@ var SimpleJobExecutor = class {
3949
4760
  }));
3950
4761
  const merged = reshuffleByTimestamp({
3951
4762
  index: nextIndex,
3952
- skip: retractionSkip(nextIndex, firstConflicting)
3953
- }, conflicting, incoming);
4763
+ skip: retractionSkip(nextIndex, firstRetracted)
4764
+ }, moving, incoming);
3954
4765
  stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
3955
4766
  if (!this.featureFlags.authEnforcement) return {
3956
4767
  writes: merged.map((operation) => ({
@@ -4098,7 +4909,7 @@ var SimpleJobExecutor = class {
4098
4909
  stores.writeCache.invalidate(job.documentId, scope, job.branch);
4099
4910
  const result = await this.processActions(tail.map((operation, i) => ({
4100
4911
  action: operation.action,
4101
- skip: i === 0 ? retractionSkip(nextIndex, tail[0].index) : 0,
4912
+ skip: i === 0 ? retractionSkip(nextIndex, tail[0].index - tail[0].skip) : 0,
4102
4913
  sourceRemote: "",
4103
4914
  deniedReason: reevaluated[firstChange + i]
4104
4915
  })), {
@@ -4187,36 +4998,32 @@ var SimpleJobExecutor = class {
4187
4998
  error: new InvalidOperationTimestampError(job.documentId, scope, operation.timestampUtcMs, `operation (index: ${operation.index})`),
4188
4999
  duration: Date.now() - startTime
4189
5000
  };
4190
- let minIncomingIndex = Number.POSITIVE_INFINITY;
4191
- let minIncomingTimestamp = job.operations[0]?.timestampUtcMs || "";
4192
- for (const operation of job.operations) {
4193
- minIncomingIndex = Math.min(minIncomingIndex, operation.index);
4194
- const ts = operation.timestampUtcMs || "";
4195
- if (Date.parse(ts) < Date.parse(minIncomingTimestamp)) minIncomingTimestamp = ts;
4196
- }
4197
- let conflictingOps;
4198
- try {
4199
- conflictingOps = (await stores.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, void 0, signal)).results;
4200
- } catch {
4201
- conflictingOps = [];
4202
- }
4203
- let allOpsFromMinConflictingIndex = conflictingOps;
4204
- if (conflictingOps.length > 0) {
4205
- const minConflictingIndex = Math.min(...conflictingOps.map((op) => op.index));
5001
+ const dropped = /* @__PURE__ */ new Set();
5002
+ const admitted = /* @__PURE__ */ new Set();
5003
+ let selection = await this.selectLoadWrites(job, job.operations, stores, signal);
5004
+ for (;;) {
5005
+ const unadmitted = selection.incomingOpsToApply.filter((operation) => !admitted.has(operation));
5006
+ if (unadmitted.length === 0) break;
5007
+ let refused;
4206
5008
  try {
4207
- allOpsFromMinConflictingIndex = (await stores.operationStore.getSince(job.documentId, scope, job.branch, minConflictingIndex - 1, void 0, void 0, signal)).results;
4208
- } catch {
4209
- allOpsFromMinConflictingIndex = conflictingOps;
5009
+ refused = await this.signatureAdmission.admitLoad(job, unadmitted, stores, signal);
5010
+ } catch (error) {
5011
+ return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
4210
5012
  }
5013
+ for (const operation of unadmitted) if (refused.has(operation)) dropped.add(operation);
5014
+ else admitted.add(operation);
5015
+ if (refused.size === 0) break;
5016
+ const remaining = job.operations.filter((operation) => !dropped.has(operation));
5017
+ if (remaining.length === 0) return {
5018
+ job,
5019
+ success: true,
5020
+ operations: [],
5021
+ operationsWithContext: [],
5022
+ duration: Date.now() - startTime
5023
+ };
5024
+ selection = await this.selectLoadWrites(job, remaining, stores, signal);
4211
5025
  }
4212
- const incomingActionIds = new Set(job.operations.map((op) => op.action.id));
4213
- const nonSupersededOps = conflictingOps.filter((op) => {
4214
- if (op.index < minIncomingIndex && !incomingActionIds.has(op.action.id)) return false;
4215
- for (const laterOp of allOpsFromMinConflictingIndex) if (laterOp.index > op.index && laterOp.skip > 0) {
4216
- if (laterOp.index - laterOp.skip <= op.index) return false;
4217
- }
4218
- return true;
4219
- });
5026
+ const { nonSupersededOps, allOpsFromMinConflictingIndex, incomingOpsToApply } = selection;
4220
5027
  const existingOpsToReshuffle = monotonicAuthStream ? [] : nonSupersededOps.filter((operation) => !isGenesisOperation(operation));
4221
5028
  const actionIdCounts = /* @__PURE__ */ new Map();
4222
5029
  for (const operation of allOpsFromMinConflictingIndex) actionIdCounts.set(operation.action.id, (actionIdCounts.get(operation.action.id) ?? 0) + 1);
@@ -4237,14 +5044,6 @@ var SimpleJobExecutor = class {
4237
5044
  const logicalSkip = latestRevision - minLogicalIndex;
4238
5045
  if (logicalSkip > skipCount) skipCount = logicalSkip;
4239
5046
  }
4240
- const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
4241
- const seenIncomingActionIds = /* @__PURE__ */ new Set();
4242
- const incomingOpsToApply = job.operations.filter((op) => {
4243
- if (existingActionIds.has(op.action.id)) return false;
4244
- if (seenIncomingActionIds.has(op.action.id)) return false;
4245
- seenIncomingActionIds.add(op.action.id);
4246
- return true;
4247
- });
4248
5047
  if (incomingOpsToApply.length === 0) return {
4249
5048
  job,
4250
5049
  success: true,
@@ -4272,7 +5071,8 @@ var SimpleJobExecutor = class {
4272
5071
  ...operation,
4273
5072
  id: operation.id
4274
5073
  })));
4275
- for (const operation of reshuffledOperations) if (operation.action.type === "NOOP" && operation.skip === 0) operation.skip = 1;
5074
+ const incomingIds = new Set(incomingOpsToApply.map((operation) => operation.action.id));
5075
+ for (const operation of reshuffledOperations) if (operation.action.type === "NOOP" && operation.skip === 0 && incomingIds.has(operation.action.id)) operation.skip = 1;
4276
5076
  let deniedReasons;
4277
5077
  if (this.featureFlags.documentDecisions) try {
4278
5078
  deniedReasons = await evaluateByPosition(this.decisionModel, {
@@ -4324,6 +5124,71 @@ var SimpleJobExecutor = class {
4324
5124
  duration: Date.now() - startTime
4325
5125
  };
4326
5126
  }
5127
+ /** The conflicting window a load opens, and which of its operations are new. */
5128
+ async selectLoadWrites(job, operations, stores, signal) {
5129
+ const scope = job.scope;
5130
+ let minIncomingIndex = Number.POSITIVE_INFINITY;
5131
+ let minIncomingTimestamp = operations[0]?.timestampUtcMs || "";
5132
+ for (const operation of operations) {
5133
+ minIncomingIndex = Math.min(minIncomingIndex, operation.index);
5134
+ const ts = operation.timestampUtcMs || "";
5135
+ if (Date.parse(ts) < Date.parse(minIncomingTimestamp)) minIncomingTimestamp = ts;
5136
+ }
5137
+ let conflictingOps;
5138
+ try {
5139
+ conflictingOps = (await stores.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, void 0, signal)).results;
5140
+ } catch {
5141
+ conflictingOps = [];
5142
+ }
5143
+ let allOpsFromMinConflictingIndex = conflictingOps;
5144
+ if (conflictingOps.length > 0) {
5145
+ const minConflictingIndex = Math.min(...conflictingOps.map((op) => op.index));
5146
+ try {
5147
+ allOpsFromMinConflictingIndex = (await stores.operationStore.getSince(job.documentId, scope, job.branch, minConflictingIndex - 1, void 0, void 0, signal)).results;
5148
+ } catch {
5149
+ allOpsFromMinConflictingIndex = conflictingOps;
5150
+ }
5151
+ }
5152
+ const incomingActionIds = new Set(operations.map((op) => op.action.id));
5153
+ const supersededBy = (op) => {
5154
+ let nearest;
5155
+ for (const laterOp of allOpsFromMinConflictingIndex) if (laterOp.index > op.index && laterOp.skip > 0 && laterOp.index - laterOp.skip <= op.index && (nearest === void 0 || laterOp.index < nearest.index)) nearest = laterOp;
5156
+ return nearest;
5157
+ };
5158
+ const undoneActionIds = /* @__PURE__ */ new Set();
5159
+ let predecessorBound = minIncomingIndex;
5160
+ const rowsByActionId = /* @__PURE__ */ new Map();
5161
+ for (const op of conflictingOps) if (incomingActionIds.has(op.action.id)) rowsByActionId.set(op.action.id, [...rowsByActionId.get(op.action.id) ?? [], op]);
5162
+ const liveOps = /* @__PURE__ */ new Set();
5163
+ for (const op of conflictingOps) if (supersededBy(op) === void 0) liveOps.add(op);
5164
+ for (const [actionId, rows] of rowsByActionId) {
5165
+ if (rows.some((row) => liveOps.has(row))) continue;
5166
+ if (rows.some((row) => supersededBy(row)?.action.type === "NOOP")) undoneActionIds.add(actionId);
5167
+ else predecessorBound = Math.min(predecessorBound, Math.max(...rows.map((row) => row.index)));
5168
+ }
5169
+ const nonSupersededOps = conflictingOps.filter((op) => {
5170
+ if (op.index < predecessorBound && !incomingActionIds.has(op.action.id)) return false;
5171
+ return liveOps.has(op);
5172
+ });
5173
+ let rewoundFrom = Number.POSITIVE_INFINITY;
5174
+ for (const op of nonSupersededOps) if (!isGenesisOperation(op)) rewoundFrom = Math.min(rewoundFrom, op.index - op.skip);
5175
+ const reappended = new Set(nonSupersededOps.map((op) => op.index));
5176
+ for (const op of allOpsFromMinConflictingIndex) if (op.index >= rewoundFrom && !reappended.has(op.index) && supersededBy(op) === void 0) nonSupersededOps.push(op);
5177
+ const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
5178
+ const seenIncomingActionIds = /* @__PURE__ */ new Set();
5179
+ const incomingOpsToApply = operations.filter((op) => {
5180
+ if (existingActionIds.has(op.action.id)) return false;
5181
+ if (undoneActionIds.has(op.action.id)) return false;
5182
+ if (seenIncomingActionIds.has(op.action.id)) return false;
5183
+ seenIncomingActionIds.add(op.action.id);
5184
+ return true;
5185
+ });
5186
+ return {
5187
+ nonSupersededOps,
5188
+ allOpsFromMinConflictingIndex,
5189
+ incomingOpsToApply
5190
+ };
5191
+ }
4327
5192
  accumulateResultOrReturnError(result, generatedOperations, operationsWithContext) {
4328
5193
  if (!result.success) return result;
4329
5194
  if (result.operations && result.operations.length > 0) generatedOperations.push(...result.operations);
@@ -4470,6 +5335,7 @@ var DocumentModelRegistry = class {
4470
5335
  //#endregion
4471
5336
  //#region src/storage/kysely/keyframe-store.ts
4472
5337
  var KyselyKeyframeStore = class KyselyKeyframeStore {
5338
+ db;
4473
5339
  trx;
4474
5340
  constructor(db) {
4475
5341
  this.db = db;
@@ -4554,6 +5420,11 @@ function paginateRows(rows, paging, cursorOf, toItem, refetch) {
4554
5420
  //#endregion
4555
5421
  //#region src/storage/txn.ts
4556
5422
  var AtomicTransaction = class {
5423
+ documentId;
5424
+ documentType;
5425
+ scope;
5426
+ branch;
5427
+ baseRevision;
4557
5428
  operations = [];
4558
5429
  constructor(documentId, documentType, scope, branch, baseRevision) {
4559
5430
  this.documentId = documentId;
@@ -4587,6 +5458,11 @@ var AtomicTransaction = class {
4587
5458
  //#endregion
4588
5459
  //#region src/storage/kysely/store.ts
4589
5460
  var _UniqueConstraintContext = class extends Error {
5461
+ documentId;
5462
+ scope;
5463
+ branch;
5464
+ revision;
5465
+ stagedOps;
4590
5466
  constructor(documentId, scope, branch, revision, stagedOps) {
4591
5467
  super("unique constraint");
4592
5468
  this.documentId = documentId;
@@ -4598,6 +5474,7 @@ var _UniqueConstraintContext = class extends Error {
4598
5474
  }
4599
5475
  };
4600
5476
  var KyselyOperationStore = class KyselyOperationStore {
5477
+ db;
4601
5478
  trx;
4602
5479
  constructor(db) {
4603
5480
  this.db = db;
@@ -4697,12 +5574,13 @@ var KyselyOperationStore = class KyselyOperationStore {
4697
5574
  * otherwise read a snapshot taken before the locks were held.
4698
5575
  */
4699
5576
  async acquireStreamLocks(trx, documentId, scope, branch, condition) {
4700
- const keys = new Set([`${documentId}:${scope}:${branch}`]);
5577
+ const keys = /* @__PURE__ */ new Set([`${documentId}:${scope}:${branch}`]);
4701
5578
  for (const stream of condition.streams) keys.add(`${stream.documentId}:${stream.scope}:${stream.branch}`);
5579
+ const sortedKeys = sql.join([...keys].sort());
4702
5580
  await sql`
4703
5581
  with ordered as materialized (
4704
5582
  select key
4705
- from unnest(array[${sql.join([...keys].sort())}]::text[]) with ordinality as t(key, ord)
5583
+ from unnest(array[${sortedKeys}]::text[]) with ordinality as t(key, ord)
4706
5584
  order by ord
4707
5585
  )
4708
5586
  select pg_advisory_xact_lock(hashtext(key)) from ordered
@@ -4779,6 +5657,14 @@ var KyselyOperationStore = class KyselyOperationStore {
4779
5657
  }
4780
5658
  return false;
4781
5659
  }
5660
+ /**
5661
+ * The paging cursor here encodes the index to resume from (one past the
5662
+ * last row returned), not the last row's own index. This keeps "0" an
5663
+ * unambiguous start-of-stream sentinel even when a page ends at index 0
5664
+ * (e.g. `limit: 1` on a fresh stream), which would otherwise make
5665
+ * `nextCursor` equal the start cursor and loop a caller that walks pages
5666
+ * forever.
5667
+ */
4782
5668
  async getSince(documentId, scope, branch, revision, filter, paging, signal) {
4783
5669
  throwIfAborted(signal);
4784
5670
  let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("index", ">", revision).orderBy("index", "asc");
@@ -4793,10 +5679,10 @@ var KyselyOperationStore = class KyselyOperationStore {
4793
5679
  }
4794
5680
  if (paging) {
4795
5681
  const cursorValue = Number.parseInt(paging.cursor, 10);
4796
- if (cursorValue > 0) query = query.where("index", ">", cursorValue);
5682
+ if (Number.isFinite(cursorValue) && cursorValue > 0) query = query.where("index", ">=", cursorValue);
4797
5683
  if (paging.limit) query = query.limit(paging.limit + 1);
4798
5684
  }
4799
- return paginateRows(await query.execute(), paging, (row) => row.index, (row) => this.rowToOperation(row), (cursor, limit) => this.getSince(documentId, scope, branch, revision, filter, {
5685
+ return paginateRows(await query.execute(), paging, (row) => row.index + 1, (row) => this.rowToOperation(row), (cursor, limit) => this.getSince(documentId, scope, branch, revision, filter, {
4800
5686
  cursor,
4801
5687
  limit
4802
5688
  }, signal));
@@ -4814,26 +5700,28 @@ var KyselyOperationStore = class KyselyOperationStore {
4814
5700
  limit
4815
5701
  }, signal));
4816
5702
  }
5703
+ /**
5704
+ * The paging cursor here encodes the index to resume from (one past the
5705
+ * last row returned), not the last row's own index, for the same reason as
5706
+ * `getSince`: a page ending at index 0 must not produce a cursor that is
5707
+ * indistinguishable from the start-of-stream sentinel.
5708
+ */
4817
5709
  async getConflicting(documentId, scope, branch, minTimestamp, paging, signal) {
4818
5710
  throwIfAborted(signal);
4819
5711
  let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("timestampUtcMs", ">=", new Date(minTimestamp)).orderBy("index", "asc");
4820
5712
  if (paging) {
4821
5713
  const cursorValue = Number.parseInt(paging.cursor, 10);
4822
- if (cursorValue > 0) query = query.where("index", ">", cursorValue);
5714
+ if (Number.isFinite(cursorValue) && cursorValue > 0) query = query.where("index", ">=", cursorValue);
4823
5715
  if (paging.limit) query = query.limit(paging.limit + 1);
4824
5716
  }
4825
- return paginateRows(await query.execute(), paging, (row) => row.index, (row) => this.rowToOperation(row), (cursor, limit) => this.getConflicting(documentId, scope, branch, minTimestamp, {
5717
+ return paginateRows(await query.execute(), paging, (row) => row.index + 1, (row) => this.rowToOperation(row), (cursor, limit) => this.getConflicting(documentId, scope, branch, minTimestamp, {
4826
5718
  cursor,
4827
5719
  limit
4828
5720
  }, signal));
4829
5721
  }
4830
5722
  async getRevisions(documentId, branch, signal) {
4831
5723
  throwIfAborted(signal);
4832
- const scopeRevisions = await this.queryExecutor.selectFrom("Operation as o1").select([
4833
- "o1.scope",
4834
- "o1.index",
4835
- "o1.timestampUtcMs"
4836
- ]).where("o1.documentId", "=", documentId).where("o1.branch", "=", branch).where((eb) => eb("o1.index", "=", eb.selectFrom("Operation as o2").select((eb2) => eb2.fn.max("o2.index").as("maxIndex")).where("o2.documentId", "=", eb.ref("o1.documentId")).where("o2.branch", "=", eb.ref("o1.branch")).where("o2.scope", "=", eb.ref("o1.scope")))).execute();
5724
+ const scopeRevisions = await this.queryExecutor.selectFrom("Operation").select("scope").select((eb) => eb.fn.max("index").as("index")).where("documentId", "=", documentId).where("branch", "=", branch).groupBy("scope").execute();
4837
5725
  const latest = await this.queryExecutor.selectFrom("Operation").select((eb) => eb.fn.max("timestampUtcMs").as("latestTimestamp")).where("documentId", "=", documentId).where("branch", "=", branch).executeTakeFirst();
4838
5726
  const revision = {};
4839
5727
  for (const row of scopeRevisions) revision[row.scope] = row.index + 1;
@@ -4846,6 +5734,20 @@ var KyselyOperationStore = class KyselyOperationStore {
4846
5734
  const latest = await this.queryExecutor.selectFrom("Operation").select((eb) => eb.fn.max("timestampUtcMs").as("latestTimestamp")).where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).executeTakeFirst();
4847
5735
  return latest?.latestTimestamp ? new Date(latest.latestTimestamp).toISOString() : void 0;
4848
5736
  }
5737
+ async findOperationIds(documentId, scope, branch, opIds, signal) {
5738
+ throwIfAborted(signal);
5739
+ if (opIds.length === 0) return /* @__PURE__ */ new Set();
5740
+ const rows = await this.queryExecutor.selectFrom("Operation").select("opId").distinct().where("opId", "in", opIds).where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).execute();
5741
+ return new Set(rows.map((row) => row.opId));
5742
+ }
5743
+ async getOperationsByIds(documentId, scope, branch, opIds, signal) {
5744
+ throwIfAborted(signal);
5745
+ if (opIds.length === 0) return [];
5746
+ const rows = await this.queryExecutor.selectFrom("Operation").selectAll().where("opId", "in", opIds).where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).orderBy("index", "asc").execute();
5747
+ const latest = /* @__PURE__ */ new Map();
5748
+ for (const row of rows) latest.set(row.opId, this.rowToOperation(row));
5749
+ return [...latest.values()];
5750
+ }
4849
5751
  rowToOperation(row) {
4850
5752
  return {
4851
5753
  index: row.index,
@@ -4939,8 +5841,8 @@ function createForwardingPoolInstrumentation(name) {
4939
5841
  }
4940
5842
  //#endregion
4941
5843
  //#region src/storage/migrations/001_create_operation_table.ts
4942
- var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$18 });
4943
- async function up$18(db) {
5844
+ var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$19 });
5845
+ async function up$19(db) {
4944
5846
  await db.schema.createTable("Operation").addColumn("id", "serial", (col) => col.primaryKey()).addColumn("jobId", "text", (col) => col.notNull()).addColumn("opId", "text", (col) => col.notNull()).addColumn("prevOpId", "text", (col) => col.notNull()).addColumn("writeTimestampUtcMs", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("documentId", "text", (col) => col.notNull()).addColumn("documentType", "text", (col) => col.notNull()).addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("timestampUtcMs", "timestamptz", (col) => col.notNull()).addColumn("index", "integer", (col) => col.notNull()).addColumn("action", "jsonb", (col) => col.notNull()).addColumn("skip", "integer", (col) => col.notNull()).addColumn("error", "text").addColumn("hash", "text", (col) => col.notNull()).addUniqueConstraint("unique_revision", [
4945
5847
  "documentId",
4946
5848
  "scope",
@@ -4965,8 +5867,8 @@ async function up$18(db) {
4965
5867
  }
4966
5868
  //#endregion
4967
5869
  //#region src/storage/migrations/002_create_keyframe_table.ts
4968
- var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$17 });
4969
- async function up$17(db) {
5870
+ var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$18 });
5871
+ async function up$18(db) {
4970
5872
  await db.schema.createTable("Keyframe").addColumn("id", "serial", (col) => col.primaryKey()).addColumn("documentId", "text", (col) => col.notNull()).addColumn("documentType", "text", (col) => col.notNull()).addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("revision", "integer", (col) => col.notNull()).addColumn("document", "jsonb", (col) => col.notNull()).addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addUniqueConstraint("unique_keyframe", [
4971
5873
  "documentId",
4972
5874
  "scope",
@@ -4982,14 +5884,14 @@ async function up$17(db) {
4982
5884
  }
4983
5885
  //#endregion
4984
5886
  //#region src/storage/migrations/003_create_document_table.ts
4985
- var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$16 });
4986
- async function up$16(db) {
5887
+ var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$17 });
5888
+ async function up$17(db) {
4987
5889
  await db.schema.createTable("Document").addColumn("id", "text", (col) => col.primaryKey()).addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
4988
5890
  }
4989
5891
  //#endregion
4990
5892
  //#region src/storage/migrations/004_create_document_relationship_table.ts
4991
- var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$15 });
4992
- async function up$15(db) {
5893
+ var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$16 });
5894
+ async function up$16(db) {
4993
5895
  await db.schema.createTable("DocumentRelationship").addColumn("id", "text", (col) => col.primaryKey()).addColumn("sourceId", "text", (col) => col.notNull().references("Document.id").onDelete("cascade")).addColumn("targetId", "text", (col) => col.notNull().references("Document.id").onDelete("cascade")).addColumn("relationshipType", "text", (col) => col.notNull()).addColumn("metadata", "jsonb").addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addUniqueConstraint("unique_source_target_type", [
4994
5896
  "sourceId",
4995
5897
  "targetId",
@@ -5001,14 +5903,14 @@ async function up$15(db) {
5001
5903
  }
5002
5904
  //#endregion
5003
5905
  //#region src/storage/migrations/005_create_indexer_state_table.ts
5004
- var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$14 });
5005
- async function up$14(db) {
5906
+ var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$15 });
5907
+ async function up$15(db) {
5006
5908
  await db.schema.createTable("IndexerState").addColumn("id", "integer", (col) => col.primaryKey().generatedAlwaysAsIdentity()).addColumn("lastOperationId", "integer", (col) => col.notNull()).addColumn("lastOperationTimestamp", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
5007
5909
  }
5008
5910
  //#endregion
5009
5911
  //#region src/storage/migrations/006_create_document_snapshot_table.ts
5010
- var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$13 });
5011
- async function up$13(db) {
5912
+ var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$14 });
5913
+ async function up$14(db) {
5012
5914
  await db.schema.createTable("DocumentSnapshot").addColumn("id", "text", (col) => col.primaryKey()).addColumn("documentId", "text", (col) => col.notNull()).addColumn("slug", "text").addColumn("name", "text").addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("content", "jsonb", (col) => col.notNull()).addColumn("documentType", "text", (col) => col.notNull()).addColumn("lastOperationIndex", "integer", (col) => col.notNull()).addColumn("lastOperationHash", "text", (col) => col.notNull()).addColumn("lastUpdatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("snapshotVersion", "integer", (col) => col.notNull().defaultTo(1)).addColumn("identifiers", "jsonb").addColumn("metadata", "jsonb").addColumn("isDeleted", "boolean", (col) => col.notNull().defaultTo(false)).addColumn("deletedAt", "timestamptz").addUniqueConstraint("unique_doc_scope_branch", [
5013
5915
  "documentId",
5014
5916
  "scope",
@@ -5029,8 +5931,8 @@ async function up$13(db) {
5029
5931
  }
5030
5932
  //#endregion
5031
5933
  //#region src/storage/migrations/007_create_slug_mapping_table.ts
5032
- var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$12 });
5033
- async function up$12(db) {
5934
+ var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$13 });
5935
+ async function up$13(db) {
5034
5936
  await db.schema.createTable("SlugMapping").addColumn("slug", "text", (col) => col.primaryKey()).addColumn("documentId", "text", (col) => col.notNull()).addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addUniqueConstraint("unique_docid_scope_branch", [
5035
5937
  "documentId",
5036
5938
  "scope",
@@ -5040,14 +5942,14 @@ async function up$12(db) {
5040
5942
  }
5041
5943
  //#endregion
5042
5944
  //#region src/storage/migrations/008_create_view_state_table.ts
5043
- var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$11 });
5044
- async function up$11(db) {
5945
+ var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$12 });
5946
+ async function up$12(db) {
5045
5947
  await db.schema.createTable("ViewState").addColumn("readModelId", "text", (col) => col.primaryKey()).addColumn("lastOrdinal", "integer", (col) => col.notNull().defaultTo(0)).addColumn("lastOperationTimestamp", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
5046
5948
  }
5047
5949
  //#endregion
5048
5950
  //#region src/storage/migrations/009_create_operation_index_tables.ts
5049
- var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$10 });
5050
- async function up$10(db) {
5951
+ var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$11 });
5952
+ async function up$11(db) {
5051
5953
  await db.schema.createTable("document_collections").addColumn("documentId", "text", (col) => col.notNull()).addColumn("collectionId", "text", (col) => col.notNull()).addColumn("joinedOrdinal", "bigint", (col) => col.notNull().defaultTo(0)).addColumn("leftOrdinal", "bigint").addPrimaryKeyConstraint("document_collections_pkey", ["documentId", "collectionId"]).execute();
5052
5954
  await db.schema.createIndex("idx_document_collections_collectionId").on("document_collections").column("collectionId").execute();
5053
5955
  await db.schema.createIndex("idx_doc_collections_collection_range").on("document_collections").columns(["collectionId", "joinedOrdinal"]).execute();
@@ -5061,8 +5963,8 @@ async function up$10(db) {
5061
5963
  }
5062
5964
  //#endregion
5063
5965
  //#region src/storage/migrations/010_create_sync_tables.ts
5064
- var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$9 });
5065
- async function up$9(db) {
5966
+ var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$10 });
5967
+ async function up$10(db) {
5066
5968
  await db.schema.createTable("sync_remotes").addColumn("name", "text", (col) => col.primaryKey()).addColumn("collection_id", "text", (col) => col.notNull()).addColumn("channel_type", "text", (col) => col.notNull()).addColumn("channel_id", "text", (col) => col.notNull().defaultTo("")).addColumn("remote_name", "text", (col) => col.notNull().defaultTo("")).addColumn("channel_parameters", "jsonb", (col) => col.notNull().defaultTo(sql`'{}'::jsonb`)).addColumn("filter_document_ids", "jsonb").addColumn("filter_scopes", "jsonb").addColumn("filter_branch", "text", (col) => col.notNull().defaultTo("main")).addColumn("push_state", "text", (col) => col.notNull().defaultTo("idle")).addColumn("push_last_success_utc_ms", "text").addColumn("push_last_failure_utc_ms", "text").addColumn("push_failure_count", "integer", (col) => col.notNull().defaultTo(0)).addColumn("pull_state", "text", (col) => col.notNull().defaultTo("idle")).addColumn("pull_last_success_utc_ms", "text").addColumn("pull_last_failure_utc_ms", "text").addColumn("pull_failure_count", "integer", (col) => col.notNull().defaultTo(0)).addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
5067
5969
  await db.schema.createIndex("idx_sync_remotes_collection").on("sync_remotes").column("collection_id").execute();
5068
5970
  await db.schema.createTable("sync_cursors").addColumn("remote_name", "text", (col) => col.primaryKey().references("sync_remotes.name").onDelete("cascade")).addColumn("cursor_ordinal", "bigint", (col) => col.notNull().defaultTo(0)).addColumn("last_synced_at_utc_ms", "text").addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
@@ -5070,8 +5972,8 @@ async function up$9(db) {
5070
5972
  }
5071
5973
  //#endregion
5072
5974
  //#region src/storage/migrations/011_add_cursor_type_column.ts
5073
- var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$8 });
5074
- async function up$8(db) {
5975
+ var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$9 });
5976
+ async function up$9(db) {
5075
5977
  await db.deleteFrom("sync_cursors").where("remote_name", "like", "outbox::%").execute();
5076
5978
  await db.deleteFrom("sync_remotes").where("name", "like", "outbox::%").execute();
5077
5979
  await db.schema.dropTable("sync_cursors").execute();
@@ -5080,64 +5982,64 @@ async function up$8(db) {
5080
5982
  }
5081
5983
  //#endregion
5082
5984
  //#region src/storage/migrations/012_add_source_remote_column.ts
5083
- var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$7 });
5084
- async function up$7(db) {
5985
+ var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$8 });
5986
+ async function up$8(db) {
5085
5987
  await db.schema.alterTable("operation_index_operations").addColumn("sourceRemote", "text", (col) => col.notNull().defaultTo("")).execute();
5086
5988
  }
5087
5989
  //#endregion
5088
5990
  //#region src/storage/migrations/013_create_sync_dead_letters_table.ts
5089
- var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$6 });
5090
- async function up$6(db) {
5991
+ var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$7 });
5992
+ async function up$7(db) {
5091
5993
  await db.schema.createTable("sync_dead_letters").addColumn("ordinal", "serial", (col) => col.primaryKey()).addColumn("id", "text", (col) => col.unique().notNull()).addColumn("job_id", "text", (col) => col.notNull()).addColumn("job_dependencies", "jsonb", (col) => col.notNull().defaultTo(sql`'[]'::jsonb`)).addColumn("remote_name", "text", (col) => col.notNull().references("sync_remotes.name").onDelete("cascade")).addColumn("document_id", "text", (col) => col.notNull()).addColumn("scopes", "jsonb", (col) => col.notNull().defaultTo(sql`'[]'::jsonb`)).addColumn("branch", "text", (col) => col.notNull()).addColumn("operations", "jsonb", (col) => col.notNull().defaultTo(sql`'[]'::jsonb`)).addColumn("error_source", "text", (col) => col.notNull()).addColumn("error_message", "text", (col) => col.notNull()).addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
5092
5994
  await db.schema.createIndex("idx_sync_dead_letters_remote").on("sync_dead_letters").column("remote_name").execute();
5093
5995
  }
5094
5996
  //#endregion
5095
5997
  //#region src/storage/migrations/014_create_processor_cursor_table.ts
5096
- var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$5 });
5097
- async function up$5(db) {
5998
+ var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$6 });
5999
+ async function up$6(db) {
5098
6000
  await db.schema.createTable("ProcessorCursor").addColumn("processorId", "text", (col) => col.primaryKey()).addColumn("factoryId", "text", (col) => col.notNull()).addColumn("driveId", "text", (col) => col.notNull()).addColumn("processorIndex", "integer", (col) => col.notNull()).addColumn("lastOrdinal", "integer", (col) => col.notNull().defaultTo(sql`0`)).addColumn("status", "text", (col) => col.notNull().defaultTo(sql`'active'`)).addColumn("lastError", "text").addColumn("lastErrorTimestamp", "timestamptz").addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
5099
6001
  }
5100
6002
  //#endregion
5101
6003
  //#region src/storage/migrations/015_add_operation_denied_reason.ts
5102
6004
  var _015_add_operation_denied_reason_exports = /* @__PURE__ */ __exportAll({
5103
- down: () => down$4,
5104
- up: () => up$4
6005
+ down: () => down$5,
6006
+ up: () => up$5
5105
6007
  });
5106
6008
  /**
5107
6009
  * Records why authorization refused an operation. Separate from `error` so a
5108
6010
  * denial is distinguishable from a reducer failure without matching on a
5109
6011
  * message. Null for every operation written before decisions were enforced.
5110
6012
  */
5111
- async function up$4(db) {
6013
+ async function up$5(db) {
5112
6014
  await db.schema.alterTable("Operation").addColumn("deniedReason", "text").execute();
5113
6015
  await db.schema.alterTable("operation_index_operations").addColumn("deniedReason", "text").execute();
5114
6016
  }
5115
- async function down$4(db) {
6017
+ async function down$5(db) {
5116
6018
  await db.schema.alterTable("operation_index_operations").dropColumn("deniedReason").execute();
5117
6019
  await db.schema.alterTable("Operation").dropColumn("deniedReason").execute();
5118
6020
  }
5119
6021
  //#endregion
5120
6022
  //#region src/storage/migrations/016_add_dead_letter_error_type.ts
5121
6023
  var _016_add_dead_letter_error_type_exports = /* @__PURE__ */ __exportAll({
5122
- down: () => down$3,
5123
- up: () => up$3
6024
+ down: () => down$4,
6025
+ up: () => up$4
5124
6026
  });
5125
6027
  /**
5126
6028
  * The classification a dead letter falls into, stored because it decides whether
5127
6029
  * the document stays quarantined and the in-memory error is gone after a restart.
5128
6030
  * Defaulted rather than nullable, so a pre-existing row rehydrates.
5129
6031
  */
5130
- async function up$3(db) {
6032
+ async function up$4(db) {
5131
6033
  await db.schema.alterTable("sync_dead_letters").addColumn("error_type", "text", (col) => col.notNull().defaultTo("UNCLASSIFIED")).execute();
5132
6034
  }
5133
- async function down$3(db) {
6035
+ async function down$4(db) {
5134
6036
  await db.schema.alterTable("sync_dead_letters").dropColumn("error_type").execute();
5135
6037
  }
5136
6038
  //#endregion
5137
6039
  //#region src/storage/migrations/017_create_group_references.ts
5138
6040
  var _017_create_group_references_exports = /* @__PURE__ */ __exportAll({
5139
- down: () => down$2,
5140
- up: () => up$2
6041
+ down: () => down$3,
6042
+ up: () => up$3
5141
6043
  });
5142
6044
  /**
5143
6045
  * One row per (document, group) reference ever discovered from an auth
@@ -5148,18 +6050,18 @@ var _017_create_group_references_exports = /* @__PURE__ */ __exportAll({
5148
6050
  * requires (sync), and by groupId for the documents a group change affects
5149
6051
  * (re-evaluation).
5150
6052
  */
5151
- async function up$2(db) {
6053
+ async function up$3(db) {
5152
6054
  await db.schema.createTable("group_references").addColumn("documentId", "text", (col) => col.notNull()).addColumn("groupId", "text", (col) => col.notNull()).addPrimaryKeyConstraint("group_references_pkey", ["documentId", "groupId"]).execute();
5153
6055
  await db.schema.createIndex("idx_group_references_groupId").on("group_references").column("groupId").execute();
5154
6056
  }
5155
- async function down$2(db) {
6057
+ async function down$3(db) {
5156
6058
  await db.schema.dropTable("group_references").execute();
5157
6059
  }
5158
6060
  //#endregion
5159
6061
  //#region src/storage/migrations/018_add_sync_remote_bound_address.ts
5160
6062
  var _018_add_sync_remote_bound_address_exports = /* @__PURE__ */ __exportAll({
5161
- down: () => down$1,
5162
- up: () => up$1
6063
+ down: () => down$2,
6064
+ up: () => up$2
5163
6065
  });
5164
6066
  /**
5165
6067
  * The address a sync channel is bound to, so a channel created by one subject
@@ -5170,17 +6072,17 @@ var _018_add_sync_remote_bound_address_exports = /* @__PURE__ */ __exportAll({
5170
6072
  * until its first authenticated poll adopts it. A default would claim them all
5171
6073
  * for one address.
5172
6074
  */
5173
- async function up$1(db) {
6075
+ async function up$2(db) {
5174
6076
  await db.schema.alterTable("sync_remotes").addColumn("bound_address", "text").execute();
5175
6077
  }
5176
- async function down$1(db) {
6078
+ async function down$2(db) {
5177
6079
  await db.schema.alterTable("sync_remotes").dropColumn("bound_address").execute();
5178
6080
  }
5179
6081
  //#endregion
5180
6082
  //#region src/storage/migrations/019_require_action_id.ts
5181
6083
  var _019_require_action_id_exports = /* @__PURE__ */ __exportAll({
5182
- down: () => down,
5183
- up: () => up
6084
+ down: () => down$1,
6085
+ up: () => up$1
5184
6086
  });
5185
6087
  /**
5186
6088
  * Makes an operation whose action carries no id physically unstorable.
@@ -5207,7 +6109,7 @@ var _019_require_action_id_exports = /* @__PURE__ */ __exportAll({
5207
6109
  * The empty string is rejected alongside null. It derives the same colliding
5208
6110
  * operation id as an absent id, so admitting it would leave the hole open.
5209
6111
  */
5210
- async function up(db) {
6112
+ async function up$1(db) {
5211
6113
  await db.updateTable("Operation").set({ action: sql`jsonb_set(action, '{id}', to_jsonb(gen_random_uuid()::text))` }).where(sql`jsonb_typeof(action) = 'object' and coalesce(action->>'id', '') = ''`).execute();
5212
6114
  await db.updateTable("operation_index_operations as oio").from("Operation as op").set({ action: sql`jsonb_set(oio.action, '{id}', to_jsonb(op.action->>'id'))` }).whereRef("oio.opId", "=", "op.opId").whereRef("oio.index", "=", "op.index").whereRef("oio.skip", "=", "op.skip").where(sql`jsonb_typeof(oio.action) = 'object' and coalesce(oio.action->>'id', '') = ''`).where(sql`coalesce(op.action->>'id', '') <> ''`).execute();
5213
6115
  await db.updateTable("operation_index_operations").set({ action: sql`jsonb_set(action, '{id}', to_jsonb(gen_random_uuid()::text))` }).where(sql`jsonb_typeof(action) = 'object' and coalesce(action->>'id', '') = ''`).execute();
@@ -5219,11 +6121,24 @@ async function up(db) {
5219
6121
  * their operations are now known by, and reverting them would reintroduce the
5220
6122
  * collision the migration removed.
5221
6123
  */
5222
- async function down(db) {
6124
+ async function down$1(db) {
5223
6125
  await db.schema.alterTable("operation_index_operations").dropConstraint("action_must_have_id").execute();
5224
6126
  await db.schema.alterTable("Operation").dropConstraint("action_must_have_id").execute();
5225
6127
  }
5226
6128
  //#endregion
6129
+ //#region src/storage/migrations/020_add_snapshot_operation_ordinal.ts
6130
+ var _020_add_snapshot_operation_ordinal_exports = /* @__PURE__ */ __exportAll({
6131
+ down: () => down,
6132
+ up: () => up
6133
+ });
6134
+ /** Orders header row writes across scopes; `lastOperationIndex` is per scope. */
6135
+ async function up(db) {
6136
+ await db.schema.alterTable("DocumentSnapshot").addColumn("lastOperationOrdinal", "integer", (col) => col.notNull().defaultTo(0)).execute();
6137
+ }
6138
+ async function down(db) {
6139
+ await db.schema.alterTable("DocumentSnapshot").dropColumn("lastOperationOrdinal").execute();
6140
+ }
6141
+ //#endregion
5227
6142
  //#region src/storage/migrations/migrator.ts
5228
6143
  const REACTOR_SCHEMA = "reactor";
5229
6144
  const migrations = {
@@ -5245,7 +6160,8 @@ const migrations = {
5245
6160
  "016_add_dead_letter_error_type": _016_add_dead_letter_error_type_exports,
5246
6161
  "017_create_group_references": _017_create_group_references_exports,
5247
6162
  "018_add_sync_remote_bound_address": _018_add_sync_remote_bound_address_exports,
5248
- "019_require_action_id": _019_require_action_id_exports
6163
+ "019_require_action_id": _019_require_action_id_exports,
6164
+ "020_add_snapshot_operation_ordinal": _020_add_snapshot_operation_ordinal_exports
5249
6165
  };
5250
6166
  var ProgrammaticMigrationProvider = class {
5251
6167
  getMigrations() {
@@ -5303,8 +6219,148 @@ async function getMigrationStatus(db, schema = REACTOR_SCHEMA) {
5303
6219
  }
5304
6220
  //#endregion
5305
6221
  //#region src/core/drive-container-types.ts
5306
- const DEFAULT_DRIVE_CONTAINER_TYPES = new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
6222
+ const DEFAULT_DRIVE_CONTAINER_TYPES = /* @__PURE__ */ new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
6223
+ //#endregion
6224
+ //#region src/executor/worker/error-info.ts
6225
+ const MAX_STACK_LENGTH = 8192;
6226
+ const MAX_CAUSE_DEPTH = 8;
6227
+ function toErrorInfo(err, depth = 0) {
6228
+ if (depth > MAX_CAUSE_DEPTH) return {
6229
+ name: "Error",
6230
+ message: "cause chain truncated"
6231
+ };
6232
+ if (err instanceof Error) {
6233
+ const info = {
6234
+ name: err.name || "Error",
6235
+ message: err.message
6236
+ };
6237
+ if (typeof err.stack === "string") info.stack = err.stack.length > MAX_STACK_LENGTH ? err.stack.slice(0, MAX_STACK_LENGTH) : err.stack;
6238
+ if (err.cause !== void 0) info.cause = toErrorInfo(err.cause, depth + 1);
6239
+ return info;
6240
+ }
6241
+ if (typeof err === "string") return {
6242
+ name: "Error",
6243
+ message: err
6244
+ };
6245
+ if (err && typeof err === "object") {
6246
+ const record = err;
6247
+ return {
6248
+ name: typeof record.name === "string" ? record.name : "Error",
6249
+ message: typeof record.message === "string" ? record.message : safeStringify(err)
6250
+ };
6251
+ }
6252
+ return {
6253
+ name: "Error",
6254
+ message: safeStringify(err)
6255
+ };
6256
+ }
6257
+ function safeStringify(value) {
6258
+ if (value === null || value === void 0) return String(value);
6259
+ if (typeof value === "string") return value;
6260
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
6261
+ try {
6262
+ return JSON.stringify(value);
6263
+ } catch {
6264
+ return "[unserializable]";
6265
+ }
6266
+ }
6267
+ function fromErrorInfo(info) {
6268
+ const err = new Error(info.message);
6269
+ Object.defineProperty(err, "name", {
6270
+ value: info.name,
6271
+ configurable: true,
6272
+ writable: true
6273
+ });
6274
+ if (info.stack !== void 0) err.stack = info.stack;
6275
+ if (info.cause !== void 0) Object.defineProperty(err, "cause", {
6276
+ value: fromErrorInfo(info.cause),
6277
+ configurable: true,
6278
+ writable: true
6279
+ });
6280
+ return err;
6281
+ }
6282
+ //#endregion
6283
+ //#region src/executor/worker/sanitize.ts
6284
+ const MAX_DEPTH = 8;
6285
+ function errorToInfo(err) {
6286
+ return toErrorInfo(err);
6287
+ }
6288
+ function sanitizeArg(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
6289
+ if (depth > MAX_DEPTH) return "[Truncated]";
6290
+ if (value === null || value === void 0) return null;
6291
+ if (typeof value === "boolean") return value;
6292
+ if (typeof value === "number") return value;
6293
+ if (typeof value === "string") return value;
6294
+ if (typeof value === "bigint") return value.toString();
6295
+ if (typeof value === "function" || typeof value === "symbol") return null;
6296
+ if (value instanceof Error) return toErrorInfo(value);
6297
+ if (value instanceof Date) return value.toISOString();
6298
+ if (value instanceof Map) {
6299
+ const obj = {};
6300
+ for (const [k, v] of value.entries()) obj[String(k)] = sanitizeArg(v, depth + 1, seen);
6301
+ return obj;
6302
+ }
6303
+ if (value instanceof Set) {
6304
+ const arr = [];
6305
+ for (const v of value.values()) arr.push(sanitizeArg(v, depth + 1, seen));
6306
+ return arr;
6307
+ }
6308
+ if (Array.isArray(value)) {
6309
+ if (seen.has(value)) return "[Circular]";
6310
+ seen.add(value);
6311
+ const result = value.map((v) => sanitizeArg(v, depth + 1, seen));
6312
+ seen.delete(value);
6313
+ return result;
6314
+ }
6315
+ if (typeof value === "object") {
6316
+ const proto = Object.getPrototypeOf(value);
6317
+ if (proto !== Object.prototype && proto !== null) return { __nonClonable: value.constructor?.name || "Unknown" };
6318
+ if (seen.has(value)) return "[Circular]";
6319
+ seen.add(value);
6320
+ const result = {};
6321
+ for (const [k, v] of Object.entries(value)) result[k] = sanitizeArg(v, depth + 1, seen);
6322
+ seen.delete(value);
6323
+ return result;
6324
+ }
6325
+ return null;
6326
+ }
6327
+ //#endregion
6328
+ //#region src/executor/worker/forwarding-logger.ts
6329
+ function createForwardingLogger(post, level = "info", tags = []) {
6330
+ function safePost(msg) {
6331
+ try {
6332
+ post(msg);
6333
+ } catch {
6334
+ try {
6335
+ process.stderr.write(`[forwarding-logger] postMessage failed: ${msg.level} ${msg.message}\n`);
6336
+ } catch {}
6337
+ }
6338
+ }
6339
+ function emit(wireLevel, message, args) {
6340
+ const sanitizedArgs = args.map((a) => sanitizeArg(a));
6341
+ safePost({
6342
+ type: "log",
6343
+ level: wireLevel,
6344
+ message,
6345
+ args: tags.length > 0 ? [sanitizeArg({ tags }), ...sanitizedArgs] : sanitizedArgs,
6346
+ timestamp: Date.now()
6347
+ });
6348
+ }
6349
+ return {
6350
+ level,
6351
+ verbose: (message, ...args) => emit("debug", message, args),
6352
+ debug: (message, ...args) => emit("debug", message, args),
6353
+ info: (message, ...args) => emit("info", message, args),
6354
+ warn: (message, ...args) => emit("warn", message, args),
6355
+ error: (message, ...args) => emit("error", message, args),
6356
+ errorHandler: ((...data) => {
6357
+ const first = data[0];
6358
+ emit("error", typeof first === "string" ? first : "", data.slice(1));
6359
+ }),
6360
+ child: (childTags) => createForwardingLogger(post, level, [...tags, ...childTags])
6361
+ };
6362
+ }
5307
6363
  //#endregion
5308
- export { selectDecisionModel as A, RevisionMismatchError as B, InvalidModuleError as C, createEmptyConsistencyToken as D, createConsistencyToken as E, AppendConditionFailedError as F, DocumentNotFoundError as G, AuthTimestampNotMonotonicError as H, DocumentAlreadyExistsError as I, UpgradePreconditionFailedError as J, ExcessiveReshuffleError as K, DocumentExistence as L, authDecisionModel as M, buildDecisionModel as N, targetDocumentId as O, APPEND_CONDITION_FAILED_PREFIX as P, __exportAll as Q, DuplicateOperationError as R, DuplicateModuleError as S, GATED_DOCUMENT_ACTIONS as T, AuthorizationDeniedError as U, AuthEnforcementDisabledError as V, DocumentDeletedError as W, parsePagingOptions as X, matchesScope as Y, throwIfAborted as Z, DocumentMetaCache as _, createForwardingPoolInstrumentation as a, JobExecutorEventTypes as b, KyselyKeyframeStore as c, DriveCollectionId as d, KyselyExecutionScope as f, KyselyOperationIndex as g, KyselyWriteCache as h, runMigrations as i, documentDecisionModel as j, decideAtHead as k, DocumentModelRegistry as l, EventBus as m, REACTOR_SCHEMA as n, instrumentPgPool as o, resolveFeatureFlags as p, InvalidOperationTimestampError as q, getMigrationStatus as r, KyselyOperationStore as s, DEFAULT_DRIVE_CONTAINER_TYPES as t, SimpleJobExecutor as u, CollectionMembershipCache as v, ModuleNotFoundError as w, DuplicateManifestError as x, DEFAULT_DEFERRED_JOB_TTL_MS as y, OptimisticLockError as z };
6364
+ export { DocumentNotFoundError as $, ModuleNotFoundError as A, authDecisionModel as B, DocumentMetaCache as C, DuplicateManifestError as D, JobExecutorEventTypes as E, summarizeSubmittedActions as F, DocumentExistence as G, APPEND_CONDITION_FAILED_PREFIX as H, targetDocumentId as I, RevisionMismatchError as J, DuplicateOperationError as K, decideAtHead as L, createConsistencyToken as M, createEmptyConsistencyToken as N, DuplicateModuleError as O, submittedActionIds as P, DocumentDeletedError as Q, selectDecisionModel as R, KyselyOperationIndex as S, DEFAULT_DEFERRED_JOB_TTL_MS as T, AppendConditionFailedError as U, buildDecisionModel as V, DocumentAlreadyExistsError as W, AuthTimestampNotMonotonicError as X, AuthEnforcementDisabledError as Y, AuthorizationDeniedError as Z, DriveCollectionId as _, DEFAULT_DRIVE_CONTAINER_TYPES as a, EventBusAggregateError as at, EventBus as b, runMigrations as c, parsePagingOptions as ct, KyselyOperationStore as d, __exportAll as dt, ExcessiveReshuffleError as et, KyselyKeyframeStore as f, PassthroughSigner as g, verifyActionSignature as h, fromErrorInfo as i, UpgradePreconditionFailedError as it, GATED_DOCUMENT_ACTIONS as j, InvalidModuleError as k, createForwardingPoolInstrumentation as l, throwIfAborted as lt, SimpleJobExecutor as m, errorToInfo as n, InvalidSignatureError as nt, REACTOR_SCHEMA as o, ReactorEventTypes as ot, DocumentModelRegistry as p, OptimisticLockError as q, sanitizeArg as r, RelationshipNotFoundError as rt, getMigrationStatus as s, matchesScope as st, createForwardingLogger as t, InvalidOperationTimestampError as tt, instrumentPgPool as u, yieldToMain as ut, KyselyExecutionScope as v, CollectionMembershipCache as w, KyselyWriteCache as x, resolveFeatureFlags as y, documentDecisionModel as z };
5309
6365
 
5310
- //# sourceMappingURL=drive-container-types-yZrksiJR.js.map
6366
+ //# sourceMappingURL=forwarding-logger-CnuVDVze.js.map