@abinnovision/payloadcms-mcpx 2.0.0-beta.9 → 2.1.0-beta.1

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 (77) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +56 -36
  3. package/dist/admin/capability-matrix.d.mts +5 -3
  4. package/dist/admin/capability-matrix.mjs +374 -272
  5. package/dist/admin/capability-toggles.mjs +79 -42
  6. package/dist/admin/confirmations-panel.mjs +232 -0
  7. package/dist/admin/confirmations.d.mts +15 -0
  8. package/dist/admin/confirmations.mjs +71 -0
  9. package/dist/admin/endpoint-url.mjs +7 -0
  10. package/dist/admin/index.d.mts +2 -1
  11. package/dist/admin/index.mjs +2 -1
  12. package/dist/admin/setup-guide.d.mts +3 -3
  13. package/dist/admin/setup-guide.mjs +51 -33
  14. package/dist/api-keys/capability-matrix.d.mts +7 -9
  15. package/dist/api-keys/capability-matrix.mjs +15 -37
  16. package/dist/api-keys/collection.mjs +16 -18
  17. package/dist/api-keys/confirmation-view.mjs +7 -0
  18. package/dist/api-keys/fields.mjs +50 -57
  19. package/dist/auth/resolve.mjs +69 -40
  20. package/dist/capabilities.d.mts +2 -0
  21. package/dist/capabilities.mjs +64 -16
  22. package/dist/define-tool.d.mts +4 -4
  23. package/dist/define-tool.mjs +8 -1
  24. package/dist/endpoint/confirmations.mjs +124 -0
  25. package/dist/endpoint/handler.mjs +25 -27
  26. package/dist/endpoint/http.mjs +27 -0
  27. package/dist/endpoint/server.mjs +48 -29
  28. package/dist/endpoint/upload.mjs +104 -0
  29. package/dist/guards.mjs +8 -1
  30. package/dist/index.d.mts +2 -2
  31. package/dist/index.mjs +1 -1
  32. package/dist/leases/confirmation.mjs +74 -0
  33. package/dist/leases/grant.mjs +29 -0
  34. package/dist/leases/lease.mjs +115 -0
  35. package/dist/options.mjs +68 -61
  36. package/dist/plugin.d.mts +3 -2
  37. package/dist/plugin.mjs +19 -4
  38. package/dist/request.mjs +5 -1
  39. package/dist/result.mjs +11 -1
  40. package/dist/schema/admin-hidden.mjs +4 -4
  41. package/dist/schema/describe.mjs +12 -18
  42. package/dist/schema/index.mjs +5 -6
  43. package/dist/schema/lexical-pointer.mjs +8 -8
  44. package/dist/schema/lexical.mjs +15 -13
  45. package/dist/schema/outline.mjs +4 -5
  46. package/dist/schema/path.mjs +4 -1
  47. package/dist/schema/pointer.mjs +11 -17
  48. package/dist/schema/shape.mjs +38 -50
  49. package/dist/schema/walk.mjs +57 -18
  50. package/dist/tools/builtin.mjs +7 -1
  51. package/dist/tools/create-document.mjs +38 -26
  52. package/dist/tools/delete-document.mjs +154 -0
  53. package/dist/tools/describe-schema.mjs +7 -5
  54. package/dist/tools/document.mjs +131 -45
  55. package/dist/tools/duplicate-document.mjs +50 -0
  56. package/dist/tools/find-documents.mjs +15 -20
  57. package/dist/tools/find-versions.mjs +11 -15
  58. package/dist/tools/get-document.mjs +31 -15
  59. package/dist/tools/links.mjs +53 -0
  60. package/dist/tools/list-capabilities.mjs +17 -7
  61. package/dist/tools/patch-document.mjs +57 -53
  62. package/dist/tools/publish-document.mjs +17 -29
  63. package/dist/tools/query-paths.mjs +1 -1
  64. package/dist/tools/run-confirmed.mjs +114 -0
  65. package/dist/tools/shared.mjs +53 -46
  66. package/dist/tools/validate-document.mjs +6 -8
  67. package/dist/tools/versions.mjs +11 -6
  68. package/dist/types.d.mts +158 -43
  69. package/dist/upload/file.mjs +178 -0
  70. package/dist/version.mjs +4 -2
  71. package/dist/write/draft-guard.mjs +35 -28
  72. package/dist/write/patch.mjs +4 -4
  73. package/dist/write/publish-intent.mjs +23 -11
  74. package/dist/write/row-ids.mjs +3 -1
  75. package/dist/write/write-data.mjs +18 -31
  76. package/package.json +14 -14
  77. package/dist/builtin-tool-names.mjs +0 -16
@@ -1,47 +1,25 @@
1
- import { CAPABILITIES_FIELD, canCreate, canPublish, canWrite, isLiveWrite } from "../capabilities.mjs";
1
+ import { CAPABILITIES_FIELD, STORED_OPERATIONS } from "../capabilities.mjs";
2
2
  //#region src/api-keys/capability-matrix.ts
3
3
  /**
4
4
  * The group's `admin.description`, rendered by the matrix in Payload's
5
- * description slot. It explains both markings in the table: an unticked box
6
- * and a dash.
7
- */ const CAPABILITIES_DESCRIPTION = "What this key may do. An unticked box is a refusal, and a dash means the plugin config does not expose that operation at all.";
8
- /**
9
- * The operations a collection or global can expose. The generated checkboxes
10
- * and the matrix column headers share this wording.
11
- */ const CAPABILITY_OPERATIONS = [
12
- {
13
- id: "read",
14
- label: "Read",
15
- description: "Describe, find and read."
16
- },
17
- {
18
- id: "write",
19
- label: "Write",
20
- description: "Create, patch and validate drafts."
21
- },
22
- {
23
- id: "publish",
24
- label: "Publish",
25
- description: "Promote the current draft to what the public sees."
26
- }
27
- ];
28
- const UPLOAD_HINT = "Files are uploaded in the admin panel.";
29
- const LIVE_HINT = "Writes go live immediately.";
30
- const hintFor = (entity) => {
31
- const hints = [canWrite(entity) && !canCreate(entity) ? UPLOAD_HINT : void 0, isLiveWrite(entity) ? LIVE_HINT : void 0].filter((hint) => hint !== void 0);
32
- return hints.length > 0 ? { hint: hints.join(" ") } : {};
33
- };
5
+ * description slot. It explains how to read the controls.
6
+ */ const CAPABILITIES_DESCRIPTION = "What this key may do. Each level includes the ones before it, and a missing segment means the plugin config does not expose it. A shield means this key's user approves each call.";
34
7
  const toRow = (entity) => ({
35
8
  fieldName: entity.fieldName,
36
- label: entity.slug,
9
+ slug: entity.slug,
37
10
  read: entity.read,
38
- write: canWrite(entity),
39
- publish: canPublish(entity),
40
- ...hintFor(entity)
11
+ write: entity.write,
12
+ publish: entity.publish,
13
+ delete: entity.delete,
14
+ deleteUnattended: entity.deleteUnattended,
15
+ ...entity.liveWrite ? { live: true } : {}
41
16
  });
42
17
  /**
18
+ * The operations a row exposes, in stored order.
19
+ */ const rowOperations = (row) => STORED_OPERATIONS.filter((operation) => row[operation.id]);
20
+ /**
43
21
  * The single description of what the config exposes. `createCapabilityFields`
44
- * and the admin component both draw from it, so a cell cannot appear without a
22
+ * and the admin component both draw from it, so a control cannot appear without a
45
23
  * field behind it.
46
24
  */ const createCapabilityMatrix = (options) => ({
47
25
  collections: options.collections.map(toRow),
@@ -55,6 +33,6 @@ const cellPath = (basePath, namespace, fieldName, operation) => `${basePath}.${n
55
33
  const toolPath = (basePath, name) => `${basePath}.tools.${name}`;
56
34
  /**
57
35
  * Every path the matrix can address, in a stable order.
58
- */ const capabilityPaths = (matrix, basePath = CAPABILITIES_FIELD) => [...["collections", "globals"].flatMap((namespace) => matrix[namespace].flatMap((row) => CAPABILITY_OPERATIONS.filter((operation) => row[operation.id]).map((operation) => cellPath(basePath, namespace, row.fieldName, operation.id)))), ...matrix.tools.map((tool) => toolPath(basePath, tool.name))];
36
+ */ const capabilityPaths = (matrix, basePath = CAPABILITIES_FIELD) => [...["collections", "globals"].flatMap((namespace) => matrix[namespace].flatMap((row) => rowOperations(row).map((operation) => cellPath(basePath, namespace, row.fieldName, operation.id)))), ...matrix.tools.map((tool) => toolPath(basePath, tool.name))];
59
37
  //#endregion
60
- export { CAPABILITIES_DESCRIPTION, CAPABILITY_OPERATIONS, capabilityPaths, cellPath, createCapabilityMatrix, toolPath };
38
+ export { CAPABILITIES_DESCRIPTION, capabilityPaths, cellPath, createCapabilityMatrix, rowOperations, toolPath };
@@ -1,4 +1,4 @@
1
- import { createCapabilityFields, createKeyFields, withSetupGuideTab } from "./fields.mjs";
1
+ import { SERVER_ONLY, createCapabilityFields, createConfirmationFields, createKeyFields, withKeyTabs } from "./fields.mjs";
2
2
  import { generateApiKey, hashApiKey } from "./key.mjs";
3
3
  //#region src/api-keys/collection.ts
4
4
  /**
@@ -28,7 +28,12 @@ import { generateApiKey, hashApiKey } from "./key.mjs";
28
28
  admin: {
29
29
  group: "MCP",
30
30
  useAsTitle: "label",
31
- description: "Keys for MCP clients. Each key acts as its user and may only do what its capabilities allow."
31
+ description: "Keys for MCP clients. Each key acts as its user and may only do what its capabilities allow.",
32
+ ...options.setupGuide && { components: { edit: { beforeDocumentControls: [{
33
+ path: "@abinnovision/payloadcms-mcpx/admin",
34
+ exportName: "McpxSetupGuide",
35
+ clientProps: { endpointPath: options.endpointPath }
36
+ }] } } }
32
37
  },
33
38
  access: {
34
39
  create: isUser,
@@ -37,22 +42,15 @@ import { generateApiKey, hashApiKey } from "./key.mjs";
37
42
  delete: ownKeysOnly
38
43
  },
39
44
  hooks: { beforeChange: [keyBeforeChange] },
40
- fields: withSetupGuideTab([
41
- {
42
- name: "user",
43
- type: "relationship",
44
- relationTo: userCollection,
45
- required: true,
46
- access: {
47
- create: () => false,
48
- update: () => false
49
- },
50
- defaultValue: ({ req }) => isUser({ req }) ? req.user?.id : void 0,
51
- admin: { description: "The user this key acts as." }
52
- },
53
- ...createKeyFields(),
54
- ...createCapabilityFields(options)
55
- ], options)
45
+ fields: [...createConfirmationFields(options), ...withKeyTabs([{
46
+ name: "user",
47
+ type: "relationship",
48
+ relationTo: userCollection,
49
+ required: true,
50
+ access: SERVER_ONLY,
51
+ defaultValue: ({ req }) => isUser({ req }) ? req.user?.id : void 0,
52
+ admin: { description: "The user this key acts as." }
53
+ }, ...createKeyFields()], createCapabilityFields(options))]
56
54
  };
57
55
  };
58
56
  //#endregion
@@ -0,0 +1,7 @@
1
+ //#region src/api-keys/confirmation-view.ts
2
+ /**
3
+ * Path of the confirmations endpoints below the plugin's endpoint path.
4
+ */ const CONFIRMATIONS_PATH = "/confirmations";
5
+ const DECISIONS = ["approved", "rejected"];
6
+ //#endregion
7
+ export { CONFIRMATIONS_PATH, DECISIONS };
@@ -1,5 +1,5 @@
1
1
  import { CAPABILITIES_FIELD } from "../capabilities.mjs";
2
- import { CAPABILITIES_DESCRIPTION, CAPABILITY_OPERATIONS, createCapabilityMatrix } from "./capability-matrix.mjs";
2
+ import { CAPABILITIES_DESCRIPTION, createCapabilityMatrix, rowOperations } from "./capability-matrix.mjs";
3
3
  //#region src/api-keys/fields.ts
4
4
  const encryptKey = ({ req, value }) => typeof value === "string" ? req.payload.encrypt(value) : value;
5
5
  const decryptKey = ({ req, value }) => {
@@ -16,7 +16,24 @@ const checkbox = (name, description) => ({
16
16
  defaultValue: false,
17
17
  admin: { description }
18
18
  });
19
- const SETUP_GUIDE_FIELD = "setupGuide";
19
+ /**
20
+ * Access for fields only the server writes.
21
+ */ const SERVER_ONLY = {
22
+ create: () => false,
23
+ update: () => false
24
+ };
25
+ const adminField = (name, exportName, options) => ({
26
+ name,
27
+ type: "ui",
28
+ admin: {
29
+ disableListColumn: true,
30
+ components: { Field: {
31
+ path: "@abinnovision/payloadcms-mcpx/admin",
32
+ exportName,
33
+ clientProps: { endpointPath: options.endpointPath }
34
+ } }
35
+ }
36
+ });
20
37
  /**
21
38
  * Fields every key carries. Key generation and the HMAC index live in the
22
39
  * collection-level `beforeChange` hook (see `collection.ts`), because sibling
@@ -45,10 +62,7 @@ const SETUP_GUIDE_FIELD = "setupGuide";
45
62
  {
46
63
  name: "lastUsedAt",
47
64
  type: "date",
48
- access: {
49
- create: () => false,
50
- update: () => false
51
- },
65
+ access: SERVER_ONLY,
52
66
  admin: {
53
67
  readOnly: true,
54
68
  date: { pickerAppearance: "dayAndTime" },
@@ -58,10 +72,7 @@ const SETUP_GUIDE_FIELD = "setupGuide";
58
72
  {
59
73
  name: "apiKey",
60
74
  type: "text",
61
- access: {
62
- create: () => false,
63
- update: () => false
64
- },
75
+ access: SERVER_ONLY,
65
76
  admin: {
66
77
  readOnly: true,
67
78
  description: "Generated when the key is created. Send it as `Authorization: Bearer <key>`."
@@ -79,34 +90,27 @@ const SETUP_GUIDE_FIELD = "setupGuide";
79
90
  }
80
91
  ];
81
92
  /**
82
- * Wraps the key fields and the setup guide in unnamed tabs, so wide snippets
93
+ * The pending calls of the key, above everything else on its edit view, where
94
+ * a collection exposes `delete`. The component renders nothing while none is
95
+ * pending and never touches the form, so deciding does not mark the key
96
+ * modified.
97
+ */ const createConfirmationFields = (options) => options.confirmations ? [adminField("confirmations", "McpxConfirmations", options)] : [];
98
+ /**
99
+ * Wraps the key fields and the capabilities in unnamed tabs, so wide tables
83
100
  * get the full form width. Named tabs would nest the data and move
84
101
  * `capabilities` off the document root, which capability resolution reads.
85
- *
86
- * The guide tab only shows on update: on create there is no key to hand out.
87
- */ const withSetupGuideTab = (keyFields, options) => {
88
- if (!options.setupGuide) return keyFields;
102
+ */ const withKeyTabs = (keyFields, capabilityFields) => {
103
+ const tabs = [{
104
+ label: "Key",
105
+ fields: keyFields
106
+ }];
107
+ if (capabilityFields.length > 0) tabs.push({
108
+ label: "Capabilities",
109
+ fields: capabilityFields
110
+ });
89
111
  return [{
90
112
  type: "tabs",
91
- tabs: [{
92
- label: "Key",
93
- fields: keyFields
94
- }, {
95
- label: "Connect a client",
96
- admin: { condition: (_data, _siblingData, { operation }) => operation === "update" },
97
- fields: [{
98
- name: SETUP_GUIDE_FIELD,
99
- type: "ui",
100
- admin: {
101
- disableListColumn: true,
102
- components: { Field: {
103
- path: "@abinnovision/payloadcms-mcpx/admin",
104
- exportName: "McpxSetupGuide",
105
- clientProps: { endpointPath: options.endpointPath }
106
- } }
107
- }
108
- }]
109
- }]
113
+ tabs
110
114
  }];
111
115
  };
112
116
  /**
@@ -123,29 +127,18 @@ const SETUP_GUIDE_FIELD = "setupGuide";
123
127
  const entityGroups = (rows) => rows.map((row) => ({
124
128
  name: row.fieldName,
125
129
  type: "group",
126
- label: row.label,
127
- fields: CAPABILITY_OPERATIONS.filter((operation) => row[operation.id]).map((operation) => checkbox(operation.id, operation.description))
130
+ label: row.slug,
131
+ fields: rowOperations(row).map((operation) => checkbox(operation.id, operation.description))
128
132
  }));
129
- const collectionGroups = entityGroups(matrix.collections);
130
- const globalGroups = entityGroups(matrix.globals);
131
- const toolCheckboxes = matrix.tools.map((tool) => checkbox(tool.name, tool.description));
132
133
  const groups = [
133
- ...collectionGroups.length > 0 ? [{
134
- name: "collections",
135
- type: "group",
136
- fields: collectionGroups
137
- }] : [],
138
- ...globalGroups.length > 0 ? [{
139
- name: "globals",
140
- type: "group",
141
- fields: globalGroups
142
- }] : [],
143
- ...toolCheckboxes.length > 0 ? [{
144
- name: "tools",
145
- type: "group",
146
- fields: toolCheckboxes
147
- }] : []
148
- ];
134
+ ["collections", entityGroups(matrix.collections)],
135
+ ["globals", entityGroups(matrix.globals)],
136
+ ["tools", matrix.tools.map((tool) => checkbox(tool.name, tool.description))]
137
+ ].filter(([, fields]) => fields.length > 0).map(([name, fields]) => ({
138
+ name,
139
+ type: "group",
140
+ fields
141
+ }));
149
142
  if (groups.length === 0) return [];
150
143
  return [{
151
144
  name: CAPABILITIES_FIELD,
@@ -157,7 +150,7 @@ const SETUP_GUIDE_FIELD = "setupGuide";
157
150
  exportName: "McpxCapabilityMatrix",
158
151
  clientProps: {
159
152
  matrix,
160
- withinTab: options.setupGuide
153
+ withinTab: true
161
154
  }
162
155
  } }
163
156
  },
@@ -165,4 +158,4 @@ const SETUP_GUIDE_FIELD = "setupGuide";
165
158
  }];
166
159
  };
167
160
  //#endregion
168
- export { createCapabilityFields, createKeyFields, withSetupGuideTab };
161
+ export { SERVER_ONLY, createCapabilityFields, createConfirmationFields, createKeyFields, withKeyTabs };
@@ -4,16 +4,18 @@ import { hashApiKey } from "../api-keys/key.mjs";
4
4
  const BEARER = /^Bearer\s+(\S+)\s*$/i;
5
5
  const LAST_USED_INTERVAL_MS = 36e5;
6
6
  const toTime = (value) => typeof value === "string" || value instanceof Date ? new Date(value).getTime() : NaN;
7
- const relationId = (value) => {
8
- if (typeof value === "string" || typeof value === "number") return value;
9
- if (typeof value === "object" && value !== null && "id" in value) return value.id;
7
+ const isId = (value) => typeof value === "string" || typeof value === "number";
8
+ /**
9
+ * The id of a relationship value, populated or not.
10
+ */ const relationId = (value) => {
11
+ if (isId(value)) return value;
12
+ return isPlainObject(value) && isId(value["id"]) ? value["id"] : void 0;
10
13
  };
11
14
  const parseBearer = (headers) => {
12
15
  const header = headers.get("authorization");
13
16
  if (!header) return null;
14
17
  return BEARER.exec(header.trim())?.[1] ?? null;
15
18
  };
16
- const isId = (value) => typeof value === "string" || typeof value === "number";
17
19
  /**
18
20
  * Whether a resolved auth, from the default or a custom resolver, may become
19
21
  * `req.user`: a user of the configured user collection with an id, and a key id.
@@ -22,34 +24,14 @@ const isId = (value) => typeof value === "string" || typeof value === "number";
22
24
  return isId(auth["user"]["id"]) && auth["user"]["collection"] === options.userCollection && isId(auth["apiKeyId"]);
23
25
  };
24
26
  /**
25
- * Resolves the bearer key of a request to the user it acts as.
26
- *
27
- * The key is looked up by its HMAC index, the same way Payload resolves its own
28
- * API keys. A missing, unknown, disabled or orphaned key yields `null`; nothing
29
- * here throws, so the handler alone decides how a refusal looks.
30
- */ const resolveApiKeyAuth = async (req, options) => {
31
- const key = parseBearer(req.headers);
32
- if (key === null) return null;
33
- const { payload } = req;
34
- const { docs } = await payload.find({
35
- collection: options.apiKeysSlug,
36
- where: { apiKeyIndex: { equals: hashApiKey(payload.secret, key) } },
37
- limit: 1,
38
- pagination: false,
39
- depth: 0,
40
- overrideAccess: true,
41
- select: {
42
- enabled: true,
43
- user: true,
44
- capabilities: true,
45
- expiresAt: true,
46
- lastUsedAt: true
47
- }
48
- });
49
- const keyDoc = docs[0];
27
+ * The checks a key passes once it is looked up: enabled, not expired, and its
28
+ * user exists, is verified and is not locked out. Yields the user it acts as,
29
+ * or `null`.
30
+ */ const checkApiKey = async (req, options, keyDoc) => {
50
31
  const userId = relationId(keyDoc?.user);
51
32
  if (!keyDoc || keyDoc.enabled !== true || userId === void 0) return null;
52
33
  if (toTime(keyDoc.expiresAt) <= Date.now()) return null;
34
+ const { payload } = req;
53
35
  const userCollection = payload.collections[options.userCollection];
54
36
  const lookup = {
55
37
  collection: options.userCollection,
@@ -68,6 +50,61 @@ const isId = (value) => typeof value === "string" || typeof value === "number";
68
50
  })]);
69
51
  const lockUntil = toTime(lock?.["lockUntil"]);
70
52
  if (!user || user["_verified"] === false || lockUntil > Date.now()) return null;
53
+ return {
54
+ user: {
55
+ ...user,
56
+ collection: options.userCollection,
57
+ _strategy: "mcpx-api-key"
58
+ },
59
+ apiKeyId: keyDoc.id,
60
+ capabilities: keyDoc.capabilities
61
+ };
62
+ };
63
+ const KEY_SELECT = {
64
+ enabled: true,
65
+ user: true,
66
+ capabilities: true,
67
+ expiresAt: true
68
+ };
69
+ /**
70
+ * The user the key `id` acts as, or `null`, under the same checks as the
71
+ * bearer lookup.
72
+ */ const resolveApiKeyById = async (req, options, id) => {
73
+ const keyDoc = await req.payload.findByID({
74
+ collection: options.apiKeysSlug,
75
+ id,
76
+ depth: 0,
77
+ overrideAccess: true,
78
+ disableErrors: true,
79
+ select: KEY_SELECT
80
+ });
81
+ return await checkApiKey(req, options, keyDoc ?? void 0);
82
+ };
83
+ /**
84
+ * Resolves the bearer key of a request to the user it acts as.
85
+ *
86
+ * The key is looked up by its HMAC index, the same way Payload resolves its own
87
+ * API keys. A missing, unknown, disabled or orphaned key yields `null`; nothing
88
+ * here throws, so the handler alone decides how a refusal looks.
89
+ */ const resolveApiKeyAuth = async (req, options) => {
90
+ const key = parseBearer(req.headers);
91
+ if (key === null) return null;
92
+ const { payload } = req;
93
+ const { docs } = await payload.find({
94
+ collection: options.apiKeysSlug,
95
+ where: { apiKeyIndex: { equals: hashApiKey(payload.secret, key) } },
96
+ limit: 1,
97
+ pagination: false,
98
+ depth: 0,
99
+ overrideAccess: true,
100
+ select: {
101
+ ...KEY_SELECT,
102
+ lastUsedAt: true
103
+ }
104
+ });
105
+ const keyDoc = docs[0];
106
+ const auth = await checkApiKey(req, options, keyDoc);
107
+ if (!auth || !keyDoc) return null;
71
108
  const lastUsed = toTime(keyDoc.lastUsedAt);
72
109
  if (Number.isNaN(lastUsed) || Date.now() - lastUsed > LAST_USED_INTERVAL_MS) try {
73
110
  await payload.update({
@@ -81,18 +118,10 @@ const isId = (value) => typeof value === "string" || typeof value === "number";
81
118
  } catch (error) {
82
119
  payload.logger.warn({
83
120
  err: error,
84
- msg: "Could not record the last use of an MCP API key"
121
+ msg: "[payloadcms-mcpx] Could not record the last use of an MCP API key."
85
122
  });
86
123
  }
87
- return {
88
- user: {
89
- ...user,
90
- collection: options.userCollection,
91
- _strategy: "mcpx-api-key"
92
- },
93
- apiKeyId: keyDoc.id,
94
- capabilities: keyDoc.capabilities
95
- };
124
+ return auth;
96
125
  };
97
126
  //#endregion
98
- export { isValidAuthResult, parseBearer, resolveApiKeyAuth };
127
+ export { checkApiKey, isValidAuthResult, parseBearer, relationId, resolveApiKeyAuth, resolveApiKeyById };
@@ -0,0 +1,2 @@
1
+ import "./types.mjs";
2
+ import "./options.mjs";
@@ -4,20 +4,60 @@ import { isPlainObject } from "./guards.mjs";
4
4
  * Group field holding the capability checkboxes on an API key document.
5
5
  */ const CAPABILITIES_FIELD = "capabilities";
6
6
  /**
7
- * Covers draft and live writes; {@link isLiveWrite} separates them.
8
- */ const canWrite = (entity) => entity.write !== false;
7
+ * The operations a collection or global can expose. The generated checkboxes
8
+ * and the matrix segments share this wording. `requires` names the
9
+ * operation a grant depends on, which the server and the matrix both enforce.
10
+ */ const CAPABILITY_OPERATIONS = [
11
+ {
12
+ id: "read",
13
+ label: "Read",
14
+ description: "Describe, find and read."
15
+ },
16
+ {
17
+ id: "write",
18
+ label: "Write",
19
+ description: "Create, patch and validate drafts.",
20
+ requires: "read"
21
+ },
22
+ {
23
+ id: "publish",
24
+ label: "Publish",
25
+ description: "Promote the current draft to what the public sees.",
26
+ requires: "write"
27
+ },
28
+ {
29
+ id: "delete",
30
+ label: "Delete",
31
+ description: "Delete documents. Each one is approved by this key's user in the admin panel, unless the config allows trashing directly and this key is set to.",
32
+ requires: "read"
33
+ }
34
+ ];
9
35
  /**
10
- * The config lets MCP change live content and a draft exists to promote.
11
- */ const canPublish = (entity) => entity.write === "live" && entity.hasDrafts;
36
+ * Stored as a checkbox of its own but drawn as the approval shield of the Delete
37
+ * control.
38
+ */ const DELETE_UNATTENDED = {
39
+ id: "deleteUnattended",
40
+ label: "Trash directly",
41
+ description: "Move documents to trash without asking this key's user.",
42
+ requires: "delete"
43
+ };
44
+ /**
45
+ * Every operation with a stored checkbox: the capability operations plus
46
+ * the unattended delete. Each follows the one it requires, which the resolve
47
+ * loop depends on.
48
+ */ const STORED_OPERATIONS = [...CAPABILITY_OPERATIONS, DELETE_UNATTENDED];
12
49
  /**
13
- * An upload document is a file, and no tool here carries one. Its own fields
14
- * stay patchable; the first version is made in the admin panel.
15
- */ const canCreate = (entity) => canWrite(entity) && !entity.isUpload;
50
+ * The operations in one run of the access levels, each requiring the one
51
+ * before it. Delete is chosen separately.
52
+ */ const ACCESS_CHAIN = CAPABILITY_OPERATIONS.filter((operation) => operation.id !== "delete");
16
53
  /**
17
- * Without drafts there is no draft stage, so every write is live.
18
- */ const isLiveWrite = (entity) => entity.write === "live" && !entity.hasDrafts;
54
+ * What an operation depends on. `resolveCapabilities` discards a grant whose
55
+ * prerequisite is off.
56
+ */ const requiresOf = (operation) => {
57
+ const found = STORED_OPERATIONS.find((candidate) => candidate.id === operation);
58
+ return found && "requires" in found ? found.requires : void 0;
59
+ };
19
60
  const flag = (group, name) => isPlainObject(group) && group[name] === true;
20
- const publishFlag = (entity, group) => canPublish(entity) && flag(group, "write") && flag(group, "publish");
21
61
  /**
22
62
  * Capabilities in force for a key: the plugin config decides what can exist,
23
63
  * the key's checkboxes decide what does. A missing checkbox is `false`, so keys
@@ -28,11 +68,18 @@ const publishFlag = (entity, group) => canPublish(entity) && flag(group, "write"
28
68
  const resolved = {};
29
69
  for (const entity of entities) {
30
70
  const group = isPlainObject(namespaceGroup) ? namespaceGroup[entity.fieldName] : void 0;
31
- resolved[entity.slug] = {
32
- read: entity.read && flag(group, "read"),
33
- write: canWrite(entity) && flag(group, "write"),
34
- publish: publishFlag(entity, group)
71
+ const granted = {
72
+ read: false,
73
+ write: false,
74
+ publish: false,
75
+ delete: false,
76
+ deleteUnattended: false
35
77
  };
78
+ for (const { id } of STORED_OPERATIONS) {
79
+ const needs = requiresOf(id);
80
+ granted[id] = entity[id] && flag(group, id) && (needs === void 0 || granted[needs]);
81
+ }
82
+ resolved[entity.slug] = granted;
36
83
  }
37
84
  return resolved;
38
85
  };
@@ -53,8 +100,9 @@ const publishFlag = (entity, group) => canPublish(entity) && flag(group, "write"
53
100
  return {
54
101
  readable: slugsWith("read"),
55
102
  writable: slugsWith("write"),
56
- publishable: slugsWith("publish")
103
+ publishable: slugsWith("publish"),
104
+ deletable: slugsWith("delete")
57
105
  };
58
106
  };
59
107
  //#endregion
60
- export { CAPABILITIES_FIELD, canCreate, canPublish, canWrite, isLiveWrite, resolveCapabilities, scopeSlugs };
108
+ export { ACCESS_CHAIN, CAPABILITIES_FIELD, CAPABILITY_OPERATIONS, STORED_OPERATIONS, requiresOf, resolveCapabilities, scopeSlugs };
@@ -1,23 +1,23 @@
1
- import { McpxAnyTool, McpxTool, McpxToolScope } from "./types.mjs";
1
+ import { McpxAnyTool, McpxTool, McpxToolConfirm, McpxToolDefinition, McpxToolScope } from "./types.mjs";
2
2
  import { z } from "zod";
3
3
  //#region src/define-tool.d.ts
4
4
  /**
5
5
  * Fixed shape; arguments inferred from it.
6
6
  */
7
- export declare function defineMcpxTool<Shape extends z.ZodRawShape>(tool: McpxTool<Shape> & {
7
+ export declare function defineMcpxTool<Shape extends z.ZodRawShape, Confirm extends McpxToolConfirm | undefined = undefined>(tool: McpxToolDefinition<Shape, z.infer<z.ZodObject<Shape>>, Confirm> & {
8
8
  inputSchema?: Shape;
9
9
  }): McpxTool<Shape>;
10
10
  /**
11
11
  * Per-request shape returned as an object literal; arguments inferred from it.
12
12
  */
13
- export declare function defineMcpxTool<Shape extends z.ZodRawShape>(tool: McpxTool<Shape> & {
13
+ export declare function defineMcpxTool<Shape extends z.ZodRawShape, Confirm extends McpxToolConfirm | undefined = undefined>(tool: McpxToolDefinition<Shape, z.infer<z.ZodObject<Shape>>, Confirm> & {
14
14
  inputSchema: (scope: McpxToolScope) => Shape;
15
15
  }): McpxAnyTool;
16
16
  /**
17
17
  * Per-request shape built from helpers that erase to `z.ZodRawShape`, as the
18
18
  * builtins do. Nothing to infer from, so state the arguments instead.
19
19
  */
20
- export declare function defineMcpxTool<Args>(tool: McpxTool<z.ZodRawShape, Args> & {
20
+ export declare function defineMcpxTool<Args, Confirm extends McpxToolConfirm | undefined = undefined>(tool: McpxToolDefinition<z.ZodRawShape, Args, Confirm> & {
21
21
  inputSchema: (scope: McpxToolScope) => z.ZodRawShape;
22
22
  }): McpxAnyTool;
23
23
  //#endregion
@@ -1,6 +1,13 @@
1
+ import { z } from "zod";
1
2
  //#region src/define-tool.ts
2
3
  function defineMcpxTool(tool) {
3
4
  return tool;
4
5
  }
6
+ /**
7
+ * Strict, so an unknown argument is rejected by name instead of stripped.
8
+ */ const toolInputSchema = (tool, scope) => z.strictObject(typeof tool.inputSchema === "function" ? tool.inputSchema(scope) : tool.inputSchema ?? {});
9
+ /**
10
+ * A tool that does not decide for itself is gated by its own checkbox.
11
+ */ const isToolEnabled = (tool, scope) => tool.isEnabled ? tool.isEnabled(scope) : scope.capabilities.tools[tool.name] === true;
5
12
  //#endregion
6
- export { defineMcpxTool };
13
+ export { defineMcpxTool, isToolEnabled, toolInputSchema };