@bluefields/primitives 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 +202 -0
- package/dist/canonicalize.d.ts +37 -0
- package/dist/canonicalize.js +196 -0
- package/dist/canonicalize.js.map +1 -0
- package/dist/env.d.ts +7 -0
- package/dist/env.js +15 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +63 -0
- package/dist/logger.js +118 -0
- package/dist/logger.js.map +1 -0
- package/dist/robots-txt.d.ts +65 -0
- package/dist/robots-txt.js +250 -0
- package/dist/robots-txt.js.map +1 -0
- package/dist/ssrf.d.ts +77 -0
- package/dist/ssrf.js +432 -0
- package/dist/ssrf.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +51 -0
package/dist/logger.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Structured logging (pino, JSON output).
|
|
4
|
+
*
|
|
5
|
+
* One root logger; child loggers per layer to attach consistent bindings
|
|
6
|
+
* without callers having to remember them.
|
|
7
|
+
*
|
|
8
|
+
* ## Conventions
|
|
9
|
+
*
|
|
10
|
+
* Every log call passes a structured object as the first argument with these
|
|
11
|
+
* fields when relevant:
|
|
12
|
+
* - layer: 'poll' | 'fetcher' | 'extractor' | 'differ' | 'events' | 'api' | 'control'
|
|
13
|
+
* - subscription_id: UUID
|
|
14
|
+
* - event_id: UUID
|
|
15
|
+
* - customer_id: UUID
|
|
16
|
+
* - duration_ms: int
|
|
17
|
+
* - outcome: 'ok' | 'cached' | 'blocked' | 'skipped' | 'error' | 'partial'
|
|
18
|
+
*
|
|
19
|
+
* The message string is the second argument and stays short — searchable
|
|
20
|
+
* tokens, not prose.
|
|
21
|
+
*
|
|
22
|
+
* ## What NOT to log
|
|
23
|
+
*
|
|
24
|
+
* Configured via pino's `redact` so accidental inclusion in a bindings
|
|
25
|
+
* object or a log argument silently becomes `[Redacted]`:
|
|
26
|
+
* - webhook secrets (current + previous)
|
|
27
|
+
* - API keys (full + hash)
|
|
28
|
+
* - Stripe webhook signing secret
|
|
29
|
+
* - Authorization headers + bearer tokens
|
|
30
|
+
* - Anthropic API key
|
|
31
|
+
* - Resend API key
|
|
32
|
+
* - Raw HTML / extraction body — caller's responsibility (we just don't log it)
|
|
33
|
+
*
|
|
34
|
+
* Raw HTML and raw extraction output aren't redactable by path (the field
|
|
35
|
+
* names vary by call site); the convention is: log size + hash, never bytes.
|
|
36
|
+
* Reviewers should grep `logger.*(.*html|.*markdown|.*structuredData|.*body:)`
|
|
37
|
+
* before merging changes that touch logging.
|
|
38
|
+
*/
|
|
39
|
+
import { pino } from 'pino';
|
|
40
|
+
/**
|
|
41
|
+
* Redact paths — pino walks these and replaces matches with [Redacted].
|
|
42
|
+
* Glob `*` matches a single level; `**` matches any depth.
|
|
43
|
+
*
|
|
44
|
+
* Be generous; false positives (redacting something harmless) are cheap,
|
|
45
|
+
* leaking a secret is not.
|
|
46
|
+
*/
|
|
47
|
+
const REDACT_PATHS = [
|
|
48
|
+
// Webhook secrets
|
|
49
|
+
'*.webhook_secret',
|
|
50
|
+
'*.webhookSecret',
|
|
51
|
+
'*.webhookSecretCurrent',
|
|
52
|
+
'*.webhookSecretPrevious',
|
|
53
|
+
'*.webhook_secret_current',
|
|
54
|
+
'*.webhook_secret_previous',
|
|
55
|
+
// API keys (both formats)
|
|
56
|
+
'*.api_key',
|
|
57
|
+
'*.apiKey',
|
|
58
|
+
'*.key_hash',
|
|
59
|
+
'*.keyHash',
|
|
60
|
+
// Generic secrets
|
|
61
|
+
'*.secret',
|
|
62
|
+
'*.password',
|
|
63
|
+
'*.token',
|
|
64
|
+
// HTTP headers
|
|
65
|
+
'*.authorization',
|
|
66
|
+
'*.Authorization',
|
|
67
|
+
'headers.authorization',
|
|
68
|
+
'headers.Authorization',
|
|
69
|
+
'headers["x-stripe-signature"]',
|
|
70
|
+
'headers["stripe-signature"]',
|
|
71
|
+
'headers["bluefields-signature"]',
|
|
72
|
+
// Provider keys
|
|
73
|
+
'*.STRIPE_SECRET_KEY',
|
|
74
|
+
'*.STRIPE_WEBHOOK_SECRET',
|
|
75
|
+
'*.ANTHROPIC_API_KEY',
|
|
76
|
+
'*.RESEND_API_KEY',
|
|
77
|
+
'*.IPROYAL_PASSWORD',
|
|
78
|
+
'*.R2_SECRET_KEY',
|
|
79
|
+
// Customer plaintext on creation
|
|
80
|
+
'*.apiKeyPlaintext',
|
|
81
|
+
'*.api_key_plaintext',
|
|
82
|
+
];
|
|
83
|
+
let rootLogger = null;
|
|
84
|
+
/**
|
|
85
|
+
* Build the root logger. Idempotent — subsequent calls return the same
|
|
86
|
+
* instance. Tests can pass a custom `level` override.
|
|
87
|
+
*/
|
|
88
|
+
export function getRootLogger(env = process.env) {
|
|
89
|
+
if (rootLogger)
|
|
90
|
+
return rootLogger;
|
|
91
|
+
const built = pino({
|
|
92
|
+
level: env.LOG_LEVEL ?? (env.NODE_ENV === 'production' ? 'info' : 'debug'),
|
|
93
|
+
redact: {
|
|
94
|
+
paths: REDACT_PATHS,
|
|
95
|
+
censor: '[Redacted]',
|
|
96
|
+
},
|
|
97
|
+
// ISO timestamps — easier to grep than epoch ms.
|
|
98
|
+
timestamp: () => `,"time":"${new Date().toISOString()}"`,
|
|
99
|
+
base: {
|
|
100
|
+
service: 'bluefields',
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
rootLogger = built;
|
|
104
|
+
return built;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Build a child logger pre-bound to a layer (and optional subscription /
|
|
108
|
+
* customer / event ids). Pass through callers so log calls don't have to
|
|
109
|
+
* repeat the same bindings.
|
|
110
|
+
*/
|
|
111
|
+
export function createLayerLogger(layer, bindings = {}, env) {
|
|
112
|
+
return getRootLogger(env).child({ layer, ...bindings });
|
|
113
|
+
}
|
|
114
|
+
/** Test-only — reset the cached root logger. */
|
|
115
|
+
export function _resetLoggerForTests() {
|
|
116
|
+
rootLogger = null;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,EAA6B,IAAI,EAAE,MAAM,MAAM,CAAC;AA0BvD;;;;;;GAMG;AACH,MAAM,YAAY,GAAG;IACnB,kBAAkB;IAClB,kBAAkB;IAClB,iBAAiB;IACjB,wBAAwB;IACxB,yBAAyB;IACzB,0BAA0B;IAC1B,2BAA2B;IAC3B,0BAA0B;IAC1B,WAAW;IACX,UAAU;IACV,YAAY;IACZ,WAAW;IACX,kBAAkB;IAClB,UAAU;IACV,YAAY;IACZ,SAAS;IACT,eAAe;IACf,iBAAiB;IACjB,iBAAiB;IACjB,uBAAuB;IACvB,uBAAuB;IACvB,+BAA+B;IAC/B,6BAA6B;IAC7B,iCAAiC;IACjC,gBAAgB;IAChB,qBAAqB;IACrB,yBAAyB;IACzB,qBAAqB;IACrB,kBAAkB;IAClB,oBAAoB;IACpB,iBAAiB;IACjB,iCAAiC;IACjC,mBAAmB;IACnB,qBAAqB;CACtB,CAAC;AAEF,IAAI,UAAU,GAAkB,IAAI,CAAC;AAErC;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,MAAyB,OAAO,CAAC,GAAG;IAChE,IAAI,UAAU;QAAE,OAAO,UAAU,CAAC;IAClC,MAAM,KAAK,GAAG,IAAI,CAAC;QACjB,KAAK,EAAE,GAAG,CAAC,SAAS,IAAI,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;QAC1E,MAAM,EAAE;YACN,KAAK,EAAE,YAAY;YACnB,MAAM,EAAE,YAAY;SACrB;QACD,iDAAiD;QACjD,SAAS,EAAE,GAAG,EAAE,CAAC,YAAY,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,GAAG;QACxD,IAAI,EAAE;YACJ,OAAO,EAAE,YAAY;SACtB;KACF,CAAC,CAAC;IACH,UAAU,GAAG,KAAK,CAAC;IACnB,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,KAAe,EACf,WAA0B,EAAE,EAC5B,GAAuB;IAEvB,OAAO,aAAa,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,GAAG,QAAQ,EAAE,CAAC,CAAC;AAC1D,CAAC;AAUD,gDAAgD;AAChD,MAAM,UAAU,oBAAoB;IAClC,UAAU,GAAG,IAAI,CAAC;AACpB,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* robots.txt parser + cache (chunk 9).
|
|
3
|
+
*
|
|
4
|
+
* Per the preface's "legal stuff is real" guidance: respect robots.txt
|
|
5
|
+
* by default. Fetched lazily per host with a 1-hour in-memory TTL.
|
|
6
|
+
*
|
|
7
|
+
* Defaults to ALLOWED on:
|
|
8
|
+
* - robots.txt fetch failure (404, 5xx, network) — robots being down
|
|
9
|
+
* shouldn't block customer subscriptions
|
|
10
|
+
* - robots.txt missing — same
|
|
11
|
+
* - SSRF rejection of the robots URL itself — log + allow
|
|
12
|
+
*
|
|
13
|
+
* Crawl-delay is parsed (see `parseCrawlDelays` / `evaluateRobots`) for the
|
|
14
|
+
* politeness floor. The authorized "ignore robots.txt" opt-out is enforced one
|
|
15
|
+
* layer up (the robots-gated fetcher + an API-level authorization attestation),
|
|
16
|
+
* not here — see `ROBOTS_POLICY.md`. Staging-environment robots stays out of scope.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Thrown when a URL is disallowed by robots.txt and the request carries no
|
|
20
|
+
* authorized opt-out. Carries a stable `reason` so callers (the API routes) map
|
|
21
|
+
* it to a 403 `robots_disallowed` without string-matching the message.
|
|
22
|
+
*/
|
|
23
|
+
export declare class RobotsDisallowedError extends Error {
|
|
24
|
+
readonly url: string;
|
|
25
|
+
readonly reason: "robots_disallowed";
|
|
26
|
+
constructor(url: string);
|
|
27
|
+
}
|
|
28
|
+
export declare function resetRobotsCache(): void;
|
|
29
|
+
export interface IsAllowedOptions {
|
|
30
|
+
/** Test seam: override the fetch. */
|
|
31
|
+
fetchImpl?: typeof fetch;
|
|
32
|
+
/** Test seam: override Date.now() for TTL checks. */
|
|
33
|
+
nowMsFn?: () => number;
|
|
34
|
+
/** Test seam: skip the cache. */
|
|
35
|
+
bypassCache?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface RobotsEvaluation {
|
|
38
|
+
/** False iff a Disallow rule matches the path for this user-agent. */
|
|
39
|
+
allowed: boolean;
|
|
40
|
+
/** Crawl-delay (seconds) for the matched agent group, or null if none. */
|
|
41
|
+
crawlDelaySeconds: number | null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Evaluate robots.txt for `url` + `userAgent` from a single cached fetch+parse:
|
|
45
|
+
* whether the path is allowed AND the applicable Crawl-delay. Fail-open
|
|
46
|
+
* (`{ allowed: true, crawlDelaySeconds: null }`) when robots.txt is
|
|
47
|
+
* missing/failed/SSRF-rejected — see the file header.
|
|
48
|
+
*
|
|
49
|
+
* Cache key is `${url.origin}` so http vs https are separate entries.
|
|
50
|
+
*/
|
|
51
|
+
export declare function evaluateRobots(url: string, userAgent: string, options?: IsAllowedOptions): Promise<RobotsEvaluation>;
|
|
52
|
+
/**
|
|
53
|
+
* Returns true iff `url` is allowed by the host's robots.txt for the given
|
|
54
|
+
* user-agent. Thin wrapper over `evaluateRobots`, preserved for existing
|
|
55
|
+
* callers. Fail-open if robots.txt is missing or fetch fails.
|
|
56
|
+
*/
|
|
57
|
+
export declare function isAllowedByRobotsTxt(url: string, userAgent: string, options?: IsAllowedOptions): Promise<boolean>;
|
|
58
|
+
/**
|
|
59
|
+
* Disallow prefixes per lowercased user-agent ('*' fallback). Exported for testing.
|
|
60
|
+
*/
|
|
61
|
+
export declare function parseRobotsTxt(text: string): Record<string, string[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Crawl-delay (seconds) per lowercased user-agent ('*' fallback). Exported for testing.
|
|
64
|
+
*/
|
|
65
|
+
export declare function parseCrawlDelays(text: string): Record<string, number>;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* robots.txt parser + cache (chunk 9).
|
|
4
|
+
*
|
|
5
|
+
* Per the preface's "legal stuff is real" guidance: respect robots.txt
|
|
6
|
+
* by default. Fetched lazily per host with a 1-hour in-memory TTL.
|
|
7
|
+
*
|
|
8
|
+
* Defaults to ALLOWED on:
|
|
9
|
+
* - robots.txt fetch failure (404, 5xx, network) — robots being down
|
|
10
|
+
* shouldn't block customer subscriptions
|
|
11
|
+
* - robots.txt missing — same
|
|
12
|
+
* - SSRF rejection of the robots URL itself — log + allow
|
|
13
|
+
*
|
|
14
|
+
* Crawl-delay is parsed (see `parseCrawlDelays` / `evaluateRobots`) for the
|
|
15
|
+
* politeness floor. The authorized "ignore robots.txt" opt-out is enforced one
|
|
16
|
+
* layer up (the robots-gated fetcher + an API-level authorization attestation),
|
|
17
|
+
* not here — see `ROBOTS_POLICY.md`. Staging-environment robots stays out of scope.
|
|
18
|
+
*/
|
|
19
|
+
import { createLayerLogger } from './logger.js';
|
|
20
|
+
import { SsrfValidationError, validateUrlForExternalRequest } from './ssrf.js';
|
|
21
|
+
const log = createLayerLogger('fetcher');
|
|
22
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
23
|
+
const ROBOTS_FETCH_TIMEOUT_MS = 5_000;
|
|
24
|
+
/**
|
|
25
|
+
* Thrown when a URL is disallowed by robots.txt and the request carries no
|
|
26
|
+
* authorized opt-out. Carries a stable `reason` so callers (the API routes) map
|
|
27
|
+
* it to a 403 `robots_disallowed` without string-matching the message.
|
|
28
|
+
*/
|
|
29
|
+
export class RobotsDisallowedError extends Error {
|
|
30
|
+
url;
|
|
31
|
+
reason = 'robots_disallowed';
|
|
32
|
+
constructor(url) {
|
|
33
|
+
super(`robots.txt disallows fetching ${url}`);
|
|
34
|
+
this.url = url;
|
|
35
|
+
this.name = 'RobotsDisallowedError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Mutable module-level cache. Tests reset via `resetRobotsCache`.
|
|
40
|
+
* Production process restarts naturally invalidate; for v1 that's
|
|
41
|
+
* acceptable (no shared cache across instances).
|
|
42
|
+
*/
|
|
43
|
+
const cache = new Map();
|
|
44
|
+
export function resetRobotsCache() {
|
|
45
|
+
cache.clear();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Evaluate robots.txt for `url` + `userAgent` from a single cached fetch+parse:
|
|
49
|
+
* whether the path is allowed AND the applicable Crawl-delay. Fail-open
|
|
50
|
+
* (`{ allowed: true, crawlDelaySeconds: null }`) when robots.txt is
|
|
51
|
+
* missing/failed/SSRF-rejected — see the file header.
|
|
52
|
+
*
|
|
53
|
+
* Cache key is `${url.origin}` so http vs https are separate entries.
|
|
54
|
+
*/
|
|
55
|
+
export async function evaluateRobots(url, userAgent, options = {}) {
|
|
56
|
+
const parsed = safeParseUrl(url);
|
|
57
|
+
// Can't parse → don't block; the fetcher will reject this URL anyway.
|
|
58
|
+
if (!parsed)
|
|
59
|
+
return { allowed: true, crawlDelaySeconds: null };
|
|
60
|
+
const origin = parsed.origin;
|
|
61
|
+
const path = parsed.pathname + parsed.search;
|
|
62
|
+
const now = options.nowMsFn ? options.nowMsFn() : Date.now();
|
|
63
|
+
let rules;
|
|
64
|
+
if (!options.bypassCache) {
|
|
65
|
+
const hit = cache.get(origin);
|
|
66
|
+
if (hit && now - hit.cachedAtMs < CACHE_TTL_MS) {
|
|
67
|
+
rules = hit;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!rules) {
|
|
71
|
+
rules = await fetchAndParse(origin, now, options.fetchImpl);
|
|
72
|
+
if (!options.bypassCache)
|
|
73
|
+
cache.set(origin, rules);
|
|
74
|
+
}
|
|
75
|
+
const ua = userAgent.toLowerCase();
|
|
76
|
+
return {
|
|
77
|
+
allowed: !isDisallowed(rules, ua, path),
|
|
78
|
+
crawlDelaySeconds: crawlDelayFor(rules, ua),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Returns true iff `url` is allowed by the host's robots.txt for the given
|
|
83
|
+
* user-agent. Thin wrapper over `evaluateRobots`, preserved for existing
|
|
84
|
+
* callers. Fail-open if robots.txt is missing or fetch fails.
|
|
85
|
+
*/
|
|
86
|
+
export async function isAllowedByRobotsTxt(url, userAgent, options = {}) {
|
|
87
|
+
return (await evaluateRobots(url, userAgent, options)).allowed;
|
|
88
|
+
}
|
|
89
|
+
// ── Internals ───────────────────────────────────────────────────────────
|
|
90
|
+
function safeParseUrl(s) {
|
|
91
|
+
try {
|
|
92
|
+
return new URL(s);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function fetchAndParse(origin, now, fetchImpl) {
|
|
99
|
+
const robotsUrl = `${origin}/robots.txt`;
|
|
100
|
+
// SSRF-validate. Allow on SSRF rejection (robots.txt URL is the same
|
|
101
|
+
// host as a URL we'll likely fetch anyway; rejecting here would just
|
|
102
|
+
// double-error.)
|
|
103
|
+
try {
|
|
104
|
+
await validateUrlForExternalRequest(robotsUrl);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
if (err instanceof SsrfValidationError) {
|
|
108
|
+
return { disallows: {}, crawlDelays: {}, cachedAtMs: now };
|
|
109
|
+
}
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
const f = fetchImpl ?? fetch;
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
const timeout = setTimeout(() => controller.abort(), ROBOTS_FETCH_TIMEOUT_MS);
|
|
115
|
+
try {
|
|
116
|
+
const res = await f(robotsUrl, { method: 'GET', signal: controller.signal });
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
// 404 is the common case (no robots.txt) — debug level. 5xx is
|
|
119
|
+
// worth a warn so an operator can see if a target host's robots
|
|
120
|
+
// is degraded.
|
|
121
|
+
log[res.status >= 500 ? 'warn' : 'debug']({ outcome: 'skipped', origin, status: res.status }, 'robots.txt fetch non-2xx; failing open');
|
|
122
|
+
return { disallows: {}, crawlDelays: {}, cachedAtMs: now };
|
|
123
|
+
}
|
|
124
|
+
const text = await res.text();
|
|
125
|
+
const groups = parseGroups(text);
|
|
126
|
+
return { disallows: groups.disallows, crawlDelays: groups.crawlDelays, cachedAtMs: now };
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
// Network failure, timeout, etc. → fail open per spec. Log so it's
|
|
130
|
+
// visible — fail-open is a deliberate trade-off but invisible
|
|
131
|
+
// failures hide actual outages.
|
|
132
|
+
log.warn({ outcome: 'error', origin, error: err.message }, 'robots.txt fetch threw; failing open');
|
|
133
|
+
return { disallows: {}, crawlDelays: {}, cachedAtMs: now };
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
clearTimeout(timeout);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Single-pass robots.txt group parser → Disallow prefixes + Crawl-delay seconds
|
|
141
|
+
* per lowercased user-agent ('*' fallback). Handles consecutive User-agent lines
|
|
142
|
+
* forming one group, comments, and blank-line group separators. Defensive:
|
|
143
|
+
* anything unexpected is ignored, never throws. Allow-vs-Disallow precedence and
|
|
144
|
+
* `*`/`$` wildcard path matching remain out of scope for v1 (see ROBOTS_POLICY.md).
|
|
145
|
+
*/
|
|
146
|
+
function parseGroups(text) {
|
|
147
|
+
const disallows = {};
|
|
148
|
+
const crawlDelays = {};
|
|
149
|
+
let currentAgents = [];
|
|
150
|
+
// collectingAgents: inside a stretch of consecutive User-agent lines that all
|
|
151
|
+
// belong to the same group. The next directive ends the agent-collecting phase.
|
|
152
|
+
let collectingAgents = false;
|
|
153
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
154
|
+
// Check the RAW line for blankness before stripping comments. A comment-only
|
|
155
|
+
// line is not a group separator per RFC 9309.
|
|
156
|
+
if (rawLine.trim().length === 0) {
|
|
157
|
+
currentAgents = [];
|
|
158
|
+
collectingAgents = false;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const line = rawLine.replace(/#.*$/, '').trim();
|
|
162
|
+
if (line.length === 0)
|
|
163
|
+
continue; // pure comment → skip without ending the group
|
|
164
|
+
const colon = line.indexOf(':');
|
|
165
|
+
if (colon === -1)
|
|
166
|
+
continue;
|
|
167
|
+
const key = line.slice(0, colon).trim().toLowerCase();
|
|
168
|
+
const value = line.slice(colon + 1).trim();
|
|
169
|
+
if (key === 'user-agent') {
|
|
170
|
+
if (!collectingAgents) {
|
|
171
|
+
currentAgents = [value.toLowerCase()];
|
|
172
|
+
collectingAgents = true;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
currentAgents.push(value.toLowerCase());
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else if (key === 'disallow') {
|
|
179
|
+
collectingAgents = false;
|
|
180
|
+
if (currentAgents.length === 0)
|
|
181
|
+
continue;
|
|
182
|
+
if (value.length === 0)
|
|
183
|
+
continue; // empty disallow = no rule per RFC
|
|
184
|
+
for (const agent of currentAgents) {
|
|
185
|
+
if (!disallows[agent])
|
|
186
|
+
disallows[agent] = [];
|
|
187
|
+
disallows[agent].push(value);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else if (key === 'crawl-delay') {
|
|
191
|
+
collectingAgents = false;
|
|
192
|
+
if (currentAgents.length === 0)
|
|
193
|
+
continue;
|
|
194
|
+
const seconds = Number(value);
|
|
195
|
+
// Ignore non-numeric / negative crawl-delays (defensive).
|
|
196
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
197
|
+
for (const agent of currentAgents)
|
|
198
|
+
crawlDelays[agent] = seconds;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else if (key === 'allow') {
|
|
202
|
+
collectingAgents = false;
|
|
203
|
+
// Allow lines don't add disallows; full precedence (RFC 9309) is v1-out-of-scope.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { disallows, crawlDelays };
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Disallow prefixes per lowercased user-agent ('*' fallback). Exported for testing.
|
|
210
|
+
*/
|
|
211
|
+
export function parseRobotsTxt(text) {
|
|
212
|
+
return parseGroups(text).disallows;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Crawl-delay (seconds) per lowercased user-agent ('*' fallback). Exported for testing.
|
|
216
|
+
*/
|
|
217
|
+
export function parseCrawlDelays(text) {
|
|
218
|
+
return parseGroups(text).crawlDelays;
|
|
219
|
+
}
|
|
220
|
+
function isDisallowed(rules, userAgent, path) {
|
|
221
|
+
const ua = userAgent.toLowerCase();
|
|
222
|
+
// Match either by full UA or by token prefix (most robots.txt match a
|
|
223
|
+
// bare product token like 'bluefields-fetcher').
|
|
224
|
+
const token = ua.split('/')[0]?.trim() ?? ua;
|
|
225
|
+
// RFC 9309: when a per-agent group matches, the wildcard group is
|
|
226
|
+
// NOT consulted. Only fall through to '*' when no per-agent group
|
|
227
|
+
// exists for this UA.
|
|
228
|
+
const perAgent = rules.disallows[ua] ?? rules.disallows[token];
|
|
229
|
+
if (perAgent !== undefined) {
|
|
230
|
+
return perAgent.some((p) => pathStartsWith(path, p));
|
|
231
|
+
}
|
|
232
|
+
const wildcard = rules.disallows['*'];
|
|
233
|
+
return wildcard?.some((p) => pathStartsWith(path, p)) ?? false;
|
|
234
|
+
}
|
|
235
|
+
/** Crawl-delay (seconds) for `userAgent`, per-agent overriding '*'. Null if none. */
|
|
236
|
+
function crawlDelayFor(rules, userAgent) {
|
|
237
|
+
const ua = userAgent.toLowerCase();
|
|
238
|
+
const token = ua.split('/')[0]?.trim() ?? ua;
|
|
239
|
+
const perAgent = rules.crawlDelays[ua] ?? rules.crawlDelays[token];
|
|
240
|
+
if (perAgent !== undefined)
|
|
241
|
+
return perAgent;
|
|
242
|
+
return rules.crawlDelays['*'] ?? null;
|
|
243
|
+
}
|
|
244
|
+
function pathStartsWith(path, prefix) {
|
|
245
|
+
// robots.txt prefixes match by URL path prefix. We don't implement
|
|
246
|
+
// the full RFC 9309 wildcard syntax (* and $) — too much surface for
|
|
247
|
+
// v1; defaults to "literal prefix" semantics which most robots use.
|
|
248
|
+
return path.startsWith(prefix);
|
|
249
|
+
}
|
|
250
|
+
//# sourceMappingURL=robots-txt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"robots-txt.js","sourceRoot":"","sources":["../src/robots-txt.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,6BAA6B,EAAE,MAAM,WAAW,CAAC;AAE/E,MAAM,GAAG,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;AAEzC,MAAM,YAAY,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,SAAS;AAC9C,MAAM,uBAAuB,GAAG,KAAK,CAAC;AAWtC;;;;GAIG;AACH,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAEzB;IADZ,MAAM,GAAG,mBAA4B,CAAC;IAC/C,YAAqB,GAAW;QAC9B,KAAK,CAAC,iCAAiC,GAAG,EAAE,CAAC,CAAC;QAD3B,QAAG,GAAH,GAAG,CAAQ;QAE9B,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,KAAK,GAAG,IAAI,GAAG,EAAuB,CAAC;AAE7C,MAAM,UAAU,gBAAgB;IAC9B,KAAK,CAAC,KAAK,EAAE,CAAC;AAChB,CAAC;AAkBD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,GAAW,EACX,SAAiB,EACjB,UAA4B,EAAE;IAE9B,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IACjC,sEAAsE;IACtE,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAAC;IAE/D,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7C,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;IAE7D,IAAI,KAA8B,CAAC;IACnC,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC9B,IAAI,GAAG,IAAI,GAAG,GAAG,GAAG,CAAC,UAAU,GAAG,YAAY,EAAE,CAAC;YAC/C,KAAK,GAAG,GAAG,CAAC;QACd,CAAC;IACH,CAAC;IAED,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,KAAK,GAAG,MAAM,aAAa,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;QAC5D,IAAI,CAAC,OAAO,CAAC,WAAW;YAAE,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACrD,CAAC;IAED,MAAM,EAAE,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC;IACnC,OAAO;QACL,OAAO,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,EAAE,EAAE,IAAI,CAAC;QACvC,iBAAiB,EAAE,aAAa,CAAC,KAAK,EAAE,EAAE,CAAC;KAC5C,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,GAAW,EACX,SAAiB,EACjB,UAA4B,EAAE;IAE9B,OAAO,CAAC,MAAM,cAAc,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;AACjE,CAAC;AAED,2EAA2E;AAE3E,SAAS,YAAY,CAAC,CAAS;IAC7B,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,aAAa,CAC1B,MAAc,EACd,GAAW,EACX,SAAmC;IAEnC,MAAM,SAAS,GAAG,GAAG,MAAM,aAAa,CAAC;IAEzC,qEAAqE;IACrE,qEAAqE;IACrE,iBAAiB;IACjB,IAAI,CAAC;QACH,MAAM,6BAA6B,CAAC,SAAS,CAAC,CAAC;IACjD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,mBAAmB,EAAE,CAAC;YACvC,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC;QAC7D,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;IAED,MAAM,CAAC,GAAG,SAAS,IAAI,KAAK,CAAC;IAC7B,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,uBAAuB,CAAC,CAAC;IAE9E,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;QAC7E,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,+DAA+D;YAC/D,gEAAgE;YAChE,eAAe;YACf,GAAG,CAAC,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CACvC,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,EAClD,wCAAwC,CACzC,CAAC;YACF,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC;QAC7D,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;QACjC,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC;IAC3F,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,mEAAmE;QACnE,8DAA8D;QAC9D,gCAAgC;QAChC,GAAG,CAAC,IAAI,CACN,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAG,GAAa,CAAC,OAAO,EAAE,EAC3D,sCAAsC,CACvC,CAAC;QACF,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC;IAC7D,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,OAAO,CAAC,CAAC;IACxB,CAAC;AACH,CAAC;AAOD;;;;;;GAMG;AACH,SAAS,WAAW,CAAC,IAAY;IAC/B,MAAM,SAAS,GAA6B,EAAE,CAAC;IAC/C,MAAM,WAAW,GAA2B,EAAE,CAAC;IAC/C,IAAI,aAAa,GAAa,EAAE,CAAC;IACjC,8EAA8E;IAC9E,gFAAgF;IAChF,IAAI,gBAAgB,GAAG,KAAK,CAAC;IAE7B,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QAC1C,6EAA6E;QAC7E,8CAA8C;QAC9C,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAChC,aAAa,GAAG,EAAE,CAAC;YACnB,gBAAgB,GAAG,KAAK,CAAC;YACzB,SAAS;QACX,CAAC;QACD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAChD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS,CAAC,+CAA+C;QAEhF,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,KAAK,KAAK,CAAC,CAAC;YAAE,SAAS;QAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACtD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAE3C,IAAI,GAAG,KAAK,YAAY,EAAE,CAAC;YACzB,IAAI,CAAC,gBAAgB,EAAE,CAAC;gBACtB,aAAa,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;gBACtC,gBAAgB,GAAG,IAAI,CAAC;YAC1B,CAAC;iBAAM,CAAC;gBACN,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;YAC1C,CAAC;QACH,CAAC;aAAM,IAAI,GAAG,KAAK,UAAU,EAAE,CAAC;YAC9B,gBAAgB,GAAG,KAAK,CAAC;YACzB,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YACzC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS,CAAC,mCAAmC;YACrE,KAAK,MAAM,KAAK,IAAI,aAAa,EAAE,CAAC;gBAClC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;oBAAE,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;gBAC7C,SAAS,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC/B,CAAC;QACH,CAAC;aAAM,IAAI,GAAG,KAAK,aAAa,EAAE,CAAC;YACjC,gBAAgB,GAAG,KAAK,CAAC;YACzB,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YACzC,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;YAC9B,0DAA0D;YAC1D,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,IAAI,CAAC,EAAE,CAAC;gBAC7C,KAAK,MAAM,KAAK,IAAI,aAAa;oBAAE,WAAW,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC;YAClE,CAAC;QACH,CAAC;aAAM,IAAI,GAAG,KAAK,OAAO,EAAE,CAAC;YAC3B,gBAAgB,GAAG,KAAK,CAAC;YACzB,kFAAkF;QACpF,CAAC;IACH,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC;AACpC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC;AACrC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,WAAW,CAAC;AACvC,CAAC;AAED,SAAS,YAAY,CAAC,KAAkB,EAAE,SAAiB,EAAE,IAAY;IACvE,MAAM,EAAE,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC;IACnC,sEAAsE;IACtE,iDAAiD;IACjD,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAE7C,kEAAkE;IAClE,kEAAkE;IAClE,sBAAsB;IACtB,MAAM,QAAQ,GAAG,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/D,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACvD,CAAC;IACD,MAAM,QAAQ,GAAG,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACtC,OAAO,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC;AACjE,CAAC;AAED,qFAAqF;AACrF,SAAS,aAAa,CAAC,KAAkB,EAAE,SAAiB;IAC1D,MAAM,EAAE,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC;IACnC,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC7C,MAAM,QAAQ,GAAG,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACnE,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IAC5C,OAAO,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;AACxC,CAAC;AAED,SAAS,cAAc,CAAC,IAAY,EAAE,MAAc;IAClD,mEAAmE;IACnE,qEAAqE;IACrE,oEAAoE;IACpE,OAAO,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;AACjC,CAAC"}
|
package/dist/ssrf.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF-safe URL validation and HTTP client (Reviews #6, #7, #10).
|
|
3
|
+
*
|
|
4
|
+
* Every URL that crosses a trust boundary into our platform — customer
|
|
5
|
+
* webhook URLs, customer watch URLs, MCP scrape_url arguments — must pass
|
|
6
|
+
* through `validateUrlForExternalRequest` before we make any outbound
|
|
7
|
+
* request. The validator is the first line of defense; `createSsrfSafeHttpClient`
|
|
8
|
+
* is the second, catching DNS rebinding by re-validating at connect time.
|
|
9
|
+
*
|
|
10
|
+
* This module is reused widely. Two rules to keep in mind when extending it:
|
|
11
|
+
* 1. Default-deny. Add new categories to the deny side; don't allowlist exceptions.
|
|
12
|
+
* 2. Validate, don't sanitize. We reject bad URLs; we never massage them into
|
|
13
|
+
* "fixed" URLs and proceed.
|
|
14
|
+
*/
|
|
15
|
+
import type { LookupOptions } from 'node:dns';
|
|
16
|
+
import { Agent } from 'undici';
|
|
17
|
+
export type SsrfRejectionReason = 'invalid_url' | 'unsupported_protocol' | 'metadata_endpoint' | 'internal_domain' | 'private_ip' | 'dns_failure';
|
|
18
|
+
export declare class SsrfValidationError extends Error {
|
|
19
|
+
readonly reason: SsrfRejectionReason;
|
|
20
|
+
constructor(message: string, reason: SsrfRejectionReason);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Validate that `url` is safe to use for an outbound HTTPS request.
|
|
24
|
+
*
|
|
25
|
+
* Throws `SsrfValidationError` on any rejection. Callers should treat the
|
|
26
|
+
* error as a 4xx validation failure (not a 5xx server error).
|
|
27
|
+
*/
|
|
28
|
+
export declare function validateUrlForExternalRequest(url: string): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Returns true if `ip` is in any private, reserved, loopback, link-local,
|
|
31
|
+
* multicast, broadcast, documentation, or AWS-metadata range — anything we
|
|
32
|
+
* don't want our platform's egress to reach. Exported so tests can verify
|
|
33
|
+
* each category and other callers (e.g. attestation manifest integrity
|
|
34
|
+
* checks on `proxy_egress_ip`) can reuse the same predicate.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isPrivateOrReservedIp(ip: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Undici lookup callback that re-validates each candidate address at
|
|
39
|
+
* connect time. Exported for testability; created via
|
|
40
|
+
* `createSsrfSafeHttpClient` for production use.
|
|
41
|
+
*
|
|
42
|
+
* The signature matches Node's `dns.lookup` callback so undici can call it
|
|
43
|
+
* as its `connect.lookup`. We always do an `all: true` lookup internally so
|
|
44
|
+
* a multi-record DNS response can't slip a private IP past us by chance of
|
|
45
|
+
* record ordering — we only return a public one.
|
|
46
|
+
*/
|
|
47
|
+
export declare function ssrfSafeLookup(hostname: string, options: LookupOptions, callback: (err: NodeJS.ErrnoException | null, address: string, family: number) => void): void;
|
|
48
|
+
/**
|
|
49
|
+
* Diagnostic helper — fetch a URL through an arbitrary HTTP proxy.
|
|
50
|
+
* Used by the /debug/diagnostics/proxy-test endpoint to verify
|
|
51
|
+
* provider reachability from inside the api container without going
|
|
52
|
+
* through Patchright/Chromium. Not for production traffic; the
|
|
53
|
+
* standard fetcher path uses ssrfSafeFetch.
|
|
54
|
+
*
|
|
55
|
+
* The target `url` is SSRF-validated up front (it can't go through the
|
|
56
|
+
* connect-time `ssrfSafeLookup`, since the dispatcher connects to the proxy,
|
|
57
|
+
* not the target). The proxy itself is operator-supplied via the admin-gated
|
|
58
|
+
* diagnostic, so it isn't validated here.
|
|
59
|
+
*/
|
|
60
|
+
export declare function proxyFetch(url: string, proxy: {
|
|
61
|
+
server: string;
|
|
62
|
+
username: string;
|
|
63
|
+
password: string;
|
|
64
|
+
}, timeoutMs?: number): Promise<Response>;
|
|
65
|
+
export declare function ssrfSafeFetch(url: string | URL, init?: RequestInit): Promise<Response>;
|
|
66
|
+
/**
|
|
67
|
+
* Build an undici Agent that pins each connection to a public IP and
|
|
68
|
+
* times out at 10s. Use this dispatcher wherever the platform makes an
|
|
69
|
+
* outbound request on behalf of a customer (webhook delivery, fetcher
|
|
70
|
+
* Tier 0, MCP scrape_url backend).
|
|
71
|
+
*
|
|
72
|
+
* Redirects: undici re-runs the dispatcher (and thus our lookup) for each
|
|
73
|
+
* new host encountered, so a redirect to a private IP is rejected at
|
|
74
|
+
* connect time the same way the original URL would be. Callers should
|
|
75
|
+
* still pass `maxRedirections` explicitly per-request — the default is 0.
|
|
76
|
+
*/
|
|
77
|
+
export declare function createSsrfSafeHttpClient(): Agent;
|