@aglyn/aglyn 1.0.0-beta.143

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 (963) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +11 -0
  3. package/package.json +66 -0
  4. package/src/index.d.ts +25 -0
  5. package/src/index.js +26 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/aglyn.d.ts +31 -0
  8. package/src/lib/aglyn.js +114 -0
  9. package/src/lib/aglyn.js.map +1 -0
  10. package/src/lib/app-utils/abuse-report.d.ts +193 -0
  11. package/src/lib/app-utils/abuse-report.js +321 -0
  12. package/src/lib/app-utils/abuse-report.js.map +1 -0
  13. package/src/lib/app-utils/accept-negotiation.d.ts +129 -0
  14. package/src/lib/app-utils/accept-negotiation.js +266 -0
  15. package/src/lib/app-utils/accept-negotiation.js.map +1 -0
  16. package/src/lib/app-utils/account-emails.d.ts +263 -0
  17. package/src/lib/app-utils/account-emails.js +264 -0
  18. package/src/lib/app-utils/account-emails.js.map +1 -0
  19. package/src/lib/app-utils/actions.d.ts +717 -0
  20. package/src/lib/app-utils/actions.js +1031 -0
  21. package/src/lib/app-utils/actions.js.map +1 -0
  22. package/src/lib/app-utils/activity-presenter.d.ts +152 -0
  23. package/src/lib/app-utils/activity-presenter.js +326 -0
  24. package/src/lib/app-utils/activity-presenter.js.map +1 -0
  25. package/src/lib/app-utils/advertising-tag-mounts.d.ts +133 -0
  26. package/src/lib/app-utils/advertising-tag-mounts.js +118 -0
  27. package/src/lib/app-utils/advertising-tag-mounts.js.map +1 -0
  28. package/src/lib/app-utils/advertising-tags.d.ts +307 -0
  29. package/src/lib/app-utils/advertising-tags.js +525 -0
  30. package/src/lib/app-utils/advertising-tags.js.map +1 -0
  31. package/src/lib/app-utils/agent-api-catalog.d.ts +94 -0
  32. package/src/lib/app-utils/agent-api-catalog.js +106 -0
  33. package/src/lib/app-utils/agent-api-catalog.js.map +1 -0
  34. package/src/lib/app-utils/agent-openapi.d.ts +103 -0
  35. package/src/lib/app-utils/agent-openapi.js +1128 -0
  36. package/src/lib/app-utils/agent-openapi.js.map +1 -0
  37. package/src/lib/app-utils/analytics-beacon.d.ts +154 -0
  38. package/src/lib/app-utils/analytics-beacon.js +152 -0
  39. package/src/lib/app-utils/analytics-beacon.js.map +1 -0
  40. package/src/lib/app-utils/analytics-environment.d.ts +133 -0
  41. package/src/lib/app-utils/analytics-environment.js +152 -0
  42. package/src/lib/app-utils/analytics-environment.js.map +1 -0
  43. package/src/lib/app-utils/analytics-events.d.ts +744 -0
  44. package/src/lib/app-utils/analytics-events.js +650 -0
  45. package/src/lib/app-utils/analytics-events.js.map +1 -0
  46. package/src/lib/app-utils/analytics-link-clicks.d.ts +102 -0
  47. package/src/lib/app-utils/analytics-link-clicks.js +267 -0
  48. package/src/lib/app-utils/analytics-link-clicks.js.map +1 -0
  49. package/src/lib/app-utils/analytics-path-key.d.ts +46 -0
  50. package/src/lib/app-utils/analytics-path-key.js +46 -0
  51. package/src/lib/app-utils/analytics-path-key.js.map +1 -0
  52. package/src/lib/app-utils/api-adapter.d.ts +47 -0
  53. package/src/lib/app-utils/api-adapter.js +367 -0
  54. package/src/lib/app-utils/api-adapter.js.map +1 -0
  55. package/src/lib/app-utils/api-idempotency.d.ts +169 -0
  56. package/src/lib/app-utils/api-idempotency.js +213 -0
  57. package/src/lib/app-utils/api-idempotency.js.map +1 -0
  58. package/src/lib/app-utils/api-plugins.d.ts +210 -0
  59. package/src/lib/app-utils/api-plugins.js +221 -0
  60. package/src/lib/app-utils/api-plugins.js.map +1 -0
  61. package/src/lib/app-utils/assist-credits.d.ts +482 -0
  62. package/src/lib/app-utils/assist-credits.js +509 -0
  63. package/src/lib/app-utils/assist-credits.js.map +1 -0
  64. package/src/lib/app-utils/attribution-attribute.d.ts +34 -0
  65. package/src/lib/app-utils/attribution-attribute.js +34 -0
  66. package/src/lib/app-utils/attribution-attribute.js.map +1 -0
  67. package/src/lib/app-utils/attribution-guard.d.ts +130 -0
  68. package/src/lib/app-utils/attribution-guard.js +410 -0
  69. package/src/lib/app-utils/attribution-guard.js.map +1 -0
  70. package/src/lib/app-utils/author-css.d.ts +123 -0
  71. package/src/lib/app-utils/author-css.js +275 -0
  72. package/src/lib/app-utils/author-css.js.map +1 -0
  73. package/src/lib/app-utils/author-html-markdown.d.ts +31 -0
  74. package/src/lib/app-utils/author-html-markdown.js +497 -0
  75. package/src/lib/app-utils/author-html-markdown.js.map +1 -0
  76. package/src/lib/app-utils/author-html.d.ts +135 -0
  77. package/src/lib/app-utils/author-html.js +1037 -0
  78. package/src/lib/app-utils/author-html.js.map +1 -0
  79. package/src/lib/app-utils/automation-placeholders.d.ts +72 -0
  80. package/src/lib/app-utils/automation-placeholders.js +207 -0
  81. package/src/lib/app-utils/automation-placeholders.js.map +1 -0
  82. package/src/lib/app-utils/bandwidth-cap.d.ts +183 -0
  83. package/src/lib/app-utils/bandwidth-cap.js +193 -0
  84. package/src/lib/app-utils/bandwidth-cap.js.map +1 -0
  85. package/src/lib/app-utils/binding-token-catalog.d.ts +82 -0
  86. package/src/lib/app-utils/binding-token-catalog.js +293 -0
  87. package/src/lib/app-utils/binding-token-catalog.js.map +1 -0
  88. package/src/lib/app-utils/binding-tokens.d.ts +157 -0
  89. package/src/lib/app-utils/binding-tokens.js +251 -0
  90. package/src/lib/app-utils/binding-tokens.js.map +1 -0
  91. package/src/lib/app-utils/breadcrumb-json-ld.d.ts +57 -0
  92. package/src/lib/app-utils/breadcrumb-json-ld.js +67 -0
  93. package/src/lib/app-utils/breadcrumb-json-ld.js.map +1 -0
  94. package/src/lib/app-utils/campaign-attribution.d.ts +117 -0
  95. package/src/lib/app-utils/campaign-attribution.js +162 -0
  96. package/src/lib/app-utils/campaign-attribution.js.map +1 -0
  97. package/src/lib/app-utils/campaign-forwarding.d.ts +220 -0
  98. package/src/lib/app-utils/campaign-forwarding.js +332 -0
  99. package/src/lib/app-utils/campaign-forwarding.js.map +1 -0
  100. package/src/lib/app-utils/campaign-membership.d.ts +109 -0
  101. package/src/lib/app-utils/campaign-membership.js +176 -0
  102. package/src/lib/app-utils/campaign-membership.js.map +1 -0
  103. package/src/lib/app-utils/campaign-touch.d.ts +206 -0
  104. package/src/lib/app-utils/campaign-touch.js +326 -0
  105. package/src/lib/app-utils/campaign-touch.js.map +1 -0
  106. package/src/lib/app-utils/child-contract-compose.d.ts +28 -0
  107. package/src/lib/app-utils/child-contract-compose.js +140 -0
  108. package/src/lib/app-utils/child-contract-compose.js.map +1 -0
  109. package/src/lib/app-utils/child-contract.d.ts +129 -0
  110. package/src/lib/app-utils/child-contract.js +153 -0
  111. package/src/lib/app-utils/child-contract.js.map +1 -0
  112. package/src/lib/app-utils/collection-delete.d.ts +113 -0
  113. package/src/lib/app-utils/collection-delete.js +120 -0
  114. package/src/lib/app-utils/collection-delete.js.map +1 -0
  115. package/src/lib/app-utils/collection-entries.d.ts +833 -0
  116. package/src/lib/app-utils/collection-entries.js +1285 -0
  117. package/src/lib/app-utils/collection-entries.js.map +1 -0
  118. package/src/lib/app-utils/collection-entry-date.d.ts +129 -0
  119. package/src/lib/app-utils/collection-entry-date.js +183 -0
  120. package/src/lib/app-utils/collection-entry-date.js.map +1 -0
  121. package/src/lib/app-utils/collection-kind.d.ts +55 -0
  122. package/src/lib/app-utils/collection-kind.js +69 -0
  123. package/src/lib/app-utils/collection-kind.js.map +1 -0
  124. package/src/lib/app-utils/collection-slug.d.ts +79 -0
  125. package/src/lib/app-utils/collection-slug.js +70 -0
  126. package/src/lib/app-utils/collection-slug.js.map +1 -0
  127. package/src/lib/app-utils/compose-layout-nodes.d.ts +147 -0
  128. package/src/lib/app-utils/compose-layout-nodes.js +261 -0
  129. package/src/lib/app-utils/compose-layout-nodes.js.map +1 -0
  130. package/src/lib/app-utils/compose-layout-props.d.ts +71 -0
  131. package/src/lib/app-utils/compose-layout-props.js +61 -0
  132. package/src/lib/app-utils/compose-layout-props.js.map +1 -0
  133. package/src/lib/app-utils/compose-reusable-components.d.ts +504 -0
  134. package/src/lib/app-utils/compose-reusable-components.js +1366 -0
  135. package/src/lib/app-utils/compose-reusable-components.js.map +1 -0
  136. package/src/lib/app-utils/compress.d.ts +35 -0
  137. package/src/lib/app-utils/compress.js +38 -0
  138. package/src/lib/app-utils/compress.js.map +1 -0
  139. package/src/lib/app-utils/consent-banner-ui.d.ts +226 -0
  140. package/src/lib/app-utils/consent-banner-ui.js +583 -0
  141. package/src/lib/app-utils/consent-banner-ui.js.map +1 -0
  142. package/src/lib/app-utils/consent-groups.d.ts +189 -0
  143. package/src/lib/app-utils/consent-groups.js +219 -0
  144. package/src/lib/app-utils/consent-groups.js.map +1 -0
  145. package/src/lib/app-utils/consent-preferences-dialog.d.ts +58 -0
  146. package/src/lib/app-utils/consent-preferences-dialog.js +106 -0
  147. package/src/lib/app-utils/consent-preferences-dialog.js.map +1 -0
  148. package/src/lib/app-utils/console-record-links.d.ts +163 -0
  149. package/src/lib/app-utils/console-record-links.js +178 -0
  150. package/src/lib/app-utils/console-record-links.js.map +1 -0
  151. package/src/lib/app-utils/console-routes.d.ts +658 -0
  152. package/src/lib/app-utils/console-routes.js +489 -0
  153. package/src/lib/app-utils/console-routes.js.map +1 -0
  154. package/src/lib/app-utils/console-widget-slot-context.d.ts +46 -0
  155. package/src/lib/app-utils/console-widget-slot-context.js +23 -0
  156. package/src/lib/app-utils/console-widget-slot-context.js.map +1 -0
  157. package/src/lib/app-utils/contact-custom-fields.d.ts +191 -0
  158. package/src/lib/app-utils/contact-custom-fields.js +349 -0
  159. package/src/lib/app-utils/contact-custom-fields.js.map +1 -0
  160. package/src/lib/app-utils/contact-holder.d.ts +52 -0
  161. package/src/lib/app-utils/contact-holder.js +98 -0
  162. package/src/lib/app-utils/contact-holder.js.map +1 -0
  163. package/src/lib/app-utils/contact-merge.d.ts +136 -0
  164. package/src/lib/app-utils/contact-merge.js +379 -0
  165. package/src/lib/app-utils/contact-merge.js.map +1 -0
  166. package/src/lib/app-utils/contacts.d.ts +554 -0
  167. package/src/lib/app-utils/contacts.js +401 -0
  168. package/src/lib/app-utils/contacts.js.map +1 -0
  169. package/src/lib/app-utils/content-author-profile.d.ts +68 -0
  170. package/src/lib/app-utils/content-author-profile.js +118 -0
  171. package/src/lib/app-utils/content-author-profile.js.map +1 -0
  172. package/src/lib/app-utils/content-authors.d.ts +662 -0
  173. package/src/lib/app-utils/content-authors.js +771 -0
  174. package/src/lib/app-utils/content-authors.js.map +1 -0
  175. package/src/lib/app-utils/content-schema-type.d.ts +81 -0
  176. package/src/lib/app-utils/content-schema-type.js +87 -0
  177. package/src/lib/app-utils/content-schema-type.js.map +1 -0
  178. package/src/lib/app-utils/contexts.d.ts +35 -0
  179. package/src/lib/app-utils/contexts.js +35 -0
  180. package/src/lib/app-utils/contexts.js.map +1 -0
  181. package/src/lib/app-utils/create-resource-uid.d.ts +18 -0
  182. package/src/lib/app-utils/create-resource-uid.js +23 -0
  183. package/src/lib/app-utils/create-resource-uid.js.map +1 -0
  184. package/src/lib/app-utils/crm-booking.d.ts +75 -0
  185. package/src/lib/app-utils/crm-booking.js +133 -0
  186. package/src/lib/app-utils/crm-booking.js.map +1 -0
  187. package/src/lib/app-utils/crm-csv.d.ts +232 -0
  188. package/src/lib/app-utils/crm-csv.js +432 -0
  189. package/src/lib/app-utils/crm-csv.js.map +1 -0
  190. package/src/lib/app-utils/crm-digest.d.ts +179 -0
  191. package/src/lib/app-utils/crm-digest.js +271 -0
  192. package/src/lib/app-utils/crm-digest.js.map +1 -0
  193. package/src/lib/app-utils/crm-email-templates.d.ts +172 -0
  194. package/src/lib/app-utils/crm-email-templates.js +382 -0
  195. package/src/lib/app-utils/crm-email-templates.js.map +1 -0
  196. package/src/lib/app-utils/crm-inbound.d.ts +245 -0
  197. package/src/lib/app-utils/crm-inbound.js +407 -0
  198. package/src/lib/app-utils/crm-inbound.js.map +1 -0
  199. package/src/lib/app-utils/crm-kinds.d.ts +73 -0
  200. package/src/lib/app-utils/crm-kinds.js +92 -0
  201. package/src/lib/app-utils/crm-kinds.js.map +1 -0
  202. package/src/lib/app-utils/crm-next-activity.d.ts +107 -0
  203. package/src/lib/app-utils/crm-next-activity.js +140 -0
  204. package/src/lib/app-utils/crm-next-activity.js.map +1 -0
  205. package/src/lib/app-utils/crm-reports.d.ts +479 -0
  206. package/src/lib/app-utils/crm-reports.js +680 -0
  207. package/src/lib/app-utils/crm-reports.js.map +1 -0
  208. package/src/lib/app-utils/crm-task-reminders.d.ts +129 -0
  209. package/src/lib/app-utils/crm-task-reminders.js +139 -0
  210. package/src/lib/app-utils/crm-task-reminders.js.map +1 -0
  211. package/src/lib/app-utils/crm.d.ts +1546 -0
  212. package/src/lib/app-utils/crm.js +1814 -0
  213. package/src/lib/app-utils/crm.js.map +1 -0
  214. package/src/lib/app-utils/csp-report.d.ts +84 -0
  215. package/src/lib/app-utils/csp-report.js +177 -0
  216. package/src/lib/app-utils/csp-report.js.map +1 -0
  217. package/src/lib/app-utils/csv-import.d.ts +192 -0
  218. package/src/lib/app-utils/csv-import.js +279 -0
  219. package/src/lib/app-utils/csv-import.js.map +1 -0
  220. package/src/lib/app-utils/dataset-csv.d.ts +109 -0
  221. package/src/lib/app-utils/dataset-csv.js +235 -0
  222. package/src/lib/app-utils/dataset-csv.js.map +1 -0
  223. package/src/lib/app-utils/dataset-models.d.ts +194 -0
  224. package/src/lib/app-utils/dataset-models.js +409 -0
  225. package/src/lib/app-utils/dataset-models.js.map +1 -0
  226. package/src/lib/app-utils/dataset-query.d.ts +57 -0
  227. package/src/lib/app-utils/dataset-query.js +126 -0
  228. package/src/lib/app-utils/dataset-query.js.map +1 -0
  229. package/src/lib/app-utils/datasets.d.ts +136 -0
  230. package/src/lib/app-utils/datasets.js +177 -0
  231. package/src/lib/app-utils/datasets.js.map +1 -0
  232. package/src/lib/app-utils/decompress.d.ts +36 -0
  233. package/src/lib/app-utils/decompress.js +32 -0
  234. package/src/lib/app-utils/decompress.js.map +1 -0
  235. package/src/lib/app-utils/dedicated-sending-domain.d.ts +47 -0
  236. package/src/lib/app-utils/dedicated-sending-domain.js +114 -0
  237. package/src/lib/app-utils/dedicated-sending-domain.js.map +1 -0
  238. package/src/lib/app-utils/deferred-image.d.ts +86 -0
  239. package/src/lib/app-utils/deferred-image.js +83 -0
  240. package/src/lib/app-utils/deferred-image.js.map +1 -0
  241. package/src/lib/app-utils/definition-canvas-tree.d.ts +61 -0
  242. package/src/lib/app-utils/definition-canvas-tree.js +126 -0
  243. package/src/lib/app-utils/definition-canvas-tree.js.map +1 -0
  244. package/src/lib/app-utils/deployment-shape.d.ts +167 -0
  245. package/src/lib/app-utils/deployment-shape.js +238 -0
  246. package/src/lib/app-utils/deployment-shape.js.map +1 -0
  247. package/src/lib/app-utils/detect-template-placeholders.d.ts +30 -0
  248. package/src/lib/app-utils/detect-template-placeholders.js +53 -0
  249. package/src/lib/app-utils/detect-template-placeholders.js.map +1 -0
  250. package/src/lib/app-utils/dmca-counter-notice.d.ts +185 -0
  251. package/src/lib/app-utils/dmca-counter-notice.js +277 -0
  252. package/src/lib/app-utils/dmca-counter-notice.js.map +1 -0
  253. package/src/lib/app-utils/docs-deflection.d.ts +156 -0
  254. package/src/lib/app-utils/docs-deflection.js +547 -0
  255. package/src/lib/app-utils/docs-deflection.js.map +1 -0
  256. package/src/lib/app-utils/docs-help.d.ts +57 -0
  257. package/src/lib/app-utils/docs-help.generated.d.ts +303 -0
  258. package/src/lib/app-utils/docs-help.generated.js +867 -0
  259. package/src/lib/app-utils/docs-help.generated.js.map +1 -0
  260. package/src/lib/app-utils/docs-help.js +46 -0
  261. package/src/lib/app-utils/docs-help.js.map +1 -0
  262. package/src/lib/app-utils/docs-index.generated.d.ts +29 -0
  263. package/src/lib/app-utils/docs-index.generated.js +8388 -0
  264. package/src/lib/app-utils/docs-index.generated.js.map +1 -0
  265. package/src/lib/app-utils/docs-retrieval.d.ts +67 -0
  266. package/src/lib/app-utils/docs-retrieval.js +188 -0
  267. package/src/lib/app-utils/docs-retrieval.js.map +1 -0
  268. package/src/lib/app-utils/document-landmark.d.ts +50 -0
  269. package/src/lib/app-utils/document-landmark.js +100 -0
  270. package/src/lib/app-utils/document-landmark.js.map +1 -0
  271. package/src/lib/app-utils/duplicate-resource.d.ts +101 -0
  272. package/src/lib/app-utils/duplicate-resource.js +206 -0
  273. package/src/lib/app-utils/duplicate-resource.js.map +1 -0
  274. package/src/lib/app-utils/dynamic-list-rule.d.ts +471 -0
  275. package/src/lib/app-utils/dynamic-list-rule.js +914 -0
  276. package/src/lib/app-utils/dynamic-list-rule.js.map +1 -0
  277. package/src/lib/app-utils/element-animation.d.ts +200 -0
  278. package/src/lib/app-utils/element-animation.js +233 -0
  279. package/src/lib/app-utils/element-animation.js.map +1 -0
  280. package/src/lib/app-utils/element-hidden-style.d.ts +46 -0
  281. package/src/lib/app-utils/element-hidden-style.js +48 -0
  282. package/src/lib/app-utils/element-hidden-style.js.map +1 -0
  283. package/src/lib/app-utils/element-ui.d.ts +243 -0
  284. package/src/lib/app-utils/element-ui.js +459 -0
  285. package/src/lib/app-utils/element-ui.js.map +1 -0
  286. package/src/lib/app-utils/email-starter-policy.d.ts +160 -0
  287. package/src/lib/app-utils/email-starter-policy.js +327 -0
  288. package/src/lib/app-utils/email-starter-policy.js.map +1 -0
  289. package/src/lib/app-utils/email-topics.d.ts +310 -0
  290. package/src/lib/app-utils/email-topics.js +336 -0
  291. package/src/lib/app-utils/email-topics.js.map +1 -0
  292. package/src/lib/app-utils/enabled-plugins-context.d.ts +100 -0
  293. package/src/lib/app-utils/enabled-plugins-context.js +113 -0
  294. package/src/lib/app-utils/enabled-plugins-context.js.map +1 -0
  295. package/src/lib/app-utils/ensure-canvas-root.d.ts +38 -0
  296. package/src/lib/app-utils/ensure-canvas-root.js +61 -0
  297. package/src/lib/app-utils/ensure-canvas-root.js.map +1 -0
  298. package/src/lib/app-utils/entity-picker-context.d.ts +199 -0
  299. package/src/lib/app-utils/entity-picker-context.js +93 -0
  300. package/src/lib/app-utils/entity-picker-context.js.map +1 -0
  301. package/src/lib/app-utils/error-beacon.d.ts +107 -0
  302. package/src/lib/app-utils/error-beacon.js +296 -0
  303. package/src/lib/app-utils/error-beacon.js.map +1 -0
  304. package/src/lib/app-utils/expand-repeatables.d.ts +172 -0
  305. package/src/lib/app-utils/expand-repeatables.js +299 -0
  306. package/src/lib/app-utils/expand-repeatables.js.map +1 -0
  307. package/src/lib/app-utils/foreign-script.d.ts +82 -0
  308. package/src/lib/app-utils/foreign-script.js +102 -0
  309. package/src/lib/app-utils/foreign-script.js.map +1 -0
  310. package/src/lib/app-utils/form-abuse-ceiling.d.ts +146 -0
  311. package/src/lib/app-utils/form-abuse-ceiling.js +158 -0
  312. package/src/lib/app-utils/form-abuse-ceiling.js.map +1 -0
  313. package/src/lib/app-utils/form-contract.d.ts +198 -0
  314. package/src/lib/app-utils/form-contract.js +282 -0
  315. package/src/lib/app-utils/form-contract.js.map +1 -0
  316. package/src/lib/app-utils/form-dataset-binding.d.ts +50 -0
  317. package/src/lib/app-utils/form-dataset-binding.js +74 -0
  318. package/src/lib/app-utils/form-dataset-binding.js.map +1 -0
  319. package/src/lib/app-utils/forms.d.ts +727 -0
  320. package/src/lib/app-utils/forms.js +777 -0
  321. package/src/lib/app-utils/forms.js.map +1 -0
  322. package/src/lib/app-utils/functions.d.ts +188 -0
  323. package/src/lib/app-utils/functions.js +529 -0
  324. package/src/lib/app-utils/functions.js.map +1 -0
  325. package/src/lib/app-utils/guarded-version-save.d.ts +91 -0
  326. package/src/lib/app-utils/guarded-version-save.js +131 -0
  327. package/src/lib/app-utils/guarded-version-save.js.map +1 -0
  328. package/src/lib/app-utils/health-report.d.ts +1766 -0
  329. package/src/lib/app-utils/health-report.js +2072 -0
  330. package/src/lib/app-utils/health-report.js.map +1 -0
  331. package/src/lib/app-utils/host-naming.d.ts +135 -0
  332. package/src/lib/app-utils/host-naming.js +188 -0
  333. package/src/lib/app-utils/host-naming.js.map +1 -0
  334. package/src/lib/app-utils/host-permissions.d.ts +76 -0
  335. package/src/lib/app-utils/host-permissions.js +119 -0
  336. package/src/lib/app-utils/host-permissions.js.map +1 -0
  337. package/src/lib/app-utils/host-tokens.d.ts +228 -0
  338. package/src/lib/app-utils/host-tokens.js +304 -0
  339. package/src/lib/app-utils/host-tokens.js.map +1 -0
  340. package/src/lib/app-utils/idp-profile.d.ts +83 -0
  341. package/src/lib/app-utils/idp-profile.js +268 -0
  342. package/src/lib/app-utils/idp-profile.js.map +1 -0
  343. package/src/lib/app-utils/image-upload-types.d.ts +49 -0
  344. package/src/lib/app-utils/image-upload-types.js +86 -0
  345. package/src/lib/app-utils/image-upload-types.js.map +1 -0
  346. package/src/lib/app-utils/index.d.ts +18 -0
  347. package/src/lib/app-utils/index.js +23 -0
  348. package/src/lib/app-utils/index.js.map +1 -0
  349. package/src/lib/app-utils/internal-traffic.d.ts +197 -0
  350. package/src/lib/app-utils/internal-traffic.js +288 -0
  351. package/src/lib/app-utils/internal-traffic.js.map +1 -0
  352. package/src/lib/app-utils/lineal-order.d.ts +62 -0
  353. package/src/lib/app-utils/lineal-order.js +65 -0
  354. package/src/lib/app-utils/lineal-order.js.map +1 -0
  355. package/src/lib/app-utils/link-references.d.ts +36 -0
  356. package/src/lib/app-utils/link-references.js +89 -0
  357. package/src/lib/app-utils/link-references.js.map +1 -0
  358. package/src/lib/app-utils/link-target-search-context.d.ts +123 -0
  359. package/src/lib/app-utils/link-target-search-context.js +221 -0
  360. package/src/lib/app-utils/link-target-search-context.js.map +1 -0
  361. package/src/lib/app-utils/list-assignment-policy.d.ts +169 -0
  362. package/src/lib/app-utils/list-assignment-policy.js +170 -0
  363. package/src/lib/app-utils/list-assignment-policy.js.map +1 -0
  364. package/src/lib/app-utils/list-import.d.ts +158 -0
  365. package/src/lib/app-utils/list-import.js +332 -0
  366. package/src/lib/app-utils/list-import.js.map +1 -0
  367. package/src/lib/app-utils/llms-txt.d.ts +104 -0
  368. package/src/lib/app-utils/llms-txt.js +144 -0
  369. package/src/lib/app-utils/llms-txt.js.map +1 -0
  370. package/src/lib/app-utils/local-storage-budget.d.ts +75 -0
  371. package/src/lib/app-utils/local-storage-budget.js +153 -0
  372. package/src/lib/app-utils/local-storage-budget.js.map +1 -0
  373. package/src/lib/app-utils/lockdown.d.ts +698 -0
  374. package/src/lib/app-utils/lockdown.js +929 -0
  375. package/src/lib/app-utils/lockdown.js.map +1 -0
  376. package/src/lib/app-utils/margin-utilization.d.ts +267 -0
  377. package/src/lib/app-utils/margin-utilization.js +351 -0
  378. package/src/lib/app-utils/margin-utilization.js.map +1 -0
  379. package/src/lib/app-utils/markdown-lite.d.ts +297 -0
  380. package/src/lib/app-utils/markdown-lite.js +705 -0
  381. package/src/lib/app-utils/markdown-lite.js.map +1 -0
  382. package/src/lib/app-utils/marketing-consent.d.ts +704 -0
  383. package/src/lib/app-utils/marketing-consent.js +783 -0
  384. package/src/lib/app-utils/marketing-consent.js.map +1 -0
  385. package/src/lib/app-utils/marketplace-listing-visibility.d.ts +120 -0
  386. package/src/lib/app-utils/marketplace-listing-visibility.js +139 -0
  387. package/src/lib/app-utils/marketplace-listing-visibility.js.map +1 -0
  388. package/src/lib/app-utils/marketplace-overrides.d.ts +271 -0
  389. package/src/lib/app-utils/marketplace-overrides.js +462 -0
  390. package/src/lib/app-utils/marketplace-overrides.js.map +1 -0
  391. package/src/lib/app-utils/marketplace-provenance.d.ts +150 -0
  392. package/src/lib/app-utils/marketplace-provenance.js +136 -0
  393. package/src/lib/app-utils/marketplace-provenance.js.map +1 -0
  394. package/src/lib/app-utils/marketplace-theme.d.ts +237 -0
  395. package/src/lib/app-utils/marketplace-theme.js +525 -0
  396. package/src/lib/app-utils/marketplace-theme.js.map +1 -0
  397. package/src/lib/app-utils/marketplace-update-state.d.ts +119 -0
  398. package/src/lib/app-utils/marketplace-update-state.js +132 -0
  399. package/src/lib/app-utils/marketplace-update-state.js.map +1 -0
  400. package/src/lib/app-utils/marketplace-verification.d.ts +95 -0
  401. package/src/lib/app-utils/marketplace-verification.js +81 -0
  402. package/src/lib/app-utils/marketplace-verification.js.map +1 -0
  403. package/src/lib/app-utils/measure-node-map.d.ts +76 -0
  404. package/src/lib/app-utils/measure-node-map.js +111 -0
  405. package/src/lib/app-utils/measure-node-map.js.map +1 -0
  406. package/src/lib/app-utils/media-alt.d.ts +55 -0
  407. package/src/lib/app-utils/media-alt.js +58 -0
  408. package/src/lib/app-utils/media-alt.js.map +1 -0
  409. package/src/lib/app-utils/media-asset-facts.d.ts +125 -0
  410. package/src/lib/app-utils/media-asset-facts.js +467 -0
  411. package/src/lib/app-utils/media-asset-facts.js.map +1 -0
  412. package/src/lib/app-utils/media-cdn-scope.d.ts +65 -0
  413. package/src/lib/app-utils/media-cdn-scope.js +75 -0
  414. package/src/lib/app-utils/media-cdn-scope.js.map +1 -0
  415. package/src/lib/app-utils/media-folders.d.ts +161 -0
  416. package/src/lib/app-utils/media-folders.js +230 -0
  417. package/src/lib/app-utils/media-folders.js.map +1 -0
  418. package/src/lib/app-utils/media-metadata.d.ts +277 -0
  419. package/src/lib/app-utils/media-metadata.js +388 -0
  420. package/src/lib/app-utils/media-metadata.js.map +1 -0
  421. package/src/lib/app-utils/media-picker-context.d.ts +93 -0
  422. package/src/lib/app-utils/media-picker-context.js +26 -0
  423. package/src/lib/app-utils/media-picker-context.js.map +1 -0
  424. package/src/lib/app-utils/media-quarantine.d.ts +247 -0
  425. package/src/lib/app-utils/media-quarantine.js +323 -0
  426. package/src/lib/app-utils/media-quarantine.js.map +1 -0
  427. package/src/lib/app-utils/media-ref.d.ts +661 -0
  428. package/src/lib/app-utils/media-ref.js +1055 -0
  429. package/src/lib/app-utils/media-ref.js.map +1 -0
  430. package/src/lib/app-utils/media-takedown-reach.d.ts +126 -0
  431. package/src/lib/app-utils/media-takedown-reach.js +143 -0
  432. package/src/lib/app-utils/media-takedown-reach.js.map +1 -0
  433. package/src/lib/app-utils/member-email-aliases.d.ts +180 -0
  434. package/src/lib/app-utils/member-email-aliases.js +217 -0
  435. package/src/lib/app-utils/member-email-aliases.js.map +1 -0
  436. package/src/lib/app-utils/merge-node-sx.d.ts +47 -0
  437. package/src/lib/app-utils/merge-node-sx.js +123 -0
  438. package/src/lib/app-utils/merge-node-sx.js.map +1 -0
  439. package/src/lib/app-utils/name-match.d.ts +155 -0
  440. package/src/lib/app-utils/name-match.js +175 -0
  441. package/src/lib/app-utils/name-match.js.map +1 -0
  442. package/src/lib/app-utils/name-search.d.ts +117 -0
  443. package/src/lib/app-utils/name-search.js +145 -0
  444. package/src/lib/app-utils/name-search.js.map +1 -0
  445. package/src/lib/app-utils/node-capabilities.d.ts +85 -0
  446. package/src/lib/app-utils/node-capabilities.js +88 -0
  447. package/src/lib/app-utils/node-capabilities.js.map +1 -0
  448. package/src/lib/app-utils/node-definition-sanitizer.d.ts +164 -0
  449. package/src/lib/app-utils/node-definition-sanitizer.js +402 -0
  450. package/src/lib/app-utils/node-definition-sanitizer.js.map +1 -0
  451. package/src/lib/app-utils/node-identity.d.ts +47 -0
  452. package/src/lib/app-utils/node-identity.js +50 -0
  453. package/src/lib/app-utils/node-identity.js.map +1 -0
  454. package/src/lib/app-utils/node-interactions.d.ts +102 -0
  455. package/src/lib/app-utils/node-interactions.js +192 -0
  456. package/src/lib/app-utils/node-interactions.js.map +1 -0
  457. package/src/lib/app-utils/node-url-policy.d.ts +54 -0
  458. package/src/lib/app-utils/node-url-policy.js +72 -0
  459. package/src/lib/app-utils/node-url-policy.js.map +1 -0
  460. package/src/lib/app-utils/notifications.d.ts +71 -0
  461. package/src/lib/app-utils/notifications.js +101 -0
  462. package/src/lib/app-utils/notifications.js.map +1 -0
  463. package/src/lib/app-utils/onboarding-deep-link.d.ts +120 -0
  464. package/src/lib/app-utils/onboarding-deep-link.js +142 -0
  465. package/src/lib/app-utils/onboarding-deep-link.js.map +1 -0
  466. package/src/lib/app-utils/operator-identity.d.ts +207 -0
  467. package/src/lib/app-utils/operator-identity.js +186 -0
  468. package/src/lib/app-utils/operator-identity.js.map +1 -0
  469. package/src/lib/app-utils/org-billing-doc.d.ts +231 -0
  470. package/src/lib/app-utils/org-billing-doc.js +238 -0
  471. package/src/lib/app-utils/org-billing-doc.js.map +1 -0
  472. package/src/lib/app-utils/org-override-reason.d.ts +100 -0
  473. package/src/lib/app-utils/org-override-reason.js +116 -0
  474. package/src/lib/app-utils/org-override-reason.js.map +1 -0
  475. package/src/lib/app-utils/org-permissions.d.ts +194 -0
  476. package/src/lib/app-utils/org-permissions.js +357 -0
  477. package/src/lib/app-utils/org-permissions.js.map +1 -0
  478. package/src/lib/app-utils/org-roles.d.ts +72 -0
  479. package/src/lib/app-utils/org-roles.js +131 -0
  480. package/src/lib/app-utils/org-roles.js.map +1 -0
  481. package/src/lib/app-utils/organizations.d.ts +407 -0
  482. package/src/lib/app-utils/organizations.js +511 -0
  483. package/src/lib/app-utils/organizations.js.map +1 -0
  484. package/src/lib/app-utils/page-header-actions-context.d.ts +83 -0
  485. package/src/lib/app-utils/page-header-actions-context.js +58 -0
  486. package/src/lib/app-utils/page-header-actions-context.js.map +1 -0
  487. package/src/lib/app-utils/page-header-record-context.d.ts +80 -0
  488. package/src/lib/app-utils/page-header-record-context.js +64 -0
  489. package/src/lib/app-utils/page-header-record-context.js.map +1 -0
  490. package/src/lib/app-utils/page-markdown.d.ts +103 -0
  491. package/src/lib/app-utils/page-markdown.js +385 -0
  492. package/src/lib/app-utils/page-markdown.js.map +1 -0
  493. package/src/lib/app-utils/paid-media-source.d.ts +90 -0
  494. package/src/lib/app-utils/paid-media-source.js +184 -0
  495. package/src/lib/app-utils/paid-media-source.js.map +1 -0
  496. package/src/lib/app-utils/palette-sx.d.ts +34 -0
  497. package/src/lib/app-utils/palette-sx.js +150 -0
  498. package/src/lib/app-utils/palette-sx.js.map +1 -0
  499. package/src/lib/app-utils/password-policy.d.ts +47 -0
  500. package/src/lib/app-utils/password-policy.js +46 -0
  501. package/src/lib/app-utils/password-policy.js.map +1 -0
  502. package/src/lib/app-utils/payments-configured.d.ts +84 -0
  503. package/src/lib/app-utils/payments-configured.js +88 -0
  504. package/src/lib/app-utils/payments-configured.js.map +1 -0
  505. package/src/lib/app-utils/person-erasure.d.ts +126 -0
  506. package/src/lib/app-utils/person-erasure.js +121 -0
  507. package/src/lib/app-utils/person-erasure.js.map +1 -0
  508. package/src/lib/app-utils/person-key.d.ts +32 -0
  509. package/src/lib/app-utils/person-key.js +62 -0
  510. package/src/lib/app-utils/person-key.js.map +1 -0
  511. package/src/lib/app-utils/plan-entitlements.d.ts +2756 -0
  512. package/src/lib/app-utils/plan-entitlements.js +4085 -0
  513. package/src/lib/app-utils/plan-entitlements.js.map +1 -0
  514. package/src/lib/app-utils/platform-ad-conversions.d.ts +122 -0
  515. package/src/lib/app-utils/platform-ad-conversions.js +174 -0
  516. package/src/lib/app-utils/platform-ad-conversions.js.map +1 -0
  517. package/src/lib/app-utils/platform-advertising-tags.d.ts +139 -0
  518. package/src/lib/app-utils/platform-advertising-tags.js +203 -0
  519. package/src/lib/app-utils/platform-advertising-tags.js.map +1 -0
  520. package/src/lib/app-utils/platform-brand.d.ts +151 -0
  521. package/src/lib/app-utils/platform-brand.js +175 -0
  522. package/src/lib/app-utils/platform-brand.js.map +1 -0
  523. package/src/lib/app-utils/platform-consent-default.d.ts +142 -0
  524. package/src/lib/app-utils/platform-consent-default.js +295 -0
  525. package/src/lib/app-utils/platform-consent-default.js.map +1 -0
  526. package/src/lib/app-utils/platform-marketing-consent.d.ts +151 -0
  527. package/src/lib/app-utils/platform-marketing-consent.js +182 -0
  528. package/src/lib/app-utils/platform-marketing-consent.js.map +1 -0
  529. package/src/lib/app-utils/platform-marketing-host.d.ts +33 -0
  530. package/src/lib/app-utils/platform-marketing-host.js +97 -0
  531. package/src/lib/app-utils/platform-marketing-host.js.map +1 -0
  532. package/src/lib/app-utils/platform-visitor-consent.d.ts +252 -0
  533. package/src/lib/app-utils/platform-visitor-consent.js +533 -0
  534. package/src/lib/app-utils/platform-visitor-consent.js.map +1 -0
  535. package/src/lib/app-utils/plugin-api-cross-origin.d.ts +61 -0
  536. package/src/lib/app-utils/plugin-api-cross-origin.js +202 -0
  537. package/src/lib/app-utils/plugin-api-cross-origin.js.map +1 -0
  538. package/src/lib/app-utils/plugin-api-rate-limit.d.ts +192 -0
  539. package/src/lib/app-utils/plugin-api-rate-limit.js +270 -0
  540. package/src/lib/app-utils/plugin-api-rate-limit.js.map +1 -0
  541. package/src/lib/app-utils/plugin-artifact-path.d.ts +32 -0
  542. package/src/lib/app-utils/plugin-artifact-path.js +33 -0
  543. package/src/lib/app-utils/plugin-artifact-path.js.map +1 -0
  544. package/src/lib/app-utils/plugin-bundle-checks.d.ts +148 -0
  545. package/src/lib/app-utils/plugin-bundle-checks.js +989 -0
  546. package/src/lib/app-utils/plugin-bundle-checks.js.map +1 -0
  547. package/src/lib/app-utils/plugin-manifest.d.ts +516 -0
  548. package/src/lib/app-utils/plugin-manifest.js +687 -0
  549. package/src/lib/app-utils/plugin-manifest.js.map +1 -0
  550. package/src/lib/app-utils/published-legal-pages.d.ts +71 -0
  551. package/src/lib/app-utils/published-legal-pages.js +90 -0
  552. package/src/lib/app-utils/published-legal-pages.js.map +1 -0
  553. package/src/lib/app-utils/publisher-agreement.d.ts +254 -0
  554. package/src/lib/app-utils/publisher-agreement.js +274 -0
  555. package/src/lib/app-utils/publisher-agreement.js.map +1 -0
  556. package/src/lib/app-utils/publisher-attestation.d.ts +99 -0
  557. package/src/lib/app-utils/publisher-attestation.js +158 -0
  558. package/src/lib/app-utils/publisher-attestation.js.map +1 -0
  559. package/src/lib/app-utils/recipient-email-render.d.ts +216 -0
  560. package/src/lib/app-utils/recipient-email-render.js +221 -0
  561. package/src/lib/app-utils/recipient-email-render.js.map +1 -0
  562. package/src/lib/app-utils/redispatch-caught-error.d.ts +38 -0
  563. package/src/lib/app-utils/redispatch-caught-error.js +46 -0
  564. package/src/lib/app-utils/redispatch-caught-error.js.map +1 -0
  565. package/src/lib/app-utils/registering-plugin.d.ts +19 -0
  566. package/src/lib/app-utils/registering-plugin.js +38 -0
  567. package/src/lib/app-utils/registering-plugin.js.map +1 -0
  568. package/src/lib/app-utils/release-flags.d.ts +283 -0
  569. package/src/lib/app-utils/release-flags.js +518 -0
  570. package/src/lib/app-utils/release-flags.js.map +1 -0
  571. package/src/lib/app-utils/repair-canvas-nodes.d.ts +130 -0
  572. package/src/lib/app-utils/repair-canvas-nodes.js +298 -0
  573. package/src/lib/app-utils/repair-canvas-nodes.js.map +1 -0
  574. package/src/lib/app-utils/repeat-infringer.d.ts +166 -0
  575. package/src/lib/app-utils/repeat-infringer.js +188 -0
  576. package/src/lib/app-utils/repeat-infringer.js.map +1 -0
  577. package/src/lib/app-utils/repeat-sources.d.ts +115 -0
  578. package/src/lib/app-utils/repeat-sources.js +86 -0
  579. package/src/lib/app-utils/repeat-sources.js.map +1 -0
  580. package/src/lib/app-utils/request-geo.d.ts +114 -0
  581. package/src/lib/app-utils/request-geo.js +247 -0
  582. package/src/lib/app-utils/request-geo.js.map +1 -0
  583. package/src/lib/app-utils/request-ip.d.ts +213 -0
  584. package/src/lib/app-utils/request-ip.js +344 -0
  585. package/src/lib/app-utils/request-ip.js.map +1 -0
  586. package/src/lib/app-utils/resolve-named-tokens.d.ts +26 -0
  587. package/src/lib/app-utils/resolve-named-tokens.js +59 -0
  588. package/src/lib/app-utils/resolve-named-tokens.js.map +1 -0
  589. package/src/lib/app-utils/responsive-typography-sx.d.ts +77 -0
  590. package/src/lib/app-utils/responsive-typography-sx.js +122 -0
  591. package/src/lib/app-utils/responsive-typography-sx.js.map +1 -0
  592. package/src/lib/app-utils/reusable-component-keys.d.ts +135 -0
  593. package/src/lib/app-utils/reusable-component-keys.js +140 -0
  594. package/src/lib/app-utils/reusable-component-keys.js.map +1 -0
  595. package/src/lib/app-utils/reusable-prop-values.d.ts +199 -0
  596. package/src/lib/app-utils/reusable-prop-values.js +494 -0
  597. package/src/lib/app-utils/reusable-prop-values.js.map +1 -0
  598. package/src/lib/app-utils/safe-json-ld.d.ts +29 -0
  599. package/src/lib/app-utils/safe-json-ld.js +37 -0
  600. package/src/lib/app-utils/safe-json-ld.js.map +1 -0
  601. package/src/lib/app-utils/sanitize-svg.d.ts +55 -0
  602. package/src/lib/app-utils/sanitize-svg.js +460 -0
  603. package/src/lib/app-utils/sanitize-svg.js.map +1 -0
  604. package/src/lib/app-utils/scheme-sx.d.ts +84 -0
  605. package/src/lib/app-utils/scheme-sx.js +148 -0
  606. package/src/lib/app-utils/scheme-sx.js.map +1 -0
  607. package/src/lib/app-utils/scope-tokens.d.ts +249 -0
  608. package/src/lib/app-utils/scope-tokens.js +331 -0
  609. package/src/lib/app-utils/scope-tokens.js.map +1 -0
  610. package/src/lib/app-utils/screen-link-context-value.d.ts +57 -0
  611. package/src/lib/app-utils/screen-link-context-value.js +43 -0
  612. package/src/lib/app-utils/screen-link-context-value.js.map +1 -0
  613. package/src/lib/app-utils/screen-link-context.d.ts +185 -0
  614. package/src/lib/app-utils/screen-link-context.js +304 -0
  615. package/src/lib/app-utils/screen-link-context.js.map +1 -0
  616. package/src/lib/app-utils/screen-link-value.d.ts +269 -0
  617. package/src/lib/app-utils/screen-link-value.js +384 -0
  618. package/src/lib/app-utils/screen-link-value.js.map +1 -0
  619. package/src/lib/app-utils/screen-route.d.ts +669 -0
  620. package/src/lib/app-utils/screen-route.js +751 -0
  621. package/src/lib/app-utils/screen-route.js.map +1 -0
  622. package/src/lib/app-utils/screen-seo-fields.d.ts +37 -0
  623. package/src/lib/app-utils/screen-seo-fields.js +42 -0
  624. package/src/lib/app-utils/screen-seo-fields.js.map +1 -0
  625. package/src/lib/app-utils/search-indexing.d.ts +162 -0
  626. package/src/lib/app-utils/search-indexing.js +207 -0
  627. package/src/lib/app-utils/search-indexing.js.map +1 -0
  628. package/src/lib/app-utils/seo-listing-fields.d.ts +70 -0
  629. package/src/lib/app-utils/seo-listing-fields.js +75 -0
  630. package/src/lib/app-utils/seo-listing-fields.js.map +1 -0
  631. package/src/lib/app-utils/seo-locale.d.ts +119 -0
  632. package/src/lib/app-utils/seo-locale.js +133 -0
  633. package/src/lib/app-utils/seo-locale.js.map +1 -0
  634. package/src/lib/app-utils/seo-title-variables.d.ts +149 -0
  635. package/src/lib/app-utils/seo-title-variables.js +190 -0
  636. package/src/lib/app-utils/seo-title-variables.js.map +1 -0
  637. package/src/lib/app-utils/seo-title.d.ts +70 -0
  638. package/src/lib/app-utils/seo-title.js +117 -0
  639. package/src/lib/app-utils/seo-title.js.map +1 -0
  640. package/src/lib/app-utils/server.d.ts +174 -0
  641. package/src/lib/app-utils/server.js +314 -0
  642. package/src/lib/app-utils/server.js.map +1 -0
  643. package/src/lib/app-utils/site-context.d.ts +106 -0
  644. package/src/lib/app-utils/site-context.js +100 -0
  645. package/src/lib/app-utils/site-context.js.map +1 -0
  646. package/src/lib/app-utils/sitemap.d.ts +117 -0
  647. package/src/lib/app-utils/sitemap.js +183 -0
  648. package/src/lib/app-utils/sitemap.js.map +1 -0
  649. package/src/lib/app-utils/social-image.d.ts +112 -0
  650. package/src/lib/app-utils/social-image.js +191 -0
  651. package/src/lib/app-utils/social-image.js.map +1 -0
  652. package/src/lib/app-utils/starter-templates.d.ts +99 -0
  653. package/src/lib/app-utils/starter-templates.js +579 -0
  654. package/src/lib/app-utils/starter-templates.js.map +1 -0
  655. package/src/lib/app-utils/stored-nodes.d.ts +120 -0
  656. package/src/lib/app-utils/stored-nodes.js +174 -0
  657. package/src/lib/app-utils/stored-nodes.js.map +1 -0
  658. package/src/lib/app-utils/strip-undefined.d.ts +59 -0
  659. package/src/lib/app-utils/strip-undefined.js +95 -0
  660. package/src/lib/app-utils/strip-undefined.js.map +1 -0
  661. package/src/lib/app-utils/stripe-deployment-mode.d.ts +47 -0
  662. package/src/lib/app-utils/stripe-deployment-mode.js +70 -0
  663. package/src/lib/app-utils/stripe-deployment-mode.js.map +1 -0
  664. package/src/lib/app-utils/support-tiers.d.ts +108 -0
  665. package/src/lib/app-utils/support-tiers.js +201 -0
  666. package/src/lib/app-utils/support-tiers.js.map +1 -0
  667. package/src/lib/app-utils/timezone-geo-hint.d.ts +36 -0
  668. package/src/lib/app-utils/timezone-geo-hint.js +180 -0
  669. package/src/lib/app-utils/timezone-geo-hint.js.map +1 -0
  670. package/src/lib/app-utils/upload-cors.d.ts +155 -0
  671. package/src/lib/app-utils/upload-cors.js +254 -0
  672. package/src/lib/app-utils/upload-cors.js.map +1 -0
  673. package/src/lib/app-utils/upload-inspection.d.ts +159 -0
  674. package/src/lib/app-utils/upload-inspection.js +539 -0
  675. package/src/lib/app-utils/upload-inspection.js.map +1 -0
  676. package/src/lib/app-utils/url-slug.d.ts +40 -0
  677. package/src/lib/app-utils/url-slug.js +42 -0
  678. package/src/lib/app-utils/url-slug.js.map +1 -0
  679. package/src/lib/app-utils/usage-budget.d.ts +429 -0
  680. package/src/lib/app-utils/usage-budget.js +421 -0
  681. package/src/lib/app-utils/usage-budget.js.map +1 -0
  682. package/src/lib/app-utils/variables.d.ts +96 -0
  683. package/src/lib/app-utils/variables.js +233 -0
  684. package/src/lib/app-utils/variables.js.map +1 -0
  685. package/src/lib/app-utils/video-asset-facts.d.ts +61 -0
  686. package/src/lib/app-utils/video-asset-facts.js +70 -0
  687. package/src/lib/app-utils/video-asset-facts.js.map +1 -0
  688. package/src/lib/app-utils/video-object.d.ts +89 -0
  689. package/src/lib/app-utils/video-object.js +245 -0
  690. package/src/lib/app-utils/video-object.js.map +1 -0
  691. package/src/lib/app-utils/visitor-consent.d.ts +757 -0
  692. package/src/lib/app-utils/visitor-consent.js +981 -0
  693. package/src/lib/app-utils/visitor-consent.js.map +1 -0
  694. package/src/lib/app-utils/visitor-record-ceiling.d.ts +223 -0
  695. package/src/lib/app-utils/visitor-record-ceiling.js +215 -0
  696. package/src/lib/app-utils/visitor-record-ceiling.js.map +1 -0
  697. package/src/lib/app-utils/web-vitals-rum.d.ts +53 -0
  698. package/src/lib/app-utils/web-vitals-rum.js +278 -0
  699. package/src/lib/app-utils/web-vitals-rum.js.map +1 -0
  700. package/src/lib/app-utils/webhook-delivery.d.ts +324 -0
  701. package/src/lib/app-utils/webhook-delivery.js +413 -0
  702. package/src/lib/app-utils/webhook-delivery.js.map +1 -0
  703. package/src/lib/app-utils/where-used.d.ts +57 -0
  704. package/src/lib/app-utils/where-used.js +59 -0
  705. package/src/lib/app-utils/where-used.js.map +1 -0
  706. package/src/lib/app-utils/wistia-embed.d.ts +86 -0
  707. package/src/lib/app-utils/wistia-embed.js +119 -0
  708. package/src/lib/app-utils/wistia-embed.js.map +1 -0
  709. package/src/lib/app-utils/workflows.d.ts +136 -0
  710. package/src/lib/app-utils/workflows.js +346 -0
  711. package/src/lib/app-utils/workflows.js.map +1 -0
  712. package/src/lib/canvas-manager/canvas-manager-tree.d.ts +109 -0
  713. package/src/lib/canvas-manager/canvas-manager-tree.js +139 -0
  714. package/src/lib/canvas-manager/canvas-manager-tree.js.map +1 -0
  715. package/src/lib/canvas-manager/canvas-manager.d.ts +549 -0
  716. package/src/lib/canvas-manager/canvas-manager.js +1311 -0
  717. package/src/lib/canvas-manager/canvas-manager.js.map +1 -0
  718. package/src/lib/canvas-manager/index.d.ts +20 -0
  719. package/src/lib/canvas-manager/index.js +21 -0
  720. package/src/lib/canvas-manager/index.js.map +1 -0
  721. package/src/lib/canvas-manager/stores/index.d.ts +17 -0
  722. package/src/lib/canvas-manager/stores/index.js +18 -0
  723. package/src/lib/canvas-manager/stores/index.js.map +1 -0
  724. package/src/lib/canvas-manager/stores/node.store.d.ts +30 -0
  725. package/src/lib/canvas-manager/stores/node.store.js +45 -0
  726. package/src/lib/canvas-manager/stores/node.store.js.map +1 -0
  727. package/src/lib/components-manager/components-manager.d.ts +56 -0
  728. package/src/lib/components-manager/components-manager.js +191 -0
  729. package/src/lib/components-manager/components-manager.js.map +1 -0
  730. package/src/lib/components-manager/index.d.ts +18 -0
  731. package/src/lib/components-manager/index.js +19 -0
  732. package/src/lib/components-manager/index.js.map +1 -0
  733. package/src/lib/emit-manager/emit-manager.d.ts +56 -0
  734. package/src/lib/emit-manager/emit-manager.js +60 -0
  735. package/src/lib/emit-manager/emit-manager.js.map +1 -0
  736. package/src/lib/emit-manager/index.d.ts +18 -0
  737. package/src/lib/emit-manager/index.js +19 -0
  738. package/src/lib/emit-manager/index.js.map +1 -0
  739. package/src/lib/foundation/constants/_internal.d.ts +18 -0
  740. package/src/lib/foundation/constants/_internal.js +62 -0
  741. package/src/lib/foundation/constants/_internal.js.map +1 -0
  742. package/src/lib/foundation/constants/app.d.ts +23 -0
  743. package/src/lib/foundation/constants/app.js +25 -0
  744. package/src/lib/foundation/constants/app.js.map +1 -0
  745. package/src/lib/foundation/constants/canvas.d.ts +18 -0
  746. package/src/lib/foundation/constants/canvas.js +18 -0
  747. package/src/lib/foundation/constants/canvas.js.map +1 -0
  748. package/src/lib/foundation/constants/components.d.ts +54 -0
  749. package/src/lib/foundation/constants/components.js +61 -0
  750. package/src/lib/foundation/constants/components.js.map +1 -0
  751. package/src/lib/foundation/constants/shared.d.ts +50 -0
  752. package/src/lib/foundation/constants/shared.js +49 -0
  753. package/src/lib/foundation/constants/shared.js.map +1 -0
  754. package/src/lib/foundation/constants/symbol.d.ts +63 -0
  755. package/src/lib/foundation/constants/symbol.js +75 -0
  756. package/src/lib/foundation/constants/symbol.js.map +1 -0
  757. package/src/lib/foundation/definitions/components.types.d.ts +372 -0
  758. package/src/lib/foundation/definitions/components.types.js +180 -0
  759. package/src/lib/foundation/definitions/components.types.js.map +1 -0
  760. package/src/lib/foundation/definitions/contact.types.d.ts +73 -0
  761. package/src/lib/foundation/definitions/contact.types.js +107 -0
  762. package/src/lib/foundation/definitions/contact.types.js.map +1 -0
  763. package/src/lib/foundation/definitions/host-content-collections.d.ts +137 -0
  764. package/src/lib/foundation/definitions/host-content-collections.js +219 -0
  765. package/src/lib/foundation/definitions/host-content-collections.js.map +1 -0
  766. package/src/lib/foundation/definitions/org-billing.types.d.ts +1361 -0
  767. package/src/lib/foundation/definitions/org-billing.types.js +77 -0
  768. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -0
  769. package/src/lib/foundation/definitions/organization.types.d.ts +307 -0
  770. package/src/lib/foundation/definitions/organization.types.js +42 -0
  771. package/src/lib/foundation/definitions/organization.types.js.map +1 -0
  772. package/src/lib/foundation/definitions/platform.types.d.ts +1326 -0
  773. package/src/lib/foundation/definitions/platform.types.js +124 -0
  774. package/src/lib/foundation/definitions/platform.types.js.map +1 -0
  775. package/src/lib/foundation/definitions/property-kinds.d.ts +189 -0
  776. package/src/lib/foundation/definitions/property-kinds.js +565 -0
  777. package/src/lib/foundation/definitions/property-kinds.js.map +1 -0
  778. package/src/lib/foundation/definitions/shared.d.ts +23 -0
  779. package/src/lib/foundation/definitions/shared.js +18 -0
  780. package/src/lib/foundation/definitions/shared.js.map +1 -0
  781. package/src/lib/foundation/definitions/write-deny-coverage.util.d.ts +249 -0
  782. package/src/lib/foundation/definitions/write-deny-coverage.util.js +499 -0
  783. package/src/lib/foundation/definitions/write-deny-coverage.util.js.map +1 -0
  784. package/src/lib/foundation/index.d.ts +32 -0
  785. package/src/lib/foundation/index.js +67 -0
  786. package/src/lib/foundation/index.js.map +1 -0
  787. package/src/lib/lifecycle.d.ts +24 -0
  788. package/src/lib/lifecycle.js +39 -0
  789. package/src/lib/lifecycle.js.map +1 -0
  790. package/src/lib/mst/common.d.ts +17 -0
  791. package/src/lib/mst/common.js +23 -0
  792. package/src/lib/mst/common.js.map +1 -0
  793. package/src/lib/mst/index.d.ts +19 -0
  794. package/src/lib/mst/index.js +20 -0
  795. package/src/lib/mst/index.js.map +1 -0
  796. package/src/lib/mst/json.d.ts +22 -0
  797. package/src/lib/mst/json.js +23 -0
  798. package/src/lib/mst/json.js.map +1 -0
  799. package/src/lib/mst/types.d.ts +20 -0
  800. package/src/lib/mst/types.js +18 -0
  801. package/src/lib/mst/types.js.map +1 -0
  802. package/src/lib/plugin-manager/billing-webhook-hooks.d.ts +77 -0
  803. package/src/lib/plugin-manager/billing-webhook-hooks.js +55 -0
  804. package/src/lib/plugin-manager/billing-webhook-hooks.js.map +1 -0
  805. package/src/lib/plugin-manager/commerce-zone-props.d.ts +172 -0
  806. package/src/lib/plugin-manager/commerce-zone-props.js +31 -0
  807. package/src/lib/plugin-manager/commerce-zone-props.js.map +1 -0
  808. package/src/lib/plugin-manager/custom-fields.d.ts +60 -0
  809. package/src/lib/plugin-manager/custom-fields.js +39 -0
  810. package/src/lib/plugin-manager/custom-fields.js.map +1 -0
  811. package/src/lib/plugin-manager/editor-sessions.d.ts +109 -0
  812. package/src/lib/plugin-manager/editor-sessions.js +137 -0
  813. package/src/lib/plugin-manager/editor-sessions.js.map +1 -0
  814. package/src/lib/plugin-manager/enabled-plugins.d.ts +438 -0
  815. package/src/lib/plugin-manager/enabled-plugins.js +403 -0
  816. package/src/lib/plugin-manager/enabled-plugins.js.map +1 -0
  817. package/src/lib/plugin-manager/feature-plugins.d.ts +1314 -0
  818. package/src/lib/plugin-manager/feature-plugins.js +609 -0
  819. package/src/lib/plugin-manager/feature-plugins.js.map +1 -0
  820. package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +11 -0
  821. package/src/lib/plugin-manager/first-party-plugins.generated.js +149 -0
  822. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -0
  823. package/src/lib/plugin-manager/index.d.ts +42 -0
  824. package/src/lib/plugin-manager/index.js +46 -0
  825. package/src/lib/plugin-manager/index.js.map +1 -0
  826. package/src/lib/plugin-manager/install-presets.d.ts +93 -0
  827. package/src/lib/plugin-manager/install-presets.js +86 -0
  828. package/src/lib/plugin-manager/install-presets.js.map +1 -0
  829. package/src/lib/plugin-manager/media-delivery-provider.d.ts +124 -0
  830. package/src/lib/plugin-manager/media-delivery-provider.js +53 -0
  831. package/src/lib/plugin-manager/media-delivery-provider.js.map +1 -0
  832. package/src/lib/plugin-manager/order-fulfilment.d.ts +142 -0
  833. package/src/lib/plugin-manager/order-fulfilment.js +104 -0
  834. package/src/lib/plugin-manager/order-fulfilment.js.map +1 -0
  835. package/src/lib/plugin-manager/plugin-activity-actions.d.ts +122 -0
  836. package/src/lib/plugin-manager/plugin-activity-actions.js +140 -0
  837. package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -0
  838. package/src/lib/plugin-manager/plugin-bridge.d.ts +98 -0
  839. package/src/lib/plugin-manager/plugin-bridge.js +154 -0
  840. package/src/lib/plugin-manager/plugin-bridge.js.map +1 -0
  841. package/src/lib/plugin-manager/plugin-config.d.ts +148 -0
  842. package/src/lib/plugin-manager/plugin-config.js +228 -0
  843. package/src/lib/plugin-manager/plugin-config.js.map +1 -0
  844. package/src/lib/plugin-manager/plugin-console-crons.d.ts +139 -0
  845. package/src/lib/plugin-manager/plugin-console-crons.js +177 -0
  846. package/src/lib/plugin-manager/plugin-console-crons.js.map +1 -0
  847. package/src/lib/plugin-manager/plugin-contact-capture.d.ts +220 -0
  848. package/src/lib/plugin-manager/plugin-contact-capture.js +89 -0
  849. package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -0
  850. package/src/lib/plugin-manager/plugin-contributions.d.ts +244 -0
  851. package/src/lib/plugin-manager/plugin-contributions.js +283 -0
  852. package/src/lib/plugin-manager/plugin-contributions.js.map +1 -0
  853. package/src/lib/plugin-manager/plugin-entitlement-keys.d.ts +169 -0
  854. package/src/lib/plugin-manager/plugin-entitlement-keys.js +84 -0
  855. package/src/lib/plugin-manager/plugin-entitlement-keys.js.map +1 -0
  856. package/src/lib/plugin-manager/plugin-entitlements.d.ts +181 -0
  857. package/src/lib/plugin-manager/plugin-entitlements.js +188 -0
  858. package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -0
  859. package/src/lib/plugin-manager/plugin-events.d.ts +145 -0
  860. package/src/lib/plugin-manager/plugin-events.js +79 -0
  861. package/src/lib/plugin-manager/plugin-events.js.map +1 -0
  862. package/src/lib/plugin-manager/plugin-figures.d.ts +251 -0
  863. package/src/lib/plugin-manager/plugin-figures.js +252 -0
  864. package/src/lib/plugin-manager/plugin-figures.js.map +1 -0
  865. package/src/lib/plugin-manager/plugin-host-collections.d.ts +146 -0
  866. package/src/lib/plugin-manager/plugin-host-collections.js +116 -0
  867. package/src/lib/plugin-manager/plugin-host-collections.js.map +1 -0
  868. package/src/lib/plugin-manager/plugin-jobs.d.ts +130 -0
  869. package/src/lib/plugin-manager/plugin-jobs.js +182 -0
  870. package/src/lib/plugin-manager/plugin-jobs.js.map +1 -0
  871. package/src/lib/plugin-manager/plugin-loader.d.ts +86 -0
  872. package/src/lib/plugin-manager/plugin-loader.js +223 -0
  873. package/src/lib/plugin-manager/plugin-loader.js.map +1 -0
  874. package/src/lib/plugin-manager/plugin-manager.d.ts +95 -0
  875. package/src/lib/plugin-manager/plugin-manager.js +343 -0
  876. package/src/lib/plugin-manager/plugin-manager.js.map +1 -0
  877. package/src/lib/plugin-manager/plugin-metered-lines.d.ts +97 -0
  878. package/src/lib/plugin-manager/plugin-metered-lines.js +123 -0
  879. package/src/lib/plugin-manager/plugin-metered-lines.js.map +1 -0
  880. package/src/lib/plugin-manager/plugin-org-erasure.d.ts +52 -0
  881. package/src/lib/plugin-manager/plugin-org-erasure.js +100 -0
  882. package/src/lib/plugin-manager/plugin-org-erasure.js.map +1 -0
  883. package/src/lib/plugin-manager/plugin-permissions.d.ts +69 -0
  884. package/src/lib/plugin-manager/plugin-permissions.js +77 -0
  885. package/src/lib/plugin-manager/plugin-permissions.js.map +1 -0
  886. package/src/lib/plugin-manager/plugin-person-erasure.d.ts +58 -0
  887. package/src/lib/plugin-manager/plugin-person-erasure.js +109 -0
  888. package/src/lib/plugin-manager/plugin-person-erasure.js.map +1 -0
  889. package/src/lib/plugin-manager/plugin-record-cards.d.ts +99 -0
  890. package/src/lib/plugin-manager/plugin-record-cards.js +59 -0
  891. package/src/lib/plugin-manager/plugin-record-cards.js.map +1 -0
  892. package/src/lib/plugin-manager/plugin-record-facts.d.ts +111 -0
  893. package/src/lib/plugin-manager/plugin-record-facts.js +51 -0
  894. package/src/lib/plugin-manager/plugin-record-facts.js.map +1 -0
  895. package/src/lib/plugin-manager/plugin-record-routes.d.ts +129 -0
  896. package/src/lib/plugin-manager/plugin-record-routes.js +93 -0
  897. package/src/lib/plugin-manager/plugin-record-routes.js.map +1 -0
  898. package/src/lib/plugin-manager/plugin-record-timeline.d.ts +147 -0
  899. package/src/lib/plugin-manager/plugin-record-timeline.js +39 -0
  900. package/src/lib/plugin-manager/plugin-record-timeline.js.map +1 -0
  901. package/src/lib/plugin-manager/plugin-resource-drafts.d.ts +137 -0
  902. package/src/lib/plugin-manager/plugin-resource-drafts.js +51 -0
  903. package/src/lib/plugin-manager/plugin-resource-drafts.js.map +1 -0
  904. package/src/lib/plugin-manager/plugin-services.d.ts +94 -0
  905. package/src/lib/plugin-manager/plugin-services.js +153 -0
  906. package/src/lib/plugin-manager/plugin-services.js.map +1 -0
  907. package/src/lib/plugin-manager/plugin-styles-ui.d.ts +71 -0
  908. package/src/lib/plugin-manager/plugin-styles-ui.js +80 -0
  909. package/src/lib/plugin-manager/plugin-styles-ui.js.map +1 -0
  910. package/src/lib/plugin-manager/plugin-styles.d.ts +176 -0
  911. package/src/lib/plugin-manager/plugin-styles.js +186 -0
  912. package/src/lib/plugin-manager/plugin-styles.js.map +1 -0
  913. package/src/lib/plugin-manager/plugin-subprocessors.d.ts +135 -0
  914. package/src/lib/plugin-manager/plugin-subprocessors.js +129 -0
  915. package/src/lib/plugin-manager/plugin-subprocessors.js.map +1 -0
  916. package/src/lib/plugin-manager/plugin-tax-profile.d.ts +84 -0
  917. package/src/lib/plugin-manager/plugin-tax-profile.js +39 -0
  918. package/src/lib/plugin-manager/plugin-tax-profile.js.map +1 -0
  919. package/src/lib/plugin-manager/plugin-user-erasure.d.ts +53 -0
  920. package/src/lib/plugin-manager/plugin-user-erasure.js +84 -0
  921. package/src/lib/plugin-manager/plugin-user-erasure.js.map +1 -0
  922. package/src/lib/plugin-manager/plugin-zones.d.ts +121 -0
  923. package/src/lib/plugin-manager/plugin-zones.js +74 -0
  924. package/src/lib/plugin-manager/plugin-zones.js.map +1 -0
  925. package/src/lib/plugin-manager/realm-plugins.d.ts +112 -0
  926. package/src/lib/plugin-manager/realm-plugins.js +196 -0
  927. package/src/lib/plugin-manager/realm-plugins.js.map +1 -0
  928. package/src/lib/plugin-manager/realm-server.d.ts +54 -0
  929. package/src/lib/plugin-manager/realm-server.js +88 -0
  930. package/src/lib/plugin-manager/realm-server.js.map +1 -0
  931. package/src/lib/plugin-manager/record-zone-props.d.ts +126 -0
  932. package/src/lib/plugin-manager/record-zone-props.js +31 -0
  933. package/src/lib/plugin-manager/record-zone-props.js.map +1 -0
  934. package/src/lib/plugin-manager/site-page-hooks.d.ts +102 -0
  935. package/src/lib/plugin-manager/site-page-hooks.js +114 -0
  936. package/src/lib/plugin-manager/site-page-hooks.js.map +1 -0
  937. package/src/lib/plugin-manager/site-runtime.d.ts +57 -0
  938. package/src/lib/plugin-manager/site-runtime.js +25 -0
  939. package/src/lib/plugin-manager/site-runtime.js.map +1 -0
  940. package/src/lib/plugin-manager/usage-alert-contributors.d.ts +115 -0
  941. package/src/lib/plugin-manager/usage-alert-contributors.js +106 -0
  942. package/src/lib/plugin-manager/usage-alert-contributors.js.map +1 -0
  943. package/src/lib/types/ast.d.ts +199 -0
  944. package/src/lib/types/ast.js +20 -0
  945. package/src/lib/types/ast.js.map +1 -0
  946. package/src/lib/types/index.d.ts +20 -0
  947. package/src/lib/types/index.js +21 -0
  948. package/src/lib/types/index.js.map +1 -0
  949. package/src/lib/types/nodes.d.ts +608 -0
  950. package/src/lib/types/nodes.js +28 -0
  951. package/src/lib/types/nodes.js.map +1 -0
  952. package/src/lib/types/routing.d.ts +17 -0
  953. package/src/lib/types/routing.js +18 -0
  954. package/src/lib/types/routing.js.map +1 -0
  955. package/src/lib/types/screen.d.ts +23 -0
  956. package/src/lib/types/screen.js +18 -0
  957. package/src/lib/types/screen.js.map +1 -0
  958. package/src/lib/utils.d.ts +25 -0
  959. package/src/lib/utils.js +42 -0
  960. package/src/lib/utils.js.map +1 -0
  961. package/src/server.d.ts +37 -0
  962. package/src/server.js +55 -0
  963. package/src/server.js.map +1 -0
@@ -0,0 +1,4085 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
18
+ import { isOrgOverrideReasonCode } from "./org-override-reason.js";
19
+ import { PLATFORM_BRAND_NAME, PLATFORM_BRANDING_PROFILE } from "./platform-brand.js";
20
+ import { listPluginFeatures, listPluginSeatAddons, pluginSeatAddon } from "../plugin-manager/plugin-entitlements.js";
21
+ /** Sentinel for quotas a plan does not cap; `checkQuota` always allows. */ export const UNLIMITED = Number.POSITIVE_INFINITY;
22
+ /**
23
+ * How many saved form definitions one site may hold, on every plan that can
24
+ * build them at all.
25
+ *
26
+ * ⛔ **Not a tier lever, and must not become one.** The website-building field
27
+ * does not meter form COUNT: Squarespace, HubSpot and Mailchimp publish no cap
28
+ * whatsoever, Webflow abandoned the lever above its free tier, and of the two
29
+ * that do meter it one is Wix (4/10/25/75) and the other is Jotform
30
+ * (5/25/50/100), a form-first product where a form IS the billable unit. What
31
+ * this platform meters on the forms axis is `formSubmissionsPerMonth`, which
32
+ * is tiered, metered and part of a charged price.
33
+ *
34
+ * So this is an abuse ceiling wearing an entitlement's clothes. It rides
35
+ * `formsPerHost` because that is where `checkQuota` can refuse a create inside
36
+ * the counting transaction, not because the number is sold — and it resolves
37
+ * to the same value on Starter through Enterprise. Only Free differs, and only
38
+ * because the form entity rides `reusableComponents`, which Free lacks.
39
+ *
40
+ * The value clears every published competitor number by 5x and the largest
41
+ * real catalog by far, and it stays STRICTLY below `FORMS_MAX_PER_HOST` — the
42
+ * page size two listing reads use. The gap is deliberate: a catalog can sit
43
+ * above this ceiling when a per-org override is withdrawn, and the two
44
+ * numbers must stay tellable apart so a surface reading the window where it
45
+ * means the ceiling is never accidentally right. `forms.spec.ts` pins both
46
+ * halves.
47
+ *
48
+ * A per-org `entitlements.formsPerHost` override still resolves ahead of this,
49
+ * so a contract can raise or lower it for one org without moving the ceiling
50
+ * everyone else is measured against.
51
+ */ export const FORMS_PER_HOST_CEILING = 500;
52
+ /**
53
+ * Is this quota value "no cap"? (AGL-2482; AGL-2223 is the same class.)
54
+ *
55
+ * WHY A PREDICATE AND NOT `x === UNLIMITED`. `UNLIMITED` is
56
+ * `Number.POSITIVE_INFINITY`, and `JSON.stringify(Infinity)` is **`null`** —
57
+ * so the moment an entitlement crosses a route boundary the sentinel is gone
58
+ * and `=== UNLIMITED` is false for a plan that really is uncapped. Worse, the
59
+ * client then reads `Number(null)`, which is `0`, and `Number.isFinite(0)` is
60
+ * TRUE: the value sails through every guard written to reject a payload that
61
+ * cannot state the terms, and the console renders a cap of zero for the most
62
+ * expensive plan on the price list.
63
+ *
64
+ * So: `Infinity`, `null`, `undefined` and `NaN` all read as unlimited here.
65
+ *
66
+ * THIS IS A BACKSTOP, NOT THE FIX. `null` on the wire genuinely cannot
67
+ * distinguish "unlimited" from "the field is missing", and answering
68
+ * "unlimited" for a missing field is the permissive direction — the same
69
+ * shape as `checkQuota(undefined)` resolving to Free. A route that serialises
70
+ * a quota must therefore send a FINITE number plus an explicit boolean flag
71
+ * (see `/api/billing/storage-overage`), and the client must rebuild the
72
+ * sentinel from the flag with `restoreQuotaLimit` before it does arithmetic.
73
+ * This predicate exists so that a surface which has not been given the flag
74
+ * still renders "Unlimited" instead of `0`, `null` or `Infinity`.
75
+ */ export function isUnlimitedQuota(limit) {
76
+ return limit == null || !Number.isFinite(limit);
77
+ }
78
+ /**
79
+ * A quota cap, written the way every surface in the console must write it:
80
+ * `Unlimited` for an uncapped plan, the number otherwise.
81
+ *
82
+ * ONE implementation on purpose. Six surfaces had grown their own copy of the
83
+ * `limit === UNLIMITED ? 'Unlimited' : String(limit)` line and a seventh
84
+ * category — the ones that interpolated the raw number — shipped
85
+ * "Every site can run null registers on your current plan" and
86
+ * "Screen limit reached (Infinity)" to customers.
87
+ *
88
+ * @param limit The cap. Non-finite, `null` and `undefined` are unlimited.
89
+ * @param unit Appended to a finite number only — `MB`, `registers`. Never
90
+ * appended to `Unlimited`, because "Unlimited MB" reads as a typo.
91
+ */ export function formatQuotaLimit(limit, unit) {
92
+ if (isUnlimitedQuota(limit)) return 'Unlimited';
93
+ return unit ? `${limit} ${unit}` : String(limit);
94
+ }
95
+ /**
96
+ * Rebuild the `UNLIMITED` sentinel a JSON round-trip flattened, from the
97
+ * explicit flag the route sent alongside the number.
98
+ *
99
+ * The client half of the wire contract. Once this has run, every comparison
100
+ * downstream is ordinary arithmetic that happens to be correct: `used >
101
+ * Infinity` is false, `Math.max(0, used - Infinity)` is `0`, and nothing has
102
+ * to remember that this particular number might be a `null` in disguise.
103
+ *
104
+ * That mattered more than the display did. `1 > null` is **true**, so a site
105
+ * on an unlimited plan was reported "1 over the limit" while the readout
106
+ * beside it said `1/∞` — a false limit warning on a plan that has no limit.
107
+ *
108
+ * @param value The finite number the route sent (or the `null` it sent for a
109
+ * pre-fix payload).
110
+ * @param unlimited The route's explicit flag. `undefined` means the route has
111
+ * not been taught to send one, and then `value` decides — non-finite reads
112
+ * as unlimited, which is the `isUnlimitedQuota` backstop.
113
+ */ export function restoreQuotaLimit(value, unlimited) {
114
+ if (unlimited === true) return UNLIMITED;
115
+ if (unlimited === false) return Number(value);
116
+ return isUnlimitedQuota(value) ? UNLIMITED : Number(value);
117
+ }
118
+ /**
119
+ * Campaign emails an Enterprise agreement includes per month, before the
120
+ * contract says otherwise.
121
+ *
122
+ * ## Why the one Enterprise quota that is a number
123
+ *
124
+ * Every other Enterprise band is `UNLIMITED` because the thing behind it is
125
+ * infrastructure we already meter and price into the deal — storage, page
126
+ * views, seats. Email is not that. It is a per-message charge from a third
127
+ * party, so an unbounded allowance is an unbounded liability with no meter
128
+ * underneath it and no ceiling a negotiation ever had to name.
129
+ *
130
+ * `UNLIMITED` was also unrepresentable off-process. It is
131
+ * `Number.POSITIVE_INFINITY`, `JSON.stringify(Infinity)` is `null`, and
132
+ * `Number(null)` is `0` — so the most expensive plan on the price list
133
+ * serialised to a cap of ZERO through any route that did not also send the
134
+ * explicit flag `restoreQuotaLimit` rebuilds from. A finite number crosses
135
+ * the wire as itself.
136
+ *
137
+ * ## Why this number
138
+ *
139
+ * Twice Agency's band — the rule every Enterprise fallback follows since the
140
+ * 2026-09-07 pricing decision (see the `enterprise` row of
141
+ * `PLAN_ENTITLEMENTS`) — and inside what the sending platform can actually
142
+ * deliver to one workspace with room to spare. At the shipped constants a
143
+ * workspace's share is 500 messages an hour and a projected month is 720
144
+ * hours, so 360,000 is the ceiling nothing can exceed however it is sold.
145
+ * 260,000 spends 520 of those 720 hours, leaving the rest for bursts, retries
146
+ * and domain warm-up — `email-ceiling-dimensioning.spec.ts` holds that
147
+ * relation, and holds the platform rate still so the gap cannot be closed by
148
+ * quietly raising it.
149
+ *
150
+ * Selling more than that would be selling mail that cannot leave the
151
+ * building: the hourly ceiling DEFERS rather than refuses, so the excess does
152
+ * not bounce, it simply never sends.
153
+ *
154
+ * ## It is a DEFAULT, and a contract raises it
155
+ *
156
+ * `resolveOrgEntitlements` applies a per-org `entitlements.emailSendsPerMonth`
157
+ * override ahead of this, so a deal that buys more email buys it on that org
158
+ * without moving the figure every other agreement is measured against — the
159
+ * same mechanism every numeric entitlement uses.
160
+ */ export const ENTERPRISE_EMAIL_SENDS_PER_MONTH = 260000;
161
+ /**
162
+ * Aglyn Assist credits an Enterprise agreement includes per month, before the
163
+ * contract says otherwise.
164
+ *
165
+ * ## Never `UNLIMITED`, and the reason is not stylistic
166
+ *
167
+ * `UNLIMITED` is `Number.POSITIVE_INFINITY`, `JSON.stringify(Infinity)` is
168
+ * `null`, and `Number(null)` is `0`. An unbounded assist band therefore
169
+ * serialises to a band of ZERO through every route that does not also send
170
+ * the explicit flag `restoreQuotaLimit` rebuilds from — which would hand the
171
+ * only customers with a signed contract the one budget that refuses
172
+ * everything. A finite number crosses the wire as itself.
173
+ *
174
+ * The band is also a real liability rather than a capacity we already own.
175
+ * Storage and page views are infrastructure metered into the deal; assist is
176
+ * a per-token charge from a third party, and generative building spends it in
177
+ * units two orders of magnitude apart.
178
+ *
179
+ * ## Why this number
180
+ *
181
+ * Enterprise carries no list price, so the band cannot be sized the way every
182
+ * self-serve tier's is — against what that tier's other cost terms leave out
183
+ * of a known price. It is anchored to the top of the ladder instead: 116,000
184
+ * credits is twice Agency's band, the rule every Enterprise fallback follows
185
+ * since the 2026-09-07 pricing decision (see the `enterprise` row of
186
+ * `PLAN_ENTITLEMENTS`). In cost that is $116 of provider spend a month, or
187
+ * under 9% of any deal priced at or above the self-serve top. A deal below
188
+ * that is sold as Agency, not written as an Enterprise agreement.
189
+ *
190
+ * ## It is a DEFAULT, and a contract raises it
191
+ *
192
+ * `resolveOrgEntitlements` applies a per-org `entitlements.assistCreditsPerMonth`
193
+ * override ahead of this, so a deal that buys more assist buys it on that org
194
+ * without moving the figure every other agreement is measured against — the
195
+ * same mechanism the other contracted bands use.
196
+ */ export const ENTERPRISE_ASSIST_CREDITS_PER_MONTH = 116000;
197
+ /**
198
+ * The Free AI taste (AGL-2925): the assist credits a Free workspace draws
199
+ * on each month, behind a hard wall.
200
+ *
201
+ * ## Why it exists
202
+ *
203
+ * So the AI landing pages can say "generate your first page free" and mean
204
+ * it. Three hundred credits is one or two generated sections or a handful
205
+ * of copy rewrites — enough to see what the assistant does, not enough to
206
+ * build a site on. The band is a WALL: `PLAN_PRICING.free
207
+ * .extraAssistCreditsUsdPer1k` stays `null`, so `assistBandRefuses` answers
208
+ * true and the reservation refuses at 100% with no overage, no switch and no
209
+ * invoice. Nothing about a Free workspace can produce a charge.
210
+ *
211
+ * ## What it costs, and why that is the number
212
+ *
213
+ * A credit is `ASSIST_CREDIT_COST_USD` of provider spend, so the band costs
214
+ * at most **$0.30 per Free workspace per month** — asserted by
215
+ * `apps/console/specs/tier-margin-floor.spec.ts`. It is the one AI band with
216
+ * no invoice behind it, which is why the same issue closes every multiplier
217
+ * a script could use before the flag flips:
218
+ *
219
+ * - it is metered per ACCOUNT as well as per workspace, so the three free
220
+ * workspaces an account may hold (AGL-2265) share one 300-credit
221
+ * allowance rather than tripling it (`reserveAssistMessage`);
222
+ * - a fresh account waits `AI_FREE_MIN_ACCOUNT_AGE_HOURS` before it can
223
+ * spend, and every free request is bounded per IP, per uid and per
224
+ * account per day;
225
+ * - a platform-wide daily ceiling on free spend pauses the taste for
226
+ * everyone until the UTC day rolls, paid workspaces untouched.
227
+ *
228
+ * The account allowance reads THIS constant rather than the workspace's
229
+ * resolved band, because a staff override that widens one workspace's band
230
+ * is a decision about that workspace, not about how much one person may
231
+ * draw across all of theirs.
232
+ */ export const FREE_AI_TASTE_CREDITS_PER_MONTH = 300;
233
+ export const PLAN_ENTITLEMENTS = {
234
+ free: {
235
+ hostLimit: 1,
236
+ screensPerHost: 5,
237
+ sharedLayoutsPerHost: 1,
238
+ templatesPerHost: 10,
239
+ storagePerHostMb: 250,
240
+ membersPerHost: 1,
241
+ managersPerOrg: 1,
242
+ maxManagersPerOrg: 1,
243
+ maxMembersPerHost: 1,
244
+ bandwidthGb: 2,
245
+ formSubmissionsPerMonth: 20,
246
+ // No saved-form CATALOG on Free: the form entity rides
247
+ // `reusableComponents`, which is Starter-and-above, so
248
+ // `/api/hosts/resources` refuses the create on the entitlement before it
249
+ // ever reaches this number. Zero is what a Free site actually gets.
250
+ //
251
+ // It does NOT mean a Free site has no forms. A `Form` node placed on a
252
+ // page needs no definition to collect, and the 20 submissions above are
253
+ // the band those replies spend.
254
+ //
255
+ // Every other plan carries `FORMS_PER_HOST_CEILING`. This is the one
256
+ // number on the axis that differs, and it differs because the entitlement
257
+ // gate above it says so, not because the catalog is sold by the tier.
258
+ formsPerHost: 0,
259
+ variablesPerHost: 3,
260
+ functionsPerHost: 1,
261
+ workflowsPerHost: 0,
262
+ workflowRunsPerMonth: 0,
263
+ servicesPerHost: 0,
264
+ redirectsPerHost: 0,
265
+ contactsPerHost: 100,
266
+ // No one-to-one email on Free, which has no CRM (below): the send lives
267
+ // on a record's page, and Free opens no record. Zero here is what
268
+ // `checkCrmEmailQuota` refuses
269
+ // against, so a per-org grant of `features.crm` alone still sends
270
+ // nothing until the band is raised with it.
271
+ crmEmailsPerDay: 0,
272
+ emailSendsPerMonth: 0,
273
+ actionRunsPerMonth: 0,
274
+ // The Free AI taste (AGL-2925): a real band, and a wall. See the
275
+ // constant for what it buys, what it costs and the precautions that
276
+ // keep one person from drawing it three times over.
277
+ assistCreditsPerMonth: FREE_AI_TASTE_CREDITS_PER_MONTH,
278
+ apiRequestsPerMonth: 0,
279
+ datasetsPerOrg: 0,
280
+ maxDatasetsPerOrg: 0,
281
+ recordsPerDataset: 0,
282
+ dataStorageMbPerOrg: 0,
283
+ productsPerHost: 0,
284
+ inventoryLocations: 1,
285
+ posRegisters: 0,
286
+ transactionFeePhysicalPct: 0,
287
+ transactionFeeDigitalPct: 0,
288
+ // Marketplace take rate (AGL-46/1543): free-plan sellers pay a higher
289
+ // share. `marketplaceSelling` is false here, so this rate only prices
290
+ // an org GRANTED selling via a per-org feature override — and any org
291
+ // whose dead subscription resolved it down to free.
292
+ marketplaceFeePct: 30,
293
+ features: {
294
+ abTesting: false,
295
+ versioning: false,
296
+ reusableComponents: false,
297
+ customDomain: false,
298
+ // Sending as a domain the customer owns starts at Pro, with campaign
299
+ // email. Free sends no marketing at all — `emailSendsPerMonth` is 0 —
300
+ // and its transactional mail leaves on the shared pool, which needs no
301
+ // DNS from anybody.
302
+ customSendingDomain: false,
303
+ // The expensive shape, and the one Free must never reach by default: a
304
+ // provider slot, three records in the platform's own zone and a place
305
+ // in the re-verification sweep, spent per site on a tier that pays
306
+ // nothing. The pool carries this tier's mail at no per-site cost.
307
+ dedicatedSendingDomain: false,
308
+ removeBranding: false,
309
+ scheduledPublishing: false,
310
+ marketplaceSelling: false,
311
+ // The two AI flags point opposite ways on Free, on purpose (AGL-2925).
312
+ // `aiAssist` is the guided rung of the console assistant — page-aware
313
+ // level-2 answers and the copy assistant — and stays Pro and up; Free
314
+ // keeps the docs-grounded level-1 chat it always had. `aiGenerative`
315
+ // is the generative door the taste is FOR: a Free workspace may
316
+ // generate against its 300-credit band, behind the wall and every
317
+ // precaution the constant lists, so the landing page's "generate your
318
+ // first page free" is a thing the product does.
319
+ aiAssist: false,
320
+ aiGenerative: true,
321
+ workflows: false,
322
+ dataStore: false,
323
+ videoMedia: false,
324
+ bookings: false,
325
+ // The CRM starts at Starter (AGL-2611), whole, and Free has none of it
326
+ // (AGL-2851): no Leads, Contacts, Companies, Deals, Tasks, Reports,
327
+ // Fields or Settings, no CRM automation step and no CRM REST resource.
328
+ // What Free's sites capture still counts against its 100-record band,
329
+ // and exporting and erasing those people stay on every plan.
330
+ // The Drive pricing decision of 2026-09-05 records why Starter and
331
+ // not Pro: the field prices a CRM seat at $14–25 a month, so Starter
332
+ // with the suite included is the competitive entry, and gating a
333
+ // tier higher hands the small-business buyer to a free CRM elsewhere.
334
+ crm: false,
335
+ // Sequences (AGL-2974) is on NO plan, this one through Enterprise: which
336
+ // tiers carry sequences and connected mailboxes, and at what caps, is
337
+ // an owner decision still to be made. Until then an organization gets
338
+ // it only through the per-org `entitlements.features.outreach`
339
+ // override, and the Firestore rules read that override alone.
340
+ outreach: false,
341
+ interactions: true,
342
+ actions: false,
343
+ webhooks: false,
344
+ apiAccess: false,
345
+ siteExport: false,
346
+ multilingual: false,
347
+ eventCalendar: false,
348
+ redirects: false,
349
+ screenAnalytics: false,
350
+ /**
351
+ * CDN delivery and responsive image variants, on every plan (AGL-1152).
352
+ *
353
+ * Without this, media is addressed by its absolute
354
+ * `firebasestorage.googleapis.com` URL: a visitor fetches the full-size
355
+ * original from Storage egress on every request, with no shared edge
356
+ * cache in front of it. With it, media is served over the CDN path,
357
+ * which is edge-cacheable and returns a resized WebP variant.
358
+ *
359
+ * The ungated path therefore costs more to run, on both bytes per
360
+ * request and origin requests per byte. Gating this would raise the cost
361
+ * of the tier that pays nothing.
362
+ */ mediaCdn: true,
363
+ marketingOverlays: false,
364
+ commerce: false,
365
+ pos: false,
366
+ storefrontSubscriptions: false,
367
+ contentGating: false,
368
+ giftCards: false,
369
+ productReviews: false,
370
+ abandonedCart: false,
371
+ dropshipRouting: false,
372
+ commerceAnalytics: false,
373
+ whiteLabel: false,
374
+ ssoEnabled: false
375
+ }
376
+ },
377
+ starter: {
378
+ hostLimit: 1,
379
+ screensPerHost: 25,
380
+ sharedLayoutsPerHost: 3,
381
+ templatesPerHost: 50,
382
+ storagePerHostMb: 2048,
383
+ membersPerHost: 3,
384
+ managersPerOrg: 2,
385
+ maxManagersPerOrg: 5,
386
+ maxMembersPerHost: 10,
387
+ bandwidthGb: 50,
388
+ formSubmissionsPerMonth: 200,
389
+ formsPerHost: FORMS_PER_HOST_CEILING,
390
+ variablesPerHost: 25,
391
+ functionsPerHost: 10,
392
+ workflowsPerHost: 3,
393
+ workflowRunsPerMonth: 500,
394
+ servicesPerHost: 1,
395
+ redirectsPerHost: 25,
396
+ contactsPerHost: 1000,
397
+ // One-to-one email from a CRM record, per UTC day (AGL-2611). A hard
398
+ // daily pace rather than a monthly meter — see `crmEmailsPerDay` on
399
+ // `OrgEntitlements` — sized so the whole day spent at $0.0009 a message
400
+ // leaves the tier's CRM axis inside the 20% cost share the Drive
401
+ // pricing decision of 2026-09-05 sets. 50 a day is $1.35 a month against
402
+ // a $16 annual price. Every send still lands on the `emailSends` cost
403
+ // meter beside the transactional mail this tier already sends.
404
+ crmEmailsPerDay: 50,
405
+ // Campaign email starts at Pro. A site that may send campaigns needs its
406
+ // own verified provider sending domain, and provisioning one is a real
407
+ // per-site operational cost that a campaign allowance commits the platform
408
+ // to the moment the tier includes it. Holding the allowance at 0 here
409
+ // keeps that cost attached to the tiers that carry it.
410
+ //
411
+ // Zero is a REFUSAL, not a formality: `reserveCampaignEmailSends` compares
412
+ // `used + count > limit` inside its transaction, so any campaign of one or
413
+ // more recipients is refused before a message leaves.
414
+ //
415
+ // It does NOT stop transactional mail. Receipts, invites, booking
416
+ // reminders, workflow notifications and password resets are refused by no
417
+ // quota at any tier (see `emailSendsPerMonth` on `OrgEntitlements`), so a
418
+ // Starter store still confirms its orders. Those sends still count on the
419
+ // `emailSends` cost meter; they are simply not billable here, because
420
+ // `emailSendsOverage` yields 0 against a non-positive band.
421
+ //
422
+ // The contacts band above is untouched: holding an audience is not sending
423
+ // to it, and the CRM is what a Starter site builds before it upgrades.
424
+ emailSendsPerMonth: 0,
425
+ actionRunsPerMonth: 0,
426
+ // The first paid rung of the assist ladder (AGL-3203). Starter banded at
427
+ // 0 until 2026-09-20, which put a PAYING workspace below the Free taste's
428
+ // 300 on the one axis a visitor compares straight down the column — the
429
+ // `/pricing` row read `300 / mo` for Free and `—` for Starter. A paid
430
+ // plan that includes less of something than the free plan is not a
431
+ // packaging subtlety; it is the comparison table arguing against the
432
+ // upgrade.
433
+ //
434
+ // 750 is deliberately small: 2.5x the taste, enough that the assistant is
435
+ // a thing the tier HAS rather than a thing it is shown, and $0.75 of
436
+ // provider spend a month at `ASSIST_CREDIT_COST_USD` against a $16 annual
437
+ // price — a twentieth of the $8.81 the tier's other metered bands already
438
+ // cost, and the widest margin on the ladder absorbs it with room left
439
+ // (`tier-margin-floor.spec.ts` carries the arithmetic).
440
+ //
441
+ // It is NOT a wall, unlike Free's: `PLAN_PRICING.starter
442
+ // .extraAssistCreditsUsdPer1k` is $3.00, so past 750 the tier meters and
443
+ // bills like every paid plan above it. The two fields move together by
444
+ // rule — a positive band with no rate beside it is usage past a bound
445
+ // that is silently free, which `plan-entitlements.spec.ts` forbids.
446
+ //
447
+ // `features.aiAssist` stays false: the band is credits, not the guided
448
+ // rung of the console assistant, exactly as on Free. The AI add-on adds
449
+ // `AI_ADDON_CREDITS_PER_MONTH.starter` on top and switches both flags on.
450
+ assistCreditsPerMonth: 750,
451
+ apiRequestsPerMonth: 0,
452
+ datasetsPerOrg: 3,
453
+ maxDatasetsPerOrg: 10,
454
+ recordsPerDataset: 1000,
455
+ dataStorageMbPerOrg: 1024,
456
+ productsPerHost: 100,
457
+ inventoryLocations: 1,
458
+ posRegisters: 0,
459
+ transactionFeePhysicalPct: 2,
460
+ // Pricing v3 (2026-07): softened 7→5 — a 7% digital fee stacked on the
461
+ // ~2.9%+30¢ processor was ~10% all-in, far above the website-builder
462
+ // norm (Wix/BigCommerce/Squarespace-Core = 0% platform fee). See the
463
+ // Pricing Decision Log + Competitive Benchmark.
464
+ transactionFeeDigitalPct: 5,
465
+ marketplaceFeePct: 20,
466
+ features: {
467
+ abTesting: false,
468
+ versioning: false,
469
+ reusableComponents: true,
470
+ customDomain: true,
471
+ // Starter's allowance is 0 campaign sends, so a verified domain of the
472
+ // customer's own would carry receipts and nothing else — the one class
473
+ // of mail the shared pool already delivers with no DNS work at all. The
474
+ // capability arrives with the sends that need it.
475
+ customSendingDomain: false,
476
+ // Same tier as the customer-owned shape and for the stronger reason:
477
+ // this one draws on a provider allowance that cannot be bought past a
478
+ // certain size, so it is rationed above the tier that carries no
479
+ // campaign sends at all.
480
+ dedicatedSendingDomain: false,
481
+ removeBranding: true,
482
+ scheduledPublishing: false,
483
+ marketplaceSelling: false,
484
+ aiAssist: false,
485
+ aiGenerative: false,
486
+ workflows: true,
487
+ dataStore: true,
488
+ videoMedia: false,
489
+ bookings: true,
490
+ // The whole CRM, from the first paid tier (AGL-2611). At 100% of
491
+ // its band and its one-to-one email cap this costs $1.73 a month
492
+ // against a $16 annual price — the Drive pricing decision of
493
+ // 2026-09-05 has the arithmetic — and it is the upgrade motive from
494
+ // Free, not a line on top of a plan.
495
+ crm: true,
496
+ outreach: false,
497
+ interactions: true,
498
+ actions: false,
499
+ webhooks: false,
500
+ apiAccess: false,
501
+ siteExport: false,
502
+ multilingual: false,
503
+ eventCalendar: false,
504
+ redirects: true,
505
+ screenAnalytics: false,
506
+ mediaCdn: true,
507
+ marketingOverlays: true,
508
+ commerce: true,
509
+ pos: false,
510
+ storefrontSubscriptions: false,
511
+ contentGating: false,
512
+ giftCards: false,
513
+ productReviews: false,
514
+ abandonedCart: false,
515
+ dropshipRouting: false,
516
+ commerceAnalytics: false,
517
+ whiteLabel: false,
518
+ ssoEnabled: false
519
+ }
520
+ },
521
+ pro: {
522
+ hostLimit: 3,
523
+ screensPerHost: 100,
524
+ sharedLayoutsPerHost: UNLIMITED,
525
+ templatesPerHost: UNLIMITED,
526
+ storagePerHostMb: 10240,
527
+ membersPerHost: 10,
528
+ managersPerOrg: 5,
529
+ maxManagersPerOrg: 20,
530
+ maxMembersPerHost: 25,
531
+ // Page views are the largest term of this tier's modeled COGS — one GB
532
+ // is 1,035 views at `ESTIMATED_PAGE_TRANSFER_BYTES`, or $0.167 of
533
+ // measured cost — so the bandwidth band is what decides whether the tier
534
+ // survives a customer spending the whole allowance it was sold.
535
+ //
536
+ // The invariant the band is sized against is the ANNUAL price ($39 a
537
+ // month) net of Stripe's fee, with every band at 100% and the CRM seat
538
+ // and one-to-one email terms counted — the 2026-09-07 pricing decision.
539
+ // At 225 GB that price was 44% under water; 125 GB is what it carries.
540
+ // `tier-margin-floor.spec.ts` holds the model and pins the figure.
541
+ //
542
+ // `meteredInfraPassThrough` is true here, so traffic past the band BILLS
543
+ // at the page-view pass-through rather than being refused or absorbed.
544
+ bandwidthGb: 125,
545
+ formSubmissionsPerMonth: 1000,
546
+ formsPerHost: FORMS_PER_HOST_CEILING,
547
+ variablesPerHost: 100,
548
+ functionsPerHost: 50,
549
+ workflowsPerHost: 25,
550
+ workflowRunsPerMonth: 5000,
551
+ servicesPerHost: UNLIMITED,
552
+ redirectsPerHost: 100,
553
+ contactsPerHost: 10000,
554
+ crmEmailsPerDay: 150,
555
+ emailSendsPerMonth: 5000,
556
+ actionRunsPerMonth: 5000,
557
+ // $2.75 of provider spend, against the $8.35 the tier's other seven cost
558
+ // terms leave out of $56. See `OrgEntitlements.assistCreditsPerMonth` for
559
+ // why the remainder and not the price is what sizes this.
560
+ assistCreditsPerMonth: 2750,
561
+ apiRequestsPerMonth: 0,
562
+ datasetsPerOrg: 15,
563
+ maxDatasetsPerOrg: 50,
564
+ recordsPerDataset: 10000,
565
+ dataStorageMbPerOrg: 5120,
566
+ productsPerHost: 2500,
567
+ inventoryLocations: 2,
568
+ posRegisters: 1,
569
+ transactionFeePhysicalPct: 0,
570
+ // Pricing v3 (2026-07): softened 5→3 to smooth the digital fee ladder
571
+ // (Starter 5 → Pro 3 → Business 2 → Scale 1 → Advanced/Agency 0).
572
+ transactionFeeDigitalPct: 3,
573
+ marketplaceFeePct: 20,
574
+ features: {
575
+ abTesting: false,
576
+ versioning: true,
577
+ reusableComponents: true,
578
+ customDomain: true,
579
+ // The tier campaign email starts at, and therefore the tier a domain
580
+ // whose reputation belongs to the merchant starts at: marketing may not
581
+ // leave on the shared pool, because one merchant's complaint rate would
582
+ // be charged against every other site's password resets.
583
+ //
584
+ // It costs the platform nothing in our own zone — the customer
585
+ // publishes the records — which is what keeps the platform's domain
586
+ // count flat as the customer base grows. The platform subdomain a site
587
+ // is issued is the expensive half and stays an optimization on top of
588
+ // the pool.
589
+ customSendingDomain: true,
590
+ // The floor for the platform subdomain too, and the tier this one is
591
+ // sized against: a merchant who may send campaigns is a merchant whose
592
+ // mail needs a name of its own, and Pro is where the revenue starts
593
+ // covering a provider slot and three zone records.
594
+ //
595
+ // A flag rather than a tier comparison, so a single org can be granted
596
+ // one through `entitlements.features` without being moved up a plan —
597
+ // the support case this capability meets most often, answered without
598
+ // repricing an account. What the grant does not do is provision: it
599
+ // admits a REQUEST, and the provider ceiling still decides whether the
600
+ // request is filled.
601
+ dedicatedSendingDomain: true,
602
+ removeBranding: true,
603
+ scheduledPublishing: false,
604
+ marketplaceSelling: true,
605
+ aiAssist: true,
606
+ aiGenerative: false,
607
+ workflows: true,
608
+ dataStore: true,
609
+ videoMedia: true,
610
+ bookings: true,
611
+ crm: true,
612
+ outreach: false,
613
+ interactions: true,
614
+ actions: true,
615
+ webhooks: false,
616
+ apiAccess: false,
617
+ siteExport: true,
618
+ multilingual: false,
619
+ eventCalendar: false,
620
+ redirects: true,
621
+ screenAnalytics: true,
622
+ mediaCdn: true,
623
+ marketingOverlays: true,
624
+ commerce: true,
625
+ pos: true,
626
+ storefrontSubscriptions: false,
627
+ contentGating: false,
628
+ giftCards: false,
629
+ productReviews: true,
630
+ abandonedCart: true,
631
+ dropshipRouting: true,
632
+ commerceAnalytics: true,
633
+ whiteLabel: false,
634
+ ssoEnabled: false
635
+ }
636
+ },
637
+ business: {
638
+ hostLimit: 10,
639
+ screensPerHost: UNLIMITED,
640
+ sharedLayoutsPerHost: UNLIMITED,
641
+ templatesPerHost: UNLIMITED,
642
+ storagePerHostMb: 20480,
643
+ membersPerHost: 50,
644
+ managersPerOrg: 15,
645
+ maxManagersPerOrg: 100,
646
+ maxMembersPerHost: 100,
647
+ // Sized by the same annual-price invariant as Pro's — see that band.
648
+ bandwidthGb: 185,
649
+ formSubmissionsPerMonth: 8000,
650
+ formsPerHost: FORMS_PER_HOST_CEILING,
651
+ variablesPerHost: 1000,
652
+ functionsPerHost: 250,
653
+ workflowsPerHost: 100,
654
+ workflowRunsPerMonth: 50000,
655
+ servicesPerHost: UNLIMITED,
656
+ redirectsPerHost: UNLIMITED,
657
+ contactsPerHost: 50000,
658
+ crmEmailsPerDay: 200,
659
+ emailSendsPerMonth: 25000,
660
+ actionRunsPerMonth: 50000,
661
+ // $7.50, against the $22.69 the other seven terms leave out of $139.
662
+ assistCreditsPerMonth: 7500,
663
+ apiRequestsPerMonth: 100000,
664
+ datasetsPerOrg: 100,
665
+ maxDatasetsPerOrg: 250,
666
+ recordsPerDataset: 100000,
667
+ dataStorageMbPerOrg: 25600,
668
+ productsPerHost: 10000,
669
+ inventoryLocations: 4,
670
+ posRegisters: 2,
671
+ transactionFeePhysicalPct: 0,
672
+ transactionFeeDigitalPct: 2,
673
+ marketplaceFeePct: 20,
674
+ features: {
675
+ abTesting: true,
676
+ versioning: true,
677
+ reusableComponents: true,
678
+ customDomain: true,
679
+ customSendingDomain: true,
680
+ dedicatedSendingDomain: true,
681
+ removeBranding: true,
682
+ scheduledPublishing: true,
683
+ marketplaceSelling: true,
684
+ aiAssist: true,
685
+ aiGenerative: false,
686
+ workflows: true,
687
+ dataStore: true,
688
+ videoMedia: true,
689
+ bookings: true,
690
+ crm: true,
691
+ outreach: false,
692
+ interactions: true,
693
+ actions: true,
694
+ webhooks: true,
695
+ apiAccess: true,
696
+ siteExport: true,
697
+ multilingual: true,
698
+ eventCalendar: false,
699
+ redirects: true,
700
+ screenAnalytics: true,
701
+ mediaCdn: true,
702
+ marketingOverlays: true,
703
+ commerce: true,
704
+ pos: true,
705
+ storefrontSubscriptions: true,
706
+ contentGating: true,
707
+ giftCards: true,
708
+ productReviews: true,
709
+ abandonedCart: true,
710
+ dropshipRouting: true,
711
+ commerceAnalytics: true,
712
+ whiteLabel: false,
713
+ ssoEnabled: false
714
+ }
715
+ },
716
+ // Scale (Pricing v3, 2026-07): fills the $139→$399 gap. Feature set equals
717
+ // Business/Advanced (all commerce features); limits interpolate between
718
+ // Business and Advanced; digital platform fee drops to 1%.
719
+ scale: {
720
+ hostLimit: 15,
721
+ screensPerHost: UNLIMITED,
722
+ sharedLayoutsPerHost: UNLIMITED,
723
+ templatesPerHost: UNLIMITED,
724
+ storagePerHostMb: 30720,
725
+ membersPerHost: 75,
726
+ managersPerOrg: 25,
727
+ maxManagersPerOrg: 150,
728
+ maxMembersPerHost: 150,
729
+ bandwidthGb: 290,
730
+ formSubmissionsPerMonth: 25000,
731
+ formsPerHost: FORMS_PER_HOST_CEILING,
732
+ variablesPerHost: 5000,
733
+ functionsPerHost: 500,
734
+ workflowsPerHost: 250,
735
+ workflowRunsPerMonth: 150000,
736
+ servicesPerHost: UNLIMITED,
737
+ redirectsPerHost: UNLIMITED,
738
+ contactsPerHost: 100000,
739
+ crmEmailsPerDay: 300,
740
+ emailSendsPerMonth: 40000,
741
+ actionRunsPerMonth: 100000,
742
+ // $10.00, against the $30.62 the other seven terms leave out of $249.
743
+ assistCreditsPerMonth: 10000,
744
+ apiRequestsPerMonth: 300000,
745
+ datasetsPerOrg: 250,
746
+ maxDatasetsPerOrg: 500,
747
+ recordsPerDataset: 500000,
748
+ dataStorageMbPerOrg: 51200,
749
+ productsPerHost: 25000,
750
+ inventoryLocations: 6,
751
+ posRegisters: 3,
752
+ transactionFeePhysicalPct: 0,
753
+ transactionFeeDigitalPct: 1,
754
+ marketplaceFeePct: 20,
755
+ features: {
756
+ abTesting: true,
757
+ versioning: true,
758
+ reusableComponents: true,
759
+ customDomain: true,
760
+ customSendingDomain: true,
761
+ dedicatedSendingDomain: true,
762
+ removeBranding: true,
763
+ scheduledPublishing: true,
764
+ marketplaceSelling: true,
765
+ aiAssist: true,
766
+ aiGenerative: false,
767
+ workflows: true,
768
+ dataStore: true,
769
+ videoMedia: true,
770
+ bookings: true,
771
+ crm: true,
772
+ outreach: false,
773
+ interactions: true,
774
+ actions: true,
775
+ webhooks: true,
776
+ apiAccess: true,
777
+ siteExport: true,
778
+ multilingual: true,
779
+ eventCalendar: false,
780
+ redirects: true,
781
+ screenAnalytics: true,
782
+ mediaCdn: true,
783
+ marketingOverlays: true,
784
+ commerce: true,
785
+ pos: true,
786
+ storefrontSubscriptions: true,
787
+ contentGating: true,
788
+ giftCards: true,
789
+ productReviews: true,
790
+ abandonedCart: true,
791
+ dropshipRouting: true,
792
+ commerceAnalytics: true,
793
+ whiteLabel: false,
794
+ ssoEnabled: false
795
+ }
796
+ },
797
+ advanced: {
798
+ hostLimit: 25,
799
+ screensPerHost: UNLIMITED,
800
+ sharedLayoutsPerHost: UNLIMITED,
801
+ templatesPerHost: UNLIMITED,
802
+ storagePerHostMb: 40960,
803
+ membersPerHost: 100,
804
+ managersPerOrg: 50,
805
+ maxManagersPerOrg: 250,
806
+ maxMembersPerHost: 250,
807
+ bandwidthGb: 345,
808
+ formSubmissionsPerMonth: 40000,
809
+ formsPerHost: FORMS_PER_HOST_CEILING,
810
+ variablesPerHost: UNLIMITED,
811
+ functionsPerHost: 1000,
812
+ workflowsPerHost: 500,
813
+ workflowRunsPerMonth: 500000,
814
+ servicesPerHost: UNLIMITED,
815
+ redirectsPerHost: UNLIMITED,
816
+ contactsPerHost: 150000,
817
+ crmEmailsPerDay: 500,
818
+ emailSendsPerMonth: 65000,
819
+ actionRunsPerMonth: 250000,
820
+ // $13.00, against the $39.74 the other seven terms leave out of $399 —
821
+ // the thinnest remainder on the ladder, and the tier that binds the
822
+ // whole assist ladder's share of it.
823
+ assistCreditsPerMonth: 13000,
824
+ apiRequestsPerMonth: 1000000,
825
+ datasetsPerOrg: 500,
826
+ maxDatasetsPerOrg: 1000,
827
+ recordsPerDataset: 1000000,
828
+ dataStorageMbPerOrg: 102400,
829
+ productsPerHost: UNLIMITED,
830
+ inventoryLocations: 10,
831
+ posRegisters: 5,
832
+ transactionFeePhysicalPct: 0,
833
+ transactionFeeDigitalPct: 0,
834
+ marketplaceFeePct: 20,
835
+ features: {
836
+ abTesting: true,
837
+ versioning: true,
838
+ reusableComponents: true,
839
+ customDomain: true,
840
+ customSendingDomain: true,
841
+ dedicatedSendingDomain: true,
842
+ removeBranding: true,
843
+ scheduledPublishing: true,
844
+ marketplaceSelling: true,
845
+ aiAssist: true,
846
+ aiGenerative: false,
847
+ workflows: true,
848
+ dataStore: true,
849
+ videoMedia: true,
850
+ bookings: true,
851
+ crm: true,
852
+ outreach: false,
853
+ interactions: true,
854
+ actions: true,
855
+ webhooks: true,
856
+ apiAccess: true,
857
+ siteExport: true,
858
+ multilingual: true,
859
+ eventCalendar: false,
860
+ redirects: true,
861
+ screenAnalytics: true,
862
+ mediaCdn: true,
863
+ marketingOverlays: true,
864
+ commerce: true,
865
+ pos: true,
866
+ storefrontSubscriptions: true,
867
+ contentGating: true,
868
+ giftCards: true,
869
+ productReviews: true,
870
+ abandonedCart: true,
871
+ dropshipRouting: true,
872
+ commerceAnalytics: true,
873
+ whiteLabel: false,
874
+ ssoEnabled: false
875
+ }
876
+ },
877
+ // Agency (Pricing v3, 2026-07): high-volume multi-site tier above Advanced.
878
+ // 100 sites, 0% fees, top-of-ladder capacity. Positioned for agencies /
879
+ // resellers — white-label, SSO, and an SLA are the follow-on product work
880
+ // that will further differentiate it (see the Pricing Decision Log).
881
+ agency: {
882
+ hostLimit: 100,
883
+ screensPerHost: UNLIMITED,
884
+ sharedLayoutsPerHost: UNLIMITED,
885
+ templatesPerHost: UNLIMITED,
886
+ storagePerHostMb: 61440,
887
+ membersPerHost: 250,
888
+ managersPerOrg: 100,
889
+ maxManagersPerOrg: 500,
890
+ maxMembersPerHost: 1000,
891
+ bandwidthGb: 1540,
892
+ // FINITE, where every other capacity row on this tier is unbounded.
893
+ // `formSubmissionsPerMonth` is multiplied by `hostLimit` to get the
894
+ // org-wide band, so at 100 hosts an unbounded figure was not merely
895
+ // large — it made this tier's cost model unbounded, and an unbounded
896
+ // term reads as ZERO in any analysis that scores an absent band as
897
+ // nothing. That is how it stayed invisible: the biggest line item on the
898
+ // most expensive self-serve plan, contributing 0 to every total.
899
+ //
900
+ // Metering makes it safe to bound. `meteredInfraPassThrough` is true
901
+ // here, so submissions past the band BILL at the pass-through rate
902
+ // rather than being refused — a merchant's lead form does not stop
903
+ // working at 25,000.
904
+ formSubmissionsPerMonth: 25000,
905
+ formsPerHost: FORMS_PER_HOST_CEILING,
906
+ variablesPerHost: UNLIMITED,
907
+ functionsPerHost: UNLIMITED,
908
+ workflowsPerHost: UNLIMITED,
909
+ workflowRunsPerMonth: 2000000,
910
+ servicesPerHost: UNLIMITED,
911
+ redirectsPerHost: UNLIMITED,
912
+ contactsPerHost: 500000,
913
+ crmEmailsPerDay: 1000,
914
+ emailSendsPerMonth: 130000,
915
+ actionRunsPerMonth: 1000000,
916
+ // $58.00, against the $176.71 the other seven terms leave out of $1,299.
917
+ assistCreditsPerMonth: 58000,
918
+ apiRequestsPerMonth: 5000000,
919
+ datasetsPerOrg: 2000,
920
+ maxDatasetsPerOrg: 5000,
921
+ recordsPerDataset: UNLIMITED,
922
+ dataStorageMbPerOrg: 512000,
923
+ productsPerHost: UNLIMITED,
924
+ inventoryLocations: 50,
925
+ posRegisters: 20,
926
+ transactionFeePhysicalPct: 0,
927
+ transactionFeeDigitalPct: 0,
928
+ marketplaceFeePct: 20,
929
+ features: {
930
+ abTesting: true,
931
+ versioning: true,
932
+ reusableComponents: true,
933
+ customDomain: true,
934
+ customSendingDomain: true,
935
+ dedicatedSendingDomain: true,
936
+ removeBranding: true,
937
+ scheduledPublishing: true,
938
+ marketplaceSelling: true,
939
+ aiAssist: true,
940
+ aiGenerative: false,
941
+ workflows: true,
942
+ dataStore: true,
943
+ videoMedia: true,
944
+ bookings: true,
945
+ crm: true,
946
+ outreach: false,
947
+ interactions: true,
948
+ actions: true,
949
+ webhooks: true,
950
+ apiAccess: true,
951
+ siteExport: true,
952
+ multilingual: true,
953
+ eventCalendar: false,
954
+ redirects: true,
955
+ screenAnalytics: true,
956
+ mediaCdn: true,
957
+ marketingOverlays: true,
958
+ commerce: true,
959
+ pos: true,
960
+ storefrontSubscriptions: true,
961
+ contentGating: true,
962
+ giftCards: true,
963
+ productReviews: true,
964
+ abandonedCart: true,
965
+ dropshipRouting: true,
966
+ commerceAnalytics: true,
967
+ // White-Label Phase 1: the Agency-tier ($1,299) differentiator. Only
968
+ // this tier and Enterprise ship white-label by default.
969
+ whiteLabel: true,
970
+ ssoEnabled: false
971
+ }
972
+ },
973
+ // Enterprise (AGL-1118): the top of the ladder and the ONLY custom-priced
974
+ // tier — staff-provisioned per deal (AGL-1110), never sold self-serve, so it
975
+ // is excluded from `SELF_SERVE_PLANS` and carries no list price in
976
+ // `PLAN_PRICING`. White-label AND SSO ship on the plan itself rather than
977
+ // through the per-org `entitlements` override that Enterprise-as-a-label
978
+ // needed.
979
+ //
980
+ // ## Every band here is a FALLBACK, and it is FINITE (2026-09-07)
981
+ //
982
+ // The row used to read UNLIMITED on every axis that costs infrastructure,
983
+ // on the theory that the deal prices the usage. It does not, unless the
984
+ // deal says so: `meteredInfraPassThrough` is false here and every overage
985
+ // rate is the "not for sale" sentinel, so an Enterprise org provisioned
986
+ // without per-org figures was the one org on the platform whose cost had no
987
+ // bound at all — the pricing soundness audit of 2026-09-07 found its worst
988
+ // case unbounded on every axis. A default that a contract must OVERRIDE to
989
+ // become finite is the wrong way round.
990
+ //
991
+ // So the rule is Agency's band × 2 on every axis Agency bounds, and
992
+ // UNLIMITED only where Agency is already UNLIMITED (screens, layouts,
993
+ // templates, variables, functions, workflows, services, redirects, records
994
+ // per dataset, products). `plan-entitlements.spec.ts` holds that relation
995
+ // key by key, so moving an Agency band without moving this one goes red.
996
+ //
997
+ // A contracted per-org `entitlements.*` value still wins on every one of
998
+ // these — `resolveOrgEntitlements` honors a numeric override on every
999
+ // axis, which is the instrument a deal that needs more is written with. A
1000
+ // fallback is what an agreement gets before anybody has written it down.
1001
+ //
1002
+ // These are CAPS, not prices: with no rate and no pass-through the quota
1003
+ // gates refuse at the line (`checkContactQuota`, `checkApiRequestQuota`,
1004
+ // `checkFormSubmissionQuota`, `mediaStorageGate` …), exactly as Free's do.
1005
+ // At twice the largest self-serve tier's bands that line is one a deal
1006
+ // reaches only by being written without its numbers. The bandwidth abuse
1007
+ // ceiling still flags and pages staff at 3× the band, and does NOT degrade
1008
+ // the render on this plan — see `bandwidthCeilingDegradesRender`.
1009
+ //
1010
+ // `formsPerHost` is the same flat ceiling every plan carries; it bounds a
1011
+ // collection, not a tier, and the 2026-08-30 decision withdrew the ladder.
1012
+ enterprise: {
1013
+ hostLimit: 200,
1014
+ screensPerHost: UNLIMITED,
1015
+ sharedLayoutsPerHost: UNLIMITED,
1016
+ templatesPerHost: UNLIMITED,
1017
+ storagePerHostMb: 122880,
1018
+ membersPerHost: 500,
1019
+ managersPerOrg: 200,
1020
+ maxManagersPerOrg: 1000,
1021
+ maxMembersPerHost: 2000,
1022
+ bandwidthGb: 3080,
1023
+ formSubmissionsPerMonth: 50000,
1024
+ formsPerHost: FORMS_PER_HOST_CEILING,
1025
+ variablesPerHost: UNLIMITED,
1026
+ functionsPerHost: UNLIMITED,
1027
+ workflowsPerHost: UNLIMITED,
1028
+ workflowRunsPerMonth: 4000000,
1029
+ servicesPerHost: UNLIMITED,
1030
+ redirectsPerHost: UNLIMITED,
1031
+ contactsPerHost: 1000000,
1032
+ // Finite like the campaign allowance beside it, and for the same reason
1033
+ // one-to-one mail is capped on every other tier: a rep with a template
1034
+ // and a list can put thousands of messages onto the platform's sending
1035
+ // reputation in an afternoon. A negotiated agreement whose reps need more
1036
+ // than 2,000 a day says so, and the override is what carries it.
1037
+ crmEmailsPerDay: 2000,
1038
+ emailSendsPerMonth: ENTERPRISE_EMAIL_SENDS_PER_MONTH,
1039
+ actionRunsPerMonth: 2000000,
1040
+ assistCreditsPerMonth: ENTERPRISE_ASSIST_CREDITS_PER_MONTH,
1041
+ apiRequestsPerMonth: 10000000,
1042
+ datasetsPerOrg: 4000,
1043
+ maxDatasetsPerOrg: 10000,
1044
+ recordsPerDataset: UNLIMITED,
1045
+ dataStorageMbPerOrg: 1024000,
1046
+ productsPerHost: UNLIMITED,
1047
+ inventoryLocations: 100,
1048
+ posRegisters: 40,
1049
+ transactionFeePhysicalPct: 0,
1050
+ transactionFeeDigitalPct: 0,
1051
+ marketplaceFeePct: 20,
1052
+ features: {
1053
+ abTesting: true,
1054
+ versioning: true,
1055
+ reusableComponents: true,
1056
+ customDomain: true,
1057
+ customSendingDomain: true,
1058
+ dedicatedSendingDomain: true,
1059
+ removeBranding: true,
1060
+ scheduledPublishing: true,
1061
+ marketplaceSelling: true,
1062
+ aiAssist: true,
1063
+ aiGenerative: true,
1064
+ workflows: true,
1065
+ dataStore: true,
1066
+ videoMedia: true,
1067
+ bookings: true,
1068
+ crm: true,
1069
+ outreach: false,
1070
+ interactions: true,
1071
+ actions: true,
1072
+ webhooks: true,
1073
+ apiAccess: true,
1074
+ siteExport: true,
1075
+ multilingual: true,
1076
+ // Event Calendar stays an add-on purchase, as on every other tier.
1077
+ eventCalendar: false,
1078
+ redirects: true,
1079
+ screenAnalytics: true,
1080
+ mediaCdn: true,
1081
+ marketingOverlays: true,
1082
+ commerce: true,
1083
+ pos: true,
1084
+ storefrontSubscriptions: true,
1085
+ contentGating: true,
1086
+ giftCards: true,
1087
+ productReviews: true,
1088
+ abandonedCart: true,
1089
+ dropshipRouting: true,
1090
+ commerceAnalytics: true,
1091
+ whiteLabel: true,
1092
+ // SSO (AGL-1101) is the Enterprise differentiator — the only plan that
1093
+ // carries it by default. Lower tiers still need a per-org override.
1094
+ ssoEnabled: true
1095
+ }
1096
+ }
1097
+ };
1098
+ /**
1099
+ * The tiers a customer can buy themselves — every plan EXCEPT `enterprise`
1100
+ * (AGL-1118), which is priced per deal and provisioned by staff (AGL-1110).
1101
+ * Any surface that offers plans for sale (the console plan-cards grid, the
1102
+ * marketing pricing table, the plan-change dialog) MUST iterate this rather
1103
+ * than `Object.keys(PLAN_ENTITLEMENTS)`, so a custom-priced tier can never
1104
+ * show a self-serve Upgrade button or a $0 headline price.
1105
+ */ export const SELF_SERVE_PLANS = [
1106
+ 'free',
1107
+ 'starter',
1108
+ 'pro',
1109
+ 'business',
1110
+ 'scale',
1111
+ 'advanced',
1112
+ 'agency'
1113
+ ];
1114
+ /**
1115
+ * Whether the plan has no list price and is quoted per deal (AGL-1118).
1116
+ * `PLAN_PRICING` still carries a row for it — the record is exhaustive by
1117
+ * type — but every figure there is 0/null and means "no list price", NOT
1118
+ * "free". Revenue for such an org comes from `subscription.customMonthlyUsd`.
1119
+ */ export function isCustomPricedPlan(plan) {
1120
+ return plan === 'enterprise';
1121
+ }
1122
+ /** Human plan names — the ONE source every plan badge/label reads. */ export const PLAN_LABELS = {
1123
+ free: 'Free',
1124
+ starter: 'Starter',
1125
+ pro: 'Pro',
1126
+ business: 'Business',
1127
+ scale: 'Scale',
1128
+ advanced: 'Advanced',
1129
+ agency: 'Agency',
1130
+ enterprise: 'Enterprise'
1131
+ };
1132
+ /**
1133
+ * Event Calendar add-on (AGL-145): first-party, Aglyn-supported, $9/mo
1134
+ * **per organization** — cost×1.3 floor honored (mostly Firestore reads).
1135
+ * Enabled via the `eventCalendar` entitlement override at purchase.
1136
+ *
1137
+ * NOT per host, despite what this comment said until AGL-1279. AGL-145
1138
+ * shipped it per host; AGL-526 then made add-ons self-serve and capped this
1139
+ * one at `return 1` in `app/api/billing/addons/route.ts`, and
1140
+ * `resolveOrgEntitlements` flips a single org-level `features.eventCalendar`
1141
+ * boolean from any quantity >= 1. One purchase covers every host in the org.
1142
+ * The marketing copy on /pricing is written against that behaviour.
1143
+ */ export const EVENT_CALENDAR_ADDON_MONTHLY_USD = 9;
1144
+ /**
1145
+ * POS Pro register add-on (AGL-329): $89/mo per extra register/location
1146
+ * (Shopify POS Pro parity). Purchased add-ons land as a per-org
1147
+ * `posRegisters` entitlement override, which
1148
+ * `resolveOrgEntitlements` already applies over the plan default.
1149
+ */ export const POS_REGISTER_ADDON_MONTHLY_USD = 89;
1150
+ /**
1151
+ * The Aglyn AI add-on's credit band (AGL-2896): the credits a month the
1152
+ * add-on ADDS to the plan's `assistCreditsPerMonth` when
1153
+ * `seatAddons.aiAddon` is set. One pool and one meter — the add-on widens
1154
+ * the band the assist meter already draws on rather than opening a second
1155
+ * one, so `resolveAssistCreditBudget`, the overage price and the hard-cap
1156
+ * switch all see one number.
1157
+ *
1158
+ * ## Sized against the add-on's own price
1159
+ *
1160
+ * A credit is `ASSIST_CREDIT_COST_USD` of provider spend, so each band is a
1161
+ * cost figure: 1,000 credits are $1.00. Every band here costs at most 50% of
1162
+ * `PlanPricing.aiAddonMonthlyUsd` at full consumption — Starter $4 of $9,
1163
+ * Pro $9 of $19, Business $19 of $39, Scale $34 of $69, Advanced $49 of
1164
+ * $99, Agency $149 of $299 — which is the same line-margin floor
1165
+ * (`ASSIST_CREDIT_MIN_MARGIN_PCT`) every retail assist rate clears.
1166
+ * `tier-margin-floor.spec.ts` holds the whole tier non-negative with the
1167
+ * add-on band at 100% and the add-on's revenue counted, and pins each band's
1168
+ * share of its price.
1169
+ *
1170
+ * ## Free and Enterprise
1171
+ *
1172
+ * Free is 0: the plan sells no add-on (`aiAddonMonthlyUsd` is null), so a
1173
+ * quantity written onto a Free org adds nothing. Enterprise is finite at
1174
+ * twice Agency's, the rule every Enterprise fallback follows since the
1175
+ * 2026-09-07 decision (AGL-2654) — never `UNLIMITED`, for the reason
1176
+ * `ENTERPRISE_ASSIST_CREDITS_PER_MONTH` gives: an unbounded band reads back
1177
+ * as zero off the wire. Enterprise carries `aiGenerative` in the agreement
1178
+ * and its price is null too, so this figure is what a contract that names
1179
+ * the add-on starts from before its own number is written.
1180
+ */ export const AI_ADDON_CREDITS_PER_MONTH = {
1181
+ free: 0,
1182
+ starter: 4000,
1183
+ pro: 9000,
1184
+ business: 19000,
1185
+ scale: 34000,
1186
+ advanced: 49000,
1187
+ agency: 149000,
1188
+ enterprise: 298000
1189
+ };
1190
+ /**
1191
+ * The generative add-on's name as a person sees it (AGL-2896): the brand,
1192
+ * then "AI". Read from the platform brand by default and from the org's
1193
+ * resolved `productName` where the surface has one, for the reason every
1194
+ * rendered product name is: a self-host operator renames the product by
1195
+ * configuration, and a white-label org must not see ours.
1196
+ */ export const aiAddonName = (brand = PLATFORM_BRAND_NAME)=>`${brand} AI`;
1197
+ /**
1198
+ * Purchase ceilings for the two add-on kinds that have NO per-plan hard max
1199
+ * (AGL-1738). They live here, beside the bands and the resolver they bound,
1200
+ * because they ARE the bound — and until now they were two private literals
1201
+ * in `app/api/billing/addons/route.ts`, seventeen hundred lines and one
1202
+ * project away from the `+` they cap.
1203
+ *
1204
+ * That distance is why AGL-1738 read `resolveOrgEntitlements` as unbounded
1205
+ * and proposed a `maxPosRegistersPerHost` clamp. Do not add one. Seats and
1206
+ * datasets clamp at `checkSeatQuota` / `checkDatasetQuota` because their
1207
+ * plans genuinely refuse to sell past a band (`upgradeRequired` flips and
1208
+ * the answer is a plan change). Registers and extra sites are sold flat
1209
+ * instead: `addonMax()` will sell an org up to these quantities on ANY plan
1210
+ * carrying the feature, so a band-shaped clamp in the resolver would take
1211
+ * back registers the merchant is being invoiced $89/mo each for — the
1212
+ * expensive direction of a wrong clamp, and the reason the add stays open.
1213
+ *
1214
+ * The self-serve route is the only path that writes `seatAddons` from a
1215
+ * purchase, so these are the real ceilings: a plan's band plus at most
1216
+ * `POS_REGISTERS_ADDON_MAX` registers, or plus `EXTRA_HOSTS_ADDON_MAX` sites.
1217
+ * Any console surface that windows a head-count must clear the SUM, not the
1218
+ * band (`registers-card.component.tsx` still windows at 25; AGL-1738).
1219
+ */ export const EXTRA_HOSTS_ADDON_MAX = 100;
1220
+ /** @see EXTRA_HOSTS_ADDON_MAX — the flat register ceiling, per org. */ export const POS_REGISTERS_ADDON_MAX = 50;
1221
+ /**
1222
+ * Flat subscription pricing per tier (AGL-68). Kept beside the entitlements
1223
+ * so price changes ride the same review path; Stripe price ids map to plans
1224
+ * via `STRIPE_PRICE_*` env vars on the billing API routes.
1225
+ */ export const PLAN_PRICING = {
1226
+ free: {
1227
+ basePriceMonthlyUsd: 0,
1228
+ basePriceAnnualMonthlyUsd: 0,
1229
+ extraHostMonthlyUsd: null,
1230
+ extraSeatMonthlyUsd: null,
1231
+ extraCollaboratorMonthlyUsd: null,
1232
+ extraDatasetMonthlyUsd: null,
1233
+ extraDataGbMonthlyUsd: null,
1234
+ extraApiRequestsUsdPer1k: null,
1235
+ extraAssistCreditsUsdPer1k: null,
1236
+ aiAddonMonthlyUsd: null,
1237
+ extraContactsUsdPer1k: null,
1238
+ extraEmailSendsUsdPer1k: null,
1239
+ meteredInfraPassThrough: false
1240
+ },
1241
+ starter: {
1242
+ basePriceMonthlyUsd: 25,
1243
+ basePriceAnnualMonthlyUsd: 16,
1244
+ extraHostMonthlyUsd: 20,
1245
+ extraSeatMonthlyUsd: 5,
1246
+ extraCollaboratorMonthlyUsd: 3,
1247
+ extraDatasetMonthlyUsd: 2,
1248
+ extraDataGbMonthlyUsd: 0.36,
1249
+ extraApiRequestsUsdPer1k: null,
1250
+ // The top of the retail ladder, which Starter JOINS rather than steps
1251
+ // above (AGL-3203): the ladder descends with the tier, and Starter is the
1252
+ // rung below Pro, not a new one over it. Moves in lockstep with the 750
1253
+ // credit band on the Starter entitlements — the band without this rate is
1254
+ // the silent free overage, and this rate without the band is a fee
1255
+ // advertised on a quantity the plan never sold.
1256
+ extraAssistCreditsUsdPer1k: 3,
1257
+ aiAddonMonthlyUsd: 9,
1258
+ extraContactsUsdPer1k: 1,
1259
+ // No email band to be "over" — see `emailSendsPerMonth` on the Starter
1260
+ // entitlements. `emailSendsOverage` returns 0 for any non-positive band,
1261
+ // so a rate here would price volume that can never be computed, which is
1262
+ // the same defect as a rate beside an UNLIMITED band.
1263
+ extraEmailSendsUsdPer1k: null,
1264
+ meteredInfraPassThrough: true
1265
+ },
1266
+ pro: {
1267
+ basePriceMonthlyUsd: 56,
1268
+ basePriceAnnualMonthlyUsd: 39,
1269
+ extraHostMonthlyUsd: 20,
1270
+ extraSeatMonthlyUsd: 4,
1271
+ extraCollaboratorMonthlyUsd: 2,
1272
+ extraDatasetMonthlyUsd: 2,
1273
+ extraDataGbMonthlyUsd: 0.36,
1274
+ extraApiRequestsUsdPer1k: null,
1275
+ extraAssistCreditsUsdPer1k: 3,
1276
+ aiAddonMonthlyUsd: 19,
1277
+ extraContactsUsdPer1k: 0.75,
1278
+ extraEmailSendsUsdPer1k: 2.25,
1279
+ meteredInfraPassThrough: true
1280
+ },
1281
+ business: {
1282
+ basePriceMonthlyUsd: 139,
1283
+ basePriceAnnualMonthlyUsd: 99,
1284
+ extraHostMonthlyUsd: 20,
1285
+ extraSeatMonthlyUsd: 3,
1286
+ extraCollaboratorMonthlyUsd: 1,
1287
+ extraDatasetMonthlyUsd: 1,
1288
+ extraDataGbMonthlyUsd: 0.36,
1289
+ extraApiRequestsUsdPer1k: 0.5,
1290
+ extraAssistCreditsUsdPer1k: 2.75,
1291
+ aiAddonMonthlyUsd: 39,
1292
+ extraContactsUsdPer1k: 0.5,
1293
+ extraEmailSendsUsdPer1k: 2,
1294
+ meteredInfraPassThrough: true
1295
+ },
1296
+ scale: {
1297
+ basePriceMonthlyUsd: 249,
1298
+ basePriceAnnualMonthlyUsd: 179,
1299
+ extraHostMonthlyUsd: 20,
1300
+ extraSeatMonthlyUsd: 2,
1301
+ extraCollaboratorMonthlyUsd: 1,
1302
+ extraDatasetMonthlyUsd: 1,
1303
+ extraDataGbMonthlyUsd: 0.36,
1304
+ extraApiRequestsUsdPer1k: 0.35,
1305
+ extraAssistCreditsUsdPer1k: 2.5,
1306
+ aiAddonMonthlyUsd: 69,
1307
+ extraContactsUsdPer1k: 0.4,
1308
+ extraEmailSendsUsdPer1k: 1.9,
1309
+ meteredInfraPassThrough: true
1310
+ },
1311
+ advanced: {
1312
+ basePriceMonthlyUsd: 399,
1313
+ basePriceAnnualMonthlyUsd: 299,
1314
+ extraHostMonthlyUsd: 20,
1315
+ extraSeatMonthlyUsd: 2,
1316
+ extraCollaboratorMonthlyUsd: 1,
1317
+ extraDatasetMonthlyUsd: 1,
1318
+ extraDataGbMonthlyUsd: 0.36,
1319
+ extraApiRequestsUsdPer1k: 0.2,
1320
+ extraAssistCreditsUsdPer1k: 2.25,
1321
+ aiAddonMonthlyUsd: 99,
1322
+ // FLOORED at the 50% retail margin, not stepped down again. The ladder
1323
+ // above it descends $1.00 → $0.75 → $0.50 → $0.40, and one more step
1324
+ // would have reached $0.25 against a `perContactMonth` cost of $0.20 per
1325
+ // 1,000 — a 20% line margin on a retail price, thinner than the 23% the
1326
+ // infrastructure pass-through earns while being sold as cost recovery.
1327
+ extraContactsUsdPer1k: 0.4,
1328
+ extraEmailSendsUsdPer1k: 1.85,
1329
+ meteredInfraPassThrough: true
1330
+ },
1331
+ // Agency is the one tier whose PRICE moved. It included 100 hosts, 20 TB of
1332
+ // bandwidth and an unbounded form-submission band against $799, which made
1333
+ // it both the worst margin on the ladder and the most underpriced against
1334
+ // the field: Duda charges ~$1,396-1,493/mo for 100 sites, BigCommerce
1335
+ // Enterprise starts at $1,499 and Shopify Plus at $2,300. $1,299 still
1336
+ // undercuts all three for the buyer this architecture is built for.
1337
+ agency: {
1338
+ basePriceMonthlyUsd: 1299,
1339
+ // $1,049/mo is $12,588 a year, which is what the live Stripe yearly price
1340
+ // charges. Stripe prices are immutable, so the constant every surface
1341
+ // quotes has to match the one a customer is actually billed by — a
1342
+ // ratio-derived figure that no price object backs would quote one number
1343
+ // and charge another. It lands within a dollar of holding the previous
1344
+ // 18.8% annual discount across the rise, which is what stops the annual
1345
+ // interval becoming the cheap way past a repricing.
1346
+ basePriceAnnualMonthlyUsd: 1049,
1347
+ extraHostMonthlyUsd: 20,
1348
+ extraSeatMonthlyUsd: 2,
1349
+ extraCollaboratorMonthlyUsd: 1,
1350
+ extraDatasetMonthlyUsd: 1,
1351
+ extraDataGbMonthlyUsd: 0.36,
1352
+ extraApiRequestsUsdPer1k: 0.15,
1353
+ extraAssistCreditsUsdPer1k: 2,
1354
+ aiAddonMonthlyUsd: 299,
1355
+ // $0.40, because the band it meters is now FINITE.
1356
+ //
1357
+ // This row has been wrong in both directions. It shipped $0.20 against an
1358
+ // UNLIMITED band, advertising a fee that could not be charged —
1359
+ // `checkContactQuota` computes `Math.max(0, used - Infinity)`, which is 0
1360
+ // at every usage level (AGL-2439) — and was corrected to `null` to stop
1361
+ // publishing a claim the code could not honour. Bounding the band inverts
1362
+ // the problem: a finite band with no rate is usage past a bound that is
1363
+ // silently free, and the bound achieves nothing.
1364
+ //
1365
+ // $0.40 rather than a further step down the ladder because that is the
1366
+ // 50% retail floor against a `perContactMonth` cost of $0.20 per 1,000 —
1367
+ // the same figure Scale and Advanced carry, and the same floor that moved
1368
+ // Advanced off $0.25.
1369
+ //
1370
+ // It METERS rather than walls: a rate is what makes `checkContactQuota`
1371
+ // return `allowed: true` past the band, so a growing audience is billed
1372
+ // and never dropped. That is what makes bounding this band safe here and
1373
+ // NOT safe on enterprise, whose every rate is the "not for sale"
1374
+ // sentinel.
1375
+ extraContactsUsdPer1k: 0.4,
1376
+ extraEmailSendsUsdPer1k: 1.8,
1377
+ meteredInfraPassThrough: true
1378
+ },
1379
+ // Enterprise (AGL-1118) has NO list price — every figure here is the
1380
+ // "not for sale" sentinel, not a $0 offer (`isCustomPricedPlan`). The org's
1381
+ // real price is the negotiated `subscription.customMonthlyUsd` the
1382
+ // enterprise-billing flow writes (AGL-1110), which `orgListPriceMonthlyUsd`
1383
+ // already prefers over the plan default; a comped enterprise org genuinely
1384
+ // bills $0, and that is what this row then reports. Nulls also mean the
1385
+ // plan sells no self-serve add-on seats — enterprise capacity is unlimited,
1386
+ // so there is nothing to buy.
1387
+ enterprise: {
1388
+ basePriceMonthlyUsd: 0,
1389
+ basePriceAnnualMonthlyUsd: 0,
1390
+ extraHostMonthlyUsd: null,
1391
+ extraSeatMonthlyUsd: null,
1392
+ extraCollaboratorMonthlyUsd: null,
1393
+ extraDatasetMonthlyUsd: null,
1394
+ extraDataGbMonthlyUsd: null,
1395
+ extraApiRequestsUsdPer1k: null,
1396
+ extraAssistCreditsUsdPer1k: null,
1397
+ aiAddonMonthlyUsd: null,
1398
+ extraContactsUsdPer1k: null,
1399
+ extraEmailSendsUsdPer1k: null,
1400
+ meteredInfraPassThrough: false
1401
+ }
1402
+ };
1403
+ /**
1404
+ * The price list a plan's row becomes when a staff COMP is what grants it
1405
+ * (AGL-3034): the list prices kept, and nothing for sale.
1406
+ *
1407
+ * Every other figure on a row prices something sold on top of a subscription —
1408
+ * an add-on item, or usage past a band billed onto that subscription's next
1409
+ * invoice. A comp has no subscription to add an item to or to invoice, so
1410
+ * every one of them is `null` and the pass-through is off. That is `free`'s
1411
+ * reasoning (`meteredInfraPassThrough`: "there is no subscription to hang a
1412
+ * metered item on"), and it turns each band into a wall rather than a line
1413
+ * past which usage is sold to nobody.
1414
+ *
1415
+ * It is also what keeps a comp away from Stripe without a check anyone could
1416
+ * forget: the AI overage charge claims only what `assistMonthOverage` prices,
1417
+ * the usage sweep bills only what these rates price, and a `null` rate prices
1418
+ * zero. A comp that needs more of something gets a quota override beside it,
1419
+ * or is uncapped outright (AGL-3049) — and an uncapped comp reads THIS row
1420
+ * too, so lifting every band sells nothing past any of them.
1421
+ *
1422
+ * Derived from the row rather than written out, so a rate added to
1423
+ * `PlanPricing` later is withheld from a comp on the day it ships.
1424
+ */ function compPricing(pricing) {
1425
+ const withheld = {};
1426
+ for (const [key, value] of Object.entries(pricing)){
1427
+ if (key === 'basePriceMonthlyUsd' || key === 'basePriceAnnualMonthlyUsd') {
1428
+ withheld[key] = value;
1429
+ } else if (typeof value === 'boolean') {
1430
+ withheld[key] = false;
1431
+ } else {
1432
+ withheld[key] = null;
1433
+ }
1434
+ }
1435
+ return withheld;
1436
+ }
1437
+ const PLAN_COMP_PRICING = Object.fromEntries(Object.entries(PLAN_PRICING).map(([plan, pricing])=>[
1438
+ plan,
1439
+ compPricing(pricing)
1440
+ ]));
1441
+ /**
1442
+ * The prices the org's usage and add-ons are sold at (AGL-3034): its
1443
+ * effective plan's row, or that row with nothing for sale when a staff comp
1444
+ * is what grants the plan — see `compPricing`.
1445
+ *
1446
+ * EVERY reader that decides whether something past a band is sold, or what a
1447
+ * purchasable seat costs, reads this rather than indexing `PLAN_PRICING` by
1448
+ * the effective plan. Indexing it directly is how a comp would be quoted a
1449
+ * rate nobody can be invoiced at.
1450
+ */ export function resolvePlanPricing(org) {
1451
+ const plan = resolveEffectivePlan(org);
1452
+ return resolvePlanComp(org) ? PLAN_COMP_PRICING[plan] : PLAN_PRICING[plan];
1453
+ }
1454
+ /**
1455
+ * Subscription states that stop paying for the plan (AGL-247). `past_due`
1456
+ * keeps working as a dunning grace period; these do not.
1457
+ *
1458
+ * `incomplete_expired` is the state a signup reaches when its first payment
1459
+ * never completes — an abandoned SCA challenge, a card that never
1460
+ * authenticated — and Stripe stamps it about a day later. It has charged
1461
+ * nothing and never will, so it belongs here beside `incomplete`, which is the
1462
+ * same subscription an hour earlier. `LIVE_SUBSCRIPTION_STATUSES` in
1463
+ * `org-billing-doc.ts` already excludes it and `DEAD_STATUSES` in
1464
+ * `utils/subscription-period-notice.ts` already names it; this set is the one
1465
+ * that decides both what a workspace GETS and whether we count it as revenue,
1466
+ * so it is the one place the omission cost money in both directions at once.
1467
+ *
1468
+ * THE DENYLIST FORM IS THE HAZARD, and it is why the omission was silent: an
1469
+ * allowlist of live statuses cannot rot when Stripe adds a status, whereas a
1470
+ * denylist of dead ones silently grants the plan — and books the MRR — for
1471
+ * every status nobody has enumerated yet. The three readers below keep the
1472
+ * denylist anyway, because the direction they fail in is not the same as the
1473
+ * live-side question's: an unrecognized status here must not revoke a paying
1474
+ * workspace's features, while `isLiveSubscriptionStatus` refusing to call an
1475
+ * unrecognized status live only ever declines to sell a second subscription.
1476
+ * A new Stripe status therefore has to be added HERE by hand, which is a
1477
+ * standing obligation rather than a property of the code.
1478
+ */ const DEAD_SUBSCRIPTION_STATUSES = new Set([
1479
+ 'canceled',
1480
+ 'unpaid',
1481
+ 'incomplete',
1482
+ 'incomplete_expired'
1483
+ ]);
1484
+ /**
1485
+ * The subscription status entitlement resolution runs on (AGL-1028).
1486
+ *
1487
+ * `subscription` moved to `orgs/{orgId}/billing/stripe`, behind
1488
+ * `canManageOrg()` — but the status word is an ENTITLEMENT input, not a
1489
+ * commercial secret: `resolveEffectivePlan` downgrades a paid plan to free on a
1490
+ * dead subscription, and `resolvePurchasedAddons` stops counting add-ons on
1491
+ * one. Both resolve in the tenant runtime and in console components belonging
1492
+ * to members who cannot read a manager-gated doc, so the webhook mirrors the
1493
+ * bare status string back onto the org doc as `billingStatus`.
1494
+ *
1495
+ * Reads the mirror first, then the inline `subscription` — the fallback is what
1496
+ * keeps entitlements correct for orgs the backfill has not reached yet, and for
1497
+ * any caller still passing a whole billing doc rather than an org doc.
1498
+ */ function subscriptionStatusOf(org) {
1499
+ var _org_subscription;
1500
+ const mirrored = org == null ? void 0 : org.billingStatus;
1501
+ if (typeof mirrored === 'string' && mirrored) return mirrored;
1502
+ return org == null ? void 0 : (_org_subscription = org.subscription) == null ? void 0 : _org_subscription.status;
1503
+ }
1504
+ export function orgSubscriptionState(org) {
1505
+ const status = subscriptionStatusOf(org);
1506
+ if (!status) return 'none';
1507
+ return DEAD_SUBSCRIPTION_STATUSES.has(status) ? 'dead' : 'live';
1508
+ }
1509
+ /**
1510
+ * An instant off a stored value in whichever form it reached this reader: a
1511
+ * Firestore `Timestamp` from either SDK, the `{ seconds }`/`{ _seconds }`
1512
+ * shape a timestamp takes after crossing JSON, a `Date`, or an ISO string.
1513
+ */ function instantOf(value) {
1514
+ var _record_seconds;
1515
+ if (value instanceof Date) {
1516
+ return Number.isNaN(value.getTime()) ? null : value.toISOString();
1517
+ }
1518
+ if (typeof value === 'string') {
1519
+ const millis = Date.parse(value);
1520
+ return Number.isFinite(millis) ? new Date(millis).toISOString() : null;
1521
+ }
1522
+ if (!value || typeof value !== 'object') return null;
1523
+ const toDate = value.toDate;
1524
+ if (typeof toDate === 'function') {
1525
+ const date = toDate.call(value);
1526
+ return date instanceof Date && !Number.isNaN(date.getTime()) ? date.toISOString() : null;
1527
+ }
1528
+ const record = value;
1529
+ const seconds = Number((_record_seconds = record.seconds) != null ? _record_seconds : record._seconds);
1530
+ if (!Number.isFinite(seconds)) return null;
1531
+ // Out of `Date`'s range is Invalid Date, whose `toISOString` throws.
1532
+ const date = new Date(seconds * 1000);
1533
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
1534
+ }
1535
+ function trimmedOrNull(value) {
1536
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
1537
+ }
1538
+ /**
1539
+ * The org's STORED comp (`entitlements.planComp`), whether or not it is in
1540
+ * force, or `null` when there is none (AGL-3034).
1541
+ *
1542
+ * Strict about the one field that grants anything: `plan` must be a paid plan
1543
+ * this build knows. A comp of `free` grants nothing and an unknown plan could
1544
+ * grant anything, so both read as no comp at all — the org then resolves
1545
+ * exactly as it would without one, which is the direction a malformed grant
1546
+ * must fail in. The descriptive fields are narrowed and never block the plan:
1547
+ * a comp whose note or stamp did not read back is still the comp staff wrote.
1548
+ *
1549
+ * `uncapped` is strict in the same direction (AGL-3049): it lifts every cap,
1550
+ * so only a literal `true` does. A `"true"`, a `1` or a map written by hand
1551
+ * reads as a capped comp — the plan still stands, and its bands stay walls.
1552
+ */ export function readOrgPlanComp(org) {
1553
+ const entitlements = org == null ? void 0 : org.entitlements;
1554
+ const raw = entitlements && typeof entitlements === 'object' ? entitlements.planComp : undefined;
1555
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
1556
+ const record = raw;
1557
+ const plan = record['plan'];
1558
+ if (typeof plan !== 'string' || plan === 'free' || !Object.prototype.hasOwnProperty.call(PLAN_ENTITLEMENTS, plan)) {
1559
+ return null;
1560
+ }
1561
+ const reason = record['reason'];
1562
+ return {
1563
+ plan: plan,
1564
+ uncapped: record['uncapped'] === true,
1565
+ reason: isOrgOverrideReasonCode(reason) ? reason : null,
1566
+ note: trimmedOrNull(record['note']),
1567
+ grantedBy: trimmedOrNull(record['grantedBy']),
1568
+ grantedAt: instantOf(record['grantedAt'])
1569
+ };
1570
+ }
1571
+ /**
1572
+ * The comp IN FORCE: the stored comp while no subscription decides the plan,
1573
+ * and `null` otherwise (AGL-3034).
1574
+ *
1575
+ * A live subscription always wins. The comp is not removed by one starting —
1576
+ * nothing but the staff override writes it — so it waits, dormant, and
1577
+ * applies again if that subscription ends. Staff views say so.
1578
+ */ export function resolvePlanComp(org) {
1579
+ if (orgSubscriptionState(org) === 'live') return null;
1580
+ return readOrgPlanComp(org);
1581
+ }
1582
+ /**
1583
+ * Whether an UNCAPPED comp is in force (AGL-3049): every band and quota the
1584
+ * org resolves reads as `UNLIMITED`, whatever its plan row and its per-org
1585
+ * overrides say — see `withEveryCapLifted`.
1586
+ *
1587
+ * In force only, like the comp itself. A live subscription decides the plan
1588
+ * and its bands, so a dormant uncapped comp lifts nothing, and a canceled one
1589
+ * lifts every cap again the moment the subscription ends.
1590
+ *
1591
+ * What it does NOT lift is anything that is not a plan band: the fee
1592
+ * percentages (`PRICE_ENTITLEMENT_KEYS`) are prices, and the platform's own
1593
+ * guards live outside the entitlements map and are not the plan's to give
1594
+ * away — the form-submission and bandwidth abuse ceilings (which read an
1595
+ * unlimited band as `FORM_ABUSE_CEILING_UNLIMITED` and
1596
+ * `BANDWIDTH_ABUSE_CEILING_UNLIMITED`), the flat per-site caps on webhooks,
1597
+ * actions and collection entries, the email send pacing, the assist runaway
1598
+ * message cap, an operator's explicit spend ceiling, and staff pauses and
1599
+ * lockdowns. It never sells anything either: the pricing stays the comp's,
1600
+ * with every rate withheld, so an unbounded band is still billed nothing.
1601
+ *
1602
+ * The lifted value is `UNLIMITED` in process and nowhere else. Every route
1603
+ * that sends a band off-process sends a finite number with an explicit flag,
1604
+ * or `null` with a documented meaning, because `JSON.stringify(Infinity)` is
1605
+ * `null` — and the comp itself is stored and sent as a boolean.
1606
+ */ export function isUncappedPlanComp(org) {
1607
+ var _resolvePlanComp;
1608
+ return ((_resolvePlanComp = resolvePlanComp(org)) == null ? void 0 : _resolvePlanComp.uncapped) === true;
1609
+ }
1610
+ /**
1611
+ * How a staff chip names a stored comp (AGL-3049): the plan, then whether it
1612
+ * is uncapped and whether a live subscription holds it dormant —
1613
+ * `Enterprise (uncapped)`, `Pro (dormant)`, `Agency (uncapped, dormant)`.
1614
+ *
1615
+ * One reading of the terms, so the orgs list, the org summary and the
1616
+ * override dialog cannot describe the same grant differently. `name` is the
1617
+ * plan as the surface already writes plans: the label by default, the stored
1618
+ * key on the surfaces whose other chips show keys.
1619
+ */ export function planCompLabel(comp, inForce, name = PLAN_LABELS[comp.plan]) {
1620
+ const terms = [
1621
+ comp.uncapped ? 'uncapped' : null,
1622
+ inForce ? null : 'dormant'
1623
+ ].filter(Boolean).join(', ');
1624
+ return terms ? `${name} (${terms})` : name;
1625
+ }
1626
+ /**
1627
+ * A stored comp as a sentence names it (AGL-3049): `uncapped Enterprise
1628
+ * comp`, `Pro comp`. Shared by the plan description and the override route's
1629
+ * account of what it changed.
1630
+ */ export function planCompPhrase(comp) {
1631
+ return `${comp.uncapped ? 'uncapped ' : ''}${PLAN_LABELS[comp.plan]} comp`;
1632
+ }
1633
+ /** "A Pro comp", "An uncapped Enterprise comp": the article a phrase takes. */ function withArticle(phrase) {
1634
+ return `${/^[aeiou]/i.test(phrase) ? 'An' : 'A'} ${phrase}`;
1635
+ }
1636
+ /**
1637
+ * The org's plan, described rather than only resolved (AGL-3034) — the ONE
1638
+ * reading the staff override route answers with and the staff override
1639
+ * dialog renders, so the response and the dialog cannot tell staff two
1640
+ * different stories about the same document.
1641
+ */ export function describeOrgPlan(org) {
1642
+ const subscription = orgSubscriptionState(org);
1643
+ const status = subscriptionStatusOf(org);
1644
+ const stored = org == null ? void 0 : org.plan;
1645
+ const storedPlan = typeof stored === 'string' && Object.prototype.hasOwnProperty.call(PLAN_ENTITLEMENTS, stored) ? stored : null;
1646
+ const comp = readOrgPlanComp(org);
1647
+ const compInForce = comp !== null && subscription !== 'live';
1648
+ const decidedBy = subscription === 'live' ? 'subscription' : compInForce ? 'comp' : subscription === 'dead' ? 'lapsed' : storedPlan && storedPlan !== 'free' ? 'stored-plan' : 'no-plan';
1649
+ return {
1650
+ effectivePlan: resolveEffectivePlan(org),
1651
+ storedPlan,
1652
+ subscriptionStatus: typeof status === 'string' && status ? status : null,
1653
+ subscription,
1654
+ comp,
1655
+ compInForce,
1656
+ uncapped: compInForce && (comp == null ? void 0 : comp.uncapped) === true,
1657
+ decidedBy
1658
+ };
1659
+ }
1660
+ /**
1661
+ * One sentence for staff saying what decides the plan and what a plan set in
1662
+ * the override dialog would do (AGL-3034). Shared by the route's response and
1663
+ * the dialog, for the reason `describeOrgPlan` is.
1664
+ *
1665
+ * A comp in force also says whether its bands are walls or lifted
1666
+ * (AGL-3049), because that is the question a comped workspace refused at a
1667
+ * band brings to staff.
1668
+ */ export function orgPlanDescriptionSentence(description) {
1669
+ var _description_subscriptionStatus;
1670
+ const label = (plan)=>plan ? PLAN_LABELS[plan] : 'no plan';
1671
+ const status = (_description_subscriptionStatus = description.subscriptionStatus) != null ? _description_subscriptionStatus : 'none';
1672
+ const dormant = description.comp && !description.compInForce ? ` ${withArticle(planCompPhrase(description.comp))} is stored and ` + 'dormant: it takes effect if the subscription ends.' : '';
1673
+ switch(description.decidedBy){
1674
+ case 'subscription':
1675
+ return `The live subscription (${status}) decides the plan: ` + `${label(description.effectivePlan)}. Stripe's next subscription ` + `event rewrites a plan set here.${dormant}`;
1676
+ case 'comp':
1677
+ return `${label(description.effectivePlan)} is in force as ` + `${description.uncapped ? 'an uncapped' : 'a'} staff comp, ` + (description.subscription === 'dead' ? `because the subscription is ${status}. ` : 'because no subscription pays for this workspace. ') + (description.uncapped ? 'It bills nothing, every band and quota reads as unlimited, ' : 'It bills nothing, every band is a hard limit that a quota ' + 'override can raise, ') + 'and it lasts until it is removed or a live subscription starts.';
1678
+ case 'lapsed':
1679
+ return `The subscription is ${status}, so this workspace resolves as ` + `${label('free')}` + (description.storedPlan && description.storedPlan !== 'free' ? ` — the stored ${label(description.storedPlan)} grants nothing` : '') + '. A plan set here is written as a staff comp.';
1680
+ case 'stored-plan':
1681
+ return `No subscription: the plan stored directly, ` + `${label(description.storedPlan)}, decides it. A plan set here is ` + 'written as a staff comp.';
1682
+ default:
1683
+ return `No subscription and no paid plan, so this workspace resolves as ` + `${label('free')}. A plan set here is written as a staff comp.`;
1684
+ }
1685
+ }
1686
+ /**
1687
+ * Whether the org is actually being charged for its plan right now.
1688
+ *
1689
+ * This is deliberately stricter than `resolveEffectivePlan`: entitlement
1690
+ * asks "what does this org get", revenue asks "who is paying us". A staff
1691
+ * plan override (`/admin/orgs`) never writes `subscription` — before AGL-3034
1692
+ * it wrote `plan`, and now it writes a comp — so a comped or dark-launched
1693
+ * org resolves to a paid plan while billing nothing, and counting it inflates
1694
+ * MRR (AGL-925). No subscription mirror means no Stripe subscription means no
1695
+ * revenue.
1696
+ *
1697
+ * A comp never reaches this answer: it reads the stored `plan` and the
1698
+ * status, never `entitlements.planComp`, and a comp is only ever in force
1699
+ * where the status is dead or absent — both of which answer false here.
1700
+ *
1701
+ * `past_due` counts: Stripe is still retrying and the plan is still owed.
1702
+ */ export function isBillingSubscription(org) {
1703
+ const plan = org == null ? void 0 : org.plan;
1704
+ if (!plan || plan === 'free' || !(plan in PLAN_ENTITLEMENTS)) return false;
1705
+ const status = subscriptionStatusOf(org);
1706
+ if (!status) return false;
1707
+ return !DEAD_SUBSCRIPTION_STATUSES.has(status);
1708
+ }
1709
+ /**
1710
+ * The org's monthly LIST price in USD — plan base plus every purchased add-on
1711
+ * (manager/collaborator seats, datasets, extra hosts, POS registers, Event
1712
+ * Calendar), BEFORE any applied discount, and 0 for anyone not actually
1713
+ * billing. This is the sticker price the plan + add-ons would bill at.
1714
+ *
1715
+ * Annual subscriptions contribute their per-month equivalent
1716
+ * (`basePriceAnnualMonthlyUsd`), not the month-to-month price, so a mixed
1717
+ * book does not read high by the size of the annual discount. Add-ons bill
1718
+ * per-month (annual variants are ×12 with no discount), so each contributes
1719
+ * its monthly price on either interval.
1720
+ *
1721
+ * Pricing-v3 fix (2026-07): previously omitted the host, POS-register, and
1722
+ * Event-Calendar add-ons, understating the total for every org carrying them.
1723
+ *
1724
+ * AGL-1105: split out of `orgMonthlyRevenueUsd`, which now subtracts an
1725
+ * applied per-org discount on top of this. Callers wanting the pre-discount
1726
+ * sticker (the margin guardrail, a "what would this bill at" quote) use this;
1727
+ * callers wanting the actual money collected use `orgMonthlyRevenueUsd`.
1728
+ */ export function orgListPriceMonthlyUsd(org) {
1729
+ var _ref;
1730
+ var _org_subscription, _org_subscription1;
1731
+ if (!isBillingSubscription(org)) return 0;
1732
+ // Custom enterprise price (AGL-1110): a negotiated amount is the list price
1733
+ // outright — it already reflects the whole deal, so it overrides the plan
1734
+ // default AND the seat/host add-on math (those are folded into the quote).
1735
+ // Stored monthly-normalized, so it is used as-is.
1736
+ const custom = org == null ? void 0 : (_org_subscription = org.subscription) == null ? void 0 : _org_subscription.customMonthlyUsd;
1737
+ if (typeof custom === 'number' && custom > 0) {
1738
+ return Math.round(custom * 100) / 100;
1739
+ }
1740
+ const pricing = PLAN_PRICING[org == null ? void 0 : org.plan];
1741
+ if (!pricing) return 0;
1742
+ const annual = (org == null ? void 0 : (_org_subscription1 = org.subscription) == null ? void 0 : _org_subscription1.interval) === 'year';
1743
+ const addons = (_ref = org == null ? void 0 : org.seatAddons) != null ? _ref : {};
1744
+ const seats = (quantity, price)=>Math.max(0, quantity != null ? quantity : 0) * (price != null ? price : 0);
1745
+ return (annual ? pricing.basePriceAnnualMonthlyUsd : pricing.basePriceMonthlyUsd) + seats(addons.managers, pricing.extraSeatMonthlyUsd) + seats(addons.members, pricing.extraCollaboratorMonthlyUsd) + seats(addons.datasets, pricing.extraDatasetMonthlyUsd) + seats(addons.hosts, pricing.extraHostMonthlyUsd) + seats(addons.posRegisters, POS_REGISTER_ADDON_MONTHLY_USD) + seats(addons.eventCalendar, EVENT_CALENDAR_ADDON_MONTHLY_USD) + // A toggle, not a quantity: one purchase covers the org, so a stored
1746
+ // quantity above one bills once.
1747
+ seats(aiAddonUnits(addons), pricing.aiAddonMonthlyUsd);
1748
+ }
1749
+ /**
1750
+ * How many Aglyn AI add-ons a purchase map counts as: 0 or 1 (AGL-2896).
1751
+ * The add-on is org-wide like Event Calendar, so a quantity above one — a
1752
+ * hand edit, a doubled webhook item — is one purchase, and a non-number,
1753
+ * `NaN` or a negative is none.
1754
+ */ export function aiAddonUnits(addons) {
1755
+ var _ref;
1756
+ const quantity = Number((_ref = addons == null ? void 0 : addons.aiAddon) != null ? _ref : 0);
1757
+ return Number.isFinite(quantity) && quantity >= 1 ? 1 : 0;
1758
+ }
1759
+ /**
1760
+ * A list price net of a discount (AGL-1105). Percentage discounts scale the
1761
+ * whole bill; fixed discounts subtract a flat USD amount. Never below 0, and
1762
+ * a percentage is clamped to 0–100. Prefers `percentOff` when both are
1763
+ * somehow set (Stripe coupons carry only one). Rounds to whole cents.
1764
+ */ export function applyDiscountUsd(listPriceUsd, discount) {
1765
+ if (!(listPriceUsd > 0) || !discount) return Math.max(0, listPriceUsd);
1766
+ if (typeof discount.percentOff === 'number') {
1767
+ const pct = Math.min(100, Math.max(0, discount.percentOff));
1768
+ return Math.round(listPriceUsd * (1 - pct / 100) * 100) / 100;
1769
+ }
1770
+ if (typeof discount.amountOffUsd === 'number') {
1771
+ return Math.max(0, Math.round((listPriceUsd - discount.amountOffUsd) * 100) / 100);
1772
+ }
1773
+ return listPriceUsd;
1774
+ }
1775
+ /**
1776
+ * Whether an org should read as **"Enterprise"** everywhere the plan is shown.
1777
+ * Three ways to qualify:
1778
+ * - **`plan === 'enterprise'`** — the real plan (AGL-1118). This is the path
1779
+ * every new enterprise org takes, and the only one that also grants
1780
+ * Enterprise *entitlements*; the two below are display-only overlays on a
1781
+ * lower base plan;
1782
+ * - a billing org on a **negotiated custom price** (`customMonthlyUsd > 0`) —
1783
+ * a paying enterprise deal still parked on its pre-AGL-1118 base plan; or
1784
+ * - an explicit **`org.enterprise`** marker — a *comped* enterprise account,
1785
+ * 100%-discounted so it collects $0 while infra cost is still metered.
1786
+ *
1787
+ * ⚠️ This function answers ONE question — "does this org read as Enterprise" —
1788
+ * and it is not the same question as "what does this org get". Only the first
1789
+ * bullet grants anything: `resolveEffectivePlan` reads `org.plan` and nothing
1790
+ * else, so `org.enterprise` and `customMonthlyUsd` are read NOWHERE but here.
1791
+ * A comped or custom-priced org resolves its BASE plan's entitlements, and
1792
+ * gets Enterprise capability through a per-org `entitlements` override — see
1793
+ * `ssoEnabled`, whose docblock calls that "how enterprise orgs provisioned
1794
+ * before that plan existed still get it".
1795
+ *
1796
+ * This paragraph replaces a parenthetical claiming the comped marker carried
1797
+ * "full Enterprise capability + SSO" (AGL-2297). It never did, and the billing
1798
+ * page believed it: the Enterprise card ticked five entitlements against any
1799
+ * org this function marked, so a comped org whose overrides granted SSO but
1800
+ * not white-label was shown a green tick for full white-label while the
1801
+ * branding card correctly refused it.
1802
+ *
1803
+ * The last two are kept for orgs provisioned before `enterprise` was a plan;
1804
+ * migrating such an org to `plan: 'enterprise'` is the clean end state and
1805
+ * changes nothing about how it reads here. List-priced orgs with none of the
1806
+ * three are not enterprise.
1807
+ *
1808
+ * A staff comp in force names the plan instead of the stored one (AGL-3034),
1809
+ * exactly as it does for entitlements: a workspace comped to Enterprise reads
1810
+ * as Enterprise, and one comped to a lower tier does not read as Enterprise
1811
+ * because of a plan stored before the comp. The explicit marker still counts.
1812
+ */ export function isEnterpriseOrg(org) {
1813
+ var _ref;
1814
+ var _org_subscription;
1815
+ const comp = resolvePlanComp(org);
1816
+ if (comp) return comp.plan === 'enterprise' || (org == null ? void 0 : org.enterprise) === true;
1817
+ if ((org == null ? void 0 : org.plan) === 'enterprise') return true;
1818
+ if ((org == null ? void 0 : org.enterprise) === true) return true;
1819
+ return isBillingSubscription(org) && ((_ref = org == null ? void 0 : (_org_subscription = org.subscription) == null ? void 0 : _org_subscription.customMonthlyUsd) != null ? _ref : 0) > 0;
1820
+ }
1821
+ /**
1822
+ * Human label shown for `enterprise` in plan badges. Kept as a named export
1823
+ * for the surfaces that label a custom-priced org whose stored `plan` is still
1824
+ * a lower tier; a real `plan: 'enterprise'` org gets the same string from
1825
+ * `PLAN_LABELS`.
1826
+ */ export const ENTERPRISE_PLAN_LABEL = PLAN_LABELS.enterprise;
1827
+ /**
1828
+ * The org's monthly recurring revenue in USD — its LIST price
1829
+ * (`orgListPriceMonthlyUsd`) net of any applied per-org discount
1830
+ * (`org.discount`, AGL-1105), and 0 for anyone who is not actually billing. A
1831
+ * comped enterprise deal genuinely collects less, so the discount belongs in
1832
+ * MRR; the pre-discount sticker stays available as `orgListPriceMonthlyUsd`.
1833
+ */ export function orgMonthlyRevenueUsd(org) {
1834
+ const listPrice = orgListPriceMonthlyUsd(org);
1835
+ if (listPrice <= 0) return 0;
1836
+ return applyDiscountUsd(listPrice, org == null ? void 0 : org.discount);
1837
+ }
1838
+ /**
1839
+ * Stripe's standard processing fee on Aglyn's OWN subscription charges
1840
+ * (AGL-1108) — 2.9% + $0.30 per charge. This is the fee Aglyn pays Stripe to
1841
+ * collect a subscription; it is DISTINCT from the storefront platform fee
1842
+ * (`transactionFee*Pct`, the Connect `application_fee`) that a seller's own
1843
+ * Stripe pays on their storefront sales. Revenue/margin should be reasoned on
1844
+ * the NET figure, not gross.
1845
+ */ export const STRIPE_PROCESSOR_FEE_PCT = 0.029;
1846
+ export const STRIPE_PROCESSOR_FEE_FIXED_USD = 0.3;
1847
+ /**
1848
+ * A gross monthly USD amount net of Stripe's processing fee. Annual
1849
+ * subscriptions are charged once per year, so the fixed 30¢ amortizes to
1850
+ * $0.30/12 per month; monthly subscriptions pay it every month. Never
1851
+ * returns below 0.
1852
+ */ export function netOfProcessorFee(grossMonthlyUsd, annual = false) {
1853
+ if (!(grossMonthlyUsd > 0)) return 0;
1854
+ const fixedPerMonth = annual ? STRIPE_PROCESSOR_FEE_FIXED_USD / 12 : STRIPE_PROCESSOR_FEE_FIXED_USD;
1855
+ const net = grossMonthlyUsd * (1 - STRIPE_PROCESSOR_FEE_PCT) - fixedPerMonth;
1856
+ return Math.max(0, Math.round(net * 100) / 100);
1857
+ }
1858
+ /**
1859
+ * The org's monthly recurring revenue NET of Stripe's processing fee
1860
+ * (AGL-1108) — what Aglyn actually keeps before infra COGS, and the correct
1861
+ * base for margin/discount reasoning. Gross is `orgMonthlyRevenueUsd`; this
1862
+ * subtracts ~2.9%+30¢, interval-aware. 0 for anyone not actually billing.
1863
+ */ export function orgNetMonthlyRevenueUsd(org) {
1864
+ var _org_subscription;
1865
+ const gross = orgMonthlyRevenueUsd(org);
1866
+ if (gross <= 0) return 0;
1867
+ return netOfProcessorFee(gross, (org == null ? void 0 : (_org_subscription = org.subscription) == null ? void 0 : _org_subscription.interval) === 'year');
1868
+ }
1869
+ /**
1870
+ * Net-margin floor for discount underwriting (AGL-1105). Mirrors the
1871
+ * Enterprise-Pricing-Calculator's ~75% gross-margin target: a discounted
1872
+ * subscription should still keep ≥75% of its net (post-processor-fee) revenue
1873
+ * after infrastructure COGS. Below the floor a discount is warned; well below
1874
+ * it is blocked without explicit sign-off.
1875
+ *
1876
+ * WHAT THIS IS A FLOOR ON (AGL-1930): net revenue less INFRASTRUCTURE COGS,
1877
+ * and nothing else. It is a contribution margin — see `MARGIN_SCOPE_NOTE`,
1878
+ * which is the sentence every staff surface quoting this number has to print
1879
+ * beside it.
1880
+ *
1881
+ * Read as a dollar guard it says: net revenue must be at least
1882
+ * `cogs / (1 − floor)` = **4 × COGS**, and blocks below
1883
+ * `cogs / (1 − floor + band)` ≈ **2.86 × COGS**. That restatement is the point
1884
+ * of AGL-1930 — the percentage is scale-invariant, so the only thing that
1885
+ * moves the real guard line in dollars is the COGS figure fed into it, and on
1886
+ * production today that figure is `INFRA_COGS_PER_SITE_USD × sites` for
1887
+ * **every** org (14 of 14 usage rollups across all 6 orgs, measured
1888
+ * 2026-08-24). Re-tuning this constant therefore re-tunes a multiplier on the
1889
+ * flat floor, not on anything measured. Calibrate the floor first.
1890
+ */ export const NET_MARGIN_FLOOR_PCT = 0.75;
1891
+ /**
1892
+ * How far under the floor a discount may sit and still only WARN (AGL-1105).
1893
+ * Within `[floor − band, floor)` staff see a caution; below `floor − band`
1894
+ * the guardrail blocks unless overridden. 10 points, matching the
1895
+ * calculator's soft/hard split.
1896
+ */ export const NET_MARGIN_WARN_BAND_PCT = 0.1;
1897
+ /**
1898
+ * What every "net margin" figure in the staff console actually measures
1899
+ * (AGL-1930).
1900
+ *
1901
+ * `checkDiscountMargin`, the enterprise quote card and the coupon badge all
1902
+ * compute `(net revenue − infrastructure COGS) / net revenue`. Support,
1903
+ * acquisition and overhead are not in the model anywhere — no meter records
1904
+ * them and no constant estimates them — so "net margin 78%" is a CONTRIBUTION
1905
+ * margin, and a staff member signing a deal on it is reading a ceiling, not a
1906
+ * profit.
1907
+ *
1908
+ * One exported string rather than three literals: the three surfaces that
1909
+ * print a margin already drifted apart once (AGL-1134), and a caveat that is
1910
+ * true on one screen and absent on the next is worse than none.
1911
+ */ export const MARGIN_SCOPE_NOTE = 'Contribution margin: net revenue less infrastructure COGS only. ' + 'Support, customer acquisition (CAC) and overhead are not in this figure.';
1912
+ /**
1913
+ * The `ok` / `warn` / `block` band a cost-recovery margin falls in (AGL-1930).
1914
+ *
1915
+ * Extracted so `NET_MARGIN_FLOOR_PCT` and `NET_MARGIN_WARN_BAND_PCT` have ONE
1916
+ * reader. The staff org page's enterprise quote card had `>= 0.75` and
1917
+ * `>= 0.65` written into its JSX, so re-tuning either constant — the whole
1918
+ * subject of AGL-1930 — would have moved the discount guardrail and left that
1919
+ * card rating deals against the old numbers, silently and with money on the
1920
+ * line.
1921
+ */ export function netMarginRating(marginPct) {
1922
+ return marginPct >= NET_MARGIN_FLOOR_PCT ? 'ok' : marginPct >= NET_MARGIN_FLOOR_PCT - NET_MARGIN_WARN_BAND_PCT ? 'warn' : 'block';
1923
+ }
1924
+ /**
1925
+ * Per-site monthly infrastructure COGS in USD (AGL-1105). The
1926
+ * Enterprise-Pricing-Calculator's ~$2/site figure — Firebase + Vercel at
1927
+ * cost — charged against every host the org runs. The margin guardrail
1928
+ * subtracts this from net revenue before rating a discount.
1929
+ */ export const INFRA_COGS_PER_SITE_USD = 2;
1930
+ /**
1931
+ * Platform markup on passed-through infrastructure costs (AGL-41).
1932
+ *
1933
+ * DEFINED HERE, not in `apps/console/utils/usage-metering.ts`, and re-exported
1934
+ * from there — the same move AGL-2155 made for `pageViewsFromBandwidthGb`, and
1935
+ * for the same reason (AGL-2194). The published `/pricing` pass-through table
1936
+ * states both a cost column and a "you pay (+30%)" column, and
1937
+ * `tools/marketing/build-pricing-tables.mts` must generate both from code so
1938
+ * `npm run check:pricing-tables` can fail on drift. That generator resolves no
1939
+ * `@aglyn/*` path alias, so it can import `plan-entitlements.ts` and nothing
1940
+ * under `apps/console`. A constant the published page depends on must live
1941
+ * where the page's generator can read it.
1942
+ *
1943
+ * The console keeps importing it from `usage-metering` unchanged.
1944
+ */ export const METERED_MARKUP = 1.3;
1945
+ /**
1946
+ * Monthly unit costs for the meters the rollup already records (AGL-1134).
1947
+ *
1948
+ * These are OUR costs, not prices — the org is billed at cost × 1.30
1949
+ * (`METERED_MARKUP`, AGL-41). Storage/page-view/form rates match
1950
+ * `METERED_UNIT_RATES_USD` in the metering module on purpose: two files
1951
+ * disagreeing about what a page view costs is exactly the drift this function
1952
+ * exists to remove.
1953
+ *
1954
+ * The three added here are operator-tuned estimates of the same quality as
1955
+ * the existing three — Firestore-backed dataset storage is priced well above
1956
+ * object storage because it is, and API requests and contacts are priced off
1957
+ * the reads behind them. Treat every operator-tuned figure here as
1958
+ * provisional until validated against a real Firebase + Vercel invoice month;
1959
+ * AGL-1134 says so out loud rather than implying the numbers are derived.
1960
+ * `perEmailSend` is the exception and carries its own basis below.
1961
+ *
1962
+ * ## Three of these are shared with the billed table. The rest are NOT.
1963
+ *
1964
+ * `storagePerGbMonth`, `perPageView` and `perFormSubmission` are the metered
1965
+ * infrastructure pass-through, and the customer is charged those figures
1966
+ * times `METERED_MARKUP`. They therefore appear in `METERED_UNIT_RATES_USD`
1967
+ * as well, with identical values, and must be changed in both places at once.
1968
+ *
1969
+ * Everything else here is a cost-model input ONLY. `dataStoragePerGbMonth`,
1970
+ * `perApiRequest`, `perContactMonth` and `perEmailSend` price what an org
1971
+ * costs us; what an org is CHARGED on those axes is a retail rate on
1972
+ * `PLAN_PRICING` that descends with the tier and is not derived from cost.
1973
+ * Adding one of them to `METERED_UNIT_RATES_USD` would put it inside the
1974
+ * "at cost + 30%" claim, which is true of exactly three meters.
1975
+ *
1976
+ * The first three were corrected 2026-08-09 (AGL-1280) — storage $0.03 →
1977
+ * $0.026 and form submissions $0.0005 → $0.00005 — and `perPageView` was
1978
+ * re-pegged 2026-09-09 (AGL-2711), from $0.0001 to $0.00016153846. All three
1979
+ * MUST be changed here and in `METERED_UNIT_RATES_USD` together. See that
1980
+ * table for each figure's basis; it is the one a customer is billed against,
1981
+ * so it is the one that carries the working.
1982
+ *
1983
+ * The page-view basis is no longer an open gap. The rate priced a 627 KB page
1984
+ * against one that measured far more for weeks; the weight reduction that was
1985
+ * preferred over a re-peg has now shipped, the page was re-measured at
1986
+ * 976.1 KB, and the rate is pegged to a 1012.8 KB basis that sits ABOVE it.
1987
+ * `npm run check:page-view-rate` holds that peg, and now fails if the basis
1988
+ * ever drops back below what a published page measures.
1989
+ *
1990
+ * Raising the two here raises measured COGS, which changes no guardrail
1991
+ * verdict: `INFRA_COGS_PER_SITE_USD × sites` is the floor, and it still
1992
+ * outruns measured cost by orders of magnitude at real traffic.
1993
+ */ export const ORG_COGS_UNIT_RATES_USD = {
1994
+ storagePerGbMonth: 0.026,
1995
+ /**
1996
+ * ⛔ HALF OF A PAIR — see ESTIMATED_PAGE_TRANSFER_BYTES, which states the
1997
+ * same page in bytes as this states it in dollars. Both are calibrated
1998
+ * against a 1012.8 KB page and neither may move alone: their quotient is
1999
+ * the cost of a gigabyte, and that is what every bandwidthGb band on the
2000
+ * ladder was sized against.
2001
+ */ perPageView: 0.00016153846,
2002
+ perFormSubmission: 0.00005,
2003
+ /** Firestore-backed dataset bytes — an order pricier than object storage. */ dataStoragePerGbMonth: 0.18,
2004
+ perApiRequest: 0.000002,
2005
+ perContactMonth: 0.0002,
2006
+ /**
2007
+ * One delivered email, whatever produced it — $0.90 per 1,000.
2008
+ *
2009
+ * Unlike its five neighbours this is not an operator-tuned estimate. It is
2010
+ * the provider's own blended rate at the tier above the account's current
2011
+ * one ($90 / 100,000) and also the marginal overage rate on both that tier
2012
+ * and the current one, so it does not fall as volume grows and is the
2013
+ * conservative figure at any usage the platform can reach.
2014
+ *
2015
+ * It prices EVERY send on the cost meter — campaigns, receipts, invites,
2016
+ * password resets — because every one of them is a message the provider
2017
+ * charges for. The band on the plan and the retail overage rate beside it
2018
+ * both concern a smaller quantity; this is the whole bill.
2019
+ */ perEmailSend: 0.0009,
2020
+ /**
2021
+ * One workflow or action run — $12 per million.
2022
+ *
2023
+ * Derived from the run paths at list prices (2026-09-07), because nothing
2024
+ * measurable exists yet: runs execute inside the Vercel request that fired
2025
+ * the event, not in a function of their own, so Cloud Logging carries no
2026
+ * per-run entry to count, and Firestore's operation counts are platform-wide
2027
+ * and cannot be attributed to a run. Over the 14 days to 2026-09-07 the
2028
+ * project logged no run at all.
2029
+ *
2030
+ * The ledger, for an event that executes one workflow on a site holding a
2031
+ * handful of functions, variables and workflows (`runEventWorkflows`,
2032
+ * `runEventActions` and the inbound-webhook run are the same shape):
2033
+ *
2034
+ * - reads: the triggered query, the workflow map, the host → org lookup,
2035
+ * the run counter, the functions and the variables — about 12, at
2036
+ * nam5's $0.06 per 100,000 = $0.0000072
2037
+ * - writes: the activity row and the counter increment — 2, at $0.18 per
2038
+ * 100,000 = $0.0000036
2039
+ * - compute: a few milliseconds of active CPU and memory on Fluid compute
2040
+ * for the expression evaluation, about $0.000001
2041
+ *
2042
+ * $0.0000118, carried as $0.000012. It scales with the catalog a site holds
2043
+ * — every `limit(100)` read above is a query that costs one read per
2044
+ * document returned — and a step that writes a dataset record or calls a
2045
+ * webhook adds to it, so this is the typical figure, not the ceiling.
2046
+ *
2047
+ * It is a COST, not a price: no plan bills runs, and none refuses past the
2048
+ * band on a metered plan. It exists so a tier's run bands reach
2049
+ * `orgMonthlyCogsUsd` and `tier-margin-floor.spec.ts` — Agency's 3,000,000
2050
+ * runs are $36 a month that read as nothing while this was absent.
2051
+ */ perRun: 0.000012
2052
+ };
2053
+ /**
2054
+ * One org's monthly infrastructure COGS — the single cost model (AGL-1134).
2055
+ *
2056
+ * Replaces two things that had drifted apart: the discount guardrail read the
2057
+ * rollup's `costUsd`, and the staff MRR views read the same field separately,
2058
+ * so neither could be changed without silently changing the other's meaning.
2059
+ *
2060
+ * `costUsd` was also incomplete. The rollup records `dataStorageMb`,
2061
+ * `apiRequests` and `contactsCount`, and `estimateMonthlyUsageCost` prices
2062
+ * none of them — it sums storage, page views and form submissions only. So
2063
+ * the "measured" cost omitted three meters the platform was already at the
2064
+ * trouble of recording.
2065
+ *
2066
+ * The flat per-site figure stays a FLOOR rather than a fallback: measured
2067
+ * cost wins only when it exceeds the estimate. A rollup that has not run, or
2068
+ * an org with genuinely tiny usage, must not make the guardrail more generous
2069
+ * — that is the one direction that costs money.
2070
+ *
2071
+ * Measured against production 2026-07-30, that floor is doing all the work:
2072
+ * the largest real org's `costUsd` was **$0.0000054** against a $4.00 floor
2073
+ * for two sites. Five orders of magnitude. So this function changes no verdict
2074
+ * today, and will not until there is real traffic — which is the honest
2075
+ * reason to build it now, while nothing depends on the answer.
2076
+ */ export function orgMonthlyCogsUsd(rollup, siteCount) {
2077
+ var _ref;
2078
+ const num = (value)=>{
2079
+ const parsed = Number(value != null ? value : 0);
2080
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
2081
+ };
2082
+ const rates = ORG_COGS_UNIT_RATES_USD;
2083
+ const breakdown = {
2084
+ storage: num(rollup == null ? void 0 : rollup.storageGb) * rates.storagePerGbMonth,
2085
+ pageViews: num(rollup == null ? void 0 : rollup.pageViews) * rates.perPageView,
2086
+ formSubmissions: num(rollup == null ? void 0 : rollup.formSubmissions) * rates.perFormSubmission,
2087
+ // Megabytes on the doc, gigabytes in the rate — the unit mismatch is in
2088
+ // the stored field name, so convert here rather than in each caller.
2089
+ dataStorage: num(rollup == null ? void 0 : rollup.dataStorageMb) / 1024 * rates.dataStoragePerGbMonth,
2090
+ apiRequests: num(rollup == null ? void 0 : rollup.apiRequests) * rates.perApiRequest,
2091
+ // The records band (AGL-2611), under the axis name every breakdown reader
2092
+ // already keys on. `??` and not `||`: a rollup that measured ZERO records
2093
+ // has answered, and must not fall back to a contacts figure from a month
2094
+ // written under the narrower basis.
2095
+ contacts: num((_ref = rollup == null ? void 0 : rollup.crmRecordsCount) != null ? _ref : rollup == null ? void 0 : rollup.contactsCount) * rates.perContactMonth,
2096
+ // Every send, not the overage — the provider bills the first message of
2097
+ // the month as much as the last, so a cost model that started counting at
2098
+ // the plan's included band would report an org that stayed inside its
2099
+ // allowance as free.
2100
+ emailSends: num(rollup == null ? void 0 : rollup.emailSends) * rates.perEmailSend,
2101
+ /*==========================================
2102
+ * AGLYN ASSIST PROVIDER SPEND (AGL-2280).
2103
+ *
2104
+ * Already dollars, so ×1 — see `OrgUsageRollupInput.assistCostUsd`.
2105
+ *
2106
+ * This is the ONE meter on the platform whose unit cost is not a fraction
2107
+ * margin model read it: the discount guardrail priced six meters that
2108
+ * together measured $0.0000054 for the largest real org, and ignored the
2109
+ * only line item that can plausibly clear the $2/site floor on its own.
2110
+ * A 93%-off coupon on an org burning $40/month of tokens rated green.
2111
+ *
2112
+ * It is deliberately NOT in `billedCents` — what an org is charged is a
2113
+ * separate decision with a price sheet behind it. This is what the org
2114
+ * COSTS US, which is the only question the guardrail asks.
2115
+ *=========================================*/ assist: num(rollup == null ? void 0 : rollup.assistCostUsd),
2116
+ // Both run counters on one line: one rate, one cost, whichever builder
2117
+ // produced the run. Unpriced until 2026-09-07 — the two bands were
2118
+ // recorded on every rollup and read as nothing.
2119
+ runs: (num(rollup == null ? void 0 : rollup.workflowRuns) + num(rollup == null ? void 0 : rollup.actionRuns)) * rates.perRun
2120
+ };
2121
+ const measuredUsd = Object.values(breakdown).reduce((sum, x)=>sum + x, 0);
2122
+ const floorUsd = Math.max(0, siteCount) * INFRA_COGS_PER_SITE_USD;
2123
+ return {
2124
+ cogsUsd: Math.max(measuredUsd, floorUsd),
2125
+ basis: measuredUsd > floorUsd ? 'measured' : 'floor',
2126
+ measuredUsd,
2127
+ floorUsd,
2128
+ breakdown
2129
+ };
2130
+ }
2131
+ /**
2132
+ * Pluck the fields `orgMonthlyCogsUsd` prices out of a raw usage rollup
2133
+ * (AGL-1134) — a Firestore `snapshot.data()`, or the JSON a staff endpoint
2134
+ * served from one. ONE list of field names, in one place.
2135
+ *
2136
+ * The hazard this closes is silent. Every caller previously hand-listed the
2137
+ * six fields at its own call site — the discount route, the anomaly detector,
2138
+ * the staff org page — and `/api/admin/org-usage` projected only three of
2139
+ * them. A projection that drops a priced field does not error; the model just
2140
+ * returns a smaller number, and a smaller COGS is the direction that APPROVES
2141
+ * a discount. Same shape as the API projection that starved a predicate in
2142
+ * AGL-1355, and the two-units usage figure that read 20-45% wrong for years
2143
+ * in AGL-1402.
2144
+ *
2145
+ * UNITS, because the field names do not all say: `storageGb` and
2146
+ * `dataStorageMb` are gigabytes and MEGABYTES respectively (the conversion
2147
+ * lives in `orgMonthlyCogsUsd`, with its own test); `pageViews`,
2148
+ * `formSubmissions`, `apiRequests` and `emailSends` are counts FOR THE MONTH;
2149
+ * `contactsCount` and `crmRecordsCount` are point-in-time levels, not monthly
2150
+ * flows.
2151
+ */ export function orgCogsInputFrom(source) {
2152
+ const read = (field)=>{
2153
+ const value = source == null ? void 0 : source[field];
2154
+ return value == null ? undefined : value;
2155
+ };
2156
+ return {
2157
+ hostCount: read('hostCount'),
2158
+ storageGb: read('storageGb'),
2159
+ pageViews: read('pageViews'),
2160
+ formSubmissions: read('formSubmissions'),
2161
+ dataStorageMb: read('dataStorageMb'),
2162
+ apiRequests: read('apiRequests'),
2163
+ contactsCount: read('contactsCount'),
2164
+ // AGL-2611. The band's own figure, forwarded for the reason `assistCostUsd`
2165
+ // is: a projection that drops it prices the CRM at its contacts alone,
2166
+ // which is the direction that approves a discount.
2167
+ crmRecordsCount: read('crmRecordsCount'),
2168
+ companiesCount: read('companiesCount'),
2169
+ dealsCount: read('dealsCount'),
2170
+ emailSends: read('emailSends'),
2171
+ // AGL-2280. Dollars, not a meter — the projection still has to forward it
2172
+ // or the model prices Assist at nothing, which is the direction that
2173
+ // approves a discount.
2174
+ assistCostUsd: read('assistCostUsd'),
2175
+ // Priced since 2026-09-07 (`perRun`); a projection that drops them prices
2176
+ // the org's automations at nothing, the same direction as every other
2177
+ // omission on this list.
2178
+ workflowRuns: read('workflowRuns'),
2179
+ actionRuns: read('actionRuns')
2180
+ };
2181
+ }
2182
+ /**
2183
+ * `orgMonthlyCogsUsd` behind an explicit readiness flag (AGL-1134).
2184
+ *
2185
+ * The staff org page loads its rollup asynchronously, and an unfinished read
2186
+ * and an absent document are the same `undefined`. Priced directly, both
2187
+ * yield `basis: 'floor'` — so the enterprise pricing card printed "per-site
2188
+ * floor — no usage recorded yet" during the loading window, a
2189
+ * measurement-shaped sentence about an org whose measurements had simply not
2190
+ * arrived. That is the AGL-1380 / AGL-1422 defect exactly: a question
2191
+ * answered before its data is ready, and answered in the cheap direction.
2192
+ *
2193
+ * Returning a discriminated union rather than a nullable number is what makes
2194
+ * the window unrepresentable — a caller cannot reach a cost without first
2195
+ * handling `pending`, so the next call site cannot re-introduce this by
2196
+ * forgetting a flag it was handed.
2197
+ */ export function orgCogsPreview(ready, rollup, siteCount) {
2198
+ // Gated on the caller's flag alone. Deriving readiness from `rollup` being
2199
+ // truthy would re-create the bug: the org with no rollup yet is precisely
2200
+ // the one whose value stays falsy after the read completes.
2201
+ if (!ready) return {
2202
+ status: 'pending'
2203
+ };
2204
+ return {
2205
+ status: 'ready',
2206
+ cogs: orgMonthlyCogsUsd(rollup, siteCount)
2207
+ };
2208
+ }
2209
+ /**
2210
+ * Where a cost figure came from, in a sentence a staff member can act on
2211
+ * (AGL-1930).
2212
+ *
2213
+ * The staff quote card said `per-site floor — no usage recorded yet` for
2214
+ * EVERY `basis: 'floor'` org. Measured against production 2026-08-24 that
2215
+ * sentence is false on most of them: all 14 usage rollups across all 6 orgs
2216
+ * price under the floor, and 9 of the 14 carry genuinely non-zero usage — the
2217
+ * largest real org measured **$0.22** against its $2.00 floor. "No usage
2218
+ * recorded" and "usage recorded, and it came in under the floor" are opposite
2219
+ * facts about the cost model, and only the second one is evidence the meters
2220
+ * are working at all.
2221
+ *
2222
+ * Same failure family as AGL-1380 / AGL-1422 — a measurement-shaped sentence
2223
+ * printed over a measurement that was never consulted — one layer further in:
2224
+ * here the measurement exists and the copy denies it.
2225
+ */ export function orgCogsBasisSummary(cogs, month) {
2226
+ const when = month != null ? month : 'the latest';
2227
+ if (cogs.basis === 'measured') return `measured from ${when} usage`;
2228
+ // Absent and zero are NOT the same claim, and with `strictNullChecks` off an
2229
+ // absent meter reads as 0 all the way down — so this leans on the caller's
2230
+ // `orgCogsPreview` readiness gate for "has a rollup arrived", and reports
2231
+ // only what the priced total says.
2232
+ if (!(cogs.measuredUsd > 0)) return 'per-site floor — no usage recorded yet';
2233
+ // Cents are the wrong precision for these figures and rounding to them
2234
+ // prints `$0.00`, which reads as the "no usage" claim this function exists
2235
+ // to stop making. Every real org's measured month is under a cent.
2236
+ const measured = cogs.measuredUsd < 0.01 ? cogs.measuredUsd.toFixed(4) : cogs.measuredUsd.toFixed(2);
2237
+ return `per-site floor — ${when} usage measured ` + `$${measured}, under the $${cogs.floorUsd.toFixed(2)} floor`;
2238
+ }
2239
+ /**
2240
+ * Percent-off at or above which creating a coupon needs explicit staff
2241
+ * sign-off (AGL-1105). A ≥40%-off coupon is a real revenue commitment, so the
2242
+ * creation route rejects it unless the caller passes a confirm flag; the UI
2243
+ * surfaces a checkbox at this threshold.
2244
+ */ export const DISCOUNT_APPROVAL_THRESHOLD_PCT = 40;
2245
+ /**
2246
+ * Discount DEPTH bands (AGL-1120) — how much of list price is being given
2247
+ * away, which is a different question from whether the remainder covers
2248
+ * infrastructure.
2249
+ *
2250
+ * The original guardrail asked only the second question, so a **93% off**
2251
+ * coupon rated "OK — net margin 78.1% vs a 75% floor": $9.15 left against $2
2252
+ * of infra really is a 78% contribution margin. The ratio is scale-invariant,
2253
+ * so as long as the leftover dwarfs $2 the verdict stays green no matter how
2254
+ * little is left. Depth never entered the verdict at all.
2255
+ *
2256
+ * Warn at the threshold that already requires sign-off, so the badge and the
2257
+ * approval checkbox stop disagreeing with each other; block at 70%, where a
2258
+ * deal is being given away rather than discounted.
2259
+ */ export const DISCOUNT_DEPTH_WARN_PCT = DISCOUNT_APPROVAL_THRESHOLD_PCT;
2260
+ export const DISCOUNT_DEPTH_BLOCK_PCT = 70;
2261
+ /** How many sites (hosts) the org runs — at least 1 for a billing org. */ export function orgSiteCount(org) {
2262
+ var _ref;
2263
+ return Math.max(1, Object.keys((_ref = org == null ? void 0 : org.hosts) != null ? _ref : {}).length);
2264
+ }
2265
+ /**
2266
+ * Rate a proposed discount (AGL-1105, re-shaped in AGL-1120) — the guardrail
2267
+ * behind staff coupon creation and per-org application.
2268
+ *
2269
+ * Two independent questions, and the verdict is the WORSE of them:
2270
+ *
2271
+ * 1. **How deep is it?** `depthPct` against `DISCOUNT_DEPTH_WARN_PCT` /
2272
+ * `DISCOUNT_DEPTH_BLOCK_PCT`. This is the one that was missing. Cost
2273
+ * recovery alone is scale-invariant — 93% off $139 leaves $9.15 against
2274
+ * $2 of infra, a genuine 78% contribution margin — so the old verdict
2275
+ * rated a give-away "OK" and only warned at 94%.
2276
+ * 2. **Does the remainder cover the cost of serving them?** `marginPct`
2277
+ * against `NET_MARGIN_FLOOR_PCT`, with `NET_MARGIN_WARN_BAND_PCT` below
2278
+ * it, and an automatic block once the discount drives net ≤ 0.
2279
+ *
2280
+ * Neither subsumes the other: a shallow discount on an org running twenty
2281
+ * sites can still fail cost recovery, and a deep one on a cheap org can still
2282
+ * clear it. `reason` reports which test bound, so the UI never shows a
2283
+ * healthy-looking margin beside a red badge without explanation.
2284
+ *
2285
+ * `discount` is the PROPOSED change, independent of any discount already on
2286
+ * the org — `grossUsd` is always the list price (`orgListPriceMonthlyUsd`),
2287
+ * so this never double-counts an existing `org.discount`. A non-billing org
2288
+ * (list price 0) has no revenue to protect and rates `ok`.
2289
+ */ export function checkDiscountMargin(org, discount, options = {}) {
2290
+ var _options_measuredCogsUsd;
2291
+ var _org_subscription;
2292
+ const floorPct = NET_MARGIN_FLOOR_PCT;
2293
+ const grossUsd = orgListPriceMonthlyUsd(org);
2294
+ const flatCogsUsd = Math.round(INFRA_COGS_PER_SITE_USD * orgSiteCount(org) * 100) / 100;
2295
+ // Measured cost wins only when it EXCEEDS the flat estimate. A rollup that
2296
+ // has not run yet reads as a low number, and a guardrail that got looser
2297
+ // because the meter was behind would fail in the one direction that costs
2298
+ // money.
2299
+ const measured = Number((_options_measuredCogsUsd = options.measuredCogsUsd) != null ? _options_measuredCogsUsd : 0);
2300
+ const cogsMeasured = Number.isFinite(measured) && measured > flatCogsUsd;
2301
+ const infraCogsUsd = cogsMeasured ? Math.round(measured * 100) / 100 : flatCogsUsd;
2302
+ if (grossUsd <= 0) {
2303
+ return {
2304
+ grossUsd: 0,
2305
+ discountedUsd: 0,
2306
+ netUsd: 0,
2307
+ infraCogsUsd,
2308
+ cogsMeasured,
2309
+ marginPct: 1,
2310
+ depthPct: 0,
2311
+ rating: 'ok',
2312
+ reason: 'none',
2313
+ floorPct
2314
+ };
2315
+ }
2316
+ const discountedUsd = applyDiscountUsd(grossUsd, discount);
2317
+ const annual = (org == null ? void 0 : (_org_subscription = org.subscription) == null ? void 0 : _org_subscription.interval) === 'year';
2318
+ const netUsd = netOfProcessorFee(discountedUsd, annual);
2319
+ const depthPct = Math.round((grossUsd - discountedUsd) / grossUsd * 10000) / 10000;
2320
+ const marginPct = netUsd > 0 ? Math.round((netUsd - infraCogsUsd) / netUsd * 10000) / 10000 : -1;
2321
+ const depthRating = depthPct * 100 >= DISCOUNT_DEPTH_BLOCK_PCT ? 'block' : depthPct * 100 >= DISCOUNT_DEPTH_WARN_PCT ? 'warn' : 'ok';
2322
+ // Same bands the staff quote card renders — one reader for the two
2323
+ // constants (AGL-1930), so a re-tune cannot reach the guardrail and miss
2324
+ // the UI.
2325
+ const cogsRating = netMarginRating(marginPct);
2326
+ const severity = {
2327
+ ok: 0,
2328
+ warn: 1,
2329
+ block: 2
2330
+ };
2331
+ const rating = severity[depthRating] >= severity[cogsRating] ? depthRating : cogsRating;
2332
+ const reason = rating === 'ok' ? 'none' : netUsd <= 0 ? 'underwater' : severity[depthRating] >= severity[cogsRating] ? 'depth' : 'cogs';
2333
+ return {
2334
+ grossUsd,
2335
+ discountedUsd,
2336
+ netUsd,
2337
+ infraCogsUsd,
2338
+ cogsMeasured,
2339
+ marginPct,
2340
+ depthPct,
2341
+ rating,
2342
+ reason,
2343
+ floorPct
2344
+ };
2345
+ }
2346
+ /**
2347
+ * The plan the org actually gets (AGL-247): missing/unknown plans resolve
2348
+ * as `free`, and a paid plan whose subscription is canceled/unpaid/
2349
+ * incomplete downgrades to `free` until the webhook restores it — plan
2350
+ * fields alone are not entitlement.
2351
+ *
2352
+ * A STAFF COMP comes first (AGL-3034), and only where no subscription decides
2353
+ * the plan: `resolvePlanComp` answers `null` while one is live, so a paying
2354
+ * workspace's plan is always its subscription's. The comp is its own marker,
2355
+ * never the stored `plan` — a canceled customer whose `plan` still names what
2356
+ * they paid for resolves as Free exactly as before, because nothing about
2357
+ * their document is a comp.
2358
+ */ export function resolveEffectivePlan(org) {
2359
+ const comp = resolvePlanComp(org);
2360
+ if (comp) return comp.plan;
2361
+ const plan = org == null ? void 0 : org.plan;
2362
+ if (!plan || !(plan in PLAN_ENTITLEMENTS)) return 'free';
2363
+ const status = subscriptionStatusOf(org);
2364
+ if (plan !== 'free' && status && DEAD_SUBSCRIPTION_STATUSES.has(status)) {
2365
+ return 'free';
2366
+ }
2367
+ return plan;
2368
+ }
2369
+ function resolvePlan(org) {
2370
+ return resolveEffectivePlan(org);
2371
+ }
2372
+ /**
2373
+ * Purchased add-on quantities that currently apply (AGL-524): add-ons
2374
+ * bill as items on the org's Stripe subscription, so a dead subscription
2375
+ * (the `resolveEffectivePlan` set) stops them counting until the webhook
2376
+ * restores it. Orgs with no subscription state keep staff-set quantities
2377
+ * (comped add-ons predating self-serve billing).
2378
+ */ function resolvePurchasedAddons(org) {
2379
+ var _ref;
2380
+ const status = subscriptionStatusOf(org);
2381
+ if (status && DEAD_SUBSCRIPTION_STATUSES.has(status)) return {};
2382
+ return (_ref = org == null ? void 0 : org.seatAddons) != null ? _ref : {};
2383
+ }
2384
+ /**
2385
+ * Whether the org's Aglyn AI add-on currently applies (AGL-2896): bought,
2386
+ * and on a subscription that is still paying for it. The ONE reading of
2387
+ * `seatAddons.aiAddon` — the entitlement fold and the assist overage rate
2388
+ * both ask this, so a dead subscription drops the band and the rate
2389
+ * together rather than leaving a rate on a band that has gone.
2390
+ */ export function hasAiAddon(org) {
2391
+ return aiAddonUnits(resolvePurchasedAddons(org)) === 1;
2392
+ }
2393
+ /**
2394
+ * Numeric entitlement keys that were REMOVED from `PLAN_ENTITLEMENTS` and
2395
+ * must never be resolved again from a stored override (AGL-2133).
2396
+ *
2397
+ * `totalSiteSizeMb` is the first. It carried a value on all 8 plans and was
2398
+ * enforced by nothing — no gate, no meter, no alert. AGL-1107 added it and
2399
+ * AGL-1370 deleted its meter and its alert on measurement rather than taste:
2400
+ * `measure-node-map.ts` refuses any node map over 900 KB (AGL-678), so the
2401
+ * measurable org total tops out at 2.3–20.9% of the cap depending on plan.
2402
+ * What survived was a WRITABLE staff override field for a number nothing
2403
+ * read, so a support engineer resolving a "this site is too big" ticket
2404
+ * raised it, got a success and an audit row, and changed nothing.
2405
+ *
2406
+ * WIRING IT WAS THE OTHER OPTION AND IT IS WORSE. A cap the measurement can
2407
+ * reach a fifth of is a gate that refuses nobody — the same "reports success,
2408
+ * does nothing" shape, moved from the staff dialog to the enforcement point,
2409
+ * plus a new hard wall introduced days before launch. The per-site size
2410
+ * ceiling that actually binds is AGL-678's 900 KB node map, and it is real.
2411
+ *
2412
+ * The rollup's `siteSizeMb` MEASUREMENT stays — host-usage and the usage
2413
+ * audit read it, and it is a genuine internal signal. Only the entitlement
2414
+ * goes.
2415
+ */ export const RETIRED_ENTITLEMENT_KEYS = new Set([
2416
+ 'totalSiteSizeMb'
2417
+ ]);
2418
+ /**
2419
+ * The numeric entitlements that are PRICES rather than caps (AGL-3049): the
2420
+ * platform's percentage of a storefront or marketplace sale.
2421
+ *
2422
+ * Every other numeric key is a band — a count, a size, a monthly or daily
2423
+ * allowance — and an uncapped comp lifts it to `UNLIMITED`. These three it
2424
+ * must never touch. `transactionFee*Pct` becomes Stripe's
2425
+ * `application_fee_amount` at checkout, so an unlimited one is a fee larger
2426
+ * than the charge and every sale on the storefront fails; and "no cap" has
2427
+ * no meaning for a price in the first place. An uncapped workspace pays its
2428
+ * plan's percentages, or whatever a per-org override sets them to.
2429
+ *
2430
+ * Named rather than derived from the `Pct` suffix, so a new numeric key is a
2431
+ * band unless someone decides here that it is a price. `plan-comp.spec.ts`
2432
+ * pins these three by name and proves every other numeric key is lifted.
2433
+ */ export const PRICE_ENTITLEMENT_KEYS = new Set([
2434
+ 'transactionFeePhysicalPct',
2435
+ 'transactionFeeDigitalPct',
2436
+ 'marketplaceFeePct'
2437
+ ]);
2438
+ /**
2439
+ * The entitlements with every band lifted (AGL-3049): `UNLIMITED` on each
2440
+ * numeric key but the prices, features untouched. What an uncapped comp in
2441
+ * force resolves to — see `isUncappedPlanComp`.
2442
+ *
2443
+ * Applied LAST, over the plan row, the per-org overrides and the folded
2444
+ * add-ons alike: "every band reads as unlimited" means an override written
2445
+ * before the comp was uncapped does not quietly cap it again. The overrides
2446
+ * stay stored, and apply once more if the comp is capped or removed.
2447
+ */ function withEveryCapLifted(entitlements) {
2448
+ const lifted = _extends({}, entitlements);
2449
+ for (const [key, value] of Object.entries(entitlements)){
2450
+ if (typeof value === 'number' && !PRICE_ENTITLEMENT_KEYS.has(key)) {
2451
+ lifted[key] = UNLIMITED;
2452
+ }
2453
+ }
2454
+ return lifted;
2455
+ }
2456
+ /**
2457
+ * Effective entitlements for an org: plan defaults with the org doc's
2458
+ * per-key overrides applied (features merge key-by-key too), then
2459
+ * purchased add-ons stacked on top (AGL-524): `seatAddons.hosts` raises
2460
+ * `hostLimit`, `seatAddons.eventCalendar` switches the `eventCalendar`
2461
+ * feature on, and every seat add-on a plugin declared through
2462
+ * `registerPluginEntitlements` (AGL-2940) widens the quota it names by its
2463
+ * per-plan figure per unit and switches its features on — the Aglyn AI
2464
+ * add-on (AGL-2896) is the first such declaration. (Seat/dataset add-ons
2465
+ * instead fold in at `checkSeatQuota` / `checkDatasetQuota`, where the
2466
+ * per-plan hard max clamps them.)
2467
+ * Missing or unknown plans resolve as `free`.
2468
+ *
2469
+ * EVERY NUMERIC KEY of `OrgEntitlements` is an override axis, not a chosen
2470
+ * few: the loop below copies any number it finds under the org's
2471
+ * `entitlements`, so a contracted `bandwidthGb`, `apiRequestsPerMonth` or
2472
+ * `workflowRunsPerMonth` resolves ahead of the plan default exactly as the
2473
+ * long-documented `emailSendsPerMonth`, `formsPerHost` and
2474
+ * `assistCreditsPerMonth` overrides do. Since 2026-09-07 this is what makes
2475
+ * Enterprise's finite fallbacks safe to hold: the row in `PLAN_ENTITLEMENTS`
2476
+ * is what an agreement gets before its numbers are written down, and the
2477
+ * override is where they are written. `plan-entitlements.spec.ts` proves
2478
+ * the precedence on every axis, so a resolver that started honoring a list
2479
+ * would fail by the name of the axis it dropped.
2480
+ *
2481
+ * `hostLimit` is DELIBERATELY unclamped here (AGL-1738): there is no
2482
+ * `maxHostsPerOrg` to clamp against, because extra sites are sold flat rather
2483
+ * than up to a band. Their bound is the purchase ceiling,
2484
+ * `EXTRA_HOSTS_ADDON_MAX`, and a band-shaped `Math.min` added here would
2485
+ * silently disable sites the org is invoiced for. See that constant before
2486
+ * "fixing" this add.
2487
+ *
2488
+ * `posRegisters` IS NOT ADDED HERE, and this is the fix, not an omission
2489
+ * (AGL-1775). The register add-on is priced "per
2490
+ * extra register/location" and enforced PER SITE, so folding the org-wide
2491
+ * purchase into an org-level value every site inherits sold one register and
2492
+ * delivered `hostLimit` of them. `seatAddons.posRegisters` is now a POOL and
2493
+ * `org.registerAllocations` says which site holds each seat. The number this
2494
+ * function returns for `posRegisters` is therefore **the plan's cap alone** —
2495
+ * exactly what an unallocated site must resolve to. Per-site capacity comes
2496
+ * from `resolveHostRegisterCap(org, hostId)`; nothing else may add the pool
2497
+ * back in.
2498
+ *
2499
+ * (`eventCalendar` is still org-wide on purpose: it is documented and priced
2500
+ * as "one purchase covers every host in the org". The register add-on never
2501
+ * was, which is the whole difference.)
2502
+ *
2503
+ * AN UNCAPPED COMP IN FORCE (AGL-3049) resolves every band as `UNLIMITED`
2504
+ * on top of all of the above — `withEveryCapLifted` — and leaves the
2505
+ * features and the fee percentages exactly as resolved.
2506
+ */ export function resolveOrgEntitlements(org) {
2507
+ const resolved = resolveCappedOrgEntitlements(org);
2508
+ return isUncappedPlanComp(org) ? withEveryCapLifted(resolved) : resolved;
2509
+ }
2510
+ /** `resolveOrgEntitlements` before an uncapped comp lifts anything. */ function resolveCappedOrgEntitlements(org) {
2511
+ var _purchased_hosts, _purchased_eventCalendar;
2512
+ const defaults = withPluginFeatureDefaults(PLAN_ENTITLEMENTS[resolvePlan(org)], resolvePlan(org));
2513
+ const overrides = org == null ? void 0 : org.entitlements;
2514
+ let resolved = defaults;
2515
+ if (overrides) {
2516
+ const { features: featureOverrides, datasetsPerHost: legacyDatasets, maxDatasetsPerHost: legacyMaxDatasets } = overrides, quotaOverrides = _object_without_properties_loose(overrides, [
2517
+ "features",
2518
+ "datasetsPerHost",
2519
+ "maxDatasetsPerHost"
2520
+ ]);
2521
+ const merged = _extends({}, defaults);
2522
+ for (const [key, value] of Object.entries(quotaOverrides)){
2523
+ // A RETIRED quota's stored override is dropped, not merged (AGL-2133).
2524
+ // This loop copies any numeric key it finds, so removing a key from
2525
+ // `PLAN_ENTITLEMENTS` alone would leave the value that staff already
2526
+ // wrote onto live org documents still appearing on the resolved
2527
+ // entitlements — a number with no default behind it and no reader in
2528
+ // front of it, which is a worse artefact than the field that produced
2529
+ // it. `tools/scripts/strip-retired-entitlements.mjs` clears them from
2530
+ // Firestore; this makes the resolver correct before it has run, and
2531
+ // stays correct if a document is restored from an old backup.
2532
+ if (RETIRED_ENTITLEMENT_KEYS.has(key)) continue;
2533
+ if (typeof value === 'number') merged[key] = value;
2534
+ }
2535
+ // Pre-AGL-240 override docs keyed datasets per host; resolve them into
2536
+ // the org keys unless an org-keyed override is present.
2537
+ if (typeof legacyDatasets === 'number' && overrides.datasetsPerOrg == null) {
2538
+ merged.datasetsPerOrg = legacyDatasets;
2539
+ }
2540
+ if (typeof legacyMaxDatasets === 'number' && overrides.maxDatasetsPerOrg == null) {
2541
+ merged.maxDatasetsPerOrg = legacyMaxDatasets;
2542
+ }
2543
+ resolved = _extends({}, merged, {
2544
+ features: _extends({}, defaults.features, featureOverrides)
2545
+ });
2546
+ }
2547
+ const purchased = resolvePurchasedAddons(org);
2548
+ const extraHosts = Math.max(0, (_purchased_hosts = purchased.hosts) != null ? _purchased_hosts : 0);
2549
+ const eventCalendar = ((_purchased_eventCalendar = purchased.eventCalendar) != null ? _purchased_eventCalendar : 0) >= 1;
2550
+ // A declared add-on's quota is added AFTER the per-org override, so a
2551
+ // contracted figure and a purchased add-on stack the way a raised
2552
+ // `hostLimit` and purchased sites do. The per-unit figure is the PLAN's,
2553
+ // read off the effective plan rather than the stored one, so a dead
2554
+ // subscription that resolved to free adds free's figure.
2555
+ const folded = foldPluginSeatAddons(resolved, purchased, resolvePlan(org));
2556
+ if (!extraHosts && !eventCalendar && folded === resolved) return resolved;
2557
+ return _extends({}, folded, {
2558
+ hostLimit: folded.hostLimit + extraHosts,
2559
+ features: _extends({}, folded.features, eventCalendar ? {
2560
+ eventCalendar: true
2561
+ } : {})
2562
+ });
2563
+ }
2564
+ /**
2565
+ * How many units of a plugin-declared seat add-on a purchase map counts as
2566
+ * (AGL-2940): the stored quantity, capped at the declaration's `maxUnits`
2567
+ * — 1 for an org-wide add-on, where a doubled webhook item or a hand edit
2568
+ * above one is still one purchase. A non-number, `NaN`, a negative, or a
2569
+ * key nothing declared is none.
2570
+ */ export function pluginSeatAddonUnits(addons, key) {
2571
+ var _ref;
2572
+ const declared = pluginSeatAddon(key);
2573
+ if (!declared) return 0;
2574
+ const quantity = Number((_ref = addons == null ? void 0 : addons[key]) != null ? _ref : 0);
2575
+ if (!Number.isFinite(quantity) || quantity < 1) return 0;
2576
+ const units = Math.floor(quantity);
2577
+ return declared.maxUnits != null ? Math.min(units, declared.maxUnits) : units;
2578
+ }
2579
+ /**
2580
+ * Whether a plugin-declared seat add-on currently applies to the org: bought,
2581
+ * and on a subscription that is still paying for it — the one reading every
2582
+ * surface that shows or charges for the add-on goes through, so a dead
2583
+ * subscription drops the quota and the feature together.
2584
+ */ export function hasPluginSeatAddon(org, key) {
2585
+ return pluginSeatAddonUnits(resolvePurchasedAddons(org), key) > 0;
2586
+ }
2587
+ /**
2588
+ * Every declared seat add-on the org holds, folded in catalog order: the
2589
+ * quota each names gains its per-plan figure per unit, and its features
2590
+ * switch on. Returns the input object untouched when nothing applies, so a
2591
+ * caller can tell "nothing folded" from "folded to the same numbers".
2592
+ */ function foldPluginSeatAddons(resolved, purchased, plan) {
2593
+ let out = resolved;
2594
+ for (const addon of listPluginSeatAddons()){
2595
+ var _addon_features;
2596
+ const units = pluginSeatAddonUnits(purchased, addon.key);
2597
+ if (!units) continue;
2598
+ const next = _extends({}, out, {
2599
+ features: _extends({}, out.features)
2600
+ });
2601
+ if (addon.quota) {
2602
+ var _addon_quota_perUnitByPlan_plan, _bag_addon_quota_key;
2603
+ const perUnit = (_addon_quota_perUnitByPlan_plan = addon.quota.perUnitByPlan[plan]) != null ? _addon_quota_perUnitByPlan_plan : 0;
2604
+ const bag = next;
2605
+ const current = Number((_bag_addon_quota_key = bag[addon.quota.key]) != null ? _bag_addon_quota_key : 0);
2606
+ bag[addon.quota.key] = (Number.isFinite(current) ? current : 0) + perUnit * units;
2607
+ }
2608
+ const features = next.features;
2609
+ for (const feature of (_addon_features = addon.features) != null ? _addon_features : [])features[feature] = true;
2610
+ out = next;
2611
+ }
2612
+ return out;
2613
+ }
2614
+ /**
2615
+ * The plan row with every plugin-declared feature's default filled in
2616
+ * (AGL-2940). A feature the plan tables already carry keeps the table's
2617
+ * answer; a feature only a declaration knows reads its `defaultByPlan`, or
2618
+ * `false`. Returns the row itself when no declaration adds anything.
2619
+ */ function withPluginFeatureDefaults(row, plan) {
2620
+ const features = row.features;
2621
+ let out;
2622
+ for (const declared of listPluginFeatures()){
2623
+ var _ref;
2624
+ var _declared_defaultByPlan;
2625
+ if (features[declared.key] !== undefined) continue;
2626
+ out = out != null ? out : _extends({}, features);
2627
+ out[declared.key] = (_ref = (_declared_defaultByPlan = declared.defaultByPlan) == null ? void 0 : _declared_defaultByPlan[plan]) != null ? _ref : false;
2628
+ }
2629
+ return out ? _extends({}, row, {
2630
+ features: out
2631
+ }) : row;
2632
+ }
2633
+ /**
2634
+ * The org's POS register pool (AGL-1775): how many seats were purchased, how
2635
+ * many are assigned to a site, and how many are left to assign.
2636
+ *
2637
+ * `purchased` runs through `resolvePurchasedAddons`, so a dead subscription
2638
+ * empties the pool exactly as it empties every other add-on — the seats stop
2639
+ * counting when they stop billing.
2640
+ *
2641
+ * `byHost` is the SANITISED allocation, and it never sums past `purchased`.
2642
+ * The write path validates before it stores, so an over-allocated map means
2643
+ * the data is stale or corrupt (a purchase reduced without the allocation
2644
+ * being trimmed, a partial write, a hand edit). Both directions of getting
2645
+ * that wrong cost money, so it is resolved rather than trusted: hosts are
2646
+ * walked in sorted-id order and granted what remains of the pool, which is
2647
+ * deterministic — the same host gets the same answer on every reader — and
2648
+ * cannot hand out capacity nobody paid for.
2649
+ *
2650
+ * Every value is coerced: a non-number, `NaN`, `Infinity` or a negative reads
2651
+ * as zero seats. An `Infinity` in this map would otherwise become an
2652
+ * unbounded per-site register cap, which is the one outcome the guard in
2653
+ * `resolveHostRegisterCap` exists to make impossible.
2654
+ */ export function resolveRegisterSeatPool(org) {
2655
+ const rawPurchased = resolvePurchasedAddons(org).posRegisters;
2656
+ const purchased = Number.isFinite(rawPurchased) && rawPurchased > 0 ? Math.floor(rawPurchased) : 0;
2657
+ const raw = org == null ? void 0 : org.registerAllocations;
2658
+ const byHost = {};
2659
+ let allocated = 0;
2660
+ if (raw && typeof raw === 'object') {
2661
+ for (const hostId of Object.keys(raw).sort()){
2662
+ if (allocated >= purchased) break;
2663
+ const value = Number(raw[hostId]);
2664
+ if (!Number.isFinite(value) || value <= 0) continue;
2665
+ const seats = Math.min(Math.floor(value), purchased - allocated);
2666
+ if (seats <= 0) continue;
2667
+ byHost[hostId] = seats;
2668
+ allocated += seats;
2669
+ }
2670
+ }
2671
+ return {
2672
+ purchased,
2673
+ allocated,
2674
+ available: purchased - allocated,
2675
+ byHost
2676
+ };
2677
+ }
2678
+ /**
2679
+ * How many POS registers one SITE may run (AGL-1775): the plan's per-site cap
2680
+ * plus whatever the org has assigned to that site out of the pool.
2681
+ *
2682
+ * THE GUARD, and the reason this is a function rather than a lookup. An
2683
+ * unallocated host — no entry in `registerAllocations`, a missing map, a
2684
+ * missing org, an org still loading — resolves to `resolveOrgEntitlements(org)
2685
+ * .posRegisters`, which since AGL-1775 is the PLAN's cap and nothing else.
2686
+ * Never the pooled total, and never `Infinity` unless the plan itself is
2687
+ * `UNLIMITED` (enterprise, where an unbounded cap is what was sold).
2688
+ *
2689
+ * `checkQuota(undefined)` resolving to the Free tier is the lesson this
2690
+ * inverts: an absent input there quietly answered a question in the
2691
+ * PERMISSIVE direction on the day it mattered. Here the absent input answers
2692
+ * in the direction of the plan, which is the conservative one — a site that
2693
+ * has been assigned seats and whose allocation failed to load runs fewer
2694
+ * registers than it paid for, which the console can say out loud, rather than
2695
+ * more than it paid for, which nothing would ever notice.
2696
+ *
2697
+ * There is deliberately no separate read: the allocation lives on the org doc
2698
+ * beside the entitlements, so a caller holding one holds the other.
2699
+ */ export function resolveHostRegisterCap(org, hostId) {
2700
+ var _resolveRegisterSeatPool_byHost_hostId;
2701
+ const planCap = resolveOrgEntitlements(org).posRegisters;
2702
+ if (!hostId) return planCap;
2703
+ // An UNLIMITED plan cap plus anything is still UNLIMITED; short-circuit so
2704
+ // `Infinity + n` never has to be reasoned about downstream.
2705
+ if (planCap === UNLIMITED) return UNLIMITED;
2706
+ return planCap + ((_resolveRegisterSeatPool_byHost_hostId = resolveRegisterSeatPool(org).byHost[hostId]) != null ? _resolveRegisterSeatPool_byHost_hostId : 0);
2707
+ }
2708
+ /**
2709
+ * The org's COLLABORATOR seat pool (AGL-2439): how many per-site collaborator
2710
+ * seats were purchased, how many are assigned to a site, and how many are
2711
+ * left to assign.
2712
+ *
2713
+ * The AGL-1775 register mechanism, applied to the key it was never applied to.
2714
+ * `seatAddons.members` is bought once, org-wide; `membersPerHost` is enforced
2715
+ * PER SITE. Adding one to the other handed the whole purchase to every site,
2716
+ * so an org with 20 sites that bought ONE extra collaborator seat received 20.
2717
+ * The quantity is now the size of an org-level pool and
2718
+ * `org.collaboratorAllocations` says which site holds each seat.
2719
+ *
2720
+ * Everything about the resolution is deliberately identical to
2721
+ * `resolveRegisterSeatPool` — the same clamp, the same sorted-id determinism,
2722
+ * the same coercion of a non-number / `NaN` / `Infinity` / negative to zero.
2723
+ * This is ONE mechanism used twice, not a second mechanism that happens to
2724
+ * rhyme: a reader who has understood the register pool has understood this,
2725
+ * and a fix to one is a fix to both.
2726
+ *
2727
+ * `purchased` runs through `resolvePurchasedAddons`, so a dead subscription
2728
+ * empties the pool exactly as it empties every other add-on.
2729
+ */ export function resolveCollaboratorSeatPool(org) {
2730
+ const rawPurchased = resolvePurchasedAddons(org).members;
2731
+ const purchased = Number.isFinite(rawPurchased) && rawPurchased > 0 ? Math.floor(rawPurchased) : 0;
2732
+ const raw = org == null ? void 0 : org.collaboratorAllocations;
2733
+ const byHost = {};
2734
+ let allocated = 0;
2735
+ if (raw && typeof raw === 'object') {
2736
+ for (const hostId of Object.keys(raw).sort()){
2737
+ if (allocated >= purchased) break;
2738
+ const value = Number(raw[hostId]);
2739
+ if (!Number.isFinite(value) || value <= 0) continue;
2740
+ const seats = Math.min(Math.floor(value), purchased - allocated);
2741
+ if (seats <= 0) continue;
2742
+ byHost[hostId] = seats;
2743
+ allocated += seats;
2744
+ }
2745
+ }
2746
+ return {
2747
+ purchased,
2748
+ allocated,
2749
+ available: purchased - allocated,
2750
+ byHost
2751
+ };
2752
+ }
2753
+ /**
2754
+ * How many console COLLABORATORS one SITE may hold (AGL-2439): the plan's
2755
+ * per-site allowance plus whatever the org has assigned to that site out of
2756
+ * the pool, clamped to the plan's hard `maxMembersPerHost`.
2757
+ *
2758
+ * THE GUARD, and the reason this is a function rather than a lookup. An
2759
+ * unallocated host — no entry in `collaboratorAllocations`, a missing map, a
2760
+ * missing org, an org still loading — resolves to the PLAN's `membersPerHost`
2761
+ * and nothing else. Never the pooled total, and never `Infinity` unless the
2762
+ * plan itself is `UNLIMITED` (enterprise, where an unbounded cap is what was
2763
+ * sold). `checkQuota(undefined)` resolving to the Free tier is the lesson
2764
+ * this inverts: the absent input answers in the direction of the plan, which
2765
+ * is the conservative one.
2766
+ *
2767
+ * The clamp is what `checkSeatQuota` always did and is kept: collaborator
2768
+ * seats are sold up to a BAND (`maxMembersPerHost`), unlike registers, which
2769
+ * are sold flat. Assigning more pool seats to a site than the band allows
2770
+ * cannot lift it past the band — the org must upgrade the plan.
2771
+ *
2772
+ * There is deliberately no separate read: the allocation lives on the org doc
2773
+ * beside the entitlements, so a caller holding one holds the other.
2774
+ */ export function resolveHostCollaboratorCap(org, hostId) {
2775
+ var _resolveCollaboratorSeatPool_byHost_hostId;
2776
+ const entitlements = resolveOrgEntitlements(org);
2777
+ const included = entitlements.membersPerHost;
2778
+ const maxSeats = entitlements.maxMembersPerHost;
2779
+ if (!hostId) return Math.min(included, maxSeats);
2780
+ // An UNLIMITED plan cap plus anything is still UNLIMITED; short-circuit so
2781
+ // `Infinity + n` never has to be reasoned about downstream.
2782
+ if (included === UNLIMITED) return UNLIMITED;
2783
+ const assigned = (_resolveCollaboratorSeatPool_byHost_hostId = resolveCollaboratorSeatPool(org).byHost[hostId]) != null ? _resolveCollaboratorSeatPool_byHost_hostId : 0;
2784
+ return Math.min(included + assigned, maxSeats);
2785
+ }
2786
+ /**
2787
+ * Quota answer for a site's console COLLABORATORS (AGL-2439) — the
2788
+ * host-scoped `checkSeatQuota`, in the same `SeatQuotaResult` shape so the
2789
+ * existing call sites and refusal copy read identically.
2790
+ *
2791
+ * `checkSeatQuota(org, 'members', …)` is WRONG for a site now and this exists
2792
+ * so nobody has to remember why: since AGL-2439 that helper no longer folds
2793
+ * the org-wide purchase into a per-site number, so it answers the PLAN's cap
2794
+ * and would refuse a site the seats it holds.
2795
+ *
2796
+ * ## THE GRANDFATHER BOUNDARY — explicit, and this field is it
2797
+ *
2798
+ * The cap is corrected, and no org already above it is evicted or locked
2799
+ * out.
2800
+ *
2801
+ * The boundary is drawn between two different questions, and they are
2802
+ * different lines of code rather than one number used for two purposes:
2803
+ *
2804
+ * `allowed` — may this site take on ANOTHER collaborator? Bound
2805
+ * by `limit`, the corrected cap. This is the ONLY
2806
+ * thing the cap decides.
2807
+ * `retainedOverCap` — how many seats this site holds ABOVE `limit`. They
2808
+ * are RETAINED. Nothing in this repo revokes a
2809
+ * collaborator for being over a cap, and nothing may
2810
+ * be added that does: the cap binds ALLOCATION, never
2811
+ * ACCESS.
2812
+ *
2813
+ * So an over-cap site keeps every collaborator it has, cannot add another,
2814
+ * and returns to normal by attrition, by buying pool seats and assigning them
2815
+ * here, or by upgrading — at which point `limit` recomputes from the new plan
2816
+ * or allocation and the retention lapses on its own. That is what "they keep
2817
+ * what they have until they next change plan or seat count" means, resolved
2818
+ * rather than stored: a stored floor would need a backfill over live orgs and
2819
+ * would be a defaulted field on a `merge`-written document, which is the
2820
+ * converter hazard that destroys the real value it was meant to protect.
2821
+ *
2822
+ * `retainedOverCap` is non-zero ONLY when the site is already over. It is not
2823
+ * headroom and cannot be spent: it is a count the console says out loud so
2824
+ * the customer sees why the next Add is refused while nobody loses access.
2825
+ */ export function checkHostCollaboratorQuota(org, hostId, currentUsage) {
2826
+ var _resolveCollaboratorSeatPool_byHost_;
2827
+ const entitlements = resolveOrgEntitlements(org);
2828
+ const pricing = resolvePlanPricing(org);
2829
+ const included = entitlements.membersPerHost;
2830
+ const maxSeats = entitlements.maxMembersPerHost;
2831
+ const addonPriceUsd = pricing.extraCollaboratorMonthlyUsd;
2832
+ const assignedSeats = (_resolveCollaboratorSeatPool_byHost_ = resolveCollaboratorSeatPool(org).byHost[hostId != null ? hostId : '']) != null ? _resolveCollaboratorSeatPool_byHost_ : 0;
2833
+ const limit = resolveHostCollaboratorCap(org, hostId);
2834
+ return {
2835
+ allowed: currentUsage < limit,
2836
+ limit,
2837
+ remaining: Math.max(0, limit - currentUsage),
2838
+ included,
2839
+ purchased: assignedSeats,
2840
+ maxSeats,
2841
+ upgradeRequired: addonPriceUsd === null || limit >= maxSeats,
2842
+ addonPriceUsd,
2843
+ // THE GRANDFATHER. `Math.max(0, …)` so a site under its cap reports zero
2844
+ // rather than a negative that a caller could add and turn into headroom.
2845
+ retainedOverCap: Math.max(0, currentUsage - limit),
2846
+ assignedSeats
2847
+ };
2848
+ }
2849
+ /**
2850
+ * Quota answer for a site's POS registers (AGL-1775) — the register-shaped
2851
+ * `checkQuota`, in the same `{ allowed, limit, remaining }` shape so callers
2852
+ * read identically.
2853
+ *
2854
+ * `checkQuota(org, 'posRegisters', used)` is WRONG for registers now and this
2855
+ * exists so nobody has to remember why: that helper resolves the org-level
2856
+ * value, which is the plan cap with no pool in it, so it would refuse a site
2857
+ * the seats it holds. Everything that gates a register creation calls this.
2858
+ */ export function checkHostRegisterQuota(org, hostId, currentUsage) {
2859
+ const limit = resolveHostRegisterCap(org, hostId);
2860
+ return {
2861
+ allowed: currentUsage < limit,
2862
+ limit,
2863
+ remaining: Math.max(0, limit - currentUsage)
2864
+ };
2865
+ }
2866
+ /**
2867
+ * Platform transaction fee % for a storefront sale (AGL-278): resolved
2868
+ * from the effective plan (with per-org overrides) by product type.
2869
+ * Digital and service sales use the digital rate; AGL-307 turns this into
2870
+ * the Stripe Connect `application_fee_amount` at charge time.
2871
+ */ export function resolveTransactionFeePct(org, productType) {
2872
+ var _org_entitlements;
2873
+ const entitlements = resolveOrgEntitlements(org);
2874
+ const key = productType === 'physical' ? 'transactionFeePhysicalPct' : 'transactionFeeDigitalPct';
2875
+ // A MALFORMED value falls back to the PLAN'S OWN rate, never to zero
2876
+ // (AGL-2114). `resolveOrgEntitlements` merges any numeric override
2877
+ // indistinguishably from the default, and `strictNullChecks` is off
2878
+ // repo-wide, so a `NaN`, an `Infinity`, a stringly-typed number or a
2879
+ // negative can reach here from a hand-edited or partially-written org doc.
2880
+ // This used to answer 0 for every one of them — silently zeroing the
2881
+ // platform's cut on a PAYING merchant's storefront, which on a destination
2882
+ // charge is not "no take rate" but a loss (see below).
2883
+ //
2884
+ // The sibling `resolveMarketplaceFeePct` has forbidden exactly this since
2885
+ // AGL-1543, in as many words: "A malformed override must never zero the
2886
+ // platform's cut." This is the storefront half of the same rule. The plan
2887
+ // table is a literal and cannot itself be malformed, so it is the honest
2888
+ // floor — and it preserves the tiers whose 0% is a DELIBERATE, advertised
2889
+ // rate rather than a data fault.
2890
+ const raw = entitlements[key];
2891
+ const pct = typeof raw === 'number' && Number.isFinite(raw) && raw >= 0 ? raw : PLAN_ENTITLEMENTS[resolvePlan(org)][key];
2892
+ if (Number.isFinite(pct) && pct > 0) return pct;
2893
+ // The free plan's 0% prices a plan that CANNOT SELL: `features.commerce` is
2894
+ // false there, so on an ordinary free org this rate is never consulted. It
2895
+ // IS consulted for the one org shape that can reach a storefront on the free
2896
+ // entitlement set — a per-org `features.commerce` override — and 0% on a
2897
+ // DESTINATION charge is not "no take rate", it is a loss: Stripe's
2898
+ // processing fee (2.9% + 30¢) and any $15 dispute fee are debited from the
2899
+ // PLATFORM's balance, so every sale costs Aglyn money (AGL-2071).
2900
+ //
2901
+ // `marketplaceFeePct: 30` on the free row is the same decision already made
2902
+ // for marketplace selling, and its comment says so in as many words: the
2903
+ // rate exists to price "an org GRANTED selling via a per-org feature
2904
+ // override". This is the missing equivalent for storefront sales, expressed
2905
+ // here rather than in the table because the plan CARDS render the table —
2906
+ // `billing-plan-cards.component.tsx:444` — and a Free card advertising a
2907
+ // transaction fee for selling it does not permit would be a pricing lie.
2908
+ //
2909
+ // A staff fee override wins even at 0: `resolveOrgEntitlements` merges any
2910
+ // numeric override indistinguishably from the default, so the raw doc is
2911
+ // what separates "nobody set a rate" from a deliberately comped one. That
2912
+ // decision has a name and a reason attached (`org-override-reason.ts`), and
2913
+ // this floor must not silently overrule it.
2914
+ if (resolvePlan(org) === 'free' && entitlements.features.commerce && typeof (org == null ? void 0 : (_org_entitlements = org.entitlements) == null ? void 0 : _org_entitlements[key]) !== 'number') {
2915
+ return PLAN_ENTITLEMENTS.starter[key];
2916
+ }
2917
+ return 0;
2918
+ }
2919
+ /**
2920
+ * Stripe's processing cost on ONE storefront destination charge, in cents
2921
+ * (AGL-2152) — the dearest enabled method's percentage plus the fixed 30¢,
2922
+ * rounded UP on the percentage so the answer is never a cent short of what
2923
+ * Stripe actually debits.
2924
+ *
2925
+ * WHO PAYS THIS. Every storefront charge is a DESTINATION charge:
2926
+ * `payment_intent_data[transfer_data][destination]` names the merchant's
2927
+ * connected account and no `transfer_data[amount]` is sent, so Stripe moves
2928
+ * the whole charge to the merchant and debits its processing fee from the
2929
+ * PLATFORM's balance. Aglyn's net on an order is therefore
2930
+ *
2931
+ * application_fee_amount − (processing% × charge + 30¢)
2932
+ *
2933
+ * and with `application_fee_amount` at 0 — which is what
2934
+ * `transactionFeePhysicalPct: 0` produced on Pro, Business, Scale, Advanced,
2935
+ * Agency and Enterprise — that net is NEGATIVE on every single sale. A flat
2936
+ * percentage cannot repair it either: Starter's 2% is below even the cheapest
2937
+ * processing rate, so Starter lost money at every order size too, and any rate
2938
+ * above it still loses until the order clears `30¢ ÷ (take − processing)`. The
2939
+ * fixed 30¢ is why a percentage alone can never reach break-even on small
2940
+ * orders, and why this returns a CENTS amount rather than a rate.
2941
+ *
2942
+ * WHY THE DEAREST METHOD AND NOT 2.9%. The shopper picks how to pay AFTER the
2943
+ * session is created, and `application_fee_amount` is a fixed number Stripe
2944
+ * will not let us revise once the charge settles. No storefront path pins
2945
+ * `payment_method_types`, so the platform's payment method configuration
2946
+ * decides the set — and per AGL-2343 that configuration has `klarna` and
2947
+ * `affirm` on beside the card family. Pricing this at the card rate would be
2948
+ * wrong exactly on the orders where it cost the most, which is the same
2949
+ * reasoning `marketplaceBreakEvenUsd` already applies to the listing floor;
2950
+ * these are deliberately the SAME two constants so the two cannot drift.
2951
+ *
2952
+ * THE ONE-LINE LEVER. On a card-family order this over-recovers by the spread
2953
+ * between the two rates. Pinning `payment_method_types` on the storefront
2954
+ * sessions to the card family would make 2.9% the true binding rate, and
2955
+ * `STOREFRONT_PROCESSING_PERCENT` below is then the single identifier to
2956
+ * repoint. That is a product decision about which payment methods a storefront
2957
+ * offers, not this one; it is recorded on AGL-2152 and in the Pricing Decision
2958
+ * Log so it can be taken deliberately.
2959
+ */ // `STOREFRONT_PROCESSING_PERCENT` and `STOREFRONT_PROCESSING_FIXED_CENTS` are
2960
+ // declared beside the marketplace processing constants they are defined from,
2961
+ // further down this file — a `const` cannot be read before its declaration is
2962
+ // evaluated, and this function is only ever CALLED, never evaluated at import.
2963
+ export function storefrontProcessingCostCents(chargeCents) {
2964
+ if (!Number.isFinite(chargeCents) || chargeCents <= 0) return 0;
2965
+ return Math.ceil(chargeCents * STOREFRONT_PROCESSING_PERCENT / 100) + STOREFRONT_PROCESSING_FIXED_CENTS;
2966
+ }
2967
+ /**
2968
+ * The Stripe Connect `application_fee_amount` for ONE storefront sale
2969
+ * (AGL-2152) — the platform's advertised take PLUS Stripe's processing cost,
2970
+ * passed through at cost.
2971
+ *
2972
+ * fee = take%(feeBaseCents) + processing%(chargeCents) + 30¢
2973
+ *
2974
+ * so Aglyn's net per order is AT LEAST the advertised take, at every order
2975
+ * size down to Stripe's own 50¢ charge minimum, and never negative. The
2976
+ * September-1 price lock was lifted for this one change; see the Pricing
2977
+ * Decision Log entry of 2026-08-19. No advertised PLATFORM rate moved — a 0% tier is still 0%
2978
+ * platform take — what changed is that the card cost stops being absorbed
2979
+ * silently by Aglyn.
2980
+ *
2981
+ * `feeBaseCents` is the GOODS the take is a cut of (discounts already applied,
2982
+ * tax and shipping excluded — that has always been the basis, AGL-2317).
2983
+ * `chargeCents` is what Stripe will actually run the card for, which is goods
2984
+ * + tax + shipping, and defaults to `feeBaseCents` for the paths where they
2985
+ * are the same number. Passing the goods figure for a charge that also carries
2986
+ * tax would under-recover by the processing rate on the difference.
2987
+ *
2988
+ * Clamped to `chargeCents` because Stripe rejects an application fee larger
2989
+ * than the charge. That clamp can only bind below ~32¢, under Stripe's own 50¢
2990
+ * charge minimum, so it never trims a real order's fee.
2991
+ */ export function resolveTransactionFeeCents(org, productType, feeBaseCents, chargeCents = feeBaseCents) {
2992
+ const charge = Number.isFinite(chargeCents) && chargeCents > 0 ? Math.round(chargeCents) : 0;
2993
+ if (charge <= 0) return 0;
2994
+ const base = Number.isFinite(feeBaseCents) && feeBaseCents > 0 ? Math.round(feeBaseCents) : 0;
2995
+ const pct = resolveTransactionFeePct(org, productType);
2996
+ // The `Math.max(1, …)` every door already applied: a rate above zero on
2997
+ // goods the shopper pays for is a fee however small it rounds to.
2998
+ const take = pct > 0 && base > 0 ? Math.max(1, Math.round(base * pct / 100)) : 0;
2999
+ return Math.min(charge, take + storefrontProcessingCostCents(charge));
3000
+ }
3001
+ /**
3002
+ * Stripe's processing cost on a RECURRING charge of `amountCents`, as the
3003
+ * percent a Subscription's `application_fee_percent` can carry (AGL-2655).
3004
+ *
3005
+ * A Stripe Subscription accepts no `application_fee_amount` — only
3006
+ * `application_fee_percent`, a rate with at most two decimal places — so the
3007
+ * fixed 30¢ that `storefrontProcessingCostCents` adds in cents has to be
3008
+ * folded into the rate. `(rate × amount + fixed) ÷ amount` is, with the
3009
+ * amount cancelled,
3010
+ *
3011
+ * percent = rate + fixed ÷ amount
3012
+ *
3013
+ * which is why a smaller recurring price carries a HIGHER percent: the 30¢
3014
+ * is a bigger share of it. At 6% + 30¢ a $10 price carries 9%, $25 carries
3015
+ * 7.2% and $100 carries 6.3%.
3016
+ *
3017
+ * Computed in hundredths of a percent on integers and rounded UP to the next
3018
+ * hundredth. Up, for the reason the one-time helper rounds its percentage
3019
+ * up: rounding to nearest would leave the recovery a fraction of a cent
3020
+ * short of what Stripe debits half the time, and Stripe rejects a third
3021
+ * decimal outright, so the only honest direction is over. Integers, because
3022
+ * `0.09 * 100` is not `9` in floating point and a percent one ulp above a
3023
+ * whole hundredth would round up to the next one. The same two constants as
3024
+ * the one-time path, so repointing `STOREFRONT_PROCESSING_PERCENT` moves
3025
+ * both surfaces together. Zero for a charge that is not a charge.
3026
+ */ export function storefrontProcessingPassThroughPercent(amountCents) {
3027
+ const amount = Number.isFinite(amountCents) && amountCents > 0 ? Math.round(amountCents) : 0;
3028
+ if (amount <= 0) return 0;
3029
+ const rateHundredths = Math.round(STOREFRONT_PROCESSING_PERCENT * 100);
3030
+ // 30¢ ÷ amount is a fraction; × 100 makes it a percent, × 100 again makes
3031
+ // it hundredths of one.
3032
+ const fixedHundredths = STOREFRONT_PROCESSING_FIXED_CENTS * 10000 / amount;
3033
+ return Math.min(100, Math.ceil(rateHundredths + fixedHundredths) / 100);
3034
+ }
3035
+ /**
3036
+ * The `application_fee_percent` for ONE storefront subscription (AGL-2655):
3037
+ * the plan's advertised take PLUS Stripe's processing cost as a percent —
3038
+ * the recurring twin of `resolveTransactionFeeCents`, under the same rule.
3039
+ * Every tier carries the pass-through, a 0% tier included, because the
3040
+ * tier's 0% is a platform take and the pass-through is not one; it is what
3041
+ * Stripe charges to take the payment, which on a destination charge is
3042
+ * debited from the platform's balance whatever the tier.
3043
+ *
3044
+ * `amountCents` is the recurring items the percent will be applied to —
3045
+ * the goods after any discount, tax and shipping excluded, the AGL-2317
3046
+ * basis. Stripe applies the percent to the whole invoice and the paid-invoice
3047
+ * correction scales it back to items, so sizing on items is what makes the
3048
+ * recovery come out at cost rather than under.
3049
+ *
3050
+ * Composed in hundredths so two decimals stay two decimals (`2 + 6.34` is
3051
+ * not always `8.34` in floating point), and capped at 100, Stripe's ceiling
3052
+ * for the parameter — the recurring twin of the clamp to the charge.
3053
+ */ export function resolveSubscriptionFeePercent(org, productType, amountCents) {
3054
+ const amount = Number.isFinite(amountCents) && amountCents > 0 ? Math.round(amountCents) : 0;
3055
+ if (amount <= 0) return 0;
3056
+ const take = resolveTransactionFeePct(org, productType);
3057
+ const passThrough = storefrontProcessingPassThroughPercent(amount);
3058
+ return Math.min(100, (Math.round(take * 100) + Math.round(passThrough * 100)) / 100);
3059
+ }
3060
+ /**
3061
+ * Platform take rate % for a MARKETPLACE listing sale (AGL-1543), priced
3062
+ * off the SELLER org. Resolved through `resolveOrgEntitlements` — so a
3063
+ * dead subscription prices as free-plan (30%) even while the stale `plan`
3064
+ * field still names a paid tier, and a negotiated per-org
3065
+ * `entitlements.marketplaceFeePct` override wins over the plan default.
3066
+ * Resolve per request from the org doc; never cache the answer.
3067
+ *
3068
+ * An out-of-range or missing value falls back to the FREE-plan rate — the
3069
+ * conservative direction. A malformed override must never zero the
3070
+ * platform's cut.
3071
+ */ export function resolveMarketplaceFeePct(org) {
3072
+ const pct = resolveOrgEntitlements(org).marketplaceFeePct;
3073
+ return Number.isFinite(pct) && pct >= 0 && pct <= 100 ? pct : PLAN_ENTITLEMENTS.free.marketplaceFeePct;
3074
+ }
3075
+ /**
3076
+ * The marketplace take rate that breaks even LATEST, derived from the plan
3077
+ * table rather than copied from it (AGL-2343).
3078
+ *
3079
+ * The lowest rate is the binding one: a smaller cut of the same price against
3080
+ * the same processing cost needs a dearer listing to cover it, so advice quoted
3081
+ * at this band holds for every publisher on every plan. Derived on each call
3082
+ * because a hand-written second copy of a rate is exactly the artifact that
3083
+ * decays into wrong money copy shown to a publisher — the same reason
3084
+ * `planGrantingFeature` reads the table instead of a map.
3085
+ */ export function bindingMarketplaceFeePct() {
3086
+ return Math.min(...Object.values(PLAN_ENTITLEMENTS).map((plan)=>plan.marketplaceFeePct).filter((pct)=>Number.isFinite(pct) && pct > 0));
3087
+ }
3088
+ /**
3089
+ * What one marketplace sale costs the PLATFORM to process (AGL-2343).
3090
+ *
3091
+ * NONE OF THESE IS AN AGLYN PRICE. They are Stripe's published US rates and
3092
+ * the Texas rate the platform remits, and they exist only as inputs to a
3093
+ * figure shown to a publisher while they type. Nothing is billed from them —
3094
+ * the charged amounts are `resolveMarketplaceFeePct` and Stripe's own
3095
+ * invoicing — so changing one changes a sentence, never a bill.
3096
+ *
3097
+ * WHY THE PLATFORM PAYS THE PROCESSING FEE AT ALL. Marketplace checkout is a
3098
+ * DESTINATION charge with a fixed `transfer_data[amount]` and deliberately no
3099
+ * `application_fee_amount`, so that the sales tax stays with the platform that
3100
+ * owes it (AGL-1544). On a destination charge Stripe debits its fee from the
3101
+ * PLATFORM's balance, and it is charged on `amount_total` — the listing price
3102
+ * plus the tax added on top of it.
3103
+ *
3104
+ * So a $1 listing at a 20% take rate is a loss: the buyer pays $1.08, the
3105
+ * seller is transferred $0.80, $0.08 is owed to the state, Aglyn keeps $0.20
3106
+ * and pays Stripe about $0.33. Net −$0.13.
3107
+ *
3108
+ * THE BNPL RATE IS THE ONE THAT BINDS, and it is not hypothetical: the live
3109
+ * default payment method configuration has `klarna` and `affirm` enabled
3110
+ * alongside `card`, `cashapp`, `link`, `amazon_pay` and `apple_pay`, and the
3111
+ * marketplace session pins no `payment_method_types`, so a buyer may pay by
3112
+ * either. Break-even is computed from the dearest enabled method, because a
3113
+ * figure computed from the cheapest would be wrong exactly when it mattered.
3114
+ * IF BNPL IS EVER TURNED OFF FOR MARKETPLACE SESSIONS, this constant is what
3115
+ * has to move with it.
3116
+ */ export const MARKETPLACE_PROCESSING_PERCENT_CARD = 2.9;
3117
+ /** Klarna/Affirm, roughly. See the note above — this is the binding one. */ export const MARKETPLACE_PROCESSING_PERCENT_BNPL = 6;
3118
+ /** Stripe's per-transaction fixed component, in cents. */ export const MARKETPLACE_PROCESSING_FIXED_CENTS = 30;
3119
+ /**
3120
+ * The tax rate the break-even figure assumes. Tax is added ON TOP of the
3121
+ * listing price and enlarges the base Stripe charges its percentage against,
3122
+ * so it makes the platform's cost slightly worse; the real rate is whatever
3123
+ * `automatic_tax` computes for the buyer's address, and this is the
3124
+ * platform's own Texas rate as a representative figure.
3125
+ */ export const MARKETPLACE_ASSUMED_TAX_PERCENT = 8.25;
3126
+ /**
3127
+ * The processing rate a STOREFRONT destination charge is priced against
3128
+ * (AGL-2152), and the one identifier to repoint if the storefront's payment
3129
+ * method set changes.
3130
+ *
3131
+ * Deliberately its OWN name rather than a direct use of the marketplace
3132
+ * constants, while being defined as one of them: the two surfaces share
3133
+ * Stripe's published rates and today share the platform's payment method
3134
+ * configuration, but they are separate product decisions. A future choice to
3135
+ * pin `payment_method_types` on storefront sessions to the card family — and
3136
+ * so to recover at `MARKETPLACE_PROCESSING_PERCENT_CARD` — must not silently
3137
+ * lower the marketplace listing floor, which is derived from the same figure
3138
+ * for a different flow.
3139
+ */ export const STOREFRONT_PROCESSING_PERCENT = MARKETPLACE_PROCESSING_PERCENT_BNPL;
3140
+ /** Stripe's per-transaction fixed component, in cents. Not rate-dependent. */ export const STOREFRONT_PROCESSING_FIXED_CENTS = MARKETPLACE_PROCESSING_FIXED_CENTS;
3141
+ /**
3142
+ * The funds flow for one sale, mirroring what `checkout.ts` actually builds
3143
+ * (AGL-2343): tax exclusive and on top, a fixed transfer of the seller's share
3144
+ * of the PRE-tax price, and Stripe's fee charged on the buyer's total.
3145
+ */ export function marketplaceSaleEconomics(priceUsd, feePercent, processingPercent = MARKETPLACE_PROCESSING_PERCENT_BNPL) {
3146
+ const priceCents = Math.max(0, Math.round(priceUsd * 100));
3147
+ const taxCents = Math.round(priceCents * MARKETPLACE_ASSUMED_TAX_PERCENT / 100);
3148
+ const buyerPaysCents = priceCents + taxCents;
3149
+ const platformFeeCents = Math.round(priceCents * feePercent / 100);
3150
+ const sellerReceivesCents = priceCents - platformFeeCents;
3151
+ // A zero-price listing takes no payment at all, so there is no fee to pay.
3152
+ const processingCents = priceCents === 0 ? 0 : Math.round(buyerPaysCents * processingPercent / 100 + MARKETPLACE_PROCESSING_FIXED_CENTS);
3153
+ return {
3154
+ priceCents,
3155
+ taxCents,
3156
+ buyerPaysCents,
3157
+ sellerReceivesCents,
3158
+ platformFeeCents,
3159
+ processingCents,
3160
+ platformNetCents: platformFeeCents - processingCents
3161
+ };
3162
+ }
3163
+ /**
3164
+ * The cheapest WHOLE-DOLLAR price at which the platform does not lose money on
3165
+ * a sale (AGL-2343).
3166
+ *
3167
+ * Whole dollars because every publish route rounds `priceUsd` to one, so a
3168
+ * fractional break-even is not a price anyone can actually set. Searched
3169
+ * rather than solved algebraically so that it can never disagree with
3170
+ * `marketplaceSaleEconomics` — the rounding in there is what decides the
3171
+ * answer at these amounts, and a closed form would drift from it silently.
3172
+ *
3173
+ * THIS IS THE ENFORCED FLOOR: `marketplaceMinPriceUsd` returns this figure
3174
+ * and every publish door refuses a paid listing under it. Recorded in the Pricing
3175
+ * Decision Log and on AGL-2343.
3176
+ */ export function marketplaceBreakEvenUsd(feePercent = bindingMarketplaceFeePct(), processingPercent = MARKETPLACE_PROCESSING_PERCENT_BNPL) {
3177
+ // The listing price ceiling is the marketplace plugin's
3178
+ // `MARKETPLACE_MAX_PRICE_USD`, which this library may not import (an app-tier
3179
+ // lib cannot depend on an addon). The loop only needs SOME bound, and the
3180
+ // answer is single digits at every rate the table holds, so a generous local
3181
+ // one costs nothing and cannot drift into a wrong figure.
3182
+ const searchCeilingUsd = 1000;
3183
+ for(let priceUsd = 1; priceUsd <= searchCeilingUsd; priceUsd += 1){
3184
+ if (marketplaceSaleEconomics(priceUsd, feePercent, processingPercent).platformNetCents >= 0) {
3185
+ return priceUsd;
3186
+ }
3187
+ }
3188
+ return searchCeilingUsd;
3189
+ }
3190
+ /**
3191
+ * THE MINIMUM PRICE a paid marketplace listing may carry (AGL-2343).
3192
+ *
3193
+ * The floor is not a chosen round number — it is the break-even price itself,
3194
+ * the
3195
+ * cheapest whole dollar at which `marketplaceSaleEconomics` stops returning a
3196
+ * negative platform net, computed at the take rate that breaks even LATEST and
3197
+ * the dearest payment method the live configuration enables. Today that is $3.
3198
+ *
3199
+ * DERIVED, NEVER RESTATED. A second hand-written copy of this number in a
3200
+ * publish route or a form is the artifact that decays into a route refusing a
3201
+ * price a form invited, so both the server-side validator
3202
+ * (`MARKETPLACE_MIN_PRICE_USD` in the marketplace model) and every price field
3203
+ * read this. If the payment method set changes, or the plan table's lowest
3204
+ * take rate moves, the floor moves with them on the next call.
3205
+ *
3206
+ * ZERO IS NOT BELOW THE FLOOR. A free listing takes no payment at all, so it
3207
+ * costs nothing to process; the floor is a minimum on PAID listings only.
3208
+ */ export function marketplaceMinPriceUsd() {
3209
+ return marketplaceBreakEvenUsd();
3210
+ }
3211
+ /**
3212
+ * Whether a price is a PAID price that the floor refuses (AGL-2343) — the one
3213
+ * predicate a form uses to mark its price field in error and hold its publish
3214
+ * button, so that the button and the server agree about what is publishable.
3215
+ *
3216
+ * Rounds first because every publish route does, and a form's value is a
3217
+ * string mid-type: `'2.6'` is stored as $3 and must read as allowed here, or
3218
+ * the button would refuse a price the route accepts.
3219
+ */ export function isBelowMarketplacePriceFloor(priceUsd) {
3220
+ const price = Math.round(Number(priceUsd) || 0);
3221
+ return price > 0 && price < marketplaceMinPriceUsd();
3222
+ }
3223
+ /**
3224
+ * The sentence a publish form shows under its price field when the price is
3225
+ * below the floor (AGL-2343), or `undefined` when there is nothing to say.
3226
+ *
3227
+ * NOT ADVISORY ANY MORE. The same figure is enforced by every publish route
3228
+ * through `publishPreconditionRefusal`, so this text has to read as a
3229
+ * requirement rather than a warning — a publisher who is told "consider
3230
+ * raising it" and then refused has been lied to by the form. It still explains
3231
+ * WHY, because a bare minimum with no arithmetic behind it reads as an
3232
+ * arbitrary tax on cheap listings.
3233
+ *
3234
+ * Shared by every publish form rather than written into each, because the
3235
+ * figure has to be the same one in all of them and the wording is the only
3236
+ * place a publisher ever sees it.
3237
+ *
3238
+ * Quoted at the take rate that breaks even LATEST, so the sentence holds for a
3239
+ * free-plan publisher too.
3240
+ */ export function marketplacePriceCostNote(priceUsd) {
3241
+ const price = Math.round(Number(priceUsd) || 0);
3242
+ const minimum = marketplaceMinPriceUsd();
3243
+ // A free listing takes no payment, so it costs nothing to process and has
3244
+ // nothing to warn about — silence there is the point, not an oversight.
3245
+ if (!isBelowMarketplacePriceFloor(price)) return undefined;
3246
+ const { processingCents, platformFeeCents } = marketplaceSaleEconomics(price, bindingMarketplaceFeePct());
3247
+ const usd = (cents)=>`$${(cents / 100).toFixed(2)}`;
3248
+ return `Too low to publish. Processing a $${price} payment costs about ` + `${usd(processingCents)}, more than the ${usd(platformFeeCents)} platform ` + `fee — the sale would lose money. The minimum paid price is ` + `$${minimum}, and a free listing ($0) takes no payment at all.`;
3249
+ }
3250
+ /**
3251
+ * The standing helper sentence a price field shows when there is nothing wrong
3252
+ * (AGL-2343): the minimum stated up front, so a publisher meets it while
3253
+ * typing instead of discovering it as a refusal.
3254
+ *
3255
+ * A capability that is not surfaced in the console does not count as shipped,
3256
+ * and that applies to a REFUSAL as much as to a feature: a floor only a route
3257
+ * knows about is a trap.
3258
+ *
3259
+ * @param suffix whatever else that particular form needs to say, appended.
3260
+ */ export function marketplacePriceFloorHint(suffix) {
3261
+ return `0 for free, or $${marketplaceMinPriceUsd()} and up for a paid listing.` + (suffix ? ` ${suffix}` : '');
3262
+ }
3263
+ /**
3264
+ * The cheapest plan whose BASE features include `feature`, in ladder order —
3265
+ * the answer to "which plan do I need for this?", which is the one thing an
3266
+ * upsell has to say and the one thing no gate can hard-code without drifting
3267
+ * from the table it is quoting.
3268
+ *
3269
+ * Returns `undefined` when NO plan carries it on the base tier. That is not
3270
+ * an error: `eventCalendar` is an add-on purchased through
3271
+ * `resolveOrgEntitlements`' override path and is false on all eight tiers, so
3272
+ * "upgrade to X" is the wrong sentence for it. Callers must handle undefined
3273
+ * rather than defaulting to a tier name.
3274
+ *
3275
+ * Derived from PLAN_ENTITLEMENTS on every call rather than from a
3276
+ * hand-written map: a table of "feature → plan" is exactly the artifact that
3277
+ * decays silently when a tier's flags are re-cut, and it would decay into
3278
+ * pricing copy shown to a customer.
3279
+ *
3280
+ * Ladder order is `SELF_SERVE_PLANS` then `enterprise` — the same order the
3281
+ * plan grid renders — so an enterprise-only feature (`ssoEnabled`) reports
3282
+ * Enterprise rather than nothing.
3283
+ */ export function planGrantingFeature(feature) {
3284
+ return [
3285
+ ...SELF_SERVE_PLANS,
3286
+ 'enterprise'
3287
+ ].find((plan)=>Boolean(PLAN_ENTITLEMENTS[plan].features[feature]));
3288
+ }
3289
+ /**
3290
+ * The plan name an upsell should name, ready to interpolate — "Business",
3291
+ * or undefined when {@link planGrantingFeature} has no answer.
3292
+ */ export function planLabelGrantingFeature(feature) {
3293
+ const plan = planGrantingFeature(feature);
3294
+ return plan ? PLAN_LABELS[plan] : undefined;
3295
+ }
3296
+ /**
3297
+ * True when the org's plan (or overrides) enables the boolean feature. A
3298
+ * key outside `OrgFeatureFlags` is one a plugin declared (AGL-2940) and is
3299
+ * answered the same way.
3300
+ */ export function checkEntitlement(org, feature) {
3301
+ const features = resolveOrgEntitlements(org).features;
3302
+ return Boolean(features[feature]);
3303
+ }
3304
+ export { PLATFORM_BRANDING_PROFILE } from "./platform-brand.js";
3305
+ /** A non-empty trimmed string, else undefined — blanks never override a default. */ function cleanBrandString(value) {
3306
+ const trimmed = typeof value === 'string' ? value.trim() : '';
3307
+ return trimmed.length ? trimmed : undefined;
3308
+ }
3309
+ /**
3310
+ * The effective brand for an org (White-Label Phase 1). EVERY branded
3311
+ * surface — published-site branding, transactional email, console chrome —
3312
+ * MUST read the brand through this one resolver so a white-label org can
3313
+ * never partly-render as Aglyn (the multi-surface drift that dogged
3314
+ * `removeBranding`, which each surface re-derived on its own).
3315
+ *
3316
+ * When the org carries the `whiteLabel` entitlement (Agency tier, or an
3317
+ * Enterprise per-org override), the org's `brandingProfile` values win and
3318
+ * any field it leaves blank falls back to the Aglyn default. Without the
3319
+ * entitlement the stored profile is ignored entirely and the full Aglyn
3320
+ * brand is returned — so an org that white-labeled and then downgraded
3321
+ * reverts cleanly.
3322
+ */ export function resolveBrandingProfile(org) {
3323
+ var _ref, _cleanBrandString, _cleanBrandString1, _cleanBrandString2, _cleanBrandString3, _cleanBrandString4, _cleanBrandString5, _cleanBrandString6, _cleanBrandString7, _cleanBrandString8;
3324
+ if (!checkEntitlement(org, 'whiteLabel')) return PLATFORM_BRANDING_PROFILE;
3325
+ const profile = (_ref = org == null ? void 0 : org.brandingProfile) != null ? _ref : {};
3326
+ return {
3327
+ productName: (_cleanBrandString = cleanBrandString(profile.productName)) != null ? _cleanBrandString : PLATFORM_BRANDING_PROFILE.productName,
3328
+ // NO FALLBACK — and this became load-bearing the moment
3329
+ // `PLATFORM_BRANDING_PROFILE.logoUrl` stopped being null. Inheriting it
3330
+ // would stamp the PLATFORM's mark on a white-label org's published sites,
3331
+ // which is precisely the disclosure white-label is sold to prevent. A
3332
+ // white-label org that set no logo shows none.
3333
+ logoUrl: (_cleanBrandString1 = cleanBrandString(profile.logoUrl)) != null ? _cleanBrandString1 : null,
3334
+ faviconUrl: (_cleanBrandString2 = cleanBrandString(profile.faviconUrl)) != null ? _cleanBrandString2 : PLATFORM_BRANDING_PROFILE.faviconUrl,
3335
+ primaryColor: (_cleanBrandString3 = cleanBrandString(profile.primaryColor)) != null ? _cleanBrandString3 : PLATFORM_BRANDING_PROFILE.primaryColor,
3336
+ // NO FALLBACK, and this is the one field that gets none (AGL-2428).
3337
+ //
3338
+ // Only a white-label org reaches this line — the guard above hands every
3339
+ // other org the platform profile whole. So a blank here belongs to an
3340
+ // organization whose transactional mail reads as *theirs* to a recipient
3341
+ // who is *their* customer, and filling the gap with Aglyn's desk sends
3342
+ // that person to a support team that cannot help them, while disclosing
3343
+ // a vendor they were never told about. `platform-brand.ts` calls the
3344
+ // same substitution in the self-host case "a trap dressed as a default";
3345
+ // this is that trap with a different victim.
3346
+ //
3347
+ // Blank therefore means NO LINK, exactly as `emailLogoUrl` one field
3348
+ // down already means no logo. An email with a gap where a link should be
3349
+ // reads as broken; an email with no support line reads as plain, which
3350
+ // is the right appearance for an organization that has not set one.
3351
+ supportUrl: (_cleanBrandString4 = cleanBrandString(profile.supportUrl)) != null ? _cleanBrandString4 : null,
3352
+ // A white-label org has no separate "home" field to store, and the same
3353
+ // no-fallback rule applies for the same reason: `PLATFORM_HOME_URL` here
3354
+ // would put a link to the platform's marketing site on the badge of an
3355
+ // org that pays to conceal the platform exists. Their support URL is the
3356
+ // only destination they have told us about, which is exactly what this
3357
+ // badge already linked to before the split — so white-label badges are
3358
+ // unchanged, and only the platform's own badge stops opening a mail
3359
+ // client.
3360
+ homeUrl: (_cleanBrandString5 = cleanBrandString(profile.supportUrl)) != null ? _cleanBrandString5 : null,
3361
+ fromName: (_cleanBrandString6 = cleanBrandString(profile.fromName)) != null ? _cleanBrandString6 : PLATFORM_BRANDING_PROFILE.fromName,
3362
+ emailLogoUrl: (_cleanBrandString7 = cleanBrandString(profile.emailLogoUrl)) != null ? _cleanBrandString7 : PLATFORM_BRANDING_PROFILE.emailLogoUrl,
3363
+ customConsoleDomain: (_cleanBrandString8 = cleanBrandString(profile.customConsoleDomain)) != null ? _cleanBrandString8 : PLATFORM_BRANDING_PROFILE.customConsoleDomain
3364
+ };
3365
+ }
3366
+ /**
3367
+ * The brand as MERGE TOKENS, for a staff-designed system-email template
3368
+ * (AGL-2139).
3369
+ *
3370
+ * White-label inverted precisely when staff published a template, which is
3371
+ * that feature's normal steady state. Every org-context sender has the shape
3372
+ * `subject: designed?.subject ?? <branded fallback>` — the DESIGNED template
3373
+ * wins — and the catalog copy hard-coded "Aglyn" throughout. There was no
3374
+ * brand token to design against either: the merge maps carried `org.name`,
3375
+ * `invite.role`, `signInUrl` and friends and nothing about the brand, and
3376
+ * `blankUnresolvedTokens` BLANKS any token the caller did not supply. So a
3377
+ * designer who reached for `{{brand.productName}}` would have shipped an
3378
+ * email with a hole in the sentence.
3379
+ *
3380
+ * Token bodies are namespaced `brand.*` so a designer can tell them from the
3381
+ * per-send values, and they resolve for EVERY org — Aglyn's own included,
3382
+ * since `PLATFORM_BRANDING_PROFILE.productName` is `'Aglyn'`. That is what makes
3383
+ * a single template correct for both populations instead of one hard-coded
3384
+ * for the majority.
3385
+ *
3386
+ * The email LOGO is deliberately absent. It is structural — `renderEmailHtml`
3387
+ * emits it as a header row above the designed body, or emits nothing — so it
3388
+ * travels as an option rather than as a token a template could forget to
3389
+ * place, and there is no sample URL a staff test-send could render without
3390
+ * showing a broken image.
3391
+ */ export function brandMergeTokens(branding) {
3392
+ var _branding_supportUrl;
3393
+ return {
3394
+ 'brand.productName': branding.productName,
3395
+ 'brand.fromName': branding.fromName,
3396
+ // EMPTY, never omitted, when the org has no Support URL (AGL-2428).
3397
+ //
3398
+ // `renderLoadedSystemEmail` merges these OVER `DEFAULT_BRAND_TOKENS`,
3399
+ // which carries the platform's own support URL for the genuinely
3400
+ // org-less senders. Dropping the key here would let that default show
3401
+ // through and mail a white-label org's customer a link to Aglyn — the
3402
+ // exact leak this closes — so the key must be present and empty in order
3403
+ // to overwrite it. A designed template that interpolates the token
3404
+ // renders nothing where it sits.
3405
+ 'brand.supportUrl': (_branding_supportUrl = branding.supportUrl) != null ? _branding_supportUrl : ''
3406
+ };
3407
+ }
3408
+ /**
3409
+ * The value of the `<meta name="generator">` tag and of the `x-powered-by`
3410
+ * header on published sites (AGL-2088) — the canonical CMS signal WordPress,
3411
+ * Squarespace, Drupal and Ghost all ship, and the thing tech-stack detectors
3412
+ * key on.
3413
+ *
3414
+ * Deliberately VERSIONLESS. The accidental headers this replaced carried
3415
+ * `1.0.0-alpha.0` and a Node version, which dated every deployment and named
3416
+ * our runtime to no consumer. A detector needs the product name and nothing
3417
+ * else.
3418
+ */ export const PLATFORM_GENERATOR_NAME = PLATFORM_BRAND_NAME;
3419
+ /**
3420
+ * May this org's published pages say they were built with Aglyn? (AGL-2088)
3421
+ *
3422
+ * The gate for the `<meta name="generator">` tag and the `x-powered-by`
3423
+ * header. It reads the SAME entitlement `resolveBrandingProfile` reads —
3424
+ * `whiteLabel` — rather than inventing a parallel one, so an agency's
3425
+ * concealment promise has exactly one definition.
3426
+ *
3427
+ * WHY `whiteLabel` AND NOT `showBranding`. The "Made with Aglyn" badge is
3428
+ * gated on `removeBranding`, which every plan above free grants, so reusing
3429
+ * that gate would confine the signal to free-tier sites — the corpus this
3430
+ * exists to build would be almost empty on day one. The two promises are also
3431
+ * different in kind: `removeBranding` buys a page with no Aglyn credit on it,
3432
+ * an aesthetic commitment about what a VISITOR sees; `whiteLabel` buys
3433
+ * concealment of who built the site, which is the promise a machine-readable
3434
+ * fingerprint actually breaks. A paying customer on a mid plan has bought a
3435
+ * clean page, not anonymity.
3436
+ *
3437
+ * ⚠️ AN UNRESOLVED ORG SUPPRESSES, and this is the whole reason the function
3438
+ * exists instead of a bare `!checkEntitlement(org, 'whiteLabel')` at each call
3439
+ * site. `resolveOrgEntitlements(null)` resolves to the FREE plan — a loading
3440
+ * default answering a question it was never asked — so a null org reads as
3441
+ * "no white-label" and would EMIT. That null is not hypothetical: the tenant's
3442
+ * `getOrgBilling` fails open with `org: null` on a Firestore error, so a
3443
+ * transient read failure on an Agency site would have stamped the generator
3444
+ * tag onto it. The asymmetry decides the direction: suppressing wrongly costs
3445
+ * us one detection sample, emitting wrongly breaks a paid promise on a
3446
+ * customer's own domain. So the org must be PRESENT and lack the entitlement;
3447
+ * absent is not the same as free here.
3448
+ */ export function showsPlatformAttribution(org) {
3449
+ // `{}` is treated as unresolved alongside `null`, and that is not
3450
+ // pedantry: no org doc that was actually READ is empty — the converter
3451
+ // yields `$id` at minimum — so an empty object only ever arrives from a
3452
+ // placeholder or a loading default, the same shape `useBranding` hands back
3453
+ // before its read lands. It resolves to the free plan like every other
3454
+ // absent org, which is the right default for a quota and the wrong one for
3455
+ // a disclosure.
3456
+ if (!org || Object.keys(org).length === 0) return false;
3457
+ return !checkEntitlement(org, 'whiteLabel');
3458
+ }
3459
+ /**
3460
+ * Whether the org has actually **configured** a white-label brand (AGL-1110) —
3461
+ * a non-empty product name or logo. White-label chrome should activate only
3462
+ * when this is true, not merely because the plan grants the `whiteLabel`
3463
+ * entitlement: an Agency org (or a comped Enterprise org) that never set up a
3464
+ * brand keeps the plain Aglyn wordmark — no product-name-text render, no
3465
+ * flash — until it opts in on the branding page.
3466
+ */ export function hasBrandingProfile(org) {
3467
+ const profile = org == null ? void 0 : org.brandingProfile;
3468
+ if (!profile) return false;
3469
+ return !!(cleanBrandString(profile.productName) || cleanBrandString(profile.logoUrl));
3470
+ }
3471
+ /**
3472
+ * Seat quota check (AGL-112): seats differ from plain quotas because orgs
3473
+ * can buy addon seats (`org.seatAddons`) up to a per-plan hard max —
3474
+ * beyond the max the only path is upgrading the plan. `managers` counts
3475
+ * org-manager seats org-wide; `members` counts per-site COLLABORATORS
3476
+ * (`hosts/{id}/members`, viewer/editor/admin — legacy key name, AGL-888).
3477
+ * End-user member accounts (`siteMembers`) are unlimited and never pass
3478
+ * through here.
3479
+ *
3480
+ * SINCE AGL-2439 THIS IS THE PLAN'S CAP FOR `members`, with no purchased
3481
+ * seats in it — see the comment at the add below. A per-site answer must come
3482
+ * from `checkHostCollaboratorQuota(org, hostId, used)`, which is the only
3483
+ * thing that knows which site holds which pool seat. Every caller that has a
3484
+ * `hostId` must use that one; this stays correct for a plan-level readout
3485
+ * (what the plan includes per site, before anything is assigned).
3486
+ */ export function checkSeatQuota(org, kind, currentUsage) {
3487
+ var _resolvePurchasedAddons_kind;
3488
+ const entitlements = resolveOrgEntitlements(org);
3489
+ const pricing = resolvePlanPricing(org);
3490
+ const included = kind === 'managers' ? entitlements.managersPerOrg : entitlements.membersPerHost;
3491
+ const maxSeats = kind === 'managers' ? entitlements.maxManagersPerOrg : entitlements.maxMembersPerHost;
3492
+ const addonPriceUsd = kind === 'managers' ? pricing.extraSeatMonthlyUsd : pricing.extraCollaboratorMonthlyUsd;
3493
+ // `members` IS NOT ADDED HERE, and this is the fix, not an omission
3494
+ // (AGL-2439) — the AGL-1775 shape, applied to
3495
+ // the key that never got it. `seatAddons.members` is an ORG-LEVEL purchased
3496
+ // quantity and `membersPerHost` is enforced PER SITE, so folding one into
3497
+ // the other handed every site the whole purchase: an org with 20 sites that
3498
+ // bought ONE extra collaborator seat received 20 seats for one seat's price.
3499
+ // The quantity is now a POOL and `org.collaboratorAllocations` says which
3500
+ // site holds each seat. What this function returns for `members` is
3501
+ // therefore **the plan's cap alone** — exactly what an unallocated site must
3502
+ // resolve to. Per-site capacity comes from `checkHostCollaboratorQuota`;
3503
+ // nothing else may add the pool back in.
3504
+ //
3505
+ // `managers` is unaffected and still adds: `managersPerOrg` really is
3506
+ // org-level, so an org-level purchase raising an org-level cap is correct
3507
+ // there. That difference is the whole reason this is a `kind` switch rather
3508
+ // than one rule.
3509
+ const purchased = kind === 'managers' ? Math.max(0, (_resolvePurchasedAddons_kind = resolvePurchasedAddons(org)[kind]) != null ? _resolvePurchasedAddons_kind : 0) : 0;
3510
+ const limit = Math.min(included + purchased, maxSeats);
3511
+ return {
3512
+ allowed: currentUsage < limit,
3513
+ limit,
3514
+ remaining: Math.max(0, limit - currentUsage),
3515
+ included,
3516
+ purchased,
3517
+ maxSeats,
3518
+ upgradeRequired: addonPriceUsd === null || limit >= maxSeats,
3519
+ addonPriceUsd
3520
+ };
3521
+ }
3522
+ export function checkQuota(org, quota, currentUsage) {
3523
+ const limit = resolveOrgEntitlements(org)[quota];
3524
+ return {
3525
+ allowed: currentUsage < limit,
3526
+ limit,
3527
+ remaining: Math.max(0, limit - currentUsage)
3528
+ };
3529
+ }
3530
+ /**
3531
+ * Dataset quota check (AGL-132/240), mirroring `checkSeatQuota`: orgs can
3532
+ * buy addon datasets (`org.seatAddons.datasets`, org-wide) up to the
3533
+ * plan's hard max; beyond the max the only path is upgrading.
3534
+ */ export function checkDatasetQuota(org, currentUsage) {
3535
+ var _resolvePurchasedAddons_datasets;
3536
+ const entitlements = resolveOrgEntitlements(org);
3537
+ const pricing = resolvePlanPricing(org);
3538
+ const included = entitlements.datasetsPerOrg;
3539
+ const maxDatasets = entitlements.maxDatasetsPerOrg;
3540
+ const addonPriceUsd = pricing.extraDatasetMonthlyUsd;
3541
+ const purchased = Math.max(0, (_resolvePurchasedAddons_datasets = resolvePurchasedAddons(org).datasets) != null ? _resolvePurchasedAddons_datasets : 0);
3542
+ const limit = Math.min(included + purchased, maxDatasets);
3543
+ return {
3544
+ allowed: currentUsage < limit,
3545
+ limit,
3546
+ remaining: Math.max(0, limit - currentUsage),
3547
+ included,
3548
+ purchased,
3549
+ maxDatasets,
3550
+ upgradeRequired: addonPriceUsd === null || limit >= maxDatasets,
3551
+ addonPriceUsd
3552
+ };
3553
+ }
3554
+ /**
3555
+ * Org dataset-storage meter (AGL-240): aggregate stored document bytes
3556
+ * across `orgs/{orgId}/datasets`. Plans with an `extraDataGbMonthlyUsd`
3557
+ * rate meter the overage onto the monthly invoice (cost-plus, AGL-41);
3558
+ * plans without one (free) hard-block at the included size.
3559
+ */ export function checkDataStorageQuota(org, usedMb) {
3560
+ const entitlements = resolveOrgEntitlements(org);
3561
+ const pricing = resolvePlanPricing(org);
3562
+ const includedMb = entitlements.dataStorageMbPerOrg;
3563
+ const overageRateUsd = pricing.extraDataGbMonthlyUsd;
3564
+ const used = Math.max(0, usedMb);
3565
+ const overageMb = Math.max(0, used - includedMb);
3566
+ const overageGb = overageMb / 1024;
3567
+ return {
3568
+ allowed: overageRateUsd !== null ? true : used < includedMb,
3569
+ includedMb,
3570
+ usedMb: used,
3571
+ remainingMb: Math.max(0, includedMb - used),
3572
+ overageGb,
3573
+ overageMonthlyUsd: overageRateUsd === null ? 0 : Math.round(overageGb * overageRateUsd * 100) / 100,
3574
+ overageRateUsd
3575
+ };
3576
+ }
3577
+ /**
3578
+ * Customer REST API request meter (AGL-634): the monthly request count for an
3579
+ * org. Business and every tier above it carry `apiAccess` and an
3580
+ * `extraApiRequestsUsdPer1k` rate, so requests past the included quota meter
3581
+ * onto the monthly invoice
3582
+ * (cost-plus, like storage overage — never a hard wall mid-integration); plans
3583
+ * without API access have `included: 0` and always block.
3584
+ */ export function checkApiRequestQuota(org, usedRequests) {
3585
+ const entitlements = resolveOrgEntitlements(org);
3586
+ const pricing = resolvePlanPricing(org);
3587
+ const included = entitlements.apiRequestsPerMonth;
3588
+ const overageRateUsd = pricing.extraApiRequestsUsdPer1k;
3589
+ const used = Math.max(0, usedRequests);
3590
+ const overageRequests = Math.max(0, used - included);
3591
+ return {
3592
+ allowed: overageRateUsd !== null ? true : used < included,
3593
+ included,
3594
+ used,
3595
+ remaining: Math.max(0, included - used),
3596
+ overageRequests,
3597
+ overageMonthlyUsd: overageRateUsd === null ? 0 : Math.round(overageRequests / 1000 * overageRateUsd * 100) / 100,
3598
+ overageRateUsd
3599
+ };
3600
+ }
3601
+ /**
3602
+ * CRM records band meter (AGL-890, widened in AGL-2611): contacts, companies
3603
+ * and deals, counted together across the org and measured against the ONE
3604
+ * band `contactsPerHost` names. Paid plans carry an `extraContactsUsdPer1k`
3605
+ * rate, so records past the included band meter onto the monthly invoice
3606
+ * (cost-plus, like storage and API overage — a growing audience is never
3607
+ * dropped); free has no rate and hard-bands at the included count, refusing
3608
+ * the next record of any of the three kinds.
3609
+ *
3610
+ * ONE band for three collections, and not three bands, because a company or
3611
+ * a deal costs less to hold than a contact — a ~2 KB document with few index
3612
+ * entries and no dynamic-list scan — so counting them at the contacts rate
3613
+ * over-covers them, and a second and third number to explain would buy
3614
+ * nothing a customer could act on. The caller passes the SUM; the server
3615
+ * helper that takes the three aggregate reads is `countCrmRecords`, and the
3616
+ * billing meter shows the three components in its caption so the sum is
3617
+ * legible rather than opaque. Tasks and activities are not counted: they
3618
+ * are bounded by human effort, and the activity log is capped per record by
3619
+ * `CRM_ACTIVITIES_PER_RECORD_CEILING` instead.
3620
+ *
3621
+ * End-user member ACCOUNTS are unlimited on every plan (AGL-889) — this
3622
+ * meters only the CRM record.
3623
+ */ export function checkCrmRecordsQuota(org, usedRecords) {
3624
+ const entitlements = resolveOrgEntitlements(org);
3625
+ const pricing = resolvePlanPricing(org);
3626
+ const included = entitlements.contactsPerHost;
3627
+ const overageRateUsd = pricing.extraContactsUsdPer1k;
3628
+ const used = Math.max(0, usedRecords);
3629
+ const overageRecords = Math.max(0, used - included);
3630
+ return {
3631
+ allowed: overageRateUsd !== null ? true : used < included,
3632
+ included,
3633
+ used,
3634
+ remaining: Math.max(0, included - used),
3635
+ overageRecords,
3636
+ overageMonthlyUsd: overageRateUsd === null ? 0 : Math.round(overageRecords / 1000 * overageRateUsd * 100) / 100,
3637
+ overageRateUsd
3638
+ };
3639
+ }
3640
+ /**
3641
+ * The name the band's gate carried while it counted contacts alone. The SAME
3642
+ * function — every caller that still says "contact" is measuring the records
3643
+ * band and must hand it the summed count, not the contacts aggregate.
3644
+ * @deprecated Call `checkCrmRecordsQuota`.
3645
+ */ export const checkContactQuota = checkCrmRecordsQuota;
3646
+ /**
3647
+ * `orgs/{orgId}/crmEmailUsage/{YYYY-MM-DD}` — the durable per-day counter of
3648
+ * one-to-one CRM emails, `{ count, day, updatedAt }`, server-written only and
3649
+ * member-readable, exactly as `apiUsage/{month}` is for API requests.
3650
+ */ export const CRM_EMAIL_USAGE_COLLECTION = 'crmEmailUsage';
3651
+ /**
3652
+ * The document id of today's one-to-one email counter, `YYYY-MM-DD` in UTC.
3653
+ *
3654
+ * UTC rather than the workspace's zone, for the reason `apiUsageMonth` keys
3655
+ * the API counter in UTC: the cap is enforced on a server that has no zone
3656
+ * of the customer's to read, and a boundary that moved with a browser's
3657
+ * clock would let one day's sends be counted against two.
3658
+ */ export function crmEmailUsageDayKey(now = new Date()) {
3659
+ return now.toISOString().slice(0, 10);
3660
+ }
3661
+ /**
3662
+ * One-to-one CRM email cap (AGL-2611): how many messages a workspace may
3663
+ * still send from CRM records today, mirroring `checkApiRequestQuota`.
3664
+ *
3665
+ * A HARD cap with no overage rate, on every tier — the one respect in which
3666
+ * it differs from the API meter it is shaped after. `allowed` is therefore
3667
+ * `used < included` outright. Every paid tier's cap is finite, Enterprise's
3668
+ * 2,000 included, and 0 on Free makes it false at zero, so a per-org
3669
+ * `features.crm` grant does not send until `crmEmailsPerDay` is raised
3670
+ * beside it.
3671
+ *
3672
+ * ## The contract for the send route
3673
+ *
3674
+ * The route that sends a one-to-one email takes the send as a SLOT on
3675
+ * `orgs/{orgId}/crmEmailUsage/{crmEmailUsageDayKey()}` inside one Firestore
3676
+ * transaction — `reserveCrmEmailSend` in `@aglyn/tenant-data-admin` reads
3677
+ * `count`, calls this with that figure, REFUSES with the plan reason when
3678
+ * `allowed` is false and otherwise writes `used + 1` in the same transaction
3679
+ * (AGL-2645). A read-then-write cap let two reps at the last slot both pass
3680
+ * one reading of it; the transaction re-runs the second on the raised
3681
+ * figure. The slot is taken before the message leaves and handed back by
3682
+ * `releaseCrmEmailSend` when the provider refuses it. A delivered send is
3683
+ * then recorded on the org's `emailSends` cost meter through
3684
+ * `recordEmailSends` as `transactional`, because every message the provider
3685
+ * charged for is priced into COGS and the campaign cap does not govern it.
3686
+ * The count enforced is the count billed, from one document, the way the
3687
+ * API meter works.
3688
+ *
3689
+ * Read the counter with `used` from the server: the console meter on the
3690
+ * billing page reads the same document, member-readable, so a rep is never
3691
+ * told their limit by being refused.
3692
+ */ export function checkCrmEmailQuota(org, usedToday, now = new Date()) {
3693
+ const included = resolveOrgEntitlements(org).crmEmailsPerDay;
3694
+ const used = Math.max(0, Math.floor(Number(usedToday) || 0));
3695
+ const resetsAt = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
3696
+ return {
3697
+ allowed: used < included,
3698
+ included,
3699
+ used,
3700
+ remaining: Math.max(0, included - used),
3701
+ resetsAt
3702
+ };
3703
+ }
3704
+ /**
3705
+ * Volume above the plan's included band, in emails.
3706
+ *
3707
+ * Transactional mail is never refused, so an org CAN and will finish a month
3708
+ * over its included allowance. That is not an error and not a failed send —
3709
+ * it is the overage the cap chose not to enforce, and recording it is what
3710
+ * keeps it from being a surprise at invoicing. `UNLIMITED` (and any
3711
+ * non-positive limit, which is how "no included allowance" is expressed)
3712
+ * yields 0 rather than the whole month's volume.
3713
+ *
3714
+ * DEFINED HERE and re-exported from `email-metering.ts` (AGL-2155's move):
3715
+ * the billing page renders this figure before anything is invoiced, and it
3716
+ * is a client component that cannot pull in the Admin SDK. One subtraction,
3717
+ * so the readout and the invoice cannot disagree.
3718
+ */ export function emailSendsOverage(total, limit) {
3719
+ const sends = Number(total);
3720
+ const included = Number(limit);
3721
+ if (!Number.isFinite(sends) || sends <= 0) return 0;
3722
+ if (!Number.isFinite(included) || included <= 0) return 0;
3723
+ return Math.max(0, sends - included);
3724
+ }
3725
+ /**
3726
+ * Prices email volume past a plan's included band.
3727
+ *
3728
+ * ## No `allowed`, deliberately
3729
+ *
3730
+ * Every other meter's helper here answers "may this proceed" alongside "what
3731
+ * does it cost", because on those axes one gate decides both. Email has two
3732
+ * gates that are not the same gate: `reserveCampaignEmailSends` refuses a
3733
+ * CAMPAIGN at the band inside a transaction, and nothing at all refuses
3734
+ * transactional mail at any tier. A field named `allowed` on this result
3735
+ * would be a plausible-looking thing for a sender to consult, and a
3736
+ * transactional sender consulting it is how a password reset stops going out.
3737
+ * So the result carries money and nothing else.
3738
+ *
3739
+ * ## Takes the overage, does not recompute it
3740
+ *
3741
+ * `emailSendsOverage` already derives the excess from the cost meter and the
3742
+ * resolved band, on the path that records it. Re-deriving it here from a raw
3743
+ * total would be a second overage model to drift from the first — which is
3744
+ * the shape AGL-1402 documented, where two figures that should have been one
3745
+ * disagreed by 20-45% for years.
3746
+ *
3747
+ * A null rate yields zero, structurally rather than by a check, which is what
3748
+ * keeps free at zero on this axis for the same reason it is zero on the other
3749
+ * three.
3750
+ */ export function priceEmailSendOverage(org, overageSends) {
3751
+ const overageRateUsd = resolvePlanPricing(org).extraEmailSendsUsdPer1k;
3752
+ const sends = Number(overageSends);
3753
+ const over = Number.isFinite(sends) && sends > 0 ? sends : 0;
3754
+ return {
3755
+ overageSends: over,
3756
+ overageMonthlyUsd: overageRateUsd === null ? 0 : Math.round(over / 1000 * overageRateUsd * 100) / 100,
3757
+ overageRateUsd
3758
+ };
3759
+ }
3760
+ /**
3761
+ * Is this org's plan subject to the metered infrastructure pass-through
3762
+ * (AGL-1280)? The single answer behind three otherwise-unrelated behaviours:
3763
+ * whether storage/bandwidth/form usage past the included band is BILLED, and
3764
+ * whether the form-submission cap is a wall or a meter.
3765
+ *
3766
+ * Deliberately a plan-level question, not an org-level one — an org with no
3767
+ * plan resolves as free, so an unknown org meters nothing. Billing usage to
3768
+ * someone with no subscription is the one error direction with no recovery.
3769
+ * A staff comp is that someone too (AGL-3034): it grants a paid plan with no
3770
+ * subscription behind it, so `resolvePlanPricing` answers `false` for it and
3771
+ * its bands are walls.
3772
+ */ export function planMetersInfraOverage(org) {
3773
+ return resolvePlanPricing(org).meteredInfraPassThrough;
3774
+ }
3775
+ /**
3776
+ * Monthly form-submission meter (AGL-76, made coherent in AGL-1280).
3777
+ *
3778
+ * Submissions were BOTH hard-capped at `formSubmissionsPerMonth` (429) and
3779
+ * metered — which made the published promise that usage past the
3780
+ * included band is billed rather than cut off false for the one meter where
3781
+ * being cut off costs the customer a lead. Now it mirrors the other three
3782
+ * overage meters exactly: a plan with the metered pass-through always accepts
3783
+ * and prices the excess (`estimateMonthlyUsageCost`); free has no
3784
+ * subscription to bill, so its included count stays a hard wall.
3785
+ *
3786
+ * Priced per ORG at cost-plus, but capped per SITE, because that is where the
3787
+ * counter lives (`hosts/{id}/counters/formSubmissions`). Callers gating a
3788
+ * single submission pass that site's month count.
3789
+ */ export function checkFormSubmissionQuota(org, usedThisMonth) {
3790
+ const included = resolveOrgEntitlements(org).formSubmissionsPerMonth;
3791
+ const metered = planMetersInfraOverage(org);
3792
+ const used = Math.max(0, usedThisMonth);
3793
+ return {
3794
+ allowed: metered ? true : used < included,
3795
+ included,
3796
+ used,
3797
+ remaining: Math.max(0, included - used),
3798
+ overageSubmissions: Math.max(0, used - included),
3799
+ metered
3800
+ };
3801
+ }
3802
+ /**
3803
+ * How far past a plan's own included band the abuse ceiling sits (AGL-1655).
3804
+ * Ten times what the customer bought is not growth — a site that legitimately
3805
+ * outgrows its band by an order of magnitude has an upgrade conversation
3806
+ * waiting, not a silent invoice.
3807
+ */ export const FORM_ABUSE_CEILING_MULTIPLE = 10;
3808
+ /**
3809
+ * Absolute floor for the abuse ceiling, so the small plans get real headroom
3810
+ * rather than a second, tighter plan limit wearing a different name. Starter
3811
+ * includes 200/month; 10× would be 2,000, which a genuinely busy site could
3812
+ * reach. 5,000 is ~167 submissions a day on ONE site — no legitimate site on
3813
+ * a small plan reaches that, and no small customer can be billed past
3814
+ * 5,000 × $0.000065 ≈ $0.33 of abuse.
3815
+ */ export const FORM_ABUSE_CEILING_FLOOR = 5000;
3816
+ /**
3817
+ * Ceiling for plans whose included count is UNLIMITED (Agency, Enterprise),
3818
+ * where a multiple has nothing to multiply. Kept at or above the ceiling of
3819
+ * every finite plan below it (Advanced includes 100,000 → 1,000,000) so the
3820
+ * ladder never inverts and a bigger plan never gets the smaller ceiling.
3821
+ */ export const FORM_ABUSE_CEILING_UNLIMITED = 1000000;
3822
+ /**
3823
+ * Per-site monthly abuse ceiling for form submissions (AGL-1655).
3824
+ *
3825
+ * Deliberately NOT part of `checkFormSubmissionQuota`. That function answers
3826
+ * a plan question — "did the customer buy this?" — and AGL-1280 correctly
3827
+ * made its answer on a metered plan always yes, because usage past the band
3828
+ * bills rather than blocks. This one answers a different question: "is this
3829
+ * still a customer's traffic at all?" Conflating them is how the plan gate
3830
+ * ended up as the anti-abuse control it was never designed to be, and how
3831
+ * `allowed: metered ? true : …` came to mean an unbounded bill.
3832
+ *
3833
+ * So the ceiling is containment, not capacity. It is far above anything a
3834
+ * legitimate site reaches, it is the same shape on every plan, and crossing
3835
+ * it is an INCIDENT: the caller is expected to refuse the write, leave the
3836
+ * billable counter alone, and make the trip visible to the site's managers
3837
+ * and to staff. A ceiling that silently absorbed submissions would only move
3838
+ * the free plan's lost-lead failure up to a bigger number.
3839
+ *
3840
+ * Evaluated against the same month counter the plan gate reads — which is
3841
+ * Admin-SDK-only (`counters` is excluded from every client write in
3842
+ * `cloud/firebase-firestore.rules`), so the count behind the ceiling cannot
3843
+ * be lowered to launder past it.
3844
+ */ export function checkFormSubmissionAbuseCeiling(org, usedThisMonth) {
3845
+ const included = resolveOrgEntitlements(org).formSubmissionsPerMonth;
3846
+ const ceiling = Number.isFinite(included) ? Math.max(FORM_ABUSE_CEILING_FLOOR, included * FORM_ABUSE_CEILING_MULTIPLE) : FORM_ABUSE_CEILING_UNLIMITED;
3847
+ const used = Math.max(0, usedThisMonth);
3848
+ return {
3849
+ exceeded: used >= ceiling,
3850
+ ceiling,
3851
+ used
3852
+ };
3853
+ }
3854
+ /**
3855
+ * Average transfer per page view (HTML + JS + a few images), the constant
3856
+ * that turns the analytics view counter into a bandwidth number and back.
3857
+ *
3858
+ * ## ⛔ HALF OF A PAIR. It moves with `perPageView`, or neither moves.
3859
+ *
3860
+ * This is the BYTES one page view weighs;
3861
+ * {@link ORG_COGS_UNIT_RATES_USD}`.perPageView` is the DOLLARS the same page
3862
+ * view costs. They are one physical measurement written in two units, and
3863
+ * what every `bandwidthGb` band is actually sized against is neither of them
3864
+ * alone but their quotient — `(1 GB ÷ this) × perPageView`, the cost of a
3865
+ * gigabyte. That quotient is stable even while the page's weight is not,
3866
+ * which is precisely why moving one half re-prices every band on the ladder
3867
+ * without touching a band: a heavier page costs more per view and buys fewer
3868
+ * views per gigabyte, and the two cancel.
3869
+ *
3870
+ * Both are therefore stated against the SAME weight — **1012.8 KB**, the
3871
+ * basis recorded in `tools/tenant-page-budget.json` and recoverable from the
3872
+ * rate itself — and `npm run check:page-view-rate` compares the two and
3873
+ * refuses a mismatch. There is no version of this constant that is correct
3874
+ * against a `perPageView` calibrated for a different page.
3875
+ *
3876
+ * The basis is FIRST-PARTY encoded bytes at settle, because that is what an
3877
+ * egress bill is denominated in: third-party tags leave someone else's
3878
+ * network and cost the platform nothing, so counting them here would inflate
3879
+ * a customer's measured bandwidth with bytes nobody pays us for.
3880
+ *
3881
+ * Lives HERE rather than beside the metering rates (AGL-2155). It used to sit
3882
+ * in `apps/console/utils/usage-metering.ts`, which the console imports and the
3883
+ * tenant app cannot — and the bandwidth ceiling below has to be evaluated in
3884
+ * the tenant app, at the beacon that writes the counter. Re-deriving the
3885
+ * conversion there would have been a fourth hand-rolled copy of
3886
+ * `× 1024³ / page weight`, which is the exact drift AGL-1371 collapsed into
3887
+ * one function. `usage-metering` now re-exports these three, so every existing
3888
+ * importer is unchanged and there is still one definition.
3889
+ */ export const ESTIMATED_PAGE_TRANSFER_BYTES = 1012.8 * 1024;
3890
+ /**
3891
+ * Bandwidth ⇄ page views: the one conversion three surfaces used to each
3892
+ * write out by hand (AGL-1371).
3893
+ *
3894
+ * `bandwidthGb` is not a second cap next to metered page views — it IS the
3895
+ * included band of the page-view meter, expressed in the unit customers
3896
+ * understand. It is therefore used in both directions: forward by
3897
+ * `meteredIncludedAllowance` to size the band the invoice subtracts, backward
3898
+ * by the usage-alerts cron and the console meter to render live page views as
3899
+ * GB.
3900
+ */ export function pageViewsFromBandwidthGb(bandwidthGb) {
3901
+ return bandwidthGb * 1024 * 1024 * 1024 / ESTIMATED_PAGE_TRANSFER_BYTES;
3902
+ }
3903
+ /** @see pageViewsFromBandwidthGb — the same constant, the other way. */ export function bandwidthGbFromPageViews(pageViews) {
3904
+ return Math.max(0, Number(pageViews) || 0) * ESTIMATED_PAGE_TRANSFER_BYTES / (1024 * 1024 * 1024);
3905
+ }
3906
+ /**
3907
+ * How far past a plan's own included bandwidth the abuse ceiling sits
3908
+ * (AGL-2155). Three times the bandwidth the customer bought is not growth; it
3909
+ * is an upgrade conversation, or it is not the customer's traffic at all.
3910
+ *
3911
+ * It is a CAP, not a price, and it is lower than {@link
3912
+ * FORM_ABUSE_CEILING_MULTIPLE} because bandwidth is the largest cost line the
3913
+ * platform carries and the one a stranger can spend on the customer's behalf.
3914
+ * Past the band every 1,000 views bills $0.21 against about $0.16 of real
3915
+ * cost, so the tail is no longer a loss — but a scraper, a hotlinked asset or
3916
+ * a botnet still bills the account holder for traffic they did not ask for,
3917
+ * and the ceiling is what bounds that before staff look at it. At 10× the
3918
+ * band that tail was open-ended on Agency; at 3× it is bounded.
3919
+ */ export const BANDWIDTH_ABUSE_CEILING_MULTIPLE = 3;
3920
+ /**
3921
+ * Absolute floor for the bandwidth ceiling, in PAGE VIEWS per month, so the
3922
+ * small plans get real headroom instead of a second, tighter plan limit
3923
+ * wearing a different name.
3924
+ *
3925
+ * Free includes 2 GB ≈ 2,071 views; 3× would be ~6,212, which a genuinely
3926
+ * successful hobby site (a post that lands on Hacker News) reaches in an
3927
+ * afternoon and would be a miserable first experience of the platform.
3928
+ * 100,000 views/month ≈ 96.6 GB ≈ **$16 of real COGS** at
3929
+ * `METERED_UNIT_RATES_USD.perPageView` — well above the free band, and an
3930
+ * order of magnitude below the $162 a million views costs. It is the number
3931
+ * that makes "free stays free" true without making it stingy.
3932
+ */ export const BANDWIDTH_ABUSE_CEILING_FLOOR = 100000;
3933
+ /**
3934
+ * Ceiling for an org whose bandwidth band resolves to UNLIMITED — a
3935
+ * contracted per-org override, since no plan row carries the sentinel any
3936
+ * more — where a multiple has nothing to multiply. Kept at or above the
3937
+ * ceiling of every finite band below it so the ladder never inverts and a
3938
+ * bigger plan never inherits a smaller ceiling.
3939
+ */ export const BANDWIDTH_ABUSE_CEILING_UNLIMITED = 500000000;
3940
+ /**
3941
+ * Machine-readable marker for a bandwidth containment, so a capped response
3942
+ * is distinguishable from the maintenance/lockdown notices it shares a shell
3943
+ * with. Deliberately NOT a {@link LockdownReasonCode}: a lockdown is a staff
3944
+ * takedown, this is a billing containment that clears itself at the month
3945
+ * boundary with nobody doing anything.
3946
+ */ export const BANDWIDTH_CEILING_CODE = 'bandwidth_ceiling';
3947
+ /**
3948
+ * Per-site monthly bandwidth abuse ceiling (AGL-2155).
3949
+ *
3950
+ * ## The hole this closes
3951
+ *
3952
+ * Every other free dimension had a runtime brace — `mediaStorageGate`,
3953
+ * `checkFormSubmissionQuota`, a zero band for dataset storage and API, a
3954
+ * hard band for contacts. Bandwidth had **none**: nothing anywhere refused a
3955
+ * page view, so a free site that went viral served a million views — roughly
3956
+ * **$162 of real COGS** — with no wall, no throttle and no alert. It was
3957
+ * protected only by the structural zero on the billing side (free carries no
3958
+ * rate, so the invoice stays $0), which protects the *bill* and not the
3959
+ * *bleeding*. `free-tier-never-billed.spec.ts` said so in its own table.
3960
+ *
3961
+ * ## Why an abuse ceiling and not a plan gate
3962
+ *
3963
+ * Exactly the distinction {@link checkFormSubmissionAbuseCeiling} draws, and
3964
+ * for the same reason. A plan gate answers "did the customer buy this?" — and
3965
+ * for a metered plan the answer past the band is *yes, and we bill it*. This
3966
+ * answers "is this still a customer's traffic at all?", and crossing it is an
3967
+ * INCIDENT rather than a quota: the site is flagged, staff are told, and the
3968
+ * render path degrades to a static notice instead of paying ~40 Firestore
3969
+ * reads and ~1,013 KB of egress per view.
3970
+ *
3971
+ * ## Where it is evaluated
3972
+ *
3973
+ * NOT in the render path. The page-view counter is written *after* the render
3974
+ * by `/api/analytics/collect`; that route evaluates this on a sampled cadence
3975
+ * and stamps `bandwidthCeiling` onto the host document. The render path reads
3976
+ * that field off a host doc **it already loads** (`load-page-data.ts` reads
3977
+ * `hostRes.host` for the lockdown branch), so the containment costs the happy
3978
+ * path exactly zero extra reads.
3979
+ *
3980
+ * An edge/CDN rule would be the other natural home and is deliberately not
3981
+ * the answer here: it is not a repo change, nothing in this suite could
3982
+ * verify it, and it cannot see which plan a host is on.
3983
+ */ export function checkBandwidthAbuseCeiling(org, usedPageViewsThisMonth) {
3984
+ var _resolveOrgEntitlements_bandwidthGb;
3985
+ const includedGb = (_resolveOrgEntitlements_bandwidthGb = resolveOrgEntitlements(org).bandwidthGb) != null ? _resolveOrgEntitlements_bandwidthGb : 0;
3986
+ const includedViews = pageViewsFromBandwidthGb(includedGb);
3987
+ const ceiling = Number.isFinite(includedViews) ? Math.max(BANDWIDTH_ABUSE_CEILING_FLOOR, Math.round(includedViews * BANDWIDTH_ABUSE_CEILING_MULTIPLE)) : BANDWIDTH_ABUSE_CEILING_UNLIMITED;
3988
+ const used = Math.max(0, Number(usedPageViewsThisMonth) || 0);
3989
+ return {
3990
+ exceeded: used >= ceiling,
3991
+ ceiling,
3992
+ used
3993
+ };
3994
+ }
3995
+ /**
3996
+ * Does crossing the ceiling **degrade what visitors see**, or only raise the
3997
+ * incident? (AGL-2155)
3998
+ *
3999
+ * Only on a plan that does NOT meter the infra pass-through — which today is
4000
+ * free/hobby, where the requirement is that free always actually stays free.
4001
+ * On free the traffic is uncompensated, so serving it is a
4002
+ * straight loss and the least destructive way to stop the bleeding is to stop
4003
+ * paying for the expensive render.
4004
+ *
4005
+ * On a metered plan the same traffic is *invoiced*, so taking a paying
4006
+ * customer's site off the air would trade a bill they agreed to for an outage
4007
+ * they did not. Their ceiling still trips, still flags the host and still
4008
+ * escalates to staff — it just does not change what a visitor gets. Whether a
4009
+ * paying host past its OWN ceiling should also be degraded is a product call
4010
+ * and is left open on purpose; flipping it is this one function.
4011
+ *
4012
+ * Enterprise does not meter either, and is NOT degraded, for the same reason
4013
+ * a metered plan is not: its traffic is paid for, by agreement rather than by
4014
+ * invoice. Since 2026-09-07 its bandwidth band is a finite fallback (twice
4015
+ * Agency's), so without this exclusion the ceiling an agreement never named
4016
+ * would have taken a contracted customer's site off the air three times past
4017
+ * a default nobody chose for them. It still trips, flags and pages.
4018
+ */ export function bandwidthCeilingDegradesRender(org) {
4019
+ return !planMetersInfraOverage(org) && !isCustomPricedPlan(resolvePlan(org));
4020
+ }
4021
+ /**
4022
+ * Read the host document's containment flag, tolerantly. Returns null unless
4023
+ * the flag is present, well-formed AND belongs to `month` — an August trip
4024
+ * must not silence a September visitor.
4025
+ */ export function normalizeHostBandwidthCeiling(host, month) {
4026
+ var _raw_month, _raw_ceiling, _raw_used, _raw_trippedAtMs;
4027
+ const raw = host == null ? void 0 : host['bandwidthCeiling'];
4028
+ if (!raw || typeof raw !== 'object') return null;
4029
+ if (String((_raw_month = raw.month) != null ? _raw_month : '') !== month) return null;
4030
+ const ceiling = Number((_raw_ceiling = raw.ceiling) != null ? _raw_ceiling : 0);
4031
+ if (!Number.isFinite(ceiling) || ceiling <= 0) return null;
4032
+ return {
4033
+ month,
4034
+ ceiling,
4035
+ used: Math.max(0, Number((_raw_used = raw.used) != null ? _raw_used : 0) || 0),
4036
+ trippedAtMs: Math.max(0, Number((_raw_trippedAtMs = raw.trippedAtMs) != null ? _raw_trippedAtMs : 0) || 0),
4037
+ degraded: raw.degraded === true
4038
+ };
4039
+ }
4040
+ /**
4041
+ * Is the render path required to serve the capped notice instead of the page?
4042
+ *
4043
+ * Both halves must hold: the flag is for THIS month, and the trip was
4044
+ * recorded as one that degrades. `degraded` is written by the evaluator from
4045
+ * {@link bandwidthCeilingDegradesRender} at trip time rather than re-derived
4046
+ * here, so a host that upgrades mid-month is not still being degraded by a
4047
+ * flag written while it was free — and, in the other direction, a flag from a
4048
+ * paying host can never take that host down.
4049
+ */ export function bandwidthCeilingDegradesHost(host, month) {
4050
+ var _normalizeHostBandwidthCeiling;
4051
+ return ((_normalizeHostBandwidthCeiling = normalizeHostBandwidthCeiling(host, month)) == null ? void 0 : _normalizeHostBandwidthCeiling.degraded) === true;
4052
+ }
4053
+ /** UTC `YYYY-MM`, the key both the beacon and the render path agree on. */ export function bandwidthCeilingMonthKey(now = new Date()) {
4054
+ return now.toISOString().slice(0, 7);
4055
+ }
4056
+ /**
4057
+ * Visitor-facing copy for a contained site. Says what happened and that it
4058
+ * ends, and names nothing about the plan, the count or the owner — this is
4059
+ * read by a stranger to the site, exactly like the form ceiling's refusal
4060
+ * body (AGL-1666).
4061
+ */ export function bandwidthCeilingNotice() {
4062
+ return {
4063
+ title: 'This site is temporarily unavailable',
4064
+ body: 'It has served more traffic this month than its plan includes. It will be back at the start of next month, or sooner if the owner upgrades.'
4065
+ };
4066
+ }
4067
+ function enforcementShape(atZero, atCeiling) {
4068
+ if (!atZero && !atCeiling) return 'always-blocks';
4069
+ if (atZero && atCeiling) return 'never-blocks';
4070
+ return 'measure';
4071
+ }
4072
+ /**
4073
+ * Enforcement shape of {@link checkDataStorageQuota} for this org, before
4074
+ * anything has been measured. @see QuotaEnforcementShape
4075
+ */ export function dataStorageEnforcementShape(org) {
4076
+ return enforcementShape(checkDataStorageQuota(org, 0).allowed, checkDataStorageQuota(org, Number.MAX_SAFE_INTEGER).allowed);
4077
+ }
4078
+ /**
4079
+ * Enforcement shape of {@link checkApiRequestQuota} for this org, before
4080
+ * anything has been measured. @see QuotaEnforcementShape
4081
+ */ export function apiRequestEnforcementShape(org) {
4082
+ return enforcementShape(checkApiRequestQuota(org, 0).allowed, checkApiRequestQuota(org, Number.MAX_SAFE_INTEGER).allowed);
4083
+ }
4084
+
4085
+ //# sourceMappingURL=plan-entitlements.js.map