@aglyn/aglyn 1.0.0-beta.269 → 1.0.0-beta.271

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/account-preferences.d.ts +248 -0
  3. package/src/lib/app-utils/account-preferences.js +434 -0
  4. package/src/lib/app-utils/account-preferences.js.map +1 -0
  5. package/src/lib/app-utils/activity-labels.js +2 -0
  6. package/src/lib/app-utils/activity-labels.js.map +1 -1
  7. package/src/lib/app-utils/analytics-events.d.ts +13 -0
  8. package/src/lib/app-utils/analytics-events.js +2 -0
  9. package/src/lib/app-utils/analytics-events.js.map +1 -1
  10. package/src/lib/app-utils/append-only-history.d.ts +94 -0
  11. package/src/lib/app-utils/append-only-history.js +73 -0
  12. package/src/lib/app-utils/append-only-history.js.map +1 -0
  13. package/src/lib/app-utils/bundle-intent.d.ts +186 -0
  14. package/src/lib/app-utils/bundle-intent.js +386 -0
  15. package/src/lib/app-utils/bundle-intent.js.map +1 -0
  16. package/src/lib/app-utils/client-build.d.ts +204 -0
  17. package/src/lib/app-utils/client-build.js +237 -0
  18. package/src/lib/app-utils/client-build.js.map +1 -0
  19. package/src/lib/app-utils/console-routes.d.ts +4 -0
  20. package/src/lib/app-utils/console-routes.js +2 -0
  21. package/src/lib/app-utils/console-routes.js.map +1 -1
  22. package/src/lib/app-utils/country-options.d.ts +27 -0
  23. package/src/lib/app-utils/country-options.js +96 -0
  24. package/src/lib/app-utils/country-options.js.map +1 -0
  25. package/src/lib/app-utils/docs-help-sections.generated.js +7 -5
  26. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
  27. package/src/lib/app-utils/docs-help.generated.d.ts +11 -5
  28. package/src/lib/app-utils/docs-help.generated.js +22 -2
  29. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  30. package/src/lib/app-utils/docs-index.generated.js +116 -39
  31. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  32. package/src/lib/app-utils/margin-utilization.js +6 -4
  33. package/src/lib/app-utils/margin-utilization.js.map +1 -1
  34. package/src/lib/app-utils/media-filter.d.ts +26 -0
  35. package/src/lib/app-utils/media-filter.js +32 -0
  36. package/src/lib/app-utils/media-filter.js.map +1 -1
  37. package/src/lib/app-utils/mobile-push.d.ts +8 -0
  38. package/src/lib/app-utils/mobile-push.js +4 -1
  39. package/src/lib/app-utils/mobile-push.js.map +1 -1
  40. package/src/lib/app-utils/native-app-association.d.ts +3 -2
  41. package/src/lib/app-utils/native-app-association.js +4 -2
  42. package/src/lib/app-utils/native-app-association.js.map +1 -1
  43. package/src/lib/app-utils/native-nav.d.ts +265 -0
  44. package/src/lib/app-utils/native-nav.js +741 -0
  45. package/src/lib/app-utils/native-nav.js.map +1 -0
  46. package/src/lib/app-utils/native-themes.d.ts +249 -0
  47. package/src/lib/app-utils/native-themes.js +732 -0
  48. package/src/lib/app-utils/native-themes.js.map +1 -0
  49. package/src/lib/app-utils/notifications.d.ts +1 -1
  50. package/src/lib/app-utils/notifications.js +10 -0
  51. package/src/lib/app-utils/notifications.js.map +1 -1
  52. package/src/lib/app-utils/onboarding-deep-link.d.ts +36 -2
  53. package/src/lib/app-utils/onboarding-deep-link.js +48 -5
  54. package/src/lib/app-utils/onboarding-deep-link.js.map +1 -1
  55. package/src/lib/app-utils/one-box-search.d.ts +108 -0
  56. package/src/lib/app-utils/one-box-search.js +167 -0
  57. package/src/lib/app-utils/one-box-search.js.map +1 -0
  58. package/src/lib/app-utils/operator-alerts.js +21 -0
  59. package/src/lib/app-utils/operator-alerts.js.map +1 -1
  60. package/src/lib/app-utils/org-permissions.js +10 -61
  61. package/src/lib/app-utils/org-permissions.js.map +1 -1
  62. package/src/lib/app-utils/org-role-grants.d.ts +95 -0
  63. package/src/lib/app-utils/org-role-grants.js +193 -0
  64. package/src/lib/app-utils/org-role-grants.js.map +1 -0
  65. package/src/lib/app-utils/owner-approval-record.d.ts +102 -0
  66. package/src/lib/app-utils/owner-approval-record.js +148 -0
  67. package/src/lib/app-utils/owner-approval-record.js.map +1 -0
  68. package/src/lib/app-utils/plan-entitlements.d.ts +224 -68
  69. package/src/lib/app-utils/plan-entitlements.js +278 -61
  70. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  71. package/src/lib/app-utils/release-flags.d.ts +1 -1
  72. package/src/lib/app-utils/release-flags.js +10 -0
  73. package/src/lib/app-utils/release-flags.js.map +1 -1
  74. package/src/lib/app-utils/server.d.ts +4 -0
  75. package/src/lib/app-utils/server.js +7 -0
  76. package/src/lib/app-utils/server.js.map +1 -1
  77. package/src/lib/app-utils/site-care-project.d.ts +227 -0
  78. package/src/lib/app-utils/site-care-project.js +423 -0
  79. package/src/lib/app-utils/site-care-project.js.map +1 -0
  80. package/src/lib/app-utils/site-care.d.ts +268 -0
  81. package/src/lib/app-utils/site-care.js +283 -0
  82. package/src/lib/app-utils/site-care.js.map +1 -0
  83. package/src/lib/app-utils/tenant-dns-targets.d.ts +62 -0
  84. package/src/lib/app-utils/tenant-dns-targets.js +80 -0
  85. package/src/lib/app-utils/tenant-dns-targets.js.map +1 -0
  86. package/src/lib/app-utils/upgrade-proposal.d.ts +29 -4
  87. package/src/lib/app-utils/upgrade-proposal.js +60 -9
  88. package/src/lib/app-utils/upgrade-proposal.js.map +1 -1
  89. package/src/lib/foundation/definitions/org-billing.types.d.ts +54 -10
  90. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  91. package/src/lib/foundation/definitions/organization.types.d.ts +3 -1
  92. package/src/lib/foundation/definitions/organization.types.js.map +1 -1
  93. package/src/lib/foundation/definitions/platform.types.d.ts +9 -0
  94. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  95. package/src/lib/foundation/index.d.ts +1 -1
  96. package/src/lib/foundation/index.js.map +1 -1
  97. package/src/lib/plugin-manager/feature-plugins.d.ts +86 -0
  98. package/src/lib/plugin-manager/feature-plugins.js +20 -0
  99. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  100. package/src/lib/plugin-manager/first-party-plugins.generated.js +22 -1
  101. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  102. package/src/lib/plugin-manager/plugin-ai-capabilities.d.ts +19 -0
  103. package/src/lib/plugin-manager/plugin-ai-capabilities.js.map +1 -1
  104. package/src/lib/plugin-manager/plugin-domain-events.d.ts +4 -3
  105. package/src/lib/plugin-manager/plugin-domain-events.js.map +1 -1
  106. package/src/lib/plugin-manager/plugin-domain-search.d.ts +81 -0
  107. package/src/lib/plugin-manager/plugin-domain-search.js +63 -0
  108. package/src/lib/plugin-manager/plugin-domain-search.js.map +1 -0
  109. package/src/lib/plugin-manager/plugin-entitlements.d.ts +23 -5
  110. package/src/lib/plugin-manager/plugin-entitlements.js +10 -0
  111. package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
  112. package/src/lib/plugin-manager/plugin-media-ingest.d.ts +25 -0
  113. package/src/lib/plugin-manager/plugin-media-ingest.js.map +1 -1
  114. package/src/lib/plugin-manager/plugin-search-sources.d.ts +85 -0
  115. package/src/lib/plugin-manager/plugin-search-sources.js +91 -0
  116. package/src/lib/plugin-manager/plugin-search-sources.js.map +1 -0
  117. package/src/lib/plugin-manager/plugin-services.js +6 -0
  118. package/src/lib/plugin-manager/plugin-services.js.map +1 -1
  119. package/src/lib/plugin-manager/purchase-gate-preclearance.d.ts +61 -0
  120. package/src/lib/plugin-manager/purchase-gate-preclearance.js +102 -0
  121. package/src/lib/plugin-manager/purchase-gate-preclearance.js.map +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/bundle-intent.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * THE ADD-ONS A PLAN SELLS, AND THE BUNDLE A LINK ASKS FOR (AGL-3955).\n *\n * One list of add-on kinds and one parser for the add-ons a link, an upgrade\n * proposal or a staff dialog names, so the Billing card, the bundle checkout\n * link, the hand-off proposal (AGL-3956) and `/api/billing/checkout` agree on\n * what `addons=siteCare,ai,extraHost:2` means.\n *\n * The kinds are `OrgSeatAddons`' keys: what `resolvePurchasedAddons` counts,\n * what the webhook mirrors from Stripe, and what `/api/billing/addons` sells.\n * The prices and ceilings here are the ones `billing-addons.ts` (server)\n * re-exports, so a kind the link can preselect is always a kind the\n * checkout can price.\n *\n * A link is typed by a person and passed through hops nobody controls, so\n * the parser never throws and never guesses: an unknown token is dropped and\n * reported, so the page can say so in one line instead of failing.\n */\n\nimport type {\n AiTierId,\n OrgPlan,\n OrgSeatAddons,\n} from '../foundation/definitions/org-billing.types'\nimport {\n AI_TIER_IDS,\n AI_TIERS,\n aiAddonName,\n aiTierAddonKey,\n aiTierName,\n defaultAiTierForPlan,\n EVENT_CALENDAR_ADDON_MONTHLY_USD,\n EXTRA_HOSTS_ADDON_MAX,\n PLAN_LABELS,\n parseAiTierAddonKey,\n PLAN_PRICING,\n planSellsAiTiers,\n planSellsSiteCare,\n POS_REGISTER_ADDON_MONTHLY_USD,\n POS_REGISTERS_ADDON_MAX,\n resolveOrgEntitlements,\n SITE_CARE_ADDON_MONTHLY_USD,\n UNLIMITED,\n} from './plan-entitlements'\n\n/** An add-on kind: a key of `org.seatAddons`. */\nexport type AddonKind = keyof Required<OrgSeatAddons>\n\n/**\n * Every add-on kind Billing sells, in the order the Billing card lists them.\n * `/api/billing/addons`, the webhook's price map and the native add-ons\n * screen all read this list.\n */\nexport const ADDON_KINDS: readonly AddonKind[] = [\n 'managers',\n 'members',\n 'datasets',\n 'hosts',\n 'posRegisters',\n 'eventCalendar',\n 'aiAddon',\n 'siteCare',\n]\n\n/** The org-wide 0/1 kinds: one purchase covers the workspace. */\nexport const TOGGLE_ADDON_KIND_LIST: readonly AddonKind[] = [\n 'eventCalendar',\n 'aiAddon',\n 'siteCare',\n]\n\nexport function isAddonKind(value: unknown): value is AddonKind {\n return typeof value === 'string' && (ADDON_KINDS as readonly string[]).includes(value)\n}\n\n/**\n * Monthly USD per unit for an add-on on a plan (yearly prices are ×12 of\n * this); null when the plan doesn't sell the kind. Single pricing source\n * of truth: `PLAN_PRICING`, the three flat add-on constants and `AI_TIERS`\n * (`tier` as `addonPriceId` reads it; absent, the tier a plan-named purchase\n * means, `defaultAiTierForPlan`, AGL-3965).\n */\nexport function addonUnitUsd(\n kind: AddonKind,\n plan: OrgPlan,\n tier?: AiTierId | null,\n): number | null {\n const pricing = PLAN_PRICING[plan]\n switch (kind) {\n case 'managers':\n return pricing.extraSeatMonthlyUsd\n case 'members':\n return pricing.extraCollaboratorMonthlyUsd\n case 'datasets':\n return pricing.extraDatasetMonthlyUsd\n case 'hosts':\n return pricing.extraHostMonthlyUsd\n case 'posRegisters':\n return POS_REGISTER_ADDON_MONTHLY_USD\n case 'eventCalendar':\n return EVENT_CALENDAR_ADDON_MONTHLY_USD\n case 'aiAddon': {\n const chosen = planSellsAiTiers(plan) ? (tier ?? defaultAiTierForPlan(plan)) : null\n return chosen ? AI_TIERS[chosen].monthlyUsd : null\n }\n case 'siteCare':\n return SITE_CARE_ADDON_MONTHLY_USD\n }\n}\n\n/**\n * Max purchasable quantity per kind, from a purchases-free entitlement\n * resolution (plan defaults + staff overrides only) so the ceiling does not\n * drift as the org buys: seat/dataset kinds stop at the plan's hard max,\n * hosts/registers use flat ceilings, the Event Calendar, Aglyn AI and Site\n * Care are 0/1 toggles. POS registers additionally require the `pos`\n * feature, and Site Care a plan that sells it (`planSellsSiteCare`), which is\n * why the plan travels with the baseline.\n *\n * One definition for every reader: `/api/billing/addons` asks how many the\n * org may buy on its plan, a plan change asks how many of what it bought the\n * new plan can deliver, and a bundle link asks how many it may preselect.\n */\nexport function addonMaxForBaseline(\n kind: AddonKind,\n baseline: ReturnType<typeof resolveOrgEntitlements>,\n plan: OrgPlan,\n): number {\n const bounded = (included: number, max: number) =>\n Number.isFinite(max) ? Math.max(0, max - included) : EXTRA_HOSTS_ADDON_MAX\n switch (kind) {\n case 'managers':\n return bounded(baseline.managersPerOrg, baseline.maxManagersPerOrg)\n case 'members':\n return bounded(baseline.membersPerHost, baseline.maxMembersPerHost)\n case 'datasets':\n return bounded(baseline.datasetsPerOrg, baseline.maxDatasetsPerOrg)\n case 'hosts':\n return baseline.hostLimit === UNLIMITED ? 0 : EXTRA_HOSTS_ADDON_MAX\n case 'posRegisters':\n return baseline.features.pos ? POS_REGISTERS_ADDON_MAX : 0\n case 'eventCalendar':\n return 1\n case 'aiAddon':\n // Org-wide like the Event Calendar: one purchase, of one tier, on any\n // plan that sells tiers (AGL-3965) — not Free, not Enterprise.\n return planSellsAiTiers(plan) ? 1 : 0\n case 'siteCare':\n // Flat-priced, so the price map cannot say \"not sold here\" the way it\n // does for Aglyn AI. This ceiling is the rule that keeps Free (no\n // subscription) and Enterprise (quoted per deal) from buying it.\n return planSellsSiteCare(plan) ? 1 : 0\n }\n}\n\n/** The ceiling a PLAN would impose on an add-on kind, purchases excluded. */\nexport function addonMaxForPlan(kind: AddonKind, plan: OrgPlan): number {\n return addonMaxForBaseline(\n kind,\n resolveOrgEntitlements({ plan } as never),\n plan,\n )\n}\n\n/** Whether a plan sells an add-on at all: priced, with a ceiling above zero. */\nexport function planSellsAddon(kind: AddonKind, plan: OrgPlan): boolean {\n return addonUnitUsd(kind, plan) != null && addonMaxForPlan(kind, plan) > 0\n}\n\n/** Quantities by kind, absent kinds meaning none. */\nexport type BundleAddons = Partial<Record<AddonKind, number>>\n\n/**\n * What a link writes for each kind. `ai` and `extraHost` are the words the\n * account owner chose (AGL-3955); the rest follow the same shape.\n */\nexport const BUNDLE_ADDON_TOKENS: Record<AddonKind, string> = {\n managers: 'extraSeat',\n members: 'extraMember',\n datasets: 'extraDataset',\n hosts: 'extraHost',\n posRegisters: 'posRegister',\n eventCalendar: 'eventCalendar',\n aiAddon: 'ai',\n siteCare: 'siteCare',\n}\n\n/**\n * Every spelling the parser accepts, lower-cased: the written token, the\n * stored kind, and the plurals a person types.\n */\nconst TOKEN_KINDS: Record<string, AddonKind> = (() => {\n const map: Record<string, AddonKind> = {}\n const add = (kind: AddonKind, ...words: string[]) => {\n for (const word of [kind, BUNDLE_ADDON_TOKENS[kind], ...words]) {\n map[word.toLowerCase()] = kind\n }\n }\n add('managers', 'extraSeats', 'seats', 'managerSeats')\n add('members', 'extraMembers', 'collaborators', 'collaboratorSeats')\n add('datasets', 'extraDatasets')\n add('hosts', 'extraHosts', 'extraSite', 'extraSites', 'sites')\n add('posRegisters', 'registers', 'register')\n add('eventCalendar', 'events', 'calendar')\n add('aiAddon', 'aglynAi')\n add('siteCare', 'site-care', 'site_care', 'care')\n return map\n})()\n\n/** The most of a quantity kind a link may ask for, before the plan's ceiling. */\nconst MAX_LINK_QUANTITY = 999\n\n/** One add-on a token names, with the Aglyn AI tier when it names one. */\ninterface ParsedToken {\n kind: AddonKind\n quantity: number\n tier?: AiTierId\n}\n\n/**\n * THE `ai:<tier>` TOKENS (AGL-3965).\n *\n * Aglyn AI is a tier chosen on its own, on any paid plan: `ai:boost` adds\n * Aglyn AI Boost. The words are the tier ids, lower-cased as every token is,\n * and resolve through `parseAiTierAddonKey`, the one spelling of the\n * `ai:<tier>` key. A bare `ai` (or `ai:1`) still means \"add it\" and sells\n * the plan's default tier (`defaultAiTierForPlan`). An unknown tier is\n * dropped with the notice like any other unknown add-on, never read as\n * \"add whichever tier\": that would sell a tier nobody chose.\n */\nexport const AI_ADDON_TIER_TOKENS: Readonly<Record<string, AiTierId>> = Object.freeze(\n Object.fromEntries(AI_TIER_IDS.map((tier) => [tier.toLowerCase(), tier])),\n)\n\nexport function resolveAiAddonTier(tier: string): ParsedToken | null {\n const word = tier.trim().toLowerCase()\n if (!Object.prototype.hasOwnProperty.call(AI_ADDON_TIER_TOKENS, word)) return null\n const parsed = parseAiTierAddonKey(aiTierAddonKey(AI_ADDON_TIER_TOKENS[word]))\n return parsed ? { kind: 'aiAddon', quantity: 1, tier: parsed } : null\n}\n\nexport interface ParsedBundleAddons {\n addons: BundleAddons\n /** The tokens nothing understood, as they were written. */\n dropped: string[]\n /** The Aglyn AI tier an `ai:<tier>` token named (AGL-3965), if any. */\n aiTier?: AiTierId\n}\n\n/** One token, `kind` or `kind:quantity`, or null when nothing understands it. */\nfunction parseToken(raw: string): ParsedToken | null {\n const token = raw.trim()\n if (!token) return null\n const [name, ...rest] = token.split(':')\n const kind = TOKEN_KINDS[name.trim().toLowerCase()]\n if (!kind) return null\n const value = rest.join(':').trim()\n if (!value) return { kind, quantity: 1 }\n if (/^\\d+$/.test(value)) {\n const quantity = Math.min(MAX_LINK_QUANTITY, Number(value))\n if (quantity < 1) return null\n return {\n kind,\n quantity: (TOGGLE_ADDON_KIND_LIST as readonly string[]).includes(kind) ? 1 : quantity,\n }\n }\n return kind === 'aiAddon' ? resolveAiAddonTier(value) : null\n}\n\n/**\n * Read the add-ons a link, a proposal or a request names.\n *\n * Accepts the link form (`\"siteCare,ai,extraHost:2\"`), a list of tokens, or a\n * map of kind → quantity or flag (`{ siteCare: true, hosts: 2 }`, the shape\n * the native staff screens post from their toggles). Never throws: anything\n * it cannot read is reported in `dropped` and left out.\n */\nexport function parseBundleAddons(input: unknown): ParsedBundleAddons {\n const addons: BundleAddons = {}\n const dropped: string[] = []\n let aiTier: AiTierId | undefined\n const take = (parsed: ParsedToken | null, raw: string) => {\n if (!parsed) {\n if (raw.trim()) dropped.push(raw.trim().slice(0, 40))\n return\n }\n addons[parsed.kind] = Math.max(addons[parsed.kind] ?? 0, parsed.quantity)\n // The first tier named wins: one workspace holds one tier.\n if (parsed.tier && !aiTier) aiTier = parsed.tier\n }\n if (typeof input === 'string') {\n for (const raw of input.split(/[,\\s]+/)) take(parseToken(raw), raw)\n } else if (Array.isArray(input)) {\n for (const raw of input) {\n if (typeof raw === 'string') take(parseToken(raw), raw)\n }\n } else if (input && typeof input === 'object') {\n for (const [name, value] of Object.entries(input as Record<string, unknown>)) {\n const off =\n value === false ||\n value === 0 ||\n value === null ||\n value === undefined ||\n value === '' ||\n value === 'false' ||\n value === '0'\n if (off) continue\n const quantity =\n value === true || value === 'true'\n ? ''\n : typeof value === 'number' || typeof value === 'string'\n ? String(value)\n : ''\n take(parseToken(quantity ? `${name}:${quantity}` : name), name)\n }\n }\n return { addons, dropped, ...(aiTier ? { aiTier } : {}) }\n}\n\nexport interface FittedBundleAddons {\n addons: BundleAddons\n /** Kinds the plan does not sell, dropped. */\n notSold: AddonKind[]\n}\n\n/**\n * The add-ons a plan can actually carry: kinds it does not sell are dropped,\n * quantities are held to its ceiling. What a link preselects on one plan is\n * re-fitted when the owner picks another, so the checkout is never asked for\n * something it would refuse.\n */\nexport function fitBundleAddonsToPlan(\n addons: BundleAddons | null | undefined,\n plan: OrgPlan,\n): FittedBundleAddons {\n const fitted: BundleAddons = {}\n const notSold: AddonKind[] = []\n for (const kind of ADDON_KINDS) {\n const wanted = Math.floor(Number(addons?.[kind] ?? 0))\n if (!(wanted > 0)) continue\n if (!planSellsAddon(kind, plan)) {\n notSold.push(kind)\n continue\n }\n fitted[kind] = Math.min(wanted, addonMaxForPlan(kind, plan))\n }\n return { addons: fitted, notSold }\n}\n\n/**\n * The add-ons as the link writes them: `siteCare,ai:boost,extraHost:2`.\n * Aglyn AI is written with its tier when one is named (AGL-3965), as bare\n * `ai` otherwise.\n */\nexport function bundleAddonsParam(\n addons: BundleAddons | null | undefined,\n aiTier?: AiTierId | null,\n): string {\n return ADDON_KINDS.filter((kind) => (addons?.[kind] ?? 0) > 0)\n .map((kind) => {\n if (kind === 'aiAddon' && aiTier) return aiTierAddonKey(aiTier)\n const quantity = addons?.[kind] ?? 0\n return quantity > 1 ? `${BUNDLE_ADDON_TOKENS[kind]}:${quantity}` : BUNDLE_ADDON_TOKENS[kind]\n })\n .join(',')\n}\n\n/** Whether any add-on is asked for. */\nexport function hasBundleAddons(addons: BundleAddons | null | undefined): boolean {\n return ADDON_KINDS.some((kind) => (addons?.[kind] ?? 0) > 0)\n}\n\n/**\n * How each kind is named to a customer, singular. Aglyn AI carries its tier\n * when one is named: \"Aglyn AI Boost\" (AGL-3965).\n */\nexport function bundleAddonLabel(\n kind: AddonKind,\n brand?: string,\n aiTier?: AiTierId | null,\n): string {\n switch (kind) {\n case 'managers':\n return 'Extra manager seat'\n case 'members':\n return 'Extra collaborator seat'\n case 'datasets':\n return 'Extra dataset'\n case 'hosts':\n return 'Extra site'\n case 'posRegisters':\n return 'POS register'\n case 'eventCalendar':\n return 'Event Calendar'\n case 'aiAddon':\n return aiTier ? aiTierName(aiTier, brand) : aiAddonName(brand)\n case 'siteCare':\n return 'Site Care'\n }\n}\n\n/** \"Site Care\", \"2 × Extra site\", \"Aglyn AI Boost\". */\nexport function describeBundleAddon(\n kind: AddonKind,\n quantity: number,\n brand?: string,\n aiTier?: AiTierId | null,\n): string {\n const label = bundleAddonLabel(kind, brand, aiTier)\n return quantity > 1 ? `${quantity} × ${label}` : label\n}\n\n/** The longest coupon code a link carries. Stripe codes are far shorter. */\nexport const BUNDLE_COUPON_MAX = 64\n\n/**\n * A promotion code as a link carries it, or undefined. Only the characters\n * Stripe allows in a customer-facing code; anything else is not a code\n * somebody was handed, and is dropped rather than sent to Stripe.\n *\n * The code is only APPLIED here. Codes are minted on the staff Coupons page,\n * which audits them; nothing on this path creates one.\n */\nexport function normalizeBundleCoupon(raw: unknown): string | undefined {\n if (typeof raw !== 'string') return undefined\n const code = raw.trim()\n if (!code || code.length > BUNDLE_COUPON_MAX) return undefined\n return /^[A-Za-z0-9_-]+$/.test(code) ? code : undefined\n}\n\n/**\n * A domain name as a bundle carries it — lower-cased, no scheme, no path —\n * or undefined. Whether it is for sale and at what price is the registrar's\n * answer, re-checked at purchase; this only refuses what cannot be a name.\n */\nexport function normalizeBundleDomain(raw: unknown): string | undefined {\n if (typeof raw !== 'string') return undefined\n const name = raw\n .trim()\n .toLowerCase()\n .replace(/^https?:\\/\\//, '')\n .replace(/\\/.*$/, '')\n .replace(/\\.$/, '')\n if (!name || name.length > 253) return undefined\n const labels = name.split('.')\n if (labels.length < 2) return undefined\n const ok = labels.every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label))\n return ok && /^[a-z]{2,63}$/.test(labels[labels.length - 1]) ? name : undefined\n}\n\n/** \"$15.99\". */\nexport function formatBundleCents(cents: number): string {\n return `$${(Math.max(0, Math.round(cents)) / 100).toFixed(2)}`\n}\n\n/**\n * The domain line a bundle shows (AGL-3955): \"example.com — $15.99 first\n * year, renews yearly at $15.99\". The price is the registrar's at the moment\n * the link was minted; the purchase re-checks it.\n */\nexport function bundleDomainLine(domain: {\n name: string\n firstYearCents: number\n renewalCents: number\n}): string {\n return (\n `${domain.name} — ${formatBundleCents(domain.firstYearCents)} first year, ` +\n `renews yearly at ${formatBundleCents(domain.renewalCents)}`\n )\n}\n\n/**\n * The bundle in one line, for an email, a dialog or a snackbar:\n * \"Starter with Site Care and Aglyn AI, billed yearly, code BOOTH25\".\n */\nexport function describeBundle(\n bundle: {\n plan: OrgPlan\n addons?: BundleAddons | null\n interval?: 'month' | 'year' | null\n coupon?: string | null\n /** The Aglyn AI tier, named with the AI add-on (AGL-3965). */\n aiTier?: AiTierId | null\n },\n options: { brand?: string } = {},\n): string {\n const parts = ADDON_KINDS.filter((kind) => (bundle.addons?.[kind] ?? 0) > 0).map((kind) =>\n describeBundleAddon(kind, bundle.addons?.[kind] ?? 0, options.brand, bundle.aiTier),\n )\n const addons =\n parts.length === 0\n ? ''\n : parts.length === 1\n ? ` with ${parts[0]}`\n : ` with ${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`\n const interval =\n bundle.interval === 'year'\n ? ', billed yearly'\n : bundle.interval === 'month'\n ? ', billed monthly'\n : ''\n const coupon = bundle.coupon ? `, code ${bundle.coupon}` : ''\n return `${PLAN_LABELS[bundle.plan] ?? bundle.plan}${addons}${interval}${coupon}`\n}\n"],"names":["AI_TIER_IDS","AI_TIERS","aiAddonName","aiTierAddonKey","aiTierName","defaultAiTierForPlan","EVENT_CALENDAR_ADDON_MONTHLY_USD","EXTRA_HOSTS_ADDON_MAX","PLAN_LABELS","parseAiTierAddonKey","PLAN_PRICING","planSellsAiTiers","planSellsSiteCare","POS_REGISTER_ADDON_MONTHLY_USD","POS_REGISTERS_ADDON_MAX","resolveOrgEntitlements","SITE_CARE_ADDON_MONTHLY_USD","UNLIMITED","ADDON_KINDS","TOGGLE_ADDON_KIND_LIST","isAddonKind","value","includes","addonUnitUsd","kind","plan","tier","pricing","extraSeatMonthlyUsd","extraCollaboratorMonthlyUsd","extraDatasetMonthlyUsd","extraHostMonthlyUsd","chosen","monthlyUsd","addonMaxForBaseline","baseline","bounded","included","max","Number","isFinite","Math","managersPerOrg","maxManagersPerOrg","membersPerHost","maxMembersPerHost","datasetsPerOrg","maxDatasetsPerOrg","hostLimit","features","pos","addonMaxForPlan","planSellsAddon","BUNDLE_ADDON_TOKENS","managers","members","datasets","hosts","posRegisters","eventCalendar","aiAddon","siteCare","TOKEN_KINDS","map","add","words","word","toLowerCase","MAX_LINK_QUANTITY","AI_ADDON_TIER_TOKENS","Object","freeze","fromEntries","resolveAiAddonTier","trim","prototype","hasOwnProperty","call","parsed","quantity","parseToken","raw","token","name","rest","split","join","test","min","parseBundleAddons","input","addons","dropped","aiTier","take","push","slice","Array","isArray","entries","off","undefined","String","fitBundleAddonsToPlan","fitted","notSold","wanted","floor","bundleAddonsParam","filter","hasBundleAddons","some","bundleAddonLabel","brand","describeBundleAddon","label","BUNDLE_COUPON_MAX","normalizeBundleCoupon","code","length","normalizeBundleDomain","replace","labels","ok","every","formatBundleCents","cents","round","toFixed","bundleDomainLine","domain","firstYearCents","renewalCents","describeBundle","bundle","options","parts","interval","coupon"],"mappings":"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"}
@@ -0,0 +1,204 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * A SITE STAFF BUILD FOR A CLIENT, ON ONE SCREEN (AGL-3958).
19
+ *
20
+ * At a booth or on a doorstep there is no laptop: staff create the client's
21
+ * workspace with "Build for a client" on (AGL-3952), and land on one screen
22
+ * that asks the business details once, records the domain the owner wants,
23
+ * shows the photo link's QR (AGL-3957), starts the guided AI start with those
24
+ * answers, and sends the owner the preview. This module is that screen's
25
+ * vocabulary: what is asked, what an answer may be, and the intent the
26
+ * hand-off carries into the owner's checkout.
27
+ *
28
+ * Pure: shapes, bounds and parsers, read by the screen, the staff route that
29
+ * stores the record and the hand-off that reads its intent. No React, no
30
+ * network.
31
+ */
32
+ import type { OrgPlan, OrgSeatAddons } from '../foundation/definitions/org-billing.types';
33
+ /** What staff ask the owner, once, and the guided start reads instead of asking again. */
34
+ export interface ClientBuildIntake {
35
+ /** The business's name, as its header and footer will say it. */
36
+ businessName: string;
37
+ /** What the business does, in a few words: the guided start's first answer. */
38
+ whatTheyDo: string;
39
+ /** The towns or area it serves; empty when it serves no one place. */
40
+ towns: string;
41
+ /** The owner's phone: the preview is texted here, and the site may show it. */
42
+ phone: string;
43
+ /** The owner's email: the preview is emailed here, and the site may show it. */
44
+ email: string;
45
+ /** Opening hours as the owner says them, e.g. "Mon–Fri 8–5". */
46
+ hours: string;
47
+ /** The owner's own words about the business, in one box. */
48
+ ownerWords: string;
49
+ }
50
+ export type ClientBuildIntakeField = keyof ClientBuildIntake;
51
+ /** The longest each answer may be, in characters. */
52
+ export declare const CLIENT_BUILD_LIMITS: Readonly<Record<ClientBuildIntakeField, number>>;
53
+ /** An intake with nothing typed: what the screen starts on. */
54
+ export declare const EMPTY_CLIENT_BUILD_INTAKE: ClientBuildIntake;
55
+ /**
56
+ * Why each answer cannot be used, in a sentence shown beside it; an answer
57
+ * that is fine has no entry. The name and what they do are required, since
58
+ * the site cannot be named or planned without them; the phone and the email
59
+ * are each optional, but the preview needs one of them to reach the owner.
60
+ */
61
+ export declare function clientBuildIntakeErrors(intake: ClientBuildIntake): Partial<Record<ClientBuildIntakeField, string>>;
62
+ /**
63
+ * An intake as the route admits it: every answer a trimmed string within its
64
+ * bound, or the first reason it is not. Fields the body does not carry are
65
+ * empty, never `undefined`, so the record always has the same shape.
66
+ */
67
+ export declare function parseClientBuildIntake(raw: unknown): ClientBuildIntake | string;
68
+ /** The owner's phone as a text goes to it, E.164, or `null` when there is none to text. */
69
+ export declare function clientBuildPhone(intake: Pick<ClientBuildIntake, 'phone'>): string | null;
70
+ /**
71
+ * What the owner chose on the Domain step (Zach, 2026-10-19).
72
+ *
73
+ * - `free`: the site stays on its free address for now.
74
+ * - `register`: a name the owner wants to buy. NOT bought here: it rides the
75
+ * bundle link, so the owner buys it on their own phone after they
76
+ * subscribe, with their own registrant details and their own tick on the
77
+ * registrar's terms.
78
+ * - `own`: a domain the owner already has, which the hand-off and the Site
79
+ * Care intake connect (one-click DNS opens the owner's provider and needs
80
+ * their login, so staff do it with them, or hand them the records).
81
+ */
82
+ export type ClientBuildDomain = {
83
+ choice: 'free';
84
+ } | {
85
+ choice: 'register';
86
+ name: string;
87
+ firstYearUsd: number;
88
+ renewalUsd: number;
89
+ } | {
90
+ choice: 'own';
91
+ name: string;
92
+ };
93
+ /**
94
+ * A domain name as typed, normalized — lower case, no scheme, no `www.`, no
95
+ * path — or `null` when it is not one. "I have example.com" is typed on a
96
+ * phone, often as the address bar showed it.
97
+ */
98
+ export declare function normalizeClientBuildDomainName(raw: unknown): string | null;
99
+ /** A Domain step answer as the route admits it, or the reason it is not one. */
100
+ export declare function parseClientBuildDomain(raw: unknown): ClientBuildDomain | string;
101
+ /** "$12.99" for a whole-dollar-and-cents price. */
102
+ export declare function formatDomainUsd(amount: number): string;
103
+ /** "$12.99 the first year, then $18.99 a year": both prices together, always. */
104
+ export declare function domainPriceText(prices: {
105
+ firstYearUsd: number;
106
+ renewalUsd: number;
107
+ }): string;
108
+ /** Add-ons, by their `OrgSeatAddons` key, as Billing sells them. */
109
+ export type ClientBuildAddon = keyof OrgSeatAddons;
110
+ /**
111
+ * What the owner is asked to buy, as the build leaves it for the hand-off
112
+ * (AGL-3955, AGL-3956): the plan, the add-ons, the interval, a coupon, and
113
+ * the domain they picked. The hand-off dialog opens on it and the bundle
114
+ * link preselects it, so the owner's checkout is one tap from paid.
115
+ *
116
+ * `domain` is a name to REGISTER, bought by the owner after they subscribe;
117
+ * `existingDomain` is one they already hold, which is connected rather than
118
+ * bought. At most one of the two is set.
119
+ */
120
+ export interface ClientBuildIntent {
121
+ plan: OrgPlan;
122
+ addons: ClientBuildAddon[];
123
+ interval: 'month' | 'year';
124
+ coupon?: string;
125
+ domain?: {
126
+ name: string;
127
+ firstYearUsd: number;
128
+ renewalUsd: number;
129
+ };
130
+ existingDomain?: string;
131
+ }
132
+ /** Site Care rides every build for a client unless staff take it off (AGL-3954). */
133
+ export declare const CLIENT_BUILD_DEFAULT_ADDONS: readonly ClientBuildAddon[];
134
+ /** The intent a build starts with: the plan the workspace was comped on, Site Care, monthly. */
135
+ export declare function clientBuildIntent(options: {
136
+ plan: OrgPlan | null | undefined;
137
+ domain?: ClientBuildDomain | null;
138
+ }): ClientBuildIntent;
139
+ /**
140
+ * A stored intent, narrowed, or `null` when it is not one worth acting on.
141
+ * What the hand-off reads: a plan this build cannot sell, an add-on it does
142
+ * not know or a malformed domain reads as absent rather than as a checkout
143
+ * for something that does not exist.
144
+ */
145
+ export declare function readClientBuildIntent(raw: unknown): ClientBuildIntent | null;
146
+ /**
147
+ * The build as it is stored, one per workspace, at
148
+ * `orgs/{orgId}/clientBuild/{CLIENT_BUILD_DOC_ID}`: a subcollection the rules
149
+ * open to no client, written and read only by `/api/admin/client-build`
150
+ * (staff) through the Admin SDK. Not on the org document, which every
151
+ * member reads: the owner's phone and email are staff's to hold until the
152
+ * owner joins.
153
+ */
154
+ export interface ClientBuildRecord {
155
+ intake: ClientBuildIntake;
156
+ /** `null` until the Domain step is answered. */
157
+ domain: ClientBuildDomain | null;
158
+ intent: ClientBuildIntent;
159
+ /** The site the build made, once it has. */
160
+ hostId: string | null;
161
+ /** Its subdomain, which is what a console URL names a site by. */
162
+ host: string | null;
163
+ /** The guided start's job, once it is started. */
164
+ jobId: string | null;
165
+ /**
166
+ * The starter site was given instead of a guided start (no AI on the
167
+ * workspace, or staff chose it), so the build step is finished without a job.
168
+ */
169
+ starter: boolean;
170
+ /** When the preview was last sent to the owner, ms; `null` before. */
171
+ previewSentAt: number | null;
172
+ updatedAt: number;
173
+ /** The staff uid that last wrote it. */
174
+ updatedBy: string;
175
+ }
176
+ /** The subcollection and document the record lives at under its org. */
177
+ export declare const CLIENT_BUILD_COLLECTION = "clientBuild";
178
+ export declare const CLIENT_BUILD_DOC_ID = "current";
179
+ /** The screen's steps, in order. */
180
+ export declare const CLIENT_BUILD_STEPS: readonly ["details", "domain", "photos", "build", "send"];
181
+ export type ClientBuildStep = (typeof CLIENT_BUILD_STEPS)[number];
182
+ /**
183
+ * Where a stored build resumes: the first step it has not finished. A phone
184
+ * that reloads mid-build, or a second phone, opens where the first left off.
185
+ */
186
+ export declare function clientBuildResumeStep(record: Pick<ClientBuildRecord, 'domain' | 'hostId' | 'jobId'> & {
187
+ starter?: boolean;
188
+ } | null): ClientBuildStep;
189
+ /**
190
+ * The address the site is going to live at, as the owner is told it: the
191
+ * domain they picked or already have, else the free address it is on.
192
+ */
193
+ export declare function clientBuildSiteAddress(domain: ClientBuildDomain | null | undefined, liveUrl: string | null): string | null;
194
+ /**
195
+ * The text the owner gets with their preview: short enough for one or two
196
+ * segments, and naming who it is from, since it arrives from a number they
197
+ * do not know. The email says the same at length, from the catalog.
198
+ */
199
+ export declare function clientBuildPreviewText(options: {
200
+ businessName: string;
201
+ previewUrl: string;
202
+ siteAddress: string | null;
203
+ productName: string;
204
+ }): string;
@@ -0,0 +1,237 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * A SITE STAFF BUILD FOR A CLIENT, ON ONE SCREEN (AGL-3958).
18
+ *
19
+ * At a booth or on a doorstep there is no laptop: staff create the client's
20
+ * workspace with "Build for a client" on (AGL-3952), and land on one screen
21
+ * that asks the business details once, records the domain the owner wants,
22
+ * shows the photo link's QR (AGL-3957), starts the guided AI start with those
23
+ * answers, and sends the owner the preview. This module is that screen's
24
+ * vocabulary: what is asked, what an answer may be, and the intent the
25
+ * hand-off carries into the owner's checkout.
26
+ *
27
+ * Pure: shapes, bounds and parsers, read by the screen, the staff route that
28
+ * stores the record and the hand-off that reads its intent. No React, no
29
+ * network.
30
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
31
+ import { normalizePhone } from "../foundation/definitions/contact.types.js";
32
+ import { SELF_SERVE_PLANS } from "./plan-entitlements.js";
33
+ /** The longest each answer may be, in characters. */ export const CLIENT_BUILD_LIMITS = {
34
+ // The three the guided start passes on as its own inputs, which it bounds
35
+ // at 120 (`AI_SITE_INPUT_MAX_CHARS`).
36
+ businessName: 120,
37
+ whatTheyDo: 120,
38
+ towns: 120,
39
+ phone: 32,
40
+ email: 254,
41
+ hours: 200,
42
+ ownerWords: 1500
43
+ };
44
+ /** An intake with nothing typed: what the screen starts on. */ export const EMPTY_CLIENT_BUILD_INTAKE = {
45
+ businessName: '',
46
+ whatTheyDo: '',
47
+ towns: '',
48
+ phone: '',
49
+ email: '',
50
+ hours: '',
51
+ ownerWords: ''
52
+ };
53
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
54
+ /**
55
+ * Why each answer cannot be used, in a sentence shown beside it; an answer
56
+ * that is fine has no entry. The name and what they do are required, since
57
+ * the site cannot be named or planned without them; the phone and the email
58
+ * are each optional, but the preview needs one of them to reach the owner.
59
+ */ export function clientBuildIntakeErrors(intake) {
60
+ var _errors, _businessName, _errors1, _whatTheyDo, _errors2, _phone, _errors3, _email, _errors4, _phone1;
61
+ const errors = {};
62
+ for (const field of Object.keys(CLIENT_BUILD_LIMITS)){
63
+ if (intake[field].trim().length > CLIENT_BUILD_LIMITS[field]) {
64
+ errors[field] = `Keep this under ${CLIENT_BUILD_LIMITS[field].toLocaleString('en-US')} characters.`;
65
+ }
66
+ }
67
+ if (!intake.businessName.trim()) (_businessName = (_errors = errors).businessName) != null ? _businessName : _errors.businessName = 'Name the business.';
68
+ if (!intake.whatTheyDo.trim()) (_whatTheyDo = (_errors1 = errors).whatTheyDo) != null ? _whatTheyDo : _errors1.whatTheyDo = 'Say what the business does.';
69
+ const phone = intake.phone.trim();
70
+ const email = intake.email.trim();
71
+ if (phone && !normalizePhone(phone)) (_phone = (_errors2 = errors).phone) != null ? _phone : _errors2.phone = 'Type a phone number with its area code.';
72
+ if (email && !EMAIL_PATTERN.test(email)) (_email = (_errors3 = errors).email) != null ? _email : _errors3.email = 'Type an email address like name@example.com.';
73
+ if (!phone && !email) (_phone1 = (_errors4 = errors).phone) != null ? _phone1 : _errors4.phone = 'Add a phone or an email, so the owner gets the preview.';
74
+ return errors;
75
+ }
76
+ /**
77
+ * An intake as the route admits it: every answer a trimmed string within its
78
+ * bound, or the first reason it is not. Fields the body does not carry are
79
+ * empty, never `undefined`, so the record always has the same shape.
80
+ */ export function parseClientBuildIntake(raw) {
81
+ if (!raw || typeof raw !== 'object') return 'Missing the business details';
82
+ const source = raw;
83
+ const intake = _extends({}, EMPTY_CLIENT_BUILD_INTAKE);
84
+ for (const field of Object.keys(CLIENT_BUILD_LIMITS)){
85
+ const value = source[field];
86
+ if (value === undefined || value === null) continue;
87
+ if (typeof value !== 'string') return `${field} must be text`;
88
+ intake[field] = value.trim();
89
+ }
90
+ const errors = clientBuildIntakeErrors(intake);
91
+ const first = Object.values(errors)[0];
92
+ return first != null ? first : intake;
93
+ }
94
+ /** The owner's phone as a text goes to it, E.164, or `null` when there is none to text. */ export function clientBuildPhone(intake) {
95
+ return normalizePhone(intake.phone);
96
+ }
97
+ /** A registrable name: labels of letters, digits and hyphens, and a TLD of letters. */ const DOMAIN_PATTERN = /^(?=.{3,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
98
+ /**
99
+ * A domain name as typed, normalized — lower case, no scheme, no `www.`, no
100
+ * path — or `null` when it is not one. "I have example.com" is typed on a
101
+ * phone, often as the address bar showed it.
102
+ */ export function normalizeClientBuildDomainName(raw) {
103
+ if (typeof raw !== 'string') return null;
104
+ const name = raw.trim().toLowerCase().replace(/^[a-z]+:\/\//, '').replace(/^www\./, '').replace(/[/?#].*$/, '').replace(/\.$/, '');
105
+ return DOMAIN_PATTERN.test(name) ? name : null;
106
+ }
107
+ const isPrice = (value)=>typeof value === 'number' && Number.isFinite(value) && value >= 0 && value < 1000000;
108
+ /** A Domain step answer as the route admits it, or the reason it is not one. */ export function parseClientBuildDomain(raw) {
109
+ if (!raw || typeof raw !== 'object') return 'Missing the domain choice';
110
+ const source = raw;
111
+ if (source['choice'] === 'free') return {
112
+ choice: 'free'
113
+ };
114
+ const name = normalizeClientBuildDomainName(source['name']);
115
+ if (source['choice'] === 'own') {
116
+ return name ? {
117
+ choice: 'own',
118
+ name
119
+ } : 'Type the domain the owner has, like example.com';
120
+ }
121
+ if (source['choice'] === 'register') {
122
+ if (!name) return 'Pick a domain from the search';
123
+ if (!isPrice(source['firstYearUsd']) || !isPrice(source['renewalUsd'])) {
124
+ return 'The domain needs its first-year and renewal prices';
125
+ }
126
+ return {
127
+ choice: 'register',
128
+ name,
129
+ firstYearUsd: source['firstYearUsd'],
130
+ renewalUsd: source['renewalUsd']
131
+ };
132
+ }
133
+ return 'Unknown domain choice';
134
+ }
135
+ /** "$12.99" for a whole-dollar-and-cents price. */ export function formatDomainUsd(amount) {
136
+ return `$${amount.toFixed(2)}`;
137
+ }
138
+ /** "$12.99 the first year, then $18.99 a year": both prices together, always. */ export function domainPriceText(prices) {
139
+ return prices.firstYearUsd === prices.renewalUsd ? `${formatDomainUsd(prices.firstYearUsd)} a year` : `${formatDomainUsd(prices.firstYearUsd)} the first year, then ${formatDomainUsd(prices.renewalUsd)} a year`;
140
+ }
141
+ /** Site Care rides every build for a client unless staff take it off (AGL-3954). */ export const CLIENT_BUILD_DEFAULT_ADDONS = [
142
+ 'siteCare'
143
+ ];
144
+ /** The plans a build may propose: every self-serve tier but Free. */ const INTENT_PLANS = SELF_SERVE_PLANS.filter((plan)=>plan !== 'free');
145
+ /** The intent a build starts with: the plan the workspace was comped on, Site Care, monthly. */ export function clientBuildIntent(options) {
146
+ const plan = options.plan && INTENT_PLANS.includes(options.plan) ? options.plan : 'starter';
147
+ const domain = options.domain;
148
+ return _extends({
149
+ plan,
150
+ addons: [
151
+ ...CLIENT_BUILD_DEFAULT_ADDONS
152
+ ],
153
+ interval: 'month'
154
+ }, (domain == null ? void 0 : domain.choice) === 'register' ? {
155
+ domain: {
156
+ name: domain.name,
157
+ firstYearUsd: domain.firstYearUsd,
158
+ renewalUsd: domain.renewalUsd
159
+ }
160
+ } : {}, (domain == null ? void 0 : domain.choice) === 'own' ? {
161
+ existingDomain: domain.name
162
+ } : {});
163
+ }
164
+ /**
165
+ * A stored intent, narrowed, or `null` when it is not one worth acting on.
166
+ * What the hand-off reads: a plan this build cannot sell, an add-on it does
167
+ * not know or a malformed domain reads as absent rather than as a checkout
168
+ * for something that does not exist.
169
+ */ export function readClientBuildIntent(raw) {
170
+ if (!raw || typeof raw !== 'object') return null;
171
+ const source = raw;
172
+ const plan = source['plan'];
173
+ if (typeof plan !== 'string' || !INTENT_PLANS.includes(plan)) return null;
174
+ const addons = Array.isArray(source['addons']) ? source['addons'].filter((addon)=>typeof addon === 'string' && addon.length > 0) : [];
175
+ const interval = source['interval'] === 'year' ? 'year' : 'month';
176
+ const coupon = typeof source['coupon'] === 'string' && source['coupon'].trim() ? source['coupon'].trim() : undefined;
177
+ const rawDomain = source['domain'];
178
+ const parsed = rawDomain && typeof rawDomain === 'object' ? parseClientBuildDomain(_extends({}, rawDomain, {
179
+ choice: 'register'
180
+ })) : null;
181
+ const domain = parsed && typeof parsed === 'object' && parsed.choice === 'register' ? parsed : null;
182
+ const existing = normalizeClientBuildDomainName(source['existingDomain']);
183
+ return _extends({
184
+ plan: plan,
185
+ addons,
186
+ interval
187
+ }, coupon ? {
188
+ coupon
189
+ } : {}, domain ? {
190
+ domain: {
191
+ name: domain.name,
192
+ firstYearUsd: domain.firstYearUsd,
193
+ renewalUsd: domain.renewalUsd
194
+ }
195
+ } : existing ? {
196
+ existingDomain: existing
197
+ } : {});
198
+ }
199
+ /** The subcollection and document the record lives at under its org. */ export const CLIENT_BUILD_COLLECTION = 'clientBuild';
200
+ export const CLIENT_BUILD_DOC_ID = 'current';
201
+ /** The screen's steps, in order. */ export const CLIENT_BUILD_STEPS = [
202
+ 'details',
203
+ 'domain',
204
+ 'photos',
205
+ 'build',
206
+ 'send'
207
+ ];
208
+ /**
209
+ * Where a stored build resumes: the first step it has not finished. A phone
210
+ * that reloads mid-build, or a second phone, opens where the first left off.
211
+ */ export function clientBuildResumeStep(record) {
212
+ if (!record) return 'details';
213
+ if (!record.domain) return 'domain';
214
+ // The site exists before its photos are asked for, and the guided start
215
+ // waits on them: until a job is started, or the starter site was given
216
+ // instead, the build is at the photos.
217
+ if (!record.hostId || !record.jobId && !record.starter) return 'photos';
218
+ return 'send';
219
+ }
220
+ /**
221
+ * The address the site is going to live at, as the owner is told it: the
222
+ * domain they picked or already have, else the free address it is on.
223
+ */ export function clientBuildSiteAddress(domain, liveUrl) {
224
+ if (domain && domain.choice !== 'free') return domain.name;
225
+ return liveUrl ? liveUrl.replace(/^https?:\/\//, '') : null;
226
+ }
227
+ /**
228
+ * The text the owner gets with their preview: short enough for one or two
229
+ * segments, and naming who it is from, since it arrives from a number they
230
+ * do not know. The email says the same at length, from the catalog.
231
+ */ export function clientBuildPreviewText(options) {
232
+ const name = options.businessName.trim() || 'your business';
233
+ const address = options.siteAddress && options.siteAddress !== options.previewUrl.replace(/^https?:\/\//, '') ? ` It will live at ${options.siteAddress}.` : '';
234
+ return `${options.productName}: the new website for ${name} is ready to look at: ${options.previewUrl}${address}`;
235
+ }
236
+
237
+ //# sourceMappingURL=client-build.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/client-build.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A SITE STAFF BUILD FOR A CLIENT, ON ONE SCREEN (AGL-3958).\n *\n * At a booth or on a doorstep there is no laptop: staff create the client's\n * workspace with \"Build for a client\" on (AGL-3952), and land on one screen\n * that asks the business details once, records the domain the owner wants,\n * shows the photo link's QR (AGL-3957), starts the guided AI start with those\n * answers, and sends the owner the preview. This module is that screen's\n * vocabulary: what is asked, what an answer may be, and the intent the\n * hand-off carries into the owner's checkout.\n *\n * Pure: shapes, bounds and parsers, read by the screen, the staff route that\n * stores the record and the hand-off that reads its intent. No React, no\n * network.\n */\n\nimport type { OrgPlan, OrgSeatAddons } from '../foundation/definitions/org-billing.types'\nimport { normalizePhone } from '../foundation/definitions/contact.types'\nimport { SELF_SERVE_PLANS } from './plan-entitlements'\n\n/** What staff ask the owner, once, and the guided start reads instead of asking again. */\nexport interface ClientBuildIntake {\n /** The business's name, as its header and footer will say it. */\n businessName: string\n /** What the business does, in a few words: the guided start's first answer. */\n whatTheyDo: string\n /** The towns or area it serves; empty when it serves no one place. */\n towns: string\n /** The owner's phone: the preview is texted here, and the site may show it. */\n phone: string\n /** The owner's email: the preview is emailed here, and the site may show it. */\n email: string\n /** Opening hours as the owner says them, e.g. \"Mon–Fri 8–5\". */\n hours: string\n /** The owner's own words about the business, in one box. */\n ownerWords: string\n}\n\nexport type ClientBuildIntakeField = keyof ClientBuildIntake\n\n/** The longest each answer may be, in characters. */\nexport const CLIENT_BUILD_LIMITS: Readonly<Record<ClientBuildIntakeField, number>> = {\n // The three the guided start passes on as its own inputs, which it bounds\n // at 120 (`AI_SITE_INPUT_MAX_CHARS`).\n businessName: 120,\n whatTheyDo: 120,\n towns: 120,\n phone: 32,\n email: 254,\n hours: 200,\n ownerWords: 1_500,\n}\n\n/** An intake with nothing typed: what the screen starts on. */\nexport const EMPTY_CLIENT_BUILD_INTAKE: ClientBuildIntake = {\n businessName: '',\n whatTheyDo: '',\n towns: '',\n phone: '',\n email: '',\n hours: '',\n ownerWords: '',\n}\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/**\n * Why each answer cannot be used, in a sentence shown beside it; an answer\n * that is fine has no entry. The name and what they do are required, since\n * the site cannot be named or planned without them; the phone and the email\n * are each optional, but the preview needs one of them to reach the owner.\n */\nexport function clientBuildIntakeErrors(\n intake: ClientBuildIntake,\n): Partial<Record<ClientBuildIntakeField, string>> {\n const errors: Partial<Record<ClientBuildIntakeField, string>> = {}\n for (const field of Object.keys(CLIENT_BUILD_LIMITS) as ClientBuildIntakeField[]) {\n if (intake[field].trim().length > CLIENT_BUILD_LIMITS[field]) {\n errors[field] = `Keep this under ${CLIENT_BUILD_LIMITS[field].toLocaleString('en-US')} characters.`\n }\n }\n if (!intake.businessName.trim()) errors.businessName ??= 'Name the business.'\n if (!intake.whatTheyDo.trim()) errors.whatTheyDo ??= 'Say what the business does.'\n const phone = intake.phone.trim()\n const email = intake.email.trim()\n if (phone && !normalizePhone(phone)) errors.phone ??= 'Type a phone number with its area code.'\n if (email && !EMAIL_PATTERN.test(email)) errors.email ??= 'Type an email address like name@example.com.'\n if (!phone && !email) errors.phone ??= 'Add a phone or an email, so the owner gets the preview.'\n return errors\n}\n\n/**\n * An intake as the route admits it: every answer a trimmed string within its\n * bound, or the first reason it is not. Fields the body does not carry are\n * empty, never `undefined`, so the record always has the same shape.\n */\nexport function parseClientBuildIntake(raw: unknown): ClientBuildIntake | string {\n if (!raw || typeof raw !== 'object') return 'Missing the business details'\n const source = raw as Record<string, unknown>\n const intake = { ...EMPTY_CLIENT_BUILD_INTAKE }\n for (const field of Object.keys(CLIENT_BUILD_LIMITS) as ClientBuildIntakeField[]) {\n const value = source[field]\n if (value === undefined || value === null) continue\n if (typeof value !== 'string') return `${field} must be text`\n intake[field] = value.trim()\n }\n const errors = clientBuildIntakeErrors(intake)\n const first = Object.values(errors)[0]\n return first ?? intake\n}\n\n/** The owner's phone as a text goes to it, E.164, or `null` when there is none to text. */\nexport function clientBuildPhone(intake: Pick<ClientBuildIntake, 'phone'>): string | null {\n return normalizePhone(intake.phone)\n}\n\n// ── The domain ─────────────────────────────────────────────────────────────\n\n/**\n * What the owner chose on the Domain step (Zach, 2026-10-19).\n *\n * - `free`: the site stays on its free address for now.\n * - `register`: a name the owner wants to buy. NOT bought here: it rides the\n * bundle link, so the owner buys it on their own phone after they\n * subscribe, with their own registrant details and their own tick on the\n * registrar's terms.\n * - `own`: a domain the owner already has, which the hand-off and the Site\n * Care intake connect (one-click DNS opens the owner's provider and needs\n * their login, so staff do it with them, or hand them the records).\n */\nexport type ClientBuildDomain =\n | { choice: 'free' }\n | { choice: 'register'; name: string; firstYearUsd: number; renewalUsd: number }\n | { choice: 'own'; name: string }\n\n/** A registrable name: labels of letters, digits and hyphens, and a TLD of letters. */\nconst DOMAIN_PATTERN = /^(?=.{3,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$/\n\n/**\n * A domain name as typed, normalized — lower case, no scheme, no `www.`, no\n * path — or `null` when it is not one. \"I have example.com\" is typed on a\n * phone, often as the address bar showed it.\n */\nexport function normalizeClientBuildDomainName(raw: unknown): string | null {\n if (typeof raw !== 'string') return null\n const name = raw\n .trim()\n .toLowerCase()\n .replace(/^[a-z]+:\\/\\//, '')\n .replace(/^www\\./, '')\n .replace(/[/?#].*$/, '')\n .replace(/\\.$/, '')\n return DOMAIN_PATTERN.test(name) ? name : null\n}\n\nconst isPrice = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value) && value >= 0 && value < 1_000_000\n\n/** A Domain step answer as the route admits it, or the reason it is not one. */\nexport function parseClientBuildDomain(raw: unknown): ClientBuildDomain | string {\n if (!raw || typeof raw !== 'object') return 'Missing the domain choice'\n const source = raw as Record<string, unknown>\n if (source['choice'] === 'free') return { choice: 'free' }\n const name = normalizeClientBuildDomainName(source['name'])\n if (source['choice'] === 'own') {\n return name ? { choice: 'own', name } : 'Type the domain the owner has, like example.com'\n }\n if (source['choice'] === 'register') {\n if (!name) return 'Pick a domain from the search'\n if (!isPrice(source['firstYearUsd']) || !isPrice(source['renewalUsd'])) {\n return 'The domain needs its first-year and renewal prices'\n }\n return {\n choice: 'register',\n name,\n firstYearUsd: source['firstYearUsd'],\n renewalUsd: source['renewalUsd'],\n }\n }\n return 'Unknown domain choice'\n}\n\n/** \"$12.99\" for a whole-dollar-and-cents price. */\nexport function formatDomainUsd(amount: number): string {\n return `$${amount.toFixed(2)}`\n}\n\n/** \"$12.99 the first year, then $18.99 a year\": both prices together, always. */\nexport function domainPriceText(prices: { firstYearUsd: number; renewalUsd: number }): string {\n return prices.firstYearUsd === prices.renewalUsd\n ? `${formatDomainUsd(prices.firstYearUsd)} a year`\n : `${formatDomainUsd(prices.firstYearUsd)} the first year, then ${formatDomainUsd(prices.renewalUsd)} a year`\n}\n\n// ── The intent the hand-off carries ────────────────────────────────────────\n\n/** Add-ons, by their `OrgSeatAddons` key, as Billing sells them. */\nexport type ClientBuildAddon = keyof OrgSeatAddons\n\n/**\n * What the owner is asked to buy, as the build leaves it for the hand-off\n * (AGL-3955, AGL-3956): the plan, the add-ons, the interval, a coupon, and\n * the domain they picked. The hand-off dialog opens on it and the bundle\n * link preselects it, so the owner's checkout is one tap from paid.\n *\n * `domain` is a name to REGISTER, bought by the owner after they subscribe;\n * `existingDomain` is one they already hold, which is connected rather than\n * bought. At most one of the two is set.\n */\nexport interface ClientBuildIntent {\n plan: OrgPlan\n addons: ClientBuildAddon[]\n interval: 'month' | 'year'\n coupon?: string\n domain?: { name: string; firstYearUsd: number; renewalUsd: number }\n existingDomain?: string\n}\n\n/** Site Care rides every build for a client unless staff take it off (AGL-3954). */\nexport const CLIENT_BUILD_DEFAULT_ADDONS: readonly ClientBuildAddon[] = ['siteCare']\n\n/** The plans a build may propose: every self-serve tier but Free. */\nconst INTENT_PLANS: readonly OrgPlan[] = SELF_SERVE_PLANS.filter((plan) => plan !== 'free')\n\n/** The intent a build starts with: the plan the workspace was comped on, Site Care, monthly. */\nexport function clientBuildIntent(options: {\n plan: OrgPlan | null | undefined\n domain?: ClientBuildDomain | null\n}): ClientBuildIntent {\n const plan = options.plan && INTENT_PLANS.includes(options.plan) ? options.plan : 'starter'\n const domain = options.domain\n return {\n plan,\n addons: [...CLIENT_BUILD_DEFAULT_ADDONS],\n interval: 'month',\n ...(domain?.choice === 'register'\n ? { domain: { name: domain.name, firstYearUsd: domain.firstYearUsd, renewalUsd: domain.renewalUsd } }\n : {}),\n ...(domain?.choice === 'own' ? { existingDomain: domain.name } : {}),\n }\n}\n\n/**\n * A stored intent, narrowed, or `null` when it is not one worth acting on.\n * What the hand-off reads: a plan this build cannot sell, an add-on it does\n * not know or a malformed domain reads as absent rather than as a checkout\n * for something that does not exist.\n */\nexport function readClientBuildIntent(raw: unknown): ClientBuildIntent | null {\n if (!raw || typeof raw !== 'object') return null\n const source = raw as Record<string, unknown>\n const plan = source['plan']\n if (typeof plan !== 'string' || !(INTENT_PLANS as readonly string[]).includes(plan)) return null\n const addons = Array.isArray(source['addons'])\n ? source['addons'].filter((addon): addon is ClientBuildAddon => typeof addon === 'string' && addon.length > 0)\n : []\n const interval = source['interval'] === 'year' ? 'year' : 'month'\n const coupon = typeof source['coupon'] === 'string' && source['coupon'].trim() ? source['coupon'].trim() : undefined\n const rawDomain = source['domain']\n const parsed =\n rawDomain && typeof rawDomain === 'object' ? parseClientBuildDomain({ ...rawDomain, choice: 'register' }) : null\n const domain = parsed && typeof parsed === 'object' && parsed.choice === 'register' ? parsed : null\n const existing = normalizeClientBuildDomainName(source['existingDomain'])\n return {\n plan: plan as OrgPlan,\n addons,\n interval,\n ...(coupon ? { coupon } : {}),\n ...(domain\n ? { domain: { name: domain.name, firstYearUsd: domain.firstYearUsd, renewalUsd: domain.renewalUsd } }\n : existing\n ? { existingDomain: existing }\n : {}),\n }\n}\n\n// ── The record ─────────────────────────────────────────────────────────────\n\n/**\n * The build as it is stored, one per workspace, at\n * `orgs/{orgId}/clientBuild/{CLIENT_BUILD_DOC_ID}`: a subcollection the rules\n * open to no client, written and read only by `/api/admin/client-build`\n * (staff) through the Admin SDK. Not on the org document, which every\n * member reads: the owner's phone and email are staff's to hold until the\n * owner joins.\n */\nexport interface ClientBuildRecord {\n intake: ClientBuildIntake\n /** `null` until the Domain step is answered. */\n domain: ClientBuildDomain | null\n intent: ClientBuildIntent\n /** The site the build made, once it has. */\n hostId: string | null\n /** Its subdomain, which is what a console URL names a site by. */\n host: string | null\n /** The guided start's job, once it is started. */\n jobId: string | null\n /**\n * The starter site was given instead of a guided start (no AI on the\n * workspace, or staff chose it), so the build step is finished without a job.\n */\n starter: boolean\n /** When the preview was last sent to the owner, ms; `null` before. */\n previewSentAt: number | null\n updatedAt: number\n /** The staff uid that last wrote it. */\n updatedBy: string\n}\n\n/** The subcollection and document the record lives at under its org. */\nexport const CLIENT_BUILD_COLLECTION = 'clientBuild'\nexport const CLIENT_BUILD_DOC_ID = 'current'\n\n/** The screen's steps, in order. */\nexport const CLIENT_BUILD_STEPS = ['details', 'domain', 'photos', 'build', 'send'] as const\nexport type ClientBuildStep = (typeof CLIENT_BUILD_STEPS)[number]\n\n/**\n * Where a stored build resumes: the first step it has not finished. A phone\n * that reloads mid-build, or a second phone, opens where the first left off.\n */\nexport function clientBuildResumeStep(\n record: Pick<ClientBuildRecord, 'domain' | 'hostId' | 'jobId'> & { starter?: boolean } | null,\n): ClientBuildStep {\n if (!record) return 'details'\n if (!record.domain) return 'domain'\n // The site exists before its photos are asked for, and the guided start\n // waits on them: until a job is started, or the starter site was given\n // instead, the build is at the photos.\n if (!record.hostId || (!record.jobId && !record.starter)) return 'photos'\n return 'send'\n}\n\n/**\n * The address the site is going to live at, as the owner is told it: the\n * domain they picked or already have, else the free address it is on.\n */\nexport function clientBuildSiteAddress(\n domain: ClientBuildDomain | null | undefined,\n liveUrl: string | null,\n): string | null {\n if (domain && domain.choice !== 'free') return domain.name\n return liveUrl ? liveUrl.replace(/^https?:\\/\\//, '') : null\n}\n\n/**\n * The text the owner gets with their preview: short enough for one or two\n * segments, and naming who it is from, since it arrives from a number they\n * do not know. The email says the same at length, from the catalog.\n */\nexport function clientBuildPreviewText(options: {\n businessName: string\n previewUrl: string\n siteAddress: string | null\n productName: string\n}): string {\n const name = options.businessName.trim() || 'your business'\n const address =\n options.siteAddress && options.siteAddress !== options.previewUrl.replace(/^https?:\\/\\//, '')\n ? ` It will live at ${options.siteAddress}.`\n : ''\n return `${options.productName}: the new website for ${name} is ready to look at: ${options.previewUrl}${address}`\n}\n"],"names":["normalizePhone","SELF_SERVE_PLANS","CLIENT_BUILD_LIMITS","businessName","whatTheyDo","towns","phone","email","hours","ownerWords","EMPTY_CLIENT_BUILD_INTAKE","EMAIL_PATTERN","clientBuildIntakeErrors","intake","errors","field","Object","keys","trim","length","toLocaleString","test","parseClientBuildIntake","raw","source","value","undefined","first","values","clientBuildPhone","DOMAIN_PATTERN","normalizeClientBuildDomainName","name","toLowerCase","replace","isPrice","Number","isFinite","parseClientBuildDomain","choice","firstYearUsd","renewalUsd","formatDomainUsd","amount","toFixed","domainPriceText","prices","CLIENT_BUILD_DEFAULT_ADDONS","INTENT_PLANS","filter","plan","clientBuildIntent","options","includes","domain","addons","interval","existingDomain","readClientBuildIntent","Array","isArray","addon","coupon","rawDomain","parsed","existing","CLIENT_BUILD_COLLECTION","CLIENT_BUILD_DOC_ID","CLIENT_BUILD_STEPS","clientBuildResumeStep","record","hostId","jobId","starter","clientBuildSiteAddress","liveUrl","clientBuildPreviewText","address","siteAddress","previewUrl","productName"],"mappings":"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"}
@@ -43,6 +43,7 @@ export declare enum Route {
43
43
  ADMIN_EMAILS = "/admin/emails",
44
44
  ADMIN_EMAIL_BESIGNER = "/admin/emails/[templateKey]/versions/[versionId]/besigner",
45
45
  ORG_HOME = "/[orgSlug]",
46
+ ORG_CLIENT_BUILD = "/[orgSlug]/client-build",
46
47
  ORG_MEDIA = "/[orgSlug]/media",
47
48
  ORG_DATA = "/[orgSlug]/data",
48
49
  ORG_CRM = "/[orgSlug]/crm",
@@ -190,6 +191,9 @@ export interface RoutePayload {
190
191
  [Route.ORG_HOME]: {
191
192
  orgSlug: string;
192
193
  };
194
+ [Route.ORG_CLIENT_BUILD]: {
195
+ orgSlug: string;
196
+ };
193
197
  [Route.HOST_DASHBOARD]: {
194
198
  orgSlug: string;
195
199
  host: string;
@@ -105,6 +105,8 @@ export var Route = /*#__PURE__*/ function(Route) {
105
105
  // like the rest of `/admin/*` rather than nested under `[orgSlug]`.
106
106
  Route["ADMIN_EMAIL_BESIGNER"] = "/admin/emails/[templateKey]/versions/[versionId]/besigner";
107
107
  Route["ORG_HOME"] = "/[orgSlug]";
108
+ // Staff build a site for a client on one phone screen (AGL-3958).
109
+ Route["ORG_CLIENT_BUILD"] = "/[orgSlug]/client-build";
108
110
  Route["ORG_MEDIA"] = "/[orgSlug]/media";
109
111
  Route["ORG_DATA"] = "/[orgSlug]/data";
110
112
  /*