@corenel/mcp 0.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.
Files changed (74) hide show
  1. package/LICENSE +93 -0
  2. package/dist/asAgentTools.d.ts +12 -0
  3. package/dist/asAgentTools.d.ts.map +1 -0
  4. package/dist/asAgentTools.js +71 -0
  5. package/dist/asAgentTools.js.map +1 -0
  6. package/dist/auth.d.ts +25 -0
  7. package/dist/auth.d.ts.map +1 -0
  8. package/dist/auth.js +97 -0
  9. package/dist/auth.js.map +1 -0
  10. package/dist/cache.d.ts +17 -0
  11. package/dist/cache.d.ts.map +1 -0
  12. package/dist/cache.js +81 -0
  13. package/dist/cache.js.map +1 -0
  14. package/dist/config.d.ts +71 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +239 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/gatedAgentTools.d.ts +12 -0
  19. package/dist/gatedAgentTools.d.ts.map +1 -0
  20. package/dist/gatedAgentTools.js +36 -0
  21. package/dist/gatedAgentTools.js.map +1 -0
  22. package/dist/httpClient.d.ts +47 -0
  23. package/dist/httpClient.d.ts.map +1 -0
  24. package/dist/httpClient.js +196 -0
  25. package/dist/httpClient.js.map +1 -0
  26. package/dist/index.d.ts +15 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +9 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/oauth/discovery.d.ts +55 -0
  31. package/dist/oauth/discovery.d.ts.map +1 -0
  32. package/dist/oauth/discovery.js +211 -0
  33. package/dist/oauth/discovery.js.map +1 -0
  34. package/dist/oauth/login.d.ts +41 -0
  35. package/dist/oauth/login.d.ts.map +1 -0
  36. package/dist/oauth/login.js +206 -0
  37. package/dist/oauth/login.js.map +1 -0
  38. package/dist/oauth/loopback.d.ts +58 -0
  39. package/dist/oauth/loopback.d.ts.map +1 -0
  40. package/dist/oauth/loopback.js +165 -0
  41. package/dist/oauth/loopback.js.map +1 -0
  42. package/dist/oauth/pkce.d.ts +45 -0
  43. package/dist/oauth/pkce.d.ts.map +1 -0
  44. package/dist/oauth/pkce.js +67 -0
  45. package/dist/oauth/pkce.js.map +1 -0
  46. package/dist/oauth/register.d.ts +34 -0
  47. package/dist/oauth/register.d.ts.map +1 -0
  48. package/dist/oauth/register.js +57 -0
  49. package/dist/oauth/register.js.map +1 -0
  50. package/dist/oauth/tokens.d.ts +62 -0
  51. package/dist/oauth/tokens.d.ts.map +1 -0
  52. package/dist/oauth/tokens.js +118 -0
  53. package/dist/oauth/tokens.js.map +1 -0
  54. package/dist/policy.d.ts +10 -0
  55. package/dist/policy.d.ts.map +1 -0
  56. package/dist/policy.js +71 -0
  57. package/dist/policy.js.map +1 -0
  58. package/dist/registry.d.ts +124 -0
  59. package/dist/registry.d.ts.map +1 -0
  60. package/dist/registry.js +265 -0
  61. package/dist/registry.js.map +1 -0
  62. package/dist/timedFetch.d.ts +10 -0
  63. package/dist/timedFetch.d.ts.map +1 -0
  64. package/dist/timedFetch.js +39 -0
  65. package/dist/timedFetch.js.map +1 -0
  66. package/dist/toolNames.d.ts +2 -0
  67. package/dist/toolNames.d.ts.map +1 -0
  68. package/dist/toolNames.js +14 -0
  69. package/dist/toolNames.js.map +1 -0
  70. package/dist/types.d.ts +62 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +19 -0
  73. package/dist/types.js.map +1 -0
  74. package/package.json +44 -0
@@ -0,0 +1,165 @@
1
+ /* THE LOOPBACK REDIRECT LISTENER -- the one-shot HTTP server that catches an
2
+ * OAuth authorization code on 127.0.0.1 and nowhere else.
3
+ *
4
+ * Mirrors the shape of auth/login.ts's `awaitCallback` (the sidecar's OWN
5
+ * sign-in flow already solved this problem once) but narrower on purpose:
6
+ * this module answers only "wait for one redirect, checking state, then
7
+ * stop". Picking a port, building the authorization URL and opening a
8
+ * browser are the login command's job (config/mcpCmd.ts), not this module's.
9
+ *
10
+ * THREE SECURITY PROPERTIES THIS FILE OWNS -- losing any one of them is a
11
+ * real vulnerability, not a style nit:
12
+ *
13
+ * 1. Binds 127.0.0.1 ONLY, never 0.0.0.0. A device elsewhere on the LAN
14
+ * (or another user on a shared machine, on some platforms) must not be
15
+ * able to deliver a code to this listener.
16
+ * 2. `state` is checked BEFORE the code is even read out of the redirect --
17
+ * a mismatched state means this request did not come from the browser
18
+ * flow that started this login, and its code must never be exchanged.
19
+ * 3. The server closes after exactly one redirect, whatever the outcome.
20
+ * It never sits open waiting for a second attempt.
21
+ */
22
+ import { createServer } from 'node:http';
23
+ const PAGE_OK = `<!doctype html><meta charset="utf-8"><title>Signed in</title>
24
+ <body style="font:16px system-ui;padding:3rem;text-align:center">
25
+ <h1>Signed in</h1><p>You can close this tab and return to your terminal.</p></body>`;
26
+ const PAGE_FAIL = `<!doctype html><meta charset="utf-8"><title>Sign-in failed</title>
27
+ <body style="font:16px system-ui;padding:3rem;text-align:center">
28
+ <h1>Sign-in failed</h1><p>Return to your terminal for details.</p></body>`;
29
+ /** Five minutes -- long enough to sign in and clear an MFA challenge, short
30
+ * enough that a forgotten terminal does not hold a port open all day. */
31
+ const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
32
+ /**
33
+ * Listen on 127.0.0.1:`port` for exactly one OAuth redirect, and resolve
34
+ * with its authorization code -- or reject. Either way the listener closes
35
+ * before the returned promise settles.
36
+ *
37
+ * `expectedState` must equal the redirect's `state` query parameter or the
38
+ * request is refused and the promise rejects, with the `code` parameter
39
+ * never read into a variable that could be exchanged. Without this check the
40
+ * listener would accept a code from anyone who can reach the port, which on
41
+ * a shared machine is everyone with a login shell.
42
+ */
43
+ export function awaitRedirect(port, expectedState, opts = {}) {
44
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
45
+ return new Promise((resolve, reject) => {
46
+ let settled = false;
47
+ /* Declared up here because `finish` closes it. See the listen call below for
48
+ * why there are two servers. */
49
+ const v6 = createServer((req, res) => onRequest(req, res));
50
+ const finish = (fn) => {
51
+ if (settled)
52
+ return;
53
+ settled = true;
54
+ clearTimeout(timer);
55
+ // Stop listening after exactly one redirect, success or failure -- this
56
+ // server exists for one exchange and must never wait around for a
57
+ // second code. Both loopback listeners close: whichever one the browser
58
+ // reached, the other is still holding the port.
59
+ server.close();
60
+ v6.close();
61
+ fn();
62
+ };
63
+ const onRequest = (req, res) => {
64
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1');
65
+ const state = url.searchParams.get('state');
66
+ const err = url.searchParams.get('error');
67
+ const code = url.searchParams.get('code');
68
+ // State check FIRST, before the code is even looked at.
69
+ if (state !== expectedState) {
70
+ res.writeHead(400, { 'content-type': 'text/html' }).end(PAGE_FAIL);
71
+ finish(() => reject(new Error('redirect state did not match the request that started this sign-in')));
72
+ return;
73
+ }
74
+ if (err) {
75
+ res.writeHead(400, { 'content-type': 'text/html' }).end(PAGE_FAIL);
76
+ finish(() => reject(new Error(`authorization failed: ${err}`)));
77
+ return;
78
+ }
79
+ if (!code) {
80
+ res.writeHead(400, { 'content-type': 'text/html' }).end(PAGE_FAIL);
81
+ finish(() => reject(new Error('redirect carried no authorization code')));
82
+ return;
83
+ }
84
+ res.writeHead(200, { 'content-type': 'text/html' }).end(PAGE_OK);
85
+ finish(() => resolve({ code }));
86
+ };
87
+ const server = createServer(onRequest);
88
+ const timer = setTimeout(() => {
89
+ finish(() => reject(new Error('timed out waiting for the browser redirect')));
90
+ }, timeoutMs);
91
+ timer.unref?.();
92
+ server.once('error', (e) => {
93
+ finish(() => reject(e instanceof Error ? e : new Error(String(e))));
94
+ });
95
+ if (opts.signal) {
96
+ if (opts.signal.aborted) {
97
+ finish(() => reject(new Error('sign-in cancelled')));
98
+ }
99
+ else {
100
+ opts.signal.addEventListener('abort', () => finish(() => reject(new Error('sign-in cancelled'))), { once: true });
101
+ }
102
+ }
103
+ /* BOTH LOOPBACK ADDRESSES, because the redirect now names `localhost` and
104
+ * the browser decides what that resolves to. On Windows it commonly tries
105
+ * ::1 first; binding only 127.0.0.1 would leave the callback failing on
106
+ * exactly the machines this is most used on. Two servers on one port is
107
+ * legal because they hold different addresses, and both are loopback -- the
108
+ * module's promise ("127.0.0.1, never 0.0.0.0") is about not being
109
+ * reachable from the network, which ::1 equally is not.
110
+ *
111
+ * The v6 bind is best-effort: a host with IPv6 disabled fails it, and that
112
+ * is fine -- the v4 listener is the one that must work, and its error is
113
+ * still fatal. */
114
+ v6.on('error', () => { });
115
+ try {
116
+ v6.listen(port, '::1');
117
+ }
118
+ catch { /* same */ }
119
+ // 127.0.0.1 explicitly -- never 0.0.0.0 -- see module comment.
120
+ server.listen(port, '127.0.0.1', () => {
121
+ const addr = server.address();
122
+ opts.onListening?.(addr.address, addr.port);
123
+ });
124
+ });
125
+ }
126
+ /**
127
+ * A free ephemeral port on 127.0.0.1, for a caller to build a `redirect_uri`
128
+ * from before starting `awaitRedirect` on the same port -- the authorization
129
+ * URL needs the real port before the browser opens.
130
+ *
131
+ * There is an inherent, unavoidable race between this probe closing and the
132
+ * listener re-binding the same port: any "find a free port, then use it"
133
+ * strategy on a shared loopback interface has it. Acceptable here because
134
+ * this is a local, single-user, one-shot login flow, not a long-lived
135
+ * service placing a hard dependency on the exact port.
136
+ */
137
+ /**
138
+ * Whether `port` can be bound on 127.0.0.1 right now.
139
+ *
140
+ * Only meaningful for a PINNED port -- one an operator registered with a
141
+ * service, or one a previous registration named. An ephemeral port comes from
142
+ * `getFreePort` and is free by construction. The same inherent race as
143
+ * `getFreePort` applies and is acceptable for the same reason: this answers
144
+ * "is something already sitting on it", not "reserve it for me".
145
+ */
146
+ export function portFree(port) {
147
+ return new Promise((resolve) => {
148
+ const probe = createServer();
149
+ probe.once('error', () => resolve(false));
150
+ probe.listen(port, '127.0.0.1', () => {
151
+ probe.close(() => resolve(true));
152
+ });
153
+ });
154
+ }
155
+ export function getFreePort() {
156
+ return new Promise((resolve, reject) => {
157
+ const probe = createServer();
158
+ probe.once('error', reject);
159
+ probe.listen(0, '127.0.0.1', () => {
160
+ const addr = probe.address();
161
+ probe.close(() => resolve(addr.port));
162
+ });
163
+ });
164
+ }
165
+ //# sourceMappingURL=loopback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopback.js","sourceRoot":"","sources":["../../src/oauth/loopback.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ import type { AuthMetadata } from './discovery.ts';
2
+ export interface Pkce {
3
+ verifier: string;
4
+ challenge: string;
5
+ }
6
+ /**
7
+ * Generate a fresh PKCE verifier/challenge pair.
8
+ *
9
+ * The verifier is 32 bytes (256 bits) from `node:crypto`'s `randomBytes` --
10
+ * NOT `Math.random()`, which is not cryptographically secure and would make
11
+ * the verifier guessable, defeating PKCE's entire purpose. The challenge is
12
+ * BASE64URL(SHA256(verifier)); RFC 7636 also allows a `plain` method (the
13
+ * challenge equals the verifier) but that provides no protection against an
14
+ * intercepted authorization request and is deliberately not offered here.
15
+ *
16
+ * Must be called fresh for every authorization attempt -- a cached or
17
+ * module-level verifier would let one stolen/replayed code authorize a
18
+ * second, unrelated flow.
19
+ */
20
+ export declare function makePkce(): Pkce;
21
+ /** A fresh, per-request opaque value for CSRF protection on the redirect.
22
+ * Same freshness requirement as the verifier: generated per call, never
23
+ * cached. */
24
+ export declare function makeState(): string;
25
+ export interface AuthorizationUrlOptions {
26
+ clientId: string;
27
+ redirectUri: string;
28
+ codeChallenge: string;
29
+ state: string;
30
+ /** RFC 8707 resource indicator -- the MCP server URL this token must be
31
+ * scoped to. Without it, a token minted for one MCP server that trusts
32
+ * this authorization server can be replayed against another. */
33
+ resource: string;
34
+ }
35
+ /**
36
+ * Build the authorization request URL for the user's browser.
37
+ *
38
+ * Always includes `response_type=code`, `client_id`, `redirect_uri`,
39
+ * `code_challenge`/`code_challenge_method=S256`, `state`, and `resource`
40
+ * (RFC 8707). `scope` is added only when the authorization server declared
41
+ * `scopesSupported` -- asking for scopes a server never advertised is at
42
+ * best ignored and at worst rejected as invalid.
43
+ */
44
+ export declare function authorizationUrl(meta: AuthMetadata, opts: AuthorizationUrlOptions): string;
45
+ //# sourceMappingURL=pkce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../../src/oauth/pkce.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAUnD,MAAM,WAAW,IAAI;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,QAAQ,IAAI,IAAI,CAI/B;AAED;;cAEc;AACd,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd;;qEAEiE;IACjE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,uBAAuB,GAAG,MAAM,CAa1F"}
@@ -0,0 +1,67 @@
1
+ /* PKCE (RFC 7636) AND THE AUTHORIZATION REQUEST URL (RFC 8707 resource
2
+ * indicators included).
3
+ *
4
+ * This module does not talk to the network -- it only produces the
5
+ * verifier/challenge pair and builds the URL the browser is sent to. The
6
+ * loopback listener that receives the redirect and the token exchange that
7
+ * spends the verifier are later tasks (Task 5); this module's job ends at
8
+ * handing back a URL and a verifier the caller must hold onto until the
9
+ * redirect comes back.
10
+ */
11
+ import { randomBytes, createHash } from 'node:crypto';
12
+ /** Base64url per RFC 4648 section 5: `+`/`/` swapped for `-`/`_`, and the
13
+ * `=` padding dropped entirely. A standard base64 encode (or padded
14
+ * base64url) is a DIFFERENT string that most authorization servers will
15
+ * reject outright rather than silently accept. */
16
+ function base64url(bytes) {
17
+ return bytes.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
18
+ }
19
+ /**
20
+ * Generate a fresh PKCE verifier/challenge pair.
21
+ *
22
+ * The verifier is 32 bytes (256 bits) from `node:crypto`'s `randomBytes` --
23
+ * NOT `Math.random()`, which is not cryptographically secure and would make
24
+ * the verifier guessable, defeating PKCE's entire purpose. The challenge is
25
+ * BASE64URL(SHA256(verifier)); RFC 7636 also allows a `plain` method (the
26
+ * challenge equals the verifier) but that provides no protection against an
27
+ * intercepted authorization request and is deliberately not offered here.
28
+ *
29
+ * Must be called fresh for every authorization attempt -- a cached or
30
+ * module-level verifier would let one stolen/replayed code authorize a
31
+ * second, unrelated flow.
32
+ */
33
+ export function makePkce() {
34
+ const verifier = base64url(randomBytes(32));
35
+ const challenge = base64url(createHash('sha256').update(verifier).digest());
36
+ return { verifier, challenge };
37
+ }
38
+ /** A fresh, per-request opaque value for CSRF protection on the redirect.
39
+ * Same freshness requirement as the verifier: generated per call, never
40
+ * cached. */
41
+ export function makeState() {
42
+ return base64url(randomBytes(16));
43
+ }
44
+ /**
45
+ * Build the authorization request URL for the user's browser.
46
+ *
47
+ * Always includes `response_type=code`, `client_id`, `redirect_uri`,
48
+ * `code_challenge`/`code_challenge_method=S256`, `state`, and `resource`
49
+ * (RFC 8707). `scope` is added only when the authorization server declared
50
+ * `scopesSupported` -- asking for scopes a server never advertised is at
51
+ * best ignored and at worst rejected as invalid.
52
+ */
53
+ export function authorizationUrl(meta, opts) {
54
+ const url = new URL(meta.authorizationEndpoint);
55
+ url.searchParams.set('response_type', 'code');
56
+ url.searchParams.set('client_id', opts.clientId);
57
+ url.searchParams.set('redirect_uri', opts.redirectUri);
58
+ url.searchParams.set('code_challenge', opts.codeChallenge);
59
+ url.searchParams.set('code_challenge_method', 'S256');
60
+ url.searchParams.set('state', opts.state);
61
+ url.searchParams.set('resource', opts.resource);
62
+ if (meta.scopesSupported && meta.scopesSupported.length > 0) {
63
+ url.searchParams.set('scope', meta.scopesSupported.join(' '));
64
+ }
65
+ return url.toString();
66
+ }
67
+ //# sourceMappingURL=pkce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../src/oauth/pkce.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAItD;;;mDAGmD;AACnD,SAAS,SAAS,CAAC,KAAa;IAC9B,OAAO,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAC7F,CAAC;AAOD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,QAAQ;IACtB,MAAM,QAAQ,GAAG,SAAS,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5C,MAAM,SAAS,GAAG,SAAS,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5E,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AACjC,CAAC;AAED;;cAEc;AACd,MAAM,UAAU,SAAS;IACvB,OAAO,SAAS,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC;AACpC,CAAC;AAaD;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAkB,EAAE,IAA6B;IAChF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;IAChD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;IAC9C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IACjD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,cAAc,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IACvD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,gBAAgB,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;IAC3D,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,uBAAuB,EAAE,MAAM,CAAC,CAAC;IACtD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChD,IAAI,IAAI,CAAC,eAAe,IAAI,IAAI,CAAC,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5D,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;AACxB,CAAC"}
@@ -0,0 +1,34 @@
1
+ import type { AuthMetadata } from './discovery.ts';
2
+ /** Shape of a fetch Response this module actually uses. */
3
+ type MinimalResponse = {
4
+ ok: boolean;
5
+ status: number;
6
+ json(): Promise<unknown>;
7
+ };
8
+ type FetchLike = (url: string, init: {
9
+ method: string;
10
+ headers: Record<string, string>;
11
+ body: string;
12
+ }) => Promise<MinimalResponse>;
13
+ export interface RegisteredClient {
14
+ clientId: string;
15
+ clientSecret?: string;
16
+ }
17
+ /**
18
+ * Register a client with the authorization server's dynamic client
19
+ * registration endpoint, if it has one.
20
+ *
21
+ * Returns `null` -- not a rejected promise -- when `meta.registrationEndpoint`
22
+ * is absent, because that is a normal, supported configuration (the server
23
+ * expects a pre-registered client id) rather than a failure. Callers must
24
+ * fall back to a configured client id in that case.
25
+ *
26
+ * `grant_types`/`response_types` are fixed to the authorization-code +
27
+ * refresh-token flow this sidecar actually uses; `token_endpoint_auth_method`
28
+ * is `none` because this is a public client (no client secret can be kept
29
+ * confidential in a local process) using PKCE for proof of possession
30
+ * instead.
31
+ */
32
+ export declare function registerClient(meta: AuthMetadata, redirectUri: string, fetchImpl: FetchLike): Promise<RegisteredClient | null>;
33
+ export {};
34
+ //# sourceMappingURL=register.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../src/oauth/register.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,2DAA2D;AAC3D,KAAK,eAAe,GAAG;IACrB,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1B,CAAC;AAEF,KAAK,SAAS,GAAG,CACf,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,KACpE,OAAO,CAAC,eAAe,CAAC,CAAC;AAE9B,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,YAAY,EAClB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,SAAS,GACnB,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAgClC"}
@@ -0,0 +1,57 @@
1
+ /* DYNAMIC CLIENT REGISTRATION (RFC 7591).
2
+ *
3
+ * `registration_endpoint` is OPTIONAL in an authorization server's metadata.
4
+ * A server without one expects the caller to already hold a pre-registered
5
+ * client id (configured out of band). Registration must therefore never be
6
+ * the reason a login against such a server fails -- the caller decides what
7
+ * "no endpoint" means (skip and use a configured client id), this module
8
+ * only performs the registration when there is somewhere to send it.
9
+ *
10
+ * `fetch` is injected (no default) so tests never open a socket -- same
11
+ * pattern as discovery.ts.
12
+ */
13
+ /**
14
+ * Register a client with the authorization server's dynamic client
15
+ * registration endpoint, if it has one.
16
+ *
17
+ * Returns `null` -- not a rejected promise -- when `meta.registrationEndpoint`
18
+ * is absent, because that is a normal, supported configuration (the server
19
+ * expects a pre-registered client id) rather than a failure. Callers must
20
+ * fall back to a configured client id in that case.
21
+ *
22
+ * `grant_types`/`response_types` are fixed to the authorization-code +
23
+ * refresh-token flow this sidecar actually uses; `token_endpoint_auth_method`
24
+ * is `none` because this is a public client (no client secret can be kept
25
+ * confidential in a local process) using PKCE for proof of possession
26
+ * instead.
27
+ */
28
+ export async function registerClient(meta, redirectUri, fetchImpl) {
29
+ if (!meta.registrationEndpoint)
30
+ return null;
31
+ const res = await fetchImpl(meta.registrationEndpoint, {
32
+ method: 'POST',
33
+ headers: { 'content-type': 'application/json' },
34
+ body: JSON.stringify({
35
+ client_name: 'Corenel Sidecar',
36
+ redirect_uris: [redirectUri],
37
+ grant_types: ['authorization_code', 'refresh_token'],
38
+ response_types: ['code'],
39
+ token_endpoint_auth_method: 'none',
40
+ }),
41
+ });
42
+ if (!res.ok) {
43
+ throw new Error(`Client registration failed at ${meta.registrationEndpoint} (HTTP ${res.status})`);
44
+ }
45
+ const body = await res.json();
46
+ if (typeof body !== 'object' || body === null || Array.isArray(body)) {
47
+ throw new Error(`Client registration at ${meta.registrationEndpoint} returned a malformed response`);
48
+ }
49
+ const record = body;
50
+ const clientId = record.client_id;
51
+ if (typeof clientId !== 'string' || clientId.length === 0) {
52
+ throw new Error(`Client registration at ${meta.registrationEndpoint} did not return a client_id`);
53
+ }
54
+ const clientSecret = typeof record.client_secret === 'string' ? record.client_secret : undefined;
55
+ return { clientId, clientSecret };
56
+ }
57
+ //# sourceMappingURL=register.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../../src/oauth/register.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAqBH;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,IAAkB,EAClB,WAAmB,EACnB,SAAoB;IAEpB,IAAI,CAAC,IAAI,CAAC,oBAAoB;QAAE,OAAO,IAAI,CAAC;IAE5C,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,oBAAoB,EAAE;QACrD,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,WAAW,EAAE,iBAAiB;YAC9B,aAAa,EAAE,CAAC,WAAW,CAAC;YAC5B,WAAW,EAAE,CAAC,oBAAoB,EAAE,eAAe,CAAC;YACpD,cAAc,EAAE,CAAC,MAAM,CAAC;YACxB,0BAA0B,EAAE,MAAM;SACnC,CAAC;KACH,CAAC,CAAC;IAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,iCAAiC,IAAI,CAAC,oBAAoB,UAAU,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC;IACrG,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,KAAK,CAAC,0BAA0B,IAAI,CAAC,oBAAoB,gCAAgC,CAAC,CAAC;IACvG,CAAC;IACD,MAAM,MAAM,GAAG,IAA+B,CAAC;IAE/C,MAAM,QAAQ,GAAG,MAAM,CAAC,SAAS,CAAC;IAClC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,KAAK,CAAC,0BAA0B,IAAI,CAAC,oBAAoB,6BAA6B,CAAC,CAAC;IACpG,CAAC;IAED,MAAM,YAAY,GAAG,OAAO,MAAM,CAAC,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC;IACjG,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;AACpC,CAAC"}
@@ -0,0 +1,62 @@
1
+ import type { AuthMetadata } from './discovery.ts';
2
+ import type { McpOAuthCredential } from '../types.ts';
3
+ /** Shape of a fetch Response this module actually reads from. */
4
+ type MinimalResponse = {
5
+ ok: boolean;
6
+ status: number;
7
+ json(): Promise<unknown>;
8
+ };
9
+ type FetchLike = (url: string, init: {
10
+ method: string;
11
+ headers: Record<string, string>;
12
+ body: string;
13
+ }) => Promise<MinimalResponse>;
14
+ export interface ExchangeOptions {
15
+ clientId: string;
16
+ clientSecret?: string;
17
+ code: string;
18
+ verifier: string;
19
+ redirectUri: string;
20
+ /** RFC 8707 resource indicator -- the MCP server URL this token must be
21
+ * scoped to, matching the one sent in the authorization request. */
22
+ resource: string;
23
+ authServer: string;
24
+ }
25
+ /**
26
+ * Exchange an authorization code (and its PKCE verifier) for a fresh
27
+ * `McpOAuthCredential`.
28
+ *
29
+ * `expiresAt` is computed as an ABSOLUTE epoch ms from the response's
30
+ * relative `expires_in`, using the injected clock -- absent when the server
31
+ * did not send `expires_in`, matching McpOAuthCredential's documented
32
+ * convention (absent means "did not say", not "does not expire").
33
+ */
34
+ export declare function exchangeCode(meta: AuthMetadata, opts: ExchangeOptions, fetchImpl?: FetchLike, now?: () => number): Promise<McpOAuthCredential>;
35
+ /**
36
+ * Refresh an OAuth credential against its own authorization server.
37
+ *
38
+ * Throws if `cred` carries no `refreshToken` -- there is nothing to spend,
39
+ * and the caller (mcp/auth.ts) must send the user back through `login`
40
+ * rather than treat a thrown refresh as some other kind of failure.
41
+ *
42
+ * When the response omits a new `refresh_token`, the OLD one is kept.
43
+ * Rotation is optional per RFC 6749 section 6; a server that does not rotate
44
+ * simply does not send one, and treating that as "no refresh token anymore"
45
+ * would strand the credential at the NEXT refresh -- silently, since this
46
+ * one still succeeds.
47
+ */
48
+ export declare function refresh(meta: AuthMetadata, cred: McpOAuthCredential, fetchImpl?: FetchLike, now?: () => number): Promise<McpOAuthCredential>;
49
+ /**
50
+ * True when `cred` is expired, or will expire within the next
51
+ * `EXPIRY_SKEW_MS` -- treated as expired now so a caller refreshes ahead of
52
+ * the deadline rather than racing it.
53
+ *
54
+ * No `expiresAt` means the issuer did not say, which is NOT the same as
55
+ * "does not expire" -- but assuming expired in that case would force a
56
+ * refresh before every single call, so absent is treated as valid.
57
+ */
58
+ export declare function isExpired(cred: {
59
+ expiresAt?: number;
60
+ }, nowMs: number): boolean;
61
+ export {};
62
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/oauth/tokens.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,iEAAiE;AACjE,KAAK,eAAe,GAAG;IACrB,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1B,CAAC;AAEF,KAAK,SAAS,GAAG,CACf,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,KACpE,OAAO,CAAC,eAAe,CAAC,CAAC;AA4C9B,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB;yEACqE;IACrE,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,YAAY,EAClB,IAAI,EAAE,eAAe,EACrB,SAAS,GAAE,SAAyC,EACpD,GAAG,GAAE,MAAM,MAAiB,GAC3B,OAAO,CAAC,kBAAkB,CAAC,CAuB7B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,OAAO,CAC3B,IAAI,EAAE,YAAY,EAClB,IAAI,EAAE,kBAAkB,EACxB,SAAS,GAAE,SAAyC,EACpD,GAAG,GAAE,MAAM,MAAiB,GAC3B,OAAO,CAAC,kBAAkB,CAAC,CAyB7B;AAMD;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAG9E"}
@@ -0,0 +1,118 @@
1
+ function parseTokenResponse(body) {
2
+ if (typeof body !== 'object' || body === null || Array.isArray(body)) {
3
+ throw new Error('token endpoint returned a malformed response');
4
+ }
5
+ const r = body;
6
+ if (typeof r.access_token !== 'string' || r.access_token.length === 0) {
7
+ // Never echo the rest of the body: on a misconfigured server it can carry
8
+ // another caller's data, and this lands in an error a terminal may log.
9
+ throw new Error('token endpoint response did not include an access_token');
10
+ }
11
+ return {
12
+ access_token: r.access_token,
13
+ ...(typeof r.refresh_token === 'string' ? { refresh_token: r.refresh_token } : {}),
14
+ ...(typeof r.expires_in === 'number' ? { expires_in: r.expires_in } : {}),
15
+ ...(typeof r.scope === 'string' ? { scope: r.scope } : {}),
16
+ };
17
+ }
18
+ /** POST `body` to `endpoint` and parse a token response, or throw naming the
19
+ * endpoint and HTTP status -- never the response body, which on a token
20
+ * endpoint can carry a code, a token, or another caller's data. */
21
+ async function postToken(endpoint, body, fetchImpl) {
22
+ const res = await fetchImpl(endpoint, {
23
+ method: 'POST',
24
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
25
+ body: body.toString(),
26
+ });
27
+ if (!res.ok) {
28
+ throw new Error(`token request failed at ${endpoint} (HTTP ${res.status})`);
29
+ }
30
+ return parseTokenResponse(await res.json());
31
+ }
32
+ /**
33
+ * Exchange an authorization code (and its PKCE verifier) for a fresh
34
+ * `McpOAuthCredential`.
35
+ *
36
+ * `expiresAt` is computed as an ABSOLUTE epoch ms from the response's
37
+ * relative `expires_in`, using the injected clock -- absent when the server
38
+ * did not send `expires_in`, matching McpOAuthCredential's documented
39
+ * convention (absent means "did not say", not "does not expire").
40
+ */
41
+ export async function exchangeCode(meta, opts, fetchImpl = fetch, now = Date.now) {
42
+ const body = new URLSearchParams({
43
+ grant_type: 'authorization_code',
44
+ client_id: opts.clientId,
45
+ code: opts.code,
46
+ code_verifier: opts.verifier,
47
+ redirect_uri: opts.redirectUri,
48
+ resource: opts.resource,
49
+ });
50
+ if (opts.clientSecret)
51
+ body.set('client_secret', opts.clientSecret);
52
+ const tokens = await postToken(meta.tokenEndpoint, body, fetchImpl);
53
+ return {
54
+ kind: 'oauth',
55
+ accessToken: tokens.access_token,
56
+ ...(tokens.refresh_token ? { refreshToken: tokens.refresh_token } : {}),
57
+ ...(typeof tokens.expires_in === 'number' ? { expiresAt: now() + tokens.expires_in * 1000 } : {}),
58
+ clientId: opts.clientId,
59
+ ...(opts.clientSecret ? { clientSecret: opts.clientSecret } : {}),
60
+ authServer: opts.authServer,
61
+ ...(tokens.scope ? { scope: tokens.scope } : {}),
62
+ };
63
+ }
64
+ /**
65
+ * Refresh an OAuth credential against its own authorization server.
66
+ *
67
+ * Throws if `cred` carries no `refreshToken` -- there is nothing to spend,
68
+ * and the caller (mcp/auth.ts) must send the user back through `login`
69
+ * rather than treat a thrown refresh as some other kind of failure.
70
+ *
71
+ * When the response omits a new `refresh_token`, the OLD one is kept.
72
+ * Rotation is optional per RFC 6749 section 6; a server that does not rotate
73
+ * simply does not send one, and treating that as "no refresh token anymore"
74
+ * would strand the credential at the NEXT refresh -- silently, since this
75
+ * one still succeeds.
76
+ */
77
+ export async function refresh(meta, cred, fetchImpl = fetch, now = Date.now) {
78
+ if (!cred.refreshToken) {
79
+ throw new Error('credential has no refresh token to refresh with -- sign in again');
80
+ }
81
+ const body = new URLSearchParams({
82
+ grant_type: 'refresh_token',
83
+ refresh_token: cred.refreshToken,
84
+ client_id: cred.clientId,
85
+ });
86
+ if (cred.clientSecret)
87
+ body.set('client_secret', cred.clientSecret);
88
+ const tokens = await postToken(meta.tokenEndpoint, body, fetchImpl);
89
+ return {
90
+ kind: 'oauth',
91
+ accessToken: tokens.access_token,
92
+ // Keep the old refresh token when the server did not send a new one.
93
+ refreshToken: tokens.refresh_token ?? cred.refreshToken,
94
+ ...(typeof tokens.expires_in === 'number' ? { expiresAt: now() + tokens.expires_in * 1000 } : {}),
95
+ clientId: cred.clientId,
96
+ ...(cred.clientSecret ? { clientSecret: cred.clientSecret } : {}),
97
+ authServer: cred.authServer,
98
+ scope: tokens.scope ?? cred.scope,
99
+ };
100
+ }
101
+ /** 60 seconds. A call already in flight when a token expires must not 401
102
+ * because the check ran a moment too late. */
103
+ const EXPIRY_SKEW_MS = 60_000;
104
+ /**
105
+ * True when `cred` is expired, or will expire within the next
106
+ * `EXPIRY_SKEW_MS` -- treated as expired now so a caller refreshes ahead of
107
+ * the deadline rather than racing it.
108
+ *
109
+ * No `expiresAt` means the issuer did not say, which is NOT the same as
110
+ * "does not expire" -- but assuming expired in that case would force a
111
+ * refresh before every single call, so absent is treated as valid.
112
+ */
113
+ export function isExpired(cred, nowMs) {
114
+ if (cred.expiresAt === undefined)
115
+ return false;
116
+ return cred.expiresAt - nowMs <= EXPIRY_SKEW_MS;
117
+ }
118
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/oauth/tokens.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { Policy } from '@corenel/protocol';
2
+ export type McpCallDecision = {
3
+ ok: true;
4
+ } | {
5
+ ok: false;
6
+ reason: string;
7
+ };
8
+ /** Deny-by-default gate for one `mcp:<server>/<tool>` address. */
9
+ export declare function mcpCallAllowed(address: string, policy: Policy): McpCallDecision;
10
+ //# sourceMappingURL=policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAkDA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD,MAAM,MAAM,eAAe,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3E,kEAAkE;AAClE,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,eAAe,CAoB/E"}