/** * The attachment extractor seam (#1575, K-43): where file formats stop and text begins. * * **The kernel indexes attachment text; it does not parse file formats** (K-43). Every * parser — text and HTML decoding, the zip reader and its inflate budget, DOCX/XLSX/PPTX, * PDF, and later OCR — lives behind this seam in a host-side package * (`@substrat-run/attachment-extractors`), which neither the kernel nor an adapter imports. * Whoever constructs the host passes the extractors in; a host given none records every * type `unsupported`, with that reason, which is a valid configuration rather than a broken * one. `lint:deps` refuses an import of that package from the kernel or an adapter. * * The parsers are the riskiest code in the feature — a zip reader exists to be handed hostile * input — so this file holds them to the guarantees that protect the SCOPE, whatever an * extractor does: * * - **Declarations are checked when the host is built** (`assertAttachmentExtractors`): a * name, `accepts` and `extract`, and a `maxInputBytes` that is a positive integer if it is * given at all. A host refuses a list it could not honour rather than misreading it later. * - **The input bound is judged before a byte is fetched**, on the RECORDED size: the kernel's * ceiling FIRST and on its own, then the extractor's declared bound if it is a valid one. * Neither can widen the other — a declaration that slipped past the check (a `NaN`) is * ignored, never allowed to disable the ceiling. * - **A throw is an outcome, not a retry.** An extractor that throws records `failed`, so a * file that crashes its parser cannot fail its job forever. The thrown message is NOT * recorded: it is the parser's text, and it may quote the file. * - **The result is validated.** Anything but exactly one of `{ text }` or `{ failed }` is * `failed`. * - **The output cap is enforced here, after the extractor returns** — normalized, then cut to * `maxTextBytes` of UTF-8 on a code point boundary and recorded `truncated`. An extractor * is told the cap as a hint (so it can stop reading early) and cannot exceed it. * * ## The time budget is cooperative, and what that means * * An extractor that has not answered within `timeoutMs` is recorded `failed`, its `signal` is * aborted, and anything it answers afterwards is DISCARDED — including an answer that arrives * "late" because the extractor never yielded: after it returns, the kernel gives the timer the * turn it was owed, and a result the deadline passed is never indexed. * * What the kernel cannot do from inside one isolate is STOP code that does not yield. A * synchronous loop holds the thread until it returns, timer or not; the abort is a request, * honoured by an extractor that checks its `signal` and yields a turn at least every * `EXTRACTION_STRIDE` units of work (the bundled parsers do, every native search included). * So the guarantee is: a cooperative extractor stops * promptly, an uncooperative one cannot get its late answer indexed, and only the runtime's * own CPU limit ends it sooner. A hard deadline on uncooperative code needs process or * isolate isolation — a host can provide it by running its extractors in a separate worker * and handing the kernel a thin extractor that calls it. Under K-43 an extractor is * HOST-supplied code, trusted as the host is; the budget protects the scope from a slow or * broken parser, not from a hostile one, and the bundled parsers bound their own CPU by * construction (linear scans under fixed input caps — their package says how far). */ /** * How much work a cooperative extractor does between two checks of its `signal`: 256 Ki * units, a unit being a character scanned or decoded, or a byte inflated or decoded. Part of * the seam's contract rather than of any one parser, because it is what "stops promptly" * means above — and the one number both sides of the seam cite. */ export declare const EXTRACTION_STRIDE: number; /** * The cancellation an extractor is handed — the runtime's own `AbortSignal`, typed here by the * one member the kernel promises: once `aborted` is true, nothing the extractor answers will be * used, and it should stop. */ export interface ExtractionSignal { readonly aborted: boolean; } /** What an extractor is handed. `maxTextBytes` is the kernel's output cap, as a hint. */ export interface AttachmentExtractorInput { readonly body: Uint8Array; readonly contentType: string; readonly filename: string; /** The kernel cuts the text to this many UTF-8 bytes anyway; an extractor may stop early. */ readonly maxTextBytes: number; /** Aborted when the time budget runs out. A cooperative extractor checks it and yields. */ readonly signal: ExtractionSignal; } /** * What an extractor answers: the file's text, or a reason it has none. `failed` is for a * file that is not what it says or that broke a bound; its reason is recorded verbatim, so it * must never quote the file. `truncated` says the extractor itself stopped early. */ export type AttachmentExtractorResult = { text: string; truncated?: boolean; } | { failed: string; }; /** * One format's parser (K-43). The host is handed a list; the first that `accepts` a file * extracts it. */ export interface AttachmentExtractor { /** Recorded as the row's `extractor` — `text`, `html`, `docx`. Unique within a host. */ readonly name: string; /** * The largest file this extractor will be handed, judged on the recorded size BEFORE the * bytes are fetched — a positive integer. The kernel's own ceiling applies first, whatever * this says. */ readonly maxInputBytes?: number; /** Whether this extractor reads a file of this declared type (and name). */ accepts(contentType: string, filename: string): boolean; extract(input: AttachmentExtractorInput): Promise; } /** What one extraction recorded. `indexed` is the only outcome that carries text. */ export type ExtractionOutcome = { status: 'indexed'; extractor: string; text: string; truncated: boolean; } | { status: 'empty'; extractor: string; } | { status: 'unsupported'; detail: string; } | { status: 'failed'; extractor: string | null; detail: string; }; /** The bounds the kernel holds every extractor to — each a positive integer. */ export interface AttachmentTextBounds { /** The ceiling on a file's recorded size, applied before any extractor's own. */ readonly maxInputBytes: number; /** UTF-8 bytes of text one attachment may contribute to the index. */ readonly maxTextBytes: number; /** How long one extraction may take before it is recorded `failed`. */ readonly timeoutMs: number; } /** * The defaults. `maxTextBytes` is 512 KiB of UTF-8 — a few hundred pages of prose, and a * quarter of the ~2 MB a Durable Object row holds. 32 MiB of input is what a Worker can hold * as bytes beside what it decodes from them. */ export declare const DEFAULT_ATTACHMENT_TEXT_BOUNDS: AttachmentTextBounds; /** `Text/Plain; charset=UTF-8` → `text/plain`. */ export declare function mediaTypeOf(contentType: string): string; /** Refuse bounds the kernel could not honour: each must be a positive integer. */ export declare function assertAttachmentTextBounds(bounds: AttachmentTextBounds): void; /** * A host's `attachmentTextBounds` option, over the defaults: each given bound replaces the * default, and each may only TIGHTEN it. The defaults are sized to the platform — the text * cap to a Durable Object row, the input ceiling to a Worker's memory — so a looser bound is * one no adapter can keep, and refusing it when the host is built is cheaper than learning it * from a scope. A host under a CPU limit below the 30 s budget is the case for tightening. */ export declare function resolveAttachmentTextBounds(given?: Partial): AttachmentTextBounds; /** * Refuse, when the host is built, an extractor list it could not honour: a missing or * repeated name, an `accepts` or `extract` that is not a function, or a `maxInputBytes` that * is not a positive integer. Failing here, once, is the alternative to misreading the * declaration on every job. */ export declare function assertAttachmentExtractors(extractors: readonly AttachmentExtractor[]): void; /** * Why a file of `size` bytes may not be handed to `extractor`, or null when it may. * * The kernel's ceiling is judged FIRST and on its own, then the extractor's declaration — * and only a valid declaration, so one that slipped past `assertAttachmentExtractors` can * narrow nothing and widen nothing. Never a `Math.min` over the two: a `NaN` there makes every * comparison false, which is the ceiling switched off. */ export declare function inputBoundRefusal(size: number, extractor: Pick, bounds: Pick): string | null; /** The first extractor that reads this file, or none. An `accepts` that throws reads as a no. */ export declare function chooseAttachmentExtractor(extractors: readonly AttachmentExtractor[], contentType: string, filename: string): AttachmentExtractor | undefined; /** * Cut `text` to at most `maxBytes` of UTF-8, on a code point boundary. * * Encodes once and backs off over continuation bytes (`10xxxxxx`), so a cut never splits * a multi-byte character into a replacement character at the end of the index. */ export declare function truncateUtf8(text: string, maxBytes: number): { text: string; truncated: boolean; }; /** * The timers the time budget runs on: the deadline, and the turn owed to it after a * synchronous extractor returns. Both go through the same pair, because the guarantee is an * ORDER between them — the deadline, if it passed, runs first. The runtime's own by default; a * caller that passes others (a test, on a clock it moves itself) moves both together, so the * outcome is decided by that clock rather than by how busy the machine is (#2085). */ export interface ExtractionTimers { setTimeout(fn: () => void, ms: number): unknown; clearTimeout(handle: unknown): void; } /** * Run one extractor and turn whatever it does into an outcome the kernel can record: the * time budget, the throw, the shape and the output cap, all judged here, after it returns. * See this file's header for what the budget can and cannot stop. */ export declare function runAttachmentExtractor(extractor: AttachmentExtractor, input: { body: Uint8Array; contentType: string; filename: string; }, bounds?: AttachmentTextBounds, timers?: ExtractionTimers): Promise; //# sourceMappingURL=attachment-extractor.d.ts.map