payload-mcp-toolkit 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +100 -237
  2. package/dist/api-keys.js +16 -0
  3. package/dist/api-keys.js.map +1 -1
  4. package/dist/auth-strategy.js +49 -49
  5. package/dist/auth-strategy.js.map +1 -1
  6. package/dist/components/AgentConnectPill.d.ts +3 -0
  7. package/dist/components/AgentConnectPill.js +121 -0
  8. package/dist/components/AgentConnectPill.js.map +1 -0
  9. package/dist/components/CollectionScopesMatrix.js.map +1 -1
  10. package/dist/components/ConsentPermissions.d.ts +10 -0
  11. package/dist/components/ConsentPermissions.js +165 -0
  12. package/dist/components/ConsentPermissions.js.map +1 -0
  13. package/dist/components/GlobalScopesMatrix.js.map +1 -1
  14. package/dist/components/OAuthView.d.ts +4 -0
  15. package/dist/components/OAuthView.js +269 -0
  16. package/dist/components/OAuthView.js.map +1 -0
  17. package/dist/components/ScopesTable.d.ts +28 -0
  18. package/dist/components/ScopesTable.js +165 -130
  19. package/dist/components/ScopesTable.js.map +1 -1
  20. package/dist/components/agentInstructions.d.ts +2 -0
  21. package/dist/components/agentInstructions.js +34 -0
  22. package/dist/components/agentInstructions.js.map +1 -0
  23. package/dist/components/index.d.ts +2 -0
  24. package/dist/components/index.js +2 -0
  25. package/dist/components/index.js.map +1 -1
  26. package/dist/components/oauth.css +111 -0
  27. package/dist/conflict-detection.js +13 -13
  28. package/dist/conflict-detection.js.map +1 -1
  29. package/dist/draft-workflow.js +26 -26
  30. package/dist/draft-workflow.js.map +1 -1
  31. package/dist/endpoint.d.ts +5 -0
  32. package/dist/endpoint.js +21 -13
  33. package/dist/endpoint.js.map +1 -1
  34. package/dist/hash.js +15 -15
  35. package/dist/hash.js.map +1 -1
  36. package/dist/index.d.ts +2 -0
  37. package/dist/index.js +67 -3
  38. package/dist/index.js.map +1 -1
  39. package/dist/introspection.js +47 -47
  40. package/dist/introspection.js.map +1 -1
  41. package/dist/oauth-permissions.d.ts +52 -0
  42. package/dist/oauth-permissions.js +100 -0
  43. package/dist/oauth-permissions.js.map +1 -0
  44. package/dist/oauth-store.d.ts +19 -0
  45. package/dist/oauth-store.js +122 -0
  46. package/dist/oauth-store.js.map +1 -0
  47. package/dist/oauth.d.ts +19 -0
  48. package/dist/oauth.js +523 -0
  49. package/dist/oauth.js.map +1 -0
  50. package/dist/prompts.js +5 -5
  51. package/dist/prompts.js.map +1 -1
  52. package/dist/registry.js +7 -1
  53. package/dist/registry.js.map +1 -1
  54. package/dist/resources.js +10 -10
  55. package/dist/resources.js.map +1 -1
  56. package/dist/scope/audit-log.js +9 -9
  57. package/dist/scope/audit-log.js.map +1 -1
  58. package/dist/scope/policy.js +7 -7
  59. package/dist/scope/policy.js.map +1 -1
  60. package/dist/tools/_helpers.d.ts +10 -0
  61. package/dist/tools/_helpers.js +53 -41
  62. package/dist/tools/_helpers.js.map +1 -1
  63. package/dist/tools/_layout-helpers.js +33 -33
  64. package/dist/tools/_layout-helpers.js.map +1 -1
  65. package/dist/tools/create-document.js +20 -19
  66. package/dist/tools/create-document.js.map +1 -1
  67. package/dist/tools/delete-document.js +10 -9
  68. package/dist/tools/delete-document.js.map +1 -1
  69. package/dist/tools/find-document.js +18 -15
  70. package/dist/tools/find-document.js.map +1 -1
  71. package/dist/tools/find-global.js +13 -13
  72. package/dist/tools/find-global.js.map +1 -1
  73. package/dist/tools/global-versions.js +8 -7
  74. package/dist/tools/global-versions.js.map +1 -1
  75. package/dist/tools/patch-global-layout.js +9 -9
  76. package/dist/tools/patch-global-layout.js.map +1 -1
  77. package/dist/tools/patch-layout.js +15 -14
  78. package/dist/tools/patch-layout.js.map +1 -1
  79. package/dist/tools/publish-draft.js +2 -1
  80. package/dist/tools/publish-draft.js.map +1 -1
  81. package/dist/tools/publish-global-draft.js +4 -4
  82. package/dist/tools/publish-global-draft.js.map +1 -1
  83. package/dist/tools/resolve-reference.js.map +1 -1
  84. package/dist/tools/safe-delete.js +14 -14
  85. package/dist/tools/safe-delete.js.map +1 -1
  86. package/dist/tools/schedule-publish.js +21 -20
  87. package/dist/tools/schedule-publish.js.map +1 -1
  88. package/dist/tools/search-content.js +12 -12
  89. package/dist/tools/search-content.js.map +1 -1
  90. package/dist/tools/update-document.js +6 -5
  91. package/dist/tools/update-document.js.map +1 -1
  92. package/dist/tools/update-global.js +9 -9
  93. package/dist/tools/update-global.js.map +1 -1
  94. package/dist/tools/upload-media.js +2 -1
  95. package/dist/tools/upload-media.js.map +1 -1
  96. package/dist/tools/versions.js +10 -8
  97. package/dist/tools/versions.js.map +1 -1
  98. package/dist/types.d.ts +3 -0
  99. package/dist/types.js.map +1 -1
  100. package/docs/oauth.md +96 -0
  101. package/package.json +7 -2
package/dist/oauth.js ADDED
@@ -0,0 +1,523 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { extractBearerToken, hashKey, verifyHash } from './hash';
3
+ import { clampScopes, permissionsFrom, summarize } from './oauth-permissions';
4
+ import { consumeRecord, isLive, readRecord, revokeGrant, tokenKey, writeRecord, OAUTH_COLLECTION } from './oauth-store';
5
+ const BASE = '/mcp/oauth';
6
+ const READ = 'mcp:read';
7
+ const WRITE = 'mcp:write';
8
+ const ACCESS_SECONDS = 3600;
9
+ const GRANT_SECONDS = 30 * 24 * 3600;
10
+ // ChatGPT uses this fixed callback only when the server supports RFC 9207 (`iss` in the authorization response).
11
+ const DEFAULT_REDIRECTS = [
12
+ 'https://claude.ai/api/mcp/auth_callback',
13
+ 'https://claude.com/api/mcp/auth_callback',
14
+ 'https://chatgpt.com/connector_platform_oauth_redirect'
15
+ ];
16
+ class OAuthError extends Error {
17
+ code;
18
+ status;
19
+ constructor(code, status = 400){
20
+ super(code), this.code = code, this.status = status;
21
+ }
22
+ }
23
+ const levelOf = (scope)=>scope.split(' ').includes(WRITE) ? 'editor' : 'read-only';
24
+ const randomToken = ()=>randomBytes(32).toString('base64url');
25
+ const expires = (seconds)=>new Date(Date.now() + seconds * 1000).toISOString();
26
+ const json = (body, status = 200)=>Response.json(body, {
27
+ status,
28
+ headers: {
29
+ 'Cache-Control': 'no-store',
30
+ Pragma: 'no-cache'
31
+ }
32
+ });
33
+ const redirect = (url)=>new Response(null, {
34
+ status: 303,
35
+ headers: {
36
+ Location: url,
37
+ 'Cache-Control': 'no-store',
38
+ 'Referrer-Policy': 'no-referrer'
39
+ }
40
+ });
41
+ // The consent form carries the permission choice, which grows with the number of collections.
42
+ async function parameters(req, limit = 16_384) {
43
+ if (!req.text || !req.headers.get('content-type')?.startsWith('application/x-www-form-urlencoded')) throw new OAuthError('invalid_request');
44
+ const text = await req.text();
45
+ if (text.length > limit) throw new OAuthError('invalid_request');
46
+ const params = new URLSearchParams(text);
47
+ for (const key of params.keys())if (params.getAll(key).length !== 1) throw new OAuthError('invalid_request');
48
+ return params;
49
+ }
50
+ /** `catalog` is what the consent screen offers: exposed collections, globals and registered tools. */ export function createOAuth(options, config, userCollection, catalog) {
51
+ if (typeof options.canAuthorize !== 'function') throw new Error('OAuth requires canAuthorize(req).');
52
+ const origin = new URL(config.serverURL || '');
53
+ if (origin.pathname !== '/' || origin.search || origin.hash || origin.username || origin.password || origin.protocol !== 'https:' && !(origin.protocol === 'http:' && [
54
+ 'localhost',
55
+ '127.0.0.1',
56
+ '[::1]'
57
+ ].includes(origin.hostname))) {
58
+ throw new Error('OAuth requires an HTTPS serverURL origin (HTTP loopback is allowed for development).');
59
+ }
60
+ if ((config.routes?.api ?? '/api') !== '/api') throw new Error('OAuth currently requires routes.api = /api.');
61
+ if (!config.collections?.some((c)=>c.slug === userCollection && c.auth)) throw new Error('OAuth requires an authenticated user collection.');
62
+ const issuer = origin.origin;
63
+ const baseURL = `${issuer}/api${BASE}`;
64
+ const resource = `${issuer}/api/mcp`;
65
+ const metadataURL = `${baseURL}/resource`;
66
+ const adminURL = `${issuer}${config.routes?.admin ?? '/admin'}`;
67
+ const redirects = options.redirectURIs ?? DEFAULT_REDIRECTS;
68
+ if (!redirects.length || redirects.length > 20) throw new Error('OAuth requires 1–20 exact redirectURIs.');
69
+ for (const value of redirects){
70
+ const url = new URL(value);
71
+ if (url.hash || url.username || url.password || url.searchParams.has('code') || url.searchParams.has('state') || url.protocol !== 'https:' && !(url.protocol === 'http:' && [
72
+ '127.0.0.1',
73
+ '[::1]',
74
+ 'localhost'
75
+ ].includes(url.hostname))) {
76
+ throw new Error('OAuth redirectURIs must be HTTPS URLs or HTTP loopback URLs without fragments or credentials.');
77
+ }
78
+ }
79
+ const supportedScopes = options.access === 'editor' ? [
80
+ READ,
81
+ WRITE
82
+ ] : [
83
+ READ
84
+ ];
85
+ // Registration is limited to operator-trusted callbacks. Stable public IDs need no client table.
86
+ function clients(payload) {
87
+ return redirects.map((uri)=>({
88
+ uri,
89
+ id: `mcp_${hashKey(`oauth-client:${issuer}:${uri}`, payload.secret)}`
90
+ }));
91
+ }
92
+ function client(req, id) {
93
+ const result = clients(req.payload).find((c)=>c.id === id);
94
+ if (!result) throw new OAuthError('invalid_client', 401);
95
+ return result;
96
+ }
97
+ // Scopes this site does not grant (offline_access, openid, write on a read-only site) are dropped, never granted.
98
+ // RFC 6749 §3.3 lets the server narrow a request, and some clients (ChatGPT) add scopes of their own.
99
+ // Returns '' when nothing supported remains; each caller picks its own fallback.
100
+ function scopes(value) {
101
+ return [
102
+ ...new Set(value?.split(' ').filter((s)=>supportedScopes.includes(s)))
103
+ ].sort().join(' ');
104
+ }
105
+ function signed(payload, value) {
106
+ const body = Buffer.from(JSON.stringify(value)).toString('base64url');
107
+ return `${body}.${hashKey(`oauth-consent:${body}`, payload.secret)}`;
108
+ }
109
+ function verified(payload, value) {
110
+ const [body, signature, extra] = value.split('.');
111
+ if (!body || !signature || extra || !verifyHash(signature, hashKey(`oauth-consent:${body}`, payload.secret))) throw new OAuthError('invalid_request');
112
+ const data = JSON.parse(Buffer.from(body, 'base64url').toString());
113
+ if (typeof data.expires !== 'number' || data.expires <= Date.now()) throw new OAuthError('invalid_request');
114
+ return data;
115
+ }
116
+ async function session(req) {
117
+ const user = req.user;
118
+ if (!user || user.collection !== userCollection || user._mcpKey || req.headers.has('authorization') || user._verified === false || typeof user.lockUntil === 'string' && Date.parse(user.lockUntil) > Date.now()) return false;
119
+ return await options.canAuthorize(req) === true;
120
+ }
121
+ function login(req) {
122
+ const requestURL = new URL(req.url);
123
+ const returnTo = `${requestURL.pathname}${requestURL.search}`;
124
+ const url = new URL(options.loginURL?.(returnTo) ?? `${config.routes?.admin ?? '/admin'}/login?redirect=${encodeURIComponent(returnTo)}`, issuer);
125
+ if (url.origin !== issuer) throw new OAuthError('invalid_request');
126
+ return redirect(url.href);
127
+ }
128
+ function csrf(req, value) {
129
+ if (req.headers.get('origin') !== issuer || String(value.user) !== String(req.user?.id)) throw new OAuthError('access_denied', 403);
130
+ }
131
+ async function grantFor(payload, record) {
132
+ const grant = typeof record.data.grant === 'string' ? await readRecord(payload, record.data.grant) : undefined;
133
+ if (!isLive(grant) || grant.kind !== 'grant' || grant.data.resource !== resource) throw new OAuthError('invalid_grant');
134
+ return grant;
135
+ }
136
+ async function userFor(req, grant) {
137
+ const id = grant.data.user;
138
+ if (typeof id !== 'string' && typeof id !== 'number') throw new OAuthError('invalid_grant');
139
+ const result = await req.payload.find({
140
+ collection: userCollection,
141
+ where: {
142
+ id: {
143
+ equals: id
144
+ }
145
+ },
146
+ limit: 1,
147
+ depth: 0,
148
+ overrideAccess: true
149
+ });
150
+ const user = result.docs[0];
151
+ if (!user || user._verified === false || typeof user.lockUntil === 'string' && Date.parse(user.lockUntil) > Date.now()) throw new OAuthError('invalid_grant');
152
+ const authenticated = {
153
+ ...user,
154
+ collection: userCollection
155
+ };
156
+ const policyReq = Object.assign(Object.create(Object.getPrototypeOf(req)), req, {
157
+ user: authenticated
158
+ });
159
+ if (await options.canAuthorize(policyReq) !== true) throw new OAuthError('invalid_grant');
160
+ return authenticated;
161
+ }
162
+ async function issue(req, grant, scope) {
163
+ const access = `mcp_at_${randomToken()}`;
164
+ const refresh = `mcp_rt_${randomToken()}`;
165
+ const data = {
166
+ grant: grant.key,
167
+ client: grant.data.client,
168
+ resource,
169
+ scope
170
+ };
171
+ await writeRecord(req.payload, {
172
+ key: tokenKey(req.payload, access),
173
+ kind: 'access',
174
+ expiresAt: expires(ACCESS_SECONDS),
175
+ data
176
+ });
177
+ await writeRecord(req.payload, {
178
+ key: tokenKey(req.payload, refresh),
179
+ kind: 'refresh',
180
+ expiresAt: grant.expiresAt,
181
+ data
182
+ });
183
+ return json({
184
+ access_token: access,
185
+ refresh_token: refresh,
186
+ token_type: 'Bearer',
187
+ expires_in: ACCESS_SECONDS,
188
+ scope
189
+ });
190
+ }
191
+ const endpoints = [];
192
+ function endpoint(path, method, handler) {
193
+ endpoints.push({
194
+ path: `${BASE}${path}`,
195
+ method,
196
+ handler: async (req)=>{
197
+ try {
198
+ return await handler(req);
199
+ } catch (error) {
200
+ if (error instanceof OAuthError) return json({
201
+ error: error.code
202
+ }, error.status);
203
+ req.payload.logger.error({
204
+ event: 'mcp.oauth.failed'
205
+ }, '[payload-mcp-toolkit] OAuth operation failed');
206
+ return json({
207
+ error: 'server_error'
208
+ }, 500);
209
+ }
210
+ }
211
+ });
212
+ }
213
+ endpoint('/resource', 'get', ()=>json({
214
+ resource,
215
+ authorization_servers: [
216
+ issuer
217
+ ],
218
+ scopes_supported: supportedScopes,
219
+ bearer_methods_supported: [
220
+ 'header'
221
+ ]
222
+ }));
223
+ endpoint('/metadata', 'get', ()=>json({
224
+ issuer,
225
+ authorization_endpoint: `${baseURL}/authorize`,
226
+ token_endpoint: `${baseURL}/token`,
227
+ registration_endpoint: `${baseURL}/register`,
228
+ revocation_endpoint: `${baseURL}/revoke`,
229
+ response_types_supported: [
230
+ 'code'
231
+ ],
232
+ grant_types_supported: [
233
+ 'authorization_code',
234
+ 'refresh_token'
235
+ ],
236
+ token_endpoint_auth_methods_supported: [
237
+ 'none'
238
+ ],
239
+ revocation_endpoint_auth_methods_supported: [
240
+ 'none'
241
+ ],
242
+ code_challenge_methods_supported: [
243
+ 'S256'
244
+ ],
245
+ scopes_supported: supportedScopes,
246
+ authorization_response_iss_parameter_supported: true
247
+ }));
248
+ endpoint('/register', 'post', async (req)=>{
249
+ if (!req.text || !req.headers.get('content-type')?.startsWith('application/json')) throw new OAuthError('invalid_client_metadata');
250
+ const text = await req.text();
251
+ if (text.length > 4096) throw new OAuthError('invalid_client_metadata');
252
+ let data;
253
+ try {
254
+ data = JSON.parse(text);
255
+ } catch {
256
+ throw new OAuthError('invalid_client_metadata');
257
+ }
258
+ if (!data || !Array.isArray(data.redirect_uris) || data.redirect_uris.length !== 1 || data.token_endpoint_auth_method && data.token_endpoint_auth_method !== 'none' || data.grant_types && (!Array.isArray(data.grant_types) || data.grant_types.some((t)=>![
259
+ 'authorization_code',
260
+ 'refresh_token'
261
+ ].includes(t))) || data.response_types && (!Array.isArray(data.response_types) || data.response_types.length !== 1 || data.response_types[0] !== 'code')) throw new OAuthError('invalid_client_metadata');
262
+ const registered = clients(req.payload).find((c)=>c.uri === data.redirect_uris[0]);
263
+ if (!registered) throw new OAuthError('invalid_redirect_uri');
264
+ return json({
265
+ client_id: registered.id,
266
+ client_name: new URL(registered.uri).hostname,
267
+ redirect_uris: [
268
+ registered.uri
269
+ ],
270
+ token_endpoint_auth_method: 'none',
271
+ grant_types: [
272
+ 'authorization_code',
273
+ 'refresh_token'
274
+ ],
275
+ response_types: [
276
+ 'code'
277
+ ]
278
+ }, 201);
279
+ });
280
+ endpoint('/authorize', 'get', async (req)=>{
281
+ const params = new URL(req.url).searchParams;
282
+ for (const key of params.keys())if (params.getAll(key).length !== 1) throw new OAuthError('invalid_request');
283
+ const registered = client(req, params.get('client_id'));
284
+ if (params.get('redirect_uri') !== registered.uri || params.get('response_type') !== 'code' || params.get('resource') !== resource || params.get('code_challenge_method') !== 'S256' || !/^[A-Za-z0-9_-]{43}$/.test(params.get('code_challenge') ?? '') || (params.get('state')?.length ?? 0) > 2048) throw new OAuthError('invalid_request');
285
+ const scope = scopes(params.get('scope')) || READ;
286
+ if (!req.user) return login(req);
287
+ if (!await session(req)) throw new OAuthError('access_denied', 403);
288
+ const consent = signed(req.payload, {
289
+ user: req.user.id,
290
+ client: registered.id,
291
+ redirect: registered.uri,
292
+ challenge: params.get('code_challenge'),
293
+ state: params.get('state'),
294
+ scope,
295
+ resource,
296
+ expires: Date.now() + 600_000,
297
+ nonce: randomToken()
298
+ });
299
+ if (!req.headers.get('accept')?.includes('application/json')) return redirect(`${adminURL}/mcp-authorize?${params}`);
300
+ return json({
301
+ consent,
302
+ clientName: new URL(registered.uri).hostname,
303
+ account: String(req.user.email ?? req.user.id),
304
+ scope,
305
+ catalog
306
+ });
307
+ });
308
+ endpoint('/authorize', 'post', async (req)=>{
309
+ if (!await session(req)) throw new OAuthError('access_denied', 403);
310
+ const params = await parameters(req, 131_072);
311
+ const consent = verified(req.payload, params.get('consent') ?? '');
312
+ csrf(req, consent);
313
+ const registered = client(req, String(consent.client));
314
+ if (consent.redirect !== registered.uri || consent.resource !== resource) throw new OAuthError('invalid_request');
315
+ const callback = new URL(registered.uri);
316
+ if (typeof consent.state === 'string') callback.searchParams.set('state', consent.state);
317
+ callback.searchParams.set('iss', issuer);
318
+ if (params.get('decision') === 'deny') {
319
+ callback.searchParams.set('error', 'access_denied');
320
+ return redirect(callback.href);
321
+ }
322
+ if (params.get('decision') !== 'allow') throw new OAuthError('invalid_request');
323
+ // The consent screen submits what the user picked; without it the grant gets everything the request allows.
324
+ let choice = {
325
+ level: 'editor'
326
+ };
327
+ const picked = params.get('permissions');
328
+ if (picked !== null) {
329
+ try {
330
+ choice = JSON.parse(picked);
331
+ } catch {
332
+ throw new OAuthError('invalid_request');
333
+ }
334
+ if (!choice || typeof choice !== 'object' || Array.isArray(choice)) throw new OAuthError('invalid_request');
335
+ }
336
+ const { level, permissions } = permissionsFrom(choice, catalog, levelOf(scopes(String(consent.scope)) || READ));
337
+ const scope = level === 'editor' ? `${READ} ${WRITE}` : READ;
338
+ const used = {
339
+ id: '',
340
+ key: tokenKey(req.payload, String(consent.nonce)),
341
+ kind: 'code',
342
+ expiresAt: new Date(Number(consent.expires)).toISOString(),
343
+ data: {}
344
+ };
345
+ if (!await consumeRecord(req.payload, used)) throw new OAuthError('invalid_request');
346
+ const grant = {
347
+ key: `grant:${randomToken()}`,
348
+ kind: 'grant',
349
+ expiresAt: expires(GRANT_SECONDS),
350
+ owner: String(req.user.id),
351
+ data: {
352
+ user: req.user.id,
353
+ client: registered.id,
354
+ redirect: registered.uri,
355
+ resource,
356
+ scope,
357
+ permissions
358
+ }
359
+ };
360
+ await writeRecord(req.payload, grant);
361
+ const code = randomToken();
362
+ await writeRecord(req.payload, {
363
+ key: tokenKey(req.payload, code),
364
+ kind: 'code',
365
+ expiresAt: expires(300),
366
+ data: {
367
+ grant: grant.key,
368
+ client: registered.id,
369
+ redirect: registered.uri,
370
+ challenge: consent.challenge,
371
+ resource,
372
+ scope
373
+ }
374
+ });
375
+ callback.searchParams.set('code', code);
376
+ return redirect(callback.href);
377
+ });
378
+ endpoint('/token', 'post', async (req)=>{
379
+ const params = await parameters(req);
380
+ const registered = client(req, params.get('client_id'));
381
+ if (req.headers.has('authorization') || params.has('client_secret')) throw new OAuthError('invalid_client', 401);
382
+ const type = params.get('grant_type');
383
+ if (type !== 'authorization_code' && type !== 'refresh_token') throw new OAuthError('unsupported_grant_type');
384
+ // RFC 8707 makes `resource` optional here (some clients omit it on refresh); codes and refresh tokens are already bound to it.
385
+ if (params.has('resource') && params.get('resource') !== resource) throw new OAuthError('invalid_target');
386
+ const raw = params.get(type === 'authorization_code' ? 'code' : 'refresh_token') ?? '';
387
+ const record = await readRecord(req.payload, tokenKey(req.payload, raw));
388
+ if (!isLive(record) || record.kind !== (type === 'authorization_code' ? 'code' : 'refresh') || record.data.client !== registered.id || record.data.resource !== resource) throw new OAuthError('invalid_grant');
389
+ const grant = await grantFor(req.payload, record);
390
+ await userFor(req, grant);
391
+ if (type === 'authorization_code') {
392
+ const verifier = params.get('code_verifier') ?? '';
393
+ if (params.get('redirect_uri') !== record.data.redirect || !/^[A-Za-z0-9._~-]{43,128}$/.test(verifier) || createHash('sha256').update(verifier).digest('base64url') !== record.data.challenge) throw new OAuthError('invalid_grant');
394
+ }
395
+ // A scope list with nothing this site grants counts as omitted (RFC 6749 §6), so it keeps the stored scope.
396
+ const scope = scopes(params.get('scope')) || String(record.data.scope);
397
+ if (scope.split(' ').some((s)=>!String(record.data.scope).split(' ').includes(s))) throw new OAuthError('invalid_scope');
398
+ if (!await consumeRecord(req.payload, record)) {
399
+ await revokeGrant(req.payload, grant);
400
+ throw new OAuthError('invalid_grant');
401
+ }
402
+ return issue(req, grant, scope);
403
+ });
404
+ endpoint('/revoke', 'post', async (req)=>{
405
+ const params = await parameters(req);
406
+ const registered = client(req, params.get('client_id'));
407
+ const record = await readRecord(req.payload, tokenKey(req.payload, params.get('token') ?? ''));
408
+ if (record && [
409
+ 'access',
410
+ 'refresh'
411
+ ].includes(record.kind) && record.data.client === registered.id) {
412
+ const grant = await readRecord(req.payload, String(record.data.grant));
413
+ if (grant?.kind === 'grant') await revokeGrant(req.payload, grant);
414
+ }
415
+ return new Response(null, {
416
+ status: 200,
417
+ headers: {
418
+ 'Cache-Control': 'no-store'
419
+ }
420
+ });
421
+ });
422
+ endpoint('/connections', 'get', async (req)=>{
423
+ if (!req.user) return login(req);
424
+ // Revocation remains available even if the site's authorization policy has since removed access.
425
+ if (req.user.collection !== userCollection || req.headers.has('authorization') || req.user._mcpKey) throw new OAuthError('access_denied', 403);
426
+ if (!req.headers.get('accept')?.includes('application/json')) return redirect(`${adminURL}/mcp-connections`);
427
+ const records = await req.payload.find({
428
+ collection: OAUTH_COLLECTION,
429
+ where: {
430
+ and: [
431
+ {
432
+ kind: {
433
+ equals: 'grant'
434
+ }
435
+ },
436
+ {
437
+ owner: {
438
+ equals: String(req.user.id)
439
+ }
440
+ },
441
+ {
442
+ expiresAt: {
443
+ greater_than: new Date().toISOString()
444
+ }
445
+ }
446
+ ]
447
+ },
448
+ limit: 0,
449
+ pagination: false,
450
+ depth: 0,
451
+ overrideAccess: true
452
+ });
453
+ const grants = records.docs.filter((g)=>String(g.data.user) === String(req.user.id));
454
+ // Setup steps show only to accounts that may connect; a failing policy must not block disconnecting.
455
+ const eligible = await session(req).catch(()=>false);
456
+ return json({
457
+ resource,
458
+ access: options.access ?? 'read-only',
459
+ eligible,
460
+ grants: grants.map((g)=>({
461
+ clientName: new URL(String(g.data.redirect)).hostname,
462
+ scope: String(g.data.scope),
463
+ // Current level: the site's access setting may have dropped write since consent.
464
+ summary: summarize(g.data.permissions, levelOf(scopes(String(g.data.scope))), catalog),
465
+ consent: signed(req.payload, {
466
+ user: req.user.id,
467
+ grant: g.key,
468
+ expires: Date.now() + 600_000
469
+ })
470
+ }))
471
+ });
472
+ });
473
+ endpoint('/connections', 'post', async (req)=>{
474
+ if (!req.user || req.user.collection !== userCollection || req.headers.has('authorization') || req.user._mcpKey) throw new OAuthError('access_denied', 403);
475
+ const params = await parameters(req);
476
+ const consent = verified(req.payload, params.get('consent') ?? '');
477
+ csrf(req, consent);
478
+ const grant = await readRecord(req.payload, String(consent.grant));
479
+ if (!grant || grant.kind !== 'grant' || String(grant.data.user) !== String(req.user.id)) throw new OAuthError('access_denied', 403);
480
+ await revokeGrant(req.payload, grant);
481
+ return redirect(`${baseURL}/connections`);
482
+ });
483
+ async function authenticate(req) {
484
+ const token = extractBearerToken(req.headers.get('authorization'));
485
+ if (!token?.startsWith('mcp_at_')) return false;
486
+ try {
487
+ const record = await readRecord(req.payload, tokenKey(req.payload, token));
488
+ if (!isLive(record) || record.kind !== 'access' || record.data.resource !== resource) return false;
489
+ const grant = await grantFor(req.payload, record);
490
+ const user = await userFor(req, grant);
491
+ const scope = scopes(String(record.data.scope));
492
+ if (!scope) return false;
493
+ // Grants store what the user picked at consent; older grants have no list and get the whole level.
494
+ const stored = grant.data.permissions;
495
+ req.user = {
496
+ ...user,
497
+ _strategy: 'mcp-toolkit-oauth',
498
+ _mcpKey: {
499
+ keyId: grant.id,
500
+ keyPrefix: null,
501
+ scopes: stored ? clampScopes(stored, levelOf(scope)) : {
502
+ preset: levelOf(scope)
503
+ }
504
+ }
505
+ };
506
+ return true;
507
+ } catch (error) {
508
+ if (!(error instanceof OAuthError)) req.payload.logger.error({
509
+ event: 'mcp.oauth.authentication_failed'
510
+ }, '[payload-mcp-toolkit] OAuth authentication failed');
511
+ return false;
512
+ }
513
+ }
514
+ // Clients request the challenge scope first, so it must include write when the site allows it.
515
+ return {
516
+ endpoints,
517
+ authenticate,
518
+ metadataURL,
519
+ scope: supportedScopes.join(' ')
520
+ };
521
+ }
522
+
523
+ //# sourceMappingURL=oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/oauth.ts"],"sourcesContent":["import { createHash, randomBytes } from 'node:crypto'\nimport type { Config, Endpoint, Payload, PayloadRequest } from 'payload'\nimport { extractBearerToken, hashKey, verifyHash } from './hash'\nimport { clampScopes, permissionsFrom, summarize, type Catalog, type Choice, type Level } from './oauth-permissions'\nimport { consumeRecord, isLive, readRecord, revokeGrant, tokenKey, writeRecord, OAUTH_COLLECTION, type OAuthRecord } from './oauth-store'\nimport type { KeyScopes } from './types'\n\nconst BASE = '/mcp/oauth'\nconst READ = 'mcp:read'\nconst WRITE = 'mcp:write'\nconst ACCESS_SECONDS = 3600\nconst GRANT_SECONDS = 30 * 24 * 3600\n// ChatGPT uses this fixed callback only when the server supports RFC 9207 (`iss` in the authorization response).\nconst DEFAULT_REDIRECTS = ['https://claude.ai/api/mcp/auth_callback', 'https://claude.com/api/mcp/auth_callback',\n 'https://chatgpt.com/connector_platform_oauth_redirect']\n\nexport interface OAuthOptions {\n /** Required site policy. Rechecked at consent, refresh and every MCP request. */\n canAuthorize: (req: PayloadRequest) => boolean | Promise<boolean>\n /** Defaults to Payload admin login. Return a same-origin login URL that returns to returnTo. */\n loginURL?: (returnTo: string) => string\n /** Exact trusted callbacks; defaults to the Claude (web/Desktop) and ChatGPT remote connector callbacks. */\n redirectURIs?: string[]\n /** Read-only by default. Editor enables create/update, still subject to Payload access controls. */\n access?: 'read-only' | 'editor'\n}\n\nclass OAuthError extends Error {\n constructor(public code: string, public status = 400) { super(code) }\n}\n\nconst levelOf = (scope: string): Level => scope.split(' ').includes(WRITE) ? 'editor' : 'read-only'\nconst randomToken = () => randomBytes(32).toString('base64url')\nconst expires = (seconds: number) => new Date(Date.now() + seconds * 1000).toISOString()\nconst json = (body: unknown, status = 200) => Response.json(body, { status, headers: { 'Cache-Control': 'no-store', Pragma: 'no-cache' } })\nconst redirect = (url: string) => new Response(null, { status: 303, headers: { Location: url, 'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer' } })\n\n// The consent form carries the permission choice, which grows with the number of collections.\nasync function parameters(req: PayloadRequest, limit = 16_384): Promise<URLSearchParams> {\n if (!req.text || !req.headers.get('content-type')?.startsWith('application/x-www-form-urlencoded')) throw new OAuthError('invalid_request')\n const text = await req.text()\n if (text.length > limit) throw new OAuthError('invalid_request')\n const params = new URLSearchParams(text)\n for (const key of params.keys()) if (params.getAll(key).length !== 1) throw new OAuthError('invalid_request')\n return params\n}\n\n/** `catalog` is what the consent screen offers: exposed collections, globals and registered tools. */\nexport function createOAuth(options: OAuthOptions, config: Config, userCollection: string, catalog: Catalog) {\n if (typeof options.canAuthorize !== 'function') throw new Error('OAuth requires canAuthorize(req).')\n const origin = new URL(config.serverURL || '')\n if (origin.pathname !== '/' || origin.search || origin.hash || origin.username || origin.password ||\n (origin.protocol !== 'https:' && !(origin.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(origin.hostname)))) {\n throw new Error('OAuth requires an HTTPS serverURL origin (HTTP loopback is allowed for development).')\n }\n if ((config.routes?.api ?? '/api') !== '/api') throw new Error('OAuth currently requires routes.api = /api.')\n if (!config.collections?.some(c => c.slug === userCollection && c.auth)) throw new Error('OAuth requires an authenticated user collection.')\n const issuer = origin.origin\n const baseURL = `${issuer}/api${BASE}`\n const resource = `${issuer}/api/mcp`\n const metadataURL = `${baseURL}/resource`\n const adminURL = `${issuer}${config.routes?.admin ?? '/admin'}`\n const redirects = options.redirectURIs ?? DEFAULT_REDIRECTS\n if (!redirects.length || redirects.length > 20) throw new Error('OAuth requires 1–20 exact redirectURIs.')\n for (const value of redirects) {\n const url = new URL(value)\n if (url.hash || url.username || url.password || url.searchParams.has('code') || url.searchParams.has('state') ||\n (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['127.0.0.1', '[::1]', 'localhost'].includes(url.hostname)))) {\n throw new Error('OAuth redirectURIs must be HTTPS URLs or HTTP loopback URLs without fragments or credentials.')\n }\n }\n const supportedScopes = options.access === 'editor' ? [READ, WRITE] : [READ]\n // Registration is limited to operator-trusted callbacks. Stable public IDs need no client table.\n function clients(payload: Payload) {\n return redirects.map(uri => ({ uri, id: `mcp_${hashKey(`oauth-client:${issuer}:${uri}`, payload.secret)}` }))\n }\n function client(req: PayloadRequest, id: string | null) {\n const result = clients(req.payload).find(c => c.id === id)\n if (!result) throw new OAuthError('invalid_client', 401)\n return result\n }\n // Scopes this site does not grant (offline_access, openid, write on a read-only site) are dropped, never granted.\n // RFC 6749 §3.3 lets the server narrow a request, and some clients (ChatGPT) add scopes of their own.\n // Returns '' when nothing supported remains; each caller picks its own fallback.\n function scopes(value: string | null): string {\n return [...new Set(value?.split(' ').filter(s => supportedScopes.includes(s)))].sort().join(' ')\n }\n function signed(payload: Payload, value: Record<string, unknown>): string {\n const body = Buffer.from(JSON.stringify(value)).toString('base64url')\n return `${body}.${hashKey(`oauth-consent:${body}`, payload.secret)}`\n }\n function verified(payload: Payload, value: string): Record<string, unknown> {\n const [body, signature, extra] = value.split('.')\n if (!body || !signature || extra || !verifyHash(signature, hashKey(`oauth-consent:${body}`, payload.secret))) throw new OAuthError('invalid_request')\n const data = JSON.parse(Buffer.from(body, 'base64url').toString()) as Record<string, unknown>\n if (typeof data.expires !== 'number' || data.expires <= Date.now()) throw new OAuthError('invalid_request')\n return data\n }\n async function session(req: PayloadRequest): Promise<boolean> {\n const user = req.user as Record<string, unknown> | null\n if (!user || user.collection !== userCollection || user._mcpKey || req.headers.has('authorization') || user._verified === false ||\n (typeof user.lockUntil === 'string' && Date.parse(user.lockUntil) > Date.now())) return false\n return (await options.canAuthorize(req)) === true\n }\n function login(req: PayloadRequest): Response {\n const requestURL = new URL(req.url!)\n const returnTo = `${requestURL.pathname}${requestURL.search}`\n const url = new URL(options.loginURL?.(returnTo) ?? `${config.routes?.admin ?? '/admin'}/login?redirect=${encodeURIComponent(returnTo)}`, issuer)\n if (url.origin !== issuer) throw new OAuthError('invalid_request')\n return redirect(url.href)\n }\n function csrf(req: PayloadRequest, value: Record<string, unknown>) {\n if (req.headers.get('origin') !== issuer || String(value.user) !== String(req.user?.id)) throw new OAuthError('access_denied', 403)\n }\n async function grantFor(payload: Payload, record: OAuthRecord): Promise<OAuthRecord> {\n const grant = typeof record.data.grant === 'string' ? await readRecord(payload, record.data.grant) : undefined\n if (!isLive(grant) || grant.kind !== 'grant' || grant.data.resource !== resource) throw new OAuthError('invalid_grant')\n return grant\n }\n async function userFor(req: PayloadRequest, grant: OAuthRecord): Promise<PayloadRequest['user']> {\n const id = grant.data.user\n if (typeof id !== 'string' && typeof id !== 'number') throw new OAuthError('invalid_grant')\n const result = await req.payload.find({ collection: userCollection, where: { id: { equals: id } }, limit: 1, depth: 0, overrideAccess: true })\n const user = result.docs[0]\n if (!user || (user._verified === false) || (typeof user.lockUntil === 'string' && Date.parse(user.lockUntil) > Date.now())) throw new OAuthError('invalid_grant')\n const authenticated = { ...user, collection: userCollection } as PayloadRequest['user']\n const policyReq = Object.assign(Object.create(Object.getPrototypeOf(req)), req, { user: authenticated }) as PayloadRequest\n if ((await options.canAuthorize(policyReq)) !== true) throw new OAuthError('invalid_grant')\n return authenticated\n }\n async function issue(req: PayloadRequest, grant: OAuthRecord, scope: string): Promise<Response> {\n const access = `mcp_at_${randomToken()}`\n const refresh = `mcp_rt_${randomToken()}`\n const data = { grant: grant.key, client: grant.data.client, resource, scope }\n await writeRecord(req.payload, { key: tokenKey(req.payload, access), kind: 'access', expiresAt: expires(ACCESS_SECONDS), data })\n await writeRecord(req.payload, { key: tokenKey(req.payload, refresh), kind: 'refresh', expiresAt: grant.expiresAt, data })\n return json({ access_token: access, refresh_token: refresh, token_type: 'Bearer', expires_in: ACCESS_SECONDS, scope })\n }\n const endpoints: Endpoint[] = []\n function endpoint(path: string, method: Endpoint['method'], handler: (req: PayloadRequest) => Promise<Response> | Response) {\n endpoints.push({ path: `${BASE}${path}`, method, handler: async req => {\n try { return await handler(req) } catch (error) {\n if (error instanceof OAuthError) return json({ error: error.code }, error.status)\n req.payload.logger.error({ event: 'mcp.oauth.failed' }, '[payload-mcp-toolkit] OAuth operation failed')\n return json({ error: 'server_error' }, 500)\n }\n } })\n }\n endpoint('/resource', 'get', () => json({ resource, authorization_servers: [issuer], scopes_supported: supportedScopes, bearer_methods_supported: ['header'] }))\n endpoint('/metadata', 'get', () => json({ issuer, authorization_endpoint: `${baseURL}/authorize`, token_endpoint: `${baseURL}/token`,\n registration_endpoint: `${baseURL}/register`, revocation_endpoint: `${baseURL}/revoke`, response_types_supported: ['code'],\n grant_types_supported: ['authorization_code', 'refresh_token'], token_endpoint_auth_methods_supported: ['none'],\n revocation_endpoint_auth_methods_supported: ['none'], code_challenge_methods_supported: ['S256'], scopes_supported: supportedScopes,\n authorization_response_iss_parameter_supported: true }))\n endpoint('/register', 'post', async req => {\n if (!req.text || !req.headers.get('content-type')?.startsWith('application/json')) throw new OAuthError('invalid_client_metadata')\n const text = await req.text()\n if (text.length > 4096) throw new OAuthError('invalid_client_metadata')\n let data: Record<string, unknown>\n try { data = JSON.parse(text) } catch { throw new OAuthError('invalid_client_metadata') }\n if (!data || !Array.isArray(data.redirect_uris) || data.redirect_uris.length !== 1 ||\n (data.token_endpoint_auth_method && data.token_endpoint_auth_method !== 'none') ||\n (data.grant_types && (!Array.isArray(data.grant_types) || data.grant_types.some(t => !['authorization_code', 'refresh_token'].includes(t)))) ||\n (data.response_types && (!Array.isArray(data.response_types) || data.response_types.length !== 1 || data.response_types[0] !== 'code'))) throw new OAuthError('invalid_client_metadata')\n const registered = clients(req.payload).find(c => c.uri === (data.redirect_uris as unknown[])[0])\n if (!registered) throw new OAuthError('invalid_redirect_uri')\n return json({ client_id: registered.id, client_name: new URL(registered.uri).hostname, redirect_uris: [registered.uri],\n token_endpoint_auth_method: 'none', grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'] }, 201)\n })\n endpoint('/authorize', 'get', async req => {\n const params = new URL(req.url!).searchParams\n for (const key of params.keys()) if (params.getAll(key).length !== 1) throw new OAuthError('invalid_request')\n const registered = client(req, params.get('client_id'))\n if (params.get('redirect_uri') !== registered.uri || params.get('response_type') !== 'code' ||\n params.get('resource') !== resource || params.get('code_challenge_method') !== 'S256' ||\n !/^[A-Za-z0-9_-]{43}$/.test(params.get('code_challenge') ?? '') || (params.get('state')?.length ?? 0) > 2048) throw new OAuthError('invalid_request')\n const scope = scopes(params.get('scope')) || READ\n if (!req.user) return login(req)\n if (!await session(req)) throw new OAuthError('access_denied', 403)\n const consent = signed(req.payload, { user: req.user.id, client: registered.id, redirect: registered.uri,\n challenge: params.get('code_challenge'), state: params.get('state'), scope, resource, expires: Date.now() + 600_000, nonce: randomToken() })\n if (!req.headers.get('accept')?.includes('application/json')) return redirect(`${adminURL}/mcp-authorize?${params}`)\n return json({ consent, clientName: new URL(registered.uri).hostname, account: String(req.user.email ?? req.user.id), scope, catalog })\n })\n endpoint('/authorize', 'post', async req => {\n if (!await session(req)) throw new OAuthError('access_denied', 403)\n const params = await parameters(req, 131_072)\n const consent = verified(req.payload, params.get('consent') ?? '')\n csrf(req, consent)\n const registered = client(req, String(consent.client))\n if (consent.redirect !== registered.uri || consent.resource !== resource) throw new OAuthError('invalid_request')\n const callback = new URL(registered.uri)\n if (typeof consent.state === 'string') callback.searchParams.set('state', consent.state)\n callback.searchParams.set('iss', issuer)\n if (params.get('decision') === 'deny') { callback.searchParams.set('error', 'access_denied'); return redirect(callback.href) }\n if (params.get('decision') !== 'allow') throw new OAuthError('invalid_request')\n // The consent screen submits what the user picked; without it the grant gets everything the request allows.\n let choice: Choice = { level: 'editor' }\n const picked = params.get('permissions')\n if (picked !== null) {\n try { choice = JSON.parse(picked) } catch { throw new OAuthError('invalid_request') }\n if (!choice || typeof choice !== 'object' || Array.isArray(choice)) throw new OAuthError('invalid_request')\n }\n const { level, permissions } = permissionsFrom(choice, catalog, levelOf(scopes(String(consent.scope)) || READ))\n const scope = level === 'editor' ? `${READ} ${WRITE}` : READ\n const used: OAuthRecord = { id: '', key: tokenKey(req.payload, String(consent.nonce)), kind: 'code', expiresAt: new Date(Number(consent.expires)).toISOString(), data: {} }\n if (!await consumeRecord(req.payload, used)) throw new OAuthError('invalid_request')\n const grant = { key: `grant:${randomToken()}`, kind: 'grant' as const, expiresAt: expires(GRANT_SECONDS), owner: String(req.user!.id),\n data: { user: req.user!.id, client: registered.id, redirect: registered.uri, resource, scope, permissions } }\n await writeRecord(req.payload, grant)\n const code = randomToken()\n await writeRecord(req.payload, { key: tokenKey(req.payload, code), kind: 'code', expiresAt: expires(300),\n data: { grant: grant.key, client: registered.id, redirect: registered.uri, challenge: consent.challenge, resource, scope } })\n callback.searchParams.set('code', code)\n return redirect(callback.href)\n })\n endpoint('/token', 'post', async req => {\n const params = await parameters(req)\n const registered = client(req, params.get('client_id'))\n if (req.headers.has('authorization') || params.has('client_secret')) throw new OAuthError('invalid_client', 401)\n const type = params.get('grant_type')\n if (type !== 'authorization_code' && type !== 'refresh_token') throw new OAuthError('unsupported_grant_type')\n // RFC 8707 makes `resource` optional here (some clients omit it on refresh); codes and refresh tokens are already bound to it.\n if (params.has('resource') && params.get('resource') !== resource) throw new OAuthError('invalid_target')\n const raw = params.get(type === 'authorization_code' ? 'code' : 'refresh_token') ?? ''\n const record = await readRecord(req.payload, tokenKey(req.payload, raw))\n if (!isLive(record) || record.kind !== (type === 'authorization_code' ? 'code' : 'refresh') || record.data.client !== registered.id || record.data.resource !== resource) throw new OAuthError('invalid_grant')\n const grant = await grantFor(req.payload, record)\n await userFor(req, grant)\n if (type === 'authorization_code') {\n const verifier = params.get('code_verifier') ?? ''\n if (params.get('redirect_uri') !== record.data.redirect || !/^[A-Za-z0-9._~-]{43,128}$/.test(verifier) ||\n createHash('sha256').update(verifier).digest('base64url') !== record.data.challenge) throw new OAuthError('invalid_grant')\n }\n // A scope list with nothing this site grants counts as omitted (RFC 6749 §6), so it keeps the stored scope.\n const scope = scopes(params.get('scope')) || String(record.data.scope)\n if (scope.split(' ').some(s => !String(record.data.scope).split(' ').includes(s))) throw new OAuthError('invalid_scope')\n if (!await consumeRecord(req.payload, record)) { await revokeGrant(req.payload, grant); throw new OAuthError('invalid_grant') }\n return issue(req, grant, scope)\n })\n endpoint('/revoke', 'post', async req => {\n const params = await parameters(req)\n const registered = client(req, params.get('client_id'))\n const record = await readRecord(req.payload, tokenKey(req.payload, params.get('token') ?? ''))\n if (record && ['access', 'refresh'].includes(record.kind) && record.data.client === registered.id) {\n const grant = await readRecord(req.payload, String(record.data.grant))\n if (grant?.kind === 'grant') await revokeGrant(req.payload, grant)\n }\n return new Response(null, { status: 200, headers: { 'Cache-Control': 'no-store' } })\n })\n endpoint('/connections', 'get', async req => {\n if (!req.user) return login(req)\n // Revocation remains available even if the site's authorization policy has since removed access.\n if (req.user.collection !== userCollection || req.headers.has('authorization') || (req.user as Record<string, unknown>)._mcpKey) throw new OAuthError('access_denied', 403)\n if (!req.headers.get('accept')?.includes('application/json')) return redirect(`${adminURL}/mcp-connections`)\n const records = await req.payload.find({ collection: OAUTH_COLLECTION, where: { and: [{ kind: { equals: 'grant' } }, { owner: { equals: String(req.user.id) } }, { expiresAt: { greater_than: new Date().toISOString() } }] }, limit: 0, pagination: false, depth: 0, overrideAccess: true })\n const grants = (records.docs as unknown as OAuthRecord[]).filter(g => String(g.data.user) === String(req.user!.id))\n // Setup steps show only to accounts that may connect; a failing policy must not block disconnecting.\n const eligible = await session(req).catch(() => false)\n return json({ resource, access: options.access ?? 'read-only', eligible, grants: grants.map(g => ({\n clientName: new URL(String(g.data.redirect)).hostname, scope: String(g.data.scope),\n // Current level: the site's access setting may have dropped write since consent.\n summary: summarize(g.data.permissions as KeyScopes | undefined, levelOf(scopes(String(g.data.scope))), catalog),\n consent: signed(req.payload, { user: req.user!.id, grant: g.key, expires: Date.now() + 600_000 }),\n })) })\n })\n endpoint('/connections', 'post', async req => {\n if (!req.user || req.user.collection !== userCollection || req.headers.has('authorization') || (req.user as Record<string, unknown>)._mcpKey) throw new OAuthError('access_denied', 403)\n const params = await parameters(req)\n const consent = verified(req.payload, params.get('consent') ?? '')\n csrf(req, consent)\n const grant = await readRecord(req.payload, String(consent.grant))\n if (!grant || grant.kind !== 'grant' || String(grant.data.user) !== String(req.user.id)) throw new OAuthError('access_denied', 403)\n await revokeGrant(req.payload, grant)\n return redirect(`${baseURL}/connections`)\n })\n\n async function authenticate(req: PayloadRequest): Promise<boolean> {\n const token = extractBearerToken(req.headers.get('authorization'))\n if (!token?.startsWith('mcp_at_')) return false\n try {\n const record = await readRecord(req.payload, tokenKey(req.payload, token))\n if (!isLive(record) || record.kind !== 'access' || record.data.resource !== resource) return false\n const grant = await grantFor(req.payload, record)\n const user = await userFor(req, grant)\n const scope = scopes(String(record.data.scope))\n if (!scope) return false\n // Grants store what the user picked at consent; older grants have no list and get the whole level.\n const stored = grant.data.permissions as KeyScopes | undefined\n req.user = { ...user, _strategy: 'mcp-toolkit-oauth', _mcpKey: { keyId: grant.id, keyPrefix: null,\n scopes: stored ? clampScopes(stored, levelOf(scope)) : { preset: levelOf(scope) } } } as PayloadRequest['user']\n return true\n } catch (error) {\n if (!(error instanceof OAuthError)) req.payload.logger.error({ event: 'mcp.oauth.authentication_failed' }, '[payload-mcp-toolkit] OAuth authentication failed')\n return false\n }\n }\n // Clients request the challenge scope first, so it must include write when the site allows it.\n return { endpoints, authenticate, metadataURL, scope: supportedScopes.join(' ') }\n}\n"],"names":["createHash","randomBytes","extractBearerToken","hashKey","verifyHash","clampScopes","permissionsFrom","summarize","consumeRecord","isLive","readRecord","revokeGrant","tokenKey","writeRecord","OAUTH_COLLECTION","BASE","READ","WRITE","ACCESS_SECONDS","GRANT_SECONDS","DEFAULT_REDIRECTS","OAuthError","Error","code","status","levelOf","scope","split","includes","randomToken","toString","expires","seconds","Date","now","toISOString","json","body","Response","headers","Pragma","redirect","url","Location","parameters","req","limit","text","get","startsWith","length","params","URLSearchParams","key","keys","getAll","createOAuth","options","config","userCollection","catalog","canAuthorize","origin","URL","serverURL","pathname","search","hash","username","password","protocol","hostname","routes","api","collections","some","c","slug","auth","issuer","baseURL","resource","metadataURL","adminURL","admin","redirects","redirectURIs","value","searchParams","has","supportedScopes","access","clients","payload","map","uri","id","secret","client","result","find","scopes","Set","filter","s","sort","join","signed","Buffer","from","JSON","stringify","verified","signature","extra","data","parse","session","user","collection","_mcpKey","_verified","lockUntil","login","requestURL","returnTo","loginURL","encodeURIComponent","href","csrf","String","grantFor","record","grant","undefined","kind","userFor","where","equals","depth","overrideAccess","docs","authenticated","policyReq","Object","assign","create","getPrototypeOf","issue","refresh","expiresAt","access_token","refresh_token","token_type","expires_in","endpoints","endpoint","path","method","handler","push","error","logger","event","authorization_servers","scopes_supported","bearer_methods_supported","authorization_endpoint","token_endpoint","registration_endpoint","revocation_endpoint","response_types_supported","grant_types_supported","token_endpoint_auth_methods_supported","revocation_endpoint_auth_methods_supported","code_challenge_methods_supported","authorization_response_iss_parameter_supported","Array","isArray","redirect_uris","token_endpoint_auth_method","grant_types","t","response_types","registered","client_id","client_name","test","consent","challenge","state","nonce","clientName","account","email","callback","set","choice","level","picked","permissions","used","Number","owner","type","raw","verifier","update","digest","records","and","greater_than","pagination","grants","g","eligible","catch","summary","authenticate","token","stored","_strategy","keyId","keyPrefix","preset"],"mappings":"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"}
package/dist/prompts.js CHANGED
@@ -1,8 +1,8 @@
1
- /**
2
- * Generate MCP prompts that teach the AI about the content model.
3
- *
4
- * Auto-generates content model overview, block composition guide, and
5
- * draft workflow guide. User-supplied domain prompts are appended.
1
+ /**
2
+ * Generate MCP prompts that teach the AI about the content model.
3
+ *
4
+ * Auto-generates content model overview, block composition guide, and
5
+ * draft workflow guide. User-supplied domain prompts are appended.
6
6
  */ export function generatePrompts(schemas, catalog, nesting, relationships, domainPrompts) {
7
7
  const prompts = [
8
8
  buildContentModelOverview(schemas, relationships),