mcp-trellis 2.2.0 → 3.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 +61 -52
- package/dist/adapters/node-request.d.ts +5 -0
- package/dist/adapters/node-request.d.ts.map +1 -1
- package/dist/adapters/node-request.js +17 -1
- package/dist/adapters/node-request.js.map +1 -1
- package/dist/adapters/node.d.ts +15 -0
- package/dist/adapters/node.d.ts.map +1 -1
- package/dist/adapters/node.js +38 -0
- package/dist/adapters/node.js.map +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +1 -0
- package/dist/app.js.map +1 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +63 -6
- package/dist/audit.js.map +1 -1
- package/dist/auth/signed/payload.d.ts +31 -0
- package/dist/auth/signed/payload.d.ts.map +1 -0
- package/dist/auth/signed/payload.js +85 -0
- package/dist/auth/signed/payload.js.map +1 -0
- package/dist/auth/signed/refresh.d.ts +34 -0
- package/dist/auth/signed/refresh.d.ts.map +1 -0
- package/dist/auth/signed/refresh.js +95 -0
- package/dist/auth/signed/refresh.js.map +1 -0
- package/dist/auth/signed/token.d.ts +41 -0
- package/dist/auth/signed/token.d.ts.map +1 -0
- package/dist/auth/signed/token.js +103 -0
- package/dist/auth/signed/token.js.map +1 -0
- package/dist/clients.js +2 -2
- package/dist/clients.js.map +1 -1
- package/dist/deprecated.d.ts +15 -15
- package/dist/deprecated.d.ts.map +1 -1
- package/dist/deprecated.js +15 -15
- package/dist/deprecated.js.map +1 -1
- package/dist/http/body.d.ts +4 -0
- package/dist/http/body.d.ts.map +1 -1
- package/dist/http/body.js +9 -2
- package/dist/http/body.js.map +1 -1
- package/dist/http/origins.d.ts.map +1 -1
- package/dist/http/origins.js +13 -2
- package/dist/http/origins.js.map +1 -1
- package/dist/mcp/dispatch.d.ts.map +1 -1
- package/dist/mcp/dispatch.js +2 -1
- package/dist/mcp/dispatch.js.map +1 -1
- package/dist/mcp/methods.d.ts +8 -0
- package/dist/mcp/methods.d.ts.map +1 -1
- package/dist/mcp/methods.js +14 -1
- package/dist/mcp/methods.js.map +1 -1
- package/dist/mcp/registry.d.ts.map +1 -1
- package/dist/mcp/registry.js +15 -3
- package/dist/mcp/registry.js.map +1 -1
- package/dist/mcp/rpc-handlers.d.ts.map +1 -1
- package/dist/mcp/rpc-handlers.js +60 -13
- package/dist/mcp/rpc-handlers.js.map +1 -1
- package/dist/mcp/tools.d.ts +14 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +118 -23
- package/dist/mcp/tools.js.map +1 -1
- package/dist/mcp/validate.d.ts +7 -1
- package/dist/mcp/validate.d.ts.map +1 -1
- package/dist/mcp/validate.js +58 -24
- package/dist/mcp/validate.js.map +1 -1
- package/dist/oauth/audit.d.ts +10 -0
- package/dist/oauth/audit.d.ts.map +1 -1
- package/dist/oauth/audit.js +25 -1
- package/dist/oauth/audit.js.map +1 -1
- package/dist/oauth/cimd/cimd-parse.d.ts.map +1 -1
- package/dist/oauth/cimd/cimd-parse.js +27 -14
- package/dist/oauth/cimd/cimd-parse.js.map +1 -1
- package/dist/oauth/cimd/cimd-ssrf.d.ts +8 -4
- package/dist/oauth/cimd/cimd-ssrf.d.ts.map +1 -1
- package/dist/oauth/cimd/cimd-ssrf.js +13 -39
- package/dist/oauth/cimd/cimd-ssrf.js.map +1 -1
- package/dist/oauth/cimd/cimd.d.ts +14 -18
- package/dist/oauth/cimd/cimd.d.ts.map +1 -1
- package/dist/oauth/cimd/cimd.js +53 -37
- package/dist/oauth/cimd/cimd.js.map +1 -1
- package/dist/oauth/cimd/dns.d.ts +4 -0
- package/dist/oauth/cimd/dns.d.ts.map +1 -0
- package/dist/oauth/cimd/dns.js +45 -0
- package/dist/oauth/cimd/dns.js.map +1 -0
- package/dist/oauth/cimd/failure.d.ts +19 -0
- package/dist/oauth/cimd/failure.d.ts.map +1 -0
- package/dist/oauth/cimd/failure.js +31 -0
- package/dist/oauth/cimd/failure.js.map +1 -0
- package/dist/oauth/cimd/types.d.ts +15 -1
- package/dist/oauth/cimd/types.d.ts.map +1 -1
- package/dist/oauth/cimd/types.js +1 -1
- package/dist/oauth/cimd/types.js.map +1 -1
- package/dist/oauth/cimd/url-guard.d.ts +5 -0
- package/dist/oauth/cimd/url-guard.d.ts.map +1 -0
- package/dist/oauth/cimd/url-guard.js +53 -0
- package/dist/oauth/cimd/url-guard.js.map +1 -0
- package/dist/oauth/config.d.ts +3 -0
- package/dist/oauth/config.d.ts.map +1 -1
- package/dist/oauth/config.js +5 -1
- package/dist/oauth/config.js.map +1 -1
- package/dist/oauth/crypto/clientauth.d.ts.map +1 -1
- package/dist/oauth/crypto/clientauth.js +7 -6
- package/dist/oauth/crypto/clientauth.js.map +1 -1
- package/dist/oauth/deprecated.d.ts +22 -22
- package/dist/oauth/deprecated.d.ts.map +1 -1
- package/dist/oauth/deprecated.js +22 -22
- package/dist/oauth/deprecated.js.map +1 -1
- package/dist/oauth/endpoints/authorize.d.ts.map +1 -1
- package/dist/oauth/endpoints/authorize.js +25 -7
- package/dist/oauth/endpoints/authorize.js.map +1 -1
- package/dist/oauth/endpoints/consent/error-page.d.ts +6 -0
- package/dist/oauth/endpoints/consent/error-page.d.ts.map +1 -0
- package/dist/oauth/endpoints/consent/error-page.js +60 -0
- package/dist/oauth/endpoints/consent/error-page.js.map +1 -0
- package/dist/oauth/endpoints/{consent.d.ts → consent/handler.d.ts} +2 -2
- package/dist/oauth/endpoints/consent/handler.d.ts.map +1 -0
- package/dist/oauth/endpoints/{consent.js → consent/handler.js} +20 -13
- package/dist/oauth/endpoints/consent/handler.js.map +1 -0
- package/dist/oauth/endpoints/{consent-page.d.ts → consent/page.d.ts} +2 -2
- package/dist/oauth/endpoints/consent/page.d.ts.map +1 -0
- package/dist/oauth/endpoints/consent/page.js +106 -0
- package/dist/oauth/endpoints/consent/page.js.map +1 -0
- package/dist/oauth/endpoints/metadata.d.ts +0 -1
- package/dist/oauth/endpoints/metadata.d.ts.map +1 -1
- package/dist/oauth/endpoints/metadata.js.map +1 -1
- package/dist/oauth/endpoints/register.js +3 -3
- package/dist/oauth/endpoints/register.js.map +1 -1
- package/dist/oauth/endpoints/token.d.ts.map +1 -1
- package/dist/oauth/endpoints/token.js +9 -6
- package/dist/oauth/endpoints/token.js.map +1 -1
- package/dist/oauth/endpoints/wellknown.d.ts.map +1 -1
- package/dist/oauth/endpoints/wellknown.js +2 -9
- package/dist/oauth/endpoints/wellknown.js.map +1 -1
- package/dist/oauth/policy/redirect.d.ts +4 -0
- package/dist/oauth/policy/redirect.d.ts.map +1 -1
- package/dist/oauth/policy/redirect.js +15 -1
- package/dist/oauth/policy/redirect.js.map +1 -1
- package/dist/oauth/policy/reqbody.d.ts +6 -1
- package/dist/oauth/policy/reqbody.d.ts.map +1 -1
- package/dist/oauth/policy/reqbody.js +8 -3
- package/dist/oauth/policy/reqbody.js.map +1 -1
- package/dist/oauth/router.js +1 -1
- package/dist/oauth/router.js.map +1 -1
- package/dist/oauth/types.d.ts +3 -1
- package/dist/oauth/types.d.ts.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/util/clip.d.ts +7 -0
- package/dist/util/clip.d.ts.map +1 -0
- package/dist/util/clip.js +13 -0
- package/dist/util/clip.js.map +1 -0
- package/dist/util/json.d.ts +5 -0
- package/dist/util/json.d.ts.map +1 -1
- package/dist/util/json.js +21 -0
- package/dist/util/json.js.map +1 -1
- package/dist/util/own.d.ts +8 -0
- package/dist/util/own.d.ts.map +1 -0
- package/dist/util/own.js +8 -0
- package/dist/util/own.js.map +1 -0
- package/docs/ROADMAP.md +17 -229
- package/docs/compatibility.md +15 -3
- package/docs/guide.md +15 -4
- package/docs/reference.md +5 -3
- package/docs/security.md +63 -7
- package/docs/troubleshooting.md +4 -0
- package/package.json +7 -4
- package/dist/auth/signed-token.d.ts +0 -26
- package/dist/auth/signed-token.d.ts.map +0 -1
- package/dist/auth/signed-token.js +0 -96
- package/dist/auth/signed-token.js.map +0 -1
- package/dist/oauth/endpoints/consent-page.d.ts.map +0 -1
- package/dist/oauth/endpoints/consent-page.js +0 -63
- package/dist/oauth/endpoints/consent-page.js.map +0 -1
- package/dist/oauth/endpoints/consent.d.ts.map +0 -1
- package/dist/oauth/endpoints/consent.js.map +0 -1
- package/docs/diagrams/demo-tool-result.png +0 -0
- package/docs/diagrams/mcp-trellis-architecture.html +0 -305
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SSRF
|
|
3
|
-
* IP blocks
|
|
2
|
+
* SSRF address policy for CIMD URL fetch: hostname normalization and
|
|
3
|
+
* private/NAT64 IP blocks. `url-guard.ts` applies it before connect.
|
|
4
4
|
*/
|
|
5
|
+
/** Strip WHATWG brackets from IPv6 hostnames (`[::1]` → `::1`). */
|
|
6
|
+
export declare const normalizeHostname: (hostname: string) => string;
|
|
7
|
+
export declare const isBlockedIp: (ip: string) => boolean;
|
|
8
|
+
export declare const isBlockedHostname: (hostname: string) => boolean;
|
|
5
9
|
export declare const isLoopbackHostname: (hostname: string) => boolean;
|
|
6
|
-
|
|
7
|
-
export declare const
|
|
10
|
+
/** A canonical IPv4 or well-formed IPv6 literal — nothing for DNS to resolve. */
|
|
11
|
+
export declare const isIpLiteral: (hostname: string) => boolean;
|
|
8
12
|
//# sourceMappingURL=cimd-ssrf.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cimd-ssrf.d.ts","sourceRoot":"","sources":["../../../src/oauth/cimd/cimd-ssrf.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"cimd-ssrf.d.ts","sourceRoot":"","sources":["../../../src/oauth/cimd/cimd-ssrf.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mEAAmE;AACnE,eAAO,MAAM,iBAAiB,GAAI,UAAU,MAAM,KAAG,MAGpD,CAAC;AAsJF,eAAO,MAAM,WAAW,GAAI,IAAI,MAAM,KAAG,OAaxC,CAAC;AAKF,eAAO,MAAM,iBAAiB,GAAI,UAAU,MAAM,KAAG,OAKpD,CAAC;AAIF,eAAO,MAAM,kBAAkB,GAAI,UAAU,MAAM,KAAG,OAEL,CAAC;AAElD,iFAAiF;AACjF,eAAO,MAAM,WAAW,GAAI,UAAU,MAAM,KAAG,OAG9C,CAAC"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SSRF
|
|
3
|
-
* IP blocks
|
|
2
|
+
* SSRF address policy for CIMD URL fetch: hostname normalization and
|
|
3
|
+
* private/NAT64 IP blocks. `url-guard.ts` applies it before connect.
|
|
4
4
|
*/
|
|
5
5
|
/** Strip WHATWG brackets from IPv6 hostnames (`[::1]` → `::1`). */
|
|
6
|
-
const normalizeHostname = (hostname) => {
|
|
6
|
+
export const normalizeHostname = (hostname) => {
|
|
7
7
|
const host = hostname.toLowerCase().replace(/\.$/, "");
|
|
8
8
|
return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
9
9
|
};
|
|
@@ -103,7 +103,7 @@ const expandIpv6Hextets = (ip) => {
|
|
|
103
103
|
const isNonCanonicalIpv4 = (v4) => IPV4_CANDIDATE.test(v4) && !DOTTED_QUAD.test(v4);
|
|
104
104
|
const isPrivateDottedQuad = (v4) => DOTTED_QUAD.test(v4) && isPrivateV4(v4);
|
|
105
105
|
const IPV6_BLOCKED_EXACT = new Set(["::1", "::"]);
|
|
106
|
-
const IPV6_BLOCKED_PREFIXES = ["
|
|
106
|
+
const IPV6_BLOCKED_PREFIXES = ["::ffff:"];
|
|
107
107
|
/** ULA fc00::/7 and multicast ff00::/8. */
|
|
108
108
|
const IPV6_BLOCKED_PREFIX_RES = [/^f[cd][0-9a-f]{2}:/i, /^ff[0-9a-f]{0,2}:/];
|
|
109
109
|
const isBlockedIpv6Literal = (ip) => IPV6_BLOCKED_EXACT.has(ip) ||
|
|
@@ -124,10 +124,13 @@ const isBlockedIpv4Compatible = (n, raw) => {
|
|
|
124
124
|
const HEXTET_BLOCK_RULES = [
|
|
125
125
|
isIpv4MappedHextets,
|
|
126
126
|
isBlockedIpv4Compatible,
|
|
127
|
+
(n) => ((n[0] ?? 0) & 0xffc0) === 0xfe80, // link-local fe80::/10 (fe80–febf)
|
|
128
|
+
(n) => ((n[0] ?? 0) & 0xffc0) === 0xfec0, // deprecated site-local fec0::/10
|
|
129
|
+
(n) => n[0] === 0x2001 && n[1] === 0, // Teredo 2001::/32
|
|
127
130
|
(n) => n[0] === 0x2002, // 6to4 2002::/16
|
|
128
131
|
(n) => n[0] === 0x64 && n[1] === 0xff9b, // NAT64 64:ff9b::/96
|
|
129
132
|
];
|
|
130
|
-
const isBlockedIp = (ip) => {
|
|
133
|
+
export const isBlockedIp = (ip) => {
|
|
131
134
|
const normalized = normalizeHostname(ip);
|
|
132
135
|
const v4 = expandIpv4Mapped(normalized) ?? normalized;
|
|
133
136
|
if (isNonCanonicalIpv4(v4) || isPrivateDottedQuad(v4))
|
|
@@ -144,7 +147,7 @@ const isBlockedIp = (ip) => {
|
|
|
144
147
|
};
|
|
145
148
|
const BLOCKED_HOST_EXACT = new Set(["localhost"]);
|
|
146
149
|
const BLOCKED_HOST_SUFFIXES = [".localhost", ".local"];
|
|
147
|
-
const isBlockedHostname = (hostname) => {
|
|
150
|
+
export const isBlockedHostname = (hostname) => {
|
|
148
151
|
const host = normalizeHostname(hostname);
|
|
149
152
|
if (BLOCKED_HOST_EXACT.has(host))
|
|
150
153
|
return true;
|
|
@@ -156,38 +159,9 @@ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "::1"]);
|
|
|
156
159
|
export const isLoopbackHostname = (hostname) =>
|
|
157
160
|
// `localhost` deliberately excluded — RFC 8252 §8.3, same as policy/redirect.ts.
|
|
158
161
|
LOOPBACK_HOSTS.has(normalizeHostname(hostname));
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return results.map((r) => r.address);
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
166
|
-
throw new Error("CIMD DNS lookup unavailable");
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
const URL_SAFETY = {
|
|
170
|
-
https: {
|
|
171
|
-
ok: (url) => url.protocol === "https:",
|
|
172
|
-
error: "CIMD client_id must be https",
|
|
173
|
-
},
|
|
174
|
-
noCredentials: {
|
|
175
|
-
ok: (url) => !url.username && !url.password,
|
|
176
|
-
error: "CIMD URL must not carry credentials",
|
|
177
|
-
},
|
|
178
|
-
publicHostname: {
|
|
179
|
-
ok: (url) => !isBlockedHostname(url.hostname),
|
|
180
|
-
error: "CIMD hostname is not publicly routable",
|
|
181
|
-
},
|
|
182
|
-
};
|
|
183
|
-
export const assertSafeUrl = async (url, lookup) => {
|
|
184
|
-
for (const rule of Object.values(URL_SAFETY)) {
|
|
185
|
-
if (!rule.ok(url))
|
|
186
|
-
throw new Error(rule.error);
|
|
187
|
-
}
|
|
188
|
-
const addrs = await lookup(normalizeHostname(url.hostname));
|
|
189
|
-
if (addrs.length === 0 || addrs.some(isBlockedIp)) {
|
|
190
|
-
throw new Error("CIMD resolved to a non-public address");
|
|
191
|
-
}
|
|
162
|
+
/** A canonical IPv4 or well-formed IPv6 literal — nothing for DNS to resolve. */
|
|
163
|
+
export const isIpLiteral = (hostname) => {
|
|
164
|
+
const host = normalizeHostname(hostname);
|
|
165
|
+
return DOTTED_QUAD.test(host) || expandIpv6Hextets(host) !== null;
|
|
192
166
|
};
|
|
193
167
|
//# sourceMappingURL=cimd-ssrf.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cimd-ssrf.js","sourceRoot":"","sources":["../../../src/oauth/cimd/cimd-ssrf.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mEAAmE;AACnE,MAAM,iBAAiB,GAAG,CAAC,QAAgB,EAAU,EAAE;
|
|
1
|
+
{"version":3,"file":"cimd-ssrf.js","sourceRoot":"","sources":["../../../src/oauth/cimd/cimd-ssrf.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,25 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client ID Metadata Documents (CIMD) — HTTPS URL `client_id` metadata.
|
|
3
|
-
* Off by default (`cimd: true`). SSRF guards live in `
|
|
3
|
+
* Off by default (`cimd: true`). SSRF guards live in `url-guard.ts`.
|
|
4
4
|
*
|
|
5
5
|
* ponytail: DNS checked before/after fetch; TCP connect is not pinned (Web
|
|
6
6
|
* fetch). Upgrade: custom dispatcher dialing the resolved IP + Host header.
|
|
7
7
|
*/
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
export type CimdFetchOptions
|
|
11
|
-
/** Override fetch (tests). Defaults to globalThis.fetch. */
|
|
12
|
-
fetch?: typeof fetch;
|
|
13
|
-
cache?: CimdCache;
|
|
14
|
-
/**
|
|
15
|
-
* DNS lookup for rebinding checks. When omitted, the library tries
|
|
16
|
-
* `node:dns/promises.lookup` if available; if that is also unavailable
|
|
17
|
-
* (e.g. some Workers), resolution is refused rather than fetched blind.
|
|
18
|
-
*/
|
|
19
|
-
lookup?: (hostname: string) => Promise<string[]>;
|
|
20
|
-
/** Per-hop timeout covering connect **and** body read. Tests only — default 10 s. */
|
|
21
|
-
timeoutMs?: number;
|
|
22
|
-
};
|
|
8
|
+
import { type CimdFailure } from "./failure.js";
|
|
9
|
+
import type { CimdCache, CimdDocument, CimdFetchOptions } from "./types.js";
|
|
10
|
+
export type { CimdCache, CimdDocument, CimdFetchOptions } from "./types.js";
|
|
23
11
|
/** Build fetch options from router/app CIMD ports. */
|
|
24
12
|
export declare const cimdFetchOpts: (ports: {
|
|
25
13
|
cimdCache?: CimdCache;
|
|
@@ -27,10 +15,18 @@ export declare const cimdFetchOpts: (ports: {
|
|
|
27
15
|
}) => Pick<CimdFetchOptions, "cache" | "lookup">;
|
|
28
16
|
/** True when `client_id` is an HTTPS URL eligible for CIMD resolution. */
|
|
29
17
|
export declare const isCimdClientId: (clientId: string) => boolean;
|
|
18
|
+
export type CimdOutcome = {
|
|
19
|
+
doc: CimdDocument;
|
|
20
|
+
} | {
|
|
21
|
+
failure: CimdFailure;
|
|
22
|
+
};
|
|
30
23
|
/**
|
|
31
24
|
* Fetch and validate a CIMD document for `clientId` (the HTTPS URL itself).
|
|
32
|
-
*
|
|
33
|
-
*
|
|
25
|
+
* Never throws for a fetch / validation problem: the failure comes back as a
|
|
26
|
+
* code for the operator's audit sink, and callers still answer the connector
|
|
27
|
+
* with one generic `unauthorized_client` — no fetch details leak to it.
|
|
34
28
|
*/
|
|
29
|
+
export declare const resolveCimdOutcome: (clientId: string, options?: CimdFetchOptions) => Promise<CimdOutcome>;
|
|
30
|
+
/** `null` on any failure — see `resolveCimdOutcome` for the reason. */
|
|
35
31
|
export declare const resolveCimdClient: (clientId: string, options?: CimdFetchOptions) => Promise<CimdDocument | null>;
|
|
36
32
|
//# sourceMappingURL=cimd.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cimd.d.ts","sourceRoot":"","sources":["../../../src/oauth/cimd/cimd.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"cimd.d.ts","sourceRoot":"","sources":["../../../src/oauth/cimd/cimd.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,EAAa,KAAK,WAAW,EAAuB,MAAM,cAAc,CAAC;AAChF,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG5E,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAM5E,sDAAsD;AACtD,eAAO,MAAM,aAAa,GAAI,OAAO;IACnC,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CACtD,KAAG,IAAI,CAAC,gBAAgB,EAAE,OAAO,GAAG,QAAQ,CAG3C,CAAC;AAEH,0EAA0E;AAC1E,eAAO,MAAM,cAAc,GAAI,UAAU,MAAM,KAAG,OAajD,CAAC;AAkHF,MAAM,MAAM,WAAW,GAAG;IAAE,GAAG,EAAE,YAAY,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,WAAW,CAAA;CAAE,CAAC;AAE3E;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAC7B,UAAU,MAAM,EAChB,UAAS,gBAAqB,KAC7B,OAAO,CAAC,WAAW,CAoBrB,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,iBAAiB,GAC5B,UAAU,MAAM,EAChB,UAAS,gBAAqB,KAC7B,OAAO,CAAC,YAAY,GAAG,IAAI,CAG7B,CAAC"}
|
package/dist/oauth/cimd/cimd.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client ID Metadata Documents (CIMD) — HTTPS URL `client_id` metadata.
|
|
3
|
-
* Off by default (`cimd: true`). SSRF guards live in `
|
|
3
|
+
* Off by default (`cimd: true`). SSRF guards live in `url-guard.ts`.
|
|
4
4
|
*
|
|
5
5
|
* ponytail: DNS checked before/after fetch; TCP connect is not pinned (Web
|
|
6
6
|
* fetch). Upgrade: custom dispatcher dialing the resolved IP + Host header.
|
|
7
7
|
*/
|
|
8
8
|
import { readBoundedText } from "../../http/body.js";
|
|
9
9
|
import { parseCacheMaxAge, parseDocument } from "./cimd-parse.js";
|
|
10
|
-
import {
|
|
10
|
+
import { defaultLookup } from "./dns.js";
|
|
11
|
+
import { CimdError, classifyCimdFailure } from "./failure.js";
|
|
12
|
+
import { assertSafeUrl } from "./url-guard.js";
|
|
11
13
|
const CIMD_MAX_BYTES = 5 * 1024;
|
|
12
14
|
const CIMD_TIMEOUT_MS = 10_000;
|
|
13
15
|
const CIMD_MAX_REDIRECTS = 3;
|
|
@@ -32,31 +34,31 @@ export const isCimdClientId = (clientId) => {
|
|
|
32
34
|
};
|
|
33
35
|
const readCimdBody = async (ctx, response, current) => {
|
|
34
36
|
if (!response.ok)
|
|
35
|
-
|
|
37
|
+
throw new CimdError("http_status", `status ${response.status}`);
|
|
36
38
|
// Stream-capped at CIMD_MAX_BYTES; signal cancels the locked reader on hop
|
|
37
39
|
// timeout (body.cancel() is a no-op while getReader() holds the lock).
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
40
|
+
const text = await readBoundedText(response, CIMD_MAX_BYTES, ctx.signal);
|
|
41
|
+
// Re-check DNS after the body — see file-level ponytail on connect pinning.
|
|
42
|
+
await assertSafeUrl(current, ctx.lookup);
|
|
43
|
+
return {
|
|
44
|
+
doc: parseDocument(ctx.clientId, text),
|
|
45
|
+
maxAge: parseCacheMaxAge(response.headers.get("cache-control")),
|
|
46
|
+
};
|
|
50
47
|
};
|
|
51
48
|
const followRedirect = async (ctx, response, current) => {
|
|
52
49
|
const location = response.headers.get("location");
|
|
53
|
-
if (!location
|
|
54
|
-
|
|
50
|
+
if (!location)
|
|
51
|
+
throw new CimdError("redirect_refused", "redirect without Location");
|
|
52
|
+
// Refused before the target is even looked up.
|
|
53
|
+
if (ctx.redirect === CIMD_MAX_REDIRECTS) {
|
|
54
|
+
throw new CimdError("redirect_refused", "too many redirects");
|
|
55
|
+
}
|
|
55
56
|
const next = new URL(location, current);
|
|
56
57
|
// Same-origin only: a cross-origin hop would let an open redirect on a
|
|
57
58
|
// reputable host vouch for a document the attacker actually serves.
|
|
58
|
-
if (next.origin !== new URL(ctx.clientId).origin)
|
|
59
|
-
|
|
59
|
+
if (next.origin !== new URL(ctx.clientId).origin) {
|
|
60
|
+
throw new CimdError("redirect_refused", "cross-origin redirect");
|
|
61
|
+
}
|
|
60
62
|
await assertSafeUrl(next, ctx.lookup);
|
|
61
63
|
return next;
|
|
62
64
|
};
|
|
@@ -79,8 +81,7 @@ const fetchHop = async (settings, current, redirect) => {
|
|
|
79
81
|
try {
|
|
80
82
|
const response = await getCimdResponse(settings.fetch, current.toString(), controller.signal);
|
|
81
83
|
if (response.status >= 300 && response.status < 400) {
|
|
82
|
-
|
|
83
|
-
return next ? { next } : { done: null };
|
|
84
|
+
return { next: await followRedirect(ctx, response, current) };
|
|
84
85
|
}
|
|
85
86
|
// `await` keeps the body read inside this try — without it `finally`
|
|
86
87
|
// clears the timer as soon as headers arrive.
|
|
@@ -93,40 +94,55 @@ const fetchHop = async (settings, current, redirect) => {
|
|
|
93
94
|
const fetchCimdDocument = async (settings) => {
|
|
94
95
|
let current = new URL(settings.clientId);
|
|
95
96
|
await assertSafeUrl(current, settings.lookup);
|
|
96
|
-
|
|
97
|
+
// Ends: followRedirect refuses any redirect past CIMD_MAX_REDIRECTS.
|
|
98
|
+
for (let redirect = 0;; redirect += 1) {
|
|
97
99
|
const hop = await fetchHop(settings, current, redirect);
|
|
98
100
|
if ("done" in hop)
|
|
99
101
|
return hop.done;
|
|
100
102
|
current = hop.next;
|
|
101
103
|
}
|
|
102
|
-
|
|
104
|
+
};
|
|
105
|
+
/** The document is already valid; a failed write only means the next call re-fetches. */
|
|
106
|
+
const cacheBestEffort = async (cache, clientId, fetched) => {
|
|
107
|
+
try {
|
|
108
|
+
await cache.set(clientId, fetched.doc, fetched.maxAge);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Best-effort by design — see above.
|
|
112
|
+
}
|
|
103
113
|
};
|
|
104
114
|
/**
|
|
105
115
|
* Fetch and validate a CIMD document for `clientId` (the HTTPS URL itself).
|
|
106
|
-
*
|
|
107
|
-
*
|
|
116
|
+
* Never throws for a fetch / validation problem: the failure comes back as a
|
|
117
|
+
* code for the operator's audit sink, and callers still answer the connector
|
|
118
|
+
* with one generic `unauthorized_client` — no fetch details leak to it.
|
|
108
119
|
*/
|
|
109
|
-
export const
|
|
110
|
-
if (!isCimdClientId(clientId))
|
|
111
|
-
return
|
|
120
|
+
export const resolveCimdOutcome = async (clientId, options = {}) => {
|
|
121
|
+
if (!isCimdClientId(clientId)) {
|
|
122
|
+
return { failure: { code: "blocked_url", detail: "client_id is not an https URL" } };
|
|
123
|
+
}
|
|
112
124
|
const cached = options.cache ? await options.cache.get(clientId) : null;
|
|
113
125
|
if (cached)
|
|
114
|
-
return cached;
|
|
126
|
+
return { doc: cached };
|
|
127
|
+
let fetched;
|
|
115
128
|
try {
|
|
116
|
-
|
|
129
|
+
fetched = await fetchCimdDocument({
|
|
117
130
|
clientId,
|
|
118
131
|
fetch: options.fetch ?? globalThis.fetch,
|
|
119
132
|
lookup: options.lookup ?? defaultLookup,
|
|
120
133
|
timeoutMs: options.timeoutMs ?? CIMD_TIMEOUT_MS,
|
|
121
134
|
});
|
|
122
|
-
if (!result)
|
|
123
|
-
return null;
|
|
124
|
-
if (options.cache)
|
|
125
|
-
await options.cache.set(clientId, result.doc, result.maxAge);
|
|
126
|
-
return result.doc;
|
|
127
135
|
}
|
|
128
|
-
catch {
|
|
129
|
-
return
|
|
136
|
+
catch (caught) {
|
|
137
|
+
return { failure: classifyCimdFailure(caught) };
|
|
130
138
|
}
|
|
139
|
+
if (options.cache)
|
|
140
|
+
await cacheBestEffort(options.cache, clientId, fetched);
|
|
141
|
+
return { doc: fetched.doc };
|
|
142
|
+
};
|
|
143
|
+
/** `null` on any failure — see `resolveCimdOutcome` for the reason. */
|
|
144
|
+
export const resolveCimdClient = async (clientId, options = {}) => {
|
|
145
|
+
const outcome = await resolveCimdOutcome(clientId, options);
|
|
146
|
+
return "doc" in outcome ? outcome.doc : null;
|
|
131
147
|
};
|
|
132
148
|
//# sourceMappingURL=cimd.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cimd.js","sourceRoot":"","sources":["../../../src/oauth/cimd/cimd.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"cimd.js","sourceRoot":"","sources":["../../../src/oauth/cimd/cimd.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dns.d.ts","sourceRoot":"","sources":["../../../src/oauth/cimd/dns.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAiB5E,eAAO,MAAM,YAAY,GAAI,QAAQ,OAAO,KAAG,MAG9C,CAAC;AAqBF,eAAO,MAAM,aAAa,GAAU,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,EAAE,CAUtE,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Default DNS resolution for CIMD SSRF checks, across Node and Workers. */
|
|
2
|
+
import { CimdError } from "./failure.js";
|
|
3
|
+
const importDns = async () => {
|
|
4
|
+
try {
|
|
5
|
+
return await import("node:dns/promises");
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
throw new CimdError("dns_unavailable", "no node:dns on this platform; pass cimdLookup");
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
/** Resolver error codes only (`ENOTFOUND`, `ETIMEOUT`) — never free text from the error. */
|
|
12
|
+
const DNS_ERROR_CODE = /^[A-Z][A-Z0-9_]{0,31}$/;
|
|
13
|
+
export const dnsErrorCode = (caught) => {
|
|
14
|
+
const code = caught instanceof Error && "code" in caught ? caught.code : undefined;
|
|
15
|
+
return typeof code === "string" && DNS_ERROR_CODE.test(code) ? code : "unknown";
|
|
16
|
+
};
|
|
17
|
+
/** Workers (nodejs_compat) ships `node:dns` with `lookup` stubbed out to throw. */
|
|
18
|
+
const isLookupNotImplemented = (caught) => caught instanceof Error && /not implemented/i.test(caught.message);
|
|
19
|
+
/**
|
|
20
|
+
* ponytail: `resolve4`/`resolve6` return A/AAAA only (no hosts file, no
|
|
21
|
+
* search domains) — enough for a public HTTPS client_id host. Upgrade: a
|
|
22
|
+
* host-supplied `cimdLookup` backed by its own resolver.
|
|
23
|
+
*/
|
|
24
|
+
const resolveAddresses = async (dns, hostname) => {
|
|
25
|
+
const settled = await Promise.allSettled([dns.resolve4(hostname), dns.resolve6(hostname)]);
|
|
26
|
+
const addresses = settled.flatMap((result) => result.status === "fulfilled" ? result.value : []);
|
|
27
|
+
if (addresses.length > 0)
|
|
28
|
+
return addresses;
|
|
29
|
+
const rejected = settled.find((result) => result.status === "rejected");
|
|
30
|
+
throw new CimdError("dns_failed", `DNS resolution failed (${dnsErrorCode(rejected?.reason)})`);
|
|
31
|
+
};
|
|
32
|
+
export const defaultLookup = async (hostname) => {
|
|
33
|
+
const dns = await importDns();
|
|
34
|
+
try {
|
|
35
|
+
const results = await dns.lookup(hostname, { all: true, verbatim: true });
|
|
36
|
+
return results.map((r) => r.address);
|
|
37
|
+
}
|
|
38
|
+
catch (caught) {
|
|
39
|
+
if (isLookupNotImplemented(caught))
|
|
40
|
+
return resolveAddresses(dns, hostname);
|
|
41
|
+
// url-guard.ts turns any lookup failure into `dns_failed` with its code.
|
|
42
|
+
throw caught;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=dns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dns.js","sourceRoot":"","sources":["../../../src/oauth/cimd/dns.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAE5E,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAIzC,MAAM,SAAS,GAAG,KAAK,IAAsB,EAAE;IAC7C,IAAI,CAAC;QACH,OAAO,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,SAAS,CAAC,iBAAiB,EAAE,+CAA+C,CAAC,CAAC;IAC1F,CAAC;AACH,CAAC,CAAC;AAEF,4FAA4F;AAC5F,MAAM,cAAc,GAAG,wBAAwB,CAAC;AAEhD,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAe,EAAU,EAAE;IACtD,MAAM,IAAI,GAAG,MAAM,YAAY,KAAK,IAAI,MAAM,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IACnF,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AAClF,CAAC,CAAC;AAEF,mFAAmF;AACnF,MAAM,sBAAsB,GAAG,CAAC,MAAe,EAAW,EAAE,CAC1D,MAAM,YAAY,KAAK,IAAI,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AAErE;;;;GAIG;AACH,MAAM,gBAAgB,GAAG,KAAK,EAAE,GAAY,EAAE,QAAgB,EAAqB,EAAE;IACnF,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC3F,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAC3C,MAAM,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAClD,CAAC;IACF,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IAC3C,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC;IACxE,MAAM,IAAI,SAAS,CAAC,YAAY,EAAE,0BAA0B,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;AACjG,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAAE,QAAgB,EAAqB,EAAE;IACzE,MAAM,GAAG,GAAG,MAAM,SAAS,EAAE,CAAC;IAC9B,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1E,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IACvC,CAAC;IAAC,OAAO,MAAM,EAAE,CAAC;QAChB,IAAI,sBAAsB,CAAC,MAAM,CAAC;YAAE,OAAO,gBAAgB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAC3E,yEAAyE;QACzE,MAAM,MAAM,CAAC;IACf,CAAC;AACH,CAAC,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a CIMD document could not be resolved. Reasons reach the operator's
|
|
3
|
+
* audit sink only — the connector always sees one generic error — so a detail
|
|
4
|
+
* is limited to static text, an HTTP status or a DNS error code: never a
|
|
5
|
+
* resolved address, a response header or body, or a redirect target.
|
|
6
|
+
*/
|
|
7
|
+
export type CimdFailureCode = "dns_unavailable" | "dns_failed" | "blocked_address" | "blocked_url" | "http_status" | "redirect_refused" | "too_large" | "timeout" | "not_json" | "document_invalid" | "client_id_mismatch" | "auth_method_unsupported" | "redirect_uris_invalid" | "fetch_failed";
|
|
8
|
+
export type CimdFailure = {
|
|
9
|
+
code: CimdFailureCode;
|
|
10
|
+
detail?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare class CimdError extends Error {
|
|
13
|
+
readonly code: CimdFailureCode;
|
|
14
|
+
constructor(code: CimdFailureCode, message: string);
|
|
15
|
+
}
|
|
16
|
+
/** Maps whatever a fetch attempt threw to a code; unknown errors keep only their class name. */
|
|
17
|
+
export declare const classifyCimdFailure: (caught: unknown) => CimdFailure;
|
|
18
|
+
export declare const formatCimdFailure: ({ code, detail }: CimdFailure) => string;
|
|
19
|
+
//# sourceMappingURL=failure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"failure.d.ts","sourceRoot":"","sources":["../../../src/oauth/cimd/failure.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,MAAM,eAAe,GACvB,iBAAiB,GACjB,YAAY,GACZ,iBAAiB,GACjB,aAAa,GACb,aAAa,GACb,kBAAkB,GAClB,WAAW,GACX,SAAS,GACT,UAAU,GACV,kBAAkB,GAClB,oBAAoB,GACpB,yBAAyB,GACzB,uBAAuB,GACvB,cAAc,CAAC;AAEnB,MAAM,MAAM,WAAW,GAAG;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAErE,qBAAa,SAAU,SAAQ,KAAK;IAClC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;gBAEnB,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM;CAKnD;AAID,gGAAgG;AAChG,eAAO,MAAM,mBAAmB,GAAI,QAAQ,OAAO,KAAG,WAOrD,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,kBAAkB,WAAW,KAAG,MAC5B,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a CIMD document could not be resolved. Reasons reach the operator's
|
|
3
|
+
* audit sink only — the connector always sees one generic error — so a detail
|
|
4
|
+
* is limited to static text, an HTTP status or a DNS error code: never a
|
|
5
|
+
* resolved address, a response header or body, or a redirect target.
|
|
6
|
+
*/
|
|
7
|
+
import { BodyReadAbortedError, BodyTooLargeError } from "../../http/body.js";
|
|
8
|
+
export class CimdError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
constructor(code, message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "CimdError";
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const TIMEOUT_ERROR_NAMES = new Set(["AbortError", "TimeoutError"]);
|
|
17
|
+
/** Maps whatever a fetch attempt threw to a code; unknown errors keep only their class name. */
|
|
18
|
+
export const classifyCimdFailure = (caught) => {
|
|
19
|
+
if (caught instanceof CimdError)
|
|
20
|
+
return { code: caught.code, detail: caught.message };
|
|
21
|
+
if (caught instanceof BodyTooLargeError)
|
|
22
|
+
return { code: "too_large" };
|
|
23
|
+
// The hop timer is the only thing that aborts a CIMD body read.
|
|
24
|
+
if (caught instanceof BodyReadAbortedError)
|
|
25
|
+
return { code: "timeout" };
|
|
26
|
+
if (caught instanceof Error && TIMEOUT_ERROR_NAMES.has(caught.name))
|
|
27
|
+
return { code: "timeout" };
|
|
28
|
+
return { code: "fetch_failed", ...(caught instanceof Error ? { detail: caught.name } : {}) };
|
|
29
|
+
};
|
|
30
|
+
export const formatCimdFailure = ({ code, detail }) => detail ? `${code}: ${detail}` : code;
|
|
31
|
+
//# sourceMappingURL=failure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"failure.js","sourceRoot":"","sources":["../../../src/oauth/cimd/failure.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAoB7E,MAAM,OAAO,SAAU,SAAQ,KAAK;IACzB,IAAI,CAAkB;IAE/B,YAAY,IAAqB,EAAE,OAAe;QAChD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC,CAAC;AAEpE,gGAAgG;AAChG,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAAe,EAAe,EAAE;IAClE,IAAI,MAAM,YAAY,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;IACtF,IAAI,MAAM,YAAY,iBAAiB;QAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IACtE,gEAAgE;IAChE,IAAI,MAAM,YAAY,oBAAoB;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IACvE,IAAI,MAAM,YAAY,KAAK,IAAI,mBAAmB,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAChG,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,CAAC,MAAM,YAAY,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AAC/F,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,EAAE,IAAI,EAAE,MAAM,EAAe,EAAU,EAAE,CACzE,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** CIMD document and
|
|
1
|
+
/** CIMD document, cache and fetch-option shapes, shared by the fetcher and the parser. */
|
|
2
2
|
export type CimdDocument = {
|
|
3
3
|
client_id: string;
|
|
4
4
|
redirect_uris: string[];
|
|
@@ -9,4 +9,18 @@ export type CimdCache = {
|
|
|
9
9
|
get: (clientId: string) => Promise<CimdDocument | null> | CimdDocument | null;
|
|
10
10
|
set: (clientId: string, doc: CimdDocument, maxAgeSeconds: number) => Promise<void> | void;
|
|
11
11
|
};
|
|
12
|
+
export type CimdFetchOptions = {
|
|
13
|
+
/** Override fetch (tests). Defaults to globalThis.fetch. */
|
|
14
|
+
fetch?: typeof fetch;
|
|
15
|
+
cache?: CimdCache;
|
|
16
|
+
/**
|
|
17
|
+
* DNS lookup for rebinding checks. When omitted, the library uses
|
|
18
|
+
* `node:dns/promises.lookup`, falling back to `resolve4` / `resolve6` where
|
|
19
|
+
* `lookup` is stubbed (Workers `nodejs_compat`); with no `node:dns` at all,
|
|
20
|
+
* resolution is refused rather than fetched blind.
|
|
21
|
+
*/
|
|
22
|
+
lookup?: (hostname: string) => Promise<string[]>;
|
|
23
|
+
/** Per-hop timeout covering connect **and** body read. Tests only — default 10 s. */
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
};
|
|
12
26
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/oauth/cimd/types.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/oauth/cimd/types.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAE1F,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,GAAG,YAAY,GAAG,IAAI,CAAC;IAC9E,GAAG,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC3F,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,4DAA4D;IAC5D,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACjD,qFAAqF;IACrF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC"}
|
package/dist/oauth/cimd/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/oauth/cimd/types.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/oauth/cimd/types.ts"],"names":[],"mappings":"AAAA,0FAA0F"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Applies the SSRF address policy to a CIMD URL before (and after) it is fetched. */
|
|
2
|
+
type Lookup = (hostname: string) => Promise<string[]>;
|
|
3
|
+
export declare const assertSafeUrl: (url: URL, lookup: Lookup) => Promise<void>;
|
|
4
|
+
export {};
|
|
5
|
+
//# sourceMappingURL=url-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url-guard.d.ts","sourceRoot":"","sources":["../../../src/oauth/cimd/url-guard.ts"],"names":[],"mappings":"AAAA,sFAAsF;AAMtF,KAAK,MAAM,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;AAyCtD,eAAO,MAAM,aAAa,GAAU,KAAK,GAAG,EAAE,QAAQ,MAAM,KAAG,OAAO,CAAC,IAAI,CAS1E,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Applies the SSRF address policy to a CIMD URL before (and after) it is fetched. */
|
|
2
|
+
import { isBlockedHostname, isBlockedIp, isIpLiteral, normalizeHostname } from "./cimd-ssrf.js";
|
|
3
|
+
import { dnsErrorCode } from "./dns.js";
|
|
4
|
+
import { CimdError } from "./failure.js";
|
|
5
|
+
const URL_SAFETY = {
|
|
6
|
+
https: {
|
|
7
|
+
ok: (url) => url.protocol === "https:",
|
|
8
|
+
code: "blocked_url",
|
|
9
|
+
error: "CIMD client_id must be https",
|
|
10
|
+
},
|
|
11
|
+
noCredentials: {
|
|
12
|
+
ok: (url) => !url.username && !url.password,
|
|
13
|
+
code: "blocked_url",
|
|
14
|
+
error: "CIMD URL must not carry credentials",
|
|
15
|
+
},
|
|
16
|
+
publicHostname: {
|
|
17
|
+
ok: (url) => !isBlockedHostname(url.hostname),
|
|
18
|
+
code: "blocked_address",
|
|
19
|
+
error: "CIMD hostname is not publicly routable",
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The addresses `hostname` resolves to. An IP literal is its own answer, and
|
|
24
|
+
* any lookup failure — the default's or a host-supplied `cimdLookup`'s — is a
|
|
25
|
+
* `dns_failed`, never mistaken for a fetch error or a blocked address.
|
|
26
|
+
*/
|
|
27
|
+
const resolveHost = async (hostname, lookup) => {
|
|
28
|
+
if (isIpLiteral(hostname))
|
|
29
|
+
return [hostname];
|
|
30
|
+
let addrs;
|
|
31
|
+
try {
|
|
32
|
+
addrs = await lookup(hostname);
|
|
33
|
+
}
|
|
34
|
+
catch (caught) {
|
|
35
|
+
if (caught instanceof CimdError)
|
|
36
|
+
throw caught;
|
|
37
|
+
throw new CimdError("dns_failed", `DNS lookup failed (${dnsErrorCode(caught)})`);
|
|
38
|
+
}
|
|
39
|
+
if (addrs.length === 0)
|
|
40
|
+
throw new CimdError("dns_failed", "DNS lookup returned no addresses");
|
|
41
|
+
return addrs;
|
|
42
|
+
};
|
|
43
|
+
export const assertSafeUrl = async (url, lookup) => {
|
|
44
|
+
for (const rule of Object.values(URL_SAFETY)) {
|
|
45
|
+
if (!rule.ok(url))
|
|
46
|
+
throw new CimdError(rule.code, rule.error);
|
|
47
|
+
}
|
|
48
|
+
const addrs = await resolveHost(normalizeHostname(url.hostname), lookup);
|
|
49
|
+
if (addrs.some(isBlockedIp)) {
|
|
50
|
+
throw new CimdError("blocked_address", "CIMD resolved to a non-public address");
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=url-guard.js.map
|