@substrat-run/kernel 0.133.0 → 0.134.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 (40) hide show
  1. package/dist/attachment-extractor.d.ts +180 -0
  2. package/dist/attachment-extractor.d.ts.map +1 -0
  3. package/dist/attachment-extractor.js +230 -0
  4. package/dist/attachment-extractor.js.map +1 -0
  5. package/dist/attachment-text.d.ts +283 -0
  6. package/dist/attachment-text.d.ts.map +1 -0
  7. package/dist/attachment-text.js +497 -0
  8. package/dist/attachment-text.js.map +1 -0
  9. package/dist/idempotency.d.ts +4 -3
  10. package/dist/idempotency.d.ts.map +1 -1
  11. package/dist/idempotency.js +15 -7
  12. package/dist/idempotency.js.map +1 -1
  13. package/dist/index.d.ts +8 -4
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +4 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/job-run.d.ts.map +1 -1
  18. package/dist/job-run.js +3 -2
  19. package/dist/job-run.js.map +1 -1
  20. package/dist/list-index.d.ts +2 -1
  21. package/dist/list-index.d.ts.map +1 -1
  22. package/dist/list-index.js +9 -0
  23. package/dist/list-index.js.map +1 -1
  24. package/dist/platform-sweep.d.ts +9 -21
  25. package/dist/platform-sweep.d.ts.map +1 -1
  26. package/dist/platform-sweep.js +30 -1
  27. package/dist/platform-sweep.js.map +1 -1
  28. package/dist/scope-host.d.ts +89 -1
  29. package/dist/scope-host.d.ts.map +1 -1
  30. package/dist/scope-host.js +75 -1
  31. package/dist/scope-host.js.map +1 -1
  32. package/dist/search-index.d.ts +7 -0
  33. package/dist/search-index.d.ts.map +1 -1
  34. package/dist/search-index.js +7 -0
  35. package/dist/search-index.js.map +1 -1
  36. package/dist/subject-redaction.d.ts +151 -6
  37. package/dist/subject-redaction.d.ts.map +1 -1
  38. package/dist/subject-redaction.js +212 -8
  39. package/dist/subject-redaction.js.map +1 -1
  40. package/package.json +2 -2
@@ -0,0 +1,497 @@
1
+ /**
2
+ * Attachment content search (#1575): the text an attachment's bytes carry, extracted off
3
+ * the upload by a job and indexed in the scope, keyed by attachment id.
4
+ *
5
+ * `ctx.search` answers over an entity's declared columns. An attachment had nothing but
6
+ * its filename there, because nothing ever read its bytes. This is the reading, and the
7
+ * place the result lands. It is kernel-owned for the reasons the entity index is (see
8
+ * `search-index.ts`): no module may write `_substrat_attachments`'s neighbourhood, a
9
+ * kernel-maintained index cannot be desynchronised by a writer that forgets it, and the
10
+ * permission gate is already the kernel's.
11
+ *
12
+ * ## The flow
13
+ *
14
+ * 1. **Upload** writes the attachment row, a `pending` text row and a job run
15
+ * (`ATTACHMENT_TEXT_MODULE` / `ATTACHMENT_TEXT_JOB`, instance = the attachment id), all
16
+ * in the upload's own transaction. Nothing is extracted on the upload path, so an
17
+ * extraction failure cannot fail an upload: the bytes and the record have landed
18
+ * whatever happens next.
19
+ * 2. **The job driver** (`runDueJobs`, which the scope sweeper calls when a deployment
20
+ * opts into `runJobs`) runs `attachmentTextJob`: read the record, read the bytes, run
21
+ * the host's extractor for its type (K-43), write the outcome. Each adapter supplies
22
+ * the handler itself for this key, so no deployment has to register it.
23
+ * 3. **Search** is `ScopeAttachments.search`, authorized before it matches (below).
24
+ *
25
+ * ## Reading the bytes needs no grant, and that is deliberate
26
+ *
27
+ * Extraction is a derivation the kernel owns, like the entity index's triggers reading a
28
+ * module's rows. Reading through `getSystemAttachments` instead would need
29
+ * `system:<module>` to hold every target's read key, and a CP-less vertical has no way to
30
+ * seat that grant short of declaring a schedule it does not have. The gate that decides
31
+ * what a PERSON learns is at search time, and it is the same check `list` and `open` make.
32
+ *
33
+ * ## The search gate, and what it refuses to leak
34
+ *
35
+ * A search AUTHORIZES FIRST, from the scope and the caller alone, and only then matches
36
+ * (`searchAttachments`). The owners the caller may read — the check `open` makes, the
37
+ * target's `readPermission` on the owning entity — are decided before the term is looked
38
+ * at, and handed to the query, so `ORDER BY … LIMIT` runs over readable rows only. A match
39
+ * the caller cannot open neither appears nor takes a slot, however many there are: the
40
+ * page a caller gets is the page they would get if those attachments did not exist. And
41
+ * because nothing is examined and then dropped, there is no scan bound whose cutoff — or
42
+ * whose running time — depends on hidden rows. Three more choices follow from the same
43
+ * rule, each because the alternative would leak.
44
+ *
45
+ * - **No count and no "more" marker.** Either would count matches the caller cannot see.
46
+ * - **No score, and newest-first order instead of relevance.** FTS5's `bm25` weighs a term
47
+ * by how many documents in the WHOLE index contain it, including the ones the caller
48
+ * cannot open. Exposing the score, or ordering a multi-term result by it, would let a
49
+ * caller infer how often a term occurs in attachments they may not read. Ordering by
50
+ * attachment id (a ULID, so upload time) depends only on rows the caller can see.
51
+ * - **No snippet.** A snippet is text from the attachment, and producing one for a denied
52
+ * row would be the leak itself.
53
+ *
54
+ * The bound that remains is on authorization, not on matches: a caller without scope-level
55
+ * read on a target type has its owners checked one by one, at most
56
+ * `ATTACHMENT_SEARCH_OWNER_MAX` of them, and past that the search is refused with a stable
57
+ * reason (`ATTACHMENT_SEARCH_TOO_MANY_OWNERS`). Whether it refuses depends on the scope's
58
+ * owner count and the caller's rights — the same answer for every term. What still scales
59
+ * with every match is the FTS index's own read of its posting lists, which is the same work
60
+ * whoever asks.
61
+ *
62
+ * ## Where the text lives, and where it does not go
63
+ *
64
+ * Both tables carry the `_substrat_search_` prefix, which is what keeps them out of every
65
+ * scope dump (`isSearchIndexTable`). That is the point, not a convenience: a dump carries
66
+ * an attachment's METADATA row and never its bytes, so the text extracted from those bytes
67
+ * stays where the bytes are. A restore or a preview carry that brings an attachment row
68
+ * back re-queues its extraction (`reconcileAttachmentText`), which re-reads the bytes if
69
+ * they are still there — the same scope — and records a legible failure where they are
70
+ * not, as in a fork into a new scope. The lake streams the outbox only, so the text never
71
+ * reaches it either.
72
+ *
73
+ * Erasure: `shredSubject` does not reach attachments — they carry no subject and their
74
+ * bytes are not sealed per subject — so there is no shred path for the text to outlive.
75
+ * What does make bytes unreadable is deleting the attachment row (`remove`, or any other
76
+ * delete), and a trigger on `_substrat_attachments` removes the text row with it, which
77
+ * takes its index entries out through the index's own triggers.
78
+ */
79
+ import { attachmentRecord, substratError, utf8Length, } from '@substrat-run/contracts';
80
+ import { attachmentSha256 } from './scope-host.js';
81
+ import { searchLimit, searchMatchExpression } from './search-index.js';
82
+ import { DEFAULT_ATTACHMENT_TEXT_BOUNDS, assertAttachmentTextBounds, chooseAttachmentExtractor, inputBoundRefusal, mediaTypeOf, runAttachmentExtractor, } from './attachment-extractor.js';
83
+ /** The job's module: the kernel itself, which owns the index. Parses as a `ModuleId`. */
84
+ export const ATTACHMENT_TEXT_MODULE = '@substrat-run/kernel';
85
+ /** The job's name. With the module, the key every adapter supplies a handler for. */
86
+ export const ATTACHMENT_TEXT_JOB = 'attachment-text';
87
+ /** Is this run one of the kernel's extraction runs? */
88
+ export function isAttachmentTextRun(run) {
89
+ return run.module_id === ATTACHMENT_TEXT_MODULE && run.job === ATTACHMENT_TEXT_JOB;
90
+ }
91
+ /**
92
+ * Refuse a job registered under the kernel's own module id — every adapter's `registerJob`
93
+ * calls this first.
94
+ *
95
+ * The kernel's jobs are each host's own: it supplies their handlers at dispatch, bound to
96
+ * the scope it is driving, and dispatch looks there before the registry. A handler
97
+ * registered under the kernel's id would therefore never run, and say nothing — the one
98
+ * outcome a registration must not have. The whole id is reserved, not only today's job
99
+ * name, so a later kernel job cannot be shadowed by a registration that predates it.
100
+ */
101
+ export function assertJobRegistrable(moduleId, name) {
102
+ if (moduleId === ATTACHMENT_TEXT_MODULE) {
103
+ throw new Error(`job '${moduleId}/${name}' is reserved: '${ATTACHMENT_TEXT_MODULE}' jobs are the host's own, ` +
104
+ 'so a handler registered under that module id would never run');
105
+ }
106
+ }
107
+ /**
108
+ * The text rows and their index, as both adapters build them.
109
+ *
110
+ * Shared for the reason `JOB_RUN_DDL` is: `lint:spine-ddl` compares what each adapter's
111
+ * `KERNEL_DDL` executes, and one definition keeps the two the same shape.
112
+ *
113
+ * **The double underscore is a reservation.** A module's derived index is named
114
+ * `_substrat_search_<slug(module)>_<slug(entity)>`, and `slug` can never produce a leading
115
+ * underscore, so no module's index can ever collide with these two names.
116
+ *
117
+ * **External content.** The index stores terms and points at the text row's `rid`; the
118
+ * text is stored once, in the row. The three triggers keep the index in step with the
119
+ * row however it is written, and the fourth removes the row when its attachment goes.
120
+ * Interpolated into each adapter's `KERNEL_DDL` after `_substrat_attachments`, which the
121
+ * fourth trigger names.
122
+ *
123
+ * **Only a body reaches the index.** Most rows never carry one — every upload starts
124
+ * `pending`, and an image or a PDF stays bodiless — so the index triggers are guarded on
125
+ * it: a NULL body would index no term and still cost the index's own bookkeeping writes,
126
+ * inside the upload's transaction. An update that leaves the body as it was (a replayed
127
+ * extraction) re-tokenizes nothing.
128
+ */
129
+ export const ATTACHMENT_TEXT_DDL = `
130
+ CREATE TABLE IF NOT EXISTS _substrat_search__attachment_text (
131
+ -- The rowid alias the index points at. Never exposed: the attachment id is the key.
132
+ rid INTEGER PRIMARY KEY,
133
+ attachment_id TEXT NOT NULL UNIQUE,
134
+ -- 'pending' | 'indexed' | 'empty' | 'unsupported' | 'failed'. Only 'indexed' has a body.
135
+ status TEXT NOT NULL,
136
+ -- Which extractor ran ('text', 'html', 'docx', 'xlsx', 'pptx'). NULL while pending,
137
+ -- and for a type no extractor reads.
138
+ extractor TEXT,
139
+ -- The extracted text, normalized and capped. NULL unless indexed.
140
+ body TEXT,
141
+ -- UTF-8 bytes of body. NULL unless indexed.
142
+ body_bytes INTEGER,
143
+ -- 1 when body was cut at the per-attachment bound.
144
+ truncated INTEGER NOT NULL DEFAULT 0,
145
+ -- Why there is no body: the unsupported type, the damaged file, the bound exceeded.
146
+ -- Never quotes the file.
147
+ detail TEXT,
148
+ updated_at TEXT NOT NULL
149
+ );
150
+ CREATE VIRTUAL TABLE IF NOT EXISTS _substrat_search__attachments USING fts5(
151
+ body, content='_substrat_search__attachment_text', content_rowid='rid', tokenize='unicode61'
152
+ );
153
+ CREATE TRIGGER IF NOT EXISTS _substrat_search__attachment_text_ai
154
+ AFTER INSERT ON _substrat_search__attachment_text WHEN new.body IS NOT NULL BEGIN
155
+ INSERT INTO _substrat_search__attachments(rowid, body) VALUES (new.rid, new.body);
156
+ END;
157
+ CREATE TRIGGER IF NOT EXISTS _substrat_search__attachment_text_ad
158
+ AFTER DELETE ON _substrat_search__attachment_text WHEN old.body IS NOT NULL BEGIN
159
+ INSERT INTO _substrat_search__attachments(_substrat_search__attachments, rowid, body)
160
+ VALUES ('delete', old.rid, old.body);
161
+ END;
162
+ CREATE TRIGGER IF NOT EXISTS _substrat_search__attachment_text_au
163
+ AFTER UPDATE ON _substrat_search__attachment_text WHEN old.body IS NOT new.body BEGIN
164
+ INSERT INTO _substrat_search__attachments(_substrat_search__attachments, rowid, body)
165
+ SELECT 'delete', old.rid, old.body WHERE old.body IS NOT NULL;
166
+ INSERT INTO _substrat_search__attachments(rowid, body)
167
+ SELECT new.rid, new.body WHERE new.body IS NOT NULL;
168
+ END;
169
+ CREATE TRIGGER IF NOT EXISTS _substrat_attachments_text_ad
170
+ AFTER DELETE ON _substrat_attachments BEGIN
171
+ DELETE FROM _substrat_search__attachment_text WHERE attachment_id = old.id;
172
+ END;
173
+ `;
174
+ /*
175
+ * The writes below take the kernel's own UNGUARDED spine handle (`spineSql` on the pure
176
+ * adapter, `doSpineSql` on the DO). Both drivers are synchronous, so each write runs inside
177
+ * whatever transaction the caller already holds — the upload's, or a restore's.
178
+ */
179
+ const RUN_PAYLOAD_KEY = 'attachmentId';
180
+ /**
181
+ * Start one attachment's extraction run unless a LIVE one exists — the driver's own
182
+ * coalescing rule (`startJobRun`), as one statement, so a re-queue joins an extraction in
183
+ * flight rather than racing it.
184
+ */
185
+ function startAttachmentTextRun(sql, attachmentId, runId, at) {
186
+ sql.exec(`INSERT INTO _substrat_job_runs
187
+ (id, module_id, job, instance, payload, subject_id, status, cursor, counters, attempts,
188
+ last_error, started_at, updated_at, next_attempt_at, ended_at)
189
+ SELECT ?, ?, ?, ?, ?, NULL, 'running', NULL, '{}', 0, NULL, ?, ?, NULL, NULL
190
+ WHERE NOT EXISTS (
191
+ SELECT 1 FROM _substrat_job_runs
192
+ WHERE module_id = ? AND job = ? AND instance = ? AND status = 'running'
193
+ )`, [
194
+ runId,
195
+ ATTACHMENT_TEXT_MODULE,
196
+ ATTACHMENT_TEXT_JOB,
197
+ attachmentId,
198
+ JSON.stringify({ [RUN_PAYLOAD_KEY]: attachmentId }),
199
+ at,
200
+ at,
201
+ ATTACHMENT_TEXT_MODULE,
202
+ ATTACHMENT_TEXT_JOB,
203
+ attachmentId,
204
+ ]);
205
+ }
206
+ /**
207
+ * Queue one attachment's extraction: a `pending` row and a coalesced run, inside the
208
+ * caller's transaction. An existing text row is left as it is: re-extracting an indexed
209
+ * attachment keeps its current text searchable until the new outcome replaces it.
210
+ */
211
+ export function enqueueAttachmentText(sql, attachmentId, runId, at) {
212
+ sql.exec(`INSERT INTO _substrat_search__attachment_text (attachment_id, status, updated_at)
213
+ VALUES (?, 'pending', ?)
214
+ ON CONFLICT (attachment_id) DO NOTHING`, [attachmentId, at]);
215
+ startAttachmentTextRun(sql, attachmentId, runId, at);
216
+ }
217
+ /**
218
+ * Write one extraction outcome — only while the attachment still exists.
219
+ *
220
+ * A run reads the bytes outside any lock, so the attachment can be removed while it
221
+ * extracts. The remove's trigger has already taken the text row away; an unguarded upsert
222
+ * would put the text of a deleted attachment straight back. So the existence check and
223
+ * the write are one statement, and the return says which happened.
224
+ *
225
+ * Idempotent: the same outcome written twice leaves one row and one set of index entries,
226
+ * because an upsert on a present row is an UPDATE, whose trigger replaces the old terms
227
+ * with the new ones (and touches nothing when the body did not change).
228
+ */
229
+ export function recordAttachmentText(sql, attachmentId, outcome, at) {
230
+ const indexed = outcome.status === 'indexed' ? outcome : null;
231
+ const { changes } = sql.exec(`INSERT INTO _substrat_search__attachment_text
232
+ (attachment_id, status, extractor, body, body_bytes, truncated, detail, updated_at)
233
+ SELECT ?, ?, ?, ?, ?, ?, ?, ?
234
+ WHERE EXISTS (SELECT 1 FROM _substrat_attachments WHERE id = ?)
235
+ ON CONFLICT (attachment_id) DO UPDATE SET
236
+ status = excluded.status, extractor = excluded.extractor, body = excluded.body,
237
+ body_bytes = excluded.body_bytes, truncated = excluded.truncated,
238
+ detail = excluded.detail, updated_at = excluded.updated_at`, [
239
+ attachmentId,
240
+ outcome.status,
241
+ 'extractor' in outcome ? outcome.extractor : null,
242
+ indexed?.text ?? null,
243
+ indexed ? utf8Length(indexed.text) : null,
244
+ indexed?.truncated ? 1 : 0,
245
+ 'detail' in outcome ? outcome.detail : null,
246
+ at,
247
+ attachmentId,
248
+ ]);
249
+ return changes > 0;
250
+ }
251
+ /**
252
+ * After a restore or a fork: drop text whose attachment the load did not bring back, and
253
+ * queue extraction for every attachment that has no text row.
254
+ *
255
+ * The text tables are not in a dump, so a load leaves them as they were: an in-place
256
+ * restore keeps the text of attachments that exist on both sides (an attachment id names
257
+ * one write-once object, so that text is still the text of those bytes), and holds rows
258
+ * for attachments the restore rewound away, which must go. A fork starts with none.
259
+ * Every attachment row without text is queued; its run re-reads the bytes, and where they
260
+ * are not reachable — a fork, whose objects stay under the source scope's key — the
261
+ * outcome is a `failed` row that says so.
262
+ */
263
+ export function reconcileAttachmentText(sql, mintId, at) {
264
+ const removed = sql.query(`DELETE FROM _substrat_search__attachment_text
265
+ WHERE attachment_id NOT IN (SELECT id FROM _substrat_attachments)
266
+ RETURNING attachment_id`).length;
267
+ const queued = sql.query(`INSERT INTO _substrat_search__attachment_text (attachment_id, status, updated_at)
268
+ SELECT id, 'pending', ? FROM _substrat_attachments
269
+ WHERE id NOT IN (SELECT attachment_id FROM _substrat_search__attachment_text)
270
+ RETURNING attachment_id`, [at]);
271
+ // One run each: a run id is a ULID, which SQL cannot mint.
272
+ for (const { attachment_id } of queued)
273
+ startAttachmentTextRun(sql, attachment_id, mintId(), at);
274
+ return { removed, queued: queued.length };
275
+ }
276
+ /**
277
+ * An attachment's extraction state, or null when none was ever recorded — an attachment
278
+ * uploaded before extraction existed, or one a fork carried without its text.
279
+ *
280
+ * Reads the spine through `ctx.sql`, which module code may (it may never write it). Like
281
+ * `readTimeline`, it checks no permission: the caller does, first, as it would before
282
+ * `open`.
283
+ *
284
+ * A `pending` row whose latest run has FAILED is reported as `failed`, with the run's
285
+ * error. That run will not be retried, so "pending" would be a promise nobody keeps; the
286
+ * row itself is left alone so a re-queue picks it up.
287
+ */
288
+ export function readAttachmentText(ctx, attachmentId) {
289
+ const rows = ctx.sql.query(`SELECT t.attachment_id, t.status, t.extractor, t.body_bytes, t.truncated, t.detail, t.updated_at,
290
+ r.status AS run_status, r.last_error AS run_error
291
+ FROM _substrat_search__attachment_text t
292
+ LEFT JOIN _substrat_job_runs r ON r.id = (
293
+ SELECT id FROM _substrat_job_runs
294
+ WHERE module_id = ? AND job = ? AND instance = t.attachment_id
295
+ ORDER BY id DESC LIMIT 1
296
+ )
297
+ WHERE t.attachment_id = ?`, [ATTACHMENT_TEXT_MODULE, ATTACHMENT_TEXT_JOB, attachmentId]);
298
+ const row = rows[0];
299
+ if (!row)
300
+ return null;
301
+ const stalled = row.status === 'pending' && row.run_status === 'failed';
302
+ return {
303
+ attachmentId: row.attachment_id,
304
+ status: stalled ? 'failed' : row.status,
305
+ extractor: row.extractor,
306
+ bytes: row.body_bytes === null ? null : Number(row.body_bytes),
307
+ truncated: Number(row.truncated) === 1,
308
+ detail: stalled ? `extraction run failed: ${row.run_error ?? 'unknown error'}` : row.detail,
309
+ updatedAt: row.updated_at,
310
+ };
311
+ }
312
+ // -- search -----------------------------------------------------------------------------
313
+ /**
314
+ * The most owners a search will check one by one, for a caller without scope-level read
315
+ * on their type. Past it the search is REFUSED (`ATTACHMENT_SEARCH_TOO_MANY_OWNERS`), never
316
+ * truncated: a truncated owner set would drop readable hits silently, by an order the
317
+ * caller cannot see.
318
+ */
319
+ export const ATTACHMENT_SEARCH_OWNER_MAX = 2_000;
320
+ /** The `forbidden` reason a refused search carries — stable, so a UI can explain it. */
321
+ export const ATTACHMENT_SEARCH_TOO_MANY_OWNERS = 'attachment_search_too_many_owners';
322
+ /** A check that cannot answer is a check that refuses: an evaluator failure is never a disclosure. */
323
+ const allows = async (decision) => {
324
+ try {
325
+ return (await decision()).allowed;
326
+ }
327
+ catch {
328
+ return false;
329
+ }
330
+ };
331
+ /**
332
+ * Authorize FIRST, from the scope and the caller alone — never from the term or its matches.
333
+ *
334
+ * 1. Each declared target type is checked once at the SCOPE: an allow there is an allow on
335
+ * every entity of the type (the evaluator grants at the node before it walks an entity,
336
+ * and the walk only adds), so the type is "wide" and costs nothing more. A caller with
337
+ * a role or a scope grant — staff, editors — stops here: one check per declared type.
338
+ * 2. For every other type, the owners that HAVE indexed text are enumerated and checked one
339
+ * by one. Only a narrowed caller pays this (entity grants, a capability), and it is
340
+ * bounded by `ATTACHMENT_SEARCH_OWNER_MAX`.
341
+ *
342
+ * Neither step reads a match, so what this costs and whether it refuses depend on the
343
+ * scope's owners and the caller's rights only: nothing about what the term would find.
344
+ */
345
+ async function readableOwners(sql, gate) {
346
+ const wideTypes = [];
347
+ const narrowTypes = [];
348
+ for (const [entityType, target] of gate.targets) {
349
+ (await allows(() => gate.check(target.read)) ? wideTypes : narrowTypes).push(entityType);
350
+ }
351
+ if (narrowTypes.length === 0)
352
+ return { wideTypes, owners: [] };
353
+ const candidates = sql.query(ATTACHMENT_SEARCH_OWNERS_SQL, [
354
+ JSON.stringify(narrowTypes),
355
+ ATTACHMENT_SEARCH_OWNER_MAX + 1,
356
+ ]);
357
+ if (candidates.length > ATTACHMENT_SEARCH_OWNER_MAX) {
358
+ throw substratError('forbidden', `attachment search: this scope has more than ${ATTACHMENT_SEARCH_OWNER_MAX} owners with indexed ` +
359
+ 'files of a type the caller holds no scope-level read on, too many to authorize one by one', { reason: ATTACHMENT_SEARCH_TOO_MANY_OWNERS });
360
+ }
361
+ const owners = [];
362
+ for (const { entity_type: entityType, entity_id: entityId } of candidates) {
363
+ const read = gate.targets.get(entityType).read;
364
+ if (await allows(() => gate.check(read, { entityType, entityId })))
365
+ owners.push([entityType, entityId]);
366
+ }
367
+ return { wideTypes, owners };
368
+ }
369
+ /**
370
+ * The owners a narrowed caller is checked over: those of the given types that HAVE text,
371
+ * one more than the cap so a refusal can tell "at the cap" from "past it". Walks the
372
+ * `(entity_type, entity_id)` index on `_substrat_attachments`. Params: types (JSON array),
373
+ * limit.
374
+ */
375
+ export const ATTACHMENT_SEARCH_OWNERS_SQL = `SELECT DISTINCT a.entity_type, a.entity_id
376
+ FROM _substrat_attachments a
377
+ JOIN _substrat_search__attachment_text t ON t.attachment_id = a.id
378
+ WHERE t.body IS NOT NULL AND a.entity_type IN (SELECT value FROM json_each(?))
379
+ ORDER BY a.entity_type, a.entity_id
380
+ LIMIT ?`;
381
+ /**
382
+ * The match, restricted to readable owners BEFORE the order and the limit. A wide type is
383
+ * one `IN` over the JSON array of types; a narrowed owner is a row-value `IN` over JSON
384
+ * pairs, which SQLite answers from an ephemeral index on the subquery rather than per
385
+ * pair — one bound JSON array, never a `?` per owner, since a Durable Object refuses the
386
+ * 101st parameter. Params: match, wide types (JSON array), owners (JSON array of
387
+ * `[entityType, entityId]`), limit.
388
+ */
389
+ export const ATTACHMENT_SEARCH_SQL = `SELECT a.id, a.entity_type, a.entity_id, a.filename, a.content_type, a.size,
390
+ a.sha256, a.visibility, a.created_by, a.created_at
391
+ FROM _substrat_search__attachments
392
+ JOIN _substrat_search__attachment_text t ON t.rid = _substrat_search__attachments.rowid
393
+ JOIN _substrat_attachments a ON a.id = t.attachment_id
394
+ WHERE _substrat_search__attachments MATCH ?
395
+ AND (a.entity_type IN (SELECT value FROM json_each(?))
396
+ OR (a.entity_type, a.entity_id) IN
397
+ (SELECT json_extract(value, '$[0]'), json_extract(value, '$[1]') FROM json_each(?)))
398
+ ORDER BY a.id DESC
399
+ LIMIT ?`;
400
+ /**
401
+ * Search extracted text: attachments the caller may open, newest first, at most `limit`.
402
+ *
403
+ * The readable owners are decided first (`readableOwners`) and handed to the query, so the
404
+ * `ORDER BY … LIMIT` runs over readable rows only: a match the caller cannot open neither
405
+ * appears nor takes a slot, however many there are or how new. Nothing is examined and then
406
+ * dropped, so there is no scan bound whose cutoff could depend on hidden rows.
407
+ *
408
+ * The term is judged before anything else, so a too-short term refuses at no cost. The FTS
409
+ * table is not aliased: its own name is the hidden column `MATCH` reads (`searchQuery`).
410
+ */
411
+ export async function searchAttachments(sql, gate, term, limit) {
412
+ const match = searchMatchExpression(term, 'prefix');
413
+ const { wideTypes, owners } = await readableOwners(sql, gate);
414
+ if (wideTypes.length === 0 && owners.length === 0)
415
+ return [];
416
+ return sql
417
+ .query(ATTACHMENT_SEARCH_SQL, [
418
+ match,
419
+ JSON.stringify(wideTypes),
420
+ JSON.stringify(owners),
421
+ searchLimit(limit),
422
+ ])
423
+ .map(attachmentRecordOfRow);
424
+ }
425
+ /** The metadata fact an attachment row records — parsed, so a malformed row throws here. */
426
+ export function attachmentRecordOfRow(row) {
427
+ return attachmentRecord.parse({
428
+ id: row.id,
429
+ entity: { entityType: row.entity_type, entityId: row.entity_id },
430
+ filename: row.filename,
431
+ contentType: row.content_type,
432
+ size: Number(row.size),
433
+ sha256: row.sha256,
434
+ visibility: row.visibility,
435
+ createdBy: row.created_by,
436
+ createdAt: row.created_at,
437
+ });
438
+ }
439
+ function attachmentIdOf(payload) {
440
+ const id = payload?.[RUN_PAYLOAD_KEY];
441
+ if (typeof id !== 'string' || id.length === 0) {
442
+ throw new Error('attachment-text run payload carries no attachmentId');
443
+ }
444
+ return id;
445
+ }
446
+ /**
447
+ * The extraction job: one pass per attachment, done when its outcome is written.
448
+ *
449
+ * Everything decidable without the bytes is decided first — no extractor for the type, a
450
+ * recorded size over the input bound — so those files are never fetched. Bytes that are
451
+ * gone, or that no longer match the recorded hash, are recorded `failed`: retrying cannot
452
+ * bring them back. Only a throw from the store is retried, by the driver's backoff; what
453
+ * the EXTRACTOR does, it answers for in its outcome (`runAttachmentExtractor`).
454
+ *
455
+ * The extractors are the host's, handed in (K-43): the kernel parses no format itself.
456
+ *
457
+ * No `step()`: the pass is one idempotent unit (read, extract, upsert), so a replay after
458
+ * a crash simply does it again, and a step memo would only park the extracted text in
459
+ * `_substrat_job_steps`, which a dump carries.
460
+ */
461
+ export function attachmentTextJob(source, extractors, bounds = DEFAULT_ATTACHMENT_TEXT_BOUNDS) {
462
+ assertAttachmentTextBounds(bounds);
463
+ const outcomeFor = async (record) => {
464
+ const extractor = chooseAttachmentExtractor(extractors, record.contentType, record.filename);
465
+ if (!extractor) {
466
+ return {
467
+ status: 'unsupported',
468
+ detail: `no extractor for content type '${mediaTypeOf(record.contentType) || 'unknown'}'`,
469
+ };
470
+ }
471
+ const failed = (detail) => ({ status: 'failed', extractor: extractor.name, detail });
472
+ // The kernel's ceiling first and on its own, then the extractor's own bound if it is a valid
473
+ // one: an invalid declaration (a `NaN` past the host's check) can disable neither.
474
+ const refusal = inputBoundRefusal(record.size, extractor, bounds);
475
+ if (refusal !== null)
476
+ return failed(refusal);
477
+ const body = await source.bytes(record);
478
+ if (body === null)
479
+ return failed('the bytes are missing from the blob store');
480
+ if ((await attachmentSha256(body)) !== record.sha256)
481
+ return failed('the bytes do not match the recorded sha256');
482
+ return runAttachmentExtractor(extractor, { body, contentType: record.contentType, filename: record.filename }, bounds);
483
+ };
484
+ return async (pass) => {
485
+ const attachmentId = attachmentIdOf(pass.payload);
486
+ const record = await source.record(attachmentId);
487
+ if (!record) {
488
+ pass.count('gone');
489
+ return { done: true };
490
+ }
491
+ const outcome = await outcomeFor(record);
492
+ const kept = await source.write(attachmentId, outcome);
493
+ pass.count(kept ? outcome.status : 'gone');
494
+ return { done: true };
495
+ };
496
+ }
497
+ //# sourceMappingURL=attachment-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-text.js","sourceRoot":"","sources":["../src/attachment-text.ts"],"names":[],"mappings":"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"}
@@ -98,9 +98,10 @@ export declare function idempotencyLookupQuery(subject: CheckSubject, key: strin
98
98
  * - **Reuse.** Same key, different request. The client's assertion that this is
99
99
  * the request it sent before is false, and the one thing that must not happen
100
100
  * is serving the earlier request's response to it.
101
- * - **Unavailable.** The original response was too large to record. Refused
102
- * rather than re-executed, which is the fail-closed direction: an error the
103
- * caller can act on, instead of the duplicate work the key was sent to avoid.
101
+ * - **Unavailable.** The original response was too large to record, or a subject
102
+ * erasure has since redacted it (#1632). Refused rather than re-executed, which
103
+ * is the fail-closed direction: an error the caller can act on, instead of the
104
+ * duplicate work the key was sent to avoid.
104
105
  */
105
106
  export declare function replayFor(key: string, fingerprint: string, row: IdempotencyRow): IdempotentReplay;
106
107
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../src/idempotency.ts"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,YAAY,EAClB,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,+nCAuB3B,CAAC;AAEF,wDAAwD;AACxD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CACxC;AAED,kFAAkF;AAClF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CACvC;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAEhE;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAMtD;AAED,2CAA2C;AAC3C,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,MAAM,GACV;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAO3C;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CACvB,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,cAAc,GAClB,gBAAgB,CAsBlB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,OAAO,EACf,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,EAAE,EAAE,MAAM,GACT;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,CAAC,CAAA;CAAE,CAmBzG;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAA;CAAE,CAMxF;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAKpE"}
1
+ {"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../src/idempotency.ts"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,YAAY,EAClB,MAAM,yBAAyB,CAAC;AAGjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,+nCAuB3B,CAAC;AAEF,wDAAwD;AACxD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CACxC;AAED,kFAAkF;AAClF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CACvC;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAEhE;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAMtD;AAED,2CAA2C;AAC3C,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,MAAM,GACV;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAO3C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CACvB,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,cAAc,GAClB,gBAAgB,CAgClB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,OAAO,EACf,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,EAAE,EAAE,MAAM,GACT;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,CAAC,CAAA;CAAE,CAmBzG;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAA;CAAE,CAMxF;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAKpE"}
@@ -1,4 +1,5 @@
1
1
  import { IDEMPOTENCY_REPLAY_UNAVAILABLE, IDEMPOTENCY_RESULT_LIMIT, IDEMPOTENCY_RETENTION_MS, IDEMPOTENCY_REUSED, isValidIdempotencyKey, subjectRef, substratError, } from '@substrat-run/contracts';
2
+ import { isRedactedPayload } from './subject-redaction.js';
2
3
  /**
3
4
  * The spine half of request idempotency (#116) — what remembers a key, and what
4
5
  * a second request carrying it is answered with.
@@ -117,9 +118,10 @@ export function idempotencyLookupQuery(subject, key) {
117
118
  * - **Reuse.** Same key, different request. The client's assertion that this is
118
119
  * the request it sent before is false, and the one thing that must not happen
119
120
  * is serving the earlier request's response to it.
120
- * - **Unavailable.** The original response was too large to record. Refused
121
- * rather than re-executed, which is the fail-closed direction: an error the
122
- * caller can act on, instead of the duplicate work the key was sent to avoid.
121
+ * - **Unavailable.** The original response was too large to record, or a subject
122
+ * erasure has since redacted it (#1632). Refused rather than re-executed, which
123
+ * is the fail-closed direction: an error the caller can act on, instead of the
124
+ * duplicate work the key was sent to avoid.
123
125
  */
124
126
  export function replayFor(key, fingerprint, row) {
125
127
  if (row.fingerprint !== fingerprint) {
@@ -131,10 +133,16 @@ export function replayFor(key, fingerprint, row) {
131
133
  'so this retry cannot be answered from it — the original request did complete, ' +
132
134
  'and re-running it would duplicate the work the key exists to prevent', { reason: IDEMPOTENCY_REPLAY_UNAVAILABLE });
133
135
  }
134
- return {
135
- result: row.result === null ? undefined : JSON.parse(row.result),
136
- entityVersion: row.entity_version,
137
- };
136
+ const result = row.result === null ? undefined : JSON.parse(row.result);
137
+ // A subject erasure (#1632) replaced the recorded response with its tombstone. Refused like
138
+ // an oversized one, for the same reason: the original did complete, so running it again is
139
+ // the one answer that is certainly wrong, and the tombstone is not the response it sent.
140
+ if (isRedactedPayload(result)) {
141
+ throw substratError('conflict', `the original response for Idempotency-Key '${key}' was redacted by a subject erasure, ` +
142
+ 'so this retry cannot be answered from it — the original request did complete, ' +
143
+ 'and re-running it would duplicate the work the key exists to prevent', { reason: IDEMPOTENCY_REPLAY_UNAVAILABLE });
144
+ }
145
+ return { result, entityVersion: row.entity_version };
138
146
  }
139
147
  /**
140
148
  * The row a completed operation leaves behind.
@@ -1 +1 @@
1
- {"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../src/idempotency.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../src/idempotency.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export type { AccessLogFilter, AttachmentUploadInput, AuditLogFilter, BlobStoreProvisionInput, BlobStoreRecord, ConsumerHandler, ImportHandler, ExecutorDeadLetter, ExecutorDrainReport, ExecutorHandler, ExecutorRetryPolicy, ConnectorConnection, ScopedConnectorConnection, ConnectorContext, ConnectorHandler, ConnectorOptions, ConnectorRequestInit, ConnectorResponse, Clock, FetchLike, GuardPredicate, HostAdmin, MigrateScopeOutcome, MigrationFrontier, ModuleRegistration, ConsumersOf, EventContract, EventPayloadOf, EventTypeOf, TypedConsumerHandler, TypedConsumers, OpenedAttachment, OperationContext, PageParams, OperationHandler, AppliedMigration, OpsFailureFilter, OpsFailureInput, IssueFilter, SweepRunFilter, SweepRunInput, ProvisionScopeInput, RoleFilter, FreshnessRegistration, FreshnessReport, LiveChange, LiveReadSurface, LiveRefusal, LiveUpgradeRequest, ScheduleRegistration, ScheduleRunReport, ScopeAttachments, ScopedSql, ScopeFilter, ScopeHost, ScopeStub, ScopeStubOptions, InvokeOptions, EmittedEvent, EmittedReport, SqlMigration, SqlValue, TenantBlobStore, TenantRelationalStore, TenantStoreProvisionInput, TenantStoreRecord, } from './scope-host.js';
1
+ export type { AccessLogFilter, AttachmentSearchOptions, AttachmentUploadInput, AuditLogFilter, BlobStoreProvisionInput, BlobStoreRecord, ConsumerHandler, ImportHandler, ExecutorDeadLetter, ExecutorDrainReport, ExecutorHandler, ExecutorRetryPolicy, ConnectorConnection, ScopedConnectorConnection, ConnectorContext, ConnectorHandler, ConnectorOptions, ConnectorRequestInit, ConnectorResponse, Clock, FetchLike, GuardPredicate, HostAdmin, MigrateScopeOutcome, MigrationFrontier, ModuleRegistration, ConsumersOf, EventContract, EventPayloadOf, EventTypeOf, TypedConsumerHandler, TypedConsumers, OpenedAttachment, OperationContext, PageParams, OperationHandler, AppliedMigration, OpsFailureFilter, OpsFailureInput, IssueFilter, TelemetryPruneReport, SweepRunFilter, SweepRunInput, ProvisionScopeInput, RoleFilter, FreshnessRegistration, FreshnessReport, LiveChange, LiveReadSurface, LiveRefusal, LiveUpgradeRequest, ScheduleRegistration, ScheduleRunReport, ScopeAttachments, ScopedSql, ScopeFilter, ScopeHost, ScopeStub, ScopeStubOptions, InvokeOptions, EmittedEvent, EmittedReport, SqlMigration, SqlValue, TenantBlobStore, TenantRelationalStore, TenantStoreProvisionInput, TenantStoreRecord, } from './scope-host.js';
2
2
  export type { AnalyticsEngineDatasetLike, ConnectionUseOutcome, ConnectionUseTiming, ConnectorCallErrorType, ConnectorCallRecord, ConnectorCallRecorder, CountingConnectorCallRecorder, } from './connector-calls.js';
3
3
  export { CONNECTOR_CALL_DATA_POINT_LAYOUT, CONNECTOR_CALL_ERROR_TYPES, analyticsEngineConnectorCallRecorder, connectorCallDataPoint, connectorCallErrorType, connectorCallRecord, noopConnectorCallRecorder, recordConnectorCall, settleConnectionUse, } from './connector-calls.js';
4
- export { assertRedrainWindow, attachmentBlobKey, consumersFor, entitlementDenial, backoffAt, globalFetch, parseValidationRecords, resolveRetryPolicy, OPS_FAILURE_RETENTION_DAYS, SWEEP_RUN_RETENTION_DAYS, SWEEP_RUNS_INTENT_INDEX, sweepRunsIntentHasKind, ISSUE_RETENTION_DAYS, EMITTED_REPORT_CAP, LIVE_MODE_HEADER, isUpgradeRequest, } from './scope-host.js';
4
+ export { assertRedrainWindow, attachmentBlobKey, attachmentSha256, consumersFor, entitlementDenial, backoffAt, globalFetch, parseValidationRecords, resolveRetryPolicy, OPS_FAILURE_RETENTION_DAYS, SWEEP_RUN_RETENTION_DAYS, SWEEP_RUNS_INTENT_INDEX, sweepRunsIntentHasKind, ISSUE_RETENTION_DAYS, telemetryRetentionStatements, TELEMETRY_PRUNE_BATCH, assertRowLimit, assertRowOffset, EMITTED_REPORT_CAP, LIVE_MODE_HEADER, isUpgradeRequest, } from './scope-host.js';
5
5
  export { isSecretBoxConfigured, SecretBoxUnconfiguredError, unconfiguredSecretBox, webCryptoSecretBox, } from './secret-box.js';
6
6
  export type { SealedSecret, SecretBox } from './secret-box.js';
7
7
  export { ConnectionSealingKeyUnavailableError, generateSealingKeyPair, noSealingKeyMessage, openSealed, sealTo, SealedKeyUnavailableError, } from './sealed-box.js';
@@ -23,6 +23,10 @@ export { createAtomic } from './sub-transaction.js';
23
23
  export type { RunSub, AtomicMarks } from './sub-transaction.js';
24
24
  export { DEFAULT_SEARCH_LIMIT, MAX_SEARCH_LIMIT, MIN_SEARCH_TERM, NotSearchable, SEARCH_INDEX_PREFIX, SearchTermTooShort, isSearchIndexTable, searchIndexDdl, searchIndexMigrations, searchIndexPlans, searchLimit, searchMatchExpression, searchPlansByEntityType, searchQuery, } from './search-index.js';
25
25
  export type { SearchHit, SearchIndexPlan, SearchOptions, SearchTokenizer, SearchableDeclaration, } from './search-index.js';
26
+ export { ATTACHMENT_SEARCH_OWNER_MAX, ATTACHMENT_SEARCH_OWNERS_SQL, ATTACHMENT_SEARCH_SQL, ATTACHMENT_SEARCH_TOO_MANY_OWNERS, ATTACHMENT_TEXT_DDL, ATTACHMENT_TEXT_JOB, ATTACHMENT_TEXT_MODULE, assertJobRegistrable, attachmentRecordOfRow, attachmentTextJob, enqueueAttachmentText, isAttachmentTextRun, readAttachmentText, reconcileAttachmentText, recordAttachmentText, searchAttachments, } from './attachment-text.js';
27
+ export type { AttachmentRowShape, AttachmentSearchGate, AttachmentTextSource, AttachmentTextState, AttachmentTextStatus, } from './attachment-text.js';
28
+ export { DEFAULT_ATTACHMENT_TEXT_BOUNDS, assertAttachmentExtractors, assertAttachmentTextBounds, chooseAttachmentExtractor, inputBoundRefusal, isPositiveIntegerBound, mediaTypeOf, normalizeExtractedText, runAttachmentExtractor, truncateUtf8, } from './attachment-extractor.js';
29
+ export type { AttachmentExtractor, AttachmentExtractorInput, AttachmentExtractorResult, AttachmentTextBounds, ExtractionOutcome, ExtractionSignal, } from './attachment-extractor.js';
26
30
  export { FilterNotDeclared, LIST_INDEX_PREFIX, NotListable, SortNotDeclared, cursorOf, isListIndexName, listIndexColumns, listIndexDdl, listIndexMigrations, listIndexPlans, listPlansByEntityType, listQuery, splitCursor, } from './list-index.js';
27
31
  export type { ComposedListQuery, ListDeclaration, ListIndexPlan, ListQueryParams, } from './list-index.js';
28
32
  export { moduleMigrations } from './module-migrations.js';
@@ -41,8 +45,8 @@ export { signConnectState, verifyConnectState, ConnectStateError, CONNECT_STATE_
41
45
  export type { ConnectStateClaim } from './connect-state.js';
42
46
  export { PLATFORM_REQUEST_COLUMNS, platformRequestHistoryQuery, platformRequestOf, UNDECODED_REQUESTER, } from './platform-request-query.js';
43
47
  export type { PlatformRequestRawRow } from './platform-request-query.js';
44
- export { CANCELLED_INTENT_NOTE, CANCELLED_JOB_NOTE, DELIVERY_ERROR_REDACTION_SQL, intentPayloadCarriesSubject, JOB_RUN_REDACTION_SQL, JOB_STEP_REDACTION_SQL, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, REDACTED_DELIVERY_NOTE, REDACTED_INTENT_MARKER, REDACTED_INTENT_NOTE, REDACTED_JOB_NOTE, redactedIntentPayload, redactSubjectJobRuns, } from './subject-redaction.js';
45
- export type { LegacySubjectRedactionCounts, PlatformRequestRedactionCandidate, RedactionSql, SubjectRedactionCounts, } from './subject-redaction.js';
48
+ export { CANCELLED_INTENT_NOTE, CANCELLED_JOB_NOTE, DELIVERY_ERROR_REDACTION_SQL, intentPayloadCarriesSubject, JOB_RUN_REDACTION_SQL, JOB_STEP_REDACTION_SQL, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, REDACTED_DELIVERY_NOTE, REDACTED_INTENT_MARKER, REDACTED_INTENT_NOTE, REDACTED_JOB_NOTE, redactedIntentPayload, redactSubjectJobRuns, REDACTED_FAILURE_NOTE, redactSubjectScopeText, ISSUE_EXEMPLAR_OWNER_BACKFILL_SQL, issueExemplarOwner, redactSubjectDirectoryText, platformIntentFailureMessage, intentIdOfFailureMessage, } from './subject-redaction.js';
49
+ export type { LegacySubjectRedactionCounts, PlatformRequestRedactionCandidate, RedactionSql, SubjectRedactionCounts, SubjectTextTarget, IssueExemplarOwner, } from './subject-redaction.js';
46
50
  export { effectiveRoleGrantQuery, seatScopeTuple } from './scope-tuple-seat.js';
47
51
  export { repointScopeGrants, type RepointSource } from './scope-repoint.js';
48
52
  export { LEGACY_SCOPE_ROWS_BACKFILL, assertDirectoryTablesBuilt, assertSpineTablesBuilt, dumpRowsInsert, isSpineTable, loadDirectoryDump, spineColumnAdditions, spineRowsInsert, type KernelColumnsOf } from './spine-restore.js';