@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,62 @@
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
+ * Where a customer's domain must point to reach the tenant edge: the CNAME
19
+ * target and the apex address pool (AGL-3963).
20
+ *
21
+ * A leaf of its own so the three readers that must agree read ONE copy: the
22
+ * console's connect card prints these (`apps/console/utils/tenant-dns.ts`
23
+ * re-exports them beside the instruction copy), the DNS check in
24
+ * `@aglyn/tenant-data-admin/server/tenant-dns-check` accepts them, and the
25
+ * domains plugin's one-click DNS checks the records a provider wrote against
26
+ * them. The plugin cannot import the console app, so the values could only
27
+ * be shared from a lib; a copy in the plugin is the AGL-733 split again.
28
+ *
29
+ * Client-safe on purpose: no Node import, and every `NEXT_PUBLIC_*` read is
30
+ * dot notation so Next inlines it into the card's browser bundle.
31
+ */
32
+ /**
33
+ * The CNAME target a subdomain points at. Public because the wizard prints it.
34
+ */
35
+ export declare const CNAME_TARGET: string;
36
+ /**
37
+ * Addresses the platform host publishes for the apexes it serves; it routes by
38
+ * `Host` rather than by IP, so an apex pointed here never shares an address
39
+ * with `CNAME_TARGET` yet is configured correctly (AGL-1264).
40
+ *
41
+ * `NEXT_PUBLIC_…` is read first so the browser bundle can name the same
42
+ * addresses the route accepts; the server-only name is kept as a fallback so an
43
+ * existing deployment that sets only that one keeps verifying exactly as
44
+ * before. `76.76.21.21` is the host's legacy address — still served, so zones
45
+ * pointed at it must keep verifying, but it is NOT what a newly-pointed apex
46
+ * lands on and must never be the address we recommend (AGL-1264).
47
+ */
48
+ export declare const HOST_APEX_ADDRESSES: string[];
49
+ /**
50
+ * The single address to put in front of a customer *when an address is all the
51
+ * registrar can express*. The accepted list spans the host's whole pool plus the
52
+ * legacy address, but an A record holds exactly one value, and any one address
53
+ * in the pool routes correctly.
54
+ *
55
+ * This is the FALLBACK, not the recommendation: pinning it in a customer zone
56
+ * re-creates by hand the coupling the verify route refuses to make, whose own
57
+ * comment warns that pinning edge IPs "would turn a routine infrastructure
58
+ * change into every customer's domain reporting itself broken". Prefer the
59
+ * ALIAS instruction below, which names `CNAME_TARGET` and so follows the pool
60
+ * (AGL-1327).
61
+ */
62
+ export declare const RECOMMENDED_APEX_ADDRESS: string;
@@ -0,0 +1,80 @@
1
+ var _HOST_APEX_ADDRESSES_;
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ /**
18
+ * Where a customer's domain must point to reach the tenant edge: the CNAME
19
+ * target and the apex address pool (AGL-3963).
20
+ *
21
+ * A leaf of its own so the three readers that must agree read ONE copy: the
22
+ * console's connect card prints these (`apps/console/utils/tenant-dns.ts`
23
+ * re-exports them beside the instruction copy), the DNS check in
24
+ * `@aglyn/tenant-data-admin/server/tenant-dns-check` accepts them, and the
25
+ * domains plugin's one-click DNS checks the records a provider wrote against
26
+ * them. The plugin cannot import the console app, so the values could only
27
+ * be shared from a lib; a copy in the plugin is the AGL-733 split again.
28
+ *
29
+ * Client-safe on purpose: no Node import, and every `NEXT_PUBLIC_*` read is
30
+ * dot notation so Next inlines it into the card's browser bundle.
31
+ */ /**
32
+ * The CNAME target a subdomain points at. Public because the wizard prints it.
33
+ */ export const CNAME_TARGET = // Dot notation, and it is the whole fix (AGL-2037). Next inlines
34
+ // `NEXT_PUBLIC_*` into the browser bundle by textually substituting
35
+ // `process.env.NAME`; the bracket form is never substituted. This module is
36
+ // imported by `custom-domain-card.component.tsx`, which is `'use client'`,
37
+ // so the bracket form read `undefined` in the browser and this constant
38
+ // collapsed to Aglyn's target no matter what an operator configured — their
39
+ // wizard telling their customer to create a DNS record pointing at OUR
40
+ // infrastructure. Note this is the same invariant the header above is about,
41
+ // broken through a different mechanism: the env name was unified by AGL-733,
42
+ // and the ACCESS FORM silently re-split displayed target from verified
43
+ // target, because the verify route runs server-side where brackets work.
44
+ //
45
+ // `||` not `??`, matching AGL-2022: an empty string is not a configured
46
+ // value, and an empty CNAME target is a wizard printing nothing.
47
+ (process.env.NEXT_PUBLIC_AGLYN_TENANT_HOST_CNAME || 'sites.aglyn.app').toLowerCase();
48
+ /**
49
+ * Addresses the platform host publishes for the apexes it serves; it routes by
50
+ * `Host` rather than by IP, so an apex pointed here never shares an address
51
+ * with `CNAME_TARGET` yet is configured correctly (AGL-1264).
52
+ *
53
+ * `NEXT_PUBLIC_…` is read first so the browser bundle can name the same
54
+ * addresses the route accepts; the server-only name is kept as a fallback so an
55
+ * existing deployment that sets only that one keeps verifying exactly as
56
+ * before. `76.76.21.21` is the host's legacy address — still served, so zones
57
+ * pointed at it must keep verifying, but it is NOT what a newly-pointed apex
58
+ * lands on and must never be the address we recommend (AGL-1264).
59
+ */ export const HOST_APEX_ADDRESSES = // Dot form for the public name — same reason as `CNAME_TARGET` above, and it
60
+ // was broken the same way: the browser read `undefined` and the wizard
61
+ // printed Vercel's addresses to an operator's customer. The server-only name
62
+ // below KEEPS the bracket form deliberately: it is never substituted by
63
+ // design, is only meaningful server-side, and the dot form would inline a
64
+ // server-only value into the client bundle.
65
+ (process.env.NEXT_PUBLIC_AGLYN_TENANT_APEX_ADDRESSES || process.env['AGLYN_TENANT_APEX_ADDRESSES'] || '216.198.79.1,216.198.79.65,64.29.17.1,64.29.17.65,76.76.21.21').split(',').map((address)=>address.trim()).filter(Boolean);
66
+ /**
67
+ * The single address to put in front of a customer *when an address is all the
68
+ * registrar can express*. The accepted list spans the host's whole pool plus the
69
+ * legacy address, but an A record holds exactly one value, and any one address
70
+ * in the pool routes correctly.
71
+ *
72
+ * This is the FALLBACK, not the recommendation: pinning it in a customer zone
73
+ * re-creates by hand the coupling the verify route refuses to make, whose own
74
+ * comment warns that pinning edge IPs "would turn a routine infrastructure
75
+ * change into every customer's domain reporting itself broken". Prefer the
76
+ * ALIAS instruction below, which names `CNAME_TARGET` and so follows the pool
77
+ * (AGL-1327).
78
+ */ export const RECOMMENDED_APEX_ADDRESS = (_HOST_APEX_ADDRESSES_ = HOST_APEX_ADDRESSES[0]) != null ? _HOST_APEX_ADDRESSES_ : '216.198.79.1';
79
+
80
+ //# sourceMappingURL=tenant-dns-targets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/tenant-dns-targets.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 * Where a customer's domain must point to reach the tenant edge: the CNAME\n * target and the apex address pool (AGL-3963).\n *\n * A leaf of its own so the three readers that must agree read ONE copy: the\n * console's connect card prints these (`apps/console/utils/tenant-dns.ts`\n * re-exports them beside the instruction copy), the DNS check in\n * `@aglyn/tenant-data-admin/server/tenant-dns-check` accepts them, and the\n * domains plugin's one-click DNS checks the records a provider wrote against\n * them. The plugin cannot import the console app, so the values could only\n * be shared from a lib; a copy in the plugin is the AGL-733 split again.\n *\n * Client-safe on purpose: no Node import, and every `NEXT_PUBLIC_*` read is\n * dot notation so Next inlines it into the card's browser bundle.\n */\n\n/**\n * The CNAME target a subdomain points at. Public because the wizard prints it.\n */\nexport const CNAME_TARGET = (\n // Dot notation, and it is the whole fix (AGL-2037). Next inlines\n // `NEXT_PUBLIC_*` into the browser bundle by textually substituting\n // `process.env.NAME`; the bracket form is never substituted. This module is\n // imported by `custom-domain-card.component.tsx`, which is `'use client'`,\n // so the bracket form read `undefined` in the browser and this constant\n // collapsed to Aglyn's target no matter what an operator configured — their\n // wizard telling their customer to create a DNS record pointing at OUR\n // infrastructure. Note this is the same invariant the header above is about,\n // broken through a different mechanism: the env name was unified by AGL-733,\n // and the ACCESS FORM silently re-split displayed target from verified\n // target, because the verify route runs server-side where brackets work.\n //\n // `||` not `??`, matching AGL-2022: an empty string is not a configured\n // value, and an empty CNAME target is a wizard printing nothing.\n process.env.NEXT_PUBLIC_AGLYN_TENANT_HOST_CNAME || 'sites.aglyn.app'\n).toLowerCase()\n\n/**\n * Addresses the platform host publishes for the apexes it serves; it routes by\n * `Host` rather than by IP, so an apex pointed here never shares an address\n * with `CNAME_TARGET` yet is configured correctly (AGL-1264).\n *\n * `NEXT_PUBLIC_…` is read first so the browser bundle can name the same\n * addresses the route accepts; the server-only name is kept as a fallback so an\n * existing deployment that sets only that one keeps verifying exactly as\n * before. `76.76.21.21` is the host's legacy address — still served, so zones\n * pointed at it must keep verifying, but it is NOT what a newly-pointed apex\n * lands on and must never be the address we recommend (AGL-1264).\n */\nexport const HOST_APEX_ADDRESSES = (\n // Dot form for the public name — same reason as `CNAME_TARGET` above, and it\n // was broken the same way: the browser read `undefined` and the wizard\n // printed Vercel's addresses to an operator's customer. The server-only name\n // below KEEPS the bracket form deliberately: it is never substituted by\n // design, is only meaningful server-side, and the dot form would inline a\n // server-only value into the client bundle.\n process.env.NEXT_PUBLIC_AGLYN_TENANT_APEX_ADDRESSES ||\n process.env['AGLYN_TENANT_APEX_ADDRESSES'] ||\n '216.198.79.1,216.198.79.65,64.29.17.1,64.29.17.65,76.76.21.21'\n)\n .split(',')\n .map((address) => address.trim())\n .filter(Boolean)\n\n/**\n * The single address to put in front of a customer *when an address is all the\n * registrar can express*. The accepted list spans the host's whole pool plus the\n * legacy address, but an A record holds exactly one value, and any one address\n * in the pool routes correctly.\n *\n * This is the FALLBACK, not the recommendation: pinning it in a customer zone\n * re-creates by hand the coupling the verify route refuses to make, whose own\n * comment warns that pinning edge IPs \"would turn a routine infrastructure\n * change into every customer's domain reporting itself broken\". Prefer the\n * ALIAS instruction below, which names `CNAME_TARGET` and so follows the pool\n * (AGL-1327).\n */\nexport const RECOMMENDED_APEX_ADDRESS = HOST_APEX_ADDRESSES[0] ?? '216.198.79.1'\n"],"names":["HOST_APEX_ADDRESSES","CNAME_TARGET","process","env","NEXT_PUBLIC_AGLYN_TENANT_HOST_CNAME","toLowerCase","NEXT_PUBLIC_AGLYN_TENANT_APEX_ADDRESSES","split","map","address","trim","filter","Boolean","RECOMMENDED_APEX_ADDRESS"],"mappings":"IA8FwCA;AA9FxC;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;CAcC,GAED;;CAEC,GACD,OAAO,MAAMC,eAAe,AAC1B,iEAAiE;AACjE,oEAAoE;AACpE,4EAA4E;AAC5E,2EAA2E;AAC3E,wEAAwE;AACxE,4EAA4E;AAC5E,uEAAuE;AACvE,6EAA6E;AAC7E,6EAA6E;AAC7E,uEAAuE;AACvE,yEAAyE;AACzE,EAAE;AACF,wEAAwE;AACxE,iEAAiE;AACjEC,CAAAA,QAAQC,GAAG,CAACC,mCAAmC,IAAI,iBAAgB,EACnEC,WAAW,GAAE;AAEf;;;;;;;;;;;CAWC,GACD,OAAO,MAAML,sBAAsB,AACjC,6EAA6E;AAC7E,uEAAuE;AACvE,6EAA6E;AAC7E,wEAAwE;AACxE,0EAA0E;AAC1E,4CAA4C;AAC5CE,CAAAA,QAAQC,GAAG,CAACG,uCAAuC,IACnDJ,QAAQC,GAAG,CAAC,8BAA8B,IAC1C,+DAA8D,EAE7DI,KAAK,CAAC,KACNC,GAAG,CAAC,CAACC,UAAYA,QAAQC,IAAI,IAC7BC,MAAM,CAACC,SAAQ;AAElB;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,4BAA2Bb,wBAAAA,mBAAmB,CAAC,EAAE,YAAtBA,wBAA0B,eAAc"}
@@ -44,6 +44,20 @@ export declare function normalizeUpgradeProposalNote(note: unknown): string | un
44
44
  * button opens a checkout for an unknown tier is worse than no card.
45
45
  */
46
46
  export declare function readUpgradeProposal(org: Partial<AglynOrgBilling> | null | undefined): OrgUpgradeProposal | null;
47
+ /**
48
+ * The bundle half of a proposal (AGL-3956) as it is stored: add-ons through
49
+ * the shared parser and fitted to the plan, an interval only when one was
50
+ * chosen, a coupon only when it reads as a code, and the Aglyn AI tier
51
+ * (AGL-3965) only with the AI add-on — an `ai:<tier>` entry, or the stored
52
+ * `aiTier`. Whatever cannot be honored is left out rather than stored, so
53
+ * Billing is never asked to preselect something the checkout would refuse.
54
+ */
55
+ export declare function normalizeUpgradeProposalBundle(plan: OrgPlan, input: {
56
+ addons?: unknown;
57
+ interval?: unknown;
58
+ coupon?: unknown;
59
+ aiTier?: unknown;
60
+ } | null | undefined): Pick<OrgUpgradeProposal, 'addons' | 'interval' | 'coupon' | 'aiTier'>;
47
61
  /**
48
62
  * The proposal the workspace should be shown: the stored one, while no
49
63
  * subscription is live.
@@ -55,12 +69,23 @@ export declare function readUpgradeProposal(org: Partial<AglynOrgBilling> | null
55
69
  * clears the field then, and this hides it in the meantime.
56
70
  */
57
71
  export declare function standingUpgradeProposal(org: Partial<AglynOrgBilling> | null | undefined): OrgUpgradeProposal | null;
58
- /** The plan intent a proposal carries into Billing: the plan, no interval. */
59
- export declare function upgradeProposalIntent(proposal: Pick<OrgUpgradeProposal, 'plan'>): OnboardingPlanIntent;
60
- /** Where the proposal's button goes: Billing, with the plan preselected. */
61
- export declare function upgradeProposalDestination(orgSlug: string, proposal: Pick<OrgUpgradeProposal, 'plan'>): string;
72
+ type ProposalBundle = Pick<OrgUpgradeProposal, 'plan'> & Partial<Pick<OrgUpgradeProposal, 'addons' | 'interval' | 'coupon' | 'aiTier'>>;
73
+ /**
74
+ * The intent a proposal carries into Billing: the plan, and the add-ons,
75
+ * cadence and coupon staff proposed with it (AGL-3956) — the same fields the
76
+ * bundle checkout link carries (AGL-3955), so the proposal card and the link
77
+ * land on the same preselected page.
78
+ */
79
+ export declare function upgradeProposalIntent(proposal: ProposalBundle): OnboardingPlanIntent;
80
+ /** Where the proposal's button goes: Billing, with the bundle preselected. */
81
+ export declare function upgradeProposalDestination(orgSlug: string, proposal: ProposalBundle): string;
82
+ /** "Starter with Site Care, billed yearly, code BOOTH25". */
83
+ export declare function upgradeProposalSummary(proposal: ProposalBundle, options?: {
84
+ brand?: string;
85
+ }): string;
62
86
  /** "Acme's Aglyn team proposed the Starter plan". */
63
87
  export declare function upgradeProposalHeadline(proposal: Pick<OrgUpgradeProposal, 'plan'>, options: {
64
88
  orgName?: string | null;
65
89
  productName: string;
66
90
  }): string;
91
+ export {};
@@ -23,9 +23,10 @@
23
23
  * `orgs/{orgId}.upgradeProposal`; this module decides whether it still
24
24
  * stands, what it may name, and where the button under it goes.
25
25
  */ import { _ as _extends } from "@swc/helpers/_/_extends";
26
+ import { describeBundle, fitBundleAddonsToPlan, hasBundleAddons, normalizeBundleCoupon, parseBundleAddons } from "./bundle-intent.js";
26
27
  import { onboardingDestination } from "./onboarding-deep-link.js";
27
28
  import { isLiveSubscriptionStatus } from "./org-billing-doc.js";
28
- import { PLAN_LABELS, SELF_SERVE_PLANS } from "./plan-entitlements.js";
29
+ import { isAiTierId, PLAN_LABELS, SELF_SERVE_PLANS } from "./plan-entitlements.js";
29
30
  /** The longest staff note a proposal carries. */ export const UPGRADE_PROPOSAL_NOTE_MAX = 500;
30
31
  /**
31
32
  * The plans staff may propose: every tier a customer can buy themselves,
@@ -51,12 +52,36 @@ export function isUpgradeProposalPlan(plan) {
51
52
  return null;
52
53
  }
53
54
  const note = normalizeUpgradeProposalNote(raw.note);
55
+ const bundle = normalizeUpgradeProposalBundle(raw.plan, raw);
54
56
  return _extends({
55
57
  plan: raw.plan,
56
58
  proposedBy: typeof raw.proposedBy === 'string' ? raw.proposedBy : '',
57
59
  proposedAt: Number.isFinite(raw.proposedAt) ? Number(raw.proposedAt) : 0
58
60
  }, note ? {
59
61
  note
62
+ } : {}, bundle);
63
+ }
64
+ /**
65
+ * The bundle half of a proposal (AGL-3956) as it is stored: add-ons through
66
+ * the shared parser and fitted to the plan, an interval only when one was
67
+ * chosen, a coupon only when it reads as a code, and the Aglyn AI tier
68
+ * (AGL-3965) only with the AI add-on — an `ai:<tier>` entry, or the stored
69
+ * `aiTier`. Whatever cannot be honored is left out rather than stored, so
70
+ * Billing is never asked to preselect something the checkout would refuse.
71
+ */ export function normalizeUpgradeProposalBundle(plan, input) {
72
+ const parsed = parseBundleAddons(input == null ? void 0 : input.addons);
73
+ const addons = fitBundleAddonsToPlan(parsed.addons, plan).addons;
74
+ const interval = (input == null ? void 0 : input.interval) === 'year' || (input == null ? void 0 : input.interval) === 'month' ? input.interval : undefined;
75
+ const coupon = normalizeBundleCoupon(input == null ? void 0 : input.coupon);
76
+ const aiTier = addons.aiAddon ? isAiTierId(input == null ? void 0 : input.aiTier) ? input.aiTier : parsed.aiTier : undefined;
77
+ return _extends({}, hasBundleAddons(addons) ? {
78
+ addons
79
+ } : {}, interval ? {
80
+ interval
81
+ } : {}, coupon ? {
82
+ coupon
83
+ } : {}, aiTier ? {
84
+ aiTier
60
85
  } : {});
61
86
  }
62
87
  /**
@@ -72,19 +97,45 @@ export function isUpgradeProposalPlan(plan) {
72
97
  if (isLiveSubscriptionStatus(org == null ? void 0 : org.billingStatus)) return null;
73
98
  return readUpgradeProposal(org);
74
99
  }
75
- /** The plan intent a proposal carries into Billing: the plan, no interval. */ export function upgradeProposalIntent(proposal) {
76
- return {
100
+ /**
101
+ * The intent a proposal carries into Billing: the plan, and the add-ons,
102
+ * cadence and coupon staff proposed with it (AGL-3956) — the same fields the
103
+ * bundle checkout link carries (AGL-3955), so the proposal card and the link
104
+ * land on the same preselected page.
105
+ */ export function upgradeProposalIntent(proposal) {
106
+ const addons = fitBundleAddonsToPlan(proposal.addons, proposal.plan).addons;
107
+ const coupon = normalizeBundleCoupon(proposal.coupon);
108
+ const interval = proposal.interval === 'year' || proposal.interval === 'month' ? proposal.interval : null;
109
+ return _extends({
77
110
  plan: proposal.plan,
78
- interval: 'month',
79
- // The proposal names a plan, not a cadence, so the page keeps whatever
80
- // its toggle says rather than being told "monthly".
81
- intervalStated: false,
111
+ interval: interval != null ? interval : 'month',
112
+ // A proposal with no cadence names a plan, not a cadence, so the page
113
+ // keeps whatever its toggle says rather than being told "monthly".
114
+ intervalStated: interval !== null,
82
115
  contactSales: false
83
- };
116
+ }, addons.aiAddon ? {
117
+ ai: true
118
+ } : {}, addons.aiAddon && isAiTierId(proposal.aiTier) ? {
119
+ aiTier: proposal.aiTier
120
+ } : {}, hasBundleAddons(addons) ? {
121
+ addons
122
+ } : {}, coupon ? {
123
+ coupon
124
+ } : {});
84
125
  }
85
- /** Where the proposal's button goes: Billing, with the plan preselected. */ export function upgradeProposalDestination(orgSlug, proposal) {
126
+ /** Where the proposal's button goes: Billing, with the bundle preselected. */ export function upgradeProposalDestination(orgSlug, proposal) {
86
127
  return onboardingDestination(orgSlug, upgradeProposalIntent(proposal));
87
128
  }
129
+ /** "Starter with Site Care, billed yearly, code BOOTH25". */ export function upgradeProposalSummary(proposal, options = {}) {
130
+ var _proposal_addons, _proposal_interval, _proposal_coupon, _proposal_aiTier;
131
+ return describeBundle({
132
+ plan: proposal.plan,
133
+ addons: (_proposal_addons = proposal.addons) != null ? _proposal_addons : null,
134
+ interval: (_proposal_interval = proposal.interval) != null ? _proposal_interval : null,
135
+ coupon: (_proposal_coupon = proposal.coupon) != null ? _proposal_coupon : null,
136
+ aiTier: (_proposal_aiTier = proposal.aiTier) != null ? _proposal_aiTier : null
137
+ }, options);
138
+ }
88
139
  /** "Acme's Aglyn team proposed the Starter plan". */ export function upgradeProposalHeadline(proposal, options) {
89
140
  var _options_orgName;
90
141
  const workspace = (_options_orgName = options.orgName) == null ? void 0 : _options_orgName.trim();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/upgrade-proposal.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 platform team's request that a workspace move to a paid plan\n * (AGL-3466), read in one place.\n *\n * A workspace staff build for a client is handed over first and sold second:\n * the client joins, looks around on whatever the workspace already has, and\n * only then is asked to upgrade. Staff record that ask as\n * `orgs/{orgId}.upgradeProposal`; this module decides whether it still\n * stands, what it may name, and where the button under it goes.\n */\n\nimport type {\n AglynOrgBilling,\n OrgPlan,\n OrgUpgradeProposal,\n} from '../foundation/definitions/org-billing.types'\nimport {\n onboardingDestination,\n type OnboardingPlanIntent,\n} from './onboarding-deep-link'\nimport { isLiveSubscriptionStatus } from './org-billing-doc'\nimport { PLAN_LABELS, SELF_SERVE_PLANS } from './plan-entitlements'\n\n/** The longest staff note a proposal carries. */\nexport const UPGRADE_PROPOSAL_NOTE_MAX = 500\n\n/**\n * The plans staff may propose: every tier a customer can buy themselves,\n * less Free, which is not a purchase. Enterprise is quoted per deal, so it\n * is never proposed through a checkout link.\n */\nexport const UPGRADE_PROPOSAL_PLANS: readonly OrgPlan[] = SELF_SERVE_PLANS.filter(\n (plan) => plan !== 'free',\n)\n\nexport function isUpgradeProposalPlan(plan: unknown): plan is OrgPlan {\n return (\n typeof plan === 'string' &&\n (UPGRADE_PROPOSAL_PLANS as readonly string[]).includes(plan)\n )\n}\n\n/** A staff note as it is stored: trimmed, bounded, and absent when blank. */\nexport function normalizeUpgradeProposalNote(note: unknown): string | undefined {\n if (typeof note !== 'string') return undefined\n const trimmed = note.trim().slice(0, UPGRADE_PROPOSAL_NOTE_MAX)\n return trimmed || undefined\n}\n\n/**\n * The stored proposal, narrowed, or null when there is none worth showing.\n *\n * A proposal naming a plan this build cannot sell reads as none: a card whose\n * button opens a checkout for an unknown tier is worse than no card.\n */\nexport function readUpgradeProposal(\n org: Partial<AglynOrgBilling> | null | undefined,\n): OrgUpgradeProposal | null {\n const raw = org?.upgradeProposal as Partial<OrgUpgradeProposal> | null | undefined\n if (!raw || typeof raw !== 'object' || !isUpgradeProposalPlan(raw.plan)) {\n return null\n }\n const note = normalizeUpgradeProposalNote(raw.note)\n return {\n plan: raw.plan,\n proposedBy: typeof raw.proposedBy === 'string' ? raw.proposedBy : '',\n proposedAt: Number.isFinite(raw.proposedAt) ? Number(raw.proposedAt) : 0,\n ...(note ? { note } : {}),\n }\n}\n\n/**\n * The proposal the workspace should be shown: the stored one, while no\n * subscription is live.\n *\n * `billingStatus` is the org document's mirror of the subscription status,\n * readable by every member, so this answers from the document the console\n * already holds. A live subscription means the workspace has bought a plan,\n * which settles the question the proposal asked; `writeOrgBilling`'s caller\n * clears the field then, and this hides it in the meantime.\n */\nexport function standingUpgradeProposal(\n org: Partial<AglynOrgBilling> | null | undefined,\n): OrgUpgradeProposal | null {\n if (isLiveSubscriptionStatus(org?.billingStatus)) return null\n return readUpgradeProposal(org)\n}\n\n/** The plan intent a proposal carries into Billing: the plan, no interval. */\nexport function upgradeProposalIntent(\n proposal: Pick<OrgUpgradeProposal, 'plan'>,\n): OnboardingPlanIntent {\n return {\n plan: proposal.plan,\n interval: 'month',\n // The proposal names a plan, not a cadence, so the page keeps whatever\n // its toggle says rather than being told \"monthly\".\n intervalStated: false,\n contactSales: false,\n }\n}\n\n/** Where the proposal's button goes: Billing, with the plan preselected. */\nexport function upgradeProposalDestination(\n orgSlug: string,\n proposal: Pick<OrgUpgradeProposal, 'plan'>,\n): string {\n return onboardingDestination(orgSlug, upgradeProposalIntent(proposal))\n}\n\n/** \"Acme's Aglyn team proposed the Starter plan\". */\nexport function upgradeProposalHeadline(\n proposal: Pick<OrgUpgradeProposal, 'plan'>,\n options: { orgName?: string | null; productName: string },\n): string {\n const workspace = options.orgName?.trim()\n const team = workspace\n ? `${workspace}'s ${options.productName} team`\n : `Your ${options.productName} team`\n return `${team} proposed the ${PLAN_LABELS[proposal.plan]} plan`\n}\n"],"names":["onboardingDestination","isLiveSubscriptionStatus","PLAN_LABELS","SELF_SERVE_PLANS","UPGRADE_PROPOSAL_NOTE_MAX","UPGRADE_PROPOSAL_PLANS","filter","plan","isUpgradeProposalPlan","includes","normalizeUpgradeProposalNote","note","undefined","trimmed","trim","slice","readUpgradeProposal","org","raw","upgradeProposal","proposedBy","proposedAt","Number","isFinite","standingUpgradeProposal","billingStatus","upgradeProposalIntent","proposal","interval","intervalStated","contactSales","upgradeProposalDestination","orgSlug","upgradeProposalHeadline","options","workspace","orgName","team","productName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;CASC;AAOD,SACEA,qBAAqB,QAEhB,4BAAwB;AAC/B,SAASC,wBAAwB,QAAQ,uBAAmB;AAC5D,SAASC,WAAW,EAAEC,gBAAgB,QAAQ,yBAAqB;AAEnE,+CAA+C,GAC/C,OAAO,MAAMC,4BAA4B,IAAG;AAE5C;;;;CAIC,GACD,OAAO,MAAMC,yBAA6CF,iBAAiBG,MAAM,CAC/E,CAACC,OAASA,SAAS,QACpB;AAED,OAAO,SAASC,sBAAsBD,IAAa;IACjD,OACE,OAAOA,SAAS,YAChB,AAACF,uBAA6CI,QAAQ,CAACF;AAE3D;AAEA,2EAA2E,GAC3E,OAAO,SAASG,6BAA6BC,IAAa;IACxD,IAAI,OAAOA,SAAS,UAAU,OAAOC;IACrC,MAAMC,UAAUF,KAAKG,IAAI,GAAGC,KAAK,CAAC,GAAGX;IACrC,OAAOS,WAAWD;AACpB;AAEA;;;;;CAKC,GACD,OAAO,SAASI,oBACdC,GAAgD;IAEhD,MAAMC,MAAMD,uBAAAA,IAAKE,eAAe;IAChC,IAAI,CAACD,OAAO,OAAOA,QAAQ,YAAY,CAACV,sBAAsBU,IAAIX,IAAI,GAAG;QACvE,OAAO;IACT;IACA,MAAMI,OAAOD,6BAA6BQ,IAAIP,IAAI;IAClD,OAAO;QACLJ,MAAMW,IAAIX,IAAI;QACda,YAAY,OAAOF,IAAIE,UAAU,KAAK,WAAWF,IAAIE,UAAU,GAAG;QAClEC,YAAYC,OAAOC,QAAQ,CAACL,IAAIG,UAAU,IAAIC,OAAOJ,IAAIG,UAAU,IAAI;OACnEV,OAAO;QAAEA;IAAK,IAAI,CAAC;AAE3B;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASa,wBACdP,GAAgD;IAEhD,IAAIhB,yBAAyBgB,uBAAAA,IAAKQ,aAAa,GAAG,OAAO;IACzD,OAAOT,oBAAoBC;AAC7B;AAEA,4EAA4E,GAC5E,OAAO,SAASS,sBACdC,QAA0C;IAE1C,OAAO;QACLpB,MAAMoB,SAASpB,IAAI;QACnBqB,UAAU;QACV,uEAAuE;QACvE,oDAAoD;QACpDC,gBAAgB;QAChBC,cAAc;IAChB;AACF;AAEA,0EAA0E,GAC1E,OAAO,SAASC,2BACdC,OAAe,EACfL,QAA0C;IAE1C,OAAO3B,sBAAsBgC,SAASN,sBAAsBC;AAC9D;AAEA,mDAAmD,GACnD,OAAO,SAASM,wBACdN,QAA0C,EAC1CO,OAAyD;QAEvCA;IAAlB,MAAMC,aAAYD,mBAAAA,QAAQE,OAAO,qBAAfF,iBAAiBpB,IAAI;IACvC,MAAMuB,OAAOF,YACT,GAAGA,UAAU,GAAG,EAAED,QAAQI,WAAW,CAAC,KAAK,CAAC,GAC5C,CAAC,KAAK,EAAEJ,QAAQI,WAAW,CAAC,KAAK,CAAC;IACtC,OAAO,GAAGD,KAAK,cAAc,EAAEnC,WAAW,CAACyB,SAASpB,IAAI,CAAC,CAAC,KAAK,CAAC;AAClE"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/upgrade-proposal.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 platform team's request that a workspace move to a paid plan\n * (AGL-3466), read in one place.\n *\n * A workspace staff build for a client is handed over first and sold second:\n * the client joins, looks around on whatever the workspace already has, and\n * only then is asked to upgrade. Staff record that ask as\n * `orgs/{orgId}.upgradeProposal`; this module decides whether it still\n * stands, what it may name, and where the button under it goes.\n */\n\nimport type {\n AglynOrgBilling,\n OrgPlan,\n OrgUpgradeProposal,\n} from '../foundation/definitions/org-billing.types'\nimport {\n describeBundle,\n fitBundleAddonsToPlan,\n hasBundleAddons,\n normalizeBundleCoupon,\n parseBundleAddons,\n type BundleAddons,\n} from './bundle-intent'\nimport {\n onboardingDestination,\n type OnboardingPlanIntent,\n} from './onboarding-deep-link'\nimport { isLiveSubscriptionStatus } from './org-billing-doc'\nimport { isAiTierId, PLAN_LABELS, SELF_SERVE_PLANS } from './plan-entitlements'\n\n/** The longest staff note a proposal carries. */\nexport const UPGRADE_PROPOSAL_NOTE_MAX = 500\n\n/**\n * The plans staff may propose: every tier a customer can buy themselves,\n * less Free, which is not a purchase. Enterprise is quoted per deal, so it\n * is never proposed through a checkout link.\n */\nexport const UPGRADE_PROPOSAL_PLANS: readonly OrgPlan[] = SELF_SERVE_PLANS.filter(\n (plan) => plan !== 'free',\n)\n\nexport function isUpgradeProposalPlan(plan: unknown): plan is OrgPlan {\n return (\n typeof plan === 'string' &&\n (UPGRADE_PROPOSAL_PLANS as readonly string[]).includes(plan)\n )\n}\n\n/** A staff note as it is stored: trimmed, bounded, and absent when blank. */\nexport function normalizeUpgradeProposalNote(note: unknown): string | undefined {\n if (typeof note !== 'string') return undefined\n const trimmed = note.trim().slice(0, UPGRADE_PROPOSAL_NOTE_MAX)\n return trimmed || undefined\n}\n\n/**\n * The stored proposal, narrowed, or null when there is none worth showing.\n *\n * A proposal naming a plan this build cannot sell reads as none: a card whose\n * button opens a checkout for an unknown tier is worse than no card.\n */\nexport function readUpgradeProposal(\n org: Partial<AglynOrgBilling> | null | undefined,\n): OrgUpgradeProposal | null {\n const raw = org?.upgradeProposal as Partial<OrgUpgradeProposal> | null | undefined\n if (!raw || typeof raw !== 'object' || !isUpgradeProposalPlan(raw.plan)) {\n return null\n }\n const note = normalizeUpgradeProposalNote(raw.note)\n const bundle = normalizeUpgradeProposalBundle(raw.plan, raw)\n return {\n plan: raw.plan,\n proposedBy: typeof raw.proposedBy === 'string' ? raw.proposedBy : '',\n proposedAt: Number.isFinite(raw.proposedAt) ? Number(raw.proposedAt) : 0,\n ...(note ? { note } : {}),\n ...bundle,\n }\n}\n\n/**\n * The bundle half of a proposal (AGL-3956) as it is stored: add-ons through\n * the shared parser and fitted to the plan, an interval only when one was\n * chosen, a coupon only when it reads as a code, and the Aglyn AI tier\n * (AGL-3965) only with the AI add-on — an `ai:<tier>` entry, or the stored\n * `aiTier`. Whatever cannot be honored is left out rather than stored, so\n * Billing is never asked to preselect something the checkout would refuse.\n */\nexport function normalizeUpgradeProposalBundle(\n plan: OrgPlan,\n input:\n | { addons?: unknown; interval?: unknown; coupon?: unknown; aiTier?: unknown }\n | null\n | undefined,\n): Pick<OrgUpgradeProposal, 'addons' | 'interval' | 'coupon' | 'aiTier'> {\n const parsed = parseBundleAddons(input?.addons)\n const addons: BundleAddons = fitBundleAddonsToPlan(parsed.addons, plan).addons\n const interval =\n input?.interval === 'year' || input?.interval === 'month' ? input.interval : undefined\n const coupon = normalizeBundleCoupon(input?.coupon)\n const aiTier = addons.aiAddon\n ? isAiTierId(input?.aiTier)\n ? input.aiTier\n : parsed.aiTier\n : undefined\n return {\n ...(hasBundleAddons(addons) ? { addons } : {}),\n ...(interval ? { interval } : {}),\n ...(coupon ? { coupon } : {}),\n ...(aiTier ? { aiTier } : {}),\n }\n}\n\n/**\n * The proposal the workspace should be shown: the stored one, while no\n * subscription is live.\n *\n * `billingStatus` is the org document's mirror of the subscription status,\n * readable by every member, so this answers from the document the console\n * already holds. A live subscription means the workspace has bought a plan,\n * which settles the question the proposal asked; `writeOrgBilling`'s caller\n * clears the field then, and this hides it in the meantime.\n */\nexport function standingUpgradeProposal(\n org: Partial<AglynOrgBilling> | null | undefined,\n): OrgUpgradeProposal | null {\n if (isLiveSubscriptionStatus(org?.billingStatus)) return null\n return readUpgradeProposal(org)\n}\n\ntype ProposalBundle = Pick<OrgUpgradeProposal, 'plan'> &\n Partial<Pick<OrgUpgradeProposal, 'addons' | 'interval' | 'coupon' | 'aiTier'>>\n\n/**\n * The intent a proposal carries into Billing: the plan, and the add-ons,\n * cadence and coupon staff proposed with it (AGL-3956) — the same fields the\n * bundle checkout link carries (AGL-3955), so the proposal card and the link\n * land on the same preselected page.\n */\nexport function upgradeProposalIntent(proposal: ProposalBundle): OnboardingPlanIntent {\n const addons = fitBundleAddonsToPlan(proposal.addons, proposal.plan).addons\n const coupon = normalizeBundleCoupon(proposal.coupon)\n const interval = proposal.interval === 'year' || proposal.interval === 'month'\n ? proposal.interval\n : null\n return {\n plan: proposal.plan,\n interval: interval ?? 'month',\n // A proposal with no cadence names a plan, not a cadence, so the page\n // keeps whatever its toggle says rather than being told \"monthly\".\n intervalStated: interval !== null,\n contactSales: false,\n ...(addons.aiAddon ? { ai: true as const } : {}),\n ...(addons.aiAddon && isAiTierId(proposal.aiTier) ? { aiTier: proposal.aiTier } : {}),\n ...(hasBundleAddons(addons) ? { addons } : {}),\n ...(coupon ? { coupon } : {}),\n }\n}\n\n/** Where the proposal's button goes: Billing, with the bundle preselected. */\nexport function upgradeProposalDestination(\n orgSlug: string,\n proposal: ProposalBundle,\n): string {\n return onboardingDestination(orgSlug, upgradeProposalIntent(proposal))\n}\n\n/** \"Starter with Site Care, billed yearly, code BOOTH25\". */\nexport function upgradeProposalSummary(\n proposal: ProposalBundle,\n options: { brand?: string } = {},\n): string {\n return describeBundle(\n {\n plan: proposal.plan,\n addons: proposal.addons ?? null,\n interval: proposal.interval ?? null,\n coupon: proposal.coupon ?? null,\n aiTier: proposal.aiTier ?? null,\n },\n options,\n )\n}\n\n/** \"Acme's Aglyn team proposed the Starter plan\". */\nexport function upgradeProposalHeadline(\n proposal: Pick<OrgUpgradeProposal, 'plan'>,\n options: { orgName?: string | null; productName: string },\n): string {\n const workspace = options.orgName?.trim()\n const team = workspace\n ? `${workspace}'s ${options.productName} team`\n : `Your ${options.productName} team`\n return `${team} proposed the ${PLAN_LABELS[proposal.plan]} plan`\n}\n"],"names":["describeBundle","fitBundleAddonsToPlan","hasBundleAddons","normalizeBundleCoupon","parseBundleAddons","onboardingDestination","isLiveSubscriptionStatus","isAiTierId","PLAN_LABELS","SELF_SERVE_PLANS","UPGRADE_PROPOSAL_NOTE_MAX","UPGRADE_PROPOSAL_PLANS","filter","plan","isUpgradeProposalPlan","includes","normalizeUpgradeProposalNote","note","undefined","trimmed","trim","slice","readUpgradeProposal","org","raw","upgradeProposal","bundle","normalizeUpgradeProposalBundle","proposedBy","proposedAt","Number","isFinite","input","parsed","addons","interval","coupon","aiTier","aiAddon","standingUpgradeProposal","billingStatus","upgradeProposalIntent","proposal","intervalStated","contactSales","ai","upgradeProposalDestination","orgSlug","upgradeProposalSummary","options","upgradeProposalHeadline","workspace","orgName","team","productName"],"mappings":"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"}
@@ -135,11 +135,10 @@ export interface CoreOrgFeatureFlags {
135
135
  * generate-section rung every tier from Pro up carries. This is the
136
136
  * expensive rung: a generated surface carries the node tree, the catalog
137
137
  * and the theme in and iterates, so it draws credits by the hundreds where
138
- * a question draws tens. No self-serve tier includes it; the Aglyn AI
139
- * add-on (`OrgSeatAddons.aiAddon`) switches it on and adds the credit band
140
- * that funds it, and `resolveOrgEntitlements` flips this flag together
141
- * with `aiAssist` when the add-on is present. Enterprise carries it in the
142
- * agreement.
138
+ * a question draws tens. Since AGL-3965 every plan carries it: the plan's
139
+ * included `assistCreditsPerMonth` pays for generation and assist alike
140
+ * (one pool), an Aglyn AI tier adds credits on top, and Free's taste keeps
141
+ * its own wall. Enterprise carries it in the agreement.
143
142
  */
144
143
  aiGenerative?: boolean;
145
144
  /** No-code workflow builder (AGL-101). */
@@ -222,6 +221,13 @@ export interface CoreOrgFeatureFlags {
222
221
  multilingual?: boolean;
223
222
  /** Event Calendar add-on (AGL-145); paid, not part of any base tier. */
224
223
  eventCalendar?: boolean;
224
+ /**
225
+ * Site Care add-on (AGL-3954): staff make the owner's small site changes,
226
+ * about an hour of work a month. Paid, never part of any base tier — true
227
+ * only while `OrgSeatAddons.siteCare` is bought on a live subscription, or
228
+ * by a staff override.
229
+ */
230
+ siteCare?: boolean;
225
231
  /** URL redirects manager (AGL-154). */
226
232
  redirects?: boolean;
227
233
  /** Per-screen traffic analytics (AGL-150). */
@@ -654,6 +660,11 @@ export interface OrgPlanComp {
654
660
  /** When it was granted — the server's clock. */
655
661
  grantedAt: ITimestamp | null;
656
662
  }
663
+ /**
664
+ * The Aglyn AI tiers (AGL-3965), smallest first. Sold on their own to any
665
+ * paid plan; the figures are `AI_TIERS` in `plan-entitlements.ts`.
666
+ */
667
+ export type AiTierId = 'spark' | 'boost' | 'plus' | 'max' | 'ultra' | 'studio';
657
668
  /**
658
669
  * Paid addon quantities (AGL-112/524) purchased on top of the plan's
659
670
  * included allowances, billed as items on the org's Stripe subscription.
@@ -699,14 +710,20 @@ export interface OrgSeatAddons {
699
710
  posRegisters?: number;
700
711
  /** Event Calendar org-wide toggle, 0/1 (AGL-145/524). */
701
712
  eventCalendar?: number;
713
+ /**
714
+ * Site Care org-wide toggle, 0/1 (AGL-3954): the managed-website add-on,
715
+ * flat at `SITE_CARE_ADDON_MONTHLY_USD` on every paid self-serve plan.
716
+ * `resolveOrgEntitlements` reads any quantity >= 1 as one and switches
717
+ * `features.siteCare` on; it raises no quota.
718
+ */
719
+ siteCare?: number;
702
720
  /**
703
721
  * Aglyn AI org-wide toggle, 0/1 (AGL-2896). One purchase covers every
704
722
  * host in the org, the way Event Calendar does: `resolveOrgEntitlements`
705
- * reads any quantity >= 1 as one, adds the plan's
706
- * `AI_ADDON_CREDITS_PER_MONTH` band to `assistCreditsPerMonth`, and
707
- * switches `features.aiGenerative` and `features.aiAssist` on. Priced per
708
- * plan at `PlanPricing.aiAddonMonthlyUsd`; `null` there means the plan
709
- * does not sell it.
723
+ * reads any quantity >= 1 as one and adds the purchased tier's credits
724
+ * (`AI_TIERS[org.aiTier].credits`, AGL-3965) to `assistCreditsPerMonth`.
725
+ * WHICH tier is `AglynOrgBilling.aiTier`; the price is the tier's, on any
726
+ * paid plan (`planSellsAiTiers`).
710
727
  */
711
728
  aiAddon?: number;
712
729
  }
@@ -1128,6 +1145,18 @@ export interface AglynOrgBilling extends AglynDocument {
1128
1145
  enabledPlugins?: string[];
1129
1146
  /** Purchased addon seats (AGL-112); billed monthly per seat. */
1130
1147
  seatAddons?: OrgSeatAddons;
1148
+ /**
1149
+ * Which Aglyn AI tier `seatAddons.aiAddon` is (AGL-3965): Spark through
1150
+ * Studio, read off the subscription's AI item by the webhook and the
1151
+ * add-ons route, and written beside `seatAddons` by the same writes. An
1152
+ * ENTITLEMENT INPUT like `seatAddons`, so Admin-SDK-only.
1153
+ *
1154
+ * Absent on an org whose add-on was bought before tiers existed: it then
1155
+ * reads as the tier with that plan's old band and price
1156
+ * (`LEGACY_AI_ADDON_TIER_BY_PLAN`), so no bill and no credit figure moved.
1157
+ * `null` once written means no tier is held.
1158
+ */
1159
+ aiTier?: AiTierId | null;
1131
1160
  /**
1132
1161
  * POS register seats assigned out of the org pool (AGL-1775). An
1133
1162
  * ENTITLEMENT INPUT: it raises a per-site cap, so it is Admin-SDK-only.
@@ -1245,6 +1274,21 @@ export interface OrgUpgradeProposal {
1245
1274
  proposedAt: number;
1246
1275
  /** An optional line from staff, shown with the proposal. */
1247
1276
  note?: string;
1277
+ /**
1278
+ * The add-ons proposed with the plan (AGL-3956), by `org.seatAddons` kind,
1279
+ * fitted to what the plan sells. Billing preselects them beside the plan.
1280
+ */
1281
+ addons?: Partial<Record<keyof Required<OrgSeatAddons>, number>>;
1282
+ /** The cadence proposed, when staff chose one; absent leaves Billing's toggle alone. */
1283
+ interval?: 'month' | 'year';
1284
+ /** A promotion code minted on the staff Coupons page, applied at checkout. */
1285
+ coupon?: string;
1286
+ /**
1287
+ * The Aglyn AI tier proposed with the AI add-on (AGL-3965), written
1288
+ * `ai:<tier>` in the add-on list. Absent, checkout sells the plan's
1289
+ * default tier.
1290
+ */
1291
+ aiTier?: AiTierId;
1248
1292
  }
1249
1293
  /**
1250
1294
  * What the CRM lets an organization decide for every site at once