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.
Files changed (92) hide show
  1. package/README.md +26 -26
  2. package/dist/adt/config.d.ts +1 -1
  3. package/dist/adt/config.d.ts.map +1 -1
  4. package/dist/adt/http.d.ts +1 -1
  5. package/dist/adt/http.d.ts.map +1 -1
  6. package/dist/adt/http.js.map +1 -1
  7. package/dist/authz/policy.d.ts +6 -0
  8. package/dist/authz/policy.d.ts.map +1 -1
  9. package/dist/authz/policy.js +20 -0
  10. package/dist/authz/policy.js.map +1 -1
  11. package/dist/cli.js +21 -3
  12. package/dist/cli.js.map +1 -1
  13. package/dist/handlers/dispatch.d.ts +3 -0
  14. package/dist/handlers/dispatch.d.ts.map +1 -1
  15. package/dist/handlers/dispatch.js +71 -53
  16. package/dist/handlers/dispatch.js.map +1 -1
  17. package/dist/handlers/schemas.d.ts +4 -4
  18. package/dist/plugins/manifest-interpreter.d.ts +25 -0
  19. package/dist/plugins/manifest-interpreter.d.ts.map +1 -0
  20. package/dist/plugins/manifest-interpreter.js +124 -0
  21. package/dist/plugins/manifest-interpreter.js.map +1 -0
  22. package/dist/public/define-tool.d.ts +9 -0
  23. package/dist/public/define-tool.d.ts.map +1 -0
  24. package/dist/public/define-tool.js +25 -0
  25. package/dist/public/define-tool.js.map +1 -0
  26. package/dist/public/index.d.ts +9 -0
  27. package/dist/public/index.d.ts.map +1 -0
  28. package/dist/public/index.js +10 -0
  29. package/dist/public/index.js.map +1 -0
  30. package/dist/public/testing.d.ts +26 -0
  31. package/dist/public/testing.d.ts.map +1 -0
  32. package/dist/public/testing.js +39 -0
  33. package/dist/public/testing.js.map +1 -0
  34. package/dist/public/types.d.ts +87 -0
  35. package/dist/public/types.d.ts.map +1 -0
  36. package/dist/public/types.js +4 -0
  37. package/dist/public/types.js.map +1 -0
  38. package/dist/registry/tool-registry.d.ts +74 -0
  39. package/dist/registry/tool-registry.d.ts.map +1 -0
  40. package/dist/registry/tool-registry.js +59 -0
  41. package/dist/registry/tool-registry.js.map +1 -0
  42. package/dist/server/app-url.d.ts +31 -0
  43. package/dist/server/app-url.d.ts.map +1 -0
  44. package/dist/server/app-url.js +50 -0
  45. package/dist/server/app-url.js.map +1 -0
  46. package/dist/server/audit.d.ts +4 -0
  47. package/dist/server/audit.d.ts.map +1 -1
  48. package/dist/server/audit.js.map +1 -1
  49. package/dist/server/config.d.ts.map +1 -1
  50. package/dist/server/config.js +19 -0
  51. package/dist/server/config.js.map +1 -1
  52. package/dist/server/http.d.ts +15 -46
  53. package/dist/server/http.d.ts.map +1 -1
  54. package/dist/server/http.js +105 -375
  55. package/dist/server/http.js.map +1 -1
  56. package/dist/server/logger.d.ts +22 -0
  57. package/dist/server/logger.d.ts.map +1 -1
  58. package/dist/server/logger.js +22 -0
  59. package/dist/server/logger.js.map +1 -1
  60. package/dist/server/plugin-loader.d.ts +19 -0
  61. package/dist/server/plugin-loader.d.ts.map +1 -0
  62. package/dist/server/plugin-loader.js +162 -0
  63. package/dist/server/plugin-loader.js.map +1 -0
  64. package/dist/server/safe-http-client.d.ts +38 -0
  65. package/dist/server/safe-http-client.d.ts.map +1 -0
  66. package/dist/server/safe-http-client.js +129 -0
  67. package/dist/server/safe-http-client.js.map +1 -0
  68. package/dist/server/server.d.ts +2 -2
  69. package/dist/server/server.d.ts.map +1 -1
  70. package/dist/server/server.js +36 -7
  71. package/dist/server/server.js.map +1 -1
  72. package/dist/server/types.d.ts +8 -0
  73. package/dist/server/types.d.ts.map +1 -1
  74. package/dist/server/types.js +2 -0
  75. package/dist/server/types.js.map +1 -1
  76. package/package.json +24 -8
  77. package/dist/adt/btp.d.ts +0 -140
  78. package/dist/adt/btp.d.ts.map +0 -1
  79. package/dist/adt/btp.js +0 -427
  80. package/dist/adt/btp.js.map +0 -1
  81. package/dist/server/oauth-state.d.ts +0 -92
  82. package/dist/server/oauth-state.d.ts.map +0 -1
  83. package/dist/server/oauth-state.js +0 -163
  84. package/dist/server/oauth-state.js.map +0 -1
  85. package/dist/server/stateless-client-store.d.ts +0 -173
  86. package/dist/server/stateless-client-store.d.ts.map +0 -1
  87. package/dist/server/stateless-client-store.js +0 -503
  88. package/dist/server/stateless-client-store.js.map +0 -1
  89. package/dist/server/xsuaa.d.ts +0 -188
  90. package/dist/server/xsuaa.d.ts.map +0 -1
  91. package/dist/server/xsuaa.js +0 -464
  92. 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":"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"}
@@ -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"}