mcp-trellis 0.2.1 → 2.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 +70 -620
- package/dist/adapters/node-request.d.ts +53 -0
- package/dist/adapters/node-request.d.ts.map +1 -0
- package/dist/adapters/node-request.js +165 -0
- package/dist/adapters/node-request.js.map +1 -0
- package/dist/adapters/node.d.ts +16 -24
- package/dist/adapters/node.d.ts.map +1 -1
- package/dist/adapters/node.js +69 -88
- package/dist/adapters/node.js.map +1 -1
- package/dist/advanced.d.ts +18 -0
- package/dist/advanced.d.ts.map +1 -0
- package/dist/advanced.js +18 -0
- package/dist/advanced.js.map +1 -0
- package/dist/app-options.d.ts +132 -0
- package/dist/app-options.d.ts.map +1 -0
- package/dist/app-options.js +2 -0
- package/dist/app-options.js.map +1 -0
- package/dist/app.d.ts +2 -66
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +150 -57
- package/dist/app.js.map +1 -1
- package/dist/audit.d.ts +5 -5
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +19 -8
- package/dist/audit.js.map +1 -1
- package/dist/auth/bearer.d.ts +4 -1
- package/dist/auth/bearer.d.ts.map +1 -1
- package/dist/auth/bearer.js +25 -5
- package/dist/auth/bearer.js.map +1 -1
- package/dist/clients.d.ts.map +1 -1
- package/dist/clients.js +3 -6
- package/dist/clients.js.map +1 -1
- package/dist/deprecated.d.ts +42 -0
- package/dist/deprecated.d.ts.map +1 -0
- package/dist/deprecated.js +35 -0
- package/dist/deprecated.js.map +1 -0
- package/dist/http/body.d.ts +69 -0
- package/dist/http/body.d.ts.map +1 -0
- package/dist/http/body.js +165 -0
- package/dist/http/body.js.map +1 -0
- package/dist/http/http.d.ts +42 -0
- package/dist/http/http.d.ts.map +1 -0
- package/dist/http/http.js +100 -0
- package/dist/http/http.js.map +1 -0
- package/dist/http/origins.d.ts +37 -0
- package/dist/http/origins.d.ts.map +1 -0
- package/dist/http/origins.js +87 -0
- package/dist/http/origins.js.map +1 -0
- package/dist/mcp/dispatch.d.ts +7 -0
- package/dist/mcp/dispatch.d.ts.map +1 -0
- package/dist/mcp/dispatch.js +99 -0
- package/dist/mcp/dispatch.js.map +1 -0
- package/dist/{jsonrpc.d.ts → mcp/jsonrpc.d.ts} +10 -0
- package/dist/mcp/jsonrpc.d.ts.map +1 -0
- package/dist/mcp/jsonrpc.js +88 -0
- package/dist/mcp/jsonrpc.js.map +1 -0
- package/dist/mcp/methods.d.ts +133 -0
- package/dist/mcp/methods.d.ts.map +1 -0
- package/dist/mcp/methods.js +84 -0
- package/dist/mcp/methods.js.map +1 -0
- package/dist/mcp/protocol.d.ts +16 -0
- package/dist/mcp/protocol.d.ts.map +1 -0
- package/dist/mcp/protocol.js +22 -0
- package/dist/mcp/protocol.js.map +1 -0
- package/dist/mcp/registry.d.ts +58 -0
- package/dist/mcp/registry.d.ts.map +1 -0
- package/dist/mcp/registry.js +81 -0
- package/dist/mcp/registry.js.map +1 -0
- package/dist/mcp/rpc-handlers.d.ts +11 -0
- package/dist/mcp/rpc-handlers.d.ts.map +1 -0
- package/dist/mcp/rpc-handlers.js +133 -0
- package/dist/mcp/rpc-handlers.js.map +1 -0
- package/dist/{tools.d.ts → mcp/tools.d.ts} +30 -2
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +166 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/{validate.d.ts → mcp/validate.d.ts} +24 -12
- package/dist/mcp/validate.d.ts.map +1 -0
- package/dist/mcp/validate.js +186 -0
- package/dist/mcp/validate.js.map +1 -0
- package/dist/oauth/audit.d.ts +20 -0
- package/dist/oauth/audit.d.ts.map +1 -0
- package/dist/oauth/audit.js +29 -0
- package/dist/oauth/audit.js.map +1 -0
- package/dist/oauth/cimd/cimd-parse.d.ts +4 -0
- package/dist/oauth/cimd/cimd-parse.d.ts.map +1 -0
- package/dist/oauth/cimd/cimd-parse.js +70 -0
- package/dist/oauth/cimd/cimd-parse.js.map +1 -0
- package/dist/oauth/cimd/cimd-ssrf.d.ts +8 -0
- package/dist/oauth/cimd/cimd-ssrf.d.ts.map +1 -0
- package/dist/oauth/cimd/cimd-ssrf.js +193 -0
- package/dist/oauth/cimd/cimd-ssrf.js.map +1 -0
- package/dist/oauth/cimd/cimd.d.ts +36 -0
- package/dist/oauth/cimd/cimd.d.ts.map +1 -0
- package/dist/oauth/cimd/cimd.js +132 -0
- package/dist/oauth/cimd/cimd.js.map +1 -0
- package/dist/oauth/cimd/types.d.ts +12 -0
- package/dist/oauth/cimd/types.d.ts.map +1 -0
- package/dist/oauth/cimd/types.js +3 -0
- package/dist/oauth/cimd/types.js.map +1 -0
- package/dist/oauth/config.d.ts +51 -0
- package/dist/oauth/config.d.ts.map +1 -0
- package/dist/oauth/config.js +108 -0
- package/dist/oauth/config.js.map +1 -0
- package/dist/oauth/constants.d.ts +15 -0
- package/dist/oauth/constants.d.ts.map +1 -1
- package/dist/oauth/constants.js +15 -0
- package/dist/oauth/constants.js.map +1 -1
- package/dist/oauth/{base64url.d.ts → crypto/base64url.d.ts} +1 -2
- package/dist/oauth/crypto/base64url.d.ts.map +1 -0
- package/dist/oauth/{base64url.js → crypto/base64url.js} +1 -5
- package/dist/oauth/crypto/base64url.js.map +1 -0
- package/dist/oauth/crypto/clientauth.d.ts +13 -0
- package/dist/oauth/crypto/clientauth.d.ts.map +1 -0
- package/dist/oauth/crypto/clientauth.js +195 -0
- package/dist/oauth/crypto/clientauth.js.map +1 -0
- package/dist/oauth/crypto/codes.d.ts +65 -0
- package/dist/oauth/crypto/codes.d.ts.map +1 -0
- package/dist/oauth/crypto/codes.js +121 -0
- package/dist/oauth/crypto/codes.js.map +1 -0
- package/dist/oauth/crypto/hkdf.d.ts +13 -0
- package/dist/oauth/crypto/hkdf.d.ts.map +1 -0
- package/dist/oauth/crypto/hkdf.js +30 -0
- package/dist/oauth/crypto/hkdf.js.map +1 -0
- package/dist/oauth/crypto/keycache.d.ts +19 -0
- package/dist/oauth/crypto/keycache.d.ts.map +1 -0
- package/dist/oauth/crypto/keycache.js +44 -0
- package/dist/oauth/crypto/keycache.js.map +1 -0
- package/dist/oauth/crypto/pkce.d.ts.map +1 -0
- package/dist/oauth/{pkce.js → crypto/pkce.js} +1 -1
- package/dist/oauth/crypto/pkce.js.map +1 -0
- package/dist/oauth/crypto/sealed.d.ts +26 -0
- package/dist/oauth/crypto/sealed.d.ts.map +1 -0
- package/dist/oauth/crypto/sealed.js +65 -0
- package/dist/oauth/crypto/sealed.js.map +1 -0
- package/dist/oauth/crypto/secrethash.d.ts +58 -0
- package/dist/oauth/crypto/secrethash.d.ts.map +1 -0
- package/dist/oauth/crypto/secrethash.js +82 -0
- package/dist/oauth/crypto/secrethash.js.map +1 -0
- package/dist/oauth/deprecated.d.ts +52 -0
- package/dist/oauth/deprecated.d.ts.map +1 -0
- package/dist/oauth/deprecated.js +49 -0
- package/dist/oauth/deprecated.js.map +1 -0
- package/dist/oauth/endpoints/authorize.d.ts +3 -0
- package/dist/oauth/endpoints/authorize.d.ts.map +1 -0
- package/dist/oauth/endpoints/authorize.js +209 -0
- package/dist/oauth/endpoints/authorize.js.map +1 -0
- package/dist/oauth/endpoints/callback.d.ts +20 -0
- package/dist/oauth/endpoints/callback.d.ts.map +1 -0
- package/dist/oauth/endpoints/callback.js +31 -0
- package/dist/oauth/endpoints/callback.js.map +1 -0
- package/dist/oauth/endpoints/consent-page.d.ts +5 -0
- package/dist/oauth/endpoints/consent-page.d.ts.map +1 -0
- package/dist/oauth/endpoints/consent-page.js +63 -0
- package/dist/oauth/endpoints/consent-page.js.map +1 -0
- package/dist/oauth/endpoints/consent.d.ts +29 -0
- package/dist/oauth/endpoints/consent.d.ts.map +1 -0
- package/dist/oauth/endpoints/consent.js +98 -0
- package/dist/oauth/endpoints/consent.js.map +1 -0
- package/dist/oauth/{metadata.d.ts → endpoints/metadata.d.ts} +14 -8
- package/dist/oauth/endpoints/metadata.d.ts.map +1 -0
- package/dist/oauth/{metadata.js → endpoints/metadata.js} +10 -4
- package/dist/oauth/endpoints/metadata.js.map +1 -0
- package/dist/oauth/endpoints/register.d.ts +10 -0
- package/dist/oauth/endpoints/register.d.ts.map +1 -0
- package/dist/oauth/endpoints/register.js +96 -0
- package/dist/oauth/endpoints/register.js.map +1 -0
- package/dist/oauth/{revoke.d.ts → endpoints/revoke.d.ts} +1 -1
- package/dist/oauth/endpoints/revoke.d.ts.map +1 -0
- package/dist/oauth/{revoke.js → endpoints/revoke.js} +11 -9
- package/dist/oauth/endpoints/revoke.js.map +1 -0
- package/dist/oauth/endpoints/token.d.ts +3 -0
- package/dist/oauth/endpoints/token.d.ts.map +1 -0
- package/dist/oauth/{token.js → endpoints/token.js} +33 -47
- package/dist/oauth/endpoints/token.js.map +1 -0
- package/dist/oauth/{wellknown.d.ts → endpoints/wellknown.d.ts} +1 -1
- package/dist/oauth/endpoints/wellknown.d.ts.map +1 -0
- package/dist/oauth/{wellknown.js → endpoints/wellknown.js} +7 -7
- package/dist/oauth/endpoints/wellknown.js.map +1 -0
- package/dist/oauth/{redirect.d.ts → policy/redirect.d.ts} +6 -1
- package/dist/oauth/policy/redirect.d.ts.map +1 -0
- package/dist/oauth/policy/redirect.js +39 -0
- package/dist/oauth/policy/redirect.js.map +1 -0
- package/dist/oauth/policy/reqbody.d.ts +18 -0
- package/dist/oauth/policy/reqbody.d.ts.map +1 -0
- package/dist/oauth/policy/reqbody.js +68 -0
- package/dist/oauth/policy/reqbody.js.map +1 -0
- package/dist/oauth/policy/resource.d.ts +37 -0
- package/dist/oauth/policy/resource.d.ts.map +1 -0
- package/dist/oauth/policy/resource.js +74 -0
- package/dist/oauth/policy/resource.js.map +1 -0
- package/dist/oauth/policy/scope.d.ts +20 -0
- package/dist/oauth/policy/scope.d.ts.map +1 -0
- package/dist/oauth/policy/scope.js +35 -0
- package/dist/oauth/policy/scope.js.map +1 -0
- package/dist/oauth/router.d.ts +12 -12
- package/dist/oauth/router.d.ts.map +1 -1
- package/dist/oauth/router.js +105 -87
- package/dist/oauth/router.js.map +1 -1
- package/dist/oauth/types.d.ts +177 -26
- package/dist/oauth/types.d.ts.map +1 -1
- package/dist/oauth/types.js +1 -27
- package/dist/oauth/types.js.map +1 -1
- package/dist/server.d.ts +14 -13
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +10 -9
- package/dist/server.js.map +1 -1
- package/dist/util/defined.d.ts +11 -0
- package/dist/util/defined.d.ts.map +1 -0
- package/dist/util/defined.js +15 -0
- package/dist/util/defined.js.map +1 -0
- package/dist/util/json.d.ts +3 -0
- package/dist/util/json.d.ts.map +1 -0
- package/dist/util/json.js +3 -0
- package/dist/util/json.js.map +1 -0
- package/docs/ROADMAP.md +283 -0
- package/docs/compatibility.md +84 -0
- package/docs/diagrams/mcp-trellis-architecture.html +305 -0
- package/docs/guide.md +441 -0
- package/docs/reference.md +210 -0
- package/docs/security.md +112 -0
- package/docs/troubleshooting.md +19 -0
- package/package.json +24 -8
- package/dist/adapters/origins.d.ts +0 -19
- package/dist/adapters/origins.d.ts.map +0 -1
- package/dist/adapters/origins.js +0 -55
- package/dist/adapters/origins.js.map +0 -1
- package/dist/dispatch.d.ts +0 -7
- package/dist/dispatch.d.ts.map +0 -1
- package/dist/dispatch.js +0 -79
- package/dist/dispatch.js.map +0 -1
- package/dist/http.d.ts +0 -17
- package/dist/http.d.ts.map +0 -1
- package/dist/http.js +0 -38
- package/dist/http.js.map +0 -1
- package/dist/jsonrpc.d.ts.map +0 -1
- package/dist/jsonrpc.js +0 -17
- package/dist/jsonrpc.js.map +0 -1
- package/dist/methods.d.ts +0 -61
- package/dist/methods.d.ts.map +0 -1
- package/dist/methods.js +0 -114
- package/dist/methods.js.map +0 -1
- package/dist/oauth/authorize.d.ts +0 -3
- package/dist/oauth/authorize.d.ts.map +0 -1
- package/dist/oauth/authorize.js +0 -109
- package/dist/oauth/authorize.js.map +0 -1
- package/dist/oauth/base64url.d.ts.map +0 -1
- package/dist/oauth/base64url.js.map +0 -1
- package/dist/oauth/clientauth.d.ts +0 -19
- package/dist/oauth/clientauth.d.ts.map +0 -1
- package/dist/oauth/clientauth.js +0 -76
- package/dist/oauth/clientauth.js.map +0 -1
- package/dist/oauth/codes.d.ts +0 -30
- package/dist/oauth/codes.d.ts.map +0 -1
- package/dist/oauth/codes.js +0 -93
- package/dist/oauth/codes.js.map +0 -1
- package/dist/oauth/metadata.d.ts.map +0 -1
- package/dist/oauth/metadata.js.map +0 -1
- package/dist/oauth/pkce.d.ts.map +0 -1
- package/dist/oauth/pkce.js.map +0 -1
- package/dist/oauth/redirect.d.ts.map +0 -1
- package/dist/oauth/redirect.js +0 -26
- package/dist/oauth/redirect.js.map +0 -1
- package/dist/oauth/resource.d.ts +0 -7
- package/dist/oauth/resource.d.ts.map +0 -1
- package/dist/oauth/resource.js +0 -44
- package/dist/oauth/resource.js.map +0 -1
- package/dist/oauth/revoke.d.ts.map +0 -1
- package/dist/oauth/revoke.js.map +0 -1
- package/dist/oauth/scope.d.ts +0 -11
- package/dist/oauth/scope.d.ts.map +0 -1
- package/dist/oauth/scope.js +0 -24
- package/dist/oauth/scope.js.map +0 -1
- package/dist/oauth/token.d.ts +0 -3
- package/dist/oauth/token.d.ts.map +0 -1
- package/dist/oauth/token.js.map +0 -1
- package/dist/oauth/wellknown.d.ts.map +0 -1
- package/dist/oauth/wellknown.js.map +0 -1
- package/dist/protocol.d.ts +0 -4
- package/dist/protocol.d.ts.map +0 -1
- package/dist/protocol.js +0 -14
- package/dist/protocol.js.map +0 -1
- package/dist/registry.d.ts +0 -37
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js +0 -69
- package/dist/registry.js.map +0 -1
- package/dist/tools.d.ts.map +0 -1
- package/dist/tools.js +0 -97
- package/dist/tools.js.map +0 -1
- package/dist/validate.d.ts.map +0 -1
- package/dist/validate.js +0 -130
- package/dist/validate.js.map +0 -1
- /package/dist/oauth/{pkce.d.ts → crypto/pkce.d.ts} +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/** Token-endpoint client authentication (RFC 6749 §2.3.1). */
|
|
2
|
+
import { auditLegacyKeyUsage, safeOAuthAudit } from "../audit.js";
|
|
3
|
+
import { cimdFetchOpts, isCimdClientId, resolveCimdClient } from "../cimd/cimd.js";
|
|
4
|
+
import { oauthError, registeredClientsRequired, resolveSecrets, unregisteredClientsAllowed, } from "../config.js";
|
|
5
|
+
import { OAUTH_ERRORS, TOKEN_ENDPOINT_AUTH_METHODS, } from "../constants.js";
|
|
6
|
+
import { unsealAny } from "./sealed.js";
|
|
7
|
+
import { verifyClientSecretAny } from "./secrethash.js";
|
|
8
|
+
/** RFC 7235 §2.1 — the auth-scheme token is case-insensitive. */
|
|
9
|
+
const BASIC_SCHEME_RE = /^basic\s+/i;
|
|
10
|
+
/** RFC 6749 §2.3.1 — id and secret are form-urlencoded, so `+` means a space. */
|
|
11
|
+
const decodeFormComponent = (value) => decodeURIComponent(value.replace(/\+/g, " "));
|
|
12
|
+
/** `Authorization: Basic base64(urlencode(id):urlencode(secret))`. */
|
|
13
|
+
const parseBasicAuth = (header) => {
|
|
14
|
+
const scheme = header ? BASIC_SCHEME_RE.exec(header) : null;
|
|
15
|
+
if (!header || !scheme)
|
|
16
|
+
return null;
|
|
17
|
+
try {
|
|
18
|
+
const decoded = atob(header.slice(scheme[0].length).trim());
|
|
19
|
+
const separator = decoded.indexOf(":");
|
|
20
|
+
if (separator < 0)
|
|
21
|
+
return null;
|
|
22
|
+
const clientId = decodeFormComponent(decoded.slice(0, separator));
|
|
23
|
+
if (!clientId)
|
|
24
|
+
return null;
|
|
25
|
+
return {
|
|
26
|
+
clientId,
|
|
27
|
+
secret: decodeFormComponent(decoded.slice(separator + 1)),
|
|
28
|
+
method: TOKEN_ENDPOINT_AUTH_METHODS.basic,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
/** Credentials from the Authorization header, else from the body. */
|
|
36
|
+
export const readClientAuth = (request, body) => {
|
|
37
|
+
const basic = parseBasicAuth(request.headers.get("Authorization"));
|
|
38
|
+
if (basic)
|
|
39
|
+
return basic;
|
|
40
|
+
const clientId = body.client_id ?? "";
|
|
41
|
+
if (!clientId)
|
|
42
|
+
return null;
|
|
43
|
+
const secret = body.client_secret ?? "";
|
|
44
|
+
return {
|
|
45
|
+
clientId,
|
|
46
|
+
secret: secret.length > 0 ? secret : null,
|
|
47
|
+
method: secret.length > 0 ? TOKEN_ENDPOINT_AUTH_METHODS.post : TOKEN_ENDPOINT_AUTH_METHODS.none,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Every failure below returns the same code, status, and description.
|
|
52
|
+
* Distinguishable messages here ("client requires client_secret_basic" vs.
|
|
53
|
+
* "unknown client_id" vs. "client_secret required" vs. …) let an
|
|
54
|
+
* unauthenticated caller enumerate which client_ids this server actually
|
|
55
|
+
* knows about and how each is configured, just by reading error text —
|
|
56
|
+
* a real information leak on the one endpoint whose entire job is to
|
|
57
|
+
* reject an untrusted caller. `/authorize`'s `unauthorized_client` stays
|
|
58
|
+
* descriptive on purpose: it's a human debugging an OAuth flow in a
|
|
59
|
+
* browser, not a bare API response, and a locked-down server already
|
|
60
|
+
* announces the same fact by omitting `registration_endpoint` from its
|
|
61
|
+
* metadata.
|
|
62
|
+
*/
|
|
63
|
+
const CLIENT_AUTH_FAILED = "client authentication failed";
|
|
64
|
+
/** The generic response every failure below returns to the caller. */
|
|
65
|
+
const invalidClient = () => oauthError(OAUTH_ERRORS.invalidClient, 401, CLIENT_AUTH_FAILED);
|
|
66
|
+
/** The response above, plus the real reason to `ports.audit` — never to the caller. */
|
|
67
|
+
const invalidClientAudited = async ({ auth, options }, reason) => {
|
|
68
|
+
await safeOAuthAudit(options, { event: "client_auth_failed", clientId: auth.clientId, reason });
|
|
69
|
+
return invalidClient();
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Never a real credential — exists only so the locked-down branch below
|
|
73
|
+
* does the same amount of comparison work whether or not a secret was
|
|
74
|
+
* actually presented, and whether or not a stored hash actually exists.
|
|
75
|
+
*/
|
|
76
|
+
const DUMMY_SECRET = "dummy-secret-for-constant-time-padding";
|
|
77
|
+
const DUMMY_STORED_HASH = "hmac-sha256$AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
|
78
|
+
/**
|
|
79
|
+
* Rejects with `reason`, but only after paying the same
|
|
80
|
+
* `verifyClientSecretAny` cost a real (wrong-secret) rejection would — a
|
|
81
|
+
* missing or empty secret is replaced by `DUMMY_SECRET`, so a caller who
|
|
82
|
+
* presented nothing can't be told apart, by timing, from a wrong secret.
|
|
83
|
+
*/
|
|
84
|
+
const rejectWithDummyCompare = async (context, reason) => {
|
|
85
|
+
const codeSecretValues = await resolveSecrets(context.options.ports, context.request);
|
|
86
|
+
const presented = context.auth.secret || DUMMY_SECRET;
|
|
87
|
+
await verifyClientSecretAny(presented, DUMMY_STORED_HASH, codeSecretValues);
|
|
88
|
+
return invalidClientAudited(context, reason);
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* True when `clientId` is this server's own sealed `/register` assertion or,
|
|
92
|
+
* with CIMD enabled, a resolvable CIMD document. URL-shaped ids are always
|
|
93
|
+
* re-resolved, never accepted by shape alone.
|
|
94
|
+
*/
|
|
95
|
+
const isSelfRegisteredOrCimdClient = async ({ auth, options, request, }) => {
|
|
96
|
+
const codeSecretValues = await resolveSecrets(options.ports, request);
|
|
97
|
+
const unsealed = await unsealAny(codeSecretValues, "client", auth.clientId);
|
|
98
|
+
if (unsealed) {
|
|
99
|
+
await auditLegacyKeyUsage(options, {
|
|
100
|
+
clientId: auth.clientId,
|
|
101
|
+
keyIndex: unsealed.keyIndex,
|
|
102
|
+
source: "client assertion unsealed",
|
|
103
|
+
});
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
if (options.cimd !== true || !isCimdClientId(auth.clientId))
|
|
107
|
+
return false;
|
|
108
|
+
if (await resolveCimdClient(auth.clientId, cimdFetchOpts(options)))
|
|
109
|
+
return true;
|
|
110
|
+
// Without `cimdCache` every refresh/revoke re-fetches — a flaky document
|
|
111
|
+
// host logs users out. Surface that distinctly.
|
|
112
|
+
await safeOAuthAudit(options, {
|
|
113
|
+
event: "cimd_resolve_failed",
|
|
114
|
+
clientId: auth.clientId,
|
|
115
|
+
reason: "CIMD document could not be resolved at token/revoke time",
|
|
116
|
+
});
|
|
117
|
+
return false;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* An unknown client_id is treated as public — PKCE is its only proof —
|
|
121
|
+
* unless `allowUnregisteredClients` is false, in which case it is rejected.
|
|
122
|
+
* On that locked-down path a dummy secret comparison runs even for a
|
|
123
|
+
* client_id this server has never heard of, so timing can't distinguish
|
|
124
|
+
* "unknown" from "known, wrong secret". Public clients never pay that cost.
|
|
125
|
+
*
|
|
126
|
+
* Under `requireRegisteredClients`, a client_id resolved by neither
|
|
127
|
+
* `clientStore`, this server's own sealed `/register` assertion, nor CIMD is
|
|
128
|
+
* rejected — the same rule `/authorize` enforces. It lives in this shared
|
|
129
|
+
* gate so the `refresh_token` grant and `/revoke` cannot be reached with an
|
|
130
|
+
* invented id that never went through `/authorize`.
|
|
131
|
+
*/
|
|
132
|
+
const unregisteredClientAuthError = async (context) => {
|
|
133
|
+
if (!unregisteredClientsAllowed(context.options)) {
|
|
134
|
+
return rejectWithDummyCompare(context, "unknown client_id");
|
|
135
|
+
}
|
|
136
|
+
if (!registeredClientsRequired(context.options))
|
|
137
|
+
return null;
|
|
138
|
+
if (await isSelfRegisteredOrCimdClient(context))
|
|
139
|
+
return null;
|
|
140
|
+
return invalidClientAudited(context, "client_id must come from clientStore or this server's own /register");
|
|
141
|
+
};
|
|
142
|
+
/** Compare against `ClientStore.secretHash` — the same cost whether a hash is on file or not. */
|
|
143
|
+
const secretHashAuthError = async (context, secret, secretHash) => {
|
|
144
|
+
const { auth, options, request } = context;
|
|
145
|
+
const codeSecretValues = await resolveSecrets(options.ports, request);
|
|
146
|
+
const stored = await secretHash(auth.clientId);
|
|
147
|
+
const { ok, keyIndex } = await verifyClientSecretAny(secret, stored ?? DUMMY_STORED_HASH, codeSecretValues);
|
|
148
|
+
if (ok && stored !== null) {
|
|
149
|
+
await auditLegacyKeyUsage(options, {
|
|
150
|
+
clientId: auth.clientId,
|
|
151
|
+
keyIndex,
|
|
152
|
+
source: "client_secret_hash verified",
|
|
153
|
+
});
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return invalidClientAudited(context, stored === null ? "no secretHash on file for this client" : "secretHash mismatch");
|
|
157
|
+
};
|
|
158
|
+
/** Delegate the comparison to the host's `ClientStore.verifySecret`. */
|
|
159
|
+
const verifySecretAuthError = async (context, secret, verifySecret) => {
|
|
160
|
+
if (!verifySecret) {
|
|
161
|
+
return invalidClientAudited(context, "clientStore.verifySecret is not configured");
|
|
162
|
+
}
|
|
163
|
+
if (!(await verifySecret(context.auth.clientId, secret))) {
|
|
164
|
+
return invalidClientAudited(context, "verifySecret returned false");
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
};
|
|
168
|
+
/** Enforce a known client's own `tokenEndpointAuthMethod`. */
|
|
169
|
+
const registeredClientAuthError = async (context, registered) => {
|
|
170
|
+
const { auth, options } = context;
|
|
171
|
+
const expected = registered.tokenEndpointAuthMethod;
|
|
172
|
+
if (expected === TOKEN_ENDPOINT_AUTH_METHODS.none)
|
|
173
|
+
return null;
|
|
174
|
+
// A wrong method or a missing secret pays the same comparison cost as a
|
|
175
|
+
// wrong secret, so neither branch leaks "this client exists and needs a
|
|
176
|
+
// secret" through response timing.
|
|
177
|
+
if (auth.method !== expected) {
|
|
178
|
+
return rejectWithDummyCompare(context, `client requires ${expected}, got ${auth.method}`);
|
|
179
|
+
}
|
|
180
|
+
if (!auth.secret)
|
|
181
|
+
return rejectWithDummyCompare(context, "client_secret required");
|
|
182
|
+
const store = options.ports.clientStore;
|
|
183
|
+
return store?.secretHash
|
|
184
|
+
? secretHashAuthError(context, auth.secret, store.secretHash)
|
|
185
|
+
: verifySecretAuthError(context, auth.secret, store?.verifySecret);
|
|
186
|
+
};
|
|
187
|
+
/** Enforce the registered auth method for known clients — see the helpers above for each branch. */
|
|
188
|
+
export const firstClientAuthError = async (auth, options, request) => {
|
|
189
|
+
const context = { auth, options, request };
|
|
190
|
+
const registered = (await options.ports.clientStore?.get(auth.clientId)) ?? null;
|
|
191
|
+
return registered
|
|
192
|
+
? registeredClientAuthError(context, registered)
|
|
193
|
+
: unregisteredClientAuthError(context);
|
|
194
|
+
};
|
|
195
|
+
//# sourceMappingURL=clientauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clientauth.js","sourceRoot":"","sources":["../../../src/oauth/crypto/clientauth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export type AuthCodeRecord = {
|
|
2
|
+
clientId: string;
|
|
3
|
+
redirectUri: string;
|
|
4
|
+
codeChallenge: string;
|
|
5
|
+
/** Consenting user id — required so minted tokens are bound. */
|
|
6
|
+
userId: string;
|
|
7
|
+
/** RFC 8707 resource indicator — audience for the eventual access token. */
|
|
8
|
+
resource: string;
|
|
9
|
+
/** Space-delimited scope granted at authorize time. */
|
|
10
|
+
scope: string;
|
|
11
|
+
exp: number;
|
|
12
|
+
};
|
|
13
|
+
export type CodeStore = {
|
|
14
|
+
/**
|
|
15
|
+
* Mark a code jti as consumed; return false if already used.
|
|
16
|
+
* `expMs` is the code expiry (ms since epoch) for TTL-backed stores.
|
|
17
|
+
*/
|
|
18
|
+
consume: (jti: string, expMs: number) => Promise<boolean> | boolean;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Process-local jti → expMs map when no shared codeStore is provided.
|
|
22
|
+
* ponytail: single-process only; multi-instance → pass ports.codeStore (KV/Redis SET NX EX).
|
|
23
|
+
*/
|
|
24
|
+
export declare const createMemoryCodeStore: () => CodeStore;
|
|
25
|
+
/**
|
|
26
|
+
* The one process-wide in-memory store. Shared rather than per router: an
|
|
27
|
+
* app built per request (`buildApp(env).fetch(req)`) would otherwise get a
|
|
28
|
+
* fresh map each time and lose single-use entirely. `createOAuthRouter`
|
|
29
|
+
* resolves it into `ports.codeStore` only when `allowInMemoryCodeStore` is
|
|
30
|
+
* set; endpoint handlers never fall back on their own.
|
|
31
|
+
*/
|
|
32
|
+
export declare const processCodeStore: CodeStore;
|
|
33
|
+
/**
|
|
34
|
+
* Issue an AES-GCM sealed auth code (v2). Single-use via codeStore, or
|
|
35
|
+
* pruning in-memory Map when omitted.
|
|
36
|
+
*/
|
|
37
|
+
export declare const issueAuthCode: (secret: string, record: Omit<AuthCodeRecord, "exp">, nowMs?: number) => Promise<string>;
|
|
38
|
+
/**
|
|
39
|
+
* Rotation-aware `consumeAuthCode`, also reporting which `secret` entry
|
|
40
|
+
* actually unsealed the code — used only by `token.ts` to audit
|
|
41
|
+
* `"legacy_code_secret_used"` without a second decrypt pass. Not part of
|
|
42
|
+
* the public API; `consumeAuthCode` below is the stable, exported surface.
|
|
43
|
+
*/
|
|
44
|
+
export declare const consumeAuthCodeDetailed: (secret: string | readonly string[], code: string, options?: {
|
|
45
|
+
codeStore?: CodeStore | undefined;
|
|
46
|
+
nowMs?: number | undefined;
|
|
47
|
+
}) => Promise<{
|
|
48
|
+
record: AuthCodeRecord;
|
|
49
|
+
keyIndex: number;
|
|
50
|
+
} | null>;
|
|
51
|
+
/**
|
|
52
|
+
* `secret` may be a single key or a list — every entry is tried, so a code
|
|
53
|
+
* sealed under an older `codeSecret` keeps redeeming during rotation. See
|
|
54
|
+
* `resolveSecrets`/`OAuthPorts.codeSecret` for the rotation contract.
|
|
55
|
+
*
|
|
56
|
+
* Omitting `options.codeStore` is deprecated: it falls back to a
|
|
57
|
+
* process-local map that cannot enforce single use across instances, and
|
|
58
|
+
* `codeStore` becomes required in 3.0.
|
|
59
|
+
*/
|
|
60
|
+
export declare const consumeAuthCode: (secret: string | readonly string[], code: string, options?: {
|
|
61
|
+
codeStore?: CodeStore | undefined;
|
|
62
|
+
nowMs?: number | undefined;
|
|
63
|
+
}) => Promise<AuthCodeRecord | null>;
|
|
64
|
+
export declare const newClientId: () => string;
|
|
65
|
+
//# sourceMappingURL=codes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codes.d.ts","sourceRoot":"","sources":["../../../src/oauth/crypto/codes.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB;;;OAGG;IACH,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;CACrE,CAAC;AAQF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAO,SAiBxC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAmC,CAAC;AAEnE;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,QAAQ,MAAM,EACd,QAAQ,IAAI,CAAC,cAAc,EAAE,KAAK,CAAC,EACnC,cAAkB,KACjB,OAAO,CAAC,MAAM,CAOhB,CAAC;AAmCF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAClC,QAAQ,MAAM,GAAG,SAAS,MAAM,EAAE,EAClC,MAAM,MAAM,EACZ,UAAS;IAAE,SAAS,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAO,KAC9E,OAAO,CAAC;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAqB7D,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,GAC1B,QAAQ,MAAM,GAAG,SAAS,MAAM,EAAE,EAClC,MAAM,MAAM,EACZ,UAAS;IAAE,SAAS,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAO,KAC9E,OAAO,CAAC,cAAc,GAAG,IAAI,CAG/B,CAAC;AAEF,eAAO,MAAM,WAAW,QAAO,MAA6B,CAAC"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { randomBase64Url } from "./pkce.js";
|
|
2
|
+
import { seal, unsealAny } from "./sealed.js";
|
|
3
|
+
const CODE_TTL_MS = 600_000;
|
|
4
|
+
/** AES-GCM sealed auth codes — authenticated and encrypted (see `sealed.ts`). */
|
|
5
|
+
const SEALED_PREFIX = "v2.";
|
|
6
|
+
/**
|
|
7
|
+
* Prune the in-memory store at most this often: a full `Map` scan on every
|
|
8
|
+
* `consume` would run for every code and consent-ticket redemption.
|
|
9
|
+
*/
|
|
10
|
+
const PRUNE_INTERVAL_MS = 60_000;
|
|
11
|
+
/**
|
|
12
|
+
* Process-local jti → expMs map when no shared codeStore is provided.
|
|
13
|
+
* ponytail: single-process only; multi-instance → pass ports.codeStore (KV/Redis SET NX EX).
|
|
14
|
+
*/
|
|
15
|
+
export const createMemoryCodeStore = () => {
|
|
16
|
+
const used = new Map();
|
|
17
|
+
let lastPrunedAtMs = 0;
|
|
18
|
+
return {
|
|
19
|
+
consume: (jti, expMs) => {
|
|
20
|
+
const nowMs = Date.now();
|
|
21
|
+
if (nowMs - lastPrunedAtMs >= PRUNE_INTERVAL_MS) {
|
|
22
|
+
lastPrunedAtMs = nowMs;
|
|
23
|
+
for (const [key, exp] of used) {
|
|
24
|
+
if (exp < nowMs)
|
|
25
|
+
used.delete(key);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (used.has(jti))
|
|
29
|
+
return false;
|
|
30
|
+
used.set(jti, expMs);
|
|
31
|
+
return true;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The one process-wide in-memory store. Shared rather than per router: an
|
|
37
|
+
* app built per request (`buildApp(env).fetch(req)`) would otherwise get a
|
|
38
|
+
* fresh map each time and lose single-use entirely. `createOAuthRouter`
|
|
39
|
+
* resolves it into `ports.codeStore` only when `allowInMemoryCodeStore` is
|
|
40
|
+
* set; endpoint handlers never fall back on their own.
|
|
41
|
+
*/
|
|
42
|
+
export const processCodeStore = createMemoryCodeStore();
|
|
43
|
+
/**
|
|
44
|
+
* Issue an AES-GCM sealed auth code (v2). Single-use via codeStore, or
|
|
45
|
+
* pruning in-memory Map when omitted.
|
|
46
|
+
*/
|
|
47
|
+
export const issueAuthCode = async (secret, record, nowMs = Date.now()) => {
|
|
48
|
+
const body = {
|
|
49
|
+
...record,
|
|
50
|
+
exp: nowMs + CODE_TTL_MS,
|
|
51
|
+
jti: randomBase64Url(12),
|
|
52
|
+
};
|
|
53
|
+
return `${SEALED_PREFIX}${await seal(secret, "code", body)}`;
|
|
54
|
+
};
|
|
55
|
+
const shapeCheckedRecord = (data, nowMs) => {
|
|
56
|
+
if (typeof data.clientId !== "string" ||
|
|
57
|
+
typeof data.redirectUri !== "string" ||
|
|
58
|
+
typeof data.codeChallenge !== "string" ||
|
|
59
|
+
typeof data.userId !== "string" ||
|
|
60
|
+
typeof data.resource !== "string" ||
|
|
61
|
+
data.resource.length === 0 ||
|
|
62
|
+
typeof data.scope !== "string" ||
|
|
63
|
+
typeof data.exp !== "number" ||
|
|
64
|
+
data.exp < nowMs ||
|
|
65
|
+
typeof data.jti !== "string" ||
|
|
66
|
+
data.jti.length === 0) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
clientId: data.clientId,
|
|
71
|
+
redirectUri: data.redirectUri,
|
|
72
|
+
codeChallenge: data.codeChallenge,
|
|
73
|
+
userId: data.userId,
|
|
74
|
+
resource: data.resource,
|
|
75
|
+
scope: data.scope,
|
|
76
|
+
exp: data.exp,
|
|
77
|
+
jti: data.jti,
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Rotation-aware `consumeAuthCode`, also reporting which `secret` entry
|
|
82
|
+
* actually unsealed the code — used only by `token.ts` to audit
|
|
83
|
+
* `"legacy_code_secret_used"` without a second decrypt pass. Not part of
|
|
84
|
+
* the public API; `consumeAuthCode` below is the stable, exported surface.
|
|
85
|
+
*/
|
|
86
|
+
export const consumeAuthCodeDetailed = async (secret, code, options = {}) => {
|
|
87
|
+
const secrets = (Array.isArray(secret) ? secret : [secret]).filter((candidate) => Boolean(candidate));
|
|
88
|
+
if (!code || secrets.length === 0)
|
|
89
|
+
return null;
|
|
90
|
+
// Only AES-GCM sealed (`v2.`) codes redeem.
|
|
91
|
+
if (!code.startsWith(SEALED_PREFIX))
|
|
92
|
+
return null;
|
|
93
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
94
|
+
const unsealed = await unsealAny(secrets, "code", code.slice(SEALED_PREFIX.length));
|
|
95
|
+
if (!unsealed)
|
|
96
|
+
return null;
|
|
97
|
+
const record = shapeCheckedRecord(unsealed.value, nowMs);
|
|
98
|
+
if (!record)
|
|
99
|
+
return null;
|
|
100
|
+
const store = options.codeStore ?? processCodeStore;
|
|
101
|
+
const fresh = await store.consume(record.jti, record.exp);
|
|
102
|
+
if (!fresh)
|
|
103
|
+
return null;
|
|
104
|
+
const { jti: _jti, ...rest } = record;
|
|
105
|
+
return { record: rest, keyIndex: unsealed.keyIndex };
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* `secret` may be a single key or a list — every entry is tried, so a code
|
|
109
|
+
* sealed under an older `codeSecret` keeps redeeming during rotation. See
|
|
110
|
+
* `resolveSecrets`/`OAuthPorts.codeSecret` for the rotation contract.
|
|
111
|
+
*
|
|
112
|
+
* Omitting `options.codeStore` is deprecated: it falls back to a
|
|
113
|
+
* process-local map that cannot enforce single use across instances, and
|
|
114
|
+
* `codeStore` becomes required in 3.0.
|
|
115
|
+
*/
|
|
116
|
+
export const consumeAuthCode = async (secret, code, options = {}) => {
|
|
117
|
+
const result = await consumeAuthCodeDetailed(secret, code, options);
|
|
118
|
+
return result?.record ?? null;
|
|
119
|
+
};
|
|
120
|
+
export const newClientId = () => randomBase64Url(16);
|
|
121
|
+
//# sourceMappingURL=codes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codes.js","sourceRoot":"","sources":["../../../src/oauth/crypto/codes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose-bound keys derived from `codeSecret` with HKDF-SHA256, cached so
|
|
3
|
+
* rotation (`unsealAny`, `verifyClientSecretAny`) doesn't re-run
|
|
4
|
+
* `importKey` + `deriveKey` for every entry on every request.
|
|
5
|
+
*/
|
|
6
|
+
/** What a derived key is for. `info` is the domain separator: one value per purpose. */
|
|
7
|
+
export type HkdfKeyPurpose = {
|
|
8
|
+
info: string;
|
|
9
|
+
algorithm: AesKeyGenParams | HmacKeyGenParams;
|
|
10
|
+
usage: KeyUsage;
|
|
11
|
+
};
|
|
12
|
+
export declare const hkdfKey: (secret: string, purpose: HkdfKeyPurpose) => Promise<CryptoKey>;
|
|
13
|
+
//# sourceMappingURL=hkdf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hkdf.d.ts","sourceRoot":"","sources":["../../../src/oauth/crypto/hkdf.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,wFAAwF;AACxF,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,eAAe,GAAG,gBAAgB,CAAC;IAC9C,KAAK,EAAE,QAAQ,CAAC;CACjB,CAAC;AAgCF,eAAO,MAAM,OAAO,GAAU,QAAQ,MAAM,EAAE,SAAS,cAAc,KAAG,OAAO,CAAC,SAAS,CAGxF,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose-bound keys derived from `codeSecret` with HKDF-SHA256, cached so
|
|
3
|
+
* rotation (`unsealAny`, `verifyClientSecretAny`) doesn't re-run
|
|
4
|
+
* `importKey` + `deriveKey` for every entry on every request.
|
|
5
|
+
*/
|
|
6
|
+
import { bytesToBase64Url } from "./base64url.js";
|
|
7
|
+
import { createBoundedCache, settleCached } from "./keycache.js";
|
|
8
|
+
const deriveKey = async (secret, purpose) => {
|
|
9
|
+
const encoder = new TextEncoder();
|
|
10
|
+
const material = await crypto.subtle.importKey("raw", encoder.encode(secret), "HKDF", false, [
|
|
11
|
+
"deriveKey",
|
|
12
|
+
]);
|
|
13
|
+
return crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: encoder.encode(purpose.info) }, material, purpose.algorithm, false, [purpose.usage]);
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* SHA-256 fingerprint of `secret` for the cache key — not a KDF. Keeps the
|
|
17
|
+
* plaintext secret out of Map key space (material still lives inside the
|
|
18
|
+
* derived CryptoKey).
|
|
19
|
+
*/
|
|
20
|
+
const secretFingerprint = async (secret) => {
|
|
21
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`mcp-trellis:cache-key:${secret}`));
|
|
22
|
+
return bytesToBase64Url(digest);
|
|
23
|
+
};
|
|
24
|
+
const KEY_CACHE_LIMIT = 64;
|
|
25
|
+
const keyCache = createBoundedCache(KEY_CACHE_LIMIT);
|
|
26
|
+
export const hkdfKey = async (secret, purpose) => {
|
|
27
|
+
const cacheKey = `${purpose.info}:${purpose.usage}:${await secretFingerprint(secret)}`;
|
|
28
|
+
return settleCached(keyCache, cacheKey, () => deriveKey(secret, purpose));
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=hkdf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hkdf.js","sourceRoot":"","sources":["../../../src/oauth/crypto/hkdf.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AASjE,MAAM,SAAS,GAAG,KAAK,EAAE,MAAc,EAAE,OAAuB,EAAsB,EAAE;IACtF,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE;QAC3F,WAAW;KACZ,CAAC,CAAC;IACH,OAAO,MAAM,CAAC,MAAM,CAAC,SAAS,CAC5B,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAC9F,QAAQ,EACR,OAAO,CAAC,SAAS,EACjB,KAAK,EACL,CAAC,OAAO,CAAC,KAAK,CAAC,CAChB,CAAC;AACJ,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,iBAAiB,GAAG,KAAK,EAAE,MAAc,EAAmB,EAAE;IAClE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CACvC,SAAS,EACT,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,yBAAyB,MAAM,EAAE,CAAC,CAC5D,CAAC;IACF,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAC;AAClC,CAAC,CAAC;AAEF,MAAM,eAAe,GAAG,EAAE,CAAC;AAC3B,MAAM,QAAQ,GAAG,kBAAkB,CAAqB,eAAe,CAAC,CAAC;AAEzE,MAAM,CAAC,MAAM,OAAO,GAAG,KAAK,EAAE,MAAc,EAAE,OAAuB,EAAsB,EAAE;IAC3F,MAAM,QAAQ,GAAG,GAAG,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,KAAK,IAAI,MAAM,iBAAiB,CAAC,MAAM,CAAC,EAAE,CAAC;IACvF,OAAO,YAAY,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAC5E,CAAC,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small bounded cache, used by `hkdf.ts` for derived `CryptoKey`s.
|
|
3
|
+
*
|
|
4
|
+
* Insertion-order eviction, not true LRU — simpler, and the expected key
|
|
5
|
+
* count (a handful of rotation entries, rarely changing at runtime) stays
|
|
6
|
+
* well under the cap in ordinary use, so eviction order rarely matters in
|
|
7
|
+
* practice. The cap itself exists for the discouraged-but-possible case of
|
|
8
|
+
* a function-form `codeSecret` returning a different value on every call,
|
|
9
|
+
* which would otherwise grow this cache without bound.
|
|
10
|
+
*/
|
|
11
|
+
export type BoundedCache<V> = {
|
|
12
|
+
get: (key: string) => V | undefined;
|
|
13
|
+
set: (key: string, value: V) => void;
|
|
14
|
+
delete: (key: string) => void;
|
|
15
|
+
};
|
|
16
|
+
export declare const createBoundedCache: <V>(limit: number) => BoundedCache<V>;
|
|
17
|
+
/** Cache a promise; drop the entry if it rejects so the slot is not poisoned. */
|
|
18
|
+
export declare const settleCached: <T>(cache: BoundedCache<Promise<T>>, key: string, create: () => Promise<T>) => Promise<T>;
|
|
19
|
+
//# sourceMappingURL=keycache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keycache.d.ts","sourceRoot":"","sources":["../../../src/oauth/crypto/keycache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI;IAC5B,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,CAAC,GAAG,SAAS,CAAC;IACpC,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IACrC,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,CAAC,EAAE,OAAO,MAAM,KAAG,YAAY,CAAC,CAAC,CAenE,CAAC;AAEF,iFAAiF;AACjF,eAAO,MAAM,YAAY,GAAU,CAAC,EAClC,OAAO,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAC/B,KAAK,MAAM,EACX,QAAQ,MAAM,OAAO,CAAC,CAAC,CAAC,KACvB,OAAO,CAAC,CAAC,CAWX,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small bounded cache, used by `hkdf.ts` for derived `CryptoKey`s.
|
|
3
|
+
*
|
|
4
|
+
* Insertion-order eviction, not true LRU — simpler, and the expected key
|
|
5
|
+
* count (a handful of rotation entries, rarely changing at runtime) stays
|
|
6
|
+
* well under the cap in ordinary use, so eviction order rarely matters in
|
|
7
|
+
* practice. The cap itself exists for the discouraged-but-possible case of
|
|
8
|
+
* a function-form `codeSecret` returning a different value on every call,
|
|
9
|
+
* which would otherwise grow this cache without bound.
|
|
10
|
+
*/
|
|
11
|
+
export const createBoundedCache = (limit) => {
|
|
12
|
+
const map = new Map();
|
|
13
|
+
return {
|
|
14
|
+
get: (key) => map.get(key),
|
|
15
|
+
set: (key, value) => {
|
|
16
|
+
if (!map.has(key) && map.size >= limit) {
|
|
17
|
+
const oldest = map.keys().next().value;
|
|
18
|
+
if (oldest !== undefined)
|
|
19
|
+
map.delete(oldest);
|
|
20
|
+
}
|
|
21
|
+
map.set(key, value);
|
|
22
|
+
},
|
|
23
|
+
delete: (key) => {
|
|
24
|
+
map.delete(key);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
/** Cache a promise; drop the entry if it rejects so the slot is not poisoned. */
|
|
29
|
+
export const settleCached = async (cache, key, create) => {
|
|
30
|
+
const cached = cache.get(key);
|
|
31
|
+
if (cached)
|
|
32
|
+
return cached;
|
|
33
|
+
const pending = create();
|
|
34
|
+
cache.set(key, pending);
|
|
35
|
+
try {
|
|
36
|
+
return await pending;
|
|
37
|
+
}
|
|
38
|
+
catch (caught) {
|
|
39
|
+
if (cache.get(key) === pending)
|
|
40
|
+
cache.delete(key);
|
|
41
|
+
throw caught;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=keycache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keycache.js","sourceRoot":"","sources":["../../../src/oauth/crypto/keycache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAQH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAI,KAAa,EAAmB,EAAE;IACtE,MAAM,GAAG,GAAG,IAAI,GAAG,EAAa,CAAC;IACjC,OAAO;QACL,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC;QAC1B,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;YAClB,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,IAAI,IAAI,KAAK,EAAE,CAAC;gBACvC,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;gBACvC,IAAI,MAAM,KAAK,SAAS;oBAAE,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC/C,CAAC;YACD,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACtB,CAAC;QACD,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE;YACd,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC,CAAC;AAEF,iFAAiF;AACjF,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAC/B,KAA+B,EAC/B,GAAW,EACX,MAAwB,EACZ,EAAE;IACd,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC;IACzB,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACxB,IAAI,CAAC;QACH,OAAO,MAAM,OAAO,CAAC;IACvB,CAAC;IAAC,OAAO,MAAM,EAAE,CAAC;QAChB,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,OAAO;YAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAClD,MAAM,MAAM,CAAC;IACf,CAAC;AACH,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../../../src/oauth/crypto/pkce.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,eAAe,GAAU,OAAO,MAAM,KAAG,OAAO,CAAC,MAAM,CAGnE,CAAC;AAEF,eAAO,MAAM,cAAc,GAAU,UAAU,MAAM,EAAE,WAAW,MAAM,KAAG,OAAO,CAAC,OAAO,CAIzF,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,SAAS,MAAM,KAAG,MAGjD,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { timingSafeEqual } from "../../auth/bearer.js";
|
|
1
2
|
import { bytesToBase64Url } from "./base64url.js";
|
|
2
|
-
import { timingSafeEqual } from "../auth/bearer.js";
|
|
3
3
|
export const sha256Base64Url = async (input) => {
|
|
4
4
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
5
5
|
return bytesToBase64Url(digest);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../../src/oauth/crypto/pkce.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EAAE,KAAa,EAAmB,EAAE;IACtE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACtF,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAC;AAClC,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EAAE,QAAgB,EAAE,SAAiB,EAAoB,EAAE;IAC5F,IAAI,CAAC,QAAQ,IAAI,CAAC,SAAS;QAAE,OAAO,KAAK,CAAC;IAC1C,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,QAAQ,CAAC,CAAC;IACjD,OAAO,eAAe,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAC9C,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,OAAe,EAAU,EAAE;IACzD,MAAM,KAAK,GAAG,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC;IAC9D,OAAO,gBAAgB,CAAC,KAAK,CAAC,CAAC;AACjC,CAAC,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-GCM sealing for short-lived, self-describing tokens (auth codes,
|
|
3
|
+
* consent tickets, DCR client assertions), all keyed from `codeSecret`.
|
|
4
|
+
*
|
|
5
|
+
* Domain separation is mandatory, not a nice-to-have: a consent ticket must
|
|
6
|
+
* never unseal as a client-id assertion, and neither must ever unseal as an
|
|
7
|
+
* auth code. HKDF derives an independent AES-256 key per `SealedType` from
|
|
8
|
+
* the same secret, and the type is also bound in as AEAD associated data —
|
|
9
|
+
* two independent reasons decryption fails across types, not one.
|
|
10
|
+
*/
|
|
11
|
+
export type SealedType = "consent" | "client" | "code";
|
|
12
|
+
/** Seals `payload` as an opaque, type-bound string. */
|
|
13
|
+
export declare const seal: <T>(secret: string, type: SealedType, payload: T) => Promise<string>;
|
|
14
|
+
/** Unseals a string sealed with the same `secret` and `type`; `null` on any mismatch or tamper. */
|
|
15
|
+
export declare const unseal: <T>(secret: string, type: SealedType, sealed: string) => Promise<T | null>;
|
|
16
|
+
/**
|
|
17
|
+
* Rotation-aware unseal: tries each `secrets` entry in order, returning the
|
|
18
|
+
* index of the one that actually worked so a caller can audit "sealed under
|
|
19
|
+
* a non-primary key" without a second decrypt pass. `secrets` is expected
|
|
20
|
+
* non-empty (callers resolve it via `resolveSecrets`, which enforces that).
|
|
21
|
+
*/
|
|
22
|
+
export declare const unsealAny: <T>(secrets: readonly string[], type: SealedType, sealed: string) => Promise<{
|
|
23
|
+
value: T;
|
|
24
|
+
keyIndex: number;
|
|
25
|
+
} | null>;
|
|
26
|
+
//# sourceMappingURL=sealed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sealed.d.ts","sourceRoot":"","sources":["../../../src/oauth/crypto/sealed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAC;AAevD,uDAAuD;AACvD,eAAO,MAAM,IAAI,GAAU,CAAC,EAAE,QAAQ,MAAM,EAAE,MAAM,UAAU,EAAE,SAAS,CAAC,KAAG,OAAO,CAAC,MAAM,CAW1F,CAAC;AAEF,mGAAmG;AACnG,eAAO,MAAM,MAAM,GAAU,CAAC,EAC5B,QAAQ,MAAM,EACd,MAAM,UAAU,EAChB,QAAQ,MAAM,KACb,OAAO,CAAC,CAAC,GAAG,IAAI,CAsBlB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,SAAS,GAAU,CAAC,EAC/B,SAAS,SAAS,MAAM,EAAE,EAC1B,MAAM,UAAU,EAChB,QAAQ,MAAM,KACb,OAAO,CAAC;IAAE,KAAK,EAAE,CAAC,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAM/C,CAAC"}
|