@humaan/payload-plugin-ai-assistant 0.0.0-stage → 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 (190) hide show
  1. package/README.md +277 -2
  2. package/dist/admin/AiAssistantControl.d.ts +11 -0
  3. package/dist/admin/AiAssistantControl.js +31 -0
  4. package/dist/admin/AiAssistantControl.js.map +1 -0
  5. package/dist/admin/AiAssistantPanel.d.ts +10 -0
  6. package/dist/admin/AiAssistantPanel.js +208 -0
  7. package/dist/admin/AiAssistantPanel.js.map +1 -0
  8. package/dist/admin/AiAssistantPanel.module.scss +689 -0
  9. package/dist/admin/AiSourceSwitch.d.ts +13 -0
  10. package/dist/admin/AiSourceSwitch.js +14 -0
  11. package/dist/admin/AiSourceSwitch.js.map +1 -0
  12. package/dist/admin/BudgetField.d.ts +8 -0
  13. package/dist/admin/BudgetField.js +145 -0
  14. package/dist/admin/BudgetField.js.map +1 -0
  15. package/dist/admin/BudgetField.module.scss +47 -0
  16. package/dist/admin/Conversation.d.ts +12 -0
  17. package/dist/admin/Conversation.js +469 -0
  18. package/dist/admin/Conversation.js.map +1 -0
  19. package/dist/admin/Markdown.d.ts +9 -0
  20. package/dist/admin/Markdown.js +63 -0
  21. package/dist/admin/Markdown.js.map +1 -0
  22. package/dist/admin/PageContextBridge.d.ts +5 -0
  23. package/dist/admin/PageContextBridge.js +72 -0
  24. package/dist/admin/PageContextBridge.js.map +1 -0
  25. package/dist/admin/Pill.d.ts +11 -0
  26. package/dist/admin/Pill.js +27 -0
  27. package/dist/admin/Pill.js.map +1 -0
  28. package/dist/admin/PlanCard.d.ts +14 -0
  29. package/dist/admin/PlanCard.js +465 -0
  30. package/dist/admin/PlanCard.js.map +1 -0
  31. package/dist/admin/SettingsAlerts.d.ts +7 -0
  32. package/dist/admin/SettingsAlerts.js +37 -0
  33. package/dist/admin/SettingsAlerts.js.map +1 -0
  34. package/dist/admin/UsageSummary.d.ts +11 -0
  35. package/dist/admin/UsageSummary.js +129 -0
  36. package/dist/admin/UsageSummary.js.map +1 -0
  37. package/dist/admin/attachmentUpload.d.ts +25 -0
  38. package/dist/admin/attachmentUpload.js +76 -0
  39. package/dist/admin/attachmentUpload.js.map +1 -0
  40. package/dist/admin/openDocument.d.ts +13 -0
  41. package/dist/admin/openDocument.js +34 -0
  42. package/dist/admin/openDocument.js.map +1 -0
  43. package/dist/admin/percent.d.ts +10 -0
  44. package/dist/admin/percent.js +21 -0
  45. package/dist/admin/percent.js.map +1 -0
  46. package/dist/admin/threads.d.ts +21 -0
  47. package/dist/admin/threads.js +92 -0
  48. package/dist/admin/threads.js.map +1 -0
  49. package/dist/admin/toolSteps.d.ts +12 -0
  50. package/dist/admin/toolSteps.js +57 -0
  51. package/dist/admin/toolSteps.js.map +1 -0
  52. package/dist/admin/transport.d.ts +24 -0
  53. package/dist/admin/transport.js +75 -0
  54. package/dist/admin/transport.js.map +1 -0
  55. package/dist/admin/useAttachments.d.ts +32 -0
  56. package/dist/admin/useAttachments.js +91 -0
  57. package/dist/admin/useAttachments.js.map +1 -0
  58. package/dist/admin/useEntityLabel.d.ts +3 -0
  59. package/dist/admin/useEntityLabel.js +23 -0
  60. package/dist/admin/useEntityLabel.js.map +1 -0
  61. package/dist/attachments/collection.d.ts +15 -0
  62. package/dist/attachments/collection.js +90 -0
  63. package/dist/attachments/collection.js.map +1 -0
  64. package/dist/attachments/conversation.d.ts +25 -0
  65. package/dist/attachments/conversation.js +105 -0
  66. package/dist/attachments/conversation.js.map +1 -0
  67. package/dist/attachments/rules.d.ts +9 -0
  68. package/dist/attachments/rules.js +17 -0
  69. package/dist/attachments/rules.js.map +1 -0
  70. package/dist/attachments/store.d.ts +27 -0
  71. package/dist/attachments/store.js +187 -0
  72. package/dist/attachments/store.js.map +1 -0
  73. package/dist/billing.d.ts +60 -0
  74. package/dist/billing.js +116 -0
  75. package/dist/billing.js.map +1 -0
  76. package/dist/chat/history.d.ts +37 -0
  77. package/dist/chat/history.js +165 -0
  78. package/dist/chat/history.js.map +1 -0
  79. package/dist/chat/messages.d.ts +5 -0
  80. package/dist/chat/messages.js +3 -0
  81. package/dist/chat/messages.js.map +1 -0
  82. package/dist/chat/threads.d.ts +33 -0
  83. package/dist/chat/threads.js +77 -0
  84. package/dist/chat/threads.js.map +1 -0
  85. package/dist/chat/tools.d.ts +76 -0
  86. package/dist/chat/tools.js +596 -0
  87. package/dist/chat/tools.js.map +1 -0
  88. package/dist/chat/usage.d.ts +66 -0
  89. package/dist/chat/usage.js +219 -0
  90. package/dist/chat/usage.js.map +1 -0
  91. package/dist/collectionAccess.d.ts +6 -0
  92. package/dist/collectionAccess.js +32 -0
  93. package/dist/collectionAccess.js.map +1 -0
  94. package/dist/constants.d.ts +64 -0
  95. package/dist/constants.js +51 -0
  96. package/dist/constants.js.map +1 -0
  97. package/dist/endpoints/access.d.ts +4 -0
  98. package/dist/endpoints/access.js +28 -0
  99. package/dist/endpoints/access.js.map +1 -0
  100. package/dist/endpoints/allowance.d.ts +22 -0
  101. package/dist/endpoints/allowance.js +37 -0
  102. package/dist/endpoints/allowance.js.map +1 -0
  103. package/dist/endpoints/applyEndpoint.d.ts +12 -0
  104. package/dist/endpoints/applyEndpoint.js +170 -0
  105. package/dist/endpoints/applyEndpoint.js.map +1 -0
  106. package/dist/endpoints/budget.d.ts +30 -0
  107. package/dist/endpoints/budget.js +153 -0
  108. package/dist/endpoints/budget.js.map +1 -0
  109. package/dist/endpoints/chatEndpoint.d.ts +12 -0
  110. package/dist/endpoints/chatEndpoint.js +295 -0
  111. package/dist/endpoints/chatEndpoint.js.map +1 -0
  112. package/dist/endpoints/form.d.ts +23 -0
  113. package/dist/endpoints/form.js +95 -0
  114. package/dist/endpoints/form.js.map +1 -0
  115. package/dist/endpoints/usageEndpoint.d.ts +13 -0
  116. package/dist/endpoints/usageEndpoint.js +70 -0
  117. package/dist/endpoints/usageEndpoint.js.map +1 -0
  118. package/dist/index.d.ts +4 -0
  119. package/dist/index.js +5 -0
  120. package/dist/index.js.map +1 -0
  121. package/dist/lexical/editorConfig.d.ts +4 -0
  122. package/dist/lexical/editorConfig.js +19 -0
  123. package/dist/lexical/editorConfig.js.map +1 -0
  124. package/dist/lexical/lexicalToMarkdown.d.ts +10 -0
  125. package/dist/lexical/lexicalToMarkdown.js +10 -0
  126. package/dist/lexical/lexicalToMarkdown.js.map +1 -0
  127. package/dist/lexical/markdownToLexical.d.ts +7 -0
  128. package/dist/lexical/markdownToLexical.js +10 -0
  129. package/dist/lexical/markdownToLexical.js.map +1 -0
  130. package/dist/plan/allowed.d.ts +7 -0
  131. package/dist/plan/allowed.js +19 -0
  132. package/dist/plan/allowed.js.map +1 -0
  133. package/dist/plan/applyPlan.d.ts +16 -0
  134. package/dist/plan/applyPlan.js +353 -0
  135. package/dist/plan/applyPlan.js.map +1 -0
  136. package/dist/plan/catalogue.d.ts +37 -0
  137. package/dist/plan/catalogue.js +157 -0
  138. package/dist/plan/catalogue.js.map +1 -0
  139. package/dist/plan/diff.d.ts +13 -0
  140. package/dist/plan/diff.js +87 -0
  141. package/dist/plan/diff.js.map +1 -0
  142. package/dist/plan/parsePlan.d.ts +27 -0
  143. package/dist/plan/parsePlan.js +322 -0
  144. package/dist/plan/parsePlan.js.map +1 -0
  145. package/dist/plan/prompt.d.ts +1 -0
  146. package/dist/plan/prompt.js +45 -0
  147. package/dist/plan/prompt.js.map +1 -0
  148. package/dist/plan/stored.d.ts +53 -0
  149. package/dist/plan/stored.js +3 -0
  150. package/dist/plan/stored.js.map +1 -0
  151. package/dist/plan/tokens.d.ts +8 -0
  152. package/dist/plan/tokens.js +79 -0
  153. package/dist/plan/tokens.js.map +1 -0
  154. package/dist/plan/view.d.ts +4 -0
  155. package/dist/plan/view.js +43 -0
  156. package/dist/plan/view.js.map +1 -0
  157. package/dist/plugin.d.ts +3 -0
  158. package/dist/plugin.js +464 -0
  159. package/dist/plugin.js.map +1 -0
  160. package/dist/read/outline.d.ts +7 -0
  161. package/dist/read/outline.js +70 -0
  162. package/dist/read/outline.js.map +1 -0
  163. package/dist/read/readTarget.d.ts +4 -0
  164. package/dist/read/readTarget.js +13 -0
  165. package/dist/read/readTarget.js.map +1 -0
  166. package/dist/read/readable.d.ts +8 -0
  167. package/dist/read/readable.js +131 -0
  168. package/dist/read/readable.js.map +1 -0
  169. package/dist/runtime.d.ts +15 -0
  170. package/dist/runtime.js +14 -0
  171. package/dist/runtime.js.map +1 -0
  172. package/dist/schema/fields.d.ts +113 -0
  173. package/dist/schema/fields.js +1036 -0
  174. package/dist/schema/fields.js.map +1 -0
  175. package/dist/schema/listingFields.d.ts +9 -0
  176. package/dist/schema/listingFields.js +95 -0
  177. package/dist/schema/listingFields.js.map +1 -0
  178. package/dist/schema/slugify.d.ts +2 -0
  179. package/dist/schema/slugify.js +5 -0
  180. package/dist/schema/slugify.js.map +1 -0
  181. package/dist/settings.d.ts +10 -0
  182. package/dist/settings.js +54 -0
  183. package/dist/settings.js.map +1 -0
  184. package/dist/types.d.ts +198 -0
  185. package/dist/types.js +6 -0
  186. package/dist/types.js.map +1 -0
  187. package/dist/uploads/routeAsset.d.ts +27 -0
  188. package/dist/uploads/routeAsset.js +127 -0
  189. package/dist/uploads/routeAsset.js.map +1 -0
  190. package/package.json +122 -3
@@ -0,0 +1,28 @@
1
+ import { aiAssistantGlobalSlug } from '../constants.js';
2
+ import { canUseAssistant, parseSettings } from '../settings.js';
3
+ /** The stored settings when the user may run the assistant, otherwise the response to send. */ export async function allowAssistant(req, fallback) {
4
+ if (!req.user) {
5
+ return Response.json({
6
+ message: 'You need to be logged in.'
7
+ }, {
8
+ status: 401
9
+ });
10
+ }
11
+ const stored = await req.payload.findGlobal({
12
+ slug: aiAssistantGlobalSlug,
13
+ depth: 0,
14
+ overrideAccess: true,
15
+ req
16
+ });
17
+ const settings = parseSettings(stored, fallback);
18
+ if (!canUseAssistant(req.user, settings)) {
19
+ return Response.json({
20
+ message: 'You do not have access to the assistant.'
21
+ }, {
22
+ status: 403
23
+ });
24
+ }
25
+ return settings;
26
+ }
27
+
28
+ //# sourceMappingURL=access.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/access.ts"],"sourcesContent":["import type { GlobalSlug, PayloadRequest } from 'payload'\n\nimport type { AssistantSettings } from '../types.js'\n\nimport { aiAssistantGlobalSlug } from '../constants.js'\nimport { canUseAssistant, parseSettings } from '../settings.js'\n\n/** The stored settings when the user may run the assistant, otherwise the response to send. */\nexport async function allowAssistant(\n req: PayloadRequest,\n fallback: AssistantSettings,\n): Promise<AssistantSettings | Response> {\n if (!req.user) {\n return Response.json({ message: 'You need to be logged in.' }, { status: 401 })\n }\n const stored = await req.payload.findGlobal({\n slug: aiAssistantGlobalSlug as GlobalSlug,\n depth: 0,\n overrideAccess: true,\n req,\n })\n const settings = parseSettings(stored, fallback)\n if (!canUseAssistant(req.user, settings)) {\n return Response.json({ message: 'You do not have access to the assistant.' }, { status: 403 })\n }\n return settings\n}\n"],"names":["aiAssistantGlobalSlug","canUseAssistant","parseSettings","allowAssistant","req","fallback","user","Response","json","message","status","stored","payload","findGlobal","slug","depth","overrideAccess","settings"],"mappings":"AAIA,SAASA,qBAAqB,QAAQ,kBAAiB;AACvD,SAASC,eAAe,EAAEC,aAAa,QAAQ,iBAAgB;AAE/D,6FAA6F,GAC7F,OAAO,eAAeC,eACpBC,GAAmB,EACnBC,QAA2B;IAE3B,IAAI,CAACD,IAAIE,IAAI,EAAE;QACb,OAAOC,SAASC,IAAI,CAAC;YAAEC,SAAS;QAA4B,GAAG;YAAEC,QAAQ;QAAI;IAC/E;IACA,MAAMC,SAAS,MAAMP,IAAIQ,OAAO,CAACC,UAAU,CAAC;QAC1CC,MAAMd;QACNe,OAAO;QACPC,gBAAgB;QAChBZ;IACF;IACA,MAAMa,WAAWf,cAAcS,QAAQN;IACvC,IAAI,CAACJ,gBAAgBG,IAAIE,IAAI,EAAEW,WAAW;QACxC,OAAOV,SAASC,IAAI,CAAC;YAAEC,SAAS;QAA2C,GAAG;YAAEC,QAAQ;QAAI;IAC9F;IACA,OAAOO;AACT"}
@@ -0,0 +1,22 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { BudgetView } from '../types.js';
3
+ import type { ProjectBudget } from './budget.js';
4
+ /** The project's AI Gateway budget for the current period, which the percentages in the settings are shares of. */
5
+ export type Allowance = ProjectBudget;
6
+ /**
7
+ * The project's budget, or a reason it could not be read, in words for an admin. In US dollars; never sent to the
8
+ * browser. There is no fallback: the team's credit is shared with every other project, so it would mislead.
9
+ */
10
+ export declare function accountAllowance(req: PayloadRequest): Promise<Allowance | string>;
11
+ /** What is left as a share of the budget. Low once `lowBudgetUsedShare` of it is used. */
12
+ export declare function budgetView(allowance: Allowance): BudgetView;
13
+ /**
14
+ * Each editor's daily cap in US dollars: the cap's share of the budget. Undefined when there is no cap, because it is
15
+ * set to 0 or the budget could not be read.
16
+ */
17
+ export declare function dailyCapUsd(dailySpendCapPercent: number, allowance: Allowance | string): number | undefined;
18
+ /**
19
+ * A budget rejection from the gateway, reworded without its dollar amounts. Undefined for any other error.
20
+ * The gateway answers 402 with `quota_for_entity_exceeded` and a message naming the spend and limit.
21
+ */
22
+ export declare function budgetExceededMessage(message: string, allowance: Allowance | string, timeZone: string): string | undefined;
@@ -0,0 +1,37 @@
1
+ import { lowBudgetUsedShare } from '../constants.js';
2
+ import { formatReset, nextReset, projectBudget } from './budget.js';
3
+ /**
4
+ * The project's budget, or a reason it could not be read, in words for an admin. In US dollars; never sent to the
5
+ * browser. There is no fallback: the team's credit is shared with every other project, so it would mislead.
6
+ */ export async function accountAllowance(req) {
7
+ return projectBudget(req);
8
+ }
9
+ /** What is left as a share of the budget. Low once `lowBudgetUsedShare` of it is used. */ export function budgetView(allowance) {
10
+ const remainingShare = allowance.limit > 0 ? Math.max(allowance.limit - allowance.spent, 0) / allowance.limit : 0;
11
+ const low = allowance.limit > 0 && allowance.spent / allowance.limit >= lowBudgetUsedShare;
12
+ return {
13
+ remainingShare,
14
+ low
15
+ };
16
+ }
17
+ /**
18
+ * Each editor's daily cap in US dollars: the cap's share of the budget. Undefined when there is no cap, because it is
19
+ * set to 0 or the budget could not be read.
20
+ */ export function dailyCapUsd(dailySpendCapPercent, allowance) {
21
+ if (!(dailySpendCapPercent > 0) || typeof allowance === 'string' || !(allowance.limit > 0)) {
22
+ return undefined;
23
+ }
24
+ return allowance.limit * dailySpendCapPercent / 100;
25
+ }
26
+ /**
27
+ * A budget rejection from the gateway, reworded without its dollar amounts. Undefined for any other error.
28
+ * The gateway answers 402 with `quota_for_entity_exceeded` and a message naming the spend and limit.
29
+ */ export function budgetExceededMessage(message, allowance, timeZone) {
30
+ if (!/quota_for_entity_exceeded|quota limit exceeded|budget exceeded/i.test(message)) {
31
+ return undefined;
32
+ }
33
+ const reset = typeof allowance === 'string' ? undefined : nextReset(allowance.refreshPeriod);
34
+ return reset ? `Humaan AI has used its AI Gateway budget. It resets on ${formatReset(reset, timeZone)}.` : 'Humaan AI has used its AI Gateway budget. An admin can raise it in Vercel.';
35
+ }
36
+
37
+ //# sourceMappingURL=allowance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/allowance.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nimport type { BudgetView } from '../types.js'\nimport type { ProjectBudget } from './budget.js'\n\nimport { lowBudgetUsedShare } from '../constants.js'\nimport { formatReset, nextReset, projectBudget } from './budget.js'\n\n/** The project's AI Gateway budget for the current period, which the percentages in the settings are shares of. */\nexport type Allowance = ProjectBudget\n\n/**\n * The project's budget, or a reason it could not be read, in words for an admin. In US dollars; never sent to the\n * browser. There is no fallback: the team's credit is shared with every other project, so it would mislead.\n */\nexport async function accountAllowance(req: PayloadRequest): Promise<Allowance | string> {\n return projectBudget(req)\n}\n\n/** What is left as a share of the budget. Low once `lowBudgetUsedShare` of it is used. */\nexport function budgetView(allowance: Allowance): BudgetView {\n const remainingShare =\n allowance.limit > 0 ? Math.max(allowance.limit - allowance.spent, 0) / allowance.limit : 0\n const low = allowance.limit > 0 && allowance.spent / allowance.limit >= lowBudgetUsedShare\n return { remainingShare, low }\n}\n\n/**\n * Each editor's daily cap in US dollars: the cap's share of the budget. Undefined when there is no cap, because it is\n * set to 0 or the budget could not be read.\n */\nexport function dailyCapUsd(\n dailySpendCapPercent: number,\n allowance: Allowance | string,\n): number | undefined {\n if (!(dailySpendCapPercent > 0) || typeof allowance === 'string' || !(allowance.limit > 0)) {\n return undefined\n }\n return (allowance.limit * dailySpendCapPercent) / 100\n}\n\n/**\n * A budget rejection from the gateway, reworded without its dollar amounts. Undefined for any other error.\n * The gateway answers 402 with `quota_for_entity_exceeded` and a message naming the spend and limit.\n */\nexport function budgetExceededMessage(\n message: string,\n allowance: Allowance | string,\n timeZone: string,\n): string | undefined {\n if (!/quota_for_entity_exceeded|quota limit exceeded|budget exceeded/i.test(message)) {\n return undefined\n }\n const reset = typeof allowance === 'string' ? undefined : nextReset(allowance.refreshPeriod)\n return reset\n ? `Humaan AI has used its AI Gateway budget. It resets on ${formatReset(reset, timeZone)}.`\n : 'Humaan AI has used its AI Gateway budget. An admin can raise it in Vercel.'\n}\n"],"names":["lowBudgetUsedShare","formatReset","nextReset","projectBudget","accountAllowance","req","budgetView","allowance","remainingShare","limit","Math","max","spent","low","dailyCapUsd","dailySpendCapPercent","undefined","budgetExceededMessage","message","timeZone","test","reset","refreshPeriod"],"mappings":"AAKA,SAASA,kBAAkB,QAAQ,kBAAiB;AACpD,SAASC,WAAW,EAAEC,SAAS,EAAEC,aAAa,QAAQ,cAAa;AAKnE;;;CAGC,GACD,OAAO,eAAeC,iBAAiBC,GAAmB;IACxD,OAAOF,cAAcE;AACvB;AAEA,wFAAwF,GACxF,OAAO,SAASC,WAAWC,SAAoB;IAC7C,MAAMC,iBACJD,UAAUE,KAAK,GAAG,IAAIC,KAAKC,GAAG,CAACJ,UAAUE,KAAK,GAAGF,UAAUK,KAAK,EAAE,KAAKL,UAAUE,KAAK,GAAG;IAC3F,MAAMI,MAAMN,UAAUE,KAAK,GAAG,KAAKF,UAAUK,KAAK,GAAGL,UAAUE,KAAK,IAAIT;IACxE,OAAO;QAAEQ;QAAgBK;IAAI;AAC/B;AAEA;;;CAGC,GACD,OAAO,SAASC,YACdC,oBAA4B,EAC5BR,SAA6B;IAE7B,IAAI,CAAEQ,CAAAA,uBAAuB,CAAA,KAAM,OAAOR,cAAc,YAAY,CAAEA,CAAAA,UAAUE,KAAK,GAAG,CAAA,GAAI;QAC1F,OAAOO;IACT;IACA,OAAO,AAACT,UAAUE,KAAK,GAAGM,uBAAwB;AACpD;AAEA;;;CAGC,GACD,OAAO,SAASE,sBACdC,OAAe,EACfX,SAA6B,EAC7BY,QAAgB;IAEhB,IAAI,CAAC,kEAAkEC,IAAI,CAACF,UAAU;QACpF,OAAOF;IACT;IACA,MAAMK,QAAQ,OAAOd,cAAc,WAAWS,YAAYd,UAAUK,UAAUe,aAAa;IAC3F,OAAOD,QACH,CAAC,uDAAuD,EAAEpB,YAAYoB,OAAOF,UAAU,CAAC,CAAC,GACzF;AACN"}
@@ -0,0 +1,12 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { AssistantSettings } from '../types.js';
3
+ export declare function applyEndpoint(fallback: AssistantSettings): {
4
+ path: string;
5
+ method: "post";
6
+ handler: (req: PayloadRequest) => Promise<Response>;
7
+ };
8
+ export declare function cancelEndpoint(fallback: AssistantSettings): {
9
+ path: string;
10
+ method: "post";
11
+ handler: (req: PayloadRequest) => Promise<Response>;
12
+ };
@@ -0,0 +1,170 @@
1
+ import { markPlanInThread } from '../chat/threads.js';
2
+ import { aiAssistantPlanSlug, applyEndpointPath, cancelEndpointPath } from '../constants.js';
3
+ import { notAllowed } from '../plan/allowed.js';
4
+ import { applyStoredPlan } from '../plan/applyPlan.js';
5
+ import { userIdOf } from '../settings.js';
6
+ import { allowAssistant } from './access.js';
7
+ import { errorMessage, readPlanRequest } from './form.js';
8
+ function isRecord(value) {
9
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
10
+ }
11
+ export function applyEndpoint(fallback) {
12
+ return {
13
+ path: applyEndpointPath,
14
+ method: 'post',
15
+ handler: async (req)=>{
16
+ const allowed = await allowAssistant(req, fallback);
17
+ if (allowed instanceof Response) {
18
+ return allowed;
19
+ }
20
+ const form = await readPlanRequest(req);
21
+ if (form instanceof Response) {
22
+ return form;
23
+ }
24
+ const loaded = await loadPlan(req, form.planId);
25
+ if (loaded instanceof Response) {
26
+ return loaded;
27
+ }
28
+ if (loaded.status !== 'proposed') {
29
+ return Response.json({
30
+ message: 'This plan can no longer be applied.'
31
+ }, {
32
+ status: 400
33
+ });
34
+ }
35
+ const refused = notAllowed(loaded.plan, req.payload.config);
36
+ if (refused) {
37
+ return Response.json({
38
+ message: refused
39
+ }, {
40
+ status: 403
41
+ });
42
+ }
43
+ try {
44
+ const adminRoute = req.payload.config.routes?.admin ?? '/admin';
45
+ const changes = await applyStoredPlan({
46
+ req,
47
+ plan: loaded.plan,
48
+ planId: form.planId,
49
+ adminRoute
50
+ });
51
+ await recordOutcome(req, loaded.thread, form.planId, {
52
+ status: 'applied',
53
+ changes
54
+ });
55
+ return Response.json({
56
+ changes
57
+ });
58
+ } catch (error) {
59
+ const message = errorMessage(error);
60
+ req.payload.logger.error({
61
+ message,
62
+ planId: form.planId
63
+ }, 'Humaan AI apply failed');
64
+ return Response.json({
65
+ message
66
+ }, {
67
+ status: 400
68
+ });
69
+ }
70
+ }
71
+ };
72
+ }
73
+ export function cancelEndpoint(fallback) {
74
+ return {
75
+ path: cancelEndpointPath,
76
+ method: 'post',
77
+ handler: async (req)=>{
78
+ const allowed = await allowAssistant(req, fallback);
79
+ if (allowed instanceof Response) {
80
+ return allowed;
81
+ }
82
+ const form = await readPlanRequest(req);
83
+ if (form instanceof Response) {
84
+ return form;
85
+ }
86
+ const { planId } = form;
87
+ const loaded = await loadPlan(req, planId);
88
+ if (loaded instanceof Response) {
89
+ return loaded;
90
+ }
91
+ if (loaded.status === 'applied') {
92
+ return Response.json({
93
+ message: 'This plan has already been applied.'
94
+ }, {
95
+ status: 400
96
+ });
97
+ }
98
+ await req.payload.update({
99
+ collection: aiAssistantPlanSlug,
100
+ id: planId,
101
+ data: {
102
+ status: 'cancelled'
103
+ },
104
+ req
105
+ });
106
+ await recordOutcome(req, loaded.thread, planId, {
107
+ status: 'cancelled'
108
+ });
109
+ return Response.json({
110
+ cancelled: true
111
+ });
112
+ }
113
+ };
114
+ }
115
+ /** The plan is already saved, so a failure here only leaves the card in the conversation out of date. */ async function recordOutcome(req, chatId, planId, update) {
116
+ if (!chatId) {
117
+ return;
118
+ }
119
+ try {
120
+ await markPlanInThread(req, chatId, planId, update);
121
+ } catch (error) {
122
+ req.payload.logger.error({
123
+ err: error,
124
+ planId
125
+ }, 'Humaan AI could not update the conversation');
126
+ }
127
+ }
128
+ async function loadPlan(req, id) {
129
+ let doc;
130
+ try {
131
+ doc = await req.payload.findByID({
132
+ collection: aiAssistantPlanSlug,
133
+ id,
134
+ depth: 0,
135
+ overrideAccess: false,
136
+ req,
137
+ user: req.user
138
+ });
139
+ } catch {
140
+ return Response.json({
141
+ message: 'That plan could not be found.'
142
+ }, {
143
+ status: 404
144
+ });
145
+ }
146
+ if (!isRecord(doc) || !isRecord(doc.plan)) {
147
+ return Response.json({
148
+ message: 'That plan could not be read.'
149
+ }, {
150
+ status: 400
151
+ });
152
+ }
153
+ const owner = typeof doc.owner === 'string' ? doc.owner : isRecord(doc.owner) ? doc.owner.id : undefined;
154
+ if (String(owner ?? '') !== userIdOf(req.user)) {
155
+ return Response.json({
156
+ message: 'That plan belongs to someone else.'
157
+ }, {
158
+ status: 403
159
+ });
160
+ }
161
+ const status = typeof doc.status === 'string' ? doc.status : '';
162
+ const thread = typeof doc.thread === 'string' && doc.thread ? doc.thread : undefined;
163
+ return {
164
+ status,
165
+ thread,
166
+ plan: doc.plan
167
+ };
168
+ }
169
+
170
+ //# sourceMappingURL=applyEndpoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/applyEndpoint.ts"],"sourcesContent":["import type { CollectionSlug, PayloadRequest } from 'payload'\n\nimport type { StoredPlan } from '../plan/stored.js'\nimport type { AssistantChange, AssistantSettings, PlanStatus } from '../types.js'\n\nimport { markPlanInThread } from '../chat/threads.js'\nimport { aiAssistantPlanSlug, applyEndpointPath, cancelEndpointPath } from '../constants.js'\nimport { notAllowed } from '../plan/allowed.js'\nimport { applyStoredPlan } from '../plan/applyPlan.js'\nimport { userIdOf } from '../settings.js'\nimport { allowAssistant } from './access.js'\nimport { errorMessage, readPlanRequest } from './form.js'\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n}\n\nexport function applyEndpoint(fallback: AssistantSettings) {\n return {\n path: applyEndpointPath,\n method: 'post' as const,\n handler: async (req: PayloadRequest) => {\n const allowed = await allowAssistant(req, fallback)\n if (allowed instanceof Response) {\n return allowed\n }\n\n const form = await readPlanRequest(req)\n if (form instanceof Response) {\n return form\n }\n\n const loaded = await loadPlan(req, form.planId)\n if (loaded instanceof Response) {\n return loaded\n }\n if (loaded.status !== 'proposed') {\n return Response.json({ message: 'This plan can no longer be applied.' }, { status: 400 })\n }\n const refused = notAllowed(loaded.plan, req.payload.config)\n if (refused) {\n return Response.json({ message: refused }, { status: 403 })\n }\n\n try {\n const adminRoute = req.payload.config.routes?.admin ?? '/admin'\n const changes = await applyStoredPlan({\n req,\n plan: loaded.plan,\n planId: form.planId,\n adminRoute,\n })\n await recordOutcome(req, loaded.thread, form.planId, { status: 'applied', changes })\n return Response.json({ changes })\n } catch (error) {\n const message = errorMessage(error)\n req.payload.logger.error({ message, planId: form.planId }, 'Humaan AI apply failed')\n return Response.json({ message }, { status: 400 })\n }\n },\n }\n}\n\nexport function cancelEndpoint(fallback: AssistantSettings) {\n return {\n path: cancelEndpointPath,\n method: 'post' as const,\n handler: async (req: PayloadRequest) => {\n const allowed = await allowAssistant(req, fallback)\n if (allowed instanceof Response) {\n return allowed\n }\n const form = await readPlanRequest(req)\n if (form instanceof Response) {\n return form\n }\n const { planId } = form\n const loaded = await loadPlan(req, planId)\n if (loaded instanceof Response) {\n return loaded\n }\n if (loaded.status === 'applied') {\n return Response.json({ message: 'This plan has already been applied.' }, { status: 400 })\n }\n await req.payload.update({\n collection: aiAssistantPlanSlug as CollectionSlug,\n id: planId,\n data: { status: 'cancelled' } as never,\n req,\n })\n await recordOutcome(req, loaded.thread, planId, { status: 'cancelled' })\n return Response.json({ cancelled: true })\n },\n }\n}\n\n/** The plan is already saved, so a failure here only leaves the card in the conversation out of date. */\nasync function recordOutcome(\n req: PayloadRequest,\n chatId: string | undefined,\n planId: string,\n update: { changes?: AssistantChange[]; status: PlanStatus },\n): Promise<void> {\n if (!chatId) {\n return\n }\n try {\n await markPlanInThread(req, chatId, planId, update)\n } catch (error) {\n req.payload.logger.error({ err: error, planId }, 'Humaan AI could not update the conversation')\n }\n}\n\nasync function loadPlan(\n req: PayloadRequest,\n id: string,\n): Promise<{ plan: StoredPlan; status: string; thread?: string } | Response> {\n let doc: unknown\n try {\n doc = await req.payload.findByID({\n collection: aiAssistantPlanSlug as CollectionSlug,\n id,\n depth: 0,\n overrideAccess: false,\n req,\n user: req.user,\n })\n } catch {\n return Response.json({ message: 'That plan could not be found.' }, { status: 404 })\n }\n if (!isRecord(doc) || !isRecord(doc.plan)) {\n return Response.json({ message: 'That plan could not be read.' }, { status: 400 })\n }\n const owner =\n typeof doc.owner === 'string'\n ? doc.owner\n : isRecord(doc.owner)\n ? (doc.owner.id as number | string)\n : undefined\n if (String(owner ?? '') !== userIdOf(req.user)) {\n return Response.json({ message: 'That plan belongs to someone else.' }, { status: 403 })\n }\n const status = typeof doc.status === 'string' ? doc.status : ''\n const thread = typeof doc.thread === 'string' && doc.thread ? doc.thread : undefined\n return { status, thread, plan: doc.plan as unknown as StoredPlan }\n}\n"],"names":["markPlanInThread","aiAssistantPlanSlug","applyEndpointPath","cancelEndpointPath","notAllowed","applyStoredPlan","userIdOf","allowAssistant","errorMessage","readPlanRequest","isRecord","value","Boolean","Array","isArray","applyEndpoint","fallback","path","method","handler","req","allowed","Response","form","loaded","loadPlan","planId","status","json","message","refused","plan","payload","config","adminRoute","routes","admin","changes","recordOutcome","thread","error","logger","cancelEndpoint","update","collection","id","data","cancelled","chatId","err","doc","findByID","depth","overrideAccess","user","owner","undefined","String"],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ export type RefreshPeriod = 'daily' | 'monthly' | 'none' | 'weekly';
3
+ /** The Vercel project's AI Gateway budget for the current period, in US dollars. */
4
+ export interface ProjectBudget {
5
+ limit: number;
6
+ refreshPeriod: RefreshPeriod;
7
+ spent: number;
8
+ }
9
+ /** The deployment's project and the team that owns it, from its OIDC token. */
10
+ export interface OidcClaims {
11
+ projectId: string;
12
+ teamId?: string;
13
+ }
14
+ /**
15
+ * The budget of the Vercel project this site runs as, or a reason it could not be read, in words for an admin.
16
+ * The project comes from the deployment's OIDC token, the same token the gateway bills to that project.
17
+ * The access token comes from the environment, so it is never stored in the database, logged, or returned.
18
+ */
19
+ export declare function projectBudget(req: PayloadRequest): Promise<ProjectBudget | string>;
20
+ /**
21
+ * Reads the `project_id` and `owner_id` claims. The token is not verified here: it only chooses which budget to show.
22
+ * The team id is passed to Vercel as well, so a token that can reach several teams still reads the right one.
23
+ */
24
+ export declare function claimsFromOidcToken(token: string): OidcClaims | undefined;
25
+ /** The project's active budget from a budgets list, including one inherited from the team's project default. */
26
+ export declare function findProjectBudget(body: unknown, projectId: string): ProjectBudget | undefined;
27
+ /** When the budget next resets. Vercel resets at the start of each period in UTC. A period of none never resets. */
28
+ export declare function nextReset(period: RefreshPeriod, now?: Date): Date | undefined;
29
+ /** A long date and 24-hour time in the site's time zone, such as `1 November 2026, 0800`. */
30
+ export declare function formatReset(date: Date, timeZone: string): string;
@@ -0,0 +1,153 @@
1
+ import { getVercelOidcToken } from '@vercel/oidc';
2
+ import { amount } from '../chat/usage.js';
3
+ import { budgetTokenEnvVar } from '../constants.js';
4
+ const budgetsUrl = 'https://api.vercel.com/ai-gateway/budgets/list';
5
+ const cacheMs = 60_000;
6
+ /** One cached budget per project and token, so each message and each panel open does not call Vercel. */ let cached;
7
+ /**
8
+ * The budget of the Vercel project this site runs as, or a reason it could not be read, in words for an admin.
9
+ * The project comes from the deployment's OIDC token, the same token the gateway bills to that project.
10
+ * The access token comes from the environment, so it is never stored in the database, logged, or returned.
11
+ */ export async function projectBudget(req) {
12
+ const claims = await currentClaims();
13
+ if (!claims) {
14
+ return 'This site has no Vercel OIDC token. On Vercel it is issued to each deployment; locally, link the project with the Vercel CLI.';
15
+ }
16
+ const accessToken = process.env[budgetTokenEnvVar]?.trim();
17
+ if (!accessToken) {
18
+ return `Set ${budgetTokenEnvVar} to read the project's budget.`;
19
+ }
20
+ const id = `${claims.projectId}:${accessToken}`;
21
+ if (cached && cached.id === id && Date.now() - cached.at < cacheMs) {
22
+ return cached.result;
23
+ }
24
+ let result;
25
+ try {
26
+ const query = new URLSearchParams({
27
+ scopeType: 'project',
28
+ ...claims.teamId ? {
29
+ teamId: claims.teamId
30
+ } : {}
31
+ });
32
+ const response = await fetch(`${budgetsUrl}?${query}`, {
33
+ headers: {
34
+ Authorization: `Bearer ${accessToken}`
35
+ }
36
+ });
37
+ if (!response.ok) {
38
+ req.payload.logger.error({
39
+ status: response.status
40
+ }, 'Humaan AI could not read the project budget');
41
+ // Vercel answers 404 rather than 403 for a token scoped to another team.
42
+ result = response.status === 401 || response.status === 403 || response.status === 404 ? `Vercel refused ${budgetTokenEnvVar}. Check it is scoped to the team that owns this project and has not expired.` : "Vercel's budget API did not respond as expected.";
43
+ } else {
44
+ result = findProjectBudget(await response.json(), claims.projectId) ?? 'This Vercel project has no AI Gateway budget.';
45
+ }
46
+ } catch (error) {
47
+ req.payload.logger.error({
48
+ err: error
49
+ }, "Humaan AI could not reach Vercel's budget API");
50
+ result = "Vercel's budget API could not be reached.";
51
+ }
52
+ cached = {
53
+ id,
54
+ at: Date.now(),
55
+ result
56
+ };
57
+ return result;
58
+ }
59
+ /** The OIDC token's project and team, or undefined when there is no token. */ async function currentClaims() {
60
+ try {
61
+ return claimsFromOidcToken(await getVercelOidcToken());
62
+ } catch {
63
+ return undefined;
64
+ }
65
+ }
66
+ /**
67
+ * Reads the `project_id` and `owner_id` claims. The token is not verified here: it only chooses which budget to show.
68
+ * The team id is passed to Vercel as well, so a token that can reach several teams still reads the right one.
69
+ */ export function claimsFromOidcToken(token) {
70
+ const payload = token.split('.')[1];
71
+ if (!payload) {
72
+ return undefined;
73
+ }
74
+ try {
75
+ const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
76
+ if (!isRecord(claims) || typeof claims.project_id !== 'string') {
77
+ return undefined;
78
+ }
79
+ return {
80
+ projectId: claims.project_id,
81
+ ...typeof claims.owner_id === 'string' ? {
82
+ teamId: claims.owner_id
83
+ } : {}
84
+ };
85
+ } catch {
86
+ return undefined;
87
+ }
88
+ }
89
+ /** The project's active budget from a budgets list, including one inherited from the team's project default. */ export function findProjectBudget(body, projectId) {
90
+ const budgets = isRecord(body) && Array.isArray(body.budgets) ? body.budgets : [];
91
+ for (const row of budgets){
92
+ if (!isRecord(row) || row.scopeType !== 'project' || isTrue(row.archived)) {
93
+ continue;
94
+ }
95
+ if (row.scopeId !== projectId && row.quotaEntityId !== projectId) {
96
+ continue;
97
+ }
98
+ const limit = amount(row.limitAmount);
99
+ const spent = amount(row.currentSpend);
100
+ if (limit === undefined || spent === undefined) {
101
+ return undefined;
102
+ }
103
+ return {
104
+ limit,
105
+ spent,
106
+ refreshPeriod: refreshPeriodOf(row.refreshPeriod)
107
+ };
108
+ }
109
+ return undefined;
110
+ }
111
+ /** When the budget next resets. Vercel resets at the start of each period in UTC. A period of none never resets. */ export function nextReset(period, now = new Date()) {
112
+ const year = now.getUTCFullYear();
113
+ const month = now.getUTCMonth();
114
+ const day = now.getUTCDate();
115
+ if (period === 'daily') {
116
+ return new Date(Date.UTC(year, month, day + 1));
117
+ }
118
+ if (period === 'weekly') {
119
+ // getUTCDay: Sunday 0, Monday 1. Always the coming Monday, a full week ahead when today is Monday.
120
+ const daysToMonday = (8 - now.getUTCDay()) % 7 || 7;
121
+ return new Date(Date.UTC(year, month, day + daysToMonday));
122
+ }
123
+ if (period === 'monthly') {
124
+ return new Date(Date.UTC(year, month + 1, 1));
125
+ }
126
+ return undefined;
127
+ }
128
+ /** A long date and 24-hour time in the site's time zone, such as `1 November 2026, 0800`. */ export function formatReset(date, timeZone) {
129
+ const parts = Object.fromEntries(new Intl.DateTimeFormat('en-AU', {
130
+ timeZone,
131
+ day: 'numeric',
132
+ month: 'long',
133
+ year: 'numeric',
134
+ hour: '2-digit',
135
+ minute: '2-digit',
136
+ hourCycle: 'h23'
137
+ }).formatToParts(date).map((part)=>[
138
+ part.type,
139
+ part.value
140
+ ]));
141
+ return `${parts.day} ${parts.month} ${parts.year}, ${parts.hour}${parts.minute}`;
142
+ }
143
+ function refreshPeriodOf(value) {
144
+ return value === 'daily' || value === 'weekly' || value === 'monthly' ? value : 'none';
145
+ }
146
+ /** Vercel documents booleans but its examples send strings. */ function isTrue(value) {
147
+ return value === true || value === 'true';
148
+ }
149
+ function isRecord(value) {
150
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
151
+ }
152
+
153
+ //# sourceMappingURL=budget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/budget.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nimport { getVercelOidcToken } from '@vercel/oidc'\n\nimport { amount } from '../chat/usage.js'\nimport { budgetTokenEnvVar } from '../constants.js'\n\nexport type RefreshPeriod = 'daily' | 'monthly' | 'none' | 'weekly'\n\n/** The Vercel project's AI Gateway budget for the current period, in US dollars. */\nexport interface ProjectBudget {\n limit: number\n refreshPeriod: RefreshPeriod\n spent: number\n}\n\nconst budgetsUrl = 'https://api.vercel.com/ai-gateway/budgets/list'\nconst cacheMs = 60_000\n\n/** One cached budget per project and token, so each message and each panel open does not call Vercel. */\nlet cached: { at: number; id: string; result: ProjectBudget | string } | undefined\n\n/** The deployment's project and the team that owns it, from its OIDC token. */\nexport interface OidcClaims {\n projectId: string\n teamId?: string\n}\n\n/**\n * The budget of the Vercel project this site runs as, or a reason it could not be read, in words for an admin.\n * The project comes from the deployment's OIDC token, the same token the gateway bills to that project.\n * The access token comes from the environment, so it is never stored in the database, logged, or returned.\n */\nexport async function projectBudget(req: PayloadRequest): Promise<ProjectBudget | string> {\n const claims = await currentClaims()\n if (!claims) {\n return 'This site has no Vercel OIDC token. On Vercel it is issued to each deployment; locally, link the project with the Vercel CLI.'\n }\n const accessToken = process.env[budgetTokenEnvVar]?.trim()\n if (!accessToken) {\n return `Set ${budgetTokenEnvVar} to read the project's budget.`\n }\n\n const id = `${claims.projectId}:${accessToken}`\n if (cached && cached.id === id && Date.now() - cached.at < cacheMs) {\n return cached.result\n }\n\n let result: ProjectBudget | string\n try {\n const query = new URLSearchParams({\n scopeType: 'project',\n ...(claims.teamId ? { teamId: claims.teamId } : {}),\n })\n const response = await fetch(`${budgetsUrl}?${query}`, {\n headers: { Authorization: `Bearer ${accessToken}` },\n })\n if (!response.ok) {\n req.payload.logger.error(\n { status: response.status },\n 'Humaan AI could not read the project budget',\n )\n // Vercel answers 404 rather than 403 for a token scoped to another team.\n result =\n response.status === 401 || response.status === 403 || response.status === 404\n ? `Vercel refused ${budgetTokenEnvVar}. Check it is scoped to the team that owns this project and has not expired.`\n : \"Vercel's budget API did not respond as expected.\"\n } else {\n result =\n findProjectBudget(await response.json(), claims.projectId) ??\n 'This Vercel project has no AI Gateway budget.'\n }\n } catch (error) {\n req.payload.logger.error({ err: error }, \"Humaan AI could not reach Vercel's budget API\")\n result = \"Vercel's budget API could not be reached.\"\n }\n cached = { id, at: Date.now(), result }\n return result\n}\n\n/** The OIDC token's project and team, or undefined when there is no token. */\nasync function currentClaims(): Promise<OidcClaims | undefined> {\n try {\n return claimsFromOidcToken(await getVercelOidcToken())\n } catch {\n return undefined\n }\n}\n\n/**\n * Reads the `project_id` and `owner_id` claims. The token is not verified here: it only chooses which budget to show.\n * The team id is passed to Vercel as well, so a token that can reach several teams still reads the right one.\n */\nexport function claimsFromOidcToken(token: string): OidcClaims | undefined {\n const payload = token.split('.')[1]\n if (!payload) {\n return undefined\n }\n try {\n const claims: unknown = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'))\n if (!isRecord(claims) || typeof claims.project_id !== 'string') {\n return undefined\n }\n return {\n projectId: claims.project_id,\n ...(typeof claims.owner_id === 'string' ? { teamId: claims.owner_id } : {}),\n }\n } catch {\n return undefined\n }\n}\n\n/** The project's active budget from a budgets list, including one inherited from the team's project default. */\nexport function findProjectBudget(body: unknown, projectId: string): ProjectBudget | undefined {\n const budgets = isRecord(body) && Array.isArray(body.budgets) ? body.budgets : []\n for (const row of budgets) {\n if (!isRecord(row) || row.scopeType !== 'project' || isTrue(row.archived)) {\n continue\n }\n if (row.scopeId !== projectId && row.quotaEntityId !== projectId) {\n continue\n }\n const limit = amount(row.limitAmount)\n const spent = amount(row.currentSpend)\n if (limit === undefined || spent === undefined) {\n return undefined\n }\n return { limit, spent, refreshPeriod: refreshPeriodOf(row.refreshPeriod) }\n }\n return undefined\n}\n\n/** When the budget next resets. Vercel resets at the start of each period in UTC. A period of none never resets. */\nexport function nextReset(period: RefreshPeriod, now = new Date()): Date | undefined {\n const year = now.getUTCFullYear()\n const month = now.getUTCMonth()\n const day = now.getUTCDate()\n if (period === 'daily') {\n return new Date(Date.UTC(year, month, day + 1))\n }\n if (period === 'weekly') {\n // getUTCDay: Sunday 0, Monday 1. Always the coming Monday, a full week ahead when today is Monday.\n const daysToMonday = (8 - now.getUTCDay()) % 7 || 7\n return new Date(Date.UTC(year, month, day + daysToMonday))\n }\n if (period === 'monthly') {\n return new Date(Date.UTC(year, month + 1, 1))\n }\n return undefined\n}\n\n/** A long date and 24-hour time in the site's time zone, such as `1 November 2026, 0800`. */\nexport function formatReset(date: Date, timeZone: string): string {\n const parts = Object.fromEntries(\n new Intl.DateTimeFormat('en-AU', {\n timeZone,\n day: 'numeric',\n month: 'long',\n year: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n hourCycle: 'h23',\n })\n .formatToParts(date)\n .map((part) => [part.type, part.value]),\n )\n return `${parts.day} ${parts.month} ${parts.year}, ${parts.hour}${parts.minute}`\n}\n\nfunction refreshPeriodOf(value: unknown): RefreshPeriod {\n return value === 'daily' || value === 'weekly' || value === 'monthly' ? value : 'none'\n}\n\n/** Vercel documents booleans but its examples send strings. */\nfunction isTrue(value: unknown): boolean {\n return value === true || value === 'true'\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n}\n"],"names":["getVercelOidcToken","amount","budgetTokenEnvVar","budgetsUrl","cacheMs","cached","projectBudget","req","claims","currentClaims","accessToken","process","env","trim","id","projectId","Date","now","at","result","query","URLSearchParams","scopeType","teamId","response","fetch","headers","Authorization","ok","payload","logger","error","status","findProjectBudget","json","err","claimsFromOidcToken","undefined","token","split","JSON","parse","Buffer","from","toString","isRecord","project_id","owner_id","body","budgets","Array","isArray","row","isTrue","archived","scopeId","quotaEntityId","limit","limitAmount","spent","currentSpend","refreshPeriod","refreshPeriodOf","nextReset","period","year","getUTCFullYear","month","getUTCMonth","day","getUTCDate","UTC","daysToMonday","getUTCDay","formatReset","date","timeZone","parts","Object","fromEntries","Intl","DateTimeFormat","hour","minute","hourCycle","formatToParts","map","part","type","value","Boolean"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { OwnKey } from '../billing.js';
3
+ import type { AiAssistantPluginOptions, AssistantSettings } from '../types.js';
4
+ /**
5
+ * One turn of a conversation. The drawer sends the new message; the history comes from the stored thread,
6
+ * so a client cannot rewrite what the assistant said earlier. The reply streams back as UI message parts.
7
+ */
8
+ export declare function chatEndpoint(options: AiAssistantPluginOptions, fallback: AssistantSettings, siteKey: OwnKey | undefined): {
9
+ path: string;
10
+ method: "post";
11
+ handler: (req: PayloadRequest) => Promise<Response>;
12
+ };