@powerhousedao/reactor 6.2.2-dev.27 → 6.2.2-dev.28

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.
@@ -1,5 +1,5 @@
1
1
  import { n as ReactorEventTypes, t as EventBusAggregateError } from "./types-DMKLa0Ok.js";
2
- import { DowngradeNotSupportedError, applyDeleteDocumentAction, applyDeleteDocumentAction as applyDeleteDocumentAction$1, applyUpgradeDocumentAction, applyUpgradeDocumentAction as applyUpgradeDocumentAction$1, createPresignedHeader, decide, defaultBaseState, deriveOperationId, isUndoRedo } from "@powerhousedao/shared/document-model";
2
+ import { DowngradeNotSupportedError, applyDeleteDocumentAction, applyDeleteDocumentAction as applyDeleteDocumentAction$1, applyUpgradeDocumentAction, applyUpgradeDocumentAction as applyUpgradeDocumentAction$1, baseReducerVersion, createPresignedHeader, decide, defaultBaseState, deriveOperationId, garbageCollect, hashDocumentStateForScope, isDenied, isUndoRedo, sortOperations } from "@powerhousedao/shared/document-model";
3
3
  import { v4 } from "uuid";
4
4
  import { Migrator, sql } from "kysely";
5
5
  //#region \0rolldown/runtime.js
@@ -706,6 +706,7 @@ var KyselyOperationIndex = class KyselyOperationIndex {
706
706
  skip: op.skip,
707
707
  hash: op.hash,
708
708
  action: op.action,
709
+ deniedReason: op.deniedReason ?? null,
709
710
  sourceRemote: op.sourceRemote
710
711
  }));
711
712
  operationOrdinals = (await trx.insertInto("operation_index_operations").values(operationRows).returning("ordinal").execute()).map((row) => row.ordinal);
@@ -837,6 +838,7 @@ var KyselyOperationIndex = class KyselyOperationIndex {
837
838
  hash: row.hash,
838
839
  skip: row.skip,
839
840
  action: row.action,
841
+ deniedReason: row.deniedReason ?? void 0,
840
842
  id: row.opId
841
843
  },
842
844
  context: {
@@ -860,6 +862,7 @@ var KyselyOperationIndex = class KyselyOperationIndex {
860
862
  hash: row.hash,
861
863
  skip: row.skip,
862
864
  action: row.action,
865
+ deniedReason: row.deniedReason ?? void 0,
863
866
  id: row.opId,
864
867
  sourceRemote: row.sourceRemote
865
868
  };
@@ -971,6 +974,20 @@ function keyframeRevision(keyframe, documentId, scope) {
971
974
  if (typeof nextIndex !== "number") throw new Error(`Corrupt keyframe for document ${documentId} at revision ${keyframe.revision}: header carries no ${scope} revision`);
972
975
  return nextIndex - 1;
973
976
  }
977
+ /**
978
+ * Records a denied operation in the history without applying it. This is used
979
+ * for auth-rejected operations, as these need to be recorded without being
980
+ * applied.
981
+ */
982
+ function appendWithoutApplying(document, scope, operation) {
983
+ return {
984
+ ...document,
985
+ operations: {
986
+ ...document.operations,
987
+ [scope]: [...document.operations[scope] ?? [], operation]
988
+ }
989
+ };
990
+ }
974
991
  function extractModuleVersion(doc) {
975
992
  const v = doc.state.document.version;
976
993
  return v === 0 ? void 0 : v;
@@ -1254,7 +1271,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1254
1271
  for (const operation of docScopeOpsAfterKeyframe.results) {
1255
1272
  if (documentScopeBound !== void 0 && operation.index > documentScopeBound) break;
1256
1273
  lastDocumentScopeOperation = operation;
1257
- if (operation.error) continue;
1274
+ if (operation.error || isDenied(operation)) continue;
1258
1275
  if (operation.action.type === "UPGRADE_DOCUMENT") {
1259
1276
  const upgradeAction = operation.action;
1260
1277
  const fromVersion = upgradeAction.input.fromVersion;
@@ -1297,7 +1314,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1297
1314
  if (documentScopeBound !== void 0 && operation.index > documentScopeBound) break;
1298
1315
  lastDocumentScopeOperation = operation;
1299
1316
  if (operation.index === 0) continue;
1300
- if (operation.error) continue;
1317
+ if (operation.error || isDenied(operation)) continue;
1301
1318
  if (operation.action.type === "UPGRADE_DOCUMENT") {
1302
1319
  const upgradeAction = operation.action;
1303
1320
  const fromVersion = upgradeAction.input.fromVersion;
@@ -1321,7 +1338,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1321
1338
  docModule = this.registry.getModule(documentType, extractModuleVersion(document));
1322
1339
  } else if (operation.action.type === "DELETE_DOCUMENT") applyDeleteDocumentAction(document, operation.action);
1323
1340
  else {
1324
- const protocolVersion = document.header.protocolVersions?.["base-reducer"] ?? 1;
1341
+ const protocolVersion = baseReducerVersion(document.header);
1325
1342
  document = docModule.reducer(document, operation.action, void 0, {
1326
1343
  skip: operation.skip,
1327
1344
  protocolVersion
@@ -1368,11 +1385,14 @@ var KyselyWriteCache = class KyselyWriteCache {
1368
1385
  for (const operation of result.results) {
1369
1386
  if (targetRevision !== void 0 && operation.index > targetRevision) break;
1370
1387
  const moduleVersion = this.resolveModuleVersionForOp(operation.index, operation.timestampUtcMs, scope, validatedUpgrades, extractModuleVersion(document));
1371
- const protocolVersion = document.header.protocolVersions?.["base-reducer"] ?? 1;
1372
- document = getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
1373
- skip: operation.skip,
1374
- protocolVersion
1375
- });
1388
+ if (isDenied(operation)) document = appendWithoutApplying(document, scope, operation);
1389
+ else {
1390
+ const protocolVersion = baseReducerVersion(document.header);
1391
+ document = getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
1392
+ skip: operation.skip,
1393
+ protocolVersion
1394
+ });
1395
+ }
1376
1396
  }
1377
1397
  const reachedTarget = targetRevision !== void 0 && result.results.some((op) => op.index >= targetRevision);
1378
1398
  hasMorePages = Boolean(result.nextCursor) && !reachedTarget;
@@ -1437,11 +1457,14 @@ var KyselyWriteCache = class KyselyWriteCache {
1437
1457
  for (const operation of pagedResults.results) {
1438
1458
  if (signal?.aborted) throw new Error("Operation aborted");
1439
1459
  if (targetRevision !== void 0 && operation.index > targetRevision) break;
1440
- const protocolVersion = document.header.protocolVersions?.["base-reducer"] ?? 1;
1441
- document = module.reducer(document, operation.action, void 0, {
1442
- skip: operation.skip,
1443
- protocolVersion
1444
- });
1460
+ if (isDenied(operation)) document = appendWithoutApplying(document, scope, operation);
1461
+ else {
1462
+ const protocolVersion = baseReducerVersion(document.header);
1463
+ document = module.reducer(document, operation.action, void 0, {
1464
+ skip: operation.skip,
1465
+ protocolVersion
1466
+ });
1467
+ }
1445
1468
  if (targetRevision !== void 0 && operation.index === targetRevision) break;
1446
1469
  }
1447
1470
  } catch (err) {
@@ -1619,6 +1642,260 @@ function reshuffleByTimestamp(startIndex, opsA, opsB) {
1619
1642
  }));
1620
1643
  }
1621
1644
  //#endregion
1645
+ //#region src/decision/build-decision-model.ts
1646
+ /**
1647
+ * Reads each projection's stream through the write cache, recording the
1648
+ * revision observed. Static projections resolve first; derived projections
1649
+ * see only those and contribute a map from document id to state. Each
1650
+ * distinct stream is read once and yields one append condition entry.
1651
+ */
1652
+ async function buildDecisionModel(cache, definition, target, signal) {
1653
+ const decisionModel = definition(target);
1654
+ const projections = Object.entries(decisionModel.projections);
1655
+ const reads = /* @__PURE__ */ new Map();
1656
+ const model = {};
1657
+ for (const [key, projection] of projections) {
1658
+ if (typeof projection.query === "function") continue;
1659
+ model[key] = (await readStream(cache, projection.query, reads, signal)).state;
1660
+ }
1661
+ const staticModel = { ...model };
1662
+ for (const [key, projection] of projections) {
1663
+ if (typeof projection.query !== "function") continue;
1664
+ const queries = projection.query(staticModel);
1665
+ const value = {};
1666
+ for (const query of queries) {
1667
+ const read = await readStream(cache, query, reads, signal);
1668
+ value[query.documentId] = read.state;
1669
+ }
1670
+ model[key] = value;
1671
+ }
1672
+ return {
1673
+ model,
1674
+ appendCondition: { streams: [...reads.values()].map((read) => read.stream) }
1675
+ };
1676
+ }
1677
+ async function readStream(cache, query, reads, signal) {
1678
+ const key = `${query.documentId}:${query.scope}:${query.branch}`;
1679
+ const existing = reads.get(key);
1680
+ if (existing) return existing;
1681
+ const document = await cache.getState(query.documentId, query.scope, query.branch, void 0, signal);
1682
+ const read = {
1683
+ state: document.state[query.scope],
1684
+ stream: {
1685
+ documentId: query.documentId,
1686
+ scope: query.scope,
1687
+ branch: query.branch,
1688
+ revision: observedRevision(document, query.scope)
1689
+ }
1690
+ };
1691
+ reads.set(key, read);
1692
+ return read;
1693
+ }
1694
+ /**
1695
+ * The highest operation index the document reflects for the scope, or -1 if
1696
+ * empty. `header.revision` is authoritative, not the rebuilt operation list.
1697
+ */
1698
+ function observedRevision(document, scope) {
1699
+ if (scope in document.header.revision) return document.header.revision[scope] - 1;
1700
+ if (scope in document.operations) {
1701
+ const operations = document.operations[scope];
1702
+ if (operations.length > 0) return operations[operations.length - 1].index;
1703
+ }
1704
+ if (!(scope in document.header.revision)) return -1;
1705
+ return document.header.revision[scope] - 1;
1706
+ }
1707
+ /**
1708
+ * The streams a model reads whose queries are known before it is built. A
1709
+ * derived query needs the statically-queried projections first, so it is not
1710
+ * included here.
1711
+ */
1712
+ function staticReadSet(definition) {
1713
+ const streams = [];
1714
+ for (const projection of Object.values(definition.projections)) {
1715
+ if (typeof projection.query === "function") continue;
1716
+ streams.push({
1717
+ query: projection.query,
1718
+ decidingActions: projection.decidingActions,
1719
+ apply: projection.apply
1720
+ });
1721
+ }
1722
+ return streams;
1723
+ }
1724
+ //#endregion
1725
+ //#region src/decision/document-decision-model.ts
1726
+ /** Why the model refused an operation. */
1727
+ const DOCUMENT_DELETED_REASON = "document deleted";
1728
+ /**
1729
+ * The simplest decision model: one projection over the document scope, which
1730
+ * rejects on a deleted document.
1731
+ */
1732
+ function documentDecisionModel(target) {
1733
+ return {
1734
+ projections: { document: {
1735
+ decidingActions: ["DELETE_DOCUMENT"],
1736
+ apply: (document, operation) => operation.action.type === "DELETE_DOCUMENT" ? applyDeleteDocumentAction({
1737
+ ...document,
1738
+ state: { ...document.state }
1739
+ }, operation.action) : document,
1740
+ query: {
1741
+ documentId: target.documentId,
1742
+ branch: target.branch,
1743
+ scope: "document"
1744
+ }
1745
+ } },
1746
+ judgesScope() {
1747
+ return true;
1748
+ },
1749
+ decide(model) {
1750
+ return model.document.isDeleted ? "deny" : "allow";
1751
+ }
1752
+ };
1753
+ }
1754
+ //#endregion
1755
+ //#region src/decision/merged-order.ts
1756
+ /** Identifies a stream within a walk. */
1757
+ function streamKey(query) {
1758
+ return `${query.documentId}:${query.scope}:${query.branch}`;
1759
+ }
1760
+ /**
1761
+ * Orders two operations from different streams by position. Timestamp decides;
1762
+ * an equal timestamp falls to the action id and then the operation id, so that
1763
+ * two replicas holding the same operations agree on the order whatever order
1764
+ * they happen to store them in.
1765
+ */
1766
+ function comparePositions(a, b) {
1767
+ const aTime = Date.parse(a.operation.timestampUtcMs);
1768
+ const bTime = Date.parse(b.operation.timestampUtcMs);
1769
+ if (aTime !== bTime) return aTime - bTime;
1770
+ const actionIds = (a.operation.action.id ?? "").localeCompare(b.operation.action.id ?? "");
1771
+ if (actionIds !== 0) return actionIds;
1772
+ return (a.operation.id ?? "").localeCompare(b.operation.id ?? "");
1773
+ }
1774
+ /**
1775
+ * Merges the read-set streams into one sequence by position. Each stream keeps
1776
+ * its stored order; only how far to go is decided by the timestamp, so a stream
1777
+ * whose stored order disagrees with its timestamp order still applies in the
1778
+ * order it is stored.
1779
+ *
1780
+ * An operation's position in the result is the bound a decision at that
1781
+ * operation reads to: every operation before it has been applied, and it has
1782
+ * not.
1783
+ */
1784
+ function mergeByPosition(streams) {
1785
+ const merged = [];
1786
+ for (const stream of streams) for (const operation of stream.operations) merged.push({
1787
+ streamKey: stream.streamKey,
1788
+ operation
1789
+ });
1790
+ return merged.sort(comparePositions);
1791
+ }
1792
+ /**
1793
+ * For auth-related reshuffles, we may need to retract previous operations.
1794
+ * This function calculates the needed skip value.
1795
+ */
1796
+ function retractionSkip(nextIndex, firstRetractedIndex) {
1797
+ return nextIndex - firstRetractedIndex;
1798
+ }
1799
+ //#endregion
1800
+ //#region src/decision/walk.ts
1801
+ /**
1802
+ * Visits every operation in the read-set once, in the order their positions
1803
+ * fall, and hands back the state each stream held just before it. That state is
1804
+ * what a decision at that operation reads: everything ahead of it has been
1805
+ * applied and it has not.
1806
+ *
1807
+ * Skips are resolved first (i.e. this is performed on a garbage collected
1808
+ * stream), which means we can do a single forward pass.
1809
+ *
1810
+ * A denied operation is visited but not applied.
1811
+ */
1812
+ function* walkByPosition(streams) {
1813
+ const merged = mergeByPosition(streams.map((stream) => ({
1814
+ streamKey: stream.streamKey,
1815
+ operations: garbageCollect(sortOperations([...stream.operations]))
1816
+ })));
1817
+ const states = new Map(streams.map((stream) => [stream.streamKey, stream.document]));
1818
+ for (const { streamKey, operation } of merged) {
1819
+ yield {
1820
+ streamKey,
1821
+ operation,
1822
+ states: new Map(states)
1823
+ };
1824
+ if (isDenied(operation)) continue;
1825
+ const stream = streams.find((candidate) => candidate.streamKey === streamKey);
1826
+ const before = states.get(streamKey);
1827
+ if (before === void 0 || stream === void 0) throw new Error(`No state for stream ${streamKey}`);
1828
+ states.set(streamKey, stream.apply(before, operation));
1829
+ }
1830
+ }
1831
+ //#endregion
1832
+ //#region src/decision/deletion-verdicts.ts
1833
+ const WRITTEN = "written";
1834
+ /** Whether a read stream counts this action as one that changes a verdict. */
1835
+ function canRefuseOthers(operation, readSet) {
1836
+ return readSet.some((stream) => stream.decidingActions.includes(operation.action.type));
1837
+ }
1838
+ /** The deleted one of these, or the first if none of them is deleted. */
1839
+ function firstDeleted(candidates) {
1840
+ for (const candidate of candidates) if (candidate?.state.document.isDeleted) return candidate.state.document;
1841
+ return candidates[0].state.document;
1842
+ }
1843
+ /**
1844
+ * This function determines which operations will be refused because of
1845
+ * deletes, returning reasons in a parallel array (undefined means the
1846
+ * operation is not refused).
1847
+ *
1848
+ * A deletion already in the stream refuses the operations timestamped after it
1849
+ * and leaves the earlier ones alone. A deletion among the ones passed in does
1850
+ * the same to those after it.
1851
+ */
1852
+ async function deletionVerdictsByPosition(documentId, scope, branch, operations, writeCache, operationStore, signal) {
1853
+ const definition = documentDecisionModel({
1854
+ documentId,
1855
+ branch
1856
+ });
1857
+ const readSet = staticReadSet(definition);
1858
+ if (!definition.judgesScope(scope)) return operations.map(() => void 0);
1859
+ const readStreams = await Promise.all(readSet.map(async (stream) => ({
1860
+ stream,
1861
+ operations: (await operationStore.getSince(stream.query.documentId, stream.query.scope, stream.query.branch, 0, { actionTypes: stream.decidingActions }, void 0, signal)).results
1862
+ })));
1863
+ const decidingWritten = operations.filter((operation) => canRefuseOthers(operation, readSet));
1864
+ if (readStreams.every((read) => read.operations.length === 0) && decidingWritten.length === 0) return operations.map(() => void 0);
1865
+ const walked = [];
1866
+ for (const read of readStreams) {
1867
+ const before = await writeCache.getState(read.stream.query.documentId, read.stream.query.scope, read.stream.query.branch, 0, signal);
1868
+ walked.push({
1869
+ streamKey: streamKey(read.stream.query),
1870
+ document: before,
1871
+ operations: read.operations,
1872
+ apply: read.stream.apply
1873
+ });
1874
+ }
1875
+ const writtenStreamIsRead = readSet.find((stream) => stream.query.scope === scope);
1876
+ walked.push({
1877
+ streamKey: WRITTEN,
1878
+ document: walked[0].document,
1879
+ operations,
1880
+ apply: writtenStreamIsRead?.apply ?? ((document) => document)
1881
+ });
1882
+ const reasons = /* @__PURE__ */ new Map();
1883
+ for (const step of walkByPosition(walked)) {
1884
+ if (step.streamKey !== WRITTEN) continue;
1885
+ const document = firstDeleted([...step.states.values()]);
1886
+ const decision = definition.decide({ document }, {
1887
+ address: void 0,
1888
+ key: void 0
1889
+ }, {
1890
+ verb: "execute",
1891
+ scope,
1892
+ operation: step.operation.action.type
1893
+ }, { scopeState: void 0 });
1894
+ reasons.set(step.operation.id, decision === "deny" ? DOCUMENT_DELETED_REASON : void 0);
1895
+ }
1896
+ return operations.map((operation) => reasons.get(operation.id));
1897
+ }
1898
+ //#endregion
1622
1899
  //#region src/cache/operation-index-types.ts
1623
1900
  const DRIVE_COLLECTION_PREFIX = "drive.";
1624
1901
  /**
@@ -1670,11 +1947,11 @@ var DocumentActionHandler = class {
1670
1947
  this.logger = logger;
1671
1948
  this.driveContainerTypes = driveContainerTypes;
1672
1949
  }
1673
- async execute(job, action, startTime, indexTxn, stores, skip = 0, sourceRemote = "", signal) {
1950
+ async execute(job, action, startTime, indexTxn, stores, skip = 0, sourceRemote = "", signal, verdictAlreadyDecided = false) {
1674
1951
  switch (action.type) {
1675
1952
  case "CREATE_DOCUMENT": return this.executeCreate(job, action, startTime, indexTxn, stores, skip, sourceRemote, signal);
1676
- case "DELETE_DOCUMENT": return this.executeDelete(job, action, startTime, indexTxn, stores, sourceRemote, signal);
1677
- case "UPGRADE_DOCUMENT": return this.executeUpgrade(job, action, startTime, indexTxn, stores, skip, sourceRemote, signal);
1953
+ case "DELETE_DOCUMENT": return this.executeDelete(job, action, startTime, indexTxn, stores, sourceRemote, signal, verdictAlreadyDecided);
1954
+ case "UPGRADE_DOCUMENT": return this.executeUpgrade(job, action, startTime, indexTxn, stores, skip, sourceRemote, signal, verdictAlreadyDecided);
1678
1955
  case "ADD_RELATIONSHIP": return this.executeAddRelationship(job, action, startTime, indexTxn, stores, sourceRemote, signal);
1679
1956
  case "REMOVE_RELATIONSHIP": return this.executeRemoveRelationship(job, action, startTime, indexTxn, stores, sourceRemote, signal);
1680
1957
  case "UPDATE_RELATIONSHIP": return this.executeUpdateRelationship(job, action, startTime, indexTxn, stores, sourceRemote, signal);
@@ -1728,7 +2005,7 @@ var DocumentActionHandler = class {
1728
2005
  });
1729
2006
  return buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
1730
2007
  }
1731
- async executeDelete(job, action, startTime, indexTxn, stores, sourceRemote = "", signal) {
2008
+ async executeDelete(job, action, startTime, indexTxn, stores, sourceRemote = "", signal, verdictAlreadyDecided = false) {
1732
2009
  const input = action.input;
1733
2010
  if (!input.documentId) return buildErrorResult(job, /* @__PURE__ */ new Error("DELETE_DOCUMENT action requires a documentId in input"), startTime);
1734
2011
  const documentId = input.documentId;
@@ -1739,7 +2016,7 @@ var DocumentActionHandler = class {
1739
2016
  return buildErrorResult(job, /* @__PURE__ */ new Error(`Failed to fetch document before deletion: ${error instanceof Error ? error.message : String(error)}`), startTime);
1740
2017
  }
1741
2018
  const documentState = document.state.document;
1742
- if (documentState.isDeleted) return buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
2019
+ if (documentState.isDeleted && !verdictAlreadyDecided) return buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
1743
2020
  let operation = createOperation(action, getNextIndexForScope(document, job.scope), 0, {
1744
2021
  documentId,
1745
2022
  scope: job.scope,
@@ -1775,7 +2052,7 @@ var DocumentActionHandler = class {
1775
2052
  });
1776
2053
  return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
1777
2054
  }
1778
- async executeUpgrade(job, action, startTime, indexTxn, stores, skip = 0, sourceRemote = "", signal) {
2055
+ async executeUpgrade(job, action, startTime, indexTxn, stores, skip = 0, sourceRemote = "", signal, verdictAlreadyDecided = false) {
1779
2056
  const input = action.input;
1780
2057
  if (!input.documentId) return buildErrorResult(job, /* @__PURE__ */ new Error("UPGRADE_DOCUMENT action requires a documentId in input"), startTime);
1781
2058
  const documentId = input.documentId;
@@ -1788,7 +2065,7 @@ var DocumentActionHandler = class {
1788
2065
  return buildErrorResult(job, /* @__PURE__ */ new Error(`Failed to fetch document for upgrade: ${error instanceof Error ? error.message : String(error)}`), startTime);
1789
2066
  }
1790
2067
  const documentState = document.state.document;
1791
- if (documentState.isDeleted) return buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
2068
+ if (documentState.isDeleted && !verdictAlreadyDecided) return buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
1792
2069
  const nextIndex = getNextIndexForScope(document, job.scope);
1793
2070
  let upgradePath;
1794
2071
  if (fromVersion > 0 && fromVersion < toVersion) try {
@@ -2011,6 +2288,7 @@ const documentScopeActions = [
2011
2288
  */
2012
2289
  var SimpleJobExecutor = class {
2013
2290
  config;
2291
+ featureFlags;
2014
2292
  signatureVerifierModule;
2015
2293
  documentActionHandler;
2016
2294
  executionScope;
@@ -2025,6 +2303,7 @@ var SimpleJobExecutor = class {
2025
2303
  this.collectionMembershipCache = collectionMembershipCache;
2026
2304
  this.driveContainerTypes = driveContainerTypes;
2027
2305
  this.config = {
2306
+ featureFlags: config.featureFlags ?? {},
2028
2307
  maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
2029
2308
  maxConcurrency: config.maxConcurrency ?? 1,
2030
2309
  jobTimeoutMs: config.jobTimeoutMs ?? 3e4,
@@ -2032,6 +2311,7 @@ var SimpleJobExecutor = class {
2032
2311
  retryMaxDelayMs: config.retryMaxDelayMs ?? 5e3,
2033
2312
  yieldDeadlineMs: config.yieldDeadlineMs ?? 50
2034
2313
  };
2314
+ this.featureFlags = { documentDecisions: config.featureFlags?.documentDecisions ?? false };
2035
2315
  this.signatureVerifierModule = new SignatureVerifier(signatureVerifier);
2036
2316
  this.documentActionHandler = new DocumentActionHandler(registry, logger, driveContainerTypes);
2037
2317
  this.executionScope = executionScope ?? new DefaultExecutionScope(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache);
@@ -2115,7 +2395,7 @@ var SimpleJobExecutor = class {
2115
2395
  const documentIds = [...new Set(operations.map((op) => op.context.documentId))];
2116
2396
  return stores.collectionMembershipCache.getCollectionsForDocuments(documentIds);
2117
2397
  }
2118
- async processActions(job, actions, startTime, indexTxn, stores, skipValues, sourceOperations, sourceRemote = "", signal) {
2398
+ async processActions(job, actions, startTime, indexTxn, stores, skipValues, sourceOperations, sourceRemote = "", signal, deniedReasons) {
2119
2399
  const generatedOperations = [];
2120
2400
  const operationsWithContext = [];
2121
2401
  try {
@@ -2139,7 +2419,8 @@ var SimpleJobExecutor = class {
2139
2419
  const action = actions[actionIndex];
2140
2420
  const skip = skipValues?.[actionIndex] ?? 0;
2141
2421
  const sourceOperation = sourceOperations?.[actionIndex];
2142
- const result = documentScopeActions.includes(action.type) ? await this.documentActionHandler.execute(job, action, startTime, indexTxn, stores, skip, sourceRemote, signal) : await this.executeRegularAction(job, action, startTime, indexTxn, stores, skip, sourceOperation, sourceRemote, signal);
2422
+ const deniedReason = deniedReasons?.[actionIndex];
2423
+ const result = documentScopeActions.includes(action.type) ? await this.documentActionHandler.execute(job, action, startTime, indexTxn, stores, skip, sourceRemote, signal, this.featureFlags.documentDecisions && job.kind === "load") : await this.executeRegularAction(job, action, startTime, indexTxn, stores, skip, sourceOperation, sourceRemote, signal, deniedReason);
2143
2424
  const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
2144
2425
  if (error !== null) return {
2145
2426
  success: false,
@@ -2164,14 +2445,43 @@ var SimpleJobExecutor = class {
2164
2445
  operationsWithContext
2165
2446
  };
2166
2447
  }
2167
- async executeRegularAction(job, action, startTime, indexTxn, stores, skip = 0, sourceOperation, sourceRemote = "", signal) {
2168
- let docMeta;
2169
- try {
2170
- docMeta = await stores.documentMetaCache.getDocumentMeta(job.documentId, job.branch, signal);
2171
- } catch (error) {
2172
- return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
2448
+ async executeRegularAction(job, action, startTime, indexTxn, stores, skip = 0, sourceOperation, sourceRemote = "", signal, deniedReason) {
2449
+ let appendCondition;
2450
+ let documentVersion;
2451
+ const verdictAlreadyDecided = this.featureFlags.documentDecisions && job.kind === "load";
2452
+ if (this.featureFlags.documentDecisions && !verdictAlreadyDecided) {
2453
+ const target = {
2454
+ documentId: job.documentId,
2455
+ branch: job.branch
2456
+ };
2457
+ const definition = documentDecisionModel(target);
2458
+ let built;
2459
+ try {
2460
+ built = await buildDecisionModel(stores.writeCache, () => definition, target, signal);
2461
+ } catch (error) {
2462
+ return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
2463
+ }
2464
+ if (definition.decide(built.model, {
2465
+ address: action.context?.signer?.user.address,
2466
+ key: action.context?.signer?.app.key
2467
+ }, {
2468
+ verb: "execute",
2469
+ scope: action.scope,
2470
+ operation: action.type
2471
+ }, { scopeState: void 0 }) === "deny") return buildErrorResult(job, new DocumentDeletedError(job.documentId, built.model.document.deletedAtUtcIso), startTime);
2472
+ appendCondition = built.appendCondition;
2473
+ documentVersion = built.model.document.version;
2474
+ } else if (verdictAlreadyDecided) documentVersion = (await stores.writeCache.getState(job.documentId, "document", job.branch, void 0, signal)).state.document.version;
2475
+ else {
2476
+ let docMeta;
2477
+ try {
2478
+ docMeta = await stores.documentMetaCache.getDocumentMeta(job.documentId, job.branch, signal);
2479
+ } catch (error) {
2480
+ return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
2481
+ }
2482
+ if (docMeta.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
2483
+ documentVersion = docMeta.state.version;
2173
2484
  }
2174
- if (docMeta.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
2175
2485
  if (isUndoRedo(action) || action.type === "PRUNE" || action.type === "NOOP" && skip > 0) stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
2176
2486
  let document;
2177
2487
  try {
@@ -2192,14 +2502,29 @@ var SimpleJobExecutor = class {
2192
2502
  }
2193
2503
  let module;
2194
2504
  try {
2195
- const moduleVersion = docMeta.state.version === 0 ? void 0 : docMeta.state.version;
2505
+ const moduleVersion = documentVersion === 0 ? void 0 : documentVersion;
2196
2506
  module = this.registry.getModule(document.header.documentType, moduleVersion);
2197
2507
  } catch (error) {
2198
2508
  return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
2199
2509
  }
2200
2510
  let updatedDocument;
2201
- try {
2202
- const protocolVersion = document.header.protocolVersions?.["base-reducer"] ?? 1;
2511
+ if (deniedReason !== void 0) {
2512
+ const denied = createOperation(action, getNextIndexForScope(document, job.scope), skip, {
2513
+ documentId: job.documentId,
2514
+ scope: job.scope,
2515
+ branch: job.branch
2516
+ });
2517
+ denied.deniedReason = deniedReason;
2518
+ denied.hash = hashDocumentStateForScope(document, job.scope);
2519
+ updatedDocument = {
2520
+ ...document,
2521
+ operations: {
2522
+ ...document.operations,
2523
+ [job.scope]: [...document.operations[job.scope] ?? [], denied]
2524
+ }
2525
+ };
2526
+ } else try {
2527
+ const protocolVersion = baseReducerVersion(document.header);
2203
2528
  const reducerOptions = sourceOperation ? {
2204
2529
  skip,
2205
2530
  branch: job.branch,
@@ -2230,7 +2555,7 @@ var SimpleJobExecutor = class {
2230
2555
  try {
2231
2556
  storedOperations = await stores.operationStore.apply(job.documentId, document.header.documentType, scope, job.branch, newOperation.index, (txn) => {
2232
2557
  txn.addOperations(newOperation);
2233
- }, signal);
2558
+ }, signal, appendCondition);
2234
2559
  } catch (error) {
2235
2560
  this.logger.error("Error writing @Operation to IOperationStore: @Error", newOperation, error);
2236
2561
  stores.writeCache.invalidate(job.documentId, scope, job.branch);
@@ -2274,13 +2599,43 @@ var SimpleJobExecutor = class {
2274
2599
  duration: Date.now() - startTime
2275
2600
  };
2276
2601
  }
2602
+ /**
2603
+ * If an operation happened that could change a decision model verdict, we
2604
+ * need to re-evaluate across the streams that could be affected. Previously
2605
+ * approved operations may need to be denied, which will apply new denied
2606
+ * operations with appropriate skip.
2607
+ */
2608
+ async reevaluateReadingScopes(job, startTime, indexTxn, stores, signal) {
2609
+ const definition = documentDecisionModel({
2610
+ documentId: job.documentId,
2611
+ branch: job.branch
2612
+ });
2613
+ const revisions = await stores.operationStore.getRevisions(job.documentId, job.branch, signal);
2614
+ for (const scope of Object.keys(revisions.revision)) {
2615
+ if (!definition.judgesScope(scope)) continue;
2616
+ const stored = (await stores.operationStore.getSince(job.documentId, scope, job.branch, -1, void 0, void 0, signal)).results;
2617
+ const effective = garbageCollect(sortOperations([...stored]));
2618
+ if (effective.length === 0) continue;
2619
+ const recomputed = await deletionVerdictsByPosition(job.documentId, scope, job.branch, effective, stores.writeCache, stores.operationStore, signal);
2620
+ const firstChange = effective.findIndex((operation, i) => operation.deniedReason !== recomputed[i]);
2621
+ if (firstChange === -1) continue;
2622
+ const tail = effective.slice(firstChange);
2623
+ const nextIndex = revisions.revision[scope];
2624
+ stores.writeCache.invalidate(job.documentId, scope, job.branch);
2625
+ const result = await this.processActions({
2626
+ ...job,
2627
+ scope
2628
+ }, tail.map((operation) => operation.action), startTime, indexTxn, stores, tail.map((_, i) => i === 0 ? retractionSkip(nextIndex, tail[0].index) : 0), void 0, "", signal, recomputed.slice(firstChange));
2629
+ if (!result.success) return result.error ?? /* @__PURE__ */ new Error(`Re-evaluation of ${job.documentId} ${scope} failed`);
2630
+ }
2631
+ }
2277
2632
  async executeLoadJob(job, startTime, indexTxn, stores, signal) {
2278
2633
  if (job.operations.length === 0) return buildErrorResult(job, /* @__PURE__ */ new Error("Load job must include at least one operation"), startTime);
2279
2634
  let docMeta;
2280
2635
  try {
2281
2636
  docMeta = await stores.documentMetaCache.getDocumentMeta(job.documentId, job.branch, signal);
2282
2637
  } catch {}
2283
- if (docMeta?.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
2638
+ if (docMeta?.state.isDeleted && !this.featureFlags.documentDecisions) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
2284
2639
  const scope = job.scope;
2285
2640
  let latestRevision;
2286
2641
  try {
@@ -2369,8 +2724,19 @@ var SimpleJobExecutor = class {
2369
2724
  for (const operation of reshuffledOperations) if (operation.action.type === "NOOP") operation.skip = 1;
2370
2725
  const actions = reshuffledOperations.map((operation) => operation.action);
2371
2726
  const skipValues = reshuffledOperations.map((operation) => operation.skip);
2727
+ let deniedReasons;
2728
+ if (this.featureFlags.documentDecisions) try {
2729
+ deniedReasons = await deletionVerdictsByPosition(job.documentId, scope, job.branch, reshuffledOperations, stores.writeCache, stores.operationStore, signal);
2730
+ } catch (error) {
2731
+ return {
2732
+ job,
2733
+ success: false,
2734
+ error: error instanceof Error ? error : new Error(String(error)),
2735
+ duration: Date.now() - startTime
2736
+ };
2737
+ }
2372
2738
  const effectiveSourceRemote = skipCount > 0 ? "" : job.meta.sourceRemote || "";
2373
- const result = await this.processActions(job, actions, startTime, indexTxn, stores, skipValues, reshuffledOperations, effectiveSourceRemote, signal);
2739
+ const result = await this.processActions(job, actions, startTime, indexTxn, stores, skipValues, reshuffledOperations, effectiveSourceRemote, signal, deniedReasons);
2374
2740
  if (!result.success) return {
2375
2741
  job,
2376
2742
  success: false,
@@ -2379,6 +2745,23 @@ var SimpleJobExecutor = class {
2379
2745
  };
2380
2746
  stores.writeCache.invalidate(job.documentId, scope, job.branch);
2381
2747
  if (scope === "document") stores.documentMetaCache.invalidate(job.documentId, job.branch);
2748
+ const readsThisStream = staticReadSet(documentDecisionModel({
2749
+ documentId: job.documentId,
2750
+ branch: job.branch
2751
+ })).some((stream) => stream.query.documentId === job.documentId && stream.query.scope === scope && stream.query.branch === job.branch);
2752
+ if (this.featureFlags.documentDecisions && readsThisStream) {
2753
+ const revisions = await stores.operationStore.getRevisions(job.documentId, job.branch, signal);
2754
+ const latest = Date.parse(revisions.latestTimestamp);
2755
+ if (result.generatedOperations.some((operation) => Date.parse(operation.timestampUtcMs) < latest)) {
2756
+ const error = await this.reevaluateReadingScopes(job, startTime, indexTxn, stores, signal);
2757
+ if (error) return {
2758
+ job,
2759
+ success: false,
2760
+ error,
2761
+ duration: Date.now() - startTime
2762
+ };
2763
+ }
2764
+ }
2382
2765
  return {
2383
2766
  job,
2384
2767
  success: true,
@@ -2639,6 +3022,7 @@ var AtomicTransaction = class {
2639
3022
  action: JSON.stringify(op.action),
2640
3023
  skip: op.skip,
2641
3024
  error: op.error || null,
3025
+ deniedReason: op.deniedReason || null,
2642
3026
  hash: op.hash
2643
3027
  });
2644
3028
  }
@@ -2745,6 +3129,7 @@ var KyselyOperationStore = class KyselyOperationStore {
2745
3129
  hash: op.hash,
2746
3130
  skip: op.skip,
2747
3131
  error: op.error || void 0,
3132
+ deniedReason: op.deniedReason || void 0,
2748
3133
  id: op.opId,
2749
3134
  action: JSON.parse(op.action)
2750
3135
  }));
@@ -2787,6 +3172,7 @@ var KyselyOperationStore = class KyselyOperationStore {
2787
3172
  sql`${op.action}::jsonb`.as("action"),
2788
3173
  sql`${op.skip}::integer`.as("skip"),
2789
3174
  sql`${op.error ?? null}::text`.as("error"),
3175
+ sql`${op.deniedReason ?? null}::text`.as("deniedReason"),
2790
3176
  sql`${op.hash}::text`.as("hash")
2791
3177
  ]).where((eb) => eb.not(eb.exists(eb.selectFrom("Operation").select("Operation.id").where((web) => web.or(condition.streams.map((s) => web.and([
2792
3178
  web("Operation.documentId", "=", s.documentId),
@@ -2809,6 +3195,7 @@ var KyselyOperationStore = class KyselyOperationStore {
2809
3195
  "action",
2810
3196
  "skip",
2811
3197
  "error",
3198
+ "deniedReason",
2812
3199
  "hash"
2813
3200
  ]).expression(expression).returning("id").execute()).length;
2814
3201
  }
@@ -2898,6 +3285,7 @@ var KyselyOperationStore = class KyselyOperationStore {
2898
3285
  hash: row.hash,
2899
3286
  skip: row.skip,
2900
3287
  error: row.error || void 0,
3288
+ deniedReason: row.deniedReason || void 0,
2901
3289
  id: row.opId,
2902
3290
  action: row.action
2903
3291
  };
@@ -2983,8 +3371,8 @@ function createForwardingPoolInstrumentation(name) {
2983
3371
  }
2984
3372
  //#endregion
2985
3373
  //#region src/storage/migrations/001_create_operation_table.ts
2986
- var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$13 });
2987
- async function up$13(db) {
3374
+ var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$14 });
3375
+ async function up$14(db) {
2988
3376
  await db.schema.createTable("Operation").addColumn("id", "serial", (col) => col.primaryKey()).addColumn("jobId", "text", (col) => col.notNull()).addColumn("opId", "text", (col) => col.notNull()).addColumn("prevOpId", "text", (col) => col.notNull()).addColumn("writeTimestampUtcMs", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("documentId", "text", (col) => col.notNull()).addColumn("documentType", "text", (col) => col.notNull()).addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("timestampUtcMs", "timestamptz", (col) => col.notNull()).addColumn("index", "integer", (col) => col.notNull()).addColumn("action", "jsonb", (col) => col.notNull()).addColumn("skip", "integer", (col) => col.notNull()).addColumn("error", "text").addColumn("hash", "text", (col) => col.notNull()).addUniqueConstraint("unique_revision", [
2989
3377
  "documentId",
2990
3378
  "scope",
@@ -3009,8 +3397,8 @@ async function up$13(db) {
3009
3397
  }
3010
3398
  //#endregion
3011
3399
  //#region src/storage/migrations/002_create_keyframe_table.ts
3012
- var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$12 });
3013
- async function up$12(db) {
3400
+ var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$13 });
3401
+ async function up$13(db) {
3014
3402
  await db.schema.createTable("Keyframe").addColumn("id", "serial", (col) => col.primaryKey()).addColumn("documentId", "text", (col) => col.notNull()).addColumn("documentType", "text", (col) => col.notNull()).addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("revision", "integer", (col) => col.notNull()).addColumn("document", "jsonb", (col) => col.notNull()).addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addUniqueConstraint("unique_keyframe", [
3015
3403
  "documentId",
3016
3404
  "scope",
@@ -3026,14 +3414,14 @@ async function up$12(db) {
3026
3414
  }
3027
3415
  //#endregion
3028
3416
  //#region src/storage/migrations/003_create_document_table.ts
3029
- var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$11 });
3030
- async function up$11(db) {
3417
+ var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$12 });
3418
+ async function up$12(db) {
3031
3419
  await db.schema.createTable("Document").addColumn("id", "text", (col) => col.primaryKey()).addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
3032
3420
  }
3033
3421
  //#endregion
3034
3422
  //#region src/storage/migrations/004_create_document_relationship_table.ts
3035
- var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$10 });
3036
- async function up$10(db) {
3423
+ var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$11 });
3424
+ async function up$11(db) {
3037
3425
  await db.schema.createTable("DocumentRelationship").addColumn("id", "text", (col) => col.primaryKey()).addColumn("sourceId", "text", (col) => col.notNull().references("Document.id").onDelete("cascade")).addColumn("targetId", "text", (col) => col.notNull().references("Document.id").onDelete("cascade")).addColumn("relationshipType", "text", (col) => col.notNull()).addColumn("metadata", "jsonb").addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addUniqueConstraint("unique_source_target_type", [
3038
3426
  "sourceId",
3039
3427
  "targetId",
@@ -3045,14 +3433,14 @@ async function up$10(db) {
3045
3433
  }
3046
3434
  //#endregion
3047
3435
  //#region src/storage/migrations/005_create_indexer_state_table.ts
3048
- var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$9 });
3049
- async function up$9(db) {
3436
+ var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$10 });
3437
+ async function up$10(db) {
3050
3438
  await db.schema.createTable("IndexerState").addColumn("id", "integer", (col) => col.primaryKey().generatedAlwaysAsIdentity()).addColumn("lastOperationId", "integer", (col) => col.notNull()).addColumn("lastOperationTimestamp", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
3051
3439
  }
3052
3440
  //#endregion
3053
3441
  //#region src/storage/migrations/006_create_document_snapshot_table.ts
3054
- var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$8 });
3055
- async function up$8(db) {
3442
+ var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$9 });
3443
+ async function up$9(db) {
3056
3444
  await db.schema.createTable("DocumentSnapshot").addColumn("id", "text", (col) => col.primaryKey()).addColumn("documentId", "text", (col) => col.notNull()).addColumn("slug", "text").addColumn("name", "text").addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("content", "jsonb", (col) => col.notNull()).addColumn("documentType", "text", (col) => col.notNull()).addColumn("lastOperationIndex", "integer", (col) => col.notNull()).addColumn("lastOperationHash", "text", (col) => col.notNull()).addColumn("lastUpdatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("snapshotVersion", "integer", (col) => col.notNull().defaultTo(1)).addColumn("identifiers", "jsonb").addColumn("metadata", "jsonb").addColumn("isDeleted", "boolean", (col) => col.notNull().defaultTo(false)).addColumn("deletedAt", "timestamptz").addUniqueConstraint("unique_doc_scope_branch", [
3057
3445
  "documentId",
3058
3446
  "scope",
@@ -3073,8 +3461,8 @@ async function up$8(db) {
3073
3461
  }
3074
3462
  //#endregion
3075
3463
  //#region src/storage/migrations/007_create_slug_mapping_table.ts
3076
- var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$7 });
3077
- async function up$7(db) {
3464
+ var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$8 });
3465
+ async function up$8(db) {
3078
3466
  await db.schema.createTable("SlugMapping").addColumn("slug", "text", (col) => col.primaryKey()).addColumn("documentId", "text", (col) => col.notNull()).addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addUniqueConstraint("unique_docid_scope_branch", [
3079
3467
  "documentId",
3080
3468
  "scope",
@@ -3084,14 +3472,14 @@ async function up$7(db) {
3084
3472
  }
3085
3473
  //#endregion
3086
3474
  //#region src/storage/migrations/008_create_view_state_table.ts
3087
- var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$6 });
3088
- async function up$6(db) {
3475
+ var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$7 });
3476
+ async function up$7(db) {
3089
3477
  await db.schema.createTable("ViewState").addColumn("readModelId", "text", (col) => col.primaryKey()).addColumn("lastOrdinal", "integer", (col) => col.notNull().defaultTo(0)).addColumn("lastOperationTimestamp", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
3090
3478
  }
3091
3479
  //#endregion
3092
3480
  //#region src/storage/migrations/009_create_operation_index_tables.ts
3093
- var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$5 });
3094
- async function up$5(db) {
3481
+ var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$6 });
3482
+ async function up$6(db) {
3095
3483
  await db.schema.createTable("document_collections").addColumn("documentId", "text", (col) => col.notNull()).addColumn("collectionId", "text", (col) => col.notNull()).addColumn("joinedOrdinal", "bigint", (col) => col.notNull().defaultTo(0)).addColumn("leftOrdinal", "bigint").addPrimaryKeyConstraint("document_collections_pkey", ["documentId", "collectionId"]).execute();
3096
3484
  await db.schema.createIndex("idx_document_collections_collectionId").on("document_collections").column("collectionId").execute();
3097
3485
  await db.schema.createIndex("idx_doc_collections_collection_range").on("document_collections").columns(["collectionId", "joinedOrdinal"]).execute();
@@ -3105,8 +3493,8 @@ async function up$5(db) {
3105
3493
  }
3106
3494
  //#endregion
3107
3495
  //#region src/storage/migrations/010_create_sync_tables.ts
3108
- var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$4 });
3109
- async function up$4(db) {
3496
+ var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$5 });
3497
+ async function up$5(db) {
3110
3498
  await db.schema.createTable("sync_remotes").addColumn("name", "text", (col) => col.primaryKey()).addColumn("collection_id", "text", (col) => col.notNull()).addColumn("channel_type", "text", (col) => col.notNull()).addColumn("channel_id", "text", (col) => col.notNull().defaultTo("")).addColumn("remote_name", "text", (col) => col.notNull().defaultTo("")).addColumn("channel_parameters", "jsonb", (col) => col.notNull().defaultTo(sql`'{}'::jsonb`)).addColumn("filter_document_ids", "jsonb").addColumn("filter_scopes", "jsonb").addColumn("filter_branch", "text", (col) => col.notNull().defaultTo("main")).addColumn("push_state", "text", (col) => col.notNull().defaultTo("idle")).addColumn("push_last_success_utc_ms", "text").addColumn("push_last_failure_utc_ms", "text").addColumn("push_failure_count", "integer", (col) => col.notNull().defaultTo(0)).addColumn("pull_state", "text", (col) => col.notNull().defaultTo("idle")).addColumn("pull_last_success_utc_ms", "text").addColumn("pull_last_failure_utc_ms", "text").addColumn("pull_failure_count", "integer", (col) => col.notNull().defaultTo(0)).addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
3111
3499
  await db.schema.createIndex("idx_sync_remotes_collection").on("sync_remotes").column("collection_id").execute();
3112
3500
  await db.schema.createTable("sync_cursors").addColumn("remote_name", "text", (col) => col.primaryKey().references("sync_remotes.name").onDelete("cascade")).addColumn("cursor_ordinal", "bigint", (col) => col.notNull().defaultTo(0)).addColumn("last_synced_at_utc_ms", "text").addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
@@ -3114,8 +3502,8 @@ async function up$4(db) {
3114
3502
  }
3115
3503
  //#endregion
3116
3504
  //#region src/storage/migrations/011_add_cursor_type_column.ts
3117
- var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$3 });
3118
- async function up$3(db) {
3505
+ var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$4 });
3506
+ async function up$4(db) {
3119
3507
  await db.deleteFrom("sync_cursors").where("remote_name", "like", "outbox::%").execute();
3120
3508
  await db.deleteFrom("sync_remotes").where("name", "like", "outbox::%").execute();
3121
3509
  await db.schema.dropTable("sync_cursors").execute();
@@ -3124,24 +3512,43 @@ async function up$3(db) {
3124
3512
  }
3125
3513
  //#endregion
3126
3514
  //#region src/storage/migrations/012_add_source_remote_column.ts
3127
- var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$2 });
3128
- async function up$2(db) {
3515
+ var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$3 });
3516
+ async function up$3(db) {
3129
3517
  await db.schema.alterTable("operation_index_operations").addColumn("sourceRemote", "text", (col) => col.notNull().defaultTo("")).execute();
3130
3518
  }
3131
3519
  //#endregion
3132
3520
  //#region src/storage/migrations/013_create_sync_dead_letters_table.ts
3133
- var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$1 });
3134
- async function up$1(db) {
3521
+ var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$2 });
3522
+ async function up$2(db) {
3135
3523
  await db.schema.createTable("sync_dead_letters").addColumn("ordinal", "serial", (col) => col.primaryKey()).addColumn("id", "text", (col) => col.unique().notNull()).addColumn("job_id", "text", (col) => col.notNull()).addColumn("job_dependencies", "jsonb", (col) => col.notNull().defaultTo(sql`'[]'::jsonb`)).addColumn("remote_name", "text", (col) => col.notNull().references("sync_remotes.name").onDelete("cascade")).addColumn("document_id", "text", (col) => col.notNull()).addColumn("scopes", "jsonb", (col) => col.notNull().defaultTo(sql`'[]'::jsonb`)).addColumn("branch", "text", (col) => col.notNull()).addColumn("operations", "jsonb", (col) => col.notNull().defaultTo(sql`'[]'::jsonb`)).addColumn("error_source", "text", (col) => col.notNull()).addColumn("error_message", "text", (col) => col.notNull()).addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
3136
3524
  await db.schema.createIndex("idx_sync_dead_letters_remote").on("sync_dead_letters").column("remote_name").execute();
3137
3525
  }
3138
3526
  //#endregion
3139
3527
  //#region src/storage/migrations/014_create_processor_cursor_table.ts
3140
- var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up });
3141
- async function up(db) {
3528
+ var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$1 });
3529
+ async function up$1(db) {
3142
3530
  await db.schema.createTable("ProcessorCursor").addColumn("processorId", "text", (col) => col.primaryKey()).addColumn("factoryId", "text", (col) => col.notNull()).addColumn("driveId", "text", (col) => col.notNull()).addColumn("processorIndex", "integer", (col) => col.notNull()).addColumn("lastOrdinal", "integer", (col) => col.notNull().defaultTo(sql`0`)).addColumn("status", "text", (col) => col.notNull().defaultTo(sql`'active'`)).addColumn("lastError", "text").addColumn("lastErrorTimestamp", "timestamptz").addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
3143
3531
  }
3144
3532
  //#endregion
3533
+ //#region src/storage/migrations/015_add_operation_denied_reason.ts
3534
+ var _015_add_operation_denied_reason_exports = /* @__PURE__ */ __exportAll({
3535
+ down: () => down,
3536
+ up: () => up
3537
+ });
3538
+ /**
3539
+ * Records why authorization refused an operation. Separate from `error` so a
3540
+ * denial is distinguishable from a reducer failure without matching on a
3541
+ * message. Null for every operation written before decisions were enforced.
3542
+ */
3543
+ async function up(db) {
3544
+ await db.schema.alterTable("Operation").addColumn("deniedReason", "text").execute();
3545
+ await db.schema.alterTable("operation_index_operations").addColumn("deniedReason", "text").execute();
3546
+ }
3547
+ async function down(db) {
3548
+ await db.schema.alterTable("operation_index_operations").dropColumn("deniedReason").execute();
3549
+ await db.schema.alterTable("Operation").dropColumn("deniedReason").execute();
3550
+ }
3551
+ //#endregion
3145
3552
  //#region src/storage/migrations/migrator.ts
3146
3553
  const REACTOR_SCHEMA = "reactor";
3147
3554
  const migrations = {
@@ -3158,7 +3565,8 @@ const migrations = {
3158
3565
  "011_add_cursor_type_column": _011_add_cursor_type_column_exports,
3159
3566
  "012_add_source_remote_column": _012_add_source_remote_column_exports,
3160
3567
  "013_create_sync_dead_letters_table": _013_create_sync_dead_letters_table_exports,
3161
- "014_create_processor_cursor_table": _014_create_processor_cursor_table_exports
3568
+ "014_create_processor_cursor_table": _014_create_processor_cursor_table_exports,
3569
+ "015_add_operation_denied_reason": _015_add_operation_denied_reason_exports
3162
3570
  };
3163
3571
  var ProgrammaticMigrationProvider = class {
3164
3572
  getMigrations() {
@@ -3212,6 +3620,6 @@ async function getMigrationStatus(db, schema = REACTOR_SCHEMA) {
3212
3620
  //#region src/core/drive-container-types.ts
3213
3621
  const DEFAULT_DRIVE_CONTAINER_TYPES = new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
3214
3622
  //#endregion
3215
- export { DocumentDeletedError as A, OptimisticLockError as C, InvalidModuleError as D, DuplicateModuleError as E, __exportAll as F, matchesScope as M, parsePagingOptions as N, ModuleNotFoundError as O, throwIfAborted as P, DuplicateOperationError as S, DuplicateManifestError as T, createConsistencyToken as _, createForwardingPoolInstrumentation as a, APPEND_CONDITION_FAILED_PREFIX as b, KyselyKeyframeStore as c, DriveCollectionId as d, KyselyExecutionScope as f, DocumentMetaCache as g, KyselyOperationIndex as h, runMigrations as i, DocumentNotFoundError as j, AuthorizationDeniedError as k, DocumentModelRegistry as l, KyselyWriteCache as m, REACTOR_SCHEMA as n, instrumentPgPool as o, EventBus as p, getMigrationStatus as r, KyselyOperationStore as s, DEFAULT_DRIVE_CONTAINER_TYPES as t, SimpleJobExecutor as u, createEmptyConsistencyToken as v, RevisionMismatchError as w, AppendConditionFailedError as x, CollectionMembershipCache as y };
3623
+ export { AuthorizationDeniedError as A, DuplicateOperationError as C, DuplicateModuleError as D, DuplicateManifestError as E, throwIfAborted as F, __exportAll as I, DocumentNotFoundError as M, matchesScope as N, InvalidModuleError as O, parsePagingOptions as P, AppendConditionFailedError as S, RevisionMismatchError as T, DocumentMetaCache as _, createForwardingPoolInstrumentation as a, CollectionMembershipCache as b, KyselyKeyframeStore as c, DriveCollectionId as d, buildDecisionModel as f, KyselyOperationIndex as g, KyselyWriteCache as h, runMigrations as i, DocumentDeletedError as j, ModuleNotFoundError as k, DocumentModelRegistry as l, EventBus as m, REACTOR_SCHEMA as n, instrumentPgPool as o, KyselyExecutionScope as p, getMigrationStatus as r, KyselyOperationStore as s, DEFAULT_DRIVE_CONTAINER_TYPES as t, SimpleJobExecutor as u, createConsistencyToken as v, OptimisticLockError as w, APPEND_CONDITION_FAILED_PREFIX as x, createEmptyConsistencyToken as y };
3216
3624
 
3217
- //# sourceMappingURL=drive-container-types-uigTGO3i.js.map
3625
+ //# sourceMappingURL=drive-container-types-BNwEHEXP.js.map