@abinnovision/payloadcms-mcpx 2.0.0-beta.19 → 2.0.0-beta.20
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/dist/admin/capability-matrix.mjs +213 -316
- package/dist/admin/capability-toggles.mjs +24 -31
- package/dist/admin/confirmations-panel.mjs +61 -71
- package/dist/admin/confirmations.mjs +2 -1
- package/dist/admin/endpoint-url.mjs +7 -0
- package/dist/admin/setup-guide.mjs +2 -1
- package/dist/api-keys/capability-matrix.d.mts +3 -6
- package/dist/api-keys/capability-matrix.mjs +9 -48
- package/dist/api-keys/collection.mjs +2 -5
- package/dist/api-keys/confirmation-view.mjs +2 -1
- package/dist/api-keys/fields.mjs +33 -55
- package/dist/auth/resolve.mjs +29 -10
- package/dist/capabilities.d.mts +2 -0
- package/dist/capabilities.mjs +62 -19
- package/dist/endpoint/confirmations.mjs +14 -14
- package/dist/endpoint/handler.mjs +6 -22
- package/dist/endpoint/http.mjs +27 -0
- package/dist/endpoint/upload.mjs +11 -43
- package/dist/grants/confirmation.mjs +11 -42
- package/dist/grants/grant.mjs +58 -40
- package/dist/guards.mjs +8 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/options.mjs +34 -54
- package/dist/plugin.mjs +2 -1
- package/dist/request.mjs +5 -1
- package/dist/result.mjs +11 -1
- package/dist/schema/admin-hidden.mjs +4 -4
- package/dist/schema/describe.mjs +12 -18
- package/dist/schema/index.mjs +5 -6
- package/dist/schema/lexical-pointer.mjs +8 -8
- package/dist/schema/lexical.mjs +15 -13
- package/dist/schema/outline.mjs +4 -5
- package/dist/schema/path.mjs +4 -1
- package/dist/schema/pointer.mjs +11 -17
- package/dist/schema/shape.mjs +38 -50
- package/dist/schema/walk.mjs +57 -18
- package/dist/tools/create-document.mjs +16 -44
- package/dist/tools/delete-document.mjs +14 -17
- package/dist/tools/describe-schema.mjs +7 -5
- package/dist/tools/document.mjs +130 -44
- package/dist/tools/duplicate-document.mjs +11 -10
- package/dist/tools/find-documents.mjs +15 -20
- package/dist/tools/find-versions.mjs +10 -14
- package/dist/tools/get-document.mjs +8 -12
- package/dist/tools/links.mjs +2 -1
- package/dist/tools/list-capabilities.mjs +2 -1
- package/dist/tools/patch-document.mjs +12 -44
- package/dist/tools/publish-document.mjs +9 -27
- package/dist/tools/query-paths.mjs +1 -1
- package/dist/tools/run-confirmed.mjs +38 -50
- package/dist/tools/shared.mjs +43 -41
- package/dist/tools/validate-document.mjs +4 -6
- package/dist/tools/versions.mjs +11 -6
- package/dist/types.d.mts +12 -26
- package/dist/upload/file.mjs +10 -18
- package/dist/write/draft-guard.mjs +20 -20
- package/dist/write/patch.mjs +4 -4
- package/dist/write/publish-intent.mjs +22 -28
- package/dist/write/row-ids.mjs +3 -1
- package/dist/write/write-data.mjs +18 -31
- package/package.json +5 -5
- package/dist/builtin-tool-names.mjs +0 -19
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ACCESS_CHAIN, STORED_OPERATIONS, requiresOf } from "../capabilities.mjs";
|
|
2
|
+
import { cellPath, rowOperations, toolPath } from "../api-keys/capability-matrix.mjs";
|
|
2
3
|
//#region src/admin/capability-toggles.ts
|
|
3
4
|
/**
|
|
4
5
|
* How far a key may go with an entity. Each level includes the ones before it.
|
|
5
|
-
*/ const ACCESS_LEVELS = [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
];
|
|
6
|
+
*/ const ACCESS_LEVELS = ["none", ...ACCESS_CHAIN.map(({ id }) => id)];
|
|
7
|
+
const toolGranted = (values, basePath, name) => values[toolPath(basePath, name)] === true;
|
|
8
|
+
const cellGranted = (values, basePath, namespace, row, operation) => values[cellPath(basePath, namespace, row.fieldName, operation)] === true;
|
|
9
|
+
/**
|
|
10
|
+
* One value per stored operation.
|
|
11
|
+
*/ const byOperation = (valueOf) => Object.fromEntries(STORED_OPERATIONS.map(({ id }) => [id, valueOf(id)]));
|
|
11
12
|
const toolsState = (matrix, basePath, values) => {
|
|
12
|
-
const granted = matrix.tools.filter((tool) => values
|
|
13
|
+
const granted = matrix.tools.filter((tool) => toolGranted(values, basePath, tool.name)).length;
|
|
13
14
|
if (granted === 0) return "off";
|
|
14
15
|
return granted === matrix.tools.length ? "on" : "mixed";
|
|
15
16
|
};
|
|
@@ -21,24 +22,19 @@ const toolsState = (matrix, basePath, values) => {
|
|
|
21
22
|
* config exposes. A write without a read reads as none, as on the server.
|
|
22
23
|
*/ const accessLevelOf = (basePath, namespace, row, values) => {
|
|
23
24
|
let reached = "none";
|
|
24
|
-
for (const
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
reached = level;
|
|
25
|
+
for (const { id } of ACCESS_CHAIN) {
|
|
26
|
+
if (!row[id] || !cellGranted(values, basePath, namespace, row, id)) break;
|
|
27
|
+
reached = id;
|
|
28
28
|
}
|
|
29
29
|
return reached;
|
|
30
30
|
};
|
|
31
31
|
/**
|
|
32
32
|
* The delete mode a row's stored flags read as.
|
|
33
33
|
*/ const deleteModeOf = (basePath, namespace, row, values) => {
|
|
34
|
-
const granted = (operation) => values
|
|
34
|
+
const granted = (operation) => cellGranted(values, basePath, namespace, row, operation);
|
|
35
35
|
if (!granted("read") || !granted("delete")) return "off";
|
|
36
36
|
return row.deleteUnattended && granted("deleteUnattended") ? "trash" : "approve";
|
|
37
37
|
};
|
|
38
|
-
const requiresOf = (operation) => {
|
|
39
|
-
const found = STORED_OPERATIONS.find((candidate) => candidate.id === operation);
|
|
40
|
-
return found && "requires" in found ? found.requires : void 0;
|
|
41
|
-
};
|
|
42
38
|
const prerequisitesOf = (operation) => {
|
|
43
39
|
const required = requiresOf(operation);
|
|
44
40
|
return required ? [required, ...prerequisitesOf(required)] : [];
|
|
@@ -50,18 +46,18 @@ const reconcile = (next, touched) => {
|
|
|
50
46
|
return result;
|
|
51
47
|
};
|
|
52
48
|
const rowActions = (basePath, namespace, row, values, changes) => {
|
|
53
|
-
const current =
|
|
49
|
+
const current = byOperation((operation) => cellGranted(values, basePath, namespace, row, operation));
|
|
54
50
|
const touched = /* @__PURE__ */ new Set();
|
|
55
51
|
const next = { ...current };
|
|
56
|
-
for (const operation of
|
|
52
|
+
for (const operation of rowOperations(row)) {
|
|
57
53
|
const change = changes[operation.id];
|
|
58
|
-
if (change !== void 0
|
|
54
|
+
if (change !== void 0) {
|
|
59
55
|
next[operation.id] = change;
|
|
60
56
|
touched.add(operation.id);
|
|
61
57
|
}
|
|
62
58
|
}
|
|
63
59
|
const reconciled = reconcile(next, touched);
|
|
64
|
-
return
|
|
60
|
+
return rowOperations(row).filter((operation) => reconciled[operation.id] !== current[operation.id]).map((operation) => ({
|
|
65
61
|
type: "UPDATE",
|
|
66
62
|
path: cellPath(basePath, namespace, row.fieldName, operation.id),
|
|
67
63
|
value: reconciled[operation.id]
|
|
@@ -69,11 +65,7 @@ const rowActions = (basePath, namespace, row, values, changes) => {
|
|
|
69
65
|
};
|
|
70
66
|
const grantsOf = (level) => {
|
|
71
67
|
const rank = ACCESS_LEVELS.indexOf(level);
|
|
72
|
-
return {
|
|
73
|
-
read: rank >= 1,
|
|
74
|
-
write: rank >= 2,
|
|
75
|
-
publish: rank >= 3
|
|
76
|
-
};
|
|
68
|
+
return Object.fromEntries(ACCESS_CHAIN.map(({ id }, i) => [id, rank > i]));
|
|
77
69
|
};
|
|
78
70
|
/**
|
|
79
71
|
* The form-state updates one click implies, as literal actions. Every click
|
|
@@ -84,7 +76,7 @@ const grantsOf = (level) => {
|
|
|
84
76
|
* nothing does not mark the form modified. A level above a row's ceiling
|
|
85
77
|
* grants what the row exposes and no more.
|
|
86
78
|
*/ const buildToggleActions = (matrix, basePath, values, intent) => {
|
|
87
|
-
if (intent.kind === "tool" || intent.kind === "tools") return matrix.tools.filter((tool) => intent.kind === "tools" || tool.name === intent.name).filter((tool) => values
|
|
79
|
+
if (intent.kind === "tool" || intent.kind === "tools") return matrix.tools.filter((tool) => intent.kind === "tools" || tool.name === intent.name).filter((tool) => toolGranted(values, basePath, tool.name) !== intent.value).map((tool) => ({
|
|
88
80
|
type: "UPDATE",
|
|
89
81
|
path: toolPath(basePath, tool.name),
|
|
90
82
|
value: intent.value
|
|
@@ -96,22 +88,23 @@ const grantsOf = (level) => {
|
|
|
96
88
|
};
|
|
97
89
|
return rows.flatMap((row) => rowActions(basePath, intent.namespace, row, values, changes));
|
|
98
90
|
};
|
|
91
|
+
const firstNoop = (candidates, matrix, basePath, values, intentOf) => candidates.find((candidate) => buildToggleActions(matrix, basePath, values, intentOf(candidate)).length === 0);
|
|
99
92
|
/**
|
|
100
93
|
* The level a namespace's "All" control shows: the one that would change
|
|
101
94
|
* nothing if picked, so rows capped below it still count. `undefined` while
|
|
102
95
|
* the rows differ.
|
|
103
|
-
*/ const allAccessLevel = (matrix, basePath, namespace, values) => ACCESS_LEVELS
|
|
96
|
+
*/ const allAccessLevel = (matrix, basePath, namespace, values) => firstNoop(ACCESS_LEVELS, matrix, basePath, values, (level) => ({
|
|
104
97
|
kind: "access",
|
|
105
98
|
namespace,
|
|
106
99
|
level
|
|
107
|
-
})
|
|
100
|
+
}));
|
|
108
101
|
/**
|
|
109
102
|
* The delete mode the "All" control shows, by the same rule. It offers no
|
|
110
103
|
* trash, so a row that trashes directly reads as mixed.
|
|
111
|
-
*/ const allDeleteMode = (matrix, basePath, namespace, values) => ["off", "approve"]
|
|
104
|
+
*/ const allDeleteMode = (matrix, basePath, namespace, values) => firstNoop(["off", "approve"], matrix, basePath, values, (mode) => ({
|
|
112
105
|
kind: "deleteMode",
|
|
113
106
|
namespace,
|
|
114
107
|
mode
|
|
115
|
-
})
|
|
108
|
+
}));
|
|
116
109
|
//#endregion
|
|
117
110
|
export { accessLevelOf, accessLevelsOf, allAccessLevel, allDeleteMode, buildToggleActions, deleteModeOf, toolsState };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Button } from "@payloadcms/ui";
|
|
4
4
|
//#region src/admin/confirmations-panel.tsx
|
|
5
5
|
/**
|
|
@@ -58,11 +58,42 @@ const styles = {
|
|
|
58
58
|
gap: "calc(var(--base) * 0.5)"
|
|
59
59
|
}
|
|
60
60
|
};
|
|
61
|
+
const HEADS = [
|
|
62
|
+
"Collection",
|
|
63
|
+
"Document",
|
|
64
|
+
"Effect",
|
|
65
|
+
"Reason"
|
|
66
|
+
];
|
|
61
67
|
const groupsOf = (confirmations) => {
|
|
62
68
|
const groups = /* @__PURE__ */ new Map();
|
|
63
|
-
for (const confirmation of confirmations)
|
|
69
|
+
for (const confirmation of confirmations) {
|
|
70
|
+
const entries = groups.get(confirmation.group) ?? [];
|
|
71
|
+
entries.push(confirmation);
|
|
72
|
+
groups.set(confirmation.group, entries);
|
|
73
|
+
}
|
|
64
74
|
return [...groups];
|
|
65
75
|
};
|
|
76
|
+
const DecisionButtons = ({ approve, busy, onDecide, reject, small = false }) => /*#__PURE__*/ jsxs(Fragment, { children: [/*#__PURE__*/ jsx(Button, {
|
|
77
|
+
buttonStyle: "primary",
|
|
78
|
+
disabled: busy,
|
|
79
|
+
margin: false,
|
|
80
|
+
onClick: () => {
|
|
81
|
+
onDecide("approved");
|
|
82
|
+
},
|
|
83
|
+
size: small ? "small" : "medium",
|
|
84
|
+
type: "button",
|
|
85
|
+
children: approve
|
|
86
|
+
}), /*#__PURE__*/ jsx(Button, {
|
|
87
|
+
buttonStyle: "secondary",
|
|
88
|
+
disabled: busy,
|
|
89
|
+
margin: false,
|
|
90
|
+
onClick: () => {
|
|
91
|
+
onDecide("rejected");
|
|
92
|
+
},
|
|
93
|
+
size: small ? "small" : "medium",
|
|
94
|
+
type: "button",
|
|
95
|
+
children: reject
|
|
96
|
+
})] });
|
|
66
97
|
const approveAllLabel = (confirmations) => {
|
|
67
98
|
const permanent = confirmations.filter((confirmation) => confirmation.summary.permanent).length;
|
|
68
99
|
return permanent === 0 ? `Approve all (${String(confirmations.length)})` : `Approve all (${String(confirmations.length)}, ${String(permanent)} permanent)`;
|
|
@@ -124,29 +155,17 @@ const Row = ({ busy, confirmation, onDecide }) => {
|
|
|
124
155
|
}),
|
|
125
156
|
/*#__PURE__*/ jsx("td", {
|
|
126
157
|
style: cell,
|
|
127
|
-
children: /*#__PURE__*/
|
|
158
|
+
children: /*#__PURE__*/ jsx("div", {
|
|
128
159
|
style: styles.actions,
|
|
129
|
-
children:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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");
|
|
160
|
+
children: /*#__PURE__*/ jsx(DecisionButtons, {
|
|
161
|
+
approve: "Approve",
|
|
162
|
+
busy,
|
|
163
|
+
onDecide: (decision) => {
|
|
164
|
+
onDecide([handle], decision);
|
|
145
165
|
},
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
})]
|
|
166
|
+
reject: "Reject",
|
|
167
|
+
small: true
|
|
168
|
+
})
|
|
150
169
|
})
|
|
151
170
|
})
|
|
152
171
|
]
|
|
@@ -177,63 +196,34 @@ const Row = ({ busy, confirmation, onDecide }) => {
|
|
|
177
196
|
children: group
|
|
178
197
|
}), /*#__PURE__*/ jsxs("table", {
|
|
179
198
|
style: styles.table,
|
|
180
|
-
children: [/*#__PURE__*/ jsx("thead", { children: /*#__PURE__*/ jsxs("tr", { children: [
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
children: "
|
|
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
|
-
})
|
|
199
|
+
children: [/*#__PURE__*/ jsx("thead", { children: /*#__PURE__*/ jsxs("tr", { children: [HEADS.map((head) => /*#__PURE__*/ jsx("th", {
|
|
200
|
+
scope: "col",
|
|
201
|
+
style: styles.head,
|
|
202
|
+
children: head
|
|
203
|
+
}, head)), /*#__PURE__*/ jsx("th", {
|
|
204
|
+
scope: "col",
|
|
205
|
+
style: styles.head,
|
|
206
|
+
children: /*#__PURE__*/ jsx("span", {
|
|
207
|
+
className: "sr-only",
|
|
208
|
+
children: "Decision"
|
|
208
209
|
})
|
|
209
|
-
] }) }), /*#__PURE__*/ jsx("tbody", { children: entries.map((confirmation) => /*#__PURE__*/ jsx(Row, {
|
|
210
|
+
})] }) }), /*#__PURE__*/ jsx("tbody", { children: entries.map((confirmation) => /*#__PURE__*/ jsx(Row, {
|
|
210
211
|
busy,
|
|
211
212
|
confirmation,
|
|
212
213
|
onDecide
|
|
213
214
|
}, confirmation.handle)) })]
|
|
214
215
|
})]
|
|
215
216
|
}, group)),
|
|
216
|
-
/*#__PURE__*/
|
|
217
|
+
/*#__PURE__*/ jsx("div", {
|
|
217
218
|
style: styles.footer,
|
|
218
|
-
children:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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");
|
|
219
|
+
children: /*#__PURE__*/ jsx(DecisionButtons, {
|
|
220
|
+
approve: approveAllLabel(confirmations),
|
|
221
|
+
busy,
|
|
222
|
+
onDecide: (decision) => {
|
|
223
|
+
onDecide(handles, decision);
|
|
233
224
|
},
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
})]
|
|
225
|
+
reject: "Reject all"
|
|
226
|
+
})
|
|
237
227
|
})
|
|
238
228
|
]
|
|
239
229
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { CONFIRMATIONS_PATH } from "../api-keys/confirmation-view.mjs";
|
|
3
3
|
import { ConfirmationsPanel, confirmationRowId } from "./confirmations-panel.mjs";
|
|
4
|
+
import { endpointUrl } from "./endpoint-url.mjs";
|
|
4
5
|
import { jsx } from "react/jsx-runtime";
|
|
5
6
|
import { toast, useConfig, useDocumentInfo } from "@payloadcms/ui";
|
|
6
7
|
import { useCallback, useEffect, useState } from "react";
|
|
@@ -15,7 +16,7 @@ import { useCallback, useEffect, useState } from "react";
|
|
|
15
16
|
const [confirmations, setConfirmations] = useState([]);
|
|
16
17
|
const [busy, setBusy] = useState(false);
|
|
17
18
|
const [version, setVersion] = useState(0);
|
|
18
|
-
const url = `${config.serverURL
|
|
19
|
+
const url = `${endpointUrl(config.serverURL, config.routes.api, endpointPath)}${CONFIRMATIONS_PATH}`;
|
|
19
20
|
useEffect(() => {
|
|
20
21
|
if (id === void 0) return;
|
|
21
22
|
let active = true;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/admin/endpoint-url.ts
|
|
2
|
+
/**
|
|
3
|
+
* The plugin endpoint's URL below the API route, without a doubled slash
|
|
4
|
+
* after the origin.
|
|
5
|
+
*/ const endpointUrl = (origin, apiRoute, endpointPath) => `${origin.replace(/\/+$/, "")}${apiRoute}${endpointPath}`;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { endpointUrl };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { endpointUrl } from "./endpoint-url.mjs";
|
|
2
3
|
import { buildSetupGuide } from "../api-keys/setup-guide.mjs";
|
|
3
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
5
|
import { CopyToClipboard, useConfig, useDocumentInfo, useFormFields } from "@payloadcms/ui";
|
|
@@ -48,7 +49,7 @@ const styles = {
|
|
|
48
49
|
const origin = useOrigin(config.serverURL);
|
|
49
50
|
if (id === void 0) return null;
|
|
50
51
|
const sections = buildSetupGuide({
|
|
51
|
-
endpointUrl:
|
|
52
|
+
endpointUrl: endpointUrl(origin, config.routes.api, endpointPath),
|
|
52
53
|
apiKey: asString(apiKey),
|
|
53
54
|
label: asString(label)
|
|
54
55
|
});
|
|
@@ -1,20 +1,17 @@
|
|
|
1
|
+
import { McpxEntityCapabilities } from "../types.mjs";
|
|
1
2
|
import "../options.mjs";
|
|
3
|
+
import "../capabilities.mjs";
|
|
2
4
|
//#region src/api-keys/capability-matrix.d.ts
|
|
3
5
|
/**
|
|
4
6
|
* One entity's row. The booleans say what the plugin config exposes, not what
|
|
5
7
|
* the key was granted. A `false` leaves its segment out of the row's controls.
|
|
6
8
|
*/
|
|
7
|
-
export interface CapabilityRow {
|
|
9
|
+
export interface CapabilityRow extends McpxEntityCapabilities {
|
|
8
10
|
fieldName: string;
|
|
9
11
|
/**
|
|
10
12
|
* The slug, which is what MCP clients send and what refusals name.
|
|
11
13
|
*/
|
|
12
14
|
slug: string;
|
|
13
|
-
read: boolean;
|
|
14
|
-
write: boolean;
|
|
15
|
-
publish: boolean;
|
|
16
|
-
delete: boolean;
|
|
17
|
-
deleteUnattended: boolean;
|
|
18
15
|
/**
|
|
19
16
|
* Writes go live immediately, as the entity keeps no drafts.
|
|
20
17
|
*/
|
|
@@ -1,62 +1,23 @@
|
|
|
1
|
-
import { CAPABILITIES_FIELD,
|
|
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
5
|
* description slot. It explains how to read the controls.
|
|
6
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.";
|
|
7
|
-
/**
|
|
8
|
-
* The operations a collection or global can expose. The generated checkboxes
|
|
9
|
-
* and the matrix segments share this wording. `requires` names the
|
|
10
|
-
* operation a grant depends on, which the server and the matrix both enforce.
|
|
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
|
-
requires: "read"
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
id: "publish",
|
|
25
|
-
label: "Publish",
|
|
26
|
-
description: "Promote the current draft to what the public sees.",
|
|
27
|
-
requires: "write"
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
id: "delete",
|
|
31
|
-
label: "Delete",
|
|
32
|
-
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.",
|
|
33
|
-
requires: "read"
|
|
34
|
-
}
|
|
35
|
-
];
|
|
36
|
-
/**
|
|
37
|
-
* Stored as a checkbox of its own but drawn as the approval shield of the Delete
|
|
38
|
-
* control.
|
|
39
|
-
*/ const DELETE_UNATTENDED = {
|
|
40
|
-
id: "deleteUnattended",
|
|
41
|
-
label: "Trash directly",
|
|
42
|
-
description: "Move documents to trash without asking this key's user.",
|
|
43
|
-
requires: "delete"
|
|
44
|
-
};
|
|
45
|
-
/**
|
|
46
|
-
* Every operation with a stored checkbox: the capability operations plus
|
|
47
|
-
* the unattended delete.
|
|
48
|
-
*/ const STORED_OPERATIONS = [...CAPABILITY_OPERATIONS, DELETE_UNATTENDED];
|
|
49
7
|
const toRow = (entity) => ({
|
|
50
8
|
fieldName: entity.fieldName,
|
|
51
9
|
slug: entity.slug,
|
|
52
10
|
read: entity.read,
|
|
53
|
-
write:
|
|
54
|
-
publish:
|
|
11
|
+
write: entity.write,
|
|
12
|
+
publish: entity.publish,
|
|
55
13
|
delete: entity.delete,
|
|
56
14
|
deleteUnattended: entity.deleteUnattended,
|
|
57
|
-
...
|
|
15
|
+
...entity.liveWrite ? { live: true } : {}
|
|
58
16
|
});
|
|
59
17
|
/**
|
|
18
|
+
* The operations a row exposes, in stored order.
|
|
19
|
+
*/ const rowOperations = (row) => STORED_OPERATIONS.filter((operation) => row[operation.id]);
|
|
20
|
+
/**
|
|
60
21
|
* The single description of what the config exposes. `createCapabilityFields`
|
|
61
22
|
* and the admin component both draw from it, so a control cannot appear without a
|
|
62
23
|
* field behind it.
|
|
@@ -72,6 +33,6 @@ const cellPath = (basePath, namespace, fieldName, operation) => `${basePath}.${n
|
|
|
72
33
|
const toolPath = (basePath, name) => `${basePath}.tools.${name}`;
|
|
73
34
|
/**
|
|
74
35
|
* Every path the matrix can address, in a stable order.
|
|
75
|
-
*/ const capabilityPaths = (matrix, basePath = CAPABILITIES_FIELD) => [...["collections", "globals"].flatMap((namespace) => matrix[namespace].flatMap((row) =>
|
|
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))];
|
|
76
37
|
//#endregion
|
|
77
|
-
export { CAPABILITIES_DESCRIPTION,
|
|
38
|
+
export { CAPABILITIES_DESCRIPTION, capabilityPaths, cellPath, createCapabilityMatrix, rowOperations, toolPath };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createCapabilityFields, createConfirmationFields, createKeyFields, withKeyTabs } 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
|
/**
|
|
@@ -42,10 +42,7 @@ import { generateApiKey, hashApiKey } from "./key.mjs";
|
|
|
42
42
|
type: "relationship",
|
|
43
43
|
relationTo: userCollection,
|
|
44
44
|
required: true,
|
|
45
|
-
access:
|
|
46
|
-
create: () => false,
|
|
47
|
-
update: () => false
|
|
48
|
-
},
|
|
45
|
+
access: SERVER_ONLY,
|
|
49
46
|
defaultValue: ({ req }) => isUser({ req }) ? req.user?.id : void 0,
|
|
50
47
|
admin: { description: "The user this key acts as." }
|
|
51
48
|
}, ...createKeyFields()], createCapabilityFields(options), options)]
|
package/dist/api-keys/fields.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CAPABILITIES_FIELD } from "../capabilities.mjs";
|
|
2
|
-
import { CAPABILITIES_DESCRIPTION,
|
|
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
|
-
|
|
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>`."
|
|
@@ -83,18 +94,7 @@ const SETUP_GUIDE_FIELD = "setupGuide";
|
|
|
83
94
|
* a collection exposes `delete`. The component renders nothing while none is
|
|
84
95
|
* pending and never touches the form, so deciding does not mark the key
|
|
85
96
|
* 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
|
-
}] : [];
|
|
97
|
+
*/ const createConfirmationFields = (options) => options.confirmations ? [adminField("confirmations", "McpxConfirmations", options)] : [];
|
|
98
98
|
/**
|
|
99
99
|
* Wraps the key fields, the capabilities and the setup guide in unnamed tabs,
|
|
100
100
|
* so wide tables and snippets get the full form width. Named tabs would nest
|
|
@@ -114,18 +114,7 @@ const SETUP_GUIDE_FIELD = "setupGuide";
|
|
|
114
114
|
if (options.setupGuide) tabs.push({
|
|
115
115
|
label: "Connect a client",
|
|
116
116
|
admin: { condition: (_data, _siblingData, { operation }) => operation === "update" },
|
|
117
|
-
fields: [
|
|
118
|
-
name: SETUP_GUIDE_FIELD,
|
|
119
|
-
type: "ui",
|
|
120
|
-
admin: {
|
|
121
|
-
disableListColumn: true,
|
|
122
|
-
components: { Field: {
|
|
123
|
-
path: "@abinnovision/payloadcms-mcpx/admin",
|
|
124
|
-
exportName: "McpxSetupGuide",
|
|
125
|
-
clientProps: { endpointPath: options.endpointPath }
|
|
126
|
-
} }
|
|
127
|
-
}
|
|
128
|
-
}]
|
|
117
|
+
fields: [adminField("setupGuide", "McpxSetupGuide", options)]
|
|
129
118
|
});
|
|
130
119
|
return [{
|
|
131
120
|
type: "tabs",
|
|
@@ -147,28 +136,17 @@ const SETUP_GUIDE_FIELD = "setupGuide";
|
|
|
147
136
|
name: row.fieldName,
|
|
148
137
|
type: "group",
|
|
149
138
|
label: row.slug,
|
|
150
|
-
fields:
|
|
139
|
+
fields: rowOperations(row).map((operation) => checkbox(operation.id, operation.description))
|
|
151
140
|
}));
|
|
152
|
-
const collectionGroups = entityGroups(matrix.collections);
|
|
153
|
-
const globalGroups = entityGroups(matrix.globals);
|
|
154
|
-
const toolCheckboxes = matrix.tools.map((tool) => checkbox(tool.name, tool.description));
|
|
155
141
|
const groups = [
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
fields: globalGroups
|
|
165
|
-
}] : [],
|
|
166
|
-
...toolCheckboxes.length > 0 ? [{
|
|
167
|
-
name: "tools",
|
|
168
|
-
type: "group",
|
|
169
|
-
fields: toolCheckboxes
|
|
170
|
-
}] : []
|
|
171
|
-
];
|
|
142
|
+
["collections", entityGroups(matrix.collections)],
|
|
143
|
+
["globals", entityGroups(matrix.globals)],
|
|
144
|
+
["tools", matrix.tools.map((tool) => checkbox(tool.name, tool.description))]
|
|
145
|
+
].filter(([, fields]) => fields.length > 0).map(([name, fields]) => ({
|
|
146
|
+
name,
|
|
147
|
+
type: "group",
|
|
148
|
+
fields
|
|
149
|
+
}));
|
|
172
150
|
if (groups.length === 0) return [];
|
|
173
151
|
return [{
|
|
174
152
|
name: CAPABILITIES_FIELD,
|
|
@@ -188,4 +166,4 @@ const SETUP_GUIDE_FIELD = "setupGuide";
|
|
|
188
166
|
}];
|
|
189
167
|
};
|
|
190
168
|
//#endregion
|
|
191
|
-
export { createCapabilityFields, createConfirmationFields, createKeyFields, withKeyTabs };
|
|
169
|
+
export { SERVER_ONLY, createCapabilityFields, createConfirmationFields, createKeyFields, withKeyTabs };
|