payload-mcp-toolkit 0.3.4 → 0.7.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/README.md +232 -151
- package/dist/__tests__/api-keys.test.js +292 -0
- package/dist/__tests__/api-keys.test.js.map +1 -0
- package/dist/__tests__/auth-strategy.test.js +681 -0
- package/dist/__tests__/auth-strategy.test.js.map +1 -0
- package/dist/__tests__/conflict-detection.test.js +69 -0
- package/dist/__tests__/conflict-detection.test.js.map +1 -0
- package/dist/__tests__/delete-document.test.js +70 -0
- package/dist/__tests__/delete-document.test.js.map +1 -0
- package/dist/__tests__/endpoint.test.js +143 -0
- package/dist/__tests__/endpoint.test.js.map +1 -0
- package/dist/__tests__/find-document.test.js +178 -0
- package/dist/__tests__/find-document.test.js.map +1 -0
- package/dist/__tests__/find-global.test.js +173 -0
- package/dist/__tests__/find-global.test.js.map +1 -0
- package/dist/__tests__/global-versions.test.js +183 -0
- package/dist/__tests__/global-versions.test.js.map +1 -0
- package/dist/__tests__/hash.test.js +58 -0
- package/dist/__tests__/hash.test.js.map +1 -0
- package/dist/__tests__/index-integration.test.js +191 -0
- package/dist/__tests__/index-integration.test.js.map +1 -0
- package/dist/__tests__/introspection.test.js +201 -1
- package/dist/__tests__/introspection.test.js.map +1 -1
- package/dist/__tests__/patch-global-layout.test.js +474 -0
- package/dist/__tests__/patch-global-layout.test.js.map +1 -0
- package/dist/__tests__/patch-layout.test.js +171 -0
- package/dist/__tests__/patch-layout.test.js.map +1 -0
- package/dist/__tests__/registry.test.js +795 -0
- package/dist/__tests__/registry.test.js.map +1 -0
- package/dist/__tests__/resources.test.js +139 -0
- package/dist/__tests__/resources.test.js.map +1 -0
- package/dist/__tests__/update-global.test.js +157 -0
- package/dist/__tests__/update-global.test.js.map +1 -0
- package/dist/api-keys.d.ts +46 -0
- package/dist/api-keys.js +272 -0
- package/dist/api-keys.js.map +1 -0
- package/dist/auth-strategy.d.ts +85 -0
- package/dist/auth-strategy.js +219 -0
- package/dist/auth-strategy.js.map +1 -0
- package/dist/components/CollectionScopesMatrix.d.ts +8 -0
- package/dist/components/CollectionScopesMatrix.js +32 -0
- package/dist/components/CollectionScopesMatrix.js.map +1 -0
- package/dist/components/GlobalScopesMatrix.d.ts +8 -0
- package/dist/components/GlobalScopesMatrix.js +28 -0
- package/dist/components/GlobalScopesMatrix.js.map +1 -0
- package/dist/components/ScopesTable.d.ts +19 -0
- package/dist/components/ScopesTable.js +285 -0
- package/dist/components/ScopesTable.js.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +4 -0
- package/dist/components/index.js.map +1 -0
- package/dist/conflict-detection.d.ts +13 -0
- package/dist/conflict-detection.js +41 -0
- package/dist/conflict-detection.js.map +1 -0
- package/dist/draft-workflow.d.ts +46 -48
- package/dist/draft-workflow.js +53 -135
- package/dist/draft-workflow.js.map +1 -1
- package/dist/endpoint.d.ts +35 -0
- package/dist/endpoint.js +105 -0
- package/dist/endpoint.js.map +1 -0
- package/dist/hash.d.ts +21 -0
- package/dist/hash.js +36 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +9 -9
- package/dist/index.js +167 -69
- package/dist/index.js.map +1 -1
- package/dist/introspection.d.ts +17 -3
- package/dist/introspection.js +95 -36
- package/dist/introspection.js.map +1 -1
- package/dist/prompts.js +5 -5
- package/dist/prompts.js.map +1 -1
- package/dist/registry.d.ts +50 -0
- package/dist/registry.js +169 -0
- package/dist/registry.js.map +1 -0
- package/dist/resources.d.ts +5 -3
- package/dist/resources.js +23 -11
- package/dist/resources.js.map +1 -1
- package/dist/scope/audit-log.d.ts +18 -0
- package/dist/scope/audit-log.js +50 -0
- package/dist/scope/audit-log.js.map +1 -0
- package/dist/scope/policy.d.ts +73 -0
- package/dist/scope/policy.js +218 -0
- package/dist/scope/policy.js.map +1 -0
- package/dist/tools/_helpers.d.ts +28 -1
- package/dist/tools/_helpers.js +83 -0
- package/dist/tools/_helpers.js.map +1 -1
- package/dist/tools/_layout-helpers.d.ts +43 -0
- package/dist/tools/_layout-helpers.js +159 -0
- package/dist/tools/_layout-helpers.js.map +1 -0
- package/dist/tools/create-document.d.ts +5 -5
- package/dist/tools/create-document.js +25 -21
- package/dist/tools/create-document.js.map +1 -1
- package/dist/tools/delete-document.d.ts +25 -0
- package/dist/tools/delete-document.js +49 -0
- package/dist/tools/delete-document.js.map +1 -0
- package/dist/tools/find-document.d.ts +33 -0
- package/dist/tools/find-document.js +97 -0
- package/dist/tools/find-document.js.map +1 -0
- package/dist/tools/find-global.d.ts +26 -0
- package/dist/tools/find-global.js +122 -0
- package/dist/tools/find-global.js.map +1 -0
- package/dist/tools/global-versions.d.ts +39 -0
- package/dist/tools/global-versions.js +132 -0
- package/dist/tools/global-versions.js.map +1 -0
- package/dist/tools/patch-global-layout.d.ts +31 -0
- package/dist/tools/patch-global-layout.js +127 -0
- package/dist/tools/patch-global-layout.js.map +1 -0
- package/dist/tools/patch-layout.d.ts +5 -8
- package/dist/tools/patch-layout.js +18 -100
- package/dist/tools/patch-layout.js.map +1 -1
- package/dist/tools/publish-draft.d.ts +5 -4
- package/dist/tools/publish-draft.js +6 -1
- package/dist/tools/publish-draft.js.map +1 -1
- package/dist/tools/publish-global-draft.d.ts +20 -0
- package/dist/tools/publish-global-draft.js +50 -0
- package/dist/tools/publish-global-draft.js.map +1 -0
- package/dist/tools/resolve-reference.d.ts +5 -4
- package/dist/tools/resolve-reference.js +4 -0
- package/dist/tools/resolve-reference.js.map +1 -1
- package/dist/tools/safe-delete.d.ts +5 -5
- package/dist/tools/safe-delete.js +20 -15
- package/dist/tools/safe-delete.js.map +1 -1
- package/dist/tools/schedule-publish.d.ts +5 -5
- package/dist/tools/schedule-publish.js +23 -19
- package/dist/tools/schedule-publish.js.map +1 -1
- package/dist/tools/search-content.d.ts +5 -9
- package/dist/tools/search-content.js +16 -12
- package/dist/tools/search-content.js.map +1 -1
- package/dist/tools/update-document.d.ts +5 -5
- package/dist/tools/update-document.js +10 -5
- package/dist/tools/update-document.js.map +1 -1
- package/dist/tools/update-global.d.ts +27 -0
- package/dist/tools/update-global.js +72 -0
- package/dist/tools/update-global.js.map +1 -0
- package/dist/tools/upload-media.d.ts +5 -4
- package/dist/tools/upload-media.js +6 -1
- package/dist/tools/upload-media.js.map +1 -1
- package/dist/tools/versions.d.ts +10 -9
- package/dist/tools/versions.js +15 -7
- package/dist/tools/versions.js.map +1 -1
- package/dist/types.d.ts +56 -3
- package/dist/types.js +13 -6
- package/dist/types.js.map +1 -1
- package/package.json +11 -4
package/dist/api-keys.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
export const API_KEYS_DEFAULT_SLUG = 'payload-mcp-api-keys';
|
|
2
|
+
const PRESET_OPTIONS = [
|
|
3
|
+
{
|
|
4
|
+
label: 'Read-only',
|
|
5
|
+
value: 'read-only'
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
label: 'Editor (read + create + update)',
|
|
9
|
+
value: 'editor'
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
label: 'Admin (all actions)',
|
|
13
|
+
value: 'admin'
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
label: 'Custom (use overrides below)',
|
|
17
|
+
value: 'custom'
|
|
18
|
+
}
|
|
19
|
+
];
|
|
20
|
+
const isCustomPreset = (data)=>!!data && typeof data === 'object' && data.preset === 'custom';
|
|
21
|
+
/**
|
|
22
|
+
* Builds the `payload-mcp-api-keys` collection used by the v0.4 standalone
|
|
23
|
+
* plugin. Reuses Payload's built-in `useAPIKey: true` so the underlying
|
|
24
|
+
* `apiKey` / `apiKeyIndex` columns match what `@payloadcms/plugin-mcp`
|
|
25
|
+
* v0.3.x wrote — existing rows authenticate without re-issue.
|
|
26
|
+
*
|
|
27
|
+
* Layout:
|
|
28
|
+
* - Main column: name, description, preset, scopes matrix (custom only),
|
|
29
|
+
* tools collapsible (custom only).
|
|
30
|
+
* - Sidebar: user relationship, key prefix, expiresAt, revokedAt,
|
|
31
|
+
* lastUsedAt — identity + lifecycle metadata kept out of the
|
|
32
|
+
* scope-editing flow.
|
|
33
|
+
*/ export function createApiKeysCollection(options) {
|
|
34
|
+
if (!options || !options.userCollection) {
|
|
35
|
+
throw new Error('createApiKeysCollection: `userCollection` is required (slug of the user collection that owns API keys).');
|
|
36
|
+
}
|
|
37
|
+
if (!Array.isArray(options.availableCollections)) {
|
|
38
|
+
throw new Error('createApiKeysCollection: `availableCollections` is required (slugs of collections that scope overrides may target).');
|
|
39
|
+
}
|
|
40
|
+
if (!Array.isArray(options.availableTools)) {
|
|
41
|
+
throw new Error('createApiKeysCollection: `availableTools` is required (names of registered MCP tools).');
|
|
42
|
+
}
|
|
43
|
+
const slug = options.slug ?? API_KEYS_DEFAULT_SLUG;
|
|
44
|
+
const toolOptions = options.availableTools.map((t)=>({
|
|
45
|
+
label: t,
|
|
46
|
+
value: t
|
|
47
|
+
}));
|
|
48
|
+
const availableGlobals = Array.isArray(options.availableGlobals) ? options.availableGlobals : [];
|
|
49
|
+
const presetField = {
|
|
50
|
+
name: 'preset',
|
|
51
|
+
type: 'select',
|
|
52
|
+
required: true,
|
|
53
|
+
defaultValue: 'custom',
|
|
54
|
+
options: PRESET_OPTIONS,
|
|
55
|
+
admin: {
|
|
56
|
+
description: 'Role preset. "Custom" unlocks the per-collection matrix and the tool overrides below.'
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
// Stored shape: Array<{ slug: string; actions: ('read'|'create'|'update'|'delete')[] }>
|
|
60
|
+
// The default Payload UI for an `array` would force users to add rows
|
|
61
|
+
// one at a time; the custom matrix component renders all available
|
|
62
|
+
// collections at once with a checkbox grid (rows × actions).
|
|
63
|
+
//
|
|
64
|
+
// `availableCollections` is forwarded via `clientProps` — Payload v3's
|
|
65
|
+
// sanctioned escape hatch for serializable static data that the client
|
|
66
|
+
// component needs at render time.
|
|
67
|
+
const collectionScopesField = {
|
|
68
|
+
name: 'collectionScopes',
|
|
69
|
+
type: 'json',
|
|
70
|
+
admin: {
|
|
71
|
+
condition: isCustomPreset,
|
|
72
|
+
components: {
|
|
73
|
+
Field: {
|
|
74
|
+
path: 'payload-mcp-toolkit/client',
|
|
75
|
+
exportName: 'CollectionScopesMatrix',
|
|
76
|
+
clientProps: {
|
|
77
|
+
availableCollections: options.availableCollections
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
// Mirrors `collectionScopes` exactly — one additive JSONB column with a
|
|
84
|
+
// default of `'[]'`, default-rendered by `GlobalScopesMatrix`. Hidden
|
|
85
|
+
// under non-custom presets. Stored shape:
|
|
86
|
+
// Array<{ slug: string; actions: ('read'|'update')[] }>
|
|
87
|
+
// No `availableGlobals.length > 0` gate: `ScopesTable` renders its own
|
|
88
|
+
// empty-state message when zero items are passed, so the field surfaces
|
|
89
|
+
// under Custom regardless of host config, matching the collection variant.
|
|
90
|
+
const globalScopesField = {
|
|
91
|
+
name: 'globalScopes',
|
|
92
|
+
type: 'json',
|
|
93
|
+
admin: {
|
|
94
|
+
condition: isCustomPreset,
|
|
95
|
+
components: {
|
|
96
|
+
Field: {
|
|
97
|
+
path: 'payload-mcp-toolkit/client',
|
|
98
|
+
exportName: 'GlobalScopesMatrix',
|
|
99
|
+
clientProps: {
|
|
100
|
+
availableGlobals
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const toolsCollapsible = {
|
|
107
|
+
type: 'collapsible',
|
|
108
|
+
label: 'Tool overrides',
|
|
109
|
+
admin: {
|
|
110
|
+
condition: isCustomPreset,
|
|
111
|
+
description: 'Per-tool whitelist / blacklist. Layered on top of preset and collection scopes.',
|
|
112
|
+
initCollapsed: true
|
|
113
|
+
},
|
|
114
|
+
fields: [
|
|
115
|
+
{
|
|
116
|
+
name: 'toolAllow',
|
|
117
|
+
type: 'select',
|
|
118
|
+
hasMany: true,
|
|
119
|
+
options: toolOptions,
|
|
120
|
+
admin: {
|
|
121
|
+
description: 'If set, only these tools are callable with this key. Leave empty to allow any tool the collection scopes permit. Under the Custom preset, an explicit empty list is treated as deny-all on this axis; preset-mode keys created via the REST API with an empty list are coerced to "no restriction".'
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'toolDeny',
|
|
126
|
+
type: 'select',
|
|
127
|
+
hasMany: true,
|
|
128
|
+
options: toolOptions,
|
|
129
|
+
admin: {
|
|
130
|
+
description: 'These tools are blocked regardless of any other scope.'
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
slug,
|
|
137
|
+
admin: {
|
|
138
|
+
group: 'MCP',
|
|
139
|
+
useAsTitle: 'name',
|
|
140
|
+
description: 'API keys for MCP clients. Scopes control which collections and tools each key can access.',
|
|
141
|
+
defaultColumns: [
|
|
142
|
+
'name',
|
|
143
|
+
'user',
|
|
144
|
+
'keyPrefix',
|
|
145
|
+
'preset',
|
|
146
|
+
'lastUsedAt',
|
|
147
|
+
'expiresAt',
|
|
148
|
+
'revokedAt'
|
|
149
|
+
]
|
|
150
|
+
},
|
|
151
|
+
auth: {
|
|
152
|
+
disableLocalStrategy: true,
|
|
153
|
+
useAPIKey: true
|
|
154
|
+
},
|
|
155
|
+
hooks: {
|
|
156
|
+
beforeValidate: [
|
|
157
|
+
({ data })=>{
|
|
158
|
+
// REST clients that create a key with a preset (e.g. admin) but
|
|
159
|
+
// omit toolAllow / toolDeny would hit Payload's hasMany-select
|
|
160
|
+
// default of `[]`, which composeScopes correctly interprets as
|
|
161
|
+
// "deny-all on this axis" (v0.6 Track A). That is the right
|
|
162
|
+
// semantic for explicit-Custom configurations but a UX trap for
|
|
163
|
+
// preset-mode keys created via the REST API. Collapse empty
|
|
164
|
+
// arrays to null when the key is NOT on the Custom preset, so
|
|
165
|
+
// preset-driven access flows through unimpeded. Custom-preset
|
|
166
|
+
// keys keep the explicit-empty-means-deny semantic intact.
|
|
167
|
+
if (!data) return data;
|
|
168
|
+
const preset = data.preset;
|
|
169
|
+
if (preset === 'custom') return data;
|
|
170
|
+
for (const axis of [
|
|
171
|
+
'toolAllow',
|
|
172
|
+
'toolDeny'
|
|
173
|
+
]){
|
|
174
|
+
const v = data[axis];
|
|
175
|
+
if (Array.isArray(v) && v.length === 0) {
|
|
176
|
+
;
|
|
177
|
+
data[axis] = null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return data;
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
},
|
|
184
|
+
labels: {
|
|
185
|
+
plural: 'API Keys',
|
|
186
|
+
singular: 'API Key'
|
|
187
|
+
},
|
|
188
|
+
fields: [
|
|
189
|
+
// Main column.
|
|
190
|
+
{
|
|
191
|
+
name: 'name',
|
|
192
|
+
type: 'text',
|
|
193
|
+
required: true,
|
|
194
|
+
admin: {
|
|
195
|
+
description: 'Human label for this key (e.g. "Editorial team — Claude Desktop").'
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'description',
|
|
200
|
+
type: 'textarea',
|
|
201
|
+
admin: {
|
|
202
|
+
description: 'Optional notes about the purpose of this key.'
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
presetField,
|
|
206
|
+
collectionScopesField,
|
|
207
|
+
globalScopesField,
|
|
208
|
+
toolsCollapsible,
|
|
209
|
+
// Sidebar — identity + lifecycle.
|
|
210
|
+
{
|
|
211
|
+
name: 'user',
|
|
212
|
+
type: 'relationship',
|
|
213
|
+
relationTo: options.userCollection,
|
|
214
|
+
required: true,
|
|
215
|
+
admin: {
|
|
216
|
+
position: 'sidebar',
|
|
217
|
+
description: 'The user this key authenticates as. Tool calls use this user for access checks on target collections.'
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'keyPrefix',
|
|
222
|
+
type: 'text',
|
|
223
|
+
index: true,
|
|
224
|
+
admin: {
|
|
225
|
+
position: 'sidebar',
|
|
226
|
+
readOnly: true,
|
|
227
|
+
description: 'First 8 characters of the API key — used in audit logs to identify the key without exposing the full secret.'
|
|
228
|
+
},
|
|
229
|
+
hooks: {
|
|
230
|
+
beforeChange: [
|
|
231
|
+
({ data, originalDoc, value })=>{
|
|
232
|
+
if (typeof value === 'string' && value.length > 0) return value;
|
|
233
|
+
const incomingKey = data?.apiKey;
|
|
234
|
+
if (typeof incomingKey === 'string' && incomingKey.length >= 8) {
|
|
235
|
+
return incomingKey.slice(0, 8);
|
|
236
|
+
}
|
|
237
|
+
const existing = originalDoc?.keyPrefix;
|
|
238
|
+
return typeof existing === 'string' ? existing : undefined;
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: 'expiresAt',
|
|
245
|
+
type: 'date',
|
|
246
|
+
admin: {
|
|
247
|
+
position: 'sidebar',
|
|
248
|
+
description: 'Optional expiry. Requests authenticated with an expired key are rejected.'
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: 'revokedAt',
|
|
253
|
+
type: 'date',
|
|
254
|
+
admin: {
|
|
255
|
+
position: 'sidebar',
|
|
256
|
+
description: 'Set to revoke a key. Revoked keys are rejected at auth time.'
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: 'lastUsedAt',
|
|
261
|
+
type: 'date',
|
|
262
|
+
admin: {
|
|
263
|
+
position: 'sidebar',
|
|
264
|
+
readOnly: true,
|
|
265
|
+
description: 'Updated on each successful authentication. Fire-and-forget; not on the request hot path.'
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
]
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
//# sourceMappingURL=api-keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/api-keys.ts"],"sourcesContent":["import type { CollectionConfig, Field } from 'payload'\r\n\r\nexport const API_KEYS_DEFAULT_SLUG = 'payload-mcp-api-keys'\r\n\r\nexport interface CreateApiKeysCollectionOptions {\r\n /**\r\n * Collection slug. Defaults to `payload-mcp-api-keys` for zero-touch\r\n * compatibility with rows created by `@payloadcms/plugin-mcp` v0.3.x.\r\n */\r\n slug?: string\r\n /**\r\n * Slug of the user collection that API keys link to. Required.\r\n */\r\n userCollection: string\r\n /**\r\n * Collection slugs offered to the collection-scopes matrix component.\r\n * Snapshotted at plugin-init time from the host Payload config; adding a\r\n * collection requires a dev-server restart for it to surface in the\r\n * admin UI.\r\n */\r\n availableCollections: string[]\r\n /**\r\n * Tool names offered as options for the `toolAllow` / `toolDeny` selects.\r\n * Sourced from the toolkit's registered tools at plugin init.\r\n */\r\n availableTools: string[]\r\n /**\r\n * Global slugs offered to the global-scopes matrix component. Optional —\r\n * direct callers of the factory that pre-date globals support continue\r\n * to work; sites with no globals get an empty array and the second\r\n * matrix table is not rendered.\r\n */\r\n availableGlobals?: string[]\r\n}\r\n\r\nconst PRESET_OPTIONS = [\r\n { label: 'Read-only', value: 'read-only' },\r\n { label: 'Editor (read + create + update)', value: 'editor' },\r\n { label: 'Admin (all actions)', value: 'admin' },\r\n { label: 'Custom (use overrides below)', value: 'custom' },\r\n] as const\r\n\r\nconst isCustomPreset = (data: unknown): boolean =>\r\n !!data && typeof data === 'object' && (data as { preset?: unknown }).preset === 'custom'\r\n\r\n/**\r\n * Builds the `payload-mcp-api-keys` collection used by the v0.4 standalone\r\n * plugin. Reuses Payload's built-in `useAPIKey: true` so the underlying\r\n * `apiKey` / `apiKeyIndex` columns match what `@payloadcms/plugin-mcp`\r\n * v0.3.x wrote — existing rows authenticate without re-issue.\r\n *\r\n * Layout:\r\n * - Main column: name, description, preset, scopes matrix (custom only),\r\n * tools collapsible (custom only).\r\n * - Sidebar: user relationship, key prefix, expiresAt, revokedAt,\r\n * lastUsedAt — identity + lifecycle metadata kept out of the\r\n * scope-editing flow.\r\n */\r\nexport function createApiKeysCollection(\r\n options: CreateApiKeysCollectionOptions,\r\n): CollectionConfig {\r\n if (!options || !options.userCollection) {\r\n throw new Error(\r\n 'createApiKeysCollection: `userCollection` is required (slug of the user collection that owns API keys).',\r\n )\r\n }\r\n if (!Array.isArray(options.availableCollections)) {\r\n throw new Error(\r\n 'createApiKeysCollection: `availableCollections` is required (slugs of collections that scope overrides may target).',\r\n )\r\n }\r\n if (!Array.isArray(options.availableTools)) {\r\n throw new Error(\r\n 'createApiKeysCollection: `availableTools` is required (names of registered MCP tools).',\r\n )\r\n }\r\n\r\n const slug = options.slug ?? API_KEYS_DEFAULT_SLUG\r\n const toolOptions = options.availableTools.map((t) => ({ label: t, value: t }))\r\n const availableGlobals = Array.isArray(options.availableGlobals)\r\n ? options.availableGlobals\r\n : []\r\n\r\n const presetField: Field = {\r\n name: 'preset',\r\n type: 'select',\r\n required: true,\r\n defaultValue: 'custom',\r\n options: PRESET_OPTIONS as unknown as { label: string; value: string }[],\r\n admin: {\r\n description:\r\n 'Role preset. \"Custom\" unlocks the per-collection matrix and the tool overrides below.',\r\n },\r\n }\r\n\r\n // Stored shape: Array<{ slug: string; actions: ('read'|'create'|'update'|'delete')[] }>\r\n // The default Payload UI for an `array` would force users to add rows\r\n // one at a time; the custom matrix component renders all available\r\n // collections at once with a checkbox grid (rows × actions).\r\n //\r\n // `availableCollections` is forwarded via `clientProps` — Payload v3's\r\n // sanctioned escape hatch for serializable static data that the client\r\n // component needs at render time.\r\n const collectionScopesField: Field = {\r\n name: 'collectionScopes',\r\n type: 'json',\r\n admin: {\r\n condition: isCustomPreset,\r\n components: {\r\n Field: {\r\n path: 'payload-mcp-toolkit/client',\r\n exportName: 'CollectionScopesMatrix',\r\n clientProps: {\r\n availableCollections: options.availableCollections,\r\n },\r\n },\r\n },\r\n },\r\n }\r\n\r\n // Mirrors `collectionScopes` exactly — one additive JSONB column with a\r\n // default of `'[]'`, default-rendered by `GlobalScopesMatrix`. Hidden\r\n // under non-custom presets. Stored shape:\r\n // Array<{ slug: string; actions: ('read'|'update')[] }>\r\n // No `availableGlobals.length > 0` gate: `ScopesTable` renders its own\r\n // empty-state message when zero items are passed, so the field surfaces\r\n // under Custom regardless of host config, matching the collection variant.\r\n const globalScopesField: Field = {\r\n name: 'globalScopes',\r\n type: 'json',\r\n admin: {\r\n condition: isCustomPreset,\r\n components: {\r\n Field: {\r\n path: 'payload-mcp-toolkit/client',\r\n exportName: 'GlobalScopesMatrix',\r\n clientProps: {\r\n availableGlobals,\r\n },\r\n },\r\n },\r\n },\r\n }\r\n\r\n const toolsCollapsible: Field = {\r\n type: 'collapsible',\r\n label: 'Tool overrides',\r\n admin: {\r\n condition: isCustomPreset,\r\n description:\r\n 'Per-tool whitelist / blacklist. Layered on top of preset and collection scopes.',\r\n initCollapsed: true,\r\n },\r\n fields: [\r\n {\r\n name: 'toolAllow',\r\n type: 'select',\r\n hasMany: true,\r\n options: toolOptions,\r\n admin: {\r\n description:\r\n 'If set, only these tools are callable with this key. Leave empty to allow any tool the collection scopes permit. Under the Custom preset, an explicit empty list is treated as deny-all on this axis; preset-mode keys created via the REST API with an empty list are coerced to \"no restriction\".',\r\n },\r\n },\r\n {\r\n name: 'toolDeny',\r\n type: 'select',\r\n hasMany: true,\r\n options: toolOptions,\r\n admin: {\r\n description: 'These tools are blocked regardless of any other scope.',\r\n },\r\n },\r\n ],\r\n }\r\n\r\n return {\r\n slug,\r\n admin: {\r\n group: 'MCP',\r\n useAsTitle: 'name',\r\n description:\r\n 'API keys for MCP clients. Scopes control which collections and tools each key can access.',\r\n defaultColumns: ['name', 'user', 'keyPrefix', 'preset', 'lastUsedAt', 'expiresAt', 'revokedAt'],\r\n },\r\n auth: {\r\n disableLocalStrategy: true,\r\n useAPIKey: true,\r\n },\r\n hooks: {\r\n beforeValidate: [\r\n ({ data }) => {\r\n // REST clients that create a key with a preset (e.g. admin) but\r\n // omit toolAllow / toolDeny would hit Payload's hasMany-select\r\n // default of `[]`, which composeScopes correctly interprets as\r\n // \"deny-all on this axis\" (v0.6 Track A). That is the right\r\n // semantic for explicit-Custom configurations but a UX trap for\r\n // preset-mode keys created via the REST API. Collapse empty\r\n // arrays to null when the key is NOT on the Custom preset, so\r\n // preset-driven access flows through unimpeded. Custom-preset\r\n // keys keep the explicit-empty-means-deny semantic intact.\r\n if (!data) return data\r\n const preset = (data as { preset?: unknown }).preset\r\n if (preset === 'custom') return data\r\n for (const axis of ['toolAllow', 'toolDeny'] as const) {\r\n const v = (data as Record<string, unknown>)[axis]\r\n if (Array.isArray(v) && v.length === 0) {\r\n ;(data as Record<string, unknown>)[axis] = null\r\n }\r\n }\r\n return data\r\n },\r\n ],\r\n },\r\n labels: {\r\n plural: 'API Keys',\r\n singular: 'API Key',\r\n },\r\n fields: [\r\n // Main column.\r\n {\r\n name: 'name',\r\n type: 'text',\r\n required: true,\r\n admin: { description: 'Human label for this key (e.g. \"Editorial team — Claude Desktop\").' },\r\n },\r\n {\r\n name: 'description',\r\n type: 'textarea',\r\n admin: { description: 'Optional notes about the purpose of this key.' },\r\n },\r\n presetField,\r\n collectionScopesField,\r\n globalScopesField,\r\n toolsCollapsible,\r\n\r\n // Sidebar — identity + lifecycle.\r\n {\r\n name: 'user',\r\n type: 'relationship',\r\n relationTo: options.userCollection,\r\n required: true,\r\n admin: {\r\n position: 'sidebar',\r\n description:\r\n 'The user this key authenticates as. Tool calls use this user for access checks on target collections.',\r\n },\r\n },\r\n {\r\n name: 'keyPrefix',\r\n type: 'text',\r\n index: true,\r\n admin: {\r\n position: 'sidebar',\r\n readOnly: true,\r\n description:\r\n 'First 8 characters of the API key — used in audit logs to identify the key without exposing the full secret.',\r\n },\r\n hooks: {\r\n beforeChange: [\r\n ({ data, originalDoc, value }) => {\r\n if (typeof value === 'string' && value.length > 0) return value\r\n const incomingKey = (data as { apiKey?: unknown } | undefined)?.apiKey\r\n if (typeof incomingKey === 'string' && incomingKey.length >= 8) {\r\n return incomingKey.slice(0, 8)\r\n }\r\n const existing = (originalDoc as { keyPrefix?: unknown } | undefined)?.keyPrefix\r\n return typeof existing === 'string' ? existing : undefined\r\n },\r\n ],\r\n },\r\n },\r\n {\r\n name: 'expiresAt',\r\n type: 'date',\r\n admin: {\r\n position: 'sidebar',\r\n description: 'Optional expiry. Requests authenticated with an expired key are rejected.',\r\n },\r\n },\r\n {\r\n name: 'revokedAt',\r\n type: 'date',\r\n admin: {\r\n position: 'sidebar',\r\n description: 'Set to revoke a key. Revoked keys are rejected at auth time.',\r\n },\r\n },\r\n {\r\n name: 'lastUsedAt',\r\n type: 'date',\r\n admin: {\r\n position: 'sidebar',\r\n readOnly: true,\r\n description:\r\n 'Updated on each successful authentication. Fire-and-forget; not on the request hot path.',\r\n },\r\n },\r\n ],\r\n }\r\n}\r\n"],"names":["API_KEYS_DEFAULT_SLUG","PRESET_OPTIONS","label","value","isCustomPreset","data","preset","createApiKeysCollection","options","userCollection","Error","Array","isArray","availableCollections","availableTools","slug","toolOptions","map","t","availableGlobals","presetField","name","type","required","defaultValue","admin","description","collectionScopesField","condition","components","Field","path","exportName","clientProps","globalScopesField","toolsCollapsible","initCollapsed","fields","hasMany","group","useAsTitle","defaultColumns","auth","disableLocalStrategy","useAPIKey","hooks","beforeValidate","axis","v","length","labels","plural","singular","relationTo","position","index","readOnly","beforeChange","originalDoc","incomingKey","apiKey","slice","existing","keyPrefix","undefined"],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { AuthStrategy, PayloadRequest } from 'payload';
|
|
2
|
+
import type { KeyScopes, ScopePreset } from './types';
|
|
3
|
+
export type { CollectionAction, GlobalAction, KeyScopes, ScopePreset } from './types';
|
|
4
|
+
export declare const AUTH_STRATEGY_NAME = "mcp-toolkit-bearer";
|
|
5
|
+
export interface CreateBearerStrategyOptions {
|
|
6
|
+
/** Slug of the API-keys collection (defaults to `payload-mcp-api-keys`). */
|
|
7
|
+
collectionSlug: string;
|
|
8
|
+
/** Slug of the user collection that API keys link to. */
|
|
9
|
+
userCollection: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Stored shape for a row in `collectionScopes` / `globalScopes` (v0.6+).
|
|
13
|
+
* The parent field name encodes the axis — collection-vs-global is not
|
|
14
|
+
* repeated in the row payload. Legacy `collection` / `global` keys from
|
|
15
|
+
* pre-0.6 rows are tolerated by `composeScopes` for one release; v0.7
|
|
16
|
+
* drops the fallback (see CHANGELOG).
|
|
17
|
+
*/
|
|
18
|
+
interface ScopeRow {
|
|
19
|
+
slug?: unknown;
|
|
20
|
+
/** @deprecated pre-0.6 collectionScopes shape — read via fallback only. */
|
|
21
|
+
collection?: unknown;
|
|
22
|
+
/** @deprecated pre-0.6 globalScopes shape — read via fallback only. */
|
|
23
|
+
global?: unknown;
|
|
24
|
+
actions?: unknown;
|
|
25
|
+
}
|
|
26
|
+
interface ApiKeyRow {
|
|
27
|
+
id: string | number;
|
|
28
|
+
user: unknown;
|
|
29
|
+
preset?: ScopePreset | 'custom' | null;
|
|
30
|
+
collectionScopes?: ScopeRow[] | null;
|
|
31
|
+
globalScopes?: ScopeRow[] | null;
|
|
32
|
+
toolAllow?: string[] | null;
|
|
33
|
+
toolDeny?: string[] | null;
|
|
34
|
+
expiresAt?: string | Date | null;
|
|
35
|
+
revokedAt?: string | Date | null;
|
|
36
|
+
apiKey?: string | null;
|
|
37
|
+
keyPrefix?: string | null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Builds the runtime `KeyScopes` shape consumed by `registry.assertScopeAllows`
|
|
41
|
+
* from the typed scope fields on the api-key row.
|
|
42
|
+
*
|
|
43
|
+
* Returns null when no typed fields are populated AND no preset is set
|
|
44
|
+
* (= full access — back-compat for pre-0.5 rows that pre-date scoped authz).
|
|
45
|
+
*
|
|
46
|
+
* Two complementary fail-closed rules:
|
|
47
|
+
*
|
|
48
|
+
* 1. **`'custom'` deny-all sentinel.** `'custom'` is a UI sentinel meaning
|
|
49
|
+
* "use my override fields"; it never becomes `KeyScopes.preset` itself.
|
|
50
|
+
* Payload persists unset JSON / select fields as `null`, so a fresh
|
|
51
|
+
* Custom key with no overrides arrives as `{preset:'custom',
|
|
52
|
+
* collectionScopes:null, globalScopes:null, toolAllow:null,
|
|
53
|
+
* toolDeny:null}`. That row must deny everything (not fall through to
|
|
54
|
+
* full access). The sentinel emits `{collections:{}, globals:{},
|
|
55
|
+
* tools:{allow:[]}}`.
|
|
56
|
+
*
|
|
57
|
+
* 2. **Per-axis explicit-empty.** When a row carries an array value on any
|
|
58
|
+
* axis (even `[]`), that axis is honoured as written — an empty
|
|
59
|
+
* `toolAllow:[]` means "deny all tools", not "no opinion". This closes a
|
|
60
|
+
* gap where a Custom key with `collectionScopes` populated AND
|
|
61
|
+
* `toolAllow:[]` previously emitted no `tools.allow` gate (the empty
|
|
62
|
+
* array was treated as absent). `toolDeny` is a deny-list, so an empty
|
|
63
|
+
* array carries no entries — it is dropped rather than emitted.
|
|
64
|
+
*/
|
|
65
|
+
export declare function composeScopes(row: ApiKeyRow, logger?: {
|
|
66
|
+
warn?: (...args: unknown[]) => void;
|
|
67
|
+
}): KeyScopes | null;
|
|
68
|
+
/**
|
|
69
|
+
* Builds the Payload `auth.strategies` entry that authenticates MCP requests.
|
|
70
|
+
*
|
|
71
|
+
* Authenticates `Authorization: Bearer <plaintext>` by computing the
|
|
72
|
+
* upstream-compatible `apiKeyIndex` HMAC and looking up the row. On match,
|
|
73
|
+
* it fires a non-blocking `lastUsedAt` write and hydrates `req.user` with
|
|
74
|
+
* the linked user record + key context for downstream scope checks.
|
|
75
|
+
*/
|
|
76
|
+
export declare function createBearerStrategy(options: CreateBearerStrategyOptions): AuthStrategy;
|
|
77
|
+
/**
|
|
78
|
+
* Reads the per-request API-key context populated by the bearer strategy.
|
|
79
|
+
* Returns null for non-MCP requests (e.g. cookie-authenticated admin users).
|
|
80
|
+
*/
|
|
81
|
+
export declare function getApiKeyContext(req: PayloadRequest): {
|
|
82
|
+
keyId: string | number;
|
|
83
|
+
keyPrefix: string | null;
|
|
84
|
+
scopes: KeyScopes | null;
|
|
85
|
+
} | null;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { extractBearerToken, hashKey } from './hash';
|
|
2
|
+
export const AUTH_STRATEGY_NAME = 'mcp-toolkit-bearer';
|
|
3
|
+
/**
|
|
4
|
+
* Reads the row's slug, tolerating the pre-0.6 `collection` / `global`
|
|
5
|
+
* keys for one release. Logs a one-line warn when the legacy fallback
|
|
6
|
+
* fires so operators can spot keys that need re-saving. The fallback is
|
|
7
|
+
* scheduled for removal in v0.7.
|
|
8
|
+
*/ let warnedLegacyShape = false;
|
|
9
|
+
function readScopeSlug(entry, legacyKey, logger) {
|
|
10
|
+
if (typeof entry?.slug === 'string') return entry.slug;
|
|
11
|
+
const legacy = entry?.[legacyKey];
|
|
12
|
+
if (typeof legacy === 'string') {
|
|
13
|
+
if (!warnedLegacyShape) {
|
|
14
|
+
warnedLegacyShape = true;
|
|
15
|
+
logger?.warn?.({
|
|
16
|
+
event: 'mcp.auth.legacy_scope_shape',
|
|
17
|
+
legacyKey
|
|
18
|
+
}, `[payload-mcp-toolkit] composeScopes read a pre-0.6 row using \`${legacyKey}\`; resave the API key to migrate to {slug, actions}. The fallback is removed in v0.7.`);
|
|
19
|
+
}
|
|
20
|
+
return legacy;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const VALID_ACTIONS = new Set([
|
|
25
|
+
'read',
|
|
26
|
+
'create',
|
|
27
|
+
'update',
|
|
28
|
+
'delete'
|
|
29
|
+
]);
|
|
30
|
+
const VALID_GLOBAL_ACTIONS = new Set([
|
|
31
|
+
'read',
|
|
32
|
+
'update'
|
|
33
|
+
]);
|
|
34
|
+
/**
|
|
35
|
+
* Builds the runtime `KeyScopes` shape consumed by `registry.assertScopeAllows`
|
|
36
|
+
* from the typed scope fields on the api-key row.
|
|
37
|
+
*
|
|
38
|
+
* Returns null when no typed fields are populated AND no preset is set
|
|
39
|
+
* (= full access — back-compat for pre-0.5 rows that pre-date scoped authz).
|
|
40
|
+
*
|
|
41
|
+
* Two complementary fail-closed rules:
|
|
42
|
+
*
|
|
43
|
+
* 1. **`'custom'` deny-all sentinel.** `'custom'` is a UI sentinel meaning
|
|
44
|
+
* "use my override fields"; it never becomes `KeyScopes.preset` itself.
|
|
45
|
+
* Payload persists unset JSON / select fields as `null`, so a fresh
|
|
46
|
+
* Custom key with no overrides arrives as `{preset:'custom',
|
|
47
|
+
* collectionScopes:null, globalScopes:null, toolAllow:null,
|
|
48
|
+
* toolDeny:null}`. That row must deny everything (not fall through to
|
|
49
|
+
* full access). The sentinel emits `{collections:{}, globals:{},
|
|
50
|
+
* tools:{allow:[]}}`.
|
|
51
|
+
*
|
|
52
|
+
* 2. **Per-axis explicit-empty.** When a row carries an array value on any
|
|
53
|
+
* axis (even `[]`), that axis is honoured as written — an empty
|
|
54
|
+
* `toolAllow:[]` means "deny all tools", not "no opinion". This closes a
|
|
55
|
+
* gap where a Custom key with `collectionScopes` populated AND
|
|
56
|
+
* `toolAllow:[]` previously emitted no `tools.allow` gate (the empty
|
|
57
|
+
* array was treated as absent). `toolDeny` is a deny-list, so an empty
|
|
58
|
+
* array carries no entries — it is dropped rather than emitted.
|
|
59
|
+
*/ export function composeScopes(row, logger) {
|
|
60
|
+
const presetRaw = row.preset;
|
|
61
|
+
const hasPreset = typeof presetRaw === 'string' && presetRaw.length > 0;
|
|
62
|
+
const hasCollectionScopesField = Array.isArray(row.collectionScopes);
|
|
63
|
+
const hasGlobalScopesField = Array.isArray(row.globalScopes);
|
|
64
|
+
const hasToolAllowField = Array.isArray(row.toolAllow);
|
|
65
|
+
const hasToolDenyField = Array.isArray(row.toolDeny);
|
|
66
|
+
// Pre-0.5 back-compat: row has no preset and no typed scope fields at
|
|
67
|
+
// all (all null/undefined). Treat as full access.
|
|
68
|
+
if (!hasPreset && !hasCollectionScopesField && !hasGlobalScopesField && !hasToolAllowField && !hasToolDenyField) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
// Sentinel: Custom preset with every axis null/undefined (no array
|
|
72
|
+
// committed) denies everything. Payload-persisted unset JSON / select
|
|
73
|
+
// fields arrive as null; this is the fresh-Custom-key case.
|
|
74
|
+
if (presetRaw === 'custom' && !hasCollectionScopesField && !hasGlobalScopesField && !hasToolAllowField && !hasToolDenyField) {
|
|
75
|
+
return {
|
|
76
|
+
collections: {},
|
|
77
|
+
globals: {},
|
|
78
|
+
tools: {
|
|
79
|
+
allow: []
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const out = {};
|
|
84
|
+
if (hasPreset && presetRaw !== 'custom') {
|
|
85
|
+
out.preset = presetRaw;
|
|
86
|
+
}
|
|
87
|
+
if (hasCollectionScopesField) {
|
|
88
|
+
const collections = {};
|
|
89
|
+
for (const entry of row.collectionScopes){
|
|
90
|
+
const slug = readScopeSlug(entry, 'collection', logger);
|
|
91
|
+
if (!slug) continue;
|
|
92
|
+
const rawActions = Array.isArray(entry?.actions) ? entry.actions : [];
|
|
93
|
+
const actions = rawActions.filter((a)=>typeof a === 'string' && VALID_ACTIONS.has(a));
|
|
94
|
+
collections[slug] = actions;
|
|
95
|
+
}
|
|
96
|
+
out.collections = collections;
|
|
97
|
+
}
|
|
98
|
+
if (hasGlobalScopesField) {
|
|
99
|
+
const globals = {};
|
|
100
|
+
for (const entry of row.globalScopes){
|
|
101
|
+
const slug = readScopeSlug(entry, 'global', logger);
|
|
102
|
+
if (!slug) continue;
|
|
103
|
+
const rawActions = Array.isArray(entry?.actions) ? entry.actions : [];
|
|
104
|
+
const actions = rawActions.filter((a)=>typeof a === 'string' && VALID_GLOBAL_ACTIONS.has(a));
|
|
105
|
+
globals[slug] = actions;
|
|
106
|
+
}
|
|
107
|
+
out.globals = globals;
|
|
108
|
+
}
|
|
109
|
+
const toolDeny = hasToolDenyField ? row.toolDeny.filter((t)=>typeof t === 'string') : [];
|
|
110
|
+
const emitDeny = toolDeny.length > 0;
|
|
111
|
+
if (hasToolAllowField || emitDeny) {
|
|
112
|
+
out.tools = {};
|
|
113
|
+
if (hasToolAllowField) {
|
|
114
|
+
const toolAllow = row.toolAllow.filter((t)=>typeof t === 'string');
|
|
115
|
+
out.tools.allow = toolAllow;
|
|
116
|
+
}
|
|
117
|
+
if (emitDeny) out.tools.deny = toolDeny;
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Builds the Payload `auth.strategies` entry that authenticates MCP requests.
|
|
123
|
+
*
|
|
124
|
+
* Authenticates `Authorization: Bearer <plaintext>` by computing the
|
|
125
|
+
* upstream-compatible `apiKeyIndex` HMAC and looking up the row. On match,
|
|
126
|
+
* it fires a non-blocking `lastUsedAt` write and hydrates `req.user` with
|
|
127
|
+
* the linked user record + key context for downstream scope checks.
|
|
128
|
+
*/ export function createBearerStrategy(options) {
|
|
129
|
+
const { collectionSlug, userCollection } = options;
|
|
130
|
+
return {
|
|
131
|
+
name: AUTH_STRATEGY_NAME,
|
|
132
|
+
authenticate: async ({ headers, payload })=>{
|
|
133
|
+
const headersAny = headers;
|
|
134
|
+
const headerValue = typeof headersAny.get === 'function' ? headersAny.get('authorization') : headersAny.authorization;
|
|
135
|
+
const token = extractBearerToken(headerValue ?? null);
|
|
136
|
+
if (!token) return {
|
|
137
|
+
user: null
|
|
138
|
+
};
|
|
139
|
+
const keyHash = hashKey(token, payload.secret);
|
|
140
|
+
const where = {
|
|
141
|
+
apiKeyIndex: {
|
|
142
|
+
equals: keyHash
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
let docs = [];
|
|
146
|
+
try {
|
|
147
|
+
const result = await payload.find({
|
|
148
|
+
collection: collectionSlug,
|
|
149
|
+
where,
|
|
150
|
+
depth: 1,
|
|
151
|
+
limit: 1,
|
|
152
|
+
pagination: false,
|
|
153
|
+
overrideAccess: true
|
|
154
|
+
});
|
|
155
|
+
docs = result.docs ?? [];
|
|
156
|
+
} catch (err) {
|
|
157
|
+
payload.logger.error({
|
|
158
|
+
err,
|
|
159
|
+
event: 'mcp.auth.lookup_failed'
|
|
160
|
+
}, '[payload-mcp-toolkit] API-key lookup failed');
|
|
161
|
+
return {
|
|
162
|
+
user: null
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const row = docs[0];
|
|
166
|
+
if (!row) return {
|
|
167
|
+
user: null
|
|
168
|
+
};
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
if (row.revokedAt) return {
|
|
171
|
+
user: null
|
|
172
|
+
};
|
|
173
|
+
if (row.expiresAt) {
|
|
174
|
+
const expiry = new Date(row.expiresAt).getTime();
|
|
175
|
+
if (Number.isFinite(expiry) && expiry < now) return {
|
|
176
|
+
user: null
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const linkedUser = row.user;
|
|
180
|
+
if (!linkedUser || typeof linkedUser !== 'object') return {
|
|
181
|
+
user: null
|
|
182
|
+
};
|
|
183
|
+
const effectiveScopes = composeScopes(row, payload.logger);
|
|
184
|
+
// Fire-and-forget: do not block the request on this write.
|
|
185
|
+
void payload.update({
|
|
186
|
+
collection: collectionSlug,
|
|
187
|
+
id: row.id,
|
|
188
|
+
data: {
|
|
189
|
+
lastUsedAt: new Date().toISOString()
|
|
190
|
+
},
|
|
191
|
+
overrideAccess: true
|
|
192
|
+
}).catch(()=>{
|
|
193
|
+
// Intentionally swallow: lastUsedAt drift is acceptable.
|
|
194
|
+
});
|
|
195
|
+
const user = linkedUser;
|
|
196
|
+
return {
|
|
197
|
+
user: {
|
|
198
|
+
...user,
|
|
199
|
+
collection: userCollection,
|
|
200
|
+
_strategy: AUTH_STRATEGY_NAME,
|
|
201
|
+
_mcpKey: {
|
|
202
|
+
keyId: row.id,
|
|
203
|
+
keyPrefix: typeof row.keyPrefix === 'string' ? row.keyPrefix : null,
|
|
204
|
+
scopes: effectiveScopes
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Reads the per-request API-key context populated by the bearer strategy.
|
|
213
|
+
* Returns null for non-MCP requests (e.g. cookie-authenticated admin users).
|
|
214
|
+
*/ export function getApiKeyContext(req) {
|
|
215
|
+
const user = req.user;
|
|
216
|
+
return user?._mcpKey ?? null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
//# sourceMappingURL=auth-strategy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/auth-strategy.ts"],"sourcesContent":["import type { AuthStrategy, PayloadRequest, Where } from 'payload'\r\nimport { extractBearerToken, hashKey } from './hash'\r\nimport type { CollectionAction, GlobalAction, KeyScopes, ScopePreset } from './types'\r\n\r\nexport type { CollectionAction, GlobalAction, KeyScopes, ScopePreset } from './types'\r\n\r\nexport const AUTH_STRATEGY_NAME = 'mcp-toolkit-bearer'\r\n\r\nexport interface CreateBearerStrategyOptions {\r\n /** Slug of the API-keys collection (defaults to `payload-mcp-api-keys`). */\r\n collectionSlug: string\r\n /** Slug of the user collection that API keys link to. */\r\n userCollection: string\r\n}\r\n\r\n/**\r\n * Stored shape for a row in `collectionScopes` / `globalScopes` (v0.6+).\r\n * The parent field name encodes the axis — collection-vs-global is not\r\n * repeated in the row payload. Legacy `collection` / `global` keys from\r\n * pre-0.6 rows are tolerated by `composeScopes` for one release; v0.7\r\n * drops the fallback (see CHANGELOG).\r\n */\r\ninterface ScopeRow {\r\n slug?: unknown\r\n /** @deprecated pre-0.6 collectionScopes shape — read via fallback only. */\r\n collection?: unknown\r\n /** @deprecated pre-0.6 globalScopes shape — read via fallback only. */\r\n global?: unknown\r\n actions?: unknown\r\n}\r\n\r\ninterface ApiKeyRow {\r\n id: string | number\r\n user: unknown\r\n preset?: ScopePreset | 'custom' | null\r\n collectionScopes?: ScopeRow[] | null\r\n globalScopes?: ScopeRow[] | null\r\n toolAllow?: string[] | null\r\n toolDeny?: string[] | null\r\n expiresAt?: string | Date | null\r\n revokedAt?: string | Date | null\r\n apiKey?: string | null\r\n keyPrefix?: string | null\r\n}\r\n\r\n/**\r\n * Reads the row's slug, tolerating the pre-0.6 `collection` / `global`\r\n * keys for one release. Logs a one-line warn when the legacy fallback\r\n * fires so operators can spot keys that need re-saving. The fallback is\r\n * scheduled for removal in v0.7.\r\n */\r\nlet warnedLegacyShape = false\r\nfunction readScopeSlug(\r\n entry: ScopeRow,\r\n legacyKey: 'collection' | 'global',\r\n logger?: { warn?: (...args: unknown[]) => void } | undefined,\r\n): string | null {\r\n if (typeof entry?.slug === 'string') return entry.slug\r\n const legacy = entry?.[legacyKey]\r\n if (typeof legacy === 'string') {\r\n if (!warnedLegacyShape) {\r\n warnedLegacyShape = true\r\n logger?.warn?.(\r\n { event: 'mcp.auth.legacy_scope_shape', legacyKey },\r\n `[payload-mcp-toolkit] composeScopes read a pre-0.6 row using \\`${legacyKey}\\`; resave the API key to migrate to {slug, actions}. The fallback is removed in v0.7.`,\r\n )\r\n }\r\n return legacy\r\n }\r\n return null\r\n}\r\n\r\nconst VALID_ACTIONS: ReadonlySet<CollectionAction> = new Set(['read', 'create', 'update', 'delete'])\r\nconst VALID_GLOBAL_ACTIONS: ReadonlySet<GlobalAction> = new Set(['read', 'update'])\r\n\r\n/**\r\n * Builds the runtime `KeyScopes` shape consumed by `registry.assertScopeAllows`\r\n * from the typed scope fields on the api-key row.\r\n *\r\n * Returns null when no typed fields are populated AND no preset is set\r\n * (= full access — back-compat for pre-0.5 rows that pre-date scoped authz).\r\n *\r\n * Two complementary fail-closed rules:\r\n *\r\n * 1. **`'custom'` deny-all sentinel.** `'custom'` is a UI sentinel meaning\r\n * \"use my override fields\"; it never becomes `KeyScopes.preset` itself.\r\n * Payload persists unset JSON / select fields as `null`, so a fresh\r\n * Custom key with no overrides arrives as `{preset:'custom',\r\n * collectionScopes:null, globalScopes:null, toolAllow:null,\r\n * toolDeny:null}`. That row must deny everything (not fall through to\r\n * full access). The sentinel emits `{collections:{}, globals:{},\r\n * tools:{allow:[]}}`.\r\n *\r\n * 2. **Per-axis explicit-empty.** When a row carries an array value on any\r\n * axis (even `[]`), that axis is honoured as written — an empty\r\n * `toolAllow:[]` means \"deny all tools\", not \"no opinion\". This closes a\r\n * gap where a Custom key with `collectionScopes` populated AND\r\n * `toolAllow:[]` previously emitted no `tools.allow` gate (the empty\r\n * array was treated as absent). `toolDeny` is a deny-list, so an empty\r\n * array carries no entries — it is dropped rather than emitted.\r\n */\r\nexport function composeScopes(\r\n row: ApiKeyRow,\r\n logger?: { warn?: (...args: unknown[]) => void },\r\n): KeyScopes | null {\r\n const presetRaw = row.preset\r\n const hasPreset = typeof presetRaw === 'string' && presetRaw.length > 0\r\n const hasCollectionScopesField = Array.isArray(row.collectionScopes)\r\n const hasGlobalScopesField = Array.isArray(row.globalScopes)\r\n const hasToolAllowField = Array.isArray(row.toolAllow)\r\n const hasToolDenyField = Array.isArray(row.toolDeny)\r\n\r\n // Pre-0.5 back-compat: row has no preset and no typed scope fields at\r\n // all (all null/undefined). Treat as full access.\r\n if (\r\n !hasPreset &&\r\n !hasCollectionScopesField &&\r\n !hasGlobalScopesField &&\r\n !hasToolAllowField &&\r\n !hasToolDenyField\r\n ) {\r\n return null\r\n }\r\n\r\n // Sentinel: Custom preset with every axis null/undefined (no array\r\n // committed) denies everything. Payload-persisted unset JSON / select\r\n // fields arrive as null; this is the fresh-Custom-key case.\r\n if (\r\n presetRaw === 'custom' &&\r\n !hasCollectionScopesField &&\r\n !hasGlobalScopesField &&\r\n !hasToolAllowField &&\r\n !hasToolDenyField\r\n ) {\r\n return { collections: {}, globals: {}, tools: { allow: [] } }\r\n }\r\n\r\n const out: KeyScopes = {}\r\n if (hasPreset && presetRaw !== 'custom') {\r\n out.preset = presetRaw as ScopePreset\r\n }\r\n\r\n if (hasCollectionScopesField) {\r\n const collections: Record<string, CollectionAction[]> = {}\r\n for (const entry of row.collectionScopes as ScopeRow[]) {\r\n const slug = readScopeSlug(entry, 'collection', logger)\r\n if (!slug) continue\r\n const rawActions = Array.isArray(entry?.actions) ? entry.actions : []\r\n const actions = rawActions.filter(\r\n (a): a is CollectionAction => typeof a === 'string' && VALID_ACTIONS.has(a as CollectionAction),\r\n )\r\n collections[slug] = actions\r\n }\r\n out.collections = collections\r\n }\r\n\r\n if (hasGlobalScopesField) {\r\n const globals: Record<string, GlobalAction[]> = {}\r\n for (const entry of row.globalScopes as ScopeRow[]) {\r\n const slug = readScopeSlug(entry, 'global', logger)\r\n if (!slug) continue\r\n const rawActions = Array.isArray(entry?.actions) ? entry.actions : []\r\n const actions = rawActions.filter(\r\n (a): a is GlobalAction => typeof a === 'string' && VALID_GLOBAL_ACTIONS.has(a as GlobalAction),\r\n )\r\n globals[slug] = actions\r\n }\r\n out.globals = globals\r\n }\r\n\r\n const toolDeny = hasToolDenyField\r\n ? (row.toolDeny as unknown[]).filter((t): t is string => typeof t === 'string')\r\n : []\r\n const emitDeny = toolDeny.length > 0\r\n\r\n if (hasToolAllowField || emitDeny) {\r\n out.tools = {}\r\n if (hasToolAllowField) {\r\n const toolAllow = (row.toolAllow as unknown[]).filter((t): t is string => typeof t === 'string')\r\n out.tools.allow = toolAllow\r\n }\r\n if (emitDeny) out.tools.deny = toolDeny\r\n }\r\n\r\n return out\r\n}\r\n\r\n/**\r\n * Builds the Payload `auth.strategies` entry that authenticates MCP requests.\r\n *\r\n * Authenticates `Authorization: Bearer <plaintext>` by computing the\r\n * upstream-compatible `apiKeyIndex` HMAC and looking up the row. On match,\r\n * it fires a non-blocking `lastUsedAt` write and hydrates `req.user` with\r\n * the linked user record + key context for downstream scope checks.\r\n */\r\nexport function createBearerStrategy(options: CreateBearerStrategyOptions): AuthStrategy {\r\n const { collectionSlug, userCollection } = options\r\n\r\n return {\r\n name: AUTH_STRATEGY_NAME,\r\n authenticate: async ({ headers, payload }) => {\r\n const headersAny = headers as unknown as\r\n | { get?: (name: string) => string | null }\r\n | Record<string, string | undefined>\r\n const headerValue =\r\n typeof (headersAny as { get?: (name: string) => string | null }).get === 'function'\r\n ? (headersAny as { get: (name: string) => string | null }).get('authorization')\r\n : (headersAny as Record<string, string | undefined>).authorization\r\n const token = extractBearerToken(headerValue ?? null)\r\n if (!token) return { user: null }\r\n\r\n const keyHash = hashKey(token, payload.secret)\r\n const where: Where = { apiKeyIndex: { equals: keyHash } }\r\n\r\n let docs: ApiKeyRow[] = []\r\n try {\r\n const result = (await payload.find({\r\n collection: collectionSlug,\r\n where,\r\n depth: 1,\r\n limit: 1,\r\n pagination: false,\r\n overrideAccess: true,\r\n })) as unknown as { docs: ApiKeyRow[] }\r\n docs = result.docs ?? []\r\n } catch (err) {\r\n payload.logger.error(\r\n { err, event: 'mcp.auth.lookup_failed' },\r\n '[payload-mcp-toolkit] API-key lookup failed',\r\n )\r\n return { user: null }\r\n }\r\n\r\n const row = docs[0]\r\n if (!row) return { user: null }\r\n\r\n const now = Date.now()\r\n if (row.revokedAt) return { user: null }\r\n if (row.expiresAt) {\r\n const expiry = new Date(row.expiresAt).getTime()\r\n if (Number.isFinite(expiry) && expiry < now) return { user: null }\r\n }\r\n\r\n const linkedUser = row.user\r\n if (!linkedUser || typeof linkedUser !== 'object') return { user: null }\r\n\r\n const effectiveScopes: KeyScopes | null = composeScopes(row, payload.logger)\r\n\r\n // Fire-and-forget: do not block the request on this write.\r\n void payload\r\n .update({\r\n collection: collectionSlug,\r\n id: row.id,\r\n data: { lastUsedAt: new Date().toISOString() } as Record<string, unknown>,\r\n overrideAccess: true,\r\n })\r\n .catch(() => {\r\n // Intentionally swallow: lastUsedAt drift is acceptable.\r\n })\r\n\r\n const user = linkedUser as Record<string, unknown>\r\n return {\r\n user: {\r\n ...user,\r\n collection: userCollection,\r\n _strategy: AUTH_STRATEGY_NAME,\r\n _mcpKey: {\r\n keyId: row.id,\r\n keyPrefix: typeof row.keyPrefix === 'string' ? row.keyPrefix : null,\r\n scopes: effectiveScopes,\r\n },\r\n } as unknown as PayloadRequest['user'],\r\n }\r\n },\r\n }\r\n}\r\n\r\n/**\r\n * Reads the per-request API-key context populated by the bearer strategy.\r\n * Returns null for non-MCP requests (e.g. cookie-authenticated admin users).\r\n */\r\nexport function getApiKeyContext(req: PayloadRequest): {\r\n keyId: string | number\r\n keyPrefix: string | null\r\n scopes: KeyScopes | null\r\n} | null {\r\n const user = req.user as\r\n | (Record<string, unknown> & {\r\n _mcpKey?: { keyId: string | number; keyPrefix: string | null; scopes: KeyScopes | null }\r\n })\r\n | null\r\n | undefined\r\n return user?._mcpKey ?? null\r\n}\r\n"],"names":["extractBearerToken","hashKey","AUTH_STRATEGY_NAME","warnedLegacyShape","readScopeSlug","entry","legacyKey","logger","slug","legacy","warn","event","VALID_ACTIONS","Set","VALID_GLOBAL_ACTIONS","composeScopes","row","presetRaw","preset","hasPreset","length","hasCollectionScopesField","Array","isArray","collectionScopes","hasGlobalScopesField","globalScopes","hasToolAllowField","toolAllow","hasToolDenyField","toolDeny","collections","globals","tools","allow","out","rawActions","actions","filter","a","has","t","emitDeny","deny","createBearerStrategy","options","collectionSlug","userCollection","name","authenticate","headers","payload","headersAny","headerValue","get","authorization","token","user","keyHash","secret","where","apiKeyIndex","equals","docs","result","find","collection","depth","limit","pagination","overrideAccess","err","error","now","Date","revokedAt","expiresAt","expiry","getTime","Number","isFinite","linkedUser","effectiveScopes","update","id","data","lastUsedAt","toISOString","catch","_strategy","_mcpKey","keyId","keyPrefix","scopes","getApiKeyContext","req"],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface CollectionScopesMatrixProps {
|
|
3
|
+
path: string;
|
|
4
|
+
/** Forwarded via `clientProps` from `api-keys.ts`. */
|
|
5
|
+
availableCollections?: string[];
|
|
6
|
+
}
|
|
7
|
+
declare function CollectionScopesMatrix(props: CollectionScopesMatrixProps): React.ReactElement;
|
|
8
|
+
export default CollectionScopesMatrix;
|