payload-mcp-toolkit 0.3.4 → 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.
Files changed (116) hide show
  1. package/README.md +253 -151
  2. package/dist/api-keys.d.ts +46 -0
  3. package/dist/api-keys.js +308 -0
  4. package/dist/api-keys.js.map +1 -0
  5. package/dist/auth-strategy.d.ts +96 -0
  6. package/dist/auth-strategy.js +261 -0
  7. package/dist/auth-strategy.js.map +1 -0
  8. package/dist/components/CollectionScopesMatrix.d.ts +8 -0
  9. package/dist/components/CollectionScopesMatrix.js +32 -0
  10. package/dist/components/CollectionScopesMatrix.js.map +1 -0
  11. package/dist/components/GlobalScopesMatrix.d.ts +8 -0
  12. package/dist/components/GlobalScopesMatrix.js +28 -0
  13. package/dist/components/GlobalScopesMatrix.js.map +1 -0
  14. package/dist/components/ScopesTable.d.ts +19 -0
  15. package/dist/components/ScopesTable.js +285 -0
  16. package/dist/components/ScopesTable.js.map +1 -0
  17. package/dist/components/index.d.ts +2 -0
  18. package/dist/components/index.js +4 -0
  19. package/dist/components/index.js.map +1 -0
  20. package/dist/conflict-detection.d.ts +13 -0
  21. package/dist/conflict-detection.js +41 -0
  22. package/dist/conflict-detection.js.map +1 -0
  23. package/dist/draft-workflow.d.ts +46 -48
  24. package/dist/draft-workflow.js +53 -135
  25. package/dist/draft-workflow.js.map +1 -1
  26. package/dist/endpoint.d.ts +35 -0
  27. package/dist/endpoint.js +105 -0
  28. package/dist/endpoint.js.map +1 -0
  29. package/dist/hash.d.ts +21 -0
  30. package/dist/hash.js +36 -0
  31. package/dist/hash.js.map +1 -0
  32. package/dist/index.d.ts +9 -9
  33. package/dist/index.js +167 -69
  34. package/dist/index.js.map +1 -1
  35. package/dist/introspection.d.ts +17 -3
  36. package/dist/introspection.js +95 -36
  37. package/dist/introspection.js.map +1 -1
  38. package/dist/prompts.js +5 -5
  39. package/dist/prompts.js.map +1 -1
  40. package/dist/registry.d.ts +50 -0
  41. package/dist/registry.js +169 -0
  42. package/dist/registry.js.map +1 -0
  43. package/dist/resources.d.ts +5 -3
  44. package/dist/resources.js +23 -11
  45. package/dist/resources.js.map +1 -1
  46. package/dist/scope/audit-log.d.ts +18 -0
  47. package/dist/scope/audit-log.js +50 -0
  48. package/dist/scope/audit-log.js.map +1 -0
  49. package/dist/scope/policy.d.ts +73 -0
  50. package/dist/scope/policy.js +218 -0
  51. package/dist/scope/policy.js.map +1 -0
  52. package/dist/tools/_helpers.d.ts +62 -1
  53. package/dist/tools/_helpers.js +181 -0
  54. package/dist/tools/_helpers.js.map +1 -1
  55. package/dist/tools/_layout-helpers.d.ts +43 -0
  56. package/dist/tools/_layout-helpers.js +159 -0
  57. package/dist/tools/_layout-helpers.js.map +1 -0
  58. package/dist/tools/create-document.d.ts +5 -5
  59. package/dist/tools/create-document.js +25 -21
  60. package/dist/tools/create-document.js.map +1 -1
  61. package/dist/tools/delete-document.d.ts +25 -0
  62. package/dist/tools/delete-document.js +49 -0
  63. package/dist/tools/delete-document.js.map +1 -0
  64. package/dist/tools/find-document.d.ts +33 -0
  65. package/dist/tools/find-document.js +97 -0
  66. package/dist/tools/find-document.js.map +1 -0
  67. package/dist/tools/find-global.d.ts +26 -0
  68. package/dist/tools/find-global.js +122 -0
  69. package/dist/tools/find-global.js.map +1 -0
  70. package/dist/tools/global-versions.d.ts +39 -0
  71. package/dist/tools/global-versions.js +132 -0
  72. package/dist/tools/global-versions.js.map +1 -0
  73. package/dist/tools/patch-global-layout.d.ts +31 -0
  74. package/dist/tools/patch-global-layout.js +127 -0
  75. package/dist/tools/patch-global-layout.js.map +1 -0
  76. package/dist/tools/patch-layout.d.ts +5 -8
  77. package/dist/tools/patch-layout.js +18 -100
  78. package/dist/tools/patch-layout.js.map +1 -1
  79. package/dist/tools/publish-draft.d.ts +5 -4
  80. package/dist/tools/publish-draft.js +39 -2
  81. package/dist/tools/publish-draft.js.map +1 -1
  82. package/dist/tools/publish-global-draft.d.ts +20 -0
  83. package/dist/tools/publish-global-draft.js +79 -0
  84. package/dist/tools/publish-global-draft.js.map +1 -0
  85. package/dist/tools/resolve-reference.d.ts +5 -4
  86. package/dist/tools/resolve-reference.js +4 -0
  87. package/dist/tools/resolve-reference.js.map +1 -1
  88. package/dist/tools/safe-delete.d.ts +5 -5
  89. package/dist/tools/safe-delete.js +20 -15
  90. package/dist/tools/safe-delete.js.map +1 -1
  91. package/dist/tools/schedule-publish.d.ts +5 -5
  92. package/dist/tools/schedule-publish.js +23 -19
  93. package/dist/tools/schedule-publish.js.map +1 -1
  94. package/dist/tools/search-content.d.ts +5 -9
  95. package/dist/tools/search-content.js +16 -12
  96. package/dist/tools/search-content.js.map +1 -1
  97. package/dist/tools/update-document.d.ts +5 -5
  98. package/dist/tools/update-document.js +10 -5
  99. package/dist/tools/update-document.js.map +1 -1
  100. package/dist/tools/update-global.d.ts +27 -0
  101. package/dist/tools/update-global.js +72 -0
  102. package/dist/tools/update-global.js.map +1 -0
  103. package/dist/tools/upload-media.d.ts +5 -4
  104. package/dist/tools/upload-media.js +6 -1
  105. package/dist/tools/upload-media.js.map +1 -1
  106. package/dist/tools/versions.d.ts +10 -9
  107. package/dist/tools/versions.js +15 -7
  108. package/dist/tools/versions.js.map +1 -1
  109. package/dist/types.d.ts +56 -3
  110. package/dist/types.js +13 -6
  111. package/dist/types.js.map +1 -1
  112. package/package.json +39 -18
  113. package/dist/__tests__/introspection.test.js +0 -459
  114. package/dist/__tests__/introspection.test.js.map +0 -1
  115. package/dist/__tests__/url-validator.test.js +0 -326
  116. package/dist/__tests__/url-validator.test.js.map +0 -1
@@ -0,0 +1,308 @@
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. ' + 'Switching away from Custom CLEARS every override on save (collectionScopes, globalScopes, ' + 'toolAllow, toolDeny); switching back to Custom starts from a fresh deny-all baseline, so ' + 'reconfigure the matrices and tool lists before saving.'
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 or global scopes permit. Under the Custom preset, an empty list is ' + 'treated as deny-all ONLY when no collection or global scopes are set (the fresh- ' + 'Custom-key sentinel); when collection or global scopes are populated, an empty list ' + 'collapses to "no tool restriction" so the resource scopes alone determine what is ' + 'callable — to deny every tool while keeping resource scopes, enumerate them in ' + 'toolDeny instead. 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, originalDoc })=>{
158
+ // The override fields (collectionScopes, globalScopes, toolAllow,
159
+ // toolDeny) are conditionally rendered only under the Custom preset
160
+ // (`condition: isCustomPreset`). Under any other preset they are
161
+ // hidden in the admin UI, which means two things:
162
+ //
163
+ // 1. The admin form omits hidden fields from its payload on save,
164
+ // so `data` only carries the visible fields — we can't
165
+ // "collapse the empty array we see in `data`" because we never
166
+ // see it at all. The stale value lives on `originalDoc`.
167
+ // 2. A Custom→Admin switch silently keeps the prior
168
+ // `toolAllow:[...]` / `collectionScopes:[...]`, and
169
+ // `composeScopes` then emits a scope gate that rejects calls
170
+ // the user clearly intended to allow.
171
+ //
172
+ // Fix: when the preset is non-Custom, explicitly write `null` into
173
+ // `data` for every override axis (regardless of what `data` carries
174
+ // or what originalDoc holds). Payload persists nulls, so the stale
175
+ // values are erased on every save. The Custom-preset branch below
176
+ // keeps the explicit-empty-means-deny semantic intact.
177
+ if (!data) return data;
178
+ const d = data;
179
+ const orig = originalDoc ?? {};
180
+ const preset = d.preset ?? orig.preset;
181
+ // `readField` falls through to originalDoc when `data` omits the
182
+ // key entirely (admin form skipping hidden fields), but honours
183
+ // an explicit null/empty in `data` over originalDoc.
184
+ const readField = (key)=>key in d ? d[key] : orig[key];
185
+ const isNonEmptyArray = (v)=>Array.isArray(v) && v.length > 0;
186
+ const isEmptyArray = (v)=>Array.isArray(v) && v.length === 0;
187
+ const OVERRIDE_AXES = [
188
+ 'collectionScopes',
189
+ 'globalScopes',
190
+ 'toolAllow',
191
+ 'toolDeny'
192
+ ];
193
+ if (preset !== 'custom') {
194
+ for (const axis of OVERRIDE_AXES)d[axis] = null;
195
+ return data;
196
+ }
197
+ // Custom preset: the Tools collapsible is labelled as an *override*
198
+ // layered on top of collection / global scopes, and its description
199
+ // says "Leave empty to allow any tool the collection scopes permit."
200
+ // Payload's hasMany-select default of `[]` would otherwise turn the
201
+ // Tools section into a mandatory whitelist — a user who configures
202
+ // collection scopes and never opens the collapsible would silently
203
+ // store `toolAllow:[]`, which `composeScopes` honours as deny-all on
204
+ // the tools axis and rejects every call.
205
+ //
206
+ // Resolve the mismatch by coercing an empty `toolAllow` to null
207
+ // whenever the key carries any concrete resource scope (collection
208
+ // or global entries). The fresh-Custom-key sentinel in
209
+ // `composeScopes` still covers the "no scopes at all" case
210
+ // (everything null → deny-all), so users who genuinely want
211
+ // deny-all do not regress.
212
+ const hasResourceScope = isNonEmptyArray(readField('collectionScopes')) || isNonEmptyArray(readField('globalScopes'));
213
+ if (hasResourceScope && isEmptyArray(readField('toolAllow'))) {
214
+ d.toolAllow = null;
215
+ }
216
+ return data;
217
+ }
218
+ ]
219
+ },
220
+ labels: {
221
+ plural: 'API Keys',
222
+ singular: 'API Key'
223
+ },
224
+ fields: [
225
+ // Main column.
226
+ {
227
+ name: 'name',
228
+ type: 'text',
229
+ required: true,
230
+ admin: {
231
+ description: 'Human label for this key (e.g. "Editorial team — Claude Desktop").'
232
+ }
233
+ },
234
+ {
235
+ name: 'description',
236
+ type: 'textarea',
237
+ admin: {
238
+ description: 'Optional notes about the purpose of this key.'
239
+ }
240
+ },
241
+ presetField,
242
+ collectionScopesField,
243
+ globalScopesField,
244
+ toolsCollapsible,
245
+ // Sidebar — identity + lifecycle.
246
+ {
247
+ name: 'user',
248
+ type: 'relationship',
249
+ relationTo: options.userCollection,
250
+ required: true,
251
+ admin: {
252
+ position: 'sidebar',
253
+ description: 'The user this key authenticates as. Tool calls use this user for access checks on target collections.'
254
+ }
255
+ },
256
+ {
257
+ name: 'keyPrefix',
258
+ type: 'text',
259
+ index: true,
260
+ admin: {
261
+ position: 'sidebar',
262
+ readOnly: true,
263
+ description: 'First 8 characters of the API key — used in audit logs to identify the key without exposing the full secret.'
264
+ },
265
+ hooks: {
266
+ beforeChange: [
267
+ ({ data, originalDoc, value })=>{
268
+ if (typeof value === 'string' && value.length > 0) return value;
269
+ const incomingKey = data?.apiKey;
270
+ if (typeof incomingKey === 'string' && incomingKey.length >= 8) {
271
+ return incomingKey.slice(0, 8);
272
+ }
273
+ const existing = originalDoc?.keyPrefix;
274
+ return typeof existing === 'string' ? existing : undefined;
275
+ }
276
+ ]
277
+ }
278
+ },
279
+ {
280
+ name: 'expiresAt',
281
+ type: 'date',
282
+ admin: {
283
+ position: 'sidebar',
284
+ description: 'Optional expiry. Requests authenticated with an expired key are rejected.'
285
+ }
286
+ },
287
+ {
288
+ name: 'revokedAt',
289
+ type: 'date',
290
+ admin: {
291
+ position: 'sidebar',
292
+ description: 'Set to revoke a key. Revoked keys are rejected at auth time.'
293
+ }
294
+ },
295
+ {
296
+ name: 'lastUsedAt',
297
+ type: 'date',
298
+ admin: {
299
+ position: 'sidebar',
300
+ readOnly: true,
301
+ description: 'Updated on each successful authentication. Fire-and-forget; not on the request hot path.'
302
+ }
303
+ }
304
+ ]
305
+ };
306
+ }
307
+
308
+ //# sourceMappingURL=api-keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/api-keys.ts"],"sourcesContent":["import type { CollectionBeforeValidateHook, 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 'Switching away from Custom CLEARS every override on save (collectionScopes, globalScopes, ' +\r\n 'toolAllow, toolDeny); switching back to Custom starts from a fresh deny-all baseline, so ' +\r\n 'reconfigure the matrices and tool lists before saving.',\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 ' +\r\n 'the collection or global scopes permit. Under the Custom preset, an empty list is ' +\r\n 'treated as deny-all ONLY when no collection or global scopes are set (the fresh- ' +\r\n 'Custom-key sentinel); when collection or global scopes are populated, an empty list ' +\r\n 'collapses to \"no tool restriction\" so the resource scopes alone determine what is ' +\r\n 'callable — to deny every tool while keeping resource scopes, enumerate them in ' +\r\n 'toolDeny instead. Preset-mode keys created via the REST API with an empty list are ' +\r\n '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, originalDoc }) => {\r\n // The override fields (collectionScopes, globalScopes, toolAllow,\r\n // toolDeny) are conditionally rendered only under the Custom preset\r\n // (`condition: isCustomPreset`). Under any other preset they are\r\n // hidden in the admin UI, which means two things:\r\n //\r\n // 1. The admin form omits hidden fields from its payload on save,\r\n // so `data` only carries the visible fields — we can't\r\n // \"collapse the empty array we see in `data`\" because we never\r\n // see it at all. The stale value lives on `originalDoc`.\r\n // 2. A Custom→Admin switch silently keeps the prior\r\n // `toolAllow:[...]` / `collectionScopes:[...]`, and\r\n // `composeScopes` then emits a scope gate that rejects calls\r\n // the user clearly intended to allow.\r\n //\r\n // Fix: when the preset is non-Custom, explicitly write `null` into\r\n // `data` for every override axis (regardless of what `data` carries\r\n // or what originalDoc holds). Payload persists nulls, so the stale\r\n // values are erased on every save. The Custom-preset branch below\r\n // keeps the explicit-empty-means-deny semantic intact.\r\n if (!data) return data\r\n const d = data as Record<string, unknown>\r\n const orig = (originalDoc ?? {}) as Record<string, unknown>\r\n const preset = d.preset ?? orig.preset\r\n\r\n // `readField` falls through to originalDoc when `data` omits the\r\n // key entirely (admin form skipping hidden fields), but honours\r\n // an explicit null/empty in `data` over originalDoc.\r\n const readField = (key: string): unknown =>\r\n key in d ? d[key] : orig[key]\r\n const isNonEmptyArray = (v: unknown): boolean =>\r\n Array.isArray(v) && v.length > 0\r\n const isEmptyArray = (v: unknown): boolean =>\r\n Array.isArray(v) && v.length === 0\r\n\r\n const OVERRIDE_AXES = [\r\n 'collectionScopes',\r\n 'globalScopes',\r\n 'toolAllow',\r\n 'toolDeny',\r\n ] as const\r\n\r\n if (preset !== 'custom') {\r\n for (const axis of OVERRIDE_AXES) d[axis] = null\r\n return data\r\n }\r\n\r\n // Custom preset: the Tools collapsible is labelled as an *override*\r\n // layered on top of collection / global scopes, and its description\r\n // says \"Leave empty to allow any tool the collection scopes permit.\"\r\n // Payload's hasMany-select default of `[]` would otherwise turn the\r\n // Tools section into a mandatory whitelist — a user who configures\r\n // collection scopes and never opens the collapsible would silently\r\n // store `toolAllow:[]`, which `composeScopes` honours as deny-all on\r\n // the tools axis and rejects every call.\r\n //\r\n // Resolve the mismatch by coercing an empty `toolAllow` to null\r\n // whenever the key carries any concrete resource scope (collection\r\n // or global entries). The fresh-Custom-key sentinel in\r\n // `composeScopes` still covers the \"no scopes at all\" case\r\n // (everything null → deny-all), so users who genuinely want\r\n // deny-all do not regress.\r\n const hasResourceScope =\r\n isNonEmptyArray(readField('collectionScopes')) ||\r\n isNonEmptyArray(readField('globalScopes'))\r\n if (hasResourceScope && isEmptyArray(readField('toolAllow'))) {\r\n d.toolAllow = null\r\n }\r\n return data\r\n }) as CollectionBeforeValidateHook,\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","originalDoc","d","orig","readField","key","isNonEmptyArray","v","length","isEmptyArray","OVERRIDE_AXES","axis","hasResourceScope","toolAllow","labels","plural","singular","relationTo","position","index","readOnly","beforeChange","incomingKey","apiKey","slice","existing","keyPrefix","undefined"],"mappings":"AAEA,OAAO,MAAMA,wBAAwB,uBAAsB;AAiC3D,MAAMC,iBAAiB;IACrB;QAAEC,OAAO;QAAaC,OAAO;IAAY;IACzC;QAAED,OAAO;QAAmCC,OAAO;IAAS;IAC5D;QAAED,OAAO;QAAuBC,OAAO;IAAQ;IAC/C;QAAED,OAAO;QAAgCC,OAAO;IAAS;CAC1D;AAED,MAAMC,iBAAiB,CAACC,OACtB,CAAC,CAACA,QAAQ,OAAOA,SAAS,YAAY,AAACA,KAA8BC,MAAM,KAAK;AAElF;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,wBACdC,OAAuC;IAEvC,IAAI,CAACA,WAAW,CAACA,QAAQC,cAAc,EAAE;QACvC,MAAM,IAAIC,MACR;IAEJ;IACA,IAAI,CAACC,MAAMC,OAAO,CAACJ,QAAQK,oBAAoB,GAAG;QAChD,MAAM,IAAIH,MACR;IAEJ;IACA,IAAI,CAACC,MAAMC,OAAO,CAACJ,QAAQM,cAAc,GAAG;QAC1C,MAAM,IAAIJ,MACR;IAEJ;IAEA,MAAMK,OAAOP,QAAQO,IAAI,IAAIf;IAC7B,MAAMgB,cAAcR,QAAQM,cAAc,CAACG,GAAG,CAAC,CAACC,IAAO,CAAA;YAAEhB,OAAOgB;YAAGf,OAAOe;QAAE,CAAA;IAC5E,MAAMC,mBAAmBR,MAAMC,OAAO,CAACJ,QAAQW,gBAAgB,IAC3DX,QAAQW,gBAAgB,GACxB,EAAE;IAEN,MAAMC,cAAqB;QACzBC,MAAM;QACNC,MAAM;QACNC,UAAU;QACVC,cAAc;QACdhB,SAASP;QACTwB,OAAO;YACLC,aACE,2FACA,+FACA,8FACA;QACJ;IACF;IAEA,wFAAwF;IACxF,sEAAsE;IACtE,mEAAmE;IACnE,6DAA6D;IAC7D,EAAE;IACF,uEAAuE;IACvE,uEAAuE;IACvE,kCAAkC;IAClC,MAAMC,wBAA+B;QACnCN,MAAM;QACNC,MAAM;QACNG,OAAO;YACLG,WAAWxB;YACXyB,YAAY;gBACVC,OAAO;oBACLC,MAAM;oBACNC,YAAY;oBACZC,aAAa;wBACXpB,sBAAsBL,QAAQK,oBAAoB;oBACpD;gBACF;YACF;QACF;IACF;IAEA,wEAAwE;IACxE,sEAAsE;IACtE,0CAA0C;IAC1C,0DAA0D;IAC1D,uEAAuE;IACvE,wEAAwE;IACxE,2EAA2E;IAC3E,MAAMqB,oBAA2B;QAC/Bb,MAAM;QACNC,MAAM;QACNG,OAAO;YACLG,WAAWxB;YACXyB,YAAY;gBACVC,OAAO;oBACLC,MAAM;oBACNC,YAAY;oBACZC,aAAa;wBACXd;oBACF;gBACF;YACF;QACF;IACF;IAEA,MAAMgB,mBAA0B;QAC9Bb,MAAM;QACNpB,OAAO;QACPuB,OAAO;YACLG,WAAWxB;YACXsB,aACE;YACFU,eAAe;QACjB;QACAC,QAAQ;YACN;gBACEhB,MAAM;gBACNC,MAAM;gBACNgB,SAAS;gBACT9B,SAASQ;gBACTS,OAAO;oBACLC,aACE,wFACA,uFACA,sFACA,yFACA,uFACA,oFACA,wFACA;gBACJ;YACF;YACA;gBACEL,MAAM;gBACNC,MAAM;gBACNgB,SAAS;gBACT9B,SAASQ;gBACTS,OAAO;oBACLC,aAAa;gBACf;YACF;SACD;IACH;IAEA,OAAO;QACLX;QACAU,OAAO;YACLc,OAAO;YACPC,YAAY;YACZd,aACE;YACFe,gBAAgB;gBAAC;gBAAQ;gBAAQ;gBAAa;gBAAU;gBAAc;gBAAa;aAAY;QACjG;QACAC,MAAM;YACJC,sBAAsB;YACtBC,WAAW;QACb;QACAC,OAAO;YACLC,gBAAgB;gBACb,CAAC,EAAEzC,IAAI,EAAE0C,WAAW,EAAE;oBACrB,kEAAkE;oBAClE,oEAAoE;oBACpE,iEAAiE;oBACjE,kDAAkD;oBAClD,EAAE;oBACF,oEAAoE;oBACpE,4DAA4D;oBAC5D,oEAAoE;oBACpE,8DAA8D;oBAC9D,sDAAsD;oBACtD,yDAAyD;oBACzD,kEAAkE;oBAClE,2CAA2C;oBAC3C,EAAE;oBACF,mEAAmE;oBACnE,oEAAoE;oBACpE,mEAAmE;oBACnE,kEAAkE;oBAClE,uDAAuD;oBACvD,IAAI,CAAC1C,MAAM,OAAOA;oBAClB,MAAM2C,IAAI3C;oBACV,MAAM4C,OAAQF,eAAe,CAAC;oBAC9B,MAAMzC,SAAS0C,EAAE1C,MAAM,IAAI2C,KAAK3C,MAAM;oBAEtC,iEAAiE;oBACjE,gEAAgE;oBAChE,qDAAqD;oBACrD,MAAM4C,YAAY,CAACC,MACjBA,OAAOH,IAAIA,CAAC,CAACG,IAAI,GAAGF,IAAI,CAACE,IAAI;oBAC/B,MAAMC,kBAAkB,CAACC,IACvB1C,MAAMC,OAAO,CAACyC,MAAMA,EAAEC,MAAM,GAAG;oBACjC,MAAMC,eAAe,CAACF,IACpB1C,MAAMC,OAAO,CAACyC,MAAMA,EAAEC,MAAM,KAAK;oBAEnC,MAAME,gBAAgB;wBACpB;wBACA;wBACA;wBACA;qBACD;oBAED,IAAIlD,WAAW,UAAU;wBACvB,KAAK,MAAMmD,QAAQD,cAAeR,CAAC,CAACS,KAAK,GAAG;wBAC5C,OAAOpD;oBACT;oBAEA,oEAAoE;oBACpE,oEAAoE;oBACpE,qEAAqE;oBACrE,oEAAoE;oBACpE,mEAAmE;oBACnE,mEAAmE;oBACnE,qEAAqE;oBACrE,yCAAyC;oBACzC,EAAE;oBACF,gEAAgE;oBAChE,mEAAmE;oBACnE,uDAAuD;oBACvD,2DAA2D;oBAC3D,4DAA4D;oBAC5D,2BAA2B;oBAC3B,MAAMqD,mBACJN,gBAAgBF,UAAU,wBAC1BE,gBAAgBF,UAAU;oBAC5B,IAAIQ,oBAAoBH,aAAaL,UAAU,eAAe;wBAC5DF,EAAEW,SAAS,GAAG;oBAChB;oBACA,OAAOtD;gBACT;aACD;QACH;QACAuD,QAAQ;YACNC,QAAQ;YACRC,UAAU;QACZ;QACAzB,QAAQ;YACN,eAAe;YACf;gBACEhB,MAAM;gBACNC,MAAM;gBACNC,UAAU;gBACVE,OAAO;oBAAEC,aAAa;gBAAqE;YAC7F;YACA;gBACEL,MAAM;gBACNC,MAAM;gBACNG,OAAO;oBAAEC,aAAa;gBAAgD;YACxE;YACAN;YACAO;YACAO;YACAC;YAEA,kCAAkC;YAClC;gBACEd,MAAM;gBACNC,MAAM;gBACNyC,YAAYvD,QAAQC,cAAc;gBAClCc,UAAU;gBACVE,OAAO;oBACLuC,UAAU;oBACVtC,aACE;gBACJ;YACF;YACA;gBACEL,MAAM;gBACNC,MAAM;gBACN2C,OAAO;gBACPxC,OAAO;oBACLuC,UAAU;oBACVE,UAAU;oBACVxC,aACE;gBACJ;gBACAmB,OAAO;oBACLsB,cAAc;wBACZ,CAAC,EAAE9D,IAAI,EAAE0C,WAAW,EAAE5C,KAAK,EAAE;4BAC3B,IAAI,OAAOA,UAAU,YAAYA,MAAMmD,MAAM,GAAG,GAAG,OAAOnD;4BAC1D,MAAMiE,cAAe/D,MAA2CgE;4BAChE,IAAI,OAAOD,gBAAgB,YAAYA,YAAYd,MAAM,IAAI,GAAG;gCAC9D,OAAOc,YAAYE,KAAK,CAAC,GAAG;4BAC9B;4BACA,MAAMC,WAAYxB,aAAqDyB;4BACvE,OAAO,OAAOD,aAAa,WAAWA,WAAWE;wBACnD;qBACD;gBACH;YACF;YACA;gBACEpD,MAAM;gBACNC,MAAM;gBACNG,OAAO;oBACLuC,UAAU;oBACVtC,aAAa;gBACf;YACF;YACA;gBACEL,MAAM;gBACNC,MAAM;gBACNG,OAAO;oBACLuC,UAAU;oBACVtC,aAAa;gBACf;YACF;YACA;gBACEL,MAAM;gBACNC,MAAM;gBACNG,OAAO;oBACLuC,UAAU;oBACVE,UAAU;oBACVxC,aACE;gBACJ;YACF;SACD;IACH;AACF"}
@@ -0,0 +1,96 @@
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
+ /** @internal test-only: reset the one-time legacy warns. */
40
+ export declare function _resetLegacyWarnsForTests(): void;
41
+ /**
42
+ * Builds the runtime `KeyScopes` shape consumed by `registry.assertScopeAllows`
43
+ * from the typed scope fields on the api-key row.
44
+ *
45
+ * Returns null when no typed fields are populated AND no preset is set
46
+ * (= full access — back-compat for pre-0.5 rows that pre-date scoped authz).
47
+ *
48
+ * Two complementary fail-closed rules:
49
+ *
50
+ * 1. **`'custom'` deny-all sentinel.** `'custom'` is a UI sentinel meaning
51
+ * "use my override fields"; it never becomes `KeyScopes.preset` itself.
52
+ * Payload persists unset JSON / select fields as `null`, so a fresh
53
+ * Custom key with no overrides arrives as `{preset:'custom',
54
+ * collectionScopes:null, globalScopes:null, toolAllow:null,
55
+ * toolDeny:null}`. That row must deny everything (not fall through to
56
+ * full access). The sentinel emits `{collections:{}, globals:{},
57
+ * tools:{allow:[]}}`.
58
+ *
59
+ * 2. **Per-axis explicit-empty, Custom-only.** Under the Custom preset, an
60
+ * empty array on any axis (even `[]`) is honoured as written — an empty
61
+ * `toolAllow:[]` means "deny all tools on this axis", not "no opinion".
62
+ * Under non-Custom presets, empty arrays are IGNORED because Payload's
63
+ * hasMany / unpopulated-JSON reads return `[]` for fields the user
64
+ * never touched (the override matrices are hidden in the admin UI under
65
+ * non-Custom presets via `condition: isCustomPreset`). The on-write
66
+ * counterpart of this rule lives in `createApiKeysCollection`'s
67
+ * `beforeValidate` hook, which proactively nulls the override axes on
68
+ * save when the preset is non-Custom — both layers must stay in sync.
69
+ * Non-empty arrays still apply as layered narrowing under any preset.
70
+ * `toolDeny` is a deny-list, so an empty array carries no entries — it
71
+ * is dropped rather than emitted. NOTE: legacy non-Custom rows persisted
72
+ * BEFORE v0.7.1 with populated stale override arrays continue to narrow
73
+ * on read until each row is manually re-saved; the on-write fix only
74
+ * applies to fresh writes.
75
+ */
76
+ export declare function composeScopes(row: ApiKeyRow, logger?: {
77
+ warn?: (...args: unknown[]) => void;
78
+ }): KeyScopes | null;
79
+ /**
80
+ * Builds the Payload `auth.strategies` entry that authenticates MCP requests.
81
+ *
82
+ * Authenticates `Authorization: Bearer <plaintext>` by computing the
83
+ * upstream-compatible `apiKeyIndex` HMAC and looking up the row. On match,
84
+ * it fires a non-blocking `lastUsedAt` write and hydrates `req.user` with
85
+ * the linked user record + key context for downstream scope checks.
86
+ */
87
+ export declare function createBearerStrategy(options: CreateBearerStrategyOptions): AuthStrategy;
88
+ /**
89
+ * Reads the per-request API-key context populated by the bearer strategy.
90
+ * Returns null for non-MCP requests (e.g. cookie-authenticated admin users).
91
+ */
92
+ export declare function getApiKeyContext(req: PayloadRequest): {
93
+ keyId: string | number;
94
+ keyPrefix: string | null;
95
+ scopes: KeyScopes | null;
96
+ } | null;