@abinnovision/payloadcms-mcpx 2.0.0-beta.18 → 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/README.md +2 -2
- package/dist/admin/capability-matrix.d.mts +2 -1
- package/dist/admin/capability-matrix.mjs +291 -340
- 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 +4 -7
- package/dist/api-keys/capability-matrix.mjs +12 -51
- package/dist/api-keys/collection.mjs +10 -17
- package/dist/api-keys/confirmation-view.mjs +2 -1
- package/dist/api-keys/fields.mjs +53 -69
- 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,17 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { hashApiKey } from "../api-keys/key.mjs";
|
|
3
|
-
import { GRANT_ID, NO_REQ, expOf, issueRow, rowsWith } from "./grant.mjs";
|
|
4
|
-
import { ValidationError } from "payload";
|
|
1
|
+
import { NO_REQ, ROW_ID, claimRow, expOf, issueRow, rowHandle, rowPrefixes, rowsWith } from "./grant.mjs";
|
|
5
2
|
//#region src/grants/confirmation.ts
|
|
6
3
|
const CONFIRMATION_TTL_MS = 9e5;
|
|
7
4
|
const HANDLE = /^[\da-f]{64}$/;
|
|
8
|
-
const
|
|
9
|
-
const usedPrefix = (apiKeyId) => `mcpx-confirmation-used:${String(apiKeyId)}:`;
|
|
10
|
-
const asConfirmation = (row, now) => row && isPlainObject(row.data) && expOf(row.data) > now ? row.data : void 0;
|
|
11
|
-
/**
|
|
12
|
-
* The handle the admin panel uses for the entry of `id`: its HMAC, which is
|
|
13
|
-
* also what the row key holds.
|
|
14
|
-
*/ const confirmationHandle = (payload, id) => hashApiKey(payload.secret, id);
|
|
5
|
+
const asConfirmation = (row, now) => row && expOf(row.data) > now ? row.data : void 0;
|
|
15
6
|
/**
|
|
16
7
|
* Stores a pending call and returns its id. A key may hold at most
|
|
17
8
|
* {@link CONFIRMATIONS_PER_KEY} calls.
|
|
@@ -21,8 +12,7 @@ const asConfirmation = (row, now) => row && isPlainObject(row.data) && expOf(row
|
|
|
21
12
|
state: "pending"
|
|
22
13
|
};
|
|
23
14
|
return await issueRow(payload, slug, {
|
|
24
|
-
|
|
25
|
-
usedPrefix: usedPrefix(call.apiKeyId),
|
|
15
|
+
...rowPrefixes("confirmation", call.apiKeyId),
|
|
26
16
|
cap: 50,
|
|
27
17
|
capMessage: `This key already has ${String(50)} calls waiting for approval. Run or abandon them first; each expires 15 minutes after it was requested.`,
|
|
28
18
|
ttlMs: CONFIRMATION_TTL_MS,
|
|
@@ -33,40 +23,19 @@ const asConfirmation = (row, now) => row && isPlainObject(row.data) && expOf(row
|
|
|
33
23
|
* The unexpired call of `apiKeyId` with `id`, without claiming it. A call of
|
|
34
24
|
* another key is `undefined`, as is a malformed id, before any database read.
|
|
35
25
|
*/ const readConfirmation = async (payload, slug, apiKeyId, id) => {
|
|
36
|
-
if (!
|
|
37
|
-
const key = `${
|
|
26
|
+
if (!ROW_ID.test(id)) return;
|
|
27
|
+
const key = `${rowPrefixes("confirmation", apiKeyId).prefix}${rowHandle(payload, id)}`;
|
|
38
28
|
const rows = await rowsWith(payload, slug, [key]);
|
|
39
29
|
return asConfirmation(rows.find((row) => row.key === key), Date.now());
|
|
40
30
|
};
|
|
41
31
|
/**
|
|
42
|
-
* Marks the call used and removes it.
|
|
43
|
-
*
|
|
44
|
-
*/ const claimConfirmation = async (payload, slug, apiKeyId, id, exp) =>
|
|
45
|
-
const handle = confirmationHandle(payload, id);
|
|
46
|
-
try {
|
|
47
|
-
await payload.db.create({
|
|
48
|
-
collection: slug,
|
|
49
|
-
data: {
|
|
50
|
-
key: `${usedPrefix(apiKeyId)}${handle}`,
|
|
51
|
-
data: { exp }
|
|
52
|
-
},
|
|
53
|
-
req: NO_REQ
|
|
54
|
-
});
|
|
55
|
-
} catch (error) {
|
|
56
|
-
if (error instanceof ValidationError) return false;
|
|
57
|
-
throw error;
|
|
58
|
-
}
|
|
59
|
-
await payload.db.deleteOne({
|
|
60
|
-
collection: slug,
|
|
61
|
-
where: { key: { equals: `${entryPrefix(apiKeyId)}${handle}` } },
|
|
62
|
-
req: NO_REQ
|
|
63
|
-
});
|
|
64
|
-
return true;
|
|
65
|
-
};
|
|
32
|
+
* Marks the call used and removes it. Of two concurrent claims exactly one
|
|
33
|
+
* returns `true`.
|
|
34
|
+
*/ const claimConfirmation = async (payload, slug, apiKeyId, id, exp) => await claimRow(payload, slug, rowPrefixes("confirmation", apiKeyId), rowHandle(payload, id), exp);
|
|
66
35
|
/**
|
|
67
36
|
* Every unexpired call of `apiKeyId` with its handle, oldest first.
|
|
68
37
|
*/ const listConfirmations = async (payload, slug, apiKeyId) => {
|
|
69
|
-
const prefix =
|
|
38
|
+
const { prefix } = rowPrefixes("confirmation", apiKeyId);
|
|
70
39
|
const now = Date.now();
|
|
71
40
|
return (await rowsWith(payload, slug, [prefix])).flatMap((row) => {
|
|
72
41
|
const confirmation = asConfirmation(row, now);
|
|
@@ -81,7 +50,7 @@ const asConfirmation = (row, now) => row && isPlainObject(row.data) && expOf(row
|
|
|
81
50
|
* returns the handles it changed. A call that was decided, run or expired is
|
|
82
51
|
* left alone and not returned.
|
|
83
52
|
*/ const decideConfirmations = async (payload, slug, apiKeyId, handles, state) => {
|
|
84
|
-
const prefix =
|
|
53
|
+
const { prefix } = rowPrefixes("confirmation", apiKeyId);
|
|
85
54
|
const keys = new Set(handles.filter((handle) => HANDLE.test(handle)).map((handle) => `${prefix}${handle}`));
|
|
86
55
|
if (keys.size === 0) return [];
|
|
87
56
|
const now = Date.now();
|
|
@@ -103,4 +72,4 @@ const asConfirmation = (row, now) => row && isPlainObject(row.data) && expOf(row
|
|
|
103
72
|
return decided;
|
|
104
73
|
};
|
|
105
74
|
//#endregion
|
|
106
|
-
export { claimConfirmation,
|
|
75
|
+
export { claimConfirmation, decideConfirmations, issueConfirmation, listConfirmations, readConfirmation };
|
package/dist/grants/grant.mjs
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { isPlainObject } from "../guards.mjs";
|
|
2
|
-
import { hashApiKey } from "../api-keys/key.mjs";
|
|
2
|
+
import { generateApiKey, hashApiKey } from "../api-keys/key.mjs";
|
|
3
3
|
import { APIError, ValidationError } from "payload";
|
|
4
|
-
import crypto from "node:crypto";
|
|
5
4
|
//#region src/grants/grant.ts
|
|
6
5
|
const GRANT_TTL_MS = 3e5;
|
|
7
6
|
const GRANTS_PER_KEY = 10;
|
|
8
|
-
const
|
|
7
|
+
const ROW_ID = /^[\w-]{43}$/;
|
|
9
8
|
const NO_REQ = {};
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The key prefixes of a kind of row: waiting rows and the markers of used
|
|
11
|
+
* ones.
|
|
12
|
+
*/ const rowPrefixes = (kind, apiKeyId) => ({
|
|
13
|
+
prefix: `mcpx-${kind}:${String(apiKeyId)}:`,
|
|
14
|
+
usedPrefix: `mcpx-${kind}-used:${String(apiKeyId)}:`
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* What a row is addressed by: the HMAC of its id, which the row key ends in.
|
|
18
|
+
*/ const rowHandle = (payload, id) => hashApiKey(payload.secret, id);
|
|
12
19
|
const expOf = (data) => isPlainObject(data) && typeof data["exp"] === "number" ? data["exp"] : 0;
|
|
13
20
|
/**
|
|
14
21
|
* The KV collection slug, when Payload's database KV adapter backs it with a
|
|
@@ -49,12 +56,12 @@ const expOf = (data) => isPlainObject(data) && typeof data["exp"] === "number" ?
|
|
|
49
56
|
req: NO_REQ
|
|
50
57
|
});
|
|
51
58
|
if (rows.filter((row) => row.key.startsWith(args.prefix) && expOf(row.data) > now).length >= args.cap) throw new APIError(args.capMessage, 429);
|
|
52
|
-
const id =
|
|
59
|
+
const id = generateApiKey();
|
|
53
60
|
const exp = now + args.ttlMs;
|
|
54
61
|
await payload.db.create({
|
|
55
62
|
collection: slug,
|
|
56
63
|
data: {
|
|
57
|
-
key: `${args.prefix}${
|
|
64
|
+
key: `${args.prefix}${rowHandle(payload, id)}`,
|
|
58
65
|
data: {
|
|
59
66
|
...args.data,
|
|
60
67
|
exp
|
|
@@ -68,52 +75,63 @@ const expOf = (data) => isPlainObject(data) && typeof data["exp"] === "number" ?
|
|
|
68
75
|
};
|
|
69
76
|
};
|
|
70
77
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*/ const
|
|
74
|
-
const { id, exp } = await issueRow(payload, slug, {
|
|
75
|
-
prefix: grantPrefix(grant.apiKeyId),
|
|
76
|
-
usedPrefix: usedPrefix(grant.apiKeyId),
|
|
77
|
-
cap: GRANTS_PER_KEY,
|
|
78
|
-
capMessage: `This key already has ${String(GRANTS_PER_KEY)} uploads or downloads waiting. Use or abandon them first; each expires 5 minutes after it was issued.`,
|
|
79
|
-
ttlMs: GRANT_TTL_MS,
|
|
80
|
-
data: grant
|
|
81
|
-
});
|
|
82
|
-
return {
|
|
83
|
-
grantId: id,
|
|
84
|
-
exp
|
|
85
|
-
};
|
|
86
|
-
};
|
|
87
|
-
/**
|
|
88
|
-
* Loads the grant for `grantId` and marks it used, or returns `undefined` for
|
|
89
|
-
* a malformed, unknown, expired or used one. The marker is a unique-key
|
|
90
|
-
* insert, so of two concurrent claims exactly one succeeds. A malformed id
|
|
91
|
-
* is refused before any database read.
|
|
92
|
-
*/ const claimGrant = async (payload, slug, grantId) => {
|
|
93
|
-
if (!GRANT_ID.test(grantId)) return;
|
|
94
|
-
const hash = hashApiKey(payload.secret, grantId);
|
|
95
|
-
const row = (await findRows(payload, slug, [hash])).find((candidate) => candidate.key.startsWith("mcpx-grant:") && candidate.key.endsWith(`:${hash}`));
|
|
96
|
-
const grant = row?.data;
|
|
97
|
-
if (!row || !isPlainObject(grant) || row.key !== `${grantPrefix(grant.apiKeyId)}${hash}` || expOf(grant) <= Date.now()) return;
|
|
78
|
+
* Marks the row under `handle` used and removes it. The marker is a
|
|
79
|
+
* unique-key insert, so of two concurrent claims exactly one returns `true`.
|
|
80
|
+
*/ const claimRow = async (payload, slug, prefixes, handle, exp) => {
|
|
98
81
|
try {
|
|
99
82
|
await payload.db.create({
|
|
100
83
|
collection: slug,
|
|
101
84
|
data: {
|
|
102
|
-
key: `${usedPrefix
|
|
103
|
-
data: { exp
|
|
85
|
+
key: `${prefixes.usedPrefix}${handle}`,
|
|
86
|
+
data: { exp }
|
|
104
87
|
},
|
|
105
88
|
req: NO_REQ
|
|
106
89
|
});
|
|
107
90
|
} catch (error) {
|
|
108
|
-
if (error instanceof ValidationError) return;
|
|
91
|
+
if (error instanceof ValidationError) return false;
|
|
109
92
|
throw error;
|
|
110
93
|
}
|
|
111
94
|
await payload.db.deleteOne({
|
|
112
95
|
collection: slug,
|
|
113
|
-
where: { key: { equals:
|
|
96
|
+
where: { key: { equals: `${prefixes.prefix}${handle}` } },
|
|
114
97
|
req: NO_REQ
|
|
115
98
|
});
|
|
116
|
-
return
|
|
99
|
+
return true;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* The KV slug and key id of the request, which a call that stores a grant or
|
|
103
|
+
* a confirmation needs. Throws where the endpoint would not offer one.
|
|
104
|
+
*/ const grantContext = (req) => {
|
|
105
|
+
const slug = grantKvSlug(req.payload.config);
|
|
106
|
+
const apiKeyId = req.context.mcpx?.apiKeyId;
|
|
107
|
+
if (slug === void 0 || apiKeyId === void 0) throw new Error("A grant or confirmation needs a database KV and a key.");
|
|
108
|
+
return {
|
|
109
|
+
slug,
|
|
110
|
+
apiKeyId
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Stores a grant and returns its id. A key may hold at most
|
|
115
|
+
* {@link GRANTS_PER_KEY} grants.
|
|
116
|
+
*/ const issueGrant = async (payload, slug, grant) => await issueRow(payload, slug, {
|
|
117
|
+
...rowPrefixes("grant", grant.apiKeyId),
|
|
118
|
+
cap: GRANTS_PER_KEY,
|
|
119
|
+
capMessage: `This key already has ${String(GRANTS_PER_KEY)} uploads or downloads waiting. Use or abandon them first; each expires 5 minutes after it was issued.`,
|
|
120
|
+
ttlMs: GRANT_TTL_MS,
|
|
121
|
+
data: grant
|
|
122
|
+
});
|
|
123
|
+
/**
|
|
124
|
+
* Loads the grant for `grantId` and marks it used, or returns `undefined` for
|
|
125
|
+
* a malformed, unknown, expired or used one. A malformed id is refused before
|
|
126
|
+
* any database read.
|
|
127
|
+
*/ const claimGrant = async (payload, slug, grantId) => {
|
|
128
|
+
if (!ROW_ID.test(grantId)) return;
|
|
129
|
+
const handle = rowHandle(payload, grantId);
|
|
130
|
+
const row = (await findRows(payload, slug, [handle])).find((candidate) => candidate.key.startsWith("mcpx-grant:") && candidate.key.endsWith(`:${handle}`));
|
|
131
|
+
const grant = row?.data;
|
|
132
|
+
if (!row || !isPlainObject(grant) || expOf(grant) <= Date.now()) return;
|
|
133
|
+
const prefixes = rowPrefixes("grant", grant.apiKeyId);
|
|
134
|
+
return row.key === `${prefixes.prefix}${handle}` && await claimRow(payload, slug, prefixes, handle, grant.exp) ? grant : void 0;
|
|
117
135
|
};
|
|
118
136
|
//#endregion
|
|
119
|
-
export {
|
|
137
|
+
export { NO_REQ, ROW_ID, claimGrant, claimRow, expOf, grantContext, grantKvSlug, issueGrant, issueRow, rowHandle, rowPrefixes, rowsWith };
|
package/dist/guards.mjs
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
//#region src/guards.ts
|
|
2
2
|
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
3
|
/**
|
|
4
|
+
* A property of a plain object, `undefined` for any other value.
|
|
5
|
+
*/ const propOf = (value, key) => isPlainObject(value) ? value[key] : void 0;
|
|
6
|
+
/**
|
|
4
7
|
* An own property only, so a client-chosen key never reads the prototype.
|
|
5
8
|
*/ const ownValue = (record, key) => record !== void 0 && Object.hasOwn(record, key) ? record[key] : void 0;
|
|
9
|
+
/**
|
|
10
|
+
* The entries whose value is not `undefined`, for spreading optional arguments
|
|
11
|
+
* into a call.
|
|
12
|
+
*/ const definedProps = (props) => Object.fromEntries(Object.entries(props).filter(([, value]) => value !== void 0));
|
|
6
13
|
//#endregion
|
|
7
|
-
export { isPlainObject, ownValue };
|
|
14
|
+
export { definedProps, isPlainObject, ownValue, propOf };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { DocumentId } from "./entity.mjs";
|
|
2
|
-
import { McpxAnyTool, McpxAuthResult,
|
|
2
|
+
import { McpxAnyTool, McpxAuthResult, McpxCollectionOptions, McpxEntityCapabilities, McpxExposedEntity, McpxGlobalOptions, McpxPluginOptions, McpxRequestContext, McpxResolvedCapabilities, McpxScopeSlugs, McpxTool, McpxToolExtra, McpxToolScope } from "./types.mjs";
|
|
3
3
|
import { defineMcpxTool } from "./define-tool.mjs";
|
|
4
4
|
import { mcpxPlugin } from "./plugin.mjs";
|
|
5
5
|
import { isMcpxRequest } from "./request.mjs";
|
|
6
6
|
import { errorResult, jsonResult } from "./result.mjs";
|
|
7
7
|
import { mcpxReadRequest } from "./tools/read-request.mjs";
|
|
8
|
-
export { type McpxAnyTool, type McpxAuthResult, type
|
|
8
|
+
export { type McpxAnyTool, type McpxAuthResult, type McpxCollectionOptions, type DocumentId as McpxDocumentId, type McpxEntityCapabilities, type McpxExposedEntity, type McpxGlobalOptions, type McpxPluginOptions, type McpxRequestContext, type McpxResolvedCapabilities, type McpxScopeSlugs, type McpxTool, type McpxToolExtra, type McpxToolScope, defineMcpxTool, errorResult, isMcpxRequest, jsonResult, mcpxPlugin, mcpxReadRequest };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineMcpxTool } from "./define-tool.mjs";
|
|
2
|
+
import { isMcpxRequest } from "./request.mjs";
|
|
2
3
|
import { errorResult, jsonResult } from "./result.mjs";
|
|
3
4
|
import { mcpxReadRequest } from "./tools/read-request.mjs";
|
|
4
|
-
import { isMcpxRequest } from "./request.mjs";
|
|
5
5
|
import { mcpxPlugin } from "./plugin.mjs";
|
|
6
6
|
export { defineMcpxTool, errorResult, isMcpxRequest, jsonResult, mcpxPlugin, mcpxReadRequest };
|
package/dist/options.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { isPlainObject } from "./guards.mjs";
|
|
2
2
|
import "./version.mjs";
|
|
3
|
-
import { BUILTIN_TOOL_NAMES } from "./builtin-tool-names.mjs";
|
|
4
3
|
import { InvalidConfiguration } from "payload";
|
|
5
4
|
import { hasDraftsEnabled, hasLocalizeStatusEnabled } from "payload/shared";
|
|
6
5
|
//#region src/options.ts
|
|
@@ -15,10 +14,10 @@ const fail = (message) => {
|
|
|
15
14
|
/**
|
|
16
15
|
* The same transform the stock MCP plugin uses to derive field names.
|
|
17
16
|
*/ const toCamelCase = (value) => value.replace(/[-_\s]+(.)?/g, (_, char) => char ? char.toUpperCase() : "").replace(/^(.)/, (_, char) => char.toLowerCase());
|
|
18
|
-
const assertExposable = (
|
|
19
|
-
const { slug } =
|
|
20
|
-
if (slug
|
|
21
|
-
if (
|
|
17
|
+
const assertExposable = (kind, config, apiKeysSlug) => {
|
|
18
|
+
const { slug } = config;
|
|
19
|
+
if (slug.startsWith("payload-") || kind === "Collection" && slug === apiKeysSlug) fail(`${kind} "${slug}" cannot be exposed.`);
|
|
20
|
+
if ("auth" in config && config.auth) fail(`Auth collection "${slug}" cannot be exposed. Its documents carry credentials.`);
|
|
22
21
|
};
|
|
23
22
|
const OPTION_NAMES = [
|
|
24
23
|
"read",
|
|
@@ -40,8 +39,8 @@ const readFlag = (kind, slug, name, value) => {
|
|
|
40
39
|
const moved = name === "write" && typeof value === "string" ? REMOVED_WRITE_MODES[value] : void 0;
|
|
41
40
|
return fail(`${kind} "${slug}" has ${name}: ${JSON.stringify(value)}. ${moved ?? "Use true or false."}`);
|
|
42
41
|
};
|
|
43
|
-
const assertWritable = (kind, config,
|
|
44
|
-
if (
|
|
42
|
+
const assertWritable = (kind, config, publish) => {
|
|
43
|
+
if (publish && hasLocalizeStatusEnabled(config)) fail(`${kind} "${config.slug}" has versions.drafts.localizeStatus enabled, which live writes do not support yet. Set publish: false or write: false.`);
|
|
45
44
|
};
|
|
46
45
|
const normalizeDelete = (kind, config, value) => {
|
|
47
46
|
if (value !== "unattended") return {
|
|
@@ -75,68 +74,47 @@ const normalizeCapabilities = (kind, config, raw) => {
|
|
|
75
74
|
if (!read && deleting.delete) fail(`${kind} "${slug}" has read: false but delete on. Deleting needs read.`);
|
|
76
75
|
return {
|
|
77
76
|
read,
|
|
78
|
-
write
|
|
77
|
+
write,
|
|
78
|
+
publish: write && publish !== false && hasDrafts,
|
|
79
|
+
liveWrite: write && !hasDrafts,
|
|
79
80
|
hasDrafts,
|
|
80
81
|
hasVersions: read && Boolean(config.versions),
|
|
81
82
|
...deleting
|
|
82
83
|
};
|
|
83
84
|
};
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
};
|
|
87
|
-
const normalizeCollections = (config, options, apiKeysSlug) => {
|
|
88
|
-
const collections = config.collections ?? [];
|
|
85
|
+
const bySlug = (configs) => new Map(configs.map((config) => [config.slug, config]));
|
|
86
|
+
const normalizeEntities = (kind, configs, entries, apiKeysSlug) => {
|
|
89
87
|
const fieldNames = /* @__PURE__ */ new Set();
|
|
90
|
-
return Object.entries(
|
|
88
|
+
return Object.entries(entries).flatMap(([slug, raw]) => {
|
|
91
89
|
if (raw === void 0) return [];
|
|
92
|
-
const
|
|
93
|
-
if (!
|
|
94
|
-
assertExposable(
|
|
90
|
+
const config = configs.get(slug);
|
|
91
|
+
if (!config) return fail(`Exposed ${kind.toLowerCase()} "${slug}" does not exist.`);
|
|
92
|
+
assertExposable(kind, config, apiKeysSlug);
|
|
95
93
|
const normalized = {
|
|
96
94
|
slug,
|
|
97
|
-
...normalizeCapabilities(
|
|
98
|
-
isUpload: Boolean(
|
|
95
|
+
...normalizeCapabilities(kind, config, raw),
|
|
96
|
+
isUpload: "upload" in config && Boolean(config.upload),
|
|
99
97
|
fieldName: toCamelCase(slug)
|
|
100
98
|
};
|
|
101
|
-
if (normalized.write
|
|
102
|
-
if (
|
|
103
|
-
assertWritable(
|
|
99
|
+
if (normalized.write) {
|
|
100
|
+
if ("timestamps" in config && config.timestamps === false) fail(`Collection "${slug}" has timestamps disabled, which write tools need for concurrency checks.`);
|
|
101
|
+
assertWritable(kind, config, normalized.publish);
|
|
104
102
|
}
|
|
105
|
-
if (fieldNames.has(normalized.fieldName)) fail(
|
|
106
|
-
fieldNames.add(normalized.fieldName);
|
|
107
|
-
return [normalized];
|
|
108
|
-
});
|
|
109
|
-
};
|
|
110
|
-
const normalizeGlobals = (config, options) => {
|
|
111
|
-
const globals = config.globals ?? [];
|
|
112
|
-
const fieldNames = /* @__PURE__ */ new Set();
|
|
113
|
-
return Object.entries(options.globals ?? {}).flatMap(([slug, raw]) => {
|
|
114
|
-
if (raw === void 0) return [];
|
|
115
|
-
const global = globals.find((candidate) => candidate.slug === slug);
|
|
116
|
-
if (!global) return fail(`Exposed global "${slug}" does not exist.`);
|
|
117
|
-
assertGlobalExposable(global);
|
|
118
|
-
const normalized = {
|
|
119
|
-
slug,
|
|
120
|
-
...normalizeCapabilities("Global", global, raw),
|
|
121
|
-
isUpload: false,
|
|
122
|
-
fieldName: toCamelCase(slug)
|
|
123
|
-
};
|
|
124
|
-
if (normalized.write !== false) assertWritable("Global", global, normalized.write);
|
|
125
|
-
if (fieldNames.has(normalized.fieldName)) fail(`Global "${slug}" maps to capability field "${normalized.fieldName}", which another exposed global already uses.`);
|
|
103
|
+
if (fieldNames.has(normalized.fieldName)) fail(`${kind} "${slug}" maps to capability field "${normalized.fieldName}", which another exposed ${kind.toLowerCase()} already uses.`);
|
|
126
104
|
fieldNames.add(normalized.fieldName);
|
|
127
105
|
return [normalized];
|
|
128
106
|
});
|
|
129
107
|
};
|
|
130
|
-
const assertUserCollection = (
|
|
131
|
-
const collection =
|
|
108
|
+
const assertUserCollection = (collections, slug) => {
|
|
109
|
+
const collection = collections.get(slug);
|
|
132
110
|
if (!collection) fail(`User collection "${slug}" does not exist.`);
|
|
133
|
-
else if (!collection.auth) fail(`User collection "${slug}" is not an auth collection.`);
|
|
111
|
+
else if (!("auth" in collection && collection.auth)) fail(`User collection "${slug}" is not an auth collection.`);
|
|
134
112
|
};
|
|
135
|
-
const assertTools = (tools) => {
|
|
113
|
+
const assertTools = (tools, reserved) => {
|
|
136
114
|
const names = /* @__PURE__ */ new Set();
|
|
137
115
|
for (const tool of tools) {
|
|
138
116
|
if (!TOOL_NAME_PATTERN.test(tool.name)) fail(`Tool name "${tool.name}" must match ${String(TOOL_NAME_PATTERN)}.`);
|
|
139
|
-
if (
|
|
117
|
+
if (reserved.includes(tool.name)) fail(`Tool name "${tool.name}" is reserved for a builtin tool.`);
|
|
140
118
|
if (names.has(tool.name)) fail(`Tool name "${tool.name}" is used twice.`);
|
|
141
119
|
names.add(tool.name);
|
|
142
120
|
}
|
|
@@ -153,19 +131,21 @@ const normalizeLimits = (limits) => {
|
|
|
153
131
|
};
|
|
154
132
|
/**
|
|
155
133
|
* Every problem is an `InvalidConfiguration`, so it fails at startup.
|
|
156
|
-
|
|
134
|
+
* `reservedToolNames` are the builtin tool names custom tools must not reuse.
|
|
135
|
+
*/ const normalizeOptions = (config, options, reservedToolNames) => {
|
|
157
136
|
const apiKeysSlug = options.apiKeys?.slug ?? DEFAULT_API_KEYS_SLUG;
|
|
158
137
|
const userCollection = options.userCollection ?? config.admin?.user ?? "users";
|
|
159
|
-
|
|
160
|
-
|
|
138
|
+
const collectionConfigs = bySlug(config.collections ?? []);
|
|
139
|
+
if (collectionConfigs.has(apiKeysSlug)) fail(`API key collection slug "${apiKeysSlug}" is already taken.`);
|
|
140
|
+
assertUserCollection(collectionConfigs, userCollection);
|
|
161
141
|
const tools = options.tools ?? [];
|
|
162
|
-
assertTools(tools);
|
|
163
|
-
const collections =
|
|
142
|
+
assertTools(tools, reservedToolNames);
|
|
143
|
+
const collections = normalizeEntities("Collection", collectionConfigs, options.collections, apiKeysSlug);
|
|
164
144
|
const confirmations = collections.some((entity) => entity.delete);
|
|
165
145
|
if (confirmations && options.auth?.resolve) fail("delete needs approval on the API key document, which a custom auth.resolve does not support. Remove delete: true or auth.resolve.");
|
|
166
146
|
return {
|
|
167
147
|
collections,
|
|
168
|
-
globals:
|
|
148
|
+
globals: normalizeEntities("Global", bySlug(config.globals ?? []), options.globals ?? {}, apiKeysSlug),
|
|
169
149
|
userCollection,
|
|
170
150
|
apiKeysSlug,
|
|
171
151
|
endpointPath: options.endpoint?.path ?? DEFAULT_ENDPOINT_PATH,
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createApiKeysCollection } from "./api-keys/collection.mjs";
|
|
2
|
+
import { BUILTIN_TOOLS } from "./tools/builtin.mjs";
|
|
2
3
|
import { confirmationEndpoints } from "./endpoint/confirmations.mjs";
|
|
3
4
|
import { createMcpxHandler, methodNotAllowed } from "./endpoint/handler.mjs";
|
|
4
5
|
import { createDownloadHandler, createUploadHandler } from "./endpoint/upload.mjs";
|
|
@@ -14,7 +15,7 @@ import { definePlugin } from "payload";
|
|
|
14
15
|
slug: "@abinnovision/payloadcms-mcpx",
|
|
15
16
|
order: 100,
|
|
16
17
|
plugin: ({ config, plugins: _plugins, ...options }) => {
|
|
17
|
-
const normalized = normalizeOptions(config, options);
|
|
18
|
+
const normalized = normalizeOptions(config, options, BUILTIN_TOOLS.map((tool) => tool.name));
|
|
18
19
|
const apiKeys = createApiKeysCollection(normalized);
|
|
19
20
|
const apiKeysCollection = options.apiKeys?.overrideCollection?.(apiKeys) ?? apiKeys;
|
|
20
21
|
return {
|
package/dist/request.mjs
CHANGED
|
@@ -3,5 +3,9 @@
|
|
|
3
3
|
* Whether `req` came through the MCP endpoint, which stamps `req.context.mcpx`.
|
|
4
4
|
* The stamp follows `req` into every local API call, including custom tools.
|
|
5
5
|
*/ const isMcpxRequest = (req) => req.context.mcpx !== void 0;
|
|
6
|
+
/**
|
|
7
|
+
* The origin links to this server use: `serverURL` when set, else that of the
|
|
8
|
+
* MCP request.
|
|
9
|
+
*/ const publicOrigin = (req) => req.payload.config.serverURL || new URL(req.url ?? "").origin;
|
|
6
10
|
//#endregion
|
|
7
|
-
export { isMcpxRequest };
|
|
11
|
+
export { isMcpxRequest, publicOrigin };
|
package/dist/result.mjs
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
//#region src/result.ts
|
|
2
|
+
/**
|
|
3
|
+
* The JSON of a result's first text block. Other text comes back as `{ text }`.
|
|
4
|
+
*/ const parseResult = (result) => {
|
|
5
|
+
const [first] = result.content;
|
|
6
|
+
try {
|
|
7
|
+
return first?.type === "text" ? JSON.parse(first.text) : {};
|
|
8
|
+
} catch {
|
|
9
|
+
return { text: first?.type === "text" ? first.text : void 0 };
|
|
10
|
+
}
|
|
11
|
+
};
|
|
2
12
|
const jsonResult = (value) => ({ content: [{
|
|
3
13
|
type: "text",
|
|
4
14
|
text: JSON.stringify(value)
|
|
@@ -17,4 +27,4 @@ const jsonResult = (value) => ({ content: [{
|
|
|
17
27
|
isError: true
|
|
18
28
|
});
|
|
19
29
|
//#endregion
|
|
20
|
-
export { errorResult, jsonResult };
|
|
30
|
+
export { errorResult, jsonResult, parseResult };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isPlainObject } from "../guards.mjs";
|
|
2
|
-
import { RESERVED_FIELD_NAMES, ROW_KEYS, blockOf, blockSlugsOf,
|
|
2
|
+
import { RESERVED_FIELD_NAMES, ROW_KEYS, adminFlag, blockForRow, blockOf, blockSlugsOf, schemaOf } from "./walk.mjs";
|
|
3
3
|
//#region src/schema/admin-hidden.ts
|
|
4
4
|
const UPLOAD_BASE_FIELDS = /* @__PURE__ */ new Set([
|
|
5
5
|
"filename",
|
|
@@ -22,7 +22,7 @@ const stripFields = (config, fields, doc, exempt = NONE) => {
|
|
|
22
22
|
const result = { ...doc };
|
|
23
23
|
for (const field of fields) {
|
|
24
24
|
if (!("name" in field) || exempt.has(field.name) || isPayloadKey(field.name) || !Object.hasOwn(result, field.name)) continue;
|
|
25
|
-
if (
|
|
25
|
+
if (adminFlag(field, "hidden")) Reflect.deleteProperty(result, field.name);
|
|
26
26
|
else result[field.name] = stripValue(config, field, result[field.name]);
|
|
27
27
|
}
|
|
28
28
|
return result;
|
|
@@ -45,7 +45,7 @@ const stripValue = (config, field, value) => {
|
|
|
45
45
|
case "tab": return isPlainObject(value) ? stripFields(config, field.flattenedFields, value) : value;
|
|
46
46
|
case "array": return mapEach(value, (row) => isPlainObject(row) ? stripFields(config, field.flattenedFields, row) : row);
|
|
47
47
|
case "blocks": return mapEach(value, (row) => {
|
|
48
|
-
const block =
|
|
48
|
+
const block = blockForRow(config, field, row);
|
|
49
49
|
return block && isPlainObject(row) ? stripFields(config, block.flattenedFields, row) : row;
|
|
50
50
|
});
|
|
51
51
|
case "relationship":
|
|
@@ -76,7 +76,7 @@ const stripValue = (config, field, value) => {
|
|
|
76
76
|
const matches = fields.filter((field) => "name" in field && field.name === segment);
|
|
77
77
|
if (matches.length === 0) return locales.includes(segment) ? walk(fields, rest) : false;
|
|
78
78
|
return matches.some((field) => {
|
|
79
|
-
if (
|
|
79
|
+
if (adminFlag(field, "hidden")) return true;
|
|
80
80
|
if (field.type === "group" || field.type === "tab" || field.type === "array") return walk(field.flattenedFields, rest);
|
|
81
81
|
return field.type === "blocks" && blockSlugsOf(field).some((slug) => {
|
|
82
82
|
const block = blockOf(config, field, slug);
|
package/dist/schema/describe.mjs
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
|
+
import { SchemaError } from "./errors.mjs";
|
|
1
2
|
import { lexicalSubSchema, subSchemaNodeTypes } from "./lexical.mjs";
|
|
2
3
|
import { joinPath, splitPath } from "./path.mjs";
|
|
3
4
|
import { translateAny } from "../i18n.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import { SchemaError } from "./errors.mjs";
|
|
5
|
+
import { blockSlugsOf, describeFields, findFieldAt, longestMatch, requireBlock, schemaOf } from "./walk.mjs";
|
|
6
6
|
//#region src/schema/describe.ts
|
|
7
|
-
const matchSchemaSegments = (descriptors, remaining) => descriptors.map((descriptor) => splitPath(descriptor.path)).filter((parts) => parts.every((part, offset) => part === remaining[offset])).sort((left, right) => right.length - left.length)[0];
|
|
8
7
|
const stepThroughBlocks = ({ config, fields, match, remaining }) => {
|
|
9
|
-
const field =
|
|
8
|
+
const field = findFieldAt(fields, match, "blocks");
|
|
10
9
|
if (!field) throw new Error(`"${joinPath(match)}" could not be resolved.`);
|
|
11
10
|
const slug = remaining.at(match.length);
|
|
12
11
|
if (slug === void 0) throw new SchemaError(`"${joinPath(match)}" is a blocks field; append one of: ${blockSlugsOf(field).join(", ")}`);
|
|
13
|
-
const block = blockOf(config, field, slug);
|
|
14
|
-
if (!block) throw new SchemaError(`"${slug}" is not allowed at "${joinPath(match)}". Allowed: ${blockSlugsOf(field).join(", ")}`);
|
|
15
12
|
return {
|
|
16
13
|
blockType: slug,
|
|
17
|
-
fields:
|
|
14
|
+
fields: requireBlock(config, field, slug, joinPath(match)).flattenedFields,
|
|
18
15
|
rest: remaining.slice(match.length + 1)
|
|
19
16
|
};
|
|
20
17
|
};
|
|
21
18
|
const stepThroughLexical = ({ config, fields, match, remaining }) => {
|
|
22
|
-
const field =
|
|
19
|
+
const field = findFieldAt(fields, match, "richText");
|
|
23
20
|
if (!field) throw new Error(`"${joinPath(match)}" could not be resolved.`);
|
|
24
21
|
const available = subSchemaNodeTypes(field).join(", ") || "none";
|
|
25
22
|
const nodeType = remaining.at(match.length);
|
|
@@ -32,13 +29,10 @@ const stepThroughLexical = ({ config, fields, match, remaining }) => {
|
|
|
32
29
|
rest: remaining.slice(match.length + 1)
|
|
33
30
|
};
|
|
34
31
|
const slug = remaining.at(match.length + 1);
|
|
35
|
-
|
|
36
|
-
if (slug === void 0) throw new SchemaError(`"${reached}" selects a block; append one of: ${slugs}`);
|
|
37
|
-
const block = blockOf(config, sub.blocksField, slug);
|
|
38
|
-
if (!block) throw new SchemaError(`"${slug}" is not allowed at "${reached}". Allowed: ${slugs}`);
|
|
32
|
+
if (slug === void 0) throw new SchemaError(`"${reached}" selects a block; append one of: ${blockSlugsOf(sub.blocksField).join(", ")}`);
|
|
39
33
|
return {
|
|
40
34
|
blockType: slug,
|
|
41
|
-
fields:
|
|
35
|
+
fields: requireBlock(config, sub.blocksField, slug, reached).flattenedFields,
|
|
42
36
|
rest: remaining.slice(match.length + 2)
|
|
43
37
|
};
|
|
44
38
|
};
|
|
@@ -48,15 +42,15 @@ const fieldsAtSchemaPath = (config, target, schemaPath) => {
|
|
|
48
42
|
let remaining = splitPath(schemaPath);
|
|
49
43
|
while (remaining.length > 0) {
|
|
50
44
|
const descendable = describeFields(fields).filter((descriptor) => descriptor.type === "blocks" || descriptor.type === "richText");
|
|
51
|
-
const
|
|
52
|
-
if (!
|
|
45
|
+
const found = longestMatch(descendable, remaining, (part, segment) => part === segment);
|
|
46
|
+
if (!found) throw new SchemaError(`"${joinPath(remaining)}" does not address a blocks or rich text field. Available here: ${descendable.map((descriptor) => descriptor.path).join(", ") || "none"}`);
|
|
53
47
|
const at = {
|
|
54
48
|
config,
|
|
55
49
|
fields,
|
|
56
|
-
match,
|
|
50
|
+
match: found.parts,
|
|
57
51
|
remaining
|
|
58
52
|
};
|
|
59
|
-
const step =
|
|
53
|
+
const step = found.descriptor.type === "blocks" ? stepThroughBlocks(at) : stepThroughLexical(at);
|
|
60
54
|
blockType = step.blockType;
|
|
61
55
|
fields = step.fields;
|
|
62
56
|
remaining = step.rest;
|
|
@@ -69,7 +63,7 @@ const fieldsAtSchemaPath = (config, target, schemaPath) => {
|
|
|
69
63
|
const branchesOf = (fields, descriptor, schemaPath) => {
|
|
70
64
|
const base = `${schemaPath}${descriptor.path}`;
|
|
71
65
|
if (descriptor.type === "richText") {
|
|
72
|
-
const field =
|
|
66
|
+
const field = findFieldAt(fields, splitPath(descriptor.path), "richText");
|
|
73
67
|
if (!field) return [];
|
|
74
68
|
return subSchemaNodeTypes(field).flatMap((nodeType) => {
|
|
75
69
|
const sub = lexicalSubSchema(field, nodeType);
|
package/dist/schema/index.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { REQUIRED_NODE_PROPERTIES, ROOT_PROPERTIES, allowedNodeTypes, constrainsFields, lexicalSubSchema, nodeOptions, nodeProblems, nodePropertiesFor, propertyProblem, rootProblems, subSchemaNodeTypes } from "./lexical.mjs";
|
|
2
|
-
import { JSON_POINTER_PATTERN, isIndexSegment, joinPath, pointerFromPayloadPath, prototypeSegmentProblem, splitPath } from "./path.mjs";
|
|
3
|
-
import { RESERVED_FIELD_NAMES, ROW_KEYS, blockOf, blockSlugsOf, describeAddressableFields, describeFields, descriptorsUnder, findBlocksField, findRichTextField, isAdminHidden, schemaOf } from "./walk.mjs";
|
|
4
|
-
import { addressesAdminHidden, stripAdminHidden } from "./admin-hidden.mjs";
|
|
5
1
|
import { SchemaError } from "./errors.mjs";
|
|
2
|
+
import { editorRoot, nodePropertiesFor } from "./lexical.mjs";
|
|
3
|
+
import { JSON_POINTER_PATTERN, isIndexSegment, parentPointer, pointerFromPayloadPath, prototypeSegmentProblem, splitPath } from "./path.mjs";
|
|
4
|
+
import { RESERVED_FIELD_NAMES, ROW_KEYS, blockForRow, classifyKey, descriptorsUnder, findFieldAt } from "./walk.mjs";
|
|
5
|
+
import { addressesAdminHidden, stripAdminHidden } from "./admin-hidden.mjs";
|
|
6
6
|
import { nodeDescriber, reachableSchemaPaths } from "./describe.mjs";
|
|
7
|
-
import { resolveLexicalPointer } from "./lexical-pointer.mjs";
|
|
8
7
|
import { lexicalOutline } from "./outline.mjs";
|
|
9
8
|
import { resolveDataPointer } from "./pointer.mjs";
|
|
10
9
|
import { EMPTY_ROOT, validateWriteValue } from "./shape.mjs";
|
|
11
|
-
export { EMPTY_ROOT, JSON_POINTER_PATTERN,
|
|
10
|
+
export { EMPTY_ROOT, JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, ROW_KEYS, SchemaError, addressesAdminHidden, blockForRow, classifyKey, descriptorsUnder, editorRoot, findFieldAt, isIndexSegment, lexicalOutline, nodeDescriber, nodePropertiesFor, parentPointer, pointerFromPayloadPath, prototypeSegmentProblem, reachableSchemaPaths, resolveDataPointer, splitPath, stripAdminHidden, validateWriteValue };
|