arc-1 0.9.18 → 0.9.19
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 +26 -26
- package/dist/adt/config.d.ts +1 -1
- package/dist/adt/config.d.ts.map +1 -1
- package/dist/adt/http.d.ts +1 -1
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js.map +1 -1
- package/dist/authz/policy.d.ts +6 -0
- package/dist/authz/policy.d.ts.map +1 -1
- package/dist/authz/policy.js +20 -0
- package/dist/authz/policy.js.map +1 -1
- package/dist/cli.js +21 -3
- package/dist/cli.js.map +1 -1
- package/dist/handlers/dispatch.d.ts +3 -0
- package/dist/handlers/dispatch.d.ts.map +1 -1
- package/dist/handlers/dispatch.js +71 -53
- package/dist/handlers/dispatch.js.map +1 -1
- package/dist/handlers/schemas.d.ts +4 -4
- package/dist/plugins/manifest-interpreter.d.ts +25 -0
- package/dist/plugins/manifest-interpreter.d.ts.map +1 -0
- package/dist/plugins/manifest-interpreter.js +124 -0
- package/dist/plugins/manifest-interpreter.js.map +1 -0
- package/dist/public/define-tool.d.ts +9 -0
- package/dist/public/define-tool.d.ts.map +1 -0
- package/dist/public/define-tool.js +25 -0
- package/dist/public/define-tool.js.map +1 -0
- package/dist/public/index.d.ts +9 -0
- package/dist/public/index.d.ts.map +1 -0
- package/dist/public/index.js +10 -0
- package/dist/public/index.js.map +1 -0
- package/dist/public/testing.d.ts +26 -0
- package/dist/public/testing.d.ts.map +1 -0
- package/dist/public/testing.js +39 -0
- package/dist/public/testing.js.map +1 -0
- package/dist/public/types.d.ts +87 -0
- package/dist/public/types.d.ts.map +1 -0
- package/dist/public/types.js +4 -0
- package/dist/public/types.js.map +1 -0
- package/dist/registry/tool-registry.d.ts +74 -0
- package/dist/registry/tool-registry.d.ts.map +1 -0
- package/dist/registry/tool-registry.js +59 -0
- package/dist/registry/tool-registry.js.map +1 -0
- package/dist/server/app-url.d.ts +31 -0
- package/dist/server/app-url.d.ts.map +1 -0
- package/dist/server/app-url.js +50 -0
- package/dist/server/app-url.js.map +1 -0
- package/dist/server/audit.d.ts +4 -0
- package/dist/server/audit.d.ts.map +1 -1
- package/dist/server/audit.js.map +1 -1
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +19 -0
- package/dist/server/config.js.map +1 -1
- package/dist/server/http.d.ts +15 -46
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +105 -375
- package/dist/server/http.js.map +1 -1
- package/dist/server/logger.d.ts +22 -0
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/logger.js +22 -0
- package/dist/server/logger.js.map +1 -1
- package/dist/server/plugin-loader.d.ts +19 -0
- package/dist/server/plugin-loader.d.ts.map +1 -0
- package/dist/server/plugin-loader.js +162 -0
- package/dist/server/plugin-loader.js.map +1 -0
- package/dist/server/safe-http-client.d.ts +38 -0
- package/dist/server/safe-http-client.d.ts.map +1 -0
- package/dist/server/safe-http-client.js +129 -0
- package/dist/server/safe-http-client.js.map +1 -0
- package/dist/server/server.d.ts +2 -2
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +36 -7
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +8 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +2 -0
- package/dist/server/types.js.map +1 -1
- package/package.json +24 -8
- package/dist/adt/btp.d.ts +0 -140
- package/dist/adt/btp.d.ts.map +0 -1
- package/dist/adt/btp.js +0 -427
- package/dist/adt/btp.js.map +0 -1
- package/dist/server/oauth-state.d.ts +0 -92
- package/dist/server/oauth-state.d.ts.map +0 -1
- package/dist/server/oauth-state.js +0 -163
- package/dist/server/oauth-state.js.map +0 -1
- package/dist/server/stateless-client-store.d.ts +0 -173
- package/dist/server/stateless-client-store.d.ts.map +0 -1
- package/dist/server/stateless-client-store.js +0 -503
- package/dist/server/stateless-client-store.js.map +0 -1
- package/dist/server/xsuaa.d.ts +0 -188
- package/dist/server/xsuaa.d.ts.map +0 -1
- package/dist/server/xsuaa.js +0 -464
- package/dist/server/xsuaa.js.map +0 -1
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stateless, signed OAuth `state` codec for the XSUAA callback proxy.
|
|
3
|
-
*
|
|
4
|
-
* ── Why this exists (issue #214) ──────────────────────────────────────
|
|
5
|
-
* XSUAA echoes a literal `+` (not `%2B`) for any `state` value that
|
|
6
|
-
* contains `+` when it redirects back to the OAuth client. Standard base64
|
|
7
|
-
* `state` values (e.g. VS Code generates `randomBytes(16).toString('base64')`)
|
|
8
|
-
* contain `+` ~50% of the time. The receiving client parses the callback
|
|
9
|
-
* query string with `application/x-www-form-urlencoded` semantics, where
|
|
10
|
-
* `+` decodes to a space, so the round-tripped `state` no longer matches the
|
|
11
|
-
* value the client generated → "State does not match" → login fails.
|
|
12
|
-
*
|
|
13
|
-
* ARC-1 cannot influence what XSUAA emits, and XSUAA redirects DIRECTLY to
|
|
14
|
-
* the client today (ARC-1 is not in the return path). The only fix is to
|
|
15
|
-
* insert ARC-1 into the return path: send XSUAA a `state` that ARC-1
|
|
16
|
-
* controls and that is immune to the `+` bug, then re-emit the client's
|
|
17
|
-
* ORIGINAL `state` correctly when redirecting back to the client.
|
|
18
|
-
*
|
|
19
|
-
* ── How this codec is immune to the `+` bug ───────────────────────────
|
|
20
|
-
* The token is `base64url(payload) + "." + base64url(sig)`. base64url uses
|
|
21
|
-
* the alphabet `A-Za-z0-9-_` — no `+`, no `/`. The `.` separator is an
|
|
22
|
-
* RFC 3986 unreserved character. So the entire token is URL-safe: XSUAA has
|
|
23
|
-
* no `+` to mangle, and Express's `+`→space query decoding is a no-op on it.
|
|
24
|
-
* The client's real `state` (which may contain `+`) rides INSIDE the opaque
|
|
25
|
-
* base64url payload, so it survives the XSUAA round-trip untouched.
|
|
26
|
-
*
|
|
27
|
-
* ── Why stateless (vs an in-memory map) ───────────────────────────────
|
|
28
|
-
* Mirrors the StatelessDcrClientStore design (PR #212): the token carries
|
|
29
|
-
* its own payload + HMAC signature, so any instance with the same signing
|
|
30
|
-
* key can validate it. No in-memory map → survives `cf restart`, cell
|
|
31
|
-
* moves, and horizontal scale-out. The signing key is derived (HKDF-style)
|
|
32
|
-
* from the same secret the DCR store uses, with a distinct domain-separation
|
|
33
|
-
* label so the two key spaces never overlap.
|
|
34
|
-
*
|
|
35
|
-
* ── Upstream tracking / when this whole module can be deleted ──────────
|
|
36
|
-
* This is a WORKAROUND for an XSUAA bug. It can be removed ONLY when XSUAA
|
|
37
|
-
* stops emitting a literal `+` (emits `%2B`) for `state` on the authorize
|
|
38
|
-
* redirect. Tracking:
|
|
39
|
-
* - arc-1 issue: https://github.com/marianfoo/arc-1/issues/214
|
|
40
|
-
* - XSUAA root cause: no public SAP Note as of 2026-06; the `+`→literal
|
|
41
|
-
* echo is the actual defect and the only thing whose
|
|
42
|
-
* fix makes this module removable.
|
|
43
|
-
* - VS Code (client): https://github.com/microsoft/vscode/issues/314715
|
|
44
|
-
* asks VS Code to use base64url `state`. If accepted it
|
|
45
|
-
* fixes the VS Code SYMPTOM only — other MCP clients
|
|
46
|
-
* (Cursor, claude.ai, Copilot Studio, …) still send
|
|
47
|
-
* base64 `state` containing `+`, so the callback proxy
|
|
48
|
-
* stays until the XSUAA-side fix lands. Do NOT delete
|
|
49
|
-
* this module just because vscode#314715 closes.
|
|
50
|
-
* To verify whether the XSUAA bug is gone, re-run the issue #214 spectrum
|
|
51
|
-
* reproducer (see the issue thread) against the target XSUAA tenant.
|
|
52
|
-
*/
|
|
53
|
-
import crypto from 'node:crypto';
|
|
54
|
-
/** Domain-separation label for the HKDF-style key derivation. Bump the
|
|
55
|
-
* version suffix to invalidate every outstanding state token at once. */
|
|
56
|
-
const KDF_LABEL = 'arc1-oauth-state/v1';
|
|
57
|
-
/** Truncated HMAC length in bytes. 16 bytes (128 bits) is ample for a
|
|
58
|
-
* short-lived, single-use CSRF state token — matches StatelessDcrClientStore. */
|
|
59
|
-
const SIG_BYTES = 16;
|
|
60
|
-
/** Default lifetime of a state token. The OAuth authorize→callback hop is
|
|
61
|
-
* interactive (user logs in), so a few minutes covers it; XSUAA auth codes
|
|
62
|
-
* themselves expire on a similar horizon. */
|
|
63
|
-
const DEFAULT_TTL_SECONDS = 600; // 10 minutes
|
|
64
|
-
/**
|
|
65
|
-
* Signs and verifies OAuth `state` tokens for the XSUAA callback proxy.
|
|
66
|
-
*/
|
|
67
|
-
export class OAuthStateCodec {
|
|
68
|
-
hmacKey;
|
|
69
|
-
ttlSeconds;
|
|
70
|
-
constructor(signingSecret, opts = {}) {
|
|
71
|
-
if (!signingSecret) {
|
|
72
|
-
throw new Error('OAuthStateCodec requires a non-empty signingSecret');
|
|
73
|
-
}
|
|
74
|
-
// HKDF-style: derive a dedicated key from the shared secret + label.
|
|
75
|
-
// The label domain-separates this key from the DCR client-id signing key.
|
|
76
|
-
this.hmacKey = crypto.createHmac('sha256', signingSecret).update(KDF_LABEL).digest();
|
|
77
|
-
this.ttlSeconds = opts.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Encode a URL-safe, signed state token. The returned value is safe to put
|
|
81
|
-
* in a query string and round-trip through XSUAA (no `+`, no `/`).
|
|
82
|
-
*
|
|
83
|
-
* @param input.now Injectable clock (epoch ms) for deterministic tests.
|
|
84
|
-
*/
|
|
85
|
-
encode(input) {
|
|
86
|
-
const nowSec = Math.floor((input.now ?? Date.now()) / 1000);
|
|
87
|
-
const payload = {
|
|
88
|
-
v: 1,
|
|
89
|
-
r: input.clientRedirectUri,
|
|
90
|
-
cid: input.clientId,
|
|
91
|
-
exp: nowSec + this.ttlSeconds,
|
|
92
|
-
};
|
|
93
|
-
if (input.clientState !== undefined) {
|
|
94
|
-
payload.s = input.clientState;
|
|
95
|
-
}
|
|
96
|
-
const payloadB64 = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
|
|
97
|
-
return `${payloadB64}.${this.sign(payloadB64)}`;
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Decode and verify a state token. Never throws — returns a typed result.
|
|
101
|
-
*
|
|
102
|
-
* @param now Injectable clock (epoch ms) for deterministic tests.
|
|
103
|
-
*/
|
|
104
|
-
decode(token, now = Date.now()) {
|
|
105
|
-
if (typeof token !== 'string' || token.length === 0) {
|
|
106
|
-
return { kind: 'error', reason: 'malformed' };
|
|
107
|
-
}
|
|
108
|
-
const dot = token.lastIndexOf('.');
|
|
109
|
-
if (dot <= 0 || dot === token.length - 1) {
|
|
110
|
-
return { kind: 'error', reason: 'malformed' };
|
|
111
|
-
}
|
|
112
|
-
const payloadB64 = token.slice(0, dot);
|
|
113
|
-
const sigB64 = token.slice(dot + 1);
|
|
114
|
-
if (!this.verifySignature(payloadB64, sigB64)) {
|
|
115
|
-
return { kind: 'error', reason: 'bad_signature' };
|
|
116
|
-
}
|
|
117
|
-
const payload = parsePayload(payloadB64);
|
|
118
|
-
if (!payload) {
|
|
119
|
-
return { kind: 'error', reason: 'invalid_payload' };
|
|
120
|
-
}
|
|
121
|
-
if (payload.exp * 1000 <= now) {
|
|
122
|
-
return { kind: 'error', reason: 'expired' };
|
|
123
|
-
}
|
|
124
|
-
return { kind: 'ok', clientState: payload.s, clientRedirectUri: payload.r, clientId: payload.cid };
|
|
125
|
-
}
|
|
126
|
-
sign(payloadB64) {
|
|
127
|
-
const fullDigest = crypto.createHmac('sha256', this.hmacKey).update(payloadB64).digest();
|
|
128
|
-
return fullDigest.subarray(0, SIG_BYTES).toString('base64url');
|
|
129
|
-
}
|
|
130
|
-
verifySignature(payloadB64, sigB64) {
|
|
131
|
-
const expected = Buffer.from(this.sign(payloadB64), 'base64url');
|
|
132
|
-
const actual = Buffer.from(sigB64, 'base64url');
|
|
133
|
-
if (actual.length !== expected.length || actual.length !== SIG_BYTES) {
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
return crypto.timingSafeEqual(actual, expected);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Parse a base64url payload back into a typed `StatePayload`. Returns
|
|
141
|
-
* `undefined` on any failure (decode error, JSON parse error, schema mismatch).
|
|
142
|
-
*/
|
|
143
|
-
function parsePayload(payloadB64) {
|
|
144
|
-
try {
|
|
145
|
-
const json = Buffer.from(payloadB64, 'base64url').toString('utf8');
|
|
146
|
-
const obj = JSON.parse(json);
|
|
147
|
-
if (obj.v !== 1)
|
|
148
|
-
return undefined;
|
|
149
|
-
if (typeof obj.r !== 'string' || obj.r.length === 0)
|
|
150
|
-
return undefined;
|
|
151
|
-
if (typeof obj.cid !== 'string' || obj.cid.length === 0)
|
|
152
|
-
return undefined;
|
|
153
|
-
if (typeof obj.exp !== 'number' || !Number.isFinite(obj.exp))
|
|
154
|
-
return undefined;
|
|
155
|
-
if (obj.s !== undefined && typeof obj.s !== 'string')
|
|
156
|
-
return undefined;
|
|
157
|
-
return { v: 1, s: obj.s, r: obj.r, cid: obj.cid, exp: obj.exp };
|
|
158
|
-
}
|
|
159
|
-
catch {
|
|
160
|
-
return undefined;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
//# sourceMappingURL=oauth-state.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"oauth-state.js","sourceRoot":"","sources":["../../src/server/oauth-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,OAAO,MAAM,MAAM,aAAa,CAAC;AAEjC;0EAC0E;AAC1E,MAAM,SAAS,GAAG,qBAAqB,CAAC;AAExC;kFACkF;AAClF,MAAM,SAAS,GAAG,EAAE,CAAC;AAErB;;8CAE8C;AAC9C,MAAM,mBAAmB,GAAG,GAAG,CAAC,CAAC,aAAa;AA2B9C;;GAEG;AACH,MAAM,OAAO,eAAe;IACT,OAAO,CAAS;IAChB,UAAU,CAAS;IAEpC,YAAY,aAAqB,EAAE,OAAgC,EAAE;QACnE,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;QACxE,CAAC;QACD,qEAAqE;QACrE,0EAA0E;QAC1E,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,CAAC;QACrF,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,mBAAmB,CAAC;IAC3D,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,KAA0F;QAC/F,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;QAC5D,MAAM,OAAO,GAAiB;YAC5B,CAAC,EAAE,CAAC;YACJ,CAAC,EAAE,KAAK,CAAC,iBAAiB;YAC1B,GAAG,EAAE,KAAK,CAAC,QAAQ;YACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC,UAAU;SAC9B,CAAC;QACF,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;YACpC,OAAO,CAAC,CAAC,GAAG,KAAK,CAAC,WAAW,CAAC;QAChC,CAAC;QACD,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QACtF,OAAO,GAAG,UAAU,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;IAClD,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,KAAa,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;QAC5C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;QAChD,CAAC;QACD,MAAM,GAAG,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,GAAG,IAAI,CAAC,IAAI,GAAG,KAAK,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACzC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;QAChD,CAAC;QACD,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QACvC,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;QAEpC,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,UAAU,EAAE,MAAM,CAAC,EAAE,CAAC;YAC9C,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;QACpD,CAAC;QAED,MAAM,OAAO,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;QACzC,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;QACtD,CAAC;QAED,IAAI,OAAO,CAAC,GAAG,GAAG,IAAI,IAAI,GAAG,EAAE,CAAC;YAC9B,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QAC9C,CAAC;QAED,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC,EAAE,iBAAiB,EAAE,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC;IACrG,CAAC;IAEO,IAAI,CAAC,UAAkB;QAC7B,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,EAAE,CAAC;QACzF,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IACjE,CAAC;IAEO,eAAe,CAAC,UAAkB,EAAE,MAAc;QACxD,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,WAAW,CAAC,CAAC;QACjE,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QAChD,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YACrE,OAAO,KAAK,CAAC;QACf,CAAC;QACD,OAAO,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAClD,CAAC;CACF;AAED;;;GAGG;AACH,SAAS,YAAY,CAAC,UAAkB;IACtC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACnE,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAA4B,CAAC;QACxD,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QAClC,IAAI,OAAO,GAAG,CAAC,CAAC,KAAK,QAAQ,IAAI,GAAG,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QACtE,IAAI,OAAO,GAAG,CAAC,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QAC1E,IAAI,OAAO,GAAG,CAAC,GAAG,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,SAAS,CAAC;QAC/E,IAAI,GAAG,CAAC,CAAC,KAAK,SAAS,IAAI,OAAO,GAAG,CAAC,CAAC,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QACvE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAuB,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC;IACxF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stateless OAuth Dynamic Client Registration store.
|
|
3
|
-
*
|
|
4
|
-
* MCP clients (Claude Desktop, Cursor, Copilot CLI…) register dynamically
|
|
5
|
-
* via RFC 7591 and cache the returned `client_id` locally. With an
|
|
6
|
-
* in-memory or local-disk store, every CF push / restart wipes the
|
|
7
|
-
* server-side registry — the cached `client_id` then fails with
|
|
8
|
-
* `invalid_client` and the user has to clear their MCP client's OAuth
|
|
9
|
-
* cache to recover.
|
|
10
|
-
*
|
|
11
|
-
* This store eliminates the storage problem entirely. Each `client_id`
|
|
12
|
-
* is a self-validating token: it carries the registration payload
|
|
13
|
-
* (redirect_uris, grant_types, …) plus an HMAC-SHA256 signature derived
|
|
14
|
-
* from a server-held key. `getClient` re-derives the payload by
|
|
15
|
-
* verifying the signature; no persistence is needed. Any process with
|
|
16
|
-
* the same signing key can validate any client_id ever issued.
|
|
17
|
-
*
|
|
18
|
-
* Tradeoffs vs the persisted in-memory store:
|
|
19
|
-
* + Survives `cf push`, `cf restart`, cell moves, multi-instance scale-out
|
|
20
|
-
* + No external dependency, no service binding, no native module
|
|
21
|
-
* - Per-client revocation is impossible (only TTL or full key rotation)
|
|
22
|
-
* - Rotating the signing key invalidates every outstanding registration
|
|
23
|
-
*
|
|
24
|
-
* Default TTL is 30 days (matches typical refresh-token lifetimes). Setting
|
|
25
|
-
* `ttlSeconds` to `0` or a negative value disables expiration — recommended
|
|
26
|
-
* when MCP clients don't auto-re-register on `invalid_client` (Copilot CLI,
|
|
27
|
-
* Cursor) and a finite TTL just produces periodic outages without security
|
|
28
|
-
* gain. In that mode, forced revocation goes through full key rotation
|
|
29
|
-
* (rotate the signing secret or bump `KDF_LABEL` from `arc1-dcr/v1` → `v2`).
|
|
30
|
-
*
|
|
31
|
-
* The signing key is derived (via HKDF-style HMAC) from the XSUAA
|
|
32
|
-
* `clientsecret`, so it's already as stable as the service binding —
|
|
33
|
-
* service rebinding rotates both at once, which is the right boundary.
|
|
34
|
-
*/
|
|
35
|
-
import type { OAuthRegisteredClientsStore } from '@modelcontextprotocol/sdk/server/auth/clients.js';
|
|
36
|
-
import type { OAuthClientInformationFull } from '@modelcontextprotocol/sdk/shared/auth.js';
|
|
37
|
-
/**
|
|
38
|
-
* Redirect-URI allowlist for the pre-registered XSUAA default client — a vendored
|
|
39
|
-
* mirror of `oauth2-configuration.redirect-uris` in `xs-security.json`.
|
|
40
|
-
*
|
|
41
|
-
* ── Why ARC-1 must enforce this (not just XSUAA) ──
|
|
42
|
-
* The issue-#214 callback proxy (see `oauth-state.ts`) sends XSUAA ARC-1's OWN
|
|
43
|
-
* `/oauth/callback` as the redirect_uri and carries the client's real
|
|
44
|
-
* redirect_uri inside the signed state. XSUAA therefore no longer validates the
|
|
45
|
-
* client's redirect_uri — ARC-1 does. Without an allowlist, `ensureRedirectUri`
|
|
46
|
-
* would auto-trust ANY redirect_uri supplied at `/authorize` for the shared
|
|
47
|
-
* default client, letting an attacker steer a victim's authorization code to
|
|
48
|
-
* their own URI (security audit 2026-06, follow-up to PR #352).
|
|
49
|
-
*
|
|
50
|
-
* ── Why vendored, not read from xs-security.json ──
|
|
51
|
-
* `xs-security.json` is consumed by XSUAA at service-creation time and is NOT
|
|
52
|
-
* shipped with the running app (excluded by `.cfignore`, the npm `files`
|
|
53
|
-
* allowlist, and the Dockerfile), and the service binding does not expose the
|
|
54
|
-
* patterns — so ARC-1 cannot read them at runtime. To prevent drift,
|
|
55
|
-
* `tests/unit/server/stateless-client-store.test.ts` asserts this list stays
|
|
56
|
-
* equal to `xs-security.json`. Keep the two in sync when adding a client.
|
|
57
|
-
*
|
|
58
|
-
* Glob semantics (xs-security.json): `*` matches within a single host/path
|
|
59
|
-
* segment (never `/`), `**` matches across segments.
|
|
60
|
-
*/
|
|
61
|
-
export declare const XSUAA_REDIRECT_URI_PATTERNS: readonly ["http://localhost:*/**", "https://*.hana.ondemand.com/**", "https://*.applicationstudio.cloud.sap/**", "https://claude.ai/api/mcp/auth_callback", "https://callback.mistral.ai/v1/integrations_auth/oauth2_callback", "cursor://anysphere.cursor-retrieval/**", "cursor://anysphere.cursor-mcp/**", "vscode://vscode.microsoft-authentication/**", "https://global.consent.azure-apim.net/redirect/**"];
|
|
62
|
-
/**
|
|
63
|
-
* Is `uri` an allowed redirect target for the pre-registered XSUAA default
|
|
64
|
-
* client? True iff it matches an `XSUAA_REDIRECT_URI_PATTERNS` entry. Stateless,
|
|
65
|
-
* so it gives the same answer on every instance — used both to gate dynamic
|
|
66
|
-
* registration (`ensureRedirectUri`) and to validate the redirect target at
|
|
67
|
-
* `/oauth/callback` (`checkRedirectUri`).
|
|
68
|
-
*
|
|
69
|
-
* SECURITY — parse before matching: the value matched here is later re-parsed
|
|
70
|
-
* with `new URL()` and used as the 302 target that carries the OAuth `code`, so
|
|
71
|
-
* the glob decision MUST agree with how the URL actually parses. The patterns
|
|
72
|
-
* are string globs; a `*` sitting in the PORT position (the localhost pattern)
|
|
73
|
-
* would otherwise let a URL-userinfo segment ride inside the same-segment
|
|
74
|
-
* wildcard — `http://localhost:x@evil.com/cb` matches the `localhost:[^slash]`
|
|
75
|
-
* port glob yet `new URL(...).host === 'evil.com'`, steering a victim's code to an
|
|
76
|
-
* attacker host. So we reject anything that doesn't parse, reject any userinfo
|
|
77
|
-
* (`user[:pass]@`, which no legitimate redirect_uri carries), and — for http/https —
|
|
78
|
-
* match the glob against a subject rebuilt from the PARSED components rather than the
|
|
79
|
-
* raw string, so `\`, `#` and `?` (authority terminators the WHATWG parser folds) cannot
|
|
80
|
-
* relocate the host past a same-segment wildcard. After these guards, a glob match
|
|
81
|
-
* implies the parsed host is the literal host in the pattern.
|
|
82
|
-
*/
|
|
83
|
-
export declare function matchesXsuaaRedirectPattern(uri: string): boolean;
|
|
84
|
-
export interface StatelessDcrClientStoreOptions {
|
|
85
|
-
/**
|
|
86
|
-
* How long an issued client_id remains valid, in seconds. After this
|
|
87
|
-
* window `getClient()` returns undefined and clients are forced to
|
|
88
|
-
* re-register via `/register`. Default: 30 days. Set to `0` (or any
|
|
89
|
-
* non-positive value) to disable expiration — registrations then stay
|
|
90
|
-
* valid until the signing key rotates.
|
|
91
|
-
*/
|
|
92
|
-
ttlSeconds?: number;
|
|
93
|
-
/** Clock injection point for tests. Default: `Date.now`. */
|
|
94
|
-
now?: () => number;
|
|
95
|
-
}
|
|
96
|
-
export declare class StatelessDcrClientStore implements OAuthRegisteredClientsStore {
|
|
97
|
-
private readonly xsuaaClient;
|
|
98
|
-
private readonly hmacKey;
|
|
99
|
-
private readonly ttlSeconds;
|
|
100
|
-
private readonly now;
|
|
101
|
-
constructor(xsuaaClientId: string, xsuaaClientSecret: string, signingSecret: string, options?: StatelessDcrClientStoreOptions);
|
|
102
|
-
getClient(clientId: string): Promise<OAuthClientInformationFull | undefined>;
|
|
103
|
-
registerClient(client: Omit<OAuthClientInformationFull, 'client_id' | 'client_id_issued_at'>): Promise<OAuthClientInformationFull>;
|
|
104
|
-
/**
|
|
105
|
-
* Called by the MCP SDK before redirect_uri validation on `/authorize`.
|
|
106
|
-
*
|
|
107
|
-
* For the pre-registered XSUAA client we mutate the in-memory list so the
|
|
108
|
-
* SDK's exact-match check passes. The mutation is replayed on every
|
|
109
|
-
* `/authorize`, so it doesn't need to persist. SECURITY: we register a
|
|
110
|
-
* candidate URI ONLY if it matches `XSUAA_REDIRECT_URI_PATTERNS` (the vendored
|
|
111
|
-
* mirror of xs-security.json). The issue-#214 callback proxy removed XSUAA
|
|
112
|
-
* from the client-redirect path, so an un-gated add here would let an attacker
|
|
113
|
-
* register an arbitrary redirect_uri and have the SDK accept it — the entry
|
|
114
|
-
* point for authorization-code interception (security audit 2026-06). A
|
|
115
|
-
* non-matching URI is dropped (audited); the SDK's exact-match check then
|
|
116
|
-
* rejects the `/authorize` request before any state is minted.
|
|
117
|
-
*
|
|
118
|
-
* For DCR (`arc1-…`) clients we are stateless by design: there's nothing
|
|
119
|
-
* to mutate. The previous in-memory store implemented a percent-encoding
|
|
120
|
-
* loose-match (BAS/Theia registers `?x=1` then authorizes with `%3Fx=1`).
|
|
121
|
-
* Reproducing that statelessly would require either bundling every
|
|
122
|
-
* encoding variant in the signed payload or keeping a per-process scratch
|
|
123
|
-
* map, both of which undermine the "no state" goal. We accept the
|
|
124
|
-
* regression: affected clients re-register on encoding-variant mismatch,
|
|
125
|
-
* which is exactly what they did under the old store after every restart.
|
|
126
|
-
*/
|
|
127
|
-
ensureRedirectUri(clientId: string, uri: string): void;
|
|
128
|
-
/**
|
|
129
|
-
* Validate that `uri` is an allowed redirect target for `clientId` at the
|
|
130
|
-
* `/oauth/callback` proxy — the control that stops authorization-code
|
|
131
|
-
* interception (security audit 2026-06, follow-up to PR #352).
|
|
132
|
-
*
|
|
133
|
-
* - Default (pre-registered XSUAA) client → must match the redirect-uri
|
|
134
|
-
* allowlist (`matchesXsuaaRedirectPattern`). Deliberately consults the
|
|
135
|
-
* static allowlist, NOT the mutable in-memory list, so the verdict is
|
|
136
|
-
* stateless and identical on every instance — a code is never forwarded to
|
|
137
|
-
* an unlisted URI even if `/authorize` ran on a different instance.
|
|
138
|
-
* - DCR (`arc1-…`) client → must be one of the redirect_uris baked immutably
|
|
139
|
-
* into the signed client_id (re-derived by `getClient`). Returns
|
|
140
|
-
* `unknown_client` when the id is unrecognised / expired / forged.
|
|
141
|
-
*/
|
|
142
|
-
checkRedirectUri(clientId: string, uri: string): Promise<'ok' | 'unknown_client' | 'unregistered'>;
|
|
143
|
-
private payloadToClientInfo;
|
|
144
|
-
private encode;
|
|
145
|
-
/**
|
|
146
|
-
* Decode and verify a `client_id`. Returns either the parsed payload or a
|
|
147
|
-
* structured failure reason — the caller emits the failure as an audit
|
|
148
|
-
* event with the right reason code (so probing attempts are observable).
|
|
149
|
-
*/
|
|
150
|
-
private decodeAndVerify;
|
|
151
|
-
private verifySignature;
|
|
152
|
-
private sign;
|
|
153
|
-
/**
|
|
154
|
-
* The client_secret is derived deterministically from the client_id, so
|
|
155
|
-
* any instance with the same signing key can validate it. This is the
|
|
156
|
-
* core reason DCR survives container restarts and scales out horizontally
|
|
157
|
-
* with no shared state.
|
|
158
|
-
*/
|
|
159
|
-
private deriveSecret;
|
|
160
|
-
private emitLookupFailed;
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Validate a redirect URI against the allowed scheme/host policy.
|
|
164
|
-
*
|
|
165
|
-
* Allowed: `https://*`, `http://` to localhost / 127.0.0.1 / [::1], and known
|
|
166
|
-
* MCP-client custom schemes (`claude:`, `cursor:`, `vscode:`,
|
|
167
|
-
* `vscode-insiders:`).
|
|
168
|
-
*
|
|
169
|
-
* Rejected: `javascript:`, `data:`, `file:`, `ftp:`, and any `http://` to
|
|
170
|
-
* non-loopback hosts.
|
|
171
|
-
*/
|
|
172
|
-
export declare function validateRedirectUri(uri: string): void;
|
|
173
|
-
//# sourceMappingURL=stateless-client-store.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"stateless-client-store.d.ts","sourceRoot":"","sources":["../../src/server/stateless-client-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAGH,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,kDAAkD,CAAC;AACpG,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,0CAA0C,CAAC;AAuD3F;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,2BAA2B,mZAU9B,CAAC;AAwBX;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAmBhE;AAuBD,MAAM,WAAW,8BAA8B;IAC7C;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,4DAA4D;IAC5D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAuBD,qBAAa,uBAAwB,YAAW,2BAA2B;IACzE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAGjC,aAAa,EAAE,MAAM,EACrB,iBAAiB,EAAE,MAAM,EACzB,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE,8BAAmC;IA+BxC,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,0BAA0B,GAAG,SAAS,CAAC;IA4B5E,cAAc,CAClB,MAAM,EAAE,IAAI,CAAC,0BAA0B,EAAE,WAAW,GAAG,qBAAqB,CAAC,GAC5E,OAAO,CAAC,0BAA0B,CAAC;IAqDtC;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IA2BtD;;;;;;;;;;;;;OAaG;IACG,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,gBAAgB,GAAG,cAAc,CAAC;IAWxG,OAAO,CAAC,mBAAmB;IAa3B,OAAO,CAAC,MAAM;IAMd;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAsBvB,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,IAAI;IAMZ;;;;;OAKG;IACH,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,gBAAgB;CAczB;AAoBD;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CA6BrD"}
|