@mvriu5/payload-ai 0.5.9 → 0.5.10

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 (69) hide show
  1. package/README.md +62 -6
  2. package/dist/ai/{proposals.d.ts → proposalSigning.d.ts} +1 -1
  3. package/dist/ai/{proposals.js → proposalSigning.js} +6 -11
  4. package/dist/ai/providerOptions.d.ts +17 -1
  5. package/dist/ai/providerOptions.js +35 -7
  6. package/dist/ai/providerRuntime.d.ts +2 -2
  7. package/dist/ai/providerRuntime.js +50 -16
  8. package/dist/ai/sensitiveData.js +2 -6
  9. package/dist/components/AIInput.d.ts +2 -1
  10. package/dist/components/AIInput.js +210 -39
  11. package/dist/components/AIInput.module.css +15 -1
  12. package/dist/components/APIKeyField.d.ts +3 -0
  13. package/dist/components/{AIApiKeyField.js → APIKeyField.js} +3 -4
  14. package/dist/components/ActionToast.d.ts +30 -0
  15. package/dist/components/{AIActionProposalList.js → ActionToast.js} +10 -8
  16. package/dist/components/AuditLogList.d.ts +32 -0
  17. package/dist/components/{RecentChangesList.js → AuditLogList.js} +13 -12
  18. package/dist/components/{RecentChangesList.module.css → AuditLogList.module.css} +18 -1
  19. package/dist/components/DiffDialog.d.ts +10 -3
  20. package/dist/components/DiffDialog.js +164 -78
  21. package/dist/components/DiffDialog.module.css +86 -2
  22. package/dist/components/Icons.d.ts +5 -0
  23. package/dist/components/Icons.js +109 -3
  24. package/dist/components/MentionPopover.d.ts +19 -0
  25. package/dist/components/{CollectionMentionPopover.js → MentionPopover.js} +11 -4
  26. package/dist/components/{CollectionMentionPopover.module.css → MentionPopover.module.css} +0 -2
  27. package/dist/components/hooks/useAISettings.d.ts +6 -4
  28. package/dist/components/hooks/useAISettings.js +18 -3
  29. package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +3 -3
  30. package/dist/components/hooks/useDocumentMentionSuggestions.js +2 -2
  31. package/dist/exports/client.d.ts +4 -2
  32. package/dist/exports/client.js +4 -2
  33. package/dist/handlers/applyActionHandler.d.ts +8 -0
  34. package/dist/handlers/applyActionHandler.js +563 -0
  35. package/dist/handlers/auditLogHandler.d.ts +6 -0
  36. package/dist/{endpoints/aiRecentChangesEndpointHandler.js → handlers/auditLogHandler.js} +12 -8
  37. package/dist/handlers/chatHandler.d.ts +42 -0
  38. package/dist/handlers/chatHandler.js +634 -0
  39. package/dist/handlers/mentionSuggestionHandler.d.ts +7 -0
  40. package/dist/handlers/mentionSuggestionHandler.js +97 -0
  41. package/dist/handlers/proposalDiffHandler.d.ts +7 -0
  42. package/dist/handlers/proposalDiffHandler.js +208 -0
  43. package/dist/index.d.ts +6 -6
  44. package/dist/index.js +50 -18
  45. package/dist/payload/collectionPermissions.d.ts +14 -13
  46. package/dist/payload/collectionPermissions.js +3 -2
  47. package/dist/payload/normalizeData.d.ts +8 -0
  48. package/dist/payload/normalizeData.js +25 -3
  49. package/dist/payload/schemaContext.d.ts +43 -16
  50. package/dist/payload/schemaContext.js +139 -18
  51. package/dist/payload/shared.d.ts +33 -0
  52. package/dist/payload/shared.js +114 -0
  53. package/package.json +7 -2
  54. package/dist/components/AIActionProposalList.d.ts +0 -26
  55. package/dist/components/AIApiKeyField.d.ts +0 -2
  56. package/dist/components/CollectionMentionPopover.d.ts +0 -16
  57. package/dist/components/RecentChangesList.d.ts +0 -18
  58. package/dist/components/hooks/utils.d.ts +0 -1
  59. package/dist/components/hooks/utils.js +0 -3
  60. package/dist/endpoints/aiApplyActionEndpointHandler.d.ts +0 -8
  61. package/dist/endpoints/aiApplyActionEndpointHandler.js +0 -365
  62. package/dist/endpoints/aiChatEndpointHandler.d.ts +0 -35
  63. package/dist/endpoints/aiChatEndpointHandler.js +0 -382
  64. package/dist/endpoints/aiMentionSuggestionsEndpointHandler.d.ts +0 -7
  65. package/dist/endpoints/aiMentionSuggestionsEndpointHandler.js +0 -63
  66. package/dist/endpoints/aiProposalDiffEndpointHandler.d.ts +0 -7
  67. package/dist/endpoints/aiProposalDiffEndpointHandler.js +0 -138
  68. package/dist/endpoints/aiRecentChangesEndpointHandler.d.ts +0 -6
  69. /package/dist/components/{AIActionProposalList.module.css → ActionToast.module.css} +0 -0
@@ -0,0 +1,563 @@
1
+ import { verifyActionProposal } from "../ai/proposalSigning.js";
2
+ import { containsSensitiveData, redactSensitiveData } from "../ai/sensitiveData.js";
3
+ import { getCollectionFields, getLocalizedRequiredFallbackData, isAuthCollection, normalizeAuthData, normalizeDataForFields } from "../payload/normalizeData.js";
4
+ import { isCollectionActionAllowed } from "../payload/collectionPermissions.js";
5
+ import { getDefaultLocale, getJSONLineKey, getOptionalNumber, hasLocalizedData, isActionProposal, isKnownGlobal, mergeData } from "../payload/shared.js";
6
+ const getProposalMeta = (proposal)=>{
7
+ if (!proposal) return {};
8
+ return {
9
+ action: proposal.action,
10
+ collection: "collection" in proposal ? proposal.collection : undefined,
11
+ id: "id" in proposal ? proposal.id : undefined,
12
+ slug: "slug" in proposal ? proposal.slug : undefined
13
+ };
14
+ };
15
+ const getAppliedDocReference = (doc)=>{
16
+ return doc?.id === undefined ? undefined : {
17
+ id: doc.id
18
+ };
19
+ };
20
+ const isAllowedCollection = (req, collection, collections, action = "read")=>{
21
+ return isCollectionActionAllowed({
22
+ action,
23
+ permissions: collections,
24
+ req,
25
+ slug: collection
26
+ });
27
+ };
28
+ const applyLocalizedRequiredFallback = ({ data, fallbackSource, fields })=>{
29
+ return mergeData(getLocalizedRequiredFallbackData({
30
+ fields,
31
+ source: fallbackSource,
32
+ target: data
33
+ }), data);
34
+ };
35
+ const countDiffChanges = ({ after, before })=>{
36
+ const beforeLines = JSON.stringify(before, null, 2).split("\n");
37
+ const afterLines = JSON.stringify(after, null, 2).split("\n");
38
+ const dp = Array.from({
39
+ length: beforeLines.length + 1
40
+ }, ()=>Array(afterLines.length + 1).fill(0));
41
+ let additions = 0;
42
+ let removals = 0;
43
+ for(let i = beforeLines.length - 1; i >= 0; i -= 1){
44
+ for(let j = afterLines.length - 1; j >= 0; j -= 1){
45
+ dp[i][j] = beforeLines[i] === afterLines[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
46
+ }
47
+ }
48
+ let beforeIndex = 0;
49
+ let afterIndex = 0;
50
+ while(beforeIndex < beforeLines.length && afterIndex < afterLines.length){
51
+ if (beforeLines[beforeIndex] === afterLines[afterIndex]) {
52
+ beforeIndex += 1;
53
+ afterIndex += 1;
54
+ continue;
55
+ }
56
+ const beforeKey = getJSONLineKey(beforeLines[beforeIndex]);
57
+ const afterKey = getJSONLineKey(afterLines[afterIndex]);
58
+ if (beforeKey && beforeKey === afterKey) {
59
+ removals += 1;
60
+ additions += 1;
61
+ beforeIndex += 1;
62
+ afterIndex += 1;
63
+ continue;
64
+ }
65
+ if (dp[beforeIndex + 1][afterIndex] >= dp[beforeIndex][afterIndex + 1]) {
66
+ removals += 1;
67
+ beforeIndex += 1;
68
+ } else {
69
+ additions += 1;
70
+ afterIndex += 1;
71
+ }
72
+ }
73
+ removals += beforeLines.length - beforeIndex;
74
+ additions += afterLines.length - afterIndex;
75
+ return {
76
+ additions,
77
+ removals
78
+ };
79
+ };
80
+ const getTargetURL = ({ documentID, proposal, req })=>{
81
+ const adminRoute = req.payload.config.routes.admin || "/admin";
82
+ if (proposal.action === "updateGlobal") return `${adminRoute}/globals/${proposal.slug}`;
83
+ if (proposal.action === "delete") return null;
84
+ const id = proposal.action === "create" ? documentID : proposal.id;
85
+ if (!id) return null;
86
+ return `${adminRoute}/collections/${proposal.collection}/${id}`;
87
+ };
88
+ const getUserID = (req)=>{
89
+ const user = req.user;
90
+ return user?.id === undefined ? undefined : String(user.id);
91
+ };
92
+ const getUserLabel = (req)=>{
93
+ const user = req.user;
94
+ if (typeof user?.email === "string" && user.email) return user.email;
95
+ if (typeof user?.name === "string" && user.name) return user.name;
96
+ if (typeof user?.username === "string" && user.username) return user.username;
97
+ if (user?.id !== undefined) return String(user.id);
98
+ return null;
99
+ };
100
+ const logAIChange = async ({ changeLogCollection, context, target, proposal, req })=>{
101
+ if (!changeLogCollection) return null;
102
+ const before = redactSensitiveData(target.before);
103
+ const after = redactSensitiveData(target.after);
104
+ const { additions, removals } = countDiffChanges({
105
+ after,
106
+ before
107
+ });
108
+ const proposalForLog = {
109
+ ...proposal
110
+ };
111
+ delete proposalForLog._aiSignature;
112
+ try {
113
+ await req.payload.create({
114
+ collection: changeLogCollection,
115
+ data: {
116
+ action: proposal.action,
117
+ additions,
118
+ after,
119
+ aiResponse: context?.aiResponse,
120
+ before,
121
+ collection: "collection" in proposal ? proposal.collection : undefined,
122
+ documentID: target.documentID === undefined ? undefined : String(target.documentID),
123
+ inputTokens: context?.tokenUsage?.inputTokens,
124
+ outputTokens: context?.tokenUsage?.outputTokens,
125
+ prompt: context?.prompt,
126
+ proposal: redactSensitiveData(proposalForLog),
127
+ removals,
128
+ slug: "slug" in proposal ? proposal.slug : undefined,
129
+ totalTokens: context?.tokenUsage?.totalTokens,
130
+ targetType: proposal.action === "updateGlobal" ? "global" : "collection",
131
+ targetURL: getTargetURL({
132
+ documentID: target.documentID,
133
+ proposal,
134
+ req
135
+ }),
136
+ title: proposal.label,
137
+ userID: getUserID(req),
138
+ userLabel: getUserLabel(req)
139
+ },
140
+ overrideAccess: true,
141
+ req
142
+ });
143
+ return {
144
+ action: proposal.action,
145
+ additions,
146
+ after,
147
+ aiResponse: context?.aiResponse || null,
148
+ before,
149
+ collection: "collection" in proposal ? proposal.collection : null,
150
+ createdAt: new Date().toISOString(),
151
+ documentID: target.documentID === undefined ? null : String(target.documentID),
152
+ inputTokens: context?.tokenUsage?.inputTokens ?? null,
153
+ outputTokens: context?.tokenUsage?.outputTokens ?? null,
154
+ prompt: context?.prompt || null,
155
+ removals,
156
+ slug: "slug" in proposal ? proposal.slug : null,
157
+ totalTokens: context?.tokenUsage?.totalTokens ?? null,
158
+ targetType: proposal.action === "updateGlobal" ? "global" : "collection",
159
+ title: proposal.label,
160
+ userID: getUserID(req) || null,
161
+ userLabel: getUserLabel(req),
162
+ url: getTargetURL({
163
+ documentID: target.documentID,
164
+ proposal,
165
+ req
166
+ })
167
+ };
168
+ } catch (err) {
169
+ req.payload.logger.error({
170
+ err,
171
+ msg: "AI change log entry could not be written",
172
+ proposal: getProposalMeta(proposal)
173
+ });
174
+ return null;
175
+ }
176
+ };
177
+ export const createApplyActionHandler = (options = {})=>async (req)=>{
178
+ if (!req.user) return Response.json({
179
+ error: "Unauthorized"
180
+ }, {
181
+ status: 401
182
+ });
183
+ const body = req.json ? await req.json().catch(()=>null) : null;
184
+ const proposal = body?.proposal;
185
+ if (!proposal) return Response.json({
186
+ error: "Proposal is required"
187
+ }, {
188
+ status: 400
189
+ });
190
+ if (!verifyActionProposal(proposal)) return Response.json({
191
+ error: "Proposal signature is invalid or expired."
192
+ }, {
193
+ status: 400
194
+ });
195
+ if (!isActionProposal(proposal)) return Response.json({
196
+ error: "Proposal is invalid."
197
+ }, {
198
+ status: 400
199
+ });
200
+ if ("data" in proposal && proposal.data && containsSensitiveData(proposal.data)) return Response.json({
201
+ error: "Proposal contains sensitive fields and cannot be applied."
202
+ }, {
203
+ status: 400
204
+ });
205
+ if (hasLocalizedData(proposal) && Object.values(proposal.localizedData).some((value)=>containsSensitiveData(value))) return Response.json({
206
+ error: "Proposal contains sensitive fields and cannot be applied."
207
+ }, {
208
+ status: 400
209
+ });
210
+ let normalized;
211
+ const logContext = {
212
+ aiResponse: typeof body?.aiResponse === "string" && body.aiResponse.trim() ? body.aiResponse.trim() : undefined,
213
+ prompt: typeof body?.prompt === "string" && body.prompt.trim() ? body.prompt.trim() : undefined,
214
+ tokenUsage: body?.tokenUsage ? {
215
+ inputTokens: getOptionalNumber(body.tokenUsage.inputTokens),
216
+ outputTokens: getOptionalNumber(body.tokenUsage.outputTokens),
217
+ totalTokens: getOptionalNumber(body.tokenUsage.totalTokens)
218
+ } : undefined
219
+ };
220
+ try {
221
+ const defaultLocale = getDefaultLocale(req);
222
+ if (proposal.action === "updateGlobal") {
223
+ if (!isKnownGlobal(req, proposal.slug)) return Response.json({
224
+ error: "Unknown global"
225
+ }, {
226
+ status: 400
227
+ });
228
+ const globalConfig = req.payload.config.globals?.find((global)=>global.slug === proposal.slug);
229
+ if (hasLocalizedData(proposal)) {
230
+ const beforeByLocale = {};
231
+ const afterByLocale = {};
232
+ const globalFields = globalConfig?.fields || [];
233
+ const defaultLocaleDoc = defaultLocale ? await req.payload.findGlobal({
234
+ depth: 2,
235
+ fallbackLocale: false,
236
+ locale: defaultLocale,
237
+ req,
238
+ slug: proposal.slug
239
+ }) : null;
240
+ for (const [locale, localeData] of Object.entries(proposal.localizedData)){
241
+ const localeNormalized = normalizeDataForFields(globalFields, localeData);
242
+ const before = await req.payload.findGlobal({
243
+ depth: 2,
244
+ fallbackLocale: false,
245
+ locale,
246
+ req,
247
+ slug: proposal.slug
248
+ });
249
+ const completedData = applyLocalizedRequiredFallback({
250
+ data: localeNormalized.data,
251
+ fallbackSource: locale === defaultLocale ? before : defaultLocaleDoc || before,
252
+ fields: globalFields
253
+ });
254
+ await req.payload.updateGlobal({
255
+ data: completedData,
256
+ locale,
257
+ overrideAccess: false,
258
+ req,
259
+ slug: proposal.slug
260
+ });
261
+ beforeByLocale[locale] = before;
262
+ afterByLocale[locale] = mergeData(before, completedData);
263
+ }
264
+ const change = await logAIChange({
265
+ changeLogCollection: options.changeLogCollection,
266
+ context: logContext,
267
+ proposal,
268
+ req,
269
+ target: {
270
+ after: afterByLocale,
271
+ before: beforeByLocale
272
+ }
273
+ });
274
+ return Response.json({
275
+ change,
276
+ doc: undefined,
277
+ status: "applied"
278
+ });
279
+ }
280
+ normalized = normalizeDataForFields(globalConfig?.fields || [], proposal.data);
281
+ const before = await req.payload.findGlobal({
282
+ depth: 2,
283
+ ...proposal.locale ? {
284
+ locale: proposal.locale
285
+ } : {},
286
+ req,
287
+ slug: proposal.slug
288
+ });
289
+ const doc = await req.payload.updateGlobal({
290
+ data: normalized.data,
291
+ ...proposal.locale ? {
292
+ locale: proposal.locale
293
+ } : {},
294
+ overrideAccess: false,
295
+ req,
296
+ slug: proposal.slug
297
+ });
298
+ const change = await logAIChange({
299
+ changeLogCollection: options.changeLogCollection,
300
+ context: logContext,
301
+ proposal,
302
+ req,
303
+ target: {
304
+ after: mergeData(before, normalized.data),
305
+ before
306
+ }
307
+ });
308
+ return Response.json({
309
+ change,
310
+ doc: getAppliedDocReference(doc),
311
+ status: "applied"
312
+ });
313
+ }
314
+ if (!isAllowedCollection(req, proposal.collection, options.collections, proposal.action)) return Response.json({
315
+ error: "Unknown collection"
316
+ }, {
317
+ status: 400
318
+ });
319
+ if (proposal.action === "delete") {
320
+ const doc = await req.payload.delete({
321
+ collection: proposal.collection,
322
+ id: proposal.id,
323
+ overrideAccess: false,
324
+ req
325
+ });
326
+ const change = await logAIChange({
327
+ changeLogCollection: options.changeLogCollection,
328
+ context: logContext,
329
+ proposal,
330
+ req,
331
+ target: {
332
+ after: {},
333
+ before: doc,
334
+ documentID: proposal.id
335
+ }
336
+ });
337
+ return Response.json({
338
+ change,
339
+ doc: getAppliedDocReference(doc),
340
+ status: "applied"
341
+ });
342
+ }
343
+ const collectionConfig = req.payload.config.collections.find((collection)=>collection.slug === proposal.collection);
344
+ const collectionFields = getCollectionFields(collectionConfig);
345
+ const normalizeCollectionData = (data)=>normalizeAuthData(collectionConfig, normalizeDataForFields(collectionFields, data));
346
+ if (hasLocalizedData(proposal)) {
347
+ if (proposal.action === "create") {
348
+ const localeEntries = Object.entries(proposal.localizedData);
349
+ const [firstLocale, firstLocaleData] = localeEntries[0] || [];
350
+ if (!firstLocale) return Response.json({
351
+ error: "Proposal is invalid."
352
+ }, {
353
+ status: 400
354
+ });
355
+ const firstNormalized = normalizeCollectionData(firstLocaleData);
356
+ if (isAuthCollection(collectionConfig) && !firstNormalized.data.password) {
357
+ return Response.json({
358
+ error: "Password is required when creating a user."
359
+ }, {
360
+ status: 400
361
+ });
362
+ }
363
+ if (isAuthCollection(collectionConfig) && !firstNormalized.data.email) {
364
+ return Response.json({
365
+ error: "Email is required when creating a user."
366
+ }, {
367
+ status: 400
368
+ });
369
+ }
370
+ const doc = await req.payload.create({
371
+ collection: proposal.collection,
372
+ data: firstNormalized.data,
373
+ locale: firstLocale,
374
+ overrideAccess: false,
375
+ req
376
+ });
377
+ const beforeByLocale = {
378
+ [firstLocale]: {}
379
+ };
380
+ const afterByLocale = {
381
+ [firstLocale]: doc
382
+ };
383
+ let fallbackSource = doc;
384
+ for (const [locale, localeData] of localeEntries.slice(1)){
385
+ const localeNormalized = normalizeCollectionData(localeData);
386
+ const completedData = applyLocalizedRequiredFallback({
387
+ data: localeNormalized.data,
388
+ fallbackSource,
389
+ fields: collectionFields
390
+ });
391
+ await req.payload.update({
392
+ collection: proposal.collection,
393
+ data: completedData,
394
+ id: String(doc.id),
395
+ locale,
396
+ overrideAccess: false,
397
+ req
398
+ });
399
+ beforeByLocale[locale] = {};
400
+ afterByLocale[locale] = completedData;
401
+ }
402
+ const change = await logAIChange({
403
+ changeLogCollection: options.changeLogCollection,
404
+ context: logContext,
405
+ proposal,
406
+ req,
407
+ target: {
408
+ after: afterByLocale,
409
+ before: beforeByLocale,
410
+ documentID: doc.id
411
+ }
412
+ });
413
+ return Response.json({
414
+ change,
415
+ doc: getAppliedDocReference(doc),
416
+ status: "applied"
417
+ });
418
+ }
419
+ const beforeByLocale = {};
420
+ const afterByLocale = {};
421
+ const defaultLocaleDoc = defaultLocale ? await req.payload.findByID({
422
+ collection: proposal.collection,
423
+ depth: 2,
424
+ fallbackLocale: false,
425
+ id: proposal.id,
426
+ locale: defaultLocale,
427
+ req
428
+ }) : null;
429
+ for (const [locale, localeData] of Object.entries(proposal.localizedData)){
430
+ const localeNormalized = normalizeCollectionData(localeData);
431
+ const before = await req.payload.findByID({
432
+ collection: proposal.collection,
433
+ depth: 2,
434
+ fallbackLocale: false,
435
+ id: proposal.id,
436
+ locale,
437
+ req
438
+ });
439
+ const completedData = applyLocalizedRequiredFallback({
440
+ data: localeNormalized.data,
441
+ fallbackSource: locale === defaultLocale ? before : defaultLocaleDoc || before,
442
+ fields: collectionFields
443
+ });
444
+ await req.payload.update({
445
+ collection: proposal.collection,
446
+ data: completedData,
447
+ id: proposal.id,
448
+ locale,
449
+ overrideAccess: false,
450
+ req
451
+ });
452
+ beforeByLocale[locale] = before;
453
+ afterByLocale[locale] = mergeData(before, completedData);
454
+ }
455
+ const change = await logAIChange({
456
+ changeLogCollection: options.changeLogCollection,
457
+ context: logContext,
458
+ proposal,
459
+ req,
460
+ target: {
461
+ after: afterByLocale,
462
+ before: beforeByLocale,
463
+ documentID: proposal.id
464
+ }
465
+ });
466
+ return Response.json({
467
+ change,
468
+ doc: {
469
+ id: proposal.id
470
+ },
471
+ status: "applied"
472
+ });
473
+ }
474
+ normalized = normalizeCollectionData(proposal.data);
475
+ if (proposal.action === "create" && isAuthCollection(collectionConfig) && !normalized.data.password) {
476
+ return Response.json({
477
+ error: "Password is required when creating a user."
478
+ }, {
479
+ status: 400
480
+ });
481
+ }
482
+ if (proposal.action === "create" && isAuthCollection(collectionConfig) && !normalized.data.email) {
483
+ return Response.json({
484
+ error: "Email is required when creating a user."
485
+ }, {
486
+ status: 400
487
+ });
488
+ }
489
+ if (proposal.action === "create") {
490
+ const doc = await req.payload.create({
491
+ collection: proposal.collection,
492
+ data: normalized.data,
493
+ ...proposal.locale ? {
494
+ locale: proposal.locale
495
+ } : {},
496
+ overrideAccess: false,
497
+ req
498
+ });
499
+ const change = await logAIChange({
500
+ changeLogCollection: options.changeLogCollection,
501
+ context: logContext,
502
+ proposal,
503
+ req,
504
+ target: {
505
+ after: doc,
506
+ before: {},
507
+ documentID: doc.id
508
+ }
509
+ });
510
+ return Response.json({
511
+ change,
512
+ doc: getAppliedDocReference(doc),
513
+ status: "applied"
514
+ });
515
+ }
516
+ const before = await req.payload.findByID({
517
+ collection: proposal.collection,
518
+ depth: 2,
519
+ id: proposal.id,
520
+ ...proposal.locale ? {
521
+ locale: proposal.locale
522
+ } : {},
523
+ req
524
+ });
525
+ const doc = await req.payload.update({
526
+ collection: proposal.collection,
527
+ data: normalized.data,
528
+ id: proposal.id,
529
+ ...proposal.locale ? {
530
+ locale: proposal.locale
531
+ } : {},
532
+ overrideAccess: false,
533
+ req
534
+ });
535
+ const change = await logAIChange({
536
+ changeLogCollection: options.changeLogCollection,
537
+ context: logContext,
538
+ proposal,
539
+ req,
540
+ target: {
541
+ after: mergeData(before, normalized.data),
542
+ before,
543
+ documentID: proposal.id
544
+ }
545
+ });
546
+ return Response.json({
547
+ change,
548
+ doc: getAppliedDocReference(doc),
549
+ status: "applied"
550
+ });
551
+ } catch (err) {
552
+ req.payload.logger.error({
553
+ err,
554
+ msg: "AI apply action failed",
555
+ proposal: getProposalMeta(proposal)
556
+ });
557
+ return Response.json({
558
+ error: "Could not apply proposal."
559
+ }, {
560
+ status: 400
561
+ });
562
+ }
563
+ };
@@ -0,0 +1,6 @@
1
+ import type { PayloadHandler } from "payload";
2
+ type AuditLogOptions = {
3
+ changeLogCollection: string;
4
+ };
5
+ export declare const createAuditLogHandler: (options: AuditLogOptions) => PayloadHandler;
6
+ export {};
@@ -1,10 +1,5 @@
1
- const getNumber = (value)=>{
2
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
3
- };
4
- const getString = (value)=>{
5
- return typeof value === "string" ? value : null;
6
- };
7
- export const createAIRecentChangesEndpointHandler = (options)=>async (req)=>{
1
+ import { getNumber, getString } from "../payload/shared.js";
2
+ export const createAuditLogHandler = (options)=>async (req)=>{
8
3
  if (!req.user) return Response.json({
9
4
  error: "Unauthorized"
10
5
  }, {
@@ -13,7 +8,7 @@ export const createAIRecentChangesEndpointHandler = (options)=>async (req)=>{
13
8
  const result = await req.payload.find({
14
9
  collection: options.changeLogCollection,
15
10
  depth: 0,
16
- limit: 12,
11
+ limit: 10,
17
12
  overrideAccess: false,
18
13
  req,
19
14
  sort: "-createdAt"
@@ -23,12 +18,21 @@ export const createAIRecentChangesEndpointHandler = (options)=>async (req)=>{
23
18
  action: getString(doc.action),
24
19
  additions: getNumber(doc.additions),
25
20
  after: doc.after,
21
+ aiResponse: getString(doc.aiResponse),
26
22
  before: doc.before,
27
23
  collection: getString(doc.collection),
24
+ createdAt: getString(doc.createdAt),
28
25
  documentID: getString(doc.documentID),
26
+ inputTokens: getNumber(doc.inputTokens),
27
+ outputTokens: getNumber(doc.outputTokens),
28
+ prompt: getString(doc.prompt),
29
29
  removals: getNumber(doc.removals),
30
30
  slug: getString(doc.slug),
31
+ targetType: getString(doc.targetType),
32
+ totalTokens: getNumber(doc.totalTokens),
31
33
  title: getString(doc.title) || "AI change",
34
+ userID: getString(doc.userID),
35
+ userLabel: getString(doc.userLabel),
32
36
  url: getString(doc.targetURL)
33
37
  }))
34
38
  });
@@ -0,0 +1,42 @@
1
+ import type { PayloadHandler } from "payload";
2
+ import { type AIActionSignature } from "../ai/proposalSigning.js";
3
+ import { type AIModelConfig } from "../ai/providerOptions.js";
4
+ import { type ResolvedCollectionPermissionMap } from "../payload/collectionPermissions.js";
5
+ type LocalizedDataInput = Record<string, Record<string, unknown>>;
6
+ type ProposalWritePayload = {
7
+ data: Record<string, unknown>;
8
+ localizedData?: never;
9
+ } | {
10
+ data?: never;
11
+ localizedData: LocalizedDataInput;
12
+ };
13
+ export type ActionProposal = (({
14
+ action: "create";
15
+ collection: string;
16
+ label: string;
17
+ } & ProposalWritePayload) | {
18
+ action: "delete";
19
+ collection: string;
20
+ id: string;
21
+ label: string;
22
+ } | ({
23
+ action: "update";
24
+ collection: string;
25
+ id: string;
26
+ label: string;
27
+ } & ProposalWritePayload) | ({
28
+ action: "updateGlobal";
29
+ label: string;
30
+ slug: string;
31
+ } & ProposalWritePayload)) & {
32
+ _aiSignature?: AIActionSignature;
33
+ locale?: string;
34
+ };
35
+ type ChatOptions = {
36
+ allowUserApiKeys?: boolean;
37
+ collections?: ResolvedCollectionPermissionMap;
38
+ maxOutputTokens?: number;
39
+ models?: AIModelConfig;
40
+ };
41
+ export declare const createChatHandler: (options?: ChatOptions) => PayloadHandler;
42
+ export {};