@aglyn/plugins-ai 1.0.0-beta.199 → 1.0.0-beta.201
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.
- package/package.json +17 -17
- package/src/lib/components/ai-products-hub-card.component.js +3 -0
- package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
- package/src/lib/components/ai-products-proposals.component.js +4 -0
- package/src/lib/components/ai-products-proposals.component.js.map +1 -1
- package/src/lib/components/ai-site-batch-card.component.js +5 -1
- package/src/lib/components/ai-site-batch-card.component.js.map +1 -1
- package/src/lib/components/billing-ai-allotments.component.d.ts +6 -0
- package/src/lib/components/billing-ai-allotments.component.js +17 -5
- package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
- package/src/lib/components/billing-ai-top-users.component.js +8 -3
- package/src/lib/components/billing-ai-top-users.component.js.map +1 -1
- package/src/lib/components/staff-org-ai-card.component.js +12 -4
- package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
- package/src/lib/components/staff-user-ai-usage-card.component.js +7 -3
- package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +3 -1
- package/src/lib/jobs/ai-job-drafts.js +23 -7
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-jobs.js +2 -1
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/server/ai-allotments.js +2 -1
- package/src/lib/server/ai-allotments.js.map +1 -1
- package/src/lib/server/ai-usage.js +2 -1
- package/src/lib/server/ai-usage.js.map +1 -1
- package/src/lib/server/billing-overage.js +3 -2
- package/src/lib/server/billing-overage.js.map +1 -1
- package/src/lib/usage/assist-free-taste.js +2 -1
- package/src/lib/usage/assist-free-taste.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-allotments.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { pluginRequestFromWeb } from '@aglyn/aglyn/server'\nimport {\n assistCreditsFromUsd,\n resolveAssistCreditBudget,\n} from '@aglyn/aglyn/app-utils/assist-credits'\nimport { hostRoleFor, isOrgWideMember } from '@aglyn/aglyn/app-utils/organizations'\nimport { resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AglynOrgMember } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n memberHasOrgPermission,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { logAiAllotmentChanged } from '../activity/ai-activity'\nimport {\n AI_ALLOTMENT_MAX_CREDITS,\n AI_ALLOTMENT_MAX_MODELS,\n aiAllotmentCredits,\n parseAiAllotmentSubject,\n type AiAllotment,\n type AiAllotmentMode,\n type AiAllotmentSubject,\n} from '../model/ai-allotments'\nimport { aiUsageByUserMonthFrom, aiUsageMonthKeys } from '../model/ai-usage-by-user'\nimport { AI_MODEL_CATALOG } from '../providers/catalog'\nimport { AI_MODEL_RESTRICTION_PLANS } from '../providers/model-choice'\nimport { resolveAiProvider } from '../providers/routing'\nimport {\n AI_HOST_CREDITS_FIELD,\n applyAiAllotmentWrites,\n listAiAllotments,\n seedAiHostMonthCredits,\n type AiAllotmentWrite,\n} from '../usage/ai-allotments'\nimport { userAiUsageMonthRef } from '../usage/ai-usage-by-user'\nimport type {\n AiAllotmentRowWire,\n AiAllotmentsHostWire,\n AiAllotmentsMemberWire,\n AiAllotmentsWire,\n} from '../usage/ai-usage-wire'\n\n// lockdown-423: exempt — a spend control, the posture of ai/billing/overage\n// beside it: a billing-locked workspace must still be able to LOWER what its\n// people may spend, and a 423 would trap it spending.\n\n/**\n * AI ALLOTMENTS (AGL-2942) — read and set them.\n *\n * ## Who may read\n *\n * - The whole org's allotments (`orgId`): `billing.view`, the Usage page's\n * gate — every allotment is a figure about the pool it is part of.\n * - One site's (`hostId`): the same, or the site's admin when they are a\n * collaborator scoped to it — the collaborators card is theirs.\n * - One person's (`uid`): the same, or the person themselves.\n *\n * ## Who may write\n *\n * - A member's, a site's and the org-wide model restriction:\n * `billing.manage`. Each moves how much of the paid pool someone may\n * spend.\n * - A collaborator's on one site: `billing.manage`, or the admin of that\n * site when they are a collaborator scoped to it — never on their own\n * allotment, which they could otherwise raise.\n *\n * Staff pass, as at the other org routes. The org-wide restriction is set\n * only on a plan that offers it (`AI_MODEL_RESTRICTION_PLANS`); clearing it\n * is always allowed.\n *\n * ## One write, many subjects\n *\n * `POST { orgId, set, remove, everySite }`: `set` replaces allotments,\n * `remove` deletes them, and `everySite` sets the same allotment on every\n * site the org has — \"same for every client site\" — expanded here, from the\n * org's sites, rather than trusted from a client's list.\n */\n\nconst json = (body: unknown, status: number) => Response.json(body, { status })\n\n/** Batch writes a request may make; Firestore commits at most 500. */\nconst MAX_WRITES = 450\n\n/** Sites one `everySite` expands to. */\nconst MAX_SITES = 400\n\n/** Member months one `getAll` carries. */\nconst GET_ALL_CHUNK = 100\n\ninterface Caller {\n uid: string\n email: string | null\n staff: boolean\n member: AglynOrgMember | null\n}\n\nasync function authenticate(\n headers: Partial<Record<string, string>>,\n): Promise<Response | { decoded: { uid: string; email?: string; staff: boolean } }> {\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return json({ error: 'Unauthenticated' }, 401)\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n return { decoded: { uid: decoded.uid, email: decoded.email, staff: decoded['staff'] === true } }\n}\n\n/** A collaborator scoped to the site who holds its admin role there. */\nfunction isSiteAdminCollaborator(member: AglynOrgMember | null, hostId: string): boolean {\n if (!member || !hostId || isOrgWideMember(member)) return false\n return hostRoleFor(member, hostId) === 'admin'\n}\n\ninterface RosterMonth {\n member: AglynOrgMember\n credits: number\n byHost: Record<string, number>\n}\n\n/** The roster and each member's month, by path — no index, one getAll a hundred. */\nasync function readRosterMonths(\n firestore: FirebaseFirestore.Firestore,\n orgRef: FirebaseFirestore.DocumentReference,\n month: string,\n): Promise<RosterMonth[]> {\n const roster = await orgRef.collection('members').get()\n const members = roster.docs.map(\n (doc) => ({ $id: doc.id, ...doc.data() }) as AglynOrgMember,\n )\n const out: RosterMonth[] = []\n for (let start = 0; start < members.length; start += GET_ALL_CHUNK) {\n const chunk = members.slice(start, start + GET_ALL_CHUNK)\n const snapshots = chunk.length\n ? await firestore.getAll(\n ...chunk.map((member) => userAiUsageMonthRef(orgRef, member.$id, month)),\n )\n : []\n snapshots.forEach((snapshot, index) => {\n const member = chunk[index]\n const usage = aiUsageByUserMonthFrom(\n snapshot.exists ? (snapshot.data() as Record<string, unknown>) : null,\n member.$id,\n month,\n )\n out.push({ member, credits: usage.credits, byHost: usage.byHost })\n })\n }\n return out\n}\n\nconst memberName = (member: AglynOrgMember): string =>\n String(member.displayName ?? '').trim() || String(member.email ?? '').trim() || member.$id\n\n/** A site's credits this month: the org map, else its people's months summed. */\nfunction siteCredits(\n orgMonth: FirebaseFirestore.DocumentSnapshot,\n roster: readonly RosterMonth[],\n hostId: string,\n): number {\n const map = (orgMonth.exists ? orgMonth.get(AI_HOST_CREDITS_FIELD) : null) as\n | Record<string, unknown>\n | null\n if (map && map[hostId] !== undefined) {\n const value = Number(map[hostId])\n return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0\n }\n return roster.reduce((sum, entry) => sum + (entry.byHost[hostId] ?? 0), 0)\n}\n\nasync function handleGet(request: Request): Promise<Response> {\n const { query, headers } = await pluginRequestFromWeb(request)\n const params = (query ?? {}) as Record<string, unknown>\n const hostId = String(params['hostId'] ?? '').trim()\n const subjectUid = String(params['uid'] ?? '').trim()\n const auth = await authenticate(headers as Partial<Record<string, string>>)\n if (auth instanceof Response) return auth\n const { decoded } = auth\n const orgId =\n String(params['orgId'] ?? '').trim() || (hostId ? ((await resolveOrgIdForHost(hostId)) ?? '') : '')\n if (!orgId) return json({ error: 'Missing orgId' }, 400)\n\n const membership = await resolveOrgMembership(decoded.uid, orgId)\n const member = membership?.member ?? null\n if (!member && !decoded.staff) return json({ error: 'Not found' }, 404)\n const canManage =\n decoded.staff || (await memberHasOrgPermission(orgId, member, 'billing.manage'))\n const canView =\n canManage || (await memberHasOrgPermission(orgId, member, 'billing.view'))\n const siteAdmin = isSiteAdminCollaborator(member, hostId)\n const allowed = subjectUid\n ? canView || subjectUid === decoded.uid || siteAdmin\n : hostId\n ? canView || siteAdmin\n : canView\n if (!allowed) return json({ error: 'billing.view required' }, 403)\n\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const month = aiUsageMonthKeys()[0]\n const [orgSnapshot, orgMonth, allotments, roster, hostDocs] = await Promise.all([\n orgRef.get(),\n orgRef.collection('assistUsage').doc(month).get(),\n listAiAllotments(firestore, orgId, subjectUid ? { uid: subjectUid } : hostId ? { hostId } : {}),\n readRosterMonths(firestore, orgRef, month),\n hostId\n ? firestore.collection('hosts').doc(hostId).get().then((doc) => (doc.exists ? [doc] : []))\n : firestore.collection('hosts').where('orgId', '==', orgId).limit(MAX_SITES).get().then((snapshot) => snapshot.docs),\n ])\n const org = (orgSnapshot.data() ?? {}) as Record<string, unknown>\n const byUid = new Map(roster.map((entry) => [entry.member.$id, entry]))\n\n const used = (allotment: AiAllotment): number => {\n const person = allotment.uid ? byUid.get(allotment.uid) : undefined\n switch (allotment.scope) {\n case 'member':\n return person?.credits ?? 0\n case 'collab':\n return allotment.hostId ? (person?.byHost[allotment.hostId] ?? 0) : 0\n case 'host':\n return allotment.hostId ? siteCredits(orgMonth, roster, allotment.hostId) : 0\n default:\n return 0\n }\n }\n const rows: AiAllotmentRowWire[] = allotments\n // A collaborator reading their own sees their own rows and nothing else.\n .filter((allotment) => canView || siteAdmin || allotment.uid === decoded.uid)\n .map((allotment) => ({\n subject: allotment.subject,\n scope: allotment.scope,\n uid: allotment.uid,\n hostId: allotment.hostId,\n credits: allotment.credits,\n mode: allotment.mode,\n models: allotment.models,\n used: used(allotment),\n }))\n\n const visibleMembers = roster.filter((entry) =>\n subjectUid\n ? entry.member.$id === subjectUid\n : hostId\n ? hostRoleFor(entry.member, hostId) !== null\n : canView,\n )\n const members: AiAllotmentsMemberWire[] = visibleMembers.map((entry) => ({\n uid: entry.member.$id,\n name: memberName(entry.member),\n email: typeof entry.member.email === 'string' ? entry.member.email : null,\n role: typeof entry.member.role === 'string' ? entry.member.role : null,\n orgWide: isOrgWideMember(entry.member),\n credits: entry.credits,\n byHost: entry.byHost,\n }))\n const hosts: AiAllotmentsHostWire[] = hostDocs.map((doc) => ({\n hostId: doc.id,\n name: String(doc.get('displayName') ?? '').trim() || String(doc.get('subdomain') ?? '').trim() || doc.id,\n credits: siteCredits(orgMonth, roster, doc.id),\n }))\n const provider = resolveAiProvider()\n const wire: AiAllotmentsWire = {\n orgId,\n month,\n pool: {\n used: assistCreditsFromUsd(Number(orgMonth.get('estCostUsd') ?? 0)),\n limit: resolveAssistCreditBudget(org as never),\n },\n allotments: rows,\n members,\n hosts,\n models: AI_MODEL_CATALOG.filter((entry) => !provider || entry.provider === provider.id).map(\n (entry) => ({ id: entry.id, label: entry.label, tier: entry.tier }),\n ),\n restrictionAvailable: AI_MODEL_RESTRICTION_PLANS.includes(resolveEffectivePlan(org as never)),\n canEdit: {\n billing: canManage,\n collaborators: canManage || siteAdmin,\n },\n callerUid: decoded.uid,\n }\n return Response.json(wire, { status: 200, headers: { 'Cache-Control': 'no-store, private' } })\n}\n\ninterface ParsedWrite {\n subject: AiAllotmentSubject\n credits: number | null\n mode: AiAllotmentMode\n models: string[] | null\n}\n\nconst CATALOG_IDS = new Set(AI_MODEL_CATALOG.map((entry) => entry.id))\n\n/** One entry of `set`, validated; a string names what is wrong. */\nexport function parseAllotmentWrite(raw: unknown): ParsedWrite | string {\n const entry = (raw ?? {}) as Record<string, unknown>\n const subject = parseAiAllotmentSubject(entry['subject'])\n if (!subject) return 'subject must be member:{uid}, collab:{hostId}:{uid}, host:{hostId} or org'\n let credits: number | null = null\n if (entry['credits'] !== null && entry['credits'] !== undefined) {\n credits = typeof entry['credits'] === 'number' ? aiAllotmentCredits(entry['credits']) : null\n if (credits === null || (entry['credits'] as number) > AI_ALLOTMENT_MAX_CREDITS) {\n return `credits must be a whole number from 1 to ${AI_ALLOTMENT_MAX_CREDITS.toLocaleString('en-US')}`\n }\n }\n const mode = entry['mode'] === undefined ? 'hard' : entry['mode']\n if (mode !== 'hard' && mode !== 'soft') return 'mode must be hard or soft'\n let models: string[] | null = null\n if (entry['models'] !== null && entry['models'] !== undefined) {\n if (!Array.isArray(entry['models'])) return 'models must be a list of model ids'\n const ids = [...new Set(entry['models'].map((id) => String(id ?? '').trim()))].filter(Boolean)\n if (ids.length > AI_ALLOTMENT_MAX_MODELS) return `name at most ${AI_ALLOTMENT_MAX_MODELS} models`\n const unknown = ids.find((id) => !CATALOG_IDS.has(id))\n if (unknown) return `${unknown} is not a model this platform offers`\n models = ids.length ? ids : null\n }\n if (subject.scope === 'org' && credits !== null) {\n return 'the org-wide restriction carries models, not credits'\n }\n if (subject.scope !== 'org' && credits === null && models === null) {\n return 'set credits or models, or remove the allotment'\n }\n return { subject, credits, mode, models }\n}\n\nasync function handlePost(request: Request): Promise<Response> {\n const { body, headers } = await pluginRequestFromWeb(request)\n const auth = await authenticate(headers as Partial<Record<string, string>>)\n if (auth instanceof Response) return auth\n const { decoded } = auth\n const payload = (body ?? {}) as Record<string, unknown>\n const orgId = String(payload['orgId'] ?? '').trim()\n if (!orgId) return json({ error: 'Missing orgId' }, 400)\n\n const rawSet = Array.isArray(payload['set']) ? payload['set'] : []\n const rawRemove = Array.isArray(payload['remove']) ? payload['remove'] : []\n const rawEverySite = payload['everySite'] ?? null\n if (!rawSet.length && !rawRemove.length && !rawEverySite) {\n return json({ error: 'Nothing to change' }, 400)\n }\n if (rawSet.length + rawRemove.length > MAX_WRITES) {\n return json({ error: `Change at most ${MAX_WRITES} allotments at once` }, 400)\n }\n\n const writes: ParsedWrite[] = []\n for (const raw of rawSet) {\n const parsed = parseAllotmentWrite(raw)\n if (typeof parsed === 'string') return json({ error: parsed, code: 'invalid_allotment' }, 400)\n writes.push(parsed)\n }\n const removals: AiAllotmentSubject[] = []\n for (const raw of rawRemove) {\n const subject = parseAiAllotmentSubject(raw)\n if (!subject) return json({ error: `${String(raw)} is not an allotment subject` }, 400)\n removals.push(subject)\n }\n let everySite: Omit<ParsedWrite, 'subject'> | null = null\n if (rawEverySite) {\n const parsed = parseAllotmentWrite({ ...(rawEverySite as object), subject: 'host:every-site' })\n if (typeof parsed === 'string') return json({ error: parsed, code: 'invalid_allotment' }, 400)\n everySite = { credits: parsed.credits, mode: parsed.mode, models: parsed.models }\n }\n\n const membership = await resolveOrgMembership(decoded.uid, orgId)\n const caller: Caller = {\n uid: decoded.uid,\n email: decoded.email ?? null,\n staff: decoded.staff,\n member: membership?.member ?? null,\n }\n if (!caller.member && !caller.staff) return json({ error: 'Not found' }, 404)\n const canManage =\n caller.staff || (await memberHasOrgPermission(orgId, caller.member, 'billing.manage'))\n\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const orgSnapshot = await orgRef.get()\n if (!orgSnapshot.exists) return json({ error: 'Unknown organization' }, 404)\n const org = (orgSnapshot.data() ?? {}) as Record<string, unknown>\n\n // Every subject the request touches, set or removed, is decided the same\n // way: the permission first, then that the person or site belongs here.\n const touched = [...writes.map((write) => write.subject), ...removals]\n if (everySite && !canManage) return json({ error: 'billing.manage required' }, 403)\n const memberDocs = new Map<string, AglynOrgMember | null>()\n const memberOf = async (uid: string): Promise<AglynOrgMember | null> => {\n if (!memberDocs.has(uid)) {\n const snapshot = await orgRef.collection('members').doc(uid).get()\n memberDocs.set(uid, snapshot.exists ? ({ $id: uid, ...snapshot.data() } as AglynOrgMember) : null)\n }\n return memberDocs.get(uid) ?? null\n }\n const hostNames = new Map<string, string>()\n for (const subject of touched) {\n if (subject.scope === 'collab') {\n const siteAdmin = isSiteAdminCollaborator(caller.member, subject.hostId ?? '')\n if (!canManage && !siteAdmin) return json({ error: 'billing.manage required' }, 403)\n if (!canManage && subject.uid === caller.uid) {\n return json({ error: 'You cannot change your own AI allotment', code: 'own_allotment' }, 403)\n }\n const target = await memberOf(subject.uid ?? '')\n if (!target || hostRoleFor(target, subject.hostId ?? '') === null) {\n return json({ error: 'That person is not a collaborator on that site' }, 404)\n }\n // A site's admin sets allotments for the site's COLLABORATORS: a team\n // member's AI is the organization's to bound, not one site's.\n if (!canManage && isOrgWideMember(target)) {\n return json({ error: 'billing.manage required' }, 403)\n }\n } else if (!canManage) {\n return json({ error: 'billing.manage required' }, 403)\n } else if (subject.scope === 'member') {\n if (!(await memberOf(subject.uid ?? ''))) {\n return json({ error: 'That person is not a member of this organization' }, 404)\n }\n } else if (subject.scope === 'host') {\n const host = await firestore.collection('hosts').doc(subject.hostId ?? '').get()\n if (!host.exists || host.get('orgId') !== orgId) {\n return json({ error: 'That site is not part of this organization' }, 404)\n }\n hostNames.set(host.id, String(host.get('displayName') ?? '').trim() || host.id)\n }\n }\n const restricting = writes.some((write) => write.subject.scope === 'org' && write.models)\n if (restricting && !AI_MODEL_RESTRICTION_PLANS.includes(resolveEffectivePlan(org as never))) {\n return json(\n {\n error: 'Restricting models for the whole organization is available on Agency and Enterprise.',\n code: 'not_available',\n },\n 409,\n )\n }\n\n const set: AiAllotmentWrite[] = [...writes]\n if (everySite) {\n const sites = await firestore.collection('hosts').where('orgId', '==', orgId).limit(MAX_SITES).get()\n for (const site of sites.docs) {\n hostNames.set(site.id, String(site.get('displayName') ?? '').trim() || site.id)\n const subject = parseAiAllotmentSubject(`host:${site.id}`)\n if (subject) set.push({ subject, ...everySite })\n }\n if (set.length + removals.length > MAX_WRITES) {\n return json({ error: `Change at most ${MAX_WRITES} allotments at once` }, 400)\n }\n }\n\n await applyAiAllotmentWrites(\n firestore,\n orgId,\n { set, remove: removals.map((subject) => subject.id) },\n caller.uid,\n )\n\n // A site allotment set in a month the site's map has no key for yet is\n // given the month so far from its people's own months, once.\n const month = aiUsageMonthKeys()[0]\n const siteIds = set\n .filter((write) => write.subject.scope === 'host' && write.credits !== null)\n .map((write) => write.subject.hostId as string)\n if (siteIds.length) {\n const roster = await readRosterMonths(firestore, orgRef, month)\n await seedAiHostMonthCredits(firestore, orgId, siteIds, month, (siteId) =>\n roster.reduce((sum, entry) => sum + (entry.byHost[siteId] ?? 0), 0),\n ).catch((error) => console.error('[ai/allotments] site month seed failed', orgId, error))\n }\n\n void firestore\n .collection('adminAudit')\n .add({\n actorUid: caller.uid,\n actorEmail: caller.email,\n action: 'ai.allotments.set',\n target: `orgs/${orgId}/aiAllotments`,\n before: null,\n after: {\n set: set.map((write) => ({ subject: write.subject.id, credits: write.credits, mode: write.mode, models: write.models })),\n removed: removals.map((subject) => subject.id),\n },\n at: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n\n const actor = { uid: caller.uid, email: caller.email }\n const feedRow = (subject: AiAllotmentSubject) => ({\n type: subject.scope === 'host' ? ('host' as const) : subject.scope === 'org' ? ('org' as const) : ('member' as const),\n id: subject.scope === 'host' ? subject.hostId : subject.uid,\n name:\n subject.scope === 'host'\n ? hostNames.get(subject.hostId ?? '') ?? subject.hostId\n : subject.scope === 'org'\n ? 'Organization model restriction'\n : [\n memberDocs.get(subject.uid ?? '') ? memberName(memberDocs.get(subject.uid ?? '') as AglynOrgMember) : subject.uid,\n subject.scope === 'collab' ? `on ${subject.hostId}` : null,\n ]\n .filter(Boolean)\n .join(' '),\n })\n await Promise.all([\n ...set.map((write) =>\n logAiAllotmentChanged(orgId, actor, {\n subject: feedRow(write.subject),\n after: { credits: write.credits, mode: write.mode, models: write.models },\n }).catch(() => undefined),\n ),\n ...removals.map((subject) =>\n logAiAllotmentChanged(orgId, actor, { subject: feedRow(subject), after: null }).catch(\n () => undefined,\n ),\n ),\n ])\n\n return json({ ok: true, set: set.length, removed: removals.length }, 200)\n}\n\nasync function handler(request: Request): Promise<Response> {\n try {\n if (request.method === 'GET') return await handleGet(request)\n if (request.method === 'POST') return await handlePost(request)\n return json({ error: 'Method not allowed' }, 405)\n } catch (error) {\n // A refused credential is a 401, not a fault of ours (AGL-1993).\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error('[ai/allotments] failed', error)\n return json({ error: 'AI allotments unavailable' }, 500)\n }\n}\n\nexport { handler as GET, handler as POST }\n"],"names":["pluginRequestFromWeb","assistCreditsFromUsd","resolveAssistCreditBudget","hostRoleFor","isOrgWideMember","resolveEffectivePlan","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","memberHasOrgPermission","resolveOrgMembership","invalidIdTokenResponse","resolveOrgIdForHost","FieldValue","logAiAllotmentChanged","AI_ALLOTMENT_MAX_CREDITS","AI_ALLOTMENT_MAX_MODELS","aiAllotmentCredits","parseAiAllotmentSubject","aiUsageByUserMonthFrom","aiUsageMonthKeys","AI_MODEL_CATALOG","AI_MODEL_RESTRICTION_PLANS","resolveAiProvider","AI_HOST_CREDITS_FIELD","applyAiAllotmentWrites","listAiAllotments","seedAiHostMonthCredits","userAiUsageMonthRef","json","body","status","Response","MAX_WRITES","MAX_SITES","GET_ALL_CHUNK","authenticate","headers","authorization","idToken","startsWith","slice","length","undefined","error","decoded","app","auth","verifyIdToken","email_verified","uid","email","staff","isSiteAdminCollaborator","member","hostId","readRosterMonths","firestore","orgRef","month","roster","collection","get","members","docs","map","doc","$id","id","data","out","start","chunk","snapshots","getAll","forEach","snapshot","index","usage","exists","push","credits","byHost","memberName","String","displayName","trim","siteCredits","orgMonth","value","Number","isFinite","Math","floor","reduce","sum","entry","handleGet","request","params","orgSnapshot","query","subjectUid","orgId","membership","canManage","canView","siteAdmin","allowed","allotments","hostDocs","Promise","all","then","where","limit","org","byUid","Map","used","allotment","person","scope","rows","filter","subject","mode","models","visibleMembers","name","role","orgWide","hosts","provider","wire","pool","label","tier","restrictionAvailable","includes","canEdit","billing","collaborators","callerUid","CATALOG_IDS","Set","parseAllotmentWrite","raw","toLocaleString","Array","isArray","ids","Boolean","unknown","find","has","handlePost","payload","rawSet","rawRemove","rawEverySite","writes","parsed","code","removals","everySite","caller","touched","write","memberDocs","memberOf","set","hostNames","target","host","restricting","some","sites","site","remove","siteIds","siteId","catch","console","add","actorUid","actorEmail","action","before","after","removed","at","serverTimestamp","actor","feedRow","type","join","ok","handler","method","unauthenticated","GET","POST"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-allotments.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { pluginRequestFromWeb } from '@aglyn/aglyn/server'\nimport {\n assistCreditsFromUsd,\n resolveAssistCreditBudget,\n} from '@aglyn/aglyn/app-utils/assist-credits'\nimport { hostRoleFor, isOrgWideMember } from '@aglyn/aglyn/app-utils/organizations'\nimport { resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AglynOrgMember } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n memberHasOrgPermission,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { logAiAllotmentChanged } from '../activity/ai-activity'\nimport {\n AI_ALLOTMENT_MAX_CREDITS,\n AI_ALLOTMENT_MAX_MODELS,\n aiAllotmentCredits,\n parseAiAllotmentSubject,\n type AiAllotment,\n type AiAllotmentMode,\n type AiAllotmentSubject,\n} from '../model/ai-allotments'\nimport { aiUsageByUserMonthFrom, aiUsageMonthKeys } from '../model/ai-usage-by-user'\nimport { AI_MODEL_CATALOG } from '../providers/catalog'\nimport { AI_MODEL_RESTRICTION_PLANS } from '../providers/model-choice'\nimport { resolveAiProvider } from '../providers/routing'\nimport {\n AI_HOST_CREDITS_FIELD,\n applyAiAllotmentWrites,\n listAiAllotments,\n seedAiHostMonthCredits,\n type AiAllotmentWrite,\n} from '../usage/ai-allotments'\nimport { userAiUsageMonthRef } from '../usage/ai-usage-by-user'\nimport type {\n AiAllotmentRowWire,\n AiAllotmentsHostWire,\n AiAllotmentsMemberWire,\n AiAllotmentsWire,\n} from '../usage/ai-usage-wire'\nimport { addAdminAudit } from '@aglyn/tenant-data-admin/server/admin-audit-write'\n\n// lockdown-423: exempt — a spend control, the posture of ai/billing/overage\n// beside it: a billing-locked workspace must still be able to LOWER what its\n// people may spend, and a 423 would trap it spending.\n\n/**\n * AI ALLOTMENTS (AGL-2942) — read and set them.\n *\n * ## Who may read\n *\n * - The whole org's allotments (`orgId`): `billing.view`, the Usage page's\n * gate — every allotment is a figure about the pool it is part of.\n * - One site's (`hostId`): the same, or the site's admin when they are a\n * collaborator scoped to it — the collaborators card is theirs.\n * - One person's (`uid`): the same, or the person themselves.\n *\n * ## Who may write\n *\n * - A member's, a site's and the org-wide model restriction:\n * `billing.manage`. Each moves how much of the paid pool someone may\n * spend.\n * - A collaborator's on one site: `billing.manage`, or the admin of that\n * site when they are a collaborator scoped to it — never on their own\n * allotment, which they could otherwise raise.\n *\n * Staff pass, as at the other org routes. The org-wide restriction is set\n * only on a plan that offers it (`AI_MODEL_RESTRICTION_PLANS`); clearing it\n * is always allowed.\n *\n * ## One write, many subjects\n *\n * `POST { orgId, set, remove, everySite }`: `set` replaces allotments,\n * `remove` deletes them, and `everySite` sets the same allotment on every\n * site the org has — \"same for every client site\" — expanded here, from the\n * org's sites, rather than trusted from a client's list.\n */\n\nconst json = (body: unknown, status: number) => Response.json(body, { status })\n\n/** Batch writes a request may make; Firestore commits at most 500. */\nconst MAX_WRITES = 450\n\n/** Sites one `everySite` expands to. */\nconst MAX_SITES = 400\n\n/** Member months one `getAll` carries. */\nconst GET_ALL_CHUNK = 100\n\ninterface Caller {\n uid: string\n email: string | null\n staff: boolean\n member: AglynOrgMember | null\n}\n\nasync function authenticate(\n headers: Partial<Record<string, string>>,\n): Promise<Response | { decoded: { uid: string; email?: string; staff: boolean } }> {\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return json({ error: 'Unauthenticated' }, 401)\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n return { decoded: { uid: decoded.uid, email: decoded.email, staff: decoded['staff'] === true } }\n}\n\n/** A collaborator scoped to the site who holds its admin role there. */\nfunction isSiteAdminCollaborator(member: AglynOrgMember | null, hostId: string): boolean {\n if (!member || !hostId || isOrgWideMember(member)) return false\n return hostRoleFor(member, hostId) === 'admin'\n}\n\ninterface RosterMonth {\n member: AglynOrgMember\n credits: number\n byHost: Record<string, number>\n}\n\n/** The roster and each member's month, by path — no index, one getAll a hundred. */\nasync function readRosterMonths(\n firestore: FirebaseFirestore.Firestore,\n orgRef: FirebaseFirestore.DocumentReference,\n month: string,\n): Promise<RosterMonth[]> {\n const roster = await orgRef.collection('members').get()\n const members = roster.docs.map(\n (doc) => ({ $id: doc.id, ...doc.data() }) as AglynOrgMember,\n )\n const out: RosterMonth[] = []\n for (let start = 0; start < members.length; start += GET_ALL_CHUNK) {\n const chunk = members.slice(start, start + GET_ALL_CHUNK)\n const snapshots = chunk.length\n ? await firestore.getAll(\n ...chunk.map((member) => userAiUsageMonthRef(orgRef, member.$id, month)),\n )\n : []\n snapshots.forEach((snapshot, index) => {\n const member = chunk[index]\n const usage = aiUsageByUserMonthFrom(\n snapshot.exists ? (snapshot.data() as Record<string, unknown>) : null,\n member.$id,\n month,\n )\n out.push({ member, credits: usage.credits, byHost: usage.byHost })\n })\n }\n return out\n}\n\nconst memberName = (member: AglynOrgMember): string =>\n String(member.displayName ?? '').trim() || String(member.email ?? '').trim() || member.$id\n\n/** A site's credits this month: the org map, else its people's months summed. */\nfunction siteCredits(\n orgMonth: FirebaseFirestore.DocumentSnapshot,\n roster: readonly RosterMonth[],\n hostId: string,\n): number {\n const map = (orgMonth.exists ? orgMonth.get(AI_HOST_CREDITS_FIELD) : null) as\n | Record<string, unknown>\n | null\n if (map && map[hostId] !== undefined) {\n const value = Number(map[hostId])\n return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0\n }\n return roster.reduce((sum, entry) => sum + (entry.byHost[hostId] ?? 0), 0)\n}\n\nasync function handleGet(request: Request): Promise<Response> {\n const { query, headers } = await pluginRequestFromWeb(request)\n const params = (query ?? {}) as Record<string, unknown>\n const hostId = String(params['hostId'] ?? '').trim()\n const subjectUid = String(params['uid'] ?? '').trim()\n const auth = await authenticate(headers as Partial<Record<string, string>>)\n if (auth instanceof Response) return auth\n const { decoded } = auth\n const orgId =\n String(params['orgId'] ?? '').trim() || (hostId ? ((await resolveOrgIdForHost(hostId)) ?? '') : '')\n if (!orgId) return json({ error: 'Missing orgId' }, 400)\n\n const membership = await resolveOrgMembership(decoded.uid, orgId)\n const member = membership?.member ?? null\n if (!member && !decoded.staff) return json({ error: 'Not found' }, 404)\n const canManage =\n decoded.staff || (await memberHasOrgPermission(orgId, member, 'billing.manage'))\n const canView =\n canManage || (await memberHasOrgPermission(orgId, member, 'billing.view'))\n const siteAdmin = isSiteAdminCollaborator(member, hostId)\n const allowed = subjectUid\n ? canView || subjectUid === decoded.uid || siteAdmin\n : hostId\n ? canView || siteAdmin\n : canView\n if (!allowed) return json({ error: 'billing.view required' }, 403)\n\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const month = aiUsageMonthKeys()[0]\n const [orgSnapshot, orgMonth, allotments, roster, hostDocs] = await Promise.all([\n orgRef.get(),\n orgRef.collection('assistUsage').doc(month).get(),\n listAiAllotments(firestore, orgId, subjectUid ? { uid: subjectUid } : hostId ? { hostId } : {}),\n readRosterMonths(firestore, orgRef, month),\n hostId\n ? firestore.collection('hosts').doc(hostId).get().then((doc) => (doc.exists ? [doc] : []))\n : firestore.collection('hosts').where('orgId', '==', orgId).limit(MAX_SITES).get().then((snapshot) => snapshot.docs),\n ])\n const org = (orgSnapshot.data() ?? {}) as Record<string, unknown>\n const byUid = new Map(roster.map((entry) => [entry.member.$id, entry]))\n\n const used = (allotment: AiAllotment): number => {\n const person = allotment.uid ? byUid.get(allotment.uid) : undefined\n switch (allotment.scope) {\n case 'member':\n return person?.credits ?? 0\n case 'collab':\n return allotment.hostId ? (person?.byHost[allotment.hostId] ?? 0) : 0\n case 'host':\n return allotment.hostId ? siteCredits(orgMonth, roster, allotment.hostId) : 0\n default:\n return 0\n }\n }\n const rows: AiAllotmentRowWire[] = allotments\n // A collaborator reading their own sees their own rows and nothing else.\n .filter((allotment) => canView || siteAdmin || allotment.uid === decoded.uid)\n .map((allotment) => ({\n subject: allotment.subject,\n scope: allotment.scope,\n uid: allotment.uid,\n hostId: allotment.hostId,\n credits: allotment.credits,\n mode: allotment.mode,\n models: allotment.models,\n used: used(allotment),\n }))\n\n const visibleMembers = roster.filter((entry) =>\n subjectUid\n ? entry.member.$id === subjectUid\n : hostId\n ? hostRoleFor(entry.member, hostId) !== null\n : canView,\n )\n const members: AiAllotmentsMemberWire[] = visibleMembers.map((entry) => ({\n uid: entry.member.$id,\n name: memberName(entry.member),\n email: typeof entry.member.email === 'string' ? entry.member.email : null,\n role: typeof entry.member.role === 'string' ? entry.member.role : null,\n orgWide: isOrgWideMember(entry.member),\n credits: entry.credits,\n byHost: entry.byHost,\n }))\n const hosts: AiAllotmentsHostWire[] = hostDocs.map((doc) => ({\n hostId: doc.id,\n name: String(doc.get('displayName') ?? '').trim() || String(doc.get('subdomain') ?? '').trim() || doc.id,\n credits: siteCredits(orgMonth, roster, doc.id),\n }))\n const provider = resolveAiProvider()\n const wire: AiAllotmentsWire = {\n orgId,\n month,\n pool: {\n used: assistCreditsFromUsd(Number(orgMonth.get('estCostUsd') ?? 0)),\n limit: resolveAssistCreditBudget(org as never),\n },\n allotments: rows,\n members,\n hosts,\n models: AI_MODEL_CATALOG.filter((entry) => !provider || entry.provider === provider.id).map(\n (entry) => ({ id: entry.id, label: entry.label, tier: entry.tier }),\n ),\n restrictionAvailable: AI_MODEL_RESTRICTION_PLANS.includes(resolveEffectivePlan(org as never)),\n canEdit: {\n billing: canManage,\n collaborators: canManage || siteAdmin,\n },\n callerUid: decoded.uid,\n }\n return Response.json(wire, { status: 200, headers: { 'Cache-Control': 'no-store, private' } })\n}\n\ninterface ParsedWrite {\n subject: AiAllotmentSubject\n credits: number | null\n mode: AiAllotmentMode\n models: string[] | null\n}\n\nconst CATALOG_IDS = new Set(AI_MODEL_CATALOG.map((entry) => entry.id))\n\n/** One entry of `set`, validated; a string names what is wrong. */\nexport function parseAllotmentWrite(raw: unknown): ParsedWrite | string {\n const entry = (raw ?? {}) as Record<string, unknown>\n const subject = parseAiAllotmentSubject(entry['subject'])\n if (!subject) return 'subject must be member:{uid}, collab:{hostId}:{uid}, host:{hostId} or org'\n let credits: number | null = null\n if (entry['credits'] !== null && entry['credits'] !== undefined) {\n credits = typeof entry['credits'] === 'number' ? aiAllotmentCredits(entry['credits']) : null\n if (credits === null || (entry['credits'] as number) > AI_ALLOTMENT_MAX_CREDITS) {\n return `credits must be a whole number from 1 to ${AI_ALLOTMENT_MAX_CREDITS.toLocaleString('en-US')}`\n }\n }\n const mode = entry['mode'] === undefined ? 'hard' : entry['mode']\n if (mode !== 'hard' && mode !== 'soft') return 'mode must be hard or soft'\n let models: string[] | null = null\n if (entry['models'] !== null && entry['models'] !== undefined) {\n if (!Array.isArray(entry['models'])) return 'models must be a list of model ids'\n const ids = [...new Set(entry['models'].map((id) => String(id ?? '').trim()))].filter(Boolean)\n if (ids.length > AI_ALLOTMENT_MAX_MODELS) return `name at most ${AI_ALLOTMENT_MAX_MODELS} models`\n const unknown = ids.find((id) => !CATALOG_IDS.has(id))\n if (unknown) return `${unknown} is not a model this platform offers`\n models = ids.length ? ids : null\n }\n if (subject.scope === 'org' && credits !== null) {\n return 'the org-wide restriction carries models, not credits'\n }\n if (subject.scope !== 'org' && credits === null && models === null) {\n return 'set credits or models, or remove the allotment'\n }\n return { subject, credits, mode, models }\n}\n\nasync function handlePost(request: Request): Promise<Response> {\n const { body, headers } = await pluginRequestFromWeb(request)\n const auth = await authenticate(headers as Partial<Record<string, string>>)\n if (auth instanceof Response) return auth\n const { decoded } = auth\n const payload = (body ?? {}) as Record<string, unknown>\n const orgId = String(payload['orgId'] ?? '').trim()\n if (!orgId) return json({ error: 'Missing orgId' }, 400)\n\n const rawSet = Array.isArray(payload['set']) ? payload['set'] : []\n const rawRemove = Array.isArray(payload['remove']) ? payload['remove'] : []\n const rawEverySite = payload['everySite'] ?? null\n if (!rawSet.length && !rawRemove.length && !rawEverySite) {\n return json({ error: 'Nothing to change' }, 400)\n }\n if (rawSet.length + rawRemove.length > MAX_WRITES) {\n return json({ error: `Change at most ${MAX_WRITES} allotments at once` }, 400)\n }\n\n const writes: ParsedWrite[] = []\n for (const raw of rawSet) {\n const parsed = parseAllotmentWrite(raw)\n if (typeof parsed === 'string') return json({ error: parsed, code: 'invalid_allotment' }, 400)\n writes.push(parsed)\n }\n const removals: AiAllotmentSubject[] = []\n for (const raw of rawRemove) {\n const subject = parseAiAllotmentSubject(raw)\n if (!subject) return json({ error: `${String(raw)} is not an allotment subject` }, 400)\n removals.push(subject)\n }\n let everySite: Omit<ParsedWrite, 'subject'> | null = null\n if (rawEverySite) {\n const parsed = parseAllotmentWrite({ ...(rawEverySite as object), subject: 'host:every-site' })\n if (typeof parsed === 'string') return json({ error: parsed, code: 'invalid_allotment' }, 400)\n everySite = { credits: parsed.credits, mode: parsed.mode, models: parsed.models }\n }\n\n const membership = await resolveOrgMembership(decoded.uid, orgId)\n const caller: Caller = {\n uid: decoded.uid,\n email: decoded.email ?? null,\n staff: decoded.staff,\n member: membership?.member ?? null,\n }\n if (!caller.member && !caller.staff) return json({ error: 'Not found' }, 404)\n const canManage =\n caller.staff || (await memberHasOrgPermission(orgId, caller.member, 'billing.manage'))\n\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const orgSnapshot = await orgRef.get()\n if (!orgSnapshot.exists) return json({ error: 'Unknown organization' }, 404)\n const org = (orgSnapshot.data() ?? {}) as Record<string, unknown>\n\n // Every subject the request touches, set or removed, is decided the same\n // way: the permission first, then that the person or site belongs here.\n const touched = [...writes.map((write) => write.subject), ...removals]\n if (everySite && !canManage) return json({ error: 'billing.manage required' }, 403)\n const memberDocs = new Map<string, AglynOrgMember | null>()\n const memberOf = async (uid: string): Promise<AglynOrgMember | null> => {\n if (!memberDocs.has(uid)) {\n const snapshot = await orgRef.collection('members').doc(uid).get()\n memberDocs.set(uid, snapshot.exists ? ({ $id: uid, ...snapshot.data() } as AglynOrgMember) : null)\n }\n return memberDocs.get(uid) ?? null\n }\n const hostNames = new Map<string, string>()\n for (const subject of touched) {\n if (subject.scope === 'collab') {\n const siteAdmin = isSiteAdminCollaborator(caller.member, subject.hostId ?? '')\n if (!canManage && !siteAdmin) return json({ error: 'billing.manage required' }, 403)\n if (!canManage && subject.uid === caller.uid) {\n return json({ error: 'You cannot change your own AI allotment', code: 'own_allotment' }, 403)\n }\n const target = await memberOf(subject.uid ?? '')\n if (!target || hostRoleFor(target, subject.hostId ?? '') === null) {\n return json({ error: 'That person is not a collaborator on that site' }, 404)\n }\n // A site's admin sets allotments for the site's COLLABORATORS: a team\n // member's AI is the organization's to bound, not one site's.\n if (!canManage && isOrgWideMember(target)) {\n return json({ error: 'billing.manage required' }, 403)\n }\n } else if (!canManage) {\n return json({ error: 'billing.manage required' }, 403)\n } else if (subject.scope === 'member') {\n if (!(await memberOf(subject.uid ?? ''))) {\n return json({ error: 'That person is not a member of this organization' }, 404)\n }\n } else if (subject.scope === 'host') {\n const host = await firestore.collection('hosts').doc(subject.hostId ?? '').get()\n if (!host.exists || host.get('orgId') !== orgId) {\n return json({ error: 'That site is not part of this organization' }, 404)\n }\n hostNames.set(host.id, String(host.get('displayName') ?? '').trim() || host.id)\n }\n }\n const restricting = writes.some((write) => write.subject.scope === 'org' && write.models)\n if (restricting && !AI_MODEL_RESTRICTION_PLANS.includes(resolveEffectivePlan(org as never))) {\n return json(\n {\n error: 'Restricting models for the whole organization is available on Agency and Enterprise.',\n code: 'not_available',\n },\n 409,\n )\n }\n\n const set: AiAllotmentWrite[] = [...writes]\n if (everySite) {\n const sites = await firestore.collection('hosts').where('orgId', '==', orgId).limit(MAX_SITES).get()\n for (const site of sites.docs) {\n hostNames.set(site.id, String(site.get('displayName') ?? '').trim() || site.id)\n const subject = parseAiAllotmentSubject(`host:${site.id}`)\n if (subject) set.push({ subject, ...everySite })\n }\n if (set.length + removals.length > MAX_WRITES) {\n return json({ error: `Change at most ${MAX_WRITES} allotments at once` }, 400)\n }\n }\n\n await applyAiAllotmentWrites(\n firestore,\n orgId,\n { set, remove: removals.map((subject) => subject.id) },\n caller.uid,\n )\n\n // A site allotment set in a month the site's map has no key for yet is\n // given the month so far from its people's own months, once.\n const month = aiUsageMonthKeys()[0]\n const siteIds = set\n .filter((write) => write.subject.scope === 'host' && write.credits !== null)\n .map((write) => write.subject.hostId as string)\n if (siteIds.length) {\n const roster = await readRosterMonths(firestore, orgRef, month)\n await seedAiHostMonthCredits(firestore, orgId, siteIds, month, (siteId) =>\n roster.reduce((sum, entry) => sum + (entry.byHost[siteId] ?? 0), 0),\n ).catch((error) => console.error('[ai/allotments] site month seed failed', orgId, error))\n }\n\n void addAdminAudit(firestore, {\n actorUid: caller.uid,\n actorEmail: caller.email,\n action: 'ai.allotments.set',\n target: `orgs/${orgId}/aiAllotments`,\n before: null,\n after: {\n set: set.map((write) => ({ subject: write.subject.id, credits: write.credits, mode: write.mode, models: write.models })),\n removed: removals.map((subject) => subject.id),\n },\n at: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n\n const actor = { uid: caller.uid, email: caller.email }\n const feedRow = (subject: AiAllotmentSubject) => ({\n type: subject.scope === 'host' ? ('host' as const) : subject.scope === 'org' ? ('org' as const) : ('member' as const),\n id: subject.scope === 'host' ? subject.hostId : subject.uid,\n name:\n subject.scope === 'host'\n ? hostNames.get(subject.hostId ?? '') ?? subject.hostId\n : subject.scope === 'org'\n ? 'Organization model restriction'\n : [\n memberDocs.get(subject.uid ?? '') ? memberName(memberDocs.get(subject.uid ?? '') as AglynOrgMember) : subject.uid,\n subject.scope === 'collab' ? `on ${subject.hostId}` : null,\n ]\n .filter(Boolean)\n .join(' '),\n })\n await Promise.all([\n ...set.map((write) =>\n logAiAllotmentChanged(orgId, actor, {\n subject: feedRow(write.subject),\n after: { credits: write.credits, mode: write.mode, models: write.models },\n }).catch(() => undefined),\n ),\n ...removals.map((subject) =>\n logAiAllotmentChanged(orgId, actor, { subject: feedRow(subject), after: null }).catch(\n () => undefined,\n ),\n ),\n ])\n\n return json({ ok: true, set: set.length, removed: removals.length }, 200)\n}\n\nasync function handler(request: Request): Promise<Response> {\n try {\n if (request.method === 'GET') return await handleGet(request)\n if (request.method === 'POST') return await handlePost(request)\n return json({ error: 'Method not allowed' }, 405)\n } catch (error) {\n // A refused credential is a 401, not a fault of ours (AGL-1993).\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error('[ai/allotments] failed', error)\n return json({ error: 'AI allotments unavailable' }, 500)\n }\n}\n\nexport { handler as GET, handler as POST }\n"],"names":["pluginRequestFromWeb","assistCreditsFromUsd","resolveAssistCreditBudget","hostRoleFor","isOrgWideMember","resolveEffectivePlan","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","memberHasOrgPermission","resolveOrgMembership","invalidIdTokenResponse","resolveOrgIdForHost","FieldValue","logAiAllotmentChanged","AI_ALLOTMENT_MAX_CREDITS","AI_ALLOTMENT_MAX_MODELS","aiAllotmentCredits","parseAiAllotmentSubject","aiUsageByUserMonthFrom","aiUsageMonthKeys","AI_MODEL_CATALOG","AI_MODEL_RESTRICTION_PLANS","resolveAiProvider","AI_HOST_CREDITS_FIELD","applyAiAllotmentWrites","listAiAllotments","seedAiHostMonthCredits","userAiUsageMonthRef","addAdminAudit","json","body","status","Response","MAX_WRITES","MAX_SITES","GET_ALL_CHUNK","authenticate","headers","authorization","idToken","startsWith","slice","length","undefined","error","decoded","app","auth","verifyIdToken","email_verified","uid","email","staff","isSiteAdminCollaborator","member","hostId","readRosterMonths","firestore","orgRef","month","roster","collection","get","members","docs","map","doc","$id","id","data","out","start","chunk","snapshots","getAll","forEach","snapshot","index","usage","exists","push","credits","byHost","memberName","String","displayName","trim","siteCredits","orgMonth","value","Number","isFinite","Math","floor","reduce","sum","entry","handleGet","request","params","orgSnapshot","query","subjectUid","orgId","membership","canManage","canView","siteAdmin","allowed","allotments","hostDocs","Promise","all","then","where","limit","org","byUid","Map","used","allotment","person","scope","rows","filter","subject","mode","models","visibleMembers","name","role","orgWide","hosts","provider","wire","pool","label","tier","restrictionAvailable","includes","canEdit","billing","collaborators","callerUid","CATALOG_IDS","Set","parseAllotmentWrite","raw","toLocaleString","Array","isArray","ids","Boolean","unknown","find","has","handlePost","payload","rawSet","rawRemove","rawEverySite","writes","parsed","code","removals","everySite","caller","touched","write","memberDocs","memberOf","set","hostNames","target","host","restricting","some","sites","site","remove","siteIds","siteId","catch","console","actorUid","actorEmail","action","before","after","removed","at","serverTimestamp","actor","feedRow","type","join","ok","handler","method","unauthenticated","GET","POST"],"mappings":"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"}
|
|
@@ -20,6 +20,7 @@ import { readOrgAiUsageByUser, readUserAiUsageMonths } from "../usage/ai-usage-b
|
|
|
20
20
|
import { FieldValue } from "firebase-admin/firestore";
|
|
21
21
|
import { invalidIdTokenResponse } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
22
22
|
import { AI_USAGE_CSV_HEADER, AI_USAGE_EXPORT_ROWS_HEADER, aiUsageCsvLine, toAiUsageWireRow } from "../usage/ai-usage-wire.js";
|
|
23
|
+
import { addAdminAudit } from "@aglyn/tenant-data-admin/server/admin-audit-write";
|
|
23
24
|
// lockdown-423: exempt — a READ-ONLY usage report that writes nothing to the
|
|
24
25
|
// workspace, the posture of ai/billing/credits beside it: a billing-locked
|
|
25
26
|
// owner working out who spent the credits needs this table more, not less.
|
|
@@ -183,7 +184,7 @@ async function handler(request) {
|
|
|
183
184
|
});
|
|
184
185
|
// Ids and counts only, never content: a report about people leaving
|
|
185
186
|
// the platform is worth a row; who was in it is the workspace's own.
|
|
186
|
-
void firestore
|
|
187
|
+
void addAdminAudit(firestore, {
|
|
187
188
|
actorUid: decoded.uid,
|
|
188
189
|
action: 'ai-usage.exported',
|
|
189
190
|
target: `orgs/${orgId}/aiUsageByUser`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-usage.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 { csvCell, pluginRequestFromWeb } from '@aglyn/aglyn/server'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n memberHasOrgPermission,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport {\n aiUsageMonthKeys,\n aiUsageMonthWithinRetention,\n aiUsageShare,\n type AiUsageByUserMonth,\n} from '../model/ai-usage-by-user'\nimport {\n readOrgAiUsageByUser,\n readUserAiUsageMonths,\n} from '../usage/ai-usage-by-user'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport {\n AI_USAGE_CSV_HEADER,\n AI_USAGE_EXPORT_ROWS_HEADER,\n aiUsageCsvLine,\n toAiUsageWireRow,\n type OrgAiUsageTableWire,\n type UserAiUsageWire,\n} from '../usage/ai-usage-wire'\n\n// lockdown-423: exempt — a READ-ONLY usage report that writes nothing to the\n// workspace, the posture of ai/billing/credits beside it: a billing-locked\n// owner working out who spent the credits needs this table more, not less.\n\n/**\n * WHO IS GENERATING WHAT, PER SITE (AGL-2928) — the customer's read of the\n * per-user AI usage rollup.\n *\n * ## Three questions, one route\n *\n * - **The month's table** (`orgId`, `month`): every current member with a\n * month document, dearest first, with each person's share of the\n * workspace's spend. Gated on `billing.view`, the Usage page's gate, and\n * served as JSON or — `format=csv` — as a streamed file.\n * - **One person** (`uid`): that person's months, newest first, for the\n * member detail card. The subject reads their own; anyone else needs\n * `billing.view` or `org.auditLog`, which is the team page's gate and the\n * pair the security rules admit for the same documents.\n * - **One site** (`hostId`): the table with each row's credits ON THAT SITE\n * beside it, for the site's collaborators card — the agency question,\n * \"which client site's collaborators spend\".\n *\n * ## No dollar figure crosses this boundary\n *\n * The month document carries `estCostUsd`, our provider bill. The rows here\n * carry credits and a SHARE, and the share is computed on the dollars\n * server-side (`aiUsageShare`) so it is exact — a roster's credits each round\n * up, the org's round once — without the dollars ever being serialized.\n *\n * ## The month bound\n *\n * `month` must be inside the retention window (`aiUsageMonthKeys`): a\n * request for an older month is refused rather than answered empty, because\n * \"nothing for that month\" and \"that month has been reaped\" must not read\n * the same on a report somebody exports.\n */\n\n/** The most months one person's card is handed. */\nconst MAX_USER_MONTHS = 13\n\nconst json = (body: unknown, status: number) => Response.json(body, { status })\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') return json({ error: 'Method not allowed' }, 405)\n\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return json({ error: 'Unauthenticated' }, 401)\n\n const params = (query ?? {}) as Record<string, unknown>\n const orgId = String(params['orgId'] ?? '').trim()\n if (!orgId) return json({ error: 'Missing orgId' }, 400)\n const now = new Date()\n const months = aiUsageMonthKeys(now)\n const month = String(params['month'] ?? months[0]).trim()\n if (!aiUsageMonthWithinRetention(month, now)) {\n return json(\n {\n error:\n `AI usage is kept for ${months.length} months; ` +\n `ask for one of ${months[months.length - 1]} through ${months[0]}.`,\n },\n 400,\n )\n }\n const subjectUid = String(params['uid'] ?? '').trim()\n const hostId = String(params['hostId'] ?? '').trim()\n const format = String(params['format'] ?? 'json').trim()\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const staff = decoded['staff'] === true\n const membership = await resolveOrgMembership(decoded.uid, orgId)\n const member = membership?.member\n if (!member && !staff) return json({ error: 'Not found' }, 404)\n\n const firestore = firebaseAdmin.app().firestore()\n\n if (subjectUid) {\n // One person. Their own months are theirs to read; anyone else's need\n // the Usage page's permission or the team page's.\n const own = subjectUid === decoded.uid && Boolean(member)\n if (!own && !staff) {\n const allowed =\n (await memberHasOrgPermission(orgId, member, 'billing.view')) ||\n (await memberHasOrgPermission(orgId, member, 'org.auditLog'))\n if (!allowed) return json({ error: 'billing.view required' }, 403)\n }\n const limit = Math.min(\n MAX_USER_MONTHS,\n Math.max(1, Math.floor(Number(params['limit'] ?? 2) || 2)),\n )\n const rows = await readUserAiUsageMonths(firestore, orgId, subjectUid, limit)\n const orgRef = firestore.collection('orgs').doc(orgId)\n const rollups = rows.length\n ? await firestore.getAll(\n ...rows.map((row) => orgRef.collection('assistUsage').doc(row.month)),\n )\n : []\n const wire: UserAiUsageWire = {\n uid: subjectUid,\n months: rows.map((row: AiUsageByUserMonth, index) => ({\n month: row.month,\n credits: row.credits,\n share: aiUsageShare(\n row.estCostUsd,\n Number(rollups[index]?.get('estCostUsd') ?? 0),\n ),\n requests: row.requests,\n refusals: row.refusals,\n byKind: row.byKind,\n byHost: row.byHost,\n })),\n }\n return json(wire, 200)\n }\n\n // The table, which is about everyone: the Usage page's permission.\n if (!staff && !(await memberHasOrgPermission(orgId, member, 'billing.view'))) {\n return json({ error: 'billing.view required' }, 403)\n }\n const usage = await readOrgAiUsageByUser(firestore, orgId, month)\n const rows = usage.rows.map((row) => toAiUsageWireRow(row, hostId))\n if (hostId) {\n rows.sort(\n (a, b) =>\n (b.hostCredits ?? 0) - (a.hostCredits ?? 0) || b.credits - a.credits,\n )\n }\n\n if (format !== 'csv') {\n const table: OrgAiUsageTableWire = {\n month,\n months,\n orgCredits: usage.orgCredits,\n rows,\n }\n return json(table, 200)\n }\n\n /*\n * The file, streamed: the header first, then the rows in pages, so a\n * roster of any size arrives without being held whole. The row count\n * travels in a header the client checks the body against — a stream\n * that dies halfway yields a well-formed shorter file, and nothing about\n * the bytes says it is short.\n */\n const encoder = new TextEncoder()\n const PAGE = 200\n let cursor = 0\n let headed = false\n const stream = new ReadableStream<Uint8Array>({\n pull(controller) {\n if (!headed) {\n headed = true\n controller.enqueue(encoder.encode(AI_USAGE_CSV_HEADER.map(csvCell).join(',')))\n return\n }\n if (cursor < rows.length) {\n const page = rows.slice(cursor, cursor + PAGE)\n cursor += page.length\n controller.enqueue(\n encoder.encode(`\\n${page.map(aiUsageCsvLine).join('\\n')}`),\n )\n return\n }\n controller.enqueue(encoder.encode('\\n'))\n controller.close()\n },\n })\n\n // Ids and counts only, never content: a report about people leaving\n // the platform is worth a row; who was in it is the workspace's own.\n void firestore\n .collection('adminAudit')\n .add({\n actorUid: decoded.uid,\n action: 'ai-usage.exported',\n target: `orgs/${orgId}/aiUsageByUser`,\n before: null,\n after: { month, rows: rows.length, hostId: hostId || null },\n at: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n\n return new Response(stream, {\n status: 200,\n headers: {\n 'Content-Type': 'text/csv; charset=utf-8',\n 'Content-Disposition': `attachment; filename=\"ai-usage-${month}.csv\"`,\n [AI_USAGE_EXPORT_ROWS_HEADER]: String(rows.length),\n 'Cache-Control': 'no-store, private',\n },\n })\n } catch (error) {\n // A refused credential is a 401, not a fault of ours (AGL-1993). Null\n // for anything else, so a real failure keeps the answer below.\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error('[ai/usage] read failed', orgId, error)\n return json({ error: 'AI usage unavailable' }, 500)\n }\n}\n\nexport { handler as GET }\n"],"names":["csvCell","pluginRequestFromWeb","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","memberHasOrgPermission","resolveOrgMembership","aiUsageMonthKeys","aiUsageMonthWithinRetention","aiUsageShare","readOrgAiUsageByUser","readUserAiUsageMonths","FieldValue","invalidIdTokenResponse","AI_USAGE_CSV_HEADER","AI_USAGE_EXPORT_ROWS_HEADER","aiUsageCsvLine","toAiUsageWireRow","MAX_USER_MONTHS","json","body","status","Response","handler","request","headers","params","method","query","rawHeaders","error","authorization","idToken","startsWith","slice","length","undefined","orgId","String","trim","now","Date","months","month","subjectUid","hostId","format","decoded","app","auth","verifyIdToken","email_verified","staff","membership","uid","member","firestore","own","Boolean","allowed","limit","Math","min","max","floor","Number","rows","orgRef","collection","doc","rollups","getAll","map","row","wire","index","credits","share","estCostUsd","get","requests","refusals","byKind","byHost","usage","sort","a","b","hostCredits","table","orgCredits","encoder","TextEncoder","PAGE","cursor","headed","stream","ReadableStream","pull","controller","enqueue","encode","join","page","close","add","actorUid","action","target","before","after","at","serverTimestamp","catch","unauthenticated","console","GET"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-usage.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 { csvCell, pluginRequestFromWeb } from '@aglyn/aglyn/server'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n memberHasOrgPermission,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport {\n aiUsageMonthKeys,\n aiUsageMonthWithinRetention,\n aiUsageShare,\n type AiUsageByUserMonth,\n} from '../model/ai-usage-by-user'\nimport {\n readOrgAiUsageByUser,\n readUserAiUsageMonths,\n} from '../usage/ai-usage-by-user'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport {\n AI_USAGE_CSV_HEADER,\n AI_USAGE_EXPORT_ROWS_HEADER,\n aiUsageCsvLine,\n toAiUsageWireRow,\n type OrgAiUsageTableWire,\n type UserAiUsageWire,\n} from '../usage/ai-usage-wire'\nimport { addAdminAudit } from '@aglyn/tenant-data-admin/server/admin-audit-write'\n\n// lockdown-423: exempt — a READ-ONLY usage report that writes nothing to the\n// workspace, the posture of ai/billing/credits beside it: a billing-locked\n// owner working out who spent the credits needs this table more, not less.\n\n/**\n * WHO IS GENERATING WHAT, PER SITE (AGL-2928) — the customer's read of the\n * per-user AI usage rollup.\n *\n * ## Three questions, one route\n *\n * - **The month's table** (`orgId`, `month`): every current member with a\n * month document, dearest first, with each person's share of the\n * workspace's spend. Gated on `billing.view`, the Usage page's gate, and\n * served as JSON or — `format=csv` — as a streamed file.\n * - **One person** (`uid`): that person's months, newest first, for the\n * member detail card. The subject reads their own; anyone else needs\n * `billing.view` or `org.auditLog`, which is the team page's gate and the\n * pair the security rules admit for the same documents.\n * - **One site** (`hostId`): the table with each row's credits ON THAT SITE\n * beside it, for the site's collaborators card — the agency question,\n * \"which client site's collaborators spend\".\n *\n * ## No dollar figure crosses this boundary\n *\n * The month document carries `estCostUsd`, our provider bill. The rows here\n * carry credits and a SHARE, and the share is computed on the dollars\n * server-side (`aiUsageShare`) so it is exact — a roster's credits each round\n * up, the org's round once — without the dollars ever being serialized.\n *\n * ## The month bound\n *\n * `month` must be inside the retention window (`aiUsageMonthKeys`): a\n * request for an older month is refused rather than answered empty, because\n * \"nothing for that month\" and \"that month has been reaped\" must not read\n * the same on a report somebody exports.\n */\n\n/** The most months one person's card is handed. */\nconst MAX_USER_MONTHS = 13\n\nconst json = (body: unknown, status: number) => Response.json(body, { status })\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') return json({ error: 'Method not allowed' }, 405)\n\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return json({ error: 'Unauthenticated' }, 401)\n\n const params = (query ?? {}) as Record<string, unknown>\n const orgId = String(params['orgId'] ?? '').trim()\n if (!orgId) return json({ error: 'Missing orgId' }, 400)\n const now = new Date()\n const months = aiUsageMonthKeys(now)\n const month = String(params['month'] ?? months[0]).trim()\n if (!aiUsageMonthWithinRetention(month, now)) {\n return json(\n {\n error:\n `AI usage is kept for ${months.length} months; ` +\n `ask for one of ${months[months.length - 1]} through ${months[0]}.`,\n },\n 400,\n )\n }\n const subjectUid = String(params['uid'] ?? '').trim()\n const hostId = String(params['hostId'] ?? '').trim()\n const format = String(params['format'] ?? 'json').trim()\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const staff = decoded['staff'] === true\n const membership = await resolveOrgMembership(decoded.uid, orgId)\n const member = membership?.member\n if (!member && !staff) return json({ error: 'Not found' }, 404)\n\n const firestore = firebaseAdmin.app().firestore()\n\n if (subjectUid) {\n // One person. Their own months are theirs to read; anyone else's need\n // the Usage page's permission or the team page's.\n const own = subjectUid === decoded.uid && Boolean(member)\n if (!own && !staff) {\n const allowed =\n (await memberHasOrgPermission(orgId, member, 'billing.view')) ||\n (await memberHasOrgPermission(orgId, member, 'org.auditLog'))\n if (!allowed) return json({ error: 'billing.view required' }, 403)\n }\n const limit = Math.min(\n MAX_USER_MONTHS,\n Math.max(1, Math.floor(Number(params['limit'] ?? 2) || 2)),\n )\n const rows = await readUserAiUsageMonths(firestore, orgId, subjectUid, limit)\n const orgRef = firestore.collection('orgs').doc(orgId)\n const rollups = rows.length\n ? await firestore.getAll(\n ...rows.map((row) => orgRef.collection('assistUsage').doc(row.month)),\n )\n : []\n const wire: UserAiUsageWire = {\n uid: subjectUid,\n months: rows.map((row: AiUsageByUserMonth, index) => ({\n month: row.month,\n credits: row.credits,\n share: aiUsageShare(\n row.estCostUsd,\n Number(rollups[index]?.get('estCostUsd') ?? 0),\n ),\n requests: row.requests,\n refusals: row.refusals,\n byKind: row.byKind,\n byHost: row.byHost,\n })),\n }\n return json(wire, 200)\n }\n\n // The table, which is about everyone: the Usage page's permission.\n if (!staff && !(await memberHasOrgPermission(orgId, member, 'billing.view'))) {\n return json({ error: 'billing.view required' }, 403)\n }\n const usage = await readOrgAiUsageByUser(firestore, orgId, month)\n const rows = usage.rows.map((row) => toAiUsageWireRow(row, hostId))\n if (hostId) {\n rows.sort(\n (a, b) =>\n (b.hostCredits ?? 0) - (a.hostCredits ?? 0) || b.credits - a.credits,\n )\n }\n\n if (format !== 'csv') {\n const table: OrgAiUsageTableWire = {\n month,\n months,\n orgCredits: usage.orgCredits,\n rows,\n }\n return json(table, 200)\n }\n\n /*\n * The file, streamed: the header first, then the rows in pages, so a\n * roster of any size arrives without being held whole. The row count\n * travels in a header the client checks the body against — a stream\n * that dies halfway yields a well-formed shorter file, and nothing about\n * the bytes says it is short.\n */\n const encoder = new TextEncoder()\n const PAGE = 200\n let cursor = 0\n let headed = false\n const stream = new ReadableStream<Uint8Array>({\n pull(controller) {\n if (!headed) {\n headed = true\n controller.enqueue(encoder.encode(AI_USAGE_CSV_HEADER.map(csvCell).join(',')))\n return\n }\n if (cursor < rows.length) {\n const page = rows.slice(cursor, cursor + PAGE)\n cursor += page.length\n controller.enqueue(\n encoder.encode(`\\n${page.map(aiUsageCsvLine).join('\\n')}`),\n )\n return\n }\n controller.enqueue(encoder.encode('\\n'))\n controller.close()\n },\n })\n\n // Ids and counts only, never content: a report about people leaving\n // the platform is worth a row; who was in it is the workspace's own.\n void addAdminAudit(firestore, {\n actorUid: decoded.uid,\n action: 'ai-usage.exported',\n target: `orgs/${orgId}/aiUsageByUser`,\n before: null,\n after: { month, rows: rows.length, hostId: hostId || null },\n at: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n\n return new Response(stream, {\n status: 200,\n headers: {\n 'Content-Type': 'text/csv; charset=utf-8',\n 'Content-Disposition': `attachment; filename=\"ai-usage-${month}.csv\"`,\n [AI_USAGE_EXPORT_ROWS_HEADER]: String(rows.length),\n 'Cache-Control': 'no-store, private',\n },\n })\n } catch (error) {\n // A refused credential is a 401, not a fault of ours (AGL-1993). Null\n // for anything else, so a real failure keeps the answer below.\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error('[ai/usage] read failed', orgId, error)\n return json({ error: 'AI usage unavailable' }, 500)\n }\n}\n\nexport { handler as GET }\n"],"names":["csvCell","pluginRequestFromWeb","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","memberHasOrgPermission","resolveOrgMembership","aiUsageMonthKeys","aiUsageMonthWithinRetention","aiUsageShare","readOrgAiUsageByUser","readUserAiUsageMonths","FieldValue","invalidIdTokenResponse","AI_USAGE_CSV_HEADER","AI_USAGE_EXPORT_ROWS_HEADER","aiUsageCsvLine","toAiUsageWireRow","addAdminAudit","MAX_USER_MONTHS","json","body","status","Response","handler","request","headers","params","method","query","rawHeaders","error","authorization","idToken","startsWith","slice","length","undefined","orgId","String","trim","now","Date","months","month","subjectUid","hostId","format","decoded","app","auth","verifyIdToken","email_verified","staff","membership","uid","member","firestore","own","Boolean","allowed","limit","Math","min","max","floor","Number","rows","orgRef","collection","doc","rollups","getAll","map","row","wire","index","credits","share","estCostUsd","get","requests","refusals","byKind","byHost","usage","sort","a","b","hostCredits","table","orgCredits","encoder","TextEncoder","PAGE","cursor","headed","stream","ReadableStream","pull","controller","enqueue","encode","join","page","close","actorUid","action","target","before","after","at","serverTimestamp","catch","unauthenticated","console","GET"],"mappings":"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"}
|
|
@@ -26,6 +26,7 @@ import { AI_BILLING_STANDING_DOC, AI_BILLING_SUBCOLLECTION, readAiOverageMonthLe
|
|
|
26
26
|
import { AI_OVERAGE_THRESHOLD_USD, aiOverageCardOnFile, aiOverageStandingCeilingUsd, aiOverageStep, readAiOverageStanding } from "../billing/ai-overage-standing.js";
|
|
27
27
|
import { FieldValue } from "firebase-admin/firestore";
|
|
28
28
|
import { invalidIdTokenResponse } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
29
|
+
import { addAdminAudit } from "@aglyn/tenant-data-admin/server/admin-audit-write";
|
|
29
30
|
// lockdown-423: exempt — self-serve billing surface, same posture as
|
|
30
31
|
// billing/storage-overage beside it. AGL-1501 keeps billing-locked sessions
|
|
31
32
|
// alive precisely so members can reach Billing, and this is the control that
|
|
@@ -232,7 +233,7 @@ async function handler(request) {
|
|
|
232
233
|
}, {
|
|
233
234
|
merge: true
|
|
234
235
|
});
|
|
235
|
-
await firebaseAdmin.app().firestore()
|
|
236
|
+
await addAdminAudit(firebaseAdmin.app().firestore(), {
|
|
236
237
|
actorUid: decoded.uid,
|
|
237
238
|
actorEmail: (_decoded_email2 = decoded.email) != null ? _decoded_email2 : null,
|
|
238
239
|
action: 'billing.assistOverage.setCap',
|
|
@@ -291,7 +292,7 @@ async function handler(request) {
|
|
|
291
292
|
}, {
|
|
292
293
|
merge: true
|
|
293
294
|
});
|
|
294
|
-
await firebaseAdmin.app().firestore()
|
|
295
|
+
await addAdminAudit(firebaseAdmin.app().firestore(), {
|
|
295
296
|
actorUid: decoded.uid,
|
|
296
297
|
actorEmail: (_decoded_email = decoded.email) != null ? _decoded_email : null,
|
|
297
298
|
action: 'billing.assistOverage.setHardCap',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/billing-overage.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { pluginRequestFromWeb } from '@aglyn/aglyn/server'\nimport {\n ASSIST_HARD_CAP_CONTROL_LABEL,\n ASSIST_OVERAGE_CAP_CONTROL_LABEL,\n ASSIST_OVERAGE_CAP_MAX_USD,\n ASSIST_OVERAGE_CAP_MIN_USD,\n resolveAssistCreditBudget,\n resolveAssistHardCap,\n resolveAssistOverageCapUsd,\n resolveAssistOverageRateUsdPer1k,\n} from '@aglyn/aglyn/app-utils/assist-credits'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n memberHasOrgPermission,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { logAiOverageControl } from '../activity/ai-activity'\nimport { assistMonthOverage } from '@aglyn/aglyn/app-utils/assist-credits'\nimport { assistUsageMonth } from '../usage/assist-usage'\nimport { aiOverageBillsByInvoice } from '../billing/ai-overage-cutover'\nimport { aiOverageNextChargeUsd } from '../billing/ai-overage-gate'\nimport {\n AI_BILLING_STANDING_DOC,\n AI_BILLING_SUBCOLLECTION,\n readAiOverageMonthLedger,\n} from '../billing/ai-overage-ledger'\nimport {\n AI_OVERAGE_THRESHOLD_USD,\n aiOverageCardOnFile,\n aiOverageEffectiveCeilingUsd,\n aiOverageStandingCeilingUsd,\n aiOverageStep,\n readAiOverageStanding,\n} from '../billing/ai-overage-standing'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\n\n// lockdown-423: exempt — self-serve billing surface, same posture as\n// billing/storage-overage beside it. AGL-1501 keeps billing-locked sessions\n// alive precisely so members can reach Billing, and this is the control that\n// stops an org accruing assist charges, so a 423 would trap it accruing them.\n\n/**\n * Read or set the org's own assist controls: the hard-cap switch (AGL-2653)\n * and the dollar ceiling on overage (AGL-2898).\n *\n * Credits past the plan's assist band are SOLD by default, at the rate\n * `resolveAssistOverageRateUsdPer1k` answers — the plan's listed\n * `extraAssistCreditsUsdPer1k`, or the add-on's rate on a plan that carries\n * its band only with the add-on. This route holds the two\n * controls a customer has over that, and nothing else — it is not a consent\n * surface, and there is nothing here an org must do before assist works.\n *\n * `get` → whether the switch is on, the ceiling if any, the band,\n * the rate the overage bills at, and whether the plan sells\n * overage at all\n * `setHardCap` → store `hardCap: true | false`\n * `setCap` → store `capUsd: number`, or `null` to clear the ceiling\n *\n * `billing.manage`-gated, because both controls bound spend in both\n * directions: lifting either is what lets the org be invoiced further past\n * its band, and setting either is what stops a workspace's assistant.\n * Admin-SDK-only by construction — `assistOverage` is denied to every client\n * in the rules, so a member cannot lift their own ceiling or lower somebody\n * else's.\n *\n * TURNING THE SWITCH OFF AND CLEARING THE CEILING ARE ALWAYS AVAILABLE, on\n * any plan and at any usage level: an org that wants the sale back on must\n * not have to argue with a precondition. Turning the switch on, or setting a\n * ceiling, is refused, 409, on a plan that sells no overage — there the band\n * is already the wall (`assistBandRefuses`), and storing a control that\n * changes nothing would read as protection the org does not have.\n */\n/**\n * What the overage card needs to say about Aglyn's own limit (AGL-3011).\n *\n * Every figure is `null` before the cutover month. That is not a missing\n * value: it is the honest answer while overage still bills on the monthly\n * invoice, and rendering a ceiling and a \"next charge\" figure that nothing\n * enforces would be a promise the platform is not yet keeping.\n */\nasync function readAiOverageStandingForCard(\n orgRef: FirebaseFirestore.DocumentReference,\n org: Parameters<typeof aiOverageEffectiveCeilingUsd>[0],\n): Promise<{\n aglynCeilingUsd: number | null\n overageStep: number | null\n overageThresholdUsd: number | null\n overageChargedUsd: number | null\n overageChargeCount: number | null\n overageNextChargeUsd: number | null\n overagePaused: string | null\n overageCardOnFile: boolean | null\n overageBillsByInvoice: boolean\n}> {\n const now = new Date()\n const month = assistUsageMonth(now)\n const empty = {\n aglynCeilingUsd: null,\n overageStep: null,\n overageThresholdUsd: null,\n overageChargedUsd: null,\n overageChargeCount: null,\n overageNextChargeUsd: null,\n overagePaused: null,\n overageCardOnFile: null,\n overageBillsByInvoice: false,\n }\n if (!aiOverageBillsByInvoice(month)) return empty\n const [standingSnapshot, usageSnapshot] = await Promise.all([\n orgRef.collection(AI_BILLING_SUBCOLLECTION).doc(AI_BILLING_STANDING_DOC).get(),\n orgRef.collection('assistUsage').doc(month).get(),\n ])\n const standing = readAiOverageStanding(\n standingSnapshot.exists ? (standingSnapshot.data() ?? null) : null,\n )\n const ledger = readAiOverageMonthLedger(\n usageSnapshot.exists ? (usageSnapshot.data() ?? null) : null,\n )\n const priced = assistMonthOverage(org, Number(usageSnapshot.get('estCostUsd') ?? 0))\n return {\n aglynCeilingUsd: aiOverageStandingCeilingUsd(standing, now),\n overageStep: aiOverageStep(standing),\n overageThresholdUsd: AI_OVERAGE_THRESHOLD_USD,\n overageChargedUsd: ledger.paidUsd,\n overageChargeCount: ledger.charges,\n overageNextChargeUsd: aiOverageNextChargeUsd(\n priced.overageMonthlyUsd,\n ledger.invoicedUsd,\n ),\n overagePaused: standing.pause?.reason ?? null,\n overageCardOnFile: aiOverageCardOnFile(standing) ?? null,\n overageBillsByInvoice: true,\n }\n}\n\nasync function handler(request: Request): Promise<Response> {\n const { method, body, headers: rawHeaders } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'POST') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n const orgId = String(body?.orgId ?? '')\n const action = String(body?.action ?? '')\n if (!orgId || !['get', 'setHardCap', 'setCap'].includes(action)) {\n return Response.json({ error: 'Bad request' }, { status: 400 })\n }\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const isStaff = decoded['staff'] === true\n const actor = await resolveOrgMembership(decoded.uid, orgId)\n if (\n !isStaff &&\n !(await memberHasOrgPermission(orgId, actor?.member, 'billing.manage'))\n ) {\n return Response.json({ error: 'billing.manage required' }, { status: 403 })\n }\n\n const orgRef = firebaseAdmin.app().firestore().collection('orgs').doc(orgId)\n const orgSnapshot = await orgRef.get()\n if (!orgSnapshot.exists) {\n return Response.json({ error: 'Unknown organization' }, { status: 404 })\n }\n const org = (orgSnapshot.data() ?? {}) as Record<string, unknown>\n const hardCap = resolveAssistHardCap(org as never)\n const capUsd = resolveAssistOverageCapUsd(org as never)\n const bandCredits = resolveAssistCreditBudget(org as never)\n // The rate the card quotes must be the rate the rollup bills, so it is\n // read through the one resolver every reader of the rate goes through.\n // That mattered acutely under AGL-3014, when Starter's rate lived off\n // `PLAN_PRICING` and reading the table directly quoted $0.00 on an org\n // that was really being invoiced. AGL-3203 put the rate on Starter's own\n // row and deleted the exception, so the table and the resolver now agree\n // — and this still reads the resolver, because a comp is priced at\n // nothing and only `resolvePlanPricing` knows that.\n const overageRateUsdPer1k = resolveAssistOverageRateUsdPer1k(org as never)\n const sellsOverage = bandCredits !== null && overageRateUsdPer1k !== null\n\n if (action === 'get') {\n // AGLYN'S OWN LIMIT BESIDE THE WORKSPACE'S (AGL-3011). The card shows\n // both because the LOWER of the two binds, and a card that showed only\n // the figure the workspace set would explain neither the refusal it\n // gets at Aglyn's nor the charge it sees before its own is reached.\n //\n // `null` for every figure while the platform still bills overage on\n // the monthly invoice, so the card keeps saying what is true today\n // rather than describing a mechanism that is not running.\n const overage = await readAiOverageStandingForCard(orgRef, org as never)\n return Response.json(\n {\n hardCap,\n capUsd,\n bandCredits,\n overageRateUsdPer1k,\n sellsOverage,\n ...overage,\n label: ASSIST_HARD_CAP_CONTROL_LABEL,\n capLabel: ASSIST_OVERAGE_CAP_CONTROL_LABEL,\n minCapUsd: ASSIST_OVERAGE_CAP_MIN_USD,\n maxCapUsd: ASSIST_OVERAGE_CAP_MAX_USD,\n },\n { status: 200 },\n )\n }\n\n if (action === 'setCap') {\n const raw = body?.capUsd\n // `null` clears; anything else must be a real number in a real range.\n // A string \"25\" is refused rather than coerced, the way the switch\n // refuses a string \"false\": the resolver reads the field strictly, so\n // a value that reached the document as a string would be no ceiling\n // at all while the card said one was set.\n const clearing = raw === null\n if (\n !clearing &&\n (typeof raw !== 'number' ||\n !Number.isFinite(raw) ||\n raw < ASSIST_OVERAGE_CAP_MIN_USD ||\n raw > ASSIST_OVERAGE_CAP_MAX_USD)\n ) {\n return Response.json(\n {\n error:\n `Set a monthly AI overage ceiling between ` +\n `$${ASSIST_OVERAGE_CAP_MIN_USD} and ` +\n `$${ASSIST_OVERAGE_CAP_MAX_USD.toLocaleString('en-US')}, ` +\n `or clear it.`,\n code: 'invalid_cap',\n },\n { status: 400 },\n )\n }\n if (!clearing && !sellsOverage) {\n return Response.json(\n {\n error:\n bandCredits === null\n ? 'Your plan includes no AI assist credits, so there is no ' +\n 'overage to cap. Upgrade in Billing to add AI assist.'\n : 'Your plan already stops AI assist at its included band — ' +\n 'it sells no credits past it, so there is no overage to cap.',\n code: 'not_sold',\n },\n { status: 409 },\n )\n }\n const requestedCap = clearing ? null : (raw as number)\n await orgRef.set(\n {\n assistOverage: {\n capUsd: requestedCap,\n capSetAt: FieldValue.serverTimestamp(),\n capSetBy: decoded.uid,\n },\n },\n { merge: true },\n )\n await firebaseAdmin\n .app()\n .firestore()\n .collection('adminAudit')\n .add({\n actorUid: decoded.uid,\n actorEmail: decoded.email ?? null,\n action: 'billing.assistOverage.setCap',\n target: `orgs/${orgId}`,\n before: { capUsd },\n after: { capUsd: requestedCap },\n at: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n // The customer's own feed (AGL-2929): `adminAudit` is staff-only, and\n // the workspace should see who set its ceiling. Nothing when the value\n // did not move.\n await logAiOverageControl(\n orgId,\n { uid: decoded.uid, email: decoded.email ?? null },\n { control: 'cap', before: capUsd, after: requestedCap },\n )\n return Response.json({ ok: true, capUsd: requestedCap }, { status: 200 })\n }\n\n // Strictly a boolean, the same way the resolver reads it strictly: a\n // string \"false\" arriving here would otherwise be stored as `true`.\n if (typeof body?.hardCap !== 'boolean') {\n return Response.json(\n { error: 'hardCap must be true or false', code: 'invalid_value' },\n { status: 400 },\n )\n }\n const requested = body.hardCap as boolean\n\n if (requested && !sellsOverage) {\n return Response.json(\n {\n error:\n bandCredits === null\n ? 'Your plan includes no AI assist credits, so there is no band ' +\n 'to stop at. Upgrade in Billing to add AI assist.'\n : 'Your plan already stops AI assist at its included band — ' +\n 'it sells no credits past it, so there is nothing to switch off.',\n code: 'not_sold',\n },\n { status: 409 },\n )\n }\n\n await orgRef.set(\n {\n assistOverage: {\n hardCap: requested,\n hardCapSetAt: FieldValue.serverTimestamp(),\n hardCapSetBy: decoded.uid,\n },\n },\n { merge: true },\n )\n await firebaseAdmin\n .app()\n .firestore()\n .collection('adminAudit')\n .add({\n actorUid: decoded.uid,\n actorEmail: decoded.email ?? null,\n action: 'billing.assistOverage.setHardCap',\n target: `orgs/${orgId}`,\n before: { hardCap },\n after: { hardCap: requested },\n at: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n await logAiOverageControl(\n orgId,\n { uid: decoded.uid, email: decoded.email ?? null },\n { control: 'hardCap', before: hardCap, after: requested },\n )\n\n return Response.json({ ok: true, hardCap: requested }, { status: 200 })\n } catch (error) {\n // A refused credential is a 401, not a fault of ours (AGL-1993). Null\n // for anything else, so a real failure keeps the answer below.\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error(error)\n return Response.json(\n { error: 'Assist overage update failed' },\n { status: 500 },\n )\n }\n}\n\nexport const dynamic = 'force-dynamic'\nexport { handler as POST }\n"],"names":["pluginRequestFromWeb","ASSIST_HARD_CAP_CONTROL_LABEL","ASSIST_OVERAGE_CAP_CONTROL_LABEL","ASSIST_OVERAGE_CAP_MAX_USD","ASSIST_OVERAGE_CAP_MIN_USD","resolveAssistCreditBudget","resolveAssistHardCap","resolveAssistOverageCapUsd","resolveAssistOverageRateUsdPer1k","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","memberHasOrgPermission","resolveOrgMembership","logAiOverageControl","assistMonthOverage","assistUsageMonth","aiOverageBillsByInvoice","aiOverageNextChargeUsd","AI_BILLING_STANDING_DOC","AI_BILLING_SUBCOLLECTION","readAiOverageMonthLedger","AI_OVERAGE_THRESHOLD_USD","aiOverageCardOnFile","aiOverageStandingCeilingUsd","aiOverageStep","readAiOverageStanding","FieldValue","invalidIdTokenResponse","readAiOverageStandingForCard","orgRef","org","standingSnapshot","usageSnapshot","standing","now","Date","month","empty","aglynCeilingUsd","overageStep","overageThresholdUsd","overageChargedUsd","overageChargeCount","overageNextChargeUsd","overagePaused","overageCardOnFile","overageBillsByInvoice","Promise","all","collection","doc","get","exists","data","ledger","priced","Number","paidUsd","charges","overageMonthlyUsd","invoicedUsd","pause","reason","handler","request","headers","method","body","rawHeaders","Response","json","error","status","authorization","idToken","startsWith","slice","length","undefined","orgId","String","action","includes","orgSnapshot","decoded","app","auth","verifyIdToken","email_verified","isStaff","actor","uid","member","firestore","hardCap","capUsd","bandCredits","overageRateUsdPer1k","sellsOverage","overage","label","capLabel","minCapUsd","maxCapUsd","raw","clearing","isFinite","toLocaleString","code","requestedCap","set","assistOverage","capSetAt","serverTimestamp","capSetBy","merge","add","actorUid","actorEmail","email","target","before","after","at","catch","control","ok","requested","hardCapSetAt","hardCapSetBy","unauthenticated","console","dynamic","POST"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/billing-overage.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { pluginRequestFromWeb } from '@aglyn/aglyn/server'\nimport {\n ASSIST_HARD_CAP_CONTROL_LABEL,\n ASSIST_OVERAGE_CAP_CONTROL_LABEL,\n ASSIST_OVERAGE_CAP_MAX_USD,\n ASSIST_OVERAGE_CAP_MIN_USD,\n resolveAssistCreditBudget,\n resolveAssistHardCap,\n resolveAssistOverageCapUsd,\n resolveAssistOverageRateUsdPer1k,\n} from '@aglyn/aglyn/app-utils/assist-credits'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n memberHasOrgPermission,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { logAiOverageControl } from '../activity/ai-activity'\nimport { assistMonthOverage } from '@aglyn/aglyn/app-utils/assist-credits'\nimport { assistUsageMonth } from '../usage/assist-usage'\nimport { aiOverageBillsByInvoice } from '../billing/ai-overage-cutover'\nimport { aiOverageNextChargeUsd } from '../billing/ai-overage-gate'\nimport {\n AI_BILLING_STANDING_DOC,\n AI_BILLING_SUBCOLLECTION,\n readAiOverageMonthLedger,\n} from '../billing/ai-overage-ledger'\nimport {\n AI_OVERAGE_THRESHOLD_USD,\n aiOverageCardOnFile,\n aiOverageEffectiveCeilingUsd,\n aiOverageStandingCeilingUsd,\n aiOverageStep,\n readAiOverageStanding,\n} from '../billing/ai-overage-standing'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { addAdminAudit } from '@aglyn/tenant-data-admin/server/admin-audit-write'\n\n// lockdown-423: exempt — self-serve billing surface, same posture as\n// billing/storage-overage beside it. AGL-1501 keeps billing-locked sessions\n// alive precisely so members can reach Billing, and this is the control that\n// stops an org accruing assist charges, so a 423 would trap it accruing them.\n\n/**\n * Read or set the org's own assist controls: the hard-cap switch (AGL-2653)\n * and the dollar ceiling on overage (AGL-2898).\n *\n * Credits past the plan's assist band are SOLD by default, at the rate\n * `resolveAssistOverageRateUsdPer1k` answers — the plan's listed\n * `extraAssistCreditsUsdPer1k`, or the add-on's rate on a plan that carries\n * its band only with the add-on. This route holds the two\n * controls a customer has over that, and nothing else — it is not a consent\n * surface, and there is nothing here an org must do before assist works.\n *\n * `get` → whether the switch is on, the ceiling if any, the band,\n * the rate the overage bills at, and whether the plan sells\n * overage at all\n * `setHardCap` → store `hardCap: true | false`\n * `setCap` → store `capUsd: number`, or `null` to clear the ceiling\n *\n * `billing.manage`-gated, because both controls bound spend in both\n * directions: lifting either is what lets the org be invoiced further past\n * its band, and setting either is what stops a workspace's assistant.\n * Admin-SDK-only by construction — `assistOverage` is denied to every client\n * in the rules, so a member cannot lift their own ceiling or lower somebody\n * else's.\n *\n * TURNING THE SWITCH OFF AND CLEARING THE CEILING ARE ALWAYS AVAILABLE, on\n * any plan and at any usage level: an org that wants the sale back on must\n * not have to argue with a precondition. Turning the switch on, or setting a\n * ceiling, is refused, 409, on a plan that sells no overage — there the band\n * is already the wall (`assistBandRefuses`), and storing a control that\n * changes nothing would read as protection the org does not have.\n */\n/**\n * What the overage card needs to say about Aglyn's own limit (AGL-3011).\n *\n * Every figure is `null` before the cutover month. That is not a missing\n * value: it is the honest answer while overage still bills on the monthly\n * invoice, and rendering a ceiling and a \"next charge\" figure that nothing\n * enforces would be a promise the platform is not yet keeping.\n */\nasync function readAiOverageStandingForCard(\n orgRef: FirebaseFirestore.DocumentReference,\n org: Parameters<typeof aiOverageEffectiveCeilingUsd>[0],\n): Promise<{\n aglynCeilingUsd: number | null\n overageStep: number | null\n overageThresholdUsd: number | null\n overageChargedUsd: number | null\n overageChargeCount: number | null\n overageNextChargeUsd: number | null\n overagePaused: string | null\n overageCardOnFile: boolean | null\n overageBillsByInvoice: boolean\n}> {\n const now = new Date()\n const month = assistUsageMonth(now)\n const empty = {\n aglynCeilingUsd: null,\n overageStep: null,\n overageThresholdUsd: null,\n overageChargedUsd: null,\n overageChargeCount: null,\n overageNextChargeUsd: null,\n overagePaused: null,\n overageCardOnFile: null,\n overageBillsByInvoice: false,\n }\n if (!aiOverageBillsByInvoice(month)) return empty\n const [standingSnapshot, usageSnapshot] = await Promise.all([\n orgRef.collection(AI_BILLING_SUBCOLLECTION).doc(AI_BILLING_STANDING_DOC).get(),\n orgRef.collection('assistUsage').doc(month).get(),\n ])\n const standing = readAiOverageStanding(\n standingSnapshot.exists ? (standingSnapshot.data() ?? null) : null,\n )\n const ledger = readAiOverageMonthLedger(\n usageSnapshot.exists ? (usageSnapshot.data() ?? null) : null,\n )\n const priced = assistMonthOverage(org, Number(usageSnapshot.get('estCostUsd') ?? 0))\n return {\n aglynCeilingUsd: aiOverageStandingCeilingUsd(standing, now),\n overageStep: aiOverageStep(standing),\n overageThresholdUsd: AI_OVERAGE_THRESHOLD_USD,\n overageChargedUsd: ledger.paidUsd,\n overageChargeCount: ledger.charges,\n overageNextChargeUsd: aiOverageNextChargeUsd(\n priced.overageMonthlyUsd,\n ledger.invoicedUsd,\n ),\n overagePaused: standing.pause?.reason ?? null,\n overageCardOnFile: aiOverageCardOnFile(standing) ?? null,\n overageBillsByInvoice: true,\n }\n}\n\nasync function handler(request: Request): Promise<Response> {\n const { method, body, headers: rawHeaders } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'POST') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n const orgId = String(body?.orgId ?? '')\n const action = String(body?.action ?? '')\n if (!orgId || !['get', 'setHardCap', 'setCap'].includes(action)) {\n return Response.json({ error: 'Bad request' }, { status: 400 })\n }\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const isStaff = decoded['staff'] === true\n const actor = await resolveOrgMembership(decoded.uid, orgId)\n if (\n !isStaff &&\n !(await memberHasOrgPermission(orgId, actor?.member, 'billing.manage'))\n ) {\n return Response.json({ error: 'billing.manage required' }, { status: 403 })\n }\n\n const orgRef = firebaseAdmin.app().firestore().collection('orgs').doc(orgId)\n const orgSnapshot = await orgRef.get()\n if (!orgSnapshot.exists) {\n return Response.json({ error: 'Unknown organization' }, { status: 404 })\n }\n const org = (orgSnapshot.data() ?? {}) as Record<string, unknown>\n const hardCap = resolveAssistHardCap(org as never)\n const capUsd = resolveAssistOverageCapUsd(org as never)\n const bandCredits = resolveAssistCreditBudget(org as never)\n // The rate the card quotes must be the rate the rollup bills, so it is\n // read through the one resolver every reader of the rate goes through.\n // That mattered acutely under AGL-3014, when Starter's rate lived off\n // `PLAN_PRICING` and reading the table directly quoted $0.00 on an org\n // that was really being invoiced. AGL-3203 put the rate on Starter's own\n // row and deleted the exception, so the table and the resolver now agree\n // — and this still reads the resolver, because a comp is priced at\n // nothing and only `resolvePlanPricing` knows that.\n const overageRateUsdPer1k = resolveAssistOverageRateUsdPer1k(org as never)\n const sellsOverage = bandCredits !== null && overageRateUsdPer1k !== null\n\n if (action === 'get') {\n // AGLYN'S OWN LIMIT BESIDE THE WORKSPACE'S (AGL-3011). The card shows\n // both because the LOWER of the two binds, and a card that showed only\n // the figure the workspace set would explain neither the refusal it\n // gets at Aglyn's nor the charge it sees before its own is reached.\n //\n // `null` for every figure while the platform still bills overage on\n // the monthly invoice, so the card keeps saying what is true today\n // rather than describing a mechanism that is not running.\n const overage = await readAiOverageStandingForCard(orgRef, org as never)\n return Response.json(\n {\n hardCap,\n capUsd,\n bandCredits,\n overageRateUsdPer1k,\n sellsOverage,\n ...overage,\n label: ASSIST_HARD_CAP_CONTROL_LABEL,\n capLabel: ASSIST_OVERAGE_CAP_CONTROL_LABEL,\n minCapUsd: ASSIST_OVERAGE_CAP_MIN_USD,\n maxCapUsd: ASSIST_OVERAGE_CAP_MAX_USD,\n },\n { status: 200 },\n )\n }\n\n if (action === 'setCap') {\n const raw = body?.capUsd\n // `null` clears; anything else must be a real number in a real range.\n // A string \"25\" is refused rather than coerced, the way the switch\n // refuses a string \"false\": the resolver reads the field strictly, so\n // a value that reached the document as a string would be no ceiling\n // at all while the card said one was set.\n const clearing = raw === null\n if (\n !clearing &&\n (typeof raw !== 'number' ||\n !Number.isFinite(raw) ||\n raw < ASSIST_OVERAGE_CAP_MIN_USD ||\n raw > ASSIST_OVERAGE_CAP_MAX_USD)\n ) {\n return Response.json(\n {\n error:\n `Set a monthly AI overage ceiling between ` +\n `$${ASSIST_OVERAGE_CAP_MIN_USD} and ` +\n `$${ASSIST_OVERAGE_CAP_MAX_USD.toLocaleString('en-US')}, ` +\n `or clear it.`,\n code: 'invalid_cap',\n },\n { status: 400 },\n )\n }\n if (!clearing && !sellsOverage) {\n return Response.json(\n {\n error:\n bandCredits === null\n ? 'Your plan includes no AI assist credits, so there is no ' +\n 'overage to cap. Upgrade in Billing to add AI assist.'\n : 'Your plan already stops AI assist at its included band — ' +\n 'it sells no credits past it, so there is no overage to cap.',\n code: 'not_sold',\n },\n { status: 409 },\n )\n }\n const requestedCap = clearing ? null : (raw as number)\n await orgRef.set(\n {\n assistOverage: {\n capUsd: requestedCap,\n capSetAt: FieldValue.serverTimestamp(),\n capSetBy: decoded.uid,\n },\n },\n { merge: true },\n )\n await addAdminAudit(firebaseAdmin.app().firestore(), {\n actorUid: decoded.uid,\n actorEmail: decoded.email ?? null,\n action: 'billing.assistOverage.setCap',\n target: `orgs/${orgId}`,\n before: { capUsd },\n after: { capUsd: requestedCap },\n at: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n // The customer's own feed (AGL-2929): `adminAudit` is staff-only, and\n // the workspace should see who set its ceiling. Nothing when the value\n // did not move.\n await logAiOverageControl(\n orgId,\n { uid: decoded.uid, email: decoded.email ?? null },\n { control: 'cap', before: capUsd, after: requestedCap },\n )\n return Response.json({ ok: true, capUsd: requestedCap }, { status: 200 })\n }\n\n // Strictly a boolean, the same way the resolver reads it strictly: a\n // string \"false\" arriving here would otherwise be stored as `true`.\n if (typeof body?.hardCap !== 'boolean') {\n return Response.json(\n { error: 'hardCap must be true or false', code: 'invalid_value' },\n { status: 400 },\n )\n }\n const requested = body.hardCap as boolean\n\n if (requested && !sellsOverage) {\n return Response.json(\n {\n error:\n bandCredits === null\n ? 'Your plan includes no AI assist credits, so there is no band ' +\n 'to stop at. Upgrade in Billing to add AI assist.'\n : 'Your plan already stops AI assist at its included band — ' +\n 'it sells no credits past it, so there is nothing to switch off.',\n code: 'not_sold',\n },\n { status: 409 },\n )\n }\n\n await orgRef.set(\n {\n assistOverage: {\n hardCap: requested,\n hardCapSetAt: FieldValue.serverTimestamp(),\n hardCapSetBy: decoded.uid,\n },\n },\n { merge: true },\n )\n await addAdminAudit(firebaseAdmin.app().firestore(), {\n actorUid: decoded.uid,\n actorEmail: decoded.email ?? null,\n action: 'billing.assistOverage.setHardCap',\n target: `orgs/${orgId}`,\n before: { hardCap },\n after: { hardCap: requested },\n at: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n await logAiOverageControl(\n orgId,\n { uid: decoded.uid, email: decoded.email ?? null },\n { control: 'hardCap', before: hardCap, after: requested },\n )\n\n return Response.json({ ok: true, hardCap: requested }, { status: 200 })\n } catch (error) {\n // A refused credential is a 401, not a fault of ours (AGL-1993). Null\n // for anything else, so a real failure keeps the answer below.\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error(error)\n return Response.json(\n { error: 'Assist overage update failed' },\n { status: 500 },\n )\n }\n}\n\nexport const dynamic = 'force-dynamic'\nexport { handler as POST }\n"],"names":["pluginRequestFromWeb","ASSIST_HARD_CAP_CONTROL_LABEL","ASSIST_OVERAGE_CAP_CONTROL_LABEL","ASSIST_OVERAGE_CAP_MAX_USD","ASSIST_OVERAGE_CAP_MIN_USD","resolveAssistCreditBudget","resolveAssistHardCap","resolveAssistOverageCapUsd","resolveAssistOverageRateUsdPer1k","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","memberHasOrgPermission","resolveOrgMembership","logAiOverageControl","assistMonthOverage","assistUsageMonth","aiOverageBillsByInvoice","aiOverageNextChargeUsd","AI_BILLING_STANDING_DOC","AI_BILLING_SUBCOLLECTION","readAiOverageMonthLedger","AI_OVERAGE_THRESHOLD_USD","aiOverageCardOnFile","aiOverageStandingCeilingUsd","aiOverageStep","readAiOverageStanding","FieldValue","invalidIdTokenResponse","addAdminAudit","readAiOverageStandingForCard","orgRef","org","standingSnapshot","usageSnapshot","standing","now","Date","month","empty","aglynCeilingUsd","overageStep","overageThresholdUsd","overageChargedUsd","overageChargeCount","overageNextChargeUsd","overagePaused","overageCardOnFile","overageBillsByInvoice","Promise","all","collection","doc","get","exists","data","ledger","priced","Number","paidUsd","charges","overageMonthlyUsd","invoicedUsd","pause","reason","handler","request","headers","method","body","rawHeaders","Response","json","error","status","authorization","idToken","startsWith","slice","length","undefined","orgId","String","action","includes","orgSnapshot","decoded","app","auth","verifyIdToken","email_verified","isStaff","actor","uid","member","firestore","hardCap","capUsd","bandCredits","overageRateUsdPer1k","sellsOverage","overage","label","capLabel","minCapUsd","maxCapUsd","raw","clearing","isFinite","toLocaleString","code","requestedCap","set","assistOverage","capSetAt","serverTimestamp","capSetBy","merge","actorUid","actorEmail","email","target","before","after","at","catch","control","ok","requested","hardCapSetAt","hardCapSetBy","unauthenticated","console","dynamic","POST"],"mappings":"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"}
|
|
@@ -18,6 +18,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
18
18
|
import { assistUsdFromCredits } from "@aglyn/aglyn/app-utils/assist-credits";
|
|
19
19
|
import { FREE_AI_TASTE_CREDITS_PER_MONTH, resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
20
20
|
import { sendStaffAlertEmail } from "@aglyn/tenant-data-admin/server/staff-alert-email";
|
|
21
|
+
import { addAdminAudit } from "@aglyn/tenant-data-admin/server/admin-audit-write";
|
|
21
22
|
/**
|
|
22
23
|
* The Free AI taste's precautions (AGL-2925) — the half of the meter that
|
|
23
24
|
* exists only because the Free band has no invoice behind it.
|
|
@@ -267,7 +268,7 @@ export function platformFreeSpendRef(firestore, day) {
|
|
|
267
268
|
if (!crossed.alerted && !crossed.paused) return crossed;
|
|
268
269
|
const figure = `$${crossed.estCostUsd.toFixed(2)} of the $${ceilingUsd.toFixed(2)} ceiling`;
|
|
269
270
|
if (crossed.paused) {
|
|
270
|
-
await firestore
|
|
271
|
+
await addAdminAudit(firestore, {
|
|
271
272
|
actorUid: 'system:ai-free-spend',
|
|
272
273
|
action: 'platform.aiFreeSpend.paused',
|
|
273
274
|
target: `${PLATFORM_AI_FREE_SPEND_COLLECTION}/${day}`,
|