@powerhousedao/reactor 6.2.3-dev.23 → 6.2.3-dev.25
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.
- package/dist/{build-worker-executor-DUjyF4NM.js → build-worker-executor-DPGjh9Uc.js} +24 -8
- package/dist/build-worker-executor-DPGjh9Uc.js.map +1 -0
- package/dist/{document-indexer-CQzTEE1o.js → document-indexer-zbdzqZ3K.js} +18 -9
- package/dist/{document-indexer-CQzTEE1o.js.map → document-indexer-zbdzqZ3K.js.map} +1 -1
- package/dist/entry.d.ts +2 -1
- package/dist/entry.js +10 -6
- package/dist/entry.js.map +1 -1
- package/dist/{errors-D3S6Eysd.js → errors-BM_ULVWd.js} +9 -1
- package/dist/{errors-D3S6Eysd.js.map → errors-BM_ULVWd.js.map} +1 -1
- package/dist/{drive-container-types-CS5IxDiA.js → forwarding-logger-CSsCJ38f.js} +1081 -174
- package/dist/forwarding-logger-CSsCJ38f.js.map +1 -0
- package/dist/index.d.ts +2248 -1978
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +438 -258
- package/dist/index.js.map +1 -1
- package/dist/projection-entry.d.ts +2 -1
- package/dist/projection-entry.js +7 -13
- package/dist/projection-entry.js.map +1 -1
- package/dist/{projection-shard-manager-CUo4bF4O.js → projection-shard-manager-B-j8VYzA.js} +2 -3
- package/dist/{projection-shard-manager-CUo4bF4O.js.map → projection-shard-manager-B-j8VYzA.js.map} +1 -1
- package/dist/{projection-worker-wI4PwcV2.js → projection-worker-D8uNFdwX.js} +1 -1
- package/dist/{projection-worker-wI4PwcV2.js.map → projection-worker-D8uNFdwX.js.map} +1 -1
- package/dist/{transport-CuogVKN_.js → transport-B3G50KVd.js} +1 -1
- package/dist/{transport-CuogVKN_.js.map → transport-B3G50KVd.js.map} +1 -1
- package/dist/{transport-ByGviWdZ.js → transport-CZDHzfvy.js} +1 -1
- package/dist/{transport-ByGviWdZ.js.map → transport-CZDHzfvy.js.map} +1 -1
- package/dist/{worker-handle-CrERzl8s.js → worker-handle-CwWNTpUg.js} +22 -16
- package/dist/worker-handle-CwWNTpUg.js.map +1 -0
- package/package.json +7 -8
- package/dist/build-worker-executor-DUjyF4NM.js.map +0 -1
- package/dist/drive-container-types-CS5IxDiA.js.map +0 -1
- package/dist/error-info-Cpu4OY3o.js +0 -62
- package/dist/error-info-Cpu4OY3o.js.map +0 -1
- package/dist/forwarding-logger-BBkMSxuJ.js +0 -85
- package/dist/forwarding-logger-BBkMSxuJ.js.map +0 -1
- package/dist/types-DMKLa0Ok.js +0 -33
- package/dist/types-DMKLa0Ok.js.map +0 -1
- package/dist/worker-g03lNM42.js +0 -22
- package/dist/worker-g03lNM42.js.map +0 -1
- package/dist/worker-handle-CrERzl8s.js.map +0 -1
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
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, operationOutcome, 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
|
|
@@ -326,6 +359,7 @@ const APPEND_CONDITION_FAILED_PREFIX = "Append condition failed: ";
|
|
|
326
359
|
* conflict, not a fault: the caller retries against the new stream heads.
|
|
327
360
|
*/
|
|
328
361
|
var AppendConditionFailedError = class extends Error {
|
|
362
|
+
condition;
|
|
329
363
|
constructor(condition) {
|
|
330
364
|
const streams = condition.streams.map((s) => `${s.documentId}:${s.scope}:${s.branch}@${s.revision}`).join(", ");
|
|
331
365
|
super(`${APPEND_CONDITION_FAILED_PREFIX}a read-set stream advanced [${streams}]`);
|
|
@@ -714,34 +748,13 @@ function selectDecisionModel(flags, registry) {
|
|
|
714
748
|
//#endregion
|
|
715
749
|
//#region src/executor/util.ts
|
|
716
750
|
/** Actions the reactor reduces itself, onto the document scope. */
|
|
717
|
-
const DOCUMENT_SCOPE_ACTIONS =
|
|
718
|
-
"CREATE_DOCUMENT",
|
|
719
|
-
"DELETE_DOCUMENT",
|
|
720
|
-
"UPGRADE_DOCUMENT",
|
|
721
|
-
"ADD_RELATIONSHIP",
|
|
722
|
-
"REMOVE_RELATIONSHIP",
|
|
723
|
-
"UPDATE_RELATIONSHIP"
|
|
724
|
-
]);
|
|
751
|
+
const DOCUMENT_SCOPE_ACTIONS = DOCUMENT_SCOPE_ACTION_TYPES;
|
|
725
752
|
/**
|
|
726
753
|
* `CREATE_DOCUMENT` is exempt by necessity: it runs before the document exists,
|
|
727
754
|
* so building a decision model would throw and defer the job forever.
|
|
728
755
|
*/
|
|
729
756
|
const GATED_DOCUMENT_ACTIONS = new Set([...DOCUMENT_SCOPE_ACTIONS].filter((type) => type !== "CREATE_DOCUMENT"));
|
|
730
757
|
/**
|
|
731
|
-
* The document a document-scope action writes to, which is not always the job's
|
|
732
|
-
* own document: delete and upgrade name it in `input.documentId`, and the
|
|
733
|
-
* relationship actions in `input.sourceId`. `execute` only checks that a batch
|
|
734
|
-
* shares one scope, so a caller can submit an action whose target is a document
|
|
735
|
-
* other than the one the job is keyed by. The policy gate has to follow the
|
|
736
|
-
* action rather than the job, or it decides against a policy the caller may
|
|
737
|
-
* control instead of the one guarding the write.
|
|
738
|
-
*/
|
|
739
|
-
function targetDocumentId(action, fallback) {
|
|
740
|
-
const input = action.input;
|
|
741
|
-
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;
|
|
742
|
-
return typeof input?.documentId === "string" && input.documentId.length > 0 ? input.documentId : fallback;
|
|
743
|
-
}
|
|
744
|
-
/**
|
|
745
758
|
* Creates a PHDocument from a CREATE_DOCUMENT action input.
|
|
746
759
|
* Reconstructs the document header and initializes the base state.
|
|
747
760
|
*
|
|
@@ -1051,6 +1064,7 @@ const JobExecutorEventTypes = {
|
|
|
1051
1064
|
//#endregion
|
|
1052
1065
|
//#region src/cache/collection-membership-cache.ts
|
|
1053
1066
|
var CollectionMembershipCache = class CollectionMembershipCache {
|
|
1067
|
+
operationIndex;
|
|
1054
1068
|
cache = /* @__PURE__ */ new Map();
|
|
1055
1069
|
constructor(operationIndex) {
|
|
1056
1070
|
this.operationIndex = operationIndex;
|
|
@@ -1264,6 +1278,7 @@ var DocumentMetaCache = class DocumentMetaCache {
|
|
|
1264
1278
|
return {
|
|
1265
1279
|
state: document.state.document,
|
|
1266
1280
|
documentType,
|
|
1281
|
+
protocolVersions: createAction.input.protocolVersions,
|
|
1267
1282
|
documentScopeRevision: documentScopeRevision + 1
|
|
1268
1283
|
};
|
|
1269
1284
|
}
|
|
@@ -1345,6 +1360,7 @@ var KyselyOperationIndexTxn = class {
|
|
|
1345
1360
|
}
|
|
1346
1361
|
};
|
|
1347
1362
|
var KyselyOperationIndex = class KyselyOperationIndex {
|
|
1363
|
+
db;
|
|
1348
1364
|
trx;
|
|
1349
1365
|
constructor(db) {
|
|
1350
1366
|
this.db = db;
|
|
@@ -1459,6 +1475,12 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1459
1475
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
1460
1476
|
return (await this.queryExecutor.selectFrom("group_references").select("documentId").where("groupId", "=", groupId).orderBy("documentId").execute()).map((row) => row.documentId);
|
|
1461
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
|
+
}
|
|
1462
1484
|
async find(collectionId, cursor, view, paging, signal) {
|
|
1463
1485
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
1464
1486
|
const outerCursor = cursor ?? -1;
|
|
@@ -1678,21 +1700,6 @@ var RingBuffer = class {
|
|
|
1678
1700
|
}
|
|
1679
1701
|
};
|
|
1680
1702
|
//#endregion
|
|
1681
|
-
//#region src/cache/write-cache-types.ts
|
|
1682
|
-
/**
|
|
1683
|
-
* Where a snapshot sits in its stream.
|
|
1684
|
-
*
|
|
1685
|
-
* - `Head`: the newest revision of the stream when it was stored. Only these
|
|
1686
|
-
* can answer a read that asks for the head.
|
|
1687
|
-
* - `Historical`: state at an earlier revision. Usable as a starting point to
|
|
1688
|
-
* replay forward from, and as an answer to a read for that same revision.
|
|
1689
|
-
*/
|
|
1690
|
-
let SnapshotPosition = /* @__PURE__ */ function(SnapshotPosition) {
|
|
1691
|
-
SnapshotPosition["Head"] = "head";
|
|
1692
|
-
SnapshotPosition["Historical"] = "historical";
|
|
1693
|
-
return SnapshotPosition;
|
|
1694
|
-
}({});
|
|
1695
|
-
//#endregion
|
|
1696
1703
|
//#region src/cache/kysely-write-cache.ts
|
|
1697
1704
|
/**
|
|
1698
1705
|
* The last operation index a keyframe's document reflects for the scope. A
|
|
@@ -1831,13 +1838,13 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1831
1838
|
const snapshots = stream.ringBuffer.getAll();
|
|
1832
1839
|
if (targetRevision === void 0) {
|
|
1833
1840
|
const newest = highestRevision(snapshots);
|
|
1834
|
-
if (newest?.position ===
|
|
1841
|
+
if (newest?.position === "head") {
|
|
1835
1842
|
this.lruTracker.touch(streamKey);
|
|
1836
1843
|
return copyDocument(newest.document);
|
|
1837
1844
|
}
|
|
1838
1845
|
if (newest) {
|
|
1839
1846
|
const document = await this.warmMissRebuild(newest.document, newest.revision, documentId, scope, branch, void 0, signal);
|
|
1840
|
-
this.store(documentId, scope, branch, (document.header.revision[scope] ?? 0) - 1, document,
|
|
1847
|
+
this.store(documentId, scope, branch, (document.header.revision[scope] ?? 0) - 1, document, "head");
|
|
1841
1848
|
this.lruTracker.touch(streamKey);
|
|
1842
1849
|
return document;
|
|
1843
1850
|
}
|
|
@@ -1850,7 +1857,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1850
1857
|
const newestOlder = this.findNearestOlderSnapshot(snapshots, targetRevision);
|
|
1851
1858
|
if (newestOlder) {
|
|
1852
1859
|
const document = await this.warmMissRebuild(newestOlder.document, newestOlder.revision, documentId, scope, branch, targetRevision, signal);
|
|
1853
|
-
this.store(documentId, scope, branch, targetRevision, document,
|
|
1860
|
+
this.store(documentId, scope, branch, targetRevision, document, "historical");
|
|
1854
1861
|
this.lruTracker.touch(streamKey);
|
|
1855
1862
|
return document;
|
|
1856
1863
|
}
|
|
@@ -1858,7 +1865,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1858
1865
|
}
|
|
1859
1866
|
const document = await this.coldMissRebuild(documentId, scope, branch, targetRevision, signal);
|
|
1860
1867
|
const revision = targetRevision ?? (document.header.revision[scope] ?? 0) - 1;
|
|
1861
|
-
this.store(documentId, scope, branch, revision, document, targetRevision === void 0 ?
|
|
1868
|
+
this.store(documentId, scope, branch, revision, document, targetRevision === void 0 ? "head" : "historical");
|
|
1862
1869
|
return document;
|
|
1863
1870
|
}
|
|
1864
1871
|
/**
|
|
@@ -1893,7 +1900,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1893
1900
|
if (run.length === 0) return;
|
|
1894
1901
|
for (const entry of run.slice(0, -1)) this.persistKeyframe(documentId, scope, branch, entry.revision, entry.document);
|
|
1895
1902
|
const head = run[run.length - 1];
|
|
1896
|
-
this.store(documentId, scope, branch, head.revision, head.document,
|
|
1903
|
+
this.store(documentId, scope, branch, head.revision, head.document, "head");
|
|
1897
1904
|
}
|
|
1898
1905
|
store(documentId, scope, branch, revision, document, position) {
|
|
1899
1906
|
const streamKey = this.makeStreamKey(documentId, scope, branch);
|
|
@@ -2099,10 +2106,10 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2099
2106
|
for (const pending of pendingUpgrades) pending.subsequentDeletes.push(operation.action);
|
|
2100
2107
|
} else {
|
|
2101
2108
|
const protocolVersion = baseReducerVersion(document.header);
|
|
2102
|
-
document = docModule.reducer(document, operation.action, void 0, {
|
|
2109
|
+
document = withProtocolVersions(docModule.reducer(document, operation.action, void 0, {
|
|
2103
2110
|
skip: operation.skip,
|
|
2104
2111
|
protocolVersion
|
|
2105
|
-
});
|
|
2112
|
+
}), document.header.protocolVersions);
|
|
2106
2113
|
}
|
|
2107
2114
|
}
|
|
2108
2115
|
}
|
|
@@ -2151,12 +2158,12 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2151
2158
|
if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
|
|
2152
2159
|
else {
|
|
2153
2160
|
const protocolVersion = baseReducerVersion(document.header);
|
|
2154
|
-
document = getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
|
|
2161
|
+
document = withProtocolVersions(getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
|
|
2155
2162
|
skip: operation.skip,
|
|
2156
2163
|
protocolVersion,
|
|
2157
2164
|
replayOptions: { operation },
|
|
2158
2165
|
skipIndexValidation: true
|
|
2159
|
-
});
|
|
2166
|
+
}), document.header.protocolVersions);
|
|
2160
2167
|
}
|
|
2161
2168
|
}
|
|
2162
2169
|
const reachedTarget = targetRevision !== void 0 && result.results.some((op) => op.index >= targetRevision);
|
|
@@ -2280,12 +2287,12 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2280
2287
|
if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
|
|
2281
2288
|
else {
|
|
2282
2289
|
const protocolVersion = baseReducerVersion(document.header);
|
|
2283
|
-
document = module.reducer(document, operation.action, void 0, {
|
|
2290
|
+
document = withProtocolVersions(module.reducer(document, operation.action, void 0, {
|
|
2284
2291
|
skip: operation.skip,
|
|
2285
2292
|
protocolVersion,
|
|
2286
2293
|
replayOptions: { operation },
|
|
2287
2294
|
skipIndexValidation: true
|
|
2288
|
-
});
|
|
2295
|
+
}), document.header.protocolVersions);
|
|
2289
2296
|
}
|
|
2290
2297
|
if (targetRevision !== void 0 && operation.index === targetRevision) break;
|
|
2291
2298
|
}
|
|
@@ -2412,6 +2419,11 @@ function validateFeatureFlags(flags, prerequisites) {
|
|
|
2412
2419
|
//#endregion
|
|
2413
2420
|
//#region src/executor/execution-scope.ts
|
|
2414
2421
|
var DefaultExecutionScope = class {
|
|
2422
|
+
operationStore;
|
|
2423
|
+
operationIndex;
|
|
2424
|
+
writeCache;
|
|
2425
|
+
documentMetaCache;
|
|
2426
|
+
collectionMembershipCache;
|
|
2415
2427
|
constructor(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache) {
|
|
2416
2428
|
this.operationStore = operationStore;
|
|
2417
2429
|
this.operationIndex = operationIndex;
|
|
@@ -2431,6 +2443,13 @@ var DefaultExecutionScope = class {
|
|
|
2431
2443
|
}
|
|
2432
2444
|
};
|
|
2433
2445
|
var KyselyExecutionScope = class {
|
|
2446
|
+
db;
|
|
2447
|
+
operationStore;
|
|
2448
|
+
operationIndex;
|
|
2449
|
+
keyframeStore;
|
|
2450
|
+
writeCache;
|
|
2451
|
+
documentMetaCache;
|
|
2452
|
+
collectionMembershipCache;
|
|
2434
2453
|
constructor(db, operationStore, operationIndex, keyframeStore, writeCache, documentMetaCache, collectionMembershipCache) {
|
|
2435
2454
|
this.db = db;
|
|
2436
2455
|
this.operationStore = operationStore;
|
|
@@ -2458,7 +2477,7 @@ var KyselyExecutionScope = class {
|
|
|
2458
2477
|
};
|
|
2459
2478
|
//#endregion
|
|
2460
2479
|
//#region src/utils/reshuffle.ts
|
|
2461
|
-
const STRICT_ORDER_ACTION_TYPES = new Set([
|
|
2480
|
+
const STRICT_ORDER_ACTION_TYPES = /* @__PURE__ */ new Set([
|
|
2462
2481
|
"CREATE_DOCUMENT",
|
|
2463
2482
|
"DELETE_DOCUMENT",
|
|
2464
2483
|
"UPGRADE_DOCUMENT",
|
|
@@ -2789,6 +2808,8 @@ const DRIVE_COLLECTION_PREFIX = "drive.";
|
|
|
2789
2808
|
* rows and persisted remotes remain valid without migration.
|
|
2790
2809
|
*/
|
|
2791
2810
|
var DriveCollectionId = class DriveCollectionId {
|
|
2811
|
+
driveId;
|
|
2812
|
+
branch;
|
|
2792
2813
|
constructor(driveId, branch) {
|
|
2793
2814
|
this.driveId = driveId;
|
|
2794
2815
|
this.branch = branch;
|
|
@@ -2821,6 +2842,11 @@ var DriveCollectionId = class DriveCollectionId {
|
|
|
2821
2842
|
//#endregion
|
|
2822
2843
|
//#region src/executor/document-action-handler.ts
|
|
2823
2844
|
var DocumentActionHandler = class {
|
|
2845
|
+
registry;
|
|
2846
|
+
logger;
|
|
2847
|
+
driveContainerTypes;
|
|
2848
|
+
featureFlags;
|
|
2849
|
+
decisionModel;
|
|
2824
2850
|
constructor(registry, logger, driveContainerTypes, featureFlags, decisionModel) {
|
|
2825
2851
|
this.registry = registry;
|
|
2826
2852
|
this.logger = logger;
|
|
@@ -2913,7 +2939,7 @@ var DocumentActionHandler = class {
|
|
|
2913
2939
|
[job.scope]: [...standing.operations[job.scope] ?? [], operation]
|
|
2914
2940
|
};
|
|
2915
2941
|
executing.touchedStreams.add(job.documentId, job.scope, job.branch);
|
|
2916
|
-
stores.writeCache.putState(job.documentId, job.scope, job.branch, operation.index, standing,
|
|
2942
|
+
stores.writeCache.putState(job.documentId, job.scope, job.branch, operation.index, standing, "head");
|
|
2917
2943
|
indexTxn.write([{
|
|
2918
2944
|
...operation,
|
|
2919
2945
|
documentId: job.documentId,
|
|
@@ -2925,6 +2951,7 @@ var DocumentActionHandler = class {
|
|
|
2925
2951
|
stores.documentMetaCache.putDocumentMeta(job.documentId, job.branch, {
|
|
2926
2952
|
state: standing.state.document,
|
|
2927
2953
|
documentType: standing.header.documentType,
|
|
2954
|
+
protocolVersions: standing.header.protocolVersions,
|
|
2928
2955
|
documentScopeRevision: operation.index + 1
|
|
2929
2956
|
});
|
|
2930
2957
|
return buildSuccessResult(job, operation, job.documentId, standing.header.documentType, JSON.stringify({
|
|
@@ -2948,8 +2975,8 @@ var DocumentActionHandler = class {
|
|
|
2948
2975
|
branch: job.branch
|
|
2949
2976
|
});
|
|
2950
2977
|
const resultingStateObj = {
|
|
2951
|
-
|
|
2952
|
-
|
|
2978
|
+
...document.state,
|
|
2979
|
+
header: document.header
|
|
2953
2980
|
};
|
|
2954
2981
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
2955
2982
|
const writeResult = await this.writeOperationToStore({
|
|
@@ -2966,7 +2993,7 @@ var DocumentActionHandler = class {
|
|
|
2966
2993
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
2967
2994
|
};
|
|
2968
2995
|
executing.touchedStreams.add(document.header.id, job.scope, job.branch);
|
|
2969
|
-
stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document,
|
|
2996
|
+
stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document, "head");
|
|
2970
2997
|
indexTxn.write([{
|
|
2971
2998
|
...operation,
|
|
2972
2999
|
documentId: document.header.id,
|
|
@@ -2983,6 +3010,7 @@ var DocumentActionHandler = class {
|
|
|
2983
3010
|
stores.documentMetaCache.putDocumentMeta(document.header.id, job.branch, {
|
|
2984
3011
|
state: document.state.document,
|
|
2985
3012
|
documentType: document.header.documentType,
|
|
3013
|
+
protocolVersions: document.header.protocolVersions,
|
|
2986
3014
|
documentScopeRevision: 1
|
|
2987
3015
|
});
|
|
2988
3016
|
return buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
|
|
@@ -3026,7 +3054,7 @@ var DocumentActionHandler = class {
|
|
|
3026
3054
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
3027
3055
|
};
|
|
3028
3056
|
executing.touchedStreams.add(documentId, job.scope, job.branch);
|
|
3029
|
-
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document,
|
|
3057
|
+
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, "head");
|
|
3030
3058
|
indexTxn.write([{
|
|
3031
3059
|
...operation,
|
|
3032
3060
|
documentId,
|
|
@@ -3038,6 +3066,7 @@ var DocumentActionHandler = class {
|
|
|
3038
3066
|
stores.documentMetaCache.putDocumentMeta(documentId, job.branch, {
|
|
3039
3067
|
state: document.state.document,
|
|
3040
3068
|
documentType: document.header.documentType,
|
|
3069
|
+
protocolVersions: document.header.protocolVersions,
|
|
3041
3070
|
documentScopeRevision: operation.index + 1
|
|
3042
3071
|
});
|
|
3043
3072
|
return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
@@ -3076,7 +3105,7 @@ var DocumentActionHandler = class {
|
|
|
3076
3105
|
} catch (error) {
|
|
3077
3106
|
return buildErrorResult(job, /* @__PURE__ */ new Error(`Failed to fetch revisions for upgrade: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
3078
3107
|
}
|
|
3079
|
-
const revisionScopes = new Set([...Object.keys(input.revision), ...Object.keys(actualRevisions)]);
|
|
3108
|
+
const revisionScopes = /* @__PURE__ */ new Set([...Object.keys(input.revision), ...Object.keys(actualRevisions)]);
|
|
3080
3109
|
for (const revisionScope of revisionScopes) {
|
|
3081
3110
|
const snapshot = input.revision[revisionScope] ?? 0;
|
|
3082
3111
|
const actual = actualRevisions[revisionScope] ?? 0;
|
|
@@ -3118,8 +3147,8 @@ var DocumentActionHandler = class {
|
|
|
3118
3147
|
branch: job.branch
|
|
3119
3148
|
});
|
|
3120
3149
|
const resultingStateObj = {
|
|
3121
|
-
|
|
3122
|
-
|
|
3150
|
+
...document.state,
|
|
3151
|
+
header: document.header
|
|
3123
3152
|
};
|
|
3124
3153
|
if (fromVersion > 0) resultingStateObj.__migrated = true;
|
|
3125
3154
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
@@ -3137,7 +3166,7 @@ var DocumentActionHandler = class {
|
|
|
3137
3166
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
3138
3167
|
};
|
|
3139
3168
|
executing.touchedStreams.add(documentId, job.scope, job.branch);
|
|
3140
|
-
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document,
|
|
3169
|
+
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, "head");
|
|
3141
3170
|
for (const scope of otherScopes) executing.postCommitInvalidations.push({
|
|
3142
3171
|
documentId,
|
|
3143
3172
|
scope,
|
|
@@ -3154,6 +3183,7 @@ var DocumentActionHandler = class {
|
|
|
3154
3183
|
stores.documentMetaCache.putDocumentMeta(documentId, job.branch, {
|
|
3155
3184
|
state: document.state.document,
|
|
3156
3185
|
documentType: document.header.documentType,
|
|
3186
|
+
protocolVersions: document.header.protocolVersions,
|
|
3157
3187
|
documentScopeRevision: operation.index + 1
|
|
3158
3188
|
});
|
|
3159
3189
|
return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
@@ -3223,7 +3253,7 @@ var DocumentActionHandler = class {
|
|
|
3223
3253
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
3224
3254
|
executing.touchedStreams.add(input.sourceId, job.scope, job.branch);
|
|
3225
3255
|
executing.touchedStreams.add(input.targetId, job.scope, job.branch);
|
|
3226
|
-
stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc,
|
|
3256
|
+
stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc, "head");
|
|
3227
3257
|
indexTxn.write([{
|
|
3228
3258
|
...operation,
|
|
3229
3259
|
documentId: input.sourceId,
|
|
@@ -3242,6 +3272,7 @@ var DocumentActionHandler = class {
|
|
|
3242
3272
|
stores.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
|
|
3243
3273
|
state: sourceDoc.state.document,
|
|
3244
3274
|
documentType: sourceDoc.header.documentType,
|
|
3275
|
+
protocolVersions: sourceDoc.header.protocolVersions,
|
|
3245
3276
|
documentScopeRevision: operation.index + 1
|
|
3246
3277
|
});
|
|
3247
3278
|
return buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
|
|
@@ -3270,55 +3301,632 @@ var DocumentActionHandler = class {
|
|
|
3270
3301
|
}
|
|
3271
3302
|
};
|
|
3272
3303
|
//#endregion
|
|
3273
|
-
//#region src/
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
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);
|
|
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";
|
|
3360
|
+
}
|
|
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
|
+
await resolvePolicies(candidates, job.operations.map((operation) => operation.action), (documentId, branch) => storedPolicy(stores, this.policySource, documentId, branch, signal));
|
|
3733
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
3734
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
3735
|
+
const verdict = await this.verdict(candidates[i], live, void 0, "load", signal);
|
|
3736
|
+
if (verdict.ok) continue;
|
|
3737
|
+
if (this.record(job, candidates[i], verdict, "load")) dropped.add(operations[i]);
|
|
3738
|
+
}
|
|
3739
|
+
return dropped;
|
|
3740
|
+
}
|
|
3741
|
+
async verdict(entry, live, submitted, path, signal) {
|
|
3742
|
+
const verdict = await verifyActionSignature(entry.action, {
|
|
3743
|
+
documentId: entry.stream.documentId,
|
|
3744
|
+
branch: entry.stream.branch,
|
|
3745
|
+
policy: entry.policy
|
|
3746
|
+
}, path, entry.operation);
|
|
3747
|
+
if (!verdict.ok) return verdict;
|
|
3748
|
+
if (isLive(entry, live) || submitted?.has(entry.opId)) return {
|
|
3749
|
+
ok: false,
|
|
3750
|
+
scheme: verdict.scheme,
|
|
3751
|
+
code: "DUPLICATE_ACTION",
|
|
3752
|
+
reason: `action ${entry.action.id} is already in the stream`
|
|
3753
|
+
};
|
|
3754
|
+
if (verdict.scheme === "unsigned") {
|
|
3755
|
+
const address = entry.action.context?.signer?.user?.address;
|
|
3756
|
+
return address ? {
|
|
3757
|
+
ok: false,
|
|
3758
|
+
scheme: "unsigned",
|
|
3759
|
+
code: "UNSIGNED_IDENTITY",
|
|
3760
|
+
reason: `action ${entry.action.id} is unsigned but claims to act as ${address}`
|
|
3761
|
+
} : verdict;
|
|
3762
|
+
}
|
|
3763
|
+
const signer = entry.action.context.signer;
|
|
3764
|
+
if (!await this.authorize(signer, entry.stream.documentId, signal)) return {
|
|
3765
|
+
ok: false,
|
|
3766
|
+
scheme: verdict.scheme,
|
|
3767
|
+
code: "SIGNER_UNAUTHORIZED",
|
|
3768
|
+
reason: `action ${entry.action.id}: key ${signer.app.key} may not sign as ${signer.user.address}`
|
|
3769
|
+
};
|
|
3770
|
+
return verdict;
|
|
3771
|
+
}
|
|
3772
|
+
/** Throws when the policy does, times out or the job is aborted. */
|
|
3773
|
+
async authorize(signer, documentId, signal) {
|
|
3774
|
+
signal?.throwIfAborted();
|
|
3775
|
+
let timer;
|
|
3776
|
+
let onAbort;
|
|
3777
|
+
const stop = new Promise((_, reject) => {
|
|
3778
|
+
timer = setTimeout(() => {
|
|
3779
|
+
reject(/* @__PURE__ */ new Error(`signature trust policy did not answer for ${signer.app.key} within ${this.trustTimeoutMs}ms`));
|
|
3780
|
+
}, this.trustTimeoutMs);
|
|
3781
|
+
if (signal) {
|
|
3782
|
+
onAbort = () => reject(signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason)));
|
|
3783
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3299
3784
|
}
|
|
3300
|
-
|
|
3785
|
+
});
|
|
3786
|
+
try {
|
|
3787
|
+
return await Promise.race([this.trustPolicy.authorizeSigner(signer, signer.app.key, documentId), stop]);
|
|
3788
|
+
} finally {
|
|
3789
|
+
clearTimeout(timer);
|
|
3790
|
+
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
3301
3791
|
}
|
|
3302
3792
|
}
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3793
|
+
/**
|
|
3794
|
+
* The live candidates stored exactly as submitted. A synthesized operation
|
|
3795
|
+
* holds another action, so it cannot be compared; only a retry, whose first
|
|
3796
|
+
* attempt this job was, takes it as committed.
|
|
3797
|
+
*/
|
|
3798
|
+
async storedAsSubmitted(candidates, live, retry, operationStore, signal) {
|
|
3799
|
+
const committed = /* @__PURE__ */ new Map();
|
|
3800
|
+
const toCompare = [];
|
|
3801
|
+
for (const entry of candidates) if (entry.synthesizedOpId && live.has(entry.synthesizedOpId)) {
|
|
3802
|
+
if (retry) committed.set(entry, committedWrite(entry, entry.synthesizedOpId));
|
|
3803
|
+
} else if (live.has(entry.opId)) toCompare.push(entry);
|
|
3804
|
+
for (const [stream, entries] of byStream(toCompare)) {
|
|
3805
|
+
const stored = await operationStore.getOperationsByIds(stream.documentId, stream.scope, stream.branch, entries.map((entry) => entry.opId), signal);
|
|
3806
|
+
const byId = new Map(stored.map((operation) => [operation.id, operation]));
|
|
3807
|
+
for (const entry of entries) {
|
|
3808
|
+
const operation = byId.get(entry.opId);
|
|
3809
|
+
if (operation && sameContent(operation.action, entry.action)) committed.set(entry, committedWrite(entry, entry.opId));
|
|
3317
3810
|
}
|
|
3318
|
-
if (!isValid) throw new InvalidSignatureError(documentId, `Operation ${operation.id} at index ${operation.index} signature verification returned false`);
|
|
3319
3811
|
}
|
|
3812
|
+
return committed;
|
|
3813
|
+
}
|
|
3814
|
+
async liveOperationIds(candidates, operationStore, signal) {
|
|
3815
|
+
const live = /* @__PURE__ */ new Set();
|
|
3816
|
+
for (const [stream, entries] of byStream(candidates)) {
|
|
3817
|
+
const found = await operationStore.findOperationIds(stream.documentId, stream.scope, stream.branch, entries.flatMap((entry) => entry.synthesizedOpId ? [entry.opId, entry.synthesizedOpId] : [entry.opId]), signal);
|
|
3818
|
+
for (const opId of found) live.add(opId);
|
|
3819
|
+
}
|
|
3820
|
+
return live;
|
|
3821
|
+
}
|
|
3822
|
+
/** Logs and counts a refusal; returns the error only when enforcing. */
|
|
3823
|
+
record(job, entry, refusal, path) {
|
|
3824
|
+
const enforced = this.mode === "enforce";
|
|
3825
|
+
const event = {
|
|
3826
|
+
jobId: job.id,
|
|
3827
|
+
documentId: entry.stream.documentId,
|
|
3828
|
+
scope: entry.stream.scope,
|
|
3829
|
+
branch: entry.stream.branch,
|
|
3830
|
+
actionId: entry.action.id,
|
|
3831
|
+
code: refusal.code,
|
|
3832
|
+
scheme: refusal.scheme,
|
|
3833
|
+
path,
|
|
3834
|
+
enforced,
|
|
3835
|
+
reason: refusal.reason
|
|
3836
|
+
};
|
|
3837
|
+
this.logger.warn(enforced ? "Signature refused: @Refusal" : "Signature refusal logged, write admitted: @Refusal", event);
|
|
3838
|
+
this.eventBus.emit(ReactorEventTypes.SIGNATURE_REFUSED, event).catch((error) => {
|
|
3839
|
+
this.logger.error("Failed to emit SIGNATURE_REFUSED: @Error", error);
|
|
3840
|
+
});
|
|
3841
|
+
return enforced ? new InvalidSignatureError(entry.stream.documentId, refusal.code, refusal.reason) : void 0;
|
|
3320
3842
|
}
|
|
3321
3843
|
};
|
|
3844
|
+
/**
|
|
3845
|
+
* A CREATE_DOCUMENT is verified under its own input. Anything else takes the
|
|
3846
|
+
* stored document's policy, or, for a document this job creates, that CREATE's.
|
|
3847
|
+
*/
|
|
3848
|
+
async function resolvePolicies(candidates, jobActions, stored) {
|
|
3849
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
3850
|
+
for (const entry of candidates) {
|
|
3851
|
+
if (entry.action.type === "CREATE_DOCUMENT") {
|
|
3852
|
+
entry.policy = createPolicy(entry.action);
|
|
3853
|
+
continue;
|
|
3854
|
+
}
|
|
3855
|
+
const { documentId, branch } = entry.stream;
|
|
3856
|
+
const key = `${documentId}\u0000${branch}`;
|
|
3857
|
+
let policy = resolved.get(key);
|
|
3858
|
+
if (policy === void 0) {
|
|
3859
|
+
policy = await stored(documentId, branch) ?? createdPolicy(jobActions, documentId) ?? "legacy";
|
|
3860
|
+
resolved.set(key, policy);
|
|
3861
|
+
}
|
|
3862
|
+
entry.policy = policy;
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
async function storedPolicy(stores, source, documentId, branch, signal) {
|
|
3866
|
+
try {
|
|
3867
|
+
if (source === "write-cache") {
|
|
3868
|
+
const document = await stores.writeCache.getState(documentId, "document", branch, void 0, signal);
|
|
3869
|
+
return signaturePolicyOf(document.header);
|
|
3870
|
+
}
|
|
3871
|
+
const meta = await stores.documentMetaCache.getDocumentMeta(documentId, branch, signal);
|
|
3872
|
+
return signaturePolicyOf(meta.protocolVersions);
|
|
3873
|
+
} catch (error) {
|
|
3874
|
+
if (DocumentNotFoundError.isError(error)) return;
|
|
3875
|
+
throw error;
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
function createdPolicy(actions, documentId) {
|
|
3879
|
+
const create = actions.find((action) => action.type === "CREATE_DOCUMENT" && action.input?.documentId === documentId);
|
|
3880
|
+
return create ? createPolicy(create) : void 0;
|
|
3881
|
+
}
|
|
3882
|
+
function createPolicy(action) {
|
|
3883
|
+
return signaturePolicyOf(action.input);
|
|
3884
|
+
}
|
|
3885
|
+
function candidate(action, stream) {
|
|
3886
|
+
const opId = (actionId) => deriveOperationId(stream.documentId, stream.scope, stream.branch, actionId);
|
|
3887
|
+
return {
|
|
3888
|
+
action,
|
|
3889
|
+
stream,
|
|
3890
|
+
opId: opId(action.id),
|
|
3891
|
+
synthesizedOpId: SYNTHESIZING_TYPES.has(action.type) ? opId(synthesizedActionId(action.id)) : void 0
|
|
3892
|
+
};
|
|
3893
|
+
}
|
|
3894
|
+
function isLive(entry, live) {
|
|
3895
|
+
return live.has(entry.opId) || entry.synthesizedOpId !== void 0 && live.has(entry.synthesizedOpId);
|
|
3896
|
+
}
|
|
3897
|
+
function mutationStream(action, job) {
|
|
3898
|
+
return {
|
|
3899
|
+
...actionSigningTarget(action, job.documentId, job.branch),
|
|
3900
|
+
scope: job.scope
|
|
3901
|
+
};
|
|
3902
|
+
}
|
|
3903
|
+
function byStream(candidates) {
|
|
3904
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3905
|
+
for (const entry of candidates) {
|
|
3906
|
+
const key = `${entry.stream.documentId}\u0000${entry.stream.scope}\u0000${entry.stream.branch}`;
|
|
3907
|
+
const group = groups.get(key) ?? [entry.stream, []];
|
|
3908
|
+
group[1].push(entry);
|
|
3909
|
+
groups.set(key, group);
|
|
3910
|
+
}
|
|
3911
|
+
return [...groups.values()];
|
|
3912
|
+
}
|
|
3913
|
+
function isRetry(job) {
|
|
3914
|
+
return job.errorHistory.length > 0 || (job.retryCount ?? 0) > 0;
|
|
3915
|
+
}
|
|
3916
|
+
function committedWrite(entry, opId) {
|
|
3917
|
+
return {
|
|
3918
|
+
...entry.stream,
|
|
3919
|
+
actionId: entry.action.id,
|
|
3920
|
+
opId
|
|
3921
|
+
};
|
|
3922
|
+
}
|
|
3923
|
+
function sameContent(stored, submitted) {
|
|
3924
|
+
try {
|
|
3925
|
+
return canonicalJson(stored) === canonicalJson(submitted);
|
|
3926
|
+
} catch {
|
|
3927
|
+
return false;
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3322
3930
|
//#endregion
|
|
3323
3931
|
//#region src/executor/simple-job-executor.ts
|
|
3324
3932
|
const MAX_SKIP_THRESHOLD = 1e3;
|
|
@@ -3336,6 +3944,7 @@ function isValidISOTimestamp(value) {
|
|
|
3336
3944
|
* every test expect a failed job to look like.
|
|
3337
3945
|
*/
|
|
3338
3946
|
var JobRollbackSignal = class extends Error {
|
|
3947
|
+
result;
|
|
3339
3948
|
constructor(result) {
|
|
3340
3949
|
super("job rolled back");
|
|
3341
3950
|
this.result = result;
|
|
@@ -3343,13 +3952,27 @@ var JobRollbackSignal = class extends Error {
|
|
|
3343
3952
|
}
|
|
3344
3953
|
};
|
|
3345
3954
|
var SimpleJobExecutor = class {
|
|
3955
|
+
logger;
|
|
3956
|
+
registry;
|
|
3957
|
+
operationStore;
|
|
3958
|
+
eventBus;
|
|
3959
|
+
writeCache;
|
|
3960
|
+
operationIndex;
|
|
3961
|
+
documentMetaCache;
|
|
3962
|
+
collectionMembershipCache;
|
|
3963
|
+
driveContainerTypes;
|
|
3346
3964
|
config;
|
|
3347
3965
|
featureFlags;
|
|
3348
3966
|
decisionModel;
|
|
3349
|
-
|
|
3967
|
+
signatureAdmission;
|
|
3350
3968
|
documentActionHandler;
|
|
3351
3969
|
executionScope;
|
|
3352
|
-
|
|
3970
|
+
signer;
|
|
3971
|
+
/**
|
|
3972
|
+
* `signer` signs the operations the reducer synthesizes; unsigned if omitted.
|
|
3973
|
+
* `trustPolicy` decides which keys may sign as which users at admission.
|
|
3974
|
+
*/
|
|
3975
|
+
constructor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, driveContainerTypes, config, executionScope, signer, trustPolicy) {
|
|
3353
3976
|
this.logger = logger;
|
|
3354
3977
|
this.registry = registry;
|
|
3355
3978
|
this.operationStore = operationStore;
|
|
@@ -3359,6 +3982,7 @@ var SimpleJobExecutor = class {
|
|
|
3359
3982
|
this.documentMetaCache = documentMetaCache;
|
|
3360
3983
|
this.collectionMembershipCache = collectionMembershipCache;
|
|
3361
3984
|
this.driveContainerTypes = driveContainerTypes;
|
|
3985
|
+
this.signer = signer ?? new PassthroughSigner();
|
|
3362
3986
|
this.config = {
|
|
3363
3987
|
featureFlags: config.featureFlags ?? {},
|
|
3364
3988
|
maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
|
|
@@ -3368,11 +3992,17 @@ var SimpleJobExecutor = class {
|
|
|
3368
3992
|
retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
|
|
3369
3993
|
retryMaxDelayMs: config.retryMaxDelayMs ?? 5e3,
|
|
3370
3994
|
yieldDeadlineMs: config.yieldDeadlineMs ?? 50,
|
|
3371
|
-
batchApplies: config.batchApplies ?? true
|
|
3995
|
+
batchApplies: config.batchApplies ?? true,
|
|
3996
|
+
signatureVerification: config.signatureVerification ?? "enforce"
|
|
3372
3997
|
};
|
|
3373
3998
|
this.featureFlags = resolveFeatureFlags(config.featureFlags);
|
|
3374
3999
|
this.decisionModel = selectDecisionModel(this.featureFlags, registry);
|
|
3375
|
-
this.
|
|
4000
|
+
this.signatureAdmission = new SignatureAdmission(this.config.signatureVerification, logger, eventBus, this.featureFlags.documentDecisions ? "write-cache" : "meta", {
|
|
4001
|
+
signer: this.signer,
|
|
4002
|
+
authEnforcement: this.featureFlags.authEnforcement,
|
|
4003
|
+
policy: trustPolicy,
|
|
4004
|
+
timeoutMs: Math.min(DEFAULT_TRUST_TIMEOUT_MS, this.config.jobTimeoutMs / 2)
|
|
4005
|
+
});
|
|
3376
4006
|
this.documentActionHandler = new DocumentActionHandler(registry, logger, driveContainerTypes, this.featureFlags, this.decisionModel);
|
|
3377
4007
|
this.executionScope = executionScope ?? new DefaultExecutionScope(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache);
|
|
3378
4008
|
}
|
|
@@ -3502,10 +4132,29 @@ var SimpleJobExecutor = class {
|
|
|
3502
4132
|
pendingEvent
|
|
3503
4133
|
};
|
|
3504
4134
|
}
|
|
3505
|
-
|
|
4135
|
+
let admission;
|
|
4136
|
+
try {
|
|
4137
|
+
admission = await this.signatureAdmission.admitMutation(job, stores, signal);
|
|
4138
|
+
} catch (error) {
|
|
4139
|
+
return { result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) };
|
|
4140
|
+
}
|
|
4141
|
+
if (admission.kind === "refused") return { result: buildErrorResult(job, admission.error, startTime) };
|
|
4142
|
+
let committed;
|
|
4143
|
+
try {
|
|
4144
|
+
committed = await this.reloadCommitted(admission.committed, stores, signal);
|
|
4145
|
+
} catch (error) {
|
|
4146
|
+
return { result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) };
|
|
4147
|
+
}
|
|
4148
|
+
const committedIds = new Set(admission.committed.map((write) => write.actionId));
|
|
4149
|
+
const fresh = committedIds.size === 0 ? job : {
|
|
4150
|
+
...job,
|
|
4151
|
+
actions: job.actions.filter((action) => !committedIds.has(action.id))
|
|
4152
|
+
};
|
|
4153
|
+
if (fresh.actions.length === 0) return this.committedOutcome(job, committed, [], [], stores, startTime);
|
|
4154
|
+
const positioned = await this.positionByTimestamp(fresh, stores, signal);
|
|
3506
4155
|
if (positioned.error) return { result: buildErrorResult(job, positioned.error, startTime) };
|
|
3507
4156
|
const executing = {
|
|
3508
|
-
job,
|
|
4157
|
+
job: fresh,
|
|
3509
4158
|
startTime,
|
|
3510
4159
|
indexTxn,
|
|
3511
4160
|
stores,
|
|
@@ -3535,12 +4184,21 @@ var SimpleJobExecutor = class {
|
|
|
3535
4184
|
} };
|
|
3536
4185
|
const ordinals = await stores.operationIndex.commit(indexTxn, signal);
|
|
3537
4186
|
postCommitMembershipInvalidations.push(...indexTxn.getMembershipInvalidations());
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
4187
|
+
for (let i = 0; i < actionResult.operationsWithContext.length; i++) actionResult.operationsWithContext[i].context.ordinal = ordinals[i];
|
|
4188
|
+
return this.committedOutcome(job, committed, actionResult.operationsWithContext, actionResult.generatedOperations, stores, startTime);
|
|
4189
|
+
}
|
|
4190
|
+
/**
|
|
4191
|
+
* A successful mutation's result and the write-ready event it owes, carrying
|
|
4192
|
+
* what earlier attempts committed ahead of what this one wrote.
|
|
4193
|
+
*/
|
|
4194
|
+
async committedOutcome(job, reloaded, written, generated, stores, startTime) {
|
|
4195
|
+
const operationsWithContext = [...reloaded, ...written];
|
|
4196
|
+
let pendingEvent;
|
|
4197
|
+
if (operationsWithContext.length > 0) {
|
|
4198
|
+
const collectionMemberships = await this.getCollectionMembershipsForOperations(operationsWithContext, stores);
|
|
3541
4199
|
pendingEvent = {
|
|
3542
4200
|
jobId: job.id,
|
|
3543
|
-
operations:
|
|
4201
|
+
operations: operationsWithContext,
|
|
3544
4202
|
jobMeta: job.meta,
|
|
3545
4203
|
submittedActionIds: submittedActionIds(job),
|
|
3546
4204
|
collectionMemberships
|
|
@@ -3550,14 +4208,62 @@ var SimpleJobExecutor = class {
|
|
|
3550
4208
|
result: {
|
|
3551
4209
|
job,
|
|
3552
4210
|
success: true,
|
|
3553
|
-
operations:
|
|
3554
|
-
operationsWithContext
|
|
4211
|
+
operations: [...reloaded.map((entry) => entry.operation), ...generated],
|
|
4212
|
+
operationsWithContext,
|
|
3555
4213
|
duration: Date.now() - startTime
|
|
3556
4214
|
},
|
|
3557
4215
|
pendingEvent
|
|
3558
4216
|
};
|
|
3559
4217
|
}
|
|
3560
4218
|
/**
|
|
4219
|
+
* The stored operations of writes an earlier attempt committed, as a fresh
|
|
4220
|
+
* write would report them. State is rebuilt from the write cache, and left
|
|
4221
|
+
* out when it cannot be.
|
|
4222
|
+
*/
|
|
4223
|
+
async reloadCommitted(writes, stores, signal) {
|
|
4224
|
+
const streams = /* @__PURE__ */ new Map();
|
|
4225
|
+
for (const write of writes) {
|
|
4226
|
+
const key = `${write.documentId}\u0000${write.scope}\u0000${write.branch}`;
|
|
4227
|
+
streams.set(key, [...streams.get(key) ?? [], write]);
|
|
4228
|
+
}
|
|
4229
|
+
const reloaded = [];
|
|
4230
|
+
for (const group of streams.values()) {
|
|
4231
|
+
const { documentId, scope, branch } = group[0];
|
|
4232
|
+
const opIds = [...new Set(group.map((write) => write.opId))];
|
|
4233
|
+
const operations = await stores.operationStore.getOperationsByIds(documentId, scope, branch, opIds, signal);
|
|
4234
|
+
const ordinals = await stores.operationIndex.getOrdinalsByOpIds(documentId, scope, branch, opIds, signal);
|
|
4235
|
+
const { documentType } = await stores.documentMetaCache.getDocumentMeta(documentId, branch, signal);
|
|
4236
|
+
for (const operation of operations.sort((a, b) => a.index - b.index)) reloaded.push({
|
|
4237
|
+
operation,
|
|
4238
|
+
context: {
|
|
4239
|
+
documentId,
|
|
4240
|
+
scope,
|
|
4241
|
+
branch,
|
|
4242
|
+
documentType,
|
|
4243
|
+
resultingState: await this.rebuiltState(stores, {
|
|
4244
|
+
documentId,
|
|
4245
|
+
scope,
|
|
4246
|
+
branch
|
|
4247
|
+
}, operation, signal),
|
|
4248
|
+
ordinal: ordinals.get(operation.id) ?? 0
|
|
4249
|
+
}
|
|
4250
|
+
});
|
|
4251
|
+
}
|
|
4252
|
+
return reloaded;
|
|
4253
|
+
}
|
|
4254
|
+
async rebuiltState(stores, stream, operation, signal) {
|
|
4255
|
+
try {
|
|
4256
|
+
const document = await stores.writeCache.getState(stream.documentId, stream.scope, stream.branch, operation.index, signal);
|
|
4257
|
+
return JSON.stringify({
|
|
4258
|
+
...document.state,
|
|
4259
|
+
header: document.header
|
|
4260
|
+
});
|
|
4261
|
+
} catch (error) {
|
|
4262
|
+
this.logger.warn("No state for committed operation @OperationId in @Stream: @Error", operation.id, stream, error);
|
|
4263
|
+
return;
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
/**
|
|
3561
4267
|
* Drops the cached state of every stream a job wrote, after its transaction
|
|
3562
4268
|
* did not commit.
|
|
3563
4269
|
*
|
|
@@ -3586,16 +4292,6 @@ var SimpleJobExecutor = class {
|
|
|
3586
4292
|
const actions = writes.map((write) => write.action);
|
|
3587
4293
|
const generatedOperations = [];
|
|
3588
4294
|
const operationsWithContext = [];
|
|
3589
|
-
try {
|
|
3590
|
-
await this.signatureVerifierModule.verifyActions(job.documentId, job.branch, actions);
|
|
3591
|
-
} catch (error) {
|
|
3592
|
-
return {
|
|
3593
|
-
success: false,
|
|
3594
|
-
generatedOperations,
|
|
3595
|
-
operationsWithContext,
|
|
3596
|
-
error: error instanceof Error ? error : new Error(String(error))
|
|
3597
|
-
};
|
|
3598
|
-
}
|
|
3599
4295
|
for (const action of actions) if (action.timestampUtcMs && !isValidISOTimestamp(action.timestampUtcMs)) return {
|
|
3600
4296
|
success: false,
|
|
3601
4297
|
generatedOperations,
|
|
@@ -3693,7 +4389,7 @@ var SimpleJobExecutor = class {
|
|
|
3693
4389
|
if (docMeta.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
|
|
3694
4390
|
documentVersion = docMeta.state.version;
|
|
3695
4391
|
}
|
|
3696
|
-
if (isUndoRedo(action) ||
|
|
4392
|
+
if (isUndoRedo(action) || skip > 0) stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
|
|
3697
4393
|
let document;
|
|
3698
4394
|
if (baseDocument !== void 0) document = baseDocument;
|
|
3699
4395
|
else try {
|
|
@@ -3753,7 +4449,7 @@ var SimpleJobExecutor = class {
|
|
|
3753
4449
|
branch: job.branch,
|
|
3754
4450
|
protocolVersion
|
|
3755
4451
|
};
|
|
3756
|
-
updatedDocument = module.reducer(document, action, void 0, reducerOptions);
|
|
4452
|
+
updatedDocument = withProtocolVersions(module.reducer(document, action, void 0, reducerOptions), document.header.protocolVersions);
|
|
3757
4453
|
} catch (error) {
|
|
3758
4454
|
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)}`;
|
|
3759
4455
|
const enhancedError = new Error(contextMessage);
|
|
@@ -3765,6 +4461,15 @@ var SimpleJobExecutor = class {
|
|
|
3765
4461
|
if (operations.length === 0) return buildErrorResult(job, /* @__PURE__ */ new Error("No operation generated from action"), startTime);
|
|
3766
4462
|
const newOperation = operations[operations.length - 1];
|
|
3767
4463
|
if (!isUndoRedo(action)) newOperation.skip = skip;
|
|
4464
|
+
if (deniedReason === void 0 && sourceOperation === void 0 && !executing.replayingAcceptedHistory && isSynthesized(action, newOperation, document.operations[scope] ?? [])) try {
|
|
4465
|
+
await signSynthesized(newOperation, action, {
|
|
4466
|
+
documentId: job.documentId,
|
|
4467
|
+
scope,
|
|
4468
|
+
branch: job.branch
|
|
4469
|
+
}, this.signer, signal);
|
|
4470
|
+
} catch (error) {
|
|
4471
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
4472
|
+
}
|
|
3768
4473
|
const resultingState = JSON.stringify({
|
|
3769
4474
|
...updatedDocument.state,
|
|
3770
4475
|
header: updatedDocument.header
|
|
@@ -3865,7 +4570,7 @@ var SimpleJobExecutor = class {
|
|
|
3865
4570
|
* apply and its own reasons for it.
|
|
3866
4571
|
* - A positional or replayed run carries skips and re-appended operations,
|
|
3867
4572
|
* whose indices are not a simple ascending run from the head.
|
|
3868
|
-
* - UNDO, REDO
|
|
4573
|
+
* - UNDO, REDO and NOOP-with-skip each invalidate the write cache to
|
|
3869
4574
|
* force a full-history rebuild, so they cannot be reduced against state
|
|
3870
4575
|
* threaded from the write before them.
|
|
3871
4576
|
* - The auth scope decides later writes against the policy earlier ones
|
|
@@ -3883,7 +4588,7 @@ var SimpleJobExecutor = class {
|
|
|
3883
4588
|
if (scope === "auth" || scope === "document") return false;
|
|
3884
4589
|
return writes.every((write) => {
|
|
3885
4590
|
const type = write.action.type;
|
|
3886
|
-
return write.skip === 0 && write.deniedReason === void 0 && write.sourceOperation === void 0 && !DOCUMENT_SCOPE_ACTIONS.has(type) && !isUndoRedo(write.action) && type !== "
|
|
4591
|
+
return write.skip === 0 && write.deniedReason === void 0 && write.sourceOperation === void 0 && !DOCUMENT_SCOPE_ACTIONS.has(type) && !isUndoRedo(write.action) && type !== "NOOP";
|
|
3887
4592
|
});
|
|
3888
4593
|
}
|
|
3889
4594
|
/**
|
|
@@ -4270,36 +4975,32 @@ var SimpleJobExecutor = class {
|
|
|
4270
4975
|
error: new InvalidOperationTimestampError(job.documentId, scope, operation.timestampUtcMs, `operation (index: ${operation.index})`),
|
|
4271
4976
|
duration: Date.now() - startTime
|
|
4272
4977
|
};
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
const
|
|
4278
|
-
if (
|
|
4279
|
-
|
|
4280
|
-
let conflictingOps;
|
|
4281
|
-
try {
|
|
4282
|
-
conflictingOps = (await stores.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, void 0, signal)).results;
|
|
4283
|
-
} catch {
|
|
4284
|
-
conflictingOps = [];
|
|
4285
|
-
}
|
|
4286
|
-
let allOpsFromMinConflictingIndex = conflictingOps;
|
|
4287
|
-
if (conflictingOps.length > 0) {
|
|
4288
|
-
const minConflictingIndex = Math.min(...conflictingOps.map((op) => op.index));
|
|
4978
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
4979
|
+
const admitted = /* @__PURE__ */ new Set();
|
|
4980
|
+
let selection = await this.selectLoadWrites(job, job.operations, stores, signal);
|
|
4981
|
+
for (;;) {
|
|
4982
|
+
const unadmitted = selection.incomingOpsToApply.filter((operation) => !admitted.has(operation));
|
|
4983
|
+
if (unadmitted.length === 0) break;
|
|
4984
|
+
let refused;
|
|
4289
4985
|
try {
|
|
4290
|
-
|
|
4291
|
-
} catch {
|
|
4292
|
-
|
|
4986
|
+
refused = await this.signatureAdmission.admitLoad(job, unadmitted, stores, signal);
|
|
4987
|
+
} catch (error) {
|
|
4988
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
4293
4989
|
}
|
|
4990
|
+
for (const operation of unadmitted) if (refused.has(operation)) dropped.add(operation);
|
|
4991
|
+
else admitted.add(operation);
|
|
4992
|
+
if (refused.size === 0) break;
|
|
4993
|
+
const remaining = job.operations.filter((operation) => !dropped.has(operation));
|
|
4994
|
+
if (remaining.length === 0) return {
|
|
4995
|
+
job,
|
|
4996
|
+
success: true,
|
|
4997
|
+
operations: [],
|
|
4998
|
+
operationsWithContext: [],
|
|
4999
|
+
duration: Date.now() - startTime
|
|
5000
|
+
};
|
|
5001
|
+
selection = await this.selectLoadWrites(job, remaining, stores, signal);
|
|
4294
5002
|
}
|
|
4295
|
-
const
|
|
4296
|
-
const nonSupersededOps = conflictingOps.filter((op) => {
|
|
4297
|
-
if (op.index < minIncomingIndex && !incomingActionIds.has(op.action.id)) return false;
|
|
4298
|
-
for (const laterOp of allOpsFromMinConflictingIndex) if (laterOp.index > op.index && laterOp.skip > 0) {
|
|
4299
|
-
if (laterOp.index - laterOp.skip <= op.index) return false;
|
|
4300
|
-
}
|
|
4301
|
-
return true;
|
|
4302
|
-
});
|
|
5003
|
+
const { nonSupersededOps, allOpsFromMinConflictingIndex, incomingOpsToApply } = selection;
|
|
4303
5004
|
const existingOpsToReshuffle = monotonicAuthStream ? [] : nonSupersededOps.filter((operation) => !isGenesisOperation(operation));
|
|
4304
5005
|
const actionIdCounts = /* @__PURE__ */ new Map();
|
|
4305
5006
|
for (const operation of allOpsFromMinConflictingIndex) actionIdCounts.set(operation.action.id, (actionIdCounts.get(operation.action.id) ?? 0) + 1);
|
|
@@ -4320,14 +5021,6 @@ var SimpleJobExecutor = class {
|
|
|
4320
5021
|
const logicalSkip = latestRevision - minLogicalIndex;
|
|
4321
5022
|
if (logicalSkip > skipCount) skipCount = logicalSkip;
|
|
4322
5023
|
}
|
|
4323
|
-
const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
|
|
4324
|
-
const seenIncomingActionIds = /* @__PURE__ */ new Set();
|
|
4325
|
-
const incomingOpsToApply = job.operations.filter((op) => {
|
|
4326
|
-
if (existingActionIds.has(op.action.id)) return false;
|
|
4327
|
-
if (seenIncomingActionIds.has(op.action.id)) return false;
|
|
4328
|
-
seenIncomingActionIds.add(op.action.id);
|
|
4329
|
-
return true;
|
|
4330
|
-
});
|
|
4331
5024
|
if (incomingOpsToApply.length === 0) return {
|
|
4332
5025
|
job,
|
|
4333
5026
|
success: true,
|
|
@@ -4407,6 +5100,53 @@ var SimpleJobExecutor = class {
|
|
|
4407
5100
|
duration: Date.now() - startTime
|
|
4408
5101
|
};
|
|
4409
5102
|
}
|
|
5103
|
+
/** The conflicting window a load opens, and which of its operations are new. */
|
|
5104
|
+
async selectLoadWrites(job, operations, stores, signal) {
|
|
5105
|
+
const scope = job.scope;
|
|
5106
|
+
let minIncomingIndex = Number.POSITIVE_INFINITY;
|
|
5107
|
+
let minIncomingTimestamp = operations[0]?.timestampUtcMs || "";
|
|
5108
|
+
for (const operation of operations) {
|
|
5109
|
+
minIncomingIndex = Math.min(minIncomingIndex, operation.index);
|
|
5110
|
+
const ts = operation.timestampUtcMs || "";
|
|
5111
|
+
if (Date.parse(ts) < Date.parse(minIncomingTimestamp)) minIncomingTimestamp = ts;
|
|
5112
|
+
}
|
|
5113
|
+
let conflictingOps;
|
|
5114
|
+
try {
|
|
5115
|
+
conflictingOps = (await stores.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, void 0, signal)).results;
|
|
5116
|
+
} catch {
|
|
5117
|
+
conflictingOps = [];
|
|
5118
|
+
}
|
|
5119
|
+
let allOpsFromMinConflictingIndex = conflictingOps;
|
|
5120
|
+
if (conflictingOps.length > 0) {
|
|
5121
|
+
const minConflictingIndex = Math.min(...conflictingOps.map((op) => op.index));
|
|
5122
|
+
try {
|
|
5123
|
+
allOpsFromMinConflictingIndex = (await stores.operationStore.getSince(job.documentId, scope, job.branch, minConflictingIndex - 1, void 0, void 0, signal)).results;
|
|
5124
|
+
} catch {
|
|
5125
|
+
allOpsFromMinConflictingIndex = conflictingOps;
|
|
5126
|
+
}
|
|
5127
|
+
}
|
|
5128
|
+
const incomingActionIds = new Set(operations.map((op) => op.action.id));
|
|
5129
|
+
const nonSupersededOps = conflictingOps.filter((op) => {
|
|
5130
|
+
if (op.index < minIncomingIndex && !incomingActionIds.has(op.action.id)) return false;
|
|
5131
|
+
for (const laterOp of allOpsFromMinConflictingIndex) if (laterOp.index > op.index && laterOp.skip > 0) {
|
|
5132
|
+
if (laterOp.index - laterOp.skip <= op.index) return false;
|
|
5133
|
+
}
|
|
5134
|
+
return true;
|
|
5135
|
+
});
|
|
5136
|
+
const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
|
|
5137
|
+
const seenIncomingActionIds = /* @__PURE__ */ new Set();
|
|
5138
|
+
const incomingOpsToApply = operations.filter((op) => {
|
|
5139
|
+
if (existingActionIds.has(op.action.id)) return false;
|
|
5140
|
+
if (seenIncomingActionIds.has(op.action.id)) return false;
|
|
5141
|
+
seenIncomingActionIds.add(op.action.id);
|
|
5142
|
+
return true;
|
|
5143
|
+
});
|
|
5144
|
+
return {
|
|
5145
|
+
nonSupersededOps,
|
|
5146
|
+
allOpsFromMinConflictingIndex,
|
|
5147
|
+
incomingOpsToApply
|
|
5148
|
+
};
|
|
5149
|
+
}
|
|
4410
5150
|
accumulateResultOrReturnError(result, generatedOperations, operationsWithContext) {
|
|
4411
5151
|
if (!result.success) return result;
|
|
4412
5152
|
if (result.operations && result.operations.length > 0) generatedOperations.push(...result.operations);
|
|
@@ -4553,6 +5293,7 @@ var DocumentModelRegistry = class {
|
|
|
4553
5293
|
//#endregion
|
|
4554
5294
|
//#region src/storage/kysely/keyframe-store.ts
|
|
4555
5295
|
var KyselyKeyframeStore = class KyselyKeyframeStore {
|
|
5296
|
+
db;
|
|
4556
5297
|
trx;
|
|
4557
5298
|
constructor(db) {
|
|
4558
5299
|
this.db = db;
|
|
@@ -4637,6 +5378,11 @@ function paginateRows(rows, paging, cursorOf, toItem, refetch) {
|
|
|
4637
5378
|
//#endregion
|
|
4638
5379
|
//#region src/storage/txn.ts
|
|
4639
5380
|
var AtomicTransaction = class {
|
|
5381
|
+
documentId;
|
|
5382
|
+
documentType;
|
|
5383
|
+
scope;
|
|
5384
|
+
branch;
|
|
5385
|
+
baseRevision;
|
|
4640
5386
|
operations = [];
|
|
4641
5387
|
constructor(documentId, documentType, scope, branch, baseRevision) {
|
|
4642
5388
|
this.documentId = documentId;
|
|
@@ -4670,6 +5416,11 @@ var AtomicTransaction = class {
|
|
|
4670
5416
|
//#endregion
|
|
4671
5417
|
//#region src/storage/kysely/store.ts
|
|
4672
5418
|
var _UniqueConstraintContext = class extends Error {
|
|
5419
|
+
documentId;
|
|
5420
|
+
scope;
|
|
5421
|
+
branch;
|
|
5422
|
+
revision;
|
|
5423
|
+
stagedOps;
|
|
4673
5424
|
constructor(documentId, scope, branch, revision, stagedOps) {
|
|
4674
5425
|
super("unique constraint");
|
|
4675
5426
|
this.documentId = documentId;
|
|
@@ -4681,6 +5432,7 @@ var _UniqueConstraintContext = class extends Error {
|
|
|
4681
5432
|
}
|
|
4682
5433
|
};
|
|
4683
5434
|
var KyselyOperationStore = class KyselyOperationStore {
|
|
5435
|
+
db;
|
|
4684
5436
|
trx;
|
|
4685
5437
|
constructor(db) {
|
|
4686
5438
|
this.db = db;
|
|
@@ -4780,12 +5532,13 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4780
5532
|
* otherwise read a snapshot taken before the locks were held.
|
|
4781
5533
|
*/
|
|
4782
5534
|
async acquireStreamLocks(trx, documentId, scope, branch, condition) {
|
|
4783
|
-
const keys = new Set([`${documentId}:${scope}:${branch}`]);
|
|
5535
|
+
const keys = /* @__PURE__ */ new Set([`${documentId}:${scope}:${branch}`]);
|
|
4784
5536
|
for (const stream of condition.streams) keys.add(`${stream.documentId}:${stream.scope}:${stream.branch}`);
|
|
5537
|
+
const sortedKeys = sql.join([...keys].sort());
|
|
4785
5538
|
await sql`
|
|
4786
5539
|
with ordered as materialized (
|
|
4787
5540
|
select key
|
|
4788
|
-
from unnest(array[${
|
|
5541
|
+
from unnest(array[${sortedKeys}]::text[]) with ordinality as t(key, ord)
|
|
4789
5542
|
order by ord
|
|
4790
5543
|
)
|
|
4791
5544
|
select pg_advisory_xact_lock(hashtext(key)) from ordered
|
|
@@ -4939,6 +5692,20 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4939
5692
|
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();
|
|
4940
5693
|
return latest?.latestTimestamp ? new Date(latest.latestTimestamp).toISOString() : void 0;
|
|
4941
5694
|
}
|
|
5695
|
+
async findOperationIds(documentId, scope, branch, opIds, signal) {
|
|
5696
|
+
throwIfAborted(signal);
|
|
5697
|
+
if (opIds.length === 0) return /* @__PURE__ */ new Set();
|
|
5698
|
+
const rows = await this.queryExecutor.selectFrom("Operation").select("opId").distinct().where("opId", "in", opIds).where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).execute();
|
|
5699
|
+
return new Set(rows.map((row) => row.opId));
|
|
5700
|
+
}
|
|
5701
|
+
async getOperationsByIds(documentId, scope, branch, opIds, signal) {
|
|
5702
|
+
throwIfAborted(signal);
|
|
5703
|
+
if (opIds.length === 0) return [];
|
|
5704
|
+
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();
|
|
5705
|
+
const latest = /* @__PURE__ */ new Map();
|
|
5706
|
+
for (const row of rows) latest.set(row.opId, this.rowToOperation(row));
|
|
5707
|
+
return [...latest.values()];
|
|
5708
|
+
}
|
|
4942
5709
|
rowToOperation(row) {
|
|
4943
5710
|
return {
|
|
4944
5711
|
index: row.index,
|
|
@@ -5410,8 +6177,148 @@ async function getMigrationStatus(db, schema = REACTOR_SCHEMA) {
|
|
|
5410
6177
|
}
|
|
5411
6178
|
//#endregion
|
|
5412
6179
|
//#region src/core/drive-container-types.ts
|
|
5413
|
-
const DEFAULT_DRIVE_CONTAINER_TYPES = new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
|
|
6180
|
+
const DEFAULT_DRIVE_CONTAINER_TYPES = /* @__PURE__ */ new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
|
|
6181
|
+
//#endregion
|
|
6182
|
+
//#region src/executor/worker/error-info.ts
|
|
6183
|
+
const MAX_STACK_LENGTH = 8192;
|
|
6184
|
+
const MAX_CAUSE_DEPTH = 8;
|
|
6185
|
+
function toErrorInfo(err, depth = 0) {
|
|
6186
|
+
if (depth > MAX_CAUSE_DEPTH) return {
|
|
6187
|
+
name: "Error",
|
|
6188
|
+
message: "cause chain truncated"
|
|
6189
|
+
};
|
|
6190
|
+
if (err instanceof Error) {
|
|
6191
|
+
const info = {
|
|
6192
|
+
name: err.name || "Error",
|
|
6193
|
+
message: err.message
|
|
6194
|
+
};
|
|
6195
|
+
if (typeof err.stack === "string") info.stack = err.stack.length > MAX_STACK_LENGTH ? err.stack.slice(0, MAX_STACK_LENGTH) : err.stack;
|
|
6196
|
+
if (err.cause !== void 0) info.cause = toErrorInfo(err.cause, depth + 1);
|
|
6197
|
+
return info;
|
|
6198
|
+
}
|
|
6199
|
+
if (typeof err === "string") return {
|
|
6200
|
+
name: "Error",
|
|
6201
|
+
message: err
|
|
6202
|
+
};
|
|
6203
|
+
if (err && typeof err === "object") {
|
|
6204
|
+
const record = err;
|
|
6205
|
+
return {
|
|
6206
|
+
name: typeof record.name === "string" ? record.name : "Error",
|
|
6207
|
+
message: typeof record.message === "string" ? record.message : safeStringify(err)
|
|
6208
|
+
};
|
|
6209
|
+
}
|
|
6210
|
+
return {
|
|
6211
|
+
name: "Error",
|
|
6212
|
+
message: safeStringify(err)
|
|
6213
|
+
};
|
|
6214
|
+
}
|
|
6215
|
+
function safeStringify(value) {
|
|
6216
|
+
if (value === null || value === void 0) return String(value);
|
|
6217
|
+
if (typeof value === "string") return value;
|
|
6218
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
6219
|
+
try {
|
|
6220
|
+
return JSON.stringify(value);
|
|
6221
|
+
} catch {
|
|
6222
|
+
return "[unserializable]";
|
|
6223
|
+
}
|
|
6224
|
+
}
|
|
6225
|
+
function fromErrorInfo(info) {
|
|
6226
|
+
const err = new Error(info.message);
|
|
6227
|
+
Object.defineProperty(err, "name", {
|
|
6228
|
+
value: info.name,
|
|
6229
|
+
configurable: true,
|
|
6230
|
+
writable: true
|
|
6231
|
+
});
|
|
6232
|
+
if (info.stack !== void 0) err.stack = info.stack;
|
|
6233
|
+
if (info.cause !== void 0) Object.defineProperty(err, "cause", {
|
|
6234
|
+
value: fromErrorInfo(info.cause),
|
|
6235
|
+
configurable: true,
|
|
6236
|
+
writable: true
|
|
6237
|
+
});
|
|
6238
|
+
return err;
|
|
6239
|
+
}
|
|
6240
|
+
//#endregion
|
|
6241
|
+
//#region src/executor/worker/sanitize.ts
|
|
6242
|
+
const MAX_DEPTH = 8;
|
|
6243
|
+
function errorToInfo(err) {
|
|
6244
|
+
return toErrorInfo(err);
|
|
6245
|
+
}
|
|
6246
|
+
function sanitizeArg(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
6247
|
+
if (depth > MAX_DEPTH) return "[Truncated]";
|
|
6248
|
+
if (value === null || value === void 0) return null;
|
|
6249
|
+
if (typeof value === "boolean") return value;
|
|
6250
|
+
if (typeof value === "number") return value;
|
|
6251
|
+
if (typeof value === "string") return value;
|
|
6252
|
+
if (typeof value === "bigint") return value.toString();
|
|
6253
|
+
if (typeof value === "function" || typeof value === "symbol") return null;
|
|
6254
|
+
if (value instanceof Error) return toErrorInfo(value);
|
|
6255
|
+
if (value instanceof Date) return value.toISOString();
|
|
6256
|
+
if (value instanceof Map) {
|
|
6257
|
+
const obj = {};
|
|
6258
|
+
for (const [k, v] of value.entries()) obj[String(k)] = sanitizeArg(v, depth + 1, seen);
|
|
6259
|
+
return obj;
|
|
6260
|
+
}
|
|
6261
|
+
if (value instanceof Set) {
|
|
6262
|
+
const arr = [];
|
|
6263
|
+
for (const v of value.values()) arr.push(sanitizeArg(v, depth + 1, seen));
|
|
6264
|
+
return arr;
|
|
6265
|
+
}
|
|
6266
|
+
if (Array.isArray(value)) {
|
|
6267
|
+
if (seen.has(value)) return "[Circular]";
|
|
6268
|
+
seen.add(value);
|
|
6269
|
+
const result = value.map((v) => sanitizeArg(v, depth + 1, seen));
|
|
6270
|
+
seen.delete(value);
|
|
6271
|
+
return result;
|
|
6272
|
+
}
|
|
6273
|
+
if (typeof value === "object") {
|
|
6274
|
+
const proto = Object.getPrototypeOf(value);
|
|
6275
|
+
if (proto !== Object.prototype && proto !== null) return { __nonClonable: value.constructor?.name || "Unknown" };
|
|
6276
|
+
if (seen.has(value)) return "[Circular]";
|
|
6277
|
+
seen.add(value);
|
|
6278
|
+
const result = {};
|
|
6279
|
+
for (const [k, v] of Object.entries(value)) result[k] = sanitizeArg(v, depth + 1, seen);
|
|
6280
|
+
seen.delete(value);
|
|
6281
|
+
return result;
|
|
6282
|
+
}
|
|
6283
|
+
return null;
|
|
6284
|
+
}
|
|
6285
|
+
//#endregion
|
|
6286
|
+
//#region src/executor/worker/forwarding-logger.ts
|
|
6287
|
+
function createForwardingLogger(post, level = "info", tags = []) {
|
|
6288
|
+
function safePost(msg) {
|
|
6289
|
+
try {
|
|
6290
|
+
post(msg);
|
|
6291
|
+
} catch {
|
|
6292
|
+
try {
|
|
6293
|
+
process.stderr.write(`[forwarding-logger] postMessage failed: ${msg.level} ${msg.message}\n`);
|
|
6294
|
+
} catch {}
|
|
6295
|
+
}
|
|
6296
|
+
}
|
|
6297
|
+
function emit(wireLevel, message, args) {
|
|
6298
|
+
const sanitizedArgs = args.map((a) => sanitizeArg(a));
|
|
6299
|
+
safePost({
|
|
6300
|
+
type: "log",
|
|
6301
|
+
level: wireLevel,
|
|
6302
|
+
message,
|
|
6303
|
+
args: tags.length > 0 ? [sanitizeArg({ tags }), ...sanitizedArgs] : sanitizedArgs,
|
|
6304
|
+
timestamp: Date.now()
|
|
6305
|
+
});
|
|
6306
|
+
}
|
|
6307
|
+
return {
|
|
6308
|
+
level,
|
|
6309
|
+
verbose: (message, ...args) => emit("debug", message, args),
|
|
6310
|
+
debug: (message, ...args) => emit("debug", message, args),
|
|
6311
|
+
info: (message, ...args) => emit("info", message, args),
|
|
6312
|
+
warn: (message, ...args) => emit("warn", message, args),
|
|
6313
|
+
error: (message, ...args) => emit("error", message, args),
|
|
6314
|
+
errorHandler: ((...data) => {
|
|
6315
|
+
const first = data[0];
|
|
6316
|
+
emit("error", typeof first === "string" ? first : "", data.slice(1));
|
|
6317
|
+
}),
|
|
6318
|
+
child: (childTags) => createForwardingLogger(post, level, [...tags, ...childTags])
|
|
6319
|
+
};
|
|
6320
|
+
}
|
|
5414
6321
|
//#endregion
|
|
5415
|
-
export {
|
|
6322
|
+
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 };
|
|
5416
6323
|
|
|
5417
|
-
//# sourceMappingURL=
|
|
6324
|
+
//# sourceMappingURL=forwarding-logger-CSsCJ38f.js.map
|