payload-mcp-toolkit 0.3.3 ā 0.7.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/README.md +232 -150
- package/dist/__tests__/api-keys.test.js +292 -0
- package/dist/__tests__/api-keys.test.js.map +1 -0
- package/dist/__tests__/auth-strategy.test.js +681 -0
- package/dist/__tests__/auth-strategy.test.js.map +1 -0
- package/dist/__tests__/conflict-detection.test.js +69 -0
- package/dist/__tests__/conflict-detection.test.js.map +1 -0
- package/dist/__tests__/delete-document.test.js +70 -0
- package/dist/__tests__/delete-document.test.js.map +1 -0
- package/dist/__tests__/endpoint.test.js +143 -0
- package/dist/__tests__/endpoint.test.js.map +1 -0
- package/dist/__tests__/find-document.test.js +178 -0
- package/dist/__tests__/find-document.test.js.map +1 -0
- package/dist/__tests__/find-global.test.js +173 -0
- package/dist/__tests__/find-global.test.js.map +1 -0
- package/dist/__tests__/global-versions.test.js +183 -0
- package/dist/__tests__/global-versions.test.js.map +1 -0
- package/dist/__tests__/hash.test.js +58 -0
- package/dist/__tests__/hash.test.js.map +1 -0
- package/dist/__tests__/index-integration.test.js +191 -0
- package/dist/__tests__/index-integration.test.js.map +1 -0
- package/dist/__tests__/introspection.test.js +201 -1
- package/dist/__tests__/introspection.test.js.map +1 -1
- package/dist/__tests__/patch-global-layout.test.js +474 -0
- package/dist/__tests__/patch-global-layout.test.js.map +1 -0
- package/dist/__tests__/patch-layout.test.js +171 -0
- package/dist/__tests__/patch-layout.test.js.map +1 -0
- package/dist/__tests__/registry.test.js +795 -0
- package/dist/__tests__/registry.test.js.map +1 -0
- package/dist/__tests__/resources.test.js +139 -0
- package/dist/__tests__/resources.test.js.map +1 -0
- package/dist/__tests__/update-global.test.js +157 -0
- package/dist/__tests__/update-global.test.js.map +1 -0
- package/dist/api-keys.d.ts +46 -0
- package/dist/api-keys.js +272 -0
- package/dist/api-keys.js.map +1 -0
- package/dist/auth-strategy.d.ts +85 -0
- package/dist/auth-strategy.js +219 -0
- package/dist/auth-strategy.js.map +1 -0
- package/dist/components/CollectionScopesMatrix.d.ts +8 -0
- package/dist/components/CollectionScopesMatrix.js +32 -0
- package/dist/components/CollectionScopesMatrix.js.map +1 -0
- package/dist/components/GlobalScopesMatrix.d.ts +8 -0
- package/dist/components/GlobalScopesMatrix.js +28 -0
- package/dist/components/GlobalScopesMatrix.js.map +1 -0
- package/dist/components/ScopesTable.d.ts +19 -0
- package/dist/components/ScopesTable.js +285 -0
- package/dist/components/ScopesTable.js.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +4 -0
- package/dist/components/index.js.map +1 -0
- package/dist/conflict-detection.d.ts +13 -0
- package/dist/conflict-detection.js +41 -0
- package/dist/conflict-detection.js.map +1 -0
- package/dist/draft-workflow.d.ts +46 -47
- package/dist/draft-workflow.js +53 -130
- package/dist/draft-workflow.js.map +1 -1
- package/dist/endpoint.d.ts +35 -0
- package/dist/endpoint.js +105 -0
- package/dist/endpoint.js.map +1 -0
- package/dist/hash.d.ts +21 -0
- package/dist/hash.js +36 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +9 -9
- package/dist/index.js +168 -68
- package/dist/index.js.map +1 -1
- package/dist/introspection.d.ts +17 -3
- package/dist/introspection.js +95 -36
- package/dist/introspection.js.map +1 -1
- package/dist/prompts.js +5 -5
- package/dist/prompts.js.map +1 -1
- package/dist/registry.d.ts +50 -0
- package/dist/registry.js +169 -0
- package/dist/registry.js.map +1 -0
- package/dist/resources.d.ts +5 -3
- package/dist/resources.js +23 -11
- package/dist/resources.js.map +1 -1
- package/dist/scope/audit-log.d.ts +18 -0
- package/dist/scope/audit-log.js +50 -0
- package/dist/scope/audit-log.js.map +1 -0
- package/dist/scope/policy.d.ts +73 -0
- package/dist/scope/policy.js +218 -0
- package/dist/scope/policy.js.map +1 -0
- package/dist/tools/_helpers.d.ts +28 -1
- package/dist/tools/_helpers.js +83 -0
- package/dist/tools/_helpers.js.map +1 -1
- package/dist/tools/_layout-helpers.d.ts +43 -0
- package/dist/tools/_layout-helpers.js +159 -0
- package/dist/tools/_layout-helpers.js.map +1 -0
- package/dist/tools/create-document.d.ts +36 -0
- package/dist/tools/create-document.js +83 -0
- package/dist/tools/create-document.js.map +1 -0
- package/dist/tools/delete-document.d.ts +25 -0
- package/dist/tools/delete-document.js +49 -0
- package/dist/tools/delete-document.js.map +1 -0
- package/dist/tools/find-document.d.ts +33 -0
- package/dist/tools/find-document.js +97 -0
- package/dist/tools/find-document.js.map +1 -0
- package/dist/tools/find-global.d.ts +26 -0
- package/dist/tools/find-global.js +122 -0
- package/dist/tools/find-global.js.map +1 -0
- package/dist/tools/global-versions.d.ts +39 -0
- package/dist/tools/global-versions.js +132 -0
- package/dist/tools/global-versions.js.map +1 -0
- package/dist/tools/patch-global-layout.d.ts +31 -0
- package/dist/tools/patch-global-layout.js +127 -0
- package/dist/tools/patch-global-layout.js.map +1 -0
- package/dist/tools/patch-layout.d.ts +5 -8
- package/dist/tools/patch-layout.js +18 -100
- package/dist/tools/patch-layout.js.map +1 -1
- package/dist/tools/publish-draft.d.ts +5 -4
- package/dist/tools/publish-draft.js +6 -1
- package/dist/tools/publish-draft.js.map +1 -1
- package/dist/tools/publish-global-draft.d.ts +20 -0
- package/dist/tools/publish-global-draft.js +50 -0
- package/dist/tools/publish-global-draft.js.map +1 -0
- package/dist/tools/resolve-reference.d.ts +5 -4
- package/dist/tools/resolve-reference.js +4 -0
- package/dist/tools/resolve-reference.js.map +1 -1
- package/dist/tools/safe-delete.d.ts +5 -5
- package/dist/tools/safe-delete.js +20 -15
- package/dist/tools/safe-delete.js.map +1 -1
- package/dist/tools/schedule-publish.d.ts +5 -5
- package/dist/tools/schedule-publish.js +23 -19
- package/dist/tools/schedule-publish.js.map +1 -1
- package/dist/tools/search-content.d.ts +5 -9
- package/dist/tools/search-content.js +16 -12
- package/dist/tools/search-content.js.map +1 -1
- package/dist/tools/update-document.d.ts +5 -5
- package/dist/tools/update-document.js +10 -5
- package/dist/tools/update-document.js.map +1 -1
- package/dist/tools/update-global.d.ts +27 -0
- package/dist/tools/update-global.js +72 -0
- package/dist/tools/update-global.js.map +1 -0
- package/dist/tools/upload-media.d.ts +5 -4
- package/dist/tools/upload-media.js +6 -1
- package/dist/tools/upload-media.js.map +1 -1
- package/dist/tools/versions.d.ts +10 -9
- package/dist/tools/versions.js +15 -7
- package/dist/tools/versions.js.map +1 -1
- package/dist/types.d.ts +56 -3
- package/dist/types.js +13 -6
- package/dist/types.js.map +1 -1
- package/package.json +11 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/scope/audit-log.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\r\n\r\n// āāā Audit logging helpers āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\r\n\r\nconst MAX_LOGGED_STRING = 200\r\n\r\n/**\r\n * Returns the top-level keys of a JSON-string `data` arg, sanitized.\r\n * Per the Codex post-planning finding: logging key names (not values) lets us\r\n * later analyze whether the prose-only input shape is causing AI mistakes.\r\n */\r\nexport function extractDataKeys(args: Record<string, unknown>): string[] | undefined {\r\n const data = args.data\r\n if (typeof data !== 'string') return undefined\r\n try {\r\n const parsed = JSON.parse(data) as unknown\r\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\r\n return Object.keys(parsed as Record<string, unknown>)\r\n }\r\n } catch {\r\n return undefined\r\n }\r\n return undefined\r\n}\r\n\r\nexport function summariseArgs(args: Record<string, unknown>): Record<string, unknown> {\r\n const out: Record<string, unknown> = {}\r\n for (const [k, v] of Object.entries(args)) {\r\n if (typeof v === 'string' && v.length > MAX_LOGGED_STRING) {\r\n out[k] = `<truncated:${v.length}>`\r\n } else {\r\n out[k] = v\r\n }\r\n }\r\n return out\r\n}\r\n\r\nexport function getRequestId(req: PayloadRequest): string | undefined {\r\n const headers = req.headers as Headers | undefined\r\n return headers?.get?.('x-request-id') ?? undefined\r\n}\r\n\r\ntype LoggerLike = Partial<Record<'info' | 'warn' | 'error', (...args: unknown[]) => unknown>>\r\n\r\n/**\r\n * Returns a logger-invoker that swallows transport failures. Audit-log\r\n * writes must never break the tool dispatch path ā a throwing logger\r\n * transport (closed stream during HMR, a custom pino dest) would otherwise\r\n * flip a success to isError or mask the real tool error.\r\n */\r\nexport function makeSafeLog(logger: LoggerLike | undefined) {\r\n return (\r\n level: 'info' | 'warn' | 'error',\r\n payload: Record<string, unknown>,\r\n message: string,\r\n ) => {\r\n try {\r\n logger?.[level]?.(payload, message)\r\n } catch {\r\n // Logger transport failure must not break dispatch.\r\n }\r\n }\r\n}\r\n"],"names":["MAX_LOGGED_STRING","extractDataKeys","args","data","undefined","parsed","JSON","parse","Array","isArray","Object","keys","summariseArgs","out","k","v","entries","length","getRequestId","req","headers","get","makeSafeLog","logger","level","payload","message"],"mappings":"AAEA,wEAAwE;AAExE,MAAMA,oBAAoB;AAE1B;;;;CAIC,GACD,OAAO,SAASC,gBAAgBC,IAA6B;IAC3D,MAAMC,OAAOD,KAAKC,IAAI;IACtB,IAAI,OAAOA,SAAS,UAAU,OAAOC;IACrC,IAAI;QACF,MAAMC,SAASC,KAAKC,KAAK,CAACJ;QAC1B,IAAIE,UAAU,OAAOA,WAAW,YAAY,CAACG,MAAMC,OAAO,CAACJ,SAAS;YAClE,OAAOK,OAAOC,IAAI,CAACN;QACrB;IACF,EAAE,OAAM;QACN,OAAOD;IACT;IACA,OAAOA;AACT;AAEA,OAAO,SAASQ,cAAcV,IAA6B;IACzD,MAAMW,MAA+B,CAAC;IACtC,KAAK,MAAM,CAACC,GAAGC,EAAE,IAAIL,OAAOM,OAAO,CAACd,MAAO;QACzC,IAAI,OAAOa,MAAM,YAAYA,EAAEE,MAAM,GAAGjB,mBAAmB;YACzDa,GAAG,CAACC,EAAE,GAAG,CAAC,WAAW,EAAEC,EAAEE,MAAM,CAAC,CAAC,CAAC;QACpC,OAAO;YACLJ,GAAG,CAACC,EAAE,GAAGC;QACX;IACF;IACA,OAAOF;AACT;AAEA,OAAO,SAASK,aAAaC,GAAmB;IAC9C,MAAMC,UAAUD,IAAIC,OAAO;IAC3B,OAAOA,SAASC,MAAM,mBAAmBjB;AAC3C;AAIA;;;;;CAKC,GACD,OAAO,SAASkB,YAAYC,MAA8B;IACxD,OAAO,CACLC,OACAC,SACAC;QAEA,IAAI;YACFH,QAAQ,CAACC,MAAM,GAAGC,SAASC;QAC7B,EAAE,OAAM;QACN,oDAAoD;QACtD;IACF;AACF"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { CollectionAction, GlobalAction, KeyScopes, ScopePreset } from '../types';
|
|
2
|
+
export type ResourceKind = 'collection' | 'global' | 'account';
|
|
3
|
+
/**
|
|
4
|
+
* Discriminated routing tag attached to every tool factory output.
|
|
5
|
+
*
|
|
6
|
+
* Collocates the scope-routing decision with the tool definition itself ā
|
|
7
|
+
* the registry derives the collection/global/account lookups from `tools`
|
|
8
|
+
* at boot. Adding a new tool can no longer drift the routing maps out of
|
|
9
|
+
* sync because TS requires `routing` on every factory return.
|
|
10
|
+
*/
|
|
11
|
+
export type ToolRouting = {
|
|
12
|
+
kind: 'collection';
|
|
13
|
+
action: CollectionAction;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'global';
|
|
16
|
+
action: GlobalAction;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'account';
|
|
19
|
+
action: CollectionAction;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Minimal "routable tool" interface used by the policy module. The full
|
|
23
|
+
* `ToolFactoryOutput` shape (handler, parameters, description) is irrelevant
|
|
24
|
+
* here; we only need `name` + `routing` to build the lookup tables.
|
|
25
|
+
*/
|
|
26
|
+
export interface RoutableTool {
|
|
27
|
+
name: string;
|
|
28
|
+
routing: ToolRouting;
|
|
29
|
+
}
|
|
30
|
+
export declare const PRESET_ACTIONS: Record<ScopePreset, CollectionAction[]>;
|
|
31
|
+
/**
|
|
32
|
+
* Asymmetric per-preset action map for globals. `editor` is intentionally
|
|
33
|
+
* read-only on globals ā a single bad write on a singleton broadcasts
|
|
34
|
+
* site-wide with no per-document containment. Operators who want global
|
|
35
|
+
* writes promote the key to `admin` or use a Custom key with explicit
|
|
36
|
+
* `globalScopes`. README and CHANGELOG call out the asymmetry.
|
|
37
|
+
*/
|
|
38
|
+
export declare const PRESET_GLOBAL_ACTIONS: Record<ScopePreset, GlobalAction[]>;
|
|
39
|
+
export declare const PRESET_TOOL_DENY: Record<ScopePreset, string[]>;
|
|
40
|
+
export interface ScopeDecision {
|
|
41
|
+
allowed: boolean;
|
|
42
|
+
reason?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface RoutingTables {
|
|
45
|
+
collectionToolAction: ReadonlyMap<string, CollectionAction>;
|
|
46
|
+
globalToolAction: ReadonlyMap<string, GlobalAction>;
|
|
47
|
+
accountToolAction: ReadonlyMap<string, CollectionAction>;
|
|
48
|
+
toolKind: ReadonlyMap<string, ResourceKind>;
|
|
49
|
+
}
|
|
50
|
+
export declare function buildRoutingTables(tools: RoutableTool[]): RoutingTables;
|
|
51
|
+
export type ScopeChecker = (scopes: KeyScopes | null | undefined, toolName: string, resource: string | undefined) => ScopeDecision;
|
|
52
|
+
/**
|
|
53
|
+
* Build a scope checker bound to a concrete tool list. The checker is a pure
|
|
54
|
+
* function over (scopes, toolName, resource) ā the routing tables are closed
|
|
55
|
+
* over once at construction time.
|
|
56
|
+
*
|
|
57
|
+
* Fail-closed semantics:
|
|
58
|
+
* - Null/undefined scopes grant full access (back-compat).
|
|
59
|
+
* - When `scopes.collections` / `scopes.globals` is set, it is a *whitelist*
|
|
60
|
+
* for that resource kind ā unlisted resources are denied.
|
|
61
|
+
* - When a tool resolves to a collection or global kind but the corresponding
|
|
62
|
+
* scope map is undefined and `scopes.preset` is undefined, the call is
|
|
63
|
+
* denied (closes the `tools.allow`-only latent fail-open).
|
|
64
|
+
* - Account-level tools are gated by the preset's action list, if a preset
|
|
65
|
+
* is set. Without a preset, a key scoped to specific collections/globals
|
|
66
|
+
* cannot use account-level tools ā they'd broaden the surface.
|
|
67
|
+
*/
|
|
68
|
+
export declare function buildScopeChecker(tools: RoutableTool[]): ScopeChecker;
|
|
69
|
+
/**
|
|
70
|
+
* Internal pure checker. Exposed for the per-request wrapper in the registry
|
|
71
|
+
* so it can re-use the same `RoutingTables` it built once at startup.
|
|
72
|
+
*/
|
|
73
|
+
export declare function assertScopeAllows(scopes: KeyScopes | null | undefined, toolName: string, resource: string | undefined, tables: RoutingTables): ScopeDecision;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// āāā Per-preset action tables āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2
|
+
const ALL_ACTIONS = [
|
|
3
|
+
'read',
|
|
4
|
+
'create',
|
|
5
|
+
'update',
|
|
6
|
+
'delete'
|
|
7
|
+
];
|
|
8
|
+
export const PRESET_ACTIONS = {
|
|
9
|
+
'read-only': [
|
|
10
|
+
'read'
|
|
11
|
+
],
|
|
12
|
+
editor: [
|
|
13
|
+
'read',
|
|
14
|
+
'create',
|
|
15
|
+
'update'
|
|
16
|
+
],
|
|
17
|
+
admin: ALL_ACTIONS
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Asymmetric per-preset action map for globals. `editor` is intentionally
|
|
21
|
+
* read-only on globals ā a single bad write on a singleton broadcasts
|
|
22
|
+
* site-wide with no per-document containment. Operators who want global
|
|
23
|
+
* writes promote the key to `admin` or use a Custom key with explicit
|
|
24
|
+
* `globalScopes`. README and CHANGELOG call out the asymmetry.
|
|
25
|
+
*/ export const PRESET_GLOBAL_ACTIONS = {
|
|
26
|
+
'read-only': [
|
|
27
|
+
'read'
|
|
28
|
+
],
|
|
29
|
+
editor: [
|
|
30
|
+
'read'
|
|
31
|
+
],
|
|
32
|
+
admin: [
|
|
33
|
+
'read',
|
|
34
|
+
'update'
|
|
35
|
+
]
|
|
36
|
+
};
|
|
37
|
+
export const PRESET_TOOL_DENY = {
|
|
38
|
+
'read-only': [],
|
|
39
|
+
editor: [
|
|
40
|
+
'safeDelete',
|
|
41
|
+
'deleteDocument'
|
|
42
|
+
],
|
|
43
|
+
admin: []
|
|
44
|
+
};
|
|
45
|
+
export function buildRoutingTables(tools) {
|
|
46
|
+
const collectionToolAction = new Map();
|
|
47
|
+
const globalToolAction = new Map();
|
|
48
|
+
const accountToolAction = new Map();
|
|
49
|
+
const toolKind = new Map();
|
|
50
|
+
for (const t of tools){
|
|
51
|
+
toolKind.set(t.name, t.routing.kind);
|
|
52
|
+
if (t.routing.kind === 'collection') collectionToolAction.set(t.name, t.routing.action);
|
|
53
|
+
else if (t.routing.kind === 'global') globalToolAction.set(t.name, t.routing.action);
|
|
54
|
+
else accountToolAction.set(t.name, t.routing.action);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
collectionToolAction,
|
|
58
|
+
globalToolAction,
|
|
59
|
+
accountToolAction,
|
|
60
|
+
toolKind
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build a scope checker bound to a concrete tool list. The checker is a pure
|
|
65
|
+
* function over (scopes, toolName, resource) ā the routing tables are closed
|
|
66
|
+
* over once at construction time.
|
|
67
|
+
*
|
|
68
|
+
* Fail-closed semantics:
|
|
69
|
+
* - Null/undefined scopes grant full access (back-compat).
|
|
70
|
+
* - When `scopes.collections` / `scopes.globals` is set, it is a *whitelist*
|
|
71
|
+
* for that resource kind ā unlisted resources are denied.
|
|
72
|
+
* - When a tool resolves to a collection or global kind but the corresponding
|
|
73
|
+
* scope map is undefined and `scopes.preset` is undefined, the call is
|
|
74
|
+
* denied (closes the `tools.allow`-only latent fail-open).
|
|
75
|
+
* - Account-level tools are gated by the preset's action list, if a preset
|
|
76
|
+
* is set. Without a preset, a key scoped to specific collections/globals
|
|
77
|
+
* cannot use account-level tools ā they'd broaden the surface.
|
|
78
|
+
*/ export function buildScopeChecker(tools) {
|
|
79
|
+
const tables = buildRoutingTables(tools);
|
|
80
|
+
return (scopes, toolName, resource)=>assertScopeAllows(scopes, toolName, resource, tables);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Internal pure checker. Exposed for the per-request wrapper in the registry
|
|
84
|
+
* so it can re-use the same `RoutingTables` it built once at startup.
|
|
85
|
+
*/ export function assertScopeAllows(scopes, toolName, resource, tables) {
|
|
86
|
+
const resourceKind = tables.toolKind.get(toolName) ?? null;
|
|
87
|
+
// Unregistered tool ā fail-closed at request time. Adding a tool without a
|
|
88
|
+
// routing field is a TS error at the factory return site, so this branch
|
|
89
|
+
// only fires for typo'd tool names sent by the client.
|
|
90
|
+
if (resourceKind === null) {
|
|
91
|
+
return {
|
|
92
|
+
allowed: false,
|
|
93
|
+
reason: `Tool "${toolName}" has no registered scope mapping.`
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (!scopes || scopes.preset === undefined && !scopes.collections && !scopes.globals && !scopes.tools) {
|
|
97
|
+
return {
|
|
98
|
+
allowed: true
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (scopes.tools?.deny?.includes(toolName)) {
|
|
102
|
+
return {
|
|
103
|
+
allowed: false,
|
|
104
|
+
reason: `Tool "${toolName}" is denied for this API key.`
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (scopes.tools?.allow && !scopes.tools.allow.includes(toolName)) {
|
|
108
|
+
return {
|
|
109
|
+
allowed: false,
|
|
110
|
+
reason: `Tool "${toolName}" is not in the allow-list for this API key.`
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (scopes.preset && PRESET_TOOL_DENY[scopes.preset]?.includes(toolName)) {
|
|
114
|
+
return {
|
|
115
|
+
allowed: false,
|
|
116
|
+
reason: `Tool "${toolName}" is not allowed by the "${scopes.preset}" preset.`
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (resourceKind === 'account') {
|
|
120
|
+
return checkAccount(scopes, toolName, tables.accountToolAction);
|
|
121
|
+
}
|
|
122
|
+
const policy = resourceKind === 'collection' ? COLLECTION_POLICY : GLOBAL_POLICY;
|
|
123
|
+
const toolAction = resourceKind === 'collection' ? tables.collectionToolAction : tables.globalToolAction;
|
|
124
|
+
return checkResource(scopes, toolName, resource, toolAction, policy);
|
|
125
|
+
}
|
|
126
|
+
const COLLECTION_POLICY = {
|
|
127
|
+
presetActions: PRESET_ACTIONS,
|
|
128
|
+
scopeAxis: 'collections',
|
|
129
|
+
label: 'collection',
|
|
130
|
+
Label: 'Collection'
|
|
131
|
+
};
|
|
132
|
+
const GLOBAL_POLICY = {
|
|
133
|
+
presetActions: PRESET_GLOBAL_ACTIONS,
|
|
134
|
+
scopeAxis: 'globals',
|
|
135
|
+
label: 'global',
|
|
136
|
+
Label: 'Global'
|
|
137
|
+
};
|
|
138
|
+
function checkResource(scopes, toolName, resource, toolAction, policy) {
|
|
139
|
+
const action = toolAction.get(toolName);
|
|
140
|
+
const presetActions = scopes.preset ? policy.presetActions[scopes.preset] : undefined;
|
|
141
|
+
const resourceScope = scopes[policy.scopeAxis];
|
|
142
|
+
if (!resource) {
|
|
143
|
+
// Resource-keyed tool called without a slug; defer to schema validation.
|
|
144
|
+
return {
|
|
145
|
+
allowed: true
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (!action) return {
|
|
149
|
+
allowed: true
|
|
150
|
+
};
|
|
151
|
+
if (resourceScope) {
|
|
152
|
+
const override = resourceScope[resource];
|
|
153
|
+
if (!override) {
|
|
154
|
+
return {
|
|
155
|
+
allowed: false,
|
|
156
|
+
reason: `${policy.Label} "${resource}" is not in this API key's allowed ${policy.scopeAxis}.`
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (!override.includes(action)) {
|
|
160
|
+
return {
|
|
161
|
+
allowed: false,
|
|
162
|
+
reason: `Action "${action}" on ${policy.label} "${resource}" is not permitted by this API key's scope.`
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
allowed: true
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (!presetActions) {
|
|
170
|
+
// Fail-closed: `tools.allow` without a resource map or preset would
|
|
171
|
+
// otherwise broadcast the tool across every resource. Require explicit
|
|
172
|
+
// intent.
|
|
173
|
+
return {
|
|
174
|
+
allowed: false,
|
|
175
|
+
reason: `Tool "${toolName}" requires an explicit ${policy.label} scope or preset on this API key.`
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (!presetActions.includes(action)) {
|
|
179
|
+
return {
|
|
180
|
+
allowed: false,
|
|
181
|
+
reason: `Action "${action}" on ${policy.label} "${resource}" is not permitted by this API key's preset.`
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
allowed: true
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function checkAccount(scopes, toolName, toolAction) {
|
|
189
|
+
const action = toolAction.get(toolName);
|
|
190
|
+
const presetActions = scopes.preset ? PRESET_ACTIONS[scopes.preset] : undefined;
|
|
191
|
+
// Explicit resource override is the tightest signal: an account-level tool
|
|
192
|
+
// operates across the whole site (searchContent across every collection,
|
|
193
|
+
// uploadMedia into any media coll, etc.) and would broaden the key beyond
|
|
194
|
+
// the resource whitelist regardless of which preset is set. Deny account
|
|
195
|
+
// tools whenever the key carries explicit collection/global scopes.
|
|
196
|
+
if (scopes.collections || scopes.globals) {
|
|
197
|
+
return {
|
|
198
|
+
allowed: false,
|
|
199
|
+
reason: `Tool "${toolName}" is denied for keys with explicit collection or global scopes ā account-level tools would broaden access beyond the whitelist.`
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (presetActions) {
|
|
203
|
+
if (action && !presetActions.includes(action)) {
|
|
204
|
+
return {
|
|
205
|
+
allowed: false,
|
|
206
|
+
reason: `Action "${action}" is not permitted by this API key's preset.`
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
allowed: true
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
allowed: true
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
//# sourceMappingURL=policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/scope/policy.ts"],"sourcesContent":["import type { CollectionAction, GlobalAction, KeyScopes, ScopePreset } from '../types'\r\n\r\n// āāā Routing primitives āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\r\n\r\nexport type ResourceKind = 'collection' | 'global' | 'account'\r\n\r\n/**\r\n * Discriminated routing tag attached to every tool factory output.\r\n *\r\n * Collocates the scope-routing decision with the tool definition itself ā\r\n * the registry derives the collection/global/account lookups from `tools`\r\n * at boot. Adding a new tool can no longer drift the routing maps out of\r\n * sync because TS requires `routing` on every factory return.\r\n */\r\nexport type ToolRouting =\r\n | { kind: 'collection'; action: CollectionAction }\r\n | { kind: 'global'; action: GlobalAction }\r\n | { kind: 'account'; action: CollectionAction }\r\n\r\n/**\r\n * Minimal \"routable tool\" interface used by the policy module. The full\r\n * `ToolFactoryOutput` shape (handler, parameters, description) is irrelevant\r\n * here; we only need `name` + `routing` to build the lookup tables.\r\n */\r\nexport interface RoutableTool {\r\n name: string\r\n routing: ToolRouting\r\n}\r\n\r\n// āāā Per-preset action tables āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\r\n\r\nconst ALL_ACTIONS: CollectionAction[] = ['read', 'create', 'update', 'delete']\r\n\r\nexport const PRESET_ACTIONS: Record<ScopePreset, CollectionAction[]> = {\r\n 'read-only': ['read'],\r\n editor: ['read', 'create', 'update'],\r\n admin: ALL_ACTIONS,\r\n}\r\n\r\n/**\r\n * Asymmetric per-preset action map for globals. `editor` is intentionally\r\n * read-only on globals ā a single bad write on a singleton broadcasts\r\n * site-wide with no per-document containment. Operators who want global\r\n * writes promote the key to `admin` or use a Custom key with explicit\r\n * `globalScopes`. README and CHANGELOG call out the asymmetry.\r\n */\r\nexport const PRESET_GLOBAL_ACTIONS: Record<ScopePreset, GlobalAction[]> = {\r\n 'read-only': ['read'],\r\n editor: ['read'],\r\n admin: ['read', 'update'],\r\n}\r\n\r\nexport const PRESET_TOOL_DENY: Record<ScopePreset, string[]> = {\r\n 'read-only': [],\r\n editor: ['safeDelete', 'deleteDocument'],\r\n admin: [],\r\n}\r\n\r\n// āāā Routing tables built from the tool list āāāāāāāāāāāāāāāāāāāāāāāāā\r\n\r\nexport interface ScopeDecision {\r\n allowed: boolean\r\n reason?: string\r\n}\r\n\r\nexport interface RoutingTables {\r\n collectionToolAction: ReadonlyMap<string, CollectionAction>\r\n globalToolAction: ReadonlyMap<string, GlobalAction>\r\n accountToolAction: ReadonlyMap<string, CollectionAction>\r\n toolKind: ReadonlyMap<string, ResourceKind>\r\n}\r\n\r\nexport function buildRoutingTables(tools: RoutableTool[]): RoutingTables {\r\n const collectionToolAction = new Map<string, CollectionAction>()\r\n const globalToolAction = new Map<string, GlobalAction>()\r\n const accountToolAction = new Map<string, CollectionAction>()\r\n const toolKind = new Map<string, ResourceKind>()\r\n for (const t of tools) {\r\n toolKind.set(t.name, t.routing.kind)\r\n if (t.routing.kind === 'collection') collectionToolAction.set(t.name, t.routing.action)\r\n else if (t.routing.kind === 'global') globalToolAction.set(t.name, t.routing.action)\r\n else accountToolAction.set(t.name, t.routing.action)\r\n }\r\n return { collectionToolAction, globalToolAction, accountToolAction, toolKind }\r\n}\r\n\r\n// āāā Scope evaluation āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\r\n\r\nexport type ScopeChecker = (\r\n scopes: KeyScopes | null | undefined,\r\n toolName: string,\r\n resource: string | undefined,\r\n) => ScopeDecision\r\n\r\n/**\r\n * Build a scope checker bound to a concrete tool list. The checker is a pure\r\n * function over (scopes, toolName, resource) ā the routing tables are closed\r\n * over once at construction time.\r\n *\r\n * Fail-closed semantics:\r\n * - Null/undefined scopes grant full access (back-compat).\r\n * - When `scopes.collections` / `scopes.globals` is set, it is a *whitelist*\r\n * for that resource kind ā unlisted resources are denied.\r\n * - When a tool resolves to a collection or global kind but the corresponding\r\n * scope map is undefined and `scopes.preset` is undefined, the call is\r\n * denied (closes the `tools.allow`-only latent fail-open).\r\n * - Account-level tools are gated by the preset's action list, if a preset\r\n * is set. Without a preset, a key scoped to specific collections/globals\r\n * cannot use account-level tools ā they'd broaden the surface.\r\n */\r\nexport function buildScopeChecker(tools: RoutableTool[]): ScopeChecker {\r\n const tables = buildRoutingTables(tools)\r\n return (scopes, toolName, resource) => assertScopeAllows(scopes, toolName, resource, tables)\r\n}\r\n\r\n/**\r\n * Internal pure checker. Exposed for the per-request wrapper in the registry\r\n * so it can re-use the same `RoutingTables` it built once at startup.\r\n */\r\nexport function assertScopeAllows(\r\n scopes: KeyScopes | null | undefined,\r\n toolName: string,\r\n resource: string | undefined,\r\n tables: RoutingTables,\r\n): ScopeDecision {\r\n const resourceKind = tables.toolKind.get(toolName) ?? null\r\n // Unregistered tool ā fail-closed at request time. Adding a tool without a\r\n // routing field is a TS error at the factory return site, so this branch\r\n // only fires for typo'd tool names sent by the client.\r\n if (resourceKind === null) {\r\n return {\r\n allowed: false,\r\n reason: `Tool \"${toolName}\" has no registered scope mapping.`,\r\n }\r\n }\r\n\r\n if (!scopes || (scopes.preset === undefined && !scopes.collections && !scopes.globals && !scopes.tools)) {\r\n return { allowed: true }\r\n }\r\n\r\n if (scopes.tools?.deny?.includes(toolName)) {\r\n return { allowed: false, reason: `Tool \"${toolName}\" is denied for this API key.` }\r\n }\r\n if (scopes.tools?.allow && !scopes.tools.allow.includes(toolName)) {\r\n return {\r\n allowed: false,\r\n reason: `Tool \"${toolName}\" is not in the allow-list for this API key.`,\r\n }\r\n }\r\n\r\n if (scopes.preset && PRESET_TOOL_DENY[scopes.preset]?.includes(toolName)) {\r\n return {\r\n allowed: false,\r\n reason: `Tool \"${toolName}\" is not allowed by the \"${scopes.preset}\" preset.`,\r\n }\r\n }\r\n\r\n if (resourceKind === 'account') {\r\n return checkAccount(scopes, toolName, tables.accountToolAction)\r\n }\r\n const policy = resourceKind === 'collection' ? COLLECTION_POLICY : GLOBAL_POLICY\r\n const toolAction =\r\n resourceKind === 'collection' ? tables.collectionToolAction : tables.globalToolAction\r\n return checkResource(scopes, toolName, resource, toolAction, policy)\r\n}\r\n\r\n/**\r\n * Per-resource-kind policy. Collapses what used to be two near-identical\r\n * `checkCollection` / `checkGlobal` helpers ā the only differences are\r\n * the preset-actions table, the label, and which axis of `KeyScopes` to\r\n * read for explicit overrides.\r\n */\r\ninterface ResourcePolicy {\r\n presetActions: Record<ScopePreset, readonly string[]>\r\n scopeAxis: 'collections' | 'globals'\r\n label: 'collection' | 'global'\r\n Label: 'Collection' | 'Global'\r\n}\r\n\r\nconst COLLECTION_POLICY: ResourcePolicy = {\r\n presetActions: PRESET_ACTIONS,\r\n scopeAxis: 'collections',\r\n label: 'collection',\r\n Label: 'Collection',\r\n}\r\n\r\nconst GLOBAL_POLICY: ResourcePolicy = {\r\n presetActions: PRESET_GLOBAL_ACTIONS,\r\n scopeAxis: 'globals',\r\n label: 'global',\r\n Label: 'Global',\r\n}\r\n\r\nfunction checkResource(\r\n scopes: KeyScopes,\r\n toolName: string,\r\n resource: string | undefined,\r\n toolAction: ReadonlyMap<string, string>,\r\n policy: ResourcePolicy,\r\n): ScopeDecision {\r\n const action = toolAction.get(toolName)\r\n const presetActions = scopes.preset ? policy.presetActions[scopes.preset] : undefined\r\n const resourceScope = scopes[policy.scopeAxis]\r\n\r\n if (!resource) {\r\n // Resource-keyed tool called without a slug; defer to schema validation.\r\n return { allowed: true }\r\n }\r\n if (!action) return { allowed: true }\r\n\r\n if (resourceScope) {\r\n const override = resourceScope[resource]\r\n if (!override) {\r\n return {\r\n allowed: false,\r\n reason: `${policy.Label} \"${resource}\" is not in this API key's allowed ${policy.scopeAxis}.`,\r\n }\r\n }\r\n if (!override.includes(action as never)) {\r\n return {\r\n allowed: false,\r\n reason: `Action \"${action}\" on ${policy.label} \"${resource}\" is not permitted by this API key's scope.`,\r\n }\r\n }\r\n return { allowed: true }\r\n }\r\n\r\n if (!presetActions) {\r\n // Fail-closed: `tools.allow` without a resource map or preset would\r\n // otherwise broadcast the tool across every resource. Require explicit\r\n // intent.\r\n return {\r\n allowed: false,\r\n reason: `Tool \"${toolName}\" requires an explicit ${policy.label} scope or preset on this API key.`,\r\n }\r\n }\r\n\r\n if (!presetActions.includes(action)) {\r\n return {\r\n allowed: false,\r\n reason: `Action \"${action}\" on ${policy.label} \"${resource}\" is not permitted by this API key's preset.`,\r\n }\r\n }\r\n return { allowed: true }\r\n}\r\n\r\nfunction checkAccount(\r\n scopes: KeyScopes,\r\n toolName: string,\r\n toolAction: ReadonlyMap<string, CollectionAction>,\r\n): ScopeDecision {\r\n const action = toolAction.get(toolName)\r\n const presetActions = scopes.preset ? PRESET_ACTIONS[scopes.preset] : undefined\r\n\r\n // Explicit resource override is the tightest signal: an account-level tool\r\n // operates across the whole site (searchContent across every collection,\r\n // uploadMedia into any media coll, etc.) and would broaden the key beyond\r\n // the resource whitelist regardless of which preset is set. Deny account\r\n // tools whenever the key carries explicit collection/global scopes.\r\n if (scopes.collections || scopes.globals) {\r\n return {\r\n allowed: false,\r\n reason: `Tool \"${toolName}\" is denied for keys with explicit collection or global scopes ā account-level tools would broaden access beyond the whitelist.`,\r\n }\r\n }\r\n\r\n if (presetActions) {\r\n if (action && !presetActions.includes(action)) {\r\n return {\r\n allowed: false,\r\n reason: `Action \"${action}\" is not permitted by this API key's preset.`,\r\n }\r\n }\r\n return { allowed: true }\r\n }\r\n\r\n return { allowed: true }\r\n}\r\n"],"names":["ALL_ACTIONS","PRESET_ACTIONS","editor","admin","PRESET_GLOBAL_ACTIONS","PRESET_TOOL_DENY","buildRoutingTables","tools","collectionToolAction","Map","globalToolAction","accountToolAction","toolKind","t","set","name","routing","kind","action","buildScopeChecker","tables","scopes","toolName","resource","assertScopeAllows","resourceKind","get","allowed","reason","preset","undefined","collections","globals","deny","includes","allow","checkAccount","policy","COLLECTION_POLICY","GLOBAL_POLICY","toolAction","checkResource","presetActions","scopeAxis","label","Label","resourceScope","override"],"mappings":"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"}
|
package/dist/tools/_helpers.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { CollectionConfig, PayloadRequest } from 'payload';
|
|
3
|
+
/**
|
|
4
|
+
* Build a `z.enum` over a list of valid resource slugs with a friendly
|
|
5
|
+
* error message that names the valid set and clarifies why an unknown
|
|
6
|
+
* slug (e.g. one removed via `options.exclude.globals`) is rejected.
|
|
7
|
+
*
|
|
8
|
+
* Default Zod enum errors are "Invalid enum value. ā¦" ā accurate but
|
|
9
|
+
* unhelpful when the slug looks plausible to a caller who isn't aware
|
|
10
|
+
* the host config excluded it.
|
|
11
|
+
*/
|
|
12
|
+
export declare function slugEnum(slugs: string[], kind: 'global' | 'collection'): z.ZodEnum<[string, ...string[]]>;
|
|
2
13
|
export interface McpTextResponse {
|
|
3
14
|
content: Array<{
|
|
4
15
|
type: 'text';
|
|
@@ -12,3 +23,19 @@ export declare function errorMessage(error: unknown): string;
|
|
|
12
23
|
export declare function stampMcpContext(req: PayloadRequest): void;
|
|
13
24
|
export declare function getDocDisplayName(doc: unknown, fallback: string): string;
|
|
14
25
|
export declare function requireDraftCollection(collection: string, draftCollections: Set<string>, noun?: string): McpTextResponse | null;
|
|
26
|
+
/**
|
|
27
|
+
* Resolves the preview URL for a draft document by delegating to the
|
|
28
|
+
* collection's own configured preview function (`admin.livePreview.url`
|
|
29
|
+
* preferred, then `admin.preview`). Returns null when no function is
|
|
30
|
+
* configured, when it fails, or when it returns a relative path with no
|
|
31
|
+
* absolute `siteUrl` to anchor it.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolvePreviewUrl(collection: CollectionConfig, doc: Record<string, unknown>, req: PayloadRequest, siteUrl: string | undefined): Promise<string | null>;
|
|
34
|
+
/**
|
|
35
|
+
* If `doc` is a draft, appends a preview-URL hint to the MCP response so the
|
|
36
|
+
* AI can present it to the user. Falls back to a generic admin-panel hint
|
|
37
|
+
* when the collection has no preview function configured.
|
|
38
|
+
*
|
|
39
|
+
* Pure with respect to the response: a fresh content array is returned.
|
|
40
|
+
*/
|
|
41
|
+
export declare function decorateDraftResponse(response: McpTextResponse, doc: Record<string, unknown> | null | undefined, collection: CollectionConfig | undefined, req: PayloadRequest, siteUrl: string | undefined): Promise<McpTextResponse>;
|
package/dist/tools/_helpers.js
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Build a `z.enum` over a list of valid resource slugs with a friendly
|
|
4
|
+
* error message that names the valid set and clarifies why an unknown
|
|
5
|
+
* slug (e.g. one removed via `options.exclude.globals`) is rejected.
|
|
6
|
+
*
|
|
7
|
+
* Default Zod enum errors are "Invalid enum value. ā¦" ā accurate but
|
|
8
|
+
* unhelpful when the slug looks plausible to a caller who isn't aware
|
|
9
|
+
* the host config excluded it.
|
|
10
|
+
*/ export function slugEnum(slugs, kind) {
|
|
11
|
+
return z.enum(slugs, {
|
|
12
|
+
errorMap: ()=>({
|
|
13
|
+
message: `${kind === 'global' ? 'Global' : 'Collection'} slug must be one of: ${slugs.join(', ')}. Unknown or excluded slugs are rejected.`
|
|
14
|
+
})
|
|
15
|
+
});
|
|
16
|
+
}
|
|
1
17
|
export const DRAFT_NOTE = ' Document is in draft status ā use publishDraft to make it live.';
|
|
2
18
|
export function textResponse(text) {
|
|
3
19
|
return {
|
|
@@ -31,5 +47,72 @@ export function requireDraftCollection(collection, draftCollections, noun = 'dra
|
|
|
31
47
|
...draftCollections
|
|
32
48
|
].join(', ') || 'none'}`);
|
|
33
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolves the preview URL for a draft document by delegating to the
|
|
52
|
+
* collection's own configured preview function (`admin.livePreview.url`
|
|
53
|
+
* preferred, then `admin.preview`). Returns null when no function is
|
|
54
|
+
* configured, when it fails, or when it returns a relative path with no
|
|
55
|
+
* absolute `siteUrl` to anchor it.
|
|
56
|
+
*/ export async function resolvePreviewUrl(collection, doc, req, siteUrl) {
|
|
57
|
+
const admin = collection.admin ?? {};
|
|
58
|
+
const locale = req.locale ?? 'en';
|
|
59
|
+
let raw;
|
|
60
|
+
const livePreviewUrl = admin.livePreview?.url;
|
|
61
|
+
if (typeof livePreviewUrl === 'function') {
|
|
62
|
+
try {
|
|
63
|
+
raw = await livePreviewUrl({
|
|
64
|
+
data: doc,
|
|
65
|
+
locale: {
|
|
66
|
+
code: locale,
|
|
67
|
+
label: locale
|
|
68
|
+
},
|
|
69
|
+
req,
|
|
70
|
+
payload: req.payload,
|
|
71
|
+
collectionConfig: collection
|
|
72
|
+
});
|
|
73
|
+
} catch {
|
|
74
|
+
raw = null;
|
|
75
|
+
}
|
|
76
|
+
} else if (typeof livePreviewUrl === 'string') {
|
|
77
|
+
raw = livePreviewUrl;
|
|
78
|
+
}
|
|
79
|
+
if (!raw && typeof admin.preview === 'function') {
|
|
80
|
+
try {
|
|
81
|
+
raw = await admin.preview(doc, {
|
|
82
|
+
locale,
|
|
83
|
+
req,
|
|
84
|
+
token: null
|
|
85
|
+
});
|
|
86
|
+
} catch {
|
|
87
|
+
raw = null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!raw || typeof raw !== 'string') return null;
|
|
91
|
+
if (raw.startsWith('http://') || raw.startsWith('https://')) return raw;
|
|
92
|
+
if (!siteUrl) return null;
|
|
93
|
+
const base = siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl;
|
|
94
|
+
const path = raw.startsWith('/') ? raw : `/${raw}`;
|
|
95
|
+
return `${base}${path}`;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* If `doc` is a draft, appends a preview-URL hint to the MCP response so the
|
|
99
|
+
* AI can present it to the user. Falls back to a generic admin-panel hint
|
|
100
|
+
* when the collection has no preview function configured.
|
|
101
|
+
*
|
|
102
|
+
* Pure with respect to the response: a fresh content array is returned.
|
|
103
|
+
*/ export async function decorateDraftResponse(response, doc, collection, req, siteUrl) {
|
|
104
|
+
if (!doc || doc._status !== 'draft' || !collection) return response;
|
|
105
|
+
const previewUrl = await resolvePreviewUrl(collection, doc, req, siteUrl);
|
|
106
|
+
const hint = previewUrl ? `\nš This document is a draft. Preview it here: ${previewUrl}` : '\nš This document is a draft. Use the admin panel to preview it.';
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
...response.content,
|
|
110
|
+
{
|
|
111
|
+
type: 'text',
|
|
112
|
+
text: hint
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
};
|
|
116
|
+
}
|
|
34
117
|
|
|
35
118
|
//# sourceMappingURL=_helpers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tools/_helpers.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nexport interface McpTextResponse {\n content: Array<{ type: 'text'; text: string }>\n}\n\nexport const DRAFT_NOTE = ' Document is in draft status ā use publishDraft to make it live.'\n\nexport function textResponse(text: string): McpTextResponse {\n return { content: [{ type: 'text', text }] }\n}\n\nexport function jsonResponse(payload: unknown): McpTextResponse {\n return textResponse(JSON.stringify(payload))\n}\n\nexport function errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n\nexport function stampMcpContext(req: PayloadRequest): void {\n req.context = { ...req.context, source: 'mcp' }\n}\n\nexport function getDocDisplayName(doc: unknown, fallback: string): string {\n const d = doc as Record<string, unknown> | null | undefined\n return (\n (typeof d?.name === 'string' && d.name) ||\n (typeof d?.title === 'string' && d.title) ||\n (typeof d?.slug === 'string' && d.slug) ||\n fallback\n )\n}\n\nexport function requireDraftCollection(\n collection: string,\n draftCollections: Set<string>,\n noun = 'drafts',\n): McpTextResponse | null {\n if (draftCollections.has(collection)) return null\n return textResponse(\n `Error: Collection \"${collection}\" does not support ${noun}. ` +\n `Draft-enabled collections: ${[...draftCollections].join(', ') || 'none'}`,\n )\n}\n"],"names":["DRAFT_NOTE","textResponse","text","content","type","jsonResponse","payload","JSON","stringify","errorMessage","error","Error","
|
|
1
|
+
{"version":3,"sources":["../../src/tools/_helpers.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { CollectionConfig, PayloadRequest } from 'payload'\r\n\r\n/**\r\n * Build a `z.enum` over a list of valid resource slugs with a friendly\r\n * error message that names the valid set and clarifies why an unknown\r\n * slug (e.g. one removed via `options.exclude.globals`) is rejected.\r\n *\r\n * Default Zod enum errors are \"Invalid enum value. ā¦\" ā accurate but\r\n * unhelpful when the slug looks plausible to a caller who isn't aware\r\n * the host config excluded it.\r\n */\r\nexport function slugEnum(\r\n slugs: string[],\r\n kind: 'global' | 'collection',\r\n): z.ZodEnum<[string, ...string[]]> {\r\n return z.enum(slugs as [string, ...string[]], {\r\n errorMap: () => ({\r\n message: `${kind === 'global' ? 'Global' : 'Collection'} slug must be one of: ${slugs.join(', ')}. Unknown or excluded slugs are rejected.`,\r\n }),\r\n })\r\n}\r\n\r\nexport interface McpTextResponse {\r\n content: Array<{ type: 'text'; text: string }>\r\n}\r\n\r\nexport const DRAFT_NOTE = ' Document is in draft status ā use publishDraft to make it live.'\r\n\r\nexport function textResponse(text: string): McpTextResponse {\r\n return { content: [{ type: 'text', text }] }\r\n}\r\n\r\nexport function jsonResponse(payload: unknown): McpTextResponse {\r\n return textResponse(JSON.stringify(payload))\r\n}\r\n\r\nexport function errorMessage(error: unknown): string {\r\n return error instanceof Error ? error.message : String(error)\r\n}\r\n\r\nexport function stampMcpContext(req: PayloadRequest): void {\r\n req.context = { ...req.context, source: 'mcp' }\r\n}\r\n\r\nexport function getDocDisplayName(doc: unknown, fallback: string): string {\r\n const d = doc as Record<string, unknown> | null | undefined\r\n return (\r\n (typeof d?.name === 'string' && d.name) ||\r\n (typeof d?.title === 'string' && d.title) ||\r\n (typeof d?.slug === 'string' && d.slug) ||\r\n fallback\r\n )\r\n}\r\n\r\nexport function requireDraftCollection(\r\n collection: string,\r\n draftCollections: Set<string>,\r\n noun = 'drafts',\r\n): McpTextResponse | null {\r\n if (draftCollections.has(collection)) return null\r\n return textResponse(\r\n `Error: Collection \"${collection}\" does not support ${noun}. ` +\r\n `Draft-enabled collections: ${[...draftCollections].join(', ') || 'none'}`,\r\n )\r\n}\r\n\r\n/**\r\n * Resolves the preview URL for a draft document by delegating to the\r\n * collection's own configured preview function (`admin.livePreview.url`\r\n * preferred, then `admin.preview`). Returns null when no function is\r\n * configured, when it fails, or when it returns a relative path with no\r\n * absolute `siteUrl` to anchor it.\r\n */\r\nexport async function resolvePreviewUrl(\r\n collection: CollectionConfig,\r\n doc: Record<string, unknown>,\r\n req: PayloadRequest,\r\n siteUrl: string | undefined,\r\n): Promise<string | null> {\r\n const admin = (collection.admin ?? {}) as Record<string, any>\r\n const locale = (req as unknown as { locale?: string }).locale ?? 'en'\r\n\r\n let raw: string | null | undefined\r\n\r\n const livePreviewUrl = admin.livePreview?.url\r\n if (typeof livePreviewUrl === 'function') {\r\n try {\r\n raw = await livePreviewUrl({\r\n data: doc,\r\n locale: { code: locale, label: locale },\r\n req,\r\n payload: req.payload,\r\n collectionConfig: collection,\r\n })\r\n } catch {\r\n raw = null\r\n }\r\n } else if (typeof livePreviewUrl === 'string') {\r\n raw = livePreviewUrl\r\n }\r\n\r\n if (!raw && typeof admin.preview === 'function') {\r\n try {\r\n raw = await admin.preview(doc, { locale, req, token: null })\r\n } catch {\r\n raw = null\r\n }\r\n }\r\n\r\n if (!raw || typeof raw !== 'string') return null\r\n\r\n if (raw.startsWith('http://') || raw.startsWith('https://')) return raw\r\n if (!siteUrl) return null\r\n\r\n const base = siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl\r\n const path = raw.startsWith('/') ? raw : `/${raw}`\r\n return `${base}${path}`\r\n}\r\n\r\n/**\r\n * If `doc` is a draft, appends a preview-URL hint to the MCP response so the\r\n * AI can present it to the user. Falls back to a generic admin-panel hint\r\n * when the collection has no preview function configured.\r\n *\r\n * Pure with respect to the response: a fresh content array is returned.\r\n */\r\nexport async function decorateDraftResponse(\r\n response: McpTextResponse,\r\n doc: Record<string, unknown> | null | undefined,\r\n collection: CollectionConfig | undefined,\r\n req: PayloadRequest,\r\n siteUrl: string | undefined,\r\n): Promise<McpTextResponse> {\r\n if (!doc || doc._status !== 'draft' || !collection) return response\r\n\r\n const previewUrl = await resolvePreviewUrl(collection, doc, req, siteUrl)\r\n const hint = previewUrl\r\n ? `\\nš This document is a draft. Preview it here: ${previewUrl}`\r\n : '\\nš This document is a draft. Use the admin panel to preview it.'\r\n\r\n return { content: [...response.content, { type: 'text', text: hint }] }\r\n}\r\n"],"names":["z","slugEnum","slugs","kind","enum","errorMap","message","join","DRAFT_NOTE","textResponse","text","content","type","jsonResponse","payload","JSON","stringify","errorMessage","error","Error","String","stampMcpContext","req","context","source","getDocDisplayName","doc","fallback","d","name","title","slug","requireDraftCollection","collection","draftCollections","noun","has","resolvePreviewUrl","siteUrl","admin","locale","raw","livePreviewUrl","livePreview","url","data","code","label","collectionConfig","preview","token","startsWith","base","endsWith","slice","path","decorateDraftResponse","response","_status","previewUrl","hint"],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared internals for patchLayout (collections) and patchGlobalLayout
|
|
3
|
+
* (globals). Both tools differ only in their fetch/write pairs and their
|
|
4
|
+
* nesting-map filter ā every other piece of logic (block validation,
|
|
5
|
+
* operation application, dotted-path navigation, error shape) is identical
|
|
6
|
+
* and lives here. Keeping them separate as tools is intentional from the
|
|
7
|
+
* LLM's perspective; keeping the code single-sourced prevents the kind of
|
|
8
|
+
* drift that originally let only one side learn about dotted paths.
|
|
9
|
+
*/
|
|
10
|
+
export declare function errorResponse(message: string, extra?: Record<string, unknown>): import("./_helpers").McpTextResponse;
|
|
11
|
+
/**
|
|
12
|
+
* Recursively validate a block array against an allow list, descending into
|
|
13
|
+
* each block's own `blocks`-typed fields when present. A value is treated
|
|
14
|
+
* as a nested blocks field whenever it is an array of objects that each
|
|
15
|
+
* carry a `blockType` discriminator; the nesting map decides which slugs
|
|
16
|
+
* are admissible at that position.
|
|
17
|
+
*/
|
|
18
|
+
export declare function validateBlockList(blocks: Array<Record<string, unknown>>, allowedSlugs: string[], pathLabel: string, allBlockSlugs: Set<string>, nestingByBlockField: Map<string, string[]>, errors: string[]): void;
|
|
19
|
+
/**
|
|
20
|
+
* Apply a list operation against an existing array of blocks.
|
|
21
|
+
* `full` always replaces; the rest preserve the existing array.
|
|
22
|
+
*/
|
|
23
|
+
export declare function applyOperation(newBlocks: Record<string, unknown>[], operation: 'full' | 'append' | 'prepend' | 'insertAt' | 'replaceAt', insertIndex: number | undefined, existingLayout: Record<string, unknown>[] | undefined): Record<string, unknown>[];
|
|
24
|
+
/** Walk a dotted path on an object and return the value at the leaf, or `[]`
|
|
25
|
+
* if any segment is missing or not an object. Used to pull the current
|
|
26
|
+
* blocks array out of a fetched document/global. */
|
|
27
|
+
export declare function readPath(obj: Record<string, unknown> | undefined, path: string): Record<string, unknown>[];
|
|
28
|
+
/**
|
|
29
|
+
* Write `value` at a dotted `path`, returning a new object whose top-level
|
|
30
|
+
* segment is a merge of `base`'s existing siblings with the patched leaf.
|
|
31
|
+
*
|
|
32
|
+
* Load-bearing for the sibling-wipe fix in patchGlobalLayout: Payload's
|
|
33
|
+
* `updateGlobal` only merges at the top level. So if the global is
|
|
34
|
+
* `{sections: {layout: [...], copyright: 'foo'}}` and we patch
|
|
35
|
+
* `sections.layout`, naively writing `{sections: {layout: [...]}}` makes
|
|
36
|
+
* Payload overwrite the entire `sections` group and `copyright` silently
|
|
37
|
+
* vanishes. By accepting `base` (the existing document) we can splice the
|
|
38
|
+
* new layout into a copy of every parent group along the dotted path,
|
|
39
|
+
* preserving siblings at every depth. For a flat (non-dotted) path this
|
|
40
|
+
* still produces `{[path]: value}` exactly as before ā Payload then merges
|
|
41
|
+
* other top-level fields normally ā so existing callers see no change.
|
|
42
|
+
*/
|
|
43
|
+
export declare function writePath(base: Record<string, unknown> | undefined, path: string, value: unknown): Record<string, unknown>;
|