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