@aglyn/plugins-ai 1.0.0-beta.225 → 1.0.0-beta.227

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 (161) hide show
  1. package/README.md +2 -1
  2. package/package.json +17 -17
  3. package/src/lib/billing/ai-month-meter.js +5 -3
  4. package/src/lib/billing/ai-month-meter.js.map +1 -1
  5. package/src/lib/billing/ai-overage-ledger.js +3 -1
  6. package/src/lib/billing/ai-overage-ledger.js.map +1 -1
  7. package/src/lib/components/ai-brief-dialog.component.d.ts +6 -2
  8. package/src/lib/components/ai-brief-dialog.component.js +35 -17
  9. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  10. package/src/lib/components/ai-describe-automation.component.js +17 -2
  11. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  12. package/src/lib/components/ai-insight-dialog.component.js +24 -2
  13. package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
  14. package/src/lib/components/ai-job-events.d.ts +14 -0
  15. package/src/lib/components/ai-job-events.js +76 -0
  16. package/src/lib/components/ai-job-events.js.map +1 -0
  17. package/src/lib/components/ai-job-follow.component.d.ts +49 -0
  18. package/src/lib/components/ai-job-follow.component.js +210 -0
  19. package/src/lib/components/ai-job-follow.component.js.map +1 -0
  20. package/src/lib/components/ai-job-links.d.ts +42 -0
  21. package/src/lib/components/ai-job-links.js +140 -0
  22. package/src/lib/components/ai-job-links.js.map +1 -0
  23. package/src/lib/components/ai-job-plan.component.d.ts +13 -6
  24. package/src/lib/components/ai-job-plan.component.js +29 -3
  25. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  26. package/src/lib/components/ai-job-requests.d.ts +24 -0
  27. package/src/lib/components/ai-job-requests.js +76 -0
  28. package/src/lib/components/ai-job-requests.js.map +1 -0
  29. package/src/lib/components/ai-jobs-indicator.component.d.ts +25 -0
  30. package/src/lib/components/ai-jobs-indicator.component.js +80 -0
  31. package/src/lib/components/ai-jobs-indicator.component.js.map +1 -0
  32. package/src/lib/components/ai-jobs-store.d.ts +81 -0
  33. package/src/lib/components/ai-jobs-store.js +218 -0
  34. package/src/lib/components/ai-jobs-store.js.map +1 -0
  35. package/src/lib/components/ai-page-brief-dialog.component.d.ts +3 -3
  36. package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -1
  37. package/src/lib/components/ai-site-build-page.component.d.ts +23 -0
  38. package/src/lib/components/ai-site-build-page.component.js +377 -0
  39. package/src/lib/components/ai-site-build-page.component.js.map +1 -0
  40. package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
  41. package/src/lib/components/ai-site-start-card.component.js +522 -130
  42. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  43. package/src/lib/components/ai-site-starter-fallback.component.d.ts +24 -0
  44. package/src/lib/components/ai-site-starter-fallback.component.js +112 -0
  45. package/src/lib/components/ai-site-starter-fallback.component.js.map +1 -0
  46. package/src/lib/components/assist-jobs-drawer.component.d.ts +18 -20
  47. package/src/lib/components/assist-jobs-drawer.component.js +152 -172
  48. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  49. package/src/lib/components/assist-panel.component.js +62 -6
  50. package/src/lib/components/assist-panel.component.js.map +1 -1
  51. package/src/lib/components/staff-ai-credit-return.component.d.ts +55 -0
  52. package/src/lib/components/staff-ai-credit-return.component.js +346 -0
  53. package/src/lib/components/staff-ai-credit-return.component.js.map +1 -0
  54. package/src/lib/components/staff-org-ai-card.component.js +364 -279
  55. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  56. package/src/lib/components/staff-user-ai-usage-card.component.d.ts +4 -0
  57. package/src/lib/components/staff-user-ai-usage-card.component.js +83 -28
  58. package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
  59. package/src/lib/components/use-ai-job-run.js +5 -18
  60. package/src/lib/components/use-ai-job-run.js.map +1 -1
  61. package/src/lib/jobs/ai-job-auto-confirm.d.ts +52 -0
  62. package/src/lib/jobs/ai-job-auto-confirm.js +62 -0
  63. package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -0
  64. package/src/lib/jobs/ai-job-plan-step.d.ts +33 -2
  65. package/src/lib/jobs/ai-job-plan-step.js +161 -17
  66. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  67. package/src/lib/jobs/ai-job-site-step.d.ts +7 -0
  68. package/src/lib/jobs/ai-job-site-step.js +34 -4
  69. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  70. package/src/lib/jobs/ai-job-text-step.d.ts +7 -0
  71. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  72. package/src/lib/jobs/ai-jobs-notify.d.ts +37 -0
  73. package/src/lib/jobs/ai-jobs-notify.js +51 -0
  74. package/src/lib/jobs/ai-jobs-notify.js.map +1 -0
  75. package/src/lib/jobs/ai-jobs.d.ts +31 -4
  76. package/src/lib/jobs/ai-jobs.js +196 -32
  77. package/src/lib/jobs/ai-jobs.js.map +1 -1
  78. package/src/lib/model/ai-job-activity.d.ts +112 -0
  79. package/src/lib/model/ai-job-activity.js +144 -0
  80. package/src/lib/model/ai-job-activity.js.map +1 -0
  81. package/src/lib/model/ai-job-failure-copy.d.ts +69 -0
  82. package/src/lib/model/ai-job-failure-copy.js +78 -0
  83. package/src/lib/model/ai-job-failure-copy.js.map +1 -0
  84. package/src/lib/model/ai-job-notice.d.ts +61 -0
  85. package/src/lib/model/ai-job-notice.js +112 -0
  86. package/src/lib/model/ai-job-notice.js.map +1 -0
  87. package/src/lib/model/ai-jobs.types.d.ts +26 -0
  88. package/src/lib/model/ai-jobs.types.js +1 -0
  89. package/src/lib/model/ai-jobs.types.js.map +1 -1
  90. package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
  91. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  92. package/src/lib/model/ai-site-build-progress.d.ts +45 -0
  93. package/src/lib/model/ai-site-build-progress.js +76 -0
  94. package/src/lib/model/ai-site-build-progress.js.map +1 -0
  95. package/src/lib/model/ai-site-inventory.d.ts +9 -0
  96. package/src/lib/model/ai-site-inventory.js +7 -2
  97. package/src/lib/model/ai-site-inventory.js.map +1 -1
  98. package/src/lib/model/ai-site-job.d.ts +93 -2
  99. package/src/lib/model/ai-site-job.js +88 -7
  100. package/src/lib/model/ai-site-job.js.map +1 -1
  101. package/src/lib/model/ai-site-start.d.ts +3 -1
  102. package/src/lib/model/ai-site-start.js +6 -4
  103. package/src/lib/model/ai-site-start.js.map +1 -1
  104. package/src/lib/model/ai-template-subjects.d.ts +4 -3
  105. package/src/lib/model/ai-template-subjects.js +5 -4
  106. package/src/lib/model/ai-template-subjects.js.map +1 -1
  107. package/src/lib/plan-entitlements.d.ts +3 -3
  108. package/src/lib/plan-entitlements.js +3 -3
  109. package/src/lib/plan-entitlements.js.map +1 -1
  110. package/src/lib/plugin.js +28 -0
  111. package/src/lib/plugin.js.map +1 -1
  112. package/src/lib/runtime/ai-abuse-guards.d.ts +9 -6
  113. package/src/lib/runtime/ai-abuse-guards.js +10 -7
  114. package/src/lib/runtime/ai-abuse-guards.js.map +1 -1
  115. package/src/lib/runtime/ai-doctrine-validators.js +31 -4
  116. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  117. package/src/lib/runtime/ai-gate.d.ts +3 -2
  118. package/src/lib/runtime/ai-gate.js +3 -2
  119. package/src/lib/runtime/ai-gate.js.map +1 -1
  120. package/src/lib/runtime/ai-palette.generated.js +12 -4
  121. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  122. package/src/lib/runtime/site-inventory.d.ts +16 -1
  123. package/src/lib/runtime/site-inventory.js +43 -3
  124. package/src/lib/runtime/site-inventory.js.map +1 -1
  125. package/src/lib/server/ai-admin-credits.d.ts +21 -0
  126. package/src/lib/server/ai-admin-credits.js +213 -0
  127. package/src/lib/server/ai-admin-credits.js.map +1 -0
  128. package/src/lib/server/ai-admin-org.js +12 -5
  129. package/src/lib/server/ai-admin-org.js.map +1 -1
  130. package/src/lib/server/ai-admin-signals.js +3 -1
  131. package/src/lib/server/ai-admin-signals.js.map +1 -1
  132. package/src/lib/server/ai-admin-user.js +12 -1
  133. package/src/lib/server/ai-admin-user.js.map +1 -1
  134. package/src/lib/server/ai-allotments.js +3 -2
  135. package/src/lib/server/ai-allotments.js.map +1 -1
  136. package/src/lib/server/ai-jobs-route.js +20 -2
  137. package/src/lib/server/ai-jobs-route.js.map +1 -1
  138. package/src/lib/server/assist-chat.js +3 -2
  139. package/src/lib/server/assist-chat.js.map +1 -1
  140. package/src/lib/server/billing-credits.js +4 -3
  141. package/src/lib/server/billing-credits.js.map +1 -1
  142. package/src/lib/server/billing-overage.js +3 -2
  143. package/src/lib/server/billing-overage.js.map +1 -1
  144. package/src/lib/server.js +11 -0
  145. package/src/lib/server.js.map +1 -1
  146. package/src/lib/usage/assist-credit-returns-write.d.ts +75 -0
  147. package/src/lib/usage/assist-credit-returns-write.js +150 -0
  148. package/src/lib/usage/assist-credit-returns-write.js.map +1 -0
  149. package/src/lib/usage/assist-credit-returns.d.ts +68 -0
  150. package/src/lib/usage/assist-credit-returns.js +79 -0
  151. package/src/lib/usage/assist-credit-returns.js.map +1 -0
  152. package/src/lib/usage/assist-free-taste.js +6 -3
  153. package/src/lib/usage/assist-free-taste.js.map +1 -1
  154. package/src/lib/usage/assist-job-refund.d.ts +100 -0
  155. package/src/lib/usage/assist-job-refund.js +109 -0
  156. package/src/lib/usage/assist-job-refund.js.map +1 -0
  157. package/src/lib/usage/assist-usage.js +5 -2
  158. package/src/lib/usage/assist-usage.js.map +1 -1
  159. package/src/lib/usage/staff-org-ai.d.ts +31 -1
  160. package/src/lib/usage/staff-org-ai.js +19 -2
  161. package/src/lib/usage/staff-org-ai.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/billing-credits.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { pluginRequestFromWeb } from '@aglyn/aglyn/server'\nimport {\n publicAssistCredits,\n assistBandRefuses,\n resolveAssistBudgetUsd,\n} from '../usage/assist-credits'\nimport { isUncappedPlanComp } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\n\n// lockdown-423: exempt — a READ-ONLY billing surface that writes nothing,\n// same posture as billing/usage-budget beside it. AGL-1501 keeps\n// billing-locked sessions alive so members can reach Billing, and a\n// locked org needs its usage readouts more than an unlocked one does.\n\n/**\n * This month's Aglyn Assist credit standing for one org — the odometer beside\n * the band on Billing → Usage.\n *\n * ## Why a route at all\n *\n * Every other meter on that page reads its counter straight from Firestore in\n * the browser. This one cannot: `orgs/{id}/assistUsage` is deliberately absent\n * from the rules file and therefore default-deny for every client, because the\n * assist subcollections sit beside the exchange prose and are reached only\n * through the Admin SDK. A client read would fail silently and the meter would\n * sit at \"not yet metered\" forever — a surface that looks like a readout and\n * is not one, which is worse than no meter at all.\n *\n * ## Why it answers in CREDITS\n *\n * The stored figure is `estCostUsd`: our provider bill for the month at the\n * serving model's list rates. Publishing it would put our model choice and our\n * margin on a billing page, and it would move under the customer every time\n * the model is swapped. `publicAssistCredits` is the one conversion, and no\n * dollar figure crosses this boundary.\n *\n * `credits: null` is the honest answer for an org that has no assist band at\n * all. Since AGL-3203 no plan ROW bands at zero, so what reaches this is a\n * per-org override that writes one. It is not \"0 of 0\"; there is no band to\n * be a fraction of, and the page renders no meter rather than an empty one.\n *\n * An UNCAPPED staff comp (AGL-3049) has no band either, but it has a meter:\n * the workspace draws credits with no limit to draw them against. It answers\n * `credits: { used, limit: null, remaining: null }` and `unlimited: true`.\n * `null` is the band's absence on the wire — never `Infinity`, which\n * `JSON.stringify` would turn into a `null` nobody chose — and the flag says\n * which absence it is, so a reader that turns `null` into a number cannot\n * mistake an unlimited workspace for one with a band of zero.\n *\n * `stopsAtBand` says what happens at the band — the reservation's own\n * verdict (`assistBandRefuses`): `true` where it refuses, `false` where it\n * sells past at the plan's rate — so a surface that warns about the band\n * says the same thing the gate does without resolving the plan itself.\n *\n * Membership alone, not `billing.manage`: this is a capacity readout of the\n * kind every other meter on the page shows to anyone who can see the page, and\n * it carries no money. The spend view that DOES carry money is\n * `/api/billing/usage-budget`, which gates on `billing.manage`.\n */\nasync function handler(request: Request): Promise<Response> {\n const { method, headers: rawHeaders, query } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'GET') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n const orgId = String((query as Record<string, unknown>)?.orgId ?? '')\n if (!orgId) return Response.json({ error: 'Bad request' }, { status: 400 })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const isStaff = decoded['staff'] === true\n const actor = await resolveOrgMembership(decoded.uid, orgId)\n if (!isStaff && !actor?.member) {\n return Response.json({ error: 'Not a member' }, { status: 403 })\n }\n const orgRef = firebaseAdmin.app().firestore().collection('orgs').doc(orgId)\n const orgSnapshot = await orgRef.get()\n if (!orgSnapshot.exists) {\n return Response.json({ error: 'Unknown organization' }, { status: 404 })\n }\n const org = orgSnapshot.data() as never\n const budgetUsd = resolveAssistBudgetUsd(org)\n const unlimited = budgetUsd === null && isUncappedPlanComp(org)\n if (budgetUsd === null && !unlimited) return Response.json({ credits: null })\n const month = new Date().toISOString().slice(0, 7)\n const usage = await orgRef.collection('assistUsage').doc(month).get()\n const costUsd = Number(usage.get('estCostUsd') ?? 0)\n return Response.json({\n credits: publicAssistCredits(\n Number.isFinite(costUsd) && costUsd > 0 ? costUsd : 0,\n budgetUsd,\n ),\n stopsAtBand: assistBandRefuses(org),\n ...(unlimited ? { unlimited: true } : {}),\n })\n } catch (error) {\n // A refused credential is a 401, not a fault of ours (AGL-1993). Null\n // for anything else, so a real failure keeps the answer below.\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error('[assist-credits] read failed', orgId, error)\n // The meter's \"not yet metered\" state, not a zero — a failed read must\n // not render as \"you have used none of your credits\".\n return Response.json({ error: 'Assist credits unavailable' }, { status: 500 })\n }\n}\n\nexport const GET = handler\n"],"names":["pluginRequestFromWeb","publicAssistCredits","assistBandRefuses","resolveAssistBudgetUsd","isUncappedPlanComp","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","resolveOrgMembership","invalidIdTokenResponse","handler","request","headers","method","rawHeaders","query","Response","json","error","status","authorization","idToken","startsWith","slice","length","undefined","orgId","String","usage","decoded","app","auth","verifyIdToken","email_verified","isStaff","actor","uid","member","orgRef","firestore","collection","doc","orgSnapshot","get","exists","org","data","budgetUsd","unlimited","credits","month","Date","toISOString","costUsd","Number","isFinite","stopsAtBand","unauthenticated","console","GET"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/billing-credits.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { pluginRequestFromWeb } from '@aglyn/aglyn/server'\nimport {\n publicAssistCredits,\n assistBandRefuses,\n resolveAssistBudgetUsd,\n} from '../usage/assist-credits'\nimport { isUncappedPlanComp } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport {\n ASSIST_RETURNED_USD_FIELD,\n assistSpendAfterReturnsUsd,\n} from '../usage/assist-credit-returns'\n\n// lockdown-423: exempt — a READ-ONLY billing surface that writes nothing,\n// same posture as billing/usage-budget beside it. AGL-1501 keeps\n// billing-locked sessions alive so members can reach Billing, and a\n// locked org needs its usage readouts more than an unlocked one does.\n\n/**\n * This month's Aglyn Assist credit standing for one org — the odometer beside\n * the band on Billing → Usage.\n *\n * ## Why a route at all\n *\n * Every other meter on that page reads its counter straight from Firestore in\n * the browser. This one cannot: `orgs/{id}/assistUsage` is deliberately absent\n * from the rules file and therefore default-deny for every client, because the\n * assist subcollections sit beside the exchange prose and are reached only\n * through the Admin SDK. A client read would fail silently and the meter would\n * sit at \"not yet metered\" forever — a surface that looks like a readout and\n * is not one, which is worse than no meter at all.\n *\n * ## Why it answers in CREDITS\n *\n * The stored figure is `estCostUsd`: our provider bill for the month at the\n * serving model's list rates. Publishing it would put our model choice and our\n * margin on a billing page, and it would move under the customer every time\n * the model is swapped. `publicAssistCredits` is the one conversion, and no\n * dollar figure crosses this boundary.\n *\n * `credits: null` is the honest answer for an org that has no assist band at\n * all. Since AGL-3203 no plan ROW bands at zero, so what reaches this is a\n * per-org override that writes one. It is not \"0 of 0\"; there is no band to\n * be a fraction of, and the page renders no meter rather than an empty one.\n *\n * An UNCAPPED staff comp (AGL-3049) has no band either, but it has a meter:\n * the workspace draws credits with no limit to draw them against. It answers\n * `credits: { used, limit: null, remaining: null }` and `unlimited: true`.\n * `null` is the band's absence on the wire — never `Infinity`, which\n * `JSON.stringify` would turn into a `null` nobody chose — and the flag says\n * which absence it is, so a reader that turns `null` into a number cannot\n * mistake an unlimited workspace for one with a band of zero.\n *\n * `stopsAtBand` says what happens at the band — the reservation's own\n * verdict (`assistBandRefuses`): `true` where it refuses, `false` where it\n * sells past at the plan's rate — so a surface that warns about the band\n * says the same thing the gate does without resolving the plan itself.\n *\n * Membership alone, not `billing.manage`: this is a capacity readout of the\n * kind every other meter on the page shows to anyone who can see the page, and\n * it carries no money. The spend view that DOES carry money is\n * `/api/billing/usage-budget`, which gates on `billing.manage`.\n */\nasync function handler(request: Request): Promise<Response> {\n const { method, headers: rawHeaders, query } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'GET') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n const orgId = String((query as Record<string, unknown>)?.orgId ?? '')\n if (!orgId) return Response.json({ error: 'Bad request' }, { status: 400 })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const isStaff = decoded['staff'] === true\n const actor = await resolveOrgMembership(decoded.uid, orgId)\n if (!isStaff && !actor?.member) {\n return Response.json({ error: 'Not a member' }, { status: 403 })\n }\n const orgRef = firebaseAdmin.app().firestore().collection('orgs').doc(orgId)\n const orgSnapshot = await orgRef.get()\n if (!orgSnapshot.exists) {\n return Response.json({ error: 'Unknown organization' }, { status: 404 })\n }\n const org = orgSnapshot.data() as never\n const budgetUsd = resolveAssistBudgetUsd(org)\n const unlimited = budgetUsd === null && isUncappedPlanComp(org)\n if (budgetUsd === null && !unlimited) return Response.json({ credits: null })\n const month = new Date().toISOString().slice(0, 7)\n const usage = await orgRef.collection('assistUsage').doc(month).get()\n // Net of credits staff gave back (AGL-3595) — the figure the gate admits on.\n const costUsd = assistSpendAfterReturnsUsd(\n usage.get('estCostUsd'),\n usage.get(ASSIST_RETURNED_USD_FIELD),\n )\n return Response.json({\n credits: publicAssistCredits(costUsd, budgetUsd),\n stopsAtBand: assistBandRefuses(org),\n ...(unlimited ? { unlimited: true } : {}),\n })\n } catch (error) {\n // A refused credential is a 401, not a fault of ours (AGL-1993). Null\n // for anything else, so a real failure keeps the answer below.\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error('[assist-credits] read failed', orgId, error)\n // The meter's \"not yet metered\" state, not a zero — a failed read must\n // not render as \"you have used none of your credits\".\n return Response.json({ error: 'Assist credits unavailable' }, { status: 500 })\n }\n}\n\nexport const GET = handler\n"],"names":["pluginRequestFromWeb","publicAssistCredits","assistBandRefuses","resolveAssistBudgetUsd","isUncappedPlanComp","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","resolveOrgMembership","invalidIdTokenResponse","ASSIST_RETURNED_USD_FIELD","assistSpendAfterReturnsUsd","handler","request","headers","method","rawHeaders","query","Response","json","error","status","authorization","idToken","startsWith","slice","length","undefined","orgId","String","decoded","app","auth","verifyIdToken","email_verified","isStaff","actor","uid","member","orgRef","firestore","collection","doc","orgSnapshot","get","exists","org","data","budgetUsd","unlimited","credits","month","Date","toISOString","usage","costUsd","stopsAtBand","unauthenticated","console","GET"],"mappings":"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"}
@@ -19,6 +19,7 @@ import { ASSIST_HARD_CAP_CONTROL_LABEL, ASSIST_OVERAGE_CAP_CONTROL_LABEL, ASSIST
19
19
  import { emailUnverifiedResponse, firebaseAdmin, isImpersonationSession, memberHasOrgPermission, resolveOrgMembership } from "@aglyn/tenant-data-admin";
20
20
  import { logAiOverageControl } from "../activity/ai-activity.js";
21
21
  import { assistMonthOverage } from "../usage/assist-credits.js";
22
+ import { ASSIST_RETURNED_USD_FIELD, assistSpendAfterReturnsUsd } from "../usage/assist-credit-returns.js";
22
23
  import { assistUsageMonth } from "../usage/assist-usage.js";
23
24
  import { aiOverageBillsByInvoice } from "../billing/ai-overage-cutover.js";
24
25
  import { aiOverageNextChargeUsd } from "../billing/ai-overage-gate.js";
@@ -69,7 +70,7 @@ import { addAdminAudit } from "@aglyn/tenant-data-admin/server/admin-audit-write
69
70
  * invoice, and rendering a ceiling and a "next charge" figure that nothing
70
71
  * enforces would be a promise the platform is not yet keeping.
71
72
  */ async function readAiOverageStandingForCard(orgRef, org) {
72
- var _standingSnapshot_data, _usageSnapshot_data, _usageSnapshot_get, _ref, _aiOverageCardOnFile;
73
+ var _standingSnapshot_data, _usageSnapshot_data, _ref, _aiOverageCardOnFile;
73
74
  var _standing_pause;
74
75
  const now = new Date();
75
76
  const month = assistUsageMonth(now);
@@ -91,7 +92,7 @@ import { addAdminAudit } from "@aglyn/tenant-data-admin/server/admin-audit-write
91
92
  ]);
92
93
  const standing = readAiOverageStanding(standingSnapshot.exists ? (_standingSnapshot_data = standingSnapshot.data()) != null ? _standingSnapshot_data : null : null);
93
94
  const ledger = readAiOverageMonthLedger(usageSnapshot.exists ? (_usageSnapshot_data = usageSnapshot.data()) != null ? _usageSnapshot_data : null : null);
94
- const priced = assistMonthOverage(org, Number((_usageSnapshot_get = usageSnapshot.get('estCostUsd')) != null ? _usageSnapshot_get : 0));
95
+ const priced = assistMonthOverage(org, assistSpendAfterReturnsUsd(usageSnapshot.get('estCostUsd'), usageSnapshot.get(ASSIST_RETURNED_USD_FIELD)));
95
96
  return {
96
97
  aglynCeilingUsd: aiOverageStandingCeilingUsd(standing, now),
97
98
  overageStep: aiOverageStep(standing),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/billing-overage.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { pluginRequestFromWeb } from '@aglyn/aglyn/server'\nimport {\n ASSIST_HARD_CAP_CONTROL_LABEL,\n ASSIST_OVERAGE_CAP_CONTROL_LABEL,\n ASSIST_OVERAGE_CAP_MAX_USD,\n ASSIST_OVERAGE_CAP_MIN_USD,\n resolveAssistCreditBudget,\n resolveAssistHardCap,\n resolveAssistOverageCapUsd,\n resolveAssistOverageRateUsdPer1k,\n} from '../usage/assist-credits'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n memberHasOrgPermission,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { logAiOverageControl } from '../activity/ai-activity'\nimport { assistMonthOverage } from '../usage/assist-credits'\nimport { assistUsageMonth } from '../usage/assist-usage'\nimport { aiOverageBillsByInvoice } from '../billing/ai-overage-cutover'\nimport { aiOverageNextChargeUsd } from '../billing/ai-overage-gate'\nimport {\n AI_BILLING_STANDING_DOC,\n AI_BILLING_SUBCOLLECTION,\n readAiOverageMonthLedger,\n} from '../billing/ai-overage-ledger'\nimport {\n AI_OVERAGE_THRESHOLD_USD,\n aiOverageCardOnFile,\n aiOverageEffectiveCeilingUsd,\n aiOverageStandingCeilingUsd,\n aiOverageStep,\n readAiOverageStanding,\n} from '../billing/ai-overage-standing'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { addAdminAudit } from '@aglyn/tenant-data-admin/server/admin-audit-write'\n\n// lockdown-423: exempt — self-serve billing surface, same posture as\n// billing/storage-overage beside it. AGL-1501 keeps billing-locked sessions\n// alive precisely so members can reach Billing, and this is the control that\n// stops an org accruing assist charges, so a 423 would trap it accruing them.\n\n/**\n * Read or set the org's own assist controls: the hard-cap switch (AGL-2653)\n * and the dollar ceiling on overage (AGL-2898).\n *\n * Credits past the plan's assist band are SOLD by default, at the rate\n * `resolveAssistOverageRateUsdPer1k` answers — the plan's listed\n * `extraAssistCreditsUsdPer1k`, or the add-on's rate on a plan that carries\n * its band only with the add-on. This route holds the two\n * controls a customer has over that, and nothing else — it is not a consent\n * surface, and there is nothing here an org must do before assist works.\n *\n * `get` → whether the switch is on, the ceiling if any, the band,\n * the rate the overage bills at, and whether the plan sells\n * overage at all\n * `setHardCap` → store `hardCap: true | false`\n * `setCap` → store `capUsd: number`, or `null` to clear the ceiling\n *\n * `billing.manage`-gated, because both controls bound spend in both\n * directions: lifting either is what lets the org be invoiced further past\n * its band, and setting either is what stops a workspace's assistant.\n * Admin-SDK-only by construction — `assistOverage` is denied to every client\n * in the rules, so a member cannot lift their own ceiling or lower somebody\n * else's.\n *\n * TURNING THE SWITCH OFF AND CLEARING THE CEILING ARE ALWAYS AVAILABLE, on\n * any plan and at any usage level: an org that wants the sale back on must\n * not have to argue with a precondition. Turning the switch on, or setting a\n * ceiling, is refused, 409, on a plan that sells no overage — there the band\n * is already the wall (`assistBandRefuses`), and storing a control that\n * changes nothing would read as protection the org does not have.\n */\n/**\n * What the overage card needs to say about Aglyn's own limit (AGL-3011).\n *\n * Every figure is `null` before the cutover month. That is not a missing\n * value: it is the honest answer while overage still bills on the monthly\n * invoice, and rendering a ceiling and a \"next charge\" figure that nothing\n * enforces would be a promise the platform is not yet keeping.\n */\nasync function readAiOverageStandingForCard(\n orgRef: FirebaseFirestore.DocumentReference,\n org: Parameters<typeof aiOverageEffectiveCeilingUsd>[0],\n): Promise<{\n aglynCeilingUsd: number | null\n overageStep: number | null\n overageThresholdUsd: number | null\n overageChargedUsd: number | null\n overageChargeCount: number | null\n overageNextChargeUsd: number | null\n overagePaused: string | null\n overageCardOnFile: boolean | null\n overageBillsByInvoice: boolean\n}> {\n const now = new Date()\n const month = assistUsageMonth(now)\n const empty = {\n aglynCeilingUsd: null,\n overageStep: null,\n overageThresholdUsd: null,\n overageChargedUsd: null,\n overageChargeCount: null,\n overageNextChargeUsd: null,\n overagePaused: null,\n overageCardOnFile: null,\n overageBillsByInvoice: false,\n }\n if (!aiOverageBillsByInvoice(month)) return empty\n const [standingSnapshot, usageSnapshot] = await Promise.all([\n orgRef.collection(AI_BILLING_SUBCOLLECTION).doc(AI_BILLING_STANDING_DOC).get(),\n orgRef.collection('assistUsage').doc(month).get(),\n ])\n const standing = readAiOverageStanding(\n standingSnapshot.exists ? (standingSnapshot.data() ?? null) : null,\n )\n const ledger = readAiOverageMonthLedger(\n usageSnapshot.exists ? (usageSnapshot.data() ?? null) : null,\n )\n const priced = assistMonthOverage(org, Number(usageSnapshot.get('estCostUsd') ?? 0))\n return {\n aglynCeilingUsd: aiOverageStandingCeilingUsd(standing, now),\n overageStep: aiOverageStep(standing),\n overageThresholdUsd: AI_OVERAGE_THRESHOLD_USD,\n overageChargedUsd: ledger.paidUsd,\n overageChargeCount: ledger.charges,\n overageNextChargeUsd: aiOverageNextChargeUsd(\n priced.overageMonthlyUsd,\n ledger.invoicedUsd,\n ),\n overagePaused: standing.pause?.reason ?? null,\n overageCardOnFile: aiOverageCardOnFile(standing) ?? null,\n overageBillsByInvoice: true,\n }\n}\n\nasync function handler(request: Request): Promise<Response> {\n const { method, body, headers: rawHeaders } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'POST') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n const orgId = String(body?.orgId ?? '')\n const action = String(body?.action ?? '')\n if (!orgId || !['get', 'setHardCap', 'setCap'].includes(action)) {\n return Response.json({ error: 'Bad request' }, { status: 400 })\n }\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const isStaff = decoded['staff'] === true\n const actor = await resolveOrgMembership(decoded.uid, orgId)\n if (\n !isStaff &&\n !(await memberHasOrgPermission(orgId, actor?.member, 'billing.manage'))\n ) {\n return Response.json({ error: 'billing.manage required' }, { status: 403 })\n }\n\n const orgRef = firebaseAdmin.app().firestore().collection('orgs').doc(orgId)\n const orgSnapshot = await orgRef.get()\n if (!orgSnapshot.exists) {\n return Response.json({ error: 'Unknown organization' }, { status: 404 })\n }\n const org = (orgSnapshot.data() ?? {}) as Record<string, unknown>\n const hardCap = resolveAssistHardCap(org as never)\n const capUsd = resolveAssistOverageCapUsd(org as never)\n const bandCredits = resolveAssistCreditBudget(org as never)\n // The rate the card quotes must be the rate the rollup bills, so it is\n // read through the one resolver every reader of the rate goes through.\n // That mattered acutely under AGL-3014, when Starter's rate lived off\n // `PLAN_PRICING` and reading the table directly quoted $0.00 on an org\n // that was really being invoiced. AGL-3203 put the rate on Starter's own\n // row and deleted the exception, so the table and the resolver now agree\n // — and this still reads the resolver, because a comp is priced at\n // nothing and only `resolvePlanPricing` knows that.\n const overageRateUsdPer1k = resolveAssistOverageRateUsdPer1k(org as never)\n const sellsOverage = bandCredits !== null && overageRateUsdPer1k !== null\n\n if (action === 'get') {\n // AGLYN'S OWN LIMIT BESIDE THE WORKSPACE'S (AGL-3011). The card shows\n // both because the LOWER of the two binds, and a card that showed only\n // the figure the workspace set would explain neither the refusal it\n // gets at Aglyn's nor the charge it sees before its own is reached.\n //\n // `null` for every figure while the platform still bills overage on\n // the monthly invoice, so the card keeps saying what is true today\n // rather than describing a mechanism that is not running.\n const overage = await readAiOverageStandingForCard(orgRef, org as never)\n return Response.json(\n {\n hardCap,\n capUsd,\n bandCredits,\n overageRateUsdPer1k,\n sellsOverage,\n ...overage,\n label: ASSIST_HARD_CAP_CONTROL_LABEL,\n capLabel: ASSIST_OVERAGE_CAP_CONTROL_LABEL,\n minCapUsd: ASSIST_OVERAGE_CAP_MIN_USD,\n maxCapUsd: ASSIST_OVERAGE_CAP_MAX_USD,\n },\n { status: 200 },\n )\n }\n\n if (action === 'setCap') {\n const raw = body?.capUsd\n // `null` clears; anything else must be a real number in a real range.\n // A string \"25\" is refused rather than coerced, the way the switch\n // refuses a string \"false\": the resolver reads the field strictly, so\n // a value that reached the document as a string would be no ceiling\n // at all while the card said one was set.\n const clearing = raw === null\n if (\n !clearing &&\n (typeof raw !== 'number' ||\n !Number.isFinite(raw) ||\n raw < ASSIST_OVERAGE_CAP_MIN_USD ||\n raw > ASSIST_OVERAGE_CAP_MAX_USD)\n ) {\n return Response.json(\n {\n error:\n `Set a monthly AI overage ceiling between ` +\n `$${ASSIST_OVERAGE_CAP_MIN_USD} and ` +\n `$${ASSIST_OVERAGE_CAP_MAX_USD.toLocaleString('en-US')}, ` +\n `or clear it.`,\n code: 'invalid_cap',\n },\n { status: 400 },\n )\n }\n if (!clearing && !sellsOverage) {\n return Response.json(\n {\n error:\n bandCredits === null\n ? 'Your plan includes no AI assist credits, so there is no ' +\n 'overage to cap. Upgrade in Billing to add AI assist.'\n : 'Your plan already stops AI assist at its included band — ' +\n 'it sells no credits past it, so there is no overage to cap.',\n code: 'not_sold',\n },\n { status: 409 },\n )\n }\n const requestedCap = clearing ? null : (raw as number)\n await orgRef.set(\n {\n assistOverage: {\n capUsd: requestedCap,\n capSetAt: FieldValue.serverTimestamp(),\n capSetBy: decoded.uid,\n },\n },\n { merge: true },\n )\n await addAdminAudit(firebaseAdmin.app().firestore(), {\n actorUid: decoded.uid,\n actorEmail: decoded.email ?? null,\n action: 'billing.assistOverage.setCap',\n target: `orgs/${orgId}`,\n before: { capUsd },\n after: { capUsd: requestedCap },\n at: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n // The customer's own feed (AGL-2929): `adminAudit` is staff-only, and\n // the workspace should see who set its ceiling. Nothing when the value\n // did not move.\n await logAiOverageControl(\n orgId,\n { uid: decoded.uid, email: decoded.email ?? null },\n { control: 'cap', before: capUsd, after: requestedCap },\n )\n return Response.json({ ok: true, capUsd: requestedCap }, { status: 200 })\n }\n\n // Strictly a boolean, the same way the resolver reads it strictly: a\n // string \"false\" arriving here would otherwise be stored as `true`.\n if (typeof body?.hardCap !== 'boolean') {\n return Response.json(\n { error: 'hardCap must be true or false', code: 'invalid_value' },\n { status: 400 },\n )\n }\n const requested = body.hardCap as boolean\n\n if (requested && !sellsOverage) {\n return Response.json(\n {\n error:\n bandCredits === null\n ? 'Your plan includes no AI assist credits, so there is no band ' +\n 'to stop at. Upgrade in Billing to add AI assist.'\n : 'Your plan already stops AI assist at its included band — ' +\n 'it sells no credits past it, so there is nothing to switch off.',\n code: 'not_sold',\n },\n { status: 409 },\n )\n }\n\n await orgRef.set(\n {\n assistOverage: {\n hardCap: requested,\n hardCapSetAt: FieldValue.serverTimestamp(),\n hardCapSetBy: decoded.uid,\n },\n },\n { merge: true },\n )\n await addAdminAudit(firebaseAdmin.app().firestore(), {\n actorUid: decoded.uid,\n actorEmail: decoded.email ?? null,\n action: 'billing.assistOverage.setHardCap',\n target: `orgs/${orgId}`,\n before: { hardCap },\n after: { hardCap: requested },\n at: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n await logAiOverageControl(\n orgId,\n { uid: decoded.uid, email: decoded.email ?? null },\n { control: 'hardCap', before: hardCap, after: requested },\n )\n\n return Response.json({ ok: true, hardCap: requested }, { status: 200 })\n } catch (error) {\n // A refused credential is a 401, not a fault of ours (AGL-1993). Null\n // for anything else, so a real failure keeps the answer below.\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error(error)\n return Response.json(\n { error: 'Assist overage update failed' },\n { status: 500 },\n )\n }\n}\n\nexport const dynamic = 'force-dynamic'\nexport { handler as POST }\n"],"names":["pluginRequestFromWeb","ASSIST_HARD_CAP_CONTROL_LABEL","ASSIST_OVERAGE_CAP_CONTROL_LABEL","ASSIST_OVERAGE_CAP_MAX_USD","ASSIST_OVERAGE_CAP_MIN_USD","resolveAssistCreditBudget","resolveAssistHardCap","resolveAssistOverageCapUsd","resolveAssistOverageRateUsdPer1k","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","memberHasOrgPermission","resolveOrgMembership","logAiOverageControl","assistMonthOverage","assistUsageMonth","aiOverageBillsByInvoice","aiOverageNextChargeUsd","AI_BILLING_STANDING_DOC","AI_BILLING_SUBCOLLECTION","readAiOverageMonthLedger","AI_OVERAGE_THRESHOLD_USD","aiOverageCardOnFile","aiOverageStandingCeilingUsd","aiOverageStep","readAiOverageStanding","FieldValue","invalidIdTokenResponse","addAdminAudit","readAiOverageStandingForCard","orgRef","org","standingSnapshot","usageSnapshot","standing","now","Date","month","empty","aglynCeilingUsd","overageStep","overageThresholdUsd","overageChargedUsd","overageChargeCount","overageNextChargeUsd","overagePaused","overageCardOnFile","overageBillsByInvoice","Promise","all","collection","doc","get","exists","data","ledger","priced","Number","paidUsd","charges","overageMonthlyUsd","invoicedUsd","pause","reason","handler","request","headers","method","body","rawHeaders","Response","json","error","status","authorization","idToken","startsWith","slice","length","undefined","orgId","String","action","includes","orgSnapshot","decoded","app","auth","verifyIdToken","email_verified","isStaff","actor","uid","member","firestore","hardCap","capUsd","bandCredits","overageRateUsdPer1k","sellsOverage","overage","label","capLabel","minCapUsd","maxCapUsd","raw","clearing","isFinite","toLocaleString","code","requestedCap","set","assistOverage","capSetAt","serverTimestamp","capSetBy","merge","actorUid","actorEmail","email","target","before","after","at","catch","control","ok","requested","hardCapSetAt","hardCapSetBy","unauthenticated","console","dynamic","POST"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/billing-overage.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { pluginRequestFromWeb } from '@aglyn/aglyn/server'\nimport {\n ASSIST_HARD_CAP_CONTROL_LABEL,\n ASSIST_OVERAGE_CAP_CONTROL_LABEL,\n ASSIST_OVERAGE_CAP_MAX_USD,\n ASSIST_OVERAGE_CAP_MIN_USD,\n resolveAssistCreditBudget,\n resolveAssistHardCap,\n resolveAssistOverageCapUsd,\n resolveAssistOverageRateUsdPer1k,\n} from '../usage/assist-credits'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n memberHasOrgPermission,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { logAiOverageControl } from '../activity/ai-activity'\nimport { assistMonthOverage } from '../usage/assist-credits'\nimport {\n ASSIST_RETURNED_USD_FIELD,\n assistSpendAfterReturnsUsd,\n} from '../usage/assist-credit-returns'\nimport { assistUsageMonth } from '../usage/assist-usage'\nimport { aiOverageBillsByInvoice } from '../billing/ai-overage-cutover'\nimport { aiOverageNextChargeUsd } from '../billing/ai-overage-gate'\nimport {\n AI_BILLING_STANDING_DOC,\n AI_BILLING_SUBCOLLECTION,\n readAiOverageMonthLedger,\n} from '../billing/ai-overage-ledger'\nimport {\n AI_OVERAGE_THRESHOLD_USD,\n aiOverageCardOnFile,\n aiOverageEffectiveCeilingUsd,\n aiOverageStandingCeilingUsd,\n aiOverageStep,\n readAiOverageStanding,\n} from '../billing/ai-overage-standing'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { addAdminAudit } from '@aglyn/tenant-data-admin/server/admin-audit-write'\n\n// lockdown-423: exempt — self-serve billing surface, same posture as\n// billing/storage-overage beside it. AGL-1501 keeps billing-locked sessions\n// alive precisely so members can reach Billing, and this is the control that\n// stops an org accruing assist charges, so a 423 would trap it accruing them.\n\n/**\n * Read or set the org's own assist controls: the hard-cap switch (AGL-2653)\n * and the dollar ceiling on overage (AGL-2898).\n *\n * Credits past the plan's assist band are SOLD by default, at the rate\n * `resolveAssistOverageRateUsdPer1k` answers — the plan's listed\n * `extraAssistCreditsUsdPer1k`, or the add-on's rate on a plan that carries\n * its band only with the add-on. This route holds the two\n * controls a customer has over that, and nothing else — it is not a consent\n * surface, and there is nothing here an org must do before assist works.\n *\n * `get` → whether the switch is on, the ceiling if any, the band,\n * the rate the overage bills at, and whether the plan sells\n * overage at all\n * `setHardCap` → store `hardCap: true | false`\n * `setCap` → store `capUsd: number`, or `null` to clear the ceiling\n *\n * `billing.manage`-gated, because both controls bound spend in both\n * directions: lifting either is what lets the org be invoiced further past\n * its band, and setting either is what stops a workspace's assistant.\n * Admin-SDK-only by construction — `assistOverage` is denied to every client\n * in the rules, so a member cannot lift their own ceiling or lower somebody\n * else's.\n *\n * TURNING THE SWITCH OFF AND CLEARING THE CEILING ARE ALWAYS AVAILABLE, on\n * any plan and at any usage level: an org that wants the sale back on must\n * not have to argue with a precondition. Turning the switch on, or setting a\n * ceiling, is refused, 409, on a plan that sells no overage — there the band\n * is already the wall (`assistBandRefuses`), and storing a control that\n * changes nothing would read as protection the org does not have.\n */\n/**\n * What the overage card needs to say about Aglyn's own limit (AGL-3011).\n *\n * Every figure is `null` before the cutover month. That is not a missing\n * value: it is the honest answer while overage still bills on the monthly\n * invoice, and rendering a ceiling and a \"next charge\" figure that nothing\n * enforces would be a promise the platform is not yet keeping.\n */\nasync function readAiOverageStandingForCard(\n orgRef: FirebaseFirestore.DocumentReference,\n org: Parameters<typeof aiOverageEffectiveCeilingUsd>[0],\n): Promise<{\n aglynCeilingUsd: number | null\n overageStep: number | null\n overageThresholdUsd: number | null\n overageChargedUsd: number | null\n overageChargeCount: number | null\n overageNextChargeUsd: number | null\n overagePaused: string | null\n overageCardOnFile: boolean | null\n overageBillsByInvoice: boolean\n}> {\n const now = new Date()\n const month = assistUsageMonth(now)\n const empty = {\n aglynCeilingUsd: null,\n overageStep: null,\n overageThresholdUsd: null,\n overageChargedUsd: null,\n overageChargeCount: null,\n overageNextChargeUsd: null,\n overagePaused: null,\n overageCardOnFile: null,\n overageBillsByInvoice: false,\n }\n if (!aiOverageBillsByInvoice(month)) return empty\n const [standingSnapshot, usageSnapshot] = await Promise.all([\n orgRef.collection(AI_BILLING_SUBCOLLECTION).doc(AI_BILLING_STANDING_DOC).get(),\n orgRef.collection('assistUsage').doc(month).get(),\n ])\n const standing = readAiOverageStanding(\n standingSnapshot.exists ? (standingSnapshot.data() ?? null) : null,\n )\n const ledger = readAiOverageMonthLedger(\n usageSnapshot.exists ? (usageSnapshot.data() ?? null) : null,\n )\n const priced = assistMonthOverage(\n org,\n assistSpendAfterReturnsUsd(\n usageSnapshot.get('estCostUsd'),\n usageSnapshot.get(ASSIST_RETURNED_USD_FIELD),\n ),\n )\n return {\n aglynCeilingUsd: aiOverageStandingCeilingUsd(standing, now),\n overageStep: aiOverageStep(standing),\n overageThresholdUsd: AI_OVERAGE_THRESHOLD_USD,\n overageChargedUsd: ledger.paidUsd,\n overageChargeCount: ledger.charges,\n overageNextChargeUsd: aiOverageNextChargeUsd(\n priced.overageMonthlyUsd,\n ledger.invoicedUsd,\n ),\n overagePaused: standing.pause?.reason ?? null,\n overageCardOnFile: aiOverageCardOnFile(standing) ?? null,\n overageBillsByInvoice: true,\n }\n}\n\nasync function handler(request: Request): Promise<Response> {\n const { method, body, headers: rawHeaders } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'POST') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n const orgId = String(body?.orgId ?? '')\n const action = String(body?.action ?? '')\n if (!orgId || !['get', 'setHardCap', 'setCap'].includes(action)) {\n return Response.json({ error: 'Bad request' }, { status: 400 })\n }\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const isStaff = decoded['staff'] === true\n const actor = await resolveOrgMembership(decoded.uid, orgId)\n if (\n !isStaff &&\n !(await memberHasOrgPermission(orgId, actor?.member, 'billing.manage'))\n ) {\n return Response.json({ error: 'billing.manage required' }, { status: 403 })\n }\n\n const orgRef = firebaseAdmin.app().firestore().collection('orgs').doc(orgId)\n const orgSnapshot = await orgRef.get()\n if (!orgSnapshot.exists) {\n return Response.json({ error: 'Unknown organization' }, { status: 404 })\n }\n const org = (orgSnapshot.data() ?? {}) as Record<string, unknown>\n const hardCap = resolveAssistHardCap(org as never)\n const capUsd = resolveAssistOverageCapUsd(org as never)\n const bandCredits = resolveAssistCreditBudget(org as never)\n // The rate the card quotes must be the rate the rollup bills, so it is\n // read through the one resolver every reader of the rate goes through.\n // That mattered acutely under AGL-3014, when Starter's rate lived off\n // `PLAN_PRICING` and reading the table directly quoted $0.00 on an org\n // that was really being invoiced. AGL-3203 put the rate on Starter's own\n // row and deleted the exception, so the table and the resolver now agree\n // — and this still reads the resolver, because a comp is priced at\n // nothing and only `resolvePlanPricing` knows that.\n const overageRateUsdPer1k = resolveAssistOverageRateUsdPer1k(org as never)\n const sellsOverage = bandCredits !== null && overageRateUsdPer1k !== null\n\n if (action === 'get') {\n // AGLYN'S OWN LIMIT BESIDE THE WORKSPACE'S (AGL-3011). The card shows\n // both because the LOWER of the two binds, and a card that showed only\n // the figure the workspace set would explain neither the refusal it\n // gets at Aglyn's nor the charge it sees before its own is reached.\n //\n // `null` for every figure while the platform still bills overage on\n // the monthly invoice, so the card keeps saying what is true today\n // rather than describing a mechanism that is not running.\n const overage = await readAiOverageStandingForCard(orgRef, org as never)\n return Response.json(\n {\n hardCap,\n capUsd,\n bandCredits,\n overageRateUsdPer1k,\n sellsOverage,\n ...overage,\n label: ASSIST_HARD_CAP_CONTROL_LABEL,\n capLabel: ASSIST_OVERAGE_CAP_CONTROL_LABEL,\n minCapUsd: ASSIST_OVERAGE_CAP_MIN_USD,\n maxCapUsd: ASSIST_OVERAGE_CAP_MAX_USD,\n },\n { status: 200 },\n )\n }\n\n if (action === 'setCap') {\n const raw = body?.capUsd\n // `null` clears; anything else must be a real number in a real range.\n // A string \"25\" is refused rather than coerced, the way the switch\n // refuses a string \"false\": the resolver reads the field strictly, so\n // a value that reached the document as a string would be no ceiling\n // at all while the card said one was set.\n const clearing = raw === null\n if (\n !clearing &&\n (typeof raw !== 'number' ||\n !Number.isFinite(raw) ||\n raw < ASSIST_OVERAGE_CAP_MIN_USD ||\n raw > ASSIST_OVERAGE_CAP_MAX_USD)\n ) {\n return Response.json(\n {\n error:\n `Set a monthly AI overage ceiling between ` +\n `$${ASSIST_OVERAGE_CAP_MIN_USD} and ` +\n `$${ASSIST_OVERAGE_CAP_MAX_USD.toLocaleString('en-US')}, ` +\n `or clear it.`,\n code: 'invalid_cap',\n },\n { status: 400 },\n )\n }\n if (!clearing && !sellsOverage) {\n return Response.json(\n {\n error:\n bandCredits === null\n ? 'Your plan includes no AI assist credits, so there is no ' +\n 'overage to cap. Upgrade in Billing to add AI assist.'\n : 'Your plan already stops AI assist at its included band — ' +\n 'it sells no credits past it, so there is no overage to cap.',\n code: 'not_sold',\n },\n { status: 409 },\n )\n }\n const requestedCap = clearing ? null : (raw as number)\n await orgRef.set(\n {\n assistOverage: {\n capUsd: requestedCap,\n capSetAt: FieldValue.serverTimestamp(),\n capSetBy: decoded.uid,\n },\n },\n { merge: true },\n )\n await addAdminAudit(firebaseAdmin.app().firestore(), {\n actorUid: decoded.uid,\n actorEmail: decoded.email ?? null,\n action: 'billing.assistOverage.setCap',\n target: `orgs/${orgId}`,\n before: { capUsd },\n after: { capUsd: requestedCap },\n at: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n // The customer's own feed (AGL-2929): `adminAudit` is staff-only, and\n // the workspace should see who set its ceiling. Nothing when the value\n // did not move.\n await logAiOverageControl(\n orgId,\n { uid: decoded.uid, email: decoded.email ?? null },\n { control: 'cap', before: capUsd, after: requestedCap },\n )\n return Response.json({ ok: true, capUsd: requestedCap }, { status: 200 })\n }\n\n // Strictly a boolean, the same way the resolver reads it strictly: a\n // string \"false\" arriving here would otherwise be stored as `true`.\n if (typeof body?.hardCap !== 'boolean') {\n return Response.json(\n { error: 'hardCap must be true or false', code: 'invalid_value' },\n { status: 400 },\n )\n }\n const requested = body.hardCap as boolean\n\n if (requested && !sellsOverage) {\n return Response.json(\n {\n error:\n bandCredits === null\n ? 'Your plan includes no AI assist credits, so there is no band ' +\n 'to stop at. Upgrade in Billing to add AI assist.'\n : 'Your plan already stops AI assist at its included band — ' +\n 'it sells no credits past it, so there is nothing to switch off.',\n code: 'not_sold',\n },\n { status: 409 },\n )\n }\n\n await orgRef.set(\n {\n assistOverage: {\n hardCap: requested,\n hardCapSetAt: FieldValue.serverTimestamp(),\n hardCapSetBy: decoded.uid,\n },\n },\n { merge: true },\n )\n await addAdminAudit(firebaseAdmin.app().firestore(), {\n actorUid: decoded.uid,\n actorEmail: decoded.email ?? null,\n action: 'billing.assistOverage.setHardCap',\n target: `orgs/${orgId}`,\n before: { hardCap },\n after: { hardCap: requested },\n at: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n await logAiOverageControl(\n orgId,\n { uid: decoded.uid, email: decoded.email ?? null },\n { control: 'hardCap', before: hardCap, after: requested },\n )\n\n return Response.json({ ok: true, hardCap: requested }, { status: 200 })\n } catch (error) {\n // A refused credential is a 401, not a fault of ours (AGL-1993). Null\n // for anything else, so a real failure keeps the answer below.\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error(error)\n return Response.json(\n { error: 'Assist overage update failed' },\n { status: 500 },\n )\n }\n}\n\nexport const dynamic = 'force-dynamic'\nexport { handler as POST }\n"],"names":["pluginRequestFromWeb","ASSIST_HARD_CAP_CONTROL_LABEL","ASSIST_OVERAGE_CAP_CONTROL_LABEL","ASSIST_OVERAGE_CAP_MAX_USD","ASSIST_OVERAGE_CAP_MIN_USD","resolveAssistCreditBudget","resolveAssistHardCap","resolveAssistOverageCapUsd","resolveAssistOverageRateUsdPer1k","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","memberHasOrgPermission","resolveOrgMembership","logAiOverageControl","assistMonthOverage","ASSIST_RETURNED_USD_FIELD","assistSpendAfterReturnsUsd","assistUsageMonth","aiOverageBillsByInvoice","aiOverageNextChargeUsd","AI_BILLING_STANDING_DOC","AI_BILLING_SUBCOLLECTION","readAiOverageMonthLedger","AI_OVERAGE_THRESHOLD_USD","aiOverageCardOnFile","aiOverageStandingCeilingUsd","aiOverageStep","readAiOverageStanding","FieldValue","invalidIdTokenResponse","addAdminAudit","readAiOverageStandingForCard","orgRef","org","standingSnapshot","usageSnapshot","standing","now","Date","month","empty","aglynCeilingUsd","overageStep","overageThresholdUsd","overageChargedUsd","overageChargeCount","overageNextChargeUsd","overagePaused","overageCardOnFile","overageBillsByInvoice","Promise","all","collection","doc","get","exists","data","ledger","priced","paidUsd","charges","overageMonthlyUsd","invoicedUsd","pause","reason","handler","request","headers","method","body","rawHeaders","Response","json","error","status","authorization","idToken","startsWith","slice","length","undefined","orgId","String","action","includes","orgSnapshot","decoded","app","auth","verifyIdToken","email_verified","isStaff","actor","uid","member","firestore","hardCap","capUsd","bandCredits","overageRateUsdPer1k","sellsOverage","overage","label","capLabel","minCapUsd","maxCapUsd","raw","clearing","Number","isFinite","toLocaleString","code","requestedCap","set","assistOverage","capSetAt","serverTimestamp","capSetBy","merge","actorUid","actorEmail","email","target","before","after","at","catch","control","ok","requested","hardCapSetAt","hardCapSetBy","unauthenticated","console","dynamic","POST"],"mappings":"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"}
package/src/lib/server.js CHANGED
@@ -18,6 +18,7 @@ import { registerAiDeclarations } from "./declarations.js";
18
18
  import { AI_JOBS_BEAT_PATH } from "./jobs/ai-jobs-beat.js";
19
19
  import { registerAiJobPlan } from "./jobs/ai-job-plan-step.js";
20
20
  import { registerAiJobsPause } from "./jobs/ai-jobs-pause.js";
21
+ import { registerAiJobsNotify } from "./jobs/ai-jobs-notify.js";
21
22
  import { registerAiComponentJob } from "./jobs/ai-job-component-step.js";
22
23
  import { registerAiCrmJob } from "./jobs/ai-job-crm-step.js";
23
24
  import { registerAiLayoutJob } from "./jobs/ai-job-layout-step.js";
@@ -54,6 +55,7 @@ import { GET as aiAdminOrgsSpend } from "./server/ai-admin-orgs-spend.js";
54
55
  import { GET as aiAdminSignals } from "./server/ai-admin-signals.js";
55
56
  import { GET as aiAdminUser } from "./server/ai-admin-user.js";
56
57
  import { POST as aiAdminOverage } from "./server/ai-admin-overage.js";
58
+ import { POST as aiAdminCredits } from "./server/ai-admin-credits.js";
57
59
  import { GET as aiUsage } from "./server/ai-usage.js";
58
60
  import { GET as aiAllotments } from "./server/ai-allotments.js";
59
61
  import { GET as aiInsightAnswer } from "./server/ai-insight-answer.js";
@@ -88,6 +90,9 @@ export * from "./runtime/ai-runtime.js";
88
90
  // The workspace AI pause the machine asks before it claims any step, so a
89
91
  // paused workspace's queued jobs spend nothing (AGL-3037).
90
92
  registerAiJobsPause();
93
+ // The person who started a job is told when it needs them, finishes or
94
+ // stops (AGL-3593), from the write that changed it.
95
+ registerAiJobsNotify();
91
96
  // The plan step every planned kind runs first (AGL-2935).
92
97
  registerAiJobPlan();
93
98
  // Components, layouts and templates (AGL-2908, AGL-2909).
@@ -265,6 +270,12 @@ export * from "./runtime/ai-runtime.js";
265
270
  registerPluginApiRoute('ai/admin/overage', {
266
271
  web: aiAdminOverage
267
272
  });
273
+ // Credits given back to a workspace's band or a Free owner's allowance
274
+ // (AGL-3595): compensation for a fault of ours, bounded by what the month
275
+ // used, keyed against a double-click, and audited.
276
+ registerPluginApiRoute('ai/admin/credits', {
277
+ web: aiAdminCredits
278
+ });
268
279
  }
269
280
 
270
281
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { registerPluginApiRoute } from '@aglyn/aglyn/server'\nimport { registerAiDeclarations } from './declarations'\nimport { AI_JOBS_BEAT_PATH } from './jobs/ai-jobs-beat'\nimport { registerAiJobPlan } from './jobs/ai-job-plan-step'\nimport { registerAiJobsPause } from './jobs/ai-jobs-pause'\nimport { registerAiComponentJob } from './jobs/ai-job-component-step'\nimport { registerAiCrmJob } from './jobs/ai-job-crm-step'\nimport { registerAiLayoutJob } from './jobs/ai-job-layout-step'\nimport { registerAiTemplateJob } from './jobs/ai-job-template-step'\nimport { registerAiExperimentJob } from './jobs/ai-job-experiment-step'\nimport { registerAiFormJob } from './jobs/ai-job-form-step'\nimport { registerAiPageJob } from './jobs/ai-job-page-step'\nimport { registerAiProductsJob } from './jobs/ai-job-products-step'\nimport { registerAiEmailJob } from './jobs/ai-job-email-step'\nimport { registerAiCampaignJob } from './jobs/ai-job-campaign-step'\nimport { registerAiSiteJob } from './jobs/ai-job-site-step'\nimport { registerAiWorkflowJob } from './jobs/ai-job-workflow-step'\nimport { registerAiInsightJob } from './jobs/ai-job-insight-step'\nimport { registerAiFigureReaders } from './insights/ai-figure-readers'\nimport { registerAiTextGenerator } from './server/plugin-text-generation'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { ensureFirstPartyAiProviders } from './providers/registry'\nimport { aiAssistHandler } from './server/ai-assist'\nimport { GET as aiCrmAnswer } from './server/ai-crm-answer'\nimport { POST as runAiJobsBeat } from './server/ai-jobs-beat-route'\nimport { POST as cancelAiJob } from './server/ai-jobs-cancel'\nimport { GET as aiJobEvents } from './server/ai-jobs-events-route'\nimport { POST as createAiSiteBatch } from './server/ai-jobs-batch'\nimport { GET as listAiJobs, POST as createAiJob } from './server/ai-jobs-route'\nimport { POST as resumeAiJob } from './server/ai-jobs-resume'\nimport { POST as aiGenerateComponent } from './server/ai-generate-component'\nimport { POST as applyAiSeoAudit } from './server/ai-seo-apply'\nimport { POST as assistChat } from './server/assist-chat'\nimport { POST as assistEditApplied } from './server/assist-edit-applied'\nimport { POST as assistFeedback } from './server/assist-feedback'\nimport { PATCH as aiHostPermissions } from './server/ai-host-permissions'\nimport { GET as aiAdminOrg } from './server/ai-admin-org'\nimport { GET as aiAdminOrgsSpend } from './server/ai-admin-orgs-spend'\nimport { GET as aiAdminSignals } from './server/ai-admin-signals'\nimport { GET as aiAdminUser } from './server/ai-admin-user'\nimport { POST as aiAdminOverage } from './server/ai-admin-overage'\nimport { GET as aiUsage } from './server/ai-usage'\nimport { GET as aiAllotments } from './server/ai-allotments'\nimport { GET as aiInsightAnswer } from './server/ai-insight-answer'\nimport { POST as runAiInsightsDigest } from './server/ai-insight-digest-route'\nimport { AI_INSIGHTS_DIGEST_PATH } from './insights/ai-insight-digest'\nimport { GET as aiModels } from './server/ai-models'\nimport { GET as billingCredits } from './server/billing-credits'\nimport { POST as billingOverage } from './server/billing-overage'\n\nexport * from './providers/contract'\nexport * from './providers/catalog'\nexport * from './providers/registry'\nexport * from './providers/routing'\nexport * from './runtime/ai-runtime'\n\n/**\n * The two first-party adapters, registered once per process. A marketplace\n * plugin registers its own against `AI_PROVIDER_CONTRACT` from its own\n * entry and never touches this.\n */\nconst registerFirstPartyProviders = ensureFirstPartyAiProviders\n\n/**\n * Every job kind this plugin runs beyond the three the machine registers\n * itself (AGL-3025). Each is a CALL, never an import made for what the\n * module does as it loads: this package declares only this file\n * effect-ful, so a bundler honoring `sideEffects` deletes an import whose\n * exports go unused — and a step registered that way ran in every spec and\n * in neither app. `registrations-survive-a-bundler.spec.ts` holds this.\n *\n * The console surface is the only caller (AGL-3026): its doors run a job's\n * first step inline, and its beat runs every step after it. The tenant app\n * loads no server surface of this plugin at all, so nothing that serves a\n * published site can reach a provider.\n */\nfunction registerAiJobKinds(): void {\n // The workspace AI pause the machine asks before it claims any step, so a\n // paused workspace's queued jobs spend nothing (AGL-3037).\n registerAiJobsPause()\n // The plan step every planned kind runs first (AGL-2935).\n registerAiJobPlan()\n // Components, layouts and templates (AGL-2908, AGL-2909).\n registerAiComponentJob()\n registerAiLayoutJob()\n registerAiTemplateJob()\n // Forms (AGL-2913).\n registerAiExperimentJob()\n registerAiFormJob()\n // Pages, with the least time one pass needs (AGL-2907).\n registerAiPageJob()\n // Email designs and campaigns, whose drafts the email and marketing\n // plugins write on the resource-draft seam (AGL-2912).\n registerAiEmailJob()\n registerAiCampaignJob()\n // Automations drafted from a description, and explained, whose drafts the\n // workflows plugin writes on the resource-draft seam (AGL-2919).\n registerAiWorkflowJob()\n // Product copy, and a store's products, categories and discounts from a\n // brief, as proposals the commerce plugin's surfaces apply (AGL-2916).\n registerAiProductsJob()\n // The site scaffold, which builds a whole site through the steps above\n // (AGL-2911).\n registerAiSiteJob()\n // Insights (AGL-2915): a question about a site's figures, answered from\n // tables the figure readers return, and the weekly digest. The readers for\n // the platform's own records — page views, forms, datasets — are this\n // plugin's; every other plugin registers its own from its console surface.\n registerAiFigureReaders(() => firebaseAdmin.app().firestore())\n registerAiInsightJob()\n // CRM by AI (AGL-2917): a record's summary and next step, an email draft\n // and an import's column matches, read through the CRM's facts readers.\n registerAiCrmJob()\n}\n\n/**\n * The console-side API (AGL-2939): every AI door, under the `ai` and\n * `assist` prefixes the plugin owns. The URLs the panel and the besigner\n * call are unchanged — `/api/assist/chat`, `/api/ai/assist`,\n * `/api/ai/jobs` — because the dispatcher serves them from the registry\n * exactly where the named routes used to; the two billing doors and the\n * per-member usage read (`ai/usage`) live under the plugin's own prefix.\n * The jobs beat is the one path outside them, for the reason\n * `AI_JOBS_BEAT_PATH` gives.\n */\nexport function registerAiConsoleApi(): void {\n registerAiDeclarations()\n registerFirstPartyProviders()\n registerAiJobKinds()\n // This plugin as the workspace's text generator on the core's seam\n // (AGL-3324): another plugin drafts a paragraph through it under the\n // generation doors' own rules, metered as a text job is.\n registerAiTextGenerator()\n registerPluginApiRoute('assist/chat', { web: assistChat })\n registerPluginApiRoute('assist/feedback', { web: assistFeedback })\n // The applied-edit record (AGL-2906): counts of a proposal the author\n // applied in their editor, written to the site's activity log.\n registerPluginApiRoute('assist/edit-applied', { web: assistEditApplied })\n // A collaborator's AI toggles on one site (AGL-2927), set from the site's\n // collaborators card through this plugin's column there.\n registerPluginApiRoute('ai/host-permissions', { web: aiHostPermissions })\n registerPluginApiRoute('ai/assist', aiAssistHandler)\n registerPluginApiRoute('ai/jobs', {\n web: (request) => (request.method === 'GET' ? listAiJobs(request) : createAiJob(request)),\n })\n // The agency batch (AGL-2911): one brief, one `site` job per named site,\n // under one batch id. Before the `:jobId` routes, which it is not one of.\n registerPluginApiRoute('ai/jobs/batch', { web: createAiSiteBatch })\n registerPluginApiRoute('ai/jobs/:jobId/cancel', {\n web: (request, context) =>\n cancelAiJob(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n registerPluginApiRoute('ai/jobs/:jobId/resume', {\n web: (request, context) =>\n resumeAiJob(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n // An insight job's answer (AGL-2915), which is kept off the job document\n // and served only to the member who asked, or for a digest to the site's\n // members.\n registerPluginApiRoute('ai/insights/:jobId', {\n web: (request, context) =>\n aiInsightAnswer(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n // A CRM job's answer (AGL-2917), which is kept off the job document and\n // served only to a member the CRM still lets read the record.\n registerPluginApiRoute('ai/crm/:jobId', {\n web: (request, context) =>\n aiCrmAnswer(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n registerPluginApiRoute('ai/jobs/:jobId/events', {\n web: (request, context) =>\n aiJobEvents(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n // The jobs beat (AGL-3026): every step the doors leave queued, run every\n // minute by the console's scheduler on the cron secret, on the one surface\n // that holds the provider's key.\n registerPluginApiRoute(AI_JOBS_BEAT_PATH, { web: runAiJobsBeat })\n // The weekly insights (AGL-2915): Monday's digests made for the people who\n // asked for them, and delivered once the beat has written them. Beside the\n // beat under `/api/admin/`, where the console's scheduled sweeps live.\n registerPluginApiRoute(AI_INSIGHTS_DIGEST_PATH, { web: runAiInsightsDigest })\n // A site SEO audit's \"Apply all\" (AGL-2910): content fixes as new\n // unpublished versions, listing values staged for their SEO cards.\n registerPluginApiRoute('ai/seo/apply', { web: applyAiSeoAudit })\n // Save the selection as a reusable component, with AI (AGL-2908): the\n // second entry point of the component job, from the besigner's Attributes\n // panel. It proposes, and the person's Apply writes.\n registerPluginApiRoute('ai/generate/component', { web: aiGenerateComponent })\n registerPluginApiRoute('ai/billing/credits', { web: billingCredits })\n registerPluginApiRoute('ai/billing/overage', { web: billingOverage })\n registerPluginApiRoute('ai/usage', { web: aiUsage })\n // The allotments a manager sets, and the options the model switch lists\n // (AGL-2942). One handler answers the allotments' GET and POST.\n registerPluginApiRoute('ai/allotments', { web: aiAllotments })\n registerPluginApiRoute('ai/models', { web: aiModels })\n // The staff doors (AGL-2928, AGL-2930, AGL-2252): one org's AI in full,\n // one account's usage across its workspaces, and the fleet's Assist\n // signals behind the Assist signal staff page.\n registerPluginApiRoute('ai/admin/org', { web: aiAdminOrg })\n registerPluginApiRoute('ai/admin/orgs-spend', { web: aiAdminOrgsSpend })\n registerPluginApiRoute('ai/admin/user', { web: aiAdminUser })\n registerPluginApiRoute('ai/admin/signals', { web: aiAdminSignals })\n // One workspace's overage standing and the four acts staff have over it\n // (AGL-3011): the ceiling override, lifting a pause — the only way a\n // dispute pause comes off — and resetting the step. Every act audited.\n registerPluginApiRoute('ai/admin/overage', { web: aiAdminOverage })\n}\n\n// Type-only (AGL-3080): the plugin's entitlement keys, declared by module\n// augmentation, for every program that loads this entry point.\nexport type { aiPlanEntitlements } from './plan-entitlements'\n"],"names":["registerPluginApiRoute","registerAiDeclarations","AI_JOBS_BEAT_PATH","registerAiJobPlan","registerAiJobsPause","registerAiComponentJob","registerAiCrmJob","registerAiLayoutJob","registerAiTemplateJob","registerAiExperimentJob","registerAiFormJob","registerAiPageJob","registerAiProductsJob","registerAiEmailJob","registerAiCampaignJob","registerAiSiteJob","registerAiWorkflowJob","registerAiInsightJob","registerAiFigureReaders","registerAiTextGenerator","firebaseAdmin","ensureFirstPartyAiProviders","aiAssistHandler","GET","aiCrmAnswer","POST","runAiJobsBeat","cancelAiJob","aiJobEvents","createAiSiteBatch","listAiJobs","createAiJob","resumeAiJob","aiGenerateComponent","applyAiSeoAudit","assistChat","assistEditApplied","assistFeedback","PATCH","aiHostPermissions","aiAdminOrg","aiAdminOrgsSpend","aiAdminSignals","aiAdminUser","aiAdminOverage","aiUsage","aiAllotments","aiInsightAnswer","runAiInsightsDigest","AI_INSIGHTS_DIGEST_PATH","aiModels","billingCredits","billingOverage","registerFirstPartyProviders","registerAiJobKinds","app","firestore","registerAiConsoleApi","web","request","method","context","params","Promise","resolve","jobId","String"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { registerPluginApiRoute } from '@aglyn/aglyn/server'\nimport { registerAiDeclarations } from './declarations'\nimport { AI_JOBS_BEAT_PATH } from './jobs/ai-jobs-beat'\nimport { registerAiJobPlan } from './jobs/ai-job-plan-step'\nimport { registerAiJobsPause } from './jobs/ai-jobs-pause'\nimport { registerAiJobsNotify } from './jobs/ai-jobs-notify'\nimport { registerAiComponentJob } from './jobs/ai-job-component-step'\nimport { registerAiCrmJob } from './jobs/ai-job-crm-step'\nimport { registerAiLayoutJob } from './jobs/ai-job-layout-step'\nimport { registerAiTemplateJob } from './jobs/ai-job-template-step'\nimport { registerAiExperimentJob } from './jobs/ai-job-experiment-step'\nimport { registerAiFormJob } from './jobs/ai-job-form-step'\nimport { registerAiPageJob } from './jobs/ai-job-page-step'\nimport { registerAiProductsJob } from './jobs/ai-job-products-step'\nimport { registerAiEmailJob } from './jobs/ai-job-email-step'\nimport { registerAiCampaignJob } from './jobs/ai-job-campaign-step'\nimport { registerAiSiteJob } from './jobs/ai-job-site-step'\nimport { registerAiWorkflowJob } from './jobs/ai-job-workflow-step'\nimport { registerAiInsightJob } from './jobs/ai-job-insight-step'\nimport { registerAiFigureReaders } from './insights/ai-figure-readers'\nimport { registerAiTextGenerator } from './server/plugin-text-generation'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { ensureFirstPartyAiProviders } from './providers/registry'\nimport { aiAssistHandler } from './server/ai-assist'\nimport { GET as aiCrmAnswer } from './server/ai-crm-answer'\nimport { POST as runAiJobsBeat } from './server/ai-jobs-beat-route'\nimport { POST as cancelAiJob } from './server/ai-jobs-cancel'\nimport { GET as aiJobEvents } from './server/ai-jobs-events-route'\nimport { POST as createAiSiteBatch } from './server/ai-jobs-batch'\nimport { GET as listAiJobs, POST as createAiJob } from './server/ai-jobs-route'\nimport { POST as resumeAiJob } from './server/ai-jobs-resume'\nimport { POST as aiGenerateComponent } from './server/ai-generate-component'\nimport { POST as applyAiSeoAudit } from './server/ai-seo-apply'\nimport { POST as assistChat } from './server/assist-chat'\nimport { POST as assistEditApplied } from './server/assist-edit-applied'\nimport { POST as assistFeedback } from './server/assist-feedback'\nimport { PATCH as aiHostPermissions } from './server/ai-host-permissions'\nimport { GET as aiAdminOrg } from './server/ai-admin-org'\nimport { GET as aiAdminOrgsSpend } from './server/ai-admin-orgs-spend'\nimport { GET as aiAdminSignals } from './server/ai-admin-signals'\nimport { GET as aiAdminUser } from './server/ai-admin-user'\nimport { POST as aiAdminOverage } from './server/ai-admin-overage'\nimport { POST as aiAdminCredits } from './server/ai-admin-credits'\nimport { GET as aiUsage } from './server/ai-usage'\nimport { GET as aiAllotments } from './server/ai-allotments'\nimport { GET as aiInsightAnswer } from './server/ai-insight-answer'\nimport { POST as runAiInsightsDigest } from './server/ai-insight-digest-route'\nimport { AI_INSIGHTS_DIGEST_PATH } from './insights/ai-insight-digest'\nimport { GET as aiModels } from './server/ai-models'\nimport { GET as billingCredits } from './server/billing-credits'\nimport { POST as billingOverage } from './server/billing-overage'\n\nexport * from './providers/contract'\nexport * from './providers/catalog'\nexport * from './providers/registry'\nexport * from './providers/routing'\nexport * from './runtime/ai-runtime'\n\n/**\n * The two first-party adapters, registered once per process. A marketplace\n * plugin registers its own against `AI_PROVIDER_CONTRACT` from its own\n * entry and never touches this.\n */\nconst registerFirstPartyProviders = ensureFirstPartyAiProviders\n\n/**\n * Every job kind this plugin runs beyond the three the machine registers\n * itself (AGL-3025). Each is a CALL, never an import made for what the\n * module does as it loads: this package declares only this file\n * effect-ful, so a bundler honoring `sideEffects` deletes an import whose\n * exports go unused — and a step registered that way ran in every spec and\n * in neither app. `registrations-survive-a-bundler.spec.ts` holds this.\n *\n * The console surface is the only caller (AGL-3026): its doors run a job's\n * first step inline, and its beat runs every step after it. The tenant app\n * loads no server surface of this plugin at all, so nothing that serves a\n * published site can reach a provider.\n */\nfunction registerAiJobKinds(): void {\n // The workspace AI pause the machine asks before it claims any step, so a\n // paused workspace's queued jobs spend nothing (AGL-3037).\n registerAiJobsPause()\n // The person who started a job is told when it needs them, finishes or\n // stops (AGL-3593), from the write that changed it.\n registerAiJobsNotify()\n // The plan step every planned kind runs first (AGL-2935).\n registerAiJobPlan()\n // Components, layouts and templates (AGL-2908, AGL-2909).\n registerAiComponentJob()\n registerAiLayoutJob()\n registerAiTemplateJob()\n // Forms (AGL-2913).\n registerAiExperimentJob()\n registerAiFormJob()\n // Pages, with the least time one pass needs (AGL-2907).\n registerAiPageJob()\n // Email designs and campaigns, whose drafts the email and marketing\n // plugins write on the resource-draft seam (AGL-2912).\n registerAiEmailJob()\n registerAiCampaignJob()\n // Automations drafted from a description, and explained, whose drafts the\n // workflows plugin writes on the resource-draft seam (AGL-2919).\n registerAiWorkflowJob()\n // Product copy, and a store's products, categories and discounts from a\n // brief, as proposals the commerce plugin's surfaces apply (AGL-2916).\n registerAiProductsJob()\n // The site scaffold, which builds a whole site through the steps above\n // (AGL-2911).\n registerAiSiteJob()\n // Insights (AGL-2915): a question about a site's figures, answered from\n // tables the figure readers return, and the weekly digest. The readers for\n // the platform's own records — page views, forms, datasets — are this\n // plugin's; every other plugin registers its own from its console surface.\n registerAiFigureReaders(() => firebaseAdmin.app().firestore())\n registerAiInsightJob()\n // CRM by AI (AGL-2917): a record's summary and next step, an email draft\n // and an import's column matches, read through the CRM's facts readers.\n registerAiCrmJob()\n}\n\n/**\n * The console-side API (AGL-2939): every AI door, under the `ai` and\n * `assist` prefixes the plugin owns. The URLs the panel and the besigner\n * call are unchanged — `/api/assist/chat`, `/api/ai/assist`,\n * `/api/ai/jobs` — because the dispatcher serves them from the registry\n * exactly where the named routes used to; the two billing doors and the\n * per-member usage read (`ai/usage`) live under the plugin's own prefix.\n * The jobs beat is the one path outside them, for the reason\n * `AI_JOBS_BEAT_PATH` gives.\n */\nexport function registerAiConsoleApi(): void {\n registerAiDeclarations()\n registerFirstPartyProviders()\n registerAiJobKinds()\n // This plugin as the workspace's text generator on the core's seam\n // (AGL-3324): another plugin drafts a paragraph through it under the\n // generation doors' own rules, metered as a text job is.\n registerAiTextGenerator()\n registerPluginApiRoute('assist/chat', { web: assistChat })\n registerPluginApiRoute('assist/feedback', { web: assistFeedback })\n // The applied-edit record (AGL-2906): counts of a proposal the author\n // applied in their editor, written to the site's activity log.\n registerPluginApiRoute('assist/edit-applied', { web: assistEditApplied })\n // A collaborator's AI toggles on one site (AGL-2927), set from the site's\n // collaborators card through this plugin's column there.\n registerPluginApiRoute('ai/host-permissions', { web: aiHostPermissions })\n registerPluginApiRoute('ai/assist', aiAssistHandler)\n registerPluginApiRoute('ai/jobs', {\n web: (request) => (request.method === 'GET' ? listAiJobs(request) : createAiJob(request)),\n })\n // The agency batch (AGL-2911): one brief, one `site` job per named site,\n // under one batch id. Before the `:jobId` routes, which it is not one of.\n registerPluginApiRoute('ai/jobs/batch', { web: createAiSiteBatch })\n registerPluginApiRoute('ai/jobs/:jobId/cancel', {\n web: (request, context) =>\n cancelAiJob(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n registerPluginApiRoute('ai/jobs/:jobId/resume', {\n web: (request, context) =>\n resumeAiJob(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n // An insight job's answer (AGL-2915), which is kept off the job document\n // and served only to the member who asked, or for a digest to the site's\n // members.\n registerPluginApiRoute('ai/insights/:jobId', {\n web: (request, context) =>\n aiInsightAnswer(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n // A CRM job's answer (AGL-2917), which is kept off the job document and\n // served only to a member the CRM still lets read the record.\n registerPluginApiRoute('ai/crm/:jobId', {\n web: (request, context) =>\n aiCrmAnswer(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n registerPluginApiRoute('ai/jobs/:jobId/events', {\n web: (request, context) =>\n aiJobEvents(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n // The jobs beat (AGL-3026): every step the doors leave queued, run every\n // minute by the console's scheduler on the cron secret, on the one surface\n // that holds the provider's key.\n registerPluginApiRoute(AI_JOBS_BEAT_PATH, { web: runAiJobsBeat })\n // The weekly insights (AGL-2915): Monday's digests made for the people who\n // asked for them, and delivered once the beat has written them. Beside the\n // beat under `/api/admin/`, where the console's scheduled sweeps live.\n registerPluginApiRoute(AI_INSIGHTS_DIGEST_PATH, { web: runAiInsightsDigest })\n // A site SEO audit's \"Apply all\" (AGL-2910): content fixes as new\n // unpublished versions, listing values staged for their SEO cards.\n registerPluginApiRoute('ai/seo/apply', { web: applyAiSeoAudit })\n // Save the selection as a reusable component, with AI (AGL-2908): the\n // second entry point of the component job, from the besigner's Attributes\n // panel. It proposes, and the person's Apply writes.\n registerPluginApiRoute('ai/generate/component', { web: aiGenerateComponent })\n registerPluginApiRoute('ai/billing/credits', { web: billingCredits })\n registerPluginApiRoute('ai/billing/overage', { web: billingOverage })\n registerPluginApiRoute('ai/usage', { web: aiUsage })\n // The allotments a manager sets, and the options the model switch lists\n // (AGL-2942). One handler answers the allotments' GET and POST.\n registerPluginApiRoute('ai/allotments', { web: aiAllotments })\n registerPluginApiRoute('ai/models', { web: aiModels })\n // The staff doors (AGL-2928, AGL-2930, AGL-2252): one org's AI in full,\n // one account's usage across its workspaces, and the fleet's Assist\n // signals behind the Assist signal staff page.\n registerPluginApiRoute('ai/admin/org', { web: aiAdminOrg })\n registerPluginApiRoute('ai/admin/orgs-spend', { web: aiAdminOrgsSpend })\n registerPluginApiRoute('ai/admin/user', { web: aiAdminUser })\n registerPluginApiRoute('ai/admin/signals', { web: aiAdminSignals })\n // One workspace's overage standing and the four acts staff have over it\n // (AGL-3011): the ceiling override, lifting a pause — the only way a\n // dispute pause comes off — and resetting the step. Every act audited.\n registerPluginApiRoute('ai/admin/overage', { web: aiAdminOverage })\n // Credits given back to a workspace's band or a Free owner's allowance\n // (AGL-3595): compensation for a fault of ours, bounded by what the month\n // used, keyed against a double-click, and audited.\n registerPluginApiRoute('ai/admin/credits', { web: aiAdminCredits })\n}\n\n// Type-only (AGL-3080): the plugin's entitlement keys, declared by module\n// augmentation, for every program that loads this entry point.\nexport type { aiPlanEntitlements } from './plan-entitlements'\n"],"names":["registerPluginApiRoute","registerAiDeclarations","AI_JOBS_BEAT_PATH","registerAiJobPlan","registerAiJobsPause","registerAiJobsNotify","registerAiComponentJob","registerAiCrmJob","registerAiLayoutJob","registerAiTemplateJob","registerAiExperimentJob","registerAiFormJob","registerAiPageJob","registerAiProductsJob","registerAiEmailJob","registerAiCampaignJob","registerAiSiteJob","registerAiWorkflowJob","registerAiInsightJob","registerAiFigureReaders","registerAiTextGenerator","firebaseAdmin","ensureFirstPartyAiProviders","aiAssistHandler","GET","aiCrmAnswer","POST","runAiJobsBeat","cancelAiJob","aiJobEvents","createAiSiteBatch","listAiJobs","createAiJob","resumeAiJob","aiGenerateComponent","applyAiSeoAudit","assistChat","assistEditApplied","assistFeedback","PATCH","aiHostPermissions","aiAdminOrg","aiAdminOrgsSpend","aiAdminSignals","aiAdminUser","aiAdminOverage","aiAdminCredits","aiUsage","aiAllotments","aiInsightAnswer","runAiInsightsDigest","AI_INSIGHTS_DIGEST_PATH","aiModels","billingCredits","billingOverage","registerFirstPartyProviders","registerAiJobKinds","app","firestore","registerAiConsoleApi","web","request","method","context","params","Promise","resolve","jobId","String"],"mappings":"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"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type AssistCreditMeter } from './assist-credit-returns';
18
+ /** Whether `key` may name a give-back (it becomes a Firestore map key). */
19
+ export declare function isAssistCreditReturnKey(key: unknown): key is string;
20
+ /** Where one meter's month lives. */
21
+ export interface AssistCreditMeterTarget {
22
+ meter: AssistCreditMeter;
23
+ ref: FirebaseFirestore.DocumentReference;
24
+ }
25
+ /** The month document of the workspace's band. */
26
+ export declare function workspaceCreditMeter(firestore: FirebaseFirestore.Firestore, orgId: string, month: string): AssistCreditMeterTarget;
27
+ /** The month document of a Free owner's account allowance. */
28
+ export declare function accountCreditMeter(firestore: FirebaseFirestore.Firestore, accountUid: string, month: string): AssistCreditMeterTarget;
29
+ export interface AssistCreditReturnRequest {
30
+ month: string;
31
+ meters: AssistCreditMeterTarget[];
32
+ /**
33
+ * Credits to return to EACH meter, or `'all'` to return everything each
34
+ * meter reads as used this month (a reset — each meter its own figure).
35
+ */
36
+ credits: number | 'all';
37
+ key: string;
38
+ reason: string;
39
+ /** Who gave it back — a staff uid, or `system:<source>`. */
40
+ actorUid: string;
41
+ /** What asked: `staff`, or an automatic path's own name. */
42
+ source: string;
43
+ jobId?: string | null;
44
+ }
45
+ /** One meter's line in the outcome. */
46
+ export interface AssistCreditReturnLine {
47
+ meter: AssistCreditMeter;
48
+ path: string;
49
+ /** Credits the meter read as used before this give-back. */
50
+ usedBefore: number;
51
+ /** Credits this give-back returned to it (0 on a refusal or duplicate). */
52
+ credits: number;
53
+ }
54
+ export type AssistCreditReturnOutcome = {
55
+ status: 'returned';
56
+ lines: AssistCreditReturnLine[];
57
+ } | {
58
+ status: 'duplicate';
59
+ lines: AssistCreditReturnLine[];
60
+ } | {
61
+ status: 'over';
62
+ lines: AssistCreditReturnLine[];
63
+ } | {
64
+ status: 'nothing';
65
+ lines: AssistCreditReturnLine[];
66
+ };
67
+ /**
68
+ * Return credits to one or more meters, atomically.
69
+ *
70
+ * `inTransaction` runs inside the same transaction after the writes are
71
+ * queued and only when something is returned — the staff route writes its
72
+ * audit row there, so the row and the give-back commit together or not at
73
+ * all.
74
+ */
75
+ export declare function returnAssistCredits(firestore: FirebaseFirestore.Firestore, request: AssistCreditReturnRequest, inTransaction?: (tx: FirebaseFirestore.Transaction, lines: AssistCreditReturnLine[]) => void): Promise<AssistCreditReturnOutcome>;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { FieldValue } from "firebase-admin/firestore";
17
+ import { assistCreditsFromUsd, assistUsdFromCredits } from "./assist-credits.js";
18
+ import { ASSIST_CREDIT_RETURNS_FIELD, ASSIST_RETURNED_USD_FIELD, assistSpendAfterReturnsUsd } from "./assist-credit-returns.js";
19
+ import { freeAccountUsageRef } from "./assist-free-taste.js";
20
+ /**
21
+ * GIVING CREDITS BACK (AGL-3595): the one writer of `returnedUsd`.
22
+ *
23
+ * Staff compensate a customer for a fault of ours through
24
+ * `/api/ai/admin/credits`; anything else that hands credits back for a
25
+ * failed turn (a refused plan, say) calls this same function with its own
26
+ * `source`, so every give-back lands the same way and is bounded the same
27
+ * way. See `assist-credit-returns.ts` for what readers do with the field.
28
+ *
29
+ * ## Bounded, idempotent, and history-preserving
30
+ *
31
+ * - Read and written in ONE transaction, so two give-backs racing cannot
32
+ * each read "227 used" and together return 454.
33
+ * - A give-back can return at most what the meter reads as used after
34
+ * earlier give-backs — never more. Refused as `over` naming what each
35
+ * meter could take.
36
+ * - Keyed: the act is recorded under `creditReturns.{key}` on every meter it
37
+ * touched, and a second call with a key any of them already holds writes
38
+ * nothing and answers `duplicate`. A double-click returns once.
39
+ * - Nothing is deleted or overwritten: `estCostUsd` stays the month's spend,
40
+ * `returnedUsd` only grows, and the map keeps each act.
41
+ */ /** The shape of a key: a client-made UUID, or a caller's own stable id. */ const KEY_PATTERN = /^[A-Za-z0-9_-]{8,100}$/;
42
+ /** Whether `key` may name a give-back (it becomes a Firestore map key). */ export function isAssistCreditReturnKey(key) {
43
+ return typeof key === 'string' && KEY_PATTERN.test(key);
44
+ }
45
+ /** The month document of the workspace's band. */ export function workspaceCreditMeter(firestore, orgId, month) {
46
+ return {
47
+ meter: 'workspace',
48
+ ref: firestore.collection('orgs').doc(orgId).collection('assistUsage').doc(month)
49
+ };
50
+ }
51
+ /** The month document of a Free owner's account allowance. */ export function accountCreditMeter(firestore, accountUid, month) {
52
+ return {
53
+ meter: 'account',
54
+ ref: freeAccountUsageRef(firestore, accountUid, month)
55
+ };
56
+ }
57
+ /**
58
+ * Return credits to one or more meters, atomically.
59
+ *
60
+ * `inTransaction` runs inside the same transaction after the writes are
61
+ * queued and only when something is returned — the staff route writes its
62
+ * audit row there, so the row and the give-back commit together or not at
63
+ * all.
64
+ */ export async function returnAssistCredits(firestore, request, inTransaction) {
65
+ if (!isAssistCreditReturnKey(request.key)) {
66
+ throw new Error('A give-back needs an idempotency key');
67
+ }
68
+ if (!request.meters.length) throw new Error('A give-back needs a meter');
69
+ const all = request.credits === 'all';
70
+ const asked = all ? 0 : Math.floor(Number(request.credits));
71
+ if (!all && !(Number.isInteger(Number(request.credits)) && asked > 0)) {
72
+ throw new Error('A give-back is a positive whole number of credits');
73
+ }
74
+ return firestore.runTransaction(async (tx)=>{
75
+ const snapshots = await Promise.all(request.meters.map(({ ref })=>tx.get(ref)));
76
+ const measured = request.meters.map((target, index)=>{
77
+ var _ref;
78
+ const snapshot = snapshots[index];
79
+ const spentUsd = assistSpendAfterReturnsUsd(snapshot == null ? void 0 : snapshot.get('estCostUsd'), snapshot == null ? void 0 : snapshot.get(ASSIST_RETURNED_USD_FIELD));
80
+ const returns = (_ref = snapshot == null ? void 0 : snapshot.get(ASSIST_CREDIT_RETURNS_FIELD)) != null ? _ref : {};
81
+ return {
82
+ target,
83
+ spentUsd,
84
+ usedBefore: assistCreditsFromUsd(spentUsd),
85
+ seen: Boolean(returns && typeof returns === 'object' && request.key in returns)
86
+ };
87
+ });
88
+ const lines = (credits)=>measured.map((entry)=>({
89
+ meter: entry.target.meter,
90
+ path: entry.target.ref.path,
91
+ usedBefore: entry.usedBefore,
92
+ credits: credits(entry.usedBefore)
93
+ }));
94
+ if (measured.some((entry)=>entry.seen)) {
95
+ return {
96
+ status: 'duplicate',
97
+ lines: lines(()=>0)
98
+ };
99
+ }
100
+ if (!all && measured.some((entry)=>asked > entry.usedBefore)) {
101
+ return {
102
+ status: 'over',
103
+ lines: lines(()=>0)
104
+ };
105
+ }
106
+ const returned = lines((usedBefore)=>all ? usedBefore : asked);
107
+ if (returned.every((line)=>line.credits === 0)) {
108
+ return {
109
+ status: 'nothing',
110
+ lines: returned
111
+ };
112
+ }
113
+ const at = FieldValue.serverTimestamp();
114
+ measured.forEach((entry, index)=>{
115
+ var _ref, _request_jobId;
116
+ var _returned_index;
117
+ const credits = (_ref = (_returned_index = returned[index]) == null ? void 0 : _returned_index.credits) != null ? _ref : 0;
118
+ if (credits <= 0) return;
119
+ // Never more dollars than the meter reads as spent: the whole of a
120
+ // meter is `ceil` credits, and returning the rounded-up figure in
121
+ // full would bank the fraction against next month's first turn.
122
+ const usd = Math.min(assistUsdFromCredits(credits), entry.spentUsd);
123
+ tx.set(entry.target.ref, {
124
+ month: request.month,
125
+ [ASSIST_RETURNED_USD_FIELD]: FieldValue.increment(usd),
126
+ [ASSIST_CREDIT_RETURNS_FIELD]: {
127
+ [request.key]: {
128
+ credits,
129
+ usd,
130
+ reason: request.reason,
131
+ actorUid: request.actorUid,
132
+ source: request.source,
133
+ jobId: (_request_jobId = request.jobId) != null ? _request_jobId : null,
134
+ at
135
+ }
136
+ },
137
+ updatedAt: at
138
+ }, {
139
+ merge: true
140
+ });
141
+ });
142
+ inTransaction == null ? void 0 : inTransaction(tx, returned);
143
+ return {
144
+ status: 'returned',
145
+ lines: returned
146
+ };
147
+ });
148
+ }
149
+
150
+ //# sourceMappingURL=assist-credit-returns-write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/usage/assist-credit-returns-write.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { assistCreditsFromUsd, assistUsdFromCredits } from './assist-credits'\nimport {\n ASSIST_CREDIT_RETURNS_FIELD,\n ASSIST_RETURNED_USD_FIELD,\n assistSpendAfterReturnsUsd,\n type AssistCreditMeter,\n} from './assist-credit-returns'\nimport { freeAccountUsageRef } from './assist-free-taste'\n\n/**\n * GIVING CREDITS BACK (AGL-3595): the one writer of `returnedUsd`.\n *\n * Staff compensate a customer for a fault of ours through\n * `/api/ai/admin/credits`; anything else that hands credits back for a\n * failed turn (a refused plan, say) calls this same function with its own\n * `source`, so every give-back lands the same way and is bounded the same\n * way. See `assist-credit-returns.ts` for what readers do with the field.\n *\n * ## Bounded, idempotent, and history-preserving\n *\n * - Read and written in ONE transaction, so two give-backs racing cannot\n * each read \"227 used\" and together return 454.\n * - A give-back can return at most what the meter reads as used after\n * earlier give-backs — never more. Refused as `over` naming what each\n * meter could take.\n * - Keyed: the act is recorded under `creditReturns.{key}` on every meter it\n * touched, and a second call with a key any of them already holds writes\n * nothing and answers `duplicate`. A double-click returns once.\n * - Nothing is deleted or overwritten: `estCostUsd` stays the month's spend,\n * `returnedUsd` only grows, and the map keeps each act.\n */\n\n/** The shape of a key: a client-made UUID, or a caller's own stable id. */\nconst KEY_PATTERN = /^[A-Za-z0-9_-]{8,100}$/\n\n/** Whether `key` may name a give-back (it becomes a Firestore map key). */\nexport function isAssistCreditReturnKey(key: unknown): key is string {\n return typeof key === 'string' && KEY_PATTERN.test(key)\n}\n\n/** Where one meter's month lives. */\nexport interface AssistCreditMeterTarget {\n meter: AssistCreditMeter\n ref: FirebaseFirestore.DocumentReference\n}\n\n/** The month document of the workspace's band. */\nexport function workspaceCreditMeter(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n month: string,\n): AssistCreditMeterTarget {\n return {\n meter: 'workspace',\n ref: firestore.collection('orgs').doc(orgId).collection('assistUsage').doc(month),\n }\n}\n\n/** The month document of a Free owner's account allowance. */\nexport function accountCreditMeter(\n firestore: FirebaseFirestore.Firestore,\n accountUid: string,\n month: string,\n): AssistCreditMeterTarget {\n return { meter: 'account', ref: freeAccountUsageRef(firestore, accountUid, month) }\n}\n\nexport interface AssistCreditReturnRequest {\n month: string\n meters: AssistCreditMeterTarget[]\n /**\n * Credits to return to EACH meter, or `'all'` to return everything each\n * meter reads as used this month (a reset — each meter its own figure).\n */\n credits: number | 'all'\n key: string\n reason: string\n /** Who gave it back — a staff uid, or `system:<source>`. */\n actorUid: string\n /** What asked: `staff`, or an automatic path's own name. */\n source: string\n jobId?: string | null\n}\n\n/** One meter's line in the outcome. */\nexport interface AssistCreditReturnLine {\n meter: AssistCreditMeter\n path: string\n /** Credits the meter read as used before this give-back. */\n usedBefore: number\n /** Credits this give-back returned to it (0 on a refusal or duplicate). */\n credits: number\n}\n\nexport type AssistCreditReturnOutcome =\n | { status: 'returned'; lines: AssistCreditReturnLine[] }\n | { status: 'duplicate'; lines: AssistCreditReturnLine[] }\n | { status: 'over'; lines: AssistCreditReturnLine[] }\n | { status: 'nothing'; lines: AssistCreditReturnLine[] }\n\n/**\n * Return credits to one or more meters, atomically.\n *\n * `inTransaction` runs inside the same transaction after the writes are\n * queued and only when something is returned — the staff route writes its\n * audit row there, so the row and the give-back commit together or not at\n * all.\n */\nexport async function returnAssistCredits(\n firestore: FirebaseFirestore.Firestore,\n request: AssistCreditReturnRequest,\n inTransaction?: (\n tx: FirebaseFirestore.Transaction,\n lines: AssistCreditReturnLine[],\n ) => void,\n): Promise<AssistCreditReturnOutcome> {\n if (!isAssistCreditReturnKey(request.key)) {\n throw new Error('A give-back needs an idempotency key')\n }\n if (!request.meters.length) throw new Error('A give-back needs a meter')\n const all = request.credits === 'all'\n const asked = all ? 0 : Math.floor(Number(request.credits))\n if (!all && !(Number.isInteger(Number(request.credits)) && asked > 0)) {\n throw new Error('A give-back is a positive whole number of credits')\n }\n\n return firestore.runTransaction(async (tx) => {\n const snapshots = await Promise.all(request.meters.map(({ ref }) => tx.get(ref)))\n const measured = request.meters.map((target, index) => {\n const snapshot = snapshots[index]\n const spentUsd = assistSpendAfterReturnsUsd(\n snapshot?.get('estCostUsd'),\n snapshot?.get(ASSIST_RETURNED_USD_FIELD),\n )\n const returns = (snapshot?.get(ASSIST_CREDIT_RETURNS_FIELD) ?? {}) as Record<\n string,\n unknown\n >\n return {\n target,\n spentUsd,\n usedBefore: assistCreditsFromUsd(spentUsd),\n seen: Boolean(returns && typeof returns === 'object' && request.key in returns),\n }\n })\n const lines = (credits: (usedBefore: number) => number): AssistCreditReturnLine[] =>\n measured.map((entry) => ({\n meter: entry.target.meter,\n path: entry.target.ref.path,\n usedBefore: entry.usedBefore,\n credits: credits(entry.usedBefore),\n }))\n\n if (measured.some((entry) => entry.seen)) {\n return { status: 'duplicate', lines: lines(() => 0) }\n }\n if (!all && measured.some((entry) => asked > entry.usedBefore)) {\n return { status: 'over', lines: lines(() => 0) }\n }\n const returned = lines((usedBefore) => (all ? usedBefore : asked))\n if (returned.every((line) => line.credits === 0)) {\n return { status: 'nothing', lines: returned }\n }\n\n const at = FieldValue.serverTimestamp()\n measured.forEach((entry, index) => {\n const credits = returned[index]?.credits ?? 0\n if (credits <= 0) return\n // Never more dollars than the meter reads as spent: the whole of a\n // meter is `ceil` credits, and returning the rounded-up figure in\n // full would bank the fraction against next month's first turn.\n const usd = Math.min(assistUsdFromCredits(credits), entry.spentUsd)\n tx.set(\n entry.target.ref,\n {\n month: request.month,\n [ASSIST_RETURNED_USD_FIELD]: FieldValue.increment(usd),\n [ASSIST_CREDIT_RETURNS_FIELD]: {\n [request.key]: {\n credits,\n usd,\n reason: request.reason,\n actorUid: request.actorUid,\n source: request.source,\n jobId: request.jobId ?? null,\n at,\n },\n },\n updatedAt: at,\n },\n { merge: true },\n )\n })\n inTransaction?.(tx, returned)\n return { status: 'returned', lines: returned }\n })\n}\n"],"names":["FieldValue","assistCreditsFromUsd","assistUsdFromCredits","ASSIST_CREDIT_RETURNS_FIELD","ASSIST_RETURNED_USD_FIELD","assistSpendAfterReturnsUsd","freeAccountUsageRef","KEY_PATTERN","isAssistCreditReturnKey","key","test","workspaceCreditMeter","firestore","orgId","month","meter","ref","collection","doc","accountCreditMeter","accountUid","returnAssistCredits","request","inTransaction","Error","meters","length","all","credits","asked","Math","floor","Number","isInteger","runTransaction","tx","snapshots","Promise","map","get","measured","target","index","snapshot","spentUsd","returns","usedBefore","seen","Boolean","lines","entry","path","some","status","returned","every","line","at","serverTimestamp","forEach","usd","min","set","increment","reason","actorUid","source","jobId","updatedAt","merge"],"mappings":"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"}