@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.
- package/dist/attachment-extractor.d.ts +180 -0
- package/dist/attachment-extractor.d.ts.map +1 -0
- package/dist/attachment-extractor.js +230 -0
- package/dist/attachment-extractor.js.map +1 -0
- package/dist/attachment-text.d.ts +283 -0
- package/dist/attachment-text.d.ts.map +1 -0
- package/dist/attachment-text.js +497 -0
- package/dist/attachment-text.js.map +1 -0
- package/dist/idempotency.d.ts +4 -3
- package/dist/idempotency.d.ts.map +1 -1
- package/dist/idempotency.js +15 -7
- package/dist/idempotency.js.map +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/job-run.d.ts.map +1 -1
- package/dist/job-run.js +3 -2
- package/dist/job-run.js.map +1 -1
- package/dist/list-index.d.ts +2 -1
- package/dist/list-index.d.ts.map +1 -1
- package/dist/list-index.js +9 -0
- package/dist/list-index.js.map +1 -1
- package/dist/platform-sweep.d.ts +9 -21
- package/dist/platform-sweep.d.ts.map +1 -1
- package/dist/platform-sweep.js +30 -1
- package/dist/platform-sweep.js.map +1 -1
- package/dist/scope-host.d.ts +89 -1
- package/dist/scope-host.d.ts.map +1 -1
- package/dist/scope-host.js +75 -1
- package/dist/scope-host.js.map +1 -1
- package/dist/search-index.d.ts +7 -0
- package/dist/search-index.d.ts.map +1 -1
- package/dist/search-index.js +7 -0
- package/dist/search-index.js.map +1 -1
- package/dist/subject-redaction.d.ts +151 -6
- package/dist/subject-redaction.d.ts.map +1 -1
- package/dist/subject-redaction.js +212 -8
- package/dist/subject-redaction.js.map +1 -1
- 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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"}
|
package/dist/idempotency.d.ts
CHANGED
|
@@ -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
|
|
102
|
-
* rather than re-executed, which
|
|
103
|
-
* caller can act on, instead of the
|
|
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;
|
|
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"}
|
package/dist/idempotency.js
CHANGED
|
@@ -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
|
|
121
|
-
* rather than re-executed, which
|
|
122
|
-
* caller can act on, instead of the
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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.
|
package/dist/idempotency.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../src/idempotency.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,8BAA8B,EAC9B,wBAAwB,EACxB,wBAAwB,EACxB,kBAAkB,EAClB,qBAAqB,EACrB,UAAU,EACV,aAAa,GAEd,MAAM,yBAAyB,CAAC;
|
|
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';
|