@kya-os/mcp 1.7.0 → 1.9.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/CHANGELOG.md +69 -0
- package/README.md +219 -3
- package/dist/card/build.d.ts +41 -0
- package/dist/card/build.d.ts.map +1 -0
- package/dist/card/build.js +47 -0
- package/dist/card/build.js.map +1 -0
- package/dist/card/builder.d.ts +99 -0
- package/dist/card/builder.d.ts.map +1 -0
- package/dist/card/builder.js +147 -0
- package/dist/card/builder.js.map +1 -0
- package/dist/card/cimd.d.ts +92 -0
- package/dist/card/cimd.d.ts.map +1 -0
- package/dist/card/cimd.js +225 -0
- package/dist/card/cimd.js.map +1 -0
- package/dist/card/delegation.d.ts +145 -0
- package/dist/card/delegation.d.ts.map +1 -0
- package/dist/card/delegation.js +293 -0
- package/dist/card/delegation.js.map +1 -0
- package/dist/card/emit.d.ts +133 -0
- package/dist/card/emit.d.ts.map +1 -0
- package/dist/card/emit.js +127 -0
- package/dist/card/emit.js.map +1 -0
- package/dist/card/index.d.ts +43 -0
- package/dist/card/index.d.ts.map +1 -0
- package/dist/card/index.js +46 -0
- package/dist/card/index.js.map +1 -0
- package/dist/card/middleware.d.ts +112 -0
- package/dist/card/middleware.d.ts.map +1 -0
- package/dist/card/middleware.js +121 -0
- package/dist/card/middleware.js.map +1 -0
- package/dist/card/proof/build.d.ts +22 -0
- package/dist/card/proof/build.d.ts.map +1 -0
- package/dist/card/proof/build.js +55 -0
- package/dist/card/proof/build.js.map +1 -0
- package/dist/card/proof/canonical.d.ts +66 -0
- package/dist/card/proof/canonical.d.ts.map +1 -0
- package/dist/card/proof/canonical.js +131 -0
- package/dist/card/proof/canonical.js.map +1 -0
- package/dist/card/proof/http-sig.d.ts +69 -0
- package/dist/card/proof/http-sig.d.ts.map +1 -0
- package/dist/card/proof/http-sig.js +101 -0
- package/dist/card/proof/http-sig.js.map +1 -0
- package/dist/card/proof/index.d.ts +25 -0
- package/dist/card/proof/index.d.ts.map +1 -0
- package/dist/card/proof/index.js +25 -0
- package/dist/card/proof/index.js.map +1 -0
- package/dist/card/proof/nonce-cache.d.ts +67 -0
- package/dist/card/proof/nonce-cache.d.ts.map +1 -0
- package/dist/card/proof/nonce-cache.js +88 -0
- package/dist/card/proof/nonce-cache.js.map +1 -0
- package/dist/card/proof/signer.d.ts +32 -0
- package/dist/card/proof/signer.d.ts.map +1 -0
- package/dist/card/proof/signer.js +59 -0
- package/dist/card/proof/signer.js.map +1 -0
- package/dist/card/proof/types.d.ts +219 -0
- package/dist/card/proof/types.d.ts.map +1 -0
- package/dist/card/proof/types.js +87 -0
- package/dist/card/proof/types.js.map +1 -0
- package/dist/card/proof/verify.d.ts +27 -0
- package/dist/card/proof/verify.d.ts.map +1 -0
- package/dist/card/proof/verify.js +236 -0
- package/dist/card/proof/verify.js.map +1 -0
- package/dist/card/resolve.d.ts +97 -0
- package/dist/card/resolve.d.ts.map +1 -0
- package/dist/card/resolve.js +223 -0
- package/dist/card/resolve.js.map +1 -0
- package/dist/card/revocation.d.ts +96 -0
- package/dist/card/revocation.d.ts.map +1 -0
- package/dist/card/revocation.js +190 -0
- package/dist/card/revocation.js.map +1 -0
- package/dist/card/schema.d.ts +177 -0
- package/dist/card/schema.d.ts.map +1 -0
- package/dist/card/schema.js +119 -0
- package/dist/card/schema.js.map +1 -0
- package/dist/card/verify.d.ts +144 -0
- package/dist/card/verify.d.ts.map +1 -0
- package/dist/card/verify.js +132 -0
- package/dist/card/verify.js.map +1 -0
- package/dist/delegation/bitstring.d.ts +9 -7
- package/dist/delegation/bitstring.d.ts.map +1 -1
- package/dist/delegation/bitstring.js +70 -37
- package/dist/delegation/bitstring.js.map +1 -1
- package/dist/delegation/did-linkage.d.ts +23 -0
- package/dist/delegation/did-linkage.d.ts.map +1 -0
- package/dist/delegation/did-linkage.js +57 -0
- package/dist/delegation/did-linkage.js.map +1 -0
- package/dist/delegation/did-resolver-registry.d.ts +7 -0
- package/dist/delegation/did-resolver-registry.d.ts.map +1 -0
- package/dist/delegation/did-resolver-registry.js +20 -0
- package/dist/delegation/did-resolver-registry.js.map +1 -0
- package/dist/delegation/did-web-resolver.d.ts +29 -18
- package/dist/delegation/did-web-resolver.d.ts.map +1 -1
- package/dist/delegation/did-web-resolver.js +65 -35
- package/dist/delegation/did-web-resolver.js.map +1 -1
- package/dist/delegation/index.d.ts +3 -0
- package/dist/delegation/index.d.ts.map +1 -1
- package/dist/delegation/index.js +3 -0
- package/dist/delegation/index.js.map +1 -1
- package/dist/delegation/statuslist-manager.d.ts.map +1 -1
- package/dist/delegation/statuslist-manager.js +16 -1
- package/dist/delegation/statuslist-manager.js.map +1 -1
- package/dist/delegation/vc-jwt-verify.d.ts +61 -0
- package/dist/delegation/vc-jwt-verify.d.ts.map +1 -0
- package/dist/delegation/vc-jwt-verify.js +131 -0
- package/dist/delegation/vc-jwt-verify.js.map +1 -0
- package/dist/delegation/vc-verification-checks.d.ts +50 -0
- package/dist/delegation/vc-verification-checks.d.ts.map +1 -0
- package/dist/delegation/vc-verification-checks.js +212 -0
- package/dist/delegation/vc-verification-checks.js.map +1 -0
- package/dist/delegation/vc-verifier.d.ts +24 -61
- package/dist/delegation/vc-verifier.d.ts.map +1 -1
- package/dist/delegation/vc-verifier.js +57 -180
- package/dist/delegation/vc-verifier.js.map +1 -1
- package/dist/delegation/vc-verifier.types.d.ts +88 -0
- package/dist/delegation/vc-verifier.types.d.ts.map +1 -0
- package/dist/delegation/vc-verifier.types.js +9 -0
- package/dist/delegation/vc-verifier.types.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/integrations/cheqd/dlr.d.ts +44 -0
- package/dist/integrations/cheqd/dlr.d.ts.map +1 -0
- package/dist/integrations/cheqd/dlr.js +86 -0
- package/dist/integrations/cheqd/dlr.js.map +1 -0
- package/dist/integrations/cheqd/index.d.ts +5 -0
- package/dist/integrations/cheqd/index.d.ts.map +1 -0
- package/dist/integrations/cheqd/index.js +5 -0
- package/dist/integrations/cheqd/index.js.map +1 -0
- package/dist/integrations/cheqd/linkage.d.ts +20 -0
- package/dist/integrations/cheqd/linkage.d.ts.map +1 -0
- package/dist/integrations/cheqd/linkage.js +32 -0
- package/dist/integrations/cheqd/linkage.js.map +1 -0
- package/dist/integrations/cheqd/registrar.d.ts +85 -0
- package/dist/integrations/cheqd/registrar.d.ts.map +1 -0
- package/dist/integrations/cheqd/registrar.js +304 -0
- package/dist/integrations/cheqd/registrar.js.map +1 -0
- package/dist/integrations/cheqd/resolver.d.ts +38 -0
- package/dist/integrations/cheqd/resolver.d.ts.map +1 -0
- package/dist/integrations/cheqd/resolver.js +156 -0
- package/dist/integrations/cheqd/resolver.js.map +1 -0
- package/dist/middleware/index.d.ts +2 -0
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +5 -0
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/with-kya-os.config-types.d.ts +139 -0
- package/dist/middleware/with-kya-os.config-types.d.ts.map +1 -0
- package/dist/middleware/with-kya-os.config-types.js +9 -0
- package/dist/middleware/with-kya-os.config-types.js.map +1 -0
- package/dist/middleware/with-kya-os.d.ts +3 -267
- package/dist/middleware/with-kya-os.d.ts.map +1 -1
- package/dist/middleware/with-kya-os.delegation-gate.d.ts +19 -0
- package/dist/middleware/with-kya-os.delegation-gate.d.ts.map +1 -0
- package/dist/middleware/with-kya-os.delegation-gate.js +175 -0
- package/dist/middleware/with-kya-os.delegation-gate.js.map +1 -0
- package/dist/middleware/with-kya-os.delegation-verify.d.ts +51 -0
- package/dist/middleware/with-kya-os.delegation-verify.d.ts.map +1 -0
- package/dist/middleware/with-kya-os.delegation-verify.js +165 -0
- package/dist/middleware/with-kya-os.delegation-verify.js.map +1 -0
- package/dist/middleware/with-kya-os.deps.d.ts +40 -0
- package/dist/middleware/with-kya-os.deps.d.ts.map +1 -0
- package/dist/middleware/with-kya-os.deps.js +9 -0
- package/dist/middleware/with-kya-os.deps.js.map +1 -0
- package/dist/middleware/with-kya-os.grants.d.ts +30 -0
- package/dist/middleware/with-kya-os.grants.d.ts.map +1 -0
- package/dist/middleware/with-kya-os.grants.js +145 -0
- package/dist/middleware/with-kya-os.grants.js.map +1 -0
- package/dist/middleware/with-kya-os.helpers.d.ts +24 -0
- package/dist/middleware/with-kya-os.helpers.d.ts.map +1 -0
- package/dist/middleware/with-kya-os.helpers.js +57 -0
- package/dist/middleware/with-kya-os.helpers.js.map +1 -0
- package/dist/middleware/with-kya-os.js +45 -891
- package/dist/middleware/with-kya-os.js.map +1 -1
- package/dist/middleware/with-kya-os.policy-gate.d.ts +16 -0
- package/dist/middleware/with-kya-os.policy-gate.d.ts.map +1 -0
- package/dist/middleware/with-kya-os.policy-gate.js +98 -0
- package/dist/middleware/with-kya-os.policy-gate.js.map +1 -0
- package/dist/middleware/with-kya-os.protocol.d.ts +29 -0
- package/dist/middleware/with-kya-os.protocol.d.ts.map +1 -0
- package/dist/middleware/with-kya-os.protocol.js +121 -0
- package/dist/middleware/with-kya-os.protocol.js.map +1 -0
- package/dist/middleware/with-kya-os.session.d.ts +32 -0
- package/dist/middleware/with-kya-os.session.d.ts.map +1 -0
- package/dist/middleware/with-kya-os.session.js +201 -0
- package/dist/middleware/with-kya-os.session.js.map +1 -0
- package/dist/middleware/with-kya-os.types.d.ts +178 -0
- package/dist/middleware/with-kya-os.types.d.ts.map +1 -0
- package/dist/middleware/with-kya-os.types.js +13 -0
- package/dist/middleware/with-kya-os.types.js.map +1 -0
- package/dist/providers/runtime-fetch.d.ts +8 -0
- package/dist/providers/runtime-fetch.d.ts.map +1 -1
- package/dist/providers/runtime-fetch.js +17 -0
- package/dist/providers/runtime-fetch.js.map +1 -1
- package/dist/providers/web-crypto.d.ts.map +1 -1
- package/dist/providers/web-crypto.js +15 -7
- package/dist/providers/web-crypto.js.map +1 -1
- package/dist/utils/guards.d.ts +2 -0
- package/dist/utils/guards.d.ts.map +1 -0
- package/dist/utils/guards.js +4 -0
- package/dist/utils/guards.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/ip-classifier.d.ts +12 -0
- package/dist/utils/ip-classifier.d.ts.map +1 -0
- package/dist/utils/ip-classifier.js +153 -0
- package/dist/utils/ip-classifier.js.map +1 -0
- package/dist/utils/safe-fetch-transports.d.ts +40 -0
- package/dist/utils/safe-fetch-transports.d.ts.map +1 -0
- package/dist/utils/safe-fetch-transports.js +121 -0
- package/dist/utils/safe-fetch-transports.js.map +1 -0
- package/dist/utils/safe-fetch-types.d.ts +66 -0
- package/dist/utils/safe-fetch-types.d.ts.map +1 -0
- package/dist/utils/safe-fetch-types.js +10 -0
- package/dist/utils/safe-fetch-types.js.map +1 -0
- package/dist/utils/safe-fetch.d.ts +40 -0
- package/dist/utils/safe-fetch.d.ts.map +1 -0
- package/dist/utils/safe-fetch.js +188 -0
- package/dist/utils/safe-fetch.js.map +1 -0
- package/dist/utils/statuslist-purpose.d.ts +12 -0
- package/dist/utils/statuslist-purpose.d.ts.map +1 -0
- package/dist/utils/statuslist-purpose.js +19 -0
- package/dist/utils/statuslist-purpose.js.map +1 -0
- package/dist/utils/url.d.ts +2 -0
- package/dist/utils/url.d.ts.map +1 -0
- package/dist/utils/url.js +8 -0
- package/dist/utils/url.js.map +1 -0
- package/package.json +25 -5
- package/schemas/README.md +2 -1
- package/schemas/card-delegation-credential.json +239 -0
- package/schemas/kya-os-card.schema.json +284 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF-hardened fetch — the Node I/O adapters (the `SafeFetchTransport` + `DnsLookup` defaults).
|
|
3
|
+
*
|
|
4
|
+
* Two transports ship. The DEFAULT `nodeHttpsTransport` PINS the connection to the pre-validated
|
|
5
|
+
* IP (closing the DNS-rebinding TOCTOU window) via a custom `node:https` `lookup`. That pinned path
|
|
6
|
+
* misbehaves in some serverless runtimes — it failed 100% of fetches in the Vercel Node serverless
|
|
7
|
+
* runtime on 2026-07-02, a failure the mocked-transport unit suite cannot catch. For those runtimes,
|
|
8
|
+
* select `fetchTransport`: it connects by HOSTNAME through global `fetch` (the SSRF screen has
|
|
9
|
+
* already run in the policy layer via `resolveAndPin`). All addressing policy lives in
|
|
10
|
+
* `./ip-classifier`; the seam vocabulary lives in `./safe-fetch-types`. No crypto here.
|
|
11
|
+
*/
|
|
12
|
+
import type { DnsLookup, SafeFetchTransport } from './safe-fetch-types.js';
|
|
13
|
+
/** Default DNS seam — resolve every address (`all: true`) for full SSRF screening. */
|
|
14
|
+
export declare const defaultLookup: DnsLookup;
|
|
15
|
+
/**
|
|
16
|
+
* Fetch-based transport — for runtimes where the `node:https` pinned-connect path misbehaves
|
|
17
|
+
* (the Vercel Node serverless runtime failed 100% of fetches on 2026-07-02). Select it explicitly
|
|
18
|
+
* with `transport: fetchTransport`, or rely on the auto-select when `node:https` is unavailable.
|
|
19
|
+
*
|
|
20
|
+
* The SSRF screen is UNCHANGED: for non-first-party origins `resolveAndPin` has already run EVERY
|
|
21
|
+
* resolved address through the `lookup` seam + `isBlockedAddress` policy before this transport is
|
|
22
|
+
* called. This variant then connects by HOSTNAME via global `fetch` (`redirect: 'manual'`, so a
|
|
23
|
+
* 3xx flows back to the caller's redirect loop) — it does NOT pin `init.pinnedAddress`.
|
|
24
|
+
*
|
|
25
|
+
* TOCTOU: `nodeHttpsTransport` PINS the validated IP, closing the DNS-rebinding window. This
|
|
26
|
+
* variant validates-then-reconnects-by-name, so a hostile resolver could hand `lookup` a public
|
|
27
|
+
* IP and `fetch`'s own resolution a private one — a narrow validated-then-reconnected-by-name
|
|
28
|
+
* window it knowingly accepts for serverless compatibility. Prefer the pinned transport where
|
|
29
|
+
* `node:https` works; the first-party escape hatch avoids the window for a caller's own origin.
|
|
30
|
+
*/
|
|
31
|
+
export declare const fetchTransport: SafeFetchTransport;
|
|
32
|
+
/** Default transport — node:https pinned to the validated IP, TLS SNI preserved, body-capped. */
|
|
33
|
+
export declare const nodeHttpsTransport: SafeFetchTransport;
|
|
34
|
+
/**
|
|
35
|
+
* Pick the default transport: the `node:https` pinned transport when it is available, else the
|
|
36
|
+
* fetch-based transport. This only covers ABSENCE of `node:https`; a runtime where the pinned
|
|
37
|
+
* path is present-but-broken (e.g. Vercel Node serverless) must pass `transport: fetchTransport`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function selectDefaultTransport(): SafeFetchTransport;
|
|
40
|
+
//# sourceMappingURL=safe-fetch-transports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-fetch-transports.d.ts","sourceRoot":"","sources":["../../src/utils/safe-fetch-transports.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAe,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAExF,sFAAsF;AACtF,eAAO,MAAM,aAAa,EAAE,SAG3B,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,cAAc,EAAE,kBAG5B,CAAC;AA2BF,iGAAiG;AACjG,eAAO,MAAM,kBAAkB,EAAE,kBAsC7B,CAAC;AAgBL;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,kBAAkB,CAE3D"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF-hardened fetch — the Node I/O adapters (the `SafeFetchTransport` + `DnsLookup` defaults).
|
|
3
|
+
*
|
|
4
|
+
* Two transports ship. The DEFAULT `nodeHttpsTransport` PINS the connection to the pre-validated
|
|
5
|
+
* IP (closing the DNS-rebinding TOCTOU window) via a custom `node:https` `lookup`. That pinned path
|
|
6
|
+
* misbehaves in some serverless runtimes — it failed 100% of fetches in the Vercel Node serverless
|
|
7
|
+
* runtime on 2026-07-02, a failure the mocked-transport unit suite cannot catch. For those runtimes,
|
|
8
|
+
* select `fetchTransport`: it connects by HOSTNAME through global `fetch` (the SSRF screen has
|
|
9
|
+
* already run in the policy layer via `resolveAndPin`). All addressing policy lives in
|
|
10
|
+
* `./ip-classifier`; the seam vocabulary lives in `./safe-fetch-types`. No crypto here.
|
|
11
|
+
*/
|
|
12
|
+
import { lookup as nodeDnsLookup } from 'node:dns/promises';
|
|
13
|
+
import { request as httpsRequest } from 'node:https';
|
|
14
|
+
/** Default DNS seam — resolve every address (`all: true`) for full SSRF screening. */
|
|
15
|
+
export const defaultLookup = async (hostname) => {
|
|
16
|
+
const resolved = await nodeDnsLookup(hostname, { all: true, verbatim: true });
|
|
17
|
+
return resolved.map((entry) => ({ address: entry.address, family: entry.family }));
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Fetch-based transport — for runtimes where the `node:https` pinned-connect path misbehaves
|
|
21
|
+
* (the Vercel Node serverless runtime failed 100% of fetches on 2026-07-02). Select it explicitly
|
|
22
|
+
* with `transport: fetchTransport`, or rely on the auto-select when `node:https` is unavailable.
|
|
23
|
+
*
|
|
24
|
+
* The SSRF screen is UNCHANGED: for non-first-party origins `resolveAndPin` has already run EVERY
|
|
25
|
+
* resolved address through the `lookup` seam + `isBlockedAddress` policy before this transport is
|
|
26
|
+
* called. This variant then connects by HOSTNAME via global `fetch` (`redirect: 'manual'`, so a
|
|
27
|
+
* 3xx flows back to the caller's redirect loop) — it does NOT pin `init.pinnedAddress`.
|
|
28
|
+
*
|
|
29
|
+
* TOCTOU: `nodeHttpsTransport` PINS the validated IP, closing the DNS-rebinding window. This
|
|
30
|
+
* variant validates-then-reconnects-by-name, so a hostile resolver could hand `lookup` a public
|
|
31
|
+
* IP and `fetch`'s own resolution a private one — a narrow validated-then-reconnected-by-name
|
|
32
|
+
* window it knowingly accepts for serverless compatibility. Prefer the pinned transport where
|
|
33
|
+
* `node:https` works; the first-party escape hatch avoids the window for a caller's own origin.
|
|
34
|
+
*/
|
|
35
|
+
export const fetchTransport = async (url, init) => {
|
|
36
|
+
const response = await fetch(url, { method: 'GET', redirect: 'manual', signal: init.signal });
|
|
37
|
+
return readCapped(response, init.maxBytes);
|
|
38
|
+
};
|
|
39
|
+
/** Stream a fetch Response body into a RawResponse, enforcing the byte cap mid-stream. */
|
|
40
|
+
async function readCapped(response, maxBytes) {
|
|
41
|
+
const reader = response.body?.getReader();
|
|
42
|
+
const chunks = [];
|
|
43
|
+
let size = 0;
|
|
44
|
+
if (reader) {
|
|
45
|
+
for (;;) {
|
|
46
|
+
const { done, value } = await reader.read();
|
|
47
|
+
if (done)
|
|
48
|
+
break;
|
|
49
|
+
size += value.length;
|
|
50
|
+
if (size > maxBytes) {
|
|
51
|
+
await reader.cancel();
|
|
52
|
+
throw new Error(`safe-fetch: response body exceeds size cap ${maxBytes}`);
|
|
53
|
+
}
|
|
54
|
+
chunks.push(value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
58
|
+
return {
|
|
59
|
+
status: response.status,
|
|
60
|
+
headers: { get: (name) => response.headers.get(name) },
|
|
61
|
+
text: () => Promise.resolve(body),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Default transport — node:https pinned to the validated IP, TLS SNI preserved, body-capped. */
|
|
65
|
+
export const nodeHttpsTransport = (url, init) => new Promise((resolve, reject) => {
|
|
66
|
+
// Return the pinned IP for BOTH lookup call-shapes. Node ≥20 defaults to
|
|
67
|
+
// `autoSelectFamily=true`, which invokes a custom lookup with `{ all: true }` and expects an
|
|
68
|
+
// ARRAY of `{ address, family }`; a scalar 3-arg callback yields "Invalid IP address:
|
|
69
|
+
// undefined" and breaks every real request. Honour the array form when `all` is asked,
|
|
70
|
+
// the scalar form otherwise.
|
|
71
|
+
const family = init.family === 6 ? 6 : 4;
|
|
72
|
+
const pinnedLookup = (_hostname, options, cb) => {
|
|
73
|
+
if (typeof options === 'object' && options?.all) {
|
|
74
|
+
cb(null, [
|
|
75
|
+
{ address: init.pinnedAddress, family },
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
cb(null, init.pinnedAddress, family);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const req = httpsRequest(url, { method: 'GET', servername: init.hostname, signal: init.signal, lookup: pinnedLookup }, (res) => {
|
|
83
|
+
const chunks = [];
|
|
84
|
+
let size = 0;
|
|
85
|
+
res.on('data', (chunk) => {
|
|
86
|
+
size += chunk.length;
|
|
87
|
+
if (size > init.maxBytes) {
|
|
88
|
+
req.destroy();
|
|
89
|
+
reject(new Error(`safe-fetch: response body exceeds size cap ${init.maxBytes}`));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
chunks.push(chunk);
|
|
93
|
+
});
|
|
94
|
+
res.on('end', () => resolve(toRawResponse(res, chunks)));
|
|
95
|
+
res.on('error', reject);
|
|
96
|
+
});
|
|
97
|
+
req.on('error', reject);
|
|
98
|
+
req.end();
|
|
99
|
+
});
|
|
100
|
+
/** Project a node:http IncomingMessage + buffered chunks into the RawResponse shape. */
|
|
101
|
+
function toRawResponse(res, chunks) {
|
|
102
|
+
return {
|
|
103
|
+
status: res.statusCode ?? 0,
|
|
104
|
+
headers: {
|
|
105
|
+
get: (name) => {
|
|
106
|
+
const value = res.headers[name.toLowerCase()];
|
|
107
|
+
return Array.isArray(value) ? (value[0] ?? null) : (value ?? null);
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
text: () => Promise.resolve(Buffer.concat(chunks).toString('utf8')),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Pick the default transport: the `node:https` pinned transport when it is available, else the
|
|
115
|
+
* fetch-based transport. This only covers ABSENCE of `node:https`; a runtime where the pinned
|
|
116
|
+
* path is present-but-broken (e.g. Vercel Node serverless) must pass `transport: fetchTransport`.
|
|
117
|
+
*/
|
|
118
|
+
export function selectDefaultTransport() {
|
|
119
|
+
return typeof httpsRequest === 'function' ? nodeHttpsTransport : fetchTransport;
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=safe-fetch-transports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-fetch-transports.js","sourceRoot":"","sources":["../../src/utils/safe-fetch-transports.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF-hardened fetch — the shared seam types.
|
|
3
|
+
*
|
|
4
|
+
* Kept in their own module so the pure address classifier (`./ip-classifier`), the Node I/O
|
|
5
|
+
* adapters (`./safe-fetch-transports`), and the policy orchestrator (`./safe-fetch`) can all
|
|
6
|
+
* depend on the vocabulary without any import cycle. `./safe-fetch` re-exports every name here,
|
|
7
|
+
* so the public surface (`@kya-os/mcp` barrels) is unchanged.
|
|
8
|
+
*/
|
|
9
|
+
/** A resolved address + its IP family (4 or 6). */
|
|
10
|
+
export interface DnsAddress {
|
|
11
|
+
address: string;
|
|
12
|
+
family: number;
|
|
13
|
+
}
|
|
14
|
+
/** DNS resolution seam — returns every address the host resolves to. */
|
|
15
|
+
export type DnsLookup = (hostname: string) => Promise<DnsAddress[]>;
|
|
16
|
+
/** The minimal response a transport yields; safe-fetch interprets status/headers/body. */
|
|
17
|
+
export interface RawResponse {
|
|
18
|
+
status: number;
|
|
19
|
+
headers: {
|
|
20
|
+
get(name: string): string | null;
|
|
21
|
+
};
|
|
22
|
+
text: () => Promise<string>;
|
|
23
|
+
}
|
|
24
|
+
/** Per-request context handed to the transport (connection already validated + pinned). */
|
|
25
|
+
export interface TransportInit {
|
|
26
|
+
signal: AbortSignal;
|
|
27
|
+
hostname: string;
|
|
28
|
+
pinnedAddress: string;
|
|
29
|
+
family: number;
|
|
30
|
+
maxBytes: number;
|
|
31
|
+
}
|
|
32
|
+
/** Transport seam — performs ONE request (no redirect following) to the pinned address. */
|
|
33
|
+
export type SafeFetchTransport = (url: string, init: TransportInit) => Promise<RawResponse>;
|
|
34
|
+
/** The fetcher safe-fetch returns — structurally compatible with the card's `FetchLike`. */
|
|
35
|
+
export interface SafeFetchResponse {
|
|
36
|
+
ok: boolean;
|
|
37
|
+
status: number;
|
|
38
|
+
json: () => Promise<unknown>;
|
|
39
|
+
}
|
|
40
|
+
export type SafeFetch = (url: string) => Promise<SafeFetchResponse>;
|
|
41
|
+
export interface SafeFetchOptions {
|
|
42
|
+
/** DNS seam (default: `node:dns` resolving every address). */
|
|
43
|
+
lookup?: DnsLookup;
|
|
44
|
+
/**
|
|
45
|
+
* Transport seam. Default auto-selects `nodeHttpsTransport` when `node:https` is available,
|
|
46
|
+
* else `fetchTransport`. Serverless runtimes where the pinned path is broken (not absent)
|
|
47
|
+
* should pass `fetchTransport` explicitly.
|
|
48
|
+
*/
|
|
49
|
+
transport?: SafeFetchTransport;
|
|
50
|
+
/**
|
|
51
|
+
* First-party origins the caller OWNS (e.g. `["https://api.example.com"]`). A request whose
|
|
52
|
+
* origin equals one of these skips the SSRF resolve-and-pin screen and is fetched plain by
|
|
53
|
+
* name — origin-equality is not attacker-routable. Each entry MUST be a valid https origin
|
|
54
|
+
* (fail-closed otherwise); every non-listed origin still takes the guarded path.
|
|
55
|
+
*/
|
|
56
|
+
allowOrigins?: string[];
|
|
57
|
+
/** Convenience singular form of {@link SafeFetchOptions.allowOrigins}. */
|
|
58
|
+
firstPartyOrigin?: string;
|
|
59
|
+
/** Request timeout per hop, in ms (default 5000). */
|
|
60
|
+
timeoutMs?: number;
|
|
61
|
+
/** Maximum response body size, in bytes (default 1 MiB). */
|
|
62
|
+
maxBytes?: number;
|
|
63
|
+
/** Maximum same-origin redirects to follow (default 3). */
|
|
64
|
+
maxRedirects?: number;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=safe-fetch-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-fetch-types.d.ts","sourceRoot":"","sources":["../../src/utils/safe-fetch-types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,mDAAmD;AACnD,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wEAAwE;AACxE,MAAM,MAAM,SAAS,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;AAEpE,0FAA0F;AAC1F,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE;QAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAC9C,IAAI,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7B;AAED,2FAA2F;AAC3F,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,2FAA2F;AAC3F,MAAM,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;AAE5F,4FAA4F;AAC5F,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9B;AAED,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAEpE,MAAM,WAAW,gBAAgB;IAC/B,8DAA8D;IAC9D,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF-hardened fetch — the shared seam types.
|
|
3
|
+
*
|
|
4
|
+
* Kept in their own module so the pure address classifier (`./ip-classifier`), the Node I/O
|
|
5
|
+
* adapters (`./safe-fetch-transports`), and the policy orchestrator (`./safe-fetch`) can all
|
|
6
|
+
* depend on the vocabulary without any import cycle. `./safe-fetch` re-exports every name here,
|
|
7
|
+
* so the public surface (`@kya-os/mcp` barrels) is unchanged.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=safe-fetch-types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-fetch-types.js","sourceRoot":"","sources":["../../src/utils/safe-fetch-types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF-hardened outbound fetch (safe-fetch) — the policy orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* `resolveCard` — and, later, JWKS + status-list resolution — follows URLs an attacker can
|
|
5
|
+
* influence across four discovery surfaces, so every outbound request goes through this
|
|
6
|
+
* guard. `createSafeFetch` returns a fetcher structurally compatible with the card's
|
|
7
|
+
* `FetchLike` ({ ok, status, json }) that enforces, fail-closed:
|
|
8
|
+
*
|
|
9
|
+
* - https-only (no http:, file:, data:, …);
|
|
10
|
+
* - the connection IP is a PUBLIC UNICAST address (RFC1918, loopback, link-local incl. the
|
|
11
|
+
* 169.254.169.254 cloud-metadata endpoint, IPv6 ULA/link-local, NAT64/6to4/Teredo transition
|
|
12
|
+
* ranges are denied — see `./ip-classifier`). Each HOP is resolved ONCE and its connection is
|
|
13
|
+
* PINNED to that validated address, closing the DNS-rebinding (TOCTOU) window within the hop; a
|
|
14
|
+
* same-origin redirect to a different host triggers a fresh resolve-and-pin cycle for that hop;
|
|
15
|
+
* - cross-origin redirects are blocked (manual handling; a 3xx to a different origin is
|
|
16
|
+
* rejected, same-origin redirects are followed up to a small cap);
|
|
17
|
+
* - a response size cap and an AbortSignal-backed timeout bound resource use.
|
|
18
|
+
*
|
|
19
|
+
* The two Node transports live in `./safe-fetch-transports`; the seam vocabulary in
|
|
20
|
+
* `./safe-fetch-types`; the address policy in `./ip-classifier`. This module owns only the
|
|
21
|
+
* hop-by-hop POLICY (scheme, resolve-and-pin, redirect, size cap, timeout, first-party escape
|
|
22
|
+
* hatch) and re-exports the transports + classifier + types so the public surface is one import.
|
|
23
|
+
*
|
|
24
|
+
* First-party escape hatch: `allowOrigins` / `firstPartyOrigin` list origins a caller owns. A
|
|
25
|
+
* request whose origin equals a configured one is fetched plain (origin-equality is not
|
|
26
|
+
* attacker-routable); every other origin still takes the guarded path, fail-closed.
|
|
27
|
+
*
|
|
28
|
+
* All I/O is injected via pluggable seams (`lookup`, `transport`) so the policy is unit tested
|
|
29
|
+
* with mocked DNS + transport. No crypto here — no mcp-i-core dependency leaks in.
|
|
30
|
+
*/
|
|
31
|
+
import type { SafeFetch, SafeFetchOptions } from './safe-fetch-types.js';
|
|
32
|
+
export * from './safe-fetch-types.js';
|
|
33
|
+
export { isBlockedAddress } from './ip-classifier.js';
|
|
34
|
+
export { defaultLookup, fetchTransport, nodeHttpsTransport, selectDefaultTransport, } from './safe-fetch-transports.js';
|
|
35
|
+
/**
|
|
36
|
+
* Build an SSRF-hardened fetcher. The returned function is structurally compatible with the
|
|
37
|
+
* card's `FetchLike`: `(url) => Promise<{ ok, status, json }>`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createSafeFetch(options?: SafeFetchOptions): SafeFetch;
|
|
40
|
+
//# sourceMappingURL=safe-fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-fetch.d.ts","sourceRoot":"","sources":["../../src/utils/safe-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAQH,OAAO,KAAK,EAIV,SAAS,EACT,gBAAgB,EAGjB,MAAM,uBAAuB,CAAC;AAG/B,cAAc,uBAAuB,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EACL,aAAa,EACb,cAAc,EACd,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,4BAA4B,CAAC;AA6EpC;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,SAAS,CAuBzE"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF-hardened outbound fetch (safe-fetch) — the policy orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* `resolveCard` — and, later, JWKS + status-list resolution — follows URLs an attacker can
|
|
5
|
+
* influence across four discovery surfaces, so every outbound request goes through this
|
|
6
|
+
* guard. `createSafeFetch` returns a fetcher structurally compatible with the card's
|
|
7
|
+
* `FetchLike` ({ ok, status, json }) that enforces, fail-closed:
|
|
8
|
+
*
|
|
9
|
+
* - https-only (no http:, file:, data:, …);
|
|
10
|
+
* - the connection IP is a PUBLIC UNICAST address (RFC1918, loopback, link-local incl. the
|
|
11
|
+
* 169.254.169.254 cloud-metadata endpoint, IPv6 ULA/link-local, NAT64/6to4/Teredo transition
|
|
12
|
+
* ranges are denied — see `./ip-classifier`). Each HOP is resolved ONCE and its connection is
|
|
13
|
+
* PINNED to that validated address, closing the DNS-rebinding (TOCTOU) window within the hop; a
|
|
14
|
+
* same-origin redirect to a different host triggers a fresh resolve-and-pin cycle for that hop;
|
|
15
|
+
* - cross-origin redirects are blocked (manual handling; a 3xx to a different origin is
|
|
16
|
+
* rejected, same-origin redirects are followed up to a small cap);
|
|
17
|
+
* - a response size cap and an AbortSignal-backed timeout bound resource use.
|
|
18
|
+
*
|
|
19
|
+
* The two Node transports live in `./safe-fetch-transports`; the seam vocabulary in
|
|
20
|
+
* `./safe-fetch-types`; the address policy in `./ip-classifier`. This module owns only the
|
|
21
|
+
* hop-by-hop POLICY (scheme, resolve-and-pin, redirect, size cap, timeout, first-party escape
|
|
22
|
+
* hatch) and re-exports the transports + classifier + types so the public surface is one import.
|
|
23
|
+
*
|
|
24
|
+
* First-party escape hatch: `allowOrigins` / `firstPartyOrigin` list origins a caller owns. A
|
|
25
|
+
* request whose origin equals a configured one is fetched plain (origin-equality is not
|
|
26
|
+
* attacker-routable); every other origin still takes the guarded path, fail-closed.
|
|
27
|
+
*
|
|
28
|
+
* All I/O is injected via pluggable seams (`lookup`, `transport`) so the policy is unit tested
|
|
29
|
+
* with mocked DNS + transport. No crypto here — no mcp-i-core dependency leaks in.
|
|
30
|
+
*/
|
|
31
|
+
import { isBlockedAddress } from './ip-classifier.js';
|
|
32
|
+
import { defaultLookup, fetchTransport, selectDefaultTransport, } from './safe-fetch-transports.js';
|
|
33
|
+
// Re-export the full public surface so a consumer needs only `./safe-fetch` (barrels unchanged).
|
|
34
|
+
export * from './safe-fetch-types.js';
|
|
35
|
+
export { isBlockedAddress } from './ip-classifier.js';
|
|
36
|
+
export { defaultLookup, fetchTransport, nodeHttpsTransport, selectDefaultTransport, } from './safe-fetch-transports.js';
|
|
37
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
38
|
+
const DEFAULT_MAX_BYTES = 1_048_576;
|
|
39
|
+
const DEFAULT_MAX_REDIRECTS = 3;
|
|
40
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
41
|
+
/** Parse + require https; returns the normalized href. */
|
|
42
|
+
function assertHttpsUrl(raw) {
|
|
43
|
+
let url;
|
|
44
|
+
try {
|
|
45
|
+
url = new URL(raw);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error(`safe-fetch: invalid URL "${raw}"`);
|
|
49
|
+
}
|
|
50
|
+
if (url.protocol !== 'https:') {
|
|
51
|
+
throw new Error(`safe-fetch: only https is allowed (got "${url.protocol}")`);
|
|
52
|
+
}
|
|
53
|
+
return url.href;
|
|
54
|
+
}
|
|
55
|
+
/** Resolve the host ONCE, reject if ANY resolved address is non-public, and pin the first. */
|
|
56
|
+
async function resolveAndPin(hostname, lookup) {
|
|
57
|
+
const addresses = await lookup(hostname);
|
|
58
|
+
const [first] = addresses;
|
|
59
|
+
if (!first) {
|
|
60
|
+
throw new Error(`safe-fetch: DNS lookup for "${hostname}" returned no addresses`);
|
|
61
|
+
}
|
|
62
|
+
for (const candidate of addresses) {
|
|
63
|
+
if (isBlockedAddress(candidate.address)) {
|
|
64
|
+
throw new Error(`safe-fetch: refusing non-public address ${candidate.address} for host "${hostname}"`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return first;
|
|
68
|
+
}
|
|
69
|
+
/** Resolve a 3xx into the next URL, rejecting cross-origin / non-https / missing-Location. */
|
|
70
|
+
function nextRedirectUrl(currentUrl, raw) {
|
|
71
|
+
const location = raw.headers.get('location');
|
|
72
|
+
if (!location) {
|
|
73
|
+
throw new Error(`safe-fetch: ${raw.status} redirect without a Location header`);
|
|
74
|
+
}
|
|
75
|
+
let target;
|
|
76
|
+
try {
|
|
77
|
+
target = new URL(location, currentUrl);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Keep the module's fail-closed convention: a malformed Location surfaces a `safe-fetch:`-
|
|
81
|
+
// prefixed error rather than a raw TypeError leaking out of the URL constructor.
|
|
82
|
+
throw new Error(`safe-fetch: redirect location is not a valid URL "${location}"`);
|
|
83
|
+
}
|
|
84
|
+
if (target.protocol !== 'https:') {
|
|
85
|
+
throw new Error(`safe-fetch: redirect to non-https target "${target.protocol}" blocked`);
|
|
86
|
+
}
|
|
87
|
+
const origin = new URL(currentUrl).origin;
|
|
88
|
+
if (target.origin !== origin) {
|
|
89
|
+
throw new Error(`safe-fetch: cross-origin redirect blocked (${origin} → ${target.origin})`);
|
|
90
|
+
}
|
|
91
|
+
return target.href;
|
|
92
|
+
}
|
|
93
|
+
/** Enforce the size cap and project a RawResponse into the FetchLike-compatible shape. */
|
|
94
|
+
async function finalize(raw, maxBytes) {
|
|
95
|
+
const declared = Number(raw.headers.get('content-length'));
|
|
96
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
97
|
+
throw new Error(`safe-fetch: declared content-length ${declared} exceeds cap ${maxBytes}`);
|
|
98
|
+
}
|
|
99
|
+
const body = await raw.text();
|
|
100
|
+
if (Buffer.byteLength(body, 'utf8') > maxBytes) {
|
|
101
|
+
throw new Error(`safe-fetch: response body exceeds size cap ${maxBytes}`);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
ok: raw.status >= 200 && raw.status < 300,
|
|
105
|
+
status: raw.status,
|
|
106
|
+
json: () => Promise.resolve(JSON.parse(body)),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Build an SSRF-hardened fetcher. The returned function is structurally compatible with the
|
|
111
|
+
* card's `FetchLike`: `(url) => Promise<{ ok, status, json }>`.
|
|
112
|
+
*/
|
|
113
|
+
export function createSafeFetch(options = {}) {
|
|
114
|
+
const cfg = {
|
|
115
|
+
lookup: options.lookup ?? defaultLookup,
|
|
116
|
+
transport: options.transport ?? selectDefaultTransport(),
|
|
117
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
118
|
+
maxBytes: options.maxBytes ?? DEFAULT_MAX_BYTES,
|
|
119
|
+
trustedOrigins: normalizeTrustedOrigins(options),
|
|
120
|
+
};
|
|
121
|
+
const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
|
|
122
|
+
return async function safeFetch(initialUrl) {
|
|
123
|
+
let url = assertHttpsUrl(initialUrl);
|
|
124
|
+
for (let hop = 0;; hop++) {
|
|
125
|
+
const raw = await requestOnce(url, cfg);
|
|
126
|
+
if (!REDIRECT_STATUSES.has(raw.status)) {
|
|
127
|
+
return finalize(raw, cfg.maxBytes);
|
|
128
|
+
}
|
|
129
|
+
if (hop >= maxRedirects) {
|
|
130
|
+
throw new Error(`safe-fetch: too many redirects (> ${maxRedirects})`);
|
|
131
|
+
}
|
|
132
|
+
url = nextRedirectUrl(url, raw);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Perform ONE hop. First-party origins skip the resolve-and-pin SSRF screen (origin-equality is
|
|
138
|
+
* not attacker-routable) and are fetched plain by name; every other origin is resolved, screened,
|
|
139
|
+
* and pinned before the configured transport runs. Both branches are timeout-bounded.
|
|
140
|
+
*/
|
|
141
|
+
async function requestOnce(url, cfg) {
|
|
142
|
+
const { hostname, origin } = new URL(url);
|
|
143
|
+
if (cfg.trustedOrigins.has(origin)) {
|
|
144
|
+
return withTimeout(hostname, cfg.timeoutMs, (signal) => fetchTransport(url, { signal, hostname, pinnedAddress: '', family: 4, maxBytes: cfg.maxBytes }));
|
|
145
|
+
}
|
|
146
|
+
const pinned = await resolveAndPin(hostname, cfg.lookup);
|
|
147
|
+
return withTimeout(hostname, cfg.timeoutMs, (signal) => cfg.transport(url, {
|
|
148
|
+
signal,
|
|
149
|
+
hostname,
|
|
150
|
+
pinnedAddress: pinned.address,
|
|
151
|
+
family: pinned.family,
|
|
152
|
+
maxBytes: cfg.maxBytes,
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
/** Run a transport call under an AbortSignal that fires after `timeoutMs`. */
|
|
156
|
+
async function withTimeout(hostname, timeoutMs, run) {
|
|
157
|
+
const controller = new AbortController();
|
|
158
|
+
const timer = setTimeout(() => controller.abort(new Error(`safe-fetch: request to ${hostname} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
159
|
+
try {
|
|
160
|
+
return await run(controller.signal);
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Build the set of first-party origins that bypass the SSRF screen. Each `allowOrigins` /
|
|
168
|
+
* `firstPartyOrigin` entry MUST parse as an https origin — fail-closed (throw) otherwise.
|
|
169
|
+
*/
|
|
170
|
+
function normalizeTrustedOrigins(options) {
|
|
171
|
+
const raw = [...(options.allowOrigins ?? []), ...(options.firstPartyOrigin ? [options.firstPartyOrigin] : [])];
|
|
172
|
+
const origins = new Set();
|
|
173
|
+
for (const entry of raw) {
|
|
174
|
+
let url;
|
|
175
|
+
try {
|
|
176
|
+
url = new URL(entry);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
throw new Error(`safe-fetch: invalid first-party origin "${entry}"`);
|
|
180
|
+
}
|
|
181
|
+
if (url.protocol !== 'https:') {
|
|
182
|
+
throw new Error(`safe-fetch: first-party origin must be https (got "${entry}")`);
|
|
183
|
+
}
|
|
184
|
+
origins.add(url.origin);
|
|
185
|
+
}
|
|
186
|
+
return origins;
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=safe-fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-fetch.js","sourceRoot":"","sources":["../../src/utils/safe-fetch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared fail-closed status-list `statusPurpose` check — the SINGLE source of truth for the
|
|
3
|
+
* invariant both the card revocation reader (`src/card/revocation.ts`) and the delegation status
|
|
4
|
+
* manager (`src/delegation/statuslist-manager.ts`) enforce, so the two readers cannot drift.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Fail CLOSED unless the resolved status list's `statusPurpose` is present AND equals `wanted`. A
|
|
8
|
+
* list of a DIFFERENT kind (e.g. a `suspension` list read as `revocation`) must not be accepted —
|
|
9
|
+
* its clear bit would otherwise report "not revoked" from the wrong list, a fail-open bypass.
|
|
10
|
+
*/
|
|
11
|
+
export declare function assertStatusPurpose(listPurpose: unknown, wanted: string): void;
|
|
12
|
+
//# sourceMappingURL=statuslist-purpose.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"statuslist-purpose.d.ts","sourceRoot":"","sources":["../../src/utils/statuslist-purpose.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAO9E"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared fail-closed status-list `statusPurpose` check — the SINGLE source of truth for the
|
|
3
|
+
* invariant both the card revocation reader (`src/card/revocation.ts`) and the delegation status
|
|
4
|
+
* manager (`src/delegation/statuslist-manager.ts`) enforce, so the two readers cannot drift.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Fail CLOSED unless the resolved status list's `statusPurpose` is present AND equals `wanted`. A
|
|
8
|
+
* list of a DIFFERENT kind (e.g. a `suspension` list read as `revocation`) must not be accepted —
|
|
9
|
+
* its clear bit would otherwise report "not revoked" from the wrong list, a fail-open bypass.
|
|
10
|
+
*/
|
|
11
|
+
export function assertStatusPurpose(listPurpose, wanted) {
|
|
12
|
+
if (typeof listPurpose !== 'string') {
|
|
13
|
+
throw new Error(`status list has no statusPurpose (expected "${wanted}") — fail-closed`);
|
|
14
|
+
}
|
|
15
|
+
if (listPurpose !== wanted) {
|
|
16
|
+
throw new Error(`status list statusPurpose mismatch: "${listPurpose}" ≠ "${wanted}"`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=statuslist-purpose.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"statuslist-purpose.js","sourceRoot":"","sources":["../../src/utils/statuslist-purpose.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,WAAoB,EAAE,MAAc;IACtE,IAAI,OAAO,WAAW,KAAK,QAAQ,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,+CAA+C,MAAM,kBAAkB,CAAC,CAAC;IAC3F,CAAC;IACD,IAAI,WAAW,KAAK,MAAM,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,wCAAwC,WAAW,QAAQ,MAAM,GAAG,CAAC,CAAC;IACxF,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../../src/utils/url.ts"],"names":[],"mappings":"AAAA,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM1D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url.js","sourceRoot":"","sources":["../../src/utils/url.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,oBAAoB,CAAC,KAAa;IAChD,IAAI,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC;IACvB,OAAO,GAAG,GAAG,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;QACnD,GAAG,EAAE,CAAC;IACR,CAAC;IACD,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAC7B,CAAC"}
|