@simmalugnt-se/payload-editor-assistant 0.1.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.
Files changed (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -0
  3. package/dist/capabilities.d.ts +10 -0
  4. package/dist/capabilities.js +43 -0
  5. package/dist/collections/approvals.d.ts +2 -0
  6. package/dist/collections/approvals.js +36 -0
  7. package/dist/collections/audit.d.ts +2 -0
  8. package/dist/collections/audit.js +40 -0
  9. package/dist/components/EditorAssistantAction.d.ts +1 -0
  10. package/dist/components/EditorAssistantAction.js +17 -0
  11. package/dist/components/EditorAssistantAside.d.ts +1 -0
  12. package/dist/components/EditorAssistantAside.js +335 -0
  13. package/dist/components/EditorAssistantDocumentBridge.d.ts +1 -0
  14. package/dist/components/EditorAssistantDocumentBridge.js +48 -0
  15. package/dist/components/EditorAssistantProvider.d.ts +8 -0
  16. package/dist/components/EditorAssistantProvider.js +14 -0
  17. package/dist/components/assistant-api.d.ts +9 -0
  18. package/dist/components/assistant-api.js +34 -0
  19. package/dist/components/assistant-context.d.ts +66 -0
  20. package/dist/components/assistant-context.js +69 -0
  21. package/dist/config/denied-fields.d.ts +3 -0
  22. package/dist/config/denied-fields.js +34 -0
  23. package/dist/config/validate.d.ts +15 -0
  24. package/dist/config/validate.js +86 -0
  25. package/dist/domain/allowlist.d.ts +12 -0
  26. package/dist/domain/allowlist.js +14 -0
  27. package/dist/domain/audit.d.ts +17 -0
  28. package/dist/domain/audit.js +25 -0
  29. package/dist/domain/content.d.ts +30 -0
  30. package/dist/domain/content.js +117 -0
  31. package/dist/domain/decide.d.ts +30 -0
  32. package/dist/domain/decide.js +162 -0
  33. package/dist/domain/guard.d.ts +13 -0
  34. package/dist/domain/guard.js +29 -0
  35. package/dist/domain/local-api.d.ts +16 -0
  36. package/dist/domain/local-api.js +3 -0
  37. package/dist/domain/navigate.d.ts +25 -0
  38. package/dist/domain/navigate.js +85 -0
  39. package/dist/domain/propose.d.ts +26 -0
  40. package/dist/domain/propose.js +150 -0
  41. package/dist/domain/relations.d.ts +12 -0
  42. package/dist/domain/relations.js +103 -0
  43. package/dist/endpoints/chat.d.ts +3 -0
  44. package/dist/endpoints/chat.js +88 -0
  45. package/dist/endpoints/csrf.d.ts +6 -0
  46. package/dist/endpoints/csrf.js +57 -0
  47. package/dist/endpoints/decide.d.ts +3 -0
  48. package/dist/endpoints/decide.js +33 -0
  49. package/dist/endpoints/propose.d.ts +3 -0
  50. package/dist/endpoints/propose.js +32 -0
  51. package/dist/exports/client.d.ts +4 -0
  52. package/dist/exports/client.js +4 -0
  53. package/dist/form/compile.d.ts +17 -0
  54. package/dist/form/compile.js +168 -0
  55. package/dist/form/diff.d.ts +9 -0
  56. package/dist/form/diff.js +155 -0
  57. package/dist/form/empty.d.ts +2 -0
  58. package/dist/form/empty.js +79 -0
  59. package/dist/form/hash.d.ts +2 -0
  60. package/dist/form/hash.js +22 -0
  61. package/dist/form/operations.d.ts +52 -0
  62. package/dist/form/operations.js +52 -0
  63. package/dist/form/path.d.ts +5 -0
  64. package/dist/form/path.js +79 -0
  65. package/dist/form/shape.d.ts +19 -0
  66. package/dist/form/shape.js +137 -0
  67. package/dist/index.d.ts +5 -0
  68. package/dist/index.js +3 -0
  69. package/dist/package-name.d.ts +7 -0
  70. package/dist/package-name.js +7 -0
  71. package/dist/plugin.d.ts +3 -0
  72. package/dist/plugin.js +110 -0
  73. package/dist/runtime/agent.d.ts +42 -0
  74. package/dist/runtime/agent.js +262 -0
  75. package/dist/runtime/context.d.ts +37 -0
  76. package/dist/runtime/context.js +92 -0
  77. package/dist/runtime/inflight.d.ts +2 -0
  78. package/dist/runtime/inflight.js +11 -0
  79. package/dist/runtime/language.d.ts +10 -0
  80. package/dist/runtime/language.js +27 -0
  81. package/dist/runtime/propose-loop.d.ts +7 -0
  82. package/dist/runtime/propose-loop.js +11 -0
  83. package/dist/runtime/rate-limit.d.ts +1 -0
  84. package/dist/runtime/rate-limit.js +14 -0
  85. package/dist/runtime/system-prompt.d.ts +3 -0
  86. package/dist/runtime/system-prompt.js +110 -0
  87. package/dist/runtime/tools.d.ts +56 -0
  88. package/dist/runtime/tools.js +189 -0
  89. package/dist/runtime/voice.d.ts +11 -0
  90. package/dist/runtime/voice.js +55 -0
  91. package/dist/schema/discover.d.ts +35 -0
  92. package/dist/schema/discover.js +93 -0
  93. package/dist/schema/fields.d.ts +21 -0
  94. package/dist/schema/fields.js +139 -0
  95. package/dist/schema/project.d.ts +15 -0
  96. package/dist/schema/project.js +111 -0
  97. package/dist/styles/assistant.css +362 -0
  98. package/dist/types.d.ts +61 -0
  99. package/dist/types.js +3 -0
  100. package/dist/view.d.ts +22 -0
  101. package/dist/view.js +72 -0
  102. package/package.json +73 -0
@@ -0,0 +1,34 @@
1
+ import { CSRF_HEADER, ENDPOINT_PREFIX } from "../package-name.js";
2
+ export async function assistantRequest(apiBase, path, body, signal) {
3
+ const csrfResponse = await fetch(`${apiBase}${ENDPOINT_PREFIX}/csrf`, {
4
+ credentials: "include",
5
+ signal,
6
+ });
7
+ if (!csrfResponse.ok) {
8
+ return { ok: false, error: "unauthorized", status: csrfResponse.status };
9
+ }
10
+ const csrf = (await csrfResponse.json());
11
+ if (!csrf.token) {
12
+ return { ok: false, error: "unauthorized", status: 401 };
13
+ }
14
+ const response = await fetch(`${apiBase}${ENDPOINT_PREFIX}${path}`, {
15
+ method: "POST",
16
+ credentials: "include",
17
+ signal,
18
+ headers: {
19
+ "Content-Type": "application/json",
20
+ [CSRF_HEADER]: csrf.token,
21
+ },
22
+ body: JSON.stringify(body),
23
+ });
24
+ const payload = (await response.json().catch(() => ({})));
25
+ if (!response.ok) {
26
+ return {
27
+ ok: false,
28
+ error: payload.error ?? "provider_error",
29
+ message: payload.message,
30
+ status: response.status,
31
+ };
32
+ }
33
+ return { ok: true, data: payload };
34
+ }
@@ -0,0 +1,66 @@
1
+ import { type ReactNode } from "react";
2
+ import type { AssistantShortcut } from "../types.ts";
3
+ export type AssistantChromeStatus = "idle" | "unavailable" | "unauthorized" | "rate_limited" | "timeout" | "provider_error" | "tool_denied" | "stale_context" | "aborted" | "streaming";
4
+ export type DocumentBridgeSnapshot = {
5
+ collectionSlug?: string;
6
+ globalSlug?: string;
7
+ documentId?: string | number;
8
+ locale: string;
9
+ title?: string;
10
+ modified: boolean;
11
+ locked?: boolean;
12
+ hasSavePermission?: boolean;
13
+ uploadStatus?: string;
14
+ disabled?: boolean;
15
+ };
16
+ export type FormAdapter = {
17
+ getData: () => Record<string, unknown>;
18
+ reset: (data: Record<string, unknown>) => Promise<void> | void;
19
+ setModified: (modified: boolean) => void;
20
+ };
21
+ export type PendingApproval = {
22
+ title: string;
23
+ summary: string;
24
+ approvalId: string;
25
+ nonce: string;
26
+ capability: "form.propose" | "draft.create";
27
+ };
28
+ export type AssistantMessage = {
29
+ id: string;
30
+ role: "user" | "assistant" | "system";
31
+ text: string;
32
+ candidates?: Array<{
33
+ href: string;
34
+ label: string;
35
+ }>;
36
+ };
37
+ type AssistantContextValue = {
38
+ open: boolean;
39
+ collapsed: boolean;
40
+ status: AssistantChromeStatus;
41
+ messages: AssistantMessage[];
42
+ shortcuts: AssistantShortcut[];
43
+ /** Allowlisted collection slugs. */
44
+ collections: string[];
45
+ bridge: DocumentBridgeSnapshot | null;
46
+ formAdapter: FormAdapter | null;
47
+ pendingApproval: PendingApproval | null;
48
+ setOpen: (open: boolean) => void;
49
+ setCollapsed: (collapsed: boolean) => void;
50
+ setBridge: (bridge: DocumentBridgeSnapshot | null) => void;
51
+ setFormAdapter: (adapter: FormAdapter | null) => void;
52
+ setPendingApproval: (pending: PendingApproval | null) => void;
53
+ setStatus: (status: AssistantChromeStatus) => void;
54
+ setMessages: (updater: (current: AssistantMessage[]) => AssistantMessage[]) => void;
55
+ resetConversation: () => void;
56
+ stop: () => void;
57
+ abortController: AbortController | null;
58
+ setAbortController: (controller: AbortController | null) => void;
59
+ };
60
+ export declare function AssistantStateProvider({ children, shortcuts, collections, }: {
61
+ children: ReactNode;
62
+ shortcuts: AssistantShortcut[];
63
+ collections: string[];
64
+ }): import("react").JSX.Element;
65
+ export declare function useAssistantState(): AssistantContextValue;
66
+ export {};
@@ -0,0 +1,69 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useCallback, useContext, useMemo, useState } from "react";
4
+ const AssistantContext = createContext(null);
5
+ export function AssistantStateProvider({ children, shortcuts, collections, }) {
6
+ const [open, setOpen] = useState(true);
7
+ const [collapsed, setCollapsed] = useState(false);
8
+ const [status, setStatus] = useState("idle");
9
+ const [messages, setMessages] = useState([]);
10
+ const [bridge, setBridge] = useState(null);
11
+ const [formAdapter, setFormAdapter] = useState(null);
12
+ const [pendingApproval, setPendingApproval] = useState(null);
13
+ const [abortController, setAbortController] = useState(null);
14
+ const stop = useCallback(() => {
15
+ abortController?.abort();
16
+ setAbortController(null);
17
+ setStatus("aborted");
18
+ }, [abortController]);
19
+ const resetConversation = useCallback(() => {
20
+ abortController?.abort();
21
+ setAbortController(null);
22
+ setStatus("idle");
23
+ setMessages([]);
24
+ setPendingApproval(null);
25
+ }, [abortController]);
26
+ const value = useMemo(() => ({
27
+ open,
28
+ collapsed,
29
+ status,
30
+ messages,
31
+ shortcuts,
32
+ collections,
33
+ bridge,
34
+ formAdapter,
35
+ pendingApproval,
36
+ setOpen,
37
+ setCollapsed,
38
+ setBridge,
39
+ setFormAdapter,
40
+ setPendingApproval,
41
+ setStatus,
42
+ setMessages,
43
+ resetConversation,
44
+ stop,
45
+ abortController,
46
+ setAbortController,
47
+ }), [
48
+ open,
49
+ collapsed,
50
+ status,
51
+ messages,
52
+ shortcuts,
53
+ collections,
54
+ bridge,
55
+ formAdapter,
56
+ pendingApproval,
57
+ resetConversation,
58
+ stop,
59
+ abortController,
60
+ ]);
61
+ return _jsx(AssistantContext.Provider, { value: value, children: children });
62
+ }
63
+ export function useAssistantState() {
64
+ const value = useContext(AssistantContext);
65
+ if (!value) {
66
+ throw new Error("useAssistantState must be used within EditorAssistantProvider");
67
+ }
68
+ return value;
69
+ }
@@ -0,0 +1,3 @@
1
+ export declare const DENIED_COLLECTION_SLUGS: Set<string>;
2
+ export declare function isDeniedFieldName(name: string): boolean;
3
+ export declare function isDeniedCollectionSlug(slug: string): boolean;
@@ -0,0 +1,34 @@
1
+ const DENIED_FIELD_NAMES = new Set([
2
+ "password",
3
+ "hash",
4
+ "salt",
5
+ "token",
6
+ "secret",
7
+ "apiKey",
8
+ "api_key",
9
+ "resetPasswordToken",
10
+ "resetPasswordExpiration",
11
+ "loginAttempts",
12
+ "lockUntil",
13
+ "sessions",
14
+ "collection",
15
+ "_status",
16
+ "createdAt",
17
+ "updatedAt",
18
+ "deletedAt",
19
+ ]);
20
+ const DENIED_NAME_PATTERN = /(password|secret|token|api[-_]?key|hash|salt|credential)/i;
21
+ export const DENIED_COLLECTION_SLUGS = new Set([
22
+ "users",
23
+ "payload-preferences",
24
+ "payload-migrations",
25
+ ]);
26
+ export function isDeniedFieldName(name) {
27
+ if (DENIED_FIELD_NAMES.has(name)) {
28
+ return true;
29
+ }
30
+ return DENIED_NAME_PATTERN.test(name);
31
+ }
32
+ export function isDeniedCollectionSlug(slug) {
33
+ return DENIED_COLLECTION_SLUGS.has(slug) || slug.startsWith("payload-");
34
+ }
@@ -0,0 +1,15 @@
1
+ import type { EditorAssistantPluginOptions, EntityAllowlist, HostToolDefinition } from "../types.ts";
2
+ export type ValidatedEditorAssistantOptions = EditorAssistantPluginOptions & {
3
+ disabled: boolean;
4
+ collections: Record<string, EntityAllowlist>;
5
+ globals: Record<string, EntityAllowlist>;
6
+ denyFields: string[];
7
+ shortcuts: NonNullable<EditorAssistantPluginOptions["shortcuts"]>;
8
+ tools: HostToolDefinition[];
9
+ trustedOrigins: string[];
10
+ audit: {
11
+ collection: string;
12
+ retentionDays?: number;
13
+ };
14
+ };
15
+ export declare function validateEditorAssistantOptions(options: EditorAssistantPluginOptions): ValidatedEditorAssistantOptions;
@@ -0,0 +1,86 @@
1
+ import { BUILTIN_RISK, EMBEDDED_ONLY_CAPABILITIES, isBuiltinCapability, isEntityCapability, } from "../capabilities.js";
2
+ import { isDeniedCollectionSlug } from "./denied-fields.js";
3
+ const RISK_ORDER = { auto: 0, approval: 1, forbidden: 2 };
4
+ export function validateEditorAssistantOptions(options) {
5
+ const disabled = options.disabled === true;
6
+ const collections = options.collections ?? {};
7
+ const globals = options.globals ?? {};
8
+ if (!disabled) {
9
+ if (!options.model) {
10
+ throw new Error("payload-editor-assistant: `model` is required when the plugin is enabled.");
11
+ }
12
+ validateEntityMap("collections", collections, { allowDraftCreate: true });
13
+ validateEntityMap("globals", globals, { allowDraftCreate: false });
14
+ validateHostTools(options.tools ?? []);
15
+ }
16
+ return {
17
+ ...options,
18
+ disabled,
19
+ collections,
20
+ globals,
21
+ denyFields: options.denyFields ?? [],
22
+ shortcuts: options.shortcuts ?? [],
23
+ tools: options.tools ?? [],
24
+ trustedOrigins: options.trustedOrigins ?? [],
25
+ audit: {
26
+ collection: options.audit?.collection ?? "editor-assistant-audit",
27
+ retentionDays: options.audit?.retentionDays,
28
+ },
29
+ };
30
+ }
31
+ function validateEntityMap(kind, map, rules) {
32
+ for (const [slug, entry] of Object.entries(map)) {
33
+ if (kind === "collections" && isDeniedCollectionSlug(slug)) {
34
+ throw new Error(`payload-editor-assistant: collection "${slug}" cannot be allowlisted.`);
35
+ }
36
+ if (!entry?.capabilities?.length) {
37
+ throw new Error(`payload-editor-assistant: ${kind} "${slug}" must declare at least one capability.`);
38
+ }
39
+ for (const capability of entry.capabilities) {
40
+ if (!isEntityCapability(capability)) {
41
+ throw new Error(`payload-editor-assistant: unknown capability "${capability}" on ${kind} "${slug}".`);
42
+ }
43
+ if (capability === "draft.create" && !rules.allowDraftCreate) {
44
+ throw new Error(`payload-editor-assistant: globals cannot enable draft.create ("${slug}").`);
45
+ }
46
+ }
47
+ validateFields(entry, `${kind} "${slug}"`);
48
+ }
49
+ }
50
+ function validateFields(entry, label) {
51
+ const fields = entry.fields;
52
+ if (fields === undefined || fields === true) {
53
+ return;
54
+ }
55
+ if ("include" in fields && "exclude" in fields) {
56
+ throw new Error(`payload-editor-assistant: ${label} fields cannot set both include and exclude.`);
57
+ }
58
+ if ("include" in fields && !Array.isArray(fields.include)) {
59
+ throw new Error(`payload-editor-assistant: ${label} fields.include must be an array.`);
60
+ }
61
+ if ("exclude" in fields && !Array.isArray(fields.exclude)) {
62
+ throw new Error(`payload-editor-assistant: ${label} fields.exclude must be an array.`);
63
+ }
64
+ }
65
+ function validateHostTools(tools) {
66
+ for (const tool of tools) {
67
+ if (!tool.id || !tool.capabilityId) {
68
+ throw new Error("payload-editor-assistant: host tools require id and capabilityId.");
69
+ }
70
+ const risk = tool.risk ?? "approval";
71
+ if (risk !== "auto" && risk !== "approval" && risk !== "forbidden") {
72
+ throw new Error(`payload-editor-assistant: host tool "${tool.id}" has invalid risk.`);
73
+ }
74
+ if (isBuiltinCapability(tool.capabilityId)) {
75
+ if (RISK_ORDER[risk] < RISK_ORDER[BUILTIN_RISK[tool.capabilityId]]) {
76
+ throw new Error(`payload-editor-assistant: host tool "${tool.id}" cannot loosen risk below ${tool.capabilityId}.`);
77
+ }
78
+ if (tool.mcp && EMBEDDED_ONLY_CAPABILITIES.has(tool.capabilityId)) {
79
+ throw new Error(`payload-editor-assistant: host tool "${tool.id}" cannot mark ${tool.capabilityId} as MCP-eligible.`);
80
+ }
81
+ }
82
+ if (risk === "auto" && tool.mcp === true && tool.capabilityId === "form.propose") {
83
+ throw new Error(`payload-editor-assistant: host tool "${tool.id}" cannot expose form writes to MCP.`);
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,12 @@
1
+ import type { EntityCapability } from "../capabilities.ts";
2
+ import type { ValidatedEditorAssistantOptions } from "../config/validate.ts";
3
+ import type { EntityAllowlist } from "../types.ts";
4
+ export declare function resolveAllowlist(options: ValidatedEditorAssistantOptions, target: {
5
+ collection?: string;
6
+ global?: string;
7
+ }): {
8
+ kind: "collection" | "global";
9
+ slug: string;
10
+ allowlist: EntityAllowlist;
11
+ } | null;
12
+ export declare function hasCapability(allowlist: EntityAllowlist, capability: EntityCapability): boolean;
@@ -0,0 +1,14 @@
1
+ export function resolveAllowlist(options, target) {
2
+ if (target.collection) {
3
+ const allowlist = options.collections[target.collection];
4
+ return allowlist ? { kind: "collection", slug: target.collection, allowlist } : null;
5
+ }
6
+ if (target.global) {
7
+ const allowlist = options.globals[target.global];
8
+ return allowlist ? { kind: "global", slug: target.global, allowlist } : null;
9
+ }
10
+ return null;
11
+ }
12
+ export function hasCapability(allowlist, capability) {
13
+ return allowlist.capabilities.includes(capability);
14
+ }
@@ -0,0 +1,17 @@
1
+ import type { PayloadRequest } from "payload";
2
+ import type { ValidatedEditorAssistantOptions } from "../config/validate.ts";
3
+ export declare function writeAudit(req: PayloadRequest, options: ValidatedEditorAssistantOptions, event: {
4
+ capability: string;
5
+ risk: string;
6
+ outcome: string;
7
+ targetCollection?: string;
8
+ targetGlobal?: string;
9
+ documentId?: string;
10
+ locale?: string;
11
+ fieldPaths?: string[];
12
+ approvalId?: string;
13
+ nonceId?: string;
14
+ canonicalHash?: string;
15
+ baseRevision?: string;
16
+ errorClass?: string;
17
+ }): Promise<void>;
@@ -0,0 +1,25 @@
1
+ import { localApi } from "./local-api.js";
2
+ export async function writeAudit(req, options, event) {
3
+ await localApi(req).create({
4
+ collection: options.audit.collection,
5
+ data: {
6
+ actor: String(req.user?.id ?? ""),
7
+ capability: event.capability,
8
+ risk: event.risk,
9
+ outcome: event.outcome,
10
+ targetCollection: event.targetCollection,
11
+ targetGlobal: event.targetGlobal,
12
+ documentId: event.documentId,
13
+ locale: event.locale,
14
+ fieldPaths: event.fieldPaths,
15
+ approvalId: event.approvalId,
16
+ nonceId: event.nonceId,
17
+ canonicalHash: event.canonicalHash,
18
+ baseRevision: event.baseRevision,
19
+ errorClass: event.errorClass,
20
+ },
21
+ req,
22
+ overrideAccess: false,
23
+ user: req.user,
24
+ });
25
+ }
@@ -0,0 +1,30 @@
1
+ import type { PayloadRequest } from "payload";
2
+ import type { ValidatedEditorAssistantOptions } from "../config/validate.ts";
3
+ export type ContentFindInput = {
4
+ collection?: string;
5
+ query: string;
6
+ locale: string;
7
+ };
8
+ export type ContentFindByIdInput = {
9
+ collection?: string;
10
+ global?: string;
11
+ documentId?: string | number;
12
+ locale: string;
13
+ };
14
+ export declare function findContent(req: PayloadRequest, options: ValidatedEditorAssistantOptions, input: ContentFindInput): Promise<{
15
+ ok: true;
16
+ results: Record<string, unknown>[];
17
+ } | {
18
+ ok: false;
19
+ error: string;
20
+ message: string;
21
+ }>;
22
+ export declare function findContentById(req: PayloadRequest, options: ValidatedEditorAssistantOptions, input: ContentFindByIdInput): Promise<{
23
+ ok: true;
24
+ data: Record<string, unknown>;
25
+ truncated: boolean;
26
+ } | {
27
+ ok: false;
28
+ error: string;
29
+ message: string;
30
+ }>;
@@ -0,0 +1,117 @@
1
+ import { entityFields, findCollectionConfig, findGlobalConfig } from "../schema/discover.js";
2
+ import { projectIdentity, projectRecord } from "../schema/project.js";
3
+ import { hasCapability, resolveAllowlist } from "./allowlist.js";
4
+ import { localApi } from "./local-api.js";
5
+ const FIND_LIMIT = 8;
6
+ export async function findContent(req, options, input) {
7
+ const query = input.query.trim();
8
+ const slugs = input.collection
9
+ ? [input.collection]
10
+ : Object.entries(options.collections)
11
+ .filter(([, allowlist]) => hasCapability(allowlist, "content.find"))
12
+ .map(([slug]) => slug);
13
+ const results = [];
14
+ for (const slug of slugs) {
15
+ const resolved = resolveAllowlist(options, { collection: slug });
16
+ if (!resolved || !hasCapability(resolved.allowlist, "content.find")) {
17
+ if (input.collection) {
18
+ return {
19
+ ok: false,
20
+ error: "tool_denied",
21
+ message: "Collection is not allowlisted for search.",
22
+ };
23
+ }
24
+ continue;
25
+ }
26
+ const config = findCollectionConfig(req, slug);
27
+ if (!config) {
28
+ continue;
29
+ }
30
+ try {
31
+ const found = await localApi(req).find({
32
+ collection: slug,
33
+ ...(query ? { where: searchWhere(query, config.admin?.useAsTitle) } : {}),
34
+ limit: FIND_LIMIT,
35
+ depth: 0,
36
+ locale: input.locale,
37
+ draft: true,
38
+ sort: "-updatedAt",
39
+ req,
40
+ user: req.user,
41
+ overrideAccess: false,
42
+ });
43
+ for (const doc of found.docs) {
44
+ results.push({
45
+ collection: slug,
46
+ ...projectIdentity(doc, input.locale),
47
+ });
48
+ }
49
+ }
50
+ catch {
51
+ if (input.collection) {
52
+ return { ok: false, error: "tool_denied", message: "Search failed." };
53
+ }
54
+ }
55
+ if (results.length >= FIND_LIMIT) {
56
+ break;
57
+ }
58
+ }
59
+ return { ok: true, results: results.slice(0, FIND_LIMIT) };
60
+ }
61
+ export async function findContentById(req, options, input) {
62
+ const resolved = resolveAllowlist(options, input);
63
+ if (!resolved || !hasCapability(resolved.allowlist, "content.findById")) {
64
+ return { ok: false, error: "tool_denied", message: "Target is not allowlisted for read." };
65
+ }
66
+ const fields = entityFields(req, options, input);
67
+ try {
68
+ if (input.global) {
69
+ const global = findGlobalConfig(req, input.global);
70
+ if (!global) {
71
+ return { ok: false, error: "tool_denied", message: "Global was not found." };
72
+ }
73
+ const doc = await localApi(req).findGlobal({
74
+ slug: input.global,
75
+ depth: 0,
76
+ locale: input.locale,
77
+ draft: true,
78
+ req,
79
+ user: req.user,
80
+ overrideAccess: false,
81
+ });
82
+ const projected = projectRecord(doc, fields, options.redact, input.global);
83
+ return { ok: true, ...projected };
84
+ }
85
+ if (!input.collection || input.documentId === undefined) {
86
+ return {
87
+ ok: false,
88
+ error: "tool_denied",
89
+ message: "A collection and document id are required.",
90
+ };
91
+ }
92
+ const doc = await localApi(req).findByID({
93
+ collection: input.collection,
94
+ id: input.documentId,
95
+ depth: 0,
96
+ locale: input.locale,
97
+ draft: true,
98
+ req,
99
+ user: req.user,
100
+ overrideAccess: false,
101
+ });
102
+ const projected = projectRecord(doc, fields, options.redact, input.collection);
103
+ return { ok: true, ...projected };
104
+ }
105
+ catch {
106
+ return { ok: false, error: "tool_denied", message: "Document is not accessible." };
107
+ }
108
+ }
109
+ function searchWhere(query, useAsTitle) {
110
+ const clauses = [{ id: { equals: query } }];
111
+ const titleField = useAsTitle && useAsTitle !== "id" ? useAsTitle : "title";
112
+ clauses.push({ [titleField]: { contains: query } });
113
+ if (titleField !== "slug") {
114
+ clauses.push({ slug: { contains: query } });
115
+ }
116
+ return { or: clauses };
117
+ }
@@ -0,0 +1,30 @@
1
+ import type { PayloadRequest } from "payload";
2
+ import type { ValidatedEditorAssistantOptions } from "../config/validate.ts";
3
+ export type DecideInput = {
4
+ action: "approve" | "reject";
5
+ approvalId: string;
6
+ nonce: string;
7
+ collection?: string;
8
+ global?: string;
9
+ documentId?: string | number;
10
+ locale: string;
11
+ formData?: Record<string, unknown>;
12
+ };
13
+ export type DecideResult = {
14
+ ok: true;
15
+ capability: "form.propose";
16
+ patchedData: Record<string, unknown>;
17
+ } | {
18
+ ok: true;
19
+ capability: "draft.create";
20
+ documentId: string;
21
+ collection: string;
22
+ } | {
23
+ ok: true;
24
+ rejected: true;
25
+ } | {
26
+ ok: false;
27
+ error: "tool_denied" | "stale_context";
28
+ message: string;
29
+ };
30
+ export declare function decideChange(req: PayloadRequest, options: ValidatedEditorAssistantOptions, input: DecideInput): Promise<DecideResult>;