@abinnovision/payloadcms-mcpx 2.0.0-beta.12 → 2.0.0-beta.14

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 (39) hide show
  1. package/README.md +32 -20
  2. package/dist/admin/capability-toggles.mjs +28 -8
  3. package/dist/admin/confirmations-panel.mjs +242 -0
  4. package/dist/admin/confirmations.d.mts +15 -0
  5. package/dist/admin/confirmations.mjs +70 -0
  6. package/dist/admin/index.d.mts +2 -1
  7. package/dist/admin/index.mjs +2 -1
  8. package/dist/api-keys/capability-matrix.d.mts +2 -0
  9. package/dist/api-keys/capability-matrix.mjs +12 -0
  10. package/dist/api-keys/collection.mjs +3 -3
  11. package/dist/api-keys/confirmation-view.mjs +6 -0
  12. package/dist/api-keys/fields.mjs +18 -1
  13. package/dist/builtin-tool-names.mjs +3 -1
  14. package/dist/capabilities.mjs +6 -2
  15. package/dist/define-tool.d.mts +4 -4
  16. package/dist/define-tool.mjs +8 -1
  17. package/dist/endpoint/confirmations.mjs +124 -0
  18. package/dist/endpoint/handler.mjs +2 -2
  19. package/dist/endpoint/server.mjs +22 -18
  20. package/dist/endpoint/upload.mjs +5 -4
  21. package/dist/grants/confirmation.mjs +106 -0
  22. package/dist/{upload → grants}/grant.mjs +39 -16
  23. package/dist/options.mjs +28 -5
  24. package/dist/plugin.d.mts +2 -1
  25. package/dist/plugin.mjs +5 -2
  26. package/dist/tools/builtin.mjs +5 -1
  27. package/dist/tools/create-document.mjs +1 -1
  28. package/dist/tools/delete-document.mjs +157 -0
  29. package/dist/tools/find-versions.mjs +1 -1
  30. package/dist/tools/get-document.mjs +1 -1
  31. package/dist/tools/patch-document.mjs +2 -2
  32. package/dist/tools/publish-document.mjs +1 -1
  33. package/dist/tools/run-confirmed.mjs +126 -0
  34. package/dist/tools/shared.mjs +4 -0
  35. package/dist/types.d.mts +129 -18
  36. package/dist/upload/file.mjs +25 -6
  37. package/dist/write/draft-guard.mjs +11 -9
  38. package/dist/write/publish-intent.mjs +16 -6
  39. package/package.json +1 -1
package/README.md CHANGED
@@ -61,7 +61,7 @@ The plugin adds:
61
61
  - An `mcpx-api-keys` collection in the admin group "MCP", holding the keys.
62
62
  - A draft guard on every collection and global that turns MCP writes into draft saves.
63
63
 
64
- The key form uses two admin components, so regenerate the import map:
64
+ The key form uses two admin components, three with `delete`, so regenerate the import map:
65
65
 
66
66
  ```sh
67
67
  payload generate:importmap
@@ -86,24 +86,25 @@ Claude Desktop and the MCP Inspector are covered in
86
86
 
87
87
  ## Options
88
88
 
89
- | Option | Default | Description |
90
- | ---------------------------- | ----------------------------------------- | ------------------------------------------------------------------- |
91
- | `collections` | required | Collections to expose. `true` means `{}`, everything it supports. |
92
- | `collections.<slug>.read` | `true` | Expose the read tools for this collection. |
93
- | `collections.<slug>.write` | `true` | Expose the write tools. Without drafts, a write goes live. |
94
- | `collections.<slug>.publish` | `true` with drafts | Expose `publishDocument`. Needs `versions.drafts` and `write`. |
95
- | `globals` | `{}` | Globals to expose, with the same three options. |
96
- | `userCollection` | `config.admin.user`, then `users` | Auth collection whose users the keys act as. |
97
- | `apiKeys.slug` | `mcpx-api-keys` | Slug of the key collection. |
98
- | `apiKeys.setupGuide` | `true` | Add the "Connect a client" tab to saved keys. |
99
- | `apiKeys.overrideCollection` | none | Function that receives the key collection and returns it. |
100
- | `endpoint.path` | `/mcpx` | Endpoint path below the API route. |
101
- | `limits.maxLimit` | `25` | Highest `limit` a client may pass to a list tool. |
102
- | `limits.maxDepth` | `1` | Highest `depth` a client may pass to a read tool. |
103
- | `tools` | `[]` | Custom tools, see [`docs/custom-tools.md`](./docs/custom-tools.md). |
104
- | `auth.resolve` | none | Replace or wrap the key lookup. |
105
- | `serverInfo` | `payloadcms-mcpx` and the package version | `{ name, version }` reported to clients. |
106
- | `diagnostics` | `true` | Expose the server version plus repository and issue links. |
89
+ | Option | Default | Description |
90
+ | ---------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
91
+ | `collections` | required | Collections to expose. `true` means `{}`, everything it supports. |
92
+ | `collections.<slug>.read` | `true` | Expose the read tools for this collection. |
93
+ | `collections.<slug>.write` | `true` | Expose the write tools. Without drafts, a write goes live. |
94
+ | `collections.<slug>.publish` | `true` with drafts | Expose `publishDocument`. Needs `versions.drafts` and `write`. |
95
+ | `collections.<slug>.delete` | `false` | Expose `deleteDocument`. Each delete is approved in the admin. `"unattended"` also offers trashing without approval; needs `trash`. |
96
+ | `globals` | `{}` | Globals to expose, with `read`, `write` and `publish`. |
97
+ | `userCollection` | `config.admin.user`, then `users` | Auth collection whose users the keys act as. |
98
+ | `apiKeys.slug` | `mcpx-api-keys` | Slug of the key collection. |
99
+ | `apiKeys.setupGuide` | `true` | Add the "Connect a client" tab to saved keys. |
100
+ | `apiKeys.overrideCollection` | none | Function that receives the key collection and returns it. |
101
+ | `endpoint.path` | `/mcpx` | Endpoint path below the API route. |
102
+ | `limits.maxLimit` | `25` | Highest `limit` a client may pass to a list tool. |
103
+ | `limits.maxDepth` | `1` | Highest `depth` a client may pass to a read tool. |
104
+ | `tools` | `[]` | Custom tools, see [`docs/custom-tools.md`](./docs/custom-tools.md). |
105
+ | `auth.resolve` | none | Replace or wrap the key lookup. |
106
+ | `serverInfo` | `payloadcms-mcpx` and the package version | `{ name, version }` reported to clients. |
107
+ | `diagnostics` | `true` | Expose the server version plus repository and issue links. |
107
108
 
108
109
  The config only takes capabilities away: `true` exposes everything the entity supports, and a key's
109
110
  checkboxes decide what each key may do. `publish: false` keeps MCP writes as drafts. On an entity
@@ -126,6 +127,8 @@ every check.
126
127
  | `createDocument` | Creates a draft from a seed, with a file in an upload collection. | `write` |
127
128
  | `validateDocument` | Lists what blocks publishing, without saving. | `write` |
128
129
  | `publishDocument` | Publishes the current draft. | `write`, `publish` |
130
+ | `deleteDocument` | Asks to delete a document, which the key's user approves. | `delete` |
131
+ | `runConfirmed` | Runs the approved calls. | `delete` |
129
132
 
130
133
  Collections and globals use the same tools. A tool that addresses one document takes either
131
134
  `collection` and `id`, or `global` alone. [`docs/concepts.md`](./docs/concepts.md#tools) lists
@@ -138,11 +141,17 @@ A key can do something only when both the config and the key allow it.
138
141
  The plugin config sets the upper bound: which collections and globals are exposed, and whether
139
142
  each one is readable, writable and publishable. A key cannot go past it.
140
143
 
141
- Each key carries one checkbox per exposed entity and operation (`read`, `write`, `publish`), and
144
+ Each key carries one checkbox per exposed entity and operation (`read`, `write`, `publish`,
145
+ `delete`), and
142
146
  one per custom tool. The admin panel shows them as a matrix with a row per entity and a column per
143
147
  operation. A cell the config does not expose shows a dash. `publish` exists only where the config
144
148
  exposes `write` and `publish` on an entity with drafts, and counts only when `write` is ticked as
145
149
  well. A write checkbox on an entity without drafts is marked "Writes go live immediately."
150
+ `delete` exists only where the config sets `delete: true` on a collection, and counts only when
151
+ `read` is ticked as well. The row toggle leaves it alone. A delete runs only after the key's user
152
+ approved it on the key's edit view, except where the config sets `delete: "unattended"` and the
153
+ key ticks "Delete without approval": there the document moves to trash at once. See
154
+ [deleting](./docs/concepts.md#deleting).
146
155
 
147
156
  `tools/list` follows the key. A tool the key cannot use is not listed, and each `collection` and
148
157
  `global` argument lists only the slugs the key may use with that tool. A checkbox added by a later
@@ -167,6 +176,9 @@ To restrict that, use `apiKeys.overrideCollection` as shown in
167
176
  - A file is sent in a second step, a `PUT`, and downloaded with a `GET`, each with a short-lived,
168
177
  single-use grant. Only collections with `upload.mimeTypes` take files. Downloads go through the
169
178
  collection's own file access. See [uploads](./docs/security.md#uploads).
179
+ - A delete waits for the key's user to approve it in the admin panel and is then run by the same
180
+ key. Collections with `trash` move the document to trash. See
181
+ [confirmations](./docs/security.md#confirmations).
170
182
  - Custom tools are trusted code and must apply access control themselves.
171
183
 
172
184
  Open limitations: a filter through a relation into a readable collection ignores that collection's
@@ -10,27 +10,47 @@ const stateOf = (paths, values) => {
10
10
  * Whether a column's bulk toggle reads as on, off or indeterminate. Cells the
11
11
  * config does not expose are ignored: two granted cells and one dash read as on.
12
12
  */ const columnState = (matrix, basePath, namespace, operation, values) => stateOf(matrix[namespace].filter((row) => row[operation]).map((row) => cellPath(basePath, namespace, row.fieldName, operation)), values);
13
+ const ROW_OPERATIONS = CAPABILITY_OPERATIONS.filter((operation) => operation.id !== "delete" && operation.id !== "deleteUnattended");
13
14
  /**
14
15
  * Whether an entity's row toggle reads as on, off or indeterminate, over the
15
- * operations the config exposes for that row.
16
- */ const rowState = (basePath, namespace, row, values) => stateOf(CAPABILITY_OPERATIONS.filter((operation) => row[operation.id]).map((operation) => cellPath(basePath, namespace, row.fieldName, operation.id)), values);
16
+ * operations the config exposes for that row, deletes aside.
17
+ */ const rowState = (basePath, namespace, row, values) => stateOf(ROW_OPERATIONS.filter((operation) => row[operation.id]).map((operation) => cellPath(basePath, namespace, row.fieldName, operation.id)), values);
17
18
  const toolsState = (matrix, basePath, values) => stateOf(matrix.tools.map((tool) => toolPath(basePath, tool.name)), values);
18
19
  const reconcile = (next, touched) => {
19
- if (touched.has("publish") && next.publish) return {
20
- ...next,
20
+ let result = next;
21
+ if (touched.has("publish") && result.publish) result = {
22
+ ...result,
21
23
  write: true
22
24
  };
23
- if (touched.has("write") && !next.write) return {
24
- ...next,
25
+ if (touched.has("write") && !result.write) result = {
26
+ ...result,
25
27
  publish: false
26
28
  };
27
- return next;
29
+ if (touched.has("deleteUnattended") && result.deleteUnattended) result = {
30
+ ...result,
31
+ delete: true
32
+ };
33
+ if ((touched.has("delete") || touched.has("deleteUnattended")) && result.delete) result = {
34
+ ...result,
35
+ read: true
36
+ };
37
+ if (touched.has("read") && !result.read) result = {
38
+ ...result,
39
+ delete: false
40
+ };
41
+ if ((touched.has("delete") || touched.has("read")) && !result.delete) result = {
42
+ ...result,
43
+ deleteUnattended: false
44
+ };
45
+ return result;
28
46
  };
29
47
  const rowActions = (basePath, namespace, row, values, changes) => {
30
48
  const current = {
31
49
  read: values[cellPath(basePath, namespace, row.fieldName, "read")] === true,
32
50
  write: values[cellPath(basePath, namespace, row.fieldName, "write")] === true,
33
- publish: values[cellPath(basePath, namespace, row.fieldName, "publish")] === true
51
+ publish: values[cellPath(basePath, namespace, row.fieldName, "publish")] === true,
52
+ delete: values[cellPath(basePath, namespace, row.fieldName, "delete")] === true,
53
+ deleteUnattended: values[cellPath(basePath, namespace, row.fieldName, "deleteUnattended")] === true
34
54
  };
35
55
  const touched = /* @__PURE__ */ new Set();
36
56
  const next = { ...current };
@@ -0,0 +1,242 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { Button } from "@payloadcms/ui";
4
+ //#region src/admin/confirmations-panel.tsx
5
+ /**
6
+ * The element id of a call's row, which the page scrolls to.
7
+ */ const confirmationRowId = (handle) => `mcpx-confirmation-${handle}`;
8
+ const CELL_PADDING = "calc(var(--base) * 0.4) calc(var(--base) * 0.6)";
9
+ const styles = {
10
+ panel: {
11
+ marginBottom: "var(--base)",
12
+ padding: "calc(var(--base) * 0.75)",
13
+ border: "1px solid var(--theme-elevation-150)",
14
+ borderRadius: "3px",
15
+ background: "var(--theme-elevation-50)"
16
+ },
17
+ heading: { margin: 0 },
18
+ lead: {
19
+ margin: "calc(var(--base) * 0.25) 0 calc(var(--base) * 0.75)",
20
+ color: "var(--theme-elevation-500)"
21
+ },
22
+ group: { marginBottom: "calc(var(--base) * 0.75)" },
23
+ groupHeading: { margin: "0 0 calc(var(--base) * 0.25)" },
24
+ table: {
25
+ width: "100%",
26
+ borderCollapse: "collapse",
27
+ textAlign: "left"
28
+ },
29
+ head: {
30
+ padding: CELL_PADDING,
31
+ color: "var(--theme-elevation-400)",
32
+ fontWeight: "normal",
33
+ borderBottom: "1px solid var(--theme-border-color)"
34
+ },
35
+ cell: {
36
+ padding: CELL_PADDING,
37
+ verticalAlign: "middle",
38
+ borderBottom: "1px solid var(--theme-elevation-100)"
39
+ },
40
+ highlighted: { background: "var(--theme-elevation-100)" },
41
+ muted: { color: "var(--theme-elevation-500)" },
42
+ status: {
43
+ display: "block",
44
+ color: "var(--theme-elevation-500)",
45
+ fontSize: "0.85em"
46
+ },
47
+ permanent: {
48
+ color: "var(--theme-error-500)",
49
+ fontWeight: 600
50
+ },
51
+ actions: {
52
+ display: "flex",
53
+ gap: "calc(var(--base) * 0.25)",
54
+ justifyContent: "flex-end"
55
+ },
56
+ footer: {
57
+ display: "flex",
58
+ gap: "calc(var(--base) * 0.5)"
59
+ }
60
+ };
61
+ const groupsOf = (confirmations) => {
62
+ const groups = /* @__PURE__ */ new Map();
63
+ for (const confirmation of confirmations) groups.set(confirmation.group, [...groups.get(confirmation.group) ?? [], confirmation]);
64
+ return [...groups];
65
+ };
66
+ const approveAllLabel = (confirmations) => {
67
+ const permanent = confirmations.filter((confirmation) => confirmation.summary.permanent).length;
68
+ return permanent === 0 ? `Approve all (${String(confirmations.length)})` : `Approve all (${String(confirmations.length)}, ${String(permanent)} permanent)`;
69
+ };
70
+ const Row = ({ busy, confirmation, onDecide }) => {
71
+ const { handle, highlighted, summary } = confirmation;
72
+ const cell = highlighted ? {
73
+ ...styles.cell,
74
+ ...styles.highlighted
75
+ } : styles.cell;
76
+ const name = summary.title ?? summary.id ?? "Unreadable";
77
+ return /*#__PURE__*/ jsxs("tr", {
78
+ "aria-current": highlighted ? "true" : void 0,
79
+ id: confirmationRowId(handle),
80
+ children: [
81
+ /*#__PURE__*/ jsx("td", {
82
+ style: cell,
83
+ children: summary.label
84
+ }),
85
+ /*#__PURE__*/ jsxs("td", {
86
+ style: cell,
87
+ children: [
88
+ summary.href ? /*#__PURE__*/ jsx("a", {
89
+ href: summary.href,
90
+ rel: "noreferrer",
91
+ target: "_blank",
92
+ children: name
93
+ }) : name,
94
+ summary.title && summary.id ? /*#__PURE__*/ jsxs("span", {
95
+ style: styles.muted,
96
+ children: [
97
+ " (",
98
+ summary.id,
99
+ ")"
100
+ ]
101
+ }) : null,
102
+ summary.status ? /*#__PURE__*/ jsx("span", {
103
+ style: styles.status,
104
+ children: summary.status
105
+ }) : null
106
+ ]
107
+ }),
108
+ /*#__PURE__*/ jsx("td", {
109
+ style: cell,
110
+ children: summary.permanent ? /*#__PURE__*/ jsx("span", {
111
+ style: styles.permanent,
112
+ children: "Permanent"
113
+ }) : "Moves to trash"
114
+ }),
115
+ /*#__PURE__*/ jsx("td", {
116
+ style: cell,
117
+ children: summary.reason ? /*#__PURE__*/ jsxs("span", { children: [/*#__PURE__*/ jsx("span", {
118
+ style: styles.muted,
119
+ children: "The client wrote: "
120
+ }), summary.reason] }) : /*#__PURE__*/ jsx("span", {
121
+ style: styles.muted,
122
+ children: "No reason given"
123
+ })
124
+ }),
125
+ /*#__PURE__*/ jsx("td", {
126
+ style: cell,
127
+ children: /*#__PURE__*/ jsxs("div", {
128
+ style: styles.actions,
129
+ children: [/*#__PURE__*/ jsx(Button, {
130
+ buttonStyle: "primary",
131
+ disabled: busy,
132
+ margin: false,
133
+ onClick: () => {
134
+ onDecide([handle], "approved");
135
+ },
136
+ size: "small",
137
+ type: "button",
138
+ children: "Approve"
139
+ }), /*#__PURE__*/ jsx(Button, {
140
+ buttonStyle: "secondary",
141
+ disabled: busy,
142
+ margin: false,
143
+ onClick: () => {
144
+ onDecide([handle], "rejected");
145
+ },
146
+ size: "small",
147
+ type: "button",
148
+ children: "Reject"
149
+ })]
150
+ })
151
+ })
152
+ ]
153
+ });
154
+ };
155
+ /**
156
+ * The calls of an API key waiting for its user's decision, grouped by tool.
157
+ * Renders nothing when none is waiting.
158
+ */ const ConfirmationsPanel = ({ busy, confirmations, onDecide }) => {
159
+ if (confirmations.length === 0) return null;
160
+ const handles = confirmations.map((confirmation) => confirmation.handle);
161
+ return /*#__PURE__*/ jsxs("section", {
162
+ className: "field-type",
163
+ style: styles.panel,
164
+ children: [
165
+ /*#__PURE__*/ jsx("h3", {
166
+ style: styles.heading,
167
+ children: "Waiting for your approval"
168
+ }),
169
+ /*#__PURE__*/ jsx("p", {
170
+ style: styles.lead,
171
+ children: "A client using this key asked for these calls. Approving runs nothing: the client runs approved calls with runConfirmed. Each call expires 15 minutes after it was requested."
172
+ }),
173
+ groupsOf(confirmations).map(([group, entries]) => /*#__PURE__*/ jsxs("div", {
174
+ style: styles.group,
175
+ children: [/*#__PURE__*/ jsx("h4", {
176
+ style: styles.groupHeading,
177
+ children: group
178
+ }), /*#__PURE__*/ jsxs("table", {
179
+ style: styles.table,
180
+ children: [/*#__PURE__*/ jsx("thead", { children: /*#__PURE__*/ jsxs("tr", { children: [
181
+ /*#__PURE__*/ jsx("th", {
182
+ scope: "col",
183
+ style: styles.head,
184
+ children: "Collection"
185
+ }),
186
+ /*#__PURE__*/ jsx("th", {
187
+ scope: "col",
188
+ style: styles.head,
189
+ children: "Document"
190
+ }),
191
+ /*#__PURE__*/ jsx("th", {
192
+ scope: "col",
193
+ style: styles.head,
194
+ children: "Effect"
195
+ }),
196
+ /*#__PURE__*/ jsx("th", {
197
+ scope: "col",
198
+ style: styles.head,
199
+ children: "Reason"
200
+ }),
201
+ /*#__PURE__*/ jsx("th", {
202
+ scope: "col",
203
+ style: styles.head,
204
+ children: /*#__PURE__*/ jsx("span", {
205
+ className: "sr-only",
206
+ children: "Decision"
207
+ })
208
+ })
209
+ ] }) }), /*#__PURE__*/ jsx("tbody", { children: entries.map((confirmation) => /*#__PURE__*/ jsx(Row, {
210
+ busy,
211
+ confirmation,
212
+ onDecide
213
+ }, confirmation.handle)) })]
214
+ })]
215
+ }, group)),
216
+ /*#__PURE__*/ jsxs("div", {
217
+ style: styles.footer,
218
+ children: [/*#__PURE__*/ jsx(Button, {
219
+ buttonStyle: "primary",
220
+ disabled: busy,
221
+ margin: false,
222
+ onClick: () => {
223
+ onDecide(handles, "approved");
224
+ },
225
+ type: "button",
226
+ children: approveAllLabel(confirmations)
227
+ }), /*#__PURE__*/ jsx(Button, {
228
+ buttonStyle: "secondary",
229
+ disabled: busy,
230
+ margin: false,
231
+ onClick: () => {
232
+ onDecide(handles, "rejected");
233
+ },
234
+ type: "button",
235
+ children: "Reject all"
236
+ })]
237
+ })
238
+ ]
239
+ });
240
+ };
241
+ //#endregion
242
+ export { ConfirmationsPanel, confirmationRowId };
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ //#region src/admin/confirmations.d.ts
3
+ interface McpxConfirmationsProps {
4
+ /**
5
+ * Endpoint path below the API route, from the plugin options.
6
+ */
7
+ endpointPath: string;
8
+ }
9
+ /**
10
+ * The calls waiting for approval on the API key edit view. Reads and decides
11
+ * through the plugin's endpoints, never through the key's form, so a decision
12
+ * does not mark the key modified. Renders nothing while none is waiting.
13
+ */
14
+ export declare const McpxConfirmations: React.FC<McpxConfirmationsProps>;
15
+ //#endregion
@@ -0,0 +1,70 @@
1
+ "use client";
2
+ import { CONFIRMATIONS_PATH } from "../api-keys/confirmation-view.mjs";
3
+ import { ConfirmationsPanel, confirmationRowId } from "./confirmations-panel.mjs";
4
+ import { jsx } from "react/jsx-runtime";
5
+ import { toast, useConfig, useDocumentInfo } from "@payloadcms/ui";
6
+ import { useCallback, useEffect, useState } from "react";
7
+ //#region src/admin/confirmations.tsx
8
+ /**
9
+ * The calls waiting for approval on the API key edit view. Reads and decides
10
+ * through the plugin's endpoints, never through the key's form, so a decision
11
+ * does not mark the key modified. Renders nothing while none is waiting.
12
+ */ const McpxConfirmations = ({ endpointPath }) => {
13
+ const { id } = useDocumentInfo();
14
+ const { config } = useConfig();
15
+ const [confirmations, setConfirmations] = useState([]);
16
+ const [busy, setBusy] = useState(false);
17
+ const [version, setVersion] = useState(0);
18
+ const url = `${config.serverURL}${config.routes.api}${endpointPath}${CONFIRMATIONS_PATH}`;
19
+ useEffect(() => {
20
+ if (id === void 0) return;
21
+ let active = true;
22
+ const params = new URLSearchParams({ key: String(id) });
23
+ const highlight = new URLSearchParams(window.location.search).get("confirmation");
24
+ if (highlight !== null) params.set("confirmation", highlight);
25
+ fetch(`${url}?${params.toString()}`, { credentials: "include" }).then(async (response) => response.ok ? await response.json() : { confirmations: [] }).then((body) => {
26
+ if (active) setConfirmations(body.confirmations);
27
+ }).catch(() => void 0);
28
+ return () => {
29
+ active = false;
30
+ };
31
+ }, [
32
+ id,
33
+ url,
34
+ version
35
+ ]);
36
+ const highlighted = confirmations.find((entry) => entry.highlighted)?.handle;
37
+ useEffect(() => {
38
+ if (highlighted !== void 0) document.getElementById(confirmationRowId(highlighted))?.scrollIntoView({ block: "center" });
39
+ }, [highlighted]);
40
+ const decide = useCallback((handles, decision) => {
41
+ setBusy(true);
42
+ fetch(url, {
43
+ method: "POST",
44
+ credentials: "include",
45
+ headers: { "content-type": "application/json" },
46
+ body: JSON.stringify({
47
+ key: id,
48
+ handles,
49
+ decision
50
+ })
51
+ }).then(async (response) => {
52
+ if (!response.ok) {
53
+ const body = await response.json().catch(() => ({}));
54
+ toast.error(body.error ?? "The decision could not be saved.");
55
+ }
56
+ }).catch(() => {
57
+ toast.error("The decision could not be saved.");
58
+ }).finally(() => {
59
+ setBusy(false);
60
+ setVersion((current) => current + 1);
61
+ });
62
+ }, [id, url]);
63
+ return /*#__PURE__*/ jsx(ConfirmationsPanel, {
64
+ busy,
65
+ confirmations,
66
+ onDecide: decide
67
+ });
68
+ };
69
+ //#endregion
70
+ export { McpxConfirmations };
@@ -1,3 +1,4 @@
1
1
  import { McpxCapabilityMatrix } from "./capability-matrix.mjs";
2
+ import { McpxConfirmations } from "./confirmations.mjs";
2
3
  import { McpxSetupGuide } from "./setup-guide.mjs";
3
- export { McpxCapabilityMatrix, McpxSetupGuide };
4
+ export { McpxCapabilityMatrix, McpxConfirmations, McpxSetupGuide };
@@ -1,3 +1,4 @@
1
1
  import { McpxCapabilityMatrix } from "./capability-matrix.mjs";
2
+ import { McpxConfirmations } from "./confirmations.mjs";
2
3
  import { McpxSetupGuide } from "./setup-guide.mjs";
3
- export { McpxCapabilityMatrix, McpxSetupGuide };
4
+ export { McpxCapabilityMatrix, McpxConfirmations, McpxSetupGuide };
@@ -14,6 +14,8 @@ export interface CapabilityRow {
14
14
  read: boolean;
15
15
  write: boolean;
16
16
  publish: boolean;
17
+ delete: boolean;
18
+ deleteUnattended: boolean;
17
19
  /**
18
20
  * Said only where a row departs from what its column header promises.
19
21
  */
@@ -23,6 +23,16 @@ import { CAPABILITIES_FIELD, canPublish, canWrite, isLiveWrite } from "../capabi
23
23
  id: "publish",
24
24
  label: "Publish",
25
25
  description: "Promote the current draft to what the public sees."
26
+ },
27
+ {
28
+ id: "delete",
29
+ label: "Delete",
30
+ description: "Delete documents, each approved by this key's user in the admin panel."
31
+ },
32
+ {
33
+ id: "deleteUnattended",
34
+ label: "Delete without approval",
35
+ description: "Move documents to trash without asking this key's user."
26
36
  }
27
37
  ];
28
38
  const LIVE_HINT = "Writes go live immediately.";
@@ -33,6 +43,8 @@ const toRow = (entity) => ({
33
43
  read: entity.read,
34
44
  write: canWrite(entity),
35
45
  publish: canPublish(entity),
46
+ delete: entity.delete,
47
+ deleteUnattended: entity.deleteUnattended,
36
48
  ...hintFor(entity)
37
49
  });
38
50
  /**
@@ -1,4 +1,4 @@
1
- import { createCapabilityFields, createKeyFields, withSetupGuideTab } from "./fields.mjs";
1
+ import { createCapabilityFields, createConfirmationFields, createKeyFields, withSetupGuideTab } from "./fields.mjs";
2
2
  import { generateApiKey, hashApiKey } from "./key.mjs";
3
3
  //#region src/api-keys/collection.ts
4
4
  /**
@@ -37,7 +37,7 @@ import { generateApiKey, hashApiKey } from "./key.mjs";
37
37
  delete: ownKeysOnly
38
38
  },
39
39
  hooks: { beforeChange: [keyBeforeChange] },
40
- fields: withSetupGuideTab([
40
+ fields: [...createConfirmationFields(options), ...withSetupGuideTab([
41
41
  {
42
42
  name: "user",
43
43
  type: "relationship",
@@ -52,7 +52,7 @@ import { generateApiKey, hashApiKey } from "./key.mjs";
52
52
  },
53
53
  ...createKeyFields(),
54
54
  ...createCapabilityFields(options)
55
- ], options)
55
+ ], options)]
56
56
  };
57
57
  };
58
58
  //#endregion
@@ -0,0 +1,6 @@
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
+ //#endregion
6
+ export { CONFIRMATIONS_PATH };
@@ -79,6 +79,23 @@ const SETUP_GUIDE_FIELD = "setupGuide";
79
79
  }
80
80
  ];
81
81
  /**
82
+ * The pending calls of the key, above everything else on its edit view, where
83
+ * a collection exposes `delete`. The component renders nothing while none is
84
+ * pending and never touches the form, so deciding does not mark the key
85
+ * modified.
86
+ */ const createConfirmationFields = (options) => options.confirmations ? [{
87
+ name: "confirmations",
88
+ type: "ui",
89
+ admin: {
90
+ disableListColumn: true,
91
+ components: { Field: {
92
+ path: "@abinnovision/payloadcms-mcpx/admin",
93
+ exportName: "McpxConfirmations",
94
+ clientProps: { endpointPath: options.endpointPath }
95
+ } }
96
+ }
97
+ }] : [];
98
+ /**
82
99
  * Wraps the key fields and the setup guide in unnamed tabs, so wide snippets
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.
@@ -165,4 +182,4 @@ const SETUP_GUIDE_FIELD = "setupGuide";
165
182
  }];
166
183
  };
167
184
  //#endregion
168
- export { createCapabilityFields, createKeyFields, withSetupGuideTab };
185
+ export { createCapabilityFields, createConfirmationFields, createKeyFields, withSetupGuideTab };
@@ -10,7 +10,9 @@
10
10
  "patchDocument",
11
11
  "createDocument",
12
12
  "validateDocument",
13
- "publishDocument"
13
+ "publishDocument",
14
+ "deleteDocument",
15
+ "runConfirmed"
14
16
  ];
15
17
  //#endregion
16
18
  export { BUILTIN_TOOL_NAMES };
@@ -28,10 +28,13 @@ const publishFlag = (entity, group) => canPublish(entity) && flag(group, "write"
28
28
  const resolved = {};
29
29
  for (const entity of entities) {
30
30
  const group = isPlainObject(namespaceGroup) ? namespaceGroup[entity.fieldName] : void 0;
31
+ const deleting = entity.delete && flag(group, "read") && flag(group, "delete");
31
32
  resolved[entity.slug] = {
32
33
  read: entity.read && flag(group, "read"),
33
34
  write: canWrite(entity) && flag(group, "write"),
34
- publish: publishFlag(entity, group)
35
+ publish: publishFlag(entity, group),
36
+ delete: deleting,
37
+ deleteUnattended: deleting && entity.deleteUnattended && flag(group, "deleteUnattended")
35
38
  };
36
39
  }
37
40
  return resolved;
@@ -53,7 +56,8 @@ const publishFlag = (entity, group) => canPublish(entity) && flag(group, "write"
53
56
  return {
54
57
  readable: slugsWith("read"),
55
58
  writable: slugsWith("write"),
56
- publishable: slugsWith("publish")
59
+ publishable: slugsWith("publish"),
60
+ deletable: slugsWith("delete")
57
61
  };
58
62
  };
59
63
  //#endregion
@@ -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