@x12i/content-fetcher 0.1.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/README.md +133 -0
- package/dist/adapters/default-http-client.d.ts +17 -0
- package/dist/adapters/default-http-client.d.ts.map +1 -0
- package/dist/adapters/default-http-client.js +74 -0
- package/dist/adapters/default-http-client.js.map +1 -0
- package/dist/channels.d.ts +34 -0
- package/dist/channels.d.ts.map +1 -0
- package/dist/channels.js +164 -0
- package/dist/channels.js.map +1 -0
- package/dist/classify.d.ts +13 -0
- package/dist/classify.d.ts.map +1 -0
- package/dist/classify.js +122 -0
- package/dist/classify.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +197 -0
- package/dist/config.js.map +1 -0
- package/dist/create-content-fetcher.d.ts +9 -0
- package/dist/create-content-fetcher.d.ts.map +1 -0
- package/dist/create-content-fetcher.js +836 -0
- package/dist/create-content-fetcher.js.map +1 -0
- package/dist/errors.d.ts +42 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +73 -0
- package/dist/errors.js.map +1 -0
- package/dist/handoff.d.ts +8 -0
- package/dist/handoff.d.ts.map +1 -0
- package/dist/handoff.js +20 -0
- package/dist/handoff.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/ratelimit.d.ts +27 -0
- package/dist/ratelimit.d.ts.map +1 -0
- package/dist/ratelimit.js +43 -0
- package/dist/ratelimit.js.map +1 -0
- package/dist/robots/robots-service.d.ts +40 -0
- package/dist/robots/robots-service.d.ts.map +1 -0
- package/dist/robots/robots-service.js +131 -0
- package/dist/robots/robots-service.js.map +1 -0
- package/dist/robots/robots-txt.d.ts +24 -0
- package/dist/robots/robots-txt.d.ts.map +1 -0
- package/dist/robots/robots-txt.js +91 -0
- package/dist/robots/robots-txt.js.map +1 -0
- package/dist/runtime-utils.d.ts +11 -0
- package/dist/runtime-utils.d.ts.map +1 -0
- package/dist/runtime-utils.js +32 -0
- package/dist/runtime-utils.js.map +1 -0
- package/dist/security/dns.d.ts +33 -0
- package/dist/security/dns.d.ts.map +1 -0
- package/dist/security/dns.js +60 -0
- package/dist/security/dns.js.map +1 -0
- package/dist/security/headers.d.ts +16 -0
- package/dist/security/headers.d.ts.map +1 -0
- package/dist/security/headers.js +54 -0
- package/dist/security/headers.js.map +1 -0
- package/dist/security/net-guards.d.ts +9 -0
- package/dist/security/net-guards.d.ts.map +1 -0
- package/dist/security/net-guards.js +102 -0
- package/dist/security/net-guards.js.map +1 -0
- package/dist/security/url.d.ts +32 -0
- package/dist/security/url.d.ts.map +1 -0
- package/dist/security/url.js +82 -0
- package/dist/security/url.js.map +1 -0
- package/dist/storage.d.ts +38 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +109 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +232 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/util/bytes.d.ts +21 -0
- package/dist/util/bytes.d.ts.map +1 -0
- package/dist/util/bytes.js +44 -0
- package/dist/util/bytes.js.map +1 -0
- package/dist/util/hash.d.ts +5 -0
- package/dist/util/hash.d.ts.map +1 -0
- package/dist/util/hash.js +11 -0
- package/dist/util/hash.js.map +1 -0
- package/dist/util/ids.d.ts +3 -0
- package/dist/util/ids.d.ts.map +1 -0
- package/dist/util/ids.js +8 -0
- package/dist/util/ids.js.map +1 -0
- package/dist/validate-request.d.ts +9 -0
- package/dist/validate-request.d.ts.map +1 -0
- package/dist/validate-request.js +137 -0
- package/dist/validate-request.js.map +1 -0
- package/package.json +34 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* robots.txt parsing and evaluation (practical subset of RFC 9309):
|
|
3
|
+
* agent groups, allow/disallow rules, `*` wildcards, and `$` anchors.
|
|
4
|
+
* Longest matching pattern wins; ties favour "allow".
|
|
5
|
+
*/
|
|
6
|
+
export function parseRobotsTxt(text) {
|
|
7
|
+
const groups = [];
|
|
8
|
+
let current = null;
|
|
9
|
+
let lastWasAgent = false;
|
|
10
|
+
for (const rawLine of text.split(/\r\n|\r|\n/)) {
|
|
11
|
+
const line = rawLine.replace(/#.*$/, "").trim();
|
|
12
|
+
if (line === "")
|
|
13
|
+
continue;
|
|
14
|
+
const colon = line.indexOf(":");
|
|
15
|
+
if (colon === -1)
|
|
16
|
+
continue;
|
|
17
|
+
const field = line.slice(0, colon).trim().toLowerCase();
|
|
18
|
+
const value = line.slice(colon + 1).trim();
|
|
19
|
+
if (field === "user-agent") {
|
|
20
|
+
if (current === null || !lastWasAgent) {
|
|
21
|
+
current = { agents: new Set(), rules: [] };
|
|
22
|
+
groups.push(current);
|
|
23
|
+
}
|
|
24
|
+
if (value !== "")
|
|
25
|
+
current.agents.add(value.toLowerCase());
|
|
26
|
+
lastWasAgent = true;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
lastWasAgent = false;
|
|
30
|
+
if (current === null)
|
|
31
|
+
continue; // rule lines before any user-agent are ignored
|
|
32
|
+
if (field === "disallow" && value !== "") {
|
|
33
|
+
current.rules.push({ allow: false, path: value });
|
|
34
|
+
}
|
|
35
|
+
else if (field === "allow" && value !== "") {
|
|
36
|
+
current.rules.push({ allow: true, path: value });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return groups;
|
|
40
|
+
}
|
|
41
|
+
function ruleMatches(rulePath, target) {
|
|
42
|
+
let pattern = "^";
|
|
43
|
+
for (let i = 0; i < rulePath.length; i++) {
|
|
44
|
+
const ch = rulePath[i];
|
|
45
|
+
if (ch === "*") {
|
|
46
|
+
pattern += "[\\s\\S]*";
|
|
47
|
+
}
|
|
48
|
+
else if (ch === "$" && i === rulePath.length - 1) {
|
|
49
|
+
pattern += "$";
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
pattern += ch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return new RegExp(pattern).test(target);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Evaluate `target` (path + query) for the given user agent.
|
|
64
|
+
* Agent groups are matched against the full user-agent token, its product
|
|
65
|
+
* name (the part before "/"), and finally the `*` group.
|
|
66
|
+
*/
|
|
67
|
+
export function robotsAllows(groups, userAgent, target) {
|
|
68
|
+
const full = userAgent.trim().toLowerCase();
|
|
69
|
+
const firstToken = full.split(/\s+/)[0] ?? "";
|
|
70
|
+
const productName = firstToken.split("/")[0] ?? "";
|
|
71
|
+
const candidates = new Set([full, firstToken, productName].filter((c) => c !== ""));
|
|
72
|
+
let matched = groups.filter((g) => [...candidates].some((token) => g.agents.has(token)));
|
|
73
|
+
if (matched.length === 0) {
|
|
74
|
+
matched = groups.filter((g) => g.agents.has("*"));
|
|
75
|
+
}
|
|
76
|
+
const rules = matched.flatMap((g) => g.rules);
|
|
77
|
+
if (rules.length === 0)
|
|
78
|
+
return { allowed: true, matchedPath: null };
|
|
79
|
+
let best = null;
|
|
80
|
+
for (const rule of rules) {
|
|
81
|
+
if (!ruleMatches(rule.path, target))
|
|
82
|
+
continue;
|
|
83
|
+
if (best === null ||
|
|
84
|
+
rule.path.length > best.path.length ||
|
|
85
|
+
(rule.path.length === best.path.length && rule.allow && !best.allow)) {
|
|
86
|
+
best = { allow: rule.allow, path: rule.path };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return best === null ? { allowed: true, matchedPath: null } : { allowed: best.allow, matchedPath: best.path };
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=robots-txt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"robots-txt.js","sourceRoot":"","sources":["../../src/robots/robots-txt.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Minimal runtime type guards (the package has zero runtime dependencies). */
|
|
2
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
3
|
+
export declare function isNonEmptyString(value: unknown): value is string;
|
|
4
|
+
export declare function isStringArray(value: unknown): value is string[];
|
|
5
|
+
export declare function isPositiveInteger(value: unknown): value is number;
|
|
6
|
+
export declare function isNonNegativeInteger(value: unknown): value is number;
|
|
7
|
+
export declare function isBoolean(value: unknown): value is boolean;
|
|
8
|
+
export declare function isUint8Array(value: unknown): value is Uint8Array;
|
|
9
|
+
export declare function isMimeTypeLike(value: unknown): value is string;
|
|
10
|
+
export declare function isValidHostnamePattern(value: string): boolean;
|
|
11
|
+
//# sourceMappingURL=runtime-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-utils.d.ts","sourceRoot":"","sources":["../src/runtime-utils.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAE/E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzE;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAEhE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,EAAE,CAE/D;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAEjE;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAEpE;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,OAAO,CAE1D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAEhE;AAKD,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE9D;AAID,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE7D"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Minimal runtime type guards (the package has zero runtime dependencies). */
|
|
2
|
+
export function isRecord(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
export function isNonEmptyString(value) {
|
|
6
|
+
return typeof value === "string" && value.trim() !== "";
|
|
7
|
+
}
|
|
8
|
+
export function isStringArray(value) {
|
|
9
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
10
|
+
}
|
|
11
|
+
export function isPositiveInteger(value) {
|
|
12
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
13
|
+
}
|
|
14
|
+
export function isNonNegativeInteger(value) {
|
|
15
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
16
|
+
}
|
|
17
|
+
export function isBoolean(value) {
|
|
18
|
+
return typeof value === "boolean";
|
|
19
|
+
}
|
|
20
|
+
export function isUint8Array(value) {
|
|
21
|
+
return value instanceof Uint8Array;
|
|
22
|
+
}
|
|
23
|
+
// Accepts concrete MIME types and wildcard patterns ("application/*", "*/*").
|
|
24
|
+
const MIME_TYPE_RE = /^[\w.+\-*]+\/[\w.+\-*]+$/;
|
|
25
|
+
export function isMimeTypeLike(value) {
|
|
26
|
+
return typeof value === "string" && MIME_TYPE_RE.test(value.trim());
|
|
27
|
+
}
|
|
28
|
+
const HOSTNAME_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
|
|
29
|
+
export function isValidHostnamePattern(value) {
|
|
30
|
+
return HOSTNAME_RE.test(value.toLowerCase());
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=runtime-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-utils.js","sourceRoot":"","sources":["../src/runtime-utils.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAE/E,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC9C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,KAAc;IACjD,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAc;IACtC,OAAO,OAAO,KAAK,KAAK,SAAS,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,OAAO,KAAK,YAAY,UAAU,CAAC;AACrC,CAAC;AAED,8EAA8E;AAC9E,MAAM,YAAY,GAAG,0BAA0B,CAAC;AAEhD,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;AACtE,CAAC;AAED,MAAM,WAAW,GAAG,mEAAmE,CAAC;AAExF,MAAM,UAAU,sBAAsB,CAAC,KAAa;IAClD,OAAO,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;AAC/C,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { DnsResolver } from "../types.js";
|
|
2
|
+
/** System resolver used by default. */
|
|
3
|
+
export declare class SystemDnsResolver implements DnsResolver {
|
|
4
|
+
lookup(hostname: string): Promise<{
|
|
5
|
+
addresses: string[];
|
|
6
|
+
}>;
|
|
7
|
+
}
|
|
8
|
+
export type DnsFailureReason = "hostname_blocked" | "resolution_failed" | "unsafe_address";
|
|
9
|
+
/**
|
|
10
|
+
* One security gate for every outbound target. Runs:
|
|
11
|
+
* 1. blocked-hostname check (localhost, cloud metadata, *.internal),
|
|
12
|
+
* 2. IP-literal validation, or
|
|
13
|
+
* 3. DNS resolution followed by validation of every resolved address.
|
|
14
|
+
*
|
|
15
|
+
* `resolution_failed` is returned (not thrown) so callers can map it to a
|
|
16
|
+
* status; hostname/address violations throw UnsafeUrlError (fail closed).
|
|
17
|
+
*/
|
|
18
|
+
export declare class AddressValidator {
|
|
19
|
+
private readonly resolver;
|
|
20
|
+
private readonly now;
|
|
21
|
+
private readonly cache;
|
|
22
|
+
private static readonly CACHE_TTL_MS;
|
|
23
|
+
constructor(resolver: DnsResolver, now: () => number);
|
|
24
|
+
/** Validate `hostname` and return the addresses that were approved. */
|
|
25
|
+
validate(hostname: string): Promise<{
|
|
26
|
+
ok: true;
|
|
27
|
+
addresses: string[];
|
|
28
|
+
} | {
|
|
29
|
+
ok: false;
|
|
30
|
+
reason: Extract<DnsFailureReason, "resolution_failed">;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=dns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dns.d.ts","sourceRoot":"","sources":["../../src/security/dns.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI/C,uCAAuC;AACvC,qBAAa,iBAAkB,YAAW,WAAW;IAC7C,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;CAIjE;AAED,MAAM,MAAM,gBAAgB,GAAG,kBAAkB,GAAG,mBAAmB,GAAG,gBAAgB,CAAC;AAE3F;;;;;;;;GAQG;AACH,qBAAa,gBAAgB;IAKzB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,GAAG;IALtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0D;IAChF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAU;gBAG3B,QAAQ,EAAE,WAAW,EACrB,GAAG,EAAE,MAAM,MAAM;IAGpC,uEAAuE;IACjE,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,OAAO,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,CAAA;KAAE,CAAC;CAmCrJ"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { lookup as dnsLookup } from "node:dns/promises";
|
|
2
|
+
import { UnsafeUrlError } from "../errors.js";
|
|
3
|
+
import { areResolvedAddressesSafe, isBlockedHostname } from "./net-guards.js";
|
|
4
|
+
import { isIP } from "node:net";
|
|
5
|
+
/** System resolver used by default. */
|
|
6
|
+
export class SystemDnsResolver {
|
|
7
|
+
async lookup(hostname) {
|
|
8
|
+
const result = await dnsLookup(hostname, { all: true });
|
|
9
|
+
return { addresses: result.map((r) => r.address) };
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* One security gate for every outbound target. Runs:
|
|
14
|
+
* 1. blocked-hostname check (localhost, cloud metadata, *.internal),
|
|
15
|
+
* 2. IP-literal validation, or
|
|
16
|
+
* 3. DNS resolution followed by validation of every resolved address.
|
|
17
|
+
*
|
|
18
|
+
* `resolution_failed` is returned (not thrown) so callers can map it to a
|
|
19
|
+
* status; hostname/address violations throw UnsafeUrlError (fail closed).
|
|
20
|
+
*/
|
|
21
|
+
export class AddressValidator {
|
|
22
|
+
resolver;
|
|
23
|
+
now;
|
|
24
|
+
cache = new Map();
|
|
25
|
+
static CACHE_TTL_MS = 60_000;
|
|
26
|
+
constructor(resolver, now) {
|
|
27
|
+
this.resolver = resolver;
|
|
28
|
+
this.now = now;
|
|
29
|
+
}
|
|
30
|
+
/** Validate `hostname` and return the addresses that were approved. */
|
|
31
|
+
async validate(hostname) {
|
|
32
|
+
if (isBlockedHostname(hostname)) {
|
|
33
|
+
throw new UnsafeUrlError("Hostname is blocked from outbound requests.", "hostname_blocked");
|
|
34
|
+
}
|
|
35
|
+
if (isIP(hostname) !== 0) {
|
|
36
|
+
if (areResolvedAddressesSafe([hostname]))
|
|
37
|
+
return { ok: true, addresses: [hostname] };
|
|
38
|
+
throw new UnsafeUrlError("Address falls into a blocked range.", "unsafe_address");
|
|
39
|
+
}
|
|
40
|
+
const cached = this.cache.get(hostname);
|
|
41
|
+
if (cached && this.now() - cached.at < AddressValidator.CACHE_TTL_MS) {
|
|
42
|
+
// Cached resolutions were validated when stored; only blocked answers are cached.
|
|
43
|
+
return { ok: true, addresses: cached.addresses };
|
|
44
|
+
}
|
|
45
|
+
let addresses;
|
|
46
|
+
try {
|
|
47
|
+
const resolved = await this.resolver.lookup(hostname);
|
|
48
|
+
addresses = resolved.addresses ?? [];
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { ok: false, reason: "resolution_failed" };
|
|
52
|
+
}
|
|
53
|
+
if (!areResolvedAddressesSafe(addresses)) {
|
|
54
|
+
throw new UnsafeUrlError("Hostname resolves into a blocked address range.", "unsafe_resolved_address");
|
|
55
|
+
}
|
|
56
|
+
this.cache.set(hostname, { addresses, at: this.now() });
|
|
57
|
+
return { ok: true, addresses };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=dns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dns.js","sourceRoot":"","sources":["../../src/security/dns.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEhC,uCAAuC;AACvC,MAAM,OAAO,iBAAiB;IAC5B,KAAK,CAAC,MAAM,CAAC,QAAgB;QAC3B,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,QAAQ,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;QACxD,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACrD,CAAC;CACF;AAID;;;;;;;;GAQG;AACH,MAAM,OAAO,gBAAgB;IAKR;IACA;IALF,KAAK,GAAG,IAAI,GAAG,EAA+C,CAAC;IACxE,MAAM,CAAU,YAAY,GAAG,MAAM,CAAC;IAE9C,YACmB,QAAqB,EACrB,GAAiB;QADjB,aAAQ,GAAR,QAAQ,CAAa;QACrB,QAAG,GAAH,GAAG,CAAc;IACjC,CAAC;IAEJ,uEAAuE;IACvE,KAAK,CAAC,QAAQ,CAAC,QAAgB;QAC7B,IAAI,iBAAiB,CAAC,QAAQ,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,cAAc,CACtB,6CAA6C,EAC7C,kBAAkB,CACnB,CAAC;QACJ,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,IAAI,wBAAwB,CAAC,CAAC,QAAQ,CAAC,CAAC;gBAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrF,MAAM,IAAI,cAAc,CACtB,qCAAqC,EACrC,gBAAgB,CACjB,CAAC;QACJ,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,EAAE,GAAG,gBAAgB,CAAC,YAAY,EAAE,CAAC;YACrE,kFAAkF;YAClF,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC;QACnD,CAAC;QACD,IAAI,SAAmB,CAAC;QACxB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YACtD,SAAS,GAAG,QAAQ,CAAC,SAAS,IAAI,EAAE,CAAC;QACvC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;QACpD,CAAC;QACD,IAAI,CAAC,wBAAwB,CAAC,SAAS,CAAC,EAAE,CAAC;YACzC,MAAM,IAAI,cAAc,CACtB,iDAAiD,EACjD,yBAAyB,CAC1B,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACxD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IACjC,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response header handling: case-insensitive lookup, a strict retention
|
|
3
|
+
* allowlist, and Retry-After parsing. Everything outside the allowlist is
|
|
4
|
+
* redacted before anything is stored or returned.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getHeader(headers: Record<string, string>, name: string): string | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Copy only retention-permitted headers. Authentication, cookies, and any
|
|
9
|
+
* other potentially sensitive header never survive this filter.
|
|
10
|
+
*/
|
|
11
|
+
export declare function retainedResponseHeaders(headers: Record<string, string>): Record<string, string>;
|
|
12
|
+
/** Header names whose presence in any stored record is a bug. */
|
|
13
|
+
export declare const REDACTED_HEADER_NAMES: string[];
|
|
14
|
+
/** Parse Retry-After (delta-seconds or HTTP-date) into whole seconds. */
|
|
15
|
+
export declare function parseRetryAfter(headers: Record<string, string>, now: Date): number | undefined;
|
|
16
|
+
//# sourceMappingURL=headers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../src/security/headers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAeH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI3F;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQ/F;AAED,iEAAiE;AACjE,eAAO,MAAM,qBAAqB,UAAuF,CAAC;AAE1H,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,IAAI,GAAG,MAAM,GAAG,SAAS,CAa9F"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response header handling: case-insensitive lookup, a strict retention
|
|
3
|
+
* allowlist, and Retry-After parsing. Everything outside the allowlist is
|
|
4
|
+
* redacted before anything is stored or returned.
|
|
5
|
+
*/
|
|
6
|
+
const RETAINED_RESPONSE_HEADERS = new Set([
|
|
7
|
+
"content-type",
|
|
8
|
+
"content-length",
|
|
9
|
+
"etag",
|
|
10
|
+
"last-modified",
|
|
11
|
+
"date",
|
|
12
|
+
"cache-control",
|
|
13
|
+
"expires",
|
|
14
|
+
"content-encoding",
|
|
15
|
+
"x-archive-capture-timestamp",
|
|
16
|
+
"x-archive-original-url",
|
|
17
|
+
]);
|
|
18
|
+
export function getHeader(headers, name) {
|
|
19
|
+
const lower = name.toLowerCase();
|
|
20
|
+
const direct = Object.entries(headers).find(([key]) => key.toLowerCase() === lower);
|
|
21
|
+
return direct?.[1];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Copy only retention-permitted headers. Authentication, cookies, and any
|
|
25
|
+
* other potentially sensitive header never survive this filter.
|
|
26
|
+
*/
|
|
27
|
+
export function retainedResponseHeaders(headers) {
|
|
28
|
+
const out = {};
|
|
29
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
30
|
+
if (RETAINED_RESPONSE_HEADERS.has(key.toLowerCase())) {
|
|
31
|
+
out[key.toLowerCase()] = value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
/** Header names whose presence in any stored record is a bug. */
|
|
37
|
+
export const REDACTED_HEADER_NAMES = ["set-cookie", "authorization", "cookie", "proxy-authorization", "www-authenticate"];
|
|
38
|
+
/** Parse Retry-After (delta-seconds or HTTP-date) into whole seconds. */
|
|
39
|
+
export function parseRetryAfter(headers, now) {
|
|
40
|
+
const raw = getHeader(headers, "retry-after");
|
|
41
|
+
if (raw === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
const trimmed = raw.trim();
|
|
44
|
+
if (/^\d+$/.test(trimmed)) {
|
|
45
|
+
return Math.min(Number(trimmed), 86_400);
|
|
46
|
+
}
|
|
47
|
+
const asDate = Date.parse(trimmed);
|
|
48
|
+
if (!Number.isNaN(asDate)) {
|
|
49
|
+
const seconds = Math.ceil((asDate - now.getTime()) / 1000);
|
|
50
|
+
return seconds > 0 ? Math.min(seconds, 86_400) : 0;
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../../src/security/headers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,yBAAyB,GAAG,IAAI,GAAG,CAAC;IACxC,cAAc;IACd,gBAAgB;IAChB,MAAM;IACN,eAAe;IACf,MAAM;IACN,eAAe;IACf,SAAS;IACT,kBAAkB;IAClB,6BAA6B;IAC7B,wBAAwB;CACzB,CAAC,CAAC;AAEH,MAAM,UAAU,SAAS,CAAC,OAA+B,EAAE,IAAY;IACrE,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IACjC,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC,CAAC;IACpF,OAAO,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;AACrB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAA+B;IACrE,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACnD,IAAI,yBAAyB,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YACrD,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,GAAG,KAAK,CAAC;QACjC,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,iEAAiE;AACjE,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,YAAY,EAAE,eAAe,EAAE,QAAQ,EAAE,qBAAqB,EAAE,kBAAkB,CAAC,CAAC;AAE1H,yEAAyE;AACzE,MAAM,UAAU,eAAe,CAAC,OAA+B,EAAE,GAAS;IACxE,MAAM,GAAG,GAAG,SAAS,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;IAC9C,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACxC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;IAC3C,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;QAC3D,OAAO,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function isBlockedHostname(hostname: string): boolean;
|
|
2
|
+
/**
|
|
3
|
+
* True when the address (IPv4 or IPv6 literal) falls into a blocked range.
|
|
4
|
+
* Non-IP strings are treated as blocked (fail closed).
|
|
5
|
+
*/
|
|
6
|
+
export declare function isBlockedAddress(address: string): boolean;
|
|
7
|
+
/** True when every resolved address is safe to contact. */
|
|
8
|
+
export declare function areResolvedAddressesSafe(addresses: string[]): boolean;
|
|
9
|
+
//# sourceMappingURL=net-guards.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"net-guards.d.ts","sourceRoot":"","sources":["../../src/security/net-guards.ts"],"names":[],"mappings":"AAsEA,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAI3D;AAMD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAoBzD;AAED,2DAA2D;AAC3D,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAErE"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
import { BlockList } from "node:net";
|
|
3
|
+
/**
|
|
4
|
+
* Address guards implementing the package's outbound-request policy:
|
|
5
|
+
* loopback, private, link-local, cloud-metadata, shared, multicast,
|
|
6
|
+
* reserved, and documentation ranges are never contacted.
|
|
7
|
+
*
|
|
8
|
+
* IPv4 and IPv6 use separate BlockLists: Node represents IPv4 addresses as
|
|
9
|
+
* IPv4-mapped IPv6 internally, so a "::ffff:0:0/96" subnet would match every
|
|
10
|
+
* IPv4 address (mapped literals are handled explicitly below instead).
|
|
11
|
+
*/
|
|
12
|
+
const v4List = new BlockList();
|
|
13
|
+
const v6List = new BlockList();
|
|
14
|
+
function addV4(cidr) {
|
|
15
|
+
const [address, bits] = cidr.split("/");
|
|
16
|
+
v4List.addSubnet(address, Number(bits), "ipv4");
|
|
17
|
+
}
|
|
18
|
+
function addV6(cidr) {
|
|
19
|
+
const [address, bits] = cidr.split("/");
|
|
20
|
+
v6List.addSubnet(address, Number(bits), "ipv6");
|
|
21
|
+
}
|
|
22
|
+
// IPv4: loopback, private, shared (CGNAT), link-local (incl. cloud metadata
|
|
23
|
+
// 169.254.169.254), reserved, benchmarking, documentation, multicast, broadcast.
|
|
24
|
+
for (const cidr of [
|
|
25
|
+
"0.0.0.0/8",
|
|
26
|
+
"10.0.0.0/8",
|
|
27
|
+
"100.64.0.0/10",
|
|
28
|
+
"127.0.0.0/8",
|
|
29
|
+
"169.254.0.0/16",
|
|
30
|
+
"172.16.0.0/12",
|
|
31
|
+
"192.0.0.0/24",
|
|
32
|
+
"192.0.2.0/24",
|
|
33
|
+
"192.88.99.0/24",
|
|
34
|
+
"192.168.0.0/16",
|
|
35
|
+
"198.18.0.0/15",
|
|
36
|
+
"198.51.100.0/24",
|
|
37
|
+
"203.0.113.0/24",
|
|
38
|
+
"224.0.0.0/4",
|
|
39
|
+
"240.0.0.0/4",
|
|
40
|
+
]) {
|
|
41
|
+
addV4(cidr);
|
|
42
|
+
}
|
|
43
|
+
v4List.addAddress("255.255.255.255", "ipv4");
|
|
44
|
+
// IPv6: unspecified, loopback, ULA (incl. fd00:ec2::254 metadata),
|
|
45
|
+
// link-local, multicast, documentation. IPv4-mapped space is handled below.
|
|
46
|
+
for (const cidr of [
|
|
47
|
+
"::/128",
|
|
48
|
+
"::1/128",
|
|
49
|
+
"fc00::/7",
|
|
50
|
+
"fe80::/10",
|
|
51
|
+
"ff00::/8",
|
|
52
|
+
"2001:db8::/32",
|
|
53
|
+
]) {
|
|
54
|
+
addV6(cidr);
|
|
55
|
+
}
|
|
56
|
+
/** Hostnames blocked regardless of DNS answers. */
|
|
57
|
+
const BLOCKED_HOSTNAMES = new Set([
|
|
58
|
+
"localhost",
|
|
59
|
+
"metadata.google.internal",
|
|
60
|
+
"metadata.goog",
|
|
61
|
+
"instance-data",
|
|
62
|
+
]);
|
|
63
|
+
export function isBlockedHostname(hostname) {
|
|
64
|
+
const host = hostname.toLowerCase();
|
|
65
|
+
if (BLOCKED_HOSTNAMES.has(host))
|
|
66
|
+
return true;
|
|
67
|
+
return host.endsWith(".localhost") || host.endsWith(".internal");
|
|
68
|
+
}
|
|
69
|
+
function checkV4(address) {
|
|
70
|
+
return v4List.check(address, "ipv4");
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* True when the address (IPv4 or IPv6 literal) falls into a blocked range.
|
|
74
|
+
* Non-IP strings are treated as blocked (fail closed).
|
|
75
|
+
*/
|
|
76
|
+
export function isBlockedAddress(address) {
|
|
77
|
+
const family = isIP(address);
|
|
78
|
+
if (family === 0)
|
|
79
|
+
return true;
|
|
80
|
+
if (family === 6) {
|
|
81
|
+
const lower = address.toLowerCase();
|
|
82
|
+
// IPv4-mapped IPv6 in dotted form: ::ffff:127.0.0.1
|
|
83
|
+
const dotted = lower.match(/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/);
|
|
84
|
+
if (dotted?.[1] !== undefined)
|
|
85
|
+
return checkV4(dotted[1]);
|
|
86
|
+
// IPv4-mapped IPv6 in hex form: ::ffff:7f00:1
|
|
87
|
+
const hex = lower.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
88
|
+
if (hex?.[1] !== undefined && hex[2] !== undefined) {
|
|
89
|
+
const high = Number.parseInt(hex[1], 16);
|
|
90
|
+
const low = Number.parseInt(hex[2], 16);
|
|
91
|
+
const embedded = [(high >> 8) & 0xff, high & 0xff, (low >> 8) & 0xff, low & 0xff].join(".");
|
|
92
|
+
return checkV4(embedded);
|
|
93
|
+
}
|
|
94
|
+
return v6List.check(address, "ipv6");
|
|
95
|
+
}
|
|
96
|
+
return checkV4(address);
|
|
97
|
+
}
|
|
98
|
+
/** True when every resolved address is safe to contact. */
|
|
99
|
+
export function areResolvedAddressesSafe(addresses) {
|
|
100
|
+
return addresses.length > 0 && addresses.every((a) => !isBlockedAddress(a));
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=net-guards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"net-guards.js","sourceRoot":"","sources":["../../src/security/net-guards.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface ParsedTargetUrl {
|
|
2
|
+
url: URL;
|
|
3
|
+
/** Lowercase hostname, no brackets (IPv6 literal without square brackets). */
|
|
4
|
+
hostname: string;
|
|
5
|
+
protocol: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Parse a resource URL for outbound use.
|
|
9
|
+
*
|
|
10
|
+
* Throws UnsafeUrlError for non-HTTP(S) schemes and embedded credentials.
|
|
11
|
+
* Malformed or relative URLs throw UnsafeUrlError as well; callers that want
|
|
12
|
+
* request-level errors for those should pre-validate with
|
|
13
|
+
* {@link isAbsoluteHttpUrl}.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseOutboundUrl(raw: string): ParsedTargetUrl;
|
|
16
|
+
/** Loose pre-check used during request-shape validation. */
|
|
17
|
+
export declare function isAbsoluteHttpUrl(raw: string): boolean;
|
|
18
|
+
/** True when `hostname` equals an allowed domain or is a subdomain of it. */
|
|
19
|
+
export declare function hostMatchesAllowedDomains(hostname: string, allowedDomains: readonly string[]): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Strip everything that must never be persisted: credentials, query strings,
|
|
22
|
+
* and fragments. Applies to stored URLs, results, and audit records.
|
|
23
|
+
*/
|
|
24
|
+
export declare function sanitizeUrl(raw: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Derive a safe artifact file name from a URL path.
|
|
27
|
+
* Falls back to "artifact" when the path has no usable final segment.
|
|
28
|
+
*/
|
|
29
|
+
export declare function fileNameFromUrl(raw: string): string;
|
|
30
|
+
/** Sanitize a caller-supplied file name using the same rules as {@link fileNameFromUrl}. */
|
|
31
|
+
export declare function sanitizeFileName(name: string): string;
|
|
32
|
+
//# sourceMappingURL=url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../../src/security/url.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,GAAG,CAAC;IACT,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAwB7D;AAED,4DAA4D;AAC5D,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOtD;AAED,6EAA6E;AAC7E,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,SAAS,MAAM,EAAE,GAChC,OAAO,CAIT;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO/C;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAWnD;AAED,4FAA4F;AAC5F,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKrD"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { UnsafeUrlError } from "../errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parse a resource URL for outbound use.
|
|
4
|
+
*
|
|
5
|
+
* Throws UnsafeUrlError for non-HTTP(S) schemes and embedded credentials.
|
|
6
|
+
* Malformed or relative URLs throw UnsafeUrlError as well; callers that want
|
|
7
|
+
* request-level errors for those should pre-validate with
|
|
8
|
+
* {@link isAbsoluteHttpUrl}.
|
|
9
|
+
*/
|
|
10
|
+
export function parseOutboundUrl(raw) {
|
|
11
|
+
let url;
|
|
12
|
+
try {
|
|
13
|
+
url = new URL(raw);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new UnsafeUrlError("Resource URL could not be parsed.", "url_unparseable");
|
|
17
|
+
}
|
|
18
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
19
|
+
throw new UnsafeUrlError("Resource URL must use http or https.", "url_protocol_forbidden");
|
|
20
|
+
}
|
|
21
|
+
if (url.username !== "" || url.password !== "") {
|
|
22
|
+
throw new UnsafeUrlError("Resource URL must not contain credentials.", "url_credentials_forbidden");
|
|
23
|
+
}
|
|
24
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
25
|
+
if (hostname === "") {
|
|
26
|
+
throw new UnsafeUrlError("Resource URL has no host.", "url_missing_host");
|
|
27
|
+
}
|
|
28
|
+
return { url, hostname, protocol: url.protocol.replace(":", "") };
|
|
29
|
+
}
|
|
30
|
+
/** Loose pre-check used during request-shape validation. */
|
|
31
|
+
export function isAbsoluteHttpUrl(raw) {
|
|
32
|
+
try {
|
|
33
|
+
const u = new URL(raw);
|
|
34
|
+
return (u.protocol === "https:" || u.protocol === "http:") && u.hostname !== "";
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** True when `hostname` equals an allowed domain or is a subdomain of it. */
|
|
41
|
+
export function hostMatchesAllowedDomains(hostname, allowedDomains) {
|
|
42
|
+
return allowedDomains.some((domain) => domain !== "" && (hostname === domain || hostname.endsWith(`.${domain}`)));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Strip everything that must never be persisted: credentials, query strings,
|
|
46
|
+
* and fragments. Applies to stored URLs, results, and audit records.
|
|
47
|
+
*/
|
|
48
|
+
export function sanitizeUrl(raw) {
|
|
49
|
+
const url = new URL(raw);
|
|
50
|
+
url.username = "";
|
|
51
|
+
url.password = "";
|
|
52
|
+
url.hash = "";
|
|
53
|
+
url.search = "";
|
|
54
|
+
return url.toString();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Derive a safe artifact file name from a URL path.
|
|
58
|
+
* Falls back to "artifact" when the path has no usable final segment.
|
|
59
|
+
*/
|
|
60
|
+
export function fileNameFromUrl(raw) {
|
|
61
|
+
let pathname;
|
|
62
|
+
try {
|
|
63
|
+
pathname = new URL(raw).pathname;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return "artifact";
|
|
67
|
+
}
|
|
68
|
+
const segment = decodeURIComponent(pathname.split("/").filter(Boolean).pop() ?? "");
|
|
69
|
+
const safe = segment.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^\.+/, "");
|
|
70
|
+
if (safe === "" || safe === "." || safe.length > 128)
|
|
71
|
+
return "artifact";
|
|
72
|
+
return safe;
|
|
73
|
+
}
|
|
74
|
+
/** Sanitize a caller-supplied file name using the same rules as {@link fileNameFromUrl}. */
|
|
75
|
+
export function sanitizeFileName(name) {
|
|
76
|
+
const base = name.split("/").pop() ?? "";
|
|
77
|
+
const safe = base.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^\.+/, "");
|
|
78
|
+
if (safe === "" || safe === "." || safe.length > 128)
|
|
79
|
+
return "artifact";
|
|
80
|
+
return safe;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url.js","sourceRoot":"","sources":["../../src/security/url.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AcquisitionResult, ArtifactStore, FetchRequest, RightsProfile, Sha256ContentHash, SourcePolicy } from "./types.js";
|
|
2
|
+
export interface PersistArtifactInput {
|
|
3
|
+
acquisitionId: string;
|
|
4
|
+
sourceId: string;
|
|
5
|
+
fileName: string;
|
|
6
|
+
contentType: string;
|
|
7
|
+
bytes: Uint8Array;
|
|
8
|
+
rights: RightsProfile;
|
|
9
|
+
}
|
|
10
|
+
export interface PersistArtifactOutcome {
|
|
11
|
+
/** Present only when raw retention is permitted. */
|
|
12
|
+
artifact?: {
|
|
13
|
+
artifactId: string;
|
|
14
|
+
fileName: string;
|
|
15
|
+
contentType: string;
|
|
16
|
+
contentHash: Sha256ContentHash;
|
|
17
|
+
byteLength: number;
|
|
18
|
+
};
|
|
19
|
+
duplicateOfArtifactId?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Rights-gated, deduplicating persistence of raw artifacts:
|
|
23
|
+
* - raw bytes are stored only when the rights profile allows retention;
|
|
24
|
+
* - content already known for this source is never re-stored or overwritten;
|
|
25
|
+
* - every acquisition keeps its own record via putMetadata.
|
|
26
|
+
*/
|
|
27
|
+
export declare class ArtifactPersistence {
|
|
28
|
+
private readonly store;
|
|
29
|
+
constructor(store: ArtifactStore);
|
|
30
|
+
persistArtifact(input: PersistArtifactInput): Promise<PersistArtifactOutcome>;
|
|
31
|
+
/** Resolve a previous version's artifactId when the caller supplied its content hash. */
|
|
32
|
+
resolvePreviousVersionId(request: Pick<FetchRequest, "previousVersion">, policy: Pick<SourcePolicy, "sourceId">): Promise<string | undefined>;
|
|
33
|
+
/** Metadata + rights decision for one acquisition event. Never contains raw content or credentials. */
|
|
34
|
+
writeAcquisitionMetadata(result: AcquisitionResult, extras: object): Promise<void>;
|
|
35
|
+
/** Compact audit record for every completed request. Sanitized by construction. */
|
|
36
|
+
writeAuditRecord(result: AcquisitionResult, requestedAt: Date, completedAt: Date): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EACjB,aAAa,EACb,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,UAAU,CAAC;IAClB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,sBAAsB;IACrC,oDAAoD;IACpD,QAAQ,CAAC,EAAE;QACT,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,iBAAiB,CAAC;QAC/B,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAcD;;;;;GAKG;AACH,qBAAa,mBAAmB;IAClB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,aAAa;IAE3C,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAyCnF,yFAAyF;IACnF,wBAAwB,CAC5B,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,iBAAiB,CAAC,EAC9C,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,GACrC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAS9B,uGAAuG;IACjG,wBAAwB,CAAC,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBxF,mFAAmF;IAC7E,gBAAgB,CACpB,MAAM,EAAE,iBAAiB,EACzB,WAAW,EAAE,IAAI,EACjB,WAAW,EAAE,IAAI,GAChB,OAAO,CAAC,IAAI,CAAC;CAqBjB"}
|