@aglyn/plugins-ai 1.0.0-beta.284 → 1.0.0-beta.287
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/assets/site-styles/nonprofit.webp +0 -0
- package/src/lib/assets/site-styles/professional.webp +0 -0
- package/src/lib/assets/site-styles/store.webp +0 -0
- package/src/lib/assets/site-styles/trades.webp +0 -0
- package/src/lib/components/ai-theme-proposal-card.component.js +12 -3
- package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
- package/src/lib/declarations.js +5 -2
- package/src/lib/declarations.js.map +1 -1
- package/src/lib/declarations.server.js +12 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.d.ts +3 -1
- package/src/lib/jobs/ai-build-capabilities.js +27 -2
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
- package/src/lib/jobs/ai-image-reads.d.ts +59 -0
- package/src/lib/jobs/ai-image-reads.js +138 -0
- package/src/lib/jobs/ai-image-reads.js.map +1 -0
- package/src/lib/jobs/ai-job-build-step.d.ts +29 -0
- package/src/lib/jobs/ai-job-build-step.js +83 -12
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-duplicate-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-duplicate-step.js +6 -6
- package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -1
- package/src/lib/jobs/ai-job-import-step.d.ts +4 -2
- package/src/lib/jobs/ai-job-import-step.js +15 -11
- package/src/lib/jobs/ai-job-import-step.js.map +1 -1
- package/src/lib/jobs/ai-job-insight-step.d.ts +10 -0
- package/src/lib/jobs/ai-job-insight-step.js +23 -5
- package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.js +2 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-media-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-media-step.js +6 -8
- package/src/lib/jobs/ai-job-media-step.js.map +1 -1
- package/src/lib/jobs/ai-job-navigation-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-navigation-step.js +7 -9
- package/src/lib/jobs/ai-job-navigation-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.js +7 -1
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.js +16 -3
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +9 -1
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +30 -8
- package/src/lib/jobs/ai-job-plan-step.js +78 -13
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-privacy-step.d.ts +4 -2
- package/src/lib/jobs/ai-job-privacy-step.js +14 -15
- package/src/lib/jobs/ai-job-privacy-step.js.map +1 -1
- package/src/lib/jobs/ai-job-products-step.js +5 -3
- package/src/lib/jobs/ai-job-products-step.js.map +1 -1
- package/src/lib/jobs/ai-job-settings-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-settings-step.js +7 -8
- package/src/lib/jobs/ai-job-settings-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +34 -1
- package/src/lib/jobs/ai-job-site-content.js +67 -8
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +53 -3
- package/src/lib/jobs/ai-job-site-datasets.js +298 -30
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.js +6 -3
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-store-pages.js +3 -3
- package/src/lib/jobs/ai-job-site-store-pages.js.map +1 -1
- package/src/lib/jobs/ai-job-tracking-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-tracking-step.js +7 -8
- package/src/lib/jobs/ai-job-tracking-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs-beat.d.ts +11 -7
- package/src/lib/jobs/ai-jobs-beat.js +18 -8
- package/src/lib/jobs/ai-jobs-beat.js.map +1 -1
- package/src/lib/jobs/ai-jobs-notify.d.ts +66 -1
- package/src/lib/jobs/ai-jobs-notify.js +136 -3
- package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +59 -4
- package/src/lib/jobs/ai-layout-stock-photos.js +897 -81
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-product-image.d.ts +14 -0
- package/src/lib/jobs/ai-product-image.js +73 -21
- package/src/lib/jobs/ai-product-image.js.map +1 -1
- package/src/lib/jobs/ai-theme-brand-inputs.d.ts +9 -1
- package/src/lib/jobs/ai-theme-brand-inputs.js +28 -2
- package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.d.ts +175 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js +601 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-pages.d.ts +46 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js +2862 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-plans.d.ts +74 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js +888 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.d.ts +29 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js +81 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.d.ts +87 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js +225 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +11 -0
- package/src/lib/layout-language/ai-layout-compiler.js +194 -18
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-copy.d.ts +37 -0
- package/src/lib/layout-language/ai-layout-copy.js +383 -3
- package/src/lib/layout-language/ai-layout-copy.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +19 -0
- package/src/lib/layout-language/ai-layout-design.js +31 -11
- package/src/lib/layout-language/ai-layout-design.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +7 -1
- package/src/lib/layout-language/ai-layout-listings.js +9 -1
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +30 -0
- package/src/lib/layout-language/ai-layout-pictures.js +131 -3
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/layout-language/ai-layout-storefront.d.ts +18 -4
- package/src/lib/layout-language/ai-layout-storefront.js +100 -20
- package/src/lib/layout-language/ai-layout-storefront.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +7 -0
- package/src/lib/model/ai-build-job.js +36 -7
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +28 -4
- package/src/lib/model/ai-build-plan.js +49 -9
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-build-progress.js +3 -1
- package/src/lib/model/ai-build-progress.js.map +1 -1
- package/src/lib/model/ai-job-notice.d.ts +8 -0
- package/src/lib/model/ai-job-notice.js +33 -0
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-job-plan-lines.d.ts +2 -1
- package/src/lib/model/ai-job-plan-lines.js +6 -1
- package/src/lib/model/ai-job-plan-lines.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +8 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-permissions.d.ts +13 -0
- package/src/lib/model/ai-permissions.js +29 -1
- package/src/lib/model/ai-permissions.js.map +1 -1
- package/src/lib/model/ai-products.d.ts +18 -0
- package/src/lib/model/ai-products.js +32 -0
- package/src/lib/model/ai-products.js.map +1 -1
- package/src/lib/model/ai-site-inspiration.d.ts +3 -2
- package/src/lib/model/ai-site-inspiration.js +3 -2
- package/src/lib/model/ai-site-inspiration.js.map +1 -1
- package/src/lib/plugin.js +4 -13
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/providers/anthropic.js +9 -2
- package/src/lib/providers/anthropic.js.map +1 -1
- package/src/lib/providers/catalog.js +14 -5
- package/src/lib/providers/catalog.js.map +1 -1
- package/src/lib/providers/contract.d.ts +24 -1
- package/src/lib/providers/contract.js.map +1 -1
- package/src/lib/providers/routing.js +5 -2
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/runtime/ai-dataset-generation.d.ts +99 -1
- package/src/lib/runtime/ai-dataset-generation.js +251 -3
- package/src/lib/runtime/ai-dataset-generation.js.map +1 -1
- package/src/lib/runtime/ai-dev-replay.d.ts +1 -0
- package/src/lib/runtime/ai-dev-replay.js +4 -2
- package/src/lib/runtime/ai-dev-replay.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +47 -3
- package/src/lib/runtime/ai-doctrine-validators.js +343 -27
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +8 -2
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-icon-library.d.ts +27 -1
- package/src/lib/runtime/ai-icon-library.js +30 -3
- package/src/lib/runtime/ai-icon-library.js.map +1 -1
- package/src/lib/runtime/ai-products-generation.d.ts +11 -1
- package/src/lib/runtime/ai-products-generation.js +51 -4
- package/src/lib/runtime/ai-products-generation.js.map +1 -1
- package/src/lib/runtime/assist-answer-cache.d.ts +2 -0
- package/src/lib/runtime/assist-answer-cache.js +5 -2
- package/src/lib/runtime/assist-answer-cache.js.map +1 -1
- package/src/lib/server/ai-allotments.js +7 -2
- package/src/lib/server/ai-allotments.js.map +1 -1
- package/src/lib/server/ai-experiment-versions.js +5 -5
- package/src/lib/server/ai-experiment-versions.js.map +1 -1
- package/src/lib/server/ai-host-permissions-roster.d.ts +25 -0
- package/src/lib/server/ai-host-permissions-roster.js +33 -0
- package/src/lib/server/ai-host-permissions-roster.js.map +1 -0
- package/src/lib/server/ai-host-permissions.js +8 -6
- package/src/lib/server/ai-host-permissions.js.map +1 -1
- package/src/lib/server/ai-image-reads-route.d.ts +17 -0
- package/src/lib/server/ai-image-reads-route.js +106 -0
- package/src/lib/server/ai-image-reads-route.js.map +1 -0
- package/src/lib/server/ai-seo-apply.js +5 -5
- package/src/lib/server/ai-seo-apply.js.map +1 -1
- package/src/lib/server/ai-site-import-route.js +7 -4
- package/src/lib/server/ai-site-import-route.js.map +1 -1
- package/src/lib/server/assist-build.js +1 -0
- package/src/lib/server/assist-build.js.map +1 -1
- package/src/lib/server/assist-chat.d.ts +11 -7
- package/src/lib/server/assist-chat.js +185 -48
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-docs-query.d.ts +51 -0
- package/src/lib/server/assist-docs-query.js +57 -0
- package/src/lib/server/assist-docs-query.js.map +1 -0
- package/src/lib/server/assist-jobs-context.d.ts +102 -0
- package/src/lib/server/assist-jobs-context.js +227 -0
- package/src/lib/server/assist-jobs-context.js.map +1 -0
- package/src/lib/server/assist-view-context.d.ts +8 -4
- package/src/lib/server/assist-view-context.js +12 -7
- package/src/lib/server/assist-view-context.js.map +1 -1
- package/src/lib/server.js +12 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/tools/ai-dataset-tool.d.ts +11 -1
- package/src/lib/tools/ai-dataset-tool.js +24 -7
- package/src/lib/tools/ai-dataset-tool.js.map +1 -1
- package/src/lib/tools/ai-duplicate-resource-tool.js +5 -6
- package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
- package/src/lib/components/ai-collaborator-permissions-column.component.d.ts +0 -32
- package/src/lib/components/ai-collaborator-permissions-column.component.js +0 -132
- package/src/lib/components/ai-collaborator-permissions-column.component.js.map +0 -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 '../usage/assist-credits'\nimport { hostRoleFor, isOrgWideMember } from '@aglyn/aglyn/app-utils/organizations'\nimport { resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AglynOrgMember } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n memberHasOrgPermission,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { logAiAllotmentChanged } from '../activity/ai-activity'\nimport {\n AI_ALLOTMENT_MAX_CREDITS,\n AI_ALLOTMENT_MAX_MODELS,\n aiAllotmentCredits,\n parseAiAllotmentSubject,\n type AiAllotment,\n type AiAllotmentMode,\n type AiAllotmentSubject,\n} from '../model/ai-allotments'\nimport { aiUsageByUserMonthFrom, aiUsageMonthKeys } from '../model/ai-usage-by-user'\nimport { AI_MODEL_CATALOG } from '../providers/catalog'\nimport { AI_MODEL_RESTRICTION_PLANS } from '../providers/model-choice'\nimport { resolveAiProvider } from '../providers/routing'\nimport {\n AI_HOST_CREDITS_FIELD,\n applyAiAllotmentWrites,\n listAiAllotments,\n seedAiHostMonthCredits,\n type AiAllotmentWrite,\n} from '../usage/ai-allotments'\nimport { userAiUsageMonthRef } from '../usage/ai-usage-by-user'\nimport {\n ASSIST_RETURNED_USD_FIELD,\n assistSpendAfterReturnsUsd,\n} from '../usage/assist-credit-returns'\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(\n assistSpendAfterReturnsUsd(\n orgMonth.get('estCostUsd'),\n orgMonth.get(ASSIST_RETURNED_USD_FIELD),\n ),\n ),\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","ASSIST_RETURNED_USD_FIELD","assistSpendAfterReturnsUsd","addAdminAudit","json","body","status","Response","MAX_WRITES","MAX_SITES","GET_ALL_CHUNK","authenticate","headers","authorization","idToken","startsWith","slice","length","undefined","error","decoded","app","auth","verifyIdToken","email_verified","uid","email","staff","isSiteAdminCollaborator","member","hostId","readRosterMonths","firestore","orgRef","month","roster","collection","get","members","docs","map","doc","$id","id","data","out","start","chunk","snapshots","getAll","forEach","snapshot","index","usage","exists","push","credits","byHost","memberName","String","displayName","trim","siteCredits","orgMonth","value","Number","isFinite","Math","floor","reduce","sum","entry","handleGet","request","params","orgSnapshot","query","subjectUid","orgId","membership","canManage","canView","siteAdmin","allowed","allotments","hostDocs","Promise","all","then","where","limit","org","byUid","Map","used","allotment","person","scope","rows","filter","subject","mode","models","visibleMembers","name","role","orgWide","hosts","provider","wire","pool","label","tier","restrictionAvailable","includes","canEdit","billing","collaborators","callerUid","CATALOG_IDS","Set","parseAllotmentWrite","raw","toLocaleString","Array","isArray","ids","Boolean","unknown","find","has","handlePost","payload","rawSet","rawRemove","rawEverySite","writes","parsed","code","removals","everySite","caller","touched","write","memberDocs","memberOf","set","hostNames","target","host","restricting","some","sites","site","remove","siteIds","siteId","catch","console","actorUid","actorEmail","action","before","after","removed","at","serverTimestamp","actor","feedRow","type","join","ok","handler","method","unauthenticated","GET","POST"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-allotments.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { pluginRequestFromWeb } from '@aglyn/aglyn/server'\nimport {\n assistCreditsFromUsd,\n resolveAssistCreditBudget,\n} from '../usage/assist-credits'\nimport { hostRoleFor, isOrgWideMember } from '@aglyn/aglyn/app-utils/organizations'\nimport { resolveSiteRolePresetPermissions } from '@aglyn/aglyn/app-utils/host-permissions'\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 {\n ASSIST_RETURNED_USD_FIELD,\n assistSpendAfterReturnsUsd,\n} from '../usage/assist-credit-returns'\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/**\n * A collaborator scoped to the site who holds \"Manage site settings\" there\n * (`site.manage`, AGL-4047): their site role's preset (Admin), then the\n * site's toggle for them.\n */\nfunction isSiteAdminCollaborator(member: AglynOrgMember | null, hostId: string): boolean {\n if (!member || !hostId || isOrgWideMember(member)) return false\n return resolveSiteRolePresetPermissions(member, hostId)['site.manage'] === true\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(\n assistSpendAfterReturnsUsd(\n orgMonth.get('estCostUsd'),\n orgMonth.get(ASSIST_RETURNED_USD_FIELD),\n ),\n ),\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","resolveSiteRolePresetPermissions","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","ASSIST_RETURNED_USD_FIELD","assistSpendAfterReturnsUsd","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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"}
|
|
@@ -17,7 +17,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
17
17
|
*/ // lockdown-423: via libs/plugins/ai/src/lib/server/ai-jobs-gate.ts
|
|
18
18
|
// The gate carries the org verdict; the handler adds the site's own below,
|
|
19
19
|
// with the host document in hand.
|
|
20
|
-
import {
|
|
20
|
+
import { holdsSiteKey, siteKeyRefusal } from "@aglyn/aglyn/app-utils/host-permissions";
|
|
21
|
+
import { checkEntitlement, encodeStoredNodes } from "@aglyn/aglyn/server";
|
|
21
22
|
import { pageContentRootId } from "@aglyn/aglyn/app-utils/page-markdown";
|
|
22
23
|
import { decodeStoredNodes } from "@aglyn/aglyn/app-utils/stored-nodes";
|
|
23
24
|
import { lockdownRefusal } from "@aglyn/tenant-data-admin/server/lockdown";
|
|
@@ -137,7 +138,7 @@ const str = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
|
137
138
|
};
|
|
138
139
|
}
|
|
139
140
|
export async function POST(request) {
|
|
140
|
-
var _body_orgId,
|
|
141
|
+
var _body_orgId, _job_outputs;
|
|
141
142
|
var _find;
|
|
142
143
|
let body;
|
|
143
144
|
try {
|
|
@@ -176,10 +177,9 @@ export async function POST(request) {
|
|
|
176
177
|
status: 404
|
|
177
178
|
});
|
|
178
179
|
}
|
|
179
|
-
|
|
180
|
-
if (!staff && !hostRoleCanWrite(role)) {
|
|
180
|
+
if (!staff && !holdsSiteKey(host, uid, 'site.edit')) {
|
|
181
181
|
return Response.json({
|
|
182
|
-
error: '
|
|
182
|
+
error: siteKeyRefusal('site.edit')
|
|
183
183
|
}, {
|
|
184
184
|
status: 403
|
|
185
185
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-experiment-versions.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\n// lockdown-423: via libs/plugins/ai/src/lib/server/ai-jobs-gate.ts\n// The gate carries the org verdict; the handler adds the site's own below,\n// with the host document in hand.\n\nimport {\n checkEntitlement,\n encodeStoredNodes,\n hostRoleCanWrite,\n} from '@aglyn/aglyn/server'\nimport { pageContentRootId } from '@aglyn/aglyn/app-utils/page-markdown'\nimport { decodeStoredNodes } from '@aglyn/aglyn/app-utils/stored-nodes'\nimport { lockdownRefusal } from '@aglyn/tenant-data-admin/server/lockdown'\nimport { getAiJob, writeAiJobAudit } from '../jobs/ai-jobs'\nimport type { AiJob } from '../model/ai-jobs.types'\nimport {\n readAiExperimentVariants,\n type AiExperimentVariantsProposalView,\n} from '../model/ai-experiment-proposal'\nimport { applyAiExperimentVariantCopy } from '../runtime/experiment-variant-copy'\nimport { AI_SEO_APPLY_VERSIONING_COPY } from './ai-seo-apply'\nimport { aiJobsGate } from './ai-jobs-gate'\n\n/**\n * A page's or a section's A/B variants as draft versions (AGL-3603):\n * `POST /api/ai/experiments/versions { orgId, hostId, jobId, screenId, nodeId? }`.\n *\n * An `experiment` job proposes the variants' copy. For an email that copy IS\n * the variant, and the editor holds it. For a page or a section the variant is\n * a version of the page, which the editor only pins — so this door makes one\n * per proposed variant past the first: the page's PUBLISHED version is\n * copied, the variant's headline and body are put into the region the test\n * varies (`runtime/experiment-variant-copy.ts`), and the copy is stored as a\n * new unpublished version named for the variant.\n *\n * ## Nothing goes live\n *\n * The published version, the screen's `versionId` and the experiment document\n * are never touched. The A/B testing editor pins each new version to its\n * variant as an unsaved change; the person reviews the versions, saves the\n * test and starts it. The first proposed variant is the copy as it stands, so\n * it is the control and pins the published version, as the editor's default\n * already does.\n *\n * ## The rungs\n *\n * The jobs read gate (membership, the release flag, `aiGenerative`, the org\n * lockdown); then the site — the job's and the org's — a role that writes the\n * site's content, the site's own lockdown verdict, and the `versioning`\n * entitlement, because a page that already has a version gains retained\n * history here, which is what that entitlement sells. Nothing spends a token.\n *\n * Idempotent: each version's id is derived from the job and the variant, so\n * a second press finds the versions the first made.\n */\n\nconst ID_CHARS = /^[A-Za-z0-9_-]{1,100}$/\n\n/** The id a job's variant version is stored under. */\nexport function aiExperimentVersionId(jobId: string, index: number): string {\n return `ab-${jobId}-${index}`\n}\n\n/** What a version made from a variant is named in the versions list. */\nexport function aiExperimentVersionName(name: string, index: number): string {\n const label = name.replace(/\\s+/g, ' ').trim() || `Variant ${index + 1}`\n return `A/B: ${label}`.slice(0, 80)\n}\n\nexport interface AiExperimentVersionsInput {\n hostId: string\n screenId: string\n /** A section test's element; `null` for a page test. */\n nodeId: string | null\n job: Pick<AiJob, '$id'>\n proposal: AiExperimentVariantsProposalView\n uid: string\n now?: Date\n}\n\nexport interface AiExperimentVersionsResult {\n /** One per variant past the control, in order. */\n versions: Array<{ index: number; name: string; versionId: string; changed: string[] }>\n skipped: Array<{ index: number; reason: string }>\n /** Why no version could be made at all, customer-safe; `null` when one could. */\n refusal: string | null\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** Make the versions. Exported for the spec, which drives it against a Firestore double. */\nexport async function createAiExperimentVersions(\n firestore: FirebaseFirestore.Firestore,\n input: AiExperimentVersionsInput,\n): Promise<AiExperimentVersionsResult> {\n const now = input.now ?? new Date()\n const screenRef = firestore.collection('hosts').doc(input.hostId).collection('screens').doc(input.screenId)\n const screen = await screenRef.get()\n const empty = (refusal: string): AiExperimentVersionsResult => ({ versions: [], skipped: [], refusal })\n if (!screen.exists || screen.get('deletedAt')) return empty('The page under test no longer exists.')\n const publishedId = str(screen.get('versionId'))\n const source = publishedId ? await screenRef.collection('versions').doc(publishedId).get() : null\n const sourceData = source?.exists ? (source.data() as Record<string, unknown>) : null\n const nodes = sourceData ? decodeStoredNodes<Record<string, unknown>>(sourceData['nodes']) : null\n if (!sourceData || !nodes) return empty('The page has no published version to start from.')\n const rootId = typeof sourceData['rootId'] === 'string' ? sourceData['rootId'] : undefined\n const regionRootId = input.nodeId ?? pageContentRootId(nodes as never, rootId)\n\n const versions: AiExperimentVersionsResult['versions'] = []\n const skipped: AiExperimentVersionsResult['skipped'] = []\n for (const [index, variant] of input.proposal.variants.entries()) {\n // The first variant is the copy as it stands: the control, on the published version.\n if (index === 0) continue\n const versionId = aiExperimentVersionId(input.job.$id, index)\n const name = aiExperimentVersionName(variant.name, index)\n const versionRef = screenRef.collection('versions').doc(versionId)\n const earlier = await versionRef.get()\n if (earlier.exists && !earlier.get('deletedAt')) {\n versions.push({ index, name: str(earlier.get('displayName')) || name, versionId, changed: [] })\n continue\n }\n const result = applyAiExperimentVariantCopy(\n nodes,\n { headline: variant.headline, body: variant.body },\n regionRootId,\n )\n if (!result.changed.length) {\n skipped.push({ index, reason: result.reason ?? 'Nothing on the page took the variant’s copy.' })\n continue\n }\n // Compressed at rest, as every write of a version's nodes is.\n const packed = encodeStoredNodes(result.nodes)\n await versionRef.set({\n ...sourceData,\n nodes: packed ? Buffer.from(packed) : result.nodes,\n displayName: name,\n createdAt: now,\n updatedAt: now,\n createdBy: input.uid,\n })\n versions.push({ index, name, versionId, changed: result.changed })\n }\n return { versions, skipped, refusal: null }\n}\n\nexport async function POST(request: Request): Promise<Response> {\n let body: Record<string, unknown>\n try {\n body = ((await request.json()) ?? {}) as Record<string, unknown>\n } catch {\n body = {}\n }\n const gate = await aiJobsGate(request, String(body['orgId'] ?? ''))\n if (gate instanceof Response) return gate\n const hostId = str(body['hostId'])\n const jobId = str(body['jobId'])\n const screenId = str(body['screenId'])\n const nodeId = str(body['nodeId']) || null\n if (!ID_CHARS.test(hostId) || !ID_CHARS.test(jobId) || !ID_CHARS.test(screenId)) {\n return Response.json({ error: 'Name the site, the page under test and the variants to make' }, { status: 400 })\n }\n if (nodeId !== null && !ID_CHARS.test(nodeId)) {\n return Response.json({ error: 'That is not a section of the page' }, { status: 400 })\n }\n const { firestore, orgId, uid, staff, org, decoded } = gate\n\n const hostSnapshot = await firestore.collection('hosts').doc(hostId).get()\n const host = hostSnapshot.exists ? (hostSnapshot.data() as Record<string, unknown>) : null\n if (!host || host['orgId'] !== orgId) {\n return Response.json({ error: 'Unknown site' }, { status: 404 })\n }\n const role = ((host['memberRoles'] ?? {}) as Record<string, unknown>)[uid]\n if (!staff && !hostRoleCanWrite(role)) {\n return Response.json({ error: 'Editing requires the editor role' }, { status: 403 })\n }\n const locked = await lockdownRefusal({\n request,\n staff,\n uid,\n org: org as Record<string, unknown>,\n host: host as never,\n })\n if (locked) return locked\n if (!checkEntitlement(org, 'versioning')) {\n return Response.json({ error: AI_SEO_APPLY_VERSIONING_COPY }, { status: 403 })\n }\n\n const job = await getAiJob(firestore, orgId, jobId)\n if (!job || job.kind !== 'experiment' || job.hostId !== hostId) {\n return Response.json({ error: 'Unknown variants' }, { status: 404 })\n }\n if (job.status !== 'done') {\n return Response.json({ error: 'The variants are still being written' }, { status: 409 })\n }\n const proposal = readAiExperimentVariants(\n (job.outputs ?? []).find((output) => output.resource === 'experiment')?.proposal,\n )\n if (!proposal || proposal.target === 'email') {\n return Response.json({ error: 'These variants are not a page’s or a section’s' }, { status: 409 })\n }\n if (proposal.target === 'section' && !nodeId) {\n return Response.json({ error: 'Pick the section under test first' }, { status: 400 })\n }\n\n const result = await createAiExperimentVersions(firestore, {\n hostId,\n screenId,\n nodeId: proposal.target === 'section' ? nodeId : null,\n job,\n proposal,\n uid,\n })\n if (result.refusal) return Response.json({ error: result.refusal }, { status: 409 })\n const made = result.versions.filter((entry) => entry.changed.length)\n if (made.length) {\n await writeAiJobAudit(firestore, {\n action: 'ai.job.apply',\n actorUid: uid,\n actorEmail: decoded.email ?? null,\n orgId,\n jobId,\n after: { hostId, screenId, versions: made.map((entry) => entry.versionId) },\n })\n }\n return Response.json(result, { status: 200, headers: { 'Cache-Control': 'no-store' } })\n}\n\nexport const dynamic = 'force-dynamic'\n"],"names":["checkEntitlement","encodeStoredNodes","hostRoleCanWrite","pageContentRootId","decodeStoredNodes","lockdownRefusal","getAiJob","writeAiJobAudit","readAiExperimentVariants","applyAiExperimentVariantCopy","AI_SEO_APPLY_VERSIONING_COPY","aiJobsGate","ID_CHARS","aiExperimentVersionId","jobId","index","aiExperimentVersionName","name","label","replace","trim","slice","str","value","createAiExperimentVersions","firestore","input","now","Date","screenRef","collection","doc","hostId","screenId","screen","get","empty","refusal","versions","skipped","exists","publishedId","source","sourceData","data","nodes","rootId","undefined","regionRootId","nodeId","variant","proposal","variants","entries","versionId","job","$id","versionRef","earlier","push","changed","result","headline","body","length","reason","packed","set","Buffer","from","displayName","createdAt","updatedAt","createdBy","uid","POST","request","host","json","gate","String","Response","test","error","status","orgId","staff","org","decoded","hostSnapshot","role","locked","kind","outputs","find","output","resource","target","made","filter","entry","action","actorUid","actorEmail","email","after","map","headers","dynamic"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-experiment-versions.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\n// lockdown-423: via libs/plugins/ai/src/lib/server/ai-jobs-gate.ts\n// The gate carries the org verdict; the handler adds the site's own below,\n// with the host document in hand.\n\nimport { holdsSiteKey, siteKeyRefusal } from '@aglyn/aglyn/app-utils/host-permissions'\nimport {\n checkEntitlement,\n encodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport { pageContentRootId } from '@aglyn/aglyn/app-utils/page-markdown'\nimport { decodeStoredNodes } from '@aglyn/aglyn/app-utils/stored-nodes'\nimport { lockdownRefusal } from '@aglyn/tenant-data-admin/server/lockdown'\nimport { getAiJob, writeAiJobAudit } from '../jobs/ai-jobs'\nimport type { AiJob } from '../model/ai-jobs.types'\nimport {\n readAiExperimentVariants,\n type AiExperimentVariantsProposalView,\n} from '../model/ai-experiment-proposal'\nimport { applyAiExperimentVariantCopy } from '../runtime/experiment-variant-copy'\nimport { AI_SEO_APPLY_VERSIONING_COPY } from './ai-seo-apply'\nimport { aiJobsGate } from './ai-jobs-gate'\n\n/**\n * A page's or a section's A/B variants as draft versions (AGL-3603):\n * `POST /api/ai/experiments/versions { orgId, hostId, jobId, screenId, nodeId? }`.\n *\n * An `experiment` job proposes the variants' copy. For an email that copy IS\n * the variant, and the editor holds it. For a page or a section the variant is\n * a version of the page, which the editor only pins — so this door makes one\n * per proposed variant past the first: the page's PUBLISHED version is\n * copied, the variant's headline and body are put into the region the test\n * varies (`runtime/experiment-variant-copy.ts`), and the copy is stored as a\n * new unpublished version named for the variant.\n *\n * ## Nothing goes live\n *\n * The published version, the screen's `versionId` and the experiment document\n * are never touched. The A/B testing editor pins each new version to its\n * variant as an unsaved change; the person reviews the versions, saves the\n * test and starts it. The first proposed variant is the copy as it stands, so\n * it is the control and pins the published version, as the editor's default\n * already does.\n *\n * ## The rungs\n *\n * The jobs read gate (membership, the release flag, `aiGenerative`, the org\n * lockdown); then the site — the job's and the org's — a role that writes the\n * site's content, the site's own lockdown verdict, and the `versioning`\n * entitlement, because a page that already has a version gains retained\n * history here, which is what that entitlement sells. Nothing spends a token.\n *\n * Idempotent: each version's id is derived from the job and the variant, so\n * a second press finds the versions the first made.\n */\n\nconst ID_CHARS = /^[A-Za-z0-9_-]{1,100}$/\n\n/** The id a job's variant version is stored under. */\nexport function aiExperimentVersionId(jobId: string, index: number): string {\n return `ab-${jobId}-${index}`\n}\n\n/** What a version made from a variant is named in the versions list. */\nexport function aiExperimentVersionName(name: string, index: number): string {\n const label = name.replace(/\\s+/g, ' ').trim() || `Variant ${index + 1}`\n return `A/B: ${label}`.slice(0, 80)\n}\n\nexport interface AiExperimentVersionsInput {\n hostId: string\n screenId: string\n /** A section test's element; `null` for a page test. */\n nodeId: string | null\n job: Pick<AiJob, '$id'>\n proposal: AiExperimentVariantsProposalView\n uid: string\n now?: Date\n}\n\nexport interface AiExperimentVersionsResult {\n /** One per variant past the control, in order. */\n versions: Array<{ index: number; name: string; versionId: string; changed: string[] }>\n skipped: Array<{ index: number; reason: string }>\n /** Why no version could be made at all, customer-safe; `null` when one could. */\n refusal: string | null\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** Make the versions. Exported for the spec, which drives it against a Firestore double. */\nexport async function createAiExperimentVersions(\n firestore: FirebaseFirestore.Firestore,\n input: AiExperimentVersionsInput,\n): Promise<AiExperimentVersionsResult> {\n const now = input.now ?? new Date()\n const screenRef = firestore.collection('hosts').doc(input.hostId).collection('screens').doc(input.screenId)\n const screen = await screenRef.get()\n const empty = (refusal: string): AiExperimentVersionsResult => ({ versions: [], skipped: [], refusal })\n if (!screen.exists || screen.get('deletedAt')) return empty('The page under test no longer exists.')\n const publishedId = str(screen.get('versionId'))\n const source = publishedId ? await screenRef.collection('versions').doc(publishedId).get() : null\n const sourceData = source?.exists ? (source.data() as Record<string, unknown>) : null\n const nodes = sourceData ? decodeStoredNodes<Record<string, unknown>>(sourceData['nodes']) : null\n if (!sourceData || !nodes) return empty('The page has no published version to start from.')\n const rootId = typeof sourceData['rootId'] === 'string' ? sourceData['rootId'] : undefined\n const regionRootId = input.nodeId ?? pageContentRootId(nodes as never, rootId)\n\n const versions: AiExperimentVersionsResult['versions'] = []\n const skipped: AiExperimentVersionsResult['skipped'] = []\n for (const [index, variant] of input.proposal.variants.entries()) {\n // The first variant is the copy as it stands: the control, on the published version.\n if (index === 0) continue\n const versionId = aiExperimentVersionId(input.job.$id, index)\n const name = aiExperimentVersionName(variant.name, index)\n const versionRef = screenRef.collection('versions').doc(versionId)\n const earlier = await versionRef.get()\n if (earlier.exists && !earlier.get('deletedAt')) {\n versions.push({ index, name: str(earlier.get('displayName')) || name, versionId, changed: [] })\n continue\n }\n const result = applyAiExperimentVariantCopy(\n nodes,\n { headline: variant.headline, body: variant.body },\n regionRootId,\n )\n if (!result.changed.length) {\n skipped.push({ index, reason: result.reason ?? 'Nothing on the page took the variant’s copy.' })\n continue\n }\n // Compressed at rest, as every write of a version's nodes is.\n const packed = encodeStoredNodes(result.nodes)\n await versionRef.set({\n ...sourceData,\n nodes: packed ? Buffer.from(packed) : result.nodes,\n displayName: name,\n createdAt: now,\n updatedAt: now,\n createdBy: input.uid,\n })\n versions.push({ index, name, versionId, changed: result.changed })\n }\n return { versions, skipped, refusal: null }\n}\n\nexport async function POST(request: Request): Promise<Response> {\n let body: Record<string, unknown>\n try {\n body = ((await request.json()) ?? {}) as Record<string, unknown>\n } catch {\n body = {}\n }\n const gate = await aiJobsGate(request, String(body['orgId'] ?? ''))\n if (gate instanceof Response) return gate\n const hostId = str(body['hostId'])\n const jobId = str(body['jobId'])\n const screenId = str(body['screenId'])\n const nodeId = str(body['nodeId']) || null\n if (!ID_CHARS.test(hostId) || !ID_CHARS.test(jobId) || !ID_CHARS.test(screenId)) {\n return Response.json({ error: 'Name the site, the page under test and the variants to make' }, { status: 400 })\n }\n if (nodeId !== null && !ID_CHARS.test(nodeId)) {\n return Response.json({ error: 'That is not a section of the page' }, { status: 400 })\n }\n const { firestore, orgId, uid, staff, org, decoded } = gate\n\n const hostSnapshot = await firestore.collection('hosts').doc(hostId).get()\n const host = hostSnapshot.exists ? (hostSnapshot.data() as Record<string, unknown>) : null\n if (!host || host['orgId'] !== orgId) {\n return Response.json({ error: 'Unknown site' }, { status: 404 })\n }\n if (!staff && !holdsSiteKey(host, uid, 'site.edit')) {\n return Response.json({ error: siteKeyRefusal('site.edit') }, { status: 403 })\n }\n const locked = await lockdownRefusal({\n request,\n staff,\n uid,\n org: org as Record<string, unknown>,\n host: host as never,\n })\n if (locked) return locked\n if (!checkEntitlement(org, 'versioning')) {\n return Response.json({ error: AI_SEO_APPLY_VERSIONING_COPY }, { status: 403 })\n }\n\n const job = await getAiJob(firestore, orgId, jobId)\n if (!job || job.kind !== 'experiment' || job.hostId !== hostId) {\n return Response.json({ error: 'Unknown variants' }, { status: 404 })\n }\n if (job.status !== 'done') {\n return Response.json({ error: 'The variants are still being written' }, { status: 409 })\n }\n const proposal = readAiExperimentVariants(\n (job.outputs ?? []).find((output) => output.resource === 'experiment')?.proposal,\n )\n if (!proposal || proposal.target === 'email') {\n return Response.json({ error: 'These variants are not a page’s or a section’s' }, { status: 409 })\n }\n if (proposal.target === 'section' && !nodeId) {\n return Response.json({ error: 'Pick the section under test first' }, { status: 400 })\n }\n\n const result = await createAiExperimentVersions(firestore, {\n hostId,\n screenId,\n nodeId: proposal.target === 'section' ? nodeId : null,\n job,\n proposal,\n uid,\n })\n if (result.refusal) return Response.json({ error: result.refusal }, { status: 409 })\n const made = result.versions.filter((entry) => entry.changed.length)\n if (made.length) {\n await writeAiJobAudit(firestore, {\n action: 'ai.job.apply',\n actorUid: uid,\n actorEmail: decoded.email ?? null,\n orgId,\n jobId,\n after: { hostId, screenId, versions: made.map((entry) => entry.versionId) },\n })\n }\n return Response.json(result, { status: 200, headers: { 'Cache-Control': 'no-store' } })\n}\n\nexport const dynamic = 'force-dynamic'\n"],"names":["holdsSiteKey","siteKeyRefusal","checkEntitlement","encodeStoredNodes","pageContentRootId","decodeStoredNodes","lockdownRefusal","getAiJob","writeAiJobAudit","readAiExperimentVariants","applyAiExperimentVariantCopy","AI_SEO_APPLY_VERSIONING_COPY","aiJobsGate","ID_CHARS","aiExperimentVersionId","jobId","index","aiExperimentVersionName","name","label","replace","trim","slice","str","value","createAiExperimentVersions","firestore","input","now","Date","screenRef","collection","doc","hostId","screenId","screen","get","empty","refusal","versions","skipped","exists","publishedId","source","sourceData","data","nodes","rootId","undefined","regionRootId","nodeId","variant","proposal","variants","entries","versionId","job","$id","versionRef","earlier","push","changed","result","headline","body","length","reason","packed","set","Buffer","from","displayName","createdAt","updatedAt","createdBy","uid","POST","request","json","gate","String","Response","test","error","status","orgId","staff","org","decoded","hostSnapshot","host","locked","kind","outputs","find","output","resource","target","made","filter","entry","action","actorUid","actorEmail","email","after","map","headers","dynamic"],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The AI keys of a collaborator's per-site verdict, copied onto their row of
|
|
19
|
+
* the site's roster (`hosts/{hostId}/members/{memberId}.aiPermissions`)
|
|
20
|
+
* (AGL-4047). The native apps draw a collaborator's AI access from that copy;
|
|
21
|
+
* the platform's generic toggle door decides the verdict and raises
|
|
22
|
+
* `host.member.permissions.changed`, and this keeps the copy in step. A row
|
|
23
|
+
* that no longer exists is left alone.
|
|
24
|
+
*/
|
|
25
|
+
export declare function recordAiPermissionsOnRoster(hostId: string, memberId: string, permissions: Readonly<Record<string, boolean>>): Promise<void>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
17
|
+
import { aiPermissionsOf } from "../model/ai-permissions.js";
|
|
18
|
+
/**
|
|
19
|
+
* The AI keys of a collaborator's per-site verdict, copied onto their row of
|
|
20
|
+
* the site's roster (`hosts/{hostId}/members/{memberId}.aiPermissions`)
|
|
21
|
+
* (AGL-4047). The native apps draw a collaborator's AI access from that copy;
|
|
22
|
+
* the platform's generic toggle door decides the verdict and raises
|
|
23
|
+
* `host.member.permissions.changed`, and this keeps the copy in step. A row
|
|
24
|
+
* that no longer exists is left alone.
|
|
25
|
+
*/ export async function recordAiPermissionsOnRoster(hostId, memberId, permissions) {
|
|
26
|
+
const ref = firebaseAdmin.app().firestore().collection('hosts').doc(hostId).collection('members').doc(memberId);
|
|
27
|
+
if (!(await ref.get()).exists) return;
|
|
28
|
+
await ref.update({
|
|
29
|
+
aiPermissions: aiPermissionsOf(permissions)
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=ai-host-permissions-roster.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-host-permissions-roster.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 { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { aiPermissionsOf } from '../model/ai-permissions'\n\n/**\n * The AI keys of a collaborator's per-site verdict, copied onto their row of\n * the site's roster (`hosts/{hostId}/members/{memberId}.aiPermissions`)\n * (AGL-4047). The native apps draw a collaborator's AI access from that copy;\n * the platform's generic toggle door decides the verdict and raises\n * `host.member.permissions.changed`, and this keeps the copy in step. A row\n * that no longer exists is left alone.\n */\nexport async function recordAiPermissionsOnRoster(\n hostId: string,\n memberId: string,\n permissions: Readonly<Record<string, boolean>>,\n): Promise<void> {\n const ref = firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('members')\n .doc(memberId)\n if (!(await ref.get()).exists) return\n await ref.update({ aiPermissions: aiPermissionsOf(permissions) })\n}\n"],"names":["firebaseAdmin","aiPermissionsOf","recordAiPermissionsOnRoster","hostId","memberId","permissions","ref","app","firestore","collection","doc","get","exists","update","aiPermissions"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,QAAQ,2BAA0B;AACxD,SAASC,eAAe,QAAQ,6BAAyB;AAEzD;;;;;;;CAOC,GACD,OAAO,eAAeC,4BACpBC,MAAc,EACdC,QAAgB,EAChBC,WAA8C;IAE9C,MAAMC,MAAMN,cACTO,GAAG,GACHC,SAAS,GACTC,UAAU,CAAC,SACXC,GAAG,CAACP,QACJM,UAAU,CAAC,WACXC,GAAG,CAACN;IACP,IAAI,CAAC,AAAC,CAAA,MAAME,IAAIK,GAAG,EAAC,EAAGC,MAAM,EAAE;IAC/B,MAAMN,IAAIO,MAAM,CAAC;QAAEC,eAAeb,gBAAgBI;IAAa;AACjE"}
|
|
@@ -14,7 +14,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import {
|
|
17
|
+
*/ import { holdsSiteKey, siteKeyRefusal } from "@aglyn/aglyn/app-utils/host-permissions";
|
|
18
|
+
import { pluginPermissionChanges, pluginRequestFromWeb, runPluginEventHandlers } from "@aglyn/aglyn/server";
|
|
18
19
|
import { emailUnverifiedResponse, firebaseAdmin, getOrgForHost, isImpersonationSession, lockdownRefusal, logHostActivity, setHostPermissions } from "@aglyn/tenant-data-admin";
|
|
19
20
|
import { invalidIdTokenResponse } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
20
21
|
import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
|
|
@@ -78,7 +79,7 @@ import { AI_PERMISSION_KEYS, aiPermissionsOf, isAiPermission } from "../model/ai
|
|
|
78
79
|
status: 400
|
|
79
80
|
});
|
|
80
81
|
try {
|
|
81
|
-
var _ref1
|
|
82
|
+
var _ref1;
|
|
82
83
|
const app = firebaseAdmin.app();
|
|
83
84
|
const decoded = await app.auth().verifyIdToken(idToken);
|
|
84
85
|
if (!decoded.email_verified && !isImpersonationSession(decoded)) {
|
|
@@ -87,13 +88,14 @@ import { AI_PERMISSION_KEYS, aiPermissionsOf, isAiPermission } from "../model/ai
|
|
|
87
88
|
const firestore = app.firestore();
|
|
88
89
|
const hostRef = firestore.collection('hosts').doc(hostId);
|
|
89
90
|
const host = (await hostRef.get()).data();
|
|
90
|
-
|
|
91
|
+
// "Manage site settings" here (`site.manage`, AGL-4047), or the
|
|
92
|
+
// workspace's "Manage members".
|
|
91
93
|
const orgPermissions = await resolveOrgPermissions(decoded.uid, {
|
|
92
94
|
hostId
|
|
93
95
|
});
|
|
94
|
-
if (!host ||
|
|
96
|
+
if (!host || !holdsSiteKey(host, decoded.uid, 'site.manage') && !orgPermissions.permissions.manageMembers) {
|
|
95
97
|
return Response.json({
|
|
96
|
-
error: '
|
|
98
|
+
error: siteKeyRefusal('site.manage')
|
|
97
99
|
}, {
|
|
98
100
|
status: 403
|
|
99
101
|
});
|
|
@@ -123,7 +125,7 @@ import { AI_PERMISSION_KEYS, aiPermissionsOf, isAiPermission } from "../model/ai
|
|
|
123
125
|
status: 400
|
|
124
126
|
});
|
|
125
127
|
}
|
|
126
|
-
const memberId = String((
|
|
128
|
+
const memberId = String((_ref1 = body == null ? void 0 : body.memberId) != null ? _ref1 : '');
|
|
127
129
|
if (!memberId) return Response.json({
|
|
128
130
|
error: 'Missing member'
|
|
129
131
|
}, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-host-permissions.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n pluginPermissionChanges,\n pluginRequestFromWeb,\n runPluginEventHandlers,\n} from '@aglyn/aglyn/server'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n getOrgForHost,\n isImpersonationSession,\n lockdownRefusal,\n logHostActivity,\n setHostPermissions,\n} from '@aglyn/tenant-data-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n AI_PERMISSION_KEYS,\n aiPermissionsOf,\n isAiPermission,\n type AiPermissionVerdict,\n} from '../model/ai-permissions'\n\n/**\n * The `aiPermissions` field of a body: a map of AI keys to booleans, or\n * `false` for anything else. Keys outside the two and non-boolean values are\n * refused rather than dropped, so a client that mis-spells a key learns so\n * instead of saving nothing.\n */\nexport function readAiPermissionToggles(value: unknown): Partial<AiPermissionVerdict> | false {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false\n const accepted: Partial<AiPermissionVerdict> = {}\n for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {\n if (!isAiPermission(key) || typeof entry !== 'boolean') return false\n accepted[key] = entry\n }\n return Object.keys(accepted).length ? accepted : false\n}\n\n/** `assist on, generate off` — the activity entry's words for a verdict. */\nexport function describeAiPermissions(verdict: AiPermissionVerdict): string {\n return `assist ${verdict['ai.use'] ? 'on' : 'off'}, generate ${\n verdict['ai.generate'] ? 'on' : 'off'\n }`\n}\n\n/**\n * `PATCH /api/ai/host-permissions` (AGL-2927, AGL-2984): a site\n * collaborator's AI toggles, set from the site's collaborators card.\n *\n * The door is the collaborators card's own — a site admin, or an org member\n * who manages people, on a site whose organization is not locked — and the\n * write is core's per-site toggle, `setHostPermissions`, which stores the\n * toggle on the member document the AI doors read and re-projects the site.\n * The display roster carries the verdict beside it so the card renders\n * without a second read.\n *\n * Recorded twice, like every other collaborator change: a sentence in the\n * site's feed naming the state the toggles became, and one\n * `org.permissions.changed` per key that moved, which this plugin's handler\n * turns into the org feed's coded row. A toggle set to the value it already\n * had moves nothing and raises nothing. A pending invite has no member\n * document to carry a toggle, and becomes settable once it is accepted.\n */\nasync function handler(request: Request): Promise<Response> {\n const { method, body, headers: rawHeaders } = await pluginRequestFromWeb(request)\n if (method !== 'PATCH') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const headers = rawHeaders as Partial<Record<string, string>>\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n const hostId = String(body?.hostId ?? '')\n if (!hostId) return Response.json({ error: 'Missing hostId' }, { status: 400 })\n\n try {\n const app = firebaseAdmin.app()\n const decoded = await app.auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const firestore = app.firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const host = (await hostRef.get()).data()\n const memberRole = (host?.['memberRoles'] ?? {})[decoded.uid]\n const orgPermissions = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!host || (memberRole !== 'admin' && !orgPermissions.permissions.manageMembers)) {\n return Response.json({ error: 'Not a site admin' }, { status: 403 })\n }\n const resolved = await getOrgForHost(hostId)\n if (!resolved) {\n return Response.json({ error: 'This site has no organization yet' }, { status: 409 })\n }\n const { orgId, org } = resolved\n const locked = await lockdownRefusal({\n request,\n staff: decoded['staff'] === true,\n uid: decoded.uid,\n org,\n host,\n })\n if (locked) return locked\n\n const toggles = readAiPermissionToggles(body?.aiPermissions)\n if (toggles === false) {\n return Response.json(\n { error: `aiPermissions must map ${AI_PERMISSION_KEYS.join(' and ')} to booleans` },\n { status: 400 },\n )\n }\n const memberId = String(body?.memberId ?? '')\n if (!memberId) return Response.json({ error: 'Missing member' }, { status: 400 })\n const membersRef = hostRef.collection('members')\n const member = (await membersRef.doc(memberId).get()).data()\n if (!member) return Response.json({ error: 'Member not found' }, { status: 404 })\n if (!member['uid']) {\n return Response.json(\n { error: 'AI access is set once the invite is accepted' },\n { status: 409 },\n )\n }\n\n const { before, after } = await setHostPermissions({\n orgId,\n uid: String(member['uid']),\n hostId,\n permissions: toggles,\n })\n const verdict = aiPermissionsOf(after)\n await membersRef.doc(memberId).update({ aiPermissions: verdict })\n const actor = {\n uid: decoded.uid,\n email: decoded.email ? String(decoded.email) : null,\n }\n await logHostActivity(\n hostId,\n actor,\n `Changed member AI access to ${describeAiPermissions(verdict)}`,\n {\n type: 'member',\n id: memberId,\n ...(member['email'] ? { name: String(member['email']) } : {}),\n },\n )\n const subjectName = [host['displayName'], member['email']]\n .map((value) => (typeof value === 'string' ? value.trim() : ''))\n .filter(Boolean)\n .join(' · ')\n for (const change of pluginPermissionChanges(before, after)) {\n if (!isAiPermission(change.permission)) continue\n await runPluginEventHandlers('org.permissions.changed', {\n orgId,\n actor,\n subject: { type: 'host', id: hostId, name: subjectName },\n permission: change.permission,\n granted: change.granted,\n })\n }\n return Response.json({ ok: true, aiPermissions: verdict }, { status: 200 })\n } catch (error) {\n // An unverifiable 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/host-permissions]', error)\n return Response.json({ error: 'Member operation failed' }, { status: 500 })\n }\n}\n\nexport { handler as PATCH }\n"],"names":["pluginPermissionChanges","pluginRequestFromWeb","runPluginEventHandlers","emailUnverifiedResponse","firebaseAdmin","getOrgForHost","isImpersonationSession","lockdownRefusal","logHostActivity","setHostPermissions","invalidIdTokenResponse","resolveOrgPermissions","AI_PERMISSION_KEYS","aiPermissionsOf","isAiPermission","readAiPermissionToggles","value","Array","isArray","accepted","key","entry","Object","entries","keys","length","describeAiPermissions","verdict","handler","request","headers","method","body","rawHeaders","Response","json","error","status","authorization","idToken","startsWith","slice","undefined","hostId","String","app","decoded","auth","verifyIdToken","email_verified","firestore","hostRef","collection","doc","host","get","data","memberRole","uid","orgPermissions","permissions","manageMembers","resolved","orgId","org","locked","staff","toggles","aiPermissions","join","memberId","membersRef","member","before","after","update","actor","email","type","id","name","subjectName","map","trim","filter","Boolean","change","permission","subject","granted","ok","unauthenticated","console","PATCH"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-host-permissions.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 { holdsSiteKey, siteKeyRefusal } from '@aglyn/aglyn/app-utils/host-permissions'\nimport {\n pluginPermissionChanges,\n pluginRequestFromWeb,\n runPluginEventHandlers,\n} from '@aglyn/aglyn/server'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n getOrgForHost,\n isImpersonationSession,\n lockdownRefusal,\n logHostActivity,\n setHostPermissions,\n} from '@aglyn/tenant-data-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n AI_PERMISSION_KEYS,\n aiPermissionsOf,\n isAiPermission,\n type AiPermissionVerdict,\n} from '../model/ai-permissions'\n\n/**\n * The `aiPermissions` field of a body: a map of AI keys to booleans, or\n * `false` for anything else. Keys outside the two and non-boolean values are\n * refused rather than dropped, so a client that mis-spells a key learns so\n * instead of saving nothing.\n */\nexport function readAiPermissionToggles(value: unknown): Partial<AiPermissionVerdict> | false {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false\n const accepted: Partial<AiPermissionVerdict> = {}\n for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {\n if (!isAiPermission(key) || typeof entry !== 'boolean') return false\n accepted[key] = entry\n }\n return Object.keys(accepted).length ? accepted : false\n}\n\n/** `assist on, generate off` — the activity entry's words for a verdict. */\nexport function describeAiPermissions(verdict: AiPermissionVerdict): string {\n return `assist ${verdict['ai.use'] ? 'on' : 'off'}, generate ${\n verdict['ai.generate'] ? 'on' : 'off'\n }`\n}\n\n/**\n * `PATCH /api/ai/host-permissions` (AGL-2927, AGL-2984): a site\n * collaborator's AI toggles, set from the site's collaborators card.\n *\n * The door is the collaborators card's own — a site admin, or an org member\n * who manages people, on a site whose organization is not locked — and the\n * write is core's per-site toggle, `setHostPermissions`, which stores the\n * toggle on the member document the AI doors read and re-projects the site.\n * The display roster carries the verdict beside it so the card renders\n * without a second read.\n *\n * Recorded twice, like every other collaborator change: a sentence in the\n * site's feed naming the state the toggles became, and one\n * `org.permissions.changed` per key that moved, which this plugin's handler\n * turns into the org feed's coded row. A toggle set to the value it already\n * had moves nothing and raises nothing. A pending invite has no member\n * document to carry a toggle, and becomes settable once it is accepted.\n */\nasync function handler(request: Request): Promise<Response> {\n const { method, body, headers: rawHeaders } = await pluginRequestFromWeb(request)\n if (method !== 'PATCH') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const headers = rawHeaders as Partial<Record<string, string>>\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n const hostId = String(body?.hostId ?? '')\n if (!hostId) return Response.json({ error: 'Missing hostId' }, { status: 400 })\n\n try {\n const app = firebaseAdmin.app()\n const decoded = await app.auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const firestore = app.firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const host = (await hostRef.get()).data()\n // \"Manage site settings\" here (`site.manage`, AGL-4047), or the\n // workspace's \"Manage members\".\n const orgPermissions = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!host || (!holdsSiteKey(host, decoded.uid, 'site.manage') && !orgPermissions.permissions.manageMembers)) {\n return Response.json({ error: siteKeyRefusal('site.manage') }, { status: 403 })\n }\n const resolved = await getOrgForHost(hostId)\n if (!resolved) {\n return Response.json({ error: 'This site has no organization yet' }, { status: 409 })\n }\n const { orgId, org } = resolved\n const locked = await lockdownRefusal({\n request,\n staff: decoded['staff'] === true,\n uid: decoded.uid,\n org,\n host,\n })\n if (locked) return locked\n\n const toggles = readAiPermissionToggles(body?.aiPermissions)\n if (toggles === false) {\n return Response.json(\n { error: `aiPermissions must map ${AI_PERMISSION_KEYS.join(' and ')} to booleans` },\n { status: 400 },\n )\n }\n const memberId = String(body?.memberId ?? '')\n if (!memberId) return Response.json({ error: 'Missing member' }, { status: 400 })\n const membersRef = hostRef.collection('members')\n const member = (await membersRef.doc(memberId).get()).data()\n if (!member) return Response.json({ error: 'Member not found' }, { status: 404 })\n if (!member['uid']) {\n return Response.json(\n { error: 'AI access is set once the invite is accepted' },\n { status: 409 },\n )\n }\n\n const { before, after } = await setHostPermissions({\n orgId,\n uid: String(member['uid']),\n hostId,\n permissions: toggles,\n })\n const verdict = aiPermissionsOf(after)\n await membersRef.doc(memberId).update({ aiPermissions: verdict })\n const actor = {\n uid: decoded.uid,\n email: decoded.email ? String(decoded.email) : null,\n }\n await logHostActivity(\n hostId,\n actor,\n `Changed member AI access to ${describeAiPermissions(verdict)}`,\n {\n type: 'member',\n id: memberId,\n ...(member['email'] ? { name: String(member['email']) } : {}),\n },\n )\n const subjectName = [host['displayName'], member['email']]\n .map((value) => (typeof value === 'string' ? value.trim() : ''))\n .filter(Boolean)\n .join(' · ')\n for (const change of pluginPermissionChanges(before, after)) {\n if (!isAiPermission(change.permission)) continue\n await runPluginEventHandlers('org.permissions.changed', {\n orgId,\n actor,\n subject: { type: 'host', id: hostId, name: subjectName },\n permission: change.permission,\n granted: change.granted,\n })\n }\n return Response.json({ ok: true, aiPermissions: verdict }, { status: 200 })\n } catch (error) {\n // An unverifiable 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/host-permissions]', error)\n return Response.json({ error: 'Member operation failed' }, { status: 500 })\n }\n}\n\nexport { handler as PATCH }\n"],"names":["holdsSiteKey","siteKeyRefusal","pluginPermissionChanges","pluginRequestFromWeb","runPluginEventHandlers","emailUnverifiedResponse","firebaseAdmin","getOrgForHost","isImpersonationSession","lockdownRefusal","logHostActivity","setHostPermissions","invalidIdTokenResponse","resolveOrgPermissions","AI_PERMISSION_KEYS","aiPermissionsOf","isAiPermission","readAiPermissionToggles","value","Array","isArray","accepted","key","entry","Object","entries","keys","length","describeAiPermissions","verdict","handler","request","headers","method","body","rawHeaders","Response","json","error","status","authorization","idToken","startsWith","slice","undefined","hostId","String","app","decoded","auth","verifyIdToken","email_verified","firestore","hostRef","collection","doc","host","get","data","orgPermissions","uid","permissions","manageMembers","resolved","orgId","org","locked","staff","toggles","aiPermissions","join","memberId","membersRef","member","before","after","update","actor","email","type","id","name","subjectName","map","trim","filter","Boolean","change","permission","subject","granted","ok","unauthenticated","console","PATCH"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
export declare function aiImageReadsRoute(request: Request): Promise<Response>;
|