@powerhousedao/reactor 6.2.2-dev.42 → 6.2.2-dev.44

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 { AUTH_ACTION_TYPES, AUTH_DENIED_BY_GRANT_REASON, AUTH_NO_GRANT_REASON, AUTH_VERSION_UNSUPPORTED_REASON, DOCUMENT_DELETED_REASON, DowngradeNotSupportedError, appendWithoutApplying, applyAuthAction, applyDeleteDocumentAction, applyDeleteDocumentAction as applyDeleteDocumentAction$1, applyUpgradeDocumentAction, applyUpgradeDocumentAction as applyUpgradeDocumentAction$1, baseReducerVersion, createPresignedHeader, decide, defaultBaseState, deriveOperationId, evaluate, garbageCollect, hashDocumentStateForScope, isDenied, isUndoRedo, sortOperations } from "@powerhousedao/shared/document-model";
2
+ import { AUTH_ACTION_TYPES, AUTH_DENIED_BY_GRANT_REASON, AUTH_NO_GRANT_REASON, AUTH_VERSION_UNSUPPORTED_REASON, DOCUMENT_DELETED_REASON, DowngradeNotSupportedError as DowngradeNotSupportedError$1, appendWithoutApplying, applyAuthAction, applyDeleteDocumentAction, applyDeleteDocumentAction as applyDeleteDocumentAction$1, applyUpgradeDocumentAction, applyUpgradeDocumentAction as applyUpgradeDocumentAction$1, baseReducerVersion, createPresignedHeader, decide, defaultBaseState, deriveOperationId, evaluate, garbageCollect, groupDocumentType, groupMembershipActionTypes, hashDocumentStateForScope, isDenied, isUndoRedo, mentionedGroupIds, normalizeDocumentModelVersion, referencedGroupIds, sortOperations } from "@powerhousedao/shared/document-model";
3
3
  import { v4 } from "uuid";
4
4
  import { Migrator, sql } from "kysely";
5
5
  //#region \0rolldown/runtime.js
@@ -182,6 +182,28 @@ var InvalidSignatureError = class InvalidSignatureError extends Error {
182
182
  }
183
183
  };
184
184
  /**
185
+ * An UPGRADE_DOCUMENT action's preconditions (fromVersion and the per-scope
186
+ * revision snapshot) did not match the document state the executor loaded.
187
+ *
188
+ * Terminal rather than retryable: the action carries the client's snapshot,
189
+ * which stays stale no matter how often the job re-runs. The client is
190
+ * expected to re-read the document and submit a fresh action instead.
191
+ */
192
+ var UpgradePreconditionFailedError = class UpgradePreconditionFailedError extends Error {
193
+ documentId;
194
+ detail;
195
+ constructor(documentId, detail) {
196
+ super(`Upgrade precondition failed for document ${documentId}: ${detail}`);
197
+ this.name = "UpgradePreconditionFailedError";
198
+ this.documentId = documentId;
199
+ this.detail = detail;
200
+ Error.captureStackTrace(this, UpgradePreconditionFailedError);
201
+ }
202
+ static isError(error) {
203
+ return Error.isError(error) && error.name === "UpgradePreconditionFailedError";
204
+ }
205
+ };
206
+ /**
185
207
  * Error thrown when a document is not found (no operations exist for the document ID).
186
208
  */
187
209
  var DocumentNotFoundError = class DocumentNotFoundError extends Error {
@@ -736,6 +758,7 @@ var KyselyOperationIndexTxn = class {
736
758
  collections = [];
737
759
  collectionMemberships = [];
738
760
  collectionRemovals = [];
761
+ groupReferences = [];
739
762
  operations = [];
740
763
  createCollection(collectionId) {
741
764
  this.collections.push(collectionId);
@@ -758,12 +781,25 @@ var KyselyOperationIndexTxn = class {
758
781
  operationIndex: lastOpIndex
759
782
  });
760
783
  }
784
+ recordGroupReferences(documentId, groupIds) {
785
+ const lastOpIndex = this.operations.length - 1;
786
+ if (lastOpIndex < 0) throw new Error("recordGroupReferences must be called after write() - no operations in transaction");
787
+ if (groupIds.length === 0) return;
788
+ this.groupReferences.push({
789
+ documentId,
790
+ groupIds,
791
+ operationIndex: lastOpIndex
792
+ });
793
+ }
761
794
  write(operations) {
762
795
  this.operations.push(...operations);
763
796
  }
764
797
  getCollections() {
765
798
  return this.collections;
766
799
  }
800
+ getGroupReferenceRecords() {
801
+ return this.groupReferences;
802
+ }
767
803
  getCollectionMembershipRecords() {
768
804
  return this.collectionMemberships;
769
805
  }
@@ -800,10 +836,27 @@ var KyselyOperationIndex = class KyselyOperationIndex {
800
836
  });
801
837
  return resultOrdinals;
802
838
  }
839
+ /**
840
+ * A policy-driven join: keeps the earliest join so a rediscovered reference
841
+ * never shrinks a backfill window remotes already rely on, and reopens a
842
+ * closed membership because a policy reference is not a removable one.
843
+ */
844
+ async joinKeepingEarliest(trx, documentId, collectionId, ordinal) {
845
+ await trx.insertInto("document_collections").values({
846
+ documentId,
847
+ collectionId,
848
+ joinedOrdinal: ordinal,
849
+ leftOrdinal: null
850
+ }).onConflict((oc) => oc.columns(["documentId", "collectionId"]).doUpdateSet({
851
+ joinedOrdinal: sql`LEAST("document_collections"."joinedOrdinal", EXCLUDED."joinedOrdinal")`,
852
+ leftOrdinal: null
853
+ })).execute();
854
+ }
803
855
  async executeCommit(trx, kyselyTxn) {
804
856
  const collections = kyselyTxn.getCollections();
805
857
  const memberships = kyselyTxn.getCollectionMembershipRecords();
806
858
  const removals = kyselyTxn.getCollectionRemovals();
859
+ const groupReferences = kyselyTxn.getGroupReferenceRecords();
807
860
  const operations = kyselyTxn.getOperations();
808
861
  if (collections.length > 0) {
809
862
  const collectionRows = collections.map((collectionId) => ({
@@ -843,13 +896,28 @@ var KyselyOperationIndex = class KyselyOperationIndex {
843
896
  joinedOrdinal: BigInt(ordinal),
844
897
  leftOrdinal: null
845
898
  })).execute();
899
+ const references = await trx.selectFrom("group_references").select("groupId").where("documentId", "=", m.documentId).execute();
900
+ for (const { groupId } of references) await this.joinKeepingEarliest(trx, groupId, m.collectionId, BigInt(ordinal));
846
901
  }
847
902
  if (removals.length > 0) for (const r of removals) {
848
903
  const ordinal = operationOrdinals[r.operationIndex];
849
904
  await trx.updateTable("document_collections").set({ leftOrdinal: BigInt(ordinal) }).where("collectionId", "=", r.collectionId).where("documentId", "=", r.documentId).where("leftOrdinal", "is", null).execute();
850
905
  }
906
+ if (groupReferences.length > 0) for (const record of groupReferences) {
907
+ const ordinal = operationOrdinals[record.operationIndex];
908
+ await trx.insertInto("group_references").values(record.groupIds.map((groupId) => ({
909
+ documentId: record.documentId,
910
+ groupId
911
+ }))).onConflict((oc) => oc.doNothing()).execute();
912
+ const rows = await trx.selectFrom("document_collections").select("collectionId").where("documentId", "=", record.documentId).execute();
913
+ for (const groupId of record.groupIds) for (const { collectionId } of rows) await this.joinKeepingEarliest(trx, groupId, collectionId, BigInt(ordinal));
914
+ }
851
915
  return operationOrdinals;
852
916
  }
917
+ async getGroupReferencers(groupId, signal) {
918
+ if (signal?.aborted) throw new Error("Operation aborted");
919
+ return (await this.queryExecutor.selectFrom("group_references").select("documentId").where("groupId", "=", groupId).orderBy("documentId").execute()).map((row) => row.documentId);
920
+ }
853
921
  async find(collectionId, cursor, view, paging, signal) {
854
922
  if (signal?.aborted) throw new Error("Operation aborted");
855
923
  const outerCursor = cursor ?? -1;
@@ -1097,7 +1165,7 @@ function keyframeRevision(keyframe, documentId, scope) {
1097
1165
  }
1098
1166
  function extractModuleVersion(doc) {
1099
1167
  const v = doc.state.document.version;
1100
- return v === 0 ? void 0 : v;
1168
+ return normalizeDocumentModelVersion(v);
1101
1169
  }
1102
1170
  /** The highest revision held, latest push winning a tie. */
1103
1171
  function highestRevision(snapshots) {
@@ -1360,6 +1428,18 @@ var KyselyWriteCache = class KyselyWriteCache {
1360
1428
  document: keyframe.document
1361
1429
  };
1362
1430
  }
1431
+ /**
1432
+ * Rebuilds a scope from a keyframe or from the whole operation history.
1433
+ *
1434
+ * The document scope is always rebuilt first, because it carries the type,
1435
+ * the upgrades and the deletion marker. Its version-changing upgrades are not
1436
+ * applied there though: an upgrade reducer must see the state the requested
1437
+ * scope has reached at that upgrade's boundary, so each one is held back and
1438
+ * applied when the replay below crosses the boundary that
1439
+ * resolveModuleVersionForOp derives from it. Upgrades whose boundary lies past
1440
+ * the last replayed operation are applied at the end. Creation-time 0->N seed
1441
+ * upgrades carry the initial state, so they still apply immediately.
1442
+ */
1363
1443
  async coldMissRebuild(documentId, scope, branch, targetRevision, signal) {
1364
1444
  const effectiveTargetRevision = targetRevision || Number.MAX_SAFE_INTEGER;
1365
1445
  const keyframe = await this.findNearestKeyframe(documentId, scope, branch, effectiveTargetRevision, signal);
@@ -1368,6 +1448,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1368
1448
  let startRevision;
1369
1449
  let documentType;
1370
1450
  const validatedUpgrades = [];
1451
+ const pendingUpgrades = [];
1371
1452
  let lastDocumentScopeOperation;
1372
1453
  if (keyframe) {
1373
1454
  document = keyframe.document;
@@ -1397,9 +1478,17 @@ var KyselyWriteCache = class KyselyWriteCache {
1397
1478
  revision: upgradeAction.input.revision,
1398
1479
  timestampUtcMs: operation.timestampUtcMs
1399
1480
  });
1400
- document = applyUpgradeDocumentAction(document, upgradeAction, upgradePath);
1481
+ pendingUpgrades.push({
1482
+ action: upgradeAction,
1483
+ upgradePath,
1484
+ index: operation.index,
1485
+ subsequentDeletes: []
1486
+ });
1401
1487
  }
1402
- } else if (operation.action.type === "DELETE_DOCUMENT") applyDeleteDocumentAction(document, operation.action);
1488
+ } else if (operation.action.type === "DELETE_DOCUMENT") {
1489
+ applyDeleteDocumentAction(document, operation.action);
1490
+ for (const pending of pendingUpgrades) pending.subsequentDeletes.push(operation.action);
1491
+ }
1403
1492
  }
1404
1493
  } else {
1405
1494
  startRevision = -1;
@@ -1426,8 +1515,8 @@ var KyselyWriteCache = class KyselyWriteCache {
1426
1515
  const upgradeAction = operation.action;
1427
1516
  const fromVersion = upgradeAction.input.fromVersion;
1428
1517
  const toVersion = upgradeAction.input.toVersion;
1429
- let upgradePath;
1430
1518
  if (fromVersion > 0 && fromVersion < toVersion) {
1519
+ let upgradePath;
1431
1520
  try {
1432
1521
  upgradePath = this.registry.computeUpgradePath(documentType, fromVersion, toVersion);
1433
1522
  } catch (err) {
@@ -1440,11 +1529,18 @@ var KyselyWriteCache = class KyselyWriteCache {
1440
1529
  revision: upgradeAction.input.revision,
1441
1530
  timestampUtcMs: operation.timestampUtcMs
1442
1531
  });
1443
- }
1444
- document = applyUpgradeDocumentAction(document, upgradeAction, upgradePath);
1445
- docModule = this.registry.getModule(documentType, extractModuleVersion(document));
1446
- } else if (operation.action.type === "DELETE_DOCUMENT") applyDeleteDocumentAction(document, operation.action);
1447
- else {
1532
+ pendingUpgrades.push({
1533
+ action: upgradeAction,
1534
+ upgradePath,
1535
+ index: operation.index,
1536
+ subsequentDeletes: []
1537
+ });
1538
+ } else document = applyUpgradeDocumentAction(document, upgradeAction, void 0);
1539
+ docModule = this.registry.getModule(documentType, normalizeDocumentModelVersion(toVersion));
1540
+ } else if (operation.action.type === "DELETE_DOCUMENT") {
1541
+ applyDeleteDocumentAction(document, operation.action);
1542
+ for (const pending of pendingUpgrades) pending.subsequentDeletes.push(operation.action);
1543
+ } else {
1448
1544
  const protocolVersion = baseReducerVersion(document.header);
1449
1545
  document = docModule.reducer(document, operation.action, void 0, {
1450
1546
  skip: operation.skip,
@@ -1454,6 +1550,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1454
1550
  }
1455
1551
  }
1456
1552
  if (scope === "document") {
1553
+ document = this.applyPendingUpgrades(document, pendingUpgrades, Number.MAX_SAFE_INTEGER);
1457
1554
  const last = lastDocumentScopeOperation ?? await this.operationAt(documentId, "document", branch, startRevision, signal);
1458
1555
  document.operations = {
1459
1556
  ...document.operations,
@@ -1478,6 +1575,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1478
1575
  }
1479
1576
  return mod;
1480
1577
  };
1578
+ const finalVersion = validatedUpgrades.at(-1)?.toVersion ?? extractModuleVersion(document);
1481
1579
  let cursor = void 0;
1482
1580
  const pageSize = 100;
1483
1581
  let hasMorePages;
@@ -1491,7 +1589,8 @@ var KyselyWriteCache = class KyselyWriteCache {
1491
1589
  const result = await this.operationStore.getSince(documentId, scope, branch, startRevision, void 0, paging, signal);
1492
1590
  for (const operation of result.results) {
1493
1591
  if (targetRevision !== void 0 && operation.index > targetRevision) break;
1494
- const moduleVersion = this.resolveModuleVersionForOp(operation.index, operation.timestampUtcMs, scope, validatedUpgrades, extractModuleVersion(document));
1592
+ const moduleVersion = this.resolveModuleVersionForOp(operation.index, operation.timestampUtcMs, scope, validatedUpgrades, finalVersion);
1593
+ document = this.applyPendingUpgrades(document, pendingUpgrades, moduleVersion ?? Number.MAX_SAFE_INTEGER);
1495
1594
  if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
1496
1595
  else {
1497
1596
  const protocolVersion = baseReducerVersion(document.header);
@@ -1508,7 +1607,62 @@ var KyselyWriteCache = class KyselyWriteCache {
1508
1607
  throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
1509
1608
  }
1510
1609
  } while (hasMorePages);
1511
- return this.stampRevisions(document, documentId, scope, branch, targetRevision, signal);
1610
+ document = this.applyTailPendingUpgrades(document, pendingUpgrades, scope, targetRevision);
1611
+ document = await this.stampRevisions(document, documentId, scope, branch, targetRevision, signal);
1612
+ if (pendingUpgrades.length > 0) {
1613
+ const firstHeldBack = pendingUpgrades[0];
1614
+ const stamped = document.header.revision["document"] ?? 0;
1615
+ document.header.revision = {
1616
+ ...document.header.revision,
1617
+ document: Math.min(stamped, firstHeldBack.index)
1618
+ };
1619
+ }
1620
+ return document;
1621
+ }
1622
+ /**
1623
+ * Applies and removes every held-back upgrade whose target version is at or
1624
+ * below `throughVersion`, in the order the document scope recorded them.
1625
+ */
1626
+ applyPendingUpgrades(document, pendingUpgrades, throughVersion) {
1627
+ while (pendingUpgrades.length > 0) {
1628
+ const pending = pendingUpgrades[0];
1629
+ if (throughVersion < pending.action.input.toVersion) break;
1630
+ pendingUpgrades.shift();
1631
+ document = this.applyPendingUpgrade(document, pending);
1632
+ }
1633
+ return document;
1634
+ }
1635
+ /**
1636
+ * Applies the remaining held-back upgrades after the requested scope's
1637
+ * replay has finished. A head read applies them all. A positional read
1638
+ * applies only those whose boundary for this scope lies at or before the
1639
+ * target position: applying a later one would label migrated state with a
1640
+ * pre-upgrade revision, and a keyframe stored from that poisons every
1641
+ * rebuild that resumes from it. Boundaries come from the upgrade's revision
1642
+ * snapshot; an upgrade without one records no position for this scope, and
1643
+ * the replay loop not having crossed it already places it past the target.
1644
+ */
1645
+ applyTailPendingUpgrades(document, pendingUpgrades, scope, targetRevision) {
1646
+ while (pendingUpgrades.length > 0) {
1647
+ const pending = pendingUpgrades[0];
1648
+ if (targetRevision !== void 0) {
1649
+ const snapshot = pending.action.input.revision;
1650
+ if (snapshot === void 0) break;
1651
+ if ((snapshot[scope] ?? 0) > targetRevision) break;
1652
+ }
1653
+ pendingUpgrades.shift();
1654
+ document = this.applyPendingUpgrade(document, pending);
1655
+ }
1656
+ return document;
1657
+ }
1658
+ /**
1659
+ * Applies one held-back upgrade, then re-applies the deletes the document
1660
+ * scope recorded after it so the hold-back cannot invert their order.
1661
+ */
1662
+ applyPendingUpgrade(document, pending) {
1663
+ document = applyUpgradeDocumentAction(document, pending.action, pending.upgradePath);
1664
+ for (const deleteAction of pending.subsequentDeletes) document = applyDeleteDocumentAction(document, deleteAction);
1665
+ return document;
1512
1666
  }
1513
1667
  /**
1514
1668
  * Copies the current document revisions onto the document. Overwrites the
@@ -1660,7 +1814,9 @@ var EventBus = class {
1660
1814
  */
1661
1815
  const FLAG_PREREQUISITES = {
1662
1816
  documentDecisions: [],
1663
- authEnforcement: ["documentDecisions"]
1817
+ authEnforcement: ["documentDecisions"],
1818
+ authGroups: ["authEnforcement"],
1819
+ authConditions: ["authGroups"]
1664
1820
  };
1665
1821
  /**
1666
1822
  * Throws when the flags ask for enforcement the reactor cannot deliver. Either
@@ -1783,49 +1939,159 @@ function refusalReason(refusal) {
1783
1939
  case "no-applicable-grant": return AUTH_NO_GRANT_REASON;
1784
1940
  }
1785
1941
  }
1942
+ function decideAuthModel(model, subject, request, groups, conditions) {
1943
+ if (request.verb === "execute" && model.document.isDeleted) return {
1944
+ decision: "deny",
1945
+ reason: DOCUMENT_DELETED_REASON
1946
+ };
1947
+ const evaluation = evaluate(model.auth, subject, request, groups, conditions);
1948
+ if (evaluation.decision === "allow") return { decision: "allow" };
1949
+ return {
1950
+ decision: "deny",
1951
+ reason: refusalReason(evaluation.refusal)
1952
+ };
1953
+ }
1954
+ function documentProjection(target) {
1955
+ return {
1956
+ decidingActions: ["DELETE_DOCUMENT"],
1957
+ apply: (document, operation) => operation.action.type === "DELETE_DOCUMENT" ? applyDeleteDocumentAction({
1958
+ ...document,
1959
+ state: { ...document.state }
1960
+ }, operation.action) : document,
1961
+ query: {
1962
+ documentId: target.documentId,
1963
+ branch: target.branch,
1964
+ scope: "document"
1965
+ }
1966
+ };
1967
+ }
1968
+ function authProjection(target) {
1969
+ return {
1970
+ decidingActions: [...AUTH_ACTION_TYPES],
1971
+ apply: (document, operation) => applyAuthAction(document, operation.action),
1972
+ query: {
1973
+ documentId: target.documentId,
1974
+ branch: target.branch,
1975
+ scope: "auth"
1976
+ }
1977
+ };
1978
+ }
1786
1979
  /** This decision model uses both the document and the auth streams. */
1787
1980
  function authDecisionModel(target) {
1788
1981
  return {
1789
1982
  projections: {
1790
- document: {
1791
- decidingActions: ["DELETE_DOCUMENT"],
1792
- apply: (document, operation) => operation.action.type === "DELETE_DOCUMENT" ? applyDeleteDocumentAction({
1793
- ...document,
1794
- state: { ...document.state }
1795
- }, operation.action) : document,
1796
- query: {
1797
- documentId: target.documentId,
1798
- branch: target.branch,
1799
- scope: "document"
1800
- }
1801
- },
1802
- auth: {
1803
- decidingActions: [...AUTH_ACTION_TYPES],
1804
- apply: (document, operation) => applyAuthAction(document, operation.action),
1805
- query: {
1806
- documentId: target.documentId,
1807
- branch: target.branch,
1808
- scope: "auth"
1809
- }
1810
- }
1983
+ document: documentProjection(target),
1984
+ auth: authProjection(target)
1811
1985
  },
1812
1986
  evaluatesScope() {
1813
1987
  return true;
1814
1988
  },
1815
1989
  decide(model, subject, request) {
1816
- if (request.verb === "execute" && model.document.isDeleted) return {
1817
- decision: "deny",
1818
- reason: DOCUMENT_DELETED_REASON
1819
- };
1820
- const evaluation = evaluate(model.auth, subject, request);
1821
- if (evaluation.decision === "allow") return { decision: "allow" };
1822
- return {
1823
- decision: "deny",
1824
- reason: refusalReason(evaluation.refusal)
1825
- };
1990
+ return decideAuthModel(model, subject, request);
1826
1991
  }
1827
1992
  };
1828
1993
  }
1994
+ /**
1995
+ * Folds one group-stream operation with the registered group model's reducer.
1996
+ * A reactor without the module registered folds nothing, so the member list
1997
+ * stays as read and a missing reducer never widens access.
1998
+ */
1999
+ function applyGroupOperation(registry, document, operation) {
2000
+ let reducer;
2001
+ try {
2002
+ reducer = registry.getModule(groupDocumentType).reducer;
2003
+ } catch {
2004
+ return document;
2005
+ }
2006
+ return reducer(document, operation.action);
2007
+ }
2008
+ /**
2009
+ * Folds one evaluated-scope operation with the reducer registered for the
2010
+ * document's own type, at the document's stamped version. A reactor without
2011
+ * that module folds nothing, so conditions read the base state and an
2012
+ * unresolvable reducer never widens access.
2013
+ */
2014
+ function applyModelOperation(registry, document, operation) {
2015
+ let reducer;
2016
+ try {
2017
+ const version = normalizeDocumentModelVersion(document.state.document?.version);
2018
+ reducer = registry.getModule(document.header.documentType, version).reducer;
2019
+ } catch {
2020
+ return document;
2021
+ }
2022
+ return reducer(document, operation.action);
2023
+ }
2024
+ /**
2025
+ * The auth model extended with a derived groups projection: the streams it
2026
+ * reads are the group documents the folded grant list names, so adding a
2027
+ * grant that names a new group pulls that group's stream into the read-set.
2028
+ * Group queries pin the main branch, because a group's member list lives on
2029
+ * its main branch no matter which branch the referencing document is on.
2030
+ */
2031
+ function groupsProjection(registry) {
2032
+ return {
2033
+ decidingActions: [...groupMembershipActionTypes],
2034
+ apply: (document, operation) => applyGroupOperation(registry, document, operation),
2035
+ query: (model) => referencedGroupIds(model.auth?.grants ?? []).map((id) => ({
2036
+ documentId: id,
2037
+ branch: "main",
2038
+ scope: "global"
2039
+ })),
2040
+ queryOverHistory: (reads) => {
2041
+ const ids = [];
2042
+ for (const read of reads) {
2043
+ if (read.name !== "auth") continue;
2044
+ for (const operation of read.operations) for (const id of mentionedGroupIds(operation.action)) if (!ids.includes(id)) ids.push(id);
2045
+ }
2046
+ return ids.map((id) => ({
2047
+ documentId: id,
2048
+ branch: "main",
2049
+ scope: "global"
2050
+ }));
2051
+ }
2052
+ };
2053
+ }
2054
+ function authGroupsDecisionModel(registry) {
2055
+ return (target) => ({
2056
+ projections: {
2057
+ document: documentProjection(target),
2058
+ auth: authProjection(target),
2059
+ groups: groupsProjection(registry)
2060
+ },
2061
+ evaluatesScope() {
2062
+ return true;
2063
+ },
2064
+ decide(model, subject, request) {
2065
+ return decideAuthModel(model, subject, request, model.groups);
2066
+ }
2067
+ });
2068
+ }
2069
+ /**
2070
+ * The groups model with conditions live: decide hands the executing scope's
2071
+ * state and the action input through to the evaluator, so `where` clauses
2072
+ * and { match } principals apply. The model folds the evaluated scope during
2073
+ * a positional walk, so a condition reads the state as it stood at each
2074
+ * operation's position.
2075
+ */
2076
+ function authConditionsDecisionModel(registry) {
2077
+ return (target) => ({
2078
+ projections: {
2079
+ document: documentProjection(target),
2080
+ auth: authProjection(target),
2081
+ groups: groupsProjection(registry)
2082
+ },
2083
+ foldEvaluatedScope: (document, operation) => applyModelOperation(registry, document, operation),
2084
+ evaluatesScope() {
2085
+ return true;
2086
+ },
2087
+ decide(model, subject, request, ctx) {
2088
+ return decideAuthModel(model, subject, request, model.groups, {
2089
+ scopeState: ctx.scopeState,
2090
+ actionInput: ctx.actionInput
2091
+ });
2092
+ }
2093
+ });
2094
+ }
1829
2095
  //#endregion
1830
2096
  //#region src/decision/build-decision-model.ts
1831
2097
  /**
@@ -1849,7 +2115,16 @@ async function buildDecisionModel(cache, definition, target, signal) {
1849
2115
  const queries = projection.query(staticModel);
1850
2116
  const value = {};
1851
2117
  for (const query of queries) {
1852
- const read = await readStream(cache, query, reads, signal);
2118
+ let read;
2119
+ try {
2120
+ read = await readStream(cache, query, reads, signal);
2121
+ } catch (error) {
2122
+ if (error instanceof DocumentNotFoundError) {
2123
+ recordEmptyStream(query, reads);
2124
+ continue;
2125
+ }
2126
+ throw error;
2127
+ }
1853
2128
  value[query.documentId] = read.state;
1854
2129
  }
1855
2130
  model[key] = value;
@@ -1859,6 +2134,20 @@ async function buildDecisionModel(cache, definition, target, signal) {
1859
2134
  appendCondition: { streams: [...reads.values()].map((read) => read.stream) }
1860
2135
  };
1861
2136
  }
2137
+ /** Guards a stream that holds nothing yet: any operation appearing is growth. */
2138
+ function recordEmptyStream(query, reads) {
2139
+ const key = `${query.documentId}:${query.scope}:${query.branch}`;
2140
+ if (reads.has(key)) return;
2141
+ reads.set(key, {
2142
+ state: void 0,
2143
+ stream: {
2144
+ documentId: query.documentId,
2145
+ scope: query.scope,
2146
+ branch: query.branch,
2147
+ revision: -1
2148
+ }
2149
+ });
2150
+ }
1862
2151
  async function readStream(cache, query, reads, signal) {
1863
2152
  const key = `${query.documentId}:${query.scope}:${query.branch}`;
1864
2153
  const existing = reads.get(key);
@@ -1890,6 +2179,24 @@ function observedRevision(document, scope) {
1890
2179
  return document.header.revision[scope] - 1;
1891
2180
  }
1892
2181
  /**
2182
+ * The projections whose queries depend on folded state. A positional walk
2183
+ * resolves their streams through `queryOverHistory`; a projection without one
2184
+ * contributes no streams to a walk.
2185
+ */
2186
+ function derivedReadSet(definition) {
2187
+ const projections = [];
2188
+ for (const [name, projection] of Object.entries(definition.projections)) {
2189
+ if (typeof projection.query !== "function") continue;
2190
+ projections.push({
2191
+ name,
2192
+ decidingActions: projection.decidingActions,
2193
+ apply: projection.apply,
2194
+ queryOverHistory: projection.queryOverHistory
2195
+ });
2196
+ }
2197
+ return projections;
2198
+ }
2199
+ /**
1893
2200
  * The streams a model reads whose queries are known before it is built. A
1894
2201
  * derived query needs the statically-queried projections first, so it is not
1895
2202
  * included here.
@@ -1943,11 +2250,21 @@ function documentDecisionModel(target) {
1943
2250
  /**
1944
2251
  * Builds the model at the stream heads and decides one request against it. The
1945
2252
  * append condition it returns is the read-set the store enforces at write time.
2253
+ *
2254
+ * With `conditions` supplied, the executing scope's state is read at the head
2255
+ * for `doc.<scope>.*` paths. That read carries no append-condition entry of
2256
+ * its own: the written stream's expected-revision check already refuses a
2257
+ * write whose scope grew between the read and the append.
1946
2258
  */
1947
- async function decideAtHead(model, cache, target, subject, request, signal) {
2259
+ async function decideAtHead(model, cache, target, subject, request, signal, conditions) {
1948
2260
  const built = await buildDecisionModel(cache, model, target, signal);
2261
+ let scopeState;
2262
+ if (conditions !== void 0) scopeState = (await cache.getState(target.documentId, request.scope, target.branch, void 0, signal)).state[request.scope];
1949
2263
  return {
1950
- evaluation: model(target).decide(built.model, subject, request, { scopeState: void 0 }),
2264
+ evaluation: model(target).decide(built.model, subject, request, {
2265
+ scopeState,
2266
+ actionInput: conditions?.actionInput
2267
+ }),
1951
2268
  appendCondition: built.appendCondition,
1952
2269
  documentVersion: built.model.document.version,
1953
2270
  deletedAtUtcIso: built.model.document.deletedAtUtcIso ?? null
@@ -1955,9 +2272,13 @@ async function decideAtHead(model, cache, target, subject, request, signal) {
1955
2272
  }
1956
2273
  /**
1957
2274
  * The model this reactor enforces. With `authEnforcement` off the auth scope is
1958
- * absent from every append condition and no load walks it.
2275
+ * absent from every append condition and no load walks it; with `authGroups`
2276
+ * on, the group documents the grant list names join the read-set and the
2277
+ * registry supplies the reducer that folds them.
1959
2278
  */
1960
- function selectDecisionModel(flags) {
2279
+ function selectDecisionModel(flags, registry) {
2280
+ if (flags.authConditions) return authConditionsDecisionModel(registry);
2281
+ if (flags.authGroups) return authGroupsDecisionModel(registry);
1961
2282
  return flags.authEnforcement ? authDecisionModel : documentDecisionModel;
1962
2283
  }
1963
2284
  //#endregion
@@ -2087,16 +2408,24 @@ function subjectOf(operation) {
2087
2408
  };
2088
2409
  }
2089
2410
  /**
2090
- * The model as the walk reached this operation: each projection's value is its
2091
- * own scope's state, taken from the stream that projection reads.
2411
+ * The model as the walk reached this operation: each static projection's value
2412
+ * is its own scope's state, and each derived projection's value maps document
2413
+ * id to that document's state, holding only the streams this replica walked. A
2414
+ * derived stream it does not hold stays out of the map, which fails closed.
2092
2415
  */
2093
- function modelAt(readSet, states) {
2416
+ function modelAt(readSet, derivedNames, derived, states) {
2094
2417
  const model = {};
2095
2418
  for (const stream of readSet) {
2096
2419
  const document = states.get(streamKey(stream.query));
2097
2420
  if (document === void 0) throw new Error(`No state walked for projection ${stream.name}`);
2098
2421
  model[stream.name] = document.state[stream.query.scope];
2099
2422
  }
2423
+ for (const name of derivedNames) model[name] = {};
2424
+ for (const entry of derived) {
2425
+ const map = model[entry.name];
2426
+ const document = states.get(streamKey(entry.query));
2427
+ if (document !== void 0) map[entry.query.documentId] = document.state[entry.query.scope];
2428
+ }
2100
2429
  return model;
2101
2430
  }
2102
2431
  /**
@@ -2112,6 +2441,7 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
2112
2441
  const { writeCache, operationStore } = stores;
2113
2442
  const definition = model(target);
2114
2443
  const readSet = staticReadSet(definition);
2444
+ const derivedSet = derivedReadSet(definition);
2115
2445
  if (!definition.evaluatesScope(scope)) return operations.map(() => void 0);
2116
2446
  const evaluating = new Set(operations.map((operation) => operation.id));
2117
2447
  const readStreams = await Promise.all(readSet.map(async (stream) => ({
@@ -2123,24 +2453,76 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
2123
2453
  if (readStreams.length === 0) throw new Error(`Decision model for ${target.documentId} reads no stream whose query is known before it is built`);
2124
2454
  const writtenProjection = readSet.find((stream) => stream.query.scope === scope);
2125
2455
  const walked = [];
2456
+ const histories = [];
2126
2457
  for (const read of readStreams) {
2127
- const isWritten = read.stream === writtenProjection;
2458
+ const streamOperations = read.stream === writtenProjection ? [...read.operations, ...operations] : read.operations;
2128
2459
  const before = await writeCache.getState(read.stream.query.documentId, read.stream.query.scope, read.stream.query.branch, -1, signal);
2129
2460
  walked.push({
2130
2461
  streamKey: streamKey(read.stream.query),
2131
2462
  scope: read.stream.query.scope,
2132
2463
  document: before,
2133
- operations: isWritten ? [...read.operations, ...operations] : read.operations,
2464
+ operations: streamOperations,
2134
2465
  apply: read.stream.apply
2135
2466
  });
2467
+ histories.push({
2468
+ name: read.stream.name,
2469
+ operations: streamOperations
2470
+ });
2136
2471
  }
2137
- if (writtenProjection === void 0) walked.push({
2472
+ let evaluatedStateKey;
2473
+ if (writtenProjection !== void 0) evaluatedStateKey = streamKey(writtenProjection.query);
2474
+ else if (definition.foldEvaluatedScope !== void 0) {
2475
+ const query = {
2476
+ documentId: target.documentId,
2477
+ scope,
2478
+ branch: target.branch
2479
+ };
2480
+ const storedOperations = (await operationStore.getSince(query.documentId, query.scope, query.branch, -1, void 0, void 0, signal)).results.filter((operation) => !evaluating.has(operation.id));
2481
+ const before = await writeCache.getState(query.documentId, query.scope, query.branch, -1, signal);
2482
+ evaluatedStateKey = streamKey(query);
2483
+ walked.push({
2484
+ streamKey: evaluatedStateKey,
2485
+ scope,
2486
+ document: before,
2487
+ operations: [...storedOperations, ...operations],
2488
+ apply: definition.foldEvaluatedScope
2489
+ });
2490
+ } else walked.push({
2138
2491
  streamKey: EVALUATED_ONLY,
2139
2492
  scope,
2140
2493
  document: walked[0].document,
2141
2494
  operations,
2142
2495
  apply: (document) => document
2143
2496
  });
2497
+ const derivedEntries = [];
2498
+ const walkedKeys = new Set(walked.map((stream) => stream.streamKey));
2499
+ for (const projection of derivedSet) {
2500
+ const queries = projection.queryOverHistory?.(histories) ?? [];
2501
+ for (const query of queries) {
2502
+ const key = streamKey(query);
2503
+ if (walkedKeys.has(key)) continue;
2504
+ let before;
2505
+ try {
2506
+ before = await writeCache.getState(query.documentId, query.scope, query.branch, -1, signal);
2507
+ } catch (error) {
2508
+ if (error instanceof DocumentNotFoundError) continue;
2509
+ throw error;
2510
+ }
2511
+ const streamOperations = (await operationStore.getSince(query.documentId, query.scope, query.branch, -1, { actionTypes: projection.decidingActions }, void 0, signal)).results.filter((operation) => !evaluating.has(operation.id));
2512
+ walkedKeys.add(key);
2513
+ walked.push({
2514
+ streamKey: key,
2515
+ scope: query.scope,
2516
+ document: before,
2517
+ operations: streamOperations,
2518
+ apply: projection.apply
2519
+ });
2520
+ derivedEntries.push({
2521
+ name: projection.name,
2522
+ query
2523
+ });
2524
+ }
2525
+ }
2144
2526
  const reasons = /* @__PURE__ */ new Map();
2145
2527
  const walk = walkByPosition(walked);
2146
2528
  let step = walk.next(false);
@@ -2150,11 +2532,16 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
2150
2532
  step = walk.next(false);
2151
2533
  continue;
2152
2534
  }
2153
- const evaluation = definition.decide(modelAt(readSet, position.states), subjectOf(position.operation), {
2535
+ const evaluatedDocument = evaluatedStateKey === void 0 ? void 0 : position.states.get(evaluatedStateKey);
2536
+ const scopeState = evaluatedDocument === void 0 ? void 0 : evaluatedDocument.state[scope];
2537
+ const evaluation = definition.decide(modelAt(readSet, derivedSet.map((projection) => projection.name), derivedEntries, position.states), subjectOf(position.operation), {
2154
2538
  verb: "execute",
2155
2539
  scope: position.operation.action.scope,
2156
2540
  operation: position.operation.action.type
2157
- }, { scopeState: void 0 });
2541
+ }, {
2542
+ scopeState,
2543
+ actionInput: position.operation.action.input
2544
+ });
2158
2545
  const denied = evaluation.decision === "deny";
2159
2546
  reasons.set(position.operation.id, denied ? evaluation.reason : void 0);
2160
2547
  step = walk.next(denied);
@@ -2255,7 +2642,7 @@ var DocumentActionHandler = class {
2255
2642
  verb: "execute",
2256
2643
  scope: action.scope,
2257
2644
  operation: action.type
2258
- }, signal);
2645
+ }, signal, this.featureFlags.authConditions ? { actionInput: action.input } : void 0);
2259
2646
  } catch (error) {
2260
2647
  return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
2261
2648
  }
@@ -2442,13 +2829,6 @@ var DocumentActionHandler = class {
2442
2829
  }
2443
2830
  const documentState = document.state.document;
2444
2831
  if (documentState.isDeleted && !this.alreadyEvaluated(executing)) return buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
2445
- const nextIndex = getNextIndexForScope(document, job.scope);
2446
- let upgradePath;
2447
- if (fromVersion > 0 && fromVersion < toVersion) try {
2448
- upgradePath = this.registry.computeUpgradePath(document.header.documentType, fromVersion, toVersion);
2449
- } catch (error) {
2450
- return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
2451
- }
2452
2832
  if (fromVersion === toVersion && fromVersion > 0) return {
2453
2833
  job,
2454
2834
  success: true,
@@ -2456,6 +2836,48 @@ var DocumentActionHandler = class {
2456
2836
  operationsWithContext: [],
2457
2837
  duration: Date.now() - startTime
2458
2838
  };
2839
+ const arrivesDecided = executing.replayingAcceptedHistory || executing.evaluatedByPosition;
2840
+ if (fromVersion > 0 && !arrivesDecided) {
2841
+ const stampedVersion = normalizeDocumentModelVersion(documentState.version);
2842
+ if (fromVersion !== stampedVersion) return buildErrorResult(job, new UpgradePreconditionFailedError(documentId, `fromVersion ${fromVersion} does not match the document's version ${stampedVersion}`), startTime);
2843
+ if (input.revision !== void 0) {
2844
+ let actualRevisions;
2845
+ try {
2846
+ actualRevisions = (await stores.operationStore.getRevisions(documentId, job.branch, signal)).revision;
2847
+ } catch (error) {
2848
+ return buildErrorResult(job, /* @__PURE__ */ new Error(`Failed to fetch revisions for upgrade: ${error instanceof Error ? error.message : String(error)}`), startTime);
2849
+ }
2850
+ const revisionScopes = new Set([...Object.keys(input.revision), ...Object.keys(actualRevisions)]);
2851
+ for (const revisionScope of revisionScopes) {
2852
+ const snapshot = input.revision[revisionScope] ?? 0;
2853
+ const actual = actualRevisions[revisionScope] ?? 0;
2854
+ if (snapshot !== actual) return buildErrorResult(job, new UpgradePreconditionFailedError(documentId, `revision snapshot for scope "${revisionScope}" is ${snapshot} but the document is at ${actual}`), startTime);
2855
+ }
2856
+ }
2857
+ }
2858
+ let upgradePath;
2859
+ if (fromVersion > 0 && fromVersion < toVersion) try {
2860
+ upgradePath = this.registry.computeUpgradePath(document.header.documentType, fromVersion, toVersion);
2861
+ } catch (error) {
2862
+ return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
2863
+ }
2864
+ const otherScopes = Object.keys(document.state).filter((scope) => scope !== job.scope);
2865
+ if (fromVersion > 0) for (const scope of otherScopes) {
2866
+ let scopedDocument;
2867
+ try {
2868
+ scopedDocument = await stores.writeCache.getState(documentId, scope, job.branch, void 0, signal);
2869
+ } catch (error) {
2870
+ return buildErrorResult(job, /* @__PURE__ */ new Error(`Failed to fetch ${scope} scope for upgrade: ${error instanceof Error ? error.message : String(error)}`), startTime);
2871
+ }
2872
+ document = {
2873
+ ...document,
2874
+ state: {
2875
+ ...document.state,
2876
+ [scope]: scopedDocument.state[scope]
2877
+ }
2878
+ };
2879
+ }
2880
+ const nextIndex = getNextIndexForScope(document, job.scope);
2459
2881
  try {
2460
2882
  document = applyUpgradeDocumentAction$1(document, action, upgradePath);
2461
2883
  } catch (error) {
@@ -2470,6 +2892,7 @@ var DocumentActionHandler = class {
2470
2892
  header: document.header,
2471
2893
  ...document.state
2472
2894
  };
2895
+ if (fromVersion > 0) resultingStateObj.__migrated = true;
2473
2896
  const resultingState = JSON.stringify(resultingStateObj);
2474
2897
  const writeResult = await this.writeOperationToStore({
2475
2898
  documentId,
@@ -2485,6 +2908,11 @@ var DocumentActionHandler = class {
2485
2908
  [job.scope]: [...document.operations[job.scope] ?? [], operation]
2486
2909
  };
2487
2910
  stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, SnapshotPosition.Head);
2911
+ for (const scope of otherScopes) executing.postCommitInvalidations.push({
2912
+ documentId,
2913
+ scope,
2914
+ branch: job.branch
2915
+ });
2488
2916
  indexTxn.write([{
2489
2917
  ...operation,
2490
2918
  documentId,
@@ -2696,10 +3124,12 @@ var SimpleJobExecutor = class {
2696
3124
  };
2697
3125
  this.featureFlags = {
2698
3126
  documentDecisions: config.featureFlags?.documentDecisions ?? false,
2699
- authEnforcement: config.featureFlags?.authEnforcement ?? false
3127
+ authEnforcement: config.featureFlags?.authEnforcement ?? false,
3128
+ authGroups: config.featureFlags?.authGroups ?? false,
3129
+ authConditions: config.featureFlags?.authConditions ?? false
2700
3130
  };
2701
3131
  validateFeatureFlags(this.featureFlags, FLAG_PREREQUISITES);
2702
- this.decisionModel = selectDecisionModel(this.featureFlags);
3132
+ this.decisionModel = selectDecisionModel(this.featureFlags, registry);
2703
3133
  this.signatureVerifierModule = new SignatureVerifier(signatureVerifier);
2704
3134
  this.documentActionHandler = new DocumentActionHandler(registry, logger, driveContainerTypes, this.featureFlags, this.decisionModel);
2705
3135
  this.executionScope = executionScope ?? new DefaultExecutionScope(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache);
@@ -2711,6 +3141,7 @@ var SimpleJobExecutor = class {
2711
3141
  async executeJob(job, signal) {
2712
3142
  const startTime = Date.now();
2713
3143
  const touchedCacheEntries = [];
3144
+ const postCommitInvalidations = [];
2714
3145
  let pendingEvent;
2715
3146
  let result;
2716
3147
  try {
@@ -2724,7 +3155,8 @@ var SimpleJobExecutor = class {
2724
3155
  stores,
2725
3156
  signal,
2726
3157
  replayingAcceptedHistory: true,
2727
- evaluatedByPosition: false
3158
+ evaluatedByPosition: false,
3159
+ postCommitInvalidations
2728
3160
  });
2729
3161
  if (loadResult.success && loadResult.operationsWithContext) {
2730
3162
  for (const owc of loadResult.operationsWithContext) touchedCacheEntries.push({
@@ -2744,6 +3176,37 @@ var SimpleJobExecutor = class {
2744
3176
  }
2745
3177
  return loadResult;
2746
3178
  }
3179
+ if (job.kind === "reevaluation") {
3180
+ const reevalResult = await this.executeReevaluationJob({
3181
+ job,
3182
+ startTime,
3183
+ indexTxn,
3184
+ stores,
3185
+ signal,
3186
+ replayingAcceptedHistory: false,
3187
+ evaluatedByPosition: false,
3188
+ postCommitInvalidations
3189
+ });
3190
+ if (reevalResult.success && reevalResult.operationsWithContext) {
3191
+ for (const owc of reevalResult.operationsWithContext) touchedCacheEntries.push({
3192
+ documentId: owc.context.documentId,
3193
+ scope: owc.context.scope,
3194
+ branch: owc.context.branch
3195
+ });
3196
+ const ordinals = await stores.operationIndex.commit(indexTxn, signal);
3197
+ for (let i = 0; i < reevalResult.operationsWithContext.length; i++) reevalResult.operationsWithContext[i].context.ordinal = ordinals[i];
3198
+ if (reevalResult.operationsWithContext.length > 0) {
3199
+ const collectionMemberships = await this.getCollectionMembershipsForOperations(reevalResult.operationsWithContext, stores);
3200
+ pendingEvent = {
3201
+ jobId: job.id,
3202
+ operations: reevalResult.operationsWithContext,
3203
+ jobMeta: job.meta,
3204
+ collectionMemberships
3205
+ };
3206
+ }
3207
+ }
3208
+ return reevalResult;
3209
+ }
2747
3210
  const positioned = await this.positionByTimestamp(job, stores, signal);
2748
3211
  if (positioned.error) return buildErrorResult(job, positioned.error, startTime);
2749
3212
  const executing = {
@@ -2753,7 +3216,8 @@ var SimpleJobExecutor = class {
2753
3216
  stores,
2754
3217
  signal,
2755
3218
  replayingAcceptedHistory: false,
2756
- evaluatedByPosition: positioned.evaluatedByPosition
3219
+ evaluatedByPosition: positioned.evaluatedByPosition,
3220
+ postCommitInvalidations
2757
3221
  };
2758
3222
  const actionResult = await this.processActions(positioned.writes, executing);
2759
3223
  if (!actionResult.success) return {
@@ -2803,6 +3267,7 @@ var SimpleJobExecutor = class {
2803
3267
  }
2804
3268
  throw error;
2805
3269
  }
3270
+ if (result.success) for (const entry of postCommitInvalidations) this.writeCache.invalidate(entry.documentId, entry.scope, entry.branch);
2806
3271
  if (pendingEvent) this.eventBus.emit(ReactorEventTypes.JOB_WRITE_READY, pendingEvent).catch((error) => {
2807
3272
  this.logger.error("Failed to emit JOB_WRITE_READY event: @Event : @Error", pendingEvent, error);
2808
3273
  });
@@ -2880,7 +3345,7 @@ var SimpleJobExecutor = class {
2880
3345
  verb: "execute",
2881
3346
  scope: action.scope,
2882
3347
  operation: action.type
2883
- }, signal);
3348
+ }, signal, this.featureFlags.authConditions ? { actionInput: action.input } : void 0);
2884
3349
  } catch (error) {
2885
3350
  return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
2886
3351
  }
@@ -2918,8 +3383,7 @@ var SimpleJobExecutor = class {
2918
3383
  }
2919
3384
  let module;
2920
3385
  try {
2921
- const moduleVersion = documentVersion === 0 ? void 0 : documentVersion;
2922
- module = this.registry.getModule(document.header.documentType, moduleVersion);
3386
+ module = this.registry.getModule(document.header.documentType, normalizeDocumentModelVersion(documentVersion));
2923
3387
  } catch (error) {
2924
3388
  return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
2925
3389
  }
@@ -3004,6 +3468,7 @@ var SimpleJobExecutor = class {
3004
3468
  scope,
3005
3469
  sourceRemote
3006
3470
  }]);
3471
+ if (scope === "auth") indexTxn.recordGroupReferences(job.documentId, mentionedGroupIds(action));
3007
3472
  return {
3008
3473
  job,
3009
3474
  success: true,
@@ -3200,7 +3665,7 @@ var SimpleJobExecutor = class {
3200
3665
  const revisions = await stores.operationStore.getRevisions(job.documentId, job.branch, signal);
3201
3666
  const latest = Date.parse(revisions.latestTimestamp);
3202
3667
  if (!criteria.operations.some((operation) => Date.parse(operation.timestampUtcMs) < latest)) return;
3203
- return this.reevaluateDocument(executing);
3668
+ return (await this.reevaluateDocument(executing)).error;
3204
3669
  }
3205
3670
  /**
3206
3671
  * Re-evaluates every scope the model evaluates. Where an operation's
@@ -3213,6 +3678,7 @@ var SimpleJobExecutor = class {
3213
3678
  documentId: job.documentId,
3214
3679
  branch: job.branch
3215
3680
  };
3681
+ const reappended = [];
3216
3682
  const revisions = await stores.operationStore.getRevisions(job.documentId, job.branch, signal);
3217
3683
  for (const scope of this.evaluationOrder(target, revisions.revision)) {
3218
3684
  const stored = (await stores.operationStore.getSince(job.documentId, scope, job.branch, -1, void 0, void 0, signal)).results;
@@ -3241,8 +3707,60 @@ var SimpleJobExecutor = class {
3241
3707
  replayingAcceptedHistory: true,
3242
3708
  evaluatedByPosition: true
3243
3709
  });
3244
- if (!result.success) return result.error ?? /* @__PURE__ */ new Error(`Re-evaluation of ${job.documentId} ${scope} failed`);
3710
+ if (!result.success) return {
3711
+ error: result.error ?? /* @__PURE__ */ new Error(`Re-evaluation of ${job.documentId} ${scope} failed`),
3712
+ operationsWithContext: reappended
3713
+ };
3714
+ reappended.push(...result.operationsWithContext);
3715
+ }
3716
+ return { operationsWithContext: reappended };
3717
+ }
3718
+ /**
3719
+ * Re-judges a document's stored operations because a read-set stream in
3720
+ * another document (a group) gained an operation. The trigger timestamp
3721
+ * bounds the work: an operation later than everything this document holds
3722
+ * cannot change any evaluation, so the pass is skipped.
3723
+ */
3724
+ async executeReevaluationJob(executing) {
3725
+ const { job, startTime, stores, signal } = executing;
3726
+ if (!this.featureFlags.documentDecisions) return {
3727
+ job,
3728
+ success: true,
3729
+ operations: [],
3730
+ operationsWithContext: [],
3731
+ duration: Date.now() - startTime
3732
+ };
3733
+ const trigger = job.meta.triggerTimestampUtcMs;
3734
+ if (typeof trigger === "string") {
3735
+ let latestTimestamp;
3736
+ try {
3737
+ latestTimestamp = (await stores.operationStore.getRevisions(job.documentId, job.branch, signal)).latestTimestamp;
3738
+ } catch {
3739
+ return {
3740
+ job,
3741
+ success: true,
3742
+ operations: [],
3743
+ operationsWithContext: [],
3744
+ duration: Date.now() - startTime
3745
+ };
3746
+ }
3747
+ if (Date.parse(trigger) > Date.parse(latestTimestamp)) return {
3748
+ job,
3749
+ success: true,
3750
+ operations: [],
3751
+ operationsWithContext: [],
3752
+ duration: Date.now() - startTime
3753
+ };
3245
3754
  }
3755
+ const outcome = await this.reevaluateDocument(executing);
3756
+ if (outcome.error) return buildErrorResult(job, outcome.error, startTime);
3757
+ return {
3758
+ job,
3759
+ success: true,
3760
+ operations: outcome.operationsWithContext.map((owc) => owc.operation),
3761
+ operationsWithContext: outcome.operationsWithContext,
3762
+ duration: Date.now() - startTime
3763
+ };
3246
3764
  }
3247
3765
  async executeLoadJob(executing) {
3248
3766
  const { job, startTime, indexTxn, stores, signal } = executing;
@@ -3527,7 +4045,7 @@ var DocumentModelRegistry = class {
3527
4045
  }
3528
4046
  computeUpgradePath(documentType, fromVersion, toVersion) {
3529
4047
  if (fromVersion === toVersion) return [];
3530
- if (toVersion < fromVersion) throw new DowngradeNotSupportedError(documentType, fromVersion, toVersion);
4048
+ if (toVersion < fromVersion) throw new DowngradeNotSupportedError$1(documentType, fromVersion, toVersion);
3531
4049
  const manifest = this.getUpgradeManifest(documentType);
3532
4050
  const path = [];
3533
4051
  for (let v = fromVersion + 1; v <= toVersion; v++) {
@@ -4004,8 +4522,8 @@ function createForwardingPoolInstrumentation(name) {
4004
4522
  }
4005
4523
  //#endregion
4006
4524
  //#region src/storage/migrations/001_create_operation_table.ts
4007
- var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$15 });
4008
- async function up$15(db) {
4525
+ var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$16 });
4526
+ async function up$16(db) {
4009
4527
  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", [
4010
4528
  "documentId",
4011
4529
  "scope",
@@ -4030,8 +4548,8 @@ async function up$15(db) {
4030
4548
  }
4031
4549
  //#endregion
4032
4550
  //#region src/storage/migrations/002_create_keyframe_table.ts
4033
- var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$14 });
4034
- async function up$14(db) {
4551
+ var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$15 });
4552
+ async function up$15(db) {
4035
4553
  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", [
4036
4554
  "documentId",
4037
4555
  "scope",
@@ -4047,14 +4565,14 @@ async function up$14(db) {
4047
4565
  }
4048
4566
  //#endregion
4049
4567
  //#region src/storage/migrations/003_create_document_table.ts
4050
- var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$13 });
4051
- async function up$13(db) {
4568
+ var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$14 });
4569
+ async function up$14(db) {
4052
4570
  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();
4053
4571
  }
4054
4572
  //#endregion
4055
4573
  //#region src/storage/migrations/004_create_document_relationship_table.ts
4056
- var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$12 });
4057
- async function up$12(db) {
4574
+ var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$13 });
4575
+ async function up$13(db) {
4058
4576
  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", [
4059
4577
  "sourceId",
4060
4578
  "targetId",
@@ -4066,14 +4584,14 @@ async function up$12(db) {
4066
4584
  }
4067
4585
  //#endregion
4068
4586
  //#region src/storage/migrations/005_create_indexer_state_table.ts
4069
- var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$11 });
4070
- async function up$11(db) {
4587
+ var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$12 });
4588
+ async function up$12(db) {
4071
4589
  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();
4072
4590
  }
4073
4591
  //#endregion
4074
4592
  //#region src/storage/migrations/006_create_document_snapshot_table.ts
4075
- var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$10 });
4076
- async function up$10(db) {
4593
+ var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$11 });
4594
+ async function up$11(db) {
4077
4595
  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", [
4078
4596
  "documentId",
4079
4597
  "scope",
@@ -4094,8 +4612,8 @@ async function up$10(db) {
4094
4612
  }
4095
4613
  //#endregion
4096
4614
  //#region src/storage/migrations/007_create_slug_mapping_table.ts
4097
- var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$9 });
4098
- async function up$9(db) {
4615
+ var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$10 });
4616
+ async function up$10(db) {
4099
4617
  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", [
4100
4618
  "documentId",
4101
4619
  "scope",
@@ -4105,14 +4623,14 @@ async function up$9(db) {
4105
4623
  }
4106
4624
  //#endregion
4107
4625
  //#region src/storage/migrations/008_create_view_state_table.ts
4108
- var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$8 });
4109
- async function up$8(db) {
4626
+ var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$9 });
4627
+ async function up$9(db) {
4110
4628
  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();
4111
4629
  }
4112
4630
  //#endregion
4113
4631
  //#region src/storage/migrations/009_create_operation_index_tables.ts
4114
- var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$7 });
4115
- async function up$7(db) {
4632
+ var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$8 });
4633
+ async function up$8(db) {
4116
4634
  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();
4117
4635
  await db.schema.createIndex("idx_document_collections_collectionId").on("document_collections").column("collectionId").execute();
4118
4636
  await db.schema.createIndex("idx_doc_collections_collection_range").on("document_collections").columns(["collectionId", "joinedOrdinal"]).execute();
@@ -4126,8 +4644,8 @@ async function up$7(db) {
4126
4644
  }
4127
4645
  //#endregion
4128
4646
  //#region src/storage/migrations/010_create_sync_tables.ts
4129
- var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$6 });
4130
- async function up$6(db) {
4647
+ var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$7 });
4648
+ async function up$7(db) {
4131
4649
  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();
4132
4650
  await db.schema.createIndex("idx_sync_remotes_collection").on("sync_remotes").column("collection_id").execute();
4133
4651
  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();
@@ -4135,8 +4653,8 @@ async function up$6(db) {
4135
4653
  }
4136
4654
  //#endregion
4137
4655
  //#region src/storage/migrations/011_add_cursor_type_column.ts
4138
- var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$5 });
4139
- async function up$5(db) {
4656
+ var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$6 });
4657
+ async function up$6(db) {
4140
4658
  await db.deleteFrom("sync_cursors").where("remote_name", "like", "outbox::%").execute();
4141
4659
  await db.deleteFrom("sync_remotes").where("name", "like", "outbox::%").execute();
4142
4660
  await db.schema.dropTable("sync_cursors").execute();
@@ -4145,60 +4663,82 @@ async function up$5(db) {
4145
4663
  }
4146
4664
  //#endregion
4147
4665
  //#region src/storage/migrations/012_add_source_remote_column.ts
4148
- var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$4 });
4149
- async function up$4(db) {
4666
+ var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$5 });
4667
+ async function up$5(db) {
4150
4668
  await db.schema.alterTable("operation_index_operations").addColumn("sourceRemote", "text", (col) => col.notNull().defaultTo("")).execute();
4151
4669
  }
4152
4670
  //#endregion
4153
4671
  //#region src/storage/migrations/013_create_sync_dead_letters_table.ts
4154
- var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$3 });
4155
- async function up$3(db) {
4672
+ var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$4 });
4673
+ async function up$4(db) {
4156
4674
  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();
4157
4675
  await db.schema.createIndex("idx_sync_dead_letters_remote").on("sync_dead_letters").column("remote_name").execute();
4158
4676
  }
4159
4677
  //#endregion
4160
4678
  //#region src/storage/migrations/014_create_processor_cursor_table.ts
4161
- var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$2 });
4162
- async function up$2(db) {
4679
+ var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$3 });
4680
+ async function up$3(db) {
4163
4681
  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();
4164
4682
  }
4165
4683
  //#endregion
4166
4684
  //#region src/storage/migrations/015_add_operation_denied_reason.ts
4167
4685
  var _015_add_operation_denied_reason_exports = /* @__PURE__ */ __exportAll({
4168
- down: () => down$1,
4169
- up: () => up$1
4686
+ down: () => down$2,
4687
+ up: () => up$2
4170
4688
  });
4171
4689
  /**
4172
4690
  * Records why authorization refused an operation. Separate from `error` so a
4173
4691
  * denial is distinguishable from a reducer failure without matching on a
4174
4692
  * message. Null for every operation written before decisions were enforced.
4175
4693
  */
4176
- async function up$1(db) {
4694
+ async function up$2(db) {
4177
4695
  await db.schema.alterTable("Operation").addColumn("deniedReason", "text").execute();
4178
4696
  await db.schema.alterTable("operation_index_operations").addColumn("deniedReason", "text").execute();
4179
4697
  }
4180
- async function down$1(db) {
4698
+ async function down$2(db) {
4181
4699
  await db.schema.alterTable("operation_index_operations").dropColumn("deniedReason").execute();
4182
4700
  await db.schema.alterTable("Operation").dropColumn("deniedReason").execute();
4183
4701
  }
4184
4702
  //#endregion
4185
4703
  //#region src/storage/migrations/016_add_dead_letter_error_type.ts
4186
4704
  var _016_add_dead_letter_error_type_exports = /* @__PURE__ */ __exportAll({
4187
- down: () => down,
4188
- up: () => up
4705
+ down: () => down$1,
4706
+ up: () => up$1
4189
4707
  });
4190
4708
  /**
4191
4709
  * The classification a dead letter falls into, stored because it decides whether
4192
4710
  * the document stays quarantined and the in-memory error is gone after a restart.
4193
4711
  * Defaulted rather than nullable, so a pre-existing row rehydrates.
4194
4712
  */
4195
- async function up(db) {
4713
+ async function up$1(db) {
4196
4714
  await db.schema.alterTable("sync_dead_letters").addColumn("error_type", "text", (col) => col.notNull().defaultTo("UNCLASSIFIED")).execute();
4197
4715
  }
4198
- async function down(db) {
4716
+ async function down$1(db) {
4199
4717
  await db.schema.alterTable("sync_dead_letters").dropColumn("error_type").execute();
4200
4718
  }
4201
4719
  //#endregion
4720
+ //#region src/storage/migrations/017_create_group_references.ts
4721
+ var _017_create_group_references_exports = /* @__PURE__ */ __exportAll({
4722
+ down: () => down,
4723
+ up: () => up
4724
+ });
4725
+ /**
4726
+ * One row per (document, group) reference ever discovered from an auth
4727
+ * operation's input. Rows are never updated or deleted: auth evaluation is
4728
+ * positional, so a grant that named a group at any position keeps that
4729
+ * group's stream in the document's read-set even after a later operation
4730
+ * removes the reference. Read by documentId for the groups a document
4731
+ * requires (sync), and by groupId for the documents a group change affects
4732
+ * (re-evaluation).
4733
+ */
4734
+ async function up(db) {
4735
+ await db.schema.createTable("group_references").addColumn("documentId", "text", (col) => col.notNull()).addColumn("groupId", "text", (col) => col.notNull()).addPrimaryKeyConstraint("group_references_pkey", ["documentId", "groupId"]).execute();
4736
+ await db.schema.createIndex("idx_group_references_groupId").on("group_references").column("groupId").execute();
4737
+ }
4738
+ async function down(db) {
4739
+ await db.schema.dropTable("group_references").execute();
4740
+ }
4741
+ //#endregion
4202
4742
  //#region src/storage/migrations/migrator.ts
4203
4743
  const REACTOR_SCHEMA = "reactor";
4204
4744
  const migrations = {
@@ -4217,7 +4757,8 @@ const migrations = {
4217
4757
  "013_create_sync_dead_letters_table": _013_create_sync_dead_letters_table_exports,
4218
4758
  "014_create_processor_cursor_table": _014_create_processor_cursor_table_exports,
4219
4759
  "015_add_operation_denied_reason": _015_add_operation_denied_reason_exports,
4220
- "016_add_dead_letter_error_type": _016_add_dead_letter_error_type_exports
4760
+ "016_add_dead_letter_error_type": _016_add_dead_letter_error_type_exports,
4761
+ "017_create_group_references": _017_create_group_references_exports
4221
4762
  };
4222
4763
  var ProgrammaticMigrationProvider = class {
4223
4764
  getMigrations() {
@@ -4271,6 +4812,6 @@ async function getMigrationStatus(db, schema = REACTOR_SCHEMA) {
4271
4812
  //#region src/core/drive-container-types.ts
4272
4813
  const DEFAULT_DRIVE_CONTAINER_TYPES = new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
4273
4814
  //#endregion
4274
- export { OptimisticLockError as A, ExcessiveReshuffleError as B, DocumentMetaCache as C, APPEND_CONDITION_FAILED_PREFIX as D, CollectionMembershipCache as E, ModuleNotFoundError as F, __exportAll as G, matchesScope as H, AuthTimestampNotMonotonicError as I, AuthorizationDeniedError as L, DuplicateManifestError as M, DuplicateModuleError as N, AppendConditionFailedError as O, InvalidModuleError as P, DocumentDeletedError as R, KyselyOperationIndex as S, createEmptyConsistencyToken as T, parsePagingOptions as U, InvalidOperationTimestampError as V, throwIfAborted as W, KyselyExecutionScope as _, createForwardingPoolInstrumentation as a, EventBus as b, KyselyKeyframeStore as c, DriveCollectionId as d, decideAtHead as f, authDecisionModel as g, buildDecisionModel as h, runMigrations as i, RevisionMismatchError as j, DuplicateOperationError as k, DocumentModelRegistry as l, documentDecisionModel as m, REACTOR_SCHEMA as n, instrumentPgPool as o, selectDecisionModel as p, getMigrationStatus as r, KyselyOperationStore as s, DEFAULT_DRIVE_CONTAINER_TYPES as t, SimpleJobExecutor as u, FLAG_PREREQUISITES as v, createConsistencyToken as w, KyselyWriteCache as x, validateFeatureFlags as y, DocumentNotFoundError as z };
4815
+ export { OptimisticLockError as A, ExcessiveReshuffleError as B, DocumentMetaCache as C, APPEND_CONDITION_FAILED_PREFIX as D, CollectionMembershipCache as E, ModuleNotFoundError as F, throwIfAborted as G, UpgradePreconditionFailedError as H, AuthTimestampNotMonotonicError as I, __exportAll as K, AuthorizationDeniedError as L, DuplicateManifestError as M, DuplicateModuleError as N, AppendConditionFailedError as O, InvalidModuleError as P, DocumentDeletedError as R, KyselyOperationIndex as S, createEmptyConsistencyToken as T, matchesScope as U, InvalidOperationTimestampError as V, parsePagingOptions as W, KyselyExecutionScope as _, createForwardingPoolInstrumentation as a, EventBus as b, KyselyKeyframeStore as c, DriveCollectionId as d, decideAtHead as f, authDecisionModel as g, buildDecisionModel as h, runMigrations as i, RevisionMismatchError as j, DuplicateOperationError as k, DocumentModelRegistry as l, documentDecisionModel as m, REACTOR_SCHEMA as n, instrumentPgPool as o, selectDecisionModel as p, getMigrationStatus as r, KyselyOperationStore as s, DEFAULT_DRIVE_CONTAINER_TYPES as t, SimpleJobExecutor as u, FLAG_PREREQUISITES as v, createConsistencyToken as w, KyselyWriteCache as x, validateFeatureFlags as y, DocumentNotFoundError as z };
4275
4816
 
4276
- //# sourceMappingURL=drive-container-types-BJCKXJwH.js.map
4817
+ //# sourceMappingURL=drive-container-types-h3M1AK3K.js.map