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.
Files changed (56) hide show
  1. package/README.md +30 -9
  2. package/dist/api-keys.js +57 -21
  3. package/dist/api-keys.js.map +1 -1
  4. package/dist/auth-strategy.d.ts +18 -7
  5. package/dist/auth-strategy.js +54 -12
  6. package/dist/auth-strategy.js.map +1 -1
  7. package/dist/tools/_helpers.d.ts +34 -0
  8. package/dist/tools/_helpers.js +98 -0
  9. package/dist/tools/_helpers.js.map +1 -1
  10. package/dist/tools/create-document.js +8 -0
  11. package/dist/tools/create-document.js.map +1 -1
  12. package/dist/tools/delete-document.d.ts +1 -1
  13. package/dist/tools/delete-document.js +6 -6
  14. package/dist/tools/delete-document.js.map +1 -1
  15. package/dist/tools/find-document.d.ts +3 -3
  16. package/dist/tools/find-document.js +8 -8
  17. package/dist/tools/find-document.js.map +1 -1
  18. package/dist/tools/publish-draft.js +33 -1
  19. package/dist/tools/publish-draft.js.map +1 -1
  20. package/dist/tools/publish-global-draft.js +30 -1
  21. package/dist/tools/publish-global-draft.js.map +1 -1
  22. package/package.json +29 -15
  23. package/dist/__tests__/api-keys.test.js +0 -292
  24. package/dist/__tests__/api-keys.test.js.map +0 -1
  25. package/dist/__tests__/auth-strategy.test.js +0 -681
  26. package/dist/__tests__/auth-strategy.test.js.map +0 -1
  27. package/dist/__tests__/conflict-detection.test.js +0 -69
  28. package/dist/__tests__/conflict-detection.test.js.map +0 -1
  29. package/dist/__tests__/delete-document.test.js +0 -70
  30. package/dist/__tests__/delete-document.test.js.map +0 -1
  31. package/dist/__tests__/endpoint.test.js +0 -143
  32. package/dist/__tests__/endpoint.test.js.map +0 -1
  33. package/dist/__tests__/find-document.test.js +0 -178
  34. package/dist/__tests__/find-document.test.js.map +0 -1
  35. package/dist/__tests__/find-global.test.js +0 -173
  36. package/dist/__tests__/find-global.test.js.map +0 -1
  37. package/dist/__tests__/global-versions.test.js +0 -183
  38. package/dist/__tests__/global-versions.test.js.map +0 -1
  39. package/dist/__tests__/hash.test.js +0 -58
  40. package/dist/__tests__/hash.test.js.map +0 -1
  41. package/dist/__tests__/index-integration.test.js +0 -191
  42. package/dist/__tests__/index-integration.test.js.map +0 -1
  43. package/dist/__tests__/introspection.test.js +0 -659
  44. package/dist/__tests__/introspection.test.js.map +0 -1
  45. package/dist/__tests__/patch-global-layout.test.js +0 -474
  46. package/dist/__tests__/patch-global-layout.test.js.map +0 -1
  47. package/dist/__tests__/patch-layout.test.js +0 -171
  48. package/dist/__tests__/patch-layout.test.js.map +0 -1
  49. package/dist/__tests__/registry.test.js +0 -795
  50. package/dist/__tests__/registry.test.js.map +0 -1
  51. package/dist/__tests__/resources.test.js +0 -139
  52. package/dist/__tests__/resources.test.js.map +0 -1
  53. package/dist/__tests__/update-global.test.js +0 -157
  54. package/dist/__tests__/update-global.test.js.map +0 -1
  55. package/dist/__tests__/url-validator.test.js +0 -326
  56. package/dist/__tests__/url-validator.test.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payload-mcp-toolkit",
3
- "version": "0.7.0",
3
+ "version": "0.7.5",
4
4
  "description": "Standalone schema-aware MCP plugin for Payload CMS v3 — owns the /api/mcp endpoint, scoped API keys, draft workflow, and AI-friendly tools so non-technical editors can manage content via AI chat.",
5
5
  "license": "MIT",
6
6
  "author": "jon8800",
@@ -38,9 +38,29 @@
38
38
  "main": "./dist/index.js",
39
39
  "types": "./dist/index.d.ts",
40
40
  "files": [
41
- "dist"
41
+ "dist",
42
+ "!dist/__tests__",
43
+ "!dist/**/*.test.js",
44
+ "!dist/**/*.test.js.map",
45
+ "!dist/**/*.test.d.ts"
42
46
  ],
43
47
  "sideEffects": false,
48
+ "scripts": {
49
+ "build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
50
+ "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
51
+ "build:types": "tsc -p tsconfig.build.json",
52
+ "clean": "rimraf dist",
53
+ "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,svg,json}\" dist/",
54
+ "dev": "next dev dev --turbo",
55
+ "dev:generate-importmap": "cd dev && cross-env PAYLOAD_CONFIG_PATH=./payload.config.ts payload generate:importmap",
56
+ "dev:generate-types": "cd dev && cross-env PAYLOAD_CONFIG_PATH=./payload.config.ts payload generate:types",
57
+ "dev:payload": "cd dev && cross-env PAYLOAD_CONFIG_PATH=./payload.config.ts payload",
58
+ "prepack": "node scripts/prepack.mjs",
59
+ "postpack": "node scripts/postpack.mjs",
60
+ "prepublishOnly": "pnpm clean && pnpm build && pnpm test",
61
+ "test": "vitest run",
62
+ "test:watch": "vitest"
63
+ },
44
64
  "dependencies": {
45
65
  "@modelcontextprotocol/sdk": "^1.18.0",
46
66
  "mcp-handler": "^1.1.0"
@@ -76,17 +96,11 @@
76
96
  "node": "^18.20.2 || >=20.9.0",
77
97
  "pnpm": "^9 || ^10"
78
98
  },
79
- "scripts": {
80
- "build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
81
- "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
82
- "build:types": "tsc -p tsconfig.build.json",
83
- "clean": "rimraf dist",
84
- "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,svg,json}\" dist/",
85
- "dev": "next dev dev --turbo",
86
- "dev:generate-importmap": "cd dev && cross-env PAYLOAD_CONFIG_PATH=./payload.config.ts payload generate:importmap",
87
- "dev:generate-types": "cd dev && cross-env PAYLOAD_CONFIG_PATH=./payload.config.ts payload generate:types",
88
- "dev:payload": "cd dev && cross-env PAYLOAD_CONFIG_PATH=./payload.config.ts payload",
89
- "test": "vitest run",
90
- "test:watch": "vitest"
99
+ "pnpm": {
100
+ "onlyBuiltDependencies": [
101
+ "sharp",
102
+ "esbuild",
103
+ "better-sqlite3"
104
+ ]
91
105
  }
92
- }
106
+ }
@@ -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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"}