@aglyn/plugins-ai 1.0.0-beta.219 → 1.0.0-beta.221

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 (181) hide show
  1. package/package.json +18 -18
  2. package/src/lib/billing/ai-month-meter.d.ts +51 -0
  3. package/src/lib/billing/ai-month-meter.js +89 -0
  4. package/src/lib/billing/ai-month-meter.js.map +1 -0
  5. package/src/lib/billing/ai-overage-close.d.ts +11 -33
  6. package/src/lib/billing/ai-overage-close.js +4 -3
  7. package/src/lib/billing/ai-overage-close.js.map +1 -1
  8. package/src/lib/billing/ai-overage-cutover.d.ts +28 -12
  9. package/src/lib/billing/ai-overage-cutover.js +1 -28
  10. package/src/lib/billing/ai-overage-cutover.js.map +1 -1
  11. package/src/lib/billing/ai-overage-ledger.js +1 -1
  12. package/src/lib/billing/ai-overage-ledger.js.map +1 -1
  13. package/src/lib/billing/ai-overage-standing.js +1 -1
  14. package/src/lib/billing/ai-overage-standing.js.map +1 -1
  15. package/src/lib/components/ai-allotment-editor.component.js +1 -1
  16. package/src/lib/components/ai-allotment-editor.component.js.map +1 -1
  17. package/src/lib/components/ai-assist-provider.component.js +3 -2
  18. package/src/lib/components/ai-assist-provider.component.js.map +1 -1
  19. package/src/lib/components/ai-automation-zones.d.ts +63 -0
  20. package/src/lib/components/ai-automation-zones.js +31 -0
  21. package/src/lib/components/ai-automation-zones.js.map +1 -0
  22. package/src/lib/components/ai-describe-automation.component.d.ts +1 -1
  23. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  24. package/src/lib/components/ai-describe-button.component.d.ts +6 -3
  25. package/src/lib/components/ai-describe-button.component.js +4 -1
  26. package/src/lib/components/ai-describe-button.component.js.map +1 -1
  27. package/src/lib/components/ai-explain-automation.component.d.ts +1 -1
  28. package/src/lib/components/ai-explain-automation.component.js.map +1 -1
  29. package/src/lib/components/billing-ai-allotments.component.js +1 -1
  30. package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
  31. package/src/lib/components/billing-assist-overage-card.component.js +1 -1
  32. package/src/lib/components/billing-assist-overage-card.component.js.map +1 -1
  33. package/src/lib/constants.d.ts +5 -0
  34. package/src/lib/constants.js +4 -0
  35. package/src/lib/constants.js.map +1 -1
  36. package/src/lib/declarations.js +2 -0
  37. package/src/lib/declarations.js.map +1 -1
  38. package/src/lib/declarations.server.js +24 -10
  39. package/src/lib/declarations.server.js.map +1 -1
  40. package/src/lib/insights/ai-dataset-catalog.d.ts +49 -0
  41. package/src/lib/insights/ai-dataset-catalog.js +57 -0
  42. package/src/lib/insights/ai-dataset-catalog.js.map +1 -0
  43. package/src/lib/insights/ai-figure-readers.d.ts +6 -39
  44. package/src/lib/insights/ai-figure-readers.js +2 -391
  45. package/src/lib/insights/ai-figure-readers.js.map +1 -1
  46. package/src/lib/insights/ai-insight-digest.d.ts +10 -1
  47. package/src/lib/insights/ai-insight-digest.js +16 -4
  48. package/src/lib/insights/ai-insight-digest.js.map +1 -1
  49. package/src/lib/insights/ai-insight-readers.d.ts +1 -1
  50. package/src/lib/insights/ai-insight-readers.js.map +1 -1
  51. package/src/lib/jobs/ai-job-component-step.d.ts +6 -1
  52. package/src/lib/jobs/ai-job-component-step.js +23 -6
  53. package/src/lib/jobs/ai-job-component-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-draft-ids.js +11 -0
  55. package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
  56. package/src/lib/jobs/ai-job-email-step.d.ts +5 -1
  57. package/src/lib/jobs/ai-job-email-step.js +10 -1
  58. package/src/lib/jobs/ai-job-email-step.js.map +1 -1
  59. package/src/lib/jobs/ai-job-form-step.d.ts +17 -1
  60. package/src/lib/jobs/ai-job-form-step.js +60 -8
  61. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  62. package/src/lib/jobs/ai-job-generation.d.ts +21 -0
  63. package/src/lib/jobs/ai-job-generation.js +53 -1
  64. package/src/lib/jobs/ai-job-generation.js.map +1 -1
  65. package/src/lib/jobs/ai-job-insight-step.js +1 -1
  66. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  67. package/src/lib/jobs/ai-job-layout-step.d.ts +15 -4
  68. package/src/lib/jobs/ai-job-layout-step.js +45 -12
  69. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  70. package/src/lib/jobs/ai-job-plan-conformance.d.ts +5 -0
  71. package/src/lib/jobs/ai-job-plan-conformance.js +7 -1
  72. package/src/lib/jobs/ai-job-plan-conformance.js.map +1 -1
  73. package/src/lib/jobs/ai-job-plan-creations.d.ts +80 -0
  74. package/src/lib/jobs/ai-job-plan-creations.js +199 -0
  75. package/src/lib/jobs/ai-job-plan-creations.js.map +1 -0
  76. package/src/lib/jobs/ai-job-plan-step.d.ts +8 -2
  77. package/src/lib/jobs/ai-job-plan-step.js +20 -6
  78. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  79. package/src/lib/jobs/ai-job-site-step.d.ts +2 -0
  80. package/src/lib/jobs/ai-job-site-step.js +6 -6
  81. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  82. package/src/lib/jobs/ai-job-template-step.d.ts +9 -6
  83. package/src/lib/jobs/ai-job-template-step.js +72 -15
  84. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  85. package/src/lib/jobs/ai-jobs.js +1 -1
  86. package/src/lib/jobs/ai-jobs.js.map +1 -1
  87. package/src/lib/jobs/ai-workflow-records.d.ts +5 -4
  88. package/src/lib/jobs/ai-workflow-records.js +26 -22
  89. package/src/lib/jobs/ai-workflow-records.js.map +1 -1
  90. package/src/lib/model/ai-allotments.d.ts +16 -3
  91. package/src/lib/model/ai-allotments.js +34 -11
  92. package/src/lib/model/ai-allotments.js.map +1 -1
  93. package/src/lib/model/ai-job-creations.d.ts +55 -0
  94. package/src/lib/model/ai-job-creations.js +85 -0
  95. package/src/lib/model/ai-job-creations.js.map +1 -0
  96. package/src/lib/model/ai-page-job.d.ts +3 -2
  97. package/src/lib/model/ai-page-job.js +4 -3
  98. package/src/lib/model/ai-page-job.js.map +1 -1
  99. package/src/lib/model/ai-site-job.d.ts +4 -2
  100. package/src/lib/model/ai-site-job.js +8 -4
  101. package/src/lib/model/ai-site-job.js.map +1 -1
  102. package/src/lib/model/ai-template-subjects.d.ts +10 -0
  103. package/src/lib/model/ai-template-subjects.js +14 -0
  104. package/src/lib/model/ai-template-subjects.js.map +1 -1
  105. package/src/lib/model/ai-tokens.js +1 -1
  106. package/src/lib/model/ai-tokens.js.map +1 -1
  107. package/src/lib/model/ai-usage-by-user.js +1 -1
  108. package/src/lib/model/ai-usage-by-user.js.map +1 -1
  109. package/src/lib/providers/catalog.js +1 -1
  110. package/src/lib/providers/catalog.js.map +1 -1
  111. package/src/lib/providers/model-choice.js +1 -1
  112. package/src/lib/providers/model-choice.js.map +1 -1
  113. package/src/lib/runtime/ai-device-audit.d.ts +1 -1
  114. package/src/lib/runtime/ai-device-audit.js +1 -1
  115. package/src/lib/runtime/ai-device-audit.js.map +1 -1
  116. package/src/lib/runtime/ai-eval-live.js +1 -1
  117. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  118. package/src/lib/runtime/ai-eval.d.ts +1 -1
  119. package/src/lib/runtime/ai-eval.js.map +1 -1
  120. package/src/lib/runtime/ai-gate.js +1 -1
  121. package/src/lib/runtime/ai-gate.js.map +1 -1
  122. package/src/lib/runtime/ai-palette.d.ts +1 -1
  123. package/src/lib/runtime/ai-palette.generated.d.ts +1 -1
  124. package/src/lib/runtime/ai-palette.generated.js +1 -1
  125. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  126. package/src/lib/runtime/ai-palette.js.map +1 -1
  127. package/src/lib/runtime/site-inventory.js +28 -22
  128. package/src/lib/runtime/site-inventory.js.map +1 -1
  129. package/src/lib/server/ai-admin-org.js +1 -1
  130. package/src/lib/server/ai-admin-org.js.map +1 -1
  131. package/src/lib/server/ai-admin-orgs-spend.js +1 -1
  132. package/src/lib/server/ai-admin-orgs-spend.js.map +1 -1
  133. package/src/lib/server/ai-admin-signals.js +1 -1
  134. package/src/lib/server/ai-admin-signals.js.map +1 -1
  135. package/src/lib/server/ai-allotments.js +1 -1
  136. package/src/lib/server/ai-allotments.js.map +1 -1
  137. package/src/lib/server/ai-assist.js +2 -2
  138. package/src/lib/server/ai-assist.js.map +1 -1
  139. package/src/lib/server/assist-chat.d.ts +1 -1
  140. package/src/lib/server/assist-chat.js +2 -2
  141. package/src/lib/server/assist-chat.js.map +1 -1
  142. package/src/lib/server/billing-credits.d.ts +5 -0
  143. package/src/lib/server/billing-credits.js +8 -2
  144. package/src/lib/server/billing-credits.js.map +1 -1
  145. package/src/lib/server/billing-overage.js +2 -2
  146. package/src/lib/server/billing-overage.js.map +1 -1
  147. package/src/lib/testing/stand-in-dataset-index.d.ts +20 -0
  148. package/src/lib/testing/stand-in-dataset-index.js +80 -0
  149. package/src/lib/testing/stand-in-dataset-index.js.map +1 -0
  150. package/src/lib/usage/ai-allotment-alerts.d.ts +10 -7
  151. package/src/lib/usage/ai-allotment-alerts.js +31 -14
  152. package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
  153. package/src/lib/usage/ai-quota-checks.d.ts +37 -0
  154. package/src/lib/usage/ai-quota-checks.js +69 -0
  155. package/src/lib/usage/ai-quota-checks.js.map +1 -0
  156. package/src/lib/usage/ai-usage-alerts.js +22 -10
  157. package/src/lib/usage/ai-usage-alerts.js.map +1 -1
  158. package/src/lib/usage/ai-usage-by-user.js +1 -1
  159. package/src/lib/usage/ai-usage-by-user.js.map +1 -1
  160. package/src/lib/usage/assist-ceiling.d.ts +2 -2
  161. package/src/lib/usage/assist-ceiling.js +4 -4
  162. package/src/lib/usage/assist-ceiling.js.map +1 -1
  163. package/src/lib/usage/assist-credits.d.ts +494 -0
  164. package/src/lib/usage/assist-credits.js +524 -0
  165. package/src/lib/usage/assist-credits.js.map +1 -0
  166. package/src/lib/usage/assist-free-taste.d.ts +1 -1
  167. package/src/lib/usage/assist-free-taste.js +1 -1
  168. package/src/lib/usage/assist-free-taste.js.map +1 -1
  169. package/src/lib/usage/assist-signal-mining.js +1 -1
  170. package/src/lib/usage/assist-signal-mining.js.map +1 -1
  171. package/src/lib/usage/assist-spend-guards.d.ts +153 -0
  172. package/src/lib/usage/assist-spend-guards.js +182 -0
  173. package/src/lib/usage/assist-spend-guards.js.map +1 -0
  174. package/src/lib/usage/assist-usage.d.ts +1 -1
  175. package/src/lib/usage/assist-usage.js +1 -1
  176. package/src/lib/usage/assist-usage.js.map +1 -1
  177. package/src/lib/usage/staff-org-ai.js +1 -1
  178. package/src/lib/usage/staff-org-ai.js.map +1 -1
  179. package/src/lib/usage-axes.d.ts +10 -0
  180. package/src/lib/usage-axes.js +40 -2
  181. package/src/lib/usage-axes.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-admin-signals.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 {\n ORG_BILLING_DOC_ID,\n ORG_BILLING_SUBCOLLECTION,\n pluginRequestFromWeb,\n} from '@aglyn/aglyn/server'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin'\nimport {\n ASSIST_PROVIDER_COST_FIELD,\n assistCreditsFromUsd,\n assistProviderCostUsd,\n} from '@aglyn/aglyn/app-utils/assist-credits'\nimport {\n hasAiAddon,\n resolveEffectivePlan,\n} from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { readPlatformFreeSpend } from '../usage/assist-free-taste'\nimport { assistRefusalCounts } from '../usage/assist-refusals'\nimport { assistUsageDay, assistUsageMonth } from '../usage/assist-usage'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport {\n assistSignalRow,\n mineAssistSignals,\n rankAssistSpend,\n type AssistSpendRow,\n} from '../usage/assist-signal-mining'\n\n/**\n * The Assist docs-gap and cost mining view (AGL-1860, AGL-2252), served at\n * `/api/ai/admin/signals` for the plugin's Assist signal staff page.\n * Staff-gated, read-only, and the only reader `assistSignals` has.\n *\n * ## Why a route and not a client query\n *\n * `assistSignals` is absent from `firebase-firestore.rules` deliberately: the\n * org block matches its subcollections BY NAME and carries no wildcard, so an\n * unlisted name is default-deny for every client. That is the property that\n * lets the collection hold cross-tenant analytics at all. Reading it therefore\n * goes through the Admin SDK behind the staff claim, exactly as every staff\n * route does.\n *\n * ## The collection group, and the index it does not need\n *\n * One `collectionGroup('assistSignals')` read gathers every org's signals in a\n * single query; the owning org is `ref.parent.parent.id`, since the document\n * carries no `orgId` field (it carries no identifiers at all — see AGL-1972).\n * The query has no `where` and no `orderBy`, so it needs no composite index,\n * and adding one later would mean declaring a `COLLECTION_GROUP`-scoped\n * override and deploying it by hand. `/api/admin/overview` reads org billing\n * the same way and for the same reason.\n *\n * ## Truncation is reported, never hidden\n *\n * The read is bounded — an unbounded fleet-wide get is how a staff page\n * becomes an outage — but a ranking cut short looks exactly like a complete\n * one, so the ceiling is fetched PLUS ONE and the extra row sets `truncated`.\n * AGL-2220 is the standing example of the alternative: a sweep that stopped at\n * 500 orgs and reported nothing about the rest, for as long as nobody counted.\n */\n\n/**\n * Rows read in one pass. Each signal is a handful of scalars, so this is a\n * few megabytes at the ceiling — large enough to be worth ranking, small\n * enough that the honest answer to \"is it all of them\" is usually yes.\n */\nconst SCAN_CEILING = 20000\n\n/**\n * How much of an answer the prose panel shows.\n *\n * Enough to judge whether the answer was wrong, short enough that twenty-five\n * of them are still a page somebody reads.\n */\nconst ANSWER_PREVIEW_CHARS = 600\n\n/**\n * Month documents read in one pass for the spend leaderboard (AGL-2930).\n *\n * `assistUsage` holds one document per org per month, so this is orgs times\n * months — a fraction of the signal count, and the same PLUS-ONE truncation\n * probe the signals read uses. Read as a collection group with NO `where`\n * and NO `orderBy`, for the reason the module note gives: a filter on\n * `month` would need a COLLECTION_GROUP-scoped index deployed by hand, and\n * filtering the month in memory costs nothing at this size.\n */\nconst SPEND_SCAN_CEILING = 20000\n\nexport interface AssistSpendLeaderboard {\n month: string\n rows: AssistSpendRow[]\n /** How many workspaces had a document for the month — the ranked count. */\n ranked: number\n /** True when the scan hit its ceiling, so the ranking is over a sample. */\n truncated: boolean\n /**\n * True when the month documents could not be read at all. Reported rather\n * than rendered as an empty table: \"no workspace spent anything\" and \"the\n * read failed\" are opposite findings, and the rest of the board — read\n * from a different collection — is still good.\n */\n failed: boolean\n}\n\n/**\n * The month's per-org spend, refusals and plan, dearest first.\n *\n * Plan and add-on come from the org and billing documents of the ranked\n * rows only — bounded by `limit`, never by the fleet — merged org-doc-first\n * exactly as every other staff reader merges them.\n */\nasync function readAssistSpendLeaderboard(\n firestore: FirebaseFirestore.Firestore,\n limit: number,\n month: string,\n): Promise<AssistSpendLeaderboard> {\n try {\n return await rankAssistSpendForMonth(firestore, limit, month)\n } catch (error) {\n console.error('[ai/admin/signals] spend leaderboard read failed', error)\n return { month, rows: [], ranked: 0, truncated: false, failed: true }\n }\n}\n\nasync function rankAssistSpendForMonth(\n firestore: FirebaseFirestore.Firestore,\n limit: number,\n month: string,\n): Promise<AssistSpendLeaderboard> {\n const snapshot = await firestore\n .collectionGroup('assistUsage')\n .limit(SPEND_SCAN_CEILING + 1)\n .get()\n const truncated = snapshot.size > SPEND_SCAN_CEILING\n const docs = truncated ? snapshot.docs.slice(0, SPEND_SCAN_CEILING) : snapshot.docs\n const unranked: AssistSpendRow[] = []\n for (const doc of docs) {\n // The id IS the month; the field is the same value and is what an\n // older document may lack.\n if (doc.id !== month && doc.get('month') !== month) continue\n const orgId = doc.ref.parent.parent?.id\n if (!orgId) continue\n // Two figures off one document (AGL-3015): the credits a workspace drew\n // come from what it was BILLED, and the dollars staff rank workspaces by\n // come from what it COST US. Ranking on the billed figure would sort the\n // fleet by our own markup wherever workspaces differ in model mix.\n const billed = Number(doc.get('estCostUsd') ?? 0)\n const billedUsd = Number.isFinite(billed) && billed > 0 ? billed : 0\n unranked.push({\n orgId,\n plan: 'free',\n aiAddon: false,\n credits: assistCreditsFromUsd(billedUsd),\n providerCostUsd: assistProviderCostUsd(\n billedUsd,\n doc.get(ASSIST_PROVIDER_COST_FIELD),\n ),\n refusals: assistRefusalCounts(doc.get('refusals')),\n })\n }\n const { rows, ranked } = rankAssistSpend(unranked, limit)\n if (rows.length) {\n const orgRefs = rows.map((row) => firestore.collection('orgs').doc(row.orgId))\n const [orgSnaps, billingSnaps] = await Promise.all([\n firestore.getAll(...orgRefs),\n firestore.getAll(\n ...orgRefs.map((ref) =>\n ref.collection(ORG_BILLING_SUBCOLLECTION).doc(ORG_BILLING_DOC_ID),\n ),\n ),\n ])\n rows.forEach((row, index) => {\n const orgData = orgSnaps[index]?.exists ? (orgSnaps[index].data() ?? {}) : {}\n const org = {\n ...orgData,\n ...(billingSnaps[index]?.exists ? billingSnaps[index].data() : {}),\n }\n row.plan = resolveEffectivePlan(org as never)\n row.aiAddon = hasAiAddon(org as never)\n const label = (orgData['name'] ?? orgData['slug'] ?? '') as string\n row.orgLabel = label || null\n })\n }\n return { month, rows, ranked, truncated, failed: false }\n}\n\nasync function handler(request: Request): Promise<Response> {\n const { method, query, headers: rawHeaders } = 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) return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n if (!decoded['staff']) {\n return Response.json({ error: 'Staff only' }, { status: 403 })\n }\n\n const requested = Number(query.limit ?? 25)\n const limit =\n Number.isFinite(requested) && requested > 0\n ? Math.min(Math.floor(requested), 100)\n : 25\n\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collectionGroup('assistSignals')\n // Ceiling PLUS ONE: the extra document is what makes truncation\n // detectable rather than assumed.\n .limit(SCAN_CEILING + 1)\n .get()\n\n const truncated = snapshot.size > SCAN_CEILING\n const docs = truncated ? snapshot.docs.slice(0, SCAN_CEILING) : snapshot.docs\n const rows = docs.map((doc) =>\n assistSignalRow(\n doc.ref.parent.parent?.id ?? '(unknown)',\n // The signal id IS the exchange id (AGL-2314) — one id, two halves,\n // minted together by `recordAssistExchange` precisely so the join is\n // possible. Until now nothing carried it this far.\n doc.id,\n doc.data(),\n ),\n )\n\n const report = mineAssistSignals(rows, { truncated, limit })\n\n // This month's spend, per workspace (AGL-2930) — the board's only\n // month-scoped panel, read off the month documents rather than mined\n // from the all-time signals above.\n const spend = await readAssistSpendLeaderboard(\n firebaseAdmin.app().firestore(),\n limit,\n assistUsageMonth(),\n )\n\n /*==========================================\n * THE VERBATIM HALF (AGL-2314).\n *\n * `assistExchanges` stores the question a customer typed and the answer\n * we gave, for 180 days, and `assist-usage.ts` calls it \"the data loop's\n * corpus\". NOTHING read it — one write, no reader, no collection group.\n * So we retained customers' words, and committed publicly in the privacy\n * policy to retaining them, for zero product value.\n *\n * Read here rather than in the miner because the miner is pure: it does\n * the arithmetic, the route does the reading.\n *\n * ## Only the turns that FAILED\n *\n * The shortlist is thumbs-down and ungrounded turns only, capped by the\n * same `limit` as every other panel. That is a privacy decision as much\n * as a cost one: the counts already answer \"how often\", and the words are\n * fetched only where the counts CANNOT say what went wrong. Pulling every\n * exchange would be surveillance with a dashboard on it.\n *\n * `uid` is deliberately not projected. The corpus question is what people\n * asked, never who asked it — and the exchange is the only document that\n * still carries an identifier at all.\n *\n * ## An expired exchange is an ANSWER\n *\n * The 180-day TTL means a signal can outlive its prose, and it is meant\n * to. A missing document therefore reports `expired: true` rather than\n * being dropped: a shortlist silently shorter than the failure count it\n * came from would read as \"these are all the failures\", which is the\n * AGL-2220 shape.\n *=========================================*/\n const firestore = firebaseAdmin.app().firestore()\n const exchanges = report.proseCandidates.length\n ? await firestore.getAll(\n ...report.proseCandidates.map((candidate) =>\n firestore\n .collection('orgs')\n .doc(candidate.orgId)\n .collection('assistExchanges')\n .doc(candidate.exchangeId),\n ),\n )\n : []\n const prose = report.proseCandidates.map((candidate, index) => {\n const snapshot = exchanges[index]\n const data = (snapshot?.exists ? snapshot.data() : null) ?? {}\n return {\n ...candidate,\n expired: !snapshot?.exists,\n question: typeof data['question'] === 'string' ? data['question'] : null,\n // Trimmed, not omitted: judging whether an answer was wrong needs to\n // see it, and a whole answer per row would make the panel unreadable.\n answer:\n typeof data['answer'] === 'string'\n ? data['answer'].slice(0, ANSWER_PREVIEW_CHARS)\n : null,\n answerTruncated:\n typeof data['answer'] === 'string' &&\n data['answer'].length > ANSWER_PREVIEW_CHARS,\n }\n })\n\n /*==========================================\n * THE WORKSPACE, BY NAME.\n *\n * A staff reader recognizes a customer by name, never by document id,\n * and the cost board's own column header says \"Workspace\" over a column\n * of generated ids. `/api/admin/overview` fixed the same shape by\n * building a label map from a snapshot it already held; there is no such\n * snapshot here — this route reads `assistSignals` and nothing else — so\n * the labels are fetched, with the same `name` → `slug` → id precedence\n * so one workspace cannot appear under two different labels on two\n * screens.\n *\n * Bounded by what is actually RENDERED, not by the fleet. Both lists the\n * ids come from are already sliced to `limit` (25 by default, 100 at the\n * ceiling), so this is one `getAll` over at most a couple of hundred\n * documents — and it reads the orgs the reader can see rather than\n * scanning a collection to name a handful of rows.\n *\n * An org that has been deleted keeps its id rather than going blank: an\n * unfamiliar id is still a lead a staff member can paste into the org\n * search, and an empty cell is not.\n *=========================================*/\n const orgIds = [\n ...new Set(\n [\n ...report.orgs.map((row) => row.orgId),\n ...report.proseCandidates.map((row) => row.orgId),\n ].filter((orgId) => orgId && orgId !== '(unknown)'),\n ),\n ]\n const orgLabelById = new Map<string, string>()\n if (orgIds.length) {\n const orgDocs = await firestore.getAll(\n ...orgIds.map((orgId) => firestore.collection('orgs').doc(orgId)),\n )\n for (const doc of orgDocs) {\n if (!doc.exists) continue\n const data = doc.data() ?? {}\n const label = (data['name'] ?? data['slug'] ?? '') as string\n if (label) orgLabelById.set(doc.id, label)\n }\n }\n const orgLabel = (orgId: string): string =>\n orgLabelById.get(orgId) ?? orgId\n\n // Today's free-tier spend against the platform ceiling (AGL-2925): one\n // document read, and the only place staff can see the taste switching\n // itself off before the email arrives. Best-effort — a readout that\n // cannot be read must not take the rest of the board down with it.\n const freeSpend = await readPlatformFreeSpend(\n firestore,\n assistUsageDay(),\n ).catch((error) => {\n console.error('[ai/admin/signals] free spend readout failed', error)\n return undefined\n })\n\n return Response.json(\n {\n ...report,\n freeSpend,\n orgs: report.orgs.map((row) => ({\n ...row,\n orgLabel: orgLabel(row.orgId),\n })),\n prose: prose.map((row) => ({ ...row, orgLabel: orgLabel(row.orgId) })),\n spend,\n ceiling: SCAN_CEILING,\n },\n { status: 200 },\n )\n } catch (error) {\n // An unverifiable credential is a 401, not a fault of ours\n // (AGL-1993). Null for anything else, so a real failure keeps its 500.\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error('[ai/admin/signals]', error)\n return Response.json({ error: 'Assist signal lookup failed' }, { status: 500 })\n }\n}\n\nexport { handler as GET }\n"],"names":["ORG_BILLING_DOC_ID","ORG_BILLING_SUBCOLLECTION","pluginRequestFromWeb","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","ASSIST_PROVIDER_COST_FIELD","assistCreditsFromUsd","assistProviderCostUsd","hasAiAddon","resolveEffectivePlan","readPlatformFreeSpend","assistRefusalCounts","assistUsageDay","assistUsageMonth","invalidIdTokenResponse","assistSignalRow","mineAssistSignals","rankAssistSpend","SCAN_CEILING","ANSWER_PREVIEW_CHARS","SPEND_SCAN_CEILING","readAssistSpendLeaderboard","firestore","limit","month","rankAssistSpendForMonth","error","console","rows","ranked","truncated","failed","snapshot","collectionGroup","get","size","docs","slice","unranked","doc","id","orgId","ref","parent","billed","Number","billedUsd","isFinite","push","plan","aiAddon","credits","providerCostUsd","refusals","length","orgRefs","map","row","collection","orgSnaps","billingSnaps","Promise","all","getAll","forEach","index","orgData","exists","data","org","label","orgLabel","handler","request","headers","method","query","rawHeaders","Response","json","status","authorization","idToken","startsWith","undefined","decoded","app","auth","verifyIdToken","email_verified","requested","Math","min","floor","report","spend","exchanges","proseCandidates","candidate","exchangeId","prose","expired","question","answer","answerTruncated","orgIds","Set","orgs","filter","orgLabelById","Map","orgDocs","set","freeSpend","catch","ceiling","unauthenticated","GET"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-admin-signals.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 {\n ORG_BILLING_DOC_ID,\n ORG_BILLING_SUBCOLLECTION,\n pluginRequestFromWeb,\n} from '@aglyn/aglyn/server'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin'\nimport {\n ASSIST_PROVIDER_COST_FIELD,\n assistCreditsFromUsd,\n assistProviderCostUsd,\n} from '../usage/assist-credits'\nimport {\n hasAiAddon,\n resolveEffectivePlan,\n} from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { readPlatformFreeSpend } from '../usage/assist-free-taste'\nimport { assistRefusalCounts } from '../usage/assist-refusals'\nimport { assistUsageDay, assistUsageMonth } from '../usage/assist-usage'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport {\n assistSignalRow,\n mineAssistSignals,\n rankAssistSpend,\n type AssistSpendRow,\n} from '../usage/assist-signal-mining'\n\n/**\n * The Assist docs-gap and cost mining view (AGL-1860, AGL-2252), served at\n * `/api/ai/admin/signals` for the plugin's Assist signal staff page.\n * Staff-gated, read-only, and the only reader `assistSignals` has.\n *\n * ## Why a route and not a client query\n *\n * `assistSignals` is absent from `firebase-firestore.rules` deliberately: the\n * org block matches its subcollections BY NAME and carries no wildcard, so an\n * unlisted name is default-deny for every client. That is the property that\n * lets the collection hold cross-tenant analytics at all. Reading it therefore\n * goes through the Admin SDK behind the staff claim, exactly as every staff\n * route does.\n *\n * ## The collection group, and the index it does not need\n *\n * One `collectionGroup('assistSignals')` read gathers every org's signals in a\n * single query; the owning org is `ref.parent.parent.id`, since the document\n * carries no `orgId` field (it carries no identifiers at all — see AGL-1972).\n * The query has no `where` and no `orderBy`, so it needs no composite index,\n * and adding one later would mean declaring a `COLLECTION_GROUP`-scoped\n * override and deploying it by hand. `/api/admin/overview` reads org billing\n * the same way and for the same reason.\n *\n * ## Truncation is reported, never hidden\n *\n * The read is bounded — an unbounded fleet-wide get is how a staff page\n * becomes an outage — but a ranking cut short looks exactly like a complete\n * one, so the ceiling is fetched PLUS ONE and the extra row sets `truncated`.\n * AGL-2220 is the standing example of the alternative: a sweep that stopped at\n * 500 orgs and reported nothing about the rest, for as long as nobody counted.\n */\n\n/**\n * Rows read in one pass. Each signal is a handful of scalars, so this is a\n * few megabytes at the ceiling — large enough to be worth ranking, small\n * enough that the honest answer to \"is it all of them\" is usually yes.\n */\nconst SCAN_CEILING = 20000\n\n/**\n * How much of an answer the prose panel shows.\n *\n * Enough to judge whether the answer was wrong, short enough that twenty-five\n * of them are still a page somebody reads.\n */\nconst ANSWER_PREVIEW_CHARS = 600\n\n/**\n * Month documents read in one pass for the spend leaderboard (AGL-2930).\n *\n * `assistUsage` holds one document per org per month, so this is orgs times\n * months — a fraction of the signal count, and the same PLUS-ONE truncation\n * probe the signals read uses. Read as a collection group with NO `where`\n * and NO `orderBy`, for the reason the module note gives: a filter on\n * `month` would need a COLLECTION_GROUP-scoped index deployed by hand, and\n * filtering the month in memory costs nothing at this size.\n */\nconst SPEND_SCAN_CEILING = 20000\n\nexport interface AssistSpendLeaderboard {\n month: string\n rows: AssistSpendRow[]\n /** How many workspaces had a document for the month — the ranked count. */\n ranked: number\n /** True when the scan hit its ceiling, so the ranking is over a sample. */\n truncated: boolean\n /**\n * True when the month documents could not be read at all. Reported rather\n * than rendered as an empty table: \"no workspace spent anything\" and \"the\n * read failed\" are opposite findings, and the rest of the board — read\n * from a different collection — is still good.\n */\n failed: boolean\n}\n\n/**\n * The month's per-org spend, refusals and plan, dearest first.\n *\n * Plan and add-on come from the org and billing documents of the ranked\n * rows only — bounded by `limit`, never by the fleet — merged org-doc-first\n * exactly as every other staff reader merges them.\n */\nasync function readAssistSpendLeaderboard(\n firestore: FirebaseFirestore.Firestore,\n limit: number,\n month: string,\n): Promise<AssistSpendLeaderboard> {\n try {\n return await rankAssistSpendForMonth(firestore, limit, month)\n } catch (error) {\n console.error('[ai/admin/signals] spend leaderboard read failed', error)\n return { month, rows: [], ranked: 0, truncated: false, failed: true }\n }\n}\n\nasync function rankAssistSpendForMonth(\n firestore: FirebaseFirestore.Firestore,\n limit: number,\n month: string,\n): Promise<AssistSpendLeaderboard> {\n const snapshot = await firestore\n .collectionGroup('assistUsage')\n .limit(SPEND_SCAN_CEILING + 1)\n .get()\n const truncated = snapshot.size > SPEND_SCAN_CEILING\n const docs = truncated ? snapshot.docs.slice(0, SPEND_SCAN_CEILING) : snapshot.docs\n const unranked: AssistSpendRow[] = []\n for (const doc of docs) {\n // The id IS the month; the field is the same value and is what an\n // older document may lack.\n if (doc.id !== month && doc.get('month') !== month) continue\n const orgId = doc.ref.parent.parent?.id\n if (!orgId) continue\n // Two figures off one document (AGL-3015): the credits a workspace drew\n // come from what it was BILLED, and the dollars staff rank workspaces by\n // come from what it COST US. Ranking on the billed figure would sort the\n // fleet by our own markup wherever workspaces differ in model mix.\n const billed = Number(doc.get('estCostUsd') ?? 0)\n const billedUsd = Number.isFinite(billed) && billed > 0 ? billed : 0\n unranked.push({\n orgId,\n plan: 'free',\n aiAddon: false,\n credits: assistCreditsFromUsd(billedUsd),\n providerCostUsd: assistProviderCostUsd(\n billedUsd,\n doc.get(ASSIST_PROVIDER_COST_FIELD),\n ),\n refusals: assistRefusalCounts(doc.get('refusals')),\n })\n }\n const { rows, ranked } = rankAssistSpend(unranked, limit)\n if (rows.length) {\n const orgRefs = rows.map((row) => firestore.collection('orgs').doc(row.orgId))\n const [orgSnaps, billingSnaps] = await Promise.all([\n firestore.getAll(...orgRefs),\n firestore.getAll(\n ...orgRefs.map((ref) =>\n ref.collection(ORG_BILLING_SUBCOLLECTION).doc(ORG_BILLING_DOC_ID),\n ),\n ),\n ])\n rows.forEach((row, index) => {\n const orgData = orgSnaps[index]?.exists ? (orgSnaps[index].data() ?? {}) : {}\n const org = {\n ...orgData,\n ...(billingSnaps[index]?.exists ? billingSnaps[index].data() : {}),\n }\n row.plan = resolveEffectivePlan(org as never)\n row.aiAddon = hasAiAddon(org as never)\n const label = (orgData['name'] ?? orgData['slug'] ?? '') as string\n row.orgLabel = label || null\n })\n }\n return { month, rows, ranked, truncated, failed: false }\n}\n\nasync function handler(request: Request): Promise<Response> {\n const { method, query, headers: rawHeaders } = 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) return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n if (!decoded['staff']) {\n return Response.json({ error: 'Staff only' }, { status: 403 })\n }\n\n const requested = Number(query.limit ?? 25)\n const limit =\n Number.isFinite(requested) && requested > 0\n ? Math.min(Math.floor(requested), 100)\n : 25\n\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collectionGroup('assistSignals')\n // Ceiling PLUS ONE: the extra document is what makes truncation\n // detectable rather than assumed.\n .limit(SCAN_CEILING + 1)\n .get()\n\n const truncated = snapshot.size > SCAN_CEILING\n const docs = truncated ? snapshot.docs.slice(0, SCAN_CEILING) : snapshot.docs\n const rows = docs.map((doc) =>\n assistSignalRow(\n doc.ref.parent.parent?.id ?? '(unknown)',\n // The signal id IS the exchange id (AGL-2314) — one id, two halves,\n // minted together by `recordAssistExchange` precisely so the join is\n // possible. Until now nothing carried it this far.\n doc.id,\n doc.data(),\n ),\n )\n\n const report = mineAssistSignals(rows, { truncated, limit })\n\n // This month's spend, per workspace (AGL-2930) — the board's only\n // month-scoped panel, read off the month documents rather than mined\n // from the all-time signals above.\n const spend = await readAssistSpendLeaderboard(\n firebaseAdmin.app().firestore(),\n limit,\n assistUsageMonth(),\n )\n\n /*==========================================\n * THE VERBATIM HALF (AGL-2314).\n *\n * `assistExchanges` stores the question a customer typed and the answer\n * we gave, for 180 days, and `assist-usage.ts` calls it \"the data loop's\n * corpus\". NOTHING read it — one write, no reader, no collection group.\n * So we retained customers' words, and committed publicly in the privacy\n * policy to retaining them, for zero product value.\n *\n * Read here rather than in the miner because the miner is pure: it does\n * the arithmetic, the route does the reading.\n *\n * ## Only the turns that FAILED\n *\n * The shortlist is thumbs-down and ungrounded turns only, capped by the\n * same `limit` as every other panel. That is a privacy decision as much\n * as a cost one: the counts already answer \"how often\", and the words are\n * fetched only where the counts CANNOT say what went wrong. Pulling every\n * exchange would be surveillance with a dashboard on it.\n *\n * `uid` is deliberately not projected. The corpus question is what people\n * asked, never who asked it — and the exchange is the only document that\n * still carries an identifier at all.\n *\n * ## An expired exchange is an ANSWER\n *\n * The 180-day TTL means a signal can outlive its prose, and it is meant\n * to. A missing document therefore reports `expired: true` rather than\n * being dropped: a shortlist silently shorter than the failure count it\n * came from would read as \"these are all the failures\", which is the\n * AGL-2220 shape.\n *=========================================*/\n const firestore = firebaseAdmin.app().firestore()\n const exchanges = report.proseCandidates.length\n ? await firestore.getAll(\n ...report.proseCandidates.map((candidate) =>\n firestore\n .collection('orgs')\n .doc(candidate.orgId)\n .collection('assistExchanges')\n .doc(candidate.exchangeId),\n ),\n )\n : []\n const prose = report.proseCandidates.map((candidate, index) => {\n const snapshot = exchanges[index]\n const data = (snapshot?.exists ? snapshot.data() : null) ?? {}\n return {\n ...candidate,\n expired: !snapshot?.exists,\n question: typeof data['question'] === 'string' ? data['question'] : null,\n // Trimmed, not omitted: judging whether an answer was wrong needs to\n // see it, and a whole answer per row would make the panel unreadable.\n answer:\n typeof data['answer'] === 'string'\n ? data['answer'].slice(0, ANSWER_PREVIEW_CHARS)\n : null,\n answerTruncated:\n typeof data['answer'] === 'string' &&\n data['answer'].length > ANSWER_PREVIEW_CHARS,\n }\n })\n\n /*==========================================\n * THE WORKSPACE, BY NAME.\n *\n * A staff reader recognizes a customer by name, never by document id,\n * and the cost board's own column header says \"Workspace\" over a column\n * of generated ids. `/api/admin/overview` fixed the same shape by\n * building a label map from a snapshot it already held; there is no such\n * snapshot here — this route reads `assistSignals` and nothing else — so\n * the labels are fetched, with the same `name` → `slug` → id precedence\n * so one workspace cannot appear under two different labels on two\n * screens.\n *\n * Bounded by what is actually RENDERED, not by the fleet. Both lists the\n * ids come from are already sliced to `limit` (25 by default, 100 at the\n * ceiling), so this is one `getAll` over at most a couple of hundred\n * documents — and it reads the orgs the reader can see rather than\n * scanning a collection to name a handful of rows.\n *\n * An org that has been deleted keeps its id rather than going blank: an\n * unfamiliar id is still a lead a staff member can paste into the org\n * search, and an empty cell is not.\n *=========================================*/\n const orgIds = [\n ...new Set(\n [\n ...report.orgs.map((row) => row.orgId),\n ...report.proseCandidates.map((row) => row.orgId),\n ].filter((orgId) => orgId && orgId !== '(unknown)'),\n ),\n ]\n const orgLabelById = new Map<string, string>()\n if (orgIds.length) {\n const orgDocs = await firestore.getAll(\n ...orgIds.map((orgId) => firestore.collection('orgs').doc(orgId)),\n )\n for (const doc of orgDocs) {\n if (!doc.exists) continue\n const data = doc.data() ?? {}\n const label = (data['name'] ?? data['slug'] ?? '') as string\n if (label) orgLabelById.set(doc.id, label)\n }\n }\n const orgLabel = (orgId: string): string =>\n orgLabelById.get(orgId) ?? orgId\n\n // Today's free-tier spend against the platform ceiling (AGL-2925): one\n // document read, and the only place staff can see the taste switching\n // itself off before the email arrives. Best-effort — a readout that\n // cannot be read must not take the rest of the board down with it.\n const freeSpend = await readPlatformFreeSpend(\n firestore,\n assistUsageDay(),\n ).catch((error) => {\n console.error('[ai/admin/signals] free spend readout failed', error)\n return undefined\n })\n\n return Response.json(\n {\n ...report,\n freeSpend,\n orgs: report.orgs.map((row) => ({\n ...row,\n orgLabel: orgLabel(row.orgId),\n })),\n prose: prose.map((row) => ({ ...row, orgLabel: orgLabel(row.orgId) })),\n spend,\n ceiling: SCAN_CEILING,\n },\n { status: 200 },\n )\n } catch (error) {\n // An unverifiable credential is a 401, not a fault of ours\n // (AGL-1993). Null for anything else, so a real failure keeps its 500.\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error('[ai/admin/signals]', error)\n return Response.json({ error: 'Assist signal lookup failed' }, { status: 500 })\n }\n}\n\nexport { handler as GET }\n"],"names":["ORG_BILLING_DOC_ID","ORG_BILLING_SUBCOLLECTION","pluginRequestFromWeb","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","ASSIST_PROVIDER_COST_FIELD","assistCreditsFromUsd","assistProviderCostUsd","hasAiAddon","resolveEffectivePlan","readPlatformFreeSpend","assistRefusalCounts","assistUsageDay","assistUsageMonth","invalidIdTokenResponse","assistSignalRow","mineAssistSignals","rankAssistSpend","SCAN_CEILING","ANSWER_PREVIEW_CHARS","SPEND_SCAN_CEILING","readAssistSpendLeaderboard","firestore","limit","month","rankAssistSpendForMonth","error","console","rows","ranked","truncated","failed","snapshot","collectionGroup","get","size","docs","slice","unranked","doc","id","orgId","ref","parent","billed","Number","billedUsd","isFinite","push","plan","aiAddon","credits","providerCostUsd","refusals","length","orgRefs","map","row","collection","orgSnaps","billingSnaps","Promise","all","getAll","forEach","index","orgData","exists","data","org","label","orgLabel","handler","request","headers","method","query","rawHeaders","Response","json","status","authorization","idToken","startsWith","undefined","decoded","app","auth","verifyIdToken","email_verified","requested","Math","min","floor","report","spend","exchanges","proseCandidates","candidate","exchangeId","prose","expired","question","answer","answerTruncated","orgIds","Set","orgs","filter","orgLabelById","Map","orgDocs","set","freeSpend","catch","ceiling","unauthenticated","GET"],"mappings":"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"}
@@ -15,7 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */ import { pluginRequestFromWeb } from "@aglyn/aglyn/server";
18
- import { assistCreditsFromUsd, resolveAssistCreditBudget } from "@aglyn/aglyn/app-utils/assist-credits";
18
+ import { assistCreditsFromUsd, resolveAssistCreditBudget } from "../usage/assist-credits.js";
19
19
  import { hostRoleFor, isOrgWideMember } from "@aglyn/aglyn/app-utils/organizations";
20
20
  import { resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
21
21
  import { emailUnverifiedResponse, firebaseAdmin, isImpersonationSession, memberHasOrgPermission, resolveOrgMembership } from "@aglyn/tenant-data-admin";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-allotments.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 assistCreditsFromUsd,\n resolveAssistCreditBudget,\n} from '@aglyn/aglyn/app-utils/assist-credits'\nimport { hostRoleFor, isOrgWideMember } from '@aglyn/aglyn/app-utils/organizations'\nimport { resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AglynOrgMember } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n memberHasOrgPermission,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { logAiAllotmentChanged } from '../activity/ai-activity'\nimport {\n AI_ALLOTMENT_MAX_CREDITS,\n AI_ALLOTMENT_MAX_MODELS,\n aiAllotmentCredits,\n parseAiAllotmentSubject,\n type AiAllotment,\n type AiAllotmentMode,\n type AiAllotmentSubject,\n} from '../model/ai-allotments'\nimport { aiUsageByUserMonthFrom, aiUsageMonthKeys } from '../model/ai-usage-by-user'\nimport { AI_MODEL_CATALOG } from '../providers/catalog'\nimport { AI_MODEL_RESTRICTION_PLANS } from '../providers/model-choice'\nimport { resolveAiProvider } from '../providers/routing'\nimport {\n AI_HOST_CREDITS_FIELD,\n applyAiAllotmentWrites,\n listAiAllotments,\n seedAiHostMonthCredits,\n type AiAllotmentWrite,\n} from '../usage/ai-allotments'\nimport { userAiUsageMonthRef } from '../usage/ai-usage-by-user'\nimport type {\n AiAllotmentRowWire,\n AiAllotmentsHostWire,\n AiAllotmentsMemberWire,\n AiAllotmentsWire,\n} from '../usage/ai-usage-wire'\nimport { addAdminAudit } from '@aglyn/tenant-data-admin/server/admin-audit-write'\n\n// lockdown-423: exempt — a spend control, the posture of ai/billing/overage\n// beside it: a billing-locked workspace must still be able to LOWER what its\n// people may spend, and a 423 would trap it spending.\n\n/**\n * AI ALLOTMENTS (AGL-2942) — read and set them.\n *\n * ## Who may read\n *\n * - The whole org's allotments (`orgId`): `billing.view`, the Usage page's\n * gate — every allotment is a figure about the pool it is part of.\n * - One site's (`hostId`): the same, or the site's admin when they are a\n * collaborator scoped to it — the collaborators card is theirs.\n * - One person's (`uid`): the same, or the person themselves.\n *\n * ## Who may write\n *\n * - A member's, a site's and the org-wide model restriction:\n * `billing.manage`. Each moves how much of the paid pool someone may\n * spend.\n * - A collaborator's on one site: `billing.manage`, or the admin of that\n * site when they are a collaborator scoped to it — never on their own\n * allotment, which they could otherwise raise.\n *\n * Staff pass, as at the other org routes. The org-wide restriction is set\n * only on a plan that offers it (`AI_MODEL_RESTRICTION_PLANS`); clearing it\n * is always allowed.\n *\n * ## One write, many subjects\n *\n * `POST { orgId, set, remove, everySite }`: `set` replaces allotments,\n * `remove` deletes them, and `everySite` sets the same allotment on every\n * site the org has — \"same for every client site\" — expanded here, from the\n * org's sites, rather than trusted from a client's list.\n */\n\nconst json = (body: unknown, status: number) => Response.json(body, { status })\n\n/** Batch writes a request may make; Firestore commits at most 500. */\nconst MAX_WRITES = 450\n\n/** Sites one `everySite` expands to. */\nconst MAX_SITES = 400\n\n/** Member months one `getAll` carries. */\nconst GET_ALL_CHUNK = 100\n\ninterface Caller {\n uid: string\n email: string | null\n staff: boolean\n member: AglynOrgMember | null\n}\n\nasync function authenticate(\n headers: Partial<Record<string, string>>,\n): Promise<Response | { decoded: { uid: string; email?: string; staff: boolean } }> {\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return json({ error: 'Unauthenticated' }, 401)\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n return { decoded: { uid: decoded.uid, email: decoded.email, staff: decoded['staff'] === true } }\n}\n\n/** A collaborator scoped to the site who holds its admin role there. */\nfunction isSiteAdminCollaborator(member: AglynOrgMember | null, hostId: string): boolean {\n if (!member || !hostId || isOrgWideMember(member)) return false\n return hostRoleFor(member, hostId) === 'admin'\n}\n\ninterface RosterMonth {\n member: AglynOrgMember\n credits: number\n byHost: Record<string, number>\n}\n\n/** The roster and each member's month, by path — no index, one getAll a hundred. */\nasync function readRosterMonths(\n firestore: FirebaseFirestore.Firestore,\n orgRef: FirebaseFirestore.DocumentReference,\n month: string,\n): Promise<RosterMonth[]> {\n const roster = await orgRef.collection('members').get()\n const members = roster.docs.map(\n (doc) => ({ $id: doc.id, ...doc.data() }) as AglynOrgMember,\n )\n const out: RosterMonth[] = []\n for (let start = 0; start < members.length; start += GET_ALL_CHUNK) {\n const chunk = members.slice(start, start + GET_ALL_CHUNK)\n const snapshots = chunk.length\n ? await firestore.getAll(\n ...chunk.map((member) => userAiUsageMonthRef(orgRef, member.$id, month)),\n )\n : []\n snapshots.forEach((snapshot, index) => {\n const member = chunk[index]\n const usage = aiUsageByUserMonthFrom(\n snapshot.exists ? (snapshot.data() as Record<string, unknown>) : null,\n member.$id,\n month,\n )\n out.push({ member, credits: usage.credits, byHost: usage.byHost })\n })\n }\n return out\n}\n\nconst memberName = (member: AglynOrgMember): string =>\n String(member.displayName ?? '').trim() || String(member.email ?? '').trim() || member.$id\n\n/** A site's credits this month: the org map, else its people's months summed. */\nfunction siteCredits(\n orgMonth: FirebaseFirestore.DocumentSnapshot,\n roster: readonly RosterMonth[],\n hostId: string,\n): number {\n const map = (orgMonth.exists ? orgMonth.get(AI_HOST_CREDITS_FIELD) : null) as\n | Record<string, unknown>\n | null\n if (map && map[hostId] !== undefined) {\n const value = Number(map[hostId])\n return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0\n }\n return roster.reduce((sum, entry) => sum + (entry.byHost[hostId] ?? 0), 0)\n}\n\nasync function handleGet(request: Request): Promise<Response> {\n const { query, headers } = await pluginRequestFromWeb(request)\n const params = (query ?? {}) as Record<string, unknown>\n const hostId = String(params['hostId'] ?? '').trim()\n const subjectUid = String(params['uid'] ?? '').trim()\n const auth = await authenticate(headers as Partial<Record<string, string>>)\n if (auth instanceof Response) return auth\n const { decoded } = auth\n const orgId =\n String(params['orgId'] ?? '').trim() || (hostId ? ((await resolveOrgIdForHost(hostId)) ?? '') : '')\n if (!orgId) return json({ error: 'Missing orgId' }, 400)\n\n const membership = await resolveOrgMembership(decoded.uid, orgId)\n const member = membership?.member ?? null\n if (!member && !decoded.staff) return json({ error: 'Not found' }, 404)\n const canManage =\n decoded.staff || (await memberHasOrgPermission(orgId, member, 'billing.manage'))\n const canView =\n canManage || (await memberHasOrgPermission(orgId, member, 'billing.view'))\n const siteAdmin = isSiteAdminCollaborator(member, hostId)\n const allowed = subjectUid\n ? canView || subjectUid === decoded.uid || siteAdmin\n : hostId\n ? canView || siteAdmin\n : canView\n if (!allowed) return json({ error: 'billing.view required' }, 403)\n\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const month = aiUsageMonthKeys()[0]\n const [orgSnapshot, orgMonth, allotments, roster, hostDocs] = await Promise.all([\n orgRef.get(),\n orgRef.collection('assistUsage').doc(month).get(),\n listAiAllotments(firestore, orgId, subjectUid ? { uid: subjectUid } : hostId ? { hostId } : {}),\n readRosterMonths(firestore, orgRef, month),\n hostId\n ? firestore.collection('hosts').doc(hostId).get().then((doc) => (doc.exists ? [doc] : []))\n : firestore.collection('hosts').where('orgId', '==', orgId).limit(MAX_SITES).get().then((snapshot) => snapshot.docs),\n ])\n const org = (orgSnapshot.data() ?? {}) as Record<string, unknown>\n const byUid = new Map(roster.map((entry) => [entry.member.$id, entry]))\n\n const used = (allotment: AiAllotment): number => {\n const person = allotment.uid ? byUid.get(allotment.uid) : undefined\n switch (allotment.scope) {\n case 'member':\n return person?.credits ?? 0\n case 'collab':\n return allotment.hostId ? (person?.byHost[allotment.hostId] ?? 0) : 0\n case 'host':\n return allotment.hostId ? siteCredits(orgMonth, roster, allotment.hostId) : 0\n default:\n return 0\n }\n }\n const rows: AiAllotmentRowWire[] = allotments\n // A collaborator reading their own sees their own rows and nothing else.\n .filter((allotment) => canView || siteAdmin || allotment.uid === decoded.uid)\n .map((allotment) => ({\n subject: allotment.subject,\n scope: allotment.scope,\n uid: allotment.uid,\n hostId: allotment.hostId,\n credits: allotment.credits,\n mode: allotment.mode,\n models: allotment.models,\n used: used(allotment),\n }))\n\n const visibleMembers = roster.filter((entry) =>\n subjectUid\n ? entry.member.$id === subjectUid\n : hostId\n ? hostRoleFor(entry.member, hostId) !== null\n : canView,\n )\n const members: AiAllotmentsMemberWire[] = visibleMembers.map((entry) => ({\n uid: entry.member.$id,\n name: memberName(entry.member),\n email: typeof entry.member.email === 'string' ? entry.member.email : null,\n role: typeof entry.member.role === 'string' ? entry.member.role : null,\n orgWide: isOrgWideMember(entry.member),\n credits: entry.credits,\n byHost: entry.byHost,\n }))\n const hosts: AiAllotmentsHostWire[] = hostDocs.map((doc) => ({\n hostId: doc.id,\n name: String(doc.get('displayName') ?? '').trim() || String(doc.get('subdomain') ?? '').trim() || doc.id,\n credits: siteCredits(orgMonth, roster, doc.id),\n }))\n const provider = resolveAiProvider()\n const wire: AiAllotmentsWire = {\n orgId,\n month,\n pool: {\n used: assistCreditsFromUsd(Number(orgMonth.get('estCostUsd') ?? 0)),\n limit: resolveAssistCreditBudget(org as never),\n },\n allotments: rows,\n members,\n hosts,\n models: AI_MODEL_CATALOG.filter((entry) => !provider || entry.provider === provider.id).map(\n (entry) => ({ id: entry.id, label: entry.label, tier: entry.tier }),\n ),\n restrictionAvailable: AI_MODEL_RESTRICTION_PLANS.includes(resolveEffectivePlan(org as never)),\n canEdit: {\n billing: canManage,\n collaborators: canManage || siteAdmin,\n },\n callerUid: decoded.uid,\n }\n return Response.json(wire, { status: 200, headers: { 'Cache-Control': 'no-store, private' } })\n}\n\ninterface ParsedWrite {\n subject: AiAllotmentSubject\n credits: number | null\n mode: AiAllotmentMode\n models: string[] | null\n}\n\nconst CATALOG_IDS = new Set(AI_MODEL_CATALOG.map((entry) => entry.id))\n\n/** One entry of `set`, validated; a string names what is wrong. */\nexport function parseAllotmentWrite(raw: unknown): ParsedWrite | string {\n const entry = (raw ?? {}) as Record<string, unknown>\n const subject = parseAiAllotmentSubject(entry['subject'])\n if (!subject) return 'subject must be member:{uid}, collab:{hostId}:{uid}, host:{hostId} or org'\n let credits: number | null = null\n if (entry['credits'] !== null && entry['credits'] !== undefined) {\n credits = typeof entry['credits'] === 'number' ? aiAllotmentCredits(entry['credits']) : null\n if (credits === null || (entry['credits'] as number) > AI_ALLOTMENT_MAX_CREDITS) {\n return `credits must be a whole number from 1 to ${AI_ALLOTMENT_MAX_CREDITS.toLocaleString('en-US')}`\n }\n }\n const mode = entry['mode'] === undefined ? 'hard' : entry['mode']\n if (mode !== 'hard' && mode !== 'soft') return 'mode must be hard or soft'\n let models: string[] | null = null\n if (entry['models'] !== null && entry['models'] !== undefined) {\n if (!Array.isArray(entry['models'])) return 'models must be a list of model ids'\n const ids = [...new Set(entry['models'].map((id) => String(id ?? '').trim()))].filter(Boolean)\n if (ids.length > AI_ALLOTMENT_MAX_MODELS) return `name at most ${AI_ALLOTMENT_MAX_MODELS} models`\n const unknown = ids.find((id) => !CATALOG_IDS.has(id))\n if (unknown) return `${unknown} is not a model this platform offers`\n models = ids.length ? ids : null\n }\n if (subject.scope === 'org' && credits !== null) {\n return 'the org-wide restriction carries models, not credits'\n }\n if (subject.scope !== 'org' && credits === null && models === null) {\n return 'set credits or models, or remove the allotment'\n }\n return { subject, credits, mode, models }\n}\n\nasync function handlePost(request: Request): Promise<Response> {\n const { body, headers } = await pluginRequestFromWeb(request)\n const auth = await authenticate(headers as Partial<Record<string, string>>)\n if (auth instanceof Response) return auth\n const { decoded } = auth\n const payload = (body ?? {}) as Record<string, unknown>\n const orgId = String(payload['orgId'] ?? '').trim()\n if (!orgId) return json({ error: 'Missing orgId' }, 400)\n\n const rawSet = Array.isArray(payload['set']) ? payload['set'] : []\n const rawRemove = Array.isArray(payload['remove']) ? payload['remove'] : []\n const rawEverySite = payload['everySite'] ?? null\n if (!rawSet.length && !rawRemove.length && !rawEverySite) {\n return json({ error: 'Nothing to change' }, 400)\n }\n if (rawSet.length + rawRemove.length > MAX_WRITES) {\n return json({ error: `Change at most ${MAX_WRITES} allotments at once` }, 400)\n }\n\n const writes: ParsedWrite[] = []\n for (const raw of rawSet) {\n const parsed = parseAllotmentWrite(raw)\n if (typeof parsed === 'string') return json({ error: parsed, code: 'invalid_allotment' }, 400)\n writes.push(parsed)\n }\n const removals: AiAllotmentSubject[] = []\n for (const raw of rawRemove) {\n const subject = parseAiAllotmentSubject(raw)\n if (!subject) return json({ error: `${String(raw)} is not an allotment subject` }, 400)\n removals.push(subject)\n }\n let everySite: Omit<ParsedWrite, 'subject'> | null = null\n if (rawEverySite) {\n const parsed = parseAllotmentWrite({ ...(rawEverySite as object), subject: 'host:every-site' })\n if (typeof parsed === 'string') return json({ error: parsed, code: 'invalid_allotment' }, 400)\n everySite = { credits: parsed.credits, mode: parsed.mode, models: parsed.models }\n }\n\n const membership = await resolveOrgMembership(decoded.uid, orgId)\n const caller: Caller = {\n uid: decoded.uid,\n email: decoded.email ?? null,\n staff: decoded.staff,\n member: membership?.member ?? null,\n }\n if (!caller.member && !caller.staff) return json({ error: 'Not found' }, 404)\n const canManage =\n caller.staff || (await memberHasOrgPermission(orgId, caller.member, 'billing.manage'))\n\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const orgSnapshot = await orgRef.get()\n if (!orgSnapshot.exists) return json({ error: 'Unknown organization' }, 404)\n const org = (orgSnapshot.data() ?? {}) as Record<string, unknown>\n\n // Every subject the request touches, set or removed, is decided the same\n // way: the permission first, then that the person or site belongs here.\n const touched = [...writes.map((write) => write.subject), ...removals]\n if (everySite && !canManage) return json({ error: 'billing.manage required' }, 403)\n const memberDocs = new Map<string, AglynOrgMember | null>()\n const memberOf = async (uid: string): Promise<AglynOrgMember | null> => {\n if (!memberDocs.has(uid)) {\n const snapshot = await orgRef.collection('members').doc(uid).get()\n memberDocs.set(uid, snapshot.exists ? ({ $id: uid, ...snapshot.data() } as AglynOrgMember) : null)\n }\n return memberDocs.get(uid) ?? null\n }\n const hostNames = new Map<string, string>()\n for (const subject of touched) {\n if (subject.scope === 'collab') {\n const siteAdmin = isSiteAdminCollaborator(caller.member, subject.hostId ?? '')\n if (!canManage && !siteAdmin) return json({ error: 'billing.manage required' }, 403)\n if (!canManage && subject.uid === caller.uid) {\n return json({ error: 'You cannot change your own AI allotment', code: 'own_allotment' }, 403)\n }\n const target = await memberOf(subject.uid ?? '')\n if (!target || hostRoleFor(target, subject.hostId ?? '') === null) {\n return json({ error: 'That person is not a collaborator on that site' }, 404)\n }\n // A site's admin sets allotments for the site's COLLABORATORS: a team\n // member's AI is the organization's to bound, not one site's.\n if (!canManage && isOrgWideMember(target)) {\n return json({ error: 'billing.manage required' }, 403)\n }\n } else if (!canManage) {\n return json({ error: 'billing.manage required' }, 403)\n } else if (subject.scope === 'member') {\n if (!(await memberOf(subject.uid ?? ''))) {\n return json({ error: 'That person is not a member of this organization' }, 404)\n }\n } else if (subject.scope === 'host') {\n const host = await firestore.collection('hosts').doc(subject.hostId ?? '').get()\n if (!host.exists || host.get('orgId') !== orgId) {\n return json({ error: 'That site is not part of this organization' }, 404)\n }\n hostNames.set(host.id, String(host.get('displayName') ?? '').trim() || host.id)\n }\n }\n const restricting = writes.some((write) => write.subject.scope === 'org' && write.models)\n if (restricting && !AI_MODEL_RESTRICTION_PLANS.includes(resolveEffectivePlan(org as never))) {\n return json(\n {\n error: 'Restricting models for the whole organization is available on Agency and Enterprise.',\n code: 'not_available',\n },\n 409,\n )\n }\n\n const set: AiAllotmentWrite[] = [...writes]\n if (everySite) {\n const sites = await firestore.collection('hosts').where('orgId', '==', orgId).limit(MAX_SITES).get()\n for (const site of sites.docs) {\n hostNames.set(site.id, String(site.get('displayName') ?? '').trim() || site.id)\n const subject = parseAiAllotmentSubject(`host:${site.id}`)\n if (subject) set.push({ subject, ...everySite })\n }\n if (set.length + removals.length > MAX_WRITES) {\n return json({ error: `Change at most ${MAX_WRITES} allotments at once` }, 400)\n }\n }\n\n await applyAiAllotmentWrites(\n firestore,\n orgId,\n { set, remove: removals.map((subject) => subject.id) },\n caller.uid,\n )\n\n // A site allotment set in a month the site's map has no key for yet is\n // given the month so far from its people's own months, once.\n const month = aiUsageMonthKeys()[0]\n const siteIds = set\n .filter((write) => write.subject.scope === 'host' && write.credits !== null)\n .map((write) => write.subject.hostId as string)\n if (siteIds.length) {\n const roster = await readRosterMonths(firestore, orgRef, month)\n await seedAiHostMonthCredits(firestore, orgId, siteIds, month, (siteId) =>\n roster.reduce((sum, entry) => sum + (entry.byHost[siteId] ?? 0), 0),\n ).catch((error) => console.error('[ai/allotments] site month seed failed', orgId, error))\n }\n\n void addAdminAudit(firestore, {\n actorUid: caller.uid,\n actorEmail: caller.email,\n action: 'ai.allotments.set',\n target: `orgs/${orgId}/aiAllotments`,\n before: null,\n after: {\n set: set.map((write) => ({ subject: write.subject.id, credits: write.credits, mode: write.mode, models: write.models })),\n removed: removals.map((subject) => subject.id),\n },\n at: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n\n const actor = { uid: caller.uid, email: caller.email }\n const feedRow = (subject: AiAllotmentSubject) => ({\n type: subject.scope === 'host' ? ('host' as const) : subject.scope === 'org' ? ('org' as const) : ('member' as const),\n id: subject.scope === 'host' ? subject.hostId : subject.uid,\n name:\n subject.scope === 'host'\n ? hostNames.get(subject.hostId ?? '') ?? subject.hostId\n : subject.scope === 'org'\n ? 'Organization model restriction'\n : [\n memberDocs.get(subject.uid ?? '') ? memberName(memberDocs.get(subject.uid ?? '') as AglynOrgMember) : subject.uid,\n subject.scope === 'collab' ? `on ${subject.hostId}` : null,\n ]\n .filter(Boolean)\n .join(' '),\n })\n await Promise.all([\n ...set.map((write) =>\n logAiAllotmentChanged(orgId, actor, {\n subject: feedRow(write.subject),\n after: { credits: write.credits, mode: write.mode, models: write.models },\n }).catch(() => undefined),\n ),\n ...removals.map((subject) =>\n logAiAllotmentChanged(orgId, actor, { subject: feedRow(subject), after: null }).catch(\n () => undefined,\n ),\n ),\n ])\n\n return json({ ok: true, set: set.length, removed: removals.length }, 200)\n}\n\nasync function handler(request: Request): Promise<Response> {\n try {\n if (request.method === 'GET') return await handleGet(request)\n if (request.method === 'POST') return await handlePost(request)\n return json({ error: 'Method not allowed' }, 405)\n } catch (error) {\n // A refused credential is a 401, not a fault of ours (AGL-1993).\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error('[ai/allotments] failed', error)\n return json({ error: 'AI allotments unavailable' }, 500)\n }\n}\n\nexport { handler as GET, handler as POST }\n"],"names":["pluginRequestFromWeb","assistCreditsFromUsd","resolveAssistCreditBudget","hostRoleFor","isOrgWideMember","resolveEffectivePlan","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","memberHasOrgPermission","resolveOrgMembership","invalidIdTokenResponse","resolveOrgIdForHost","FieldValue","logAiAllotmentChanged","AI_ALLOTMENT_MAX_CREDITS","AI_ALLOTMENT_MAX_MODELS","aiAllotmentCredits","parseAiAllotmentSubject","aiUsageByUserMonthFrom","aiUsageMonthKeys","AI_MODEL_CATALOG","AI_MODEL_RESTRICTION_PLANS","resolveAiProvider","AI_HOST_CREDITS_FIELD","applyAiAllotmentWrites","listAiAllotments","seedAiHostMonthCredits","userAiUsageMonthRef","addAdminAudit","json","body","status","Response","MAX_WRITES","MAX_SITES","GET_ALL_CHUNK","authenticate","headers","authorization","idToken","startsWith","slice","length","undefined","error","decoded","app","auth","verifyIdToken","email_verified","uid","email","staff","isSiteAdminCollaborator","member","hostId","readRosterMonths","firestore","orgRef","month","roster","collection","get","members","docs","map","doc","$id","id","data","out","start","chunk","snapshots","getAll","forEach","snapshot","index","usage","exists","push","credits","byHost","memberName","String","displayName","trim","siteCredits","orgMonth","value","Number","isFinite","Math","floor","reduce","sum","entry","handleGet","request","params","orgSnapshot","query","subjectUid","orgId","membership","canManage","canView","siteAdmin","allowed","allotments","hostDocs","Promise","all","then","where","limit","org","byUid","Map","used","allotment","person","scope","rows","filter","subject","mode","models","visibleMembers","name","role","orgWide","hosts","provider","wire","pool","label","tier","restrictionAvailable","includes","canEdit","billing","collaborators","callerUid","CATALOG_IDS","Set","parseAllotmentWrite","raw","toLocaleString","Array","isArray","ids","Boolean","unknown","find","has","handlePost","payload","rawSet","rawRemove","rawEverySite","writes","parsed","code","removals","everySite","caller","touched","write","memberDocs","memberOf","set","hostNames","target","host","restricting","some","sites","site","remove","siteIds","siteId","catch","console","actorUid","actorEmail","action","before","after","removed","at","serverTimestamp","actor","feedRow","type","join","ok","handler","method","unauthenticated","GET","POST"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-allotments.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 assistCreditsFromUsd,\n resolveAssistCreditBudget,\n} from '../usage/assist-credits'\nimport { hostRoleFor, isOrgWideMember } from '@aglyn/aglyn/app-utils/organizations'\nimport { resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AglynOrgMember } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n memberHasOrgPermission,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { logAiAllotmentChanged } from '../activity/ai-activity'\nimport {\n AI_ALLOTMENT_MAX_CREDITS,\n AI_ALLOTMENT_MAX_MODELS,\n aiAllotmentCredits,\n parseAiAllotmentSubject,\n type AiAllotment,\n type AiAllotmentMode,\n type AiAllotmentSubject,\n} from '../model/ai-allotments'\nimport { aiUsageByUserMonthFrom, aiUsageMonthKeys } from '../model/ai-usage-by-user'\nimport { AI_MODEL_CATALOG } from '../providers/catalog'\nimport { AI_MODEL_RESTRICTION_PLANS } from '../providers/model-choice'\nimport { resolveAiProvider } from '../providers/routing'\nimport {\n AI_HOST_CREDITS_FIELD,\n applyAiAllotmentWrites,\n listAiAllotments,\n seedAiHostMonthCredits,\n type AiAllotmentWrite,\n} from '../usage/ai-allotments'\nimport { userAiUsageMonthRef } from '../usage/ai-usage-by-user'\nimport type {\n AiAllotmentRowWire,\n AiAllotmentsHostWire,\n AiAllotmentsMemberWire,\n AiAllotmentsWire,\n} from '../usage/ai-usage-wire'\nimport { addAdminAudit } from '@aglyn/tenant-data-admin/server/admin-audit-write'\n\n// lockdown-423: exempt — a spend control, the posture of ai/billing/overage\n// beside it: a billing-locked workspace must still be able to LOWER what its\n// people may spend, and a 423 would trap it spending.\n\n/**\n * AI ALLOTMENTS (AGL-2942) — read and set them.\n *\n * ## Who may read\n *\n * - The whole org's allotments (`orgId`): `billing.view`, the Usage page's\n * gate — every allotment is a figure about the pool it is part of.\n * - One site's (`hostId`): the same, or the site's admin when they are a\n * collaborator scoped to it — the collaborators card is theirs.\n * - One person's (`uid`): the same, or the person themselves.\n *\n * ## Who may write\n *\n * - A member's, a site's and the org-wide model restriction:\n * `billing.manage`. Each moves how much of the paid pool someone may\n * spend.\n * - A collaborator's on one site: `billing.manage`, or the admin of that\n * site when they are a collaborator scoped to it — never on their own\n * allotment, which they could otherwise raise.\n *\n * Staff pass, as at the other org routes. The org-wide restriction is set\n * only on a plan that offers it (`AI_MODEL_RESTRICTION_PLANS`); clearing it\n * is always allowed.\n *\n * ## One write, many subjects\n *\n * `POST { orgId, set, remove, everySite }`: `set` replaces allotments,\n * `remove` deletes them, and `everySite` sets the same allotment on every\n * site the org has — \"same for every client site\" — expanded here, from the\n * org's sites, rather than trusted from a client's list.\n */\n\nconst json = (body: unknown, status: number) => Response.json(body, { status })\n\n/** Batch writes a request may make; Firestore commits at most 500. */\nconst MAX_WRITES = 450\n\n/** Sites one `everySite` expands to. */\nconst MAX_SITES = 400\n\n/** Member months one `getAll` carries. */\nconst GET_ALL_CHUNK = 100\n\ninterface Caller {\n uid: string\n email: string | null\n staff: boolean\n member: AglynOrgMember | null\n}\n\nasync function authenticate(\n headers: Partial<Record<string, string>>,\n): Promise<Response | { decoded: { uid: string; email?: string; staff: boolean } }> {\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return json({ error: 'Unauthenticated' }, 401)\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n return { decoded: { uid: decoded.uid, email: decoded.email, staff: decoded['staff'] === true } }\n}\n\n/** A collaborator scoped to the site who holds its admin role there. */\nfunction isSiteAdminCollaborator(member: AglynOrgMember | null, hostId: string): boolean {\n if (!member || !hostId || isOrgWideMember(member)) return false\n return hostRoleFor(member, hostId) === 'admin'\n}\n\ninterface RosterMonth {\n member: AglynOrgMember\n credits: number\n byHost: Record<string, number>\n}\n\n/** The roster and each member's month, by path — no index, one getAll a hundred. */\nasync function readRosterMonths(\n firestore: FirebaseFirestore.Firestore,\n orgRef: FirebaseFirestore.DocumentReference,\n month: string,\n): Promise<RosterMonth[]> {\n const roster = await orgRef.collection('members').get()\n const members = roster.docs.map(\n (doc) => ({ $id: doc.id, ...doc.data() }) as AglynOrgMember,\n )\n const out: RosterMonth[] = []\n for (let start = 0; start < members.length; start += GET_ALL_CHUNK) {\n const chunk = members.slice(start, start + GET_ALL_CHUNK)\n const snapshots = chunk.length\n ? await firestore.getAll(\n ...chunk.map((member) => userAiUsageMonthRef(orgRef, member.$id, month)),\n )\n : []\n snapshots.forEach((snapshot, index) => {\n const member = chunk[index]\n const usage = aiUsageByUserMonthFrom(\n snapshot.exists ? (snapshot.data() as Record<string, unknown>) : null,\n member.$id,\n month,\n )\n out.push({ member, credits: usage.credits, byHost: usage.byHost })\n })\n }\n return out\n}\n\nconst memberName = (member: AglynOrgMember): string =>\n String(member.displayName ?? '').trim() || String(member.email ?? '').trim() || member.$id\n\n/** A site's credits this month: the org map, else its people's months summed. */\nfunction siteCredits(\n orgMonth: FirebaseFirestore.DocumentSnapshot,\n roster: readonly RosterMonth[],\n hostId: string,\n): number {\n const map = (orgMonth.exists ? orgMonth.get(AI_HOST_CREDITS_FIELD) : null) as\n | Record<string, unknown>\n | null\n if (map && map[hostId] !== undefined) {\n const value = Number(map[hostId])\n return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0\n }\n return roster.reduce((sum, entry) => sum + (entry.byHost[hostId] ?? 0), 0)\n}\n\nasync function handleGet(request: Request): Promise<Response> {\n const { query, headers } = await pluginRequestFromWeb(request)\n const params = (query ?? {}) as Record<string, unknown>\n const hostId = String(params['hostId'] ?? '').trim()\n const subjectUid = String(params['uid'] ?? '').trim()\n const auth = await authenticate(headers as Partial<Record<string, string>>)\n if (auth instanceof Response) return auth\n const { decoded } = auth\n const orgId =\n String(params['orgId'] ?? '').trim() || (hostId ? ((await resolveOrgIdForHost(hostId)) ?? '') : '')\n if (!orgId) return json({ error: 'Missing orgId' }, 400)\n\n const membership = await resolveOrgMembership(decoded.uid, orgId)\n const member = membership?.member ?? null\n if (!member && !decoded.staff) return json({ error: 'Not found' }, 404)\n const canManage =\n decoded.staff || (await memberHasOrgPermission(orgId, member, 'billing.manage'))\n const canView =\n canManage || (await memberHasOrgPermission(orgId, member, 'billing.view'))\n const siteAdmin = isSiteAdminCollaborator(member, hostId)\n const allowed = subjectUid\n ? canView || subjectUid === decoded.uid || siteAdmin\n : hostId\n ? canView || siteAdmin\n : canView\n if (!allowed) return json({ error: 'billing.view required' }, 403)\n\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const month = aiUsageMonthKeys()[0]\n const [orgSnapshot, orgMonth, allotments, roster, hostDocs] = await Promise.all([\n orgRef.get(),\n orgRef.collection('assistUsage').doc(month).get(),\n listAiAllotments(firestore, orgId, subjectUid ? { uid: subjectUid } : hostId ? { hostId } : {}),\n readRosterMonths(firestore, orgRef, month),\n hostId\n ? firestore.collection('hosts').doc(hostId).get().then((doc) => (doc.exists ? [doc] : []))\n : firestore.collection('hosts').where('orgId', '==', orgId).limit(MAX_SITES).get().then((snapshot) => snapshot.docs),\n ])\n const org = (orgSnapshot.data() ?? {}) as Record<string, unknown>\n const byUid = new Map(roster.map((entry) => [entry.member.$id, entry]))\n\n const used = (allotment: AiAllotment): number => {\n const person = allotment.uid ? byUid.get(allotment.uid) : undefined\n switch (allotment.scope) {\n case 'member':\n return person?.credits ?? 0\n case 'collab':\n return allotment.hostId ? (person?.byHost[allotment.hostId] ?? 0) : 0\n case 'host':\n return allotment.hostId ? siteCredits(orgMonth, roster, allotment.hostId) : 0\n default:\n return 0\n }\n }\n const rows: AiAllotmentRowWire[] = allotments\n // A collaborator reading their own sees their own rows and nothing else.\n .filter((allotment) => canView || siteAdmin || allotment.uid === decoded.uid)\n .map((allotment) => ({\n subject: allotment.subject,\n scope: allotment.scope,\n uid: allotment.uid,\n hostId: allotment.hostId,\n credits: allotment.credits,\n mode: allotment.mode,\n models: allotment.models,\n used: used(allotment),\n }))\n\n const visibleMembers = roster.filter((entry) =>\n subjectUid\n ? entry.member.$id === subjectUid\n : hostId\n ? hostRoleFor(entry.member, hostId) !== null\n : canView,\n )\n const members: AiAllotmentsMemberWire[] = visibleMembers.map((entry) => ({\n uid: entry.member.$id,\n name: memberName(entry.member),\n email: typeof entry.member.email === 'string' ? entry.member.email : null,\n role: typeof entry.member.role === 'string' ? entry.member.role : null,\n orgWide: isOrgWideMember(entry.member),\n credits: entry.credits,\n byHost: entry.byHost,\n }))\n const hosts: AiAllotmentsHostWire[] = hostDocs.map((doc) => ({\n hostId: doc.id,\n name: String(doc.get('displayName') ?? '').trim() || String(doc.get('subdomain') ?? '').trim() || doc.id,\n credits: siteCredits(orgMonth, roster, doc.id),\n }))\n const provider = resolveAiProvider()\n const wire: AiAllotmentsWire = {\n orgId,\n month,\n pool: {\n used: assistCreditsFromUsd(Number(orgMonth.get('estCostUsd') ?? 0)),\n limit: resolveAssistCreditBudget(org as never),\n },\n allotments: rows,\n members,\n hosts,\n models: AI_MODEL_CATALOG.filter((entry) => !provider || entry.provider === provider.id).map(\n (entry) => ({ id: entry.id, label: entry.label, tier: entry.tier }),\n ),\n restrictionAvailable: AI_MODEL_RESTRICTION_PLANS.includes(resolveEffectivePlan(org as never)),\n canEdit: {\n billing: canManage,\n collaborators: canManage || siteAdmin,\n },\n callerUid: decoded.uid,\n }\n return Response.json(wire, { status: 200, headers: { 'Cache-Control': 'no-store, private' } })\n}\n\ninterface ParsedWrite {\n subject: AiAllotmentSubject\n credits: number | null\n mode: AiAllotmentMode\n models: string[] | null\n}\n\nconst CATALOG_IDS = new Set(AI_MODEL_CATALOG.map((entry) => entry.id))\n\n/** One entry of `set`, validated; a string names what is wrong. */\nexport function parseAllotmentWrite(raw: unknown): ParsedWrite | string {\n const entry = (raw ?? {}) as Record<string, unknown>\n const subject = parseAiAllotmentSubject(entry['subject'])\n if (!subject) return 'subject must be member:{uid}, collab:{hostId}:{uid}, host:{hostId} or org'\n let credits: number | null = null\n if (entry['credits'] !== null && entry['credits'] !== undefined) {\n credits = typeof entry['credits'] === 'number' ? aiAllotmentCredits(entry['credits']) : null\n if (credits === null || (entry['credits'] as number) > AI_ALLOTMENT_MAX_CREDITS) {\n return `credits must be a whole number from 1 to ${AI_ALLOTMENT_MAX_CREDITS.toLocaleString('en-US')}`\n }\n }\n const mode = entry['mode'] === undefined ? 'hard' : entry['mode']\n if (mode !== 'hard' && mode !== 'soft') return 'mode must be hard or soft'\n let models: string[] | null = null\n if (entry['models'] !== null && entry['models'] !== undefined) {\n if (!Array.isArray(entry['models'])) return 'models must be a list of model ids'\n const ids = [...new Set(entry['models'].map((id) => String(id ?? '').trim()))].filter(Boolean)\n if (ids.length > AI_ALLOTMENT_MAX_MODELS) return `name at most ${AI_ALLOTMENT_MAX_MODELS} models`\n const unknown = ids.find((id) => !CATALOG_IDS.has(id))\n if (unknown) return `${unknown} is not a model this platform offers`\n models = ids.length ? ids : null\n }\n if (subject.scope === 'org' && credits !== null) {\n return 'the org-wide restriction carries models, not credits'\n }\n if (subject.scope !== 'org' && credits === null && models === null) {\n return 'set credits or models, or remove the allotment'\n }\n return { subject, credits, mode, models }\n}\n\nasync function handlePost(request: Request): Promise<Response> {\n const { body, headers } = await pluginRequestFromWeb(request)\n const auth = await authenticate(headers as Partial<Record<string, string>>)\n if (auth instanceof Response) return auth\n const { decoded } = auth\n const payload = (body ?? {}) as Record<string, unknown>\n const orgId = String(payload['orgId'] ?? '').trim()\n if (!orgId) return json({ error: 'Missing orgId' }, 400)\n\n const rawSet = Array.isArray(payload['set']) ? payload['set'] : []\n const rawRemove = Array.isArray(payload['remove']) ? payload['remove'] : []\n const rawEverySite = payload['everySite'] ?? null\n if (!rawSet.length && !rawRemove.length && !rawEverySite) {\n return json({ error: 'Nothing to change' }, 400)\n }\n if (rawSet.length + rawRemove.length > MAX_WRITES) {\n return json({ error: `Change at most ${MAX_WRITES} allotments at once` }, 400)\n }\n\n const writes: ParsedWrite[] = []\n for (const raw of rawSet) {\n const parsed = parseAllotmentWrite(raw)\n if (typeof parsed === 'string') return json({ error: parsed, code: 'invalid_allotment' }, 400)\n writes.push(parsed)\n }\n const removals: AiAllotmentSubject[] = []\n for (const raw of rawRemove) {\n const subject = parseAiAllotmentSubject(raw)\n if (!subject) return json({ error: `${String(raw)} is not an allotment subject` }, 400)\n removals.push(subject)\n }\n let everySite: Omit<ParsedWrite, 'subject'> | null = null\n if (rawEverySite) {\n const parsed = parseAllotmentWrite({ ...(rawEverySite as object), subject: 'host:every-site' })\n if (typeof parsed === 'string') return json({ error: parsed, code: 'invalid_allotment' }, 400)\n everySite = { credits: parsed.credits, mode: parsed.mode, models: parsed.models }\n }\n\n const membership = await resolveOrgMembership(decoded.uid, orgId)\n const caller: Caller = {\n uid: decoded.uid,\n email: decoded.email ?? null,\n staff: decoded.staff,\n member: membership?.member ?? null,\n }\n if (!caller.member && !caller.staff) return json({ error: 'Not found' }, 404)\n const canManage =\n caller.staff || (await memberHasOrgPermission(orgId, caller.member, 'billing.manage'))\n\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const orgSnapshot = await orgRef.get()\n if (!orgSnapshot.exists) return json({ error: 'Unknown organization' }, 404)\n const org = (orgSnapshot.data() ?? {}) as Record<string, unknown>\n\n // Every subject the request touches, set or removed, is decided the same\n // way: the permission first, then that the person or site belongs here.\n const touched = [...writes.map((write) => write.subject), ...removals]\n if (everySite && !canManage) return json({ error: 'billing.manage required' }, 403)\n const memberDocs = new Map<string, AglynOrgMember | null>()\n const memberOf = async (uid: string): Promise<AglynOrgMember | null> => {\n if (!memberDocs.has(uid)) {\n const snapshot = await orgRef.collection('members').doc(uid).get()\n memberDocs.set(uid, snapshot.exists ? ({ $id: uid, ...snapshot.data() } as AglynOrgMember) : null)\n }\n return memberDocs.get(uid) ?? null\n }\n const hostNames = new Map<string, string>()\n for (const subject of touched) {\n if (subject.scope === 'collab') {\n const siteAdmin = isSiteAdminCollaborator(caller.member, subject.hostId ?? '')\n if (!canManage && !siteAdmin) return json({ error: 'billing.manage required' }, 403)\n if (!canManage && subject.uid === caller.uid) {\n return json({ error: 'You cannot change your own AI allotment', code: 'own_allotment' }, 403)\n }\n const target = await memberOf(subject.uid ?? '')\n if (!target || hostRoleFor(target, subject.hostId ?? '') === null) {\n return json({ error: 'That person is not a collaborator on that site' }, 404)\n }\n // A site's admin sets allotments for the site's COLLABORATORS: a team\n // member's AI is the organization's to bound, not one site's.\n if (!canManage && isOrgWideMember(target)) {\n return json({ error: 'billing.manage required' }, 403)\n }\n } else if (!canManage) {\n return json({ error: 'billing.manage required' }, 403)\n } else if (subject.scope === 'member') {\n if (!(await memberOf(subject.uid ?? ''))) {\n return json({ error: 'That person is not a member of this organization' }, 404)\n }\n } else if (subject.scope === 'host') {\n const host = await firestore.collection('hosts').doc(subject.hostId ?? '').get()\n if (!host.exists || host.get('orgId') !== orgId) {\n return json({ error: 'That site is not part of this organization' }, 404)\n }\n hostNames.set(host.id, String(host.get('displayName') ?? '').trim() || host.id)\n }\n }\n const restricting = writes.some((write) => write.subject.scope === 'org' && write.models)\n if (restricting && !AI_MODEL_RESTRICTION_PLANS.includes(resolveEffectivePlan(org as never))) {\n return json(\n {\n error: 'Restricting models for the whole organization is available on Agency and Enterprise.',\n code: 'not_available',\n },\n 409,\n )\n }\n\n const set: AiAllotmentWrite[] = [...writes]\n if (everySite) {\n const sites = await firestore.collection('hosts').where('orgId', '==', orgId).limit(MAX_SITES).get()\n for (const site of sites.docs) {\n hostNames.set(site.id, String(site.get('displayName') ?? '').trim() || site.id)\n const subject = parseAiAllotmentSubject(`host:${site.id}`)\n if (subject) set.push({ subject, ...everySite })\n }\n if (set.length + removals.length > MAX_WRITES) {\n return json({ error: `Change at most ${MAX_WRITES} allotments at once` }, 400)\n }\n }\n\n await applyAiAllotmentWrites(\n firestore,\n orgId,\n { set, remove: removals.map((subject) => subject.id) },\n caller.uid,\n )\n\n // A site allotment set in a month the site's map has no key for yet is\n // given the month so far from its people's own months, once.\n const month = aiUsageMonthKeys()[0]\n const siteIds = set\n .filter((write) => write.subject.scope === 'host' && write.credits !== null)\n .map((write) => write.subject.hostId as string)\n if (siteIds.length) {\n const roster = await readRosterMonths(firestore, orgRef, month)\n await seedAiHostMonthCredits(firestore, orgId, siteIds, month, (siteId) =>\n roster.reduce((sum, entry) => sum + (entry.byHost[siteId] ?? 0), 0),\n ).catch((error) => console.error('[ai/allotments] site month seed failed', orgId, error))\n }\n\n void addAdminAudit(firestore, {\n actorUid: caller.uid,\n actorEmail: caller.email,\n action: 'ai.allotments.set',\n target: `orgs/${orgId}/aiAllotments`,\n before: null,\n after: {\n set: set.map((write) => ({ subject: write.subject.id, credits: write.credits, mode: write.mode, models: write.models })),\n removed: removals.map((subject) => subject.id),\n },\n at: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n\n const actor = { uid: caller.uid, email: caller.email }\n const feedRow = (subject: AiAllotmentSubject) => ({\n type: subject.scope === 'host' ? ('host' as const) : subject.scope === 'org' ? ('org' as const) : ('member' as const),\n id: subject.scope === 'host' ? subject.hostId : subject.uid,\n name:\n subject.scope === 'host'\n ? hostNames.get(subject.hostId ?? '') ?? subject.hostId\n : subject.scope === 'org'\n ? 'Organization model restriction'\n : [\n memberDocs.get(subject.uid ?? '') ? memberName(memberDocs.get(subject.uid ?? '') as AglynOrgMember) : subject.uid,\n subject.scope === 'collab' ? `on ${subject.hostId}` : null,\n ]\n .filter(Boolean)\n .join(' '),\n })\n await Promise.all([\n ...set.map((write) =>\n logAiAllotmentChanged(orgId, actor, {\n subject: feedRow(write.subject),\n after: { credits: write.credits, mode: write.mode, models: write.models },\n }).catch(() => undefined),\n ),\n ...removals.map((subject) =>\n logAiAllotmentChanged(orgId, actor, { subject: feedRow(subject), after: null }).catch(\n () => undefined,\n ),\n ),\n ])\n\n return json({ ok: true, set: set.length, removed: removals.length }, 200)\n}\n\nasync function handler(request: Request): Promise<Response> {\n try {\n if (request.method === 'GET') return await handleGet(request)\n if (request.method === 'POST') return await handlePost(request)\n return json({ error: 'Method not allowed' }, 405)\n } catch (error) {\n // A refused credential is a 401, not a fault of ours (AGL-1993).\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error('[ai/allotments] failed', error)\n return json({ error: 'AI allotments unavailable' }, 500)\n }\n}\n\nexport { handler as GET, handler as POST }\n"],"names":["pluginRequestFromWeb","assistCreditsFromUsd","resolveAssistCreditBudget","hostRoleFor","isOrgWideMember","resolveEffectivePlan","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","memberHasOrgPermission","resolveOrgMembership","invalidIdTokenResponse","resolveOrgIdForHost","FieldValue","logAiAllotmentChanged","AI_ALLOTMENT_MAX_CREDITS","AI_ALLOTMENT_MAX_MODELS","aiAllotmentCredits","parseAiAllotmentSubject","aiUsageByUserMonthFrom","aiUsageMonthKeys","AI_MODEL_CATALOG","AI_MODEL_RESTRICTION_PLANS","resolveAiProvider","AI_HOST_CREDITS_FIELD","applyAiAllotmentWrites","listAiAllotments","seedAiHostMonthCredits","userAiUsageMonthRef","addAdminAudit","json","body","status","Response","MAX_WRITES","MAX_SITES","GET_ALL_CHUNK","authenticate","headers","authorization","idToken","startsWith","slice","length","undefined","error","decoded","app","auth","verifyIdToken","email_verified","uid","email","staff","isSiteAdminCollaborator","member","hostId","readRosterMonths","firestore","orgRef","month","roster","collection","get","members","docs","map","doc","$id","id","data","out","start","chunk","snapshots","getAll","forEach","snapshot","index","usage","exists","push","credits","byHost","memberName","String","displayName","trim","siteCredits","orgMonth","value","Number","isFinite","Math","floor","reduce","sum","entry","handleGet","request","params","orgSnapshot","query","subjectUid","orgId","membership","canManage","canView","siteAdmin","allowed","allotments","hostDocs","Promise","all","then","where","limit","org","byUid","Map","used","allotment","person","scope","rows","filter","subject","mode","models","visibleMembers","name","role","orgWide","hosts","provider","wire","pool","label","tier","restrictionAvailable","includes","canEdit","billing","collaborators","callerUid","CATALOG_IDS","Set","parseAllotmentWrite","raw","toLocaleString","Array","isArray","ids","Boolean","unknown","find","has","handlePost","payload","rawSet","rawRemove","rawEverySite","writes","parsed","code","removals","everySite","caller","touched","write","memberDocs","memberOf","set","hostNames","target","host","restricting","some","sites","site","remove","siteIds","siteId","catch","console","actorUid","actorEmail","action","before","after","removed","at","serverTimestamp","actor","feedRow","type","join","ok","handler","method","unauthenticated","GET","POST"],"mappings":"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"}
@@ -15,7 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */ import { checkEntitlement } from "@aglyn/aglyn/server";
18
- import { assistCreditsFromUsd, assistFreeTasteRefusalText, assistOwnControlRefusalText } from "@aglyn/aglyn/app-utils/assist-credits";
18
+ import { assistCreditsFromUsd, assistFreeTasteRefusalText, assistNotOnPlanText, assistOwnControlRefusalText } from "../usage/assist-credits.js";
19
19
  import { resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
20
20
  import { aiAllotmentRefusalText } from "../model/ai-allotments.js";
21
21
  import { resolveAiModelChoice } from "../providers/model-choice.js";
@@ -237,7 +237,7 @@ const ELEMENT_MAX_TOKENS = AI_ROUTING_TABLE['copy.element'].maxTokens;
237
237
  const entitled = checkEntitlement(org, 'aiAssist');
238
238
  if (!entitled) {
239
239
  return res.status(403).json({
240
- error: 'AI assist requires a Pro plan'
240
+ error: assistNotOnPlanText()
241
241
  });
242
242
  }
243
243
  // Org-scoped lockdown. The dispatcher already evaluates platform and user
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-assist.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 {\n checkEntitlement,\n type PluginApiHandler,\n type PluginApiResponse,\n} from '@aglyn/aglyn/server'\nimport {\n assistCreditsFromUsd,\n assistFreeTasteRefusalText,\n assistOwnControlRefusalText,\n} from '@aglyn/aglyn/app-utils/assist-credits'\nimport { resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { aiAllotmentRefusalText } from '../model/ai-allotments'\nimport { resolveAiModelChoice } from '../providers/model-choice'\nimport { aiUsageMeter } from '../usage/ai-usage-meter'\nimport {\n permissionRefusal,\n checkRateLimit,\n featureLockdownRefusal,\n firebaseAdmin,\n getOrgForUser,\n lockdownRefusal,\n memberHasPermissionOnHost,\n rateLimitHeaders,\n} from '@aglyn/tenant-data-admin'\nimport { aiOverageReservationRefusal } from '../billing/ai-overage-gate'\nimport { recordUserAiRefusal } from '../usage/ai-usage-by-user'\nimport {\n estimateAssistCostUsd,\n publicAssistQuota,\n recordAssistCost,\n releaseAssistMessage,\n reserveAssistMessage,\n type AssistReservation,\n} from '../usage/assist-usage'\nimport { logAiAssistSection } from '../activity/ai-activity'\n// By its own entry point rather than the barrel (AGL-2903): this handler's\n// spec replaces the barrel with a closed-world factory, and nothing replaces\n// the entry point, so the spec exercises the real request shape and the real\n// error boundary rather than a stub that agrees with the handler.\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n AiUpstreamError,\n aiProviderReady,\n runAiRequest,\n type AiResult,\n type AiTool,\n} from '../runtime/ai-runtime'\nimport {\n ASSIST_SECTION_TOOL_NAME,\n assistModeSystemBlocks,\n assistSectionTool,\n readAssistSection,\n type AiAssistMode,\n} from './ai-assist-prompts'\n// By its own entry point for the same reason (AGL-2925): the per-address\n// rung is real in the spec, keyed on whatever address the harness sends.\nimport { checkAiClientIpRateLimit } from '../runtime/ai-abuse-guards'\n\n/**\n * The model each mode is served by, through the routing table — tiered by\n * what the mode actually asks for (AGL-2486), not one model for all three.\n *\n * - **element** → the fast tier (`copy.element`). Rewriting one button label\n * or one heading to an instruction is a short, bounded transformation with\n * the source text supplied: the least model-shaped work on this route and\n * the highest-volume (a user clicking \"improve this\" runs it repeatedly on\n * the same element).\n * - **blog** → the balanced tier (`copy.blog`). A post body is real long-form\n * writing, and the difference between tiers is legible to the reader.\n * - **section** → the balanced tier (`copy.section`). A constrained-JSON node\n * subtree over the published-component allowlist is a structured-generation\n * task with a schema the sanitizer will reject if it is got wrong, and a\n * rejected generation costs the whole request rather than part of it.\n *\n * ⚠️ NO PROMPT CACHING IS LOST BY THE FAST RUNG. Each mode's static text now\n * carries a breakpoint (`assistModeSystemBlocks`), but none of the three is\n * long enough for its model to cache: the catalog states each model's\n * minimum and `runtime/ai-prompt-cache.spec.ts` measures every door against\n * it. That is why the tier drop is free here and NOT free on\n * `/api/assist/chat`, whose ~1,030-token prefix clears the balanced tier's\n * minimum and would stop caching entirely on the fast tier's — cheaper per\n * token, dearer per request, and invisible either way without that\n * measurement. See the header of `assist-chat.ts`.\n *\n * The cost meter prices by model id, so the value returned here MUST be the\n * one passed to `recordAssistCost` — same call, same variable. A tiering\n * change that updates the request and not the meter reports one tier's money\n * for another tier's tokens, which is the failure the by-model rate table\n * exists to stop.\n */\nexport function assistModelForMode(mode: AiAssistMode): string {\n return aiModelForStep(assistStepKindForMode(mode))\n}\n\n/** The routing table's step kind for each mode. */\nexport function assistStepKindForMode(\n mode: AiAssistMode,\n): 'copy.element' | 'copy.blog' | 'copy.section' {\n return mode === 'element' ? 'copy.element' : mode === 'blog' ? 'copy.blog' : 'copy.section'\n}\n\n/**\n * Per-uid burst limit, matched to `/api/assist/chat`'s 20/min so a client\n * cannot arbitrage the two assist doors against each other.\n */\nconst ASSIST_RATE_LIMIT = 20\nconst ASSIST_RATE_WINDOW_MS = 60_000\n\nconst SECTION_MAX_TOKENS = AI_ROUTING_TABLE['copy.section'].maxTokens\nconst BLOG_MAX_TOKENS = AI_ROUTING_TABLE['copy.blog'].maxTokens\nconst ELEMENT_MAX_TOKENS = AI_ROUTING_TABLE['copy.element'].maxTokens\n\n/**\n * What a `refusal` stop is answered with. The model declined the brief —\n * a policy verdict on the instruction, not a fault — so the sentence asks\n * for a different one rather than a retry. The tokens were spent and are\n * metered before this is sent.\n */\nconst REFUSED = {\n error: 'The AI declined that instruction — try a different one.',\n}\n\n/**\n * AI assist (AGL-89/130/169), the besigner copy assistant's door — the\n * section mode composes over the published-component allowlist and every\n * generated subtree passes the node-definition sanitizer a marketplace\n * install passes, before it reaches a canvas. `/api/ai/assist` is served by\n * the plugin API dispatcher under the `ai` prefix. Gated on a ready provider,\n * one registered with its key set (501 without, the same degrade pattern as\n * Stripe); auth via Firebase ID token.\n *\n * The provider call is `runAiRequest`, the runtime every AI door shares\n * (AGL-2903). This handler owns the ladder below and the response shapes;\n * `ai-assist-prompts.ts` owns the three prompts and the section tool; the\n * runtime owns the wire, the usage arithmetic and the error boundary. The\n * request keeps the model per mode, the `max_tokens` and the absent\n * `thinking` field it has always had — a fast-tier model rejects an explicit\n * one, and the balanced modes keep the model default.\n *\n * A section arrives through a strict tool (AGL-2937). It used to be asked\n * for as bare JSON and parsed: a parse that failed answered 502 with the\n * tokens already spent, so the whole request bought nothing and the member's\n * next move was to spend it again. The text parse is kept behind the tool,\n * for a provider whose adapter has none.\n *\n * ── The spend ladder (AGL-2073) ────────────────────────────────────────────\n *\n * This route reached the provider behind nothing but an entitlement check. Free\n * orgs could not get in, so the free tier was safe — but for any Pro org it\n * had no rate limit, no quota, no per-org cost telemetry, and it resolved the\n * org from the USER rather than the request, so someone with one paid org got\n * assist charged against whichever org `getOrgForUser` happened to return. One\n * subscription, unbounded and unmeasured provider spend.\n *\n * The ladder now, in order, each rung able to go red on its own:\n * 405 → 501 no ready provider → 401 no token → 400 bad body (the request\n * must NAME the org it is metered against) → 403 not a member of that org →\n * 423 the workspace's AI pause → 403 the member's role lacks `ai.use`, or\n * `ai.generate` for a section (AGL-2927; staff pass) → 403 no `aiAssist`\n * entitlement → 423 org lockdown → 429 rate limit → 429 quota → the model\n * call → meter.\n *\n * Two deliberate choices about which way each rung fails:\n *\n * - **The rate limiter fails SOFT and is not the spend bound.** It is a\n * per-instance in-memory window, so N serverless instances admit N × 20/min;\n * it smooths bursts and nothing more. Treating it as the cap would be the\n * fail-open this issue is about.\n * - **The reservation fails CLOSED.** It is the only global, atomic bound on\n * provider spend, so a reservation that cannot be taken — a Firestore\n * outage, a contended transaction — refuses the request with 503 rather than\n * calling the provider uncapped. An unavailable meter must stop the spending,\n * not wave it through.\n *\n * The reservation is `reserveAssistMessage` — the same shared counter the\n * console chat route moves (AGL-2057), not a second mechanism. That is what\n * closes the \"uncapped for paid\" hole the earlier audit named: an entitled org\n * carries the monthly runaway guard, and because both doors move the SAME\n * counter, an org's monthly assist budget is shared across them rather than\n * doubled by having two.\n *\n * Metering is `recordAssistCost`, the signal + rollup half of the assist\n * meters WITHOUT the verbatim half: what a customer types here is their own\n * site copy and blog bodies, and retaining that for 180 days is a data flow\n * the published privacy disclosure does not describe. The margin question is\n * answered by the signal alone.\n */\nexport const aiAssistHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n if (!aiProviderReady()) {\n return res\n .status(501)\n .json({ error: 'AI assist is not configured (no AI provider key).' })\n }\n\n const headers = req.headers as Partial<Record<string, string>>\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n const body = req.body ?? {}\n const text = String(body?.text ?? '').slice(0, 12000)\n const instruction = String(body?.instruction ?? '').slice(0, 500)\n // Modes (AGL-130): 'element' rewrites short copy; 'blog' writes or\n // improves markdown-lite entry bodies with title/excerpt context.\n const mode =\n body?.mode === 'blog'\n ? 'blog'\n : body?.mode === 'section'\n ? 'section'\n : 'element'\n const title = String(body?.title ?? '').slice(0, 200)\n const excerpt = String(body?.excerpt ?? '').slice(0, 500)\n // The request must NAME the org it is about to be metered against\n // (AGL-2073, the AGL-1934 rule applied to this door). Resolving the org\n // from the user alone charged a multi-org user's assist to whichever org\n // came back first — and handed their free workspaces a paid feature.\n const orgId = String(body?.orgId ?? '').trim()\n const hostId = String(body?.hostId ?? '')\n .trim()\n .slice(0, 128)\n if (!instruction) {\n return res.status(400).json({ error: 'Missing instruction' })\n }\n if (!orgId) {\n return res\n .status(400)\n .json({ error: 'Open a workspace before using AI assist' })\n }\n\n let reservation: AssistReservation | null = null\n let providerAnswered = false\n let firestore: FirebaseFirestore.Firestore | null = null\n\n try {\n const app = firebaseAdmin.app()\n firestore = app.firestore()\n const decoded = await app.auth().verifyIdToken(idToken)\n\n // Plan gate (AGL-469): AI assist is a Pro+ entitlement with real token\n // cost — resolve the caller's org and check it server-side; a plan-less\n // org resolves as `free` and is denied. Scoped to the NAMED org, so\n // membership of some other paid org is not a key to this one.\n const resolved = await getOrgForUser(decoded.uid, orgId)\n if (!resolved || resolved.orgId !== orgId) {\n return res\n .status(403)\n .json({ error: 'You are not a member of that organization' })\n }\n const org = resolved.org ?? {}\n\n // Permission (AGL-2927), directly after membership. The element and\n // blog modes are assistance and sell under `ai.use`; a generated\n // section is a generation and sells under `ai.generate`. Decided on the\n // caller's own axis — the org catalog for an org-wide member, the site\n // the body named for a collaborator, who is refused when it named none.\n // A fact about the caller, so it is answered before the plan is. Staff\n // pass, as at every other org route. Beside it, the workspace-scoped\n // pause on `ai-assist`: the dispatcher already applied the platform-wide\n // switch by path, before it knew which org the body would name.\n const staff = decoded['staff'] === true\n const aiPaused = await featureLockdownRefusal({\n feature: 'ai-assist',\n staff,\n orgId,\n })\n if (aiPaused) return forwardRefusal(res, aiPaused)\n const permission = mode === 'section' ? 'ai.generate' : 'ai.use'\n if (\n !staff &&\n !(await memberHasPermissionOnHost(orgId, hostId, resolved.member, permission))\n ) {\n return forwardRefusal(res, permissionRefusal(permission))\n }\n\n const entitled = checkEntitlement(org, 'aiAssist')\n if (!entitled) {\n return res.status(403).json({ error: 'AI assist requires a Pro plan' })\n }\n\n // Org-scoped lockdown. The dispatcher already evaluates platform and user\n // scope plus the `ai-assist` feature kill switch, but its org scope needs\n // a `hostId` this route's callers do not send — so a suspended workspace\n // kept spending. The org doc is already in hand, so the verdict is free.\n const locked = await lockdownRefusal({\n request: { method: req.method },\n staff,\n uid: decoded.uid,\n org: org as Record<string, unknown>,\n })\n if (locked) return forwardRefusal(res, locked)\n\n const rate = checkRateLimit(`ai-assist:${decoded.uid}`, {\n limit: ASSIST_RATE_LIMIT,\n windowMs: ASSIST_RATE_WINDOW_MS,\n })\n if (!rate.allowed) {\n for (const [name, value] of Object.entries(rateLimitHeaders(rate))) {\n res.setHeader(name, value)\n }\n return res\n .status(429)\n .json({ error: 'Too many AI requests — slow down a moment' })\n }\n // The same window keyed on the trusted-hop client address (AGL-2925):\n // accounts rotated behind one address share one budget.\n const ipRate = checkAiClientIpRateLimit(headers)\n if (ipRate && !ipRate.allowed) {\n for (const [name, value] of Object.entries(rateLimitHeaders(ipRate))) {\n res.setHeader(name, value)\n }\n return res\n .status(429)\n .json({ error: 'Too many AI requests — slow down a moment' })\n }\n\n // RESERVED, not merely checked (AGL-2057/2073). Read-and-increment in one\n // transaction, BEFORE a single token is spent, so N concurrent requests\n // cannot all read the same \"under the cap\" and a client that drops the\n // connection has already been counted.\n try {\n // `org` is the document `entitled` came from, and it is what makes\n // the plan's own assist band bind rather than the operator backstop\n // alone. Generative building is the dearest thing this route serves,\n // so the band it spends against has to be the one that was sold.\n reservation = await reserveAssistMessage(\n firestore,\n orgId,\n entitled,\n new Date(),\n org,\n // The allotments that apply (AGL-2942): the asker's own, theirs on\n // the canvas's site, and the site's.\n { uid: decoded.uid, hostId: hostId || null },\n )\n } catch (error) {\n // FAIL CLOSED. The reservation is the only global bound on what this\n // route can spend at the provider; if it cannot be taken there is no cap,\n // and no cap is worse than no answer.\n console.error('assist reservation failed', error)\n return res\n .status(503)\n .json({ error: 'AI assist is temporarily unavailable' })\n }\n // A refusal is the asker's as well as the workspace's (AGL-2928): their\n // month counts it beside the org counter the reservation moved.\n recordUserAiRefusal(firestore, orgId, decoded.uid, reservation)\n if (!reservation.allowed) {\n const refusedBy = reservation.refusedBy\n // A hard allotment (AGL-2942): the monthly line a manager drew for\n // the asker or the site, with the sentence naming who can raise it.\n if (refusedBy === 'allotment') {\n return res.status(429).json({\n error: aiAllotmentRefusalText(reservation.allotment?.refusal?.scope),\n reason: 'quota',\n quota: publicAssistQuota(reservation),\n meter: aiUsageMeter(reservation),\n })\n }\n // The org's own controls are a 402, not a 429: credits past the band\n // are for sale and this workspace either switched the sale off\n // (AGL-2653) or capped what it would buy (AGL-2898), so the sentence\n // names the control that refused — the same sentence the console\n // assistant gives, from the same helper. Every other refusal keeps\n // the 429.\n // AGLYN'S OWN OVERAGE GUARDS (AGL-3011): a card, a pause, this\n // month's ceiling or a settling balance, each with the status that\n // says whether the workspace can act on it. Before the workspace's\n // own controls, which answer only for the controls it set itself.\n const overage = aiOverageReservationRefusal(reservation)\n if (overage) {\n return res.status(overage.status).json({\n error: overage.text,\n reason: 'quota',\n quota: publicAssistQuota(reservation),\n meter: aiUsageMeter(reservation),\n })\n }\n const ownControl = assistOwnControlRefusalText(org, refusedBy)\n return res.status(ownControl ? 402 : 429).json({\n error:\n ownControl ??\n // The Free taste's own precautions (AGL-2925), in the sentences\n // the console assistant uses.\n assistFreeTasteRefusalText(refusedBy) ??\n 'This workspace reached its AI assist limit for the month — contact support if you need a higher cap',\n // CREDITS, never the reservation itself — see `publicAssistQuota`.\n quota: publicAssistQuota(reservation),\n meter: aiUsageMeter(reservation),\n })\n }\n\n const tier: 'free' | 'entitled' = entitled ? 'entitled' : 'free'\n // ONE resolution, read by both the request and the meter — see\n // `assistModelForMode`. Two call sites would let them drift. A model the\n // asker picked (AGL-2942) replaces the routing table's answer only when\n // the plan, the org's restriction and the allotment allowlists all allow\n // it; anything else runs on Auto.\n const held = reservation\n const choice = resolveAiModelChoice(assistStepKindForMode(mode), body?.model, {\n plan: resolveEffectivePlan(org),\n allotmentModels: held.allotment?.models ?? null,\n orgModels: held.allotment?.orgModels ?? null,\n })\n const model = choice?.model ?? assistModelForMode(mode)\n /** The usage strip's envelope for an answered request (AGL-2942). */\n const meterFor = (result: AiResult) =>\n aiUsageMeter(held, {\n lastCredits: assistCreditsFromUsd(estimateAssistCostUsd(result.usage, model)),\n model: { id: model, auto: choice?.auto ?? true },\n })\n /** Meter what the provider actually charged us for, per org. */\n // The account the reservation drew on (AGL-2925), read once here: the\n // closure below runs after `reservation` may have been cleared by a\n // refund, and a refund never reaches the meter.\n const free = reservation?.free ?? null\n const meter = (result: AiResult): Promise<unknown> =>\n recordAssistCost(firestore, orgId, {\n route: `/api/ai/assist/${mode}`,\n hostId: hostId || null,\n model,\n tier,\n usage: result.usage,\n docsPaths: [],\n stopReason: result.stopReason,\n free,\n }).catch((error) => console.error('assist cost metering failed', error))\n\n /**\n * One provider call, with the refund decided at the boundary: a refused\n * request (a non-2xx answer) spent nothing, so the reservation goes back\n * — against the keys it RECORDED, never against \"now\" — and the caller\n * gets the runtime's fixed sentence, never the provider's (AGL-2815).\n * A result, even a refusal, means the tokens ARE spent: every exit after\n * it meters and none refunds. Anything else thrown never reached the\n * provider and is left to the outer catch, which refunds too.\n */\n const ask = async (\n content: string,\n maxTokens: number,\n tools?: AiTool[],\n ): Promise<AiResult | null> => {\n try {\n const result = await runAiRequest({\n model,\n maxTokens,\n stream: false,\n // The mode's prompt with the acceptable-use rules ahead of it\n // (AGL-2925), from `ai-assist-prompts.ts` so a spec can read what\n // this route sends without standing up the route.\n system: assistModeSystemBlocks(mode),\n messages: [{ role: 'user', content }],\n ...(tools ? { tools } : {}),\n })\n providerAnswered = true\n await meter(result)\n return result\n } catch (error) {\n if (!(error instanceof AiUpstreamError)) throw error\n providerAnswered = true\n await releaseReservation(firestore, orgId, reservation as AssistReservation)\n reservation = null\n res.status(502).json({ error: error.message })\n return null\n }\n }\n\n // Generate section (AGL-169): a constrained-JSON node subtree over the\n // marketplace component allowlist; the response passes the same\n // sanitizer as marketplace installs before it ever reaches a canvas.\n if (mode === 'section') {\n const result = await ask(\n `Section to design: ${instruction}`,\n SECTION_MAX_TOKENS,\n [assistSectionTool()],\n )\n if (!result) return\n if (result.kind === 'refusal') {\n return res.status(502).json({ ...REFUSED, meter: meterFor(result) })\n }\n const read = readAssistSection(result)\n if (read.status !== 'ok') {\n // Unreadable is the provider's fault and rejected is the answer's,\n // the two statuses this route has always drawn that line between.\n return res.status(read.status === 'unreadable' ? 502 : 422).json({ error: read.error })\n }\n // A subtree is going back (AGL-2929): the act, its size and its site in\n // the customer's feed — never the instruction or the copy generated.\n await logAiAssistSection(\n { uid: decoded.uid, email: decoded.email ?? null },\n {\n orgId,\n hostId: hostId || null,\n nodeCount: Object.keys(read.section.nodes).length,\n },\n )\n return res.status(200).json({ section: read.section, meter: meterFor(result) })\n }\n\n const result = await ask(\n mode === 'blog'\n ? [\n title && `Title: ${title}`,\n excerpt && `Excerpt: ${excerpt}`,\n text && `Current body:\\n${text}`,\n `Instruction: ${instruction}`,\n ]\n .filter(Boolean)\n .join('\\n\\n')\n : text\n ? `Current element text:\\n${text}\\n\\nInstruction: ${instruction}`\n : `Write website element text. Instruction: ${instruction}`,\n mode === 'blog' ? BLOG_MAX_TOKENS : ELEMENT_MAX_TOKENS,\n )\n if (!result) return\n if (result.kind === 'refusal') {\n return res.status(502).json({ ...REFUSED, meter: meterFor(result) })\n }\n const output = result.text.trim()\n if (!output) {\n return res.status(502).json({ error: 'Empty AI response', meter: meterFor(result) })\n }\n return res.status(200).json({ text: output, meter: meterFor(result) })\n } catch (error) {\n console.error(error)\n // The provider was never reached — a token verification failure, a DNS\n // error, a socket reset before any response. Nothing was spent, so the\n // reservation must not be. If the provider DID answer, the tokens are\n // real and the reservation stays consumed however the request ends.\n if (firestore && reservation && !providerAnswered) {\n await releaseReservation(firestore, orgId, reservation)\n }\n return res.status(500).json({ error: 'Assist failed' })\n }\n}\n\n/**\n * Hand a reservation back, never letting the refund itself fail the request.\n * Released against the keys the RESERVATION recorded — a reservation taken at\n * 23:59:59 and released at 00:00:01 must not credit the next day.\n */\nasync function releaseReservation(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n reservation: AssistReservation,\n): Promise<void> {\n await releaseAssistMessage(firestore, orgId, reservation).catch((error) =>\n console.error('assist reservation release failed', error),\n )\n}\n\n/** Re-emit a lockdown `Response` through the plugin API's res object. */\nasync function forwardRefusal(\n res: PluginApiResponse,\n refusal: Response,\n): Promise<void> {\n const retryAfter = refusal.headers.get('Retry-After')\n if (retryAfter) res.setHeader('Retry-After', retryAfter)\n const body = await refusal.json().catch(() => ({ error: 'locked' }))\n res.status(refusal.status).json(body)\n}\n"],"names":["checkEntitlement","assistCreditsFromUsd","assistFreeTasteRefusalText","assistOwnControlRefusalText","resolveEffectivePlan","aiAllotmentRefusalText","resolveAiModelChoice","aiUsageMeter","permissionRefusal","checkRateLimit","featureLockdownRefusal","firebaseAdmin","getOrgForUser","lockdownRefusal","memberHasPermissionOnHost","rateLimitHeaders","aiOverageReservationRefusal","recordUserAiRefusal","estimateAssistCostUsd","publicAssistQuota","recordAssistCost","releaseAssistMessage","reserveAssistMessage","logAiAssistSection","AI_ROUTING_TABLE","aiModelForStep","AiUpstreamError","aiProviderReady","runAiRequest","assistModeSystemBlocks","assistSectionTool","readAssistSection","checkAiClientIpRateLimit","assistModelForMode","mode","assistStepKindForMode","ASSIST_RATE_LIMIT","ASSIST_RATE_WINDOW_MS","SECTION_MAX_TOKENS","maxTokens","BLOG_MAX_TOKENS","ELEMENT_MAX_TOKENS","REFUSED","error","aiAssistHandler","req","res","headers","method","status","json","authorization","idToken","startsWith","slice","length","undefined","body","text","String","instruction","title","excerpt","orgId","trim","hostId","reservation","providerAnswered","firestore","resolved","held","app","decoded","auth","verifyIdToken","uid","org","staff","aiPaused","feature","forwardRefusal","permission","member","entitled","locked","request","rate","limit","windowMs","allowed","name","value","Object","entries","setHeader","ipRate","Date","console","ownControl","refusedBy","allotment","refusal","scope","reason","quota","meter","overage","tier","choice","model","plan","allotmentModels","models","orgModels","meterFor","result","lastCredits","usage","id","auto","free","route","docsPaths","stopReason","catch","ask","content","tools","stream","system","messages","role","releaseReservation","message","kind","read","email","nodeCount","keys","section","nodes","filter","Boolean","join","output","retryAfter","get"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-assist.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 {\n checkEntitlement,\n type PluginApiHandler,\n type PluginApiResponse,\n} from '@aglyn/aglyn/server'\nimport {\n assistCreditsFromUsd,\n assistFreeTasteRefusalText,\n assistNotOnPlanText,\n assistOwnControlRefusalText,\n} from '../usage/assist-credits'\nimport { resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { aiAllotmentRefusalText } from '../model/ai-allotments'\nimport { resolveAiModelChoice } from '../providers/model-choice'\nimport { aiUsageMeter } from '../usage/ai-usage-meter'\nimport {\n permissionRefusal,\n checkRateLimit,\n featureLockdownRefusal,\n firebaseAdmin,\n getOrgForUser,\n lockdownRefusal,\n memberHasPermissionOnHost,\n rateLimitHeaders,\n} from '@aglyn/tenant-data-admin'\nimport { aiOverageReservationRefusal } from '../billing/ai-overage-gate'\nimport { recordUserAiRefusal } from '../usage/ai-usage-by-user'\nimport {\n estimateAssistCostUsd,\n publicAssistQuota,\n recordAssistCost,\n releaseAssistMessage,\n reserveAssistMessage,\n type AssistReservation,\n} from '../usage/assist-usage'\nimport { logAiAssistSection } from '../activity/ai-activity'\n// By its own entry point rather than the barrel (AGL-2903): this handler's\n// spec replaces the barrel with a closed-world factory, and nothing replaces\n// the entry point, so the spec exercises the real request shape and the real\n// error boundary rather than a stub that agrees with the handler.\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n AiUpstreamError,\n aiProviderReady,\n runAiRequest,\n type AiResult,\n type AiTool,\n} from '../runtime/ai-runtime'\nimport {\n ASSIST_SECTION_TOOL_NAME,\n assistModeSystemBlocks,\n assistSectionTool,\n readAssistSection,\n type AiAssistMode,\n} from './ai-assist-prompts'\n// By its own entry point for the same reason (AGL-2925): the per-address\n// rung is real in the spec, keyed on whatever address the harness sends.\nimport { checkAiClientIpRateLimit } from '../runtime/ai-abuse-guards'\n\n/**\n * The model each mode is served by, through the routing table — tiered by\n * what the mode actually asks for (AGL-2486), not one model for all three.\n *\n * - **element** → the fast tier (`copy.element`). Rewriting one button label\n * or one heading to an instruction is a short, bounded transformation with\n * the source text supplied: the least model-shaped work on this route and\n * the highest-volume (a user clicking \"improve this\" runs it repeatedly on\n * the same element).\n * - **blog** → the balanced tier (`copy.blog`). A post body is real long-form\n * writing, and the difference between tiers is legible to the reader.\n * - **section** → the balanced tier (`copy.section`). A constrained-JSON node\n * subtree over the published-component allowlist is a structured-generation\n * task with a schema the sanitizer will reject if it is got wrong, and a\n * rejected generation costs the whole request rather than part of it.\n *\n * ⚠️ NO PROMPT CACHING IS LOST BY THE FAST RUNG. Each mode's static text now\n * carries a breakpoint (`assistModeSystemBlocks`), but none of the three is\n * long enough for its model to cache: the catalog states each model's\n * minimum and `runtime/ai-prompt-cache.spec.ts` measures every door against\n * it. That is why the tier drop is free here and NOT free on\n * `/api/assist/chat`, whose ~1,030-token prefix clears the balanced tier's\n * minimum and would stop caching entirely on the fast tier's — cheaper per\n * token, dearer per request, and invisible either way without that\n * measurement. See the header of `assist-chat.ts`.\n *\n * The cost meter prices by model id, so the value returned here MUST be the\n * one passed to `recordAssistCost` — same call, same variable. A tiering\n * change that updates the request and not the meter reports one tier's money\n * for another tier's tokens, which is the failure the by-model rate table\n * exists to stop.\n */\nexport function assistModelForMode(mode: AiAssistMode): string {\n return aiModelForStep(assistStepKindForMode(mode))\n}\n\n/** The routing table's step kind for each mode. */\nexport function assistStepKindForMode(\n mode: AiAssistMode,\n): 'copy.element' | 'copy.blog' | 'copy.section' {\n return mode === 'element' ? 'copy.element' : mode === 'blog' ? 'copy.blog' : 'copy.section'\n}\n\n/**\n * Per-uid burst limit, matched to `/api/assist/chat`'s 20/min so a client\n * cannot arbitrage the two assist doors against each other.\n */\nconst ASSIST_RATE_LIMIT = 20\nconst ASSIST_RATE_WINDOW_MS = 60_000\n\nconst SECTION_MAX_TOKENS = AI_ROUTING_TABLE['copy.section'].maxTokens\nconst BLOG_MAX_TOKENS = AI_ROUTING_TABLE['copy.blog'].maxTokens\nconst ELEMENT_MAX_TOKENS = AI_ROUTING_TABLE['copy.element'].maxTokens\n\n/**\n * What a `refusal` stop is answered with. The model declined the brief —\n * a policy verdict on the instruction, not a fault — so the sentence asks\n * for a different one rather than a retry. The tokens were spent and are\n * metered before this is sent.\n */\nconst REFUSED = {\n error: 'The AI declined that instruction — try a different one.',\n}\n\n/**\n * AI assist (AGL-89/130/169), the besigner copy assistant's door — the\n * section mode composes over the published-component allowlist and every\n * generated subtree passes the node-definition sanitizer a marketplace\n * install passes, before it reaches a canvas. `/api/ai/assist` is served by\n * the plugin API dispatcher under the `ai` prefix. Gated on a ready provider,\n * one registered with its key set (501 without, the same degrade pattern as\n * Stripe); auth via Firebase ID token.\n *\n * The provider call is `runAiRequest`, the runtime every AI door shares\n * (AGL-2903). This handler owns the ladder below and the response shapes;\n * `ai-assist-prompts.ts` owns the three prompts and the section tool; the\n * runtime owns the wire, the usage arithmetic and the error boundary. The\n * request keeps the model per mode, the `max_tokens` and the absent\n * `thinking` field it has always had — a fast-tier model rejects an explicit\n * one, and the balanced modes keep the model default.\n *\n * A section arrives through a strict tool (AGL-2937). It used to be asked\n * for as bare JSON and parsed: a parse that failed answered 502 with the\n * tokens already spent, so the whole request bought nothing and the member's\n * next move was to spend it again. The text parse is kept behind the tool,\n * for a provider whose adapter has none.\n *\n * ── The spend ladder (AGL-2073) ────────────────────────────────────────────\n *\n * This route reached the provider behind nothing but an entitlement check. Free\n * orgs could not get in, so the free tier was safe — but for any Pro org it\n * had no rate limit, no quota, no per-org cost telemetry, and it resolved the\n * org from the USER rather than the request, so someone with one paid org got\n * assist charged against whichever org `getOrgForUser` happened to return. One\n * subscription, unbounded and unmeasured provider spend.\n *\n * The ladder now, in order, each rung able to go red on its own:\n * 405 → 501 no ready provider → 401 no token → 400 bad body (the request\n * must NAME the org it is metered against) → 403 not a member of that org →\n * 423 the workspace's AI pause → 403 the member's role lacks `ai.use`, or\n * `ai.generate` for a section (AGL-2927; staff pass) → 403 no `aiAssist`\n * entitlement → 423 org lockdown → 429 rate limit → 429 quota → the model\n * call → meter.\n *\n * Two deliberate choices about which way each rung fails:\n *\n * - **The rate limiter fails SOFT and is not the spend bound.** It is a\n * per-instance in-memory window, so N serverless instances admit N × 20/min;\n * it smooths bursts and nothing more. Treating it as the cap would be the\n * fail-open this issue is about.\n * - **The reservation fails CLOSED.** It is the only global, atomic bound on\n * provider spend, so a reservation that cannot be taken — a Firestore\n * outage, a contended transaction — refuses the request with 503 rather than\n * calling the provider uncapped. An unavailable meter must stop the spending,\n * not wave it through.\n *\n * The reservation is `reserveAssistMessage` — the same shared counter the\n * console chat route moves (AGL-2057), not a second mechanism. That is what\n * closes the \"uncapped for paid\" hole the earlier audit named: an entitled org\n * carries the monthly runaway guard, and because both doors move the SAME\n * counter, an org's monthly assist budget is shared across them rather than\n * doubled by having two.\n *\n * Metering is `recordAssistCost`, the signal + rollup half of the assist\n * meters WITHOUT the verbatim half: what a customer types here is their own\n * site copy and blog bodies, and retaining that for 180 days is a data flow\n * the published privacy disclosure does not describe. The margin question is\n * answered by the signal alone.\n */\nexport const aiAssistHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n if (!aiProviderReady()) {\n return res\n .status(501)\n .json({ error: 'AI assist is not configured (no AI provider key).' })\n }\n\n const headers = req.headers as Partial<Record<string, string>>\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n const body = req.body ?? {}\n const text = String(body?.text ?? '').slice(0, 12000)\n const instruction = String(body?.instruction ?? '').slice(0, 500)\n // Modes (AGL-130): 'element' rewrites short copy; 'blog' writes or\n // improves markdown-lite entry bodies with title/excerpt context.\n const mode =\n body?.mode === 'blog'\n ? 'blog'\n : body?.mode === 'section'\n ? 'section'\n : 'element'\n const title = String(body?.title ?? '').slice(0, 200)\n const excerpt = String(body?.excerpt ?? '').slice(0, 500)\n // The request must NAME the org it is about to be metered against\n // (AGL-2073, the AGL-1934 rule applied to this door). Resolving the org\n // from the user alone charged a multi-org user's assist to whichever org\n // came back first — and handed their free workspaces a paid feature.\n const orgId = String(body?.orgId ?? '').trim()\n const hostId = String(body?.hostId ?? '')\n .trim()\n .slice(0, 128)\n if (!instruction) {\n return res.status(400).json({ error: 'Missing instruction' })\n }\n if (!orgId) {\n return res\n .status(400)\n .json({ error: 'Open a workspace before using AI assist' })\n }\n\n let reservation: AssistReservation | null = null\n let providerAnswered = false\n let firestore: FirebaseFirestore.Firestore | null = null\n\n try {\n const app = firebaseAdmin.app()\n firestore = app.firestore()\n const decoded = await app.auth().verifyIdToken(idToken)\n\n // Plan gate (AGL-469): AI assist is a Pro+ entitlement with real token\n // cost — resolve the caller's org and check it server-side; a plan-less\n // org resolves as `free` and is denied. Scoped to the NAMED org, so\n // membership of some other paid org is not a key to this one.\n const resolved = await getOrgForUser(decoded.uid, orgId)\n if (!resolved || resolved.orgId !== orgId) {\n return res\n .status(403)\n .json({ error: 'You are not a member of that organization' })\n }\n const org = resolved.org ?? {}\n\n // Permission (AGL-2927), directly after membership. The element and\n // blog modes are assistance and sell under `ai.use`; a generated\n // section is a generation and sells under `ai.generate`. Decided on the\n // caller's own axis — the org catalog for an org-wide member, the site\n // the body named for a collaborator, who is refused when it named none.\n // A fact about the caller, so it is answered before the plan is. Staff\n // pass, as at every other org route. Beside it, the workspace-scoped\n // pause on `ai-assist`: the dispatcher already applied the platform-wide\n // switch by path, before it knew which org the body would name.\n const staff = decoded['staff'] === true\n const aiPaused = await featureLockdownRefusal({\n feature: 'ai-assist',\n staff,\n orgId,\n })\n if (aiPaused) return forwardRefusal(res, aiPaused)\n const permission = mode === 'section' ? 'ai.generate' : 'ai.use'\n if (\n !staff &&\n !(await memberHasPermissionOnHost(orgId, hostId, resolved.member, permission))\n ) {\n return forwardRefusal(res, permissionRefusal(permission))\n }\n\n const entitled = checkEntitlement(org, 'aiAssist')\n if (!entitled) {\n return res.status(403).json({ error: assistNotOnPlanText() })\n }\n\n // Org-scoped lockdown. The dispatcher already evaluates platform and user\n // scope plus the `ai-assist` feature kill switch, but its org scope needs\n // a `hostId` this route's callers do not send — so a suspended workspace\n // kept spending. The org doc is already in hand, so the verdict is free.\n const locked = await lockdownRefusal({\n request: { method: req.method },\n staff,\n uid: decoded.uid,\n org: org as Record<string, unknown>,\n })\n if (locked) return forwardRefusal(res, locked)\n\n const rate = checkRateLimit(`ai-assist:${decoded.uid}`, {\n limit: ASSIST_RATE_LIMIT,\n windowMs: ASSIST_RATE_WINDOW_MS,\n })\n if (!rate.allowed) {\n for (const [name, value] of Object.entries(rateLimitHeaders(rate))) {\n res.setHeader(name, value)\n }\n return res\n .status(429)\n .json({ error: 'Too many AI requests — slow down a moment' })\n }\n // The same window keyed on the trusted-hop client address (AGL-2925):\n // accounts rotated behind one address share one budget.\n const ipRate = checkAiClientIpRateLimit(headers)\n if (ipRate && !ipRate.allowed) {\n for (const [name, value] of Object.entries(rateLimitHeaders(ipRate))) {\n res.setHeader(name, value)\n }\n return res\n .status(429)\n .json({ error: 'Too many AI requests — slow down a moment' })\n }\n\n // RESERVED, not merely checked (AGL-2057/2073). Read-and-increment in one\n // transaction, BEFORE a single token is spent, so N concurrent requests\n // cannot all read the same \"under the cap\" and a client that drops the\n // connection has already been counted.\n try {\n // `org` is the document `entitled` came from, and it is what makes\n // the plan's own assist band bind rather than the operator backstop\n // alone. Generative building is the dearest thing this route serves,\n // so the band it spends against has to be the one that was sold.\n reservation = await reserveAssistMessage(\n firestore,\n orgId,\n entitled,\n new Date(),\n org,\n // The allotments that apply (AGL-2942): the asker's own, theirs on\n // the canvas's site, and the site's.\n { uid: decoded.uid, hostId: hostId || null },\n )\n } catch (error) {\n // FAIL CLOSED. The reservation is the only global bound on what this\n // route can spend at the provider; if it cannot be taken there is no cap,\n // and no cap is worse than no answer.\n console.error('assist reservation failed', error)\n return res\n .status(503)\n .json({ error: 'AI assist is temporarily unavailable' })\n }\n // A refusal is the asker's as well as the workspace's (AGL-2928): their\n // month counts it beside the org counter the reservation moved.\n recordUserAiRefusal(firestore, orgId, decoded.uid, reservation)\n if (!reservation.allowed) {\n const refusedBy = reservation.refusedBy\n // A hard allotment (AGL-2942): the monthly line a manager drew for\n // the asker or the site, with the sentence naming who can raise it.\n if (refusedBy === 'allotment') {\n return res.status(429).json({\n error: aiAllotmentRefusalText(reservation.allotment?.refusal?.scope),\n reason: 'quota',\n quota: publicAssistQuota(reservation),\n meter: aiUsageMeter(reservation),\n })\n }\n // The org's own controls are a 402, not a 429: credits past the band\n // are for sale and this workspace either switched the sale off\n // (AGL-2653) or capped what it would buy (AGL-2898), so the sentence\n // names the control that refused — the same sentence the console\n // assistant gives, from the same helper. Every other refusal keeps\n // the 429.\n // AGLYN'S OWN OVERAGE GUARDS (AGL-3011): a card, a pause, this\n // month's ceiling or a settling balance, each with the status that\n // says whether the workspace can act on it. Before the workspace's\n // own controls, which answer only for the controls it set itself.\n const overage = aiOverageReservationRefusal(reservation)\n if (overage) {\n return res.status(overage.status).json({\n error: overage.text,\n reason: 'quota',\n quota: publicAssistQuota(reservation),\n meter: aiUsageMeter(reservation),\n })\n }\n const ownControl = assistOwnControlRefusalText(org, refusedBy)\n return res.status(ownControl ? 402 : 429).json({\n error:\n ownControl ??\n // The Free taste's own precautions (AGL-2925), in the sentences\n // the console assistant uses.\n assistFreeTasteRefusalText(refusedBy) ??\n 'This workspace reached its AI assist limit for the month — contact support if you need a higher cap',\n // CREDITS, never the reservation itself — see `publicAssistQuota`.\n quota: publicAssistQuota(reservation),\n meter: aiUsageMeter(reservation),\n })\n }\n\n const tier: 'free' | 'entitled' = entitled ? 'entitled' : 'free'\n // ONE resolution, read by both the request and the meter — see\n // `assistModelForMode`. Two call sites would let them drift. A model the\n // asker picked (AGL-2942) replaces the routing table's answer only when\n // the plan, the org's restriction and the allotment allowlists all allow\n // it; anything else runs on Auto.\n const held = reservation\n const choice = resolveAiModelChoice(assistStepKindForMode(mode), body?.model, {\n plan: resolveEffectivePlan(org),\n allotmentModels: held.allotment?.models ?? null,\n orgModels: held.allotment?.orgModels ?? null,\n })\n const model = choice?.model ?? assistModelForMode(mode)\n /** The usage strip's envelope for an answered request (AGL-2942). */\n const meterFor = (result: AiResult) =>\n aiUsageMeter(held, {\n lastCredits: assistCreditsFromUsd(estimateAssistCostUsd(result.usage, model)),\n model: { id: model, auto: choice?.auto ?? true },\n })\n /** Meter what the provider actually charged us for, per org. */\n // The account the reservation drew on (AGL-2925), read once here: the\n // closure below runs after `reservation` may have been cleared by a\n // refund, and a refund never reaches the meter.\n const free = reservation?.free ?? null\n const meter = (result: AiResult): Promise<unknown> =>\n recordAssistCost(firestore, orgId, {\n route: `/api/ai/assist/${mode}`,\n hostId: hostId || null,\n model,\n tier,\n usage: result.usage,\n docsPaths: [],\n stopReason: result.stopReason,\n free,\n }).catch((error) => console.error('assist cost metering failed', error))\n\n /**\n * One provider call, with the refund decided at the boundary: a refused\n * request (a non-2xx answer) spent nothing, so the reservation goes back\n * — against the keys it RECORDED, never against \"now\" — and the caller\n * gets the runtime's fixed sentence, never the provider's (AGL-2815).\n * A result, even a refusal, means the tokens ARE spent: every exit after\n * it meters and none refunds. Anything else thrown never reached the\n * provider and is left to the outer catch, which refunds too.\n */\n const ask = async (\n content: string,\n maxTokens: number,\n tools?: AiTool[],\n ): Promise<AiResult | null> => {\n try {\n const result = await runAiRequest({\n model,\n maxTokens,\n stream: false,\n // The mode's prompt with the acceptable-use rules ahead of it\n // (AGL-2925), from `ai-assist-prompts.ts` so a spec can read what\n // this route sends without standing up the route.\n system: assistModeSystemBlocks(mode),\n messages: [{ role: 'user', content }],\n ...(tools ? { tools } : {}),\n })\n providerAnswered = true\n await meter(result)\n return result\n } catch (error) {\n if (!(error instanceof AiUpstreamError)) throw error\n providerAnswered = true\n await releaseReservation(firestore, orgId, reservation as AssistReservation)\n reservation = null\n res.status(502).json({ error: error.message })\n return null\n }\n }\n\n // Generate section (AGL-169): a constrained-JSON node subtree over the\n // marketplace component allowlist; the response passes the same\n // sanitizer as marketplace installs before it ever reaches a canvas.\n if (mode === 'section') {\n const result = await ask(\n `Section to design: ${instruction}`,\n SECTION_MAX_TOKENS,\n [assistSectionTool()],\n )\n if (!result) return\n if (result.kind === 'refusal') {\n return res.status(502).json({ ...REFUSED, meter: meterFor(result) })\n }\n const read = readAssistSection(result)\n if (read.status !== 'ok') {\n // Unreadable is the provider's fault and rejected is the answer's,\n // the two statuses this route has always drawn that line between.\n return res.status(read.status === 'unreadable' ? 502 : 422).json({ error: read.error })\n }\n // A subtree is going back (AGL-2929): the act, its size and its site in\n // the customer's feed — never the instruction or the copy generated.\n await logAiAssistSection(\n { uid: decoded.uid, email: decoded.email ?? null },\n {\n orgId,\n hostId: hostId || null,\n nodeCount: Object.keys(read.section.nodes).length,\n },\n )\n return res.status(200).json({ section: read.section, meter: meterFor(result) })\n }\n\n const result = await ask(\n mode === 'blog'\n ? [\n title && `Title: ${title}`,\n excerpt && `Excerpt: ${excerpt}`,\n text && `Current body:\\n${text}`,\n `Instruction: ${instruction}`,\n ]\n .filter(Boolean)\n .join('\\n\\n')\n : text\n ? `Current element text:\\n${text}\\n\\nInstruction: ${instruction}`\n : `Write website element text. Instruction: ${instruction}`,\n mode === 'blog' ? BLOG_MAX_TOKENS : ELEMENT_MAX_TOKENS,\n )\n if (!result) return\n if (result.kind === 'refusal') {\n return res.status(502).json({ ...REFUSED, meter: meterFor(result) })\n }\n const output = result.text.trim()\n if (!output) {\n return res.status(502).json({ error: 'Empty AI response', meter: meterFor(result) })\n }\n return res.status(200).json({ text: output, meter: meterFor(result) })\n } catch (error) {\n console.error(error)\n // The provider was never reached — a token verification failure, a DNS\n // error, a socket reset before any response. Nothing was spent, so the\n // reservation must not be. If the provider DID answer, the tokens are\n // real and the reservation stays consumed however the request ends.\n if (firestore && reservation && !providerAnswered) {\n await releaseReservation(firestore, orgId, reservation)\n }\n return res.status(500).json({ error: 'Assist failed' })\n }\n}\n\n/**\n * Hand a reservation back, never letting the refund itself fail the request.\n * Released against the keys the RESERVATION recorded — a reservation taken at\n * 23:59:59 and released at 00:00:01 must not credit the next day.\n */\nasync function releaseReservation(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n reservation: AssistReservation,\n): Promise<void> {\n await releaseAssistMessage(firestore, orgId, reservation).catch((error) =>\n console.error('assist reservation release failed', error),\n )\n}\n\n/** Re-emit a lockdown `Response` through the plugin API's res object. */\nasync function forwardRefusal(\n res: PluginApiResponse,\n refusal: Response,\n): Promise<void> {\n const retryAfter = refusal.headers.get('Retry-After')\n if (retryAfter) res.setHeader('Retry-After', retryAfter)\n const body = await refusal.json().catch(() => ({ error: 'locked' }))\n res.status(refusal.status).json(body)\n}\n"],"names":["checkEntitlement","assistCreditsFromUsd","assistFreeTasteRefusalText","assistNotOnPlanText","assistOwnControlRefusalText","resolveEffectivePlan","aiAllotmentRefusalText","resolveAiModelChoice","aiUsageMeter","permissionRefusal","checkRateLimit","featureLockdownRefusal","firebaseAdmin","getOrgForUser","lockdownRefusal","memberHasPermissionOnHost","rateLimitHeaders","aiOverageReservationRefusal","recordUserAiRefusal","estimateAssistCostUsd","publicAssistQuota","recordAssistCost","releaseAssistMessage","reserveAssistMessage","logAiAssistSection","AI_ROUTING_TABLE","aiModelForStep","AiUpstreamError","aiProviderReady","runAiRequest","assistModeSystemBlocks","assistSectionTool","readAssistSection","checkAiClientIpRateLimit","assistModelForMode","mode","assistStepKindForMode","ASSIST_RATE_LIMIT","ASSIST_RATE_WINDOW_MS","SECTION_MAX_TOKENS","maxTokens","BLOG_MAX_TOKENS","ELEMENT_MAX_TOKENS","REFUSED","error","aiAssistHandler","req","res","headers","method","status","json","authorization","idToken","startsWith","slice","length","undefined","body","text","String","instruction","title","excerpt","orgId","trim","hostId","reservation","providerAnswered","firestore","resolved","held","app","decoded","auth","verifyIdToken","uid","org","staff","aiPaused","feature","forwardRefusal","permission","member","entitled","locked","request","rate","limit","windowMs","allowed","name","value","Object","entries","setHeader","ipRate","Date","console","ownControl","refusedBy","allotment","refusal","scope","reason","quota","meter","overage","tier","choice","model","plan","allotmentModels","models","orgModels","meterFor","result","lastCredits","usage","id","auto","free","route","docsPaths","stopReason","catch","ask","content","tools","stream","system","messages","role","releaseReservation","message","kind","read","email","nodeCount","keys","section","nodes","filter","Boolean","join","output","retryAfter","get"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,gBAAgB,QAGX,sBAAqB;AAC5B,SACEC,oBAAoB,EACpBC,0BAA0B,EAC1BC,mBAAmB,EACnBC,2BAA2B,QACtB,6BAAyB;AAChC,SAASC,oBAAoB,QAAQ,2CAA0C;AAC/E,SAASC,sBAAsB,QAAQ,4BAAwB;AAC/D,SAASC,oBAAoB,QAAQ,+BAA2B;AAChE,SAASC,YAAY,QAAQ,6BAAyB;AACtD,SACEC,iBAAiB,EACjBC,cAAc,EACdC,sBAAsB,EACtBC,aAAa,EACbC,aAAa,EACbC,eAAe,EACfC,yBAAyB,EACzBC,gBAAgB,QACX,2BAA0B;AACjC,SAASC,2BAA2B,QAAQ,gCAA4B;AACxE,SAASC,mBAAmB,QAAQ,+BAA2B;AAC/D,SACEC,qBAAqB,EACrBC,iBAAiB,EACjBC,gBAAgB,EAChBC,oBAAoB,EACpBC,oBAAoB,QAEf,2BAAuB;AAC9B,SAASC,kBAAkB,QAAQ,6BAAyB;AAC5D,2EAA2E;AAC3E,6EAA6E;AAC7E,6EAA6E;AAC7E,kEAAkE;AAClE,SAASC,gBAAgB,EAAEC,cAAc,QAAQ,0BAAsB;AACvE,SACEC,eAAe,EACfC,eAAe,EACfC,YAAY,QAGP,2BAAuB;AAC9B,SAEEC,sBAAsB,EACtBC,iBAAiB,EACjBC,iBAAiB,QAEZ,yBAAqB;AAC5B,yEAAyE;AACzE,yEAAyE;AACzE,SAASC,wBAAwB,QAAQ,gCAA4B;AAErE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BC,GACD,OAAO,SAASC,mBAAmBC,IAAkB;IACnD,OAAOT,eAAeU,sBAAsBD;AAC9C;AAEA,iDAAiD,GACjD,OAAO,SAASC,sBACdD,IAAkB;IAElB,OAAOA,SAAS,YAAY,iBAAiBA,SAAS,SAAS,cAAc;AAC/E;AAEA;;;CAGC,GACD,MAAME,oBAAoB;AAC1B,MAAMC,wBAAwB;AAE9B,MAAMC,qBAAqBd,gBAAgB,CAAC,eAAe,CAACe,SAAS;AACrE,MAAMC,kBAAkBhB,gBAAgB,CAAC,YAAY,CAACe,SAAS;AAC/D,MAAME,qBAAqBjB,gBAAgB,CAAC,eAAe,CAACe,SAAS;AAErE;;;;;CAKC,GACD,MAAMG,UAAU;IACdC,OAAO;AACT;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgEC,GACD,OAAO,MAAMC,kBAAoC,OAAOC,KAAKC;QAWrCC,wBAMTF;IAhBb,IAAIA,IAAIG,MAAM,KAAK,QAAQ;QACzB,OAAOF,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEP,OAAO;QAAqB;IAC5D;IACA,IAAI,CAAChB,mBAAmB;QACtB,OAAOmB,IACJG,MAAM,CAAC,KACPC,IAAI,CAAC;YAAEP,OAAO;QAAoD;IACvE;IAEA,MAAMI,UAAUF,IAAIE,OAAO;IAC3B,MAAMI,iBAAgBJ,yBAAAA,QAAQI,aAAa,YAArBJ,yBAAyB;IAC/C,MAAMK,UAAUD,cAAcE,UAAU,CAAC,aACrCF,cAAcG,KAAK,CAAC,UAAUC,MAAM,IACpCC;IACJ,IAAI,CAACJ,SAAS,OAAON,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEP,OAAO;IAAkB;IAErE,MAAMc,QAAOZ,YAAAA,IAAIY,IAAI,YAARZ,YAAY,CAAC;IAC1B,MAAMa,OAAOC,eAAOF,wBAAAA,KAAMC,IAAI,mBAAI,IAAIJ,KAAK,CAAC,GAAG;IAC/C,MAAMM,cAAcD,gBAAOF,wBAAAA,KAAMG,WAAW,oBAAI,IAAIN,KAAK,CAAC,GAAG;IAC7D,mEAAmE;IACnE,kEAAkE;IAClE,MAAMpB,OACJuB,CAAAA,wBAAAA,KAAMvB,IAAI,MAAK,SACX,SACAuB,CAAAA,wBAAAA,KAAMvB,IAAI,MAAK,YACb,YACA;IACR,MAAM2B,QAAQF,gBAAOF,wBAAAA,KAAMI,KAAK,oBAAI,IAAIP,KAAK,CAAC,GAAG;IACjD,MAAMQ,UAAUH,gBAAOF,wBAAAA,KAAMK,OAAO,oBAAI,IAAIR,KAAK,CAAC,GAAG;IACrD,kEAAkE;IAClE,wEAAwE;IACxE,yEAAyE;IACzE,qEAAqE;IACrE,MAAMS,QAAQJ,gBAAOF,wBAAAA,KAAMM,KAAK,oBAAI,IAAIC,IAAI;IAC5C,MAAMC,SAASN,gBAAOF,wBAAAA,KAAMQ,MAAM,oBAAI,IACnCD,IAAI,GACJV,KAAK,CAAC,GAAG;IACZ,IAAI,CAACM,aAAa;QAChB,OAAOd,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEP,OAAO;QAAsB;IAC7D;IACA,IAAI,CAACoB,OAAO;QACV,OAAOjB,IACJG,MAAM,CAAC,KACPC,IAAI,CAAC;YAAEP,OAAO;QAA0C;IAC7D;IAEA,IAAIuB,cAAwC;IAC5C,IAAIC,mBAAmB;IACvB,IAAIC,YAAgD;IAEpD,IAAI;YAeUC;YAwJOC,iBACNA;QAvKb,MAAMC,MAAM5D,cAAc4D,GAAG;QAC7BH,YAAYG,IAAIH,SAAS;QACzB,MAAMI,UAAU,MAAMD,IAAIE,IAAI,GAAGC,aAAa,CAACtB;QAE/C,uEAAuE;QACvE,wEAAwE;QACxE,oEAAoE;QACpE,8DAA8D;QAC9D,MAAMiB,WAAW,MAAMzD,cAAc4D,QAAQG,GAAG,EAAEZ;QAClD,IAAI,CAACM,YAAYA,SAASN,KAAK,KAAKA,OAAO;YACzC,OAAOjB,IACJG,MAAM,CAAC,KACPC,IAAI,CAAC;gBAAEP,OAAO;YAA4C;QAC/D;QACA,MAAMiC,OAAMP,gBAAAA,SAASO,GAAG,YAAZP,gBAAgB,CAAC;QAE7B,oEAAoE;QACpE,iEAAiE;QACjE,wEAAwE;QACxE,uEAAuE;QACvE,wEAAwE;QACxE,uEAAuE;QACvE,qEAAqE;QACrE,yEAAyE;QACzE,gEAAgE;QAChE,MAAMQ,QAAQL,OAAO,CAAC,QAAQ,KAAK;QACnC,MAAMM,WAAW,MAAMpE,uBAAuB;YAC5CqE,SAAS;YACTF;YACAd;QACF;QACA,IAAIe,UAAU,OAAOE,eAAelC,KAAKgC;QACzC,MAAMG,aAAa/C,SAAS,YAAY,gBAAgB;QACxD,IACE,CAAC2C,SACD,CAAE,MAAM/D,0BAA0BiD,OAAOE,QAAQI,SAASa,MAAM,EAAED,aAClE;YACA,OAAOD,eAAelC,KAAKtC,kBAAkByE;QAC/C;QAEA,MAAME,WAAWpF,iBAAiB6E,KAAK;QACvC,IAAI,CAACO,UAAU;YACb,OAAOrC,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEP,OAAOzC;YAAsB;QAC7D;QAEA,0EAA0E;QAC1E,0EAA0E;QAC1E,yEAAyE;QACzE,yEAAyE;QACzE,MAAMkF,SAAS,MAAMvE,gBAAgB;YACnCwE,SAAS;gBAAErC,QAAQH,IAAIG,MAAM;YAAC;YAC9B6B;YACAF,KAAKH,QAAQG,GAAG;YAChBC,KAAKA;QACP;QACA,IAAIQ,QAAQ,OAAOJ,eAAelC,KAAKsC;QAEvC,MAAME,OAAO7E,eAAe,CAAC,UAAU,EAAE+D,QAAQG,GAAG,EAAE,EAAE;YACtDY,OAAOnD;YACPoD,UAAUnD;QACZ;QACA,IAAI,CAACiD,KAAKG,OAAO,EAAE;YACjB,KAAK,MAAM,CAACC,MAAMC,MAAM,IAAIC,OAAOC,OAAO,CAAC9E,iBAAiBuE,OAAQ;gBAClExC,IAAIgD,SAAS,CAACJ,MAAMC;YACtB;YACA,OAAO7C,IACJG,MAAM,CAAC,KACPC,IAAI,CAAC;gBAAEP,OAAO;YAA4C;QAC/D;QACA,sEAAsE;QACtE,wDAAwD;QACxD,MAAMoD,SAAS/D,yBAAyBe;QACxC,IAAIgD,UAAU,CAACA,OAAON,OAAO,EAAE;YAC7B,KAAK,MAAM,CAACC,MAAMC,MAAM,IAAIC,OAAOC,OAAO,CAAC9E,iBAAiBgF,SAAU;gBACpEjD,IAAIgD,SAAS,CAACJ,MAAMC;YACtB;YACA,OAAO7C,IACJG,MAAM,CAAC,KACPC,IAAI,CAAC;gBAAEP,OAAO;YAA4C;QAC/D;QAEA,0EAA0E;QAC1E,wEAAwE;QACxE,uEAAuE;QACvE,uCAAuC;QACvC,IAAI;YACF,mEAAmE;YACnE,oEAAoE;YACpE,qEAAqE;YACrE,iEAAiE;YACjEuB,cAAc,MAAM5C,qBAClB8C,WACAL,OACAoB,UACA,IAAIa,QACJpB,KACA,mEAAmE;YACnE,qCAAqC;YACrC;gBAAED,KAAKH,QAAQG,GAAG;gBAAEV,QAAQA,UAAU;YAAK;QAE/C,EAAE,OAAOtB,OAAO;YACd,qEAAqE;YACrE,0EAA0E;YAC1E,sCAAsC;YACtCsD,QAAQtD,KAAK,CAAC,6BAA6BA;YAC3C,OAAOG,IACJG,MAAM,CAAC,KACPC,IAAI,CAAC;gBAAEP,OAAO;YAAuC;QAC1D;QACA,wEAAwE;QACxE,gEAAgE;QAChE1B,oBAAoBmD,WAAWL,OAAOS,QAAQG,GAAG,EAAET;QACnD,IAAI,CAACA,YAAYuB,OAAO,EAAE;gBAkCpBS;YAjCJ,MAAMC,YAAYjC,YAAYiC,SAAS;YACvC,mEAAmE;YACnE,oEAAoE;YACpE,IAAIA,cAAc,aAAa;oBAEGjC,gCAAAA;gBADhC,OAAOpB,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;oBAC1BP,OAAOtC,wBAAuB6D,yBAAAA,YAAYkC,SAAS,sBAArBlC,iCAAAA,uBAAuBmC,OAAO,qBAA9BnC,+BAAgCoC,KAAK;oBACnEC,QAAQ;oBACRC,OAAOrF,kBAAkB+C;oBACzBuC,OAAOlG,aAAa2D;gBACtB;YACF;YACA,qEAAqE;YACrE,+DAA+D;YAC/D,qEAAqE;YACrE,iEAAiE;YACjE,mEAAmE;YACnE,WAAW;YACX,+DAA+D;YAC/D,mEAAmE;YACnE,mEAAmE;YACnE,kEAAkE;YAClE,MAAMwC,UAAU1F,4BAA4BkD;YAC5C,IAAIwC,SAAS;gBACX,OAAO5D,IAAIG,MAAM,CAACyD,QAAQzD,MAAM,EAAEC,IAAI,CAAC;oBACrCP,OAAO+D,QAAQhD,IAAI;oBACnB6C,QAAQ;oBACRC,OAAOrF,kBAAkB+C;oBACzBuC,OAAOlG,aAAa2D;gBACtB;YACF;YACA,MAAMgC,aAAa/F,4BAA4ByE,KAAKuB;YACpD,OAAOrD,IAAIG,MAAM,CAACiD,aAAa,MAAM,KAAKhD,IAAI,CAAC;gBAC7CP,KAAK,GACHuD,SAAAA,qBAAAA,aACA,gEAAgE;gBAChE,8BAA8B;gBAC9BjG,2BAA2BkG,sBAH3BD,SAIA;gBACF,mEAAmE;gBACnEM,OAAOrF,kBAAkB+C;gBACzBuC,OAAOlG,aAAa2D;YACtB;QACF;QAEA,MAAMyC,OAA4BxB,WAAW,aAAa;QAC1D,+DAA+D;QAC/D,yEAAyE;QACzE,wEAAwE;QACxE,yEAAyE;QACzE,kCAAkC;QAClC,MAAMb,OAAOJ;QACb,MAAM0C,SAAStG,qBAAqB6B,sBAAsBD,OAAOuB,wBAAAA,KAAMoD,KAAK,EAAE;YAC5EC,MAAM1G,qBAAqBwE;YAC3BmC,eAAe,YAAEzC,kBAAAA,KAAK8B,SAAS,qBAAd9B,gBAAgB0C,MAAM,oBAAI;YAC3CC,SAAS,YAAE3C,mBAAAA,KAAK8B,SAAS,qBAAd9B,iBAAgB2C,SAAS,oBAAI;QAC1C;QACA,MAAMJ,iBAAQD,0BAAAA,OAAQC,KAAK,oBAAI5E,mBAAmBC;QAClD,mEAAmE,GACnE,MAAMgF,WAAW,CAACC;;mBAChB5G,aAAa+D,MAAM;gBACjB8C,aAAapH,qBAAqBkB,sBAAsBiG,OAAOE,KAAK,EAAER;gBACtEA,OAAO;oBAAES,IAAIT;oBAAOU,IAAI,UAAEX,0BAAAA,OAAQW,IAAI,mBAAI;gBAAK;YACjD;;QACF,8DAA8D,GAC9D,sEAAsE;QACtE,oEAAoE;QACpE,gDAAgD;QAChD,MAAMC,gBAAOtD,+BAAAA,YAAasD,IAAI,oBAAI;QAClC,MAAMf,QAAQ,CAACU,SACb/F,iBAAiBgD,WAAWL,OAAO;gBACjC0D,OAAO,CAAC,eAAe,EAAEvF,MAAM;gBAC/B+B,QAAQA,UAAU;gBAClB4C;gBACAF;gBACAU,OAAOF,OAAOE,KAAK;gBACnBK,WAAW,EAAE;gBACbC,YAAYR,OAAOQ,UAAU;gBAC7BH;YACF,GAAGI,KAAK,CAAC,CAACjF,QAAUsD,QAAQtD,KAAK,CAAC,+BAA+BA;QAEnE;;;;;;;;KAQC,GACD,MAAMkF,MAAM,OACVC,SACAvF,WACAwF;YAEA,IAAI;gBACF,MAAMZ,SAAS,MAAMvF,aAAa;oBAChCiF;oBACAtE;oBACAyF,QAAQ;oBACR,8DAA8D;oBAC9D,kEAAkE;oBAClE,kDAAkD;oBAClDC,QAAQpG,uBAAuBK;oBAC/BgG,UAAU;wBAAC;4BAAEC,MAAM;4BAAQL;wBAAQ;qBAAE;mBACjCC,QAAQ;oBAAEA;gBAAM,IAAI,CAAC;gBAE3B5D,mBAAmB;gBACnB,MAAMsC,MAAMU;gBACZ,OAAOA;YACT,EAAE,OAAOxE,OAAO;gBACd,IAAI,CAAEA,CAAAA,iBAAiBjB,eAAc,GAAI,MAAMiB;gBAC/CwB,mBAAmB;gBACnB,MAAMiE,mBAAmBhE,WAAWL,OAAOG;gBAC3CA,cAAc;gBACdpB,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;oBAAEP,OAAOA,MAAM0F,OAAO;gBAAC;gBAC5C,OAAO;YACT;QACF;QAEA,uEAAuE;QACvE,gEAAgE;QAChE,qEAAqE;QACrE,IAAInG,SAAS,WAAW;gBAmBOsC;YAlB7B,MAAM2C,SAAS,MAAMU,IACnB,CAAC,mBAAmB,EAAEjE,aAAa,EACnCtB,oBACA;gBAACR;aAAoB;YAEvB,IAAI,CAACqF,QAAQ;YACb,IAAIA,OAAOmB,IAAI,KAAK,WAAW;gBAC7B,OAAOxF,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC,aAAKR;oBAAS+D,OAAOS,SAASC;;YAC5D;YACA,MAAMoB,OAAOxG,kBAAkBoF;YAC/B,IAAIoB,KAAKtF,MAAM,KAAK,MAAM;gBACxB,mEAAmE;gBACnE,kEAAkE;gBAClE,OAAOH,IAAIG,MAAM,CAACsF,KAAKtF,MAAM,KAAK,eAAe,MAAM,KAAKC,IAAI,CAAC;oBAAEP,OAAO4F,KAAK5F,KAAK;gBAAC;YACvF;YACA,wEAAwE;YACxE,qEAAqE;YACrE,MAAMpB,mBACJ;gBAAEoD,KAAKH,QAAQG,GAAG;gBAAE6D,KAAK,GAAEhE,iBAAAA,QAAQgE,KAAK,YAAbhE,iBAAiB;YAAK,GACjD;gBACET;gBACAE,QAAQA,UAAU;gBAClBwE,WAAW7C,OAAO8C,IAAI,CAACH,KAAKI,OAAO,CAACC,KAAK,EAAErF,MAAM;YACnD;YAEF,OAAOT,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEyF,SAASJ,KAAKI,OAAO;gBAAElC,OAAOS,SAASC;YAAQ;QAC/E;QAEA,MAAMA,SAAS,MAAMU,IACnB3F,SAAS,SACL;YACE2B,SAAS,CAAC,OAAO,EAAEA,OAAO;YAC1BC,WAAW,CAAC,SAAS,EAAEA,SAAS;YAChCJ,QAAQ,CAAC,eAAe,EAAEA,MAAM;YAChC,CAAC,aAAa,EAAEE,aAAa;SAC9B,CACEiF,MAAM,CAACC,SACPC,IAAI,CAAC,UACRrF,OACE,CAAC,uBAAuB,EAAEA,KAAK,iBAAiB,EAAEE,aAAa,GAC/D,CAAC,yCAAyC,EAAEA,aAAa,EAC/D1B,SAAS,SAASM,kBAAkBC;QAEtC,IAAI,CAAC0E,QAAQ;QACb,IAAIA,OAAOmB,IAAI,KAAK,WAAW;YAC7B,OAAOxF,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC,aAAKR;gBAAS+D,OAAOS,SAASC;;QAC5D;QACA,MAAM6B,SAAS7B,OAAOzD,IAAI,CAACM,IAAI;QAC/B,IAAI,CAACgF,QAAQ;YACX,OAAOlG,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEP,OAAO;gBAAqB8D,OAAOS,SAASC;YAAQ;QACpF;QACA,OAAOrE,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEQ,MAAMsF;YAAQvC,OAAOS,SAASC;QAAQ;IACtE,EAAE,OAAOxE,OAAO;QACdsD,QAAQtD,KAAK,CAACA;QACd,uEAAuE;QACvE,uEAAuE;QACvE,sEAAsE;QACtE,oEAAoE;QACpE,IAAIyB,aAAaF,eAAe,CAACC,kBAAkB;YACjD,MAAMiE,mBAAmBhE,WAAWL,OAAOG;QAC7C;QACA,OAAOpB,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEP,OAAO;QAAgB;IACvD;AACF,EAAC;AAED;;;;CAIC,GACD,eAAeyF,mBACbhE,SAAsC,EACtCL,KAAa,EACbG,WAA8B;IAE9B,MAAM7C,qBAAqB+C,WAAWL,OAAOG,aAAa0D,KAAK,CAAC,CAACjF,QAC/DsD,QAAQtD,KAAK,CAAC,qCAAqCA;AAEvD;AAEA,uEAAuE,GACvE,eAAeqC,eACblC,GAAsB,EACtBuD,OAAiB;IAEjB,MAAM4C,aAAa5C,QAAQtD,OAAO,CAACmG,GAAG,CAAC;IACvC,IAAID,YAAYnG,IAAIgD,SAAS,CAAC,eAAemD;IAC7C,MAAMxF,OAAO,MAAM4C,QAAQnD,IAAI,GAAG0E,KAAK,CAAC,IAAO,CAAA;YAAEjF,OAAO;QAAS,CAAA;IACjEG,IAAIG,MAAM,CAACoD,QAAQpD,MAAM,EAAEC,IAAI,CAACO;AAClC"}
@@ -74,7 +74,7 @@
74
74
  * in the author's editor, on confirm. Below the rung the request is answered
75
75
  * exactly as it would be without a canvas, and the outline is never parsed.
76
76
  *
77
- * Capability tiers: entitled orgs (`aiAssist`, Pro+) get docs-grounded
77
+ * Capability tiers: entitled orgs (`aiAssist`, Starter+) get docs-grounded
78
78
  * answers PLUS page-context awareness (level 2 — the current route/host is
79
79
  * injected so the assistant can walk the user through the view they are
80
80
  * on). Free orgs get level 1 only: docs-grounded answers and deep links;