@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.
- package/LICENSE +93 -0
- package/dist/asAgentTools.d.ts +12 -0
- package/dist/asAgentTools.d.ts.map +1 -0
- package/dist/asAgentTools.js +71 -0
- package/dist/asAgentTools.js.map +1 -0
- package/dist/auth.d.ts +25 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +97 -0
- package/dist/auth.js.map +1 -0
- package/dist/cache.d.ts +17 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +81 -0
- package/dist/cache.js.map +1 -0
- package/dist/config.d.ts +71 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +239 -0
- package/dist/config.js.map +1 -0
- package/dist/gatedAgentTools.d.ts +12 -0
- package/dist/gatedAgentTools.d.ts.map +1 -0
- package/dist/gatedAgentTools.js +36 -0
- package/dist/gatedAgentTools.js.map +1 -0
- package/dist/httpClient.d.ts +47 -0
- package/dist/httpClient.d.ts.map +1 -0
- package/dist/httpClient.js +196 -0
- package/dist/httpClient.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth/discovery.d.ts +55 -0
- package/dist/oauth/discovery.d.ts.map +1 -0
- package/dist/oauth/discovery.js +211 -0
- package/dist/oauth/discovery.js.map +1 -0
- package/dist/oauth/login.d.ts +41 -0
- package/dist/oauth/login.d.ts.map +1 -0
- package/dist/oauth/login.js +206 -0
- package/dist/oauth/login.js.map +1 -0
- package/dist/oauth/loopback.d.ts +58 -0
- package/dist/oauth/loopback.d.ts.map +1 -0
- package/dist/oauth/loopback.js +165 -0
- package/dist/oauth/loopback.js.map +1 -0
- package/dist/oauth/pkce.d.ts +45 -0
- package/dist/oauth/pkce.d.ts.map +1 -0
- package/dist/oauth/pkce.js +67 -0
- package/dist/oauth/pkce.js.map +1 -0
- package/dist/oauth/register.d.ts +34 -0
- package/dist/oauth/register.d.ts.map +1 -0
- package/dist/oauth/register.js +57 -0
- package/dist/oauth/register.js.map +1 -0
- package/dist/oauth/tokens.d.ts +62 -0
- package/dist/oauth/tokens.d.ts.map +1 -0
- package/dist/oauth/tokens.js +118 -0
- package/dist/oauth/tokens.js.map +1 -0
- package/dist/policy.d.ts +10 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +71 -0
- package/dist/policy.js.map +1 -0
- package/dist/registry.d.ts +124 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +265 -0
- package/dist/registry.js.map +1 -0
- package/dist/timedFetch.d.ts +10 -0
- package/dist/timedFetch.d.ts.map +1 -0
- package/dist/timedFetch.js +39 -0
- package/dist/timedFetch.js.map +1 -0
- package/dist/toolNames.d.ts +2 -0
- package/dist/toolNames.d.ts.map +1 -0
- package/dist/toolNames.js +14 -0
- package/dist/toolNames.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- 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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"}
|
package/dist/policy.d.ts
ADDED
|
@@ -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"}
|