@aglyn/plugins-ai 1.0.0-beta.226 → 1.0.0-beta.228
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/README.md +3 -2
- package/package.json +17 -17
- package/src/lib/billing/ai-month-meter.js +5 -3
- package/src/lib/billing/ai-month-meter.js.map +1 -1
- package/src/lib/billing/ai-overage-ledger.js +3 -1
- package/src/lib/billing/ai-overage-ledger.js.map +1 -1
- package/src/lib/components/ai-brief-dialog.component.d.ts +6 -2
- package/src/lib/components/ai-brief-dialog.component.js +36 -18
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-describe-automation.component.js +24 -4
- package/src/lib/components/ai-describe-automation.component.js.map +1 -1
- package/src/lib/components/ai-describe-button.component.js +6 -1
- package/src/lib/components/ai-describe-button.component.js.map +1 -1
- package/src/lib/components/ai-describe-page.component.d.ts +1 -1
- package/src/lib/components/ai-describe-page.component.js +1 -1
- package/src/lib/components/ai-describe-page.component.js.map +1 -1
- package/src/lib/components/ai-insight-dialog.component.js +24 -2
- package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
- package/src/lib/components/ai-job-events.d.ts +20 -0
- package/src/lib/components/ai-job-events.js +78 -0
- package/src/lib/components/ai-job-events.js.map +1 -0
- package/src/lib/components/ai-job-follow.component.d.ts +49 -0
- package/src/lib/components/ai-job-follow.component.js +210 -0
- package/src/lib/components/ai-job-follow.component.js.map +1 -0
- package/src/lib/components/ai-job-links.d.ts +42 -0
- package/src/lib/components/ai-job-links.js +140 -0
- package/src/lib/components/ai-job-links.js.map +1 -0
- package/src/lib/components/ai-job-plan.component.d.ts +13 -6
- package/src/lib/components/ai-job-plan.component.js +29 -3
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +24 -0
- package/src/lib/components/ai-job-requests.js +76 -0
- package/src/lib/components/ai-job-requests.js.map +1 -0
- package/src/lib/components/ai-job-site.d.ts +22 -0
- package/src/lib/components/ai-job-site.js +73 -0
- package/src/lib/components/ai-job-site.js.map +1 -0
- package/src/lib/components/ai-jobs-indicator.component.d.ts +25 -0
- package/src/lib/components/ai-jobs-indicator.component.js +80 -0
- package/src/lib/components/ai-jobs-indicator.component.js.map +1 -0
- package/src/lib/components/ai-jobs-list-page.component.d.ts +23 -0
- package/src/lib/components/ai-jobs-list-page.component.js +303 -0
- package/src/lib/components/ai-jobs-list-page.component.js.map +1 -0
- package/src/lib/components/ai-jobs-page.component.d.ts +7 -0
- package/src/lib/components/ai-jobs-page.component.js +30 -0
- package/src/lib/components/ai-jobs-page.component.js.map +1 -0
- package/src/lib/components/ai-jobs-store.d.ts +81 -0
- package/src/lib/components/ai-jobs-store.js +218 -0
- package/src/lib/components/ai-jobs-store.js.map +1 -0
- package/src/lib/components/ai-page-brief-dialog.component.d.ts +4 -4
- package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.d.ts +51 -0
- package/src/lib/components/ai-site-build-page.component.js +495 -0
- package/src/lib/components/ai-site-build-page.component.js.map +1 -0
- package/src/lib/components/ai-site-start-card.component.d.ts +9 -1
- package/src/lib/components/ai-site-start-card.component.js +533 -132
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-site-starter-fallback.component.d.ts +24 -0
- package/src/lib/components/ai-site-starter-fallback.component.js +112 -0
- package/src/lib/components/ai-site-starter-fallback.component.js.map +1 -0
- package/src/lib/components/assist-jobs-drawer.component.d.ts +18 -20
- package/src/lib/components/assist-jobs-drawer.component.js +152 -172
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +69 -8
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/staff-ai-credit-return.component.d.ts +55 -0
- package/src/lib/components/staff-ai-credit-return.component.js +346 -0
- package/src/lib/components/staff-ai-credit-return.component.js.map +1 -0
- package/src/lib/components/staff-org-ai-card.component.js +364 -279
- package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
- package/src/lib/components/staff-user-ai-usage-card.component.d.ts +4 -0
- package/src/lib/components/staff-user-ai-usage-card.component.js +83 -28
- package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
- package/src/lib/components/use-ai-job-run.js +5 -18
- package/src/lib/components/use-ai-job-run.js.map +1 -1
- package/src/lib/jobs/ai-job-auto-confirm.d.ts +52 -0
- package/src/lib/jobs/ai-job-auto-confirm.js +62 -0
- package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -0
- package/src/lib/jobs/ai-job-draft-ids.d.ts +6 -0
- package/src/lib/jobs/ai-job-draft-ids.js +9 -0
- package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +7 -0
- package/src/lib/jobs/ai-job-drafts.js +16 -7
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +2 -1
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.d.ts +6 -5
- package/src/lib/jobs/ai-job-page-sections.js +3 -3
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +19 -1
- package/src/lib/jobs/ai-job-page-step.js +50 -8
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +33 -2
- package/src/lib/jobs/ai-job-plan-step.js +167 -18
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +28 -0
- package/src/lib/jobs/ai-job-site-step.js +130 -21
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.d.ts +14 -1
- package/src/lib/jobs/ai-job-text-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs-notify.d.ts +37 -0
- package/src/lib/jobs/ai-jobs-notify.js +51 -0
- package/src/lib/jobs/ai-jobs-notify.js.map +1 -0
- package/src/lib/jobs/ai-jobs.d.ts +39 -4
- package/src/lib/jobs/ai-jobs.js +205 -34
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.d.ts +98 -0
- package/src/lib/jobs/ai-site-publish.js +372 -0
- package/src/lib/jobs/ai-site-publish.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-page-briefs.d.ts +7 -6
- package/src/lib/jobs/fixtures/ai-page-briefs.js +23 -56
- package/src/lib/jobs/fixtures/ai-page-briefs.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +112 -0
- package/src/lib/model/ai-job-activity.js +144 -0
- package/src/lib/model/ai-job-activity.js.map +1 -0
- package/src/lib/model/ai-job-failure-copy.d.ts +69 -0
- package/src/lib/model/ai-job-failure-copy.js +78 -0
- package/src/lib/model/ai-job-failure-copy.js.map +1 -0
- package/src/lib/model/ai-job-notice.d.ts +63 -0
- package/src/lib/model/ai-job-notice.js +129 -0
- package/src/lib/model/ai-job-notice.js.map +1 -0
- package/src/lib/model/ai-jobs.types.d.ts +53 -0
- package/src/lib/model/ai-jobs.types.js +1 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-plan-capabilities.d.ts +32 -10
- package/src/lib/model/ai-plan-capabilities.js +15 -2
- package/src/lib/model/ai-plan-capabilities.js.map +1 -1
- package/src/lib/model/ai-site-build-progress.d.ts +61 -0
- package/src/lib/model/ai-site-build-progress.js +136 -0
- package/src/lib/model/ai-site-build-progress.js.map +1 -0
- package/src/lib/model/ai-site-inventory.d.ts +9 -0
- package/src/lib/model/ai-site-inventory.js +7 -2
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +93 -2
- package/src/lib/model/ai-site-job.js +88 -7
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-start-seo.d.ts +19 -0
- package/src/lib/model/ai-site-start-seo.js +91 -3
- package/src/lib/model/ai-site-start-seo.js.map +1 -1
- package/src/lib/model/ai-site-start.d.ts +3 -1
- package/src/lib/model/ai-site-start.js +6 -4
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/plan-entitlements.d.ts +3 -3
- package/src/lib/plan-entitlements.js +3 -3
- package/src/lib/plan-entitlements.js.map +1 -1
- package/src/lib/plugin.js +38 -2
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/runtime/ai-abuse-guards.d.ts +9 -6
- package/src/lib/runtime/ai-abuse-guards.js +10 -7
- package/src/lib/runtime/ai-abuse-guards.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +16 -12
- package/src/lib/runtime/ai-doctrine-validators.js +50 -40
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +1 -1
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-eval-live.d.ts +1 -1
- package/src/lib/runtime/ai-eval-live.js +1 -1
- package/src/lib/runtime/ai-eval-live.js.map +1 -1
- package/src/lib/runtime/ai-gate.d.ts +3 -2
- package/src/lib/runtime/ai-gate.js +3 -2
- package/src/lib/runtime/ai-gate.js.map +1 -1
- package/src/lib/runtime/site-inventory.d.ts +16 -1
- package/src/lib/runtime/site-inventory.js +43 -3
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server/ai-admin-credits.d.ts +21 -0
- package/src/lib/server/ai-admin-credits.js +213 -0
- package/src/lib/server/ai-admin-credits.js.map +1 -0
- package/src/lib/server/ai-admin-org.js +12 -5
- package/src/lib/server/ai-admin-org.js.map +1 -1
- package/src/lib/server/ai-admin-signals.js +3 -1
- package/src/lib/server/ai-admin-signals.js.map +1 -1
- package/src/lib/server/ai-admin-user.js +12 -1
- package/src/lib/server/ai-admin-user.js.map +1 -1
- package/src/lib/server/ai-allotments.js +3 -2
- package/src/lib/server/ai-allotments.js.map +1 -1
- package/src/lib/server/ai-jobs-live-sites.d.ts +26 -0
- package/src/lib/server/ai-jobs-live-sites.js +58 -0
- package/src/lib/server/ai-jobs-live-sites.js.map +1 -0
- package/src/lib/server/ai-jobs-route.js +42 -5
- package/src/lib/server/ai-jobs-route.js.map +1 -1
- package/src/lib/server/assist-chat.js +3 -2
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/billing-credits.js +4 -3
- package/src/lib/server/billing-credits.js.map +1 -1
- package/src/lib/server/billing-overage.js +3 -2
- package/src/lib/server/billing-overage.js.map +1 -1
- package/src/lib/server.js +11 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/usage/assist-credit-returns-write.d.ts +75 -0
- package/src/lib/usage/assist-credit-returns-write.js +150 -0
- package/src/lib/usage/assist-credit-returns-write.js.map +1 -0
- package/src/lib/usage/assist-credit-returns.d.ts +68 -0
- package/src/lib/usage/assist-credit-returns.js +79 -0
- package/src/lib/usage/assist-credit-returns.js.map +1 -0
- package/src/lib/usage/assist-free-taste.js +6 -3
- package/src/lib/usage/assist-free-taste.js.map +1 -1
- package/src/lib/usage/assist-job-refund.d.ts +100 -0
- package/src/lib/usage/assist-job-refund.js +109 -0
- package/src/lib/usage/assist-job-refund.js.map +1 -0
- package/src/lib/usage/assist-usage.js +5 -2
- package/src/lib/usage/assist-usage.js.map +1 -1
- package/src/lib/usage/staff-org-ai.d.ts +31 -1
- package/src/lib/usage/staff-org-ai.js +19 -2
- package/src/lib/usage/staff-org-ai.js.map +1 -1
package/src/lib/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/plugin.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 { PLATFORM_BRAND_NAME, registerConsoleExtension } from '@aglyn/aglyn'\nimport { AiCollaboratorPermissionsCell } from './components/ai-collaborator-permissions-column.component'\nimport { AiSaveAsComponent } from './components/ai-save-as-component.component'\nimport {\n AiGenerateSectionControl,\n AiRewriteControl,\n} from './components/besigner-ai-controls.component'\nimport {\n AiAssistProviderOnHost,\n AssistPanelOnHost,\n} from './components/ai-permissions-on-host.component'\nimport AiCreditsCard from './components/ai-credits-card.component'\nimport {\n AiCollaboratorCreditsCell,\n AiCollaboratorCreditsHeader,\n AiMemberCreditsCell,\n AiMemberCreditsHeader,\n} from './components/ai-credits-columns.component'\nimport { AssistSignalsPage } from './components/assist-signals-page.component'\nimport BillingAssistOverageCard from './components/billing-assist-overage-card.component'\nimport { AiTopUsersCard } from './components/billing-ai-top-users.component'\nimport { AiAllotmentsCard } from './components/billing-ai-allotments.component'\nimport {\n AiCollaboratorAllotmentCell,\n AiCollaboratorAllotmentHeader,\n AiSiteAllotmentCard,\n} from './components/host-ai-allotments.component'\nimport MemberAiAllotmentCard from './components/member-ai-allotment-card.component'\nimport MemberAiUsageCard from './components/member-ai-usage-card.component'\nimport AiThemeProposalCard from './components/ai-theme-proposal-card.component'\nimport StaffOrgAiCard from './components/staff-org-ai-card.component'\nimport {\n StaffOrgUsageAiCreditsCell,\n StaffOrgUsageAiOverageCell,\n StaffOrgUsageAiPool,\n StaffOrgUsageAssistCell,\n} from './components/staff-org-usage-ai-columns.component'\nimport {\n StaffOrgsAiSpendCell,\n StaffOrgsAiSpendHeader,\n} from './components/staff-orgs-ai-spend-column.component'\nimport StaffUserAiUsageCard from './components/staff-user-ai-usage-card.component'\nimport AiSeoAuditCard from './components/ai-seo-audit-card.component'\nimport AiSiteBatchCard from './components/ai-site-batch-card.component'\nimport AiSiteSeoStartCard from './components/ai-site-seo-start-card.component'\nimport AiSiteStartCard from './components/ai-site-start-card.component'\nimport AiSeoFieldsCard from './components/ai-seo-fields-card.component'\nimport AiDescribeAutomationButton from './components/ai-describe-automation.component'\nimport AiCrmEmailDraft from './components/ai-crm-email-draft.component'\nimport AiCrmImportMapping from './components/ai-crm-import-mapping.component'\nimport AiCrmRecordCard from './components/ai-crm-record-card.component'\nimport AiDescribePageButton from './components/ai-describe-page.component'\nimport {\n AiDescribeComponentButton,\n AiDescribeFormButton,\n AiDescribeLayoutButton,\n AiDescribeTemplateButton,\n} from './components/ai-describe-button.component'\nimport {\n AiExperimentResultCard,\n AiExperimentVariantsCard,\n} from './components/ai-experiment-cards.component'\nimport {\n AiExplainAutomation,\n AiExplainRunFailure,\n} from './components/ai-explain-automation.component'\nimport AiProductCopyCard from './components/ai-product-copy-card.component'\nimport AiProductImportOption from './components/ai-product-import-option.component'\nimport AiProductsHubCard from './components/ai-products-hub-card.component'\nimport { AI_PLUGIN_ID } from './constants'\nimport { registerAiDeclarations } from './declarations'\n\n/**\n * The Aglyn AI plugin's console half (AGL-2939): the assistant dock, the\n * besigner copy assistant's provider, the billing, member and staff cards,\n * the member tables' credit columns and the Assist signal staff page — each\n * mounted through a shell-owned zone (AGL-2940) or the staff area's generic\n * route, so no console page imports this plugin.\n *\n * No `featureFlag` on the extension: the plugin's doors are gated one by\n * one — the dock reads the `release_assist` verdict the shell hands it, the\n * generative doors answer 404 behind `release_ai_generative`, the billing\n * cards render only where the plan sells a band — and a flag here would\n * switch off the released doors with the unreleased ones.\n */\nexport function registerAiConsole(): void {\n registerAiDeclarations()\n registerConsoleExtension({\n pluginId: AI_PLUGIN_ID,\n displayName: 'AI',\n // The besigner copy assistant (AGL-89/419): mounted by the shell around\n // every console page, and opened by this plugin's besigner controls.\n providers: [AiAssistProviderOnHost],\n // The Assist docs-gap and cost board (AGL-1860, AGL-2252), at the staff\n // URL and under the tab it has always had.\n staffPages: [\n {\n id: 'assist-signals',\n label: 'Assist signal',\n header: {\n // The configured brand, not ours (AGL-2153/2260): a white-label\n // deployment and a self-host operator both read this header.\n title: `${PLATFORM_BRAND_NAME} Assist Signal`,\n docsTopic: 'assistSignals',\n },\n Component: AssistSignalsPage,\n },\n ],\n widgets: [\n {\n slot: 'consoleDock',\n widgetId: 'ai-assist-dock',\n title: 'Assistant',\n Component: AssistPanelOnHost,\n },\n {\n slot: 'orgBillingUsage',\n widgetId: 'ai-credits-meter',\n title: 'AI credits',\n Component: AiCreditsCard,\n },\n {\n slot: 'orgBillingUsage',\n widgetId: 'ai-credits-overage',\n title: 'AI credits overage',\n Component: BillingAssistOverageCard,\n },\n // The per-member usage (AGL-2928): who drew the credits the meter\n // counts, on Billing → Usage, on each member's page, on the staff\n // user page, and as a column of both member tables.\n {\n slot: 'orgBillingUsage',\n widgetId: 'ai-usage-by-member',\n title: 'Who is generating what',\n permission: 'billing.view',\n Component: AiTopUsersCard,\n },\n // The collaborators table's AI column (AGL-2927): whether each person\n // may open the AI doors on the site, per site, ahead of the columns\n // that report what they drew.\n {\n slot: 'hostMembers',\n widgetId: 'ai-collaborator-permissions',\n title: 'AI',\n column: { header: 'AI' },\n Component: AiCollaboratorPermissionsCell,\n },\n // The copy assistant's besigner controls (AGL-89, AGL-169): Generate a\n // section on every editor's toolbar, and Rewrite with AI under the\n // selected element's fields. Each opens the provider's dialog, and is\n // drawn only while the reader holds the door's permission.\n {\n slot: 'besignerToolbar',\n widgetId: 'ai-generate-section',\n title: 'Generate a section with AI',\n Component: AiGenerateSectionControl,\n },\n {\n slot: 'besignerInspector',\n widgetId: 'ai-rewrite-copy',\n title: 'Rewrite with AI',\n Component: AiRewriteControl,\n },\n // Save the selection as a reusable component (AGL-2908): the component\n // job's second entry point, which proposes the properties a section\n // should declare and applies them as the manual promote does.\n {\n slot: 'besignerInspector',\n widgetId: 'ai-save-as-component',\n title: 'Make a reusable component with AI',\n permission: 'ai.generate',\n Component: AiSaveAsComponent,\n },\n {\n slot: 'orgMember',\n widgetId: 'ai-member-usage',\n title: 'AI usage',\n Component: MemberAiUsageCard,\n },\n // The allotments (AGL-2942): a member's or a site's monthly share of\n // the pool, set on Billing → Usage, on each member's page and on a\n // site's collaborators card — each card asks the route what its\n // reader may see and change.\n {\n slot: 'orgBillingUsage',\n widgetId: 'ai-allotments',\n title: 'AI allotments',\n permission: 'billing.view',\n Component: AiAllotmentsCard,\n },\n {\n slot: 'orgMember',\n widgetId: 'ai-member-allotment',\n title: 'AI allotment',\n Component: MemberAiAllotmentCard,\n },\n {\n slot: 'hostMembers',\n widgetId: 'ai-collaborator-allotment',\n title: 'AI allotment',\n column: {\n header: 'AI allotment',\n align: 'right',\n Header: AiCollaboratorAllotmentHeader,\n },\n Component: AiCollaboratorAllotmentCell,\n },\n {\n slot: 'hostMembers',\n widgetId: 'ai-site-allotment',\n title: 'Site AI allotment',\n Component: AiSiteAllotmentCard,\n },\n // Themes by AI (AGL-2938): a brief on the site's Theme section, a\n // proposal previewed before and after, and the editor's own Save to\n // keep it. Generation is sold as `aiGenerative` and held by\n // `ai.generate`, so the shell withholds the card from a plan or a\n // member without either; the card itself asks the route about the\n // release flag before it shows anything.\n {\n slot: 'hostTheme',\n widgetId: 'ai-theme-proposal',\n title: 'Theme assistant',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiThemeProposalCard,\n },\n {\n slot: 'staffOrg',\n widgetId: 'ai-org-usage',\n title: 'AI usage',\n Component: StaffOrgAiCard,\n },\n {\n slot: 'staffUser',\n widgetId: 'ai-account-usage',\n title: 'AI usage across organizations',\n Component: StaffUserAiUsageCard,\n },\n // The staff tables' AI figures (AGL-2984): the Organizations list's\n // spend column, and the usage table's three columns with the credit\n // pool line the org page draws above it.\n {\n slot: 'staffOrgsListColumn',\n widgetId: 'ai-orgs-spend',\n title: 'AI spend (month)',\n column: {\n header: 'AI spend (month)',\n align: 'right',\n Header: StaffOrgsAiSpendHeader,\n },\n Component: StaffOrgsAiSpendCell,\n },\n {\n slot: 'staffOrgUsageColumn',\n widgetId: 'ai-usage-assist-cost',\n title: 'Assist',\n column: { header: 'Assist', align: 'right' },\n Component: StaffOrgUsageAssistCell,\n },\n {\n slot: 'staffOrgUsageColumn',\n widgetId: 'ai-usage-credits',\n title: 'AI credits used',\n column: { header: 'AI credits used', align: 'right' },\n Component: StaffOrgUsageAiCreditsCell,\n },\n {\n slot: 'staffOrgUsageColumn',\n widgetId: 'ai-usage-overage',\n title: 'AI overage billed ($)',\n column: { header: 'AI overage billed ($)', align: 'right' },\n Component: StaffOrgUsageAiOverageCell,\n },\n {\n slot: 'staffOrgUsageColumn',\n widgetId: 'ai-usage-pool',\n title: 'AI credit pool',\n Component: StaffOrgUsageAiPool,\n },\n {\n slot: 'orgMembersListColumn',\n widgetId: 'ai-member-credits',\n title: 'AI credits (month)',\n column: { header: 'AI credits (month)', align: 'right', Header: AiMemberCreditsHeader },\n Component: AiMemberCreditsCell,\n },\n {\n slot: 'hostMembers',\n widgetId: 'ai-collaborator-credits',\n title: 'AI credits (site, month)',\n column: {\n header: 'AI credits (site, month)',\n align: 'right',\n Header: AiCollaboratorCreditsHeader,\n },\n Component: AiCollaboratorCreditsCell,\n },\n // SEO by AI (AGL-2910): \"Write SEO\" inside every search listing editor\n // — a page's SEO card and a product's listing — and the site audit on\n // the site's SEO section. Generation is sold as `aiGenerative` and held\n // by `ai.generate`, so the shell withholds both from a plan or a member\n // without either; each card asks the jobs route about the release flag\n // before it shows anything, and neither ever writes what it proposes.\n {\n slot: 'seoFields',\n widgetId: 'ai-seo-fields',\n title: 'SEO assistant',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiSeoFieldsCard,\n },\n {\n slot: 'hostSeo',\n widgetId: 'ai-seo-audit',\n title: 'SEO audit',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiSeoAuditCard,\n },\n // A page from a brief (AGL-2907): \"Describe it\" beside Templates and\n // Create New Screen. Gated as the other generative widgets are, and it\n // asks the jobs route about the release flag before it shows anything.\n {\n slot: 'hostScreens',\n widgetId: 'ai-describe-page',\n title: 'Describe a page',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribePageButton,\n },\n // The same entry for a page template, a layout and a form (AGL-3043),\n // beside the create action of the page that lists each: the same\n // dialog, the same gates, and the same question to the jobs route\n // before it shows anything.\n {\n slot: 'hostTemplates',\n widgetId: 'ai-describe-template',\n title: 'Describe a page template',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribeTemplateButton,\n },\n {\n slot: 'hostLayouts',\n widgetId: 'ai-describe-layout',\n title: 'Describe a layout',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribeLayoutButton,\n },\n {\n slot: 'hostForms',\n widgetId: 'ai-describe-form',\n title: 'Describe a form',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribeFormButton,\n },\n // And a reusable component from a brief (AGL-3051), beside Templates\n // and Create Component: the component job's first entry point, which\n // had none in the console.\n {\n slot: 'hostComponents',\n widgetId: 'ai-describe-component',\n title: 'Describe a reusable component',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribeComponentButton,\n },\n // Automations by AI (AGL-2919), in the zones the workflows plugin hosts\n // on its Automation page: \"Describe it\" beside Add action and Recipes,\n // \"Explain it\" in the editor of a saved automation, and \"Why did this\n // fail?\" on a failed run. Gated as the other generative widgets are;\n // each asks the jobs route about the release flag before it shows\n // anything, and none of them changes an automation.\n {\n slot: 'hostAutomations',\n widgetId: 'ai-describe-automation',\n title: 'Describe an automation',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribeAutomationButton,\n },\n {\n slot: 'automationEditor',\n widgetId: 'ai-explain-automation',\n title: 'Explain this automation',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiExplainAutomation,\n },\n {\n slot: 'automationRun',\n widgetId: 'ai-explain-run-failure',\n title: 'Why did this run fail?',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiExplainRunFailure,\n },\n // CRM by AI (AGL-2917): a record's summary and next step on its page,\n // a draft in the one-to-one composer, and an import's column matches.\n // The CRM hosts each zone, so it is drawn only where the CRM is; the\n // shell holds the plan and `ai.generate`, each card asks the jobs route\n // about the release flag before it shows anything, and none writes what\n // it proposes.\n {\n slot: 'recordInsights',\n widgetId: 'ai-crm-record',\n title: 'AI summary',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiCrmRecordCard,\n },\n {\n slot: 'recordEmail',\n widgetId: 'ai-crm-email',\n title: 'Draft with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiCrmEmailDraft,\n },\n {\n slot: 'importMapping',\n widgetId: 'ai-crm-import-mapping',\n title: 'Match columns with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiCrmImportMapping,\n },\n // The guided start (AGL-2918), on the page a newly created site lands\n // on: a few questions that become a site scaffold. Gated as the other\n // generative widgets are, and it asks the jobs route about the release\n // flag before it shows anything — so a workspace the feature is not\n // released to gets the blank page and nothing else. Skipping is the\n // zone's own `startBlank`, which creates nothing.\n {\n slot: 'hostFirstRun',\n widgetId: 'ai-site-start',\n title: 'Start this site with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiSiteStartCard,\n },\n // The listing those answers describe (AGL-2918), on the site's SEO\n // section: the site-wide search title and description the guided start\n // implies, offered where they are edited. The same gates as every\n // generative widget, and it stages into the SEO form rather than\n // writing it — a person's Update is the write, as it is for the audit\n // card beside it.\n {\n slot: 'hostSeo',\n widgetId: 'ai-site-seo-start',\n title: 'The listing your answers describe',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiSiteSeoStartCard,\n },\n // The agency batch (AGL-2911): one brief across many of the org's\n // sites, from the page that lists them. The card asks the jobs route\n // about the release flag before it shows anything, and its own door\n // holds the plan band and the caller's permission on every site it is\n // pointed at.\n {\n slot: 'orgSites',\n widgetId: 'ai-site-batch',\n title: 'Generate sites with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiSiteBatchCard,\n },\n // Commerce by AI (AGL-2916): product copy in the product editor, the\n // catalog, categories and discounts on the products hub, and copy for\n // an import as it lands. The commerce plugin hosts the zones and makes\n // every write; each card asks the jobs route about the release flag\n // before it shows anything.\n {\n slot: 'productEditor',\n widgetId: 'ai-product-copy',\n title: 'Product copy assistant',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiProductCopyCard,\n },\n {\n slot: 'productsHub',\n widgetId: 'ai-products-hub',\n title: 'Build your catalog with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiProductsHubCard,\n },\n {\n slot: 'productImport',\n widgetId: 'ai-product-import',\n title: 'Write imported product copy with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiProductImportOption,\n },\n // A/B tests by AI (AGL-2914): variants written in the experiment\n // editor, and one test's result read in plain words below its figures.\n // The A/B testing card hosts both zones, so they are drawn only where\n // that card is and only on a site with A/B testing; the shell holds the\n // plan band and `ai.generate`, and each card asks the jobs route about\n // the release flag before it shows anything. Neither writes: the\n // experiment editor's Save is the only write, and an explanation has\n // nothing to apply.\n {\n slot: 'experimentVariants',\n widgetId: 'ai-experiment-variants',\n title: 'Write variants with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiExperimentVariantsCard,\n },\n {\n slot: 'experimentResult',\n widgetId: 'ai-experiment-result',\n title: 'Explain this result with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiExperimentResultCard,\n },\n ],\n })\n}\n"],"names":["PLATFORM_BRAND_NAME","registerConsoleExtension","AiCollaboratorPermissionsCell","AiSaveAsComponent","AiGenerateSectionControl","AiRewriteControl","AiAssistProviderOnHost","AssistPanelOnHost","AiCreditsCard","AiCollaboratorCreditsCell","AiCollaboratorCreditsHeader","AiMemberCreditsCell","AiMemberCreditsHeader","AssistSignalsPage","BillingAssistOverageCard","AiTopUsersCard","AiAllotmentsCard","AiCollaboratorAllotmentCell","AiCollaboratorAllotmentHeader","AiSiteAllotmentCard","MemberAiAllotmentCard","MemberAiUsageCard","AiThemeProposalCard","StaffOrgAiCard","StaffOrgUsageAiCreditsCell","StaffOrgUsageAiOverageCell","StaffOrgUsageAiPool","StaffOrgUsageAssistCell","StaffOrgsAiSpendCell","StaffOrgsAiSpendHeader","StaffUserAiUsageCard","AiSeoAuditCard","AiSiteBatchCard","AiSiteSeoStartCard","AiSiteStartCard","AiSeoFieldsCard","AiDescribeAutomationButton","AiCrmEmailDraft","AiCrmImportMapping","AiCrmRecordCard","AiDescribePageButton","AiDescribeComponentButton","AiDescribeFormButton","AiDescribeLayoutButton","AiDescribeTemplateButton","AiExperimentResultCard","AiExperimentVariantsCard","AiExplainAutomation","AiExplainRunFailure","AiProductCopyCard","AiProductImportOption","AiProductsHubCard","AI_PLUGIN_ID","registerAiDeclarations","registerAiConsole","pluginId","displayName","providers","staffPages","id","label","header","title","docsTopic","Component","widgets","slot","widgetId","permission","column","align","Header","featureFlag"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/plugin.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 { PLATFORM_BRAND_NAME, registerConsoleExtension } from '@aglyn/aglyn'\nimport { AiCollaboratorPermissionsCell } from './components/ai-collaborator-permissions-column.component'\nimport { AiSaveAsComponent } from './components/ai-save-as-component.component'\nimport {\n AiGenerateSectionControl,\n AiRewriteControl,\n} from './components/besigner-ai-controls.component'\nimport {\n AiAssistProviderOnHost,\n AssistPanelOnHost,\n} from './components/ai-permissions-on-host.component'\nimport AiCreditsCard from './components/ai-credits-card.component'\nimport AiJobsTopBarIndicator from './components/ai-jobs-indicator.component'\nimport {\n AiCollaboratorCreditsCell,\n AiCollaboratorCreditsHeader,\n AiMemberCreditsCell,\n AiMemberCreditsHeader,\n} from './components/ai-credits-columns.component'\nimport { AssistSignalsPage } from './components/assist-signals-page.component'\nimport BillingAssistOverageCard from './components/billing-assist-overage-card.component'\nimport { AiTopUsersCard } from './components/billing-ai-top-users.component'\nimport { AiAllotmentsCard } from './components/billing-ai-allotments.component'\nimport {\n AiCollaboratorAllotmentCell,\n AiCollaboratorAllotmentHeader,\n AiSiteAllotmentCard,\n} from './components/host-ai-allotments.component'\nimport MemberAiAllotmentCard from './components/member-ai-allotment-card.component'\nimport MemberAiUsageCard from './components/member-ai-usage-card.component'\nimport AiThemeProposalCard from './components/ai-theme-proposal-card.component'\nimport StaffOrgAiCard from './components/staff-org-ai-card.component'\nimport {\n StaffOrgUsageAiCreditsCell,\n StaffOrgUsageAiOverageCell,\n StaffOrgUsageAiPool,\n StaffOrgUsageAssistCell,\n} from './components/staff-org-usage-ai-columns.component'\nimport {\n StaffOrgsAiSpendCell,\n StaffOrgsAiSpendHeader,\n} from './components/staff-orgs-ai-spend-column.component'\nimport StaffUserAiUsageCard from './components/staff-user-ai-usage-card.component'\nimport AiSeoAuditCard from './components/ai-seo-audit-card.component'\nimport AiSiteBatchCard from './components/ai-site-batch-card.component'\nimport AiSiteSeoStartCard from './components/ai-site-seo-start-card.component'\nimport AiSiteStartCard from './components/ai-site-start-card.component'\nimport AiSeoFieldsCard from './components/ai-seo-fields-card.component'\nimport AiDescribeAutomationButton from './components/ai-describe-automation.component'\nimport AiCrmEmailDraft from './components/ai-crm-email-draft.component'\nimport AiCrmImportMapping from './components/ai-crm-import-mapping.component'\nimport AiCrmRecordCard from './components/ai-crm-record-card.component'\nimport AiDescribePageButton from './components/ai-describe-page.component'\nimport {\n AiDescribeComponentButton,\n AiDescribeFormButton,\n AiDescribeLayoutButton,\n AiDescribeTemplateButton,\n} from './components/ai-describe-button.component'\nimport {\n AiExperimentResultCard,\n AiExperimentVariantsCard,\n} from './components/ai-experiment-cards.component'\nimport {\n AiExplainAutomation,\n AiExplainRunFailure,\n} from './components/ai-explain-automation.component'\nimport AiProductCopyCard from './components/ai-product-copy-card.component'\nimport AiProductImportOption from './components/ai-product-import-option.component'\nimport AiProductsHubCard from './components/ai-products-hub-card.component'\nimport AiJobsPage from './components/ai-jobs-page.component'\nimport { AI_PLUGIN_ID } from './constants'\nimport { registerAiDeclarations } from './declarations'\n\n/**\n * The Aglyn AI plugin's console half (AGL-2939): the assistant dock, the\n * besigner copy assistant's provider, the billing, member and staff cards,\n * the member tables' credit columns and the Assist signal staff page — each\n * mounted through a shell-owned zone (AGL-2940) or the staff area's generic\n * route, so no console page imports this plugin.\n *\n * No `featureFlag` on the extension: the plugin's doors are gated one by\n * one — the dock reads the `release_assist` verdict the shell hands it, the\n * generative doors answer 404 behind `release_ai_generative`, the billing\n * cards render only where the plan sells a band — and a flag here would\n * switch off the released doors with the unreleased ones.\n */\nexport function registerAiConsole(): void {\n registerAiDeclarations()\n registerConsoleExtension({\n pluginId: AI_PLUGIN_ID,\n displayName: 'AI',\n // The besigner copy assistant (AGL-89/419): mounted by the shell around\n // every console page, and opened by this plugin's besigner controls.\n providers: [AiAssistProviderOnHost],\n // The Assist docs-gap and cost board (AGL-1860, AGL-2252), at the staff\n // URL and under the tab it has always had.\n staffPages: [\n {\n id: 'assist-signals',\n label: 'Assist signal',\n header: {\n // The configured brand, not ours (AGL-2153/2260): a white-label\n // deployment and a self-host operator both read this header.\n title: `${PLATFORM_BRAND_NAME} Assist Signal`,\n docsTopic: 'assistSignals',\n },\n Component: AssistSignalsPage,\n },\n ],\n // A site's AI jobs (AGL-3596), an address under the site and no tab:\n // `/ai-jobs` lists them, and `/ai-jobs/{jobId}` is one job's page —\n // \"Building your site\" (AGL-3594), where the guided start lands a person\n // and where a site job's notification opens. The `href` is a literal,\n // `AI_SITE_BUILD_HREF`'s value, because the tab-title manifest is read\n // from this source (`ai-jobs-page.spec.tsx` holds the two equal).\n navItems: [\n {\n label: 'AI jobs',\n href: '/ai-jobs',\n recordTitle: 'Building your site',\n header: { title: 'AI jobs', docsTopic: 'howAglynAiBuilds', docsAnchor: '#finding-your-ai-jobs' },\n unlisted: true,\n ownsSubtree: true,\n permission: 'ai.generate',\n Component: AiJobsPage,\n },\n ],\n widgets: [\n {\n slot: 'consoleDock',\n widgetId: 'ai-assist-dock',\n title: 'Assistant',\n Component: AssistPanelOnHost,\n },\n // The AI jobs indicator (AGL-3593), beside the notifications bell on\n // every page: what the workspace's jobs are doing, and the way to them.\n // Gated as the other generative widgets are; it asks the shell for the\n // release flags of the jobs and of the panel it opens.\n {\n slot: 'consoleTopBar',\n widgetId: 'ai-jobs-indicator',\n title: 'AI jobs',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiJobsTopBarIndicator,\n },\n {\n slot: 'orgBillingUsage',\n widgetId: 'ai-credits-meter',\n title: 'AI credits',\n Component: AiCreditsCard,\n },\n {\n slot: 'orgBillingUsage',\n widgetId: 'ai-credits-overage',\n title: 'AI credits overage',\n Component: BillingAssistOverageCard,\n },\n // The per-member usage (AGL-2928): who drew the credits the meter\n // counts, on Billing → Usage, on each member's page, on the staff\n // user page, and as a column of both member tables.\n {\n slot: 'orgBillingUsage',\n widgetId: 'ai-usage-by-member',\n title: 'Who is generating what',\n permission: 'billing.view',\n Component: AiTopUsersCard,\n },\n // The collaborators table's AI column (AGL-2927): whether each person\n // may open the AI doors on the site, per site, ahead of the columns\n // that report what they drew.\n {\n slot: 'hostMembers',\n widgetId: 'ai-collaborator-permissions',\n title: 'AI',\n column: { header: 'AI' },\n Component: AiCollaboratorPermissionsCell,\n },\n // The copy assistant's besigner controls (AGL-89, AGL-169): Generate a\n // section on every editor's toolbar, and Rewrite with AI under the\n // selected element's fields. Each opens the provider's dialog, and is\n // drawn only while the reader holds the door's permission.\n {\n slot: 'besignerToolbar',\n widgetId: 'ai-generate-section',\n title: 'Generate a section with AI',\n Component: AiGenerateSectionControl,\n },\n {\n slot: 'besignerInspector',\n widgetId: 'ai-rewrite-copy',\n title: 'Rewrite with AI',\n Component: AiRewriteControl,\n },\n // Save the selection as a reusable component (AGL-2908): the component\n // job's second entry point, which proposes the properties a section\n // should declare and applies them as the manual promote does.\n {\n slot: 'besignerInspector',\n widgetId: 'ai-save-as-component',\n title: 'Make a reusable component with AI',\n permission: 'ai.generate',\n Component: AiSaveAsComponent,\n },\n {\n slot: 'orgMember',\n widgetId: 'ai-member-usage',\n title: 'AI usage',\n Component: MemberAiUsageCard,\n },\n // The allotments (AGL-2942): a member's or a site's monthly share of\n // the pool, set on Billing → Usage, on each member's page and on a\n // site's collaborators card — each card asks the route what its\n // reader may see and change.\n {\n slot: 'orgBillingUsage',\n widgetId: 'ai-allotments',\n title: 'AI allotments',\n permission: 'billing.view',\n Component: AiAllotmentsCard,\n },\n {\n slot: 'orgMember',\n widgetId: 'ai-member-allotment',\n title: 'AI allotment',\n Component: MemberAiAllotmentCard,\n },\n {\n slot: 'hostMembers',\n widgetId: 'ai-collaborator-allotment',\n title: 'AI allotment',\n column: {\n header: 'AI allotment',\n align: 'right',\n Header: AiCollaboratorAllotmentHeader,\n },\n Component: AiCollaboratorAllotmentCell,\n },\n {\n slot: 'hostMembers',\n widgetId: 'ai-site-allotment',\n title: 'Site AI allotment',\n Component: AiSiteAllotmentCard,\n },\n // Themes by AI (AGL-2938): a brief on the site's Theme section, a\n // proposal previewed before and after, and the editor's own Save to\n // keep it. Generation is sold as `aiGenerative` and held by\n // `ai.generate`, so the shell withholds the card from a plan or a\n // member without either; the card itself asks the route about the\n // release flag before it shows anything.\n {\n slot: 'hostTheme',\n widgetId: 'ai-theme-proposal',\n title: 'Theme assistant',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiThemeProposalCard,\n },\n {\n slot: 'staffOrg',\n widgetId: 'ai-org-usage',\n title: 'AI usage',\n Component: StaffOrgAiCard,\n },\n {\n slot: 'staffUser',\n widgetId: 'ai-account-usage',\n title: 'AI usage across organizations',\n Component: StaffUserAiUsageCard,\n },\n // The staff tables' AI figures (AGL-2984): the Organizations list's\n // spend column, and the usage table's three columns with the credit\n // pool line the org page draws above it.\n {\n slot: 'staffOrgsListColumn',\n widgetId: 'ai-orgs-spend',\n title: 'AI spend (month)',\n column: {\n header: 'AI spend (month)',\n align: 'right',\n Header: StaffOrgsAiSpendHeader,\n },\n Component: StaffOrgsAiSpendCell,\n },\n {\n slot: 'staffOrgUsageColumn',\n widgetId: 'ai-usage-assist-cost',\n title: 'Assist',\n column: { header: 'Assist', align: 'right' },\n Component: StaffOrgUsageAssistCell,\n },\n {\n slot: 'staffOrgUsageColumn',\n widgetId: 'ai-usage-credits',\n title: 'AI credits used',\n column: { header: 'AI credits used', align: 'right' },\n Component: StaffOrgUsageAiCreditsCell,\n },\n {\n slot: 'staffOrgUsageColumn',\n widgetId: 'ai-usage-overage',\n title: 'AI overage billed ($)',\n column: { header: 'AI overage billed ($)', align: 'right' },\n Component: StaffOrgUsageAiOverageCell,\n },\n {\n slot: 'staffOrgUsageColumn',\n widgetId: 'ai-usage-pool',\n title: 'AI credit pool',\n Component: StaffOrgUsageAiPool,\n },\n {\n slot: 'orgMembersListColumn',\n widgetId: 'ai-member-credits',\n title: 'AI credits (month)',\n column: { header: 'AI credits (month)', align: 'right', Header: AiMemberCreditsHeader },\n Component: AiMemberCreditsCell,\n },\n {\n slot: 'hostMembers',\n widgetId: 'ai-collaborator-credits',\n title: 'AI credits (site, month)',\n column: {\n header: 'AI credits (site, month)',\n align: 'right',\n Header: AiCollaboratorCreditsHeader,\n },\n Component: AiCollaboratorCreditsCell,\n },\n // SEO by AI (AGL-2910): \"Write SEO\" inside every search listing editor\n // — a page's SEO card and a product's listing — and the site audit on\n // the site's SEO section. Generation is sold as `aiGenerative` and held\n // by `ai.generate`, so the shell withholds both from a plan or a member\n // without either; each card asks the jobs route about the release flag\n // before it shows anything, and neither ever writes what it proposes.\n {\n slot: 'seoFields',\n widgetId: 'ai-seo-fields',\n title: 'SEO assistant',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiSeoFieldsCard,\n },\n {\n slot: 'hostSeo',\n widgetId: 'ai-seo-audit',\n title: 'SEO audit',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiSeoAuditCard,\n },\n // A page from a brief (AGL-2907): \"Create with AI\" beside Templates and\n // Create New Screen. Gated as the other generative widgets are, and it\n // asks the jobs route about the release flag before it shows anything.\n {\n slot: 'hostScreens',\n widgetId: 'ai-describe-page',\n title: 'Describe a page',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribePageButton,\n },\n // The same entry for a page template, a layout and a form (AGL-3043),\n // beside the create action of the page that lists each: the same\n // dialog, the same gates, and the same question to the jobs route\n // before it shows anything.\n {\n slot: 'hostTemplates',\n widgetId: 'ai-describe-template',\n title: 'Describe a page template',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribeTemplateButton,\n },\n {\n slot: 'hostLayouts',\n widgetId: 'ai-describe-layout',\n title: 'Describe a layout',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribeLayoutButton,\n },\n {\n slot: 'hostForms',\n widgetId: 'ai-describe-form',\n title: 'Describe a form',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribeFormButton,\n },\n // And a reusable component from a brief (AGL-3051), beside Templates\n // and Create Component: the component job's first entry point, which\n // had none in the console.\n {\n slot: 'hostComponents',\n widgetId: 'ai-describe-component',\n title: 'Describe a reusable component',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribeComponentButton,\n },\n // Automations by AI (AGL-2919), in the zones the workflows plugin hosts\n // on its Automation page: \"Create with AI\" beside Add action and Recipes,\n // \"Explain it\" in the editor of a saved automation, and \"Why did this\n // fail?\" on a failed run. Gated as the other generative widgets are;\n // each asks the jobs route about the release flag before it shows\n // anything, and none of them changes an automation.\n {\n slot: 'hostAutomations',\n widgetId: 'ai-describe-automation',\n title: 'Describe an automation',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribeAutomationButton,\n },\n {\n slot: 'automationEditor',\n widgetId: 'ai-explain-automation',\n title: 'Explain this automation',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiExplainAutomation,\n },\n {\n slot: 'automationRun',\n widgetId: 'ai-explain-run-failure',\n title: 'Why did this run fail?',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiExplainRunFailure,\n },\n // CRM by AI (AGL-2917): a record's summary and next step on its page,\n // a draft in the one-to-one composer, and an import's column matches.\n // The CRM hosts each zone, so it is drawn only where the CRM is; the\n // shell holds the plan and `ai.generate`, each card asks the jobs route\n // about the release flag before it shows anything, and none writes what\n // it proposes.\n {\n slot: 'recordInsights',\n widgetId: 'ai-crm-record',\n title: 'AI summary',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiCrmRecordCard,\n },\n {\n slot: 'recordEmail',\n widgetId: 'ai-crm-email',\n title: 'Draft with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiCrmEmailDraft,\n },\n {\n slot: 'importMapping',\n widgetId: 'ai-crm-import-mapping',\n title: 'Match columns with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiCrmImportMapping,\n },\n // The guided start (AGL-2918), on the page a newly created site lands\n // on: a few questions that become a site scaffold. Gated as the other\n // generative widgets are, and it asks the jobs route about the release\n // flag before it shows anything — so a workspace the feature is not\n // released to gets the blank page and nothing else. Skipping is the\n // zone's own `startBlank`, which creates nothing.\n {\n slot: 'hostFirstRun',\n widgetId: 'ai-site-start',\n title: 'Start this site with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiSiteStartCard,\n },\n // The listing those answers describe (AGL-2918), on the site's SEO\n // section: the site-wide search title and description the guided start\n // implies, offered where they are edited. The same gates as every\n // generative widget, and it stages into the SEO form rather than\n // writing it — a person's Update is the write, as it is for the audit\n // card beside it.\n {\n slot: 'hostSeo',\n widgetId: 'ai-site-seo-start',\n title: 'The listing your answers describe',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiSiteSeoStartCard,\n },\n // The agency batch (AGL-2911): one brief across many of the org's\n // sites, from the page that lists them. The card asks the jobs route\n // about the release flag before it shows anything, and its own door\n // holds the plan band and the caller's permission on every site it is\n // pointed at.\n {\n slot: 'orgSites',\n widgetId: 'ai-site-batch',\n title: 'Generate sites with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiSiteBatchCard,\n },\n // Commerce by AI (AGL-2916): product copy in the product editor, the\n // catalog, categories and discounts on the products hub, and copy for\n // an import as it lands. The commerce plugin hosts the zones and makes\n // every write; each card asks the jobs route about the release flag\n // before it shows anything.\n {\n slot: 'productEditor',\n widgetId: 'ai-product-copy',\n title: 'Product copy assistant',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiProductCopyCard,\n },\n {\n slot: 'productsHub',\n widgetId: 'ai-products-hub',\n title: 'Build your catalog with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiProductsHubCard,\n },\n {\n slot: 'productImport',\n widgetId: 'ai-product-import',\n title: 'Write imported product copy with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiProductImportOption,\n },\n // A/B tests by AI (AGL-2914): variants written in the experiment\n // editor, and one test's result read in plain words below its figures.\n // The A/B testing card hosts both zones, so they are drawn only where\n // that card is and only on a site with A/B testing; the shell holds the\n // plan band and `ai.generate`, and each card asks the jobs route about\n // the release flag before it shows anything. Neither writes: the\n // experiment editor's Save is the only write, and an explanation has\n // nothing to apply.\n {\n slot: 'experimentVariants',\n widgetId: 'ai-experiment-variants',\n title: 'Write variants with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiExperimentVariantsCard,\n },\n {\n slot: 'experimentResult',\n widgetId: 'ai-experiment-result',\n title: 'Explain this result with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiExperimentResultCard,\n },\n ],\n })\n}\n"],"names":["PLATFORM_BRAND_NAME","registerConsoleExtension","AiCollaboratorPermissionsCell","AiSaveAsComponent","AiGenerateSectionControl","AiRewriteControl","AiAssistProviderOnHost","AssistPanelOnHost","AiCreditsCard","AiJobsTopBarIndicator","AiCollaboratorCreditsCell","AiCollaboratorCreditsHeader","AiMemberCreditsCell","AiMemberCreditsHeader","AssistSignalsPage","BillingAssistOverageCard","AiTopUsersCard","AiAllotmentsCard","AiCollaboratorAllotmentCell","AiCollaboratorAllotmentHeader","AiSiteAllotmentCard","MemberAiAllotmentCard","MemberAiUsageCard","AiThemeProposalCard","StaffOrgAiCard","StaffOrgUsageAiCreditsCell","StaffOrgUsageAiOverageCell","StaffOrgUsageAiPool","StaffOrgUsageAssistCell","StaffOrgsAiSpendCell","StaffOrgsAiSpendHeader","StaffUserAiUsageCard","AiSeoAuditCard","AiSiteBatchCard","AiSiteSeoStartCard","AiSiteStartCard","AiSeoFieldsCard","AiDescribeAutomationButton","AiCrmEmailDraft","AiCrmImportMapping","AiCrmRecordCard","AiDescribePageButton","AiDescribeComponentButton","AiDescribeFormButton","AiDescribeLayoutButton","AiDescribeTemplateButton","AiExperimentResultCard","AiExperimentVariantsCard","AiExplainAutomation","AiExplainRunFailure","AiProductCopyCard","AiProductImportOption","AiProductsHubCard","AiJobsPage","AI_PLUGIN_ID","registerAiDeclarations","registerAiConsole","pluginId","displayName","providers","staffPages","id","label","header","title","docsTopic","Component","navItems","href","recordTitle","docsAnchor","unlisted","ownsSubtree","permission","widgets","slot","widgetId","featureFlag","column","align","Header"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,EAAEC,wBAAwB,QAAQ,eAAc;AAC5E,SAASC,6BAA6B,QAAQ,+DAA2D;AACzG,SAASC,iBAAiB,QAAQ,iDAA6C;AAC/E,SACEC,wBAAwB,EACxBC,gBAAgB,QACX,iDAA6C;AACpD,SACEC,sBAAsB,EACtBC,iBAAiB,QACZ,mDAA+C;AACtD,OAAOC,mBAAmB,4CAAwC;AAClE,OAAOC,2BAA2B,8CAA0C;AAC5E,SACEC,yBAAyB,EACzBC,2BAA2B,EAC3BC,mBAAmB,EACnBC,qBAAqB,QAChB,+CAA2C;AAClD,SAASC,iBAAiB,QAAQ,gDAA4C;AAC9E,OAAOC,8BAA8B,wDAAoD;AACzF,SAASC,cAAc,QAAQ,iDAA6C;AAC5E,SAASC,gBAAgB,QAAQ,kDAA8C;AAC/E,SACEC,2BAA2B,EAC3BC,6BAA6B,EAC7BC,mBAAmB,QACd,+CAA2C;AAClD,OAAOC,2BAA2B,qDAAiD;AACnF,OAAOC,uBAAuB,iDAA6C;AAC3E,OAAOC,yBAAyB,mDAA+C;AAC/E,OAAOC,oBAAoB,8CAA0C;AACrE,SACEC,0BAA0B,EAC1BC,0BAA0B,EAC1BC,mBAAmB,EACnBC,uBAAuB,QAClB,uDAAmD;AAC1D,SACEC,oBAAoB,EACpBC,sBAAsB,QACjB,uDAAmD;AAC1D,OAAOC,0BAA0B,qDAAiD;AAClF,OAAOC,oBAAoB,8CAA0C;AACrE,OAAOC,qBAAqB,+CAA2C;AACvE,OAAOC,wBAAwB,mDAA+C;AAC9E,OAAOC,qBAAqB,+CAA2C;AACvE,OAAOC,qBAAqB,+CAA2C;AACvE,OAAOC,gCAAgC,mDAA+C;AACtF,OAAOC,qBAAqB,+CAA2C;AACvE,OAAOC,wBAAwB,kDAA8C;AAC7E,OAAOC,qBAAqB,+CAA2C;AACvE,OAAOC,0BAA0B,6CAAyC;AAC1E,SACEC,yBAAyB,EACzBC,oBAAoB,EACpBC,sBAAsB,EACtBC,wBAAwB,QACnB,+CAA2C;AAClD,SACEC,sBAAsB,EACtBC,wBAAwB,QACnB,gDAA4C;AACnD,SACEC,mBAAmB,EACnBC,mBAAmB,QACd,kDAA8C;AACrD,OAAOC,uBAAuB,iDAA6C;AAC3E,OAAOC,2BAA2B,qDAAiD;AACnF,OAAOC,uBAAuB,iDAA6C;AAC3E,OAAOC,gBAAgB,yCAAqC;AAC5D,SAASC,YAAY,QAAQ,iBAAa;AAC1C,SAASC,sBAAsB,QAAQ,oBAAgB;AAEvD;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC;IACdD;IACAtD,yBAAyB;QACvBwD,UAAUH;QACVI,aAAa;QACb,wEAAwE;QACxE,qEAAqE;QACrEC,WAAW;YAACrD;SAAuB;QACnC,wEAAwE;QACxE,2CAA2C;QAC3CsD,YAAY;YACV;gBACEC,IAAI;gBACJC,OAAO;gBACPC,QAAQ;oBACN,gEAAgE;oBAChE,6DAA6D;oBAC7DC,OAAO,GAAGhE,oBAAoB,cAAc,CAAC;oBAC7CiE,WAAW;gBACb;gBACAC,WAAWpD;YACb;SACD;QACD,qEAAqE;QACrE,oEAAoE;QACpE,yEAAyE;QACzE,sEAAsE;QACtE,uEAAuE;QACvE,kEAAkE;QAClEqD,UAAU;YACR;gBACEL,OAAO;gBACPM,MAAM;gBACNC,aAAa;gBACbN,QAAQ;oBAAEC,OAAO;oBAAWC,WAAW;oBAAoBK,YAAY;gBAAwB;gBAC/FC,UAAU;gBACVC,aAAa;gBACbC,YAAY;gBACZP,WAAWb;YACb;SACD;QACDqB,SAAS;YACP;gBACEC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAW3D;YACb;YACA,qEAAqE;YACrE,wEAAwE;YACxE,uEAAuE;YACvE,uDAAuD;YACvD;gBACEoE,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWzD;YACb;YACA;gBACEkE,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAW1D;YACb;YACA;gBACEmE,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAWnD;YACb;YACA,kEAAkE;YAClE,kEAAkE;YAClE,oDAAoD;YACpD;gBACE4D,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPS,YAAY;gBACZP,WAAWlD;YACb;YACA,sEAAsE;YACtE,oEAAoE;YACpE,8BAA8B;YAC9B;gBACE2D,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPc,QAAQ;oBAAEf,QAAQ;gBAAK;gBACvBG,WAAWhE;YACb;YACA,uEAAuE;YACvE,mEAAmE;YACnE,sEAAsE;YACtE,2DAA2D;YAC3D;gBACEyE,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAW9D;YACb;YACA;gBACEuE,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAW7D;YACb;YACA,uEAAuE;YACvE,oEAAoE;YACpE,8DAA8D;YAC9D;gBACEsE,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPS,YAAY;gBACZP,WAAW/D;YACb;YACA;gBACEwE,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAW5C;YACb;YACA,qEAAqE;YACrE,mEAAmE;YACnE,gEAAgE;YAChE,6BAA6B;YAC7B;gBACEqD,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPS,YAAY;gBACZP,WAAWjD;YACb;YACA;gBACE0D,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAW7C;YACb;YACA;gBACEsD,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPc,QAAQ;oBACNf,QAAQ;oBACRgB,OAAO;oBACPC,QAAQ7D;gBACV;gBACA+C,WAAWhD;YACb;YACA;gBACEyD,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAW9C;YACb;YACA,kEAAkE;YAClE,oEAAoE;YACpE,4DAA4D;YAC5D,kEAAkE;YAClE,kEAAkE;YAClE,yCAAyC;YACzC;gBACEuD,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAW3C;YACb;YACA;gBACEoD,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAW1C;YACb;YACA;gBACEmD,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAWnC;YACb;YACA,oEAAoE;YACpE,oEAAoE;YACpE,yCAAyC;YACzC;gBACE4C,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPc,QAAQ;oBACNf,QAAQ;oBACRgB,OAAO;oBACPC,QAAQlD;gBACV;gBACAoC,WAAWrC;YACb;YACA;gBACE8C,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPc,QAAQ;oBAAEf,QAAQ;oBAAUgB,OAAO;gBAAQ;gBAC3Cb,WAAWtC;YACb;YACA;gBACE+C,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPc,QAAQ;oBAAEf,QAAQ;oBAAmBgB,OAAO;gBAAQ;gBACpDb,WAAWzC;YACb;YACA;gBACEkD,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPc,QAAQ;oBAAEf,QAAQ;oBAAyBgB,OAAO;gBAAQ;gBAC1Db,WAAWxC;YACb;YACA;gBACEiD,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAWvC;YACb;YACA;gBACEgD,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPc,QAAQ;oBAAEf,QAAQ;oBAAsBgB,OAAO;oBAASC,QAAQnE;gBAAsB;gBACtFqD,WAAWtD;YACb;YACA;gBACE+D,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPc,QAAQ;oBACNf,QAAQ;oBACRgB,OAAO;oBACPC,QAAQrE;gBACV;gBACAuD,WAAWxD;YACb;YACA,uEAAuE;YACvE,sEAAsE;YACtE,wEAAwE;YACxE,wEAAwE;YACxE,uEAAuE;YACvE,sEAAsE;YACtE;gBACEiE,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAW9B;YACb;YACA;gBACEuC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWlC;YACb;YACA,wEAAwE;YACxE,uEAAuE;YACvE,uEAAuE;YACvE;gBACE2C,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWzB;YACb;YACA,sEAAsE;YACtE,iEAAiE;YACjE,kEAAkE;YAClE,4BAA4B;YAC5B;gBACEkC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWrB;YACb;YACA;gBACE8B,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWtB;YACb;YACA;gBACE+B,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWvB;YACb;YACA,qEAAqE;YACrE,qEAAqE;YACrE,2BAA2B;YAC3B;gBACEgC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWxB;YACb;YACA,wEAAwE;YACxE,0EAA0E;YAC1E,sEAAsE;YACtE,qEAAqE;YACrE,kEAAkE;YAClE,oDAAoD;YACpD;gBACEiC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAW7B;YACb;YACA;gBACEsC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWlB;YACb;YACA;gBACE2B,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWjB;YACb;YACA,sEAAsE;YACtE,sEAAsE;YACtE,qEAAqE;YACrE,wEAAwE;YACxE,wEAAwE;YACxE,eAAe;YACf;gBACE0B,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAW1B;YACb;YACA;gBACEmC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAW5B;YACb;YACA;gBACEqC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAW3B;YACb;YACA,sEAAsE;YACtE,sEAAsE;YACtE,uEAAuE;YACvE,oEAAoE;YACpE,oEAAoE;YACpE,kDAAkD;YAClD;gBACEoC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAW/B;YACb;YACA,mEAAmE;YACnE,uEAAuE;YACvE,kEAAkE;YAClE,iEAAiE;YACjE,sEAAsE;YACtE,kBAAkB;YAClB;gBACEwC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWhC;YACb;YACA,kEAAkE;YAClE,qEAAqE;YACrE,oEAAoE;YACpE,sEAAsE;YACtE,cAAc;YACd;gBACEyC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWjC;YACb;YACA,qEAAqE;YACrE,sEAAsE;YACtE,uEAAuE;YACvE,oEAAoE;YACpE,4BAA4B;YAC5B;gBACE0C,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWhB;YACb;YACA;gBACEyB,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWd;YACb;YACA;gBACEuB,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWf;YACb;YACA,iEAAiE;YACjE,uEAAuE;YACvE,sEAAsE;YACtE,wEAAwE;YACxE,uEAAuE;YACvE,iEAAiE;YACjE,qEAAqE;YACrE,oBAAoB;YACpB;gBACEwB,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWnB;YACb;YACA;gBACE4B,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWpB;YACb;SACD;IACH;AACF"}
|
|
@@ -50,13 +50,16 @@ export declare const AI_IP_RATE_WINDOW_MS = 60000;
|
|
|
50
50
|
export declare function checkAiClientIpRateLimit(headers: ClientIpHeaders): RateLimitResult | null;
|
|
51
51
|
/**
|
|
52
52
|
* How old an account must be before a Free workspace it belongs to may
|
|
53
|
-
* generate (`AI_FREE_MIN_ACCOUNT_AGE_HOURS`, default
|
|
53
|
+
* generate (`AI_FREE_MIN_ACCOUNT_AGE_HOURS`, default 0 — off).
|
|
54
54
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
55
|
+
* Off by default (AGL-3591). A new site opens on Setup with the guided AI
|
|
56
|
+
* start over it, so a day-long wait refused a brand-new account at the
|
|
57
|
+
* exact moment it was invited to try the product. What bounds the Free
|
|
58
|
+
* taste's spend is the reservation — a 300-credit wall metered per account,
|
|
59
|
+
* per address and per day, under a platform-wide daily ceiling — not this
|
|
60
|
+
* rung. A deployment that wants a cooling-off period sets a positive
|
|
61
|
+
* number of hours; paid workspaces never consult it, and junk and an empty
|
|
62
|
+
* value take the default.
|
|
60
63
|
*/
|
|
61
64
|
export declare function aiFreeMinAccountAgeHours(): number;
|
|
62
65
|
/** How long one account's creation time is remembered per instance. */
|
|
@@ -53,17 +53,20 @@ export const AI_IP_RATE_WINDOW_MS = 60000;
|
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
55
|
* How old an account must be before a Free workspace it belongs to may
|
|
56
|
-
* generate (`AI_FREE_MIN_ACCOUNT_AGE_HOURS`, default
|
|
56
|
+
* generate (`AI_FREE_MIN_ACCOUNT_AGE_HOURS`, default 0 — off).
|
|
57
57
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
58
|
+
* Off by default (AGL-3591). A new site opens on Setup with the guided AI
|
|
59
|
+
* start over it, so a day-long wait refused a brand-new account at the
|
|
60
|
+
* exact moment it was invited to try the product. What bounds the Free
|
|
61
|
+
* taste's spend is the reservation — a 300-credit wall metered per account,
|
|
62
|
+
* per address and per day, under a platform-wide daily ceiling — not this
|
|
63
|
+
* rung. A deployment that wants a cooling-off period sets a positive
|
|
64
|
+
* number of hours; paid workspaces never consult it, and junk and an empty
|
|
65
|
+
* value take the default.
|
|
63
66
|
*/ export function aiFreeMinAccountAgeHours() {
|
|
64
67
|
const raw = process.env.AI_FREE_MIN_ACCOUNT_AGE_HOURS;
|
|
65
68
|
const parsed = raw ? Number(raw) : Number.NaN;
|
|
66
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed :
|
|
69
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
67
70
|
}
|
|
68
71
|
/** How long one account's creation time is remembered per instance. */ export const ACCOUNT_AGE_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
69
72
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-abuse-guards.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 { resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n readClientIp,\n type ClientIpHeaders,\n} from '@aglyn/aglyn/app-utils/request-ip'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { checkRateLimit, type RateLimitResult } from '@aglyn/tenant-data-admin/server/api-http'\n\n/**\n * The request-level rungs the Free AI taste adds to every AI door\n * (AGL-2925): a per-address limiter and a minimum account age. Both are\n * small helpers a door calls from its ladder rather than rungs written\n * into each ladder, so the three doors and the shared gate cannot drift\n * into three limits.\n *\n * Neither touches Firestore. The limiter is the same per-instance window\n * as the per-uid one and carries the same caveat — it smooths bursts and is\n * not the spend bound; the reservation is. The account-age rung reads one\n * Auth record, cached per instance, and applies only to a Free workspace.\n */\n\n/** Requests one client address may make against the AI doors a minute. */\nexport const AI_IP_RATE_LIMIT = 60\nexport const AI_IP_RATE_WINDOW_MS = 60_000\n\n/**\n * The per-address window shared by every AI door.\n *\n * Keyed on the TRUSTED-HOP client address (`readClientIp`), never on a\n * header a caller can write, for the reason that module exists: on an\n * appending proxy the leftmost `x-forwarded-for` hop is whatever the caller\n * typed, and a limiter keyed on it hands out a fresh budget per request.\n * `null` when no address is readable — the caller skips the rung, because\n * keying every anonymous request under one placeholder caps the whole\n * install at sixty requests a minute.\n *\n * Sixty a minute is three times the per-uid window (20/min at each door):\n * an office NAT signing in a team of three fits under it, and a farm\n * rotating accounts behind one address does not.\n */\nexport function checkAiClientIpRateLimit(\n headers: ClientIpHeaders,\n): RateLimitResult | null {\n const ip = readClientIp(headers)\n if (!ip) return null\n return checkRateLimit(`ai-ip:${ip}`, {\n limit: AI_IP_RATE_LIMIT,\n windowMs: AI_IP_RATE_WINDOW_MS,\n })\n}\n\n/**\n * How old an account must be before a Free workspace it belongs to may\n * generate (`AI_FREE_MIN_ACCOUNT_AGE_HOURS`, default 24).\n *\n * The signup surface already costs a working inbox per account; a day of\n * age costs a day, which is the one thing a script minting accounts cannot\n * buy in bulk. Paid workspaces never consult it. Zero switches the rung\n * off, which a self-hoster running an invite-only deployment may\n * reasonably want; junk and an empty value take the default.\n */\nexport function aiFreeMinAccountAgeHours(): number {\n const raw = process.env.AI_FREE_MIN_ACCOUNT_AGE_HOURS\n const parsed = raw ? Number(raw) : Number.NaN\n return Number.isFinite(parsed) && parsed >= 0 ? parsed : 24\n}\n\n/** How long one account's creation time is remembered per instance. */\nexport const ACCOUNT_AGE_CACHE_TTL_MS = 5 * 60 * 1000\n\n/**\n * Entries the cache holds before it is dropped whole. A creation time is a\n * few bytes, but a limiter that can be made to remember every uid ever\n * seen is a memory leak with a use case, so the map is bounded and simply\n * forgets everything when it fills — the next request re-reads.\n */\nexport const ACCOUNT_AGE_CACHE_MAX_ENTRIES = 5_000\n\nlet accountAgeCache = new Map<string, { creationTimeMs: number; at: number }>()\n\n/** Test seam — drops the per-instance cache. */\nexport function resetAccountAgeCache(): void {\n accountAgeCache = new Map()\n}\n\n/** The one thing this rung reads off an Auth record. */\nexport interface AccountAgeLookup {\n (uid: string): Promise<{ metadata: { creationTime?: string | null } }>\n}\n\n/**\n * When the account was created, in epoch milliseconds, or `null` when the\n * record carries no usable creation time.\n *\n * Read through the caller's `getUser` — the Auth pool the token was minted\n * in — because the decoded token carries `auth_time` (this sign-in) and\n * not the account's creation, and a fresh account can sign in as often as\n * it likes. Cached per instance for a few minutes: the value never changes,\n * and the doors are called far more often than once per account.\n */\nexport async function accountCreationTimeMs(\n uid: string,\n getUser: AccountAgeLookup,\n now = Date.now(),\n): Promise<number | null> {\n const cached = accountAgeCache.get(uid)\n if (cached && now - cached.at < ACCOUNT_AGE_CACHE_TTL_MS) {\n return cached.creationTimeMs\n }\n const record = await getUser(uid)\n const creationTimeMs = Date.parse(record.metadata?.creationTime ?? '')\n if (!Number.isFinite(creationTimeMs)) return null\n if (accountAgeCache.size >= ACCOUNT_AGE_CACHE_MAX_ENTRIES) resetAccountAgeCache()\n accountAgeCache.set(uid, { creationTimeMs, at: now })\n return creationTimeMs\n}\n\n/**\n * The account-age rung: the `Response` to send, or `null` to climb on.\n *\n * Applies to a FREE workspace only — `resolveEffectivePlan`, so a dead\n * subscription is Free here as everywhere — and never to staff, who are\n * the ones verifying the fix during an incident. An account younger than\n * the minimum is a 403 with `reason: 'account-age'`, and the sentence says\n * when to come back rather than hinting at a way around it.\n *\n * Fails CLOSED for a Free workspace: a record that cannot be read is a\n * 503, not an admission. The verifier just accepted this token from the\n * same Auth service, so the read failing is rare, and the rung exists\n * precisely for the request whose age cannot be vouched for. A paid\n * workspace never reaches the read.\n */\nexport async function freeAccountAgeRefusal(input: {\n uid: string\n org: Partial<AglynOrgBilling> | null | undefined\n staff: boolean\n getUser: AccountAgeLookup\n now?: Date\n}): Promise<Response | null> {\n if (input.staff) return null\n if (resolveEffectivePlan(input.org) !== 'free') return null\n const hours = aiFreeMinAccountAgeHours()\n if (hours <= 0) return null\n const now = input.now ?? new Date()\n let createdMs: number | null\n try {\n createdMs = await accountCreationTimeMs(input.uid, input.getUser, now.getTime())\n } catch (error) {\n console.error('[ai-abuse-guards] account age unavailable', error)\n return Response.json(\n { error: 'This feature is temporarily unavailable' },\n { status: 503 },\n )\n }\n const ageMs = createdMs === null ? 0 : now.getTime() - createdMs\n if (ageMs >= hours * 60 * 60 * 1000) return null\n const label = hours === 1 ? '1 hour' : `${hours} hours`\n return Response.json(\n {\n error:\n `AI generation opens ${label} after an account is created — ` +\n 'come back then, or upgrade this workspace to use it now.',\n reason: 'account-age',\n },\n { status: 403 },\n )\n}\n"],"names":["resolveEffectivePlan","readClientIp","checkRateLimit","AI_IP_RATE_LIMIT","AI_IP_RATE_WINDOW_MS","checkAiClientIpRateLimit","headers","ip","limit","windowMs","aiFreeMinAccountAgeHours","raw","process","env","AI_FREE_MIN_ACCOUNT_AGE_HOURS","parsed","Number","NaN","isFinite","ACCOUNT_AGE_CACHE_TTL_MS","ACCOUNT_AGE_CACHE_MAX_ENTRIES","accountAgeCache","Map","resetAccountAgeCache","accountCreationTimeMs","uid","getUser","now","Date","record","cached","get","at","creationTimeMs","parse","metadata","creationTime","size","set","freeAccountAgeRefusal","input","staff","org","hours","createdMs","getTime","error","console","Response","json","status","ageMs","label","reason"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,oBAAoB,QAAQ,2CAA0C;AAC/E,SACEC,YAAY,QAEP,oCAAmC;AAE1C,SAASC,cAAc,QAA8B,2CAA0C;AAE/F;;;;;;;;;;;CAWC,GAED,wEAAwE,GACxE,OAAO,MAAMC,mBAAmB,GAAE;AAClC,OAAO,MAAMC,uBAAuB,MAAM;AAE1C;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,yBACdC,OAAwB;IAExB,MAAMC,KAAKN,aAAaK;IACxB,IAAI,CAACC,IAAI,OAAO;IAChB,OAAOL,eAAe,CAAC,MAAM,EAAEK,IAAI,EAAE;QACnCC,OAAOL;QACPM,UAAUL;IACZ;AACF;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASM;IACd,MAAMC,MAAMC,QAAQC,GAAG,CAACC,6BAA6B;IACrD,MAAMC,SAASJ,MAAMK,OAAOL,OAAOK,OAAOC,GAAG;IAC7C,OAAOD,OAAOE,QAAQ,CAACH,WAAWA,UAAU,IAAIA,SAAS;AAC3D;AAEA,qEAAqE,GACrE,OAAO,MAAMI,2BAA2B,IAAI,KAAK,KAAI;AAErD;;;;;CAKC,GACD,OAAO,MAAMC,gCAAgC,KAAK;AAElD,IAAIC,kBAAkB,IAAIC;AAE1B,8CAA8C,GAC9C,OAAO,SAASC;IACdF,kBAAkB,IAAIC;AACxB;AAOA;;;;;;;;;CASC,GACD,OAAO,eAAeE,sBACpBC,GAAW,EACXC,OAAyB,EACzBC,MAAMC,KAAKD,GAAG,EAAE;;QAOkBE;IALlC,MAAMC,SAAST,gBAAgBU,GAAG,CAACN;IACnC,IAAIK,UAAUH,MAAMG,OAAOE,EAAE,GAAGb,0BAA0B;QACxD,OAAOW,OAAOG,cAAc;IAC9B;IACA,MAAMJ,SAAS,MAAMH,QAAQD;IAC7B,MAAMQ,iBAAiBL,KAAKM,KAAK,UAACL,mBAAAA,OAAOM,QAAQ,qBAAfN,iBAAiBO,YAAY,mBAAI;IACnE,IAAI,CAACpB,OAAOE,QAAQ,CAACe,iBAAiB,OAAO;IAC7C,IAAIZ,gBAAgBgB,IAAI,IAAIjB,+BAA+BG;IAC3DF,gBAAgBiB,GAAG,CAACb,KAAK;QAAEQ;QAAgBD,IAAIL;IAAI;IACnD,OAAOM;AACT;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,eAAeM,sBAAsBC,KAM3C;QAKaA;IAJZ,IAAIA,MAAMC,KAAK,EAAE,OAAO;IACxB,IAAIzC,qBAAqBwC,MAAME,GAAG,MAAM,QAAQ,OAAO;IACvD,MAAMC,QAAQjC;IACd,IAAIiC,SAAS,GAAG,OAAO;IACvB,MAAMhB,OAAMa,aAAAA,MAAMb,GAAG,YAATa,aAAa,IAAIZ;IAC7B,IAAIgB;IACJ,IAAI;QACFA,YAAY,MAAMpB,sBAAsBgB,MAAMf,GAAG,EAAEe,MAAMd,OAAO,EAAEC,IAAIkB,OAAO;IAC/E,EAAE,OAAOC,OAAO;QACdC,QAAQD,KAAK,CAAC,6CAA6CA;QAC3D,OAAOE,SAASC,IAAI,CAClB;YAAEH,OAAO;QAA0C,GACnD;YAAEI,QAAQ;QAAI;IAElB;IACA,MAAMC,QAAQP,cAAc,OAAO,IAAIjB,IAAIkB,OAAO,KAAKD;IACvD,IAAIO,SAASR,QAAQ,KAAK,KAAK,MAAM,OAAO;IAC5C,MAAMS,QAAQT,UAAU,IAAI,WAAW,GAAGA,MAAM,MAAM,CAAC;IACvD,OAAOK,SAASC,IAAI,CAClB;QACEH,OACE,CAAC,oBAAoB,EAAEM,MAAM,+BAA+B,CAAC,GAC7D;QACFC,QAAQ;IACV,GACA;QAAEH,QAAQ;IAAI;AAElB"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-abuse-guards.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 { resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n readClientIp,\n type ClientIpHeaders,\n} from '@aglyn/aglyn/app-utils/request-ip'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { checkRateLimit, type RateLimitResult } from '@aglyn/tenant-data-admin/server/api-http'\n\n/**\n * The request-level rungs the Free AI taste adds to every AI door\n * (AGL-2925): a per-address limiter and a minimum account age. Both are\n * small helpers a door calls from its ladder rather than rungs written\n * into each ladder, so the three doors and the shared gate cannot drift\n * into three limits.\n *\n * Neither touches Firestore. The limiter is the same per-instance window\n * as the per-uid one and carries the same caveat — it smooths bursts and is\n * not the spend bound; the reservation is. The account-age rung reads one\n * Auth record, cached per instance, and applies only to a Free workspace.\n */\n\n/** Requests one client address may make against the AI doors a minute. */\nexport const AI_IP_RATE_LIMIT = 60\nexport const AI_IP_RATE_WINDOW_MS = 60_000\n\n/**\n * The per-address window shared by every AI door.\n *\n * Keyed on the TRUSTED-HOP client address (`readClientIp`), never on a\n * header a caller can write, for the reason that module exists: on an\n * appending proxy the leftmost `x-forwarded-for` hop is whatever the caller\n * typed, and a limiter keyed on it hands out a fresh budget per request.\n * `null` when no address is readable — the caller skips the rung, because\n * keying every anonymous request under one placeholder caps the whole\n * install at sixty requests a minute.\n *\n * Sixty a minute is three times the per-uid window (20/min at each door):\n * an office NAT signing in a team of three fits under it, and a farm\n * rotating accounts behind one address does not.\n */\nexport function checkAiClientIpRateLimit(\n headers: ClientIpHeaders,\n): RateLimitResult | null {\n const ip = readClientIp(headers)\n if (!ip) return null\n return checkRateLimit(`ai-ip:${ip}`, {\n limit: AI_IP_RATE_LIMIT,\n windowMs: AI_IP_RATE_WINDOW_MS,\n })\n}\n\n/**\n * How old an account must be before a Free workspace it belongs to may\n * generate (`AI_FREE_MIN_ACCOUNT_AGE_HOURS`, default 0 — off).\n *\n * Off by default (AGL-3591). A new site opens on Setup with the guided AI\n * start over it, so a day-long wait refused a brand-new account at the\n * exact moment it was invited to try the product. What bounds the Free\n * taste's spend is the reservation — a 300-credit wall metered per account,\n * per address and per day, under a platform-wide daily ceiling — not this\n * rung. A deployment that wants a cooling-off period sets a positive\n * number of hours; paid workspaces never consult it, and junk and an empty\n * value take the default.\n */\nexport function aiFreeMinAccountAgeHours(): number {\n const raw = process.env.AI_FREE_MIN_ACCOUNT_AGE_HOURS\n const parsed = raw ? Number(raw) : Number.NaN\n return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0\n}\n\n/** How long one account's creation time is remembered per instance. */\nexport const ACCOUNT_AGE_CACHE_TTL_MS = 5 * 60 * 1000\n\n/**\n * Entries the cache holds before it is dropped whole. A creation time is a\n * few bytes, but a limiter that can be made to remember every uid ever\n * seen is a memory leak with a use case, so the map is bounded and simply\n * forgets everything when it fills — the next request re-reads.\n */\nexport const ACCOUNT_AGE_CACHE_MAX_ENTRIES = 5_000\n\nlet accountAgeCache = new Map<string, { creationTimeMs: number; at: number }>()\n\n/** Test seam — drops the per-instance cache. */\nexport function resetAccountAgeCache(): void {\n accountAgeCache = new Map()\n}\n\n/** The one thing this rung reads off an Auth record. */\nexport interface AccountAgeLookup {\n (uid: string): Promise<{ metadata: { creationTime?: string | null } }>\n}\n\n/**\n * When the account was created, in epoch milliseconds, or `null` when the\n * record carries no usable creation time.\n *\n * Read through the caller's `getUser` — the Auth pool the token was minted\n * in — because the decoded token carries `auth_time` (this sign-in) and\n * not the account's creation, and a fresh account can sign in as often as\n * it likes. Cached per instance for a few minutes: the value never changes,\n * and the doors are called far more often than once per account.\n */\nexport async function accountCreationTimeMs(\n uid: string,\n getUser: AccountAgeLookup,\n now = Date.now(),\n): Promise<number | null> {\n const cached = accountAgeCache.get(uid)\n if (cached && now - cached.at < ACCOUNT_AGE_CACHE_TTL_MS) {\n return cached.creationTimeMs\n }\n const record = await getUser(uid)\n const creationTimeMs = Date.parse(record.metadata?.creationTime ?? '')\n if (!Number.isFinite(creationTimeMs)) return null\n if (accountAgeCache.size >= ACCOUNT_AGE_CACHE_MAX_ENTRIES) resetAccountAgeCache()\n accountAgeCache.set(uid, { creationTimeMs, at: now })\n return creationTimeMs\n}\n\n/**\n * The account-age rung: the `Response` to send, or `null` to climb on.\n *\n * Applies to a FREE workspace only — `resolveEffectivePlan`, so a dead\n * subscription is Free here as everywhere — and never to staff, who are\n * the ones verifying the fix during an incident. An account younger than\n * the minimum is a 403 with `reason: 'account-age'`, and the sentence says\n * when to come back rather than hinting at a way around it.\n *\n * Fails CLOSED for a Free workspace: a record that cannot be read is a\n * 503, not an admission. The verifier just accepted this token from the\n * same Auth service, so the read failing is rare, and the rung exists\n * precisely for the request whose age cannot be vouched for. A paid\n * workspace never reaches the read.\n */\nexport async function freeAccountAgeRefusal(input: {\n uid: string\n org: Partial<AglynOrgBilling> | null | undefined\n staff: boolean\n getUser: AccountAgeLookup\n now?: Date\n}): Promise<Response | null> {\n if (input.staff) return null\n if (resolveEffectivePlan(input.org) !== 'free') return null\n const hours = aiFreeMinAccountAgeHours()\n if (hours <= 0) return null\n const now = input.now ?? new Date()\n let createdMs: number | null\n try {\n createdMs = await accountCreationTimeMs(input.uid, input.getUser, now.getTime())\n } catch (error) {\n console.error('[ai-abuse-guards] account age unavailable', error)\n return Response.json(\n { error: 'This feature is temporarily unavailable' },\n { status: 503 },\n )\n }\n const ageMs = createdMs === null ? 0 : now.getTime() - createdMs\n if (ageMs >= hours * 60 * 60 * 1000) return null\n const label = hours === 1 ? '1 hour' : `${hours} hours`\n return Response.json(\n {\n error:\n `AI generation opens ${label} after an account is created — ` +\n 'come back then, or upgrade this workspace to use it now.',\n reason: 'account-age',\n },\n { status: 403 },\n )\n}\n"],"names":["resolveEffectivePlan","readClientIp","checkRateLimit","AI_IP_RATE_LIMIT","AI_IP_RATE_WINDOW_MS","checkAiClientIpRateLimit","headers","ip","limit","windowMs","aiFreeMinAccountAgeHours","raw","process","env","AI_FREE_MIN_ACCOUNT_AGE_HOURS","parsed","Number","NaN","isFinite","ACCOUNT_AGE_CACHE_TTL_MS","ACCOUNT_AGE_CACHE_MAX_ENTRIES","accountAgeCache","Map","resetAccountAgeCache","accountCreationTimeMs","uid","getUser","now","Date","record","cached","get","at","creationTimeMs","parse","metadata","creationTime","size","set","freeAccountAgeRefusal","input","staff","org","hours","createdMs","getTime","error","console","Response","json","status","ageMs","label","reason"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,oBAAoB,QAAQ,2CAA0C;AAC/E,SACEC,YAAY,QAEP,oCAAmC;AAE1C,SAASC,cAAc,QAA8B,2CAA0C;AAE/F;;;;;;;;;;;CAWC,GAED,wEAAwE,GACxE,OAAO,MAAMC,mBAAmB,GAAE;AAClC,OAAO,MAAMC,uBAAuB,MAAM;AAE1C;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,yBACdC,OAAwB;IAExB,MAAMC,KAAKN,aAAaK;IACxB,IAAI,CAACC,IAAI,OAAO;IAChB,OAAOL,eAAe,CAAC,MAAM,EAAEK,IAAI,EAAE;QACnCC,OAAOL;QACPM,UAAUL;IACZ;AACF;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASM;IACd,MAAMC,MAAMC,QAAQC,GAAG,CAACC,6BAA6B;IACrD,MAAMC,SAASJ,MAAMK,OAAOL,OAAOK,OAAOC,GAAG;IAC7C,OAAOD,OAAOE,QAAQ,CAACH,WAAWA,UAAU,IAAIA,SAAS;AAC3D;AAEA,qEAAqE,GACrE,OAAO,MAAMI,2BAA2B,IAAI,KAAK,KAAI;AAErD;;;;;CAKC,GACD,OAAO,MAAMC,gCAAgC,KAAK;AAElD,IAAIC,kBAAkB,IAAIC;AAE1B,8CAA8C,GAC9C,OAAO,SAASC;IACdF,kBAAkB,IAAIC;AACxB;AAOA;;;;;;;;;CASC,GACD,OAAO,eAAeE,sBACpBC,GAAW,EACXC,OAAyB,EACzBC,MAAMC,KAAKD,GAAG,EAAE;;QAOkBE;IALlC,MAAMC,SAAST,gBAAgBU,GAAG,CAACN;IACnC,IAAIK,UAAUH,MAAMG,OAAOE,EAAE,GAAGb,0BAA0B;QACxD,OAAOW,OAAOG,cAAc;IAC9B;IACA,MAAMJ,SAAS,MAAMH,QAAQD;IAC7B,MAAMQ,iBAAiBL,KAAKM,KAAK,UAACL,mBAAAA,OAAOM,QAAQ,qBAAfN,iBAAiBO,YAAY,mBAAI;IACnE,IAAI,CAACpB,OAAOE,QAAQ,CAACe,iBAAiB,OAAO;IAC7C,IAAIZ,gBAAgBgB,IAAI,IAAIjB,+BAA+BG;IAC3DF,gBAAgBiB,GAAG,CAACb,KAAK;QAAEQ;QAAgBD,IAAIL;IAAI;IACnD,OAAOM;AACT;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,eAAeM,sBAAsBC,KAM3C;QAKaA;IAJZ,IAAIA,MAAMC,KAAK,EAAE,OAAO;IACxB,IAAIzC,qBAAqBwC,MAAME,GAAG,MAAM,QAAQ,OAAO;IACvD,MAAMC,QAAQjC;IACd,IAAIiC,SAAS,GAAG,OAAO;IACvB,MAAMhB,OAAMa,aAAAA,MAAMb,GAAG,YAATa,aAAa,IAAIZ;IAC7B,IAAIgB;IACJ,IAAI;QACFA,YAAY,MAAMpB,sBAAsBgB,MAAMf,GAAG,EAAEe,MAAMd,OAAO,EAAEC,IAAIkB,OAAO;IAC/E,EAAE,OAAOC,OAAO;QACdC,QAAQD,KAAK,CAAC,6CAA6CA;QAC3D,OAAOE,SAASC,IAAI,CAClB;YAAEH,OAAO;QAA0C,GACnD;YAAEI,QAAQ;QAAI;IAElB;IACA,MAAMC,QAAQP,cAAc,OAAO,IAAIjB,IAAIkB,OAAO,KAAKD;IACvD,IAAIO,SAASR,QAAQ,KAAK,KAAK,MAAM,OAAO;IAC5C,MAAMS,QAAQT,UAAU,IAAI,WAAW,GAAGA,MAAM,MAAM,CAAC;IACvD,OAAOK,SAASC,IAAI,CAClB;QACEH,OACE,CAAC,oBAAoB,EAAEM,MAAM,+BAA+B,CAAC,GAC7D;QACFC,QAAQ;IACV,GACA;QAAEH,QAAQ;IAAI;AAElB"}
|
|
@@ -108,10 +108,10 @@ export interface AiDoctrineTreeContext extends AiNodeTreeContext {
|
|
|
108
108
|
plannedEmbeds?: readonly Pick<AiBuildPlanEmbed, 'host' | 'url'>[];
|
|
109
109
|
framing?: AiCopyFraming;
|
|
110
110
|
/**
|
|
111
|
-
* Whether the workspace keeps reusable components
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
111
|
+
* Whether the workspace keeps reusable components (AGL-3030). `false`
|
|
112
|
+
* draws a repeated block where it repeats. Absent is `true`, the doctrine
|
|
113
|
+
* whole. Forms do not read it: a form is a saved form placed by its id on
|
|
114
|
+
* every plan, counted against `formsPerHost`, or none (AGL-3596).
|
|
115
115
|
*/
|
|
116
116
|
reusableComponents?: boolean;
|
|
117
117
|
/**
|
|
@@ -251,12 +251,14 @@ export declare function detectLayoutRegions(tree: AiDoctrineTree, outputKind: Ai
|
|
|
251
251
|
* the page never renders: the placed form's own design replaces them) are
|
|
252
252
|
* refused. A form's own design is the one place fields are drawn.
|
|
253
253
|
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
254
|
+
* On every plan (AGL-3596). A workspace that can save no form once drew an
|
|
255
|
+
* unbound Form holding its fields (AGL-3030): inputs on a page with no saved
|
|
256
|
+
* form behind them, no routing, no consent and nothing in the Forms list —
|
|
257
|
+
* which is what a guided start shipped on its first production run. A page
|
|
258
|
+
* with no saved form to place now places no form, so there is no exception
|
|
259
|
+
* left to read: `reusableComponents` decides repeats only.
|
|
258
260
|
*/
|
|
259
|
-
export declare function detectInlineForms(tree: AiDoctrineTree, outputKind: AiOutputKind
|
|
261
|
+
export declare function detectInlineForms(tree: AiDoctrineTree, outputKind: AiOutputKind): AiDoctrineViolation[];
|
|
260
262
|
/**
|
|
261
263
|
* Rule 5. Every color, spacing, radius and type size is a theme token. A
|
|
262
264
|
* color the theme lacks is a theme change for the theme door to draft, not a
|
|
@@ -629,9 +631,11 @@ export declare function detectPlanSplitLists(plan: AiBuildPlan, inventory: AiSit
|
|
|
629
631
|
*/
|
|
630
632
|
export declare function detectPlanLayoutRegions(plan: AiBuildPlan, inventory: AiSiteInventory | null, capabilities?: AiPlanCapabilities | null): AiDoctrineViolation[];
|
|
631
633
|
/**
|
|
632
|
-
* Rule 3 (plan): a section that collects answers places a form
|
|
633
|
-
*
|
|
634
|
-
* (
|
|
634
|
+
* Rule 3 (plan): a section that collects answers places a saved form — one
|
|
635
|
+
* the site has, or one the plan creates on the Forms page. Where neither is
|
|
636
|
+
* possible (the site has no form and this job may create none), the page
|
|
637
|
+
* places no form, and a section named for one is not refused: there is
|
|
638
|
+
* nothing it could place (AGL-3596). It is never drawn inline instead.
|
|
635
639
|
*/
|
|
636
640
|
export declare function detectPlanInlineForms(plan: AiBuildPlan, inventory: AiSiteInventory | null, capabilities?: AiPlanCapabilities | null): AiDoctrineViolation[];
|
|
637
641
|
/** Pages sharing one shape this many times are a template applied. */
|
|
@@ -23,7 +23,8 @@ import { REUSABLE_INSTANCE_COMPONENT_ID, REUSABLE_INSTANCE_PROP_VALUES_KEY } fro
|
|
|
23
23
|
import { parseBreakpointSpan } from "@aglyn/shared-data-enums/breakpoint-span";
|
|
24
24
|
import { renderEmailHtml } from "@aglyn/shared-util-email/email-render";
|
|
25
25
|
import { AI_BUILD_PLAN_CREATION_NOUNS, AI_BUILD_PLAN_EMBED_HOST_NAMES, AI_PLAN_ASKS_FOR_VIDEO, aiEmbedHostOf, aiEmbedVideoKey, aiPlanCreateFor, aiPlanSlugKey, aiPlanUndeclaredRefs, isAiPlanNewRef } from "../model/ai-build-plan.js";
|
|
26
|
-
import { aiCreationNoun, aiPlanUncreatable, aiPlanUncreatableKind } from "../model/ai-plan-capabilities.js";
|
|
26
|
+
import { aiCreationNoun, aiPlanCanPlaceForm, aiPlanUncreatable, aiPlanUncreatableKind } from "../model/ai-plan-capabilities.js";
|
|
27
|
+
import { aiFreeSiteSectionsWithin } from "../model/ai-site-job.js";
|
|
27
28
|
import { AI_INSTANCE_REF_PROP, validateAiNodeTree } from "./ai-node-tree.js";
|
|
28
29
|
import { AI_PAGE_SCROLL_TO_PROP, aiPageLinkTarget, aiStoredScrollTargets } from "./ai-page-links.js";
|
|
29
30
|
import { AI_EMAIL_CLIP_BYTES, AI_OUTPUT_BUDGETS, AI_OUTPUT_SURFACE, isHeadlineVariant } from "./ai-palette.js";
|
|
@@ -432,19 +433,18 @@ export const AI_LAYOUT_REGION_NAMES = Object.keys(AI_LAYOUT_REGIONS);
|
|
|
432
433
|
* the page never renders: the placed form's own design replaces them) are
|
|
433
434
|
* refused. A form's own design is the one place fields are drawn.
|
|
434
435
|
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
|
|
436
|
+
* On every plan (AGL-3596). A workspace that can save no form once drew an
|
|
437
|
+
* unbound Form holding its fields (AGL-3030): inputs on a page with no saved
|
|
438
|
+
* form behind them, no routing, no consent and nothing in the Forms list —
|
|
439
|
+
* which is what a guided start shipped on its first production run. A page
|
|
440
|
+
* with no saved form to place now places no form, so there is no exception
|
|
441
|
+
* left to read: `reusableComponents` decides repeats only.
|
|
442
|
+
*/ export function detectInlineForms(tree, outputKind) {
|
|
440
443
|
if (outputKind === 'form' || outputKind === 'email') return [];
|
|
441
|
-
const inline = context.reusableComponents === false;
|
|
442
444
|
const loose = [];
|
|
443
445
|
const unbound = [];
|
|
444
446
|
const shadowed = [];
|
|
445
|
-
const
|
|
446
|
-
const visits = walkTree(tree);
|
|
447
|
-
for (const visit of visits){
|
|
447
|
+
for (const visit of walkTree(tree)){
|
|
448
448
|
if (visit.node.componentId === 'formField' && !hasAncestor(tree, visit, (node)=>node.componentId === 'form')) {
|
|
449
449
|
loose.push(visit.id);
|
|
450
450
|
}
|
|
@@ -454,35 +454,17 @@ export const AI_LAYOUT_REGION_NAMES = Object.keys(AI_LAYOUT_REGIONS);
|
|
|
454
454
|
if (typeof bound === 'string' && bound) {
|
|
455
455
|
var _visit_node_nodes;
|
|
456
456
|
if (((_visit_node_nodes = visit.node.nodes) != null ? _visit_node_nodes : []).length) shadowed.push(visit.id);
|
|
457
|
-
} else
|
|
457
|
+
} else {
|
|
458
458
|
unbound.push(visit.id);
|
|
459
|
-
} else if (!visits.some((inner)=>inner.node.componentId === 'formField' && inner.ancestors.includes(visit.id))) {
|
|
460
|
-
empty.push(visit.id);
|
|
461
459
|
}
|
|
462
460
|
}
|
|
463
461
|
}
|
|
464
462
|
const violations = [];
|
|
465
|
-
if (
|
|
466
|
-
violations.push({
|
|
467
|
-
rule: 3,
|
|
468
|
-
code: 'loose-form-field',
|
|
469
|
-
message: 'This draws form fields outside a form, where nothing sends them. Put every Form Field inside the Form it belongs to.',
|
|
470
|
-
nodeIds: loose
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
if (empty.length) {
|
|
474
|
-
violations.push({
|
|
475
|
-
rule: 3,
|
|
476
|
-
code: 'form-without-fields',
|
|
477
|
-
message: 'This form has no field to send. Give it the Form Fields it collects.',
|
|
478
|
-
nodeIds: empty
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
if (!inline && (loose.length || unbound.length)) {
|
|
463
|
+
if (loose.length || unbound.length) {
|
|
482
464
|
violations.push({
|
|
483
465
|
rule: 3,
|
|
484
466
|
code: 'inline-form',
|
|
485
|
-
message: 'This draws a form inline.
|
|
467
|
+
message: 'This draws a form inline. Place a saved form from the Forms page by its id, or, where the site has none to place, leave the form off the page.',
|
|
486
468
|
nodeIds: [
|
|
487
469
|
...unbound,
|
|
488
470
|
...loose
|
|
@@ -2389,7 +2371,7 @@ const BUDGET_METRICS = [
|
|
|
2389
2371
|
...publish,
|
|
2390
2372
|
...detectRepeatedSubtrees(tree, otherPages, context),
|
|
2391
2373
|
...detectLayoutRegions(tree, outputKind),
|
|
2392
|
-
...detectInlineForms(tree, outputKind
|
|
2374
|
+
...detectInlineForms(tree, outputKind),
|
|
2393
2375
|
...detectLiteralStyles(tree, outputKind),
|
|
2394
2376
|
...detectInvisibleLinks(tree, outputKind),
|
|
2395
2377
|
...detectOffBrandEmail(tree, outputKind, context.brand),
|
|
@@ -2684,11 +2666,13 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
|
|
|
2684
2666
|
return violations;
|
|
2685
2667
|
}
|
|
2686
2668
|
/**
|
|
2687
|
-
* Rule 3 (plan): a section that collects answers places a form
|
|
2688
|
-
*
|
|
2689
|
-
* (
|
|
2669
|
+
* Rule 3 (plan): a section that collects answers places a saved form — one
|
|
2670
|
+
* the site has, or one the plan creates on the Forms page. Where neither is
|
|
2671
|
+
* possible (the site has no form and this job may create none), the page
|
|
2672
|
+
* places no form, and a section named for one is not refused: there is
|
|
2673
|
+
* nothing it could place (AGL-3596). It is never drawn inline instead.
|
|
2690
2674
|
*/ export function detectPlanInlineForms(plan, inventory, capabilities = null) {
|
|
2691
|
-
if ((
|
|
2675
|
+
if (!aiPlanCanPlaceForm(inventory, capabilities)) return [];
|
|
2692
2676
|
const kinds = inventoryKinds(inventory);
|
|
2693
2677
|
const unbound = sectionPaths(plan).filter(({ section })=>FORM_SECTION_NAME.test(section.name) && !section.uses.some((ref)=>refKind(ref, plan, kinds) === 'form'));
|
|
2694
2678
|
return unbound.length ? [
|
|
@@ -2976,10 +2960,13 @@ function slugKey(slug) {
|
|
|
2976
2960
|
/** Rule 10 (plan): every screen has a free, well-formed slug, an SEO title and description. */ export function detectMissingNavAndSeo(plan, inventory) {
|
|
2977
2961
|
var _ref;
|
|
2978
2962
|
const violations = [];
|
|
2979
|
-
|
|
2963
|
+
// The starter home a planned home page replaces (AGL-3594) keeps no
|
|
2964
|
+
// address from the plan: publishing the page planned at `/` retires it.
|
|
2965
|
+
const taken = new Map(((_ref = inventory == null ? void 0 : inventory.screens) != null ? _ref : []).filter((screen)=>!screen.replaceable).map((screen)=>[
|
|
2980
2966
|
slugKey(screen.slug),
|
|
2981
2967
|
screen.name
|
|
2982
2968
|
]));
|
|
2969
|
+
const clashes = new Set();
|
|
2983
2970
|
const seen = new Set();
|
|
2984
2971
|
const badSlugs = [];
|
|
2985
2972
|
const collisions = [];
|
|
@@ -2990,6 +2977,7 @@ function slugKey(slug) {
|
|
|
2990
2977
|
badSlugs.push(`screens[${index}].slug`);
|
|
2991
2978
|
} else if (seen.has(key) || taken.has(key)) {
|
|
2992
2979
|
collisions.push(`screens[${index}].slug`);
|
|
2980
|
+
clashes.add(`/${key}`);
|
|
2993
2981
|
}
|
|
2994
2982
|
seen.add(key);
|
|
2995
2983
|
if (!screen.seoTitle || screen.seoTitle.length > AI_SEO_TITLE_MAX) {
|
|
@@ -3011,7 +2999,10 @@ function slugKey(slug) {
|
|
|
3011
2999
|
violations.push({
|
|
3012
3000
|
rule: 10,
|
|
3013
3001
|
code: 'plan-slug-taken',
|
|
3014
|
-
|
|
3002
|
+
// Names the addresses, so the one re-ask can move the page off them.
|
|
3003
|
+
message: `A page reuses an address the site or the plan already uses (${[
|
|
3004
|
+
...clashes
|
|
3005
|
+
].join(', ')}). Give each page its own slug.`,
|
|
3015
3006
|
paths: collisions
|
|
3016
3007
|
});
|
|
3017
3008
|
}
|
|
@@ -3061,7 +3052,8 @@ function slugKey(slug) {
|
|
|
3061
3052
|
}
|
|
3062
3053
|
return;
|
|
3063
3054
|
}
|
|
3064
|
-
|
|
3055
|
+
// A starter home is replaced, not duplicated (AGL-3594).
|
|
3056
|
+
const nearest = ((_ref = inventory == null ? void 0 : inventory.screens) != null ? _ref : []).find((existing)=>!existing.template && !existing.replaceable && aiNamesMatch(existing.name, screen.title));
|
|
3065
3057
|
if (nearest) {
|
|
3066
3058
|
violations.push({
|
|
3067
3059
|
rule: 15,
|
|
@@ -3157,7 +3149,25 @@ function slugKey(slug) {
|
|
|
3157
3149
|
const asked = plan.screens.reduce((sum, screen)=>sum + screen.sections.length, 0);
|
|
3158
3150
|
const layouts = freePlanLayouts(plan, inventory, capabilities);
|
|
3159
3151
|
const pages = Math.max(plan.screens.length, 1);
|
|
3160
|
-
|
|
3152
|
+
// A Free site scaffold (AGL-3594) is held to its page cap first, and then to
|
|
3153
|
+
// what a SITE plan's worst case leaves for sections, with room for a retry.
|
|
3154
|
+
const sitePages = capabilities.freeSitePages;
|
|
3155
|
+
if (sitePages !== undefined && plan.screens.length > sitePages) {
|
|
3156
|
+
return [
|
|
3157
|
+
{
|
|
3158
|
+
rule: null,
|
|
3159
|
+
code: 'plan-over-free-wall',
|
|
3160
|
+
message: `This plan builds ${plan.screens.length} pages, and a Free workspace's site start builds at most ${sitePages}. Plan the home page and the one page the brief most needs.`,
|
|
3161
|
+
paths: [
|
|
3162
|
+
'screens'
|
|
3163
|
+
]
|
|
3164
|
+
}
|
|
3165
|
+
];
|
|
3166
|
+
}
|
|
3167
|
+
const fits = sitePages !== undefined ? aiFreeSiteSectionsWithin({
|
|
3168
|
+
layouts,
|
|
3169
|
+
pages
|
|
3170
|
+
}, FREE_AI_TASTE_CREDITS_PER_MONTH) : aiFreePageSectionsWithin({
|
|
3161
3171
|
layouts,
|
|
3162
3172
|
pages
|
|
3163
3173
|
});
|