payload-mcp-toolkit 0.7.0 → 0.7.4
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 +29 -8
- 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/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,292 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { createApiKeysCollection, API_KEYS_DEFAULT_SLUG } from '../api-keys';
|
|
3
|
-
const baseOptions = {
|
|
4
|
-
userCollection: 'users',
|
|
5
|
-
availableCollections: [
|
|
6
|
-
'posts',
|
|
7
|
-
'pages'
|
|
8
|
-
],
|
|
9
|
-
availableTools: [
|
|
10
|
-
'findDocument',
|
|
11
|
-
'createDocument',
|
|
12
|
-
'safeDelete'
|
|
13
|
-
]
|
|
14
|
-
};
|
|
15
|
-
function findNamed(fields, name) {
|
|
16
|
-
for (const f of fields){
|
|
17
|
-
if ('name' in f && f.name === name) return f;
|
|
18
|
-
if (f.type === 'collapsible' || f.type === 'row') {
|
|
19
|
-
const nested = findNamed(f.fields, name);
|
|
20
|
-
if (nested) return nested;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return undefined;
|
|
24
|
-
}
|
|
25
|
-
describe('createApiKeysCollection', ()=>{
|
|
26
|
-
it('defaults the slug to payload-mcp-api-keys', ()=>{
|
|
27
|
-
const collection = createApiKeysCollection(baseOptions);
|
|
28
|
-
expect(collection.slug).toBe(API_KEYS_DEFAULT_SLUG);
|
|
29
|
-
expect(collection.slug).toBe('payload-mcp-api-keys');
|
|
30
|
-
});
|
|
31
|
-
it('honours an explicit slug override', ()=>{
|
|
32
|
-
const collection = createApiKeysCollection({
|
|
33
|
-
...baseOptions,
|
|
34
|
-
slug: 'my-custom-keys'
|
|
35
|
-
});
|
|
36
|
-
expect(collection.slug).toBe('my-custom-keys');
|
|
37
|
-
});
|
|
38
|
-
it('binds the user relationship to the configured user collection', ()=>{
|
|
39
|
-
const collection = createApiKeysCollection({
|
|
40
|
-
...baseOptions,
|
|
41
|
-
userCollection: 'admins'
|
|
42
|
-
});
|
|
43
|
-
const userField = findNamed(collection.fields, 'user');
|
|
44
|
-
expect(userField).toBeDefined();
|
|
45
|
-
expect(userField.relationTo).toBe('admins');
|
|
46
|
-
});
|
|
47
|
-
it('throws a useful error when userCollection is missing', ()=>{
|
|
48
|
-
expect(()=>createApiKeysCollection({})).toThrow(/userCollection/);
|
|
49
|
-
});
|
|
50
|
-
it('throws a useful error when availableCollections is missing', ()=>{
|
|
51
|
-
expect(()=>createApiKeysCollection({
|
|
52
|
-
userCollection: 'users',
|
|
53
|
-
availableTools: []
|
|
54
|
-
})).toThrow(/availableCollections/);
|
|
55
|
-
});
|
|
56
|
-
it('throws a useful error when availableTools is missing', ()=>{
|
|
57
|
-
expect(()=>createApiKeysCollection({
|
|
58
|
-
userCollection: 'users',
|
|
59
|
-
availableCollections: []
|
|
60
|
-
})).toThrow(/availableTools/);
|
|
61
|
-
});
|
|
62
|
-
it('reuses Payload built-in API key auth so legacy rows stay authenticatable', ()=>{
|
|
63
|
-
const collection = createApiKeysCollection(baseOptions);
|
|
64
|
-
expect(collection.auth).toMatchObject({
|
|
65
|
-
useAPIKey: true,
|
|
66
|
-
disableLocalStrategy: true
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
it('declares the typed scope surface and lifecycle fields', ()=>{
|
|
70
|
-
const collection = createApiKeysCollection(baseOptions);
|
|
71
|
-
for (const expected of [
|
|
72
|
-
'name',
|
|
73
|
-
'description',
|
|
74
|
-
'user',
|
|
75
|
-
'preset',
|
|
76
|
-
'collectionScopes',
|
|
77
|
-
'toolAllow',
|
|
78
|
-
'toolDeny',
|
|
79
|
-
'keyPrefix',
|
|
80
|
-
'expiresAt',
|
|
81
|
-
'revokedAt',
|
|
82
|
-
'lastUsedAt'
|
|
83
|
-
]){
|
|
84
|
-
expect(findNamed(collection.fields, expected), `missing field: ${expected}`).toBeDefined();
|
|
85
|
-
}
|
|
86
|
-
// Legacy column must be gone.
|
|
87
|
-
expect(findNamed(collection.fields, 'scopes')).toBeUndefined();
|
|
88
|
-
});
|
|
89
|
-
it('exposes the four preset values with custom as the default', ()=>{
|
|
90
|
-
const collection = createApiKeysCollection(baseOptions);
|
|
91
|
-
const preset = findNamed(collection.fields, 'preset');
|
|
92
|
-
expect(preset.required).toBe(true);
|
|
93
|
-
expect(preset.defaultValue).toBe('custom');
|
|
94
|
-
expect(preset.options?.map((o)=>o.value)).toEqual([
|
|
95
|
-
'read-only',
|
|
96
|
-
'editor',
|
|
97
|
-
'admin',
|
|
98
|
-
'custom'
|
|
99
|
-
]);
|
|
100
|
-
});
|
|
101
|
-
it('renders collectionScopes as a JSON field with the matrix component and runtime options', ()=>{
|
|
102
|
-
const collection = createApiKeysCollection({
|
|
103
|
-
...baseOptions,
|
|
104
|
-
availableCollections: [
|
|
105
|
-
'a',
|
|
106
|
-
'b',
|
|
107
|
-
'c'
|
|
108
|
-
]
|
|
109
|
-
});
|
|
110
|
-
const scopes = findNamed(collection.fields, 'collectionScopes');
|
|
111
|
-
expect(scopes.type).toBe('json');
|
|
112
|
-
expect(scopes.admin?.components?.Field?.path).toBe('payload-mcp-toolkit/client');
|
|
113
|
-
expect(scopes.admin?.components?.Field?.exportName).toBe('CollectionScopesMatrix');
|
|
114
|
-
expect(scopes.admin?.components?.Field?.clientProps?.availableCollections).toEqual([
|
|
115
|
-
'a',
|
|
116
|
-
'b',
|
|
117
|
-
'c'
|
|
118
|
-
]);
|
|
119
|
-
expect(scopes.admin?.condition?.({
|
|
120
|
-
preset: 'custom'
|
|
121
|
-
})).toBe(true);
|
|
122
|
-
expect(scopes.admin?.condition?.({
|
|
123
|
-
preset: 'editor'
|
|
124
|
-
})).toBe(false);
|
|
125
|
-
});
|
|
126
|
-
it('renders globalScopes as a JSON field mirroring collectionScopes', ()=>{
|
|
127
|
-
const collection = createApiKeysCollection({
|
|
128
|
-
...baseOptions,
|
|
129
|
-
availableGlobals: [
|
|
130
|
-
'siteSettings',
|
|
131
|
-
'footer'
|
|
132
|
-
]
|
|
133
|
-
});
|
|
134
|
-
const scopes = findNamed(collection.fields, 'globalScopes');
|
|
135
|
-
expect(scopes.type).toBe('json');
|
|
136
|
-
expect(scopes.admin?.components?.Field?.exportName).toBe('GlobalScopesMatrix');
|
|
137
|
-
expect(scopes.admin?.components?.Field?.clientProps?.availableGlobals).toEqual([
|
|
138
|
-
'siteSettings',
|
|
139
|
-
'footer'
|
|
140
|
-
]);
|
|
141
|
-
// Conditional render mirrors collectionScopes: Custom preset only. The
|
|
142
|
-
// empty-config case is handled inside ScopesTable's empty-state render,
|
|
143
|
-
// not by gating the field's admin.condition.
|
|
144
|
-
expect(scopes.admin?.condition?.({
|
|
145
|
-
preset: 'custom'
|
|
146
|
-
})).toBe(true);
|
|
147
|
-
expect(scopes.admin?.condition?.({
|
|
148
|
-
preset: 'editor'
|
|
149
|
-
})).toBe(false);
|
|
150
|
-
});
|
|
151
|
-
it('renders the globalScopes field under Custom even when no globals are available', ()=>{
|
|
152
|
-
// No `availableGlobals.length > 0` gate — the matrix component renders
|
|
153
|
-
// its own empty-state copy ("No globals are available…") so operators
|
|
154
|
-
// see why the table is blank instead of seeing nothing at all.
|
|
155
|
-
const collection = createApiKeysCollection({
|
|
156
|
-
...baseOptions
|
|
157
|
-
});
|
|
158
|
-
const scopes = findNamed(collection.fields, 'globalScopes');
|
|
159
|
-
expect(scopes).toBeDefined();
|
|
160
|
-
expect(scopes.admin?.condition?.({
|
|
161
|
-
preset: 'custom'
|
|
162
|
-
})).toBe(true);
|
|
163
|
-
expect(scopes.admin?.condition?.({
|
|
164
|
-
preset: 'editor'
|
|
165
|
-
})).toBe(false);
|
|
166
|
-
expect(scopes.admin?.components?.Field?.clientProps?.availableGlobals).toEqual([]);
|
|
167
|
-
});
|
|
168
|
-
it('createApiKeysCollection accepts options without availableGlobals (back-compat)', ()=>{
|
|
169
|
-
// Direct callers from before globals support continue to work.
|
|
170
|
-
expect(()=>createApiKeysCollection({
|
|
171
|
-
...baseOptions
|
|
172
|
-
})).not.toThrow();
|
|
173
|
-
});
|
|
174
|
-
it('groups tool overrides into a custom-only collapsible', ()=>{
|
|
175
|
-
const collection = createApiKeysCollection(baseOptions);
|
|
176
|
-
const collapsible = collection.fields.find((f)=>f.type === 'collapsible' && f.label === 'Tool overrides');
|
|
177
|
-
expect(collapsible).toBeDefined();
|
|
178
|
-
expect(collapsible?.admin?.condition?.({
|
|
179
|
-
preset: 'custom'
|
|
180
|
-
})).toBe(true);
|
|
181
|
-
expect(collapsible?.admin?.condition?.({
|
|
182
|
-
preset: 'admin'
|
|
183
|
-
})).toBe(false);
|
|
184
|
-
const toolAllow = (collapsible?.fields ?? []).find((f)=>'name' in f && f.name === 'toolAllow');
|
|
185
|
-
const toolDeny = (collapsible?.fields ?? []).find((f)=>'name' in f && f.name === 'toolDeny');
|
|
186
|
-
expect(toolAllow?.options?.map((o)=>o.value)).toEqual(baseOptions.availableTools);
|
|
187
|
-
expect(toolDeny?.options?.map((o)=>o.value)).toEqual(baseOptions.availableTools);
|
|
188
|
-
});
|
|
189
|
-
it('places identity and lifecycle fields in the sidebar', ()=>{
|
|
190
|
-
const collection = createApiKeysCollection(baseOptions);
|
|
191
|
-
for (const expected of [
|
|
192
|
-
'user',
|
|
193
|
-
'keyPrefix',
|
|
194
|
-
'expiresAt',
|
|
195
|
-
'revokedAt',
|
|
196
|
-
'lastUsedAt'
|
|
197
|
-
]){
|
|
198
|
-
const f = findNamed(collection.fields, expected);
|
|
199
|
-
expect(f.admin?.position, `${expected} should be in the sidebar`).toBe('sidebar');
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
it('keyPrefix beforeChange captures first 8 chars of a freshly generated apiKey', ()=>{
|
|
203
|
-
const collection = createApiKeysCollection(baseOptions);
|
|
204
|
-
const prefixField = findNamed(collection.fields, 'keyPrefix');
|
|
205
|
-
const hook = prefixField.hooks?.beforeChange?.[0];
|
|
206
|
-
expect(hook).toBeDefined();
|
|
207
|
-
const result = hook({
|
|
208
|
-
data: {
|
|
209
|
-
apiKey: 'abc12345-extra-stuff'
|
|
210
|
-
},
|
|
211
|
-
originalDoc: undefined,
|
|
212
|
-
value: undefined
|
|
213
|
-
});
|
|
214
|
-
expect(result).toBe('abc12345');
|
|
215
|
-
});
|
|
216
|
-
it('keyPrefix beforeChange preserves an existing prefix when the apiKey is not present (e.g. updates)', ()=>{
|
|
217
|
-
const collection = createApiKeysCollection(baseOptions);
|
|
218
|
-
const prefixField = findNamed(collection.fields, 'keyPrefix');
|
|
219
|
-
const hook = prefixField.hooks.beforeChange[0];
|
|
220
|
-
const result = hook({
|
|
221
|
-
data: {},
|
|
222
|
-
originalDoc: {
|
|
223
|
-
keyPrefix: 'cafebabe'
|
|
224
|
-
},
|
|
225
|
-
value: undefined
|
|
226
|
-
});
|
|
227
|
-
expect(result).toBe('cafebabe');
|
|
228
|
-
});
|
|
229
|
-
it('keyPrefix beforeChange respects a value already provided', ()=>{
|
|
230
|
-
const collection = createApiKeysCollection(baseOptions);
|
|
231
|
-
const prefixField = findNamed(collection.fields, 'keyPrefix');
|
|
232
|
-
const hook = prefixField.hooks.beforeChange[0];
|
|
233
|
-
const result = hook({
|
|
234
|
-
data: {
|
|
235
|
-
apiKey: 'newvalue-ignored'
|
|
236
|
-
},
|
|
237
|
-
originalDoc: undefined,
|
|
238
|
-
value: 'preset-prefix'
|
|
239
|
-
});
|
|
240
|
-
expect(result).toBe('preset-prefix');
|
|
241
|
-
});
|
|
242
|
-
it('beforeValidate coerces empty toolAllow/toolDeny to null under preset modes (REST trap fix)', ()=>{
|
|
243
|
-
const collection = createApiKeysCollection(baseOptions);
|
|
244
|
-
const hook = (collection.hooks?.beforeValidate ?? [])[0];
|
|
245
|
-
expect(hook).toBeDefined();
|
|
246
|
-
const out = hook({
|
|
247
|
-
data: {
|
|
248
|
-
preset: 'admin',
|
|
249
|
-
toolAllow: [],
|
|
250
|
-
toolDeny: []
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
expect(out.toolAllow).toBeNull();
|
|
254
|
-
expect(out.toolDeny).toBeNull();
|
|
255
|
-
});
|
|
256
|
-
it('beforeValidate preserves explicit-empty toolAllow under the Custom preset (deny-all semantic)', ()=>{
|
|
257
|
-
const collection = createApiKeysCollection(baseOptions);
|
|
258
|
-
const hook = (collection.hooks?.beforeValidate ?? [])[0];
|
|
259
|
-
const out = hook({
|
|
260
|
-
data: {
|
|
261
|
-
preset: 'custom',
|
|
262
|
-
toolAllow: [],
|
|
263
|
-
toolDeny: []
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
expect(out.toolAllow).toEqual([]);
|
|
267
|
-
expect(out.toolDeny).toEqual([]);
|
|
268
|
-
});
|
|
269
|
-
it('beforeValidate leaves populated tool arrays untouched', ()=>{
|
|
270
|
-
const collection = createApiKeysCollection(baseOptions);
|
|
271
|
-
const hook = (collection.hooks?.beforeValidate ?? [])[0];
|
|
272
|
-
const out = hook({
|
|
273
|
-
data: {
|
|
274
|
-
preset: 'admin',
|
|
275
|
-
toolAllow: [
|
|
276
|
-
'findDocument'
|
|
277
|
-
],
|
|
278
|
-
toolDeny: [
|
|
279
|
-
'safeDelete'
|
|
280
|
-
]
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
expect(out.toolAllow).toEqual([
|
|
284
|
-
'findDocument'
|
|
285
|
-
]);
|
|
286
|
-
expect(out.toolDeny).toEqual([
|
|
287
|
-
'safeDelete'
|
|
288
|
-
]);
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
//# sourceMappingURL=api-keys.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/__tests__/api-keys.test.ts"],"sourcesContent":["import { describe, it, expect } from 'vitest'\r\nimport type { Field } from 'payload'\r\nimport { createApiKeysCollection, API_KEYS_DEFAULT_SLUG } from '../api-keys'\r\n\r\nconst baseOptions = {\r\n userCollection: 'users',\r\n availableCollections: ['posts', 'pages'],\r\n availableTools: ['findDocument', 'createDocument', 'safeDelete'],\r\n}\r\n\r\nfunction findNamed(fields: Field[], name: string): Field | undefined {\r\n for (const f of fields) {\r\n if ('name' in f && f.name === name) return f\r\n if (f.type === 'collapsible' || f.type === 'row') {\r\n const nested = findNamed(f.fields as Field[], name)\r\n if (nested) return nested\r\n }\r\n }\r\n return undefined\r\n}\r\n\r\ndescribe('createApiKeysCollection', () => {\r\n it('defaults the slug to payload-mcp-api-keys', () => {\r\n const collection = createApiKeysCollection(baseOptions)\r\n expect(collection.slug).toBe(API_KEYS_DEFAULT_SLUG)\r\n expect(collection.slug).toBe('payload-mcp-api-keys')\r\n })\r\n\r\n it('honours an explicit slug override', () => {\r\n const collection = createApiKeysCollection({ ...baseOptions, slug: 'my-custom-keys' })\r\n expect(collection.slug).toBe('my-custom-keys')\r\n })\r\n\r\n it('binds the user relationship to the configured user collection', () => {\r\n const collection = createApiKeysCollection({ ...baseOptions, userCollection: 'admins' })\r\n const userField = findNamed(collection.fields as Field[], 'user') as { relationTo?: string }\r\n expect(userField).toBeDefined()\r\n expect(userField.relationTo).toBe('admins')\r\n })\r\n\r\n it('throws a useful error when userCollection is missing', () => {\r\n expect(() =>\r\n createApiKeysCollection({} as unknown as Parameters<typeof createApiKeysCollection>[0]),\r\n ).toThrow(/userCollection/)\r\n })\r\n\r\n it('throws a useful error when availableCollections is missing', () => {\r\n expect(() =>\r\n createApiKeysCollection({\r\n userCollection: 'users',\r\n availableTools: [],\r\n } as unknown as Parameters<typeof createApiKeysCollection>[0]),\r\n ).toThrow(/availableCollections/)\r\n })\r\n\r\n it('throws a useful error when availableTools is missing', () => {\r\n expect(() =>\r\n createApiKeysCollection({\r\n userCollection: 'users',\r\n availableCollections: [],\r\n } as unknown as Parameters<typeof createApiKeysCollection>[0]),\r\n ).toThrow(/availableTools/)\r\n })\r\n\r\n it('reuses Payload built-in API key auth so legacy rows stay authenticatable', () => {\r\n const collection = createApiKeysCollection(baseOptions)\r\n expect(collection.auth).toMatchObject({\r\n useAPIKey: true,\r\n disableLocalStrategy: true,\r\n })\r\n })\r\n\r\n it('declares the typed scope surface and lifecycle fields', () => {\r\n const collection = createApiKeysCollection(baseOptions)\r\n for (const expected of [\r\n 'name',\r\n 'description',\r\n 'user',\r\n 'preset',\r\n 'collectionScopes',\r\n 'toolAllow',\r\n 'toolDeny',\r\n 'keyPrefix',\r\n 'expiresAt',\r\n 'revokedAt',\r\n 'lastUsedAt',\r\n ]) {\r\n expect(findNamed(collection.fields as Field[], expected), `missing field: ${expected}`).toBeDefined()\r\n }\r\n // Legacy column must be gone.\r\n expect(findNamed(collection.fields as Field[], 'scopes')).toBeUndefined()\r\n })\r\n\r\n it('exposes the four preset values with custom as the default', () => {\r\n const collection = createApiKeysCollection(baseOptions)\r\n const preset = findNamed(collection.fields as Field[], 'preset') as {\r\n required?: boolean\r\n defaultValue?: string\r\n options?: Array<{ value: string }>\r\n }\r\n expect(preset.required).toBe(true)\r\n expect(preset.defaultValue).toBe('custom')\r\n expect(preset.options?.map((o) => o.value)).toEqual([\r\n 'read-only',\r\n 'editor',\r\n 'admin',\r\n 'custom',\r\n ])\r\n })\r\n\r\n it('renders collectionScopes as a JSON field with the matrix component and runtime options', () => {\r\n const collection = createApiKeysCollection({\r\n ...baseOptions,\r\n availableCollections: ['a', 'b', 'c'],\r\n })\r\n const scopes = findNamed(collection.fields as Field[], 'collectionScopes') as {\r\n type?: string\r\n admin?: {\r\n components?: {\r\n Field?: {\r\n path?: string\r\n exportName?: string\r\n clientProps?: { availableCollections?: string[] }\r\n }\r\n }\r\n condition?: (data: unknown) => boolean\r\n }\r\n }\r\n expect(scopes.type).toBe('json')\r\n expect(scopes.admin?.components?.Field?.path).toBe('payload-mcp-toolkit/client')\r\n expect(scopes.admin?.components?.Field?.exportName).toBe('CollectionScopesMatrix')\r\n expect(scopes.admin?.components?.Field?.clientProps?.availableCollections).toEqual([\r\n 'a',\r\n 'b',\r\n 'c',\r\n ])\r\n expect(scopes.admin?.condition?.({ preset: 'custom' })).toBe(true)\r\n expect(scopes.admin?.condition?.({ preset: 'editor' })).toBe(false)\r\n })\r\n\r\n it('renders globalScopes as a JSON field mirroring collectionScopes', () => {\r\n const collection = createApiKeysCollection({\r\n ...baseOptions,\r\n availableGlobals: ['siteSettings', 'footer'],\r\n })\r\n const scopes = findNamed(collection.fields as Field[], 'globalScopes') as {\r\n type?: string\r\n admin?: {\r\n components?: {\r\n Field?: {\r\n path?: string\r\n exportName?: string\r\n clientProps?: { availableGlobals?: string[] }\r\n }\r\n }\r\n condition?: (data: unknown) => boolean\r\n }\r\n }\r\n expect(scopes.type).toBe('json')\r\n expect(scopes.admin?.components?.Field?.exportName).toBe('GlobalScopesMatrix')\r\n expect(scopes.admin?.components?.Field?.clientProps?.availableGlobals).toEqual([\r\n 'siteSettings',\r\n 'footer',\r\n ])\r\n // Conditional render mirrors collectionScopes: Custom preset only. The\r\n // empty-config case is handled inside ScopesTable's empty-state render,\r\n // not by gating the field's admin.condition.\r\n expect(scopes.admin?.condition?.({ preset: 'custom' })).toBe(true)\r\n expect(scopes.admin?.condition?.({ preset: 'editor' })).toBe(false)\r\n })\r\n\r\n it('renders the globalScopes field under Custom even when no globals are available', () => {\r\n // No `availableGlobals.length > 0` gate — the matrix component renders\r\n // its own empty-state copy (\"No globals are available…\") so operators\r\n // see why the table is blank instead of seeing nothing at all.\r\n const collection = createApiKeysCollection({ ...baseOptions })\r\n const scopes = findNamed(collection.fields as Field[], 'globalScopes') as {\r\n admin?: {\r\n condition?: (data: unknown) => boolean\r\n components?: { Field?: { clientProps?: { availableGlobals?: string[] } } }\r\n }\r\n }\r\n expect(scopes).toBeDefined()\r\n expect(scopes.admin?.condition?.({ preset: 'custom' })).toBe(true)\r\n expect(scopes.admin?.condition?.({ preset: 'editor' })).toBe(false)\r\n expect(scopes.admin?.components?.Field?.clientProps?.availableGlobals).toEqual([])\r\n })\r\n\r\n it('createApiKeysCollection accepts options without availableGlobals (back-compat)', () => {\r\n // Direct callers from before globals support continue to work.\r\n expect(() => createApiKeysCollection({ ...baseOptions })).not.toThrow()\r\n })\r\n\r\n it('groups tool overrides into a custom-only collapsible', () => {\r\n const collection = createApiKeysCollection(baseOptions)\r\n const collapsible = (collection.fields as Field[]).find(\r\n (f) => f.type === 'collapsible' && f.label === 'Tool overrides',\r\n ) as { admin?: { condition?: (data: unknown) => boolean }; fields?: Field[] } | undefined\r\n expect(collapsible).toBeDefined()\r\n expect(collapsible?.admin?.condition?.({ preset: 'custom' })).toBe(true)\r\n expect(collapsible?.admin?.condition?.({ preset: 'admin' })).toBe(false)\r\n\r\n const toolAllow = (collapsible?.fields ?? []).find(\r\n (f) => 'name' in f && f.name === 'toolAllow',\r\n ) as { options?: Array<{ value: string }> } | undefined\r\n const toolDeny = (collapsible?.fields ?? []).find(\r\n (f) => 'name' in f && f.name === 'toolDeny',\r\n ) as { options?: Array<{ value: string }> } | undefined\r\n expect(toolAllow?.options?.map((o) => o.value)).toEqual(baseOptions.availableTools)\r\n expect(toolDeny?.options?.map((o) => o.value)).toEqual(baseOptions.availableTools)\r\n })\r\n\r\n it('places identity and lifecycle fields in the sidebar', () => {\r\n const collection = createApiKeysCollection(baseOptions)\r\n for (const expected of ['user', 'keyPrefix', 'expiresAt', 'revokedAt', 'lastUsedAt']) {\r\n const f = findNamed(collection.fields as Field[], expected) as {\r\n admin?: { position?: string }\r\n }\r\n expect(f.admin?.position, `${expected} should be in the sidebar`).toBe('sidebar')\r\n }\r\n })\r\n\r\n it('keyPrefix beforeChange captures first 8 chars of a freshly generated apiKey', () => {\r\n const collection = createApiKeysCollection(baseOptions)\r\n const prefixField = findNamed(collection.fields as Field[], 'keyPrefix') as {\r\n hooks?: { beforeChange?: Array<(args: unknown) => unknown> }\r\n }\r\n\r\n const hook = prefixField.hooks?.beforeChange?.[0]\r\n expect(hook).toBeDefined()\r\n\r\n const result = hook!({\r\n data: { apiKey: 'abc12345-extra-stuff' },\r\n originalDoc: undefined,\r\n value: undefined,\r\n })\r\n expect(result).toBe('abc12345')\r\n })\r\n\r\n it('keyPrefix beforeChange preserves an existing prefix when the apiKey is not present (e.g. updates)', () => {\r\n const collection = createApiKeysCollection(baseOptions)\r\n const prefixField = findNamed(collection.fields as Field[], 'keyPrefix') as {\r\n hooks?: { beforeChange?: Array<(args: unknown) => unknown> }\r\n }\r\n const hook = prefixField.hooks!.beforeChange![0]\r\n\r\n const result = hook({\r\n data: {},\r\n originalDoc: { keyPrefix: 'cafebabe' },\r\n value: undefined,\r\n })\r\n expect(result).toBe('cafebabe')\r\n })\r\n\r\n it('keyPrefix beforeChange respects a value already provided', () => {\r\n const collection = createApiKeysCollection(baseOptions)\r\n const prefixField = findNamed(collection.fields as Field[], 'keyPrefix') as {\r\n hooks?: { beforeChange?: Array<(args: unknown) => unknown> }\r\n }\r\n const hook = prefixField.hooks!.beforeChange![0]\r\n\r\n const result = hook({\r\n data: { apiKey: 'newvalue-ignored' },\r\n originalDoc: undefined,\r\n value: 'preset-prefix',\r\n })\r\n expect(result).toBe('preset-prefix')\r\n })\r\n\r\n it('beforeValidate coerces empty toolAllow/toolDeny to null under preset modes (REST trap fix)', () => {\r\n const collection = createApiKeysCollection(baseOptions)\r\n const hook = (collection.hooks?.beforeValidate ?? [])[0] as\r\n | undefined\r\n | ((args: { data: unknown }) => Record<string, unknown> | undefined)\r\n expect(hook).toBeDefined()\r\n\r\n const out = hook!({\r\n data: { preset: 'admin', toolAllow: [], toolDeny: [] },\r\n }) as Record<string, unknown>\r\n expect(out.toolAllow).toBeNull()\r\n expect(out.toolDeny).toBeNull()\r\n })\r\n\r\n it('beforeValidate preserves explicit-empty toolAllow under the Custom preset (deny-all semantic)', () => {\r\n const collection = createApiKeysCollection(baseOptions)\r\n const hook = (collection.hooks?.beforeValidate ?? [])[0] as\r\n | undefined\r\n | ((args: { data: unknown }) => Record<string, unknown> | undefined)\r\n\r\n const out = hook!({\r\n data: { preset: 'custom', toolAllow: [], toolDeny: [] },\r\n }) as Record<string, unknown>\r\n expect(out.toolAllow).toEqual([])\r\n expect(out.toolDeny).toEqual([])\r\n })\r\n\r\n it('beforeValidate leaves populated tool arrays untouched', () => {\r\n const collection = createApiKeysCollection(baseOptions)\r\n const hook = (collection.hooks?.beforeValidate ?? [])[0] as\r\n | undefined\r\n | ((args: { data: unknown }) => Record<string, unknown> | undefined)\r\n\r\n const out = hook!({\r\n data: { preset: 'admin', toolAllow: ['findDocument'], toolDeny: ['safeDelete'] },\r\n }) as Record<string, unknown>\r\n expect(out.toolAllow).toEqual(['findDocument'])\r\n expect(out.toolDeny).toEqual(['safeDelete'])\r\n })\r\n})\r\n"],"names":["describe","it","expect","createApiKeysCollection","API_KEYS_DEFAULT_SLUG","baseOptions","userCollection","availableCollections","availableTools","findNamed","fields","name","f","type","nested","undefined","collection","slug","toBe","userField","toBeDefined","relationTo","toThrow","auth","toMatchObject","useAPIKey","disableLocalStrategy","expected","toBeUndefined","preset","required","defaultValue","options","map","o","value","toEqual","scopes","admin","components","Field","path","exportName","clientProps","condition","availableGlobals","not","collapsible","find","label","toolAllow","toolDeny","position","prefixField","hook","hooks","beforeChange","result","data","apiKey","originalDoc","keyPrefix","beforeValidate","out","toBeNull"],"mappings":"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"}
|