@aglyn/plugins-ai 1.0.0-beta.253 → 1.0.0-beta.255

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 (87) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-site-start-card.component.js +16 -8
  3. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  4. package/src/lib/components/assist-signals-page.component.js +5 -6
  5. package/src/lib/components/assist-signals-page.component.js.map +1 -1
  6. package/src/lib/components/staff-org-ai-card.component.d.ts +0 -10
  7. package/src/lib/components/staff-org-ai-card.component.js +10 -79
  8. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  9. package/src/lib/components/staff-org-ai-conversations.component.js +8 -41
  10. package/src/lib/components/staff-org-ai-conversations.component.js.map +1 -1
  11. package/src/lib/components/staff-user-ai-usage-card.component.js +2 -1
  12. package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
  13. package/src/lib/jobs/ai-job-page-language.d.ts +14 -1
  14. package/src/lib/jobs/ai-job-page-language.js +18 -3
  15. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  16. package/src/lib/jobs/ai-job-page-step.js +17 -3
  17. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  18. package/src/lib/jobs/ai-job-plan-step.js +17 -7
  19. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  20. package/src/lib/jobs/ai-job-site-content.d.ts +32 -16
  21. package/src/lib/jobs/ai-job-site-content.js +43 -20
  22. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  23. package/src/lib/jobs/ai-job-site-step.d.ts +3 -0
  24. package/src/lib/jobs/ai-job-site-step.js +23 -11
  25. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  26. package/src/lib/jobs/ai-layout-stock-photos.d.ts +30 -0
  27. package/src/lib/jobs/ai-layout-stock-photos.js +76 -8
  28. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  29. package/src/lib/jobs/fixtures/ai-dental-site-plan-recording.d.ts +95 -0
  30. package/src/lib/jobs/fixtures/ai-dental-site-plan-recording.js +137 -0
  31. package/src/lib/jobs/fixtures/ai-dental-site-plan-recording.js.map +1 -0
  32. package/src/lib/layout-language/ai-layout-labels.d.ts +2 -0
  33. package/src/lib/layout-language/ai-layout-labels.js +12 -6
  34. package/src/lib/layout-language/ai-layout-labels.js.map +1 -1
  35. package/src/lib/layout-language/ai-layout-pictures.d.ts +12 -3
  36. package/src/lib/layout-language/ai-layout-pictures.js +6 -3
  37. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  38. package/src/lib/model/ai-plan-capabilities.d.ts +5 -0
  39. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  40. package/src/lib/model/ai-products-bulk.d.ts +48 -0
  41. package/src/lib/model/ai-products-bulk.js +56 -0
  42. package/src/lib/model/ai-products-bulk.js.map +1 -0
  43. package/src/lib/model/ai-products.d.ts +2 -4
  44. package/src/lib/model/ai-products.js +4 -9
  45. package/src/lib/model/ai-products.js.map +1 -1
  46. package/src/lib/model/ai-site-job.d.ts +32 -12
  47. package/src/lib/model/ai-site-job.js +35 -20
  48. package/src/lib/model/ai-site-job.js.map +1 -1
  49. package/src/lib/model/ai-site-kinds.d.ts +5 -5
  50. package/src/lib/model/ai-site-kinds.js +5 -5
  51. package/src/lib/model/ai-site-kinds.js.map +1 -1
  52. package/src/lib/providers/anthropic.js +26 -5
  53. package/src/lib/providers/anthropic.js.map +1 -1
  54. package/src/lib/providers/contract.d.ts +10 -0
  55. package/src/lib/providers/contract.js.map +1 -1
  56. package/src/lib/runtime/ai-doctrine-validators.d.ts +20 -0
  57. package/src/lib/runtime/ai-doctrine-validators.js +53 -1
  58. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  59. package/src/lib/runtime/ai-doctrine.d.ts +11 -0
  60. package/src/lib/runtime/ai-doctrine.js +9 -4
  61. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  62. package/src/lib/runtime/ai-runtime.d.ts +6 -0
  63. package/src/lib/runtime/ai-runtime.js +10 -0
  64. package/src/lib/runtime/ai-runtime.js.map +1 -1
  65. package/src/lib/server/ai-admin-conversations.d.ts +2 -0
  66. package/src/lib/server/ai-admin-conversations.js +40 -7
  67. package/src/lib/server/ai-admin-conversations.js.map +1 -1
  68. package/src/lib/server/ai-admin-org.js +28 -2
  69. package/src/lib/server/ai-admin-org.js.map +1 -1
  70. package/src/lib/server/ai-admin-orgs-spend.js +19 -1
  71. package/src/lib/server/ai-admin-orgs-spend.js.map +1 -1
  72. package/src/lib/server/ai-admin-signals.js +11 -3
  73. package/src/lib/server/ai-admin-signals.js.map +1 -1
  74. package/src/lib/server/ai-admin-user.js +11 -1
  75. package/src/lib/server/ai-admin-user.js.map +1 -1
  76. package/src/lib/usage/ai-usage-wire.d.ts +5 -2
  77. package/src/lib/usage/ai-usage-wire.js +5 -2
  78. package/src/lib/usage/ai-usage-wire.js.map +1 -1
  79. package/src/lib/usage/assist-signals-view.d.ts +123 -0
  80. package/src/lib/usage/assist-signals-view.js +151 -0
  81. package/src/lib/usage/assist-signals-view.js.map +1 -0
  82. package/src/lib/usage/staff-org-ai-conversations-view.d.ts +51 -0
  83. package/src/lib/usage/staff-org-ai-conversations-view.js +127 -0
  84. package/src/lib/usage/staff-org-ai-conversations-view.js.map +1 -0
  85. package/src/lib/usage/staff-org-ai-view.d.ts +181 -0
  86. package/src/lib/usage/staff-org-ai-view.js +263 -0
  87. package/src/lib/usage/staff-org-ai-view.js.map +1 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-runtime.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 { aiCatalogEntry, aiModelCacheMinTokens } from '../providers/catalog'\nimport {\n AI_IMAGE_MAX_BYTES,\n AI_IMAGE_MEDIA_TYPES,\n AI_REQUEST_MAX_IMAGES,\n AiRequestShapeError,\n AiUpstreamError,\n aiBase64Bytes,\n aiMessageImages,\n type AiMessage,\n type AiProvider,\n type AiProviderRequest,\n type AiRequestBase,\n type AiResult,\n type AiStreamEvent,\n type AiSystemBlock,\n type AiTool,\n} from '../providers/contract'\nimport { resolveAiProvider, type AiPluginSettings } from '../providers/routing'\nimport {\n aiDevCacheTtl,\n aiDevReplayMode,\n aiIsDeployedRuntime,\n aiLiveBatchWanted,\n aiUnderLiveRun,\n aiWarnIfReplayAskedInDeployment,\n} from './ai-dev-env'\nimport { aiAnsweredOutsideBatch, aiCompleteInBatch } from './ai-live-batch'\n\nexport {\n AI_UPSTREAM_FAILURE_COPY,\n AiRequestShapeError,\n AiUpstreamError,\n type AiCompletion,\n type AiEffort,\n type AiImagePart,\n type AiMessage,\n type AiMessagePart,\n type AiRefusal,\n type AiRequestBase,\n type AiResult,\n type AiStreamEvent,\n type AiSystemBlock,\n type AiThinking,\n type AiTool,\n type AiToolUse,\n type AiUsage,\n} from '../providers/contract'\n\n/**\n * The one runtime every AI door calls (AGL-2903), provider-generic since\n * AGL-2939.\n *\n * `/api/assist/chat` and `/api/ai/assist` each carried their own `fetch` to\n * one vendor, their own SSE parser and their own usage extraction, and a\n * third door would have been a third copy. This module owns the request\n * shape, the cache-prefix rule, the provider resolution and the error\n * boundary; an adapter owns one vendor's wire protocol; a door owns its\n * prompt, its gate ladder and its wire format.\n *\n * The cache-prefix rule (AGL-2352) is enforced HERE, before any adapter:\n * a block marked `volatile` may not sit at or before the last\n * `cacheBreakpoint`, and more breakpoints than any provider caches are\n * refused. A provider without a cache ignores the markers and keeps the\n * order, so a request that is right for the caching adapter is right for\n * every adapter.\n *\n * The picture rule (AGL-2916) is enforced here too, and for the same reason:\n * a picture on an assistant turn, a picture for a model whose provider does\n * not say it reads one, or a picture past the bounds is refused before any\n * adapter is reached, so no door can send one a provider would reject.\n */\n\n/** The most `cacheBreakpoint` markers a request may carry. */\nexport const AI_MAX_CACHE_BREAKPOINTS = 4\n\n/**\n * The acceptable-use rules every generation prompt carries (AGL-2925).\n *\n * ONE constant, appended to each door's STATIC system text — inside the\n * cached prefix on the console chat, ahead of the mode prompt on the copy\n * assistant — so the rules the model is held to are the same at every\n * door and carry no per-org byte. The Free taste is what makes them worth\n * stating: a generated page that nobody paid for is the cheapest phishing\n * kit there is, and the model declining it is the first line, ahead of the\n * abuse-report route and the lockdown key.\n *\n * The categories are the platform's own Acceptable Use Policy, restated in\n * the second person for the model. A decline from the model's safety layer\n * arrives as a refusal, which the meter counts against the account's daily\n * pause and never against its credits.\n */\nexport const AI_ACCEPTABLE_USE_BLOCK = `Acceptable use — decline these rather than write them, and say briefly why:\n- Pages, emails or copy that harvest credentials, payment details or personal data by deception (phishing, fake sign-in or checkout pages, fake \"verify your account\" notices).\n- Impersonating a real business, brand, public body or person you were not told the user represents — their name, logo, sign-in page or voice — or copy that claims an endorsement, certification or partnership that was not given to you as a fact.\n- Content that promotes or sells prohibited goods and services: weapons, illegal drugs, counterfeit goods, malware or deceptive downloads, unlicensed gambling, or adult content involving minors.\n- Scams and fraud: fake investment or crypto schemes, advance-fee offers, tech-support scams, fake charities, deceptive \"you have won\" pages, or copy built to defraud.\n- Harassment, threats, or hateful content targeting a person or a group.\n- Spam farms: bulk near-duplicate pages or copy whose only purpose is to game search engines or send unsolicited mail.\nWhen a brief is ordinary business content, write it well and do not lecture.`\n\n/** A door's request, with the org's settings when the door knows the org. */\ninterface AiRuntimeOptions {\n /** The org's resolved `pluginSettings/ai`, for its provider and model choices. */\n settings?: AiPluginSettings\n /** A provider chosen by the caller, ahead of every setting; specs use it. */\n provider?: AiProvider\n}\n\nexport interface AiRequest extends AiRequestBase, AiRuntimeOptions {\n stream: false\n}\n\nexport interface AiStreamRequest extends AiRequestBase, AiRuntimeOptions {\n stream: true\n}\n\n/**\n * The cache guard, applied to the system blocks before any adapter sees\n * them. Exported so a spec and a door can assert a prompt's shape without\n * a provider.\n */\nexport function validateAiSystemBlocks(system: readonly AiSystemBlock[]): void {\n const breakpoints = system.filter((block) => block.cacheBreakpoint)\n if (breakpoints.length > AI_MAX_CACHE_BREAKPOINTS) {\n throw new AiRequestShapeError(\n `${breakpoints.length} cache breakpoints; at most ${AI_MAX_CACHE_BREAKPOINTS} are cached`,\n )\n }\n // Caching is a prefix match, so the LAST breakpoint decides what is\n // cached, and every block at or before it is inside the cached prefix. A\n // volatile block anywhere in that span — including one that is itself\n // the breakpoint — puts a per-request byte into the prefix (AGL-2352).\n const lastBreakpoint = system.reduce(\n (last, block, index) => (block.cacheBreakpoint ? index : last),\n -1,\n )\n system.forEach((block, index) => {\n if (block.volatile && index <= lastBreakpoint) {\n throw new AiRequestShapeError(\n `system block ${index} is volatile but sits inside the cached prefix (last breakpoint at block ${lastBreakpoint})`,\n )\n }\n })\n // A site's block is cached per site (AGL-3661), so it closes the cached\n // span: a platform-wide breakpoint after it would be keyed on the site too,\n // and every workspace would warm its own copy of a shared prompt.\n const firstSite = system.findIndex((block) => block.site && block.cacheBreakpoint)\n if (firstSite >= 0 && firstSite !== lastBreakpoint) {\n throw new AiRequestShapeError(\n `system block ${firstSite} is a site's cached block but breakpoint ${lastBreakpoint} follows it`,\n )\n }\n}\n\n/**\n * Whether a provider's model reads a picture in a user turn (AGL-2916): what\n * the provider's own descriptor of the model says. A model the provider does\n * not describe reads none, so an adopter's provider is never sent a picture\n * it did not say it can take.\n */\nexport function aiModelReadsImages(provider: AiProvider, model: string): boolean {\n return provider\n .models()\n .some((descriptor) => descriptor.id === model && descriptor.capabilities.vision === true)\n}\n\n/**\n * Whether a model a door resolved reads pictures on the provider that would\n * serve the request: what a door asks before it attaches one, so a model that\n * does not is sent the text alone rather than refused.\n */\nexport function aiRouteReadsImages(model: string, settings?: AiPluginSettings): boolean {\n const provider = resolveAiProvider(settings)\n return provider ? aiModelReadsImages(provider, model) : false\n}\n\n/** A character standard base64 does not use, outside its padding. */\nconst NOT_BASE64 = /[^A-Za-z0-9+/]/\n\n/**\n * Whether `data` is standard base64 and nothing else: its alphabet, then at\n * most two `=` of padding; no `data:` prefix, no line breaks.\n *\n * Scanned for a character outside the alphabet rather than matched whole: a\n * picture is megabytes of base64, and a whole-string pattern with a greedy\n * loop can exhaust the regular expression engine's backtrack stack on input\n * that large, which throws a RangeError instead of answering.\n */\nfunction isBareBase64(data: string): boolean {\n const body = data.endsWith('==') ? data.slice(0, -2) : data.endsWith('=') ? data.slice(0, -1) : data\n return body.length > 0 && !NOT_BASE64.test(body)\n}\n\n/**\n * The picture guard (AGL-2916), applied to the messages before any adapter\n * sees them. A picture rides only a user turn, only to a model that reads\n * pictures, only in a format every such model takes, as bare base64 within\n * the size a provider accepts, and no more of them than a request may carry.\n * `readsImages` is asked only when a turn carries a picture. Exported so a\n * spec and a door can assert a request's shape without a provider.\n */\nexport function validateAiMessages(messages: readonly AiMessage[], readsImages: () => boolean): void {\n messages.forEach((message, index) => {\n if (typeof message.content === 'string') return\n if (!Array.isArray(message.content) || message.content.length === 0) {\n throw new AiRequestShapeError(`message ${index} has neither text nor parts`)\n }\n for (const part of message.content) {\n if (part.type === 'text') {\n if (typeof part.text !== 'string') {\n throw new AiRequestShapeError(`message ${index} has a text part with no text`)\n }\n continue\n }\n if (part.type !== 'image') {\n throw new AiRequestShapeError(`message ${index} has a part of an unknown type`)\n }\n if (message.role !== 'user') {\n throw new AiRequestShapeError(`message ${index} is an assistant turn and carries a picture`)\n }\n if (!(AI_IMAGE_MEDIA_TYPES as readonly string[]).includes(part.mediaType)) {\n throw new AiRequestShapeError(`message ${index} carries a picture of an unsupported type`)\n }\n if (typeof part.data !== 'string') {\n throw new AiRequestShapeError(`message ${index} carries a picture that is not bare base64`)\n }\n // The size first: it needs only the length, so an oversized picture is\n // refused without reading its bytes.\n if (aiBase64Bytes(part.data) > AI_IMAGE_MAX_BYTES) {\n throw new AiRequestShapeError(\n `message ${index} carries a picture over ${AI_IMAGE_MAX_BYTES} bytes`,\n )\n }\n if (!isBareBase64(part.data)) {\n throw new AiRequestShapeError(`message ${index} carries a picture that is not bare base64`)\n }\n }\n })\n const images = aiMessageImages(messages).length\n if (images > AI_REQUEST_MAX_IMAGES) {\n throw new AiRequestShapeError(`${images} pictures; a request carries at most ${AI_REQUEST_MAX_IMAGES}`)\n }\n if (images > 0 && !readsImages()) {\n throw new AiRequestShapeError('the request carries a picture and its model does not read pictures')\n }\n}\n\n/**\n * Characters to a token when a prompt is measured rather than tokenized\n * (AGL-2937). Four is the high end of English prose, so reading a prefix\n * through it errs LOW on the token count — and a prefix counts as cached\n * only when it clears its model's minimum even at that reading. Erring the\n * other way would let a prompt claim a cache it does not get.\n */\nexport const AI_CACHE_PREFIX_TOKEN_CHARS = 4\n\n/**\n * The characters inside the cached span of a request: the tools, which a\n * provider renders ahead of the system blocks and which therefore sit in\n * every prefix, plus every system block up to and including the last\n * breakpoint. Zero when the request marks no breakpoint at all.\n */\nexport function aiCachedPrefixChars(\n system: readonly AiSystemBlock[],\n tools: readonly AiTool[] = [],\n): number {\n const lastBreakpoint = system.reduce(\n (last, block, index) => (block.cacheBreakpoint ? index : last),\n -1,\n )\n if (lastBreakpoint < 0) return 0\n const toolChars = tools.reduce((total, tool) => total + JSON.stringify(tool).length, 0)\n return system\n .slice(0, lastBreakpoint + 1)\n .reduce((total, block) => total + block.text.length, toolChars)\n}\n\n/**\n * Whether this request's cached span is long enough for the model to cache\n * it at all (AGL-2937). A prefix under the model's `cacheMinTokens` is\n * served with the breakpoints honored and nothing cached — no error, and a\n * usage report that reads as a permanent miss — so a door designed around a\n * cache has to answer this question before it believes its own markers, and\n * `runtime/ai-prompt-cache.spec.ts` answers it for every door at once.\n */\nexport function aiCachedPrefixCaches(\n model: string,\n system: readonly AiSystemBlock[],\n tools: readonly AiTool[] = [],\n): boolean {\n const chars = aiCachedPrefixChars(system, tools)\n return chars / AI_CACHE_PREFIX_TOKEN_CHARS >= aiModelCacheMinTokens(model)\n}\n\n/**\n * Whether a provider is configured to answer: registered, and its key\n * present. Every door asks this before it spends a reservation, and\n * answers 501 (or the docs fallback) when it is false.\n */\nexport function aiProviderReady(settings?: AiPluginSettings): boolean {\n return Boolean(resolveAiProvider(settings)?.readApiKey())\n}\n\n/** The provider that would answer, for a door that names it in its logs or its inventory. */\nexport function aiActiveProvider(settings?: AiPluginSettings): AiProvider | undefined {\n return resolveAiProvider(settings)\n}\n\nfunction providerFor(input: AiRuntimeOptions): { provider: AiProvider; apiKey: string } {\n const provider = input.provider ?? resolveAiProvider(input.settings)\n if (!provider) throw new Error('no AI provider is registered')\n const apiKey = provider.readApiKey()\n if (!apiKey) {\n // Every door gates on `aiProviderReady` before it gets here, so this is\n // a wiring fault rather than a customer path.\n throw new Error(`${provider.apiKeyEnv} is not set`)\n }\n return { provider, apiKey }\n}\n\n/**\n * Send one request through the resolved provider.\n *\n * Streaming: resolves once the provider has ACCEPTED the request — after\n * the headers, before the first token — with an iterator of simplified\n * events the caller wraps in its own wire format. A non-2xx answer rejects\n * with `AiUpstreamError` before the iterator exists, so a door can still\n * send a plain error status and hand back its reservation.\n *\n * Non-streaming: resolves with the parsed result, a decline typed as\n * `AiRefusal` rather than thrown.\n */\nexport async function runAiRequest(\n input: AiStreamRequest,\n): Promise<AsyncIterable<AiStreamEvent>>\nexport async function runAiRequest(input: AiRequest): Promise<AiResult>\nexport async function runAiRequest(\n input: AiRequest | AiStreamRequest,\n): Promise<AsyncIterable<AiStreamEvent> | AiResult> {\n validateAiSystemBlocks(input.system)\n refuseRetiredModelInLiveRun(input.model)\n const { provider, apiKey } = providerFor(input)\n validateAiMessages(input.messages, () => aiModelReadsImages(provider, input.model))\n const { stream, settings: _settings, provider: _provider, ...request } = input\n aiWarnIfReplayAskedInDeployment()\n const cacheTtl = request.cacheTtl ?? aiDevCacheTtl()\n const providerRequest: AiProviderRequest = { ...request, ...(cacheTtl ? { cacheTtl } : {}), apiKey }\n try {\n if (aiIsDeployedRuntime()) {\n return await (stream ? provider.stream(providerRequest) : provider.complete(providerRequest))\n }\n return await runInDevelopment(provider, providerRequest, stream)\n } catch (error) {\n await reportAccountProblem(provider, error)\n throw error\n }\n}\n\n/**\n * A live development run that names a retired model is refused before it\n * spends (AGL-3781). Routing serves a retired id's successor wherever a\n * setting or an override names it, so a retired id reaching here was asked\n * for by name — an override left in a run, or a comparison a spec kept — and\n * would bill a model production no longer serves.\n */\nfunction refuseRetiredModelInLiveRun(model: string): void {\n if (!aiUnderLiveRun()) return\n const successor = aiCatalogEntry(model)?.successor\n if (successor) {\n throw new AiRequestShapeError(`${model} is retired; ${successor} serves in its place`)\n }\n}\n\n/**\n * A request in a development process (AGL-3660): through the replay cache,\n * batch mode and the first-request-warms-the-prefix rule when this process\n * asked for them (`ai-dev-env.ts`), straight to the provider when it asked\n * for none. The file-backed half is loaded only when a layer is on, so a dev\n * server that never asked loads nothing it did not before.\n */\nasync function runInDevelopment(\n provider: AiProvider,\n request: AiProviderRequest,\n stream: boolean,\n): Promise<AsyncIterable<AiStreamEvent> | AiResult> {\n const mode = aiDevReplayMode()\n const batch = !stream && aiLiveBatchWanted() && Boolean(provider.completeBatch)\n if (mode === 'off' && !batch) {\n return stream ? provider.stream(request) : provider.complete(request)\n }\n const dev = await import('./ai-dev-replay')\n const ttlMs = request.cacheTtl === '1h' ? 60 * 60_000 : 5 * 60_000\n const prefix = dev.aiCachedPrefixKey(request)\n if (stream) {\n const live = () => dev.aiDevWarmPrefixFirst(prefix, ttlMs, () => provider.stream(request))\n return mode === 'off' ? live() : dev.aiDevReplayStream(provider, request, mode, live)\n }\n const live = async (): Promise<AiResult> => {\n const result = batch\n ? await aiCompleteInBatch(provider, request)\n : await dev.aiDevWarmPrefixFirst(prefix, ttlMs, () => provider.complete(request))\n // A request its batch left unanswered was asked on its own, at full price (AGL-3781).\n const discounted = batch && !aiAnsweredOutsideBatch(result)\n dev.aiDevCountLive(result.usage, request.model, discounted ? AI_BATCH_PRICE_FACTOR : 1)\n return result\n }\n return mode === 'off' ? live() : dev.aiDevReplayComplete(provider, request, mode, live)\n}\n\n/** What a batch's discount leaves of a request's price, for the development count. */\nconst AI_BATCH_PRICE_FACTOR = 0.5\n\n/**\n * Tells the operator when the failure is the platform's provider account —\n * a refused key, no credit — rather than the request (AGL-3377). Once per\n * provider and problem per window; loaded lazily so the runtime pulls no\n * Admin SDK until an account is actually broken. Never throws.\n */\nasync function reportAccountProblem(provider: AiProvider, error: unknown): Promise<void> {\n if (!(error instanceof AiUpstreamError) || !error.accountProblem) return\n try {\n const [{ raiseOperatorAlert }, { AI_PROVIDER_UNAVAILABLE }] = await Promise.all([\n import('./operator-alerts-seam'),\n import('../operator-alerts'),\n ])\n await raiseOperatorAlert(AI_PROVIDER_UNAVAILABLE, {\n dedupeKey: `${provider.id}:${error.accountProblem}`,\n context: {\n provider: provider.label,\n keyEnv: provider.apiKeyEnv,\n reason:\n error.accountProblem === 'credentials'\n ? `its API key was refused (HTTP ${error.status ?? 'unknown'})`\n : 'the account is out of credit',\n },\n })\n } catch (alertError) {\n console.error('[ai] the provider account alert could not be raised', alertError)\n }\n}\n"],"names":["aiCatalogEntry","aiModelCacheMinTokens","AI_IMAGE_MAX_BYTES","AI_IMAGE_MEDIA_TYPES","AI_REQUEST_MAX_IMAGES","AiRequestShapeError","AiUpstreamError","aiBase64Bytes","aiMessageImages","resolveAiProvider","aiDevCacheTtl","aiDevReplayMode","aiIsDeployedRuntime","aiLiveBatchWanted","aiUnderLiveRun","aiWarnIfReplayAskedInDeployment","aiAnsweredOutsideBatch","aiCompleteInBatch","AI_UPSTREAM_FAILURE_COPY","AI_MAX_CACHE_BREAKPOINTS","AI_ACCEPTABLE_USE_BLOCK","validateAiSystemBlocks","system","breakpoints","filter","block","cacheBreakpoint","length","lastBreakpoint","reduce","last","index","forEach","volatile","firstSite","findIndex","site","aiModelReadsImages","provider","model","models","some","descriptor","id","capabilities","vision","aiRouteReadsImages","settings","NOT_BASE64","isBareBase64","data","body","endsWith","slice","test","validateAiMessages","messages","readsImages","message","content","Array","isArray","part","type","text","role","includes","mediaType","images","AI_CACHE_PREFIX_TOKEN_CHARS","aiCachedPrefixChars","tools","toolChars","total","tool","JSON","stringify","aiCachedPrefixCaches","chars","aiProviderReady","Boolean","readApiKey","aiActiveProvider","providerFor","input","Error","apiKey","apiKeyEnv","runAiRequest","request","refuseRetiredModelInLiveRun","stream","_settings","_provider","cacheTtl","providerRequest","complete","runInDevelopment","error","reportAccountProblem","successor","mode","batch","completeBatch","dev","ttlMs","prefix","aiCachedPrefixKey","live","aiDevWarmPrefixFirst","aiDevReplayStream","result","discounted","aiDevCountLive","usage","AI_BATCH_PRICE_FACTOR","aiDevReplayComplete","accountProblem","raiseOperatorAlert","AI_PROVIDER_UNAVAILABLE","Promise","all","dedupeKey","context","label","keyEnv","reason","status","alertError","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-runtime.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 { aiCatalogEntry, aiModelCacheMinTokens } from '../providers/catalog'\nimport {\n AI_IMAGE_MAX_BYTES,\n AI_IMAGE_MEDIA_TYPES,\n AI_REQUEST_MAX_IMAGES,\n AiRequestShapeError,\n AiUpstreamError,\n aiBase64Bytes,\n aiMessageImages,\n type AiMessage,\n type AiProvider,\n type AiProviderRequest,\n type AiRequestBase,\n type AiResult,\n type AiStreamEvent,\n type AiSystemBlock,\n type AiTool,\n} from '../providers/contract'\nimport { resolveAiProvider, type AiPluginSettings } from '../providers/routing'\nimport {\n aiDevCacheTtl,\n aiDevReplayMode,\n aiIsDeployedRuntime,\n aiLiveBatchWanted,\n aiUnderLiveRun,\n aiWarnIfReplayAskedInDeployment,\n} from './ai-dev-env'\nimport { aiAnsweredOutsideBatch, aiCompleteInBatch } from './ai-live-batch'\n\nexport {\n AI_UPSTREAM_FAILURE_COPY,\n AiRequestShapeError,\n AiUpstreamError,\n type AiCompletion,\n type AiEffort,\n type AiImagePart,\n type AiMessage,\n type AiMessagePart,\n type AiRefusal,\n type AiRequestBase,\n type AiResult,\n type AiStreamEvent,\n type AiSystemBlock,\n type AiThinking,\n type AiTool,\n type AiToolUse,\n type AiUsage,\n} from '../providers/contract'\n\n/**\n * The one runtime every AI door calls (AGL-2903), provider-generic since\n * AGL-2939.\n *\n * `/api/assist/chat` and `/api/ai/assist` each carried their own `fetch` to\n * one vendor, their own SSE parser and their own usage extraction, and a\n * third door would have been a third copy. This module owns the request\n * shape, the cache-prefix rule, the provider resolution and the error\n * boundary; an adapter owns one vendor's wire protocol; a door owns its\n * prompt, its gate ladder and its wire format.\n *\n * The cache-prefix rule (AGL-2352) is enforced HERE, before any adapter:\n * a block marked `volatile` may not sit at or before the last\n * `cacheBreakpoint`, and more breakpoints than any provider caches are\n * refused. A provider without a cache ignores the markers and keeps the\n * order, so a request that is right for the caching adapter is right for\n * every adapter.\n *\n * The picture rule (AGL-2916) is enforced here too, and for the same reason:\n * a picture on an assistant turn, a picture for a model whose provider does\n * not say it reads one, or a picture past the bounds is refused before any\n * adapter is reached, so no door can send one a provider would reject.\n */\n\n/** The most `cacheBreakpoint` markers a request may carry. */\nexport const AI_MAX_CACHE_BREAKPOINTS = 4\n\n/**\n * The acceptable-use rules every generation prompt carries (AGL-2925).\n *\n * ONE constant, appended to each door's STATIC system text — inside the\n * cached prefix on the console chat, ahead of the mode prompt on the copy\n * assistant — so the rules the model is held to are the same at every\n * door and carry no per-org byte. The Free taste is what makes them worth\n * stating: a generated page that nobody paid for is the cheapest phishing\n * kit there is, and the model declining it is the first line, ahead of the\n * abuse-report route and the lockdown key.\n *\n * The categories are the platform's own Acceptable Use Policy, restated in\n * the second person for the model. A decline from the model's safety layer\n * arrives as a refusal, which the meter counts against the account's daily\n * pause and never against its credits.\n */\nexport const AI_ACCEPTABLE_USE_BLOCK = `Acceptable use — decline these rather than write them, and say briefly why:\n- Pages, emails or copy that harvest credentials, payment details or personal data by deception (phishing, fake sign-in or checkout pages, fake \"verify your account\" notices).\n- Impersonating a real business, brand, public body or person you were not told the user represents — their name, logo, sign-in page or voice — or copy that claims an endorsement, certification or partnership that was not given to you as a fact.\n- Content that promotes or sells prohibited goods and services: weapons, illegal drugs, counterfeit goods, malware or deceptive downloads, unlicensed gambling, or adult content involving minors.\n- Scams and fraud: fake investment or crypto schemes, advance-fee offers, tech-support scams, fake charities, deceptive \"you have won\" pages, or copy built to defraud.\n- Harassment, threats, or hateful content targeting a person or a group.\n- Spam farms: bulk near-duplicate pages or copy whose only purpose is to game search engines or send unsolicited mail.\nWhen a brief is ordinary business content, write it well and do not lecture.`\n\n/** A door's request, with the org's settings when the door knows the org. */\ninterface AiRuntimeOptions {\n /** The org's resolved `pluginSettings/ai`, for its provider and model choices. */\n settings?: AiPluginSettings\n /** A provider chosen by the caller, ahead of every setting; specs use it. */\n provider?: AiProvider\n}\n\nexport interface AiRequest extends AiRequestBase, AiRuntimeOptions {\n stream: false\n}\n\nexport interface AiStreamRequest extends AiRequestBase, AiRuntimeOptions {\n stream: true\n}\n\n/**\n * The cache guard, applied to the system blocks before any adapter sees\n * them. Exported so a spec and a door can assert a prompt's shape without\n * a provider.\n */\nexport function validateAiSystemBlocks(system: readonly AiSystemBlock[]): void {\n const breakpoints = system.filter((block) => block.cacheBreakpoint)\n if (breakpoints.length > AI_MAX_CACHE_BREAKPOINTS) {\n throw new AiRequestShapeError(\n `${breakpoints.length} cache breakpoints; at most ${AI_MAX_CACHE_BREAKPOINTS} are cached`,\n )\n }\n // Caching is a prefix match, so the LAST breakpoint decides what is\n // cached, and every block at or before it is inside the cached prefix. A\n // volatile block anywhere in that span — including one that is itself\n // the breakpoint — puts a per-request byte into the prefix (AGL-2352).\n const lastBreakpoint = system.reduce(\n (last, block, index) => (block.cacheBreakpoint ? index : last),\n -1,\n )\n system.forEach((block, index) => {\n if (block.volatile && index <= lastBreakpoint) {\n throw new AiRequestShapeError(\n `system block ${index} is volatile but sits inside the cached prefix (last breakpoint at block ${lastBreakpoint})`,\n )\n }\n })\n // A site's block is cached per site (AGL-3661), so it closes the cached\n // span: a platform-wide breakpoint after it would be keyed on the site too,\n // and every workspace would warm its own copy of a shared prompt.\n const firstSite = system.findIndex((block) => block.site && block.cacheBreakpoint)\n if (firstSite >= 0 && firstSite !== lastBreakpoint) {\n throw new AiRequestShapeError(\n `system block ${firstSite} is a site's cached block but breakpoint ${lastBreakpoint} follows it`,\n )\n }\n}\n\n/**\n * Whether a request may also cache its conversation (AGL-3809): the\n * conversation's breakpoint is one more marker, so it fits only where the\n * system blocks leave one of the provider's four free.\n */\nexport function aiConversationCacheFits(system: readonly AiSystemBlock[]): boolean {\n return system.filter((block) => block.cacheBreakpoint).length < AI_MAX_CACHE_BREAKPOINTS\n}\n\n/**\n * Whether a provider's model reads a picture in a user turn (AGL-2916): what\n * the provider's own descriptor of the model says. A model the provider does\n * not describe reads none, so an adopter's provider is never sent a picture\n * it did not say it can take.\n */\nexport function aiModelReadsImages(provider: AiProvider, model: string): boolean {\n return provider\n .models()\n .some((descriptor) => descriptor.id === model && descriptor.capabilities.vision === true)\n}\n\n/**\n * Whether a model a door resolved reads pictures on the provider that would\n * serve the request: what a door asks before it attaches one, so a model that\n * does not is sent the text alone rather than refused.\n */\nexport function aiRouteReadsImages(model: string, settings?: AiPluginSettings): boolean {\n const provider = resolveAiProvider(settings)\n return provider ? aiModelReadsImages(provider, model) : false\n}\n\n/** A character standard base64 does not use, outside its padding. */\nconst NOT_BASE64 = /[^A-Za-z0-9+/]/\n\n/**\n * Whether `data` is standard base64 and nothing else: its alphabet, then at\n * most two `=` of padding; no `data:` prefix, no line breaks.\n *\n * Scanned for a character outside the alphabet rather than matched whole: a\n * picture is megabytes of base64, and a whole-string pattern with a greedy\n * loop can exhaust the regular expression engine's backtrack stack on input\n * that large, which throws a RangeError instead of answering.\n */\nfunction isBareBase64(data: string): boolean {\n const body = data.endsWith('==') ? data.slice(0, -2) : data.endsWith('=') ? data.slice(0, -1) : data\n return body.length > 0 && !NOT_BASE64.test(body)\n}\n\n/**\n * The picture guard (AGL-2916), applied to the messages before any adapter\n * sees them. A picture rides only a user turn, only to a model that reads\n * pictures, only in a format every such model takes, as bare base64 within\n * the size a provider accepts, and no more of them than a request may carry.\n * `readsImages` is asked only when a turn carries a picture. Exported so a\n * spec and a door can assert a request's shape without a provider.\n */\nexport function validateAiMessages(messages: readonly AiMessage[], readsImages: () => boolean): void {\n messages.forEach((message, index) => {\n if (typeof message.content === 'string') return\n if (!Array.isArray(message.content) || message.content.length === 0) {\n throw new AiRequestShapeError(`message ${index} has neither text nor parts`)\n }\n for (const part of message.content) {\n if (part.type === 'text') {\n if (typeof part.text !== 'string') {\n throw new AiRequestShapeError(`message ${index} has a text part with no text`)\n }\n continue\n }\n if (part.type !== 'image') {\n throw new AiRequestShapeError(`message ${index} has a part of an unknown type`)\n }\n if (message.role !== 'user') {\n throw new AiRequestShapeError(`message ${index} is an assistant turn and carries a picture`)\n }\n if (!(AI_IMAGE_MEDIA_TYPES as readonly string[]).includes(part.mediaType)) {\n throw new AiRequestShapeError(`message ${index} carries a picture of an unsupported type`)\n }\n if (typeof part.data !== 'string') {\n throw new AiRequestShapeError(`message ${index} carries a picture that is not bare base64`)\n }\n // The size first: it needs only the length, so an oversized picture is\n // refused without reading its bytes.\n if (aiBase64Bytes(part.data) > AI_IMAGE_MAX_BYTES) {\n throw new AiRequestShapeError(\n `message ${index} carries a picture over ${AI_IMAGE_MAX_BYTES} bytes`,\n )\n }\n if (!isBareBase64(part.data)) {\n throw new AiRequestShapeError(`message ${index} carries a picture that is not bare base64`)\n }\n }\n })\n const images = aiMessageImages(messages).length\n if (images > AI_REQUEST_MAX_IMAGES) {\n throw new AiRequestShapeError(`${images} pictures; a request carries at most ${AI_REQUEST_MAX_IMAGES}`)\n }\n if (images > 0 && !readsImages()) {\n throw new AiRequestShapeError('the request carries a picture and its model does not read pictures')\n }\n}\n\n/**\n * Characters to a token when a prompt is measured rather than tokenized\n * (AGL-2937). Four is the high end of English prose, so reading a prefix\n * through it errs LOW on the token count — and a prefix counts as cached\n * only when it clears its model's minimum even at that reading. Erring the\n * other way would let a prompt claim a cache it does not get.\n */\nexport const AI_CACHE_PREFIX_TOKEN_CHARS = 4\n\n/**\n * The characters inside the cached span of a request: the tools, which a\n * provider renders ahead of the system blocks and which therefore sit in\n * every prefix, plus every system block up to and including the last\n * breakpoint. Zero when the request marks no breakpoint at all.\n */\nexport function aiCachedPrefixChars(\n system: readonly AiSystemBlock[],\n tools: readonly AiTool[] = [],\n): number {\n const lastBreakpoint = system.reduce(\n (last, block, index) => (block.cacheBreakpoint ? index : last),\n -1,\n )\n if (lastBreakpoint < 0) return 0\n const toolChars = tools.reduce((total, tool) => total + JSON.stringify(tool).length, 0)\n return system\n .slice(0, lastBreakpoint + 1)\n .reduce((total, block) => total + block.text.length, toolChars)\n}\n\n/**\n * Whether this request's cached span is long enough for the model to cache\n * it at all (AGL-2937). A prefix under the model's `cacheMinTokens` is\n * served with the breakpoints honored and nothing cached — no error, and a\n * usage report that reads as a permanent miss — so a door designed around a\n * cache has to answer this question before it believes its own markers, and\n * `runtime/ai-prompt-cache.spec.ts` answers it for every door at once.\n */\nexport function aiCachedPrefixCaches(\n model: string,\n system: readonly AiSystemBlock[],\n tools: readonly AiTool[] = [],\n): boolean {\n const chars = aiCachedPrefixChars(system, tools)\n return chars / AI_CACHE_PREFIX_TOKEN_CHARS >= aiModelCacheMinTokens(model)\n}\n\n/**\n * Whether a provider is configured to answer: registered, and its key\n * present. Every door asks this before it spends a reservation, and\n * answers 501 (or the docs fallback) when it is false.\n */\nexport function aiProviderReady(settings?: AiPluginSettings): boolean {\n return Boolean(resolveAiProvider(settings)?.readApiKey())\n}\n\n/** The provider that would answer, for a door that names it in its logs or its inventory. */\nexport function aiActiveProvider(settings?: AiPluginSettings): AiProvider | undefined {\n return resolveAiProvider(settings)\n}\n\nfunction providerFor(input: AiRuntimeOptions): { provider: AiProvider; apiKey: string } {\n const provider = input.provider ?? resolveAiProvider(input.settings)\n if (!provider) throw new Error('no AI provider is registered')\n const apiKey = provider.readApiKey()\n if (!apiKey) {\n // Every door gates on `aiProviderReady` before it gets here, so this is\n // a wiring fault rather than a customer path.\n throw new Error(`${provider.apiKeyEnv} is not set`)\n }\n return { provider, apiKey }\n}\n\n/**\n * Send one request through the resolved provider.\n *\n * Streaming: resolves once the provider has ACCEPTED the request — after\n * the headers, before the first token — with an iterator of simplified\n * events the caller wraps in its own wire format. A non-2xx answer rejects\n * with `AiUpstreamError` before the iterator exists, so a door can still\n * send a plain error status and hand back its reservation.\n *\n * Non-streaming: resolves with the parsed result, a decline typed as\n * `AiRefusal` rather than thrown.\n */\nexport async function runAiRequest(\n input: AiStreamRequest,\n): Promise<AsyncIterable<AiStreamEvent>>\nexport async function runAiRequest(input: AiRequest): Promise<AiResult>\nexport async function runAiRequest(\n input: AiRequest | AiStreamRequest,\n): Promise<AsyncIterable<AiStreamEvent> | AiResult> {\n validateAiSystemBlocks(input.system)\n if (input.cacheConversation && !aiConversationCacheFits(input.system)) {\n throw new AiRequestShapeError(\n `the conversation's breakpoint would be the ${AI_MAX_CACHE_BREAKPOINTS + 1}th; at most ${AI_MAX_CACHE_BREAKPOINTS} are cached`,\n )\n }\n refuseRetiredModelInLiveRun(input.model)\n const { provider, apiKey } = providerFor(input)\n validateAiMessages(input.messages, () => aiModelReadsImages(provider, input.model))\n const { stream, settings: _settings, provider: _provider, ...request } = input\n aiWarnIfReplayAskedInDeployment()\n const cacheTtl = request.cacheTtl ?? aiDevCacheTtl()\n const providerRequest: AiProviderRequest = { ...request, ...(cacheTtl ? { cacheTtl } : {}), apiKey }\n try {\n if (aiIsDeployedRuntime()) {\n return await (stream ? provider.stream(providerRequest) : provider.complete(providerRequest))\n }\n return await runInDevelopment(provider, providerRequest, stream)\n } catch (error) {\n await reportAccountProblem(provider, error)\n throw error\n }\n}\n\n/**\n * A live development run that names a retired model is refused before it\n * spends (AGL-3781). Routing serves a retired id's successor wherever a\n * setting or an override names it, so a retired id reaching here was asked\n * for by name — an override left in a run, or a comparison a spec kept — and\n * would bill a model production no longer serves.\n */\nfunction refuseRetiredModelInLiveRun(model: string): void {\n if (!aiUnderLiveRun()) return\n const successor = aiCatalogEntry(model)?.successor\n if (successor) {\n throw new AiRequestShapeError(`${model} is retired; ${successor} serves in its place`)\n }\n}\n\n/**\n * A request in a development process (AGL-3660): through the replay cache,\n * batch mode and the first-request-warms-the-prefix rule when this process\n * asked for them (`ai-dev-env.ts`), straight to the provider when it asked\n * for none. The file-backed half is loaded only when a layer is on, so a dev\n * server that never asked loads nothing it did not before.\n */\nasync function runInDevelopment(\n provider: AiProvider,\n request: AiProviderRequest,\n stream: boolean,\n): Promise<AsyncIterable<AiStreamEvent> | AiResult> {\n const mode = aiDevReplayMode()\n const batch = !stream && aiLiveBatchWanted() && Boolean(provider.completeBatch)\n if (mode === 'off' && !batch) {\n return stream ? provider.stream(request) : provider.complete(request)\n }\n const dev = await import('./ai-dev-replay')\n const ttlMs = request.cacheTtl === '1h' ? 60 * 60_000 : 5 * 60_000\n const prefix = dev.aiCachedPrefixKey(request)\n if (stream) {\n const live = () => dev.aiDevWarmPrefixFirst(prefix, ttlMs, () => provider.stream(request))\n return mode === 'off' ? live() : dev.aiDevReplayStream(provider, request, mode, live)\n }\n const live = async (): Promise<AiResult> => {\n const result = batch\n ? await aiCompleteInBatch(provider, request)\n : await dev.aiDevWarmPrefixFirst(prefix, ttlMs, () => provider.complete(request))\n // A request its batch left unanswered was asked on its own, at full price (AGL-3781).\n const discounted = batch && !aiAnsweredOutsideBatch(result)\n dev.aiDevCountLive(result.usage, request.model, discounted ? AI_BATCH_PRICE_FACTOR : 1)\n return result\n }\n return mode === 'off' ? live() : dev.aiDevReplayComplete(provider, request, mode, live)\n}\n\n/** What a batch's discount leaves of a request's price, for the development count. */\nconst AI_BATCH_PRICE_FACTOR = 0.5\n\n/**\n * Tells the operator when the failure is the platform's provider account —\n * a refused key, no credit — rather than the request (AGL-3377). Once per\n * provider and problem per window; loaded lazily so the runtime pulls no\n * Admin SDK until an account is actually broken. Never throws.\n */\nasync function reportAccountProblem(provider: AiProvider, error: unknown): Promise<void> {\n if (!(error instanceof AiUpstreamError) || !error.accountProblem) return\n try {\n const [{ raiseOperatorAlert }, { AI_PROVIDER_UNAVAILABLE }] = await Promise.all([\n import('./operator-alerts-seam'),\n import('../operator-alerts'),\n ])\n await raiseOperatorAlert(AI_PROVIDER_UNAVAILABLE, {\n dedupeKey: `${provider.id}:${error.accountProblem}`,\n context: {\n provider: provider.label,\n keyEnv: provider.apiKeyEnv,\n reason:\n error.accountProblem === 'credentials'\n ? `its API key was refused (HTTP ${error.status ?? 'unknown'})`\n : 'the account is out of credit',\n },\n })\n } catch (alertError) {\n console.error('[ai] the provider account alert could not be raised', alertError)\n }\n}\n"],"names":["aiCatalogEntry","aiModelCacheMinTokens","AI_IMAGE_MAX_BYTES","AI_IMAGE_MEDIA_TYPES","AI_REQUEST_MAX_IMAGES","AiRequestShapeError","AiUpstreamError","aiBase64Bytes","aiMessageImages","resolveAiProvider","aiDevCacheTtl","aiDevReplayMode","aiIsDeployedRuntime","aiLiveBatchWanted","aiUnderLiveRun","aiWarnIfReplayAskedInDeployment","aiAnsweredOutsideBatch","aiCompleteInBatch","AI_UPSTREAM_FAILURE_COPY","AI_MAX_CACHE_BREAKPOINTS","AI_ACCEPTABLE_USE_BLOCK","validateAiSystemBlocks","system","breakpoints","filter","block","cacheBreakpoint","length","lastBreakpoint","reduce","last","index","forEach","volatile","firstSite","findIndex","site","aiConversationCacheFits","aiModelReadsImages","provider","model","models","some","descriptor","id","capabilities","vision","aiRouteReadsImages","settings","NOT_BASE64","isBareBase64","data","body","endsWith","slice","test","validateAiMessages","messages","readsImages","message","content","Array","isArray","part","type","text","role","includes","mediaType","images","AI_CACHE_PREFIX_TOKEN_CHARS","aiCachedPrefixChars","tools","toolChars","total","tool","JSON","stringify","aiCachedPrefixCaches","chars","aiProviderReady","Boolean","readApiKey","aiActiveProvider","providerFor","input","Error","apiKey","apiKeyEnv","runAiRequest","request","cacheConversation","refuseRetiredModelInLiveRun","stream","_settings","_provider","cacheTtl","providerRequest","complete","runInDevelopment","error","reportAccountProblem","successor","mode","batch","completeBatch","dev","ttlMs","prefix","aiCachedPrefixKey","live","aiDevWarmPrefixFirst","aiDevReplayStream","result","discounted","aiDevCountLive","usage","AI_BATCH_PRICE_FACTOR","aiDevReplayComplete","accountProblem","raiseOperatorAlert","AI_PROVIDER_UNAVAILABLE","Promise","all","dedupeKey","context","label","keyEnv","reason","status","alertError","console"],"mappings":"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"}
@@ -27,5 +27,7 @@ export declare function readStaffAccountAiRequests(db: Firestore, uid: string, k
27
27
  export declare function readStaffAiConversations(db: Firestore, orgId: string, kind: StaffAiConversationKind, after: string | null): Promise<StaffAiConversationsResponse>;
28
28
  /** One job of an org, as a one-row page — what an activity row's job link opens. */
29
29
  export declare function readStaffAiJob(db: Firestore, orgId: string, jobId: string): Promise<StaffAiConversationsResponse>;
30
+ /** One Assist exchange of an org, as a one-row page — what a list row on the staff apps opens. */
31
+ export declare function readStaffAiExchange(db: Firestore, orgId: string, exchangeId: string): Promise<StaffAiConversationsResponse>;
30
32
  declare function handler(request: Request): Promise<Response>;
31
33
  export { handler as GET };
@@ -18,12 +18,14 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { emailUnverifiedResponse, firebaseAdmin, isImpersonationSession } from "@aglyn/tenant-data-admin";
19
19
  import { addAdminAudit } from "@aglyn/tenant-data-admin/server/admin-audit-write";
20
20
  import { composeStaffAiAssistRow, composeStaffAiJobRow, isStaffAiConversationKind, STAFF_AI_CONVERSATIONS_PAGE, staffAiCrmResult, staffAiInsightResult, staffAiJobResultCollection } from "../usage/staff-org-ai-conversations.js";
21
+ import { withRowViews } from "../usage/staff-org-ai-conversations-view.js";
21
22
  /**
22
23
  * `/api/ai/admin/conversations` (AGL-3675): one page of what an org asked
23
24
  * Aglyn AI and what it answered, for the staff org page.
24
25
  *
25
26
  * `?orgId=` and `?kind=assist|jobs`, newest first, and `?after=<id>` for the
26
- * next page. Staff-gated exactly as `/api/ai/admin/org` is. The collections
27
+ * next page. `?id=` reads the one exchange or job a list row opens, and
28
+ * `?view=prepared` sends each row with the lines the card prints beside it. Staff-gated exactly as `/api/ai/admin/org` is. The collections
27
29
  * it reads have no client rules at all — default-deny for every browser —
28
30
  * which is why the read is here, on the Admin SDK.
29
31
  *
@@ -225,8 +227,31 @@ async function readMembers(orgRef) {
225
227
  next: null
226
228
  };
227
229
  }
230
+ /** One Assist exchange of an org, as a one-row page — what a list row on the staff apps opens. */ export async function readStaffAiExchange(db, orgId, exchangeId) {
231
+ var _doc_data, _signal_data;
232
+ const orgRef = db.collection('orgs').doc(orgId);
233
+ const [doc, signal, members] = await Promise.all([
234
+ orgRef.collection('assistExchanges').doc(exchangeId).get(),
235
+ orgRef.collection('assistSignals').doc(exchangeId).get(),
236
+ readMembers(orgRef)
237
+ ]);
238
+ if (!doc.exists) return {
239
+ kind: 'assist',
240
+ rows: [],
241
+ next: null
242
+ };
243
+ return {
244
+ kind: 'assist',
245
+ rows: [
246
+ _extends({}, composeStaffAiAssistRow(doc.id, (_doc_data = doc.data()) != null ? _doc_data : {}, signal.exists ? (_signal_data = signal.data()) != null ? _signal_data : null : null, members), {
247
+ orgId
248
+ })
249
+ ],
250
+ next: null
251
+ };
252
+ }
228
253
  async function handler(request) {
229
- var _headers_authorization, _query_orgId, _query_uid;
254
+ var _headers_authorization, _query_orgId, _query_uid, _query_view;
230
255
  const { method, query, headers: rawHeaders } = await pluginRequestFromWeb(request);
231
256
  const headers = rawHeaders;
232
257
  if (method !== 'GET') {
@@ -252,8 +277,6 @@ async function handler(request) {
252
277
  }, {
253
278
  status: 400
254
279
  });
255
- // One job, for a link from an activity or audit row to the job behind it.
256
- const jobId = typeof query.jobId === 'string' && query.jobId ? query.jobId : null;
257
280
  const kind = query.kind;
258
281
  if (!isStaffAiConversationKind(kind)) {
259
282
  return Response.json({
@@ -262,6 +285,14 @@ async function handler(request) {
262
285
  status: 400
263
286
  });
264
287
  }
288
+ // One row, for a link from an activity or audit row to the job behind it (`jobId`), or from a list row
289
+ // to the exchange or job it opens (`id`, of the `kind` asked).
290
+ const rowId = typeof query.id === 'string' && query.id ? query.id : null;
291
+ const jobId = typeof query.jobId === 'string' && query.jobId ? query.jobId : kind === 'jobs' ? rowId : null;
292
+ const exchangeId = !jobId && kind === 'assist' ? rowId : null;
293
+ // The rows with the lines the card prints beside them, for a client that draws none of it (the staff apps).
294
+ const prepared = String((_query_view = query.view) != null ? _query_view : '') === 'prepared';
295
+ const present = (page)=>prepared ? withRowViews(page) : page;
265
296
  const after = typeof query.after === 'string' && query.after ? query.after : null;
266
297
  try {
267
298
  const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
@@ -292,7 +323,7 @@ async function handler(request) {
292
323
  after
293
324
  }
294
325
  });
295
- return Response.json(page, {
326
+ return Response.json(present(page), {
296
327
  headers: {
297
328
  'Cache-Control': 'no-store'
298
329
  }
@@ -305,7 +336,7 @@ async function handler(request) {
305
336
  status: 404
306
337
  });
307
338
  }
308
- const page = jobId ? await readStaffAiJob(db, orgId, jobId) : await readStaffAiConversations(db, orgId, kind, after);
339
+ const page = jobId ? await readStaffAiJob(db, orgId, jobId) : exchangeId ? await readStaffAiExchange(db, orgId, exchangeId) : await readStaffAiConversations(db, orgId, kind, after);
309
340
  // Written before the words leave: a read the log cannot record is not served.
310
341
  await addAdminAudit(db, {
311
342
  actorUid: decoded.uid,
@@ -318,9 +349,11 @@ async function handler(request) {
318
349
  after
319
350
  }, jobId ? {
320
351
  jobId
352
+ } : {}, exchangeId ? {
353
+ exchangeId
321
354
  } : {})
322
355
  });
323
- return Response.json(page, {
356
+ return Response.json(present(page), {
324
357
  headers: {
325
358
  'Cache-Control': 'no-store'
326
359
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-admin-conversations.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 emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin'\nimport { addAdminAudit } from '@aglyn/tenant-data-admin/server/admin-audit-write'\nimport {\n composeStaffAiAssistRow,\n composeStaffAiJobRow,\n isStaffAiConversationKind,\n STAFF_AI_CONVERSATIONS_PAGE,\n staffAiCrmResult,\n staffAiInsightResult,\n staffAiJobResultCollection,\n type StaffAiJobResult,\n type StaffAiConversationKind,\n type StaffAiConversationsResponse,\n} from '../usage/staff-org-ai-conversations'\n\n/**\n * `/api/ai/admin/conversations` (AGL-3675): one page of what an org asked\n * Aglyn AI and what it answered, for the staff org page.\n *\n * `?orgId=` and `?kind=assist|jobs`, newest first, and `?after=<id>` for the\n * next page. Staff-gated exactly as `/api/ai/admin/org` is. The collections\n * it reads have no client rules at all — default-deny for every browser —\n * which is why the read is here, on the Admin SDK.\n *\n * ## Every page read is audited, and the audit holds no text\n *\n * Unlike the usage card, which loads with the page and so writes no audit\n * row, this route returns what a person typed, and staff read it only when\n * they ask to. Each page writes an `org.ai-conversations-viewed` access row\n * naming who read which list of which org and how many rows came back. The\n * Privacy policy says our audit log records an AI job \"without the text of\n * its brief or of what it wrote\", so the row carries none of it.\n */\n\n/** Members read to name who asked; past it a row shows the uid alone. */\nconst MEMBERS_SCAN = 500\n\nconst ACTION = 'org.ai-conversations-viewed'\n/** The same read, of one account's requests across every org (AGL-3660). */\nconst USER_ACTION = 'user.ai-requests-viewed'\n\ntype Firestore = FirebaseFirestore.Firestore\ntype OrgRef = FirebaseFirestore.DocumentReference\n\n/** One page of a collection under the org, newest first, after the document `after` names. */\nasync function readPage(\n orgRef: OrgRef,\n collection: 'assistExchanges' | 'aiJobs',\n after: string | null,\n): Promise<{ docs: FirebaseFirestore.QueryDocumentSnapshot[]; next: string | null }> {\n let query = orgRef.collection(collection).orderBy('createdAt', 'desc').limit(STAFF_AI_CONVERSATIONS_PAGE + 1)\n if (after) {\n const cursor = await orgRef.collection(collection).doc(after).get()\n if (cursor.exists) query = query.startAfter(cursor)\n }\n const snapshot = await query.get()\n const docs = snapshot.docs.slice(0, STAFF_AI_CONVERSATIONS_PAGE)\n const next = snapshot.docs.length > STAFF_AI_CONVERSATIONS_PAGE ? docs[docs.length - 1]?.id ?? null : null\n return { docs, next }\n}\n\nasync function readMembers(orgRef: OrgRef): Promise<Map<string, Record<string, unknown>>> {\n const snapshot = await orgRef.collection('members').limit(MEMBERS_SCAN).get()\n return new Map(snapshot.docs.map((doc) => [doc.id, doc.data() ?? {}]))\n}\n\n/** The documents `refs` name, by id, in one round trip; a missing one is absent. */\nasync function readAll(\n db: Firestore,\n refs: FirebaseFirestore.DocumentReference[],\n): Promise<Map<string, Record<string, unknown>>> {\n if (!refs.length) return new Map()\n const snapshots = await db.getAll(...refs)\n return new Map(\n snapshots.filter((snapshot) => snapshot.exists).map((snapshot) => [snapshot.id, snapshot.data() ?? {}]),\n )\n}\n\n/** The org a document under `orgs/{orgId}/…` is filed in. */\nconst orgOf = (doc: FirebaseFirestore.DocumentSnapshot): string | null =>\n doc.ref.parent.parent?.id ?? null\n\n/**\n * One page of what ONE ACCOUNT asked Aglyn AI (AGL-3660), across every\n * organization it has asked in — including one it has since left, which is\n * the history a support question about it needs. Collection-group reads on\n * the creator, newest first (`firebase-firestore.indexes.json`).\n */\nexport async function readStaffAccountAiRequests(\n db: Firestore,\n uid: string,\n kind: StaffAiConversationKind,\n after: string | null,\n): Promise<StaffAiConversationsResponse> {\n const group = kind === 'assist' ? 'assistExchanges' : 'aiJobs'\n const field = kind === 'assist' ? 'uid' : 'createdBy'\n let query = db\n .collectionGroup(group)\n .where(field, '==', uid)\n .orderBy('createdAt', 'desc')\n .limit(STAFF_AI_CONVERSATIONS_PAGE + 1)\n if (after) {\n // The cursor is the document's PATH: an id is unique only within one org.\n const cursor = await db.doc(after).get()\n if (cursor.exists) query = query.startAfter(cursor)\n }\n const snapshot = await query.get()\n const docs = snapshot.docs.slice(0, STAFF_AI_CONVERSATIONS_PAGE)\n const next =\n snapshot.docs.length > STAFF_AI_CONVERSATIONS_PAGE ? (docs[docs.length - 1]?.ref.path ?? null) : null\n const none = new Map<string, Record<string, unknown>>()\n if (kind === 'assist') {\n const signals = await readAll(\n db,\n docs.map((doc) => doc.ref.parent.parent!.collection('assistSignals').doc(doc.id)),\n )\n return {\n kind,\n rows: docs.map((doc) => ({\n ...composeStaffAiAssistRow(doc.id, doc.data() ?? {}, signals.get(doc.id) ?? null, none),\n orgId: orgOf(doc),\n })),\n next,\n }\n }\n const results = await readJobResults(\n db,\n docs.map((doc) => ({ key: doc.ref.path, orgRef: doc.ref.parent.parent!, id: doc.id, kind: doc.get('kind') })),\n )\n return {\n kind,\n rows: docs.map((doc) => ({\n ...composeStaffAiJobRow(doc.id, doc.data() ?? {}, results.get(doc.ref.path) ?? null, none),\n orgId: orgOf(doc),\n })),\n next,\n }\n}\n\n/** The insight or CRM answer kept beside each job that keeps one, by the caller's key. */\nasync function readJobResults(\n db: Firestore,\n jobs: ReadonlyArray<{ key: string; orgRef: OrgRef; id: string; kind: unknown }>,\n): Promise<Map<string, StaffAiJobResult | null>> {\n const wanted = jobs.flatMap((job) => {\n const collection = staffAiJobResultCollection(job.kind)\n return collection ? [{ ...job, collection, ref: job.orgRef.collection(collection).doc(job.id) }] : []\n })\n const out = new Map<string, StaffAiJobResult | null>()\n if (!wanted.length) return out\n // `getAll` answers in the order it was asked.\n const snapshots = await db.getAll(...wanted.map((job) => job.ref))\n wanted.forEach((job, index) => {\n const snapshot = snapshots[index]\n const data = snapshot?.exists ? (snapshot.data() ?? {}) : null\n out.set(job.key, job.collection === 'aiInsights' ? staffAiInsightResult(data) : staffAiCrmResult(data))\n })\n return out\n}\n\n/** One page of the org's conversations of `kind`. Exported for the spec. */\nexport async function readStaffAiConversations(\n db: Firestore,\n orgId: string,\n kind: StaffAiConversationKind,\n after: string | null,\n): Promise<StaffAiConversationsResponse> {\n const orgRef = db.collection('orgs').doc(orgId)\n if (kind === 'assist') {\n const [{ docs, next }, members] = await Promise.all([readPage(orgRef, 'assistExchanges', after), readMembers(orgRef)])\n // The signal shares the exchange's id (AGL-1972): the route, the model, the cost and the thumbs.\n const signals = await readAll(db, docs.map((doc) => orgRef.collection('assistSignals').doc(doc.id)))\n return {\n kind,\n rows: docs.map((doc) => ({\n ...composeStaffAiAssistRow(doc.id, doc.data() ?? {}, signals.get(doc.id) ?? null, members),\n orgId,\n })),\n next,\n }\n }\n const [{ docs, next }, members] = await Promise.all([readPage(orgRef, 'aiJobs', after), readMembers(orgRef)])\n const insightIds = docs.filter((doc) => staffAiJobResultCollection(doc.get('kind')) === 'aiInsights').map((doc) => doc.id)\n const crmIds = docs.filter((doc) => staffAiJobResultCollection(doc.get('kind')) === 'aiCrmAnswers').map((doc) => doc.id)\n const [insights, crm] = await Promise.all([\n readAll(db, insightIds.map((id) => orgRef.collection('aiInsights').doc(id))),\n readAll(db, crmIds.map((id) => orgRef.collection('aiCrmAnswers').doc(id))),\n ])\n return {\n kind,\n rows: docs.map((doc) => {\n const collection = staffAiJobResultCollection(doc.get('kind'))\n const result =\n collection === 'aiInsights'\n ? staffAiInsightResult(insights.get(doc.id) ?? null)\n : collection === 'aiCrmAnswers'\n ? staffAiCrmResult(crm.get(doc.id) ?? null)\n : null\n return { ...composeStaffAiJobRow(doc.id, doc.data() ?? {}, result, members), orgId }\n }),\n next,\n }\n}\n\n/** One job of an org, as a one-row page — what an activity row's job link opens. */\nexport async function readStaffAiJob(\n db: Firestore,\n orgId: string,\n jobId: string,\n): Promise<StaffAiConversationsResponse> {\n const orgRef = db.collection('orgs').doc(orgId)\n const [doc, members] = await Promise.all([orgRef.collection('aiJobs').doc(jobId).get(), readMembers(orgRef)])\n if (!doc.exists) return { kind: 'jobs', rows: [], next: null }\n const results = await readJobResults(db, [{ key: jobId, orgRef, id: jobId, kind: doc.get('kind') }])\n return {\n kind: 'jobs',\n rows: [{ ...composeStaffAiJobRow(doc.id, doc.data() ?? {}, results.get(jobId) ?? null, members), orgId }],\n next: null,\n }\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 ') ? authorization.slice('Bearer '.length) : undefined\n if (!idToken) return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n const orgId = String(query.orgId ?? '')\n // An account's requests across every org (AGL-3660), for the staff\n // account page; exactly one of the two is the subject.\n const uid = String(query.uid ?? '')\n if (!orgId === !uid) return Response.json({ error: 'Pass orgId or uid' }, { status: 400 })\n // One job, for a link from an activity or audit row to the job behind it.\n const jobId = typeof query.jobId === 'string' && query.jobId ? query.jobId : null\n const kind = query.kind\n if (!isStaffAiConversationKind(kind)) {\n return Response.json({ error: 'kind must be assist or jobs' }, { status: 400 })\n }\n const after = typeof query.after === 'string' && query.after ? query.after : null\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 const db = firebaseAdmin.app().firestore()\n if (uid) {\n const page = await readStaffAccountAiRequests(db, uid, kind, after)\n // Written before the words leave, as for an org; filed against the\n // account so its page's \"Data access by staff\" lists the read.\n await addAdminAudit(db, {\n actorUid: decoded.uid,\n action: USER_ACTION,\n target: `users/${uid}`,\n subjectUid: uid,\n before: null,\n after: { kind, rows: page.rows.length, after },\n })\n return Response.json(page, { headers: { 'Cache-Control': 'no-store' } })\n }\n if (!(await db.collection('orgs').doc(orgId).get()).exists) {\n return Response.json({ error: 'No such organization' }, { status: 404 })\n }\n const page = jobId\n ? await readStaffAiJob(db, orgId, jobId)\n : await readStaffAiConversations(db, orgId, kind, after)\n // Written before the words leave: a read the log cannot record is not served.\n await addAdminAudit(db, {\n actorUid: decoded.uid,\n action: ACTION,\n target: `orgs/${orgId}`,\n before: null,\n after: { kind: jobId ? 'jobs' : kind, rows: page.rows.length, after, ...(jobId ? { jobId } : {}) },\n })\n return Response.json(page, { headers: { 'Cache-Control': 'no-store' } })\n } catch (error) {\n console.error('[ai/admin/conversations]', error)\n return Response.json({ error: 'The conversations could not be read.' }, { status: 500 })\n }\n}\n\nexport { handler as GET }\n"],"names":["pluginRequestFromWeb","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","addAdminAudit","composeStaffAiAssistRow","composeStaffAiJobRow","isStaffAiConversationKind","STAFF_AI_CONVERSATIONS_PAGE","staffAiCrmResult","staffAiInsightResult","staffAiJobResultCollection","MEMBERS_SCAN","ACTION","USER_ACTION","readPage","orgRef","collection","after","docs","query","orderBy","limit","cursor","doc","get","exists","startAfter","snapshot","slice","next","length","id","readMembers","Map","map","data","readAll","db","refs","snapshots","getAll","filter","orgOf","ref","parent","readStaffAccountAiRequests","uid","kind","group","field","collectionGroup","where","path","none","signals","rows","orgId","results","readJobResults","key","jobs","wanted","flatMap","job","out","forEach","index","set","readStaffAiConversations","members","Promise","all","insightIds","crmIds","insights","crm","result","readStaffAiJob","jobId","handler","request","headers","method","rawHeaders","Response","json","error","status","authorization","idToken","startsWith","undefined","String","decoded","app","auth","verifyIdToken","email_verified","firestore","page","actorUid","action","target","subjectUid","before","console","GET"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-admin-conversations.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 emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin'\nimport { addAdminAudit } from '@aglyn/tenant-data-admin/server/admin-audit-write'\nimport {\n composeStaffAiAssistRow,\n composeStaffAiJobRow,\n isStaffAiConversationKind,\n STAFF_AI_CONVERSATIONS_PAGE,\n staffAiCrmResult,\n staffAiInsightResult,\n staffAiJobResultCollection,\n type StaffAiJobResult,\n type StaffAiConversationKind,\n type StaffAiConversationsResponse,\n} from '../usage/staff-org-ai-conversations'\nimport { withRowViews } from '../usage/staff-org-ai-conversations-view'\n\n/**\n * `/api/ai/admin/conversations` (AGL-3675): one page of what an org asked\n * Aglyn AI and what it answered, for the staff org page.\n *\n * `?orgId=` and `?kind=assist|jobs`, newest first, and `?after=<id>` for the\n * next page. `?id=` reads the one exchange or job a list row opens, and\n * `?view=prepared` sends each row with the lines the card prints beside it. Staff-gated exactly as `/api/ai/admin/org` is. The collections\n * it reads have no client rules at all — default-deny for every browser —\n * which is why the read is here, on the Admin SDK.\n *\n * ## Every page read is audited, and the audit holds no text\n *\n * Unlike the usage card, which loads with the page and so writes no audit\n * row, this route returns what a person typed, and staff read it only when\n * they ask to. Each page writes an `org.ai-conversations-viewed` access row\n * naming who read which list of which org and how many rows came back. The\n * Privacy policy says our audit log records an AI job \"without the text of\n * its brief or of what it wrote\", so the row carries none of it.\n */\n\n/** Members read to name who asked; past it a row shows the uid alone. */\nconst MEMBERS_SCAN = 500\n\nconst ACTION = 'org.ai-conversations-viewed'\n/** The same read, of one account's requests across every org (AGL-3660). */\nconst USER_ACTION = 'user.ai-requests-viewed'\n\ntype Firestore = FirebaseFirestore.Firestore\ntype OrgRef = FirebaseFirestore.DocumentReference\n\n/** One page of a collection under the org, newest first, after the document `after` names. */\nasync function readPage(\n orgRef: OrgRef,\n collection: 'assistExchanges' | 'aiJobs',\n after: string | null,\n): Promise<{ docs: FirebaseFirestore.QueryDocumentSnapshot[]; next: string | null }> {\n let query = orgRef.collection(collection).orderBy('createdAt', 'desc').limit(STAFF_AI_CONVERSATIONS_PAGE + 1)\n if (after) {\n const cursor = await orgRef.collection(collection).doc(after).get()\n if (cursor.exists) query = query.startAfter(cursor)\n }\n const snapshot = await query.get()\n const docs = snapshot.docs.slice(0, STAFF_AI_CONVERSATIONS_PAGE)\n const next = snapshot.docs.length > STAFF_AI_CONVERSATIONS_PAGE ? docs[docs.length - 1]?.id ?? null : null\n return { docs, next }\n}\n\nasync function readMembers(orgRef: OrgRef): Promise<Map<string, Record<string, unknown>>> {\n const snapshot = await orgRef.collection('members').limit(MEMBERS_SCAN).get()\n return new Map(snapshot.docs.map((doc) => [doc.id, doc.data() ?? {}]))\n}\n\n/** The documents `refs` name, by id, in one round trip; a missing one is absent. */\nasync function readAll(\n db: Firestore,\n refs: FirebaseFirestore.DocumentReference[],\n): Promise<Map<string, Record<string, unknown>>> {\n if (!refs.length) return new Map()\n const snapshots = await db.getAll(...refs)\n return new Map(\n snapshots.filter((snapshot) => snapshot.exists).map((snapshot) => [snapshot.id, snapshot.data() ?? {}]),\n )\n}\n\n/** The org a document under `orgs/{orgId}/…` is filed in. */\nconst orgOf = (doc: FirebaseFirestore.DocumentSnapshot): string | null =>\n doc.ref.parent.parent?.id ?? null\n\n/**\n * One page of what ONE ACCOUNT asked Aglyn AI (AGL-3660), across every\n * organization it has asked in — including one it has since left, which is\n * the history a support question about it needs. Collection-group reads on\n * the creator, newest first (`firebase-firestore.indexes.json`).\n */\nexport async function readStaffAccountAiRequests(\n db: Firestore,\n uid: string,\n kind: StaffAiConversationKind,\n after: string | null,\n): Promise<StaffAiConversationsResponse> {\n const group = kind === 'assist' ? 'assistExchanges' : 'aiJobs'\n const field = kind === 'assist' ? 'uid' : 'createdBy'\n let query = db\n .collectionGroup(group)\n .where(field, '==', uid)\n .orderBy('createdAt', 'desc')\n .limit(STAFF_AI_CONVERSATIONS_PAGE + 1)\n if (after) {\n // The cursor is the document's PATH: an id is unique only within one org.\n const cursor = await db.doc(after).get()\n if (cursor.exists) query = query.startAfter(cursor)\n }\n const snapshot = await query.get()\n const docs = snapshot.docs.slice(0, STAFF_AI_CONVERSATIONS_PAGE)\n const next =\n snapshot.docs.length > STAFF_AI_CONVERSATIONS_PAGE ? (docs[docs.length - 1]?.ref.path ?? null) : null\n const none = new Map<string, Record<string, unknown>>()\n if (kind === 'assist') {\n const signals = await readAll(\n db,\n docs.map((doc) => doc.ref.parent.parent!.collection('assistSignals').doc(doc.id)),\n )\n return {\n kind,\n rows: docs.map((doc) => ({\n ...composeStaffAiAssistRow(doc.id, doc.data() ?? {}, signals.get(doc.id) ?? null, none),\n orgId: orgOf(doc),\n })),\n next,\n }\n }\n const results = await readJobResults(\n db,\n docs.map((doc) => ({ key: doc.ref.path, orgRef: doc.ref.parent.parent!, id: doc.id, kind: doc.get('kind') })),\n )\n return {\n kind,\n rows: docs.map((doc) => ({\n ...composeStaffAiJobRow(doc.id, doc.data() ?? {}, results.get(doc.ref.path) ?? null, none),\n orgId: orgOf(doc),\n })),\n next,\n }\n}\n\n/** The insight or CRM answer kept beside each job that keeps one, by the caller's key. */\nasync function readJobResults(\n db: Firestore,\n jobs: ReadonlyArray<{ key: string; orgRef: OrgRef; id: string; kind: unknown }>,\n): Promise<Map<string, StaffAiJobResult | null>> {\n const wanted = jobs.flatMap((job) => {\n const collection = staffAiJobResultCollection(job.kind)\n return collection ? [{ ...job, collection, ref: job.orgRef.collection(collection).doc(job.id) }] : []\n })\n const out = new Map<string, StaffAiJobResult | null>()\n if (!wanted.length) return out\n // `getAll` answers in the order it was asked.\n const snapshots = await db.getAll(...wanted.map((job) => job.ref))\n wanted.forEach((job, index) => {\n const snapshot = snapshots[index]\n const data = snapshot?.exists ? (snapshot.data() ?? {}) : null\n out.set(job.key, job.collection === 'aiInsights' ? staffAiInsightResult(data) : staffAiCrmResult(data))\n })\n return out\n}\n\n/** One page of the org's conversations of `kind`. Exported for the spec. */\nexport async function readStaffAiConversations(\n db: Firestore,\n orgId: string,\n kind: StaffAiConversationKind,\n after: string | null,\n): Promise<StaffAiConversationsResponse> {\n const orgRef = db.collection('orgs').doc(orgId)\n if (kind === 'assist') {\n const [{ docs, next }, members] = await Promise.all([readPage(orgRef, 'assistExchanges', after), readMembers(orgRef)])\n // The signal shares the exchange's id (AGL-1972): the route, the model, the cost and the thumbs.\n const signals = await readAll(db, docs.map((doc) => orgRef.collection('assistSignals').doc(doc.id)))\n return {\n kind,\n rows: docs.map((doc) => ({\n ...composeStaffAiAssistRow(doc.id, doc.data() ?? {}, signals.get(doc.id) ?? null, members),\n orgId,\n })),\n next,\n }\n }\n const [{ docs, next }, members] = await Promise.all([readPage(orgRef, 'aiJobs', after), readMembers(orgRef)])\n const insightIds = docs.filter((doc) => staffAiJobResultCollection(doc.get('kind')) === 'aiInsights').map((doc) => doc.id)\n const crmIds = docs.filter((doc) => staffAiJobResultCollection(doc.get('kind')) === 'aiCrmAnswers').map((doc) => doc.id)\n const [insights, crm] = await Promise.all([\n readAll(db, insightIds.map((id) => orgRef.collection('aiInsights').doc(id))),\n readAll(db, crmIds.map((id) => orgRef.collection('aiCrmAnswers').doc(id))),\n ])\n return {\n kind,\n rows: docs.map((doc) => {\n const collection = staffAiJobResultCollection(doc.get('kind'))\n const result =\n collection === 'aiInsights'\n ? staffAiInsightResult(insights.get(doc.id) ?? null)\n : collection === 'aiCrmAnswers'\n ? staffAiCrmResult(crm.get(doc.id) ?? null)\n : null\n return { ...composeStaffAiJobRow(doc.id, doc.data() ?? {}, result, members), orgId }\n }),\n next,\n }\n}\n\n/** One job of an org, as a one-row page — what an activity row's job link opens. */\nexport async function readStaffAiJob(\n db: Firestore,\n orgId: string,\n jobId: string,\n): Promise<StaffAiConversationsResponse> {\n const orgRef = db.collection('orgs').doc(orgId)\n const [doc, members] = await Promise.all([orgRef.collection('aiJobs').doc(jobId).get(), readMembers(orgRef)])\n if (!doc.exists) return { kind: 'jobs', rows: [], next: null }\n const results = await readJobResults(db, [{ key: jobId, orgRef, id: jobId, kind: doc.get('kind') }])\n return {\n kind: 'jobs',\n rows: [{ ...composeStaffAiJobRow(doc.id, doc.data() ?? {}, results.get(jobId) ?? null, members), orgId }],\n next: null,\n }\n}\n\n/** One Assist exchange of an org, as a one-row page — what a list row on the staff apps opens. */\nexport async function readStaffAiExchange(\n db: Firestore,\n orgId: string,\n exchangeId: string,\n): Promise<StaffAiConversationsResponse> {\n const orgRef = db.collection('orgs').doc(orgId)\n const [doc, signal, members] = await Promise.all([\n orgRef.collection('assistExchanges').doc(exchangeId).get(),\n orgRef.collection('assistSignals').doc(exchangeId).get(),\n readMembers(orgRef),\n ])\n if (!doc.exists) return { kind: 'assist', rows: [], next: null }\n return {\n kind: 'assist',\n rows: [\n {\n ...composeStaffAiAssistRow(doc.id, doc.data() ?? {}, signal.exists ? (signal.data() ?? null) : null, members),\n orgId,\n },\n ],\n next: null,\n }\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 ') ? authorization.slice('Bearer '.length) : undefined\n if (!idToken) return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n const orgId = String(query.orgId ?? '')\n // An account's requests across every org (AGL-3660), for the staff\n // account page; exactly one of the two is the subject.\n const uid = String(query.uid ?? '')\n if (!orgId === !uid) return Response.json({ error: 'Pass orgId or uid' }, { status: 400 })\n const kind = query.kind\n if (!isStaffAiConversationKind(kind)) {\n return Response.json({ error: 'kind must be assist or jobs' }, { status: 400 })\n }\n // One row, for a link from an activity or audit row to the job behind it (`jobId`), or from a list row\n // to the exchange or job it opens (`id`, of the `kind` asked).\n const rowId = typeof query.id === 'string' && query.id ? query.id : null\n const jobId =\n typeof query.jobId === 'string' && query.jobId ? query.jobId : kind === 'jobs' ? rowId : null\n const exchangeId = !jobId && kind === 'assist' ? rowId : null\n // The rows with the lines the card prints beside them, for a client that draws none of it (the staff apps).\n const prepared = String(query.view ?? '') === 'prepared'\n const present = (page: StaffAiConversationsResponse): StaffAiConversationsResponse =>\n prepared ? withRowViews(page) : page\n const after = typeof query.after === 'string' && query.after ? query.after : null\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 const db = firebaseAdmin.app().firestore()\n if (uid) {\n const page = await readStaffAccountAiRequests(db, uid, kind, after)\n // Written before the words leave, as for an org; filed against the\n // account so its page's \"Data access by staff\" lists the read.\n await addAdminAudit(db, {\n actorUid: decoded.uid,\n action: USER_ACTION,\n target: `users/${uid}`,\n subjectUid: uid,\n before: null,\n after: { kind, rows: page.rows.length, after },\n })\n return Response.json(present(page), { headers: { 'Cache-Control': 'no-store' } })\n }\n if (!(await db.collection('orgs').doc(orgId).get()).exists) {\n return Response.json({ error: 'No such organization' }, { status: 404 })\n }\n const page = jobId\n ? await readStaffAiJob(db, orgId, jobId)\n : exchangeId\n ? await readStaffAiExchange(db, orgId, exchangeId)\n : await readStaffAiConversations(db, orgId, kind, after)\n // Written before the words leave: a read the log cannot record is not served.\n await addAdminAudit(db, {\n actorUid: decoded.uid,\n action: ACTION,\n target: `orgs/${orgId}`,\n before: null,\n after: {\n kind: jobId ? 'jobs' : kind,\n rows: page.rows.length,\n after,\n ...(jobId ? { jobId } : {}),\n ...(exchangeId ? { exchangeId } : {}),\n },\n })\n return Response.json(present(page), { headers: { 'Cache-Control': 'no-store' } })\n } catch (error) {\n console.error('[ai/admin/conversations]', error)\n return Response.json({ error: 'The conversations could not be read.' }, { status: 500 })\n }\n}\n\nexport { handler as GET }\n"],"names":["pluginRequestFromWeb","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","addAdminAudit","composeStaffAiAssistRow","composeStaffAiJobRow","isStaffAiConversationKind","STAFF_AI_CONVERSATIONS_PAGE","staffAiCrmResult","staffAiInsightResult","staffAiJobResultCollection","withRowViews","MEMBERS_SCAN","ACTION","USER_ACTION","readPage","orgRef","collection","after","docs","query","orderBy","limit","cursor","doc","get","exists","startAfter","snapshot","slice","next","length","id","readMembers","Map","map","data","readAll","db","refs","snapshots","getAll","filter","orgOf","ref","parent","readStaffAccountAiRequests","uid","kind","group","field","collectionGroup","where","path","none","signals","rows","orgId","results","readJobResults","key","jobs","wanted","flatMap","job","out","forEach","index","set","readStaffAiConversations","members","Promise","all","insightIds","crmIds","insights","crm","result","readStaffAiJob","jobId","readStaffAiExchange","exchangeId","signal","handler","request","headers","method","rawHeaders","Response","json","error","status","authorization","idToken","startsWith","undefined","String","rowId","prepared","view","present","page","decoded","app","auth","verifyIdToken","email_verified","firestore","actorUid","action","target","subjectUid","before","console","GET"],"mappings":"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"}
@@ -22,9 +22,12 @@ import { readOrgAiUsageByUser } from "../usage/ai-usage-by-user.js";
22
22
  import { invalidIdTokenResponse } from "@aglyn/tenant-data-admin/server/id-token-refusal";
23
23
  import { addonKindFromPriceId } from "@aglyn/tenant-data-admin/server/billing-addons";
24
24
  import { orgMarginRow } from "@aglyn/aglyn/app-utils/margin-utilization";
25
+ import { aiAddonName } from "@aglyn/aglyn/app-utils/plan-entitlements";
25
26
  import { assistCogsAlertThresholdUsd, assistMarginMultiple } from "../usage/assist-spend-guards.js";
26
27
  import { freeAccountUsageRef, freeAssistAccount } from "../usage/assist-free-taste.js";
27
28
  import { composeStaffAiAccountAllowance, composeStaffOrgAiAddon, composeStaffOrgAiMargin, composeStaffOrgAiOverage, composeStaffOrgAiPool, composeStaffOrgAiTokens, emptyJobCounts, STAFF_ORG_AI_JOB_STATUSES } from "../usage/staff-org-ai.js";
29
+ import { staffOrgAiView } from "../usage/staff-org-ai-view.js";
30
+ import { staffAssistPool } from "../usage/staff-org-usage-ai.js";
28
31
  /**
29
32
  * THE STAFF AI CARD'S BACKING READ (AGL-2930).
30
33
  *
@@ -231,7 +234,7 @@ const toIso = (value)=>{
231
234
  }
232
235
  }
233
236
  async function handler(request) {
234
- var _headers_authorization, _query_orgId;
237
+ var _headers_authorization, _query_orgId, _query_view;
235
238
  const { method, query, headers: rawHeaders } = await pluginRequestFromWeb(request);
236
239
  const headers = rawHeaders;
237
240
  if (method !== 'GET') {
@@ -254,6 +257,8 @@ async function handler(request) {
254
257
  }, {
255
258
  status: 400
256
259
  });
260
+ // The card as it prints, for a client that draws none of it (the staff apps).
261
+ const prepared = String((_query_view = query.view) != null ? _query_view : '') === 'prepared';
257
262
  try {
258
263
  var _orgSnap_data, _monthSnap_data, _org_name, _rollupDoc_get, _ref, _ref1, _ref2, _accountSnap_data;
259
264
  const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
@@ -275,7 +280,8 @@ async function handler(request) {
275
280
  orgRef.get(),
276
281
  orgRef.collection(ORG_BILLING_SUBCOLLECTION).doc(ORG_BILLING_DOC_ID).get(),
277
282
  orgRef.collection('assistUsage').doc(month).get(),
278
- orgRef.collection('usage').orderBy('month', 'desc').limit(1).get()
283
+ // The newest rollup is the margin's; the prepared card also lists the table's twelve.
284
+ orgRef.collection('usage').orderBy('month', 'desc').limit(prepared ? 12 : 1).get()
279
285
  ]);
280
286
  if (!orgSnap.exists) {
281
287
  return Response.json({
@@ -346,6 +352,26 @@ async function handler(request) {
346
352
  tokens: composeStaffOrgAiTokens(monthDoc),
347
353
  account: (free == null ? void 0 : free.accountUid) && accountSnap ? composeStaffAiAccountAllowance(free.accountUid, month, accountSnap.exists ? (_accountSnap_data = accountSnap.data()) != null ? _accountSnap_data : null : null) : null
348
354
  };
355
+ if (prepared) {
356
+ // The AI columns of the usage table, as `/api/admin/org-usage` reads them off each rollup: spend is priced
357
+ // (absent reads zero), credits and overage are recorded (absent reads as not measured).
358
+ const recorded = (value)=>value != null ? Number(value) : null;
359
+ const months = rollupSnap.docs.map((doc)=>{
360
+ var _doc_get, _doc_get1;
361
+ return {
362
+ month: String((_doc_get = doc.get('month')) != null ? _doc_get : doc.id),
363
+ assistCostUsd: Number((_doc_get1 = doc.get('assistCostUsd')) != null ? _doc_get1 : 0),
364
+ assistCredits: recorded(doc.get('assistCredits')),
365
+ assistOverageUsd: recorded(doc.get('assistOverageUsd'))
366
+ };
367
+ });
368
+ return Response.json(staffOrgAiView(body, aiAddonName(), {
369
+ pool: staffAssistPool(org),
370
+ months
371
+ }), {
372
+ status: 200
373
+ });
374
+ }
349
375
  return Response.json(body, {
350
376
  status: 200
351
377
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-admin-org.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 isLiveSubscriptionStatus,\n ORG_BILLING_DOC_ID,\n ORG_BILLING_SUBCOLLECTION,\n orgCogsInputFrom,\n pluginRequestFromWeb,\n} from '@aglyn/aglyn/server'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n readOrgBilling,\n} from '@aglyn/tenant-data-admin'\nimport { assistUsageMonth } from '../usage/assist-usage'\nimport { assistRefusalCounts } from '../usage/assist-refusals'\nimport { readOrgAiUsageByUser } from '../usage/ai-usage-by-user'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { addonKindFromPriceId } from '@aglyn/tenant-data-admin/server/billing-addons'\nimport { orgMarginRow } from '@aglyn/aglyn/app-utils/margin-utilization'\nimport {\n assistCogsAlertThresholdUsd,\n assistMarginMultiple,\n} from '../usage/assist-spend-guards'\nimport {\n freeAccountUsageRef,\n freeAssistAccount,\n} from '../usage/assist-free-taste'\nimport {\n composeStaffAiAccountAllowance,\n composeStaffOrgAiAddon,\n composeStaffOrgAiMargin,\n composeStaffOrgAiOverage,\n composeStaffOrgAiPool,\n composeStaffOrgAiTokens,\n emptyJobCounts,\n STAFF_ORG_AI_JOB_STATUSES,\n type StaffOrgAiAddon,\n type StaffOrgAiJob,\n type StaffOrgAiJobs,\n type StaffOrgAiResponse,\n type StaffOrgAiUser,\n} from '../usage/staff-org-ai'\n\n/**\n * THE STAFF AI CARD'S BACKING READ (AGL-2930).\n *\n * One request composes everything the card on the staff org page shows: the\n * add-on and when it was bought, the credit pool and its parts, the month's\n * overage and the org's own ceilings, the refusal counter, the generation\n * jobs, the per-user rollup and the margin. Staff-gated exactly as\n * `/api/admin/org-usage` is, and read-only — every access below is a `get()`.\n *\n * ## The reads, stated\n *\n * The org document and its billing mirror; this month's `assistUsage`\n * document; the newest usage rollup (for the contribution margin, the same\n * read the margin route makes); a bounded page of `aiJobs`; the per-user\n * rollup for the month; and, when the add-on is on, ONE Stripe subscription\n * lookup that is cached in-process — see `addonSinceFor`.\n *\n * ## No audit row\n *\n * The card loads every time the staff org page opens, so an `adminAudit` row\n * per load recorded page views rather than decisions and buried the org's\n * real admin actions under them. The route only reads.\n */\n\n/** Generation jobs read per request; the counts are a floor past it. */\nconst JOBS_SCAN = 200\n\n/** How many jobs the card lists. */\nconst JOBS_RECENT = 10\n\n/** How many people the leaderboard names. */\nconst TOP_USERS = 10\n\n/**\n * How long a Stripe answer to \"when was the add-on bought\" is kept.\n *\n * The date does not move once the item exists, and the card is opened far\n * more often than an add-on is bought, so the only cost of a stale entry is\n * a card that says \"since —\" for up to this long after a purchase. The\n * alternative — a Stripe round trip on every render — is the thing the brief\n * forbade.\n */\nconst ADDON_SINCE_TTL_MS = 15 * 60 * 1000\n\ninterface AddonSince {\n since: string | null\n sinceSource: StaffOrgAiAddon['sinceSource']\n at: number\n}\n\nconst addonSinceCache = new Map<string, AddonSince>()\n\n/** Test seam: drop every cached Stripe answer. */\nexport function resetAddonSinceCache(): void {\n addonSinceCache.clear()\n}\n\n/**\n * The instant the add-on's subscription item was created, from Stripe.\n *\n * The org document carries the add-on as a quantity (`seatAddons.aiAddon`)\n * and nothing about WHEN — the webhook mirrors items, not their dates. Stripe\n * has it as `created` on the subscription item, found by the price id the\n * add-on sells at (`addonKindFromPriceId`), so a rename or a price change\n * does not lose it.\n */\nasync function addonSinceFor(orgId: string): Promise<AddonSince> {\n const cached = addonSinceCache.get(orgId)\n if (cached && Date.now() - cached.at < ADDON_SINCE_TTL_MS) return cached\n const answer = await readAddonSince(orgId)\n addonSinceCache.set(orgId, answer)\n return answer\n}\n\nasync function readAddonSince(orgId: string): Promise<AddonSince> {\n const at = Date.now()\n const stripeKey = process.env.STRIPE_SECRET_KEY\n if (!stripeKey) return { since: null, sinceSource: 'unavailable', at }\n try {\n const customerId = (await readOrgBilling(orgId)).stripeCustomerId\n if (!customerId) return { since: null, sinceSource: 'no-subscription', at }\n const response = await fetch(\n `https://api.stripe.com/v1/subscriptions?customer=${encodeURIComponent(\n String(customerId),\n )}&status=all&limit=10`,\n { headers: { Authorization: `Bearer ${stripeKey}` } },\n )\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n console.error('[ai/admin/org] Stripe subscription lookup failed', {\n orgId,\n status: response.status,\n detail: payload?.error?.message ?? null,\n })\n return { since: null, sinceSource: 'unavailable', at }\n }\n const subscriptions = (payload?.data ?? []) as Array<{\n status?: string\n items?: { data?: Array<{ created?: number; price?: { id?: string } }> }\n }>\n // The LIVE subscription's item, when one exists: an add-on on a canceled\n // subscription is not an add-on the org has, and `hasAiAddon` already\n // says so — the date beside \"off\" would only confuse.\n const live = subscriptions.filter((subscription) =>\n isLiveSubscriptionStatus(subscription.status),\n )\n for (const subscription of live.length ? live : subscriptions) {\n for (const item of subscription.items?.data ?? []) {\n if (addonKindFromPriceId(item.price?.id) !== 'aiAddon') continue\n const created = Number(item.created)\n if (Number.isFinite(created) && created > 0) {\n return {\n since: new Date(created * 1000).toISOString(),\n sinceSource: 'stripe',\n at,\n }\n }\n }\n }\n return { since: null, sinceSource: 'stripe', at }\n } catch (error) {\n console.error('[ai/admin/org] Stripe subscription lookup threw', error)\n return { since: null, sinceSource: 'unavailable', at }\n }\n}\n\nconst toIso = (value: unknown): string | null => {\n const converted = (value as { toDate?: () => Date } | null)?.toDate?.()\n if (converted instanceof Date) return converted.toISOString()\n if (value instanceof Date) return value.toISOString()\n return null\n}\n\n/**\n * The month's generation jobs, summarized (AGL-2904's collection).\n *\n * Reads `orgs/{orgId}/aiJobs` by `createdAt` from the first instant of the\n * month, newest first, up to `JOBS_SCAN`. An org with no such collection\n * answers an empty snapshot, which is the honest \"no generation jobs yet\";\n * a read that FAILED answers `null`, which the card renders as a failure\n * rather than as an idle org. The field names are the ones `AiJob` declares\n * in `ai-jobs.types.ts`: `kind`, `status`, `creditsReserved`, `creditsSpent`,\n * `createdAt`, `createdBy`.\n */\nexport async function readOrgAiJobsSummary(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n month: string,\n): Promise<StaffOrgAiJobs | null> {\n try {\n const monthStart = new Date(`${month}-01T00:00:00.000Z`)\n const snapshot = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection('aiJobs')\n .where('createdAt', '>=', monthStart)\n .orderBy('createdAt', 'desc')\n .limit(JOBS_SCAN + 1)\n .get()\n const truncated = snapshot.docs.length > JOBS_SCAN\n const docs = truncated ? snapshot.docs.slice(0, JOBS_SCAN) : snapshot.docs\n const counts = emptyJobCounts()\n const recent: StaffOrgAiJob[] = []\n for (const doc of docs) {\n const status = String(doc.get('status') ?? '')\n if ((STAFF_ORG_AI_JOB_STATUSES as readonly string[]).includes(status)) {\n counts[status as keyof typeof counts] += 1\n }\n if (recent.length < JOBS_RECENT) {\n recent.push({\n id: doc.id,\n kind: String(doc.get('kind') ?? ''),\n status,\n creditsReserved: Number(doc.get('creditsReserved') ?? 0) || 0,\n creditsSpent: Number(doc.get('creditsSpent') ?? 0) || 0,\n createdAt: toIso(doc.get('createdAt')),\n createdBy:\n typeof doc.get('createdBy') === 'string'\n ? String(doc.get('createdBy'))\n : null,\n })\n }\n }\n return { counts, recent, truncated }\n } catch (error) {\n console.error('[ai/admin/org] jobs read failed', error)\n return null\n }\n}\n\n/**\n * The month's per-user rollup, dearest first (AGL-2928).\n *\n * `readOrgAiUsageByUser` is the one reader of\n * `orgs/{orgId}/aiUsageByUser/{uid}/months/{month}` — the customer's Usage\n * table reads through it too — joined to the roster for names and carrying\n * each person's share of the org's measured spend. The card keeps the\n * dearest `TOP_USERS`.\n */\nasync function readOrgAiTopUsers(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n month: string,\n): Promise<StaffOrgAiUser[]> {\n try {\n const usage = await readOrgAiUsageByUser(firestore, orgId, month)\n return usage.rows.slice(0, TOP_USERS).map((row) => ({\n uid: row.uid,\n name: row.name,\n credits: row.credits,\n estCostUsd: row.estCostUsd,\n share: row.share,\n requests: row.requests,\n refusals: row.refusals,\n byKind: row.byKind,\n byHost: row.byHost,\n }))\n } catch (error) {\n console.error('[ai/admin/org] per-user read failed', error)\n return []\n }\n}\n\nasync function handler(request: Request): Promise<Response> {\n const {\n method,\n query,\n headers: rawHeaders,\n } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'GET') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken)\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n const orgId = String(query.orgId ?? '')\n if (!orgId) return Response.json({ error: 'Missing orgId' }, { status: 400 })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n if (!decoded['staff']) {\n return Response.json({ error: 'Staff only' }, { status: 403 })\n }\n\n const now = new Date()\n const month = assistUsageMonth(now)\n const db = firebaseAdmin.app().firestore()\n const orgRef = db.collection('orgs').doc(orgId)\n\n const [orgSnap, billingSnap, monthSnap, rollupSnap] = await Promise.all([\n orgRef.get(),\n orgRef.collection(ORG_BILLING_SUBCOLLECTION).doc(ORG_BILLING_DOC_ID).get(),\n orgRef.collection('assistUsage').doc(month).get(),\n orgRef.collection('usage').orderBy('month', 'desc').limit(1).get(),\n ])\n if (!orgSnap.exists) {\n return Response.json({ error: 'No such organization' }, { status: 404 })\n }\n // Org doc FIRST so a stale inline `subscription` loses to the billing\n // mirror — the same merge the margin route and the org detail read make.\n const org = {\n ...(orgSnap.data() ?? {}),\n ...(billingSnap.exists ? billingSnap.data() : {}),\n } as Record<string, unknown>\n const monthDoc = monthSnap.exists ? (monthSnap.data() ?? {}) : null\n\n // A Free workspace also draws on its OWNER's account allowance\n // (AGL-2925), which is what staff give credits back to beside the band\n // (AGL-3595). One more document, read only for a Free workspace.\n const free = freeAssistAccount(org as never)\n const [jobs, users, since, accountSnap] = await Promise.all([\n readOrgAiJobsSummary(db, orgId, month),\n readOrgAiTopUsers(db, orgId, month),\n addonSinceFor(orgId),\n free?.accountUid\n ? freeAccountUsageRef(db, free.accountUid, month).get()\n : Promise.resolve(null),\n ])\n\n const pool = composeStaffOrgAiPool(org as never, monthDoc, now)\n // The overage line is an invoice, so it is priced off what the month\n // DREW; everything below it is our bill, so it reads what the month\n // COST (AGL-3015).\n const overage = composeStaffOrgAiOverage(org as never, pool.billedUsd)\n const addon = composeStaffOrgAiAddon(org as never, {\n since: since.since,\n sinceSource: since.sinceSource,\n })\n const thresholdUsd = assistCogsAlertThresholdUsd(\n process.env.ASSIST_ORG_MONTHLY_COGS_ALERT_USD,\n )\n const rollupDoc = rollupSnap.docs[0]\n // The whole-org contribution margin, on the same row the margin\n // utilization page builds — with THIS month's live assist spend in\n // place of the figure frozen onto the rollup, as that route does.\n const contributionRow = rollupDoc\n ? orgMarginRow({\n orgId,\n name: (org['name'] as string | undefined) ?? null,\n org: org as never,\n month: String(rollupDoc.get('month') ?? rollupDoc.id),\n rollup: {\n ...orgCogsInputFrom(rollupDoc.data()),\n assistCostUsd: pool.providerUsd,\n },\n })\n : null\n const margin = composeStaffOrgAiMargin({\n org: org as never,\n pool,\n overage,\n addon,\n thresholdUsd,\n multiple: assistMarginMultiple(pool.providerUsd, thresholdUsd),\n contribution: {\n month: contributionRow?.month ?? null,\n marginPct: contributionRow?.marginPct ?? null,\n rating: contributionRow?.rating ?? null,\n },\n })\n\n const body: StaffOrgAiResponse = {\n month,\n addon,\n pool,\n overage,\n refusals: assistRefusalCounts(monthDoc?.['refusals']),\n jobs,\n users,\n margin,\n tokens: composeStaffOrgAiTokens(monthDoc),\n account:\n free?.accountUid && accountSnap\n ? composeStaffAiAccountAllowance(\n free.accountUid,\n month,\n accountSnap.exists ? (accountSnap.data() ?? null) : null,\n )\n : null,\n }\n return Response.json(body, { status: 200 })\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/org]', error)\n return Response.json({ error: 'AI lookup failed' }, { status: 500 })\n }\n}\n\nexport { handler as GET }\n"],"names":["isLiveSubscriptionStatus","ORG_BILLING_DOC_ID","ORG_BILLING_SUBCOLLECTION","orgCogsInputFrom","pluginRequestFromWeb","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","readOrgBilling","assistUsageMonth","assistRefusalCounts","readOrgAiUsageByUser","invalidIdTokenResponse","addonKindFromPriceId","orgMarginRow","assistCogsAlertThresholdUsd","assistMarginMultiple","freeAccountUsageRef","freeAssistAccount","composeStaffAiAccountAllowance","composeStaffOrgAiAddon","composeStaffOrgAiMargin","composeStaffOrgAiOverage","composeStaffOrgAiPool","composeStaffOrgAiTokens","emptyJobCounts","STAFF_ORG_AI_JOB_STATUSES","JOBS_SCAN","JOBS_RECENT","TOP_USERS","ADDON_SINCE_TTL_MS","addonSinceCache","Map","resetAddonSinceCache","clear","addonSinceFor","orgId","cached","get","Date","now","at","answer","readAddonSince","set","stripeKey","process","env","STRIPE_SECRET_KEY","since","sinceSource","customerId","stripeCustomerId","response","fetch","encodeURIComponent","String","headers","Authorization","payload","json","catch","ok","console","error","status","detail","message","subscriptions","data","live","filter","subscription","length","item","items","price","id","created","Number","isFinite","toISOString","toIso","value","converted","toDate","readOrgAiJobsSummary","firestore","month","monthStart","snapshot","collection","doc","where","orderBy","limit","truncated","docs","slice","counts","recent","includes","push","kind","creditsReserved","creditsSpent","createdAt","createdBy","readOrgAiTopUsers","usage","rows","map","row","uid","name","credits","estCostUsd","share","requests","refusals","byKind","byHost","handler","request","query","method","rawHeaders","Response","authorization","idToken","startsWith","undefined","orgSnap","monthSnap","org","rollupDoc","accountSnap","decoded","app","auth","verifyIdToken","email_verified","db","orgRef","billingSnap","rollupSnap","Promise","all","exists","monthDoc","free","jobs","users","accountUid","resolve","pool","overage","billedUsd","addon","thresholdUsd","ASSIST_ORG_MONTHLY_COGS_ALERT_USD","contributionRow","rollup","assistCostUsd","providerUsd","margin","multiple","contribution","marginPct","rating","body","tokens","account","unauthenticated","GET"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-admin-org.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 isLiveSubscriptionStatus,\n ORG_BILLING_DOC_ID,\n ORG_BILLING_SUBCOLLECTION,\n orgCogsInputFrom,\n pluginRequestFromWeb,\n} from '@aglyn/aglyn/server'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n readOrgBilling,\n} from '@aglyn/tenant-data-admin'\nimport { assistUsageMonth } from '../usage/assist-usage'\nimport { assistRefusalCounts } from '../usage/assist-refusals'\nimport { readOrgAiUsageByUser } from '../usage/ai-usage-by-user'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { addonKindFromPriceId } from '@aglyn/tenant-data-admin/server/billing-addons'\nimport { orgMarginRow } from '@aglyn/aglyn/app-utils/margin-utilization'\nimport { aiAddonName } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n assistCogsAlertThresholdUsd,\n assistMarginMultiple,\n} from '../usage/assist-spend-guards'\nimport {\n freeAccountUsageRef,\n freeAssistAccount,\n} from '../usage/assist-free-taste'\nimport {\n composeStaffAiAccountAllowance,\n composeStaffOrgAiAddon,\n composeStaffOrgAiMargin,\n composeStaffOrgAiOverage,\n composeStaffOrgAiPool,\n composeStaffOrgAiTokens,\n emptyJobCounts,\n STAFF_ORG_AI_JOB_STATUSES,\n type StaffOrgAiAddon,\n type StaffOrgAiJob,\n type StaffOrgAiJobs,\n type StaffOrgAiResponse,\n type StaffOrgAiUser,\n} from '../usage/staff-org-ai'\nimport { staffOrgAiView } from '../usage/staff-org-ai-view'\nimport { staffAssistPool } from '../usage/staff-org-usage-ai'\n\n/**\n * THE STAFF AI CARD'S BACKING READ (AGL-2930).\n *\n * One request composes everything the card on the staff org page shows: the\n * add-on and when it was bought, the credit pool and its parts, the month's\n * overage and the org's own ceilings, the refusal counter, the generation\n * jobs, the per-user rollup and the margin. Staff-gated exactly as\n * `/api/admin/org-usage` is, and read-only — every access below is a `get()`.\n *\n * ## The reads, stated\n *\n * The org document and its billing mirror; this month's `assistUsage`\n * document; the newest usage rollup (for the contribution margin, the same\n * read the margin route makes); a bounded page of `aiJobs`; the per-user\n * rollup for the month; and, when the add-on is on, ONE Stripe subscription\n * lookup that is cached in-process — see `addonSinceFor`.\n *\n * ## No audit row\n *\n * The card loads every time the staff org page opens, so an `adminAudit` row\n * per load recorded page views rather than decisions and buried the org's\n * real admin actions under them. The route only reads.\n */\n\n/** Generation jobs read per request; the counts are a floor past it. */\nconst JOBS_SCAN = 200\n\n/** How many jobs the card lists. */\nconst JOBS_RECENT = 10\n\n/** How many people the leaderboard names. */\nconst TOP_USERS = 10\n\n/**\n * How long a Stripe answer to \"when was the add-on bought\" is kept.\n *\n * The date does not move once the item exists, and the card is opened far\n * more often than an add-on is bought, so the only cost of a stale entry is\n * a card that says \"since —\" for up to this long after a purchase. The\n * alternative — a Stripe round trip on every render — is the thing the brief\n * forbade.\n */\nconst ADDON_SINCE_TTL_MS = 15 * 60 * 1000\n\ninterface AddonSince {\n since: string | null\n sinceSource: StaffOrgAiAddon['sinceSource']\n at: number\n}\n\nconst addonSinceCache = new Map<string, AddonSince>()\n\n/** Test seam: drop every cached Stripe answer. */\nexport function resetAddonSinceCache(): void {\n addonSinceCache.clear()\n}\n\n/**\n * The instant the add-on's subscription item was created, from Stripe.\n *\n * The org document carries the add-on as a quantity (`seatAddons.aiAddon`)\n * and nothing about WHEN — the webhook mirrors items, not their dates. Stripe\n * has it as `created` on the subscription item, found by the price id the\n * add-on sells at (`addonKindFromPriceId`), so a rename or a price change\n * does not lose it.\n */\nasync function addonSinceFor(orgId: string): Promise<AddonSince> {\n const cached = addonSinceCache.get(orgId)\n if (cached && Date.now() - cached.at < ADDON_SINCE_TTL_MS) return cached\n const answer = await readAddonSince(orgId)\n addonSinceCache.set(orgId, answer)\n return answer\n}\n\nasync function readAddonSince(orgId: string): Promise<AddonSince> {\n const at = Date.now()\n const stripeKey = process.env.STRIPE_SECRET_KEY\n if (!stripeKey) return { since: null, sinceSource: 'unavailable', at }\n try {\n const customerId = (await readOrgBilling(orgId)).stripeCustomerId\n if (!customerId) return { since: null, sinceSource: 'no-subscription', at }\n const response = await fetch(\n `https://api.stripe.com/v1/subscriptions?customer=${encodeURIComponent(\n String(customerId),\n )}&status=all&limit=10`,\n { headers: { Authorization: `Bearer ${stripeKey}` } },\n )\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n console.error('[ai/admin/org] Stripe subscription lookup failed', {\n orgId,\n status: response.status,\n detail: payload?.error?.message ?? null,\n })\n return { since: null, sinceSource: 'unavailable', at }\n }\n const subscriptions = (payload?.data ?? []) as Array<{\n status?: string\n items?: { data?: Array<{ created?: number; price?: { id?: string } }> }\n }>\n // The LIVE subscription's item, when one exists: an add-on on a canceled\n // subscription is not an add-on the org has, and `hasAiAddon` already\n // says so — the date beside \"off\" would only confuse.\n const live = subscriptions.filter((subscription) =>\n isLiveSubscriptionStatus(subscription.status),\n )\n for (const subscription of live.length ? live : subscriptions) {\n for (const item of subscription.items?.data ?? []) {\n if (addonKindFromPriceId(item.price?.id) !== 'aiAddon') continue\n const created = Number(item.created)\n if (Number.isFinite(created) && created > 0) {\n return {\n since: new Date(created * 1000).toISOString(),\n sinceSource: 'stripe',\n at,\n }\n }\n }\n }\n return { since: null, sinceSource: 'stripe', at }\n } catch (error) {\n console.error('[ai/admin/org] Stripe subscription lookup threw', error)\n return { since: null, sinceSource: 'unavailable', at }\n }\n}\n\nconst toIso = (value: unknown): string | null => {\n const converted = (value as { toDate?: () => Date } | null)?.toDate?.()\n if (converted instanceof Date) return converted.toISOString()\n if (value instanceof Date) return value.toISOString()\n return null\n}\n\n/**\n * The month's generation jobs, summarized (AGL-2904's collection).\n *\n * Reads `orgs/{orgId}/aiJobs` by `createdAt` from the first instant of the\n * month, newest first, up to `JOBS_SCAN`. An org with no such collection\n * answers an empty snapshot, which is the honest \"no generation jobs yet\";\n * a read that FAILED answers `null`, which the card renders as a failure\n * rather than as an idle org. The field names are the ones `AiJob` declares\n * in `ai-jobs.types.ts`: `kind`, `status`, `creditsReserved`, `creditsSpent`,\n * `createdAt`, `createdBy`.\n */\nexport async function readOrgAiJobsSummary(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n month: string,\n): Promise<StaffOrgAiJobs | null> {\n try {\n const monthStart = new Date(`${month}-01T00:00:00.000Z`)\n const snapshot = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection('aiJobs')\n .where('createdAt', '>=', monthStart)\n .orderBy('createdAt', 'desc')\n .limit(JOBS_SCAN + 1)\n .get()\n const truncated = snapshot.docs.length > JOBS_SCAN\n const docs = truncated ? snapshot.docs.slice(0, JOBS_SCAN) : snapshot.docs\n const counts = emptyJobCounts()\n const recent: StaffOrgAiJob[] = []\n for (const doc of docs) {\n const status = String(doc.get('status') ?? '')\n if ((STAFF_ORG_AI_JOB_STATUSES as readonly string[]).includes(status)) {\n counts[status as keyof typeof counts] += 1\n }\n if (recent.length < JOBS_RECENT) {\n recent.push({\n id: doc.id,\n kind: String(doc.get('kind') ?? ''),\n status,\n creditsReserved: Number(doc.get('creditsReserved') ?? 0) || 0,\n creditsSpent: Number(doc.get('creditsSpent') ?? 0) || 0,\n createdAt: toIso(doc.get('createdAt')),\n createdBy:\n typeof doc.get('createdBy') === 'string'\n ? String(doc.get('createdBy'))\n : null,\n })\n }\n }\n return { counts, recent, truncated }\n } catch (error) {\n console.error('[ai/admin/org] jobs read failed', error)\n return null\n }\n}\n\n/**\n * The month's per-user rollup, dearest first (AGL-2928).\n *\n * `readOrgAiUsageByUser` is the one reader of\n * `orgs/{orgId}/aiUsageByUser/{uid}/months/{month}` — the customer's Usage\n * table reads through it too — joined to the roster for names and carrying\n * each person's share of the org's measured spend. The card keeps the\n * dearest `TOP_USERS`.\n */\nasync function readOrgAiTopUsers(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n month: string,\n): Promise<StaffOrgAiUser[]> {\n try {\n const usage = await readOrgAiUsageByUser(firestore, orgId, month)\n return usage.rows.slice(0, TOP_USERS).map((row) => ({\n uid: row.uid,\n name: row.name,\n credits: row.credits,\n estCostUsd: row.estCostUsd,\n share: row.share,\n requests: row.requests,\n refusals: row.refusals,\n byKind: row.byKind,\n byHost: row.byHost,\n }))\n } catch (error) {\n console.error('[ai/admin/org] per-user read failed', error)\n return []\n }\n}\n\nasync function handler(request: Request): Promise<Response> {\n const {\n method,\n query,\n headers: rawHeaders,\n } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'GET') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken)\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n const orgId = String(query.orgId ?? '')\n if (!orgId) return Response.json({ error: 'Missing orgId' }, { status: 400 })\n // The card as it prints, for a client that draws none of it (the staff apps).\n const prepared = String(query.view ?? '') === 'prepared'\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 now = new Date()\n const month = assistUsageMonth(now)\n const db = firebaseAdmin.app().firestore()\n const orgRef = db.collection('orgs').doc(orgId)\n\n const [orgSnap, billingSnap, monthSnap, rollupSnap] = await Promise.all([\n orgRef.get(),\n orgRef.collection(ORG_BILLING_SUBCOLLECTION).doc(ORG_BILLING_DOC_ID).get(),\n orgRef.collection('assistUsage').doc(month).get(),\n // The newest rollup is the margin's; the prepared card also lists the table's twelve.\n orgRef.collection('usage').orderBy('month', 'desc').limit(prepared ? 12 : 1).get(),\n ])\n if (!orgSnap.exists) {\n return Response.json({ error: 'No such organization' }, { status: 404 })\n }\n // Org doc FIRST so a stale inline `subscription` loses to the billing\n // mirror — the same merge the margin route and the org detail read make.\n const org = {\n ...(orgSnap.data() ?? {}),\n ...(billingSnap.exists ? billingSnap.data() : {}),\n } as Record<string, unknown>\n const monthDoc = monthSnap.exists ? (monthSnap.data() ?? {}) : null\n\n // A Free workspace also draws on its OWNER's account allowance\n // (AGL-2925), which is what staff give credits back to beside the band\n // (AGL-3595). One more document, read only for a Free workspace.\n const free = freeAssistAccount(org as never)\n const [jobs, users, since, accountSnap] = await Promise.all([\n readOrgAiJobsSummary(db, orgId, month),\n readOrgAiTopUsers(db, orgId, month),\n addonSinceFor(orgId),\n free?.accountUid\n ? freeAccountUsageRef(db, free.accountUid, month).get()\n : Promise.resolve(null),\n ])\n\n const pool = composeStaffOrgAiPool(org as never, monthDoc, now)\n // The overage line is an invoice, so it is priced off what the month\n // DREW; everything below it is our bill, so it reads what the month\n // COST (AGL-3015).\n const overage = composeStaffOrgAiOverage(org as never, pool.billedUsd)\n const addon = composeStaffOrgAiAddon(org as never, {\n since: since.since,\n sinceSource: since.sinceSource,\n })\n const thresholdUsd = assistCogsAlertThresholdUsd(\n process.env.ASSIST_ORG_MONTHLY_COGS_ALERT_USD,\n )\n const rollupDoc = rollupSnap.docs[0]\n // The whole-org contribution margin, on the same row the margin\n // utilization page builds — with THIS month's live assist spend in\n // place of the figure frozen onto the rollup, as that route does.\n const contributionRow = rollupDoc\n ? orgMarginRow({\n orgId,\n name: (org['name'] as string | undefined) ?? null,\n org: org as never,\n month: String(rollupDoc.get('month') ?? rollupDoc.id),\n rollup: {\n ...orgCogsInputFrom(rollupDoc.data()),\n assistCostUsd: pool.providerUsd,\n },\n })\n : null\n const margin = composeStaffOrgAiMargin({\n org: org as never,\n pool,\n overage,\n addon,\n thresholdUsd,\n multiple: assistMarginMultiple(pool.providerUsd, thresholdUsd),\n contribution: {\n month: contributionRow?.month ?? null,\n marginPct: contributionRow?.marginPct ?? null,\n rating: contributionRow?.rating ?? null,\n },\n })\n\n const body: StaffOrgAiResponse = {\n month,\n addon,\n pool,\n overage,\n refusals: assistRefusalCounts(monthDoc?.['refusals']),\n jobs,\n users,\n margin,\n tokens: composeStaffOrgAiTokens(monthDoc),\n account:\n free?.accountUid && accountSnap\n ? composeStaffAiAccountAllowance(\n free.accountUid,\n month,\n accountSnap.exists ? (accountSnap.data() ?? null) : null,\n )\n : null,\n }\n if (prepared) {\n // The AI columns of the usage table, as `/api/admin/org-usage` reads them off each rollup: spend is priced\n // (absent reads zero), credits and overage are recorded (absent reads as not measured).\n const recorded = (value: unknown) => (value != null ? Number(value) : null)\n const months = rollupSnap.docs.map((doc) => ({\n month: String(doc.get('month') ?? doc.id),\n assistCostUsd: Number(doc.get('assistCostUsd') ?? 0),\n assistCredits: recorded(doc.get('assistCredits')),\n assistOverageUsd: recorded(doc.get('assistOverageUsd')),\n }))\n return Response.json(\n staffOrgAiView(body, aiAddonName(), { pool: staffAssistPool(org as never), months }),\n { status: 200 },\n )\n }\n return Response.json(body, { status: 200 })\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/org]', error)\n return Response.json({ error: 'AI lookup failed' }, { status: 500 })\n }\n}\n\nexport { handler as GET }\n"],"names":["isLiveSubscriptionStatus","ORG_BILLING_DOC_ID","ORG_BILLING_SUBCOLLECTION","orgCogsInputFrom","pluginRequestFromWeb","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","readOrgBilling","assistUsageMonth","assistRefusalCounts","readOrgAiUsageByUser","invalidIdTokenResponse","addonKindFromPriceId","orgMarginRow","aiAddonName","assistCogsAlertThresholdUsd","assistMarginMultiple","freeAccountUsageRef","freeAssistAccount","composeStaffAiAccountAllowance","composeStaffOrgAiAddon","composeStaffOrgAiMargin","composeStaffOrgAiOverage","composeStaffOrgAiPool","composeStaffOrgAiTokens","emptyJobCounts","STAFF_ORG_AI_JOB_STATUSES","staffOrgAiView","staffAssistPool","JOBS_SCAN","JOBS_RECENT","TOP_USERS","ADDON_SINCE_TTL_MS","addonSinceCache","Map","resetAddonSinceCache","clear","addonSinceFor","orgId","cached","get","Date","now","at","answer","readAddonSince","set","stripeKey","process","env","STRIPE_SECRET_KEY","since","sinceSource","customerId","stripeCustomerId","response","fetch","encodeURIComponent","String","headers","Authorization","payload","json","catch","ok","console","error","status","detail","message","subscriptions","data","live","filter","subscription","length","item","items","price","id","created","Number","isFinite","toISOString","toIso","value","converted","toDate","readOrgAiJobsSummary","firestore","month","monthStart","snapshot","collection","doc","where","orderBy","limit","truncated","docs","slice","counts","recent","includes","push","kind","creditsReserved","creditsSpent","createdAt","createdBy","readOrgAiTopUsers","usage","rows","map","row","uid","name","credits","estCostUsd","share","requests","refusals","byKind","byHost","handler","request","query","method","rawHeaders","Response","authorization","idToken","startsWith","undefined","prepared","view","orgSnap","monthSnap","org","rollupDoc","accountSnap","decoded","app","auth","verifyIdToken","email_verified","db","orgRef","billingSnap","rollupSnap","Promise","all","exists","monthDoc","free","jobs","users","accountUid","resolve","pool","overage","billedUsd","addon","thresholdUsd","ASSIST_ORG_MONTHLY_COGS_ALERT_USD","contributionRow","rollup","assistCostUsd","providerUsd","margin","multiple","contribution","marginPct","rating","body","tokens","account","recorded","months","assistCredits","assistOverageUsd","unauthenticated","GET"],"mappings":"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"}