@mvriu5/payload-ai 1.6.2 → 1.6.4

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 (126) hide show
  1. package/dist/components/action-toast/ActionToast.d.ts +1 -14
  2. package/dist/components/action-toast/ActionToast.js +1 -1
  3. package/dist/components/ai-input/AIInput.js +1 -1
  4. package/dist/components/audit-log-list/AuditLogList.d.ts +2 -3
  5. package/dist/components/diff-dialog/DiffDialog.d.ts +2 -6
  6. package/dist/components/diff-dialog/DiffDialog.js +1 -1
  7. package/dist/components/generate-field/GenerateField.js +2 -0
  8. package/dist/components/hooks/useAIChatStream.d.ts +1 -1
  9. package/dist/components/hooks/useAISettings.d.ts +1 -1
  10. package/dist/components/hooks/useAISettings.js +1 -1
  11. package/dist/components/hooks/useDocumentMentionSuggestions.js +1 -1
  12. package/dist/components/hooks/useMentions.js +1 -1
  13. package/dist/components/hooks/usePluginConfig.d.ts +6 -6
  14. package/dist/components/hooks/usePluginConfig.js +1 -1
  15. package/dist/{payload → features}/collectionPermissions.js +1 -1
  16. package/dist/{payload/textFieldGeneration.d.ts → features/content/fieldGeneration.d.ts} +4 -0
  17. package/dist/{payload/textFieldGeneration.js → features/content/fieldGeneration.js} +19 -3
  18. package/dist/{ai → features}/promptCaching.d.ts +2 -2
  19. package/dist/features/proposals/containerNormalization.d.ts +8 -0
  20. package/dist/features/proposals/containerNormalization.js +112 -0
  21. package/dist/{payload/proposalData.d.ts → features/proposals/data.d.ts} +5 -10
  22. package/dist/features/proposals/data.js +113 -0
  23. package/dist/features/proposals/fieldNormalization.d.ts +2 -0
  24. package/dist/features/proposals/fieldNormalization.js +69 -0
  25. package/dist/features/proposals/normalizationTypes.d.ts +31 -0
  26. package/dist/features/proposals/normalizationTypes.js +1 -0
  27. package/dist/features/proposals/optionInference.d.ts +13 -0
  28. package/dist/features/proposals/optionInference.js +46 -0
  29. package/dist/features/proposals/recordNormalization.d.ts +6 -0
  30. package/dist/features/proposals/recordNormalization.js +78 -0
  31. package/dist/features/proposals/relationshipNormalization.d.ts +8 -0
  32. package/dist/features/proposals/relationshipNormalization.js +71 -0
  33. package/dist/{ai/proposalRepair.d.ts → features/proposals/repair.d.ts} +1 -1
  34. package/dist/{ai/proposalSigning.d.ts → features/proposals/signing.d.ts} +2 -5
  35. package/dist/{ai/proposalSigning.js → features/proposals/signing.js} +1 -1
  36. package/dist/features/proposals/target.d.ts +52 -0
  37. package/dist/features/proposals/target.js +125 -0
  38. package/dist/features/proposals/types.d.ts +49 -0
  39. package/dist/features/proposals/types.js +1 -0
  40. package/dist/features/proposals/validation.d.ts +12 -0
  41. package/dist/features/proposals/validation.js +26 -0
  42. package/dist/features/providers/requestContext.d.ts +53 -0
  43. package/dist/features/providers/requestContext.js +90 -0
  44. package/dist/{ai/providerRuntime.d.ts → features/providers/runtime.d.ts} +1 -1
  45. package/dist/{ai/providerRuntime.js → features/providers/runtime.js} +1 -1
  46. package/dist/features/schema/context.d.ts +7 -0
  47. package/dist/features/schema/context.js +11 -0
  48. package/dist/{payload/schemaContext.d.ts → features/schema/description.d.ts} +6 -33
  49. package/dist/{payload/schemaContext.js → features/schema/description.js} +3 -207
  50. package/dist/{payload/toolFieldSelection.d.ts → features/schema/fieldSelection.d.ts} +1 -1
  51. package/dist/{payload/toolFieldSelection.js → features/schema/fieldSelection.js} +2 -2
  52. package/dist/features/schema/mentionDocuments.d.ts +15 -0
  53. package/dist/features/schema/mentionDocuments.js +50 -0
  54. package/dist/features/schema/mentions.d.ts +18 -0
  55. package/dist/features/schema/mentions.js +100 -0
  56. package/dist/{payload/normalizeData.js → features/schema/normalize.js} +1 -1
  57. package/dist/features/schema/registry.d.ts +30 -0
  58. package/dist/features/schema/registry.js +73 -0
  59. package/dist/{payload → features/schema}/toolSchemas.d.ts +1 -1
  60. package/dist/{payload → features/schema}/toolSchemas.js +1 -1
  61. package/dist/{ai → features}/sensitiveData.js +1 -1
  62. package/dist/{ai → features}/tokenUsage.d.ts +1 -2
  63. package/dist/handlers/apply/audit.d.ts +42 -0
  64. package/dist/handlers/apply/audit.js +150 -0
  65. package/dist/handlers/apply/operations.d.ts +18 -0
  66. package/dist/handlers/apply/operations.js +377 -0
  67. package/dist/handlers/apply/types.d.ts +33 -0
  68. package/dist/handlers/apply/types.js +1 -0
  69. package/dist/handlers/apply/validation.d.ts +45 -0
  70. package/dist/handlers/apply/validation.js +125 -0
  71. package/dist/handlers/applyActionHandler.d.ts +1 -6
  72. package/dist/handlers/applyActionHandler.js +13 -757
  73. package/dist/handlers/auditLogHandler.js +4 -8
  74. package/dist/handlers/chat/intent.d.ts +32 -0
  75. package/dist/handlers/chat/intent.js +155 -0
  76. package/dist/handlers/chat/prompt.d.ts +20 -0
  77. package/dist/handlers/chat/prompt.js +55 -0
  78. package/dist/handlers/chat/proposalTools.d.ts +96 -0
  79. package/dist/handlers/chat/proposalTools.js +451 -0
  80. package/dist/handlers/chat/streaming.d.ts +38 -0
  81. package/dist/handlers/chat/streaming.js +136 -0
  82. package/dist/handlers/chat/toolFactory.d.ts +79 -0
  83. package/dist/handlers/chat/toolFactory.js +156 -0
  84. package/dist/handlers/chat/types.d.ts +57 -0
  85. package/dist/handlers/chat/types.js +1 -0
  86. package/dist/handlers/chatHandler.d.ts +1 -44
  87. package/dist/handlers/chatHandler.js +186 -1162
  88. package/dist/handlers/generateFieldHandler.d.ts +3 -8
  89. package/dist/handlers/generateFieldHandler.js +71 -99
  90. package/dist/handlers/http.d.ts +17 -0
  91. package/dist/handlers/http.js +21 -0
  92. package/dist/handlers/mediaUploadHandler.js +12 -34
  93. package/dist/handlers/mentionSuggestionHandler.d.ts +1 -1
  94. package/dist/handlers/mentionSuggestionHandler.js +6 -10
  95. package/dist/handlers/proposalDiffHandler.d.ts +1 -1
  96. package/dist/handlers/proposalDiffHandler.js +72 -195
  97. package/dist/handlers/translateDocumentHandler.d.ts +3 -8
  98. package/dist/handlers/translateDocumentHandler.js +26 -82
  99. package/dist/index.d.ts +4 -30
  100. package/dist/index.js +31 -421
  101. package/dist/plugin/admin.d.ts +30 -0
  102. package/dist/plugin/admin.js +155 -0
  103. package/dist/plugin/collections.d.ts +3 -0
  104. package/dist/plugin/collections.js +169 -0
  105. package/dist/plugin/endpoints.d.ts +23 -0
  106. package/dist/plugin/endpoints.js +84 -0
  107. package/dist/plugin/options.d.ts +4 -0
  108. package/dist/plugin/options.js +13 -0
  109. package/dist/plugin/types.d.ts +28 -0
  110. package/dist/plugin/types.js +1 -0
  111. package/dist/{payload/shared.d.ts → utils/data.d.ts} +2 -12
  112. package/package.json +1 -1
  113. package/dist/payload/proposalData.js +0 -578
  114. /package/dist/{payload → features}/collectionPermissions.d.ts +0 -0
  115. /package/dist/{payload → features/content}/documentTranslation.d.ts +0 -0
  116. /package/dist/{payload → features/content}/documentTranslation.js +0 -0
  117. /package/dist/{ai → features}/promptCaching.js +0 -0
  118. /package/dist/{ai/proposalRepair.js → features/proposals/repair.js} +0 -0
  119. /package/dist/{ai/providerOptions.d.ts → features/providers/options.d.ts} +0 -0
  120. /package/dist/{ai/providerOptions.js → features/providers/options.js} +0 -0
  121. /package/dist/{payload/normalizeData.d.ts → features/schema/normalize.d.ts} +0 -0
  122. /package/dist/{ai → features}/sensitiveData.d.ts +0 -0
  123. /package/dist/{ai → features}/tokenUsage.js +0 -0
  124. /package/dist/{payload/shared.js → utils/data.js} +0 -0
  125. /package/dist/{payload → utils}/logging.d.ts +0 -0
  126. /package/dist/{payload → utils}/logging.js +0 -0
@@ -0,0 +1,13 @@
1
+ import type { FieldConfig } from "../schema/normalize.js";
2
+ export declare const getFieldOptionValues: (field: FieldConfig) => string[];
3
+ export declare const inferOptionValueFromText: ({ field, requireFieldMention, text, }: {
4
+ field: FieldConfig;
5
+ requireFieldMention?: boolean;
6
+ text?: string;
7
+ }) => string | undefined;
8
+ export declare const getDefaultFieldValue: ({ field, inferenceText, label, titleFieldName, }: {
9
+ field: FieldConfig;
10
+ inferenceText?: string;
11
+ label: string;
12
+ titleFieldName?: string;
13
+ }) => {} | null | undefined;
@@ -0,0 +1,46 @@
1
+ import { getOptionValue, getSafeProposalLabel } from "../../utils/data.js";
2
+ const isTitleLikeField = (field, titleFieldName)=>{
3
+ const normalizedName = field.name?.toLowerCase();
4
+ return Boolean(field.name && (field.name === titleFieldName || normalizedName === "title" || normalizedName === "name" || normalizedName === "label" || normalizedName === "headline"));
5
+ };
6
+ export const getFieldOptionValues = (field)=>(field.options || []).map((option)=>getOptionValue(option)).filter((option)=>Boolean(option));
7
+ const escapeRegExp = (value)=>value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8
+ const getFieldNameVariants = (fieldName)=>{
9
+ if (!fieldName) return [];
10
+ const spacedName = fieldName.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase();
11
+ return Array.from(new Set([
12
+ fieldName.toLowerCase(),
13
+ spacedName
14
+ ])).filter(Boolean);
15
+ };
16
+ export const inferOptionValueFromText = ({ field, requireFieldMention = false, text })=>{
17
+ const normalizedText = text?.trim().toLowerCase();
18
+ if (!normalizedText) return undefined;
19
+ if (requireFieldMention && !getFieldNameVariants(field.name).some((variant)=>new RegExp(`\\b${escapeRegExp(variant)}\\b`, "i").test(normalizedText))) {
20
+ return undefined;
21
+ }
22
+ return getFieldOptionValues(field).sort((left, right)=>right.length - left.length).find((optionValue)=>new RegExp(`\\b${escapeRegExp(optionValue.toLowerCase())}\\b`, "i").test(normalizedText));
23
+ };
24
+ export const getDefaultFieldValue = ({ field, inferenceText, label, titleFieldName })=>{
25
+ if (field.name === "_status") return "draft";
26
+ if (field.type === "checkbox") return false;
27
+ if (field.type === "select" || field.type === "radio") {
28
+ const inferredOptionValue = inferOptionValueFromText({
29
+ field,
30
+ requireFieldMention: true,
31
+ text: inferenceText || label
32
+ });
33
+ if (inferredOptionValue !== undefined) return inferredOptionValue;
34
+ if (field.defaultValue !== undefined) return field.defaultValue;
35
+ return getOptionValue(field.options?.[0]);
36
+ }
37
+ if (field.defaultValue !== undefined) return field.defaultValue;
38
+ if ([
39
+ "email",
40
+ "text",
41
+ "textarea"
42
+ ].includes(field.type || "") && isTitleLikeField(field, titleFieldName)) {
43
+ return getSafeProposalLabel(label);
44
+ }
45
+ return undefined;
46
+ };
@@ -0,0 +1,6 @@
1
+ import { type FieldConfig } from "../schema/normalize.js";
2
+ import type { NormalizationState } from "./normalizationTypes.js";
3
+ export declare const prepareSingleLocaleData: ({ data, fields, ...state }: NormalizationState & {
4
+ data: Record<string, unknown>;
5
+ fields: readonly FieldConfig[];
6
+ }) => Record<string, unknown>;
@@ -0,0 +1,78 @@
1
+ import { normalizeDataForFields } from "../schema/normalize.js";
2
+ import { normalizeFieldValue } from "./fieldNormalization.js";
3
+ import { getDefaultFieldValue, inferOptionValueFromText } from "./optionInference.js";
4
+ const normalizeRecordForFields = ({ allowSafeFallback, coercedFields, enforceRequiredChildren, fields, inferenceText, issues, label, mode, path, titleFieldName, value })=>{
5
+ const normalizedData = {};
6
+ const fieldsByName = new Map(fields.filter((field)=>Boolean(field.name)).map((field)=>[
7
+ field.name,
8
+ field
9
+ ]));
10
+ for (const [key, fieldValue] of Object.entries(value)){
11
+ const field = fieldsByName.get(key);
12
+ const fieldPath = path ? `${path}.${key}` : key;
13
+ if (!field) {
14
+ issues.push({
15
+ code: "unknown_field",
16
+ message: "Field does not exist in the schema.",
17
+ path: fieldPath
18
+ });
19
+ continue;
20
+ }
21
+ const normalizedValue = normalizeFieldValue({
22
+ coercedFields,
23
+ enforceRequiredChildren,
24
+ field,
25
+ inferenceText,
26
+ issues,
27
+ label,
28
+ mode,
29
+ normalizeRecord: normalizeRecordForFields,
30
+ path: fieldPath,
31
+ titleFieldName,
32
+ value: fieldValue
33
+ });
34
+ if (normalizedValue !== undefined) normalizedData[key] = normalizedValue;
35
+ }
36
+ if (mode === "create" || enforceRequiredChildren) {
37
+ for (const field of fields.filter((candidate)=>Boolean(candidate.name) && candidate.required === true)){
38
+ const fieldPath = path ? `${path}.${field.name}` : field.name;
39
+ if (normalizedData[field.name] !== undefined) continue;
40
+ const fallbackValue = allowSafeFallback ? getDefaultFieldValue({
41
+ field,
42
+ inferenceText,
43
+ label,
44
+ titleFieldName
45
+ }) : undefined;
46
+ if (fallbackValue !== undefined) {
47
+ normalizedData[field.name] = fallbackValue;
48
+ continue;
49
+ }
50
+ issues.push({
51
+ code: "missing_required_field",
52
+ message: "Required field is missing.",
53
+ path: fieldPath
54
+ });
55
+ }
56
+ }
57
+ for (const field of fields.filter((candidate)=>Boolean(candidate.name))){
58
+ if (normalizedData[field.name] !== undefined || field.type !== "select" && field.type !== "radio") continue;
59
+ const inferredOptionValue = inferOptionValueFromText({
60
+ field,
61
+ requireFieldMention: true,
62
+ text: inferenceText || label
63
+ });
64
+ if (inferredOptionValue !== undefined) {
65
+ normalizedData[field.name] = inferredOptionValue;
66
+ coercedFields.push(path ? `${path}.${field.name}` : field.name);
67
+ }
68
+ }
69
+ return normalizedData;
70
+ };
71
+ export const prepareSingleLocaleData = ({ data, fields, ...state })=>normalizeDataForFields(fields, normalizeRecordForFields({
72
+ ...state,
73
+ allowSafeFallback: true,
74
+ enforceRequiredChildren: false,
75
+ fields,
76
+ path: "",
77
+ value: data
78
+ })).data;
@@ -0,0 +1,8 @@
1
+ import type { FieldConfig } from "../schema/normalize.js";
2
+ import type { ProposalValidationIssue } from "./normalizationTypes.js";
3
+ export declare const normalizeRelationshipValue: ({ field, issues, path, value, }: {
4
+ field: FieldConfig;
5
+ issues: ProposalValidationIssue[];
6
+ path: string;
7
+ value: unknown;
8
+ }) => unknown;
@@ -0,0 +1,71 @@
1
+ import { isRecord } from "../../utils/data.js";
2
+ const normalizeRelationshipScalar = (value)=>{
3
+ if (typeof value === "number") {
4
+ if (!Number.isInteger(value) || value <= 0) return undefined;
5
+ return value;
6
+ }
7
+ const trimmedValue = value.trim();
8
+ if (!trimmedValue || /\s/.test(trimmedValue)) return undefined;
9
+ if (/^\d+$/.test(trimmedValue)) {
10
+ const numericValue = Number(trimmedValue);
11
+ return numericValue > 0 ? numericValue : undefined;
12
+ }
13
+ return trimmedValue;
14
+ };
15
+ export const normalizeRelationshipValue = ({ field, issues, path, value })=>{
16
+ const allowedRelationTargets = Array.isArray(field.relationTo) ? field.relationTo.filter((item)=>typeof item === "string") : typeof field.relationTo === "string" ? [
17
+ field.relationTo
18
+ ] : [];
19
+ const addInvalidRelationship = (message, itemPath)=>{
20
+ issues.push({
21
+ code: "invalid_relationship",
22
+ message,
23
+ path: itemPath
24
+ });
25
+ };
26
+ const normalizeSingle = (item, itemPath)=>{
27
+ if (typeof item === "number" || typeof item === "string") {
28
+ const normalizedValue = normalizeRelationshipScalar(item);
29
+ if (normalizedValue !== undefined) return normalizedValue;
30
+ addInvalidRelationship(typeof item === "string" ? "Relationship and upload fields must use a valid document ID, not free text." : "Relationship and upload fields must use a valid document ID.", itemPath);
31
+ return undefined;
32
+ }
33
+ if (!isRecord(item)) {
34
+ addInvalidRelationship("Relationship and upload fields must use a document ID or an object with id/value.", itemPath);
35
+ return undefined;
36
+ }
37
+ if (typeof item.id === "string" || typeof item.id === "number") {
38
+ const normalizedValue = normalizeRelationshipScalar(item.id);
39
+ if (normalizedValue !== undefined) return normalizedValue;
40
+ addInvalidRelationship("Relationship and upload fields must use a valid document ID.", itemPath);
41
+ return undefined;
42
+ }
43
+ if (typeof item.value === "string" || typeof item.value === "number") {
44
+ const normalizedValue = normalizeRelationshipScalar(item.value);
45
+ if (normalizedValue === undefined) {
46
+ addInvalidRelationship("Relationship and upload fields must use a valid document ID.", itemPath);
47
+ return undefined;
48
+ }
49
+ if (allowedRelationTargets.length > 1) {
50
+ const relationTo = typeof item.relationTo === "string" ? item.relationTo : null;
51
+ if (!relationTo || !allowedRelationTargets.includes(relationTo)) {
52
+ addInvalidRelationship(`Relationship must include one of: ${allowedRelationTargets.join(", ")}.`, itemPath);
53
+ return undefined;
54
+ }
55
+ return {
56
+ relationTo,
57
+ value: normalizedValue
58
+ };
59
+ }
60
+ return normalizedValue;
61
+ }
62
+ addInvalidRelationship("Relationship and upload fields must use a document ID or an object with id/value.", itemPath);
63
+ return undefined;
64
+ };
65
+ if (!field.hasMany) return normalizeSingle(value, path);
66
+ if (!Array.isArray(value)) {
67
+ addInvalidRelationship("Relationship field expects an array of document IDs.", path);
68
+ return undefined;
69
+ }
70
+ return value.map((item, index)=>normalizeSingle(item, `${path}.${index}`)).filter((item)=>item !== undefined);
71
+ };
@@ -1,4 +1,4 @@
1
- import type { ProposalValidationIssue } from "../payload/proposalData.js";
1
+ import type { ProposalValidationIssue } from "./data.js";
2
2
  export declare const maxProposalRepairAttempts = 1;
3
3
  export type CompactProposalRepairIssue = {
4
4
  code: string;
@@ -1,7 +1,5 @@
1
- export type AIActionSignature = {
2
- expiresAt: string;
3
- value: string;
4
- };
1
+ import type { AIActionSignature } from "./types.js";
2
+ export type { AIActionSignature } from "./types.js";
5
3
  type SignableProposal = Record<string, unknown> & {
6
4
  _aiSignature?: AIActionSignature;
7
5
  };
@@ -9,4 +7,3 @@ export declare const signAIActionProposal: <Proposal extends SignableProposal>(p
9
7
  _aiSignature: AIActionSignature;
10
8
  };
11
9
  export declare const verifyActionProposal: (proposal: SignableProposal) => boolean;
12
- export {};
@@ -1,5 +1,5 @@
1
1
  import { createHmac, timingSafeEqual } from "node:crypto";
2
- import { isRecord } from "../payload/shared.js";
2
+ import { isRecord } from "../../utils/data.js";
3
3
  const signatureTTL = 10 * 60 * 1000;
4
4
  const stableStringify = (value)=>{
5
5
  if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
@@ -0,0 +1,52 @@
1
+ import type { PayloadHandler } from "payload";
2
+ import type { CollectionConfig, FieldConfig } from "../schema/normalize.js";
3
+ import { type PreparedProposalData } from "./data.js";
4
+ import type { ActionProposal, LocalizedDataInput } from "./types.js";
5
+ type Request = Parameters<PayloadHandler>[0];
6
+ type WriteProposal = Exclude<ActionProposal, {
7
+ action: "delete";
8
+ }>;
9
+ type ReadOptions = {
10
+ fallbackLocale?: false;
11
+ locale?: string;
12
+ };
13
+ type BaseProposalTarget = {
14
+ fields: FieldConfig[];
15
+ read: (options?: ReadOptions) => Promise<Record<string, unknown>>;
16
+ schema: CollectionConfig;
17
+ update: (data: Record<string, unknown>, locale?: string, documentID?: string) => Promise<Record<string, unknown>>;
18
+ };
19
+ export type CollectionProposalTarget = BaseProposalTarget & {
20
+ collection: string;
21
+ create: (data: Record<string, unknown>, locale?: string) => Promise<Record<string, unknown>>;
22
+ delete: () => Promise<Record<string, unknown>>;
23
+ documentID?: string;
24
+ type: "collection";
25
+ };
26
+ export type GlobalProposalTarget = BaseProposalTarget & {
27
+ slug: string;
28
+ type: "global";
29
+ };
30
+ export type ProposalTarget = CollectionProposalTarget | GlobalProposalTarget;
31
+ export declare const resolveProposalTarget: ({ proposal, req }: {
32
+ proposal: ActionProposal;
33
+ req: Request;
34
+ }) => ProposalTarget | null;
35
+ export declare const prepareProposalTargetData: ({ inferenceText, proposal, target, }: {
36
+ inferenceText?: string;
37
+ proposal: WriteProposal;
38
+ target: ProposalTarget;
39
+ }) => PreparedProposalData;
40
+ export declare const prepareLocalizedTargetUpdates: ({ defaultLocale, localizedData, target, }: {
41
+ defaultLocale?: string;
42
+ localizedData: LocalizedDataInput;
43
+ target: ProposalTarget;
44
+ }) => Promise<{
45
+ after: Record<string, unknown>;
46
+ before: Record<string, unknown>;
47
+ data: {
48
+ [x: string]: unknown;
49
+ };
50
+ locale: string;
51
+ }[]>;
52
+ export {};
@@ -0,0 +1,125 @@
1
+ import { getSchemaFields } from "../schema/normalize.js";
2
+ import { applyLocalizedRequiredFallbackToPreparedData, prepareProposalWriteData } from "./data.js";
3
+ import { mergeData } from "../../utils/data.js";
4
+ const getLocaleOptions = ({ fallbackLocale, locale } = {})=>({
5
+ ...fallbackLocale === false ? {
6
+ fallbackLocale: false
7
+ } : {},
8
+ ...locale ? {
9
+ locale
10
+ } : {}
11
+ });
12
+ export const resolveProposalTarget = ({ proposal, req })=>{
13
+ if (proposal.action === "updateGlobal") {
14
+ const config = req.payload.config.globals?.find((global)=>global.slug === proposal.slug);
15
+ if (!config) return null;
16
+ const schema = {
17
+ fields: config.fields || [],
18
+ slug: proposal.slug
19
+ };
20
+ return {
21
+ fields: getSchemaFields(schema),
22
+ read: async (options)=>await req.payload.findGlobal({
23
+ depth: 2,
24
+ ...getLocaleOptions(options),
25
+ overrideAccess: false,
26
+ req,
27
+ slug: proposal.slug
28
+ }),
29
+ schema,
30
+ slug: proposal.slug,
31
+ type: "global",
32
+ update: async (data, locale)=>await req.payload.updateGlobal({
33
+ data,
34
+ ...locale ? {
35
+ locale
36
+ } : {},
37
+ overrideAccess: false,
38
+ req,
39
+ slug: proposal.slug
40
+ })
41
+ };
42
+ }
43
+ const config = req.payload.config.collections.find((collection)=>collection.slug === proposal.collection);
44
+ if (!config) return null;
45
+ const documentID = proposal.action === "create" ? undefined : proposal.id;
46
+ const requireDocumentID = (overrideDocumentID)=>{
47
+ const resolvedDocumentID = overrideDocumentID || documentID;
48
+ if (!resolvedDocumentID) throw new Error(`Proposal target ${proposal.collection} requires a document id.`);
49
+ return resolvedDocumentID;
50
+ };
51
+ return {
52
+ collection: proposal.collection,
53
+ create: async (data, locale)=>await req.payload.create({
54
+ collection: proposal.collection,
55
+ data,
56
+ ...locale ? {
57
+ locale
58
+ } : {},
59
+ overrideAccess: false,
60
+ req
61
+ }),
62
+ delete: async ()=>await req.payload.delete({
63
+ collection: proposal.collection,
64
+ id: requireDocumentID(),
65
+ overrideAccess: false,
66
+ req
67
+ }),
68
+ documentID,
69
+ fields: getSchemaFields(config),
70
+ read: async (options)=>await req.payload.findByID({
71
+ collection: proposal.collection,
72
+ depth: 2,
73
+ id: requireDocumentID(),
74
+ ...getLocaleOptions(options),
75
+ overrideAccess: false,
76
+ req
77
+ }),
78
+ schema: config,
79
+ type: "collection",
80
+ update: async (data, locale, updateDocumentID)=>await req.payload.update({
81
+ collection: proposal.collection,
82
+ data,
83
+ id: requireDocumentID(updateDocumentID),
84
+ ...locale ? {
85
+ locale
86
+ } : {},
87
+ overrideAccess: false,
88
+ req
89
+ })
90
+ };
91
+ };
92
+ export const prepareProposalTargetData = ({ inferenceText, proposal, target })=>prepareProposalWriteData({
93
+ collectionConfig: target.schema,
94
+ ..."localizedData" in proposal ? {
95
+ localizedData: proposal.localizedData
96
+ } : {
97
+ data: proposal.data
98
+ },
99
+ inferenceText,
100
+ label: proposal.label,
101
+ mode: proposal.action === "create" ? "create" : "update"
102
+ });
103
+ export const prepareLocalizedTargetUpdates = async ({ defaultLocale, localizedData, target })=>{
104
+ const defaultLocaleDoc = defaultLocale ? await target.read({
105
+ fallbackLocale: false,
106
+ locale: defaultLocale
107
+ }) : null;
108
+ return Promise.all(Object.entries(localizedData).map(async ([locale, localeData])=>{
109
+ const before = locale === defaultLocale && defaultLocaleDoc ? defaultLocaleDoc : await target.read({
110
+ fallbackLocale: false,
111
+ locale
112
+ });
113
+ const completedData = applyLocalizedRequiredFallbackToPreparedData({
114
+ fallbackSource: locale === defaultLocale ? before : defaultLocaleDoc || before,
115
+ fields: target.fields,
116
+ preparedData: localeData
117
+ });
118
+ return {
119
+ after: mergeData(before, completedData),
120
+ before,
121
+ data: completedData,
122
+ locale
123
+ };
124
+ }));
125
+ };
@@ -0,0 +1,49 @@
1
+ export type AIActionSignature = {
2
+ expiresAt: string;
3
+ value: string;
4
+ };
5
+ export type LocalizedDataInput = Record<string, Record<string, unknown>>;
6
+ export 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
+ collection?: string;
34
+ id?: string;
35
+ locale?: string;
36
+ slug?: string;
37
+ };
38
+ export type ActionProposalReference = {
39
+ action: ActionProposal["action"];
40
+ collection?: string;
41
+ id?: string;
42
+ label: string;
43
+ locale?: string;
44
+ slug?: string;
45
+ };
46
+ export type ProposalDiff = {
47
+ after: unknown;
48
+ before: unknown;
49
+ };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,12 @@
1
+ import type { ActionProposal } from "./types.js";
2
+ type ProposalValidationError = "invalid_shape" | "invalid_signature" | "missing";
3
+ export type SignedProposalValidation = {
4
+ error: ProposalValidationError;
5
+ ok: false;
6
+ proposal?: Partial<ActionProposal>;
7
+ } | {
8
+ ok: true;
9
+ proposal: ActionProposal;
10
+ };
11
+ export declare const validateSignedProposal: (value: unknown) => SignedProposalValidation;
12
+ export {};
@@ -0,0 +1,26 @@
1
+ import { isActionProposal, isRecord } from "../../utils/data.js";
2
+ import { verifyActionProposal } from "./signing.js";
3
+ export const validateSignedProposal = (value)=>{
4
+ if (value === undefined || value === null) return {
5
+ error: "missing",
6
+ ok: false
7
+ };
8
+ if (!isRecord(value)) return {
9
+ error: "invalid_shape",
10
+ ok: false
11
+ };
12
+ if (!verifyActionProposal(value)) return {
13
+ error: "invalid_signature",
14
+ ok: false,
15
+ proposal: value
16
+ };
17
+ if (!isActionProposal(value)) return {
18
+ error: "invalid_shape",
19
+ ok: false,
20
+ proposal: value
21
+ };
22
+ return {
23
+ ok: true,
24
+ proposal: value
25
+ };
26
+ };
@@ -0,0 +1,53 @@
1
+ import type { LanguageModel } from "ai";
2
+ import type { PayloadHandler } from "payload";
3
+ import { type AIModelConfig, type AIProvider, type ResolvedAIProviderConfig } from "./options.js";
4
+ import { type ResolvedMaxTokenUsageOptions, type TokenUsageData, type TokenUsageLimit } from "../tokenUsage.js";
5
+ export type AIRequestOptions = {
6
+ allowUserApiKeys?: boolean;
7
+ maxTokenUsage?: ResolvedMaxTokenUsageOptions;
8
+ models?: AIModelConfig;
9
+ providers?: ResolvedAIProviderConfig[];
10
+ };
11
+ export type AIRequestUser = {
12
+ aiApiKey?: string | null;
13
+ aiProvider?: AIProvider | string | null;
14
+ id: number | string;
15
+ };
16
+ type AIRequestFailure = {
17
+ code: "missing_api_key";
18
+ managedProvider: boolean;
19
+ message: string;
20
+ modelID: string;
21
+ provider: AIProvider;
22
+ providerID: string;
23
+ } | {
24
+ code: "token_limit";
25
+ message: string;
26
+ tokenLimit: TokenUsageLimit;
27
+ } | {
28
+ code: "unsupported_model" | "unsupported_provider";
29
+ message: string;
30
+ };
31
+ export type AIRequestContext = {
32
+ loadModel: () => Promise<LanguageModel>;
33
+ managedProvider: ResolvedAIProviderConfig | null;
34
+ modelID: string;
35
+ provider: AIProvider;
36
+ providerID: string;
37
+ recordUsage: (usage: TokenUsageData) => Promise<void>;
38
+ };
39
+ export type AIRequestResolution = {
40
+ context: AIRequestContext;
41
+ ok: true;
42
+ } | {
43
+ error: AIRequestFailure;
44
+ ok: false;
45
+ };
46
+ export declare const resolveAIRequestContext: ({ options, req, requestedModel, requestedProvider, user, }: {
47
+ options: AIRequestOptions;
48
+ req: Parameters<PayloadHandler>[0];
49
+ requestedModel?: string | null;
50
+ requestedProvider?: string | null;
51
+ user: AIRequestUser;
52
+ }) => Promise<AIRequestResolution>;
53
+ export {};
@@ -0,0 +1,90 @@
1
+ import { isAIProvider } from "./options.js";
2
+ import { getModel, getProviderConfig } from "./runtime.js";
3
+ import { getExceededTokenUsageLimit, recordTokenUsage } from "../tokenUsage.js";
4
+ export const resolveAIRequestContext = async ({ options, req, requestedModel, requestedProvider, user })=>{
5
+ const tokenLimit = await getExceededTokenUsageLimit({
6
+ maxTokenUsage: options.maxTokenUsage,
7
+ req,
8
+ userID: user.id
9
+ });
10
+ if (tokenLimit) {
11
+ return {
12
+ error: {
13
+ code: "token_limit",
14
+ message: "AI token usage limit reached.",
15
+ tokenLimit
16
+ },
17
+ ok: false
18
+ };
19
+ }
20
+ const managedProviders = options.providers?.length ? options.providers : null;
21
+ const providerSelection = requestedProvider || (managedProviders ? managedProviders[0].id : user.aiProvider || "openai");
22
+ const managedProvider = managedProviders?.find((providerConfig)=>providerConfig.id === providerSelection) || null;
23
+ if (managedProviders && !managedProvider || !managedProvider && !isAIProvider(providerSelection)) {
24
+ return {
25
+ error: {
26
+ code: "unsupported_provider",
27
+ message: `Unsupported AI provider: ${providerSelection}`
28
+ },
29
+ ok: false
30
+ };
31
+ }
32
+ const provider = managedProvider?.provider || providerSelection;
33
+ const modelSelection = requestedModel || managedProvider?.defaultModel;
34
+ if (managedProvider && modelSelection && !managedProvider.models.some((model)=>model.value === modelSelection)) {
35
+ return {
36
+ error: {
37
+ code: "unsupported_model",
38
+ message: `Unsupported model "${modelSelection}" for AI provider "${managedProvider.id}".`
39
+ },
40
+ ok: false
41
+ };
42
+ }
43
+ const providerConfig = getProviderConfig({
44
+ apiKey: managedProvider ? managedProvider.apiKey : options.allowUserApiKeys === false ? null : user.aiApiKey,
45
+ defaultModels: options.models?.defaults,
46
+ model: modelSelection,
47
+ provider
48
+ });
49
+ const providerID = managedProvider?.id || provider;
50
+ if (!providerConfig.apiKey) {
51
+ return {
52
+ error: {
53
+ code: "missing_api_key",
54
+ managedProvider: Boolean(managedProvider),
55
+ message: "Configure an AI provider API key first.",
56
+ modelID: providerConfig.modelID,
57
+ provider,
58
+ providerID
59
+ },
60
+ ok: false
61
+ };
62
+ }
63
+ return {
64
+ context: {
65
+ loadModel: ()=>getModel({
66
+ apiKey: providerConfig.apiKey,
67
+ ...managedProvider?.baseURL ? {
68
+ baseURL: managedProvider.baseURL
69
+ } : {},
70
+ model: providerConfig.modelID,
71
+ provider
72
+ }),
73
+ managedProvider,
74
+ modelID: providerConfig.modelID,
75
+ provider,
76
+ providerID,
77
+ recordUsage: async (usage)=>{
78
+ if (!options.maxTokenUsage) return;
79
+ await recordTokenUsage({
80
+ model: providerConfig.modelID,
81
+ provider: providerID,
82
+ req,
83
+ usage,
84
+ userID: user.id
85
+ });
86
+ }
87
+ },
88
+ ok: true
89
+ };
90
+ };