payload-mcp-toolkit 0.7.0 → 0.7.5
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 +30 -9
- package/dist/api-keys.js +57 -21
- package/dist/api-keys.js.map +1 -1
- package/dist/auth-strategy.d.ts +18 -7
- package/dist/auth-strategy.js +54 -12
- package/dist/auth-strategy.js.map +1 -1
- package/dist/tools/_helpers.d.ts +34 -0
- package/dist/tools/_helpers.js +98 -0
- package/dist/tools/_helpers.js.map +1 -1
- package/dist/tools/create-document.js +8 -0
- package/dist/tools/create-document.js.map +1 -1
- package/dist/tools/delete-document.d.ts +1 -1
- package/dist/tools/delete-document.js +6 -6
- package/dist/tools/delete-document.js.map +1 -1
- package/dist/tools/find-document.d.ts +3 -3
- package/dist/tools/find-document.js +8 -8
- package/dist/tools/find-document.js.map +1 -1
- package/dist/tools/publish-draft.js +33 -1
- package/dist/tools/publish-draft.js.map +1 -1
- package/dist/tools/publish-global-draft.js +30 -1
- package/dist/tools/publish-global-draft.js.map +1 -1
- package/package.json +29 -15
- package/dist/__tests__/api-keys.test.js +0 -292
- package/dist/__tests__/api-keys.test.js.map +0 -1
- package/dist/__tests__/auth-strategy.test.js +0 -681
- package/dist/__tests__/auth-strategy.test.js.map +0 -1
- package/dist/__tests__/conflict-detection.test.js +0 -69
- package/dist/__tests__/conflict-detection.test.js.map +0 -1
- package/dist/__tests__/delete-document.test.js +0 -70
- package/dist/__tests__/delete-document.test.js.map +0 -1
- package/dist/__tests__/endpoint.test.js +0 -143
- package/dist/__tests__/endpoint.test.js.map +0 -1
- package/dist/__tests__/find-document.test.js +0 -178
- package/dist/__tests__/find-document.test.js.map +0 -1
- package/dist/__tests__/find-global.test.js +0 -173
- package/dist/__tests__/find-global.test.js.map +0 -1
- package/dist/__tests__/global-versions.test.js +0 -183
- package/dist/__tests__/global-versions.test.js.map +0 -1
- package/dist/__tests__/hash.test.js +0 -58
- package/dist/__tests__/hash.test.js.map +0 -1
- package/dist/__tests__/index-integration.test.js +0 -191
- package/dist/__tests__/index-integration.test.js.map +0 -1
- package/dist/__tests__/introspection.test.js +0 -659
- package/dist/__tests__/introspection.test.js.map +0 -1
- package/dist/__tests__/patch-global-layout.test.js +0 -474
- package/dist/__tests__/patch-global-layout.test.js.map +0 -1
- package/dist/__tests__/patch-layout.test.js +0 -171
- package/dist/__tests__/patch-layout.test.js.map +0 -1
- package/dist/__tests__/registry.test.js +0 -795
- package/dist/__tests__/registry.test.js.map +0 -1
- package/dist/__tests__/resources.test.js +0 -139
- package/dist/__tests__/resources.test.js.map +0 -1
- package/dist/__tests__/update-global.test.js +0 -157
- package/dist/__tests__/update-global.test.js.map +0 -1
- package/dist/__tests__/url-validator.test.js +0 -326
- package/dist/__tests__/url-validator.test.js.map +0 -1
|
@@ -1,681 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { createBearerStrategy, AUTH_STRATEGY_NAME, getApiKeyContext, composeScopes } from '../auth-strategy';
|
|
3
|
-
import { hashKey } from '../hash';
|
|
4
|
-
const SECRET = 'test-payload-secret';
|
|
5
|
-
function buildPayload(opts) {
|
|
6
|
-
const findMock = vi.fn(async ()=>{
|
|
7
|
-
if (opts.findError) throw opts.findError;
|
|
8
|
-
return {
|
|
9
|
-
docs: opts.rows,
|
|
10
|
-
totalDocs: opts.rows.length
|
|
11
|
-
};
|
|
12
|
-
});
|
|
13
|
-
const updateMock = vi.fn(async ()=>{
|
|
14
|
-
if (opts.updateError) throw opts.updateError;
|
|
15
|
-
return {};
|
|
16
|
-
});
|
|
17
|
-
return {
|
|
18
|
-
secret: SECRET,
|
|
19
|
-
find: findMock,
|
|
20
|
-
update: updateMock,
|
|
21
|
-
logger: {
|
|
22
|
-
info: vi.fn(),
|
|
23
|
-
warn: vi.fn(),
|
|
24
|
-
error: vi.fn()
|
|
25
|
-
},
|
|
26
|
-
findMock,
|
|
27
|
-
updateMock
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
function makeHeaders(token) {
|
|
31
|
-
return {
|
|
32
|
-
get: (name)=>{
|
|
33
|
-
if (name.toLowerCase() === 'authorization') return token === null ? null : `Bearer ${token}`;
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
describe('composeScopes', ()=>{
|
|
39
|
-
const baseRow = {
|
|
40
|
-
id: 'k',
|
|
41
|
-
user: {
|
|
42
|
-
id: 'u'
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
it('returns null when no typed fields are populated (= full access)', ()=>{
|
|
46
|
-
expect(composeScopes({
|
|
47
|
-
...baseRow
|
|
48
|
-
})).toBeNull();
|
|
49
|
-
});
|
|
50
|
-
it('builds KeyScopes from typed fields when populated', ()=>{
|
|
51
|
-
const out = composeScopes({
|
|
52
|
-
...baseRow,
|
|
53
|
-
preset: 'editor',
|
|
54
|
-
toolDeny: [
|
|
55
|
-
'safeDelete'
|
|
56
|
-
]
|
|
57
|
-
});
|
|
58
|
-
expect(out).toEqual({
|
|
59
|
-
preset: 'editor',
|
|
60
|
-
tools: {
|
|
61
|
-
deny: [
|
|
62
|
-
'safeDelete'
|
|
63
|
-
]
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
it('treats preset === "custom" as a UI sentinel and drops it from KeyScopes', ()=>{
|
|
68
|
-
const out = composeScopes({
|
|
69
|
-
...baseRow,
|
|
70
|
-
preset: 'custom',
|
|
71
|
-
collectionScopes: [
|
|
72
|
-
{
|
|
73
|
-
slug: 'posts',
|
|
74
|
-
actions: [
|
|
75
|
-
'read',
|
|
76
|
-
'create'
|
|
77
|
-
]
|
|
78
|
-
}
|
|
79
|
-
]
|
|
80
|
-
});
|
|
81
|
-
expect(out).toEqual({
|
|
82
|
-
collections: {
|
|
83
|
-
posts: [
|
|
84
|
-
'read',
|
|
85
|
-
'create'
|
|
86
|
-
]
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
it('returns a deny-all sentinel when preset === "custom" with no overrides', ()=>{
|
|
91
|
-
// Fail-closed contract: a freshly-created Custom key with empty
|
|
92
|
-
// collectionScopes / toolAllow / toolDeny denies every dispatch instead
|
|
93
|
-
// of falling through to the "no scopes set = full access" guard.
|
|
94
|
-
expect(composeScopes({
|
|
95
|
-
...baseRow,
|
|
96
|
-
preset: 'custom'
|
|
97
|
-
})).toEqual({
|
|
98
|
-
collections: {},
|
|
99
|
-
globals: {},
|
|
100
|
-
tools: {
|
|
101
|
-
allow: []
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
it('honours partial custom overrides (only toolAllow) without injecting deny-all', ()=>{
|
|
106
|
-
// Verifies the sentinel only fires when ALL override fields are empty.
|
|
107
|
-
const out = composeScopes({
|
|
108
|
-
...baseRow,
|
|
109
|
-
preset: 'custom',
|
|
110
|
-
toolAllow: [
|
|
111
|
-
'searchContent'
|
|
112
|
-
]
|
|
113
|
-
});
|
|
114
|
-
expect(out).toEqual({
|
|
115
|
-
tools: {
|
|
116
|
-
allow: [
|
|
117
|
-
'searchContent'
|
|
118
|
-
]
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
it('preserves an empty actions array as explicit-deny-all on a listed collection', ()=>{
|
|
123
|
-
const out = composeScopes({
|
|
124
|
-
...baseRow,
|
|
125
|
-
preset: 'custom',
|
|
126
|
-
collectionScopes: [
|
|
127
|
-
{
|
|
128
|
-
slug: 'posts',
|
|
129
|
-
actions: []
|
|
130
|
-
}
|
|
131
|
-
]
|
|
132
|
-
});
|
|
133
|
-
expect(out).toEqual({
|
|
134
|
-
collections: {
|
|
135
|
-
posts: []
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
it('filters invalid action values out of collectionScopes', ()=>{
|
|
140
|
-
const out = composeScopes({
|
|
141
|
-
...baseRow,
|
|
142
|
-
preset: 'custom',
|
|
143
|
-
collectionScopes: [
|
|
144
|
-
{
|
|
145
|
-
slug: 'posts',
|
|
146
|
-
actions: [
|
|
147
|
-
'read',
|
|
148
|
-
'bogus',
|
|
149
|
-
1,
|
|
150
|
-
'update'
|
|
151
|
-
]
|
|
152
|
-
}
|
|
153
|
-
]
|
|
154
|
-
});
|
|
155
|
-
expect(out).toEqual({
|
|
156
|
-
collections: {
|
|
157
|
-
posts: [
|
|
158
|
-
'read',
|
|
159
|
-
'update'
|
|
160
|
-
]
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
// ─── Globals (U9) ────────────────────────────────────────────────
|
|
165
|
-
it('maps globalScopes to KeyScopes.globals', ()=>{
|
|
166
|
-
const out = composeScopes({
|
|
167
|
-
...baseRow,
|
|
168
|
-
preset: 'custom',
|
|
169
|
-
globalScopes: [
|
|
170
|
-
{
|
|
171
|
-
slug: 'siteSettings',
|
|
172
|
-
actions: [
|
|
173
|
-
'read',
|
|
174
|
-
'update'
|
|
175
|
-
]
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
slug: 'footer',
|
|
179
|
-
actions: [
|
|
180
|
-
'read'
|
|
181
|
-
]
|
|
182
|
-
}
|
|
183
|
-
]
|
|
184
|
-
});
|
|
185
|
-
expect(out).toEqual({
|
|
186
|
-
globals: {
|
|
187
|
-
siteSettings: [
|
|
188
|
-
'read',
|
|
189
|
-
'update'
|
|
190
|
-
],
|
|
191
|
-
footer: [
|
|
192
|
-
'read'
|
|
193
|
-
]
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
it('filters invalid global action values', ()=>{
|
|
198
|
-
const out = composeScopes({
|
|
199
|
-
...baseRow,
|
|
200
|
-
preset: 'custom',
|
|
201
|
-
globalScopes: [
|
|
202
|
-
{
|
|
203
|
-
slug: 'siteSettings',
|
|
204
|
-
actions: [
|
|
205
|
-
'read',
|
|
206
|
-
'create',
|
|
207
|
-
'delete',
|
|
208
|
-
'update'
|
|
209
|
-
]
|
|
210
|
-
}
|
|
211
|
-
]
|
|
212
|
-
});
|
|
213
|
-
// Only read/update are valid on globals — create/delete dropped.
|
|
214
|
-
expect(out).toEqual({
|
|
215
|
-
globals: {
|
|
216
|
-
siteSettings: [
|
|
217
|
-
'read',
|
|
218
|
-
'update'
|
|
219
|
-
]
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
it('preset admin + globalScopes: null produces a preset-only KeyScopes (legacy v0.5 row)', ()=>{
|
|
224
|
-
const out = composeScopes({
|
|
225
|
-
...baseRow,
|
|
226
|
-
preset: 'admin',
|
|
227
|
-
globalScopes: null
|
|
228
|
-
});
|
|
229
|
-
expect(out).toEqual({
|
|
230
|
-
preset: 'admin'
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
it('preset editor + globalScopes: [] (explicit empty) emits explicit deny-all on the globals axis', ()=>{
|
|
234
|
-
// Axis-independent rule: an explicit empty array commits "no globals
|
|
235
|
-
// allowed" even when a preset is set. Use globalScopes: null (or omit
|
|
236
|
-
// the field) to fall through to the preset default.
|
|
237
|
-
const out = composeScopes({
|
|
238
|
-
...baseRow,
|
|
239
|
-
preset: 'editor',
|
|
240
|
-
globalScopes: []
|
|
241
|
-
});
|
|
242
|
-
expect(out).toEqual({
|
|
243
|
-
preset: 'editor',
|
|
244
|
-
globals: {}
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
it('preset editor + collectionScopes: [] (explicit empty) emits explicit deny-all on the collections axis', ()=>{
|
|
248
|
-
const out = composeScopes({
|
|
249
|
-
...baseRow,
|
|
250
|
-
preset: 'editor',
|
|
251
|
-
collectionScopes: []
|
|
252
|
-
});
|
|
253
|
-
expect(out).toEqual({
|
|
254
|
-
preset: 'editor',
|
|
255
|
-
collections: {}
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
it('preset custom + populated collectionScopes + explicit toolAllow: [] honours both axes', ()=>{
|
|
259
|
-
// Closes the prior gap where toolAllow: [] (operator intent: "no tools
|
|
260
|
-
// allowed") was treated identically to absent and silently dropped, so
|
|
261
|
-
// the row authenticated with a collection scope but no tool gate.
|
|
262
|
-
const out = composeScopes({
|
|
263
|
-
...baseRow,
|
|
264
|
-
preset: 'custom',
|
|
265
|
-
collectionScopes: [
|
|
266
|
-
{
|
|
267
|
-
slug: 'posts',
|
|
268
|
-
actions: [
|
|
269
|
-
'read'
|
|
270
|
-
]
|
|
271
|
-
}
|
|
272
|
-
],
|
|
273
|
-
toolAllow: []
|
|
274
|
-
});
|
|
275
|
-
expect(out).toEqual({
|
|
276
|
-
collections: {
|
|
277
|
-
posts: [
|
|
278
|
-
'read'
|
|
279
|
-
]
|
|
280
|
-
},
|
|
281
|
-
tools: {
|
|
282
|
-
allow: []
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
});
|
|
286
|
-
it('explicit toolDeny: [] carries no entries and emits nothing', ()=>{
|
|
287
|
-
// toolDeny is a deny-list — an empty array has nothing to deny, so the
|
|
288
|
-
// axis is dropped rather than emitting `tools.deny: []`.
|
|
289
|
-
const out = composeScopes({
|
|
290
|
-
...baseRow,
|
|
291
|
-
preset: 'editor',
|
|
292
|
-
toolDeny: []
|
|
293
|
-
});
|
|
294
|
-
expect(out).toEqual({
|
|
295
|
-
preset: 'editor'
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
it('widened deny-all sentinel: empty everywhere produces both maps and tools.allow=[]', ()=>{
|
|
299
|
-
expect(composeScopes({
|
|
300
|
-
...baseRow,
|
|
301
|
-
preset: 'custom',
|
|
302
|
-
collectionScopes: [],
|
|
303
|
-
globalScopes: [],
|
|
304
|
-
toolAllow: [],
|
|
305
|
-
toolDeny: []
|
|
306
|
-
})).toEqual({
|
|
307
|
-
collections: {},
|
|
308
|
-
globals: {},
|
|
309
|
-
tools: {
|
|
310
|
-
allow: []
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
});
|
|
314
|
-
it('partial custom override on the globals axis does NOT fire the sentinel', ()=>{
|
|
315
|
-
const out = composeScopes({
|
|
316
|
-
...baseRow,
|
|
317
|
-
preset: 'custom',
|
|
318
|
-
globalScopes: [
|
|
319
|
-
{
|
|
320
|
-
slug: 'siteSettings',
|
|
321
|
-
actions: [
|
|
322
|
-
'read'
|
|
323
|
-
]
|
|
324
|
-
}
|
|
325
|
-
]
|
|
326
|
-
});
|
|
327
|
-
expect(out).toEqual({
|
|
328
|
-
globals: {
|
|
329
|
-
siteSettings: [
|
|
330
|
-
'read'
|
|
331
|
-
]
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
});
|
|
335
|
-
it('combines toolAllow and toolDeny into tools.allow / tools.deny', ()=>{
|
|
336
|
-
const out = composeScopes({
|
|
337
|
-
...baseRow,
|
|
338
|
-
toolAllow: [
|
|
339
|
-
'findDocument',
|
|
340
|
-
'searchContent'
|
|
341
|
-
],
|
|
342
|
-
toolDeny: [
|
|
343
|
-
'deleteDocument'
|
|
344
|
-
]
|
|
345
|
-
});
|
|
346
|
-
expect(out).toEqual({
|
|
347
|
-
tools: {
|
|
348
|
-
allow: [
|
|
349
|
-
'findDocument',
|
|
350
|
-
'searchContent'
|
|
351
|
-
],
|
|
352
|
-
deny: [
|
|
353
|
-
'deleteDocument'
|
|
354
|
-
]
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
|
-
});
|
|
358
|
-
// ─── Legacy row-shape fallback (pre-0.6 → 0.6 transition) ─────────
|
|
359
|
-
//
|
|
360
|
-
// The {slug, actions} normalization landed mid-0.6 — pre-existing
|
|
361
|
-
// {collection, actions} / {global, actions} rows are tolerated for one
|
|
362
|
-
// release so locally-tested 0.6 fixtures keep authenticating. The
|
|
363
|
-
// fallback is removed in v0.7.
|
|
364
|
-
it('falls back to row.collection when row.slug is missing (legacy collectionScopes)', ()=>{
|
|
365
|
-
const warn = vi.fn();
|
|
366
|
-
const out = composeScopes({
|
|
367
|
-
...baseRow,
|
|
368
|
-
preset: 'custom',
|
|
369
|
-
collectionScopes: [
|
|
370
|
-
{
|
|
371
|
-
collection: 'posts',
|
|
372
|
-
actions: [
|
|
373
|
-
'read'
|
|
374
|
-
]
|
|
375
|
-
}
|
|
376
|
-
]
|
|
377
|
-
}, {
|
|
378
|
-
warn
|
|
379
|
-
});
|
|
380
|
-
expect(out).toEqual({
|
|
381
|
-
collections: {
|
|
382
|
-
posts: [
|
|
383
|
-
'read'
|
|
384
|
-
]
|
|
385
|
-
}
|
|
386
|
-
});
|
|
387
|
-
});
|
|
388
|
-
it('falls back to row.global when row.slug is missing (legacy globalScopes)', ()=>{
|
|
389
|
-
const warn = vi.fn();
|
|
390
|
-
const out = composeScopes({
|
|
391
|
-
...baseRow,
|
|
392
|
-
preset: 'custom',
|
|
393
|
-
globalScopes: [
|
|
394
|
-
{
|
|
395
|
-
global: 'siteSettings',
|
|
396
|
-
actions: [
|
|
397
|
-
'read'
|
|
398
|
-
]
|
|
399
|
-
}
|
|
400
|
-
]
|
|
401
|
-
}, {
|
|
402
|
-
warn
|
|
403
|
-
});
|
|
404
|
-
expect(out).toEqual({
|
|
405
|
-
globals: {
|
|
406
|
-
siteSettings: [
|
|
407
|
-
'read'
|
|
408
|
-
]
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
});
|
|
412
|
-
it('prefers row.slug over the legacy row.collection key when both are set', ()=>{
|
|
413
|
-
const out = composeScopes({
|
|
414
|
-
...baseRow,
|
|
415
|
-
preset: 'custom',
|
|
416
|
-
collectionScopes: [
|
|
417
|
-
{
|
|
418
|
-
slug: 'posts',
|
|
419
|
-
collection: 'stale-slug',
|
|
420
|
-
actions: [
|
|
421
|
-
'read'
|
|
422
|
-
]
|
|
423
|
-
}
|
|
424
|
-
]
|
|
425
|
-
});
|
|
426
|
-
expect(out).toEqual({
|
|
427
|
-
collections: {
|
|
428
|
-
posts: [
|
|
429
|
-
'read'
|
|
430
|
-
]
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
});
|
|
434
|
-
});
|
|
435
|
-
describe('createBearerStrategy.authenticate', ()=>{
|
|
436
|
-
const strategy = createBearerStrategy({
|
|
437
|
-
collectionSlug: 'payload-mcp-api-keys',
|
|
438
|
-
userCollection: 'users'
|
|
439
|
-
});
|
|
440
|
-
it('exports the documented strategy name', ()=>{
|
|
441
|
-
expect(strategy.name).toBe(AUTH_STRATEGY_NAME);
|
|
442
|
-
});
|
|
443
|
-
it('returns null user when Authorization header is missing', async ()=>{
|
|
444
|
-
const payload = buildPayload({
|
|
445
|
-
rows: []
|
|
446
|
-
});
|
|
447
|
-
const result = await strategy.authenticate({
|
|
448
|
-
headers: makeHeaders(null),
|
|
449
|
-
payload: payload
|
|
450
|
-
});
|
|
451
|
-
expect(result).toEqual({
|
|
452
|
-
user: null
|
|
453
|
-
});
|
|
454
|
-
expect(payload.findMock).not.toHaveBeenCalled();
|
|
455
|
-
});
|
|
456
|
-
it('returns null user when scheme is not Bearer', async ()=>{
|
|
457
|
-
const payload = buildPayload({
|
|
458
|
-
rows: []
|
|
459
|
-
});
|
|
460
|
-
const result = await strategy.authenticate({
|
|
461
|
-
headers: {
|
|
462
|
-
get: ()=>'Basic abc'
|
|
463
|
-
},
|
|
464
|
-
payload: payload
|
|
465
|
-
});
|
|
466
|
-
expect(result).toEqual({
|
|
467
|
-
user: null
|
|
468
|
-
});
|
|
469
|
-
});
|
|
470
|
-
it('returns null user when no row matches the hashed token', async ()=>{
|
|
471
|
-
const payload = buildPayload({
|
|
472
|
-
rows: []
|
|
473
|
-
});
|
|
474
|
-
const result = await strategy.authenticate({
|
|
475
|
-
headers: makeHeaders('plaintext-key'),
|
|
476
|
-
payload: payload
|
|
477
|
-
});
|
|
478
|
-
expect(result).toEqual({
|
|
479
|
-
user: null
|
|
480
|
-
});
|
|
481
|
-
expect(payload.findMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
482
|
-
collection: 'payload-mcp-api-keys',
|
|
483
|
-
where: {
|
|
484
|
-
apiKeyIndex: {
|
|
485
|
-
equals: hashKey('plaintext-key', SECRET)
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}));
|
|
489
|
-
});
|
|
490
|
-
it('returns null user when the matched row is revoked', async ()=>{
|
|
491
|
-
const payload = buildPayload({
|
|
492
|
-
rows: [
|
|
493
|
-
{
|
|
494
|
-
id: 'k1',
|
|
495
|
-
user: {
|
|
496
|
-
id: 'u1',
|
|
497
|
-
email: 'a@b.com'
|
|
498
|
-
},
|
|
499
|
-
revokedAt: '2025-01-01T00:00:00Z'
|
|
500
|
-
}
|
|
501
|
-
]
|
|
502
|
-
});
|
|
503
|
-
const result = await strategy.authenticate({
|
|
504
|
-
headers: makeHeaders('plaintext-key'),
|
|
505
|
-
payload: payload
|
|
506
|
-
});
|
|
507
|
-
expect(result).toEqual({
|
|
508
|
-
user: null
|
|
509
|
-
});
|
|
510
|
-
});
|
|
511
|
-
it('returns null user when the matched row has expired', async ()=>{
|
|
512
|
-
const payload = buildPayload({
|
|
513
|
-
rows: [
|
|
514
|
-
{
|
|
515
|
-
id: 'k1',
|
|
516
|
-
user: {
|
|
517
|
-
id: 'u1'
|
|
518
|
-
},
|
|
519
|
-
expiresAt: new Date(Date.now() - 60_000).toISOString()
|
|
520
|
-
}
|
|
521
|
-
]
|
|
522
|
-
});
|
|
523
|
-
const result = await strategy.authenticate({
|
|
524
|
-
headers: makeHeaders('plaintext-key'),
|
|
525
|
-
payload: payload
|
|
526
|
-
});
|
|
527
|
-
expect(result).toEqual({
|
|
528
|
-
user: null
|
|
529
|
-
});
|
|
530
|
-
});
|
|
531
|
-
it('returns null user when the linked user is missing', async ()=>{
|
|
532
|
-
const payload = buildPayload({
|
|
533
|
-
rows: [
|
|
534
|
-
{
|
|
535
|
-
id: 'k1',
|
|
536
|
-
user: null
|
|
537
|
-
}
|
|
538
|
-
]
|
|
539
|
-
});
|
|
540
|
-
const result = await strategy.authenticate({
|
|
541
|
-
headers: makeHeaders('plaintext-key'),
|
|
542
|
-
payload: payload
|
|
543
|
-
});
|
|
544
|
-
expect(result).toEqual({
|
|
545
|
-
user: null
|
|
546
|
-
});
|
|
547
|
-
});
|
|
548
|
-
it('hydrates the user, key context, and fires lastUsedAt write on a happy match', async ()=>{
|
|
549
|
-
const payload = buildPayload({
|
|
550
|
-
rows: [
|
|
551
|
-
{
|
|
552
|
-
id: 'k1',
|
|
553
|
-
user: {
|
|
554
|
-
id: 'u1',
|
|
555
|
-
email: 'a@b.com'
|
|
556
|
-
},
|
|
557
|
-
preset: 'admin',
|
|
558
|
-
keyPrefix: 'abc12345'
|
|
559
|
-
}
|
|
560
|
-
]
|
|
561
|
-
});
|
|
562
|
-
const result = await strategy.authenticate({
|
|
563
|
-
headers: makeHeaders('plaintext-key'),
|
|
564
|
-
payload: payload
|
|
565
|
-
});
|
|
566
|
-
expect(result.user).toMatchObject({
|
|
567
|
-
id: 'u1',
|
|
568
|
-
email: 'a@b.com',
|
|
569
|
-
collection: 'users',
|
|
570
|
-
_strategy: AUTH_STRATEGY_NAME,
|
|
571
|
-
_mcpKey: {
|
|
572
|
-
keyId: 'k1',
|
|
573
|
-
keyPrefix: 'abc12345',
|
|
574
|
-
scopes: {
|
|
575
|
-
preset: 'admin'
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
});
|
|
579
|
-
// lastUsedAt update is fire-and-forget; allow microtask to schedule.
|
|
580
|
-
await new Promise((r)=>setImmediate(r));
|
|
581
|
-
expect(payload.updateMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
582
|
-
collection: 'payload-mcp-api-keys',
|
|
583
|
-
id: 'k1',
|
|
584
|
-
data: expect.objectContaining({
|
|
585
|
-
lastUsedAt: expect.any(String)
|
|
586
|
-
})
|
|
587
|
-
}));
|
|
588
|
-
});
|
|
589
|
-
it('hydrates _mcpKey.scopes from typed fields with collectionScopes + toolDeny', async ()=>{
|
|
590
|
-
const payload = buildPayload({
|
|
591
|
-
rows: [
|
|
592
|
-
{
|
|
593
|
-
id: 'k3',
|
|
594
|
-
user: {
|
|
595
|
-
id: 'u3'
|
|
596
|
-
},
|
|
597
|
-
keyPrefix: 'deadbeef',
|
|
598
|
-
preset: 'custom',
|
|
599
|
-
collectionScopes: [
|
|
600
|
-
{
|
|
601
|
-
slug: 'posts',
|
|
602
|
-
actions: [
|
|
603
|
-
'read',
|
|
604
|
-
'update'
|
|
605
|
-
]
|
|
606
|
-
}
|
|
607
|
-
],
|
|
608
|
-
toolDeny: [
|
|
609
|
-
'safeDelete'
|
|
610
|
-
]
|
|
611
|
-
}
|
|
612
|
-
]
|
|
613
|
-
});
|
|
614
|
-
const result = await strategy.authenticate({
|
|
615
|
-
headers: makeHeaders('plaintext-key'),
|
|
616
|
-
payload: payload
|
|
617
|
-
});
|
|
618
|
-
expect(result.user._mcpKey.scopes).toEqual({
|
|
619
|
-
collections: {
|
|
620
|
-
posts: [
|
|
621
|
-
'read',
|
|
622
|
-
'update'
|
|
623
|
-
]
|
|
624
|
-
},
|
|
625
|
-
tools: {
|
|
626
|
-
deny: [
|
|
627
|
-
'safeDelete'
|
|
628
|
-
]
|
|
629
|
-
}
|
|
630
|
-
});
|
|
631
|
-
});
|
|
632
|
-
it('does not block auth if find() throws', async ()=>{
|
|
633
|
-
const payload = buildPayload({
|
|
634
|
-
rows: [],
|
|
635
|
-
findError: new Error('db down')
|
|
636
|
-
});
|
|
637
|
-
const result = await strategy.authenticate({
|
|
638
|
-
headers: makeHeaders('plaintext-key'),
|
|
639
|
-
payload: payload
|
|
640
|
-
});
|
|
641
|
-
expect(result).toEqual({
|
|
642
|
-
user: null
|
|
643
|
-
});
|
|
644
|
-
expect(payload.logger.error).toHaveBeenCalled();
|
|
645
|
-
});
|
|
646
|
-
});
|
|
647
|
-
describe('getApiKeyContext', ()=>{
|
|
648
|
-
it('returns null for non-MCP requests', ()=>{
|
|
649
|
-
expect(getApiKeyContext({
|
|
650
|
-
user: null
|
|
651
|
-
})).toBeNull();
|
|
652
|
-
expect(getApiKeyContext({
|
|
653
|
-
user: {
|
|
654
|
-
id: 'cookie-user'
|
|
655
|
-
}
|
|
656
|
-
})).toBeNull();
|
|
657
|
-
});
|
|
658
|
-
it('returns the embedded key context when present', ()=>{
|
|
659
|
-
const ctx = getApiKeyContext({
|
|
660
|
-
user: {
|
|
661
|
-
id: 'u1',
|
|
662
|
-
_mcpKey: {
|
|
663
|
-
keyId: 'k1',
|
|
664
|
-
keyPrefix: 'abcd',
|
|
665
|
-
scopes: {
|
|
666
|
-
preset: 'editor'
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
});
|
|
671
|
-
expect(ctx).toEqual({
|
|
672
|
-
keyId: 'k1',
|
|
673
|
-
keyPrefix: 'abcd',
|
|
674
|
-
scopes: {
|
|
675
|
-
preset: 'editor'
|
|
676
|
-
}
|
|
677
|
-
});
|
|
678
|
-
});
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
//# sourceMappingURL=auth-strategy.test.js.map
|