@substrat-run/kernel 0.139.0 → 0.141.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/dist/admin-log-ddl.d.ts +21 -0
  2. package/dist/admin-log-ddl.d.ts.map +1 -0
  3. package/dist/admin-log-ddl.js +32 -0
  4. package/dist/admin-log-ddl.js.map +1 -0
  5. package/dist/attachment-extractor.d.ts +12 -1
  6. package/dist/attachment-extractor.d.ts.map +1 -1
  7. package/dist/attachment-extractor.js +10 -5
  8. package/dist/attachment-extractor.js.map +1 -1
  9. package/dist/audit-outcome.d.ts +149 -0
  10. package/dist/audit-outcome.d.ts.map +1 -0
  11. package/dist/audit-outcome.js +233 -0
  12. package/dist/audit-outcome.js.map +1 -0
  13. package/dist/capability.d.ts +5 -2
  14. package/dist/capability.d.ts.map +1 -1
  15. package/dist/capability.js +17 -8
  16. package/dist/capability.js.map +1 -1
  17. package/dist/connect-links.d.ts +91 -0
  18. package/dist/connect-links.d.ts.map +1 -0
  19. package/dist/connect-links.js +189 -0
  20. package/dist/connect-links.js.map +1 -0
  21. package/dist/connect-state.d.ts +12 -3
  22. package/dist/connect-state.d.ts.map +1 -1
  23. package/dist/connect-state.js +8 -3
  24. package/dist/connect-state.js.map +1 -1
  25. package/dist/derived-object.d.ts +14 -0
  26. package/dist/derived-object.d.ts.map +1 -0
  27. package/dist/derived-object.js +2 -0
  28. package/dist/derived-object.js.map +1 -0
  29. package/dist/derived-objects.d.ts +108 -0
  30. package/dist/derived-objects.d.ts.map +1 -0
  31. package/dist/derived-objects.js +199 -0
  32. package/dist/derived-objects.js.map +1 -0
  33. package/dist/email-intent.d.ts +56 -0
  34. package/dist/email-intent.d.ts.map +1 -0
  35. package/dist/email-intent.js +71 -0
  36. package/dist/email-intent.js.map +1 -0
  37. package/dist/entity-edges.d.ts +6 -1
  38. package/dist/entity-edges.d.ts.map +1 -1
  39. package/dist/entity-edges.js +13 -0
  40. package/dist/entity-edges.js.map +1 -1
  41. package/dist/entity-grant-shape.d.ts +94 -0
  42. package/dist/entity-grant-shape.d.ts.map +1 -0
  43. package/dist/entity-grant-shape.js +329 -0
  44. package/dist/entity-grant-shape.js.map +1 -0
  45. package/dist/entity-grant.d.ts +45 -0
  46. package/dist/entity-grant.d.ts.map +1 -0
  47. package/dist/entity-grant.js +71 -0
  48. package/dist/entity-grant.js.map +1 -0
  49. package/dist/entity-state-reads.d.ts +4 -7
  50. package/dist/entity-state-reads.d.ts.map +1 -1
  51. package/dist/entity-state-reads.js +13 -57
  52. package/dist/entity-state-reads.js.map +1 -1
  53. package/dist/entity-state.d.ts +37 -10
  54. package/dist/entity-state.d.ts.map +1 -1
  55. package/dist/entity-state.js +80 -69
  56. package/dist/entity-state.js.map +1 -1
  57. package/dist/entity-trash.d.ts +245 -0
  58. package/dist/entity-trash.d.ts.map +1 -0
  59. package/dist/entity-trash.js +380 -0
  60. package/dist/entity-trash.js.map +1 -0
  61. package/dist/grant-scoped-read.d.ts +41 -0
  62. package/dist/grant-scoped-read.d.ts.map +1 -0
  63. package/dist/grant-scoped-read.js +100 -0
  64. package/dist/grant-scoped-read.js.map +1 -0
  65. package/dist/index.d.ts +35 -8
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +22 -5
  68. package/dist/index.js.map +1 -1
  69. package/dist/kernel-outbox.d.ts +14 -0
  70. package/dist/kernel-outbox.d.ts.map +1 -0
  71. package/dist/kernel-outbox.js +37 -0
  72. package/dist/kernel-outbox.js.map +1 -0
  73. package/dist/list-index.d.ts +11 -2
  74. package/dist/list-index.d.ts.map +1 -1
  75. package/dist/list-index.js +29 -21
  76. package/dist/list-index.js.map +1 -1
  77. package/dist/mail-sender.d.ts +78 -0
  78. package/dist/mail-sender.d.ts.map +1 -0
  79. package/dist/mail-sender.js +2 -0
  80. package/dist/mail-sender.js.map +1 -0
  81. package/dist/membership-executor.d.ts +0 -52
  82. package/dist/membership-executor.d.ts.map +1 -1
  83. package/dist/membership-executor.js +19 -59
  84. package/dist/membership-executor.js.map +1 -1
  85. package/dist/migration-digest.d.ts +161 -0
  86. package/dist/migration-digest.d.ts.map +1 -0
  87. package/dist/migration-digest.js +237 -0
  88. package/dist/migration-digest.js.map +1 -0
  89. package/dist/module-erasure.d.ts +156 -0
  90. package/dist/module-erasure.d.ts.map +1 -0
  91. package/dist/module-erasure.js +440 -0
  92. package/dist/module-erasure.js.map +1 -0
  93. package/dist/peer.d.ts +9 -1
  94. package/dist/peer.d.ts.map +1 -1
  95. package/dist/peer.js +29 -0
  96. package/dist/peer.js.map +1 -1
  97. package/dist/permission-checker.d.ts +6 -0
  98. package/dist/permission-checker.d.ts.map +1 -1
  99. package/dist/permission-checker.js.map +1 -1
  100. package/dist/permission-eval.d.ts +10 -1
  101. package/dist/permission-eval.d.ts.map +1 -1
  102. package/dist/permission-eval.js +51 -2
  103. package/dist/permission-eval.js.map +1 -1
  104. package/dist/platform-sweep.d.ts +1 -1
  105. package/dist/platform-sweep.d.ts.map +1 -1
  106. package/dist/platform-sweep.js +5 -0
  107. package/dist/platform-sweep.js.map +1 -1
  108. package/dist/scope-copy-ledger.d.ts +79 -0
  109. package/dist/scope-copy-ledger.d.ts.map +1 -0
  110. package/dist/scope-copy-ledger.js +82 -0
  111. package/dist/scope-copy-ledger.js.map +1 -0
  112. package/dist/scope-host.d.ts +402 -25
  113. package/dist/scope-host.d.ts.map +1 -1
  114. package/dist/scope-host.js +74 -5
  115. package/dist/scope-host.js.map +1 -1
  116. package/dist/search-index.d.ts +8 -0
  117. package/dist/search-index.d.ts.map +1 -1
  118. package/dist/search-index.js +51 -21
  119. package/dist/search-index.js.map +1 -1
  120. package/dist/spine-guard.d.ts +7 -0
  121. package/dist/spine-guard.d.ts.map +1 -1
  122. package/dist/spine-guard.js +54 -6
  123. package/dist/spine-guard.js.map +1 -1
  124. package/dist/spine-restore.d.ts.map +1 -1
  125. package/dist/spine-restore.js +5 -0
  126. package/dist/spine-restore.js.map +1 -1
  127. package/dist/sql-statements.d.ts +71 -0
  128. package/dist/sql-statements.d.ts.map +1 -0
  129. package/dist/sql-statements.js +191 -0
  130. package/dist/sql-statements.js.map +1 -0
  131. package/dist/subject-redaction.d.ts +9 -1
  132. package/dist/subject-redaction.d.ts.map +1 -1
  133. package/dist/subject-redaction.js.map +1 -1
  134. package/dist/system-switch-record.d.ts +3 -0
  135. package/dist/system-switch-record.d.ts.map +1 -1
  136. package/dist/system-switch-record.js +2 -0
  137. package/dist/system-switch-record.js.map +1 -1
  138. package/dist/table-ownership.d.ts +103 -0
  139. package/dist/table-ownership.d.ts.map +1 -0
  140. package/dist/table-ownership.js +285 -0
  141. package/dist/table-ownership.js.map +1 -0
  142. package/dist/timeline.d.ts +4 -4
  143. package/dist/timeline.js +4 -4
  144. package/dist/visible-continuation.d.ts +38 -0
  145. package/dist/visible-continuation.d.ts.map +1 -0
  146. package/dist/visible-continuation.js +162 -0
  147. package/dist/visible-continuation.js.map +1 -0
  148. package/package.json +4 -4
@@ -0,0 +1,94 @@
1
+ import { type EntityGrantShape, type EntityRef, type PrincipalId } from '@substrat-run/contracts';
2
+ import type { SwitchSql } from './system-switch.js';
3
+ /**
4
+ * A declared entity-grant SHAPE, reconciled (#2071).
5
+ *
6
+ * A vertical declares the keys a person is given on their own record (`ENTITY_GRANTS`:
7
+ * meridian's `EMPLOYEE_SELF` on `employee`, todo's owner grant on `owner`). They used to be
8
+ * minted key by key, once, when the person arrived, so a key added to the shape by a later
9
+ * release reached only the people who arrived after it, while `PERMISSIONS.md` said everyone
10
+ * held it.
11
+ *
12
+ * Three pieces, each one statement shared by both adapters:
13
+ *
14
+ * - {@link grantEntityShapeIn}: the grant. The shape's keys plus a MARKER tuple
15
+ * (`ENTITY_SHAPE_MARKER_RELATION`) on the same entity. The marker is what makes a person a
16
+ * holder of the shape, which no set of keys can: someone `ctx.grant`ed one of those keys
17
+ * holds part of the shape and must never be topped up to all of it.
18
+ * - {@link topUpEntityGrantShapes}: one bounded pass of the reconcile, writing each top-up's
19
+ * `entity.grants-topped-up` event beside it. Each live marker whose
20
+ * entity lacks a key of the CURRENT shape gets that key. A key with any row at all is
21
+ * skipped, and that includes a tombstone (K-21): a key someone took back from that person
22
+ * stays taken back. A key merely dropped from the shape is left where it is, because silently
23
+ * revoking authority on a deploy is the riskier mistake.
24
+ * - The retirement (#2082), in the same pass and budget, between the backfill and the top-up: a
25
+ * key the shape declares `retired` is tombstoned for every live marker that still holds it
26
+ * live, with one `entity.grants-retired` event per (person, entity). The tombstones are the
27
+ * progress; a record tuple (`shape:<type>`, `shape-retired:<key>`, `scope:<id>`) ends it once
28
+ * a batch comes back short, so a key granted to a holder again afterwards is left alone. A key
29
+ * back in the shape's `permissions` tombstones that record, so a later release may retire it
30
+ * again. One tuple is one authority: a direct grant of the key on the same entity to the same
31
+ * person is that row, and goes with it.
32
+ * - The backfill, inside the same budget, until it is done once per (scope, entity type): how
33
+ * people granted before markers existed become holders. For own-record holders it reads
34
+ * provenance, never key sets alone.
35
+ * The shape's declared `holder` says whose record each entity is (`'self'`: the entity id is
36
+ * the principal; a table column naming the principal), and a person is marked only on their
37
+ * own record, and only when they hold a row there (live or tombstoned) for some key of the
38
+ * shape — evidence they were given it. Someone `ctx.grant`ed even the whole shape on another
39
+ * person's record is never marked. A shape with no `holder` gets no backfill. Each batch marks
40
+ * at most what the pass's budget allows; the markers are the progress, since a marked pair is
41
+ * no longer a candidate, and a record tuple (`shape:<type>`, `shape-backfilled`, `scope:<id>`)
42
+ * ends it once a batch comes back short.
43
+ * - `holder: 'grantee'` (#2083), for a record that names no principal (a portal customer, a
44
+ * contact): whoever holds a live current or retired key of the shape on such an entity was given it.
45
+ * Each pass records current and retired keys (`shape-grantee-key`), and the explicit tuple
46
+ * writer makes non-shape grants refuse them on that entity type. A tuple from before the
47
+ * first such pass cannot be told apart. A marker already written by a shape grant persists
48
+ * through retirement, so its holder receives the new keys even if their old key was K alone.
49
+ *
50
+ * Only a shape declared `bootstrap: true` is reconciled: one a person is GIVEN on their own
51
+ * record. A sharing shape, which people reach through `ctx.grant` (todo's `list`), is skipped
52
+ * whole, backfill included, so a sharee is never marked a holder.
53
+ */
54
+ /** Holders topped up per pass — one transaction each, so a large scope never holds one long. */
55
+ export declare const SHAPE_TOP_UP_BATCH = 500;
56
+ /** The largest batch a caller may ask for: past this a pass is the long transaction it exists to avoid. */
57
+ export declare const SHAPE_TOP_UP_BATCH_MAX = 5000;
58
+ /** `batch`, or `validation_failed`: a pass size is a positive integer no larger than the max. */
59
+ export declare function shapeTopUpBatch(batch?: number): number;
60
+ /**
61
+ * The shape's grant to one person on one entity: the marker and every key, each an explicit
62
+ * write, so a re-grant brings back what a revoke tombstoned — as `ctx.grant` does.
63
+ * `entityObjectRef` refuses a ref the walk could not read back (#1856). Run it in ONE
64
+ * transaction, so the marker and its keys land together or not at all.
65
+ */
66
+ export declare function grantEntityShapeIn(db: SwitchSql, principal: PrincipalId, entity: EntityRef, permissions: readonly string[]): void;
67
+ /** Where a pass runs and how its events are stamped — the facts only the adapter holds. */
68
+ export interface ShapePass {
69
+ tenantId: string;
70
+ scopeId: string;
71
+ shapes: readonly EntityGrantShape[];
72
+ /** The pass's one instant: marker liveness and every event's `occurredAt`. */
73
+ now: string;
74
+ /** Rows of work at most: a backfill mark or a holder topped up each count one. */
75
+ limit: number;
76
+ /** The adapter's monotonic event-id mint, given the instant in ms. */
77
+ mintEventId: (ms: number) => string;
78
+ /** The deploy writing the events, for the outbox `version` column. */
79
+ version: string | null;
80
+ }
81
+ /**
82
+ * One pass of the reconcile over one scope, at most `limit` rows of work: backfill marks for any
83
+ * shape whose backfill is not done here, then holders whose retired keys are taken back, each
84
+ * with its `entity.grants-retired` event, then holders topped up, each with its
85
+ * `entity.grants-topped-up` event. `done` is false when the budget ran out, and the caller runs
86
+ * another pass. Run it inside ONE transaction, so the keys and their events commit together.
87
+ * Re-running a finished scope writes nothing.
88
+ */
89
+ export declare function topUpEntityGrantShapes(db: SwitchSql, pass: ShapePass): {
90
+ toppedUp: number;
91
+ retired: number;
92
+ done: boolean;
93
+ };
94
+ //# sourceMappingURL=entity-grant-shape.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-grant-shape.d.ts","sourceRoot":"","sources":["../src/entity-grant-shape.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,WAAW,EACjB,MAAM,yBAAyB,CAAC;AAMjC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH,gGAAgG;AAChG,eAAO,MAAM,kBAAkB,MAAM,CAAC;AACtC,2GAA2G;AAC3G,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C,iGAAiG;AACjG,wBAAgB,eAAe,CAAC,KAAK,GAAE,MAA2B,GAAG,MAAM,CAS1E;AAiBD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CASjI;AAED,2FAA2F;AAC3F,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACpC,8EAA8E;IAC9E,GAAG,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,sEAAsE;IACtE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,SAAS,EACb,IAAI,EAAE,SAAS,GACd;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CA+EtD"}
@@ -0,0 +1,329 @@
1
+ import { ENTITY_GRANTS_RETIRED, ENTITY_GRANTS_TOPPED_UP, ENTITY_SHAPE_MARKER_RELATION, domainEvent, entityGrantsRetiredPayload, entityGrantsToppedUpPayload, entityObjectRef, eventId, } from '@substrat-run/contracts';
2
+ import { assertKernelAuthoredType, substratError } from '@substrat-run/contracts';
3
+ import { KERNEL_ACTOR, kernelOutboxInsertSql } from './kernel-outbox.js';
4
+ import { GRANTEE_KEY_RELATION, explicitTupleSql } from './entity-grant.js';
5
+ import { liveTupleSql } from './permission-eval.js';
6
+ import { assertSqlIdentifier } from './sql-identifier.js';
7
+ /**
8
+ * A declared entity-grant SHAPE, reconciled (#2071).
9
+ *
10
+ * A vertical declares the keys a person is given on their own record (`ENTITY_GRANTS`:
11
+ * meridian's `EMPLOYEE_SELF` on `employee`, todo's owner grant on `owner`). They used to be
12
+ * minted key by key, once, when the person arrived, so a key added to the shape by a later
13
+ * release reached only the people who arrived after it, while `PERMISSIONS.md` said everyone
14
+ * held it.
15
+ *
16
+ * Three pieces, each one statement shared by both adapters:
17
+ *
18
+ * - {@link grantEntityShapeIn}: the grant. The shape's keys plus a MARKER tuple
19
+ * (`ENTITY_SHAPE_MARKER_RELATION`) on the same entity. The marker is what makes a person a
20
+ * holder of the shape, which no set of keys can: someone `ctx.grant`ed one of those keys
21
+ * holds part of the shape and must never be topped up to all of it.
22
+ * - {@link topUpEntityGrantShapes}: one bounded pass of the reconcile, writing each top-up's
23
+ * `entity.grants-topped-up` event beside it. Each live marker whose
24
+ * entity lacks a key of the CURRENT shape gets that key. A key with any row at all is
25
+ * skipped, and that includes a tombstone (K-21): a key someone took back from that person
26
+ * stays taken back. A key merely dropped from the shape is left where it is, because silently
27
+ * revoking authority on a deploy is the riskier mistake.
28
+ * - The retirement (#2082), in the same pass and budget, between the backfill and the top-up: a
29
+ * key the shape declares `retired` is tombstoned for every live marker that still holds it
30
+ * live, with one `entity.grants-retired` event per (person, entity). The tombstones are the
31
+ * progress; a record tuple (`shape:<type>`, `shape-retired:<key>`, `scope:<id>`) ends it once
32
+ * a batch comes back short, so a key granted to a holder again afterwards is left alone. A key
33
+ * back in the shape's `permissions` tombstones that record, so a later release may retire it
34
+ * again. One tuple is one authority: a direct grant of the key on the same entity to the same
35
+ * person is that row, and goes with it.
36
+ * - The backfill, inside the same budget, until it is done once per (scope, entity type): how
37
+ * people granted before markers existed become holders. For own-record holders it reads
38
+ * provenance, never key sets alone.
39
+ * The shape's declared `holder` says whose record each entity is (`'self'`: the entity id is
40
+ * the principal; a table column naming the principal), and a person is marked only on their
41
+ * own record, and only when they hold a row there (live or tombstoned) for some key of the
42
+ * shape — evidence they were given it. Someone `ctx.grant`ed even the whole shape on another
43
+ * person's record is never marked. A shape with no `holder` gets no backfill. Each batch marks
44
+ * at most what the pass's budget allows; the markers are the progress, since a marked pair is
45
+ * no longer a candidate, and a record tuple (`shape:<type>`, `shape-backfilled`, `scope:<id>`)
46
+ * ends it once a batch comes back short.
47
+ * - `holder: 'grantee'` (#2083), for a record that names no principal (a portal customer, a
48
+ * contact): whoever holds a live current or retired key of the shape on such an entity was given it.
49
+ * Each pass records current and retired keys (`shape-grantee-key`), and the explicit tuple
50
+ * writer makes non-shape grants refuse them on that entity type. A tuple from before the
51
+ * first such pass cannot be told apart. A marker already written by a shape grant persists
52
+ * through retirement, so its holder receives the new keys even if their old key was K alone.
53
+ *
54
+ * Only a shape declared `bootstrap: true` is reconciled: one a person is GIVEN on their own
55
+ * record. A sharing shape, which people reach through `ctx.grant` (todo's `list`), is skipped
56
+ * whole, backfill included, so a sharee is never marked a holder.
57
+ */
58
+ /** Holders topped up per pass — one transaction each, so a large scope never holds one long. */
59
+ export const SHAPE_TOP_UP_BATCH = 500;
60
+ /** The largest batch a caller may ask for: past this a pass is the long transaction it exists to avoid. */
61
+ export const SHAPE_TOP_UP_BATCH_MAX = 5000;
62
+ /** `batch`, or `validation_failed`: a pass size is a positive integer no larger than the max. */
63
+ export function shapeTopUpBatch(batch = SHAPE_TOP_UP_BATCH) {
64
+ if (!Number.isInteger(batch) || batch < 1 || batch > SHAPE_TOP_UP_BATCH_MAX) {
65
+ throw substratError('validation_failed', `reconcileEntityGrantShapes: batch must be an integer from 1 to ${SHAPE_TOP_UP_BATCH_MAX}, not ${batch}`, { errors: [{ path: 'batch', message: `an integer from 1 to ${SHAPE_TOP_UP_BATCH_MAX}` }] });
66
+ }
67
+ return batch;
68
+ }
69
+ /** The backfill's run-once record: `(shape:<type>, backfilled, scope:<id>)`. */
70
+ const BACKFILLED_RELATION = 'shape-backfilled';
71
+ /** A retirement's run-once record, per key: `(shape:<type>, shape-retired:<key>, scope:<id>)`. */
72
+ const RETIRED_RELATION = 'shape-retired:';
73
+ /** The subject and object of a shape's run-once records on one scope. */
74
+ const recordRefs = (entityType, scopeId) => [`shape:${entityType}`, `scope:${scopeId}`];
75
+ /** The writer of the top-up events: the kernel, as no module or person acted. */
76
+ const PRINCIPAL = 'principal:';
77
+ const keysOf = (permissions) => [...new Set(permissions)].sort();
78
+ /**
79
+ * The shape's grant to one person on one entity: the marker and every key, each an explicit
80
+ * write, so a re-grant brings back what a revoke tombstoned — as `ctx.grant` does.
81
+ * `entityObjectRef` refuses a ref the walk could not read back (#1856). Run it in ONE
82
+ * transaction, so the marker and its keys land together or not at all.
83
+ */
84
+ export function grantEntityShapeIn(db, principal, entity, permissions) {
85
+ const object = entityObjectRef(entity, 'grantEntityShape');
86
+ const subject = `${PRINCIPAL}${principal}`;
87
+ for (const st of [
88
+ explicitTupleSql(subject, ENTITY_SHAPE_MARKER_RELATION, object),
89
+ ...keysOf(permissions).map((p) => explicitTupleSql(subject, `granted:${p}`, object)),
90
+ ]) {
91
+ db.run(st.sql, ...st.params);
92
+ }
93
+ }
94
+ /**
95
+ * One pass of the reconcile over one scope, at most `limit` rows of work: backfill marks for any
96
+ * shape whose backfill is not done here, then holders whose retired keys are taken back, each
97
+ * with its `entity.grants-retired` event, then holders topped up, each with its
98
+ * `entity.grants-topped-up` event. `done` is false when the budget ran out, and the caller runs
99
+ * another pass. Run it inside ONE transaction, so the keys and their events commit together.
100
+ * Re-running a finished scope writes nothing.
101
+ */
102
+ export function topUpEntityGrantShapes(db, pass) {
103
+ // Here as well as at each entry point: a pass with no budget never reports done, so a caller
104
+ // looping until it does would never stop.
105
+ let budget = shapeTopUpBatch(pass.limit);
106
+ recordGranteeKeys(db, pass.shapes);
107
+ let toppedUp = 0;
108
+ let retired = 0;
109
+ for (const shape of pass.shapes) {
110
+ // A sharing shape is never reconciled — not topped up, not backfilled, nothing retired.
111
+ if (!shape.bootstrap)
112
+ continue;
113
+ const keys = keysOf(shape.permissions);
114
+ // A key the shape grants is never taken back, whatever the declaration says.
115
+ const gone = keysOf(shape.retired ?? []).filter((k) => !keys.includes(k));
116
+ if (keys.length === 0 && gone.length === 0)
117
+ continue;
118
+ const prefix = `${shape.entityType}:`;
119
+ const json = JSON.stringify(keys);
120
+ // A live retired key still identifies a legacy holder so this pass can retire it.
121
+ // The grantee query excludes tombstoned tuples.
122
+ budget -= backfill(db, pass, shape, prefix, JSON.stringify([...keys, ...gone]), budget);
123
+ if (budget === 0)
124
+ return { toppedUp, retired, done: false };
125
+ reopenRetirements(db, pass, shape.entityType, keys);
126
+ const took = retire(db, pass, shape.entityType, prefix, gone, budget);
127
+ budget -= took;
128
+ retired += took;
129
+ if (budget === 0)
130
+ return { toppedUp, retired, done: false };
131
+ if (keys.length === 0)
132
+ continue;
133
+ const holders = db.all(`SELECT m.subject, m.object FROM _substrat_tuples m
134
+ WHERE m.relation = ? AND ${liveTupleSql('m')}
135
+ AND substr(m.subject, 1, ${PRINCIPAL.length}) = '${PRINCIPAL}'
136
+ AND substr(m.object, 1, ?) = ?
137
+ AND EXISTS (SELECT 1 FROM json_each(?) k
138
+ WHERE NOT EXISTS (SELECT 1 FROM _substrat_tuples t
139
+ WHERE t.subject = m.subject AND t.object = m.object
140
+ AND t.relation = 'granted:' || k.value))
141
+ ORDER BY m.subject, m.object
142
+ LIMIT ?`, ENTITY_SHAPE_MARKER_RELATION, pass.now, prefix.length, prefix, json, budget);
143
+ for (const h of holders) {
144
+ const added = db.all(`SELECT k.value AS key FROM json_each(?) k
145
+ WHERE NOT EXISTS (SELECT 1 FROM _substrat_tuples t
146
+ WHERE t.subject = ? AND t.object = ? AND t.relation = 'granted:' || k.value)
147
+ ORDER BY k.value`, json, h.subject, h.object).map((m) => m.key);
148
+ for (const key of added) {
149
+ db.run(`INSERT OR IGNORE INTO _substrat_tuples (subject, relation, object) VALUES (?, ?, ?)`, h.subject, `granted:${key}`, h.object);
150
+ }
151
+ const st = kernelOutboxInsertSql(shapeEvent(pass, ENTITY_GRANTS_TOPPED_UP, entityGrantsToppedUpPayload, {
152
+ principal: h.subject.slice(PRINCIPAL.length),
153
+ entity: { entityType: shape.entityType, entityId: h.object.slice(prefix.length) },
154
+ added,
155
+ }), pass.version);
156
+ db.run(st.sql, ...st.params);
157
+ }
158
+ budget -= holders.length;
159
+ toppedUp += holders.length;
160
+ if (budget === 0)
161
+ return { toppedUp, retired, done: false };
162
+ }
163
+ return { toppedUp, retired, done: true };
164
+ }
165
+ /**
166
+ * One bounded batch of a shape's retirement: at most `budget` (person, entity) pairs, each a
167
+ * live marker still holding a retired key live there, whose retired keys are tombstoned with one
168
+ * event. Keys whose retirement already finished on this scope are skipped, and a short batch
169
+ * records the rest finished. Returns how many pairs it took keys from.
170
+ */
171
+ function retire(db, pass, entityType, prefix, gone, budget) {
172
+ if (gone.length === 0)
173
+ return 0;
174
+ const [shapeRef, scopeRef] = recordRefs(entityType, pass.scopeId);
175
+ const finished = new Set(db.all(`SELECT substr(relation, ${RETIRED_RELATION.length + 1}) AS key FROM _substrat_tuples
176
+ WHERE subject = ? AND object = ? AND revoked_at IS NULL
177
+ AND relation IN (SELECT '${RETIRED_RELATION}' || value FROM json_each(?))`, shapeRef, scopeRef, JSON.stringify(gone)).map((r) => r.key));
178
+ const open = gone.filter((k) => !finished.has(k));
179
+ if (open.length === 0)
180
+ return 0;
181
+ const json = JSON.stringify(open);
182
+ const holders = db.all(`SELECT m.subject, m.object FROM _substrat_tuples m
183
+ WHERE m.relation = ? AND ${liveTupleSql('m')}
184
+ AND substr(m.subject, 1, ${PRINCIPAL.length}) = '${PRINCIPAL}'
185
+ AND substr(m.object, 1, ?) = ?
186
+ AND EXISTS (SELECT 1 FROM _substrat_tuples t
187
+ WHERE t.subject = m.subject AND t.object = m.object AND t.revoked_at IS NULL
188
+ AND t.relation IN (SELECT 'granted:' || value FROM json_each(?)))
189
+ ORDER BY m.subject, m.object
190
+ LIMIT ?`, ENTITY_SHAPE_MARKER_RELATION, pass.now, prefix.length, prefix, json, budget);
191
+ for (const h of holders) {
192
+ // Tombstoned and read back in one statement: the keys this holder lost are what it touched.
193
+ const removed = keysOf(db.all(`UPDATE _substrat_tuples SET revoked_at = ?
194
+ WHERE subject = ? AND object = ? AND revoked_at IS NULL
195
+ AND relation IN (SELECT 'granted:' || value FROM json_each(?))
196
+ RETURNING substr(relation, 9) AS key`, pass.now, h.subject, h.object, json).map((r) => r.key));
197
+ const st = kernelOutboxInsertSql(shapeEvent(pass, ENTITY_GRANTS_RETIRED, entityGrantsRetiredPayload, {
198
+ principal: h.subject.slice(PRINCIPAL.length),
199
+ entity: { entityType, entityId: h.object.slice(prefix.length) },
200
+ removed,
201
+ }), pass.version);
202
+ db.run(st.sql, ...st.params);
203
+ }
204
+ if (holders.length < budget) {
205
+ for (const k of open) {
206
+ db.run('INSERT OR REPLACE INTO _substrat_tuples (subject, relation, object) VALUES (?, ?, ?)', shapeRef, `${RETIRED_RELATION}${k}`, scopeRef);
207
+ }
208
+ }
209
+ return holders.length;
210
+ }
211
+ /**
212
+ * A key back in the shape ends its retirement's record (tombstoned, like any tuple), so a later
213
+ * release that drops and retires it again runs the retirement again.
214
+ */
215
+ function reopenRetirements(db, pass, entityType, keys) {
216
+ if (keys.length === 0)
217
+ return;
218
+ db.run(`UPDATE _substrat_tuples SET revoked_at = ?
219
+ WHERE subject = ? AND object = ? AND revoked_at IS NULL
220
+ AND relation IN (SELECT '${RETIRED_RELATION}' || value FROM json_each(?))`, pass.now, ...recordRefs(entityType, pass.scopeId), JSON.stringify(keys));
221
+ }
222
+ /**
223
+ * One bounded batch of the backfill for one shape — at most `budget` marks — and the run-once
224
+ * record when the batch comes back short. Returns how many it marked. See the module comment:
225
+ * provenance only, from the shape's declared `holder`.
226
+ */
227
+ function backfill(db, pass, shape, prefix, json, budget) {
228
+ if (!shape.holder || budget === 0)
229
+ return 0;
230
+ const [shapeRef, scopeRef] = recordRefs(shape.entityType, pass.scopeId);
231
+ const record = [shapeRef, BACKFILLED_RELATION, scopeRef];
232
+ if (db.all('SELECT 1 FROM _substrat_tuples WHERE subject = ? AND relation = ? AND object = ?', ...record).length > 0) {
233
+ return 0;
234
+ }
235
+ // Not yet a holder in any state: a tombstoned marker is a revoke of the holding, kept.
236
+ const unmarked = (subject, object) => `NOT EXISTS (SELECT 1 FROM _substrat_tuples m WHERE m.subject = ${subject} AND m.relation = '${ENTITY_SHAPE_MARKER_RELATION}' AND m.object = ${object})`;
237
+ const heldThere = (subject, object) => `EXISTS (SELECT 1 FROM _substrat_tuples g WHERE g.subject = ${subject} AND g.object = ${object}
238
+ AND g.relation IN (SELECT 'granted:' || value FROM json_each(?)))`;
239
+ let candidates;
240
+ if (shape.holder === 'self') {
241
+ // The entity id IS the principal: `owner:<p>` belongs to `principal:<p>` and nobody else.
242
+ candidates = db.all(`SELECT DISTINCT t.subject, t.object FROM _substrat_tuples t
243
+ WHERE substr(t.object, 1, ?) = ?
244
+ AND t.subject = '${PRINCIPAL}' || substr(t.object, ?)
245
+ AND t.relation IN (SELECT 'granted:' || value FROM json_each(?))
246
+ AND ${unmarked('t.subject', 't.object')}
247
+ ORDER BY t.subject, t.object
248
+ LIMIT ?`, prefix.length, prefix, prefix.length + 1, json, budget);
249
+ }
250
+ else if (shape.holder === 'grantee') {
251
+ // Whoever holds a LIVE key of the shape on an entity of this type. Live, unlike the other two
252
+ // holders: with no record naming the person, a key taken back from them is no evidence they
253
+ // still belong there, and marking them would hand a revoked portal the next key it gains.
254
+ candidates = db.all(`SELECT DISTINCT t.subject, t.object FROM _substrat_tuples t
255
+ WHERE substr(t.object, 1, ?) = ?
256
+ AND substr(t.subject, 1, ${PRINCIPAL.length}) = '${PRINCIPAL}'
257
+ AND t.relation IN (SELECT 'granted:' || value FROM json_each(?))
258
+ AND ${liveTupleSql('t')}
259
+ AND ${unmarked('t.subject', 't.object')}
260
+ ORDER BY t.subject, t.object
261
+ LIMIT ?`, prefix.length, prefix, json, pass.now, budget);
262
+ }
263
+ else {
264
+ const { table, idColumn, principalColumn } = shape.holder;
265
+ const where = `entityGrants holder of '${shape.entityType}'`;
266
+ for (const [kind, name] of [['a table', table], ['a column', idColumn], ['a column', principalColumn]]) {
267
+ assertSqlIdentifier('reconcileEntityGrantShapes', kind, name, where);
268
+ }
269
+ // The vertical's table may not exist yet on this scope (a module not migrated): nothing to
270
+ // mark, and the backfill stays open for the next reconcile rather than being recorded done.
271
+ if (db.all(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?`, table).length === 0)
272
+ return 0;
273
+ const subject = `'${PRINCIPAL}' || e."${principalColumn}"`;
274
+ const object = `? || e."${idColumn}"`;
275
+ candidates = db.all(`SELECT ${subject} AS subject, ${object} AS object FROM "${table}" e
276
+ WHERE e."${principalColumn}" IS NOT NULL
277
+ AND ${heldThere(subject, object)}
278
+ AND ${unmarked(subject, object)}
279
+ ORDER BY 1, 2
280
+ LIMIT ?`, prefix, prefix, json, prefix, budget);
281
+ }
282
+ for (const c of candidates) {
283
+ db.run('INSERT OR IGNORE INTO _substrat_tuples (subject, relation, object) VALUES (?, ?, ?)', c.subject, ENTITY_SHAPE_MARKER_RELATION, c.object);
284
+ }
285
+ if (candidates.length < budget) {
286
+ db.run('INSERT OR IGNORE INTO _substrat_tuples (subject, relation, object) VALUES (?, ?, ?)', ...record);
287
+ }
288
+ return candidates.length;
289
+ }
290
+ /**
291
+ * The current and retired keys of every `holder: 'grantee'` bootstrap shape, recorded so the
292
+ * explicit tuple writer can refuse them to every non-shape grant. A reconcile carries the
293
+ * reached version's whole reviewed registry,
294
+ * so the records are made to match it exactly: a declaration a later version drops stops
295
+ * refusing. Bounded by the declared keys, so it runs in every pass without a budget.
296
+ */
297
+ function recordGranteeKeys(db, shapes) {
298
+ const rows = shapes
299
+ .filter((s) => s.bootstrap && s.holder === 'grantee')
300
+ .flatMap((s) => keysOf([...s.permissions, ...(s.retired ?? [])]).map((k) => [`shape:${s.entityType}`, `granted:${k}`]));
301
+ db.run(
302
+ // The subject range keeps it on the primary key: `shape:` rows only, never the whole table.
303
+ `DELETE FROM _substrat_tuples WHERE subject >= 'shape:' AND subject < 'shape;' AND relation = ?
304
+ AND NOT EXISTS (SELECT 1 FROM json_each(?) r
305
+ WHERE json_extract(r.value, '$[0]') = _substrat_tuples.subject
306
+ AND json_extract(r.value, '$[1]') = _substrat_tuples.object)`, GRANTEE_KEY_RELATION, JSON.stringify(rows));
307
+ for (const [subject, object] of rows) {
308
+ db.run('INSERT OR IGNORE INTO _substrat_tuples (subject, relation, object) VALUES (?, ?, ?)', subject, GRANTEE_KEY_RELATION, object);
309
+ }
310
+ }
311
+ /**
312
+ * The kernel's event for one (person, entity) a pass changed — a top-up or a retirement — the
313
+ * audit record, on the entity's own history.
314
+ */
315
+ function shapeEvent(pass, type, schema, payload) {
316
+ return domainEvent.parse({
317
+ id: eventId.parse(pass.mintEventId(Date.parse(pass.now))),
318
+ type,
319
+ schemaVersion: 1,
320
+ occurredAt: pass.now,
321
+ tenantId: pass.tenantId,
322
+ scopeId: pass.scopeId,
323
+ actor: KERNEL_ACTOR,
324
+ entity: payload.entity,
325
+ piiClass: 'none',
326
+ payload: schema.parse(payload),
327
+ });
328
+ }
329
+ //# sourceMappingURL=entity-grant-shape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-grant-shape.js","sourceRoot":"","sources":["../src/entity-grant-shape.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,4BAA4B,EAC5B,WAAW,EACX,0BAA0B,EAC1B,2BAA2B,EAC3B,eAAe,EACf,OAAO,GAKR,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,wBAAwB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAClF,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC3E,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAG1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH,gGAAgG;AAChG,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AACtC,2GAA2G;AAC3G,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CAAC;AAE3C,iGAAiG;AACjG,MAAM,UAAU,eAAe,CAAC,KAAK,GAAW,kBAAkB;IAChE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,sBAAsB,EAAE,CAAC;QAC5E,MAAM,aAAa,CACjB,mBAAmB,EACnB,kEAAkE,sBAAsB,SAAS,KAAK,EAAE,EACxG,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,wBAAwB,sBAAsB,EAAE,EAAE,CAAC,EAAE,CAC3F,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,gFAAgF;AAChF,MAAM,mBAAmB,GAAG,kBAAkB,CAAC;AAC/C,kGAAkG;AAClG,MAAM,gBAAgB,GAAG,gBAAgB,CAAC;AAE1C,yEAAyE;AACzE,MAAM,UAAU,GAAG,CAAC,UAAkB,EAAE,OAAe,EAAE,EAAE,CAAC,CAAC,SAAS,UAAU,EAAE,EAAE,SAAS,OAAO,EAAE,CAAU,CAAC;AAEjH,iFAAiF;AAGjF,MAAM,SAAS,GAAG,YAAY,CAAC;AAE/B,MAAM,MAAM,GAAG,CAAC,WAA8B,EAAY,EAAE,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AAE9F;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,EAAa,EAAE,SAAsB,EAAE,MAAiB,EAAE,WAA8B;IACzH,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAC3D,MAAM,OAAO,GAAG,GAAG,SAAS,GAAG,SAAS,EAAE,CAAC;IAC3C,KAAK,MAAM,EAAE,IAAI;QACf,gBAAgB,CAAC,OAAO,EAAE,4BAA4B,EAAE,MAAM,CAAC;QAC/D,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,WAAW,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;KACrF,EAAE,CAAC;QACF,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC;IAC/B,CAAC;AACH,CAAC;AAiBD;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CACpC,EAAa,EACb,IAAe;IAEf,6FAA6F;IAC7F,0CAA0C;IAC1C,IAAI,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzC,iBAAiB,CAAC,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IACnC,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChC,wFAAwF;QACxF,IAAI,CAAC,KAAK,CAAC,SAAS;YAAE,SAAS;QAC/B,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QACvC,6EAA6E;QAC7E,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1E,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACrD,MAAM,MAAM,GAAG,GAAG,KAAK,CAAC,UAAU,GAAG,CAAC;QACtC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAClC,kFAAkF;QAClF,gDAAgD;QAChD,MAAM,IAAI,QAAQ,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QACxF,IAAI,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;QAC5D,iBAAiB,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QACpD,MAAM,IAAI,GAAG,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QACtE,MAAM,IAAI,IAAI,CAAC;QACf,OAAO,IAAI,IAAI,CAAC;QAChB,IAAI,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;QAC5D,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAChC,MAAM,OAAO,GAAG,EAAE,CAAC,GAAG,CACpB;mCAC6B,YAAY,CAAC,GAAG,CAAC;qCACf,SAAS,CAAC,MAAM,QAAQ,SAAS;;;;;;;gBAOtD,EACV,4BAA4B,EAC5B,IAAI,CAAC,GAAG,EACR,MAAM,CAAC,MAAM,EACb,MAAM,EACN,IAAI,EACJ,MAAM,CACkC,CAAC;QAC3C,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,MAAM,KAAK,GACT,EAAE,CAAC,GAAG,CACJ;;;6BAGmB,EACnB,IAAI,EACJ,CAAC,CAAC,OAAO,EACT,CAAC,CAAC,MAAM,CAEX,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YACpB,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;gBACxB,EAAE,CAAC,GAAG,CACJ,qFAAqF,EACrF,CAAC,CAAC,OAAO,EACT,WAAW,GAAG,EAAE,EAChB,CAAC,CAAC,MAAM,CACT,CAAC;YACJ,CAAC;YACD,MAAM,EAAE,GAAG,qBAAqB,CAC9B,UAAU,CAAC,IAAI,EAAE,uBAAuB,EAAE,2BAA2B,EAAE;gBACrE,SAAS,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAgB;gBAC3D,MAAM,EAAE,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE;gBACjF,KAAK;aACN,CAAC,EACF,IAAI,CAAC,OAAO,CACb,CAAC;YACF,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC;QAC/B,CAAC;QACD,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;QACzB,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC;QAC3B,IAAI,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAC9D,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AAC3C,CAAC;AAED;;;;;GAKG;AACH,SAAS,MAAM,CAAC,EAAa,EAAE,IAAe,EAAE,UAAkB,EAAE,MAAc,EAAE,IAAuB,EAAE,MAAc;IACzH,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IAChC,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,UAAU,CAAC,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAClE,MAAM,QAAQ,GAAG,IAAI,GAAG,CAEpB,EAAE,CAAC,GAAG,CACJ,2BAA2B,gBAAgB,CAAC,MAAM,GAAG,CAAC;;uCAEvB,gBAAgB,+BAA+B,EAC9E,QAAQ,EACR,QAAQ,EACR,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAEvB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CACpB,CAAC;IACF,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,OAAO,GAAG,EAAE,CAAC,GAAG,CACpB;iCAC6B,YAAY,CAAC,GAAG,CAAC;mCACf,SAAS,CAAC,MAAM,QAAQ,SAAS;;;;;;cAMtD,EACV,4BAA4B,EAC5B,IAAI,CAAC,GAAG,EACR,MAAM,CAAC,MAAM,EACb,MAAM,EACN,IAAI,EACJ,MAAM,CACkC,CAAC;IAC3C,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,4FAA4F;QAC5F,MAAM,OAAO,GAAG,MAAM,CAElB,EAAE,CAAC,GAAG,CACJ;;;iDAGuC,EACvC,IAAI,CAAC,GAAG,EACR,CAAC,CAAC,OAAO,EACT,CAAC,CAAC,MAAM,EACR,IAAI,CAEP,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CACpB,CAAC;QACF,MAAM,EAAE,GAAG,qBAAqB,CAC9B,UAAU,CAAC,IAAI,EAAE,qBAAqB,EAAE,0BAA0B,EAAE;YAClE,SAAS,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAgB;YAC3D,MAAM,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE;YAC/D,OAAO;SACR,CAAC,EACF,IAAI,CAAC,OAAO,CACb,CAAC;QACF,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC;IAC/B,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC;QAC5B,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;YACrB,EAAE,CAAC,GAAG,CACJ,sFAAsF,EACtF,QAAQ,EACR,GAAG,gBAAgB,GAAG,CAAC,EAAE,EACzB,QAAQ,CACT,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC,MAAM,CAAC;AACxB,CAAC;AAED;;;GAGG;AACH,SAAS,iBAAiB,CAAC,EAAa,EAAE,IAAe,EAAE,UAAkB,EAAE,IAAuB;IACpG,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAC9B,EAAE,CAAC,GAAG,CACJ;;mCAE+B,gBAAgB,+BAA+B,EAC9E,IAAI,CAAC,GAAG,EACR,GAAG,UAAU,CAAC,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,EACvC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CACrB,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAS,QAAQ,CAAC,EAAa,EAAE,IAAe,EAAE,KAAuB,EAAE,MAAc,EAAE,IAAY,EAAE,MAAc;IACrH,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IAC5C,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACxE,MAAM,MAAM,GAAG,CAAC,QAAQ,EAAE,mBAAmB,EAAE,QAAQ,CAAU,CAAC;IAClE,IAAI,EAAE,CAAC,GAAG,CAAC,kFAAkF,EAAE,GAAG,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrH,OAAO,CAAC,CAAC;IACX,CAAC;IACD,uFAAuF;IACvF,MAAM,QAAQ,GAAG,CAAC,OAAe,EAAE,MAAc,EAAE,EAAE,CACnD,kEAAkE,OAAO,sBAAsB,4BAA4B,oBAAoB,MAAM,GAAG,CAAC;IAC3J,MAAM,SAAS,GAAG,CAAC,OAAe,EAAE,MAAc,EAAE,EAAE,CACpD,8DAA8D,OAAO,mBAAmB,MAAM;iFACjB,CAAC;IAChF,IAAI,UAAiD,CAAC;IACtD,IAAI,KAAK,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC5B,0FAA0F;QAC1F,UAAU,GAAG,EAAE,CAAC,GAAG,CACjB;;6BAEuB,SAAS;;gBAEtB,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC;;gBAEjC,EACV,MAAM,CAAC,MAAM,EACb,MAAM,EACN,MAAM,CAAC,MAAM,GAAG,CAAC,EACjB,IAAI,EACJ,MAAM,CACkC,CAAC;IAC7C,CAAC;SAAM,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QACtC,8FAA8F;QAC9F,4FAA4F;QAC5F,0FAA0F;QAC1F,UAAU,GAAG,EAAE,CAAC,GAAG,CACjB;;qCAE+B,SAAS,CAAC,MAAM,QAAQ,SAAS;;gBAEtD,YAAY,CAAC,GAAG,CAAC;gBACjB,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC;;gBAEjC,EACV,MAAM,CAAC,MAAM,EACb,MAAM,EACN,IAAI,EACJ,IAAI,CAAC,GAAG,EACR,MAAM,CACkC,CAAC;IAC7C,CAAC;SAAM,CAAC;QACN,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC;QAC1D,MAAM,KAAK,GAAG,2BAA2B,KAAK,CAAC,UAAU,GAAG,CAAC;QAC7D,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,UAAU,EAAE,QAAQ,CAAC,EAAE,CAAC,UAAU,EAAE,eAAe,CAAC,CAAU,EAAE,CAAC;YAChH,mBAAmB,CAAC,4BAA4B,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QACvE,CAAC;QACD,2FAA2F;QAC3F,4FAA4F;QAC5F,IAAI,EAAE,CAAC,GAAG,CAAC,+DAA+D,EAAE,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,CAAC,CAAC;QAC1G,MAAM,OAAO,GAAG,IAAI,SAAS,WAAW,eAAe,GAAG,CAAC;QAC3D,MAAM,MAAM,GAAG,WAAW,QAAQ,GAAG,CAAC;QACtC,UAAU,GAAG,EAAE,CAAC,GAAG,CACjB,UAAU,OAAO,gBAAgB,MAAM,oBAAoB,KAAK;mBACnD,eAAe;gBAClB,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC;gBAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;;gBAEzB,EACV,MAAM,EACN,MAAM,EACN,IAAI,EACJ,MAAM,EACN,MAAM,CACkC,CAAC;IAC7C,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,EAAE,CAAC,GAAG,CAAC,qFAAqF,EAAE,CAAC,CAAC,OAAO,EAAE,4BAA4B,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;IACnJ,CAAC;IACD,IAAI,UAAU,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC;QAC/B,EAAE,CAAC,GAAG,CAAC,qFAAqF,EAAE,GAAG,MAAM,CAAC,CAAC;IAC3G,CAAC;IACD,OAAO,UAAU,CAAC,MAAM,CAAC;AAC3B,CAAC;AAED;;;;;;GAMG;AACH,SAAS,iBAAiB,CAAC,EAAa,EAAE,MAAmC;IAC3E,MAAM,IAAI,GAAG,MAAM;SAChB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC;SACpD,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAoB,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC5I,EAAE,CAAC,GAAG;IACJ,4FAA4F;IAC5F;;;wFAGoF,EACpF,oBAAoB,EACpB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CACrB,CAAC;IACF,KAAK,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrC,EAAE,CAAC,GAAG,CAAC,qFAAqF,EAAE,OAAO,EAAE,oBAAoB,EAAE,MAAM,CAAC,CAAC;IACvI,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,UAAU,CACjB,IAAe,EACf,IAAmE,EACnE,MAAoC,EACpC,OAAU;IAEV,OAAO,WAAW,CAAC,KAAK,CAAC;QACvB,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QACzD,IAAI;QACJ,aAAa,EAAE,CAAC;QAChB,UAAU,EAAE,IAAI,CAAC,GAAG;QACpB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,KAAK,EAAE,YAAY;QACnB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,QAAQ,EAAE,MAAM;QAChB,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC;KAC/B,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `ctx.grant` / `ctx.revoke`'s writes to `_substrat_tuples`, as one statement each for both
3
+ * adapters (#2071).
4
+ *
5
+ * A revoke TOMBSTONES (K-21): the row stays with `revoked_at` set, the checker's walk skips it,
6
+ * and it stays readable as evidence that the key was taken back. It used to be a `DELETE`,
7
+ * which left "revoked" and "never held" as the same absent row. That is the one distinction a
8
+ * declared entity-grant shape's top-up needs (`entity-grant-shape.ts`): a key someone took back
9
+ * from a person must not come back with the next release.
10
+ *
11
+ * A grant is the EXPLICIT write, so it clears a tombstone: a re-grant grants, as
12
+ * `assignScopeRole` and `HostAdmin.grant` already do. A live row is left exactly as it is,
13
+ * expiry included, because `ctx.grant` has never shortened or lengthened a grant it found.
14
+ */
15
+ import type { SwitchSql } from './system-switch.js';
16
+ type Params = [string, string, string];
17
+ /** A reconciled grantee shape's protected key, stored on the scope's tuple spine. */
18
+ export declare const GRANTEE_KEY_RELATION = "shape-grantee-key";
19
+ /** The explicit scope-tuple write for admin/local grants and ctx.grant, behind one shape guard. */
20
+ export declare function writeExplicitTupleIn(db: SwitchSql, subject: string, relation: string, object: string, mode: {
21
+ kind: 'replace';
22
+ expiresAt: string | null;
23
+ } | {
24
+ kind: 'delegated';
25
+ }): void;
26
+ /**
27
+ * The explicit tuple write: insert it, or bring a tombstoned one back with no expiry. A live
28
+ * row is untouched. `ctx.grant` and a declared shape's grant (`entity-grant-shape.ts`) share it.
29
+ */
30
+ export declare function explicitTupleSql(subject: string, relation: string, object: string): {
31
+ sql: string;
32
+ params: Params;
33
+ };
34
+ /** `ctx.grant`'s write — {@link explicitTupleSql} on a `granted:` relation. */
35
+ export declare function delegatedGrantSql(principal: string, permission: string, object: string): {
36
+ sql: string;
37
+ params: Params;
38
+ };
39
+ /** Tombstone a live grant. A repeat revoke, or a revoke of a grant never made, changes nothing. */
40
+ export declare function delegatedRevokeSql(principal: string, permission: string, object: string, at: string): {
41
+ sql: string;
42
+ params: [string, ...Params];
43
+ };
44
+ export {};
45
+ //# sourceMappingURL=entity-grant.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-grant.d.ts","sourceRoot":"","sources":["../src/entity-grant.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEpD,KAAK,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAEvC,qFAAqF;AACrF,eAAO,MAAM,oBAAoB,sBAAsB,CAAC;AAgBxD,mGAAmG;AACnG,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,SAAS,EACb,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GAC1E,IAAI,CAcN;AAQD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAOnH;AAED,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAExH;AAED,mGAAmG;AACnG,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,GACT;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,MAAM,EAAE,GAAG,MAAM,CAAC,CAAA;CAAE,CAM9C"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * `ctx.grant` / `ctx.revoke`'s writes to `_substrat_tuples`, as one statement each for both
3
+ * adapters (#2071).
4
+ *
5
+ * A revoke TOMBSTONES (K-21): the row stays with `revoked_at` set, the checker's walk skips it,
6
+ * and it stays readable as evidence that the key was taken back. It used to be a `DELETE`,
7
+ * which left "revoked" and "never held" as the same absent row. That is the one distinction a
8
+ * declared entity-grant shape's top-up needs (`entity-grant-shape.ts`): a key someone took back
9
+ * from a person must not come back with the next release.
10
+ *
11
+ * A grant is the EXPLICIT write, so it clears a tombstone: a re-grant grants, as
12
+ * `assignScopeRole` and `HostAdmin.grant` already do. A live row is left exactly as it is,
13
+ * expiry included, because `ctx.grant` has never shortened or lengthened a grant it found.
14
+ */
15
+ import { PermissionDenied } from './permission-checker.js';
16
+ /** A reconciled grantee shape's protected key, stored on the scope's tuple spine. */
17
+ export const GRANTEE_KEY_RELATION = 'shape-grantee-key';
18
+ /** Every explicit non-shape scope tuple write uses this guard, including administrative writes. */
19
+ function assertDelegableTuple(db, relation, object) {
20
+ if (!relation.startsWith('granted:'))
21
+ return;
22
+ const colon = object.indexOf(':');
23
+ if (colon < 0)
24
+ return;
25
+ const entityType = object.slice(0, colon);
26
+ if (db.all('SELECT 1 FROM _substrat_tuples WHERE subject = ? AND relation = ? AND object = ?', `shape:${entityType}`, GRANTEE_KEY_RELATION, relation).length > 0) {
27
+ throw new PermissionDenied(`cannot grant '${relation.slice('granted:'.length)}' on ${object} — ` +
28
+ `it is a key of the declared '${entityType}' shape, given only by the shape grant`);
29
+ }
30
+ }
31
+ /** The explicit scope-tuple write for admin/local grants and ctx.grant, behind one shape guard. */
32
+ export function writeExplicitTupleIn(db, subject, relation, object, mode) {
33
+ assertDelegableTuple(db, relation, object);
34
+ if (mode.kind === 'replace') {
35
+ db.run('INSERT OR REPLACE INTO _substrat_tuples (subject, relation, object, expires_at) VALUES (?, ?, ?, ?)', subject, relation, object, mode.expiresAt);
36
+ }
37
+ else {
38
+ const grant = explicitTupleSql(subject, relation, object);
39
+ db.run(grant.sql, ...grant.params);
40
+ }
41
+ }
42
+ const refs = (principal, permission, object) => [
43
+ `principal:${principal}`,
44
+ `granted:${permission}`,
45
+ object,
46
+ ];
47
+ /**
48
+ * The explicit tuple write: insert it, or bring a tombstoned one back with no expiry. A live
49
+ * row is untouched. `ctx.grant` and a declared shape's grant (`entity-grant-shape.ts`) share it.
50
+ */
51
+ export function explicitTupleSql(subject, relation, object) {
52
+ return {
53
+ sql: `INSERT INTO _substrat_tuples (subject, relation, object) VALUES (?, ?, ?)
54
+ ON CONFLICT (subject, relation, object) DO UPDATE SET revoked_at = NULL, expires_at = NULL
55
+ WHERE _substrat_tuples.revoked_at IS NOT NULL`,
56
+ params: [subject, relation, object],
57
+ };
58
+ }
59
+ /** `ctx.grant`'s write — {@link explicitTupleSql} on a `granted:` relation. */
60
+ export function delegatedGrantSql(principal, permission, object) {
61
+ return explicitTupleSql(...refs(principal, permission, object));
62
+ }
63
+ /** Tombstone a live grant. A repeat revoke, or a revoke of a grant never made, changes nothing. */
64
+ export function delegatedRevokeSql(principal, permission, object, at) {
65
+ return {
66
+ sql: `UPDATE _substrat_tuples SET revoked_at = ?
67
+ WHERE subject = ? AND relation = ? AND object = ? AND revoked_at IS NULL`,
68
+ params: [at, ...refs(principal, permission, object)],
69
+ };
70
+ }
71
+ //# sourceMappingURL=entity-grant.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-grant.js","sourceRoot":"","sources":["../src/entity-grant.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAK3D,qFAAqF;AACrF,MAAM,CAAC,MAAM,oBAAoB,GAAG,mBAAmB,CAAC;AAExD,mGAAmG;AACnG,SAAS,oBAAoB,CAAC,EAAa,EAAE,QAAgB,EAAE,MAAc;IAC3E,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO;IAC7C,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO;IACtB,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IAC1C,IAAI,EAAE,CAAC,GAAG,CAAC,kFAAkF,EAAE,SAAS,UAAU,EAAE,EAAE,oBAAoB,EAAE,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjK,MAAM,IAAI,gBAAgB,CACxB,iBAAiB,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,QAAQ,MAAM,KAAK;YACnE,gCAAgC,UAAU,wCAAwC,CACrF,CAAC;IACJ,CAAC;AACH,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,oBAAoB,CAClC,EAAa,EACb,OAAe,EACf,QAAgB,EAChB,MAAc,EACd,IAA2E;IAE3E,oBAAoB,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC3C,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5B,EAAE,CAAC,GAAG,CACJ,qGAAqG,EACrG,OAAO,EACP,QAAQ,EACR,MAAM,EACN,IAAI,CAAC,SAAS,CACf,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,MAAM,KAAK,GAAG,gBAAgB,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC1D,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IACrC,CAAC;AACH,CAAC;AAED,MAAM,IAAI,GAAG,CAAC,SAAiB,EAAE,UAAkB,EAAE,MAAc,EAAU,EAAE,CAAC;IAC9E,aAAa,SAAS,EAAE;IACxB,WAAW,UAAU,EAAE;IACvB,MAAM;CACP,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,QAAgB,EAAE,MAAc;IAChF,OAAO;QACL,GAAG,EAAE;;wDAE+C;QACpD,MAAM,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC;KACpC,CAAC;AACJ,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,iBAAiB,CAAC,SAAiB,EAAE,UAAkB,EAAE,MAAc;IACrF,OAAO,gBAAgB,CAAC,GAAG,IAAI,CAAC,SAAS,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC;AAClE,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,kBAAkB,CAChC,SAAiB,EACjB,UAAkB,EAClB,MAAc,EACd,EAAU;IAEV,OAAO;QACL,GAAG,EAAE;oFAC2E;QAChF,MAAM,EAAE,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;KACrD,CAAC;AACJ,CAAC"}
@@ -8,7 +8,7 @@
8
8
  import { type EntityStateName } from '@substrat-run/contracts';
9
9
  import { type EntityStatePlan, type StateCheck } from './entity-state.js';
10
10
  import { type ListIndexPlan } from './list-index.js';
11
- import type { OperationContext } from './scope-host.js';
11
+ import type { OperationContext, PageParams } from './scope-host.js';
12
12
  import { type SearchIndexPlan } from './search-index.js';
13
13
  /**
14
14
  * Refuse the bin on the UNCHECKED readers. `ctx.page` and `ctx.search` check no permission, so
@@ -29,14 +29,11 @@ export interface TrashedReadDeps {
29
29
  searchPlans: ReadonlyMap<string, SearchIndexPlan>;
30
30
  statePlans: ReadonlyMap<string, EntityStatePlan>;
31
31
  check: StateCheck;
32
- /** Rows one trashed page may read looking for visible ones — `TRASH_SCAN_BUDGET` unless a test narrows it. */
32
+ /** Test override; a sealed production walk chooses a private budget up to `TRASH_SCAN_BUDGET`. */
33
33
  scanBudget?: number;
34
+ continuation?: (entityType: string, params: PageParams) => ReturnType<OperationContext['pageContinuation']>;
34
35
  }
35
- /**
36
- * How many binned rows one `ctx.pageTrashed` call reads, at most, looking for rows the caller
37
- * may see. Each costs a permission check, and a Durable Object has a CPU budget per request, so
38
- * a bin full of other people's rows cannot be walked without bound inside one call.
39
- */
36
+ /** Maximum number of binned rows one `ctx.pageTrashed` call reads. */
40
37
  export declare const TRASH_SCAN_BUDGET = 2000;
41
38
  export type TrashedReads = Pick<OperationContext, 'pageTrashed' | 'searchTrashed'>;
42
39
  export declare function createTrashedReads(deps: TrashedReadDeps): TrashedReads;
@@ -1 +1 @@
1
- {"version":3,"file":"entity-state-reads.d.ts","sourceRoot":"","sources":["../src/entity-state-reads.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAgD,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACxH,OAAO,EAAoC,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACvF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAML,KAAK,eAAe,EACrB,MAAM,mBAAmB,CAAC;AAE3B;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,eAAe,GAAG,SAAS,CAQrH;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,EAChD,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,eAAe,GAAG,SAAS,GAChC,MAAM,GAAG,SAAS,CAGpB;AAED,sDAAsD;AACtD,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,OAAO,EAAE,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAC9E,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC9C,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAClD,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACjD,KAAK,EAAE,UAAU,CAAC;IAClB,8GAA8G;IAC9G,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,OAAQ,CAAC;AAEvC,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,gBAAgB,EAAE,aAAa,GAAG,eAAe,CAAC,CAAC;AAyBnF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,eAAe,GAAG,YAAY,CAsFtE"}
1
+ {"version":3,"file":"entity-state-reads.d.ts","sourceRoot":"","sources":["../src/entity-state-reads.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAOL,KAAK,eAAe,EAErB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAgD,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACxH,OAAO,EAAoC,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACvF,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,EAML,KAAK,eAAe,EACrB,MAAM,mBAAmB,CAAC;AAE3B;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,eAAe,GAAG,SAAS,CAQrH;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,EAChD,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,eAAe,GAAG,SAAS,GAChC,MAAM,GAAG,SAAS,CAGpB;AAED,sDAAsD;AACtD,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,OAAO,EAAE,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAC9E,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC9C,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAClD,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACjD,KAAK,EAAE,UAAU,CAAC;IAClB,kGAAkG;IAClG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,UAAU,CAAC,gBAAgB,CAAC,kBAAkB,CAAC,CAAC,CAAC;CAC7G;AAED,sEAAsE;AACtE,eAAO,MAAM,iBAAiB,OAAsB,CAAC;AAErD,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,gBAAgB,EAAE,aAAa,GAAG,eAAe,CAAC,CAAC;AAyBnF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,eAAe,GAAG,YAAY,CA2DtE"}