@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,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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"}
|