@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.
- package/README.md +32 -20
- package/dist/admin/capability-toggles.mjs +28 -8
- package/dist/admin/confirmations-panel.mjs +242 -0
- package/dist/admin/confirmations.d.mts +15 -0
- package/dist/admin/confirmations.mjs +70 -0
- package/dist/admin/index.d.mts +2 -1
- package/dist/admin/index.mjs +2 -1
- package/dist/api-keys/capability-matrix.d.mts +2 -0
- package/dist/api-keys/capability-matrix.mjs +12 -0
- package/dist/api-keys/collection.mjs +3 -3
- package/dist/api-keys/confirmation-view.mjs +6 -0
- package/dist/api-keys/fields.mjs +18 -1
- package/dist/builtin-tool-names.mjs +3 -1
- package/dist/capabilities.mjs +6 -2
- package/dist/define-tool.d.mts +4 -4
- package/dist/define-tool.mjs +8 -1
- package/dist/endpoint/confirmations.mjs +124 -0
- package/dist/endpoint/handler.mjs +2 -2
- package/dist/endpoint/server.mjs +22 -18
- package/dist/endpoint/upload.mjs +5 -4
- package/dist/grants/confirmation.mjs +106 -0
- package/dist/{upload → grants}/grant.mjs +39 -16
- package/dist/options.mjs +28 -5
- package/dist/plugin.d.mts +2 -1
- package/dist/plugin.mjs +5 -2
- package/dist/tools/builtin.mjs +5 -1
- package/dist/tools/create-document.mjs +1 -1
- package/dist/tools/delete-document.mjs +157 -0
- package/dist/tools/find-versions.mjs +1 -1
- package/dist/tools/get-document.mjs +1 -1
- package/dist/tools/patch-document.mjs +2 -2
- package/dist/tools/publish-document.mjs +1 -1
- package/dist/tools/run-confirmed.mjs +126 -0
- package/dist/tools/shared.mjs +4 -0
- package/dist/types.d.mts +129 -18
- package/dist/upload/file.mjs +25 -6
- package/dist/write/draft-guard.mjs +11 -9
- package/dist/write/publish-intent.mjs +16 -6
- 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
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `apiKeys.
|
|
99
|
-
| `apiKeys.
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `limits.
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
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
|
|
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(
|
|
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
|
-
|
|
20
|
-
|
|
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") && !
|
|
24
|
-
...
|
|
25
|
+
if (touched.has("write") && !result.write) result = {
|
|
26
|
+
...result,
|
|
25
27
|
publish: false
|
|
26
28
|
};
|
|
27
|
-
|
|
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 };
|
package/dist/admin/index.d.mts
CHANGED
|
@@ -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 };
|
package/dist/admin/index.mjs
CHANGED
|
@@ -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 };
|
|
@@ -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
|
package/dist/api-keys/fields.mjs
CHANGED
|
@@ -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 };
|
package/dist/capabilities.mjs
CHANGED
|
@@ -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
|
package/dist/define-tool.d.mts
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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
|