@bluefields/extractor 0.2.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/LICENSE +661 -0
- package/dist/cache-key.d.ts +17 -0
- package/dist/cache-key.js +37 -0
- package/dist/cache-key.js.map +1 -0
- package/dist/guards.d.ts +100 -0
- package/dist/guards.js +276 -0
- package/dist/guards.js.map +1 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +281 -0
- package/dist/index.js.map +1 -0
- package/dist/js-shell-detect.d.ts +21 -0
- package/dist/js-shell-detect.js +113 -0
- package/dist/js-shell-detect.js.map +1 -0
- package/dist/links.d.ts +34 -0
- package/dist/links.js +154 -0
- package/dist/links.js.map +1 -0
- package/dist/model-picker.d.ts +36 -0
- package/dist/model-picker.js +88 -0
- package/dist/model-picker.js.map +1 -0
- package/dist/pdf.d.ts +42 -0
- package/dist/pdf.js +110 -0
- package/dist/pdf.js.map +1 -0
- package/dist/readability.d.ts +61 -0
- package/dist/readability.js +458 -0
- package/dist/readability.js.map +1 -0
- package/dist/sitemap.d.ts +74 -0
- package/dist/sitemap.js +199 -0
- package/dist/sitemap.js.map +1 -0
- package/dist/sonnet.d.ts +64 -0
- package/dist/sonnet.js +157 -0
- package/dist/sonnet.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
/**
|
|
3
|
+
* Cache-key helpers (Review #6).
|
|
4
|
+
*
|
|
5
|
+
* The cache key for an extraction is `(content_hash, schema_hash,
|
|
6
|
+
* extractor_version)`. Two customers watching the same URL with the
|
|
7
|
+
* same JSON schema and the same extractor version share the LLM call
|
|
8
|
+
* exactly once — chunk 11 wires this into the orchestrator.
|
|
9
|
+
*
|
|
10
|
+
* Schema hashing must be deterministic across key-order permutations
|
|
11
|
+
* (`{a: 1, b: 2}` and `{b: 2, a: 1}` must produce the same hash) — we
|
|
12
|
+
* canonicalize via recursive key-sort before hashing.
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
/**
|
|
16
|
+
* SHA-256 hex of the canonical JSON form of `schema`. Returns null
|
|
17
|
+
* for null/undefined/empty-object schemas (markdown-only extractions).
|
|
18
|
+
*/
|
|
19
|
+
export function schemaHash(schema) {
|
|
20
|
+
if (schema === null || schema === undefined)
|
|
21
|
+
return null;
|
|
22
|
+
if (typeof schema === 'object' && Object.keys(schema).length === 0)
|
|
23
|
+
return null;
|
|
24
|
+
const canonical = canonicalizeJson(schema);
|
|
25
|
+
return createHash('sha256').update(canonical).digest('hex');
|
|
26
|
+
}
|
|
27
|
+
function canonicalizeJson(v) {
|
|
28
|
+
if (v === null || typeof v !== 'object')
|
|
29
|
+
return JSON.stringify(v);
|
|
30
|
+
if (Array.isArray(v)) {
|
|
31
|
+
return `[${v.map(canonicalizeJson).join(',')}]`;
|
|
32
|
+
}
|
|
33
|
+
const obj = v;
|
|
34
|
+
const keys = Object.keys(obj).sort();
|
|
35
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalizeJson(obj[k])}`).join(',')}}`;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=cache-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-key.js","sourceRoot":"","sources":["../src/cache-key.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,MAAe;IACxC,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACzD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,CAAC,MAAgB,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1F,MAAM,SAAS,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC3C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,gBAAgB,CAAC,CAAU;IAClC,IAAI,CAAC,KAAK,IAAI,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IAClE,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;QACrB,OAAO,IAAI,CAAC,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IAClD,CAAC;IACD,MAAM,GAAG,GAAG,CAA4B,CAAC;IACzC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACrC,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;AAC9F,CAAC"}
|
package/dist/guards.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input pre-flight guards shared by the extraction pipeline (perf-extract).
|
|
3
|
+
*
|
|
4
|
+
* Everything here runs on the RAW HTML STRING, before any DOM is built.
|
|
5
|
+
* The point is to keep pathological inputs (50 MB bodies, 5000-deep
|
|
6
|
+
* nesting, binary blobs mislabeled as HTML) from ever reaching jsdom,
|
|
7
|
+
* whose memory footprint is a large multiple of the input size and whose
|
|
8
|
+
* recursive walks overflow the stack on deep markup. Guards must PREVENT
|
|
9
|
+
* the allocation — an OOM cannot be caught after the fact.
|
|
10
|
+
*
|
|
11
|
+
* The fetcher caps response bodies at 5 MiB (packages/fetcher,
|
|
12
|
+
* DEFAULT_MAX_BODY_BYTES), so real traffic never trips the size ceiling;
|
|
13
|
+
* these are defense-in-depth for other ingress paths and future callers.
|
|
14
|
+
*/
|
|
15
|
+
/** Why the extractor degraded instead of running the full pipeline. */
|
|
16
|
+
export type DegradeReason = 'input_too_large' | 'nesting_too_deep' | 'binary_input' | 'parse_failed' | 'output_truncated' | 'table_recovery_capped';
|
|
17
|
+
/** Structured degrade signal attached to `ExtractedMarkdown.degraded`. */
|
|
18
|
+
export interface ExtractDegradeInfo {
|
|
19
|
+
reasons: DegradeReason[];
|
|
20
|
+
/** Short human-readable specifics (sizes, counts). */
|
|
21
|
+
detail?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Hard ceiling on the HTML string length we will hand to jsdom.
|
|
25
|
+
*
|
|
26
|
+
* Measured (profile run 2026-07-01, node --max-old-space-size=1024,
|
|
27
|
+
* mirroring the 1 GB Fly VM): peak RSS is linear at ~270 MB per MB of
|
|
28
|
+
* article HTML and ~2x that for table-dense pages; a 4.25 MB single-table
|
|
29
|
+
* page OOM-aborts outright and a 25 MB article GC-thrashes forever. The
|
|
30
|
+
* HTTP/Bright-Data fetch tiers cap bodies at 5 MiB but the browser tier
|
|
31
|
+
* (page.content()) is UNCAPPED, so oversized pages do reach the extractor
|
|
32
|
+
* in production. 1.5M chars keeps a worst-case single extraction around
|
|
33
|
+
* ~0.5-0.9 GB peak and comfortably clears the largest known-legit corpus
|
|
34
|
+
* fixture (wiki-zh-python, 1.05M chars). Inputs beyond it degrade to a
|
|
35
|
+
* bounded tag-strip instead of a DOM parse.
|
|
36
|
+
*/
|
|
37
|
+
export declare const MAX_EXTRACT_HTML_CHARS = 1500000;
|
|
38
|
+
/** Ceiling on degraded-path text output (markdown/text fields). */
|
|
39
|
+
export declare const MAX_DEGRADED_TEXT_CHARS = 1000000;
|
|
40
|
+
/** Tables with more <tr> than this are never snapshotted (checked BEFORE serializing). */
|
|
41
|
+
export declare const MAX_CAPTURE_TABLE_ROWS = 2000;
|
|
42
|
+
/** A single captured table's outerHTML may not exceed this. */
|
|
43
|
+
export declare const MAX_SINGLE_TABLE_CHARS = 1000000;
|
|
44
|
+
/** Total chars across all captured tables. */
|
|
45
|
+
export declare const MAX_TOTAL_CAPTURED_CHARS = 2000000;
|
|
46
|
+
/** Max number of tables snapshotted per page. */
|
|
47
|
+
export declare const MAX_CAPTURED_TABLES = 100;
|
|
48
|
+
/** Skip the recovery re-parse (second jsdom) when the article content exceeds this. */
|
|
49
|
+
export declare const MAX_RECOVERY_CONTENT_CHARS = 1500000;
|
|
50
|
+
/**
|
|
51
|
+
* Deepest markup we'll build a DOM for. Comfortably above the 2000-deep
|
|
52
|
+
* contract fixture (which still extracts normally) and below the ~4000-deep
|
|
53
|
+
* zone where jsdom parse time explodes and serialization overflows the stack.
|
|
54
|
+
*/
|
|
55
|
+
export declare const MAX_SAFE_NESTING_DEPTH = 2500;
|
|
56
|
+
/**
|
|
57
|
+
* Estimate the maximum element-nesting depth of an HTML string WITHOUT parsing
|
|
58
|
+
* it — a running open/close balance over element tags. Conservative (it may
|
|
59
|
+
* over-count on malformed input, which only makes the guard fire sooner) and
|
|
60
|
+
* returns as soon as the cap is exceeded, so it stays cheap on huge inputs.
|
|
61
|
+
* Comments / doctype / PIs are ignored (the tag name must start with a letter).
|
|
62
|
+
*
|
|
63
|
+
* Hand-rolled char scan, NOT a regex: the previous
|
|
64
|
+
* `/<(\/?)([a-zA-Z][\w-]*)([^>]*)>/g` backtracked quadratically on
|
|
65
|
+
* '>'-free adversarial input (ReDoS on the public sandbox route). This
|
|
66
|
+
* scan is O(n) by construction and preserves the regex's semantics.
|
|
67
|
+
*/
|
|
68
|
+
export declare function estimateMaxNestingDepth(html: string): number;
|
|
69
|
+
/**
|
|
70
|
+
* Cheap sniff for binary-pretending-to-be-HTML. True when the input starts
|
|
71
|
+
* with a known binary magic, or has a high density of NUL/C0 control
|
|
72
|
+
* characters (legit HTML text has essentially none besides whitespace —
|
|
73
|
+
* even mojibake from a wrong charset decodes to printable or C1 ranges,
|
|
74
|
+
* which we deliberately do NOT count to avoid false positives). A single
|
|
75
|
+
* stray NUL is deliberately NOT enough: a one-byte decoding artifact must
|
|
76
|
+
* not zero out an otherwise-normal page — NULs count toward the density
|
|
77
|
+
* rule instead (UTF-16-mislabeled text is ~50% NUL, real binary ~11% C0;
|
|
78
|
+
* both clear the 5% bar by a wide margin).
|
|
79
|
+
*/
|
|
80
|
+
export declare function looksLikeBinary(html: string): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Linear-time removal of <script>/<style> blocks via indexOf scanning.
|
|
83
|
+
* Replaces the old lazy-regex approach, which backtracked quadratically on
|
|
84
|
+
* adversarial input (thousands of unclosed `<script` openers). An opener
|
|
85
|
+
* with no matching closer drops the rest of the string — correct for the
|
|
86
|
+
* degraded path (script bodies are never content).
|
|
87
|
+
*/
|
|
88
|
+
export declare function stripScriptStyleBlocks(html: string): string;
|
|
89
|
+
/**
|
|
90
|
+
* Linear-time tag stripper for the degraded path, mimicking the semantics
|
|
91
|
+
* of `/<[^>]+>/g → ' '` without its backtracking (which is quadratic on
|
|
92
|
+
* '>'-free adversarial input): a complete `<...>` span with at least one
|
|
93
|
+
* char inside is replaced by a space; a `<` with no closing `>` in the
|
|
94
|
+
* remainder stays as literal text, exactly like the regex.
|
|
95
|
+
*/
|
|
96
|
+
export declare function stripTagsLinear(html: string): string;
|
|
97
|
+
/** Widest table (columns) the markdown converter will emit. */
|
|
98
|
+
export declare const MAX_TABLE_COLS = 500;
|
|
99
|
+
/** Max cells (cols × rows) the markdown converter will emit per table. */
|
|
100
|
+
export declare const MAX_TABLE_CELLS = 250000;
|
package/dist/guards.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
/**
|
|
3
|
+
* Input pre-flight guards shared by the extraction pipeline (perf-extract).
|
|
4
|
+
*
|
|
5
|
+
* Everything here runs on the RAW HTML STRING, before any DOM is built.
|
|
6
|
+
* The point is to keep pathological inputs (50 MB bodies, 5000-deep
|
|
7
|
+
* nesting, binary blobs mislabeled as HTML) from ever reaching jsdom,
|
|
8
|
+
* whose memory footprint is a large multiple of the input size and whose
|
|
9
|
+
* recursive walks overflow the stack on deep markup. Guards must PREVENT
|
|
10
|
+
* the allocation — an OOM cannot be caught after the fact.
|
|
11
|
+
*
|
|
12
|
+
* The fetcher caps response bodies at 5 MiB (packages/fetcher,
|
|
13
|
+
* DEFAULT_MAX_BODY_BYTES), so real traffic never trips the size ceiling;
|
|
14
|
+
* these are defense-in-depth for other ingress paths and future callers.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Hard ceiling on the HTML string length we will hand to jsdom.
|
|
18
|
+
*
|
|
19
|
+
* Measured (profile run 2026-07-01, node --max-old-space-size=1024,
|
|
20
|
+
* mirroring the 1 GB Fly VM): peak RSS is linear at ~270 MB per MB of
|
|
21
|
+
* article HTML and ~2x that for table-dense pages; a 4.25 MB single-table
|
|
22
|
+
* page OOM-aborts outright and a 25 MB article GC-thrashes forever. The
|
|
23
|
+
* HTTP/Bright-Data fetch tiers cap bodies at 5 MiB but the browser tier
|
|
24
|
+
* (page.content()) is UNCAPPED, so oversized pages do reach the extractor
|
|
25
|
+
* in production. 1.5M chars keeps a worst-case single extraction around
|
|
26
|
+
* ~0.5-0.9 GB peak and comfortably clears the largest known-legit corpus
|
|
27
|
+
* fixture (wiki-zh-python, 1.05M chars). Inputs beyond it degrade to a
|
|
28
|
+
* bounded tag-strip instead of a DOM parse.
|
|
29
|
+
*/
|
|
30
|
+
export const MAX_EXTRACT_HTML_CHARS = 1_500_000;
|
|
31
|
+
/** Ceiling on degraded-path text output (markdown/text fields). */
|
|
32
|
+
export const MAX_DEGRADED_TEXT_CHARS = 1_000_000;
|
|
33
|
+
// ── Table-recovery ceilings ──────────────────────────────────────────────
|
|
34
|
+
// Table capture snapshots outerHTML pre-Readability and recovery builds a
|
|
35
|
+
// SECOND jsdom of the article content — profiling shows this double-DOM is
|
|
36
|
+
// what makes table-dense pages cost ~2x articles per byte. Caps sit far
|
|
37
|
+
// above every corpus fixture (max seen: 257 rows, 13 tables, ~0.8M-char
|
|
38
|
+
// table page) so legit pages never trip them.
|
|
39
|
+
/** Tables with more <tr> than this are never snapshotted (checked BEFORE serializing). */
|
|
40
|
+
export const MAX_CAPTURE_TABLE_ROWS = 2_000;
|
|
41
|
+
/** A single captured table's outerHTML may not exceed this. */
|
|
42
|
+
export const MAX_SINGLE_TABLE_CHARS = 1_000_000;
|
|
43
|
+
/** Total chars across all captured tables. */
|
|
44
|
+
export const MAX_TOTAL_CAPTURED_CHARS = 2_000_000;
|
|
45
|
+
/** Max number of tables snapshotted per page. */
|
|
46
|
+
export const MAX_CAPTURED_TABLES = 100;
|
|
47
|
+
/** Skip the recovery re-parse (second jsdom) when the article content exceeds this. */
|
|
48
|
+
export const MAX_RECOVERY_CONTENT_CHARS = 1_500_000;
|
|
49
|
+
/**
|
|
50
|
+
* Deepest markup we'll build a DOM for. Comfortably above the 2000-deep
|
|
51
|
+
* contract fixture (which still extracts normally) and below the ~4000-deep
|
|
52
|
+
* zone where jsdom parse time explodes and serialization overflows the stack.
|
|
53
|
+
*/
|
|
54
|
+
export const MAX_SAFE_NESTING_DEPTH = 2500;
|
|
55
|
+
/** HTML void elements — they never open a nesting level. */
|
|
56
|
+
const VOID_ELEMENTS = new Set([
|
|
57
|
+
'area',
|
|
58
|
+
'base',
|
|
59
|
+
'br',
|
|
60
|
+
'col',
|
|
61
|
+
'embed',
|
|
62
|
+
'hr',
|
|
63
|
+
'img',
|
|
64
|
+
'input',
|
|
65
|
+
'link',
|
|
66
|
+
'meta',
|
|
67
|
+
'param',
|
|
68
|
+
'source',
|
|
69
|
+
'track',
|
|
70
|
+
'wbr',
|
|
71
|
+
]);
|
|
72
|
+
/** Is `c` an ASCII letter? */
|
|
73
|
+
function isAsciiLetter(c) {
|
|
74
|
+
return (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a);
|
|
75
|
+
}
|
|
76
|
+
/** Is `c` a tag-name char per the old regex's `[\w-]`? */
|
|
77
|
+
function isNameChar(c) {
|
|
78
|
+
return isAsciiLetter(c) || (c >= 0x30 && c <= 0x39) || c === 0x5f /* _ */ || c === 0x2d /* - */;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Estimate the maximum element-nesting depth of an HTML string WITHOUT parsing
|
|
82
|
+
* it — a running open/close balance over element tags. Conservative (it may
|
|
83
|
+
* over-count on malformed input, which only makes the guard fire sooner) and
|
|
84
|
+
* returns as soon as the cap is exceeded, so it stays cheap on huge inputs.
|
|
85
|
+
* Comments / doctype / PIs are ignored (the tag name must start with a letter).
|
|
86
|
+
*
|
|
87
|
+
* Hand-rolled char scan, NOT a regex: the previous
|
|
88
|
+
* `/<(\/?)([a-zA-Z][\w-]*)([^>]*)>/g` backtracked quadratically on
|
|
89
|
+
* '>'-free adversarial input (ReDoS on the public sandbox route). This
|
|
90
|
+
* scan is O(n) by construction and preserves the regex's semantics.
|
|
91
|
+
*/
|
|
92
|
+
export function estimateMaxNestingDepth(html) {
|
|
93
|
+
const n = html.length;
|
|
94
|
+
let depth = 0;
|
|
95
|
+
let max = 0;
|
|
96
|
+
let i = 0;
|
|
97
|
+
while (i < n) {
|
|
98
|
+
const lt = html.indexOf('<', i);
|
|
99
|
+
if (lt === -1)
|
|
100
|
+
break;
|
|
101
|
+
let p = lt + 1;
|
|
102
|
+
const isClosing = p < n && html.charCodeAt(p) === 0x2f; /* '/' */
|
|
103
|
+
if (isClosing)
|
|
104
|
+
p += 1;
|
|
105
|
+
if (p >= n || !isAsciiLetter(html.charCodeAt(p))) {
|
|
106
|
+
// Not an element tag (comment/doctype/PI/stray '<') — skip the '<'.
|
|
107
|
+
i = lt + 1;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const nameStart = p;
|
|
111
|
+
while (p < n && isNameChar(html.charCodeAt(p)))
|
|
112
|
+
p += 1;
|
|
113
|
+
const gt = html.indexOf('>', p);
|
|
114
|
+
if (gt === -1)
|
|
115
|
+
break; // no '>' remains — no further tag can complete
|
|
116
|
+
if (isClosing) {
|
|
117
|
+
depth = Math.max(0, depth - 1);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// Self-closing: last non-whitespace char before '>' is '/'.
|
|
121
|
+
let q = gt - 1;
|
|
122
|
+
while (q >= p) {
|
|
123
|
+
const c = html.charCodeAt(q);
|
|
124
|
+
if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d || c === 0x0c)
|
|
125
|
+
q -= 1;
|
|
126
|
+
else
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
const selfClosed = q >= p && html.charCodeAt(q) === 0x2f;
|
|
130
|
+
const name = html.slice(nameStart, p).toLowerCase();
|
|
131
|
+
if (!VOID_ELEMENTS.has(name) && !selfClosed) {
|
|
132
|
+
depth += 1;
|
|
133
|
+
if (depth > max) {
|
|
134
|
+
max = depth;
|
|
135
|
+
if (max > MAX_SAFE_NESTING_DEPTH)
|
|
136
|
+
return max; // cap hit — stop scanning
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
i = gt + 1;
|
|
141
|
+
}
|
|
142
|
+
return max;
|
|
143
|
+
}
|
|
144
|
+
/** Window of the input the binary sniff examines. */
|
|
145
|
+
const BINARY_SNIFF_CHARS = 65_536;
|
|
146
|
+
/**
|
|
147
|
+
* Magic-byte prefixes of common binary formats served with a text/html
|
|
148
|
+
* content-type (misconfigured servers, hotlinked assets). Checked after
|
|
149
|
+
* skipping an optional BOM + leading whitespace. Written as \\uXXXX escapes
|
|
150
|
+
* and kept byte-precise — a bare 'PK'/'PNG'/'ELF' prefix would
|
|
151
|
+
* false-positive on legitimate text that happens to start with those letters.
|
|
152
|
+
*/
|
|
153
|
+
const BINARY_MAGIC_PREFIXES = [
|
|
154
|
+
'%PDF-', // PDF (real PDFs are routed to extractPdfText upstream)
|
|
155
|
+
'\u0089PNG\r\n\u001a\n', // PNG
|
|
156
|
+
'GIF87a',
|
|
157
|
+
'GIF89a',
|
|
158
|
+
'\u00ff\u00d8\u00ff', // JPEG
|
|
159
|
+
'PK\u0003\u0004', // zip / docx / epub
|
|
160
|
+
'PK\u0005\u0006', // empty zip
|
|
161
|
+
'PK\u0007\u0008', // spanned zip
|
|
162
|
+
'\u007fELF', // ELF
|
|
163
|
+
'\u001f\u008b', // gzip
|
|
164
|
+
'OggS', // Ogg
|
|
165
|
+
'wOFF', // WOFF font
|
|
166
|
+
'wOF2', // WOFF2 font
|
|
167
|
+
];
|
|
168
|
+
/**
|
|
169
|
+
* Cheap sniff for binary-pretending-to-be-HTML. True when the input starts
|
|
170
|
+
* with a known binary magic, or has a high density of NUL/C0 control
|
|
171
|
+
* characters (legit HTML text has essentially none besides whitespace —
|
|
172
|
+
* even mojibake from a wrong charset decodes to printable or C1 ranges,
|
|
173
|
+
* which we deliberately do NOT count to avoid false positives). A single
|
|
174
|
+
* stray NUL is deliberately NOT enough: a one-byte decoding artifact must
|
|
175
|
+
* not zero out an otherwise-normal page — NULs count toward the density
|
|
176
|
+
* rule instead (UTF-16-mislabeled text is ~50% NUL, real binary ~11% C0;
|
|
177
|
+
* both clear the 5% bar by a wide margin).
|
|
178
|
+
*/
|
|
179
|
+
export function looksLikeBinary(html) {
|
|
180
|
+
if (html.length === 0)
|
|
181
|
+
return false;
|
|
182
|
+
const window = html.slice(0, BINARY_SNIFF_CHARS);
|
|
183
|
+
// BOM / whitespace-tolerant magic check.
|
|
184
|
+
const trimmed = window.replace(/^[\ufeff\s]+/, '');
|
|
185
|
+
for (const magic of BINARY_MAGIC_PREFIXES) {
|
|
186
|
+
if (trimmed.startsWith(magic))
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
// Density of NUL + C0 controls excluding \t \n \r \f.
|
|
190
|
+
let controls = 0;
|
|
191
|
+
for (let i = 0; i < window.length; i++) {
|
|
192
|
+
const c = window.charCodeAt(i);
|
|
193
|
+
if (c < 0x20 && c !== 0x09 && c !== 0x0a && c !== 0x0d && c !== 0x0c)
|
|
194
|
+
controls += 1;
|
|
195
|
+
}
|
|
196
|
+
return controls / window.length > 0.05;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Linear-time removal of <script>/<style> blocks via indexOf scanning.
|
|
200
|
+
* Replaces the old lazy-regex approach, which backtracked quadratically on
|
|
201
|
+
* adversarial input (thousands of unclosed `<script` openers). An opener
|
|
202
|
+
* with no matching closer drops the rest of the string — correct for the
|
|
203
|
+
* degraded path (script bodies are never content).
|
|
204
|
+
*/
|
|
205
|
+
export function stripScriptStyleBlocks(html) {
|
|
206
|
+
const lower = html.toLowerCase();
|
|
207
|
+
const parts = [];
|
|
208
|
+
let i = 0;
|
|
209
|
+
// Needle cursors advance monotonically and stick at -1 once absent —
|
|
210
|
+
// re-searching an absent needle from every position was itself quadratic.
|
|
211
|
+
let s = lower.indexOf('<script');
|
|
212
|
+
let t = lower.indexOf('<style');
|
|
213
|
+
while (i < html.length) {
|
|
214
|
+
if (s !== -1 && s < i)
|
|
215
|
+
s = lower.indexOf('<script', i);
|
|
216
|
+
if (t !== -1 && t < i)
|
|
217
|
+
t = lower.indexOf('<style', i);
|
|
218
|
+
const start = s === -1 ? t : t === -1 ? s : Math.min(s, t);
|
|
219
|
+
if (start === -1) {
|
|
220
|
+
parts.push(html.slice(i));
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
parts.push(html.slice(i, start));
|
|
224
|
+
const closer = start === s ? '</script' : '</style';
|
|
225
|
+
const end = lower.indexOf(closer, start);
|
|
226
|
+
if (end === -1)
|
|
227
|
+
break; // unclosed — drop the remainder
|
|
228
|
+
const gt = html.indexOf('>', end);
|
|
229
|
+
i = gt === -1 ? html.length : gt + 1;
|
|
230
|
+
}
|
|
231
|
+
return parts.join(' ');
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Linear-time tag stripper for the degraded path, mimicking the semantics
|
|
235
|
+
* of `/<[^>]+>/g → ' '` without its backtracking (which is quadratic on
|
|
236
|
+
* '>'-free adversarial input): a complete `<...>` span with at least one
|
|
237
|
+
* char inside is replaced by a space; a `<` with no closing `>` in the
|
|
238
|
+
* remainder stays as literal text, exactly like the regex.
|
|
239
|
+
*/
|
|
240
|
+
export function stripTagsLinear(html) {
|
|
241
|
+
const parts = [];
|
|
242
|
+
let i = 0;
|
|
243
|
+
while (i < html.length) {
|
|
244
|
+
const lt = html.indexOf('<', i);
|
|
245
|
+
if (lt === -1) {
|
|
246
|
+
parts.push(html.slice(i));
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
const gt = html.indexOf('>', lt + 1);
|
|
250
|
+
if (gt === -1) {
|
|
251
|
+
// No complete tag remains — the tail (including the '<') is text.
|
|
252
|
+
parts.push(html.slice(i));
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
if (gt === lt + 1) {
|
|
256
|
+
// '<>' — the regex required at least one char inside; keep it.
|
|
257
|
+
parts.push(html.slice(i, gt + 1));
|
|
258
|
+
i = gt + 1;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
parts.push(html.slice(i, lt));
|
|
262
|
+
parts.push(' ');
|
|
263
|
+
i = gt + 1;
|
|
264
|
+
}
|
|
265
|
+
return parts.join('');
|
|
266
|
+
}
|
|
267
|
+
// ── Markdown table geometry ceilings (gfmTable rule) ─────────────────────
|
|
268
|
+
// GFM conversion pads EVERY row to the widest row's column count, so one
|
|
269
|
+
// ragged 100k-cell row multiplies across all rows: a page under all the
|
|
270
|
+
// input caps could still emit hundreds of MB of markdown. Corpus max seen:
|
|
271
|
+
// 257 rows / small column counts — these sit far above every legit page.
|
|
272
|
+
/** Widest table (columns) the markdown converter will emit. */
|
|
273
|
+
export const MAX_TABLE_COLS = 500;
|
|
274
|
+
/** Max cells (cols × rows) the markdown converter will emit per table. */
|
|
275
|
+
export const MAX_TABLE_CELLS = 250_000;
|
|
276
|
+
//# sourceMappingURL=guards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extractor orchestrator (chunks 10 + 11).
|
|
3
|
+
*
|
|
4
|
+
* Pipeline:
|
|
5
|
+
* 1. Look up existing extraction by (content_hash, schema_hash, extractor_version)
|
|
6
|
+
* — Review #6 cross-customer cache. Cache hit → return + skip everything below.
|
|
7
|
+
* 2. JS-shell pre-flight — if the HTML is just a loading shell, return
|
|
8
|
+
* extraction_status='js_shell_detected' without burning a Sonnet call.
|
|
9
|
+
* 3. Mozilla Readability → markdown.
|
|
10
|
+
* 4. If schema is null/empty → markdown-only extraction (no Sonnet).
|
|
11
|
+
* 5. Truncate markdown if too long (>50k chars → 40k, flag status_detail).
|
|
12
|
+
* 6. Sonnet structured extraction via tool_use, threading customer intent.
|
|
13
|
+
* 7. Write the extractions row + cost_records row.
|
|
14
|
+
*
|
|
15
|
+
* Per the four-layer rule: the extractor returns the
|
|
16
|
+
* extraction row and nothing more. It does NOT poll, fetch, diff, or
|
|
17
|
+
* deliver — those are other layers.
|
|
18
|
+
*/
|
|
19
|
+
import type Anthropic from '@anthropic-ai/sdk';
|
|
20
|
+
import { type DbClient, type Extraction } from '@bluefields/db';
|
|
21
|
+
export interface ExtractInput {
|
|
22
|
+
/** Snapshot row id (extractions.snapshot_id). */
|
|
23
|
+
snapshotId: string;
|
|
24
|
+
/** Subscription this extraction belongs to (null for one-shot scrapes). */
|
|
25
|
+
subscriptionId: string | null;
|
|
26
|
+
customerId: string;
|
|
27
|
+
/** The HTML to extract from. */
|
|
28
|
+
html: string;
|
|
29
|
+
/** Source URL (canonical). Used by Readability + Turndown for resolving relative links. */
|
|
30
|
+
url: string;
|
|
31
|
+
/** SHA-256 hex of the raw response body — keys the cross-customer cache. */
|
|
32
|
+
contentHash: string;
|
|
33
|
+
/** Customer's JSON schema. Null/empty → markdown-only. */
|
|
34
|
+
schema: Record<string, unknown> | null;
|
|
35
|
+
/** Customer-provided one-sentence intent. Null if not set. */
|
|
36
|
+
intent: string | null;
|
|
37
|
+
}
|
|
38
|
+
export interface ExtractResult {
|
|
39
|
+
extraction: Extraction;
|
|
40
|
+
/** True when the result came from the (content_hash, schema_hash, extractor_version) cache. */
|
|
41
|
+
cacheHit: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Plain-text body from THIS run's Readability pass. Extraction rows don't
|
|
44
|
+
* persist text, so this is null on cache hits and failed runs — a caller
|
|
45
|
+
* that needs text on a cache hit recomputes via `extractMarkdown` (it
|
|
46
|
+
* still holds the html).
|
|
47
|
+
*/
|
|
48
|
+
text: string | null;
|
|
49
|
+
}
|
|
50
|
+
export interface ExtractOptions {
|
|
51
|
+
anthropicClient?: Anthropic;
|
|
52
|
+
anthropicApiKey?: string;
|
|
53
|
+
/** Test seam: override Date.now()-derived timestamps. */
|
|
54
|
+
nowFn?: () => Date;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Run the extraction pipeline for one (snapshot, schema) pair.
|
|
58
|
+
*
|
|
59
|
+
* On cache hit: a single SELECT and an early return. No LLM call, no
|
|
60
|
+
* cost_records row, but a new extractions row is still inserted so the
|
|
61
|
+
* subscription's extraction history is complete (the row references the
|
|
62
|
+
* cached structured_data + markdown but registers under this snapshot).
|
|
63
|
+
*
|
|
64
|
+
* Actually — Review #6 cache means we reuse the EXISTING extractions
|
|
65
|
+
* row across subscriptions for the same (content_hash, schema_hash,
|
|
66
|
+
* extractor_version). For a per-subscription history view, callers
|
|
67
|
+
* resolve via the join: events.previous_extraction_id → extractions.
|
|
68
|
+
* Two subscriptions watching the same URL with the same schema literally
|
|
69
|
+
* share an extractions row.
|
|
70
|
+
*/
|
|
71
|
+
export declare function extract(db: DbClient, input: ExtractInput, opts?: ExtractOptions): Promise<ExtractResult>;
|
|
72
|
+
export { type DegradeReason, type ExtractDegradeInfo, type ExtractedMarkdown, READABILITY_VERSION, extractMarkdown, } from './readability.js';
|
|
73
|
+
export { MAX_DEGRADED_TEXT_CHARS, MAX_EXTRACT_HTML_CHARS, MAX_SAFE_NESTING_DEPTH, } from './guards.js';
|
|
74
|
+
export { type SonnetExtractResult, SONNET_MODEL, extractStructured } from './sonnet.js';
|
|
75
|
+
export { HAIKU_MODEL, SONNET_MODEL_ID, type PickedModel, type ModelPickReason, pickModel, pickModelWithReason, } from './model-picker.js';
|
|
76
|
+
export { isJsShell } from './js-shell-detect.js';
|
|
77
|
+
export { schemaHash } from './cache-key.js';
|
|
78
|
+
export { type ExtractLinksOptions, extractLinks } from './links.js';
|
|
79
|
+
export { type PdfExtractionResult, extractPdfText, isPdfContentType, looksLikePdfByUrl, } from './pdf.js';
|
|
80
|
+
export { type DiscoverUrlsOptions, type DiscoverUrlsResult, type ParsedSitemap, discoverUrls, parseRobotsTxtForSitemaps, parseSitemapXml, } from './sitemap.js';
|