@mvriu5/payload-ai 0.5.6 → 0.5.9

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 (39) hide show
  1. package/README.md +52 -5
  2. package/dist/ai/providerOptions.d.ts +1 -14
  3. package/dist/ai/providerOptions.js +0 -23
  4. package/dist/ai/providerRuntime.d.ts +3 -2
  5. package/dist/ai/providerRuntime.js +6 -15
  6. package/dist/components/AIActionProposalList.d.ts +2 -1
  7. package/dist/components/AIActionProposalList.js +74 -6
  8. package/dist/components/AIActionProposalList.module.css +49 -5
  9. package/dist/components/AIApiKeyField.js +0 -8
  10. package/dist/components/AIInput.d.ts +1 -1
  11. package/dist/components/AIInput.js +156 -23
  12. package/dist/components/AIInput.module.css +185 -147
  13. package/dist/components/CollectionMentionPopover.d.ts +1 -1
  14. package/dist/components/DiffDialog.d.ts +12 -0
  15. package/dist/components/DiffDialog.js +310 -0
  16. package/dist/components/DiffDialog.module.css +249 -0
  17. package/dist/components/Icons.d.ts +9 -0
  18. package/dist/components/Icons.js +266 -0
  19. package/dist/components/RecentChangesList.d.ts +18 -0
  20. package/dist/components/RecentChangesList.js +66 -0
  21. package/dist/components/RecentChangesList.module.css +135 -0
  22. package/dist/components/hooks/useAISettings.d.ts +1 -1
  23. package/dist/endpoints/aiApplyActionEndpointHandler.d.ts +3 -1
  24. package/dist/endpoints/aiApplyActionEndpointHandler.js +209 -8
  25. package/dist/endpoints/aiChatEndpointHandler.d.ts +5 -1
  26. package/dist/endpoints/aiChatEndpointHandler.js +106 -21
  27. package/dist/endpoints/aiMentionSuggestionsEndpointHandler.d.ts +2 -1
  28. package/dist/endpoints/aiMentionSuggestionsEndpointHandler.js +7 -7
  29. package/dist/endpoints/aiProposalDiffEndpointHandler.d.ts +7 -0
  30. package/dist/endpoints/aiProposalDiffEndpointHandler.js +138 -0
  31. package/dist/endpoints/aiRecentChangesEndpointHandler.d.ts +6 -0
  32. package/dist/endpoints/aiRecentChangesEndpointHandler.js +35 -0
  33. package/dist/index.d.ts +3 -1
  34. package/dist/index.js +140 -18
  35. package/dist/payload/collectionPermissions.d.ts +22 -0
  36. package/dist/payload/collectionPermissions.js +68 -0
  37. package/dist/payload/schemaContext.d.ts +46 -2
  38. package/dist/payload/schemaContext.js +105 -14
  39. package/package.json +113 -121
@@ -1,6 +1,7 @@
1
1
  import { verifyAIActionProposal } from "../ai/proposals.js";
2
- import { containsSensitiveData } from "../ai/sensitiveData.js";
2
+ import { containsSensitiveData, redactSensitiveData } from "../ai/sensitiveData.js";
3
3
  import { getCollectionFields, isAuthCollection, normalizeAuthData, normalizeDataForFields } from "../payload/normalizeData.js";
4
+ import { isCollectionActionAllowed } from "../payload/collectionPermissions.js";
4
5
  const getProposalMeta = (proposal)=>{
5
6
  if (!proposal) return {};
6
7
  return {
@@ -15,12 +16,13 @@ const getAppliedDocReference = (doc)=>{
15
16
  id: doc.id
16
17
  };
17
18
  };
18
- const isKnownCollection = (req, collection)=>{
19
- return req.payload.config.collections.some((item)=>item.slug === collection);
20
- };
21
- const isAllowedCollection = (req, collection, collections)=>{
22
- if (!collections) return isKnownCollection(req, collection);
23
- return collections.includes(collection) && isKnownCollection(req, collection);
19
+ const isAllowedCollection = (req, collection, collections, action = "read")=>{
20
+ return isCollectionActionAllowed({
21
+ action,
22
+ permissions: collections,
23
+ req,
24
+ slug: collection
25
+ });
24
26
  };
25
27
  const isKnownGlobal = (req, slug)=>{
26
28
  return req.payload.config.globals?.some((item)=>item.slug === slug) || false;
@@ -28,6 +30,151 @@ const isKnownGlobal = (req, slug)=>{
28
30
  const isRecord = (value)=>{
29
31
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
30
32
  };
33
+ const mergeData = (current, next)=>{
34
+ return Object.fromEntries(Object.entries({
35
+ ...current,
36
+ ...next
37
+ }).map(([key, value])=>{
38
+ const currentValue = current[key];
39
+ if (isRecord(currentValue) && isRecord(value)) {
40
+ return [
41
+ key,
42
+ mergeData(currentValue, value)
43
+ ];
44
+ }
45
+ return [
46
+ key,
47
+ value
48
+ ];
49
+ }));
50
+ };
51
+ const getJSONLineKey = (line)=>{
52
+ const match = /^(\s*)"([^"]+)":/.exec(line);
53
+ return match ? `${match[1]}${match[2]}` : null;
54
+ };
55
+ const countDiffChanges = ({ after, before })=>{
56
+ const beforeLines = JSON.stringify(before, null, 2).split("\n");
57
+ const afterLines = JSON.stringify(after, null, 2).split("\n");
58
+ const dp = Array.from({
59
+ length: beforeLines.length + 1
60
+ }, ()=>Array(afterLines.length + 1).fill(0));
61
+ let additions = 0;
62
+ let removals = 0;
63
+ for(let i = beforeLines.length - 1; i >= 0; i -= 1){
64
+ for(let j = afterLines.length - 1; j >= 0; j -= 1){
65
+ dp[i][j] = beforeLines[i] === afterLines[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
66
+ }
67
+ }
68
+ let beforeIndex = 0;
69
+ let afterIndex = 0;
70
+ while(beforeIndex < beforeLines.length && afterIndex < afterLines.length){
71
+ if (beforeLines[beforeIndex] === afterLines[afterIndex]) {
72
+ beforeIndex += 1;
73
+ afterIndex += 1;
74
+ continue;
75
+ }
76
+ const beforeKey = getJSONLineKey(beforeLines[beforeIndex]);
77
+ const afterKey = getJSONLineKey(afterLines[afterIndex]);
78
+ if (beforeKey && beforeKey === afterKey) {
79
+ removals += 1;
80
+ additions += 1;
81
+ beforeIndex += 1;
82
+ afterIndex += 1;
83
+ continue;
84
+ }
85
+ if (dp[beforeIndex + 1][afterIndex] >= dp[beforeIndex][afterIndex + 1]) {
86
+ removals += 1;
87
+ beforeIndex += 1;
88
+ } else {
89
+ additions += 1;
90
+ afterIndex += 1;
91
+ }
92
+ }
93
+ removals += beforeLines.length - beforeIndex;
94
+ additions += afterLines.length - afterIndex;
95
+ return {
96
+ additions,
97
+ removals
98
+ };
99
+ };
100
+ const getTargetURL = ({ documentID, proposal, req })=>{
101
+ const adminRoute = req.payload.config.routes.admin || "/admin";
102
+ if (proposal.action === "updateGlobal") {
103
+ return `${adminRoute}/globals/${proposal.slug}`;
104
+ }
105
+ if (proposal.action === "delete") {
106
+ return null;
107
+ }
108
+ const id = proposal.action === "create" ? documentID : proposal.id;
109
+ if (!id) return null;
110
+ return `${adminRoute}/collections/${proposal.collection}/${id}`;
111
+ };
112
+ const getUserID = (req)=>{
113
+ const user = req.user;
114
+ return user?.id === undefined ? undefined : String(user.id);
115
+ };
116
+ const logAIChange = async ({ changeLogCollection, target, proposal, req })=>{
117
+ if (!changeLogCollection) return null;
118
+ const before = redactSensitiveData(target.before);
119
+ const after = redactSensitiveData(target.after);
120
+ const { additions, removals } = countDiffChanges({
121
+ after,
122
+ before
123
+ });
124
+ const proposalForLog = {
125
+ ...proposal
126
+ };
127
+ delete proposalForLog._aiSignature;
128
+ try {
129
+ await req.payload.create({
130
+ collection: changeLogCollection,
131
+ data: {
132
+ action: proposal.action,
133
+ additions,
134
+ after,
135
+ before,
136
+ collection: "collection" in proposal ? proposal.collection : undefined,
137
+ documentID: target.documentID === undefined ? undefined : String(target.documentID),
138
+ proposal: redactSensitiveData(proposalForLog),
139
+ removals,
140
+ slug: "slug" in proposal ? proposal.slug : undefined,
141
+ targetType: proposal.action === "updateGlobal" ? "global" : "collection",
142
+ targetURL: getTargetURL({
143
+ documentID: target.documentID,
144
+ proposal,
145
+ req
146
+ }),
147
+ title: proposal.label,
148
+ userID: getUserID(req)
149
+ },
150
+ overrideAccess: true,
151
+ req
152
+ });
153
+ return {
154
+ action: proposal.action,
155
+ additions,
156
+ after,
157
+ before,
158
+ collection: "collection" in proposal ? proposal.collection : null,
159
+ documentID: target.documentID === undefined ? null : String(target.documentID),
160
+ removals,
161
+ slug: "slug" in proposal ? proposal.slug : null,
162
+ title: proposal.label,
163
+ url: getTargetURL({
164
+ documentID: target.documentID,
165
+ proposal,
166
+ req
167
+ })
168
+ };
169
+ } catch (err) {
170
+ req.payload.logger.error({
171
+ err,
172
+ msg: "AI change log entry could not be written",
173
+ proposal: getProposalMeta(proposal)
174
+ });
175
+ return null;
176
+ }
177
+ };
31
178
  const isActionProposal = (proposal)=>{
32
179
  if (!isRecord(proposal) || typeof proposal.label !== "string") return false;
33
180
  if (proposal.action === "create") {
@@ -82,18 +229,33 @@ export const createAIApplyActionEndpointHandler = (options = {})=>async (req)=>{
82
229
  });
83
230
  const globalConfig = req.payload.config.globals?.find((global)=>global.slug === proposal.slug);
84
231
  normalized = normalizeDataForFields(globalConfig?.fields || [], proposal.data);
232
+ const before = await req.payload.findGlobal({
233
+ depth: 2,
234
+ req,
235
+ slug: proposal.slug
236
+ });
85
237
  const doc = await req.payload.updateGlobal({
86
238
  data: normalized.data,
87
239
  overrideAccess: false,
88
240
  req,
89
241
  slug: proposal.slug
90
242
  });
243
+ const change = await logAIChange({
244
+ changeLogCollection: options.changeLogCollection,
245
+ proposal,
246
+ req,
247
+ target: {
248
+ after: mergeData(before, normalized.data),
249
+ before
250
+ }
251
+ });
91
252
  return Response.json({
253
+ change,
92
254
  doc: getAppliedDocReference(doc),
93
255
  status: "applied"
94
256
  });
95
257
  }
96
- if (!isAllowedCollection(req, proposal.collection, options.collections)) return Response.json({
258
+ if (!isAllowedCollection(req, proposal.collection, options.collections, proposal.action)) return Response.json({
97
259
  error: "Unknown collection"
98
260
  }, {
99
261
  status: 400
@@ -105,7 +267,18 @@ export const createAIApplyActionEndpointHandler = (options = {})=>async (req)=>{
105
267
  overrideAccess: false,
106
268
  req
107
269
  });
270
+ const change = await logAIChange({
271
+ changeLogCollection: options.changeLogCollection,
272
+ proposal,
273
+ req,
274
+ target: {
275
+ after: {},
276
+ before: doc,
277
+ documentID: proposal.id
278
+ }
279
+ });
108
280
  return Response.json({
281
+ change,
109
282
  doc: getAppliedDocReference(doc),
110
283
  status: "applied"
111
284
  });
@@ -133,11 +306,28 @@ export const createAIApplyActionEndpointHandler = (options = {})=>async (req)=>{
133
306
  overrideAccess: false,
134
307
  req
135
308
  });
309
+ const change = await logAIChange({
310
+ changeLogCollection: options.changeLogCollection,
311
+ proposal,
312
+ req,
313
+ target: {
314
+ after: doc,
315
+ before: {},
316
+ documentID: doc.id
317
+ }
318
+ });
136
319
  return Response.json({
320
+ change,
137
321
  doc: getAppliedDocReference(doc),
138
322
  status: "applied"
139
323
  });
140
324
  }
325
+ const before = await req.payload.findByID({
326
+ collection: proposal.collection,
327
+ depth: 2,
328
+ id: proposal.id,
329
+ req
330
+ });
141
331
  const doc = await req.payload.update({
142
332
  collection: proposal.collection,
143
333
  data: normalized.data,
@@ -145,7 +335,18 @@ export const createAIApplyActionEndpointHandler = (options = {})=>async (req)=>{
145
335
  overrideAccess: false,
146
336
  req
147
337
  });
338
+ const change = await logAIChange({
339
+ changeLogCollection: options.changeLogCollection,
340
+ proposal,
341
+ req,
342
+ target: {
343
+ after: mergeData(before, normalized.data),
344
+ before,
345
+ documentID: proposal.id
346
+ }
347
+ });
148
348
  return Response.json({
349
+ change,
149
350
  doc: getAppliedDocReference(doc),
150
351
  status: "applied"
151
352
  });
@@ -1,5 +1,7 @@
1
1
  import type { PayloadHandler } from "payload";
2
2
  import { type AIActionSignature } from "../ai/proposals.js";
3
+ import { type AIModelConfig } from "../ai/providerOptions.js";
4
+ import { type ResolvedAICollectionPermissionMap } from "../payload/collectionPermissions.js";
3
5
  export type AIActionProposal = ({
4
6
  action: "create";
5
7
  collection: string;
@@ -25,7 +27,9 @@ export type AIActionProposal = ({
25
27
  _aiSignature?: AIActionSignature;
26
28
  };
27
29
  type AIChatEndpointOptions = {
28
- collections?: string[];
30
+ allowUserApiKeys?: boolean;
31
+ collections?: ResolvedAICollectionPermissionMap;
32
+ models?: AIModelConfig;
29
33
  };
30
34
  export declare const createAIChatEndpointHandler: (options?: AIChatEndpointOptions) => PayloadHandler;
31
35
  export {};
@@ -1,10 +1,18 @@
1
- import { generateText, stepCountIs } from "ai";
1
+ import { streamText, stepCountIs } from "ai";
2
2
  import { z } from "zod";
3
3
  import { signAIActionProposal } from "../ai/proposals.js";
4
4
  import { getModel, getProviderConfig } from "../ai/providerRuntime.js";
5
5
  import { isAIProvider } from "../ai/providerOptions.js";
6
6
  import { containsSensitiveData } from "../ai/sensitiveData.js";
7
- import { buildPromptWithMentionContext, collectBlocks, describeField, getAllowedCollectionSlugs, getMentionContext } from "../payload/schemaContext.js";
7
+ import { buildPromptWithMentionContext, collectBlocks, describeCollectionLikeConfig, getAllowedCollectionSlugs, getMentionContext } from "../payload/schemaContext.js";
8
+ import { isCollectionActionAllowed } from "../payload/collectionPermissions.js";
9
+ const maxProposalLabelLength = 90;
10
+ const getSafeProposalLabel = (label)=>{
11
+ const firstLine = label.replace(/[*_`>#-]/g, "").split("\n").map((line)=>line.trim()).find(Boolean);
12
+ if (!firstLine) return "Review proposed CMS change";
13
+ if (firstLine.length <= maxProposalLabelLength) return firstLine;
14
+ return `${firstLine.slice(0, maxProposalLabelLength - 3).trim()}...`;
15
+ };
8
16
  export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
9
17
  if (!req.user) return Response.json({
10
18
  error: "Unauthorized"
@@ -26,8 +34,10 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
26
34
  status: 400
27
35
  });
28
36
  const provider = requestedProvider;
37
+ const userApiKey = options.allowUserApiKeys === false ? null : user.aiApiKey;
29
38
  const providerConfig = getProviderConfig({
30
- apiKey: user.aiApiKey,
39
+ apiKey: userApiKey,
40
+ defaultModels: options.models?.defaults,
31
41
  model: body?.model,
32
42
  provider
33
43
  });
@@ -48,7 +58,7 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
48
58
  };
49
59
  if (!providerConfig.apiKey) {
50
60
  return Response.json({
51
- error: `Add a ${provider} API key to your account settings first.`
61
+ error: options.allowUserApiKeys === false ? `Configure a ${provider} API key in the server environment first.` : `Add a ${provider} API key to your account settings or configure it in the server environment first.`
52
62
  }, {
53
63
  status: 400
54
64
  });
@@ -88,11 +98,23 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
88
98
  const mentionContext = await getMentionContext({
89
99
  blockContexts,
90
100
  collectionSlugs,
101
+ collections: options.collections,
91
102
  globalSlugs,
92
103
  mentions: body?.mentions,
93
104
  req
94
105
  });
95
106
  const collectionSlugSchema = z.enum(collectionSlugs);
107
+ const getDisallowedCollectionActionError = (collection, action)=>{
108
+ if (isCollectionActionAllowed({
109
+ action,
110
+ permissions: options.collections,
111
+ req,
112
+ slug: collection
113
+ })) return null;
114
+ return {
115
+ error: `${action} is not enabled for collection: ${collection}`
116
+ };
117
+ };
96
118
  const tools = {
97
119
  getDoc: {
98
120
  description: "Read one document by collection slug and document id.",
@@ -114,10 +136,10 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
114
136
  description: "List all Payload CMS collections available in this app.",
115
137
  inputSchema: z.object({}),
116
138
  execute: async ()=>{
117
- return allowedCollections.map((collection)=>({
118
- fields: collection.fields.map(describeField),
119
- label: collection.labels?.plural || collection.slug,
120
- slug: collection.slug
139
+ return allowedCollections.map((collection)=>describeCollectionLikeConfig({
140
+ config: collection,
141
+ permissions: options.collections,
142
+ type: "collection"
121
143
  }));
122
144
  }
123
145
  },
@@ -143,10 +165,9 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
143
165
  description: "List all Payload CMS globals available in this app.",
144
166
  inputSchema: z.object({}),
145
167
  execute: async ()=>{
146
- return req.payload.config.globals?.map((global)=>({
147
- fields: global.fields.map(describeField),
148
- label: global.label || global.slug,
149
- slug: global.slug
168
+ return req.payload.config.globals?.map((global)=>describeCollectionLikeConfig({
169
+ config: global,
170
+ type: "global"
150
171
  })) || [];
151
172
  }
152
173
  },
@@ -158,11 +179,13 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
158
179
  label: z.string().min(1)
159
180
  }),
160
181
  execute: async ({ collection, data, label })=>{
182
+ const permissionError = getDisallowedCollectionActionError(collection, "create");
183
+ if (permissionError) return permissionError;
161
184
  const proposal = {
162
185
  action: "create",
163
186
  collection,
164
187
  data,
165
- label
188
+ label: getSafeProposalLabel(label)
166
189
  };
167
190
  return addSignedProposal(proposal);
168
191
  }
@@ -175,11 +198,13 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
175
198
  label: z.string().min(1)
176
199
  }),
177
200
  execute: async ({ collection, id, label })=>{
201
+ const permissionError = getDisallowedCollectionActionError(collection, "delete");
202
+ if (permissionError) return permissionError;
178
203
  const proposal = {
179
204
  action: "delete",
180
205
  collection,
181
206
  id,
182
- label
207
+ label: getSafeProposalLabel(label)
183
208
  };
184
209
  return addSignedProposal(proposal);
185
210
  }
@@ -193,12 +218,14 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
193
218
  label: z.string().min(1)
194
219
  }),
195
220
  execute: async ({ collection, data, id, label })=>{
221
+ const permissionError = getDisallowedCollectionActionError(collection, "update");
222
+ if (permissionError) return permissionError;
196
223
  const proposal = {
197
224
  action: "update",
198
225
  collection,
199
226
  data,
200
227
  id,
201
- label
228
+ label: getSafeProposalLabel(label)
202
229
  };
203
230
  return addSignedProposal(proposal);
204
231
  }
@@ -218,7 +245,7 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
218
245
  const proposal = {
219
246
  action: "updateGlobal",
220
247
  data,
221
- label,
248
+ label: getSafeProposalLabel(label),
222
249
  slug
223
250
  };
224
251
  return addSignedProposal(proposal);
@@ -256,7 +283,11 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
256
283
  }
257
284
  }
258
285
  };
259
- const result = await generateText({
286
+ const encoder = new TextEncoder();
287
+ const sendEvent = (controller, event, data)=>{
288
+ controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
289
+ };
290
+ const result = streamText({
260
291
  maxOutputTokens: 700,
261
292
  model: getModel({
262
293
  apiKey: providerConfig.apiKey,
@@ -274,13 +305,67 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
274
305
  "When mention context is provided, use it as the selected CMS scope and prefer it over guessing collection names.",
275
306
  "Never claim that a write has been applied unless the user confirms an action proposal in the UI.",
276
307
  "For create, update, and delete requests, call the proposal tools instead of directly changing data.",
277
- "Keep the visible response under 80 words. Put concrete changes in proposal tool calls instead of long prose."
308
+ "Keep the visible response under 40 words and describe only what kind of change was proposed.",
309
+ "Write visible responses as plain text only. Do not use Markdown formatting, bold markers (**), headings, bullets, tables, or code fences.",
310
+ "Do not include proposed field values, full replacement text, quoted content, markdown sections, code blocks, or headings like 'New content'/'Neuer Inhalt' in the visible response.",
311
+ "Put all concrete field values and replacement content only in proposal tool data. Proposal labels must be short action summaries and must not contain proposed content."
278
312
  ].join("\n"),
279
313
  tools
280
314
  });
281
- return Response.json({
282
- proposals,
283
- text: result.text
315
+ const stream = new ReadableStream({
316
+ start: async (controller)=>{
317
+ let didSendTerminalEvent = false;
318
+ try {
319
+ for await (const part of result.fullStream){
320
+ if (part.type === "text-delta") {
321
+ sendEvent(controller, "text", {
322
+ delta: part.text
323
+ });
324
+ continue;
325
+ }
326
+ if (part.type === "error") {
327
+ didSendTerminalEvent = true;
328
+ sendEvent(controller, "error", {
329
+ error: "AI request failed."
330
+ });
331
+ break;
332
+ }
333
+ if (part.type === "finish") {
334
+ sendEvent(controller, "proposals", {
335
+ proposals
336
+ });
337
+ sendEvent(controller, "done", {});
338
+ didSendTerminalEvent = true;
339
+ }
340
+ }
341
+ if (!didSendTerminalEvent) {
342
+ sendEvent(controller, "proposals", {
343
+ proposals
344
+ });
345
+ sendEvent(controller, "done", {});
346
+ }
347
+ } catch (err) {
348
+ req.payload.logger.error({
349
+ debug,
350
+ err,
351
+ msg: "AI chat stream failed"
352
+ });
353
+ if (!didSendTerminalEvent) {
354
+ sendEvent(controller, "error", {
355
+ error: "AI request failed."
356
+ });
357
+ }
358
+ } finally{
359
+ controller.close();
360
+ }
361
+ }
362
+ });
363
+ return new Response(stream, {
364
+ headers: {
365
+ "Cache-Control": "no-cache, no-transform",
366
+ Connection: "keep-alive",
367
+ "Content-Type": "text/event-stream; charset=utf-8"
368
+ }
284
369
  });
285
370
  } catch (err) {
286
371
  req.payload.logger.error({
@@ -1,6 +1,7 @@
1
1
  import type { PayloadHandler } from "payload";
2
+ import { type ResolvedAICollectionPermissionMap } from "../payload/collectionPermissions.js";
2
3
  type AIMentionSuggestionsEndpointOptions = {
3
- collections?: string[];
4
+ collections?: ResolvedAICollectionPermissionMap;
4
5
  };
5
6
  export declare const createAIMentionSuggestionsEndpointHandler: (options?: AIMentionSuggestionsEndpointOptions) => PayloadHandler;
6
7
  export {};
@@ -1,9 +1,4 @@
1
- import { isInternalCollection } from "../payload/shared.js";
2
- const isAllowedCollectionSlug = (slug, collections)=>{
3
- if (isInternalCollection(slug)) return false;
4
- if (!collections) return true;
5
- return collections.includes(slug);
6
- };
1
+ import { isCollectionActionAllowed } from "../payload/collectionPermissions.js";
7
2
  const getDocLabel = (doc, useAsTitle)=>{
8
3
  const titleField = useAsTitle && typeof doc[useAsTitle] === "string" ? doc[useAsTitle] : null;
9
4
  return (titleField || doc.title || doc.name || doc.email || doc.id || "Untitled").toString();
@@ -22,7 +17,12 @@ export const createAIMentionSuggestionsEndpointHandler = (options = {})=>async (
22
17
  });
23
18
  const suggestions = [];
24
19
  const collections = req.payload.config.collections.filter((collection)=>{
25
- if (!isAllowedCollectionSlug(collection.slug, options.collections)) return false;
20
+ if (!isCollectionActionAllowed({
21
+ action: "read",
22
+ permissions: options.collections,
23
+ req,
24
+ slug: collection.slug
25
+ })) return false;
26
26
  if (collectionSlug) return collection.slug === collectionSlug;
27
27
  return true;
28
28
  });
@@ -0,0 +1,7 @@
1
+ import type { PayloadHandler } from "payload";
2
+ import { type ResolvedAICollectionPermissionMap } from "../payload/collectionPermissions.js";
3
+ type AIProposalDiffEndpointOptions = {
4
+ collections?: ResolvedAICollectionPermissionMap;
5
+ };
6
+ export declare const createAIProposalDiffEndpointHandler: (options?: AIProposalDiffEndpointOptions) => PayloadHandler;
7
+ export {};