@substrat-run/kernel 0.138.0 → 0.140.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 (164) 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 +21 -2
  6. package/dist/attachment-extractor.d.ts.map +1 -1
  7. package/dist/attachment-extractor.js +31 -6
  8. package/dist/attachment-extractor.js.map +1 -1
  9. package/dist/attachment-text.d.ts +64 -0
  10. package/dist/attachment-text.d.ts.map +1 -1
  11. package/dist/attachment-text.js +87 -16
  12. package/dist/attachment-text.js.map +1 -1
  13. package/dist/attribution.d.ts +36 -3
  14. package/dist/attribution.d.ts.map +1 -1
  15. package/dist/attribution.js +21 -8
  16. package/dist/attribution.js.map +1 -1
  17. package/dist/audit-outcome.d.ts +149 -0
  18. package/dist/audit-outcome.d.ts.map +1 -0
  19. package/dist/audit-outcome.js +233 -0
  20. package/dist/audit-outcome.js.map +1 -0
  21. package/dist/connect-links.d.ts +91 -0
  22. package/dist/connect-links.d.ts.map +1 -0
  23. package/dist/connect-links.js +189 -0
  24. package/dist/connect-links.js.map +1 -0
  25. package/dist/connect-state.d.ts +12 -3
  26. package/dist/connect-state.d.ts.map +1 -1
  27. package/dist/connect-state.js +8 -3
  28. package/dist/connect-state.js.map +1 -1
  29. package/dist/derived-object.d.ts +14 -0
  30. package/dist/derived-object.d.ts.map +1 -0
  31. package/dist/derived-object.js +2 -0
  32. package/dist/derived-object.js.map +1 -0
  33. package/dist/derived-objects.d.ts +108 -0
  34. package/dist/derived-objects.d.ts.map +1 -0
  35. package/dist/derived-objects.js +199 -0
  36. package/dist/derived-objects.js.map +1 -0
  37. package/dist/email-intent.d.ts +56 -0
  38. package/dist/email-intent.d.ts.map +1 -0
  39. package/dist/email-intent.js +71 -0
  40. package/dist/email-intent.js.map +1 -0
  41. package/dist/entity-edges.d.ts +6 -1
  42. package/dist/entity-edges.d.ts.map +1 -1
  43. package/dist/entity-edges.js +13 -0
  44. package/dist/entity-edges.js.map +1 -1
  45. package/dist/entity-grant-shape.d.ts +94 -0
  46. package/dist/entity-grant-shape.d.ts.map +1 -0
  47. package/dist/entity-grant-shape.js +329 -0
  48. package/dist/entity-grant-shape.js.map +1 -0
  49. package/dist/entity-grant.d.ts +45 -0
  50. package/dist/entity-grant.d.ts.map +1 -0
  51. package/dist/entity-grant.js +71 -0
  52. package/dist/entity-grant.js.map +1 -0
  53. package/dist/entity-state-reads.d.ts +40 -0
  54. package/dist/entity-state-reads.d.ts.map +1 -0
  55. package/dist/entity-state-reads.js +91 -0
  56. package/dist/entity-state-reads.js.map +1 -0
  57. package/dist/entity-state.d.ts +204 -0
  58. package/dist/entity-state.d.ts.map +1 -0
  59. package/dist/entity-state.js +356 -0
  60. package/dist/entity-state.js.map +1 -0
  61. package/dist/entity-trash.d.ts +245 -0
  62. package/dist/entity-trash.d.ts.map +1 -0
  63. package/dist/entity-trash.js +380 -0
  64. package/dist/entity-trash.js.map +1 -0
  65. package/dist/findings.d.ts +143 -0
  66. package/dist/findings.d.ts.map +1 -0
  67. package/dist/findings.js +463 -0
  68. package/dist/findings.js.map +1 -0
  69. package/dist/grant-scoped-read.d.ts +41 -0
  70. package/dist/grant-scoped-read.d.ts.map +1 -0
  71. package/dist/grant-scoped-read.js +100 -0
  72. package/dist/grant-scoped-read.js.map +1 -0
  73. package/dist/index.d.ts +40 -8
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +26 -6
  76. package/dist/index.js.map +1 -1
  77. package/dist/kernel-outbox.d.ts +14 -0
  78. package/dist/kernel-outbox.d.ts.map +1 -0
  79. package/dist/kernel-outbox.js +37 -0
  80. package/dist/kernel-outbox.js.map +1 -0
  81. package/dist/list-index.d.ts +34 -3
  82. package/dist/list-index.d.ts.map +1 -1
  83. package/dist/list-index.js +83 -31
  84. package/dist/list-index.js.map +1 -1
  85. package/dist/mail-sender.d.ts +78 -0
  86. package/dist/mail-sender.d.ts.map +1 -0
  87. package/dist/mail-sender.js +2 -0
  88. package/dist/mail-sender.js.map +1 -0
  89. package/dist/membership-executor.d.ts +7 -47
  90. package/dist/membership-executor.d.ts.map +1 -1
  91. package/dist/membership-executor.js +28 -53
  92. package/dist/membership-executor.js.map +1 -1
  93. package/dist/migration-digest.d.ts +161 -0
  94. package/dist/migration-digest.d.ts.map +1 -0
  95. package/dist/migration-digest.js +237 -0
  96. package/dist/migration-digest.js.map +1 -0
  97. package/dist/module-erasure.d.ts +156 -0
  98. package/dist/module-erasure.d.ts.map +1 -0
  99. package/dist/module-erasure.js +440 -0
  100. package/dist/module-erasure.js.map +1 -0
  101. package/dist/module-migrations.d.ts +6 -3
  102. package/dist/module-migrations.d.ts.map +1 -1
  103. package/dist/module-migrations.js +12 -5
  104. package/dist/module-migrations.js.map +1 -1
  105. package/dist/peer.d.ts +9 -1
  106. package/dist/peer.d.ts.map +1 -1
  107. package/dist/peer.js +29 -0
  108. package/dist/peer.js.map +1 -1
  109. package/dist/permission-checker.d.ts +6 -0
  110. package/dist/permission-checker.d.ts.map +1 -1
  111. package/dist/permission-checker.js.map +1 -1
  112. package/dist/permission-eval.d.ts +51 -1
  113. package/dist/permission-eval.d.ts.map +1 -1
  114. package/dist/permission-eval.js +104 -2
  115. package/dist/permission-eval.js.map +1 -1
  116. package/dist/platform-sweep.d.ts +8 -2
  117. package/dist/platform-sweep.d.ts.map +1 -1
  118. package/dist/platform-sweep.js +17 -0
  119. package/dist/platform-sweep.js.map +1 -1
  120. package/dist/scope-host.d.ts +479 -24
  121. package/dist/scope-host.d.ts.map +1 -1
  122. package/dist/scope-host.js +82 -7
  123. package/dist/scope-host.js.map +1 -1
  124. package/dist/scope-role-admin.d.ts +68 -0
  125. package/dist/scope-role-admin.d.ts.map +1 -0
  126. package/dist/scope-role-admin.js +80 -0
  127. package/dist/scope-role-admin.js.map +1 -0
  128. package/dist/search-index.d.ts +21 -1
  129. package/dist/search-index.d.ts.map +1 -1
  130. package/dist/search-index.js +60 -37
  131. package/dist/search-index.js.map +1 -1
  132. package/dist/spine-guard.d.ts +71 -2
  133. package/dist/spine-guard.d.ts.map +1 -1
  134. package/dist/spine-guard.js +393 -10
  135. package/dist/spine-guard.js.map +1 -1
  136. package/dist/spine-restore.d.ts.map +1 -1
  137. package/dist/spine-restore.js +5 -0
  138. package/dist/spine-restore.js.map +1 -1
  139. package/dist/sql-identifier.d.ts +12 -0
  140. package/dist/sql-identifier.d.ts.map +1 -0
  141. package/dist/sql-identifier.js +17 -0
  142. package/dist/sql-identifier.js.map +1 -0
  143. package/dist/sql-statements.d.ts +71 -0
  144. package/dist/sql-statements.d.ts.map +1 -0
  145. package/dist/sql-statements.js +191 -0
  146. package/dist/sql-statements.js.map +1 -0
  147. package/dist/subject-redaction.d.ts +9 -1
  148. package/dist/subject-redaction.d.ts.map +1 -1
  149. package/dist/subject-redaction.js.map +1 -1
  150. package/dist/system-switch-record.d.ts +3 -0
  151. package/dist/system-switch-record.d.ts.map +1 -1
  152. package/dist/system-switch-record.js +2 -0
  153. package/dist/system-switch-record.js.map +1 -1
  154. package/dist/table-ownership.d.ts +103 -0
  155. package/dist/table-ownership.d.ts.map +1 -0
  156. package/dist/table-ownership.js +285 -0
  157. package/dist/table-ownership.js.map +1 -0
  158. package/dist/timeline.d.ts +4 -4
  159. package/dist/timeline.js +4 -4
  160. package/dist/visible-continuation.d.ts +38 -0
  161. package/dist/visible-continuation.d.ts.map +1 -0
  162. package/dist/visible-continuation.js +162 -0
  163. package/dist/visible-continuation.js.map +1 -0
  164. package/package.json +2 -2
@@ -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":"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"}
@@ -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"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The kernel-composed reads over archivable and trashable entities (#119), written once for
3
+ * both adapters: the view predicate `ctx.page`/`ctx.search` add, and the two trashed readers.
4
+ *
5
+ * Kept apart from `entity-state.ts` because these need the list and search composers, and those
6
+ * need the view predicate from there — one direction of import, not a cycle.
7
+ */
8
+ import { type EntityStateName } from '@substrat-run/contracts';
9
+ import { type EntityStatePlan, type StateCheck } from './entity-state.js';
10
+ import { type ListIndexPlan } from './list-index.js';
11
+ import type { OperationContext, PageParams } from './scope-host.js';
12
+ import { type SearchIndexPlan } from './search-index.js';
13
+ /**
14
+ * Refuse the bin on the UNCHECKED readers. `ctx.page` and `ctx.search` check no permission, so
15
+ * the trashed view is not theirs to serve — it reaches them as a string from the wire as easily
16
+ * as from code, and must be refused there rather than answered.
17
+ */
18
+ export declare function uncheckedView(verb: string, entityType: string, view: string | undefined): EntityStateName | undefined;
19
+ /**
20
+ * The `src`-aliased predicate a search over `entityType` adds for `view`, or `undefined` when
21
+ * the entity declares no state (every row is active).
22
+ */
23
+ export declare function searchStateWhere(statePlans: ReadonlyMap<string, EntityStatePlan>, entityType: string, view: EntityStateName | undefined): string | undefined;
24
+ /** What the trashed readers need from the adapter. */
25
+ export interface TrashedReadDeps {
26
+ /** A read on the scope's own database, inside the operation. */
27
+ query: (sql: string, params: readonly unknown[]) => Record<string, unknown>[];
28
+ listPlans: ReadonlyMap<string, ListIndexPlan>;
29
+ searchPlans: ReadonlyMap<string, SearchIndexPlan>;
30
+ statePlans: ReadonlyMap<string, EntityStatePlan>;
31
+ check: StateCheck;
32
+ /** Test override; a sealed production walk chooses a private budget up to `TRASH_SCAN_BUDGET`. */
33
+ scanBudget?: number;
34
+ continuation?: (entityType: string, params: PageParams) => ReturnType<OperationContext['pageContinuation']>;
35
+ }
36
+ /** Maximum number of binned rows one `ctx.pageTrashed` call reads. */
37
+ export declare const TRASH_SCAN_BUDGET = 2000;
38
+ export type TrashedReads = Pick<OperationContext, 'pageTrashed' | 'searchTrashed'>;
39
+ export declare function createTrashedReads(deps: TrashedReadDeps): TrashedReads;
40
+ //# sourceMappingURL=entity-state-reads.d.ts.map
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The kernel-composed reads over archivable and trashable entities (#119), written once for
3
+ * both adapters: the view predicate `ctx.page`/`ctx.search` add, and the two trashed readers.
4
+ *
5
+ * Kept apart from `entity-state.ts` because these need the list and search composers, and those
6
+ * need the view predicate from there — one direction of import, not a cycle.
7
+ */
8
+ import { pageOf, pageVisible, substratError, VISIBLE_SCAN_BUDGET, } from '@substrat-run/contracts';
9
+ import { entityStateWhere, stateColumnsOf, stateKeyOf } from './entity-state.js';
10
+ import { cursorOf, listQuery, NotListable } from './list-index.js';
11
+ import { NotSearchable, searchLimit, searchMatchExpression, searchQuery, } from './search-index.js';
12
+ /**
13
+ * Refuse the bin on the UNCHECKED readers. `ctx.page` and `ctx.search` check no permission, so
14
+ * the trashed view is not theirs to serve — it reaches them as a string from the wire as easily
15
+ * as from code, and must be refused there rather than answered.
16
+ */
17
+ export function uncheckedView(verb, entityType, view) {
18
+ if (view !== 'trashed')
19
+ return view;
20
+ throw substratError('validation_failed', `${verb}: the trash of '${entityType}' is read with ${verb === 'ctx.page' ? 'ctx.pageTrashed' : 'ctx.searchTrashed'}, ` +
21
+ 'which checks the declared trash key on every row', { errors: [{ path: 'view', message: 'trashed is not a view of this read' }] });
22
+ }
23
+ /**
24
+ * The `src`-aliased predicate a search over `entityType` adds for `view`, or `undefined` when
25
+ * the entity declares no state (every row is active).
26
+ */
27
+ export function searchStateWhere(statePlans, entityType, view) {
28
+ const plan = statePlans.get(entityType);
29
+ return entityStateWhere(entityType, plan && stateColumnsOf(plan), uncheckedView('ctx.search', entityType, view), 'src');
30
+ }
31
+ /** Maximum number of binned rows one `ctx.pageTrashed` call reads. */
32
+ export const TRASH_SCAN_BUDGET = VISIBLE_SCAN_BUDGET;
33
+ /**
34
+ * Keep the rows of a trashed read the caller may see in the bin.
35
+ *
36
+ * The declared trash key is checked per row, inside the kernel, so a handler cannot forget it:
37
+ * a member who holds the key on their own lists sees their own bin and nobody else's. Per row
38
+ * because the key is usually entity-narrowed; a scope-wide holder passes every row and pays one
39
+ * cheap check each.
40
+ */
41
+ async function keepVisible(check, key, entityType, rows, idOf) {
42
+ const kept = [];
43
+ for (const row of rows) {
44
+ const entity = { entityType, entityId: idOf(row) };
45
+ if ((await check(key, entity)).allowed)
46
+ kept.push(row);
47
+ }
48
+ return kept;
49
+ }
50
+ export function createTrashedReads(deps) {
51
+ return {
52
+ /**
53
+ * The bin uses `pageVisible` with the declared trash key as its per-row check.
54
+ * A budget stop carries a sealed continuation of the last examined row, including
55
+ * when no row passed the check. No plain position leaves the kernel (#2074, K-45).
56
+ */
57
+ async pageTrashed(entityType, params) {
58
+ const { key } = stateKeyOf(deps.statePlans, 'ctx.pageTrashed', entityType, 'trash');
59
+ const plan = deps.listPlans.get(entityType);
60
+ if (!plan)
61
+ throw new NotListable(entityType);
62
+ if (params.total) {
63
+ throw substratError('validation_failed', 'ctx.pageTrashed: a trashed page carries no total — a count over rows the caller may not see would disclose them');
64
+ }
65
+ return pageVisible(({ limit, cursor, rowCursors }) => {
66
+ const q = listQuery(plan, {
67
+ limit,
68
+ sort: params.sort,
69
+ order: params.order,
70
+ cursor,
71
+ filters: params.filters,
72
+ view: 'trashed',
73
+ });
74
+ return pageOf(deps.query(q.sql, q.params), limit, (row) => cursorOf(row, q.sortColumn, plan.idColumn, q.order, 'trashed'), rowCursors);
75
+ }, params, async (row) => (await deps.check(key, { entityType, entityId: String(row[plan.idColumn]) })).allowed, {
76
+ scanBudget: deps.scanBudget,
77
+ continuation: deps.continuation?.(entityType, params),
78
+ });
79
+ },
80
+ async searchTrashed(entityType, term, options) {
81
+ const { plan: state, key } = stateKeyOf(deps.statePlans, 'ctx.searchTrashed', entityType, 'trash');
82
+ const plan = deps.searchPlans.get(entityType);
83
+ if (!plan)
84
+ throw new NotSearchable(entityType);
85
+ const q = searchQuery(plan, searchMatchExpression(term, plan.tokenizer), searchLimit(options?.limit), entityStateWhere(entityType, stateColumnsOf(state), 'trashed', 'src'));
86
+ const hits = deps.query(q.sql, q.params).map((row) => ({ entityType, id: row.id, rank: row.rank }));
87
+ return keepVisible(deps.check, key, entityType, hits, (hit) => hit.id);
88
+ },
89
+ };
90
+ }
91
+ //# sourceMappingURL=entity-state-reads.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-state-reads.js","sourceRoot":"","sources":["../src/entity-state-reads.ts"],"names":[],"mappings":"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"}