@dain-os/mcp-server 0.11.0 → 0.13.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/dist/http-util.d.ts +24 -0
- package/dist/http-util.d.ts.map +1 -0
- package/dist/http-util.js +113 -0
- package/dist/http-util.js.map +1 -0
- package/dist/http.d.ts +5 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +92 -32
- package/dist/http.js.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/meta.d.ts +27 -0
- package/dist/meta.d.ts.map +1 -0
- package/dist/meta.js +81 -0
- package/dist/meta.js.map +1 -0
- package/dist/oauth/config.d.ts +57 -0
- package/dist/oauth/config.d.ts.map +1 -0
- package/dist/oauth/config.js +74 -0
- package/dist/oauth/config.js.map +1 -0
- package/dist/oauth/consent-page.d.ts +24 -0
- package/dist/oauth/consent-page.d.ts.map +1 -0
- package/dist/oauth/consent-page.js +98 -0
- package/dist/oauth/consent-page.js.map +1 -0
- package/dist/oauth/crypto.d.ts +48 -0
- package/dist/oauth/crypto.d.ts.map +1 -0
- package/dist/oauth/crypto.js +72 -0
- package/dist/oauth/crypto.js.map +1 -0
- package/dist/oauth/handlers.d.ts +89 -0
- package/dist/oauth/handlers.d.ts.map +1 -0
- package/dist/oauth/handlers.js +371 -0
- package/dist/oauth/handlers.js.map +1 -0
- package/dist/oauth/metadata.d.ts +29 -0
- package/dist/oauth/metadata.d.ts.map +1 -0
- package/dist/oauth/metadata.js +33 -0
- package/dist/oauth/metadata.js.map +1 -0
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +8 -0
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/semantic/developer.d.ts.map +1 -1
- package/dist/tools/semantic/developer.js +6 -0
- package/dist/tools/semantic/developer.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for the MCP server's embedded OAuth 2.1 authorization server.
|
|
3
|
+
*
|
|
4
|
+
* The cloud endpoint (mcp.dainos.app) is reachable from Claude in the browser,
|
|
5
|
+
* which only speaks OAuth — it has no field for a static Bearer token. So this
|
|
6
|
+
* server acts as a minimal, STATELESS OAuth authorization server that bridges
|
|
7
|
+
* the browser's OAuth flow onto the existing DainOS Personal Access Token (PAT)
|
|
8
|
+
* model:
|
|
9
|
+
*
|
|
10
|
+
* - The user pastes a `dain_pat_...` token on the consent page (`/authorize`).
|
|
11
|
+
* - We issue an OAuth authorization code, then access/refresh tokens, each of
|
|
12
|
+
* which is an ENCRYPTED JWT (JWE) that embeds the PAT.
|
|
13
|
+
* - On `/mcp`, we decrypt the access token to recover the PAT and call
|
|
14
|
+
* api.dainos.app exactly as the Claude Code (raw-PAT) path does today.
|
|
15
|
+
*
|
|
16
|
+
* Statelessness is deliberate: the Vercel function has no datastore. Every
|
|
17
|
+
* token carries its own state, signed + encrypted under MCP_OAUTH_SECRET.
|
|
18
|
+
*/
|
|
19
|
+
import { createHash } from 'node:crypto';
|
|
20
|
+
/** Env var holding the canonical public origin, e.g. `https://mcp.dainos.app`. */
|
|
21
|
+
export const PUBLIC_URL_ENV = 'MCP_PUBLIC_URL';
|
|
22
|
+
export const DEFAULT_PUBLIC_URL = 'https://mcp.dainos.app';
|
|
23
|
+
/** Env var holding the symmetric secret used to encrypt every issued token. */
|
|
24
|
+
export const OAUTH_SECRET_ENV = 'MCP_OAUTH_SECRET';
|
|
25
|
+
/** Token lifetimes (seconds). */
|
|
26
|
+
export const ACCESS_TOKEN_TTL_SECONDS = 60 * 60; // 1 hour
|
|
27
|
+
export const REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 90; // 90 days
|
|
28
|
+
// 30 seconds. NOTE: a stateless server cannot mark a code consumed, so a code
|
|
29
|
+
// is technically replayable inside this window. PKCE bounds the risk (a code is
|
|
30
|
+
// useless without the matching verifier, held only by the originating client),
|
|
31
|
+
// and the short TTL shrinks the window. True single-use would require a store.
|
|
32
|
+
export const AUTH_CODE_TTL_SECONDS = 30;
|
|
33
|
+
export const CLIENT_REG_TTL_SECONDS = 60 * 60 * 24 * 365; // 1 year
|
|
34
|
+
/** The single scope this server understands. */
|
|
35
|
+
export const SCOPE = 'mcp';
|
|
36
|
+
/** The DainOS PAT prefix. Kept in lock-step with the API's `PAT_PREFIX`. */
|
|
37
|
+
export const PAT_PREFIX = 'dain_pat_';
|
|
38
|
+
/** Where users mint a PAT. Surfaced on the consent page. */
|
|
39
|
+
export const PAT_SETTINGS_URL = 'https://dainos.app/settings/api-tokens';
|
|
40
|
+
/**
|
|
41
|
+
* Resolve this server's public base URL.
|
|
42
|
+
*
|
|
43
|
+
* Pinned to the configured canonical origin (MCP_PUBLIC_URL, default
|
|
44
|
+
* mcp.dainos.app). The inbound `Host` header is deliberately NOT trusted: it is
|
|
45
|
+
* attacker-controllable (a direct request to the raw function URL can carry any
|
|
46
|
+
* Host), and letting it set the issuer / authorization_endpoint / token_endpoint
|
|
47
|
+
* in the discovery documents (and the resource_metadata in the WWW-Authenticate
|
|
48
|
+
* challenge) would let a spoofed Host point an OAuth client's token fetch at an
|
|
49
|
+
* attacker-controlled endpoint. The connector always targets the canonical URL,
|
|
50
|
+
* so pinning is also correct, not just safe.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveBaseUrl() {
|
|
53
|
+
return process.env[PUBLIC_URL_ENV] ?? DEFAULT_PUBLIC_URL;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Derive a 32-byte AES key from MCP_OAUTH_SECRET via SHA-256.
|
|
57
|
+
*
|
|
58
|
+
* @throws if the secret is missing or too short. The caller turns this into a
|
|
59
|
+
* clear 500/oauth-disabled response rather than leaking a stack trace.
|
|
60
|
+
*/
|
|
61
|
+
export function getEncryptionKey() {
|
|
62
|
+
const secret = process.env[OAUTH_SECRET_ENV];
|
|
63
|
+
if (!secret || secret.length < 16) {
|
|
64
|
+
throw new Error(`${OAUTH_SECRET_ENV} is not set (need at least 16 characters). ` +
|
|
65
|
+
'The OAuth endpoints are disabled until it is configured on the deployment.');
|
|
66
|
+
}
|
|
67
|
+
return new Uint8Array(createHash('sha256').update(secret).digest());
|
|
68
|
+
}
|
|
69
|
+
/** True when the OAuth layer is configured and able to issue/verify tokens. */
|
|
70
|
+
export function isOAuthConfigured() {
|
|
71
|
+
const secret = process.env[OAUTH_SECRET_ENV];
|
|
72
|
+
return typeof secret === 'string' && secret.length >= 16;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/oauth/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,kFAAkF;AAClF,MAAM,CAAC,MAAM,cAAc,GAAG,gBAAgB,CAAC;AAC/C,MAAM,CAAC,MAAM,kBAAkB,GAAG,wBAAwB,CAAC;AAE3D,+EAA+E;AAC/E,MAAM,CAAC,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAEnD,iCAAiC;AACjC,MAAM,CAAC,MAAM,wBAAwB,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,SAAS;AAC1D,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,UAAU;AACtE,8EAA8E;AAC9E,gFAAgF;AAChF,+EAA+E;AAC/E,+EAA+E;AAC/E,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,CAAC;AACxC,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG,CAAC,CAAC,SAAS;AAEnE,gDAAgD;AAChD,MAAM,CAAC,MAAM,KAAK,GAAG,KAAK,CAAC;AAE3B,4EAA4E;AAC5E,MAAM,CAAC,MAAM,UAAU,GAAG,WAAW,CAAC;AAEtC,4DAA4D;AAC5D,MAAM,CAAC,MAAM,gBAAgB,GAAG,wCAAwC,CAAC;AAEzE;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,kBAAkB,CAAC;AAC3D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB;IAC9B,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAC7C,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CACb,GAAG,gBAAgB,6CAA6C;YAC9D,4EAA4E,CAC/E,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,UAAU,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;AACtE,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,iBAAiB;IAC/B,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAC7C,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC;AAC3D,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The paste-PAT consent page served at GET /authorize.
|
|
3
|
+
*
|
|
4
|
+
* This is the one human-facing surface of the OAuth bridge. The user mints a
|
|
5
|
+
* DainOS API token, pastes it here, and the form POSTs back to /authorize,
|
|
6
|
+
* which issues an authorization code. All copy follows the project copy-style
|
|
7
|
+
* rule: UK English, no em/en dashes, plain ASCII punctuation.
|
|
8
|
+
*
|
|
9
|
+
* Every interpolated value originates from the OAuth client request, so it is
|
|
10
|
+
* HTML-escaped before it lands in an attribute. The PAT field is never
|
|
11
|
+
* pre-filled and is rendered as a password input.
|
|
12
|
+
*/
|
|
13
|
+
export interface ConsentPageParams {
|
|
14
|
+
clientId: string;
|
|
15
|
+
redirectUri: string;
|
|
16
|
+
codeChallenge: string;
|
|
17
|
+
codeChallengeMethod: string;
|
|
18
|
+
state?: string;
|
|
19
|
+
scope?: string;
|
|
20
|
+
/** Optional error banner shown after a rejected submission. */
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function renderConsentPage(params: ConsentPageParams): string;
|
|
24
|
+
//# sourceMappingURL=consent-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consent-page.d.ts","sourceRoot":"","sources":["../../src/oauth/consent-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAgBD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAuEnE"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The paste-PAT consent page served at GET /authorize.
|
|
3
|
+
*
|
|
4
|
+
* This is the one human-facing surface of the OAuth bridge. The user mints a
|
|
5
|
+
* DainOS API token, pastes it here, and the form POSTs back to /authorize,
|
|
6
|
+
* which issues an authorization code. All copy follows the project copy-style
|
|
7
|
+
* rule: UK English, no em/en dashes, plain ASCII punctuation.
|
|
8
|
+
*
|
|
9
|
+
* Every interpolated value originates from the OAuth client request, so it is
|
|
10
|
+
* HTML-escaped before it lands in an attribute. The PAT field is never
|
|
11
|
+
* pre-filled and is rendered as a password input.
|
|
12
|
+
*/
|
|
13
|
+
import { PAT_SETTINGS_URL } from './config.js';
|
|
14
|
+
function escapeHtml(value) {
|
|
15
|
+
return value
|
|
16
|
+
.replace(/&/g, '&')
|
|
17
|
+
.replace(/</g, '<')
|
|
18
|
+
.replace(/>/g, '>')
|
|
19
|
+
.replace(/"/g, '"')
|
|
20
|
+
.replace(/'/g, ''');
|
|
21
|
+
}
|
|
22
|
+
function hiddenField(name, value) {
|
|
23
|
+
if (value === undefined)
|
|
24
|
+
return '';
|
|
25
|
+
return `<input type="hidden" name="${escapeHtml(name)}" value="${escapeHtml(value)}" />`;
|
|
26
|
+
}
|
|
27
|
+
export function renderConsentPage(params) {
|
|
28
|
+
const errorBanner = params.error
|
|
29
|
+
? `<p class="error" role="alert">${escapeHtml(params.error)}</p>`
|
|
30
|
+
: '';
|
|
31
|
+
return `<!DOCTYPE html>
|
|
32
|
+
<html lang="en-GB">
|
|
33
|
+
<head>
|
|
34
|
+
<meta charset="utf-8" />
|
|
35
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
36
|
+
<meta name="robots" content="noindex" />
|
|
37
|
+
<title>Connect Claude to DainOS</title>
|
|
38
|
+
<style>
|
|
39
|
+
:root { color-scheme: light dark; }
|
|
40
|
+
* { box-sizing: border-box; }
|
|
41
|
+
body {
|
|
42
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
43
|
+
margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center;
|
|
44
|
+
background: #0b1120; color: #e2e8f0; padding: 24px;
|
|
45
|
+
}
|
|
46
|
+
.card {
|
|
47
|
+
width: 100%; max-width: 460px; background: #111827; border: 1px solid #1f2937;
|
|
48
|
+
border-radius: 14px; padding: 32px; box-shadow: 0 10px 40px rgba(0,0,0,0.4);
|
|
49
|
+
}
|
|
50
|
+
h1 { font-size: 20px; margin: 0 0 8px; }
|
|
51
|
+
p { font-size: 14px; line-height: 1.5; color: #94a3b8; margin: 0 0 16px; }
|
|
52
|
+
ol { font-size: 14px; line-height: 1.6; color: #cbd5e1; padding-left: 20px; margin: 0 0 20px; }
|
|
53
|
+
a { color: #60a5fa; }
|
|
54
|
+
label { display: block; font-size: 13px; font-weight: 600; margin: 0 0 6px; color: #e2e8f0; }
|
|
55
|
+
input[type="password"] {
|
|
56
|
+
width: 100%; padding: 11px 12px; font-size: 14px; border-radius: 8px;
|
|
57
|
+
border: 1px solid #334155; background: #0b1120; color: #e2e8f0; font-family: monospace;
|
|
58
|
+
}
|
|
59
|
+
input[type="password"]:focus { outline: 2px solid #2563eb; border-color: #2563eb; }
|
|
60
|
+
button {
|
|
61
|
+
width: 100%; margin-top: 18px; padding: 12px; font-size: 15px; font-weight: 600;
|
|
62
|
+
border: none; border-radius: 8px; background: #2563eb; color: white; cursor: pointer;
|
|
63
|
+
}
|
|
64
|
+
button:hover { background: #1d4ed8; }
|
|
65
|
+
.error {
|
|
66
|
+
background: #7f1d1d; color: #fecaca; border: 1px solid #b91c1c;
|
|
67
|
+
border-radius: 8px; padding: 10px 12px; font-size: 13px; margin: 0 0 16px;
|
|
68
|
+
}
|
|
69
|
+
.hint { font-size: 12px; color: #64748b; margin-top: 16px; }
|
|
70
|
+
</style>
|
|
71
|
+
</head>
|
|
72
|
+
<body>
|
|
73
|
+
<div class="card">
|
|
74
|
+
<h1>Connect Claude to DainOS</h1>
|
|
75
|
+
${errorBanner}
|
|
76
|
+
<p>Claude is asking to connect to your DainOS account. Paste a personal access token to allow it.</p>
|
|
77
|
+
<ol>
|
|
78
|
+
<li>Open your <a href="${escapeHtml(PAT_SETTINGS_URL)}" target="_blank" rel="noopener">DainOS API tokens</a> page.</li>
|
|
79
|
+
<li>Create a token and copy it.</li>
|
|
80
|
+
<li>Paste it below, then choose Allow access.</li>
|
|
81
|
+
</ol>
|
|
82
|
+
<form method="POST" action="/authorize" autocomplete="off">
|
|
83
|
+
<label for="pat">DainOS API token</label>
|
|
84
|
+
<input id="pat" name="pat" type="password" placeholder="dain_pat_..." required autofocus />
|
|
85
|
+
${hiddenField('client_id', params.clientId)}
|
|
86
|
+
${hiddenField('redirect_uri', params.redirectUri)}
|
|
87
|
+
${hiddenField('code_challenge', params.codeChallenge)}
|
|
88
|
+
${hiddenField('code_challenge_method', params.codeChallengeMethod)}
|
|
89
|
+
${hiddenField('state', params.state)}
|
|
90
|
+
${hiddenField('scope', params.scope)}
|
|
91
|
+
<button type="submit">Allow access</button>
|
|
92
|
+
</form>
|
|
93
|
+
<p class="hint">Your token is encrypted into the connection and used only to act as you in DainOS. You can revoke it any time from the API tokens page.</p>
|
|
94
|
+
</div>
|
|
95
|
+
</body>
|
|
96
|
+
</html>`;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=consent-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consent-page.js","sourceRoot":"","sources":["../../src/oauth/consent-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAa/C,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,KAAK;SACT,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC5B,CAAC;AAED,SAAS,WAAW,CAAC,IAAY,EAAE,KAAyB;IAC1D,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IACnC,OAAO,8BAA8B,UAAU,CAAC,IAAI,CAAC,YAAY,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC;AAC3F,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,MAAyB;IACzD,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK;QAC9B,CAAC,CAAC,iCAAiC,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM;QACjE,CAAC,CAAC,EAAE,CAAC;IAEP,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MA4CH,WAAW;;;+BAGc,UAAU,CAAC,gBAAgB,CAAC;;;;;;;QAOnD,WAAW,CAAC,WAAW,EAAE,MAAM,CAAC,QAAQ,CAAC;QACzC,WAAW,CAAC,cAAc,EAAE,MAAM,CAAC,WAAW,CAAC;QAC/C,WAAW,CAAC,gBAAgB,EAAE,MAAM,CAAC,aAAa,CAAC;QACnD,WAAW,CAAC,uBAAuB,EAAE,MAAM,CAAC,mBAAmB,CAAC;QAChE,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC;QAClC,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC;;;;;;QAMlC,CAAC;AACT,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stateless token minting + verification for the OAuth bridge.
|
|
3
|
+
*
|
|
4
|
+
* Every artefact the authorization server hands out (authorization code,
|
|
5
|
+
* access token, refresh token, and even the dynamically-registered client_id)
|
|
6
|
+
* is an encrypted JWT (JWE, `dir` + `A256GCM`) under the key derived from
|
|
7
|
+
* MCP_OAUTH_SECRET. Encryption (not mere signing) matters because the payload
|
|
8
|
+
* embeds the user's PAT: a leaked token is no worse than a leaked PAT, which is
|
|
9
|
+
* the same blast radius as today's raw-Bearer model.
|
|
10
|
+
*
|
|
11
|
+
* `typ` discriminates the four token kinds so an access token can never be
|
|
12
|
+
* replayed as an authorization code, etc. `jose.jwtDecrypt` validates `exp`
|
|
13
|
+
* for us, so expired tokens throw on decrypt.
|
|
14
|
+
*/
|
|
15
|
+
type TokenType = 'code' | 'access' | 'refresh' | 'client';
|
|
16
|
+
export interface CodeClaims {
|
|
17
|
+
pat: string;
|
|
18
|
+
code_challenge: string;
|
|
19
|
+
client_id: string;
|
|
20
|
+
redirect_uri: string;
|
|
21
|
+
}
|
|
22
|
+
export interface AccessClaims {
|
|
23
|
+
pat: string;
|
|
24
|
+
}
|
|
25
|
+
export interface RefreshClaims {
|
|
26
|
+
pat: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ClientClaims {
|
|
29
|
+
redirect_uris: string[];
|
|
30
|
+
}
|
|
31
|
+
export declare function issueAuthorizationCode(claims: CodeClaims): Promise<string>;
|
|
32
|
+
export declare function readAuthorizationCode(token: string): Promise<CodeClaims & {
|
|
33
|
+
typ: TokenType;
|
|
34
|
+
}>;
|
|
35
|
+
export declare function issueAccessToken(claims: AccessClaims): Promise<string>;
|
|
36
|
+
export declare function readAccessToken(token: string): Promise<AccessClaims>;
|
|
37
|
+
export declare function issueRefreshToken(claims: RefreshClaims): Promise<string>;
|
|
38
|
+
export declare function readRefreshToken(token: string): Promise<RefreshClaims>;
|
|
39
|
+
export declare function issueClientId(claims: ClientClaims): Promise<string>;
|
|
40
|
+
export declare function readClientId(token: string): Promise<ClientClaims>;
|
|
41
|
+
/**
|
|
42
|
+
* Verify a PKCE `code_verifier` against the stored S256 `code_challenge`.
|
|
43
|
+
*
|
|
44
|
+
* challenge == base64url( SHA-256( verifier ) ). Compared in constant time.
|
|
45
|
+
*/
|
|
46
|
+
export declare function verifyPkceS256(verifier: string, challenge: string): boolean;
|
|
47
|
+
export {};
|
|
48
|
+
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/oauth/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAYH,KAAK,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE1D,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAuBD,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAEhF;AAED,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG;IAAE,GAAG,EAAE,SAAS,CAAA;CAAE,CAAC,CAEnG;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAE5E;AAED,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAE1E;AAED,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAE9E;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAE5E;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAEzE;AAED,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAEvE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAO3E"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stateless token minting + verification for the OAuth bridge.
|
|
3
|
+
*
|
|
4
|
+
* Every artefact the authorization server hands out (authorization code,
|
|
5
|
+
* access token, refresh token, and even the dynamically-registered client_id)
|
|
6
|
+
* is an encrypted JWT (JWE, `dir` + `A256GCM`) under the key derived from
|
|
7
|
+
* MCP_OAUTH_SECRET. Encryption (not mere signing) matters because the payload
|
|
8
|
+
* embeds the user's PAT: a leaked token is no worse than a leaked PAT, which is
|
|
9
|
+
* the same blast radius as today's raw-Bearer model.
|
|
10
|
+
*
|
|
11
|
+
* `typ` discriminates the four token kinds so an access token can never be
|
|
12
|
+
* replayed as an authorization code, etc. `jose.jwtDecrypt` validates `exp`
|
|
13
|
+
* for us, so expired tokens throw on decrypt.
|
|
14
|
+
*/
|
|
15
|
+
import { EncryptJWT, jwtDecrypt } from 'jose';
|
|
16
|
+
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
17
|
+
import { ACCESS_TOKEN_TTL_SECONDS, AUTH_CODE_TTL_SECONDS, CLIENT_REG_TTL_SECONDS, REFRESH_TOKEN_TTL_SECONDS, getEncryptionKey, } from './config.js';
|
|
18
|
+
async function encrypt(typ, claims, ttlSeconds) {
|
|
19
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
20
|
+
return new EncryptJWT({ ...claims, typ })
|
|
21
|
+
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
|
|
22
|
+
.setIssuedAt(nowSeconds)
|
|
23
|
+
.setExpirationTime(nowSeconds + ttlSeconds)
|
|
24
|
+
.encrypt(getEncryptionKey());
|
|
25
|
+
}
|
|
26
|
+
async function decrypt(token, expectedTyp) {
|
|
27
|
+
const { payload } = await jwtDecrypt(token, getEncryptionKey());
|
|
28
|
+
if (payload['typ'] !== expectedTyp) {
|
|
29
|
+
throw new Error(`Token type mismatch: expected ${expectedTyp}, got ${String(payload['typ'])}`);
|
|
30
|
+
}
|
|
31
|
+
return payload;
|
|
32
|
+
}
|
|
33
|
+
export async function issueAuthorizationCode(claims) {
|
|
34
|
+
return encrypt('code', { ...claims }, AUTH_CODE_TTL_SECONDS);
|
|
35
|
+
}
|
|
36
|
+
export async function readAuthorizationCode(token) {
|
|
37
|
+
return decrypt(token, 'code');
|
|
38
|
+
}
|
|
39
|
+
export async function issueAccessToken(claims) {
|
|
40
|
+
return encrypt('access', { ...claims }, ACCESS_TOKEN_TTL_SECONDS);
|
|
41
|
+
}
|
|
42
|
+
export async function readAccessToken(token) {
|
|
43
|
+
return decrypt(token, 'access');
|
|
44
|
+
}
|
|
45
|
+
export async function issueRefreshToken(claims) {
|
|
46
|
+
return encrypt('refresh', { ...claims }, REFRESH_TOKEN_TTL_SECONDS);
|
|
47
|
+
}
|
|
48
|
+
export async function readRefreshToken(token) {
|
|
49
|
+
return decrypt(token, 'refresh');
|
|
50
|
+
}
|
|
51
|
+
export async function issueClientId(claims) {
|
|
52
|
+
return encrypt('client', { ...claims }, CLIENT_REG_TTL_SECONDS);
|
|
53
|
+
}
|
|
54
|
+
export async function readClientId(token) {
|
|
55
|
+
return decrypt(token, 'client');
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Verify a PKCE `code_verifier` against the stored S256 `code_challenge`.
|
|
59
|
+
*
|
|
60
|
+
* challenge == base64url( SHA-256( verifier ) ). Compared in constant time.
|
|
61
|
+
*/
|
|
62
|
+
export function verifyPkceS256(verifier, challenge) {
|
|
63
|
+
if (!verifier || !challenge)
|
|
64
|
+
return false;
|
|
65
|
+
const computed = createHash('sha256').update(verifier).digest('base64url');
|
|
66
|
+
const a = Buffer.from(computed);
|
|
67
|
+
const b = Buffer.from(challenge);
|
|
68
|
+
if (a.length !== b.length)
|
|
69
|
+
return false;
|
|
70
|
+
return timingSafeEqual(a, b);
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/oauth/crypto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.1 endpoint handlers for the paste-PAT bridge.
|
|
3
|
+
*
|
|
4
|
+
* Endpoints:
|
|
5
|
+
* GET /.well-known/oauth-protected-resource metadata (RFC 9728)
|
|
6
|
+
* GET /.well-known/oauth-authorization-server metadata (RFC 8414)
|
|
7
|
+
* POST /register Dynamic Client Registration (RFC 7591)
|
|
8
|
+
* GET /authorize consent page (paste PAT)
|
|
9
|
+
* POST /authorize issue authorization code
|
|
10
|
+
* POST /token code -> token, refresh -> token
|
|
11
|
+
*
|
|
12
|
+
* The pure cores (validateAuthorizeRequest, createAuthorizationCode,
|
|
13
|
+
* exchangeAuthorizationCode, refreshTokens, recoverPat) are exported and unit
|
|
14
|
+
* tested directly; the handle* wrappers only do req/res plumbing.
|
|
15
|
+
*/
|
|
16
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
17
|
+
/** An OAuth error renderable as an RFC 6749 error response. */
|
|
18
|
+
export declare class OAuthError extends Error {
|
|
19
|
+
readonly error: string;
|
|
20
|
+
readonly status: number;
|
|
21
|
+
constructor(error: string, description: string, status?: number);
|
|
22
|
+
toBody(): {
|
|
23
|
+
error: string;
|
|
24
|
+
error_description: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export interface TokenResponse {
|
|
28
|
+
access_token: string;
|
|
29
|
+
token_type: 'Bearer';
|
|
30
|
+
expires_in: number;
|
|
31
|
+
refresh_token: string;
|
|
32
|
+
scope: string;
|
|
33
|
+
}
|
|
34
|
+
export interface AuthorizeParams {
|
|
35
|
+
clientId: string;
|
|
36
|
+
redirectUri: string;
|
|
37
|
+
codeChallenge: string;
|
|
38
|
+
codeChallengeMethod: string;
|
|
39
|
+
state?: string;
|
|
40
|
+
scope?: string;
|
|
41
|
+
}
|
|
42
|
+
export type AuthorizeValidation = {
|
|
43
|
+
ok: true;
|
|
44
|
+
parsed: AuthorizeParams;
|
|
45
|
+
} | {
|
|
46
|
+
ok: false;
|
|
47
|
+
error: string;
|
|
48
|
+
description: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Validate the inbound /authorize parameters and the dynamically-registered
|
|
52
|
+
* client. Returns a discriminated result rather than throwing so both the GET
|
|
53
|
+
* (render) and POST (submit) paths can branch cleanly.
|
|
54
|
+
*/
|
|
55
|
+
export declare function validateAuthorizeRequest(params: Record<string, string | undefined>): Promise<AuthorizeValidation>;
|
|
56
|
+
/** Mint a short-lived authorization code embedding the PAT + PKCE challenge. */
|
|
57
|
+
export declare function createAuthorizationCode(input: {
|
|
58
|
+
pat: string;
|
|
59
|
+
clientId: string;
|
|
60
|
+
redirectUri: string;
|
|
61
|
+
codeChallenge: string;
|
|
62
|
+
}): Promise<string>;
|
|
63
|
+
/** authorization_code grant: verify the code + PKCE, return fresh tokens. */
|
|
64
|
+
export declare function exchangeAuthorizationCode(form: Record<string, string>): Promise<TokenResponse>;
|
|
65
|
+
/** refresh_token grant: re-issue an access token (and rotate the refresh token). */
|
|
66
|
+
export declare function refreshTokens(form: Record<string, string>): Promise<TokenResponse>;
|
|
67
|
+
/**
|
|
68
|
+
* Recover the underlying PAT from an inbound Authorization Bearer value.
|
|
69
|
+
*
|
|
70
|
+
* Accepts either a raw `dain_pat_...` (Claude Code path, unchanged) or an
|
|
71
|
+
* OAuth access token (encrypted JWT minted by /token). Returns null when the
|
|
72
|
+
* value is neither, so the caller can emit a 401 + WWW-Authenticate challenge.
|
|
73
|
+
*/
|
|
74
|
+
export declare function recoverPat(bearer: string | null): Promise<string | null>;
|
|
75
|
+
/** Accept https on an allowlisted host, or http on loopback (local testing). */
|
|
76
|
+
export declare function isAllowedRedirectUri(uri: string): boolean;
|
|
77
|
+
export declare function handleProtectedResourceMetadata(res: ServerResponse, baseUrl: string): void;
|
|
78
|
+
export declare function handleAuthorizationServerMetadata(res: ServerResponse, baseUrl: string): void;
|
|
79
|
+
export declare function handleRegister(req: IncomingMessage & {
|
|
80
|
+
body?: unknown;
|
|
81
|
+
}, res: ServerResponse): Promise<void>;
|
|
82
|
+
/** GET shows the paste-PAT page; POST validates the PAT and issues a code. */
|
|
83
|
+
export declare function handleAuthorize(req: IncomingMessage & {
|
|
84
|
+
body?: unknown;
|
|
85
|
+
}, res: ServerResponse, query: Record<string, string | undefined>): Promise<void>;
|
|
86
|
+
export declare function handleToken(req: IncomingMessage & {
|
|
87
|
+
body?: unknown;
|
|
88
|
+
}, res: ServerResponse): Promise<void>;
|
|
89
|
+
//# sourceMappingURL=handlers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/oauth/handlers.ts"],"names":[],"mappings":"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"}
|