@forumone/throughline 2.2.0 → 2.3.0-pr-315-e1a501e
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/CHANGELOG.md +16 -0
- package/dist/audit/types.d.ts +1 -1
- package/dist/audit/types.d.ts.map +1 -1
- package/dist/audit/types.js +2 -0
- package/dist/audit/types.js.map +1 -1
- package/dist/editorial/palette/ReportsNav.d.ts +3 -1
- package/dist/editorial/palette/ReportsNav.d.ts.map +1 -1
- package/dist/editorial/palette/ReportsNav.js +3 -3
- package/dist/editorial/palette/ReportsNav.js.map +1 -1
- package/dist/exports/rsc.d.ts +1 -0
- package/dist/exports/rsc.d.ts.map +1 -1
- package/dist/exports/rsc.js +2 -0
- package/dist/exports/rsc.js.map +1 -1
- package/dist/mcp-oauth/admin/McpClientActions.d.ts +15 -0
- package/dist/mcp-oauth/admin/McpClientActions.d.ts.map +1 -0
- package/dist/mcp-oauth/admin/McpClientActions.js +79 -0
- package/dist/mcp-oauth/admin/McpClientActions.js.map +1 -0
- package/dist/mcp-oauth/admin/styles.d.ts +2 -0
- package/dist/mcp-oauth/admin/styles.d.ts.map +1 -0
- package/dist/mcp-oauth/admin/styles.js +27 -0
- package/dist/mcp-oauth/admin/styles.js.map +1 -0
- package/dist/mcp-oauth/admin/views.d.ts +4 -0
- package/dist/mcp-oauth/admin/views.d.ts.map +1 -0
- package/dist/mcp-oauth/admin/views.js +99 -0
- package/dist/mcp-oauth/admin/views.js.map +1 -0
- package/dist/mcp-oauth/auth.d.ts +12 -0
- package/dist/mcp-oauth/auth.d.ts.map +1 -0
- package/dist/mcp-oauth/auth.js +56 -0
- package/dist/mcp-oauth/auth.js.map +1 -0
- package/dist/mcp-oauth/cimd.d.ts +13 -0
- package/dist/mcp-oauth/cimd.d.ts.map +1 -0
- package/dist/mcp-oauth/cimd.js +93 -0
- package/dist/mcp-oauth/cimd.js.map +1 -0
- package/dist/mcp-oauth/clients.d.ts +42 -0
- package/dist/mcp-oauth/clients.d.ts.map +1 -0
- package/dist/mcp-oauth/clients.js +151 -0
- package/dist/mcp-oauth/clients.js.map +1 -0
- package/dist/mcp-oauth/collections.d.ts +10 -0
- package/dist/mcp-oauth/collections.d.ts.map +1 -0
- package/dist/mcp-oauth/collections.js +236 -0
- package/dist/mcp-oauth/collections.js.map +1 -0
- package/dist/mcp-oauth/crypto.d.ts +29 -0
- package/dist/mcp-oauth/crypto.d.ts.map +1 -0
- package/dist/mcp-oauth/crypto.js +91 -0
- package/dist/mcp-oauth/crypto.js.map +1 -0
- package/dist/mcp-oauth/endpoints.d.ts +11 -0
- package/dist/mcp-oauth/endpoints.d.ts.map +1 -0
- package/dist/mcp-oauth/endpoints.js +365 -0
- package/dist/mcp-oauth/endpoints.js.map +1 -0
- package/dist/mcp-oauth/index.d.ts +10 -0
- package/dist/mcp-oauth/index.d.ts.map +1 -0
- package/dist/mcp-oauth/index.js +6 -0
- package/dist/mcp-oauth/index.js.map +1 -0
- package/dist/mcp-oauth/metadata.d.ts +45 -0
- package/dist/mcp-oauth/metadata.d.ts.map +1 -0
- package/dist/mcp-oauth/metadata.js +84 -0
- package/dist/mcp-oauth/metadata.js.map +1 -0
- package/dist/mcp-oauth/plugin.d.ts +37 -0
- package/dist/mcp-oauth/plugin.d.ts.map +1 -0
- package/dist/mcp-oauth/plugin.js +90 -0
- package/dist/mcp-oauth/plugin.js.map +1 -0
- package/dist/mcp-oauth/runtime.d.ts +124 -0
- package/dist/mcp-oauth/runtime.d.ts.map +1 -0
- package/dist/mcp-oauth/runtime.js +111 -0
- package/dist/mcp-oauth/runtime.js.map +1 -0
- package/dist/mcp-oauth/store.d.ts +75 -0
- package/dist/mcp-oauth/store.d.ts.map +1 -0
- package/dist/mcp-oauth/store.js +408 -0
- package/dist/mcp-oauth/store.js.map +1 -0
- package/dist/migrate/exports.json +29 -0
- package/package.json +7 -3
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/*
|
|
2
|
+
A client ID metadata document (CIMD): a client whose `client_id` is an HTTPS URL
|
|
3
|
+
serving its own registration. Claude and ChatGPT both prefer it to registering,
|
|
4
|
+
and it is the better arrangement for this server too — nothing is written for a
|
|
5
|
+
client until somebody consents, and there is no open endpoint to fill a table.
|
|
6
|
+
draft-ietf-oauth-client-id-metadata-document.
|
|
7
|
+
|
|
8
|
+
Fetching a URL a stranger supplies is the risk, so the fetch is narrow: HTTPS on
|
|
9
|
+
the default port, a host name rather than an IP literal, no redirects followed,
|
|
10
|
+
five seconds, and 64 KB. What the document says is then held to the same
|
|
11
|
+
allowlist as a registration — its redirects must all match one enabled rule —
|
|
12
|
+
so a document can only ever describe an app this site already accepts.
|
|
13
|
+
*/
|
|
14
|
+
const MAX_BYTES = 64 * 1024;
|
|
15
|
+
const TIMEOUT_MS = 5_000;
|
|
16
|
+
/** Whether `clientId` is the kind of URL this server will fetch. */
|
|
17
|
+
export function isMetadataDocumentUrl(clientId) {
|
|
18
|
+
let url;
|
|
19
|
+
try {
|
|
20
|
+
url = new URL(clientId);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (url.protocol !== 'https:' || url.port !== '' || url.username || url.password || url.hash) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const host = url.hostname;
|
|
29
|
+
if (host === 'localhost' || host.endsWith('.localhost') || host.endsWith('.internal'))
|
|
30
|
+
return false;
|
|
31
|
+
if (/^\d+(\.\d+){3}$/.test(host) || host.startsWith('['))
|
|
32
|
+
return false;
|
|
33
|
+
if (!host.includes('.'))
|
|
34
|
+
return false;
|
|
35
|
+
return url.pathname !== '/';
|
|
36
|
+
}
|
|
37
|
+
/** The document at `clientId`, or why it could not be used. */
|
|
38
|
+
export async function fetchClientMetadata(clientId, fetchImpl = fetch) {
|
|
39
|
+
if (!isMetadataDocumentUrl(clientId))
|
|
40
|
+
return { error: 'client_id is not a URL this server fetches.' };
|
|
41
|
+
let response;
|
|
42
|
+
try {
|
|
43
|
+
response = await fetchImpl(clientId, {
|
|
44
|
+
redirect: 'manual',
|
|
45
|
+
headers: { accept: 'application/json' },
|
|
46
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return { error: 'The client metadata document could not be fetched.' };
|
|
51
|
+
}
|
|
52
|
+
if (response.status !== 200) {
|
|
53
|
+
return { error: `The client metadata document answered ${response.status}.` };
|
|
54
|
+
}
|
|
55
|
+
const declared = Number(response.headers.get('content-length') ?? '0');
|
|
56
|
+
if (declared > MAX_BYTES)
|
|
57
|
+
return { error: 'The client metadata document is too large.' };
|
|
58
|
+
const text = await response.text();
|
|
59
|
+
if (text.length > MAX_BYTES)
|
|
60
|
+
return { error: 'The client metadata document is too large.' };
|
|
61
|
+
let doc;
|
|
62
|
+
try {
|
|
63
|
+
doc = JSON.parse(text);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return { error: 'The client metadata document is not JSON.' };
|
|
67
|
+
}
|
|
68
|
+
if (!doc || typeof doc !== 'object')
|
|
69
|
+
return { error: 'The client metadata document is not an object.' };
|
|
70
|
+
const record = doc;
|
|
71
|
+
if (record['client_id'] !== clientId) {
|
|
72
|
+
return { error: 'The client metadata document names a different client_id.' };
|
|
73
|
+
}
|
|
74
|
+
const redirects = record['redirect_uris'];
|
|
75
|
+
if (!Array.isArray(redirects) || !redirects.every((each) => typeof each === 'string')) {
|
|
76
|
+
return { error: 'The client metadata document has no redirect_uris.' };
|
|
77
|
+
}
|
|
78
|
+
const method = record['token_endpoint_auth_method'];
|
|
79
|
+
if (method !== undefined && method !== 'none') {
|
|
80
|
+
const supported = record['token_endpoint_auth_methods_supported'];
|
|
81
|
+
if (!(Array.isArray(supported) && supported.includes('none'))) {
|
|
82
|
+
return {
|
|
83
|
+
error: 'This server accepts public clients only (token_endpoint_auth_method "none").',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
client_id: clientId,
|
|
89
|
+
redirect_uris: redirects,
|
|
90
|
+
...(typeof record['client_name'] === 'string' ? { client_name: record['client_name'] } : {}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=cimd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cimd.js","sourceRoot":"","sources":["../../src/mcp-oauth/cimd.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export declare const MCP_CLIENT_KINDS: readonly ["claude", "claudeCode", "chatgpt"];
|
|
2
|
+
export type McpClientKind = (typeof MCP_CLIENT_KINDS)[number];
|
|
3
|
+
export interface McpClientRule {
|
|
4
|
+
kind: McpClientKind;
|
|
5
|
+
/** What the consent screen and the Connect MCP page call it. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Redirect URIs matched exactly. */
|
|
8
|
+
redirectUris?: readonly string[];
|
|
9
|
+
/** Redirect URI prefixes, for a callback that carries a per-connector id. */
|
|
10
|
+
redirectPrefixes?: readonly string[];
|
|
11
|
+
/** Loopback paths, matched on any port. */
|
|
12
|
+
loopbackPaths?: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
export declare const MCP_CLIENT_RULES: Readonly<Record<McpClientKind, McpClientRule>>;
|
|
15
|
+
/** True for an `http://localhost`, `127.0.0.1` or `[::1]` redirect. */
|
|
16
|
+
export declare function isLoopback(uri: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* The rule a redirect URI belongs to, among those switched on — or `undefined`
|
|
19
|
+
* if it belongs to none.
|
|
20
|
+
*/
|
|
21
|
+
export declare function clientKindFor(uri: string, enabled?: (kind: McpClientKind) => boolean): McpClientKind | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* The one rule every one of a client's redirect URIs matches, or a reason.
|
|
24
|
+
*
|
|
25
|
+
* A client whose callbacks span two rules is refused rather than labelled by
|
|
26
|
+
* either: the consent screen could only name one of them, and the code could go
|
|
27
|
+
* to the other.
|
|
28
|
+
*/
|
|
29
|
+
export declare function classifyRedirects(uris: readonly unknown[], enabled?: (kind: McpClientKind) => boolean): {
|
|
30
|
+
kind: McpClientKind;
|
|
31
|
+
} | {
|
|
32
|
+
error: string;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Whether `requested` is one of a client's registered redirects.
|
|
36
|
+
*
|
|
37
|
+
* Exact, except that a loopback redirect is compared with its port ignored
|
|
38
|
+
* (RFC 8252 §7.3): a native app registers once and listens on whatever port is
|
|
39
|
+
* free each time.
|
|
40
|
+
*/
|
|
41
|
+
export declare function redirectRegistered(requested: string, registered: readonly string[]): boolean;
|
|
42
|
+
//# sourceMappingURL=clients.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clients.d.ts","sourceRoot":"","sources":["../../src/mcp-oauth/clients.ts"],"names":[],"mappings":"AA6BA,eAAO,MAAM,gBAAgB,8CAA+C,CAAA;AAC5E,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAA;AAE7D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,aAAa,CAAA;IACnB,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAA;IACb,qCAAqC;IACrC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,2CAA2C;IAC3C,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAClC;AAED,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,aAAa,EAAE,aAAa,CAAC,CAoB3E,CAAA;AAYD,uEAAuE;AACvE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAG/C;AA+BD;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,CAAC,IAAI,EAAE,aAAa,KAAK,OAAoB,GACrD,aAAa,GAAG,SAAS,CAE3B;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,SAAS,OAAO,EAAE,EACxB,OAAO,GAAE,CAAC,IAAI,EAAE,aAAa,KAAK,OAAoB,GACrD;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAmB7C;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAe5F"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Which apps may connect, decided by where they send the person back to.
|
|
3
|
+
|
|
4
|
+
An OAuth client names itself — in a registration request, or in a metadata
|
|
5
|
+
document at its `client_id` URL — and anybody can claim to be "Claude". What a
|
|
6
|
+
client cannot fake is its redirect URI: the authorization code goes there, so a
|
|
7
|
+
client that registers somebody else's callback never sees a code. That makes
|
|
8
|
+
the redirect the one honest thing about a client, and this file is the whole
|
|
9
|
+
allowlist of them.
|
|
10
|
+
|
|
11
|
+
So a client is recognised by its redirect URIs, never by its name. Registration
|
|
12
|
+
is refused unless every redirect it asks for matches one rule here, the consent
|
|
13
|
+
screen names the client by the rule it matched rather than by what it calls
|
|
14
|
+
itself, and an administrator turns each rule on or off.
|
|
15
|
+
|
|
16
|
+
The callbacks are the clients' own documentation:
|
|
17
|
+
|
|
18
|
+
- **Claude** — claude.ai, Claude Desktop, mobile and Cowork share one hosted
|
|
19
|
+
callback. https://claude.com/docs/connectors/building/authentication
|
|
20
|
+
- **Claude Code** — a native app, so an RFC 8252 loopback redirect on a port it
|
|
21
|
+
picks per session. Matched with the port ignored, on `localhost` as well as
|
|
22
|
+
the IP literals, because Claude Code declares `localhost`. Any other program
|
|
23
|
+
on the person's computer can listen there too, which is why the consent
|
|
24
|
+
screen warns when this is the rule that matched.
|
|
25
|
+
- **ChatGPT** — the stable callback when the authorization server returns `iss`
|
|
26
|
+
(RFC 9207, which this one does), and the per-connector form otherwise.
|
|
27
|
+
https://developers.openai.com/plugins/build/auth
|
|
28
|
+
*/
|
|
29
|
+
export const MCP_CLIENT_KINDS = ['claude', 'claudeCode', 'chatgpt'];
|
|
30
|
+
export const MCP_CLIENT_RULES = {
|
|
31
|
+
claude: {
|
|
32
|
+
kind: 'claude',
|
|
33
|
+
label: 'Claude',
|
|
34
|
+
redirectUris: [
|
|
35
|
+
'https://claude.ai/api/mcp/auth_callback',
|
|
36
|
+
'https://claude.com/api/mcp/auth_callback',
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
claudeCode: {
|
|
40
|
+
kind: 'claudeCode',
|
|
41
|
+
label: 'Claude Code',
|
|
42
|
+
loopbackPaths: ['/callback'],
|
|
43
|
+
},
|
|
44
|
+
chatgpt: {
|
|
45
|
+
kind: 'chatgpt',
|
|
46
|
+
label: 'ChatGPT',
|
|
47
|
+
redirectUris: ['https://chatgpt.com/connector_platform_oauth_redirect'],
|
|
48
|
+
redirectPrefixes: ['https://chatgpt.com/connector/oauth/'],
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
52
|
+
function parse(uri) {
|
|
53
|
+
try {
|
|
54
|
+
return new URL(uri);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** True for an `http://localhost`, `127.0.0.1` or `[::1]` redirect. */
|
|
61
|
+
export function isLoopback(uri) {
|
|
62
|
+
const url = parse(uri);
|
|
63
|
+
return Boolean(url && url.protocol === 'http:' && LOOPBACK_HOSTS.has(url.hostname));
|
|
64
|
+
}
|
|
65
|
+
/*
|
|
66
|
+
A URI with a fragment, credentials or an unparseable shape matches nothing:
|
|
67
|
+
RFC 6749 §3.1.2 forbids the first, and the second is how an open-looking URL
|
|
68
|
+
smuggles a different host past a prefix check (`https://chatgpt.com/connector/
|
|
69
|
+
oauth/x@evil.example` parses with `evil.example` as the host).
|
|
70
|
+
*/
|
|
71
|
+
function plain(url) {
|
|
72
|
+
return url.hash === '' && url.username === '' && url.password === '';
|
|
73
|
+
}
|
|
74
|
+
function matchesRule(uri, rule) {
|
|
75
|
+
const url = parse(uri);
|
|
76
|
+
if (!url || !plain(url))
|
|
77
|
+
return false;
|
|
78
|
+
if (rule.loopbackPaths && url.protocol === 'http:' && LOOPBACK_HOSTS.has(url.hostname)) {
|
|
79
|
+
return rule.loopbackPaths.includes(url.pathname) && url.search === '';
|
|
80
|
+
}
|
|
81
|
+
if (url.protocol !== 'https:')
|
|
82
|
+
return false;
|
|
83
|
+
if (rule.redirectUris?.includes(uri))
|
|
84
|
+
return true;
|
|
85
|
+
return Boolean(rule.redirectPrefixes?.some((prefix) => uri.startsWith(prefix) &&
|
|
86
|
+
uri.length > prefix.length &&
|
|
87
|
+
/^[A-Za-z0-9_-]+$/.test(uri.slice(prefix.length))));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The rule a redirect URI belongs to, among those switched on — or `undefined`
|
|
91
|
+
* if it belongs to none.
|
|
92
|
+
*/
|
|
93
|
+
export function clientKindFor(uri, enabled = () => true) {
|
|
94
|
+
return MCP_CLIENT_KINDS.find((kind) => enabled(kind) && matchesRule(uri, MCP_CLIENT_RULES[kind]));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The one rule every one of a client's redirect URIs matches, or a reason.
|
|
98
|
+
*
|
|
99
|
+
* A client whose callbacks span two rules is refused rather than labelled by
|
|
100
|
+
* either: the consent screen could only name one of them, and the code could go
|
|
101
|
+
* to the other.
|
|
102
|
+
*/
|
|
103
|
+
export function classifyRedirects(uris, enabled = () => true) {
|
|
104
|
+
if (uris.length === 0)
|
|
105
|
+
return { error: 'At least one redirect URI is required.' };
|
|
106
|
+
if (uris.length > 10)
|
|
107
|
+
return { error: 'At most ten redirect URIs may be registered.' };
|
|
108
|
+
let kind;
|
|
109
|
+
for (const uri of uris) {
|
|
110
|
+
if (typeof uri !== 'string')
|
|
111
|
+
return { error: 'Every redirect URI must be a string.' };
|
|
112
|
+
const matched = clientKindFor(uri, () => true);
|
|
113
|
+
if (!matched)
|
|
114
|
+
return { error: `${uri} is not a redirect this site accepts.` };
|
|
115
|
+
if (!enabled(matched)) {
|
|
116
|
+
return {
|
|
117
|
+
error: `${MCP_CLIENT_RULES[matched].label} connections are turned off on this site. An administrator can turn them on under MCP access.`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (kind && kind !== matched) {
|
|
121
|
+
return { error: 'Every redirect URI must belong to the same app.' };
|
|
122
|
+
}
|
|
123
|
+
kind = matched;
|
|
124
|
+
}
|
|
125
|
+
return kind ? { kind } : { error: 'At least one redirect URI is required.' };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Whether `requested` is one of a client's registered redirects.
|
|
129
|
+
*
|
|
130
|
+
* Exact, except that a loopback redirect is compared with its port ignored
|
|
131
|
+
* (RFC 8252 §7.3): a native app registers once and listens on whatever port is
|
|
132
|
+
* free each time.
|
|
133
|
+
*/
|
|
134
|
+
export function redirectRegistered(requested, registered) {
|
|
135
|
+
if (registered.includes(requested))
|
|
136
|
+
return true;
|
|
137
|
+
if (!isLoopback(requested))
|
|
138
|
+
return false;
|
|
139
|
+
const want = parse(requested);
|
|
140
|
+
if (!want || !plain(want))
|
|
141
|
+
return false;
|
|
142
|
+
return registered.some((each) => {
|
|
143
|
+
const have = parse(each);
|
|
144
|
+
return Boolean(have &&
|
|
145
|
+
isLoopback(each) &&
|
|
146
|
+
have.hostname === want.hostname &&
|
|
147
|
+
have.pathname === want.pathname &&
|
|
148
|
+
have.search === want.search);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=clients.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clients.js","sourceRoot":"","sources":["../../src/mcp-oauth/clients.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CollectionConfig, GlobalConfig, PayloadRequest } from 'payload';
|
|
2
|
+
import { type McpOAuthRuntime } from './runtime.js';
|
|
3
|
+
type UserDoc = Record<string, unknown>;
|
|
4
|
+
/** The person signed in to the admin, or nobody if the request came through MCP. */
|
|
5
|
+
export declare function signedInPerson(req: PayloadRequest): UserDoc | undefined;
|
|
6
|
+
export declare function clientsCollection(runtime: McpOAuthRuntime, group: string): CollectionConfig;
|
|
7
|
+
export declare function grantsCollection(runtime: McpOAuthRuntime, group: string): CollectionConfig;
|
|
8
|
+
export declare function accessGlobal(runtime: McpOAuthRuntime, group: string): GlobalConfig;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=collections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collections.d.ts","sourceRoot":"","sources":["../../src/mcp-oauth/collections.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,gBAAgB,EAAS,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAG5F,OAAO,EAKL,KAAK,eAAe,EACrB,MAAM,cAAc,CAAA;AAuBrB,KAAK,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEtC,oFAAoF;AACpF,wBAAgB,cAAc,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,GAAG,SAAS,CAMvE;AA0BD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAgD3F;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,GAAG,gBAAgB,CA0E1F;AAQD,wBAAgB,YAAY,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CA+ElF"}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { isMcpApiKeyPrincipal } from '../mcp/api-key-access.js';
|
|
2
|
+
import { MCP_CLIENT_KINDS, MCP_CLIENT_RULES } from './clients.js';
|
|
3
|
+
import { MCP_ACCESS_SLUG, MCP_OAUTH_CLIENTS_SLUG, MCP_OAUTH_GRANTS_SLUG, MCP_OAUTH_STRATEGY, } from './runtime.js';
|
|
4
|
+
/** The person signed in to the admin, or nobody if the request came through MCP. */
|
|
5
|
+
export function signedInPerson(req) {
|
|
6
|
+
const user = req.user;
|
|
7
|
+
if (!user || isMcpApiKeyPrincipal(user))
|
|
8
|
+
return undefined;
|
|
9
|
+
if (user['_strategy'] === MCP_OAUTH_STRATEGY || user['_strategy'] === 'mcp-api-key')
|
|
10
|
+
return undefined;
|
|
11
|
+
return user;
|
|
12
|
+
}
|
|
13
|
+
function adminOnly(runtime) {
|
|
14
|
+
return ({ req }) => {
|
|
15
|
+
const user = signedInPerson(req);
|
|
16
|
+
return Boolean(user && runtime.isAdmin(user));
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function ownOrAdmin(runtime) {
|
|
20
|
+
return ({ req }) => {
|
|
21
|
+
const user = signedInPerson(req);
|
|
22
|
+
if (!user)
|
|
23
|
+
return false;
|
|
24
|
+
if (runtime.isAdmin(user))
|
|
25
|
+
return true;
|
|
26
|
+
return { user: { equals: user['id'] } };
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const never = () => false;
|
|
30
|
+
const secret = { read: () => false, create: () => false, update: () => false };
|
|
31
|
+
const kindOptions = MCP_CLIENT_KINDS.map((kind) => ({
|
|
32
|
+
label: MCP_CLIENT_RULES[kind].label,
|
|
33
|
+
value: kind,
|
|
34
|
+
}));
|
|
35
|
+
export function clientsCollection(runtime, group) {
|
|
36
|
+
return {
|
|
37
|
+
slug: MCP_OAUTH_CLIENTS_SLUG,
|
|
38
|
+
labels: { singular: 'MCP client', plural: 'MCP clients' },
|
|
39
|
+
admin: {
|
|
40
|
+
group,
|
|
41
|
+
useAsTitle: 'name',
|
|
42
|
+
defaultColumns: ['name', 'kind', 'method', 'lastUsedAt'],
|
|
43
|
+
description: 'Apps that have registered to connect over MCP. Recognised by where they send people back to, not by the name they give.',
|
|
44
|
+
},
|
|
45
|
+
access: { read: adminOnly(runtime), create: never, update: never, delete: adminOnly(runtime) },
|
|
46
|
+
fields: [
|
|
47
|
+
{
|
|
48
|
+
name: 'clientId',
|
|
49
|
+
type: 'text',
|
|
50
|
+
required: true,
|
|
51
|
+
unique: true,
|
|
52
|
+
index: true,
|
|
53
|
+
admin: { readOnly: true },
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'name',
|
|
57
|
+
type: 'text',
|
|
58
|
+
admin: { readOnly: true, description: 'What the app calls itself.' },
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'kind',
|
|
62
|
+
type: 'select',
|
|
63
|
+
required: true,
|
|
64
|
+
options: kindOptions,
|
|
65
|
+
admin: { readOnly: true },
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'method',
|
|
69
|
+
type: 'select',
|
|
70
|
+
required: true,
|
|
71
|
+
options: [
|
|
72
|
+
{ label: 'Registered (DCR)', value: 'dcr' },
|
|
73
|
+
{ label: 'Metadata document (CIMD)', value: 'cimd' },
|
|
74
|
+
],
|
|
75
|
+
admin: { readOnly: true },
|
|
76
|
+
},
|
|
77
|
+
{ name: 'redirectUris', type: 'json', required: true, admin: { readOnly: true } },
|
|
78
|
+
{ name: 'fetchedAt', type: 'date', admin: { readOnly: true, hidden: true } },
|
|
79
|
+
{ name: 'lastUsedAt', type: 'date', admin: { readOnly: true } },
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function grantsCollection(runtime, group) {
|
|
84
|
+
const hidden = (name, type = 'text', index = false) => ({ name, type, index, access: secret, admin: { hidden: true } });
|
|
85
|
+
return {
|
|
86
|
+
slug: MCP_OAUTH_GRANTS_SLUG,
|
|
87
|
+
labels: { singular: 'Connected app', plural: 'Connected apps' },
|
|
88
|
+
admin: {
|
|
89
|
+
group,
|
|
90
|
+
useAsTitle: 'clientName',
|
|
91
|
+
defaultColumns: ['clientName', 'user', 'status', 'lastUsedAt', 'connectedAt'],
|
|
92
|
+
description: 'Every app somebody has connected over MCP, as them. Revoking one signs that app out at once.',
|
|
93
|
+
},
|
|
94
|
+
access: {
|
|
95
|
+
read: ownOrAdmin(runtime),
|
|
96
|
+
create: never,
|
|
97
|
+
update: never,
|
|
98
|
+
delete: adminOnly(runtime),
|
|
99
|
+
},
|
|
100
|
+
fields: [
|
|
101
|
+
{
|
|
102
|
+
name: 'user',
|
|
103
|
+
type: 'relationship',
|
|
104
|
+
relationTo: runtime.users,
|
|
105
|
+
required: true,
|
|
106
|
+
index: true,
|
|
107
|
+
admin: { readOnly: true },
|
|
108
|
+
},
|
|
109
|
+
{ name: 'clientName', type: 'text', required: true, admin: { readOnly: true } },
|
|
110
|
+
{
|
|
111
|
+
name: 'kind',
|
|
112
|
+
type: 'select',
|
|
113
|
+
required: true,
|
|
114
|
+
options: kindOptions,
|
|
115
|
+
admin: { readOnly: true },
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'client',
|
|
119
|
+
type: 'relationship',
|
|
120
|
+
relationTo: MCP_OAUTH_CLIENTS_SLUG,
|
|
121
|
+
required: true,
|
|
122
|
+
admin: { readOnly: true },
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'status',
|
|
126
|
+
type: 'select',
|
|
127
|
+
required: true,
|
|
128
|
+
defaultValue: 'pending',
|
|
129
|
+
index: true,
|
|
130
|
+
options: [
|
|
131
|
+
{ label: 'Waiting for the app', value: 'pending' },
|
|
132
|
+
{ label: 'Connected', value: 'active' },
|
|
133
|
+
{ label: 'Revoked', value: 'revoked' },
|
|
134
|
+
],
|
|
135
|
+
admin: { readOnly: true },
|
|
136
|
+
},
|
|
137
|
+
{ name: 'redirectHost', type: 'text', admin: { readOnly: true } },
|
|
138
|
+
{ name: 'connectedAt', type: 'date', admin: { readOnly: true } },
|
|
139
|
+
{ name: 'lastUsedAt', type: 'date', admin: { readOnly: true } },
|
|
140
|
+
{ name: 'revokedAt', type: 'date', admin: { readOnly: true } },
|
|
141
|
+
{ name: 'revokedReason', type: 'text', admin: { readOnly: true } },
|
|
142
|
+
hidden('codeHash', 'text', true),
|
|
143
|
+
hidden('codeChallenge'),
|
|
144
|
+
hidden('codeRedirectUri'),
|
|
145
|
+
hidden('codeExpiresAt', 'date'),
|
|
146
|
+
hidden('accessHash', 'text', true),
|
|
147
|
+
hidden('accessExpiresAt', 'date'),
|
|
148
|
+
hidden('refreshHash', 'text', true),
|
|
149
|
+
hidden('previousRefreshHash', 'text', true),
|
|
150
|
+
hidden('rotatedAt', 'date'),
|
|
151
|
+
hidden('refreshExpiresAt', 'date'),
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/*
|
|
156
|
+
The policy: which apps may connect, and what each role is offered once they
|
|
157
|
+
have. An administrator's to edit, and read by the endpoints with
|
|
158
|
+
`overrideAccess`. Its defaults are the host's, so an unsaved global behaves as
|
|
159
|
+
configured (see `readPolicy`).
|
|
160
|
+
*/
|
|
161
|
+
export function accessGlobal(runtime, group) {
|
|
162
|
+
const toolOptions = runtime.tools.map((tool) => ({ label: tool.name, value: tool.name }));
|
|
163
|
+
const admin = adminOnly(runtime);
|
|
164
|
+
return {
|
|
165
|
+
slug: MCP_ACCESS_SLUG,
|
|
166
|
+
label: 'MCP access',
|
|
167
|
+
admin: {
|
|
168
|
+
group,
|
|
169
|
+
description: 'Who can connect AI apps to this site over MCP, and what each role can do once connected. Changes apply to existing connections at their next request.',
|
|
170
|
+
},
|
|
171
|
+
access: { read: admin, update: admin },
|
|
172
|
+
fields: [
|
|
173
|
+
{
|
|
174
|
+
name: 'clients',
|
|
175
|
+
type: 'group',
|
|
176
|
+
label: 'Apps that may connect',
|
|
177
|
+
admin: {
|
|
178
|
+
description: 'Turning an app off refuses new connections and signs out existing ones at their next request.',
|
|
179
|
+
},
|
|
180
|
+
fields: MCP_CLIENT_KINDS.map((kind) => ({
|
|
181
|
+
name: kind,
|
|
182
|
+
type: 'checkbox',
|
|
183
|
+
label: MCP_CLIENT_RULES[kind].label,
|
|
184
|
+
defaultValue: runtime.defaults.clients[kind],
|
|
185
|
+
...(kind === 'claudeCode'
|
|
186
|
+
? {
|
|
187
|
+
admin: {
|
|
188
|
+
description: 'Signs in through a browser on the person’s own computer. Any app there that uses the same localhost callback is treated the same way.',
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
: {}),
|
|
192
|
+
})),
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'editors',
|
|
196
|
+
type: 'group',
|
|
197
|
+
label: 'Everyone who is not an administrator',
|
|
198
|
+
fields: [
|
|
199
|
+
{
|
|
200
|
+
name: 'tools',
|
|
201
|
+
type: 'select',
|
|
202
|
+
hasMany: true,
|
|
203
|
+
options: toolOptions,
|
|
204
|
+
defaultValue: [...runtime.defaults.editorTools],
|
|
205
|
+
admin: {
|
|
206
|
+
description: 'The tools they are offered. A tool added to the site later is not offered until it is ticked here.',
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: 'admins',
|
|
213
|
+
type: 'group',
|
|
214
|
+
label: 'Administrators',
|
|
215
|
+
fields: [
|
|
216
|
+
{
|
|
217
|
+
name: 'allTools',
|
|
218
|
+
type: 'checkbox',
|
|
219
|
+
label: 'Every tool, including ones added later',
|
|
220
|
+
defaultValue: true,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: 'tools',
|
|
224
|
+
type: 'select',
|
|
225
|
+
hasMany: true,
|
|
226
|
+
options: toolOptions,
|
|
227
|
+
admin: {
|
|
228
|
+
condition: (_, sibling) => !sibling.allTools,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=collections.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collections.js","sourceRoot":"","sources":["../../src/mcp-oauth/collections.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const TOKEN_PREFIX: {
|
|
2
|
+
readonly access: "tlo_at_";
|
|
3
|
+
readonly refresh: "tlo_rt_";
|
|
4
|
+
readonly code: "tlo_ac_";
|
|
5
|
+
readonly client: "tlo_client_";
|
|
6
|
+
};
|
|
7
|
+
/** A fresh random token with a recognisable prefix. */
|
|
8
|
+
export declare function newToken(prefix: string): string;
|
|
9
|
+
/** A client id: shorter, because it is not a secret. */
|
|
10
|
+
export declare function newClientId(): string;
|
|
11
|
+
/** A key for one purpose, from the Payload secret. */
|
|
12
|
+
export declare function deriveKey(secret: string, purpose: 'token' | 'request'): Buffer;
|
|
13
|
+
/** What the database stores in place of a token. */
|
|
14
|
+
export declare function hashToken(key: Buffer, token: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* PKCE S256 (RFC 7636 §4.6). The verifier is 43–128 unreserved characters;
|
|
17
|
+
* anything else fails before it is hashed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function verifyPkce(verifier: unknown, challenge: string): boolean;
|
|
20
|
+
/** A code challenge as RFC 7636 §4.2 shapes it: 43 base64url characters. */
|
|
21
|
+
export declare function isCodeChallenge(value: unknown): value is string;
|
|
22
|
+
/** Signs `value` with an expiry, for carrying through a URL. */
|
|
23
|
+
export declare function seal(key: Buffer, value: object, ttlSeconds: number, now?: number): string;
|
|
24
|
+
/**
|
|
25
|
+
* The sealed value, if this server signed it and it has not expired. The MAC is
|
|
26
|
+
* checked before anything is parsed.
|
|
27
|
+
*/
|
|
28
|
+
export declare function unseal<T extends object>(key: Buffer, sealed: unknown, now?: number): T | undefined;
|
|
29
|
+
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/mcp-oauth/crypto.ts"],"names":[],"mappings":"AAqBA,eAAO,MAAM,YAAY;;;;;CAKf,CAAA;AAMV,uDAAuD;AACvD,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,wDAAwD;AACxD,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,sDAAsD;AACtD,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAE9E;AAED,oDAAoD;AACpD,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAMxE;AAED,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE/D;AAED,gEAAgE;AAChE,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,MAAM,CAM7F;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,CAAC,SAAS,MAAM,EACrC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,OAAO,EACf,GAAG,SAAa,GACf,CAAC,GAAG,SAAS,CAgBf"}
|