@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,180 @@
1
+ /**
2
+ * The attachment extractor seam (#1575, K-43): where file formats stop and text begins.
3
+ *
4
+ * **The kernel indexes attachment text; it does not parse file formats** (K-43). Every
5
+ * parser — text and HTML decoding, the zip reader and its inflate budget, DOCX/XLSX/PPTX,
6
+ * and later PDF and OCR — lives behind this seam in a host-side package
7
+ * (`@substrat-run/attachment-extractors`), which neither the kernel nor an adapter imports.
8
+ * Whoever constructs the host passes the extractors in; a host given none records every
9
+ * type `unsupported`, with that reason, which is a valid configuration rather than a broken
10
+ * one. `lint:deps` refuses an import of that package from the kernel or an adapter.
11
+ *
12
+ * The parsers are the riskiest code in the feature — a zip reader exists to be handed hostile
13
+ * input — so this file holds them to the guarantees that protect the SCOPE, whatever an
14
+ * extractor does:
15
+ *
16
+ * - **Declarations are checked when the host is built** (`assertAttachmentExtractors`): a
17
+ * name, `accepts` and `extract`, and a `maxInputBytes` that is a positive integer if it is
18
+ * given at all. A host refuses a list it could not honour rather than misreading it later.
19
+ * - **The input bound is judged before a byte is fetched**, on the RECORDED size: the kernel's
20
+ * ceiling FIRST and on its own, then the extractor's declared bound if it is a valid one.
21
+ * Neither can widen the other — a declaration that slipped past the check (a `NaN`) is
22
+ * ignored, never allowed to disable the ceiling.
23
+ * - **A throw is an outcome, not a retry.** An extractor that throws records `failed`, so a
24
+ * file that crashes its parser cannot fail its job forever. The thrown message is NOT
25
+ * recorded: it is the parser's text, and it may quote the file.
26
+ * - **The result is validated.** Anything but exactly one of `{ text }` or `{ failed }` is
27
+ * `failed`.
28
+ * - **The output cap is enforced here, after the extractor returns** — normalized, then cut to
29
+ * `maxTextBytes` of UTF-8 on a code point boundary and recorded `truncated`. An extractor
30
+ * is told the cap as a hint (so it can stop reading early) and cannot exceed it.
31
+ *
32
+ * ## The time budget is cooperative, and what that means
33
+ *
34
+ * An extractor that has not answered within `timeoutMs` is recorded `failed`, its `signal` is
35
+ * aborted, and anything it answers afterwards is DISCARDED — including an answer that arrives
36
+ * "late" because the extractor never yielded: after it returns, the kernel gives the timer the
37
+ * turn it was owed, and a result the deadline passed is never indexed.
38
+ *
39
+ * What the kernel cannot do from inside one isolate is STOP code that does not yield. A
40
+ * synchronous loop holds the thread until it returns, timer or not; the abort is a request,
41
+ * honoured by an extractor that checks its `signal` and yields between units of work (the
42
+ * bundled parsers do, every 64 KiB). So the guarantee is: a cooperative extractor stops
43
+ * promptly, an uncooperative one cannot get its late answer indexed, and only the runtime's
44
+ * own CPU limit ends it sooner. A hard deadline on uncooperative code needs process or
45
+ * isolate isolation — a host can provide it by running its extractors in a separate worker
46
+ * and handing the kernel a thin extractor that calls it. Under K-43 an extractor is
47
+ * HOST-supplied code, trusted as the host is; the budget protects the scope from a slow or
48
+ * broken parser, not from a hostile one, and the bundled parsers bound their own CPU by
49
+ * construction (linear scans under fixed input caps — their package says how far).
50
+ */
51
+ /**
52
+ * The cancellation an extractor is handed — the runtime's own `AbortSignal`, typed here by the
53
+ * one member the kernel promises: once `aborted` is true, nothing the extractor answers will be
54
+ * used, and it should stop.
55
+ */
56
+ export interface ExtractionSignal {
57
+ readonly aborted: boolean;
58
+ }
59
+ /** What an extractor is handed. `maxTextBytes` is the kernel's output cap, as a hint. */
60
+ export interface AttachmentExtractorInput {
61
+ readonly body: Uint8Array;
62
+ readonly contentType: string;
63
+ readonly filename: string;
64
+ /** The kernel cuts the text to this many UTF-8 bytes anyway; an extractor may stop early. */
65
+ readonly maxTextBytes: number;
66
+ /** Aborted when the time budget runs out. A cooperative extractor checks it and yields. */
67
+ readonly signal: ExtractionSignal;
68
+ }
69
+ /**
70
+ * What an extractor answers: the file's text, or a reason it has none. `failed` is for a
71
+ * file that is not what it says or that broke a bound; its reason is recorded verbatim, so it
72
+ * must never quote the file. `truncated` says the extractor itself stopped early.
73
+ */
74
+ export type AttachmentExtractorResult = {
75
+ text: string;
76
+ truncated?: boolean;
77
+ } | {
78
+ failed: string;
79
+ };
80
+ /**
81
+ * One format's parser (K-43). The host is handed a list; the first that `accepts` a file
82
+ * extracts it.
83
+ */
84
+ export interface AttachmentExtractor {
85
+ /** Recorded as the row's `extractor` — `text`, `html`, `docx`. Unique within a host. */
86
+ readonly name: string;
87
+ /**
88
+ * The largest file this extractor will be handed, judged on the recorded size BEFORE the
89
+ * bytes are fetched — a positive integer. The kernel's own ceiling applies first, whatever
90
+ * this says.
91
+ */
92
+ readonly maxInputBytes?: number;
93
+ /** Whether this extractor reads a file of this declared type (and name). */
94
+ accepts(contentType: string, filename: string): boolean;
95
+ extract(input: AttachmentExtractorInput): Promise<AttachmentExtractorResult>;
96
+ }
97
+ /** What one extraction recorded. `indexed` is the only outcome that carries text. */
98
+ export type ExtractionOutcome = {
99
+ status: 'indexed';
100
+ extractor: string;
101
+ text: string;
102
+ truncated: boolean;
103
+ } | {
104
+ status: 'empty';
105
+ extractor: string;
106
+ } | {
107
+ status: 'unsupported';
108
+ detail: string;
109
+ } | {
110
+ status: 'failed';
111
+ extractor: string | null;
112
+ detail: string;
113
+ };
114
+ /** The bounds the kernel holds every extractor to — each a positive integer. */
115
+ export interface AttachmentTextBounds {
116
+ /** The ceiling on a file's recorded size, applied before any extractor's own. */
117
+ readonly maxInputBytes: number;
118
+ /** UTF-8 bytes of text one attachment may contribute to the index. */
119
+ readonly maxTextBytes: number;
120
+ /** How long one extraction may take before it is recorded `failed`. */
121
+ readonly timeoutMs: number;
122
+ }
123
+ /**
124
+ * The defaults. `maxTextBytes` is 512 KiB of UTF-8 — a few hundred pages of prose, and a
125
+ * quarter of the ~2 MB a Durable Object row holds. 32 MiB of input is what a Worker can hold
126
+ * as bytes beside what it decodes from them.
127
+ */
128
+ export declare const DEFAULT_ATTACHMENT_TEXT_BOUNDS: AttachmentTextBounds;
129
+ /** A bound that means something: a positive integer. `NaN`, `Infinity`, `0`, `-1` and `'8'` do not. */
130
+ export declare function isPositiveIntegerBound(value: unknown): value is number;
131
+ /** `Text/Plain; charset=UTF-8` → `text/plain`. */
132
+ export declare function mediaTypeOf(contentType: string): string;
133
+ /** Refuse bounds the kernel could not honour: each must be a positive integer. */
134
+ export declare function assertAttachmentTextBounds(bounds: AttachmentTextBounds): void;
135
+ /**
136
+ * Refuse, when the host is built, an extractor list it could not honour: a missing or
137
+ * repeated name, an `accepts` or `extract` that is not a function, or a `maxInputBytes` that
138
+ * is not a positive integer. Failing here, once, is the alternative to misreading the
139
+ * declaration on every job.
140
+ */
141
+ export declare function assertAttachmentExtractors(extractors: readonly AttachmentExtractor[]): void;
142
+ /**
143
+ * Why a file of `size` bytes may not be handed to `extractor`, or null when it may.
144
+ *
145
+ * The kernel's ceiling is judged FIRST and on its own, then the extractor's declaration —
146
+ * and only a valid declaration, so one that slipped past `assertAttachmentExtractors` can
147
+ * narrow nothing and widen nothing. Never a `Math.min` over the two: a `NaN` there makes every
148
+ * comparison false, which is the ceiling switched off.
149
+ */
150
+ export declare function inputBoundRefusal(size: number, extractor: Pick<AttachmentExtractor, 'maxInputBytes'>, bounds: Pick<AttachmentTextBounds, 'maxInputBytes'>): string | null;
151
+ /** The first extractor that reads this file, or none. An `accepts` that throws reads as a no. */
152
+ export declare function chooseAttachmentExtractor(extractors: readonly AttachmentExtractor[], contentType: string, filename: string): AttachmentExtractor | undefined;
153
+ /**
154
+ * Cut `text` to at most `maxBytes` of UTF-8, on a code point boundary.
155
+ *
156
+ * Encodes once and backs off over continuation bytes (`10xxxxxx`), so a cut never splits
157
+ * a multi-byte character into a replacement character at the end of the index.
158
+ */
159
+ export declare function truncateUtf8(text: string, maxBytes: number): {
160
+ text: string;
161
+ truncated: boolean;
162
+ };
163
+ /**
164
+ * Whitespace collapsed, control characters dropped, line structure kept.
165
+ *
166
+ * For the index whitespace is noise, and for the cap it is worse: a spreadsheet's padding
167
+ * or an HTML file's indentation would spend the per-attachment budget on nothing.
168
+ */
169
+ export declare function normalizeExtractedText(text: string): string;
170
+ /**
171
+ * Run one extractor and turn whatever it does into an outcome the kernel can record: the
172
+ * time budget, the throw, the shape and the output cap, all judged here, after it returns.
173
+ * See this file's header for what the budget can and cannot stop.
174
+ */
175
+ export declare function runAttachmentExtractor(extractor: AttachmentExtractor, input: {
176
+ body: Uint8Array;
177
+ contentType: string;
178
+ filename: string;
179
+ }, bounds?: AttachmentTextBounds): Promise<ExtractionOutcome>;
180
+ //# sourceMappingURL=attachment-extractor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-extractor.d.ts","sourceRoot":"","sources":["../src/attachment-extractor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,230 @@
1
+ /**
2
+ * The attachment extractor seam (#1575, K-43): where file formats stop and text begins.
3
+ *
4
+ * **The kernel indexes attachment text; it does not parse file formats** (K-43). Every
5
+ * parser — text and HTML decoding, the zip reader and its inflate budget, DOCX/XLSX/PPTX,
6
+ * and later PDF and OCR — lives behind this seam in a host-side package
7
+ * (`@substrat-run/attachment-extractors`), which neither the kernel nor an adapter imports.
8
+ * Whoever constructs the host passes the extractors in; a host given none records every
9
+ * type `unsupported`, with that reason, which is a valid configuration rather than a broken
10
+ * one. `lint:deps` refuses an import of that package from the kernel or an adapter.
11
+ *
12
+ * The parsers are the riskiest code in the feature — a zip reader exists to be handed hostile
13
+ * input — so this file holds them to the guarantees that protect the SCOPE, whatever an
14
+ * extractor does:
15
+ *
16
+ * - **Declarations are checked when the host is built** (`assertAttachmentExtractors`): a
17
+ * name, `accepts` and `extract`, and a `maxInputBytes` that is a positive integer if it is
18
+ * given at all. A host refuses a list it could not honour rather than misreading it later.
19
+ * - **The input bound is judged before a byte is fetched**, on the RECORDED size: the kernel's
20
+ * ceiling FIRST and on its own, then the extractor's declared bound if it is a valid one.
21
+ * Neither can widen the other — a declaration that slipped past the check (a `NaN`) is
22
+ * ignored, never allowed to disable the ceiling.
23
+ * - **A throw is an outcome, not a retry.** An extractor that throws records `failed`, so a
24
+ * file that crashes its parser cannot fail its job forever. The thrown message is NOT
25
+ * recorded: it is the parser's text, and it may quote the file.
26
+ * - **The result is validated.** Anything but exactly one of `{ text }` or `{ failed }` is
27
+ * `failed`.
28
+ * - **The output cap is enforced here, after the extractor returns** — normalized, then cut to
29
+ * `maxTextBytes` of UTF-8 on a code point boundary and recorded `truncated`. An extractor
30
+ * is told the cap as a hint (so it can stop reading early) and cannot exceed it.
31
+ *
32
+ * ## The time budget is cooperative, and what that means
33
+ *
34
+ * An extractor that has not answered within `timeoutMs` is recorded `failed`, its `signal` is
35
+ * aborted, and anything it answers afterwards is DISCARDED — including an answer that arrives
36
+ * "late" because the extractor never yielded: after it returns, the kernel gives the timer the
37
+ * turn it was owed, and a result the deadline passed is never indexed.
38
+ *
39
+ * What the kernel cannot do from inside one isolate is STOP code that does not yield. A
40
+ * synchronous loop holds the thread until it returns, timer or not; the abort is a request,
41
+ * honoured by an extractor that checks its `signal` and yields between units of work (the
42
+ * bundled parsers do, every 64 KiB). So the guarantee is: a cooperative extractor stops
43
+ * promptly, an uncooperative one cannot get its late answer indexed, and only the runtime's
44
+ * own CPU limit ends it sooner. A hard deadline on uncooperative code needs process or
45
+ * isolate isolation — a host can provide it by running its extractors in a separate worker
46
+ * and handing the kernel a thin extractor that calls it. Under K-43 an extractor is
47
+ * HOST-supplied code, trusted as the host is; the budget protects the scope from a slow or
48
+ * broken parser, not from a hostile one, and the bundled parsers bound their own CPU by
49
+ * construction (linear scans under fixed input caps — their package says how far).
50
+ */
51
+ /**
52
+ * The defaults. `maxTextBytes` is 512 KiB of UTF-8 — a few hundred pages of prose, and a
53
+ * quarter of the ~2 MB a Durable Object row holds. 32 MiB of input is what a Worker can hold
54
+ * as bytes beside what it decodes from them.
55
+ */
56
+ export const DEFAULT_ATTACHMENT_TEXT_BOUNDS = {
57
+ maxInputBytes: 32 * 1024 * 1024,
58
+ maxTextBytes: 512 * 1024,
59
+ timeoutMs: 30_000,
60
+ };
61
+ /** A bound that means something: a positive integer. `NaN`, `Infinity`, `0`, `-1` and `'8'` do not. */
62
+ export function isPositiveIntegerBound(value) {
63
+ return typeof value === 'number' && Number.isInteger(value) && value > 0;
64
+ }
65
+ /** `Text/Plain; charset=UTF-8` → `text/plain`. */
66
+ export function mediaTypeOf(contentType) {
67
+ return (contentType.split(';')[0] ?? '').trim().toLowerCase();
68
+ }
69
+ /** Refuse bounds the kernel could not honour: each must be a positive integer. */
70
+ export function assertAttachmentTextBounds(bounds) {
71
+ for (const key of ['maxInputBytes', 'maxTextBytes', 'timeoutMs']) {
72
+ if (!isPositiveIntegerBound(bounds[key])) {
73
+ throw new Error(`attachment text bound ${key} must be a positive integer, not ${String(bounds[key])}`);
74
+ }
75
+ }
76
+ }
77
+ /**
78
+ * Refuse, when the host is built, an extractor list it could not honour: a missing or
79
+ * repeated name, an `accepts` or `extract` that is not a function, or a `maxInputBytes` that
80
+ * is not a positive integer. Failing here, once, is the alternative to misreading the
81
+ * declaration on every job.
82
+ */
83
+ export function assertAttachmentExtractors(extractors) {
84
+ const seen = new Set();
85
+ for (const e of extractors) {
86
+ if (typeof e?.name !== 'string' || !/^[a-z0-9][a-z0-9._-]{0,63}$/.test(e.name)) {
87
+ throw new Error(`attachment extractor name '${String(e?.name)}' is not a short lowercase identifier`);
88
+ }
89
+ if (typeof e.accepts !== 'function' || typeof e.extract !== 'function') {
90
+ throw new Error(`attachment extractor '${e.name}' must have accepts() and extract()`);
91
+ }
92
+ if (e.maxInputBytes !== undefined && !isPositiveIntegerBound(e.maxInputBytes)) {
93
+ throw new Error(`attachment extractor '${e.name}' declares maxInputBytes ${String(e.maxInputBytes)}, ` +
94
+ 'which is not a positive integer');
95
+ }
96
+ if (seen.has(e.name))
97
+ throw new Error(`attachment extractor '${e.name}' is registered twice`);
98
+ seen.add(e.name);
99
+ }
100
+ }
101
+ /**
102
+ * Why a file of `size` bytes may not be handed to `extractor`, or null when it may.
103
+ *
104
+ * The kernel's ceiling is judged FIRST and on its own, then the extractor's declaration —
105
+ * and only a valid declaration, so one that slipped past `assertAttachmentExtractors` can
106
+ * narrow nothing and widen nothing. Never a `Math.min` over the two: a `NaN` there makes every
107
+ * comparison false, which is the ceiling switched off.
108
+ */
109
+ export function inputBoundRefusal(size, extractor, bounds) {
110
+ if (!(size <= bounds.maxInputBytes)) {
111
+ return `the file is ${size} bytes, over the ${bounds.maxInputBytes}-byte input bound`;
112
+ }
113
+ const declared = extractor.maxInputBytes;
114
+ if (isPositiveIntegerBound(declared) && size > declared) {
115
+ return `the file is ${size} bytes, over the extractor's ${declared}-byte input bound`;
116
+ }
117
+ return null;
118
+ }
119
+ /** The first extractor that reads this file, or none. An `accepts` that throws reads as a no. */
120
+ export function chooseAttachmentExtractor(extractors, contentType, filename) {
121
+ return extractors.find((e) => {
122
+ try {
123
+ return e.accepts(contentType, filename) === true;
124
+ }
125
+ catch {
126
+ return false;
127
+ }
128
+ });
129
+ }
130
+ const utf8 = new TextEncoder();
131
+ /**
132
+ * Cut `text` to at most `maxBytes` of UTF-8, on a code point boundary.
133
+ *
134
+ * Encodes once and backs off over continuation bytes (`10xxxxxx`), so a cut never splits
135
+ * a multi-byte character into a replacement character at the end of the index.
136
+ */
137
+ export function truncateUtf8(text, maxBytes) {
138
+ const bytes = utf8.encode(text);
139
+ if (bytes.length <= maxBytes)
140
+ return { text, truncated: false };
141
+ let cut = maxBytes;
142
+ while (cut > 0 && (bytes[cut] & 0xc0) === 0x80)
143
+ cut -= 1;
144
+ return { text: new TextDecoder('utf-8').decode(bytes.subarray(0, cut)), truncated: true };
145
+ }
146
+ /**
147
+ * Whitespace collapsed, control characters dropped, line structure kept.
148
+ *
149
+ * For the index whitespace is noise, and for the cap it is worse: a spreadsheet's padding
150
+ * or an HTML file's indentation would spend the per-attachment budget on nothing.
151
+ */
152
+ export function normalizeExtractedText(text) {
153
+ return text
154
+ .replace(/\r\n?/g, '\n')
155
+ // C0 controls other than tab and newline, DEL, and the C1 range.
156
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, '')
157
+ .replace(/[ \t ]+/g, ' ')
158
+ .replace(/ *\n */g, '\n')
159
+ .replace(/\n{3,}/g, '\n\n')
160
+ .trim();
161
+ }
162
+ /** One turn of the event loop — a macrotask, so a due timer runs before what follows. */
163
+ const nextTurn = () => new Promise((resolve) => setTimeout(resolve, 0));
164
+ const TIMED_OUT = Symbol('timed out');
165
+ /** A recorded reason is short: an extractor's `failed` is cut, never trusted to be. */
166
+ const DETAIL_MAX = 500;
167
+ /**
168
+ * Run one extractor and turn whatever it does into an outcome the kernel can record: the
169
+ * time budget, the throw, the shape and the output cap, all judged here, after it returns.
170
+ * See this file's header for what the budget can and cannot stop.
171
+ */
172
+ export async function runAttachmentExtractor(extractor, input, bounds = DEFAULT_ATTACHMENT_TEXT_BOUNDS) {
173
+ const failed = (detail) => ({
174
+ status: 'failed',
175
+ extractor: extractor.name,
176
+ detail: detail.slice(0, DETAIL_MAX),
177
+ });
178
+ const timeout = failed(`extractor '${extractor.name}' did not answer within ${bounds.timeoutMs} ms`);
179
+ const controller = new AbortController();
180
+ let timedOut = false;
181
+ let onTimeout = () => { };
182
+ const timer = setTimeout(() => {
183
+ timedOut = true;
184
+ controller.abort();
185
+ onTimeout(TIMED_OUT);
186
+ }, bounds.timeoutMs);
187
+ // Called inside a promise chain, so a synchronous throw lands in the catch below too. Its
188
+ // eventual rejection is handled here, so an extractor abandoned at the deadline that fails
189
+ // later cannot surface as an unhandled rejection.
190
+ const work = Promise.resolve().then(() => extractor.extract({ ...input, maxTextBytes: bounds.maxTextBytes, signal: controller.signal }));
191
+ work.catch(() => { });
192
+ let result;
193
+ let threw = undefined;
194
+ let didThrow = false;
195
+ try {
196
+ result = await Promise.race([work, new Promise((resolve) => (onTimeout = resolve))]);
197
+ }
198
+ catch (err) {
199
+ didThrow = true;
200
+ threw = err;
201
+ }
202
+ // A SYNCHRONOUS extractor returns before its timer can run, however long it took. Give the
203
+ // timer the turn it was owed: if the deadline passed meanwhile, the answer is discarded.
204
+ if (!timedOut)
205
+ await nextTurn();
206
+ clearTimeout(timer);
207
+ if (timedOut || result === TIMED_OUT)
208
+ return timeout;
209
+ if (didThrow) {
210
+ // The message is the parser's text and may quote the file; only its kind is recorded.
211
+ const kind = threw instanceof Error && /^[A-Za-z][\w$]{0,63}$/.test(threw.name) ? threw.name : 'a value';
212
+ return failed(`extractor '${extractor.name}' threw (${kind})`);
213
+ }
214
+ const r = result;
215
+ if (r !== null && typeof r === 'object' && typeof r.failed === 'string' && !('text' in r))
216
+ return failed(r.failed);
217
+ if (r === null ||
218
+ typeof r !== 'object' ||
219
+ typeof r.text !== 'string' ||
220
+ // Both answers at once is no answer: neither half can be trusted over the other.
221
+ 'failed' in r ||
222
+ (r.truncated !== undefined && typeof r.truncated !== 'boolean')) {
223
+ return failed(`extractor '${extractor.name}' returned an unreadable result`);
224
+ }
225
+ const cut = truncateUtf8(normalizeExtractedText(r.text), bounds.maxTextBytes);
226
+ if (cut.text.length === 0)
227
+ return { status: 'empty', extractor: extractor.name };
228
+ return { status: 'indexed', extractor: extractor.name, text: cut.text, truncated: cut.truncated || r.truncated === true };
229
+ }
230
+ //# sourceMappingURL=attachment-extractor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-extractor.js","sourceRoot":"","sources":["../src/attachment-extractor.ts"],"names":[],"mappings":"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"}