@aglyn/aglyn 1.0.0-beta.220 → 1.0.0-beta.222

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 (358) hide show
  1. package/package.json +15 -11
  2. package/src/lib/app-utils/agent-openapi.js +1 -1
  3. package/src/lib/app-utils/agent-openapi.js.map +1 -1
  4. package/src/lib/app-utils/analytics-events.d.ts +3 -2
  5. package/src/lib/app-utils/analytics-events.js +3 -2
  6. package/src/lib/app-utils/analytics-events.js.map +1 -1
  7. package/src/lib/app-utils/api-plugins.d.ts +30 -0
  8. package/src/lib/app-utils/api-plugins.js +24 -1
  9. package/src/lib/app-utils/api-plugins.js.map +1 -1
  10. package/src/lib/app-utils/author-html.d.ts +1 -1
  11. package/src/lib/app-utils/author-html.js.map +1 -1
  12. package/src/lib/app-utils/bandwidth-cap.d.ts +12 -1
  13. package/src/lib/app-utils/bandwidth-cap.js +17 -1
  14. package/src/lib/app-utils/bandwidth-cap.js.map +1 -1
  15. package/src/lib/app-utils/compose-reusable-components.d.ts +19 -0
  16. package/src/lib/app-utils/compose-reusable-components.js +116 -26
  17. package/src/lib/app-utils/compose-reusable-components.js.map +1 -1
  18. package/src/lib/app-utils/console-routes.d.ts +4 -0
  19. package/src/lib/app-utils/console-routes.js +3 -0
  20. package/src/lib/app-utils/console-routes.js.map +1 -1
  21. package/src/lib/app-utils/contacts.d.ts +71 -0
  22. package/src/lib/app-utils/contacts.js +55 -0
  23. package/src/lib/app-utils/contacts.js.map +1 -1
  24. package/src/lib/app-utils/contexts.d.ts +1 -0
  25. package/src/lib/app-utils/contexts.js +1 -0
  26. package/src/lib/app-utils/contexts.js.map +1 -1
  27. package/src/lib/app-utils/crm-email-templates.js +21 -4
  28. package/src/lib/app-utils/crm-email-templates.js.map +1 -1
  29. package/src/lib/app-utils/crm-kinds.d.ts +2 -0
  30. package/src/lib/app-utils/crm-kinds.js +1 -0
  31. package/src/lib/app-utils/crm-kinds.js.map +1 -1
  32. package/src/lib/app-utils/crm.d.ts +1199 -67
  33. package/src/lib/app-utils/crm.js +1951 -310
  34. package/src/lib/app-utils/crm.js.map +1 -1
  35. package/src/lib/app-utils/csv-import.d.ts +10 -38
  36. package/src/lib/app-utils/csv-import.js +17 -42
  37. package/src/lib/app-utils/csv-import.js.map +1 -1
  38. package/src/lib/app-utils/csv.d.ts +32 -11
  39. package/src/lib/app-utils/csv.js +43 -5
  40. package/src/lib/app-utils/csv.js.map +1 -1
  41. package/src/lib/app-utils/default-site.d.ts +116 -0
  42. package/src/lib/app-utils/default-site.js +1024 -0
  43. package/src/lib/app-utils/default-site.js.map +1 -0
  44. package/src/lib/app-utils/docs-help.generated.d.ts +23 -23
  45. package/src/lib/app-utils/docs-help.generated.js +47 -10
  46. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  47. package/src/lib/app-utils/docs-index.generated.js +746 -151
  48. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  49. package/src/lib/app-utils/draft-placeholders.d.ts +76 -0
  50. package/src/lib/app-utils/draft-placeholders.js +115 -0
  51. package/src/lib/app-utils/draft-placeholders.js.map +1 -0
  52. package/src/lib/app-utils/entity-picker-context.d.ts +19 -15
  53. package/src/lib/app-utils/entity-picker-context.js +4 -4
  54. package/src/lib/app-utils/entity-picker-context.js.map +1 -1
  55. package/src/lib/app-utils/expand-repeatables.d.ts +66 -10
  56. package/src/lib/app-utils/expand-repeatables.js +88 -14
  57. package/src/lib/app-utils/expand-repeatables.js.map +1 -1
  58. package/src/lib/app-utils/forms.d.ts +33 -22
  59. package/src/lib/app-utils/forms.js +47 -28
  60. package/src/lib/app-utils/forms.js.map +1 -1
  61. package/src/lib/app-utils/full-use-cost.d.ts +277 -0
  62. package/src/lib/app-utils/full-use-cost.js +421 -0
  63. package/src/lib/app-utils/full-use-cost.js.map +1 -0
  64. package/src/lib/app-utils/health-report.js +33 -0
  65. package/src/lib/app-utils/health-report.js.map +1 -1
  66. package/src/lib/app-utils/leaving-notice.d.ts +96 -0
  67. package/src/lib/app-utils/leaving-notice.js +157 -0
  68. package/src/lib/app-utils/leaving-notice.js.map +1 -0
  69. package/src/lib/app-utils/llms-txt.d.ts +20 -0
  70. package/src/lib/app-utils/llms-txt.js +7 -1
  71. package/src/lib/app-utils/llms-txt.js.map +1 -1
  72. package/src/lib/app-utils/media-asset-facts.d.ts +16 -1
  73. package/src/lib/app-utils/media-asset-facts.js +40 -13
  74. package/src/lib/app-utils/media-asset-facts.js.map +1 -1
  75. package/src/lib/app-utils/media-bandwidth.d.ts +52 -0
  76. package/src/lib/app-utils/media-bandwidth.js +94 -0
  77. package/src/lib/app-utils/media-bandwidth.js.map +1 -0
  78. package/src/lib/app-utils/media-ref.d.ts +241 -33
  79. package/src/lib/app-utils/media-ref.js +324 -43
  80. package/src/lib/app-utils/media-ref.js.map +1 -1
  81. package/src/lib/app-utils/media-src.d.ts +8 -8
  82. package/src/lib/app-utils/media-src.js +27 -11
  83. package/src/lib/app-utils/media-src.js.map +1 -1
  84. package/src/lib/app-utils/media-takedown-reach.d.ts +4 -1
  85. package/src/lib/app-utils/media-takedown-reach.js +6 -3
  86. package/src/lib/app-utils/media-takedown-reach.js.map +1 -1
  87. package/src/lib/app-utils/notifications.d.ts +1 -1
  88. package/src/lib/app-utils/notifications.js +2 -0
  89. package/src/lib/app-utils/notifications.js.map +1 -1
  90. package/src/lib/app-utils/operator-alerts.js +17 -0
  91. package/src/lib/app-utils/operator-alerts.js.map +1 -1
  92. package/src/lib/app-utils/organizations.d.ts +51 -2
  93. package/src/lib/app-utils/organizations.js +56 -3
  94. package/src/lib/app-utils/organizations.js.map +1 -1
  95. package/src/lib/app-utils/page-record-sources.d.ts +87 -0
  96. package/src/lib/app-utils/page-record-sources.js +53 -0
  97. package/src/lib/app-utils/page-record-sources.js.map +1 -0
  98. package/src/lib/app-utils/paid-media-source.d.ts +3 -7
  99. package/src/lib/app-utils/paid-media-source.js +5 -19
  100. package/src/lib/app-utils/paid-media-source.js.map +1 -1
  101. package/src/lib/app-utils/person-erasure.d.ts +5 -2
  102. package/src/lib/app-utils/person-erasure.js.map +1 -1
  103. package/src/lib/app-utils/picklists.d.ts +187 -0
  104. package/src/lib/app-utils/picklists.js +320 -0
  105. package/src/lib/app-utils/picklists.js.map +1 -0
  106. package/src/lib/app-utils/plan-entitlements.d.ts +220 -50
  107. package/src/lib/app-utils/plan-entitlements.js +309 -115
  108. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  109. package/src/lib/app-utils/realm-host-surface.generated.js +2 -0
  110. package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
  111. package/src/lib/app-utils/release-flags.d.ts +1 -1
  112. package/src/lib/app-utils/release-flags.js +11 -0
  113. package/src/lib/app-utils/release-flags.js.map +1 -1
  114. package/src/lib/app-utils/rendered-widths.d.ts +121 -0
  115. package/src/lib/app-utils/rendered-widths.js +211 -0
  116. package/src/lib/app-utils/rendered-widths.js.map +1 -0
  117. package/src/lib/app-utils/repeat-sources.d.ts +2 -1
  118. package/src/lib/app-utils/repeat-sources.js.map +1 -1
  119. package/src/lib/app-utils/screen-link-context-value.d.ts +7 -0
  120. package/src/lib/app-utils/screen-link-context-value.js.map +1 -1
  121. package/src/lib/app-utils/screen-link-context.js +14 -2
  122. package/src/lib/app-utils/screen-link-context.js.map +1 -1
  123. package/src/lib/app-utils/screen-route.d.ts +157 -36
  124. package/src/lib/app-utils/screen-route.js +191 -41
  125. package/src/lib/app-utils/screen-route.js.map +1 -1
  126. package/src/lib/app-utils/seo-audit.d.ts +11 -1
  127. package/src/lib/app-utils/seo-audit.js +33 -13
  128. package/src/lib/app-utils/seo-audit.js.map +1 -1
  129. package/src/lib/app-utils/seo-keywords.d.ts +11 -0
  130. package/src/lib/app-utils/seo-keywords.js +19 -5
  131. package/src/lib/app-utils/seo-keywords.js.map +1 -1
  132. package/src/lib/app-utils/seo-page-facts.d.ts +47 -4
  133. package/src/lib/app-utils/seo-page-facts.js +136 -31
  134. package/src/lib/app-utils/seo-page-facts.js.map +1 -1
  135. package/src/lib/app-utils/seo-site-scan.d.ts +42 -4
  136. package/src/lib/app-utils/seo-site-scan.js +113 -12
  137. package/src/lib/app-utils/seo-site-scan.js.map +1 -1
  138. package/src/lib/app-utils/server.d.ts +5 -4
  139. package/src/lib/app-utils/server.js +14 -5
  140. package/src/lib/app-utils/server.js.map +1 -1
  141. package/src/lib/app-utils/site-icon-set.d.ts +195 -0
  142. package/src/lib/app-utils/site-icon-set.js +351 -0
  143. package/src/lib/app-utils/site-icon-set.js.map +1 -0
  144. package/src/lib/app-utils/site-interactions.d.ts +101 -4
  145. package/src/lib/app-utils/site-interactions.js +120 -4
  146. package/src/lib/app-utils/site-interactions.js.map +1 -1
  147. package/src/lib/app-utils/starter-template-nodes.d.ts +7 -0
  148. package/src/lib/app-utils/starter-template-nodes.js +3 -0
  149. package/src/lib/app-utils/starter-template-nodes.js.map +1 -1
  150. package/src/lib/app-utils/starter-templates.d.ts +5 -19
  151. package/src/lib/app-utils/starter-templates.js +5 -49
  152. package/src/lib/app-utils/starter-templates.js.map +1 -1
  153. package/src/lib/app-utils/transfer-launcher-context.d.ts +136 -0
  154. package/src/lib/app-utils/transfer-launcher-context.js +23 -0
  155. package/src/lib/app-utils/transfer-launcher-context.js.map +1 -0
  156. package/src/lib/app-utils/transfer-resume-import.d.ts +39 -0
  157. package/src/lib/app-utils/transfer-resume-import.js +128 -0
  158. package/src/lib/app-utils/transfer-resume-import.js.map +1 -0
  159. package/src/lib/app-utils/upgrade-proposal.d.ts +66 -0
  160. package/src/lib/app-utils/upgrade-proposal.js +95 -0
  161. package/src/lib/app-utils/upgrade-proposal.js.map +1 -0
  162. package/src/lib/app-utils/utc-month.d.ts +33 -0
  163. package/src/lib/app-utils/utc-month.js +35 -0
  164. package/src/lib/app-utils/utc-month.js.map +1 -0
  165. package/src/lib/app-utils/visitor-record-ceiling.d.ts +4 -4
  166. package/src/lib/app-utils/visitor-record-ceiling.js +4 -4
  167. package/src/lib/app-utils/visitor-record-ceiling.js.map +1 -1
  168. package/src/lib/app-utils/wire-nodes.d.ts +39 -19
  169. package/src/lib/app-utils/wire-nodes.js +120 -34
  170. package/src/lib/app-utils/wire-nodes.js.map +1 -1
  171. package/src/lib/data-transfer/access.d.ts +74 -0
  172. package/src/lib/data-transfer/access.js +93 -0
  173. package/src/lib/data-transfer/access.js.map +1 -0
  174. package/src/lib/data-transfer/derive.d.ts +246 -0
  175. package/src/lib/data-transfer/derive.js +1007 -0
  176. package/src/lib/data-transfer/derive.js.map +1 -0
  177. package/src/lib/data-transfer/export-file.d.ts +46 -0
  178. package/src/lib/data-transfer/export-file.js +146 -0
  179. package/src/lib/data-transfer/export-file.js.map +1 -0
  180. package/src/lib/data-transfer/field-catalog.d.ts +134 -0
  181. package/src/lib/data-transfer/field-catalog.js +278 -0
  182. package/src/lib/data-transfer/field-catalog.js.map +1 -0
  183. package/src/lib/data-transfer/header-match.d.ts +129 -0
  184. package/src/lib/data-transfer/header-match.js +367 -0
  185. package/src/lib/data-transfer/header-match.js.map +1 -0
  186. package/src/lib/data-transfer/index.d.ts +34 -0
  187. package/src/lib/data-transfer/index.js +40 -0
  188. package/src/lib/data-transfer/index.js.map +1 -0
  189. package/src/lib/data-transfer/job.d.ts +190 -0
  190. package/src/lib/data-transfer/job.js +229 -0
  191. package/src/lib/data-transfer/job.js.map +1 -0
  192. package/src/lib/data-transfer/lookup.d.ts +45 -0
  193. package/src/lib/data-transfer/lookup.js +71 -0
  194. package/src/lib/data-transfer/lookup.js.map +1 -0
  195. package/src/lib/data-transfer/match.d.ts +99 -0
  196. package/src/lib/data-transfer/match.js +244 -0
  197. package/src/lib/data-transfer/match.js.map +1 -0
  198. package/src/lib/data-transfer/package-plan.d.ts +205 -0
  199. package/src/lib/data-transfer/package-plan.js +475 -0
  200. package/src/lib/data-transfer/package-plan.js.map +1 -0
  201. package/src/lib/data-transfer/package.d.ts +181 -0
  202. package/src/lib/data-transfer/package.js +426 -0
  203. package/src/lib/data-transfer/package.js.map +1 -0
  204. package/src/lib/data-transfer/picklist-map.d.ts +139 -0
  205. package/src/lib/data-transfer/picklist-map.js +271 -0
  206. package/src/lib/data-transfer/picklist-map.js.map +1 -0
  207. package/src/lib/data-transfer/plan.d.ts +176 -0
  208. package/src/lib/data-transfer/plan.js +451 -0
  209. package/src/lib/data-transfer/plan.js.map +1 -0
  210. package/src/lib/data-transfer/policy.d.ts +170 -0
  211. package/src/lib/data-transfer/policy.js +378 -0
  212. package/src/lib/data-transfer/policy.js.map +1 -0
  213. package/src/lib/data-transfer/resource.d.ts +211 -0
  214. package/src/lib/data-transfer/resource.js +241 -0
  215. package/src/lib/data-transfer/resource.js.map +1 -0
  216. package/src/lib/data-transfer/review.d.ts +70 -0
  217. package/src/lib/data-transfer/review.js +233 -0
  218. package/src/lib/data-transfer/review.js.map +1 -0
  219. package/src/lib/data-transfer/similarity.d.ts +40 -0
  220. package/src/lib/data-transfer/similarity.js +106 -0
  221. package/src/lib/data-transfer/similarity.js.map +1 -0
  222. package/src/lib/data-transfer/site-package.d.ts +365 -0
  223. package/src/lib/data-transfer/site-package.js +1077 -0
  224. package/src/lib/data-transfer/site-package.js.map +1 -0
  225. package/src/lib/data-transfer/source.d.ts +74 -0
  226. package/src/lib/data-transfer/source.js +293 -0
  227. package/src/lib/data-transfer/source.js.map +1 -0
  228. package/src/lib/data-transfer/transfer-api.d.ts +898 -0
  229. package/src/lib/data-transfer/transfer-api.js +107 -0
  230. package/src/lib/data-transfer/transfer-api.js.map +1 -0
  231. package/src/lib/foundation/definitions/org-billing.types.d.ts +31 -0
  232. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  233. package/src/lib/foundation/definitions/organization.types.d.ts +42 -1
  234. package/src/lib/foundation/definitions/organization.types.js.map +1 -1
  235. package/src/lib/foundation/definitions/platform.types.d.ts +6 -4
  236. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  237. package/src/lib/foundation/index.d.ts +1 -1
  238. package/src/lib/foundation/index.js.map +1 -1
  239. package/src/lib/plugin-manager/feature-plugins.d.ts +91 -2
  240. package/src/lib/plugin-manager/feature-plugins.js +48 -3
  241. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  242. package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +56 -0
  243. package/src/lib/plugin-manager/first-party-plugins.generated.js +1132 -27
  244. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  245. package/src/lib/plugin-manager/index.d.ts +0 -1
  246. package/src/lib/plugin-manager/index.js +0 -1
  247. package/src/lib/plugin-manager/index.js.map +1 -1
  248. package/src/lib/plugin-manager/interaction-recipes.d.ts +142 -0
  249. package/src/lib/plugin-manager/interaction-recipes.js +104 -0
  250. package/src/lib/plugin-manager/interaction-recipes.js.map +1 -0
  251. package/src/lib/plugin-manager/interaction-step-checks.d.ts +66 -0
  252. package/src/lib/plugin-manager/interaction-step-checks.js +86 -0
  253. package/src/lib/plugin-manager/interaction-step-checks.js.map +1 -0
  254. package/src/lib/plugin-manager/plugin-artifact-types.d.ts +235 -0
  255. package/src/lib/plugin-manager/plugin-artifact-types.js +99 -0
  256. package/src/lib/plugin-manager/plugin-artifact-types.js.map +1 -0
  257. package/src/lib/plugin-manager/plugin-contact-capture.d.ts +8 -0
  258. package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
  259. package/src/lib/plugin-manager/plugin-conversion-credit.d.ts +216 -0
  260. package/src/lib/plugin-manager/plugin-conversion-credit.js +91 -0
  261. package/src/lib/plugin-manager/plugin-conversion-credit.js.map +1 -0
  262. package/src/lib/plugin-manager/plugin-entity-pickers.d.ts +83 -0
  263. package/src/lib/plugin-manager/plugin-entity-pickers.js +87 -0
  264. package/src/lib/plugin-manager/plugin-entity-pickers.js.map +1 -0
  265. package/src/lib/plugin-manager/plugin-host-resources.d.ts +6 -0
  266. package/src/lib/plugin-manager/plugin-host-resources.js +6 -1
  267. package/src/lib/plugin-manager/plugin-host-resources.js.map +1 -1
  268. package/src/lib/plugin-manager/plugin-intake-gates.d.ts +75 -0
  269. package/src/lib/plugin-manager/plugin-intake-gates.js +49 -0
  270. package/src/lib/plugin-manager/plugin-intake-gates.js.map +1 -0
  271. package/src/lib/plugin-manager/plugin-jobs.d.ts +5 -5
  272. package/src/lib/plugin-manager/plugin-jobs.js +7 -7
  273. package/src/lib/plugin-manager/plugin-jobs.js.map +1 -1
  274. package/src/lib/plugin-manager/plugin-live-paths.d.ts +70 -0
  275. package/src/lib/plugin-manager/plugin-live-paths.js +66 -0
  276. package/src/lib/plugin-manager/plugin-live-paths.js.map +1 -0
  277. package/src/lib/plugin-manager/plugin-org-erasure.d.ts +3 -2
  278. package/src/lib/plugin-manager/plugin-org-erasure.js +30 -5
  279. package/src/lib/plugin-manager/plugin-org-erasure.js.map +1 -1
  280. package/src/lib/plugin-manager/plugin-person-erasure.d.ts +68 -9
  281. package/src/lib/plugin-manager/plugin-person-erasure.js +196 -51
  282. package/src/lib/plugin-manager/plugin-person-erasure.js.map +1 -1
  283. package/src/lib/plugin-manager/plugin-person-records.d.ts +275 -0
  284. package/src/lib/plugin-manager/plugin-person-records.js +107 -0
  285. package/src/lib/plugin-manager/plugin-person-records.js.map +1 -0
  286. package/src/lib/plugin-manager/plugin-record-counts.d.ts +76 -0
  287. package/src/lib/plugin-manager/plugin-record-counts.js +49 -0
  288. package/src/lib/plugin-manager/plugin-record-counts.js.map +1 -0
  289. package/src/lib/plugin-manager/plugin-record-email-state.d.ts +26 -0
  290. package/src/lib/plugin-manager/plugin-record-email-state.js +16 -0
  291. package/src/lib/plugin-manager/plugin-record-email-state.js.map +1 -1
  292. package/src/lib/plugin-manager/plugin-record-index.d.ts +10 -0
  293. package/src/lib/plugin-manager/plugin-record-index.js.map +1 -1
  294. package/src/lib/plugin-manager/plugin-record-lists.d.ts +86 -6
  295. package/src/lib/plugin-manager/plugin-record-lists.js +42 -5
  296. package/src/lib/plugin-manager/plugin-record-lists.js.map +1 -1
  297. package/src/lib/plugin-manager/plugin-record-origin.d.ts +79 -0
  298. package/src/lib/plugin-manager/plugin-record-origin.js +46 -0
  299. package/src/lib/plugin-manager/plugin-record-origin.js.map +1 -0
  300. package/src/lib/plugin-manager/plugin-record-pages.d.ts +37 -0
  301. package/src/lib/plugin-manager/plugin-record-pages.js +52 -0
  302. package/src/lib/plugin-manager/plugin-record-pages.js.map +1 -0
  303. package/src/lib/plugin-manager/plugin-record-timeline.d.ts +60 -0
  304. package/src/lib/plugin-manager/plugin-record-timeline.js +16 -0
  305. package/src/lib/plugin-manager/plugin-record-timeline.js.map +1 -1
  306. package/src/lib/plugin-manager/plugin-resource-drafts.d.ts +5 -5
  307. package/src/lib/plugin-manager/plugin-resource-drafts.js.map +1 -1
  308. package/src/lib/plugin-manager/plugin-revocations.d.ts +64 -0
  309. package/src/lib/plugin-manager/plugin-revocations.js +47 -0
  310. package/src/lib/plugin-manager/plugin-revocations.js.map +1 -0
  311. package/src/lib/plugin-manager/plugin-send-tallies.d.ts +69 -0
  312. package/src/lib/plugin-manager/plugin-send-tallies.js +45 -0
  313. package/src/lib/plugin-manager/plugin-send-tallies.js.map +1 -0
  314. package/src/lib/plugin-manager/plugin-server-steps.d.ts +170 -0
  315. package/src/lib/plugin-manager/plugin-server-steps.js +116 -0
  316. package/src/lib/plugin-manager/plugin-server-steps.js.map +1 -0
  317. package/src/lib/plugin-manager/plugin-site-bundle.d.ts +40 -0
  318. package/src/lib/plugin-manager/plugin-site-bundle.js +4 -0
  319. package/src/lib/plugin-manager/plugin-site-bundle.js.map +1 -1
  320. package/src/lib/plugin-manager/plugin-site-export.d.ts +57 -1
  321. package/src/lib/plugin-manager/plugin-site-export.js +23 -10
  322. package/src/lib/plugin-manager/plugin-site-export.js.map +1 -1
  323. package/src/lib/plugin-manager/plugin-sitemap-readers.d.ts +146 -0
  324. package/src/lib/plugin-manager/plugin-sitemap-readers.js +110 -0
  325. package/src/lib/plugin-manager/plugin-sitemap-readers.js.map +1 -0
  326. package/src/lib/plugin-manager/plugin-transfer-resources.d.ts +714 -0
  327. package/src/lib/plugin-manager/plugin-transfer-resources.js +443 -0
  328. package/src/lib/plugin-manager/plugin-transfer-resources.js.map +1 -0
  329. package/src/lib/plugin-manager/plugin-usage-axes.d.ts +54 -5
  330. package/src/lib/plugin-manager/plugin-usage-axes.js +11 -3
  331. package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -1
  332. package/src/lib/plugin-manager/plugin-user-erasure.js +9 -1
  333. package/src/lib/plugin-manager/plugin-user-erasure.js.map +1 -1
  334. package/src/lib/plugin-manager/plugin-visitor-doors.d.ts +104 -0
  335. package/src/lib/plugin-manager/plugin-visitor-doors.js +108 -0
  336. package/src/lib/plugin-manager/plugin-visitor-doors.js.map +1 -0
  337. package/src/lib/plugin-manager/realm-host-aglyn.generated.js +2 -0
  338. package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
  339. package/src/lib/plugin-manager/repeat-rows.d.ts +5 -6
  340. package/src/lib/plugin-manager/repeat-rows.js.map +1 -1
  341. package/src/lib/app-utils/actions.d.ts +0 -310
  342. package/src/lib/app-utils/actions.js +0 -506
  343. package/src/lib/app-utils/actions.js.map +0 -1
  344. package/src/lib/app-utils/automation-placeholders.d.ts +0 -72
  345. package/src/lib/app-utils/automation-placeholders.js +0 -207
  346. package/src/lib/app-utils/automation-placeholders.js.map +0 -1
  347. package/src/lib/app-utils/dataset-models.d.ts +0 -339
  348. package/src/lib/app-utils/dataset-models.js +0 -722
  349. package/src/lib/app-utils/dataset-models.js.map +0 -1
  350. package/src/lib/app-utils/datasets.d.ts +0 -171
  351. package/src/lib/app-utils/datasets.js +0 -227
  352. package/src/lib/app-utils/datasets.js.map +0 -1
  353. package/src/lib/app-utils/form-abuse-ceiling.d.ts +0 -146
  354. package/src/lib/app-utils/form-abuse-ceiling.js +0 -158
  355. package/src/lib/app-utils/form-abuse-ceiling.js.map +0 -1
  356. package/src/lib/plugin-manager/order-fulfilment.d.ts +0 -142
  357. package/src/lib/plugin-manager/order-fulfilment.js +0 -104
  358. package/src/lib/plugin-manager/order-fulfilment.js.map +0 -1
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /*==========================================
17
+ * THE TRANSFER API — what the import wizard sends the job engine, and what
18
+ * it gets back.
19
+ *
20
+ * Eight console routes, all `POST` with a JSON body (`Content-Type:
21
+ * application/json`) carrying `orgId`, and the caller's ID token as
22
+ * `Authorization: Bearer …`:
23
+ *
24
+ * fields → what a resource offers: its fields, groups, match keys,
25
+ * locked rules and the person's remembered choices
26
+ * upload → a job in `draft`, the file stored (in parts when it is large)
27
+ * analyze → `analyzed`: the header proposal, samples, picklist values;
28
+ * given a mapping, every field's derivations and the matches
29
+ * plan → `planned`: the dry run over every row; `action: 'rows'` pages it
30
+ * apply → `applying` … `applied`: chunks written until the time budget
31
+ * runs out; called again until `done`
32
+ * status → the job, its progress and counts; `download: 'results'`
33
+ * answers the per-row result file as CSV instead of JSON
34
+ * undo → `action: 'plan'` lists what undo would do and the records
35
+ * edited since; `action: 'apply'` carries it out with the
36
+ * person's decisions, called again until `done`
37
+ * export → the chosen fields of the chosen records, streamed as CSV,
38
+ * JSON or NDJSON with the row count in a header
39
+ * package → a workspace package (AGL-3535): `list` and `export` its
40
+ * items, `plan` an import, `apply` it, `undoPlan` and `undo`
41
+ * jobs → the workspace's imports, newest first, for the hub's history
42
+ *
43
+ * Every refusal is a {@link TransferErrorResponse} with an HTTP status and a
44
+ * {@link TransferErrorCode} a client can branch on — except a refusal for
45
+ * the workspace's plan, which is the plugin routes' own
46
+ * {@link TransferPlanRequiredResponse} and reads as `planRequired`. The job
47
+ * itself is also readable from Firestore at
48
+ * `orgs/{orgId}/transferJobs/{jobId}` (as a {@link TransferJobRecord}) by the
49
+ * members the routes admit, so a progress panel can listen instead of
50
+ * polling.
51
+ *
52
+ * These are types and constants only, and the one definition of every
53
+ * shape that crosses the wire: the engine is the console's
54
+ * (`@aglyn/tenant-data-admin/server/transfer-jobs`), the client the console
55
+ * hands the UI kit is built on them, and the kit's in-memory client answers
56
+ * with the same shapes.
57
+ *=========================================*/ /** Where each route is served. */ export const TRANSFER_API_ROUTES = {
58
+ fields: '/api/transfer/fields',
59
+ upload: '/api/transfer/upload',
60
+ analyze: '/api/transfer/analyze',
61
+ plan: '/api/transfer/plan',
62
+ apply: '/api/transfer/apply',
63
+ status: '/api/transfer/status',
64
+ undo: '/api/transfer/undo',
65
+ export: '/api/transfer/export',
66
+ package: '/api/transfer/package',
67
+ jobs: '/api/transfer/jobs'
68
+ };
69
+ /**
70
+ * The most bytes one upload request carries. A file larger than this is
71
+ * sent in parts (`part`, `parts`), each under the platform's request ceiling.
72
+ */ export const TRANSFER_UPLOAD_PART_MAX_BYTES = 3000000;
73
+ /** The most bytes one file may carry, unless its resource sets a lower `limits.maxBytes`. */ export const TRANSFER_UPLOAD_MAX_BYTES = 24000000;
74
+ /** The most parts one upload may be sent in. */ export const TRANSFER_UPLOAD_MAX_PARTS = Math.ceil(TRANSFER_UPLOAD_MAX_BYTES / TRANSFER_UPLOAD_PART_MAX_BYTES);
75
+ /** The most rows a file may carry, unless its resource sets `limits.maxRows`. */ export const TRANSFER_DEFAULT_MAX_ROWS = 50000;
76
+ /** The sample rows analyze returns for the mapping step. */ export const TRANSFER_ANALYZE_SAMPLE_ROWS = 20;
77
+ /** The most planned rows one `plan` page returns. */ export const TRANSFER_PLAN_PAGE_MAX = 200;
78
+ /** The most undo conflicts one `undo` plan page returns. */ export const TRANSFER_UNDO_PAGE_MAX = 200;
79
+ /** The planned rows of each verdict a dry run answers with, so every filter of the review table shows rows. */ export const TRANSFER_PLAN_SAMPLE_PER_VERDICT = 50;
80
+ /** The most conflicts a dry run lists; the count covers every one. */ export const TRANSFER_PLAN_CONFLICTS_MAX = 500;
81
+ /** The rows of each match outcome analyze lists; the summary counts every row. */ export const TRANSFER_MATCH_ROWS_PER_KIND = 100;
82
+ /** The examples each derivation or problem keeps. */ export const TRANSFER_REVIEW_SAMPLES = 5;
83
+ /** How long a job that never wrote is kept after it was last touched. */ export const TRANSFER_DRAFT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
84
+ /** Where a person's remembered choices live: `users/{uid}/transferPrefs/{resourceKey}`, a `TransferPrefs`. */ export const TRANSFER_PREFS_COLLECTION = 'transferPrefs';
85
+ /** The most selected ids one export reads. */ export const TRANSFER_EXPORT_SELECTION_MAX = 10000;
86
+ /** The rows an export asks the resource for per page. */ export const TRANSFER_EXPORT_PAGE_ROWS = 500;
87
+ /**
88
+ * The rows an export reads ahead, before the first byte, to count a
89
+ * resource that cannot count itself: a file that fits is promised whole.
90
+ */ export const TRANSFER_EXPORT_PREFETCH_ROWS = 5000;
91
+ /**
92
+ * The header a result file carries with its row count, so the client can
93
+ * tell a whole download from a cut-off one (`exportShortfall`).
94
+ */ export const TRANSFER_EXPORT_ROWS_HEADER = 'X-Aglyn-Export-Rows';
95
+ /** The result file's columns after the file's own: what happened to the row, why, and the record. */ export const TRANSFER_RESULT_COLUMNS = [
96
+ 'Outcome',
97
+ 'Reason',
98
+ 'Record ID'
99
+ ];
100
+ /*------------------------------------------
101
+ * The workspace's imports (AGL-3535): `POST /api/transfer/jobs`
102
+ *-----------------------------------------*/ /** The most jobs one page of the history holds. */ export const TRANSFER_JOBS_PAGE_MAX = 50;
103
+ /** The most unfinished imports one answer lists. */ export const TRANSFER_UNFINISHED_IMPORTS_MAX = 50;
104
+ /** The most package imports listed per site. */ export const TRANSFER_SITE_PACKAGE_IMPORTS_PER_SITE = 10;
105
+ /** The `reason` of a refusal for the workspace's plan — the REST API's `error.type`. */ export const TRANSFER_PLAN_REQUIRED = 'plan_required';
106
+
107
+ //# sourceMappingURL=transfer-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/data-transfer/transfer-api.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * THE TRANSFER API — what the import wizard sends the job engine, and what\n * it gets back.\n *\n * Eight console routes, all `POST` with a JSON body (`Content-Type:\n * application/json`) carrying `orgId`, and the caller's ID token as\n * `Authorization: Bearer …`:\n *\n * fields → what a resource offers: its fields, groups, match keys,\n * locked rules and the person's remembered choices\n * upload → a job in `draft`, the file stored (in parts when it is large)\n * analyze → `analyzed`: the header proposal, samples, picklist values;\n * given a mapping, every field's derivations and the matches\n * plan → `planned`: the dry run over every row; `action: 'rows'` pages it\n * apply → `applying` … `applied`: chunks written until the time budget\n * runs out; called again until `done`\n * status → the job, its progress and counts; `download: 'results'`\n * answers the per-row result file as CSV instead of JSON\n * undo → `action: 'plan'` lists what undo would do and the records\n * edited since; `action: 'apply'` carries it out with the\n * person's decisions, called again until `done`\n * export → the chosen fields of the chosen records, streamed as CSV,\n * JSON or NDJSON with the row count in a header\n * package → a workspace package (AGL-3535): `list` and `export` its\n * items, `plan` an import, `apply` it, `undoPlan` and `undo`\n * jobs → the workspace's imports, newest first, for the hub's history\n *\n * Every refusal is a {@link TransferErrorResponse} with an HTTP status and a\n * {@link TransferErrorCode} a client can branch on — except a refusal for\n * the workspace's plan, which is the plugin routes' own\n * {@link TransferPlanRequiredResponse} and reads as `planRequired`. The job\n * itself is also readable from Firestore at\n * `orgs/{orgId}/transferJobs/{jobId}` (as a {@link TransferJobRecord}) by the\n * members the routes admit, so a progress panel can listen instead of\n * polling.\n *\n * These are types and constants only, and the one definition of every\n * shape that crosses the wire: the engine is the console's\n * (`@aglyn/tenant-data-admin/server/transfer-jobs`), the client the console\n * hands the UI kit is built on them, and the kit's in-memory client answers\n * with the same shapes.\n *=========================================*/\n\nimport type { PicklistSpec, PicklistValue, PicklistValueSet } from '../app-utils/picklists'\nimport type { DerivationKind, DeriveOptions, DeriveProblemCode } from './derive'\nimport type {\n TransferFieldCatalog,\n TransferFieldGroup,\n TransferPresetHints,\n TransferResourcePreset,\n TransferSavedPreset,\n} from './field-catalog'\nimport type { HeaderMatchResult, MappingProblems, TransferAliasDictionary } from './header-match'\nimport type { TransferJob, TransferJobStatus, TransferResultSummary, TransferRowResult } from './job'\nimport type { MatchKeySpec, MatchedVia, RowMatchOutcome } from './match'\nimport type {\n PicklistMatched,\n PicklistUnmatched,\n PicklistValueChoice,\n} from './picklist-map'\nimport type {\n PlannedTransferRow,\n TransferPlanSummary,\n TransferRowVerdict,\n TransferWarning,\n TransferWarningClass,\n} from './plan'\nimport type {\n TransferFieldMode,\n TransferLockedRule,\n TransferPolicy,\n TransferPolicyDefaults,\n TransferPolicySource,\n} from './policy'\nimport type { TransferField, TransferFormat, TransferResourceDescriptor } from './resource'\nimport type { TransferCsvDelimiter, TransferSourceOptions } from './source'\nimport type { PackageDependency, PackageItemDecision, TransferPackage } from './package'\nimport type {\n TransferPackageDependencyChoice,\n TransferPackagePlanItem,\n TransferPackageReference,\n TransferPackageSummary,\n TransferPackageWarningClass,\n} from './package-plan'\n\n/** Where each route is served. */\nexport const TRANSFER_API_ROUTES = {\n fields: '/api/transfer/fields',\n upload: '/api/transfer/upload',\n analyze: '/api/transfer/analyze',\n plan: '/api/transfer/plan',\n apply: '/api/transfer/apply',\n status: '/api/transfer/status',\n undo: '/api/transfer/undo',\n export: '/api/transfer/export',\n package: '/api/transfer/package',\n jobs: '/api/transfer/jobs',\n} as const\n\nexport type TransferApiRoute = keyof typeof TRANSFER_API_ROUTES\n\n/**\n * The most bytes one upload request carries. A file larger than this is\n * sent in parts (`part`, `parts`), each under the platform's request ceiling.\n */\nexport const TRANSFER_UPLOAD_PART_MAX_BYTES = 3_000_000\n\n/** The most bytes one file may carry, unless its resource sets a lower `limits.maxBytes`. */\nexport const TRANSFER_UPLOAD_MAX_BYTES = 24_000_000\n\n/** The most parts one upload may be sent in. */\nexport const TRANSFER_UPLOAD_MAX_PARTS = Math.ceil(TRANSFER_UPLOAD_MAX_BYTES / TRANSFER_UPLOAD_PART_MAX_BYTES)\n\n/** The most rows a file may carry, unless its resource sets `limits.maxRows`. */\nexport const TRANSFER_DEFAULT_MAX_ROWS = 50_000\n\n/** The sample rows analyze returns for the mapping step. */\nexport const TRANSFER_ANALYZE_SAMPLE_ROWS = 20\n\n/** The most planned rows one `plan` page returns. */\nexport const TRANSFER_PLAN_PAGE_MAX = 200\n\n/** The most undo conflicts one `undo` plan page returns. */\nexport const TRANSFER_UNDO_PAGE_MAX = 200\n\n/** The planned rows of each verdict a dry run answers with, so every filter of the review table shows rows. */\nexport const TRANSFER_PLAN_SAMPLE_PER_VERDICT = 50\n\n/** The most conflicts a dry run lists; the count covers every one. */\nexport const TRANSFER_PLAN_CONFLICTS_MAX = 500\n\n/** The rows of each match outcome analyze lists; the summary counts every row. */\nexport const TRANSFER_MATCH_ROWS_PER_KIND = 100\n\n/** The examples each derivation or problem keeps. */\nexport const TRANSFER_REVIEW_SAMPLES = 5\n\n/*------------------------------------------\n * The stored job\n *-----------------------------------------*/\n\n/** A file that arrives in parts: how many, and which have landed. */\nexport interface TransferUploadState {\n parts: number\n received: number[]\n bytes: number\n complete: boolean\n}\n\n/** What undo has done so far, while it runs and after. */\nexport interface TransferUndoState {\n status: 'running' | 'done'\n /** The next undo snapshot (chunk) to revert. */\n chunk: number\n startedAt: number\n startedBy: string\n /** What happens to an edited record the decisions do not name. */\n otherwise: TransferUndoDecision\n /** The person's decision per record id, kept so the sweep can finish an undo the browser left. */\n decisions?: Record<string, TransferUndoDecision>\n counts: TransferUndoCounts\n}\n\n/**\n * What the sweep does to a job once `retainUntil` passes: `expire` deletes\n * a job that never wrote (and its file); `trim` clears an applied job's\n * dry run, undo snapshots and file once its undo window has closed, keeping\n * the job and its per-row results.\n */\nexport type TransferJobRetention = 'expire' | 'trim'\n\n/** How long a job that never wrote is kept after it was last touched. */\nexport const TRANSFER_DRAFT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000\n\n/** Undo's steps, counted. */\nexport type TransferUndoCounts = Record<'restore' | 'delete' | 'conflict' | 'nothing', number>\n\n/** Who is driving a job right now; another driver waits until it lapses. */\nexport interface TransferJobLease {\n owner: string\n expiresAt: number\n}\n\n/** A planned row that breaks one of the resource's own rules, and so fails. */\nexport interface TransferPlanInvariantFailure {\n row: number\n invariant: string\n message: string\n}\n\n/**\n * A job as `orgs/{orgId}/transferJobs/{jobId}` stores it: the core's\n * {@link TransferJob}, and what the engine keeps between requests.\n */\nexport interface TransferJobRecord extends TransferJob {\n /** Where the file is stored, once every part has landed. */\n sourcePath?: string\n upload?: TransferUploadState\n /** How the CSV is read, as the person confirmed it at upload. */\n read?: TransferSourceOptions\n /** The file's header, once analyzed. */\n headers?: string[]\n /** The person's choices the stored plan was built from. */\n picklistChoices?: TransferPicklistChoices\n /** Field id → lookup value key → what the person chose for a value that named no record. */\n lookupChoices?: Record<string, Record<string, TransferLookupChoice>>\n derive?: DeriveOptions\n /** The date order the person chose per field. */\n dateOrders?: Record<string, TransferDateOrder>\n /** What a plugin's extra wizard steps collected, by step id. */\n extras?: Record<string, unknown>\n /** Values the plan adds to the organization's picklists, by picklist id; added before the first write. */\n picklistAdditions?: Record<string, PicklistValue[]>\n picklistsAdded?: boolean\n warnings?: TransferWarning[]\n acknowledgementsRequired?: TransferWarningClass[]\n matchSummary?: Record<RowMatchOutcome['kind'], number>\n invariantFailures?: TransferPlanInvariantFailure[]\n invariantFailureCount?: number\n plannedAt?: number\n /** Set by the first write; a job that has written can be resumed, never re-planned. */\n applyStartedAt?: number\n /** Results so far, counted. */\n results?: TransferResultSummary\n lease?: TransferJobLease | null\n undo?: TransferUndoState\n /** What the sweep does once `retainUntil` passes; absent while the job is running, and once trimmed. */\n retention?: TransferJobRetention\n retainUntil?: number\n /** When the sweep cleared the dry run, undo snapshots and file of an applied job. */\n trimmedAt?: number\n /** A workspace package import's plan, as the job keeps it (`kind: 'package'`). */\n package?: TransferPackageJobState\n}\n\n/** What a workspace package import keeps on its job between requests. */\nexport interface TransferPackageJobState {\n /** Where the package came from, as its manifest says. */\n source?: string\n /** The resources its items belong to. */\n resources: string[]\n summary: TransferPackageSummary\n blocking: string[]\n acknowledgementsRequired: TransferPackageWarningClass[]\n acknowledged?: TransferPackageWarningClass[]\n unknownKinds: string[]\n /** The person's choices the stored plan was built from. */\n decisions: Record<string, PackageItemDecision>\n dependencyChoices: Record<string, TransferPackageDependencyChoice>\n}\n\n/*------------------------------------------\n * The person's choices, and what the server says about a file under them\n *-----------------------------------------*/\n\n/** The person's date-order choice for a field whose dates read either way. */\nexport type TransferDateOrder = 'mdy' | 'dmy'\n\n/** What to do with one lookup value that names no record. */\nexport type TransferLookupChoice =\n | { action: 'create' }\n | { action: 'mapTo'; recordId: string }\n | { action: 'leaveBlank' }\n | { action: 'refuseRow' }\n\n/** Everything the person has decided that changes how rows are read. */\nexport interface TransferReadChoices {\n /** Column → field id; a column left out is ignored. */\n mapping: Record<number, string>\n /** Field id → the order its ambiguous dates are read in. */\n dateOrders?: Record<string, TransferDateOrder>\n /** Field id → incoming value key → choice. */\n picklistChoices?: Record<string, Record<string, PicklistValueChoice>>\n /** Field id → incoming value key → choice. */\n lookupChoices?: Record<string, Record<string, TransferLookupChoice>>\n /** Match key field ids, in priority order. */\n matchKeys?: string[]\n}\n\n/** One kind of thing a parser did to a field's cells, counted over the file. */\nexport interface TransferDerivationCount {\n kind: DerivationKind\n /** The rule in a sentence, from the first occurrence. */\n note: string\n count: number\n flagged: boolean\n samples: { row: number; from: string; to: string }[]\n}\n\n/** One kind of cell a field could not read, counted over the file. */\nexport interface TransferProblemCount {\n code: DeriveProblemCode\n message: string\n count: number\n samples: { row: number; raw: string }[]\n}\n\n/** What reading one field's cells did, over the whole file. */\nexport interface TransferDerivationSummary {\n fieldId: string\n /** Cells that held a value. */\n filled: number\n /** Cells read as they were. */\n unchanged: number\n derivations: TransferDerivationCount[]\n problems: TransferProblemCount[]\n /** Cells whose day and month could be either way round; the person picks the order. */\n ambiguousDates: number\n}\n\n/**\n * A lookup column's values that name no record — or several — each with the\n * rows that carry it and the records it may mean (AGL-3541).\n */\nexport interface TransferLookupReview {\n fieldId: string\n /** Distinct values that named exactly one record. */\n resolved?: number\n unresolved: {\n value: string\n key: string\n count: number\n rows: number[]\n suggestions: { recordId: string; label: string }[]\n }[]\n}\n\n/** One row's match, for the matching step's lists. */\nexport interface TransferMatchRowView {\n row: number\n outcome: RowMatchOutcome\n /** A name for the row, from its own cells. */\n label?: string\n}\n\n/** Rows against existing records under the chosen keys. */\nexport interface TransferMatchReview {\n keys: MatchKeySpec[]\n summary: Record<RowMatchOutcome['kind'], number>\n /** Rows of each outcome, at most {@link TRANSFER_MATCH_ROWS_PER_KIND} of each. */\n rows: TransferMatchRowView[]\n}\n\n/** One field a matched record and the file disagree on. */\nexport interface TransferConflictField {\n fieldId: string\n before: unknown\n incoming: unknown\n /** What the policy makes of it now. */\n after: unknown\n mode: TransferFieldMode\n source: TransferPolicySource\n}\n\n/** A matched row whose file values differ from values the record already has. */\nexport interface TransferConflict {\n row: number\n recordId: string\n fields: TransferConflictField[]\n}\n\n/** A row more than one record matched. */\nexport interface TransferAmbiguity {\n row: number\n via: MatchedVia\n recordIds: string[]\n}\n\n/*------------------------------------------\n * A resource as the person sees it, and their remembered choices\n *-----------------------------------------*/\n\n/** Which records an export reads. */\nexport type TransferExportScopeKind = 'selection' | 'filter' | 'all'\n\n/** The records an export reads: the selected ids, the list's current filter, or every record. */\nexport type TransferExportScope =\n | { kind: 'selection'; ids: string[] }\n | { kind: 'filter'; filter: unknown }\n | { kind: 'all' }\n\n/** The fields, records and format the person chose to export. */\nexport interface TransferExportChoice {\n resource: string\n /** Field ids, in the file's column order. */\n fieldIds: string[]\n scope: TransferExportScope\n format: TransferFormat\n /** A CSV starts with a byte-order mark, for spreadsheets. */\n bom: boolean\n /**\n * The CSV column name for a field, by field id, in place of its label — a\n * resource preset's `headers` (see `transferExportHeaders`).\n */\n headers?: Readonly<Record<string, string>>\n}\n\n/** Where a person's remembered choices live: `users/{uid}/transferPrefs/{resourceKey}`, a `TransferPrefs`. */\nexport const TRANSFER_PREFS_COLLECTION = 'transferPrefs'\n\n/** The most selected ids one export reads. */\nexport const TRANSFER_EXPORT_SELECTION_MAX = 10_000\n\n/** The rows an export asks the resource for per page. */\nexport const TRANSFER_EXPORT_PAGE_ROWS = 500\n\n/**\n * The rows an export reads ahead, before the first byte, to count a\n * resource that cannot count itself: a file that fits is promised whole.\n */\nexport const TRANSFER_EXPORT_PREFETCH_ROWS = 5_000\n\n/** The export choice a person made last, remembered per person and resource. */\nexport interface TransferExportPrefs {\n /** A built-in preset id, a saved preset's id, or `null` for a hand-picked list. */\n presetId: string | null\n fieldIds: string[]\n format: TransferFormat\n /** A CSV starts with a byte-order mark, for spreadsheets. */\n bom: boolean\n scope: TransferExportScopeKind\n}\n\n/** What is remembered for one person and resource. */\nexport interface TransferPrefs {\n export?: TransferExportPrefs\n /** Presets the person saved, in the order they saved them. */\n presets: TransferSavedPreset[]\n}\n\n/** What a resource offers to import and export, as the person may see it. */\nexport interface TransferResourceInfo {\n resource: TransferResourceDescriptor\n /** Every field in catalog order: standard, derived, custom, then system. */\n fields: TransferField[]\n /** The groups the fields are listed under, in order. */\n groups: TransferFieldGroup[]\n /** The keys a row may be matched on, in the default priority. */\n matchKeys: MatchKeySpec[]\n /** The match keys used when the person has not chosen (every key, in order, by default). */\n defaultMatchKeys?: string[]\n presetHints?: TransferPresetHints\n /** The resource's own presets, listed after the built-in ones. */\n resourcePresets?: TransferResourcePreset[]\n /** The owning plugin's rules, shown locked with their reasons. */\n locked: TransferLockedRule[]\n /**\n * Where the resource starts the person on the Conflicts step, when it is\n * not the core's fill blanks / leave (AGL-3548). The wizard opens on it.\n */\n defaultPolicy?: TransferPolicyDefaults\n /** Other products' header spellings, from the owning plugin. */\n dictionaries?: TransferAliasDictionary[]\n prefs: TransferPrefs\n /** Whether the person may create a custom field from the mapping step. */\n canCreateCustomField?: boolean\n}\n\n/*------------------------------------------\n * Requests and responses\n *-----------------------------------------*/\n\n/** Every request names its organization. */\nexport interface TransferOrgRequest {\n orgId: string\n}\n\n/** Every request after the upload names its job. */\nexport interface TransferJobRequest extends TransferOrgRequest {\n jobId: string\n}\n\n/** Where a job stands, for a progress bar. */\nexport interface TransferProgress {\n status: TransferJobStatus\n /** The next chunk to write. */\n chunk: number\n chunkCount: number\n rowsDone: number\n rowCount: number\n results: TransferResultSummary\n}\n\n/** `fields`: what a resource offers. Answers for import and export alike. */\nexport interface TransferFieldsRequest extends TransferOrgRequest {\n resource: string\n /** The site, for a host-scoped resource. */\n hostId?: string | null\n /**\n * The list filter an export was opened on, for a resource whose fields\n * depend on the records read (one form's questions). An object, or ignored.\n */\n filter?: unknown\n}\n\nexport interface TransferFieldsResponse extends TransferResourceInfo {\n ok: true\n}\n\n/** `upload`: one file, or one part of it. */\nexport interface TransferUploadRequest extends TransferOrgRequest {\n resource: string\n /** The site, for a host-scoped resource. */\n hostId?: string | null\n fileName: string\n /** Detected from the file name, then the content, when absent. */\n format?: TransferFormat\n /** The file's text, or this part of it. */\n content: string\n /** This part's index, from 0; omit for a file sent whole. */\n part?: number\n /** How many parts the file is sent in; omit for a file sent whole. */\n parts?: number\n /** The job the first part made; required on every later part. */\n jobId?: string\n /** The CSV delimiter the person confirmed; detected when absent. Read from the first part. */\n delimiter?: TransferCsvDelimiter\n /** `false` when the CSV's first line is a row, not names. Read from the first part. */\n headerRow?: boolean\n}\n\nexport interface TransferUploadResponse {\n ok: true\n job: TransferJobRecord\n /** Whether every part has landed and the file is stored. */\n complete: boolean\n}\n\n/**\n * `analyze`: read the file and propose a mapping. Re-run with `mapping` to\n * see the file under it: each mapped field's derivations, its picklist\n * values, and how the rows match existing records under `matchKeys`.\n */\nexport interface TransferAnalyzeRequest\n extends TransferJobRequest,\n Partial<Omit<TransferReadChoices, 'mapping'>> {\n mapping?: Record<number, string | null>\n}\n\n/** One mapped picklist column's values against the organization's list. */\nexport interface TransferPicklistAnalysis {\n fieldId: string\n picklistId: string\n spec: PicklistSpec\n set: PicklistValueSet\n matched: PicklistMatched[]\n unmatched: PicklistUnmatched[]\n /** The proposed choice for each unmatched value, by its key — shown, never applied unseen. */\n proposals: Record<string, PicklistValueChoice>\n}\n\nexport interface TransferAnalyzeResponse {\n ok: true\n job: TransferJobRecord\n headers: string[]\n rowCount: number\n /** The first rows, as text, in column order. */\n samples: string[][]\n catalog: TransferFieldCatalog\n match: HeaderMatchResult\n /** The problems of the mapping analyzed (`mapping`, or the proposal). */\n mappingProblems: MappingProblems\n matchKeys: MatchKeySpec[]\n lockedRules: TransferLockedRule[]\n picklists: TransferPicklistAnalysis[]\n /** With `mapping`: what reading each mapped field did, over every row. */\n derivations?: TransferDerivationSummary[]\n /** With `mapping`: each mapped lookup column's values that name no record, with suggestions. */\n lookups?: TransferLookupReview[]\n /** With `mapping`: the rows against existing records. */\n matches?: TransferMatchReview\n /** Record id → a name for it, for the records `matches` names. */\n recordLabels?: Record<string, string>\n}\n\n/** Per mapped picklist field, the choice for each unmatched value by its key. */\nexport type TransferPicklistChoices = Record<string, Record<string, PicklistValueChoice>>\n\n/** The person's choices a dry run is built from. */\nexport interface TransferPlanChoices extends Omit<TransferReadChoices, 'mapping'> {\n /** Column → field id; `null` leaves a column out. */\n mapping: Record<number, string | null>\n /** The match keys to use, by field id, in priority order; the resource's own order when absent. */\n matchKeys?: string[]\n /** The policy; the resource's locked rules are applied over it whatever it says. */\n policy?: Partial<Omit<TransferPolicy, 'locked'>>\n picklistChoices?: TransferPicklistChoices\n derive?: DeriveOptions\n /** What a plugin's extra wizard steps collected (a consent attestation), by step id. */\n extras?: Record<string, unknown>\n}\n\n/** `plan`: build the dry run. */\nexport interface TransferPlanRequest extends TransferJobRequest, TransferPlanChoices {\n action?: 'plan'\n}\n\n/** `plan` with `action: 'rows'`: one page of the stored dry run. */\nexport interface TransferPlanRowsRequest extends TransferJobRequest {\n action: 'rows'\n offset?: number\n limit?: number\n /** Only rows with these verdicts. */\n verdicts?: TransferRowVerdict[]\n}\n\nexport interface TransferPlanRowsPage {\n rows: PlannedTransferRow[]\n offset: number\n /** The offset of the next page, or `null` after the last. */\n next: number | null\n}\n\nexport interface TransferPlanResponse {\n ok: true\n job: TransferJobRecord\n summary: TransferPlanSummary\n warnings: TransferWarning[]\n acknowledgementsRequired: TransferWarningClass[]\n matchSummary: Record<RowMatchOutcome['kind'], number>\n invariantFailures: TransferPlanInvariantFailure[]\n invariantFailureCount: number\n picklistAdditions: Record<string, PicklistValue[]>\n /** The first page of planned rows. */\n rows: TransferPlanRowsPage\n /** Up to {@link TRANSFER_PLAN_SAMPLE_PER_VERDICT} rows of each verdict, in row order. */\n sample: PlannedTransferRow[]\n /** Matched rows whose file values differ from the record's, at most {@link TRANSFER_PLAN_CONFLICTS_MAX}. */\n conflicts: TransferConflict[]\n /** Every such row, counted. */\n conflictCount: number\n /** Rows more than one record matched. */\n ambiguous: TransferAmbiguity[]\n /** Record id → a name for it, for the records the sample, conflicts and ambiguous rows name. */\n recordLabels: Record<string, string>\n}\n\nexport interface TransferPlanRowsResponse {\n ok: true\n rows: TransferPlanRowsPage\n}\n\n/** `apply`: write chunks until the request's budget runs out. */\nexport interface TransferApplyRequest extends TransferJobRequest {\n /** The warning classes the person acknowledged; required on the first call. */\n acknowledged?: TransferWarningClass[]\n}\n\nexport interface TransferApplyResponse {\n ok: true\n job: TransferJobRecord\n progress: TransferProgress\n /** Every chunk is written; stop calling. */\n done: boolean\n /** The results of the chunks this call wrote. */\n results: TransferRowResult[]\n}\n\n/** `status`: where the job stands; `download: 'results'` answers the result file, `include: 'results'` adds every row's result. */\nexport interface TransferStatusRequest extends TransferJobRequest {\n download?: 'results'\n include?: 'results'\n}\n\nexport interface TransferStatusResponse {\n ok: true\n job: TransferJobRecord\n progress: TransferProgress\n undo: {\n available: boolean\n /** When undo closes, or `null` before the job is applied. */\n expiresAt: number | null\n state: TransferUndoState | null\n }\n /** With `include: 'results'`: every written row's result, in row order. */\n rows?: TransferRowResult[]\n}\n\n/**\n * The header a result file carries with its row count, so the client can\n * tell a whole download from a cut-off one (`exportShortfall`).\n */\nexport const TRANSFER_EXPORT_ROWS_HEADER = 'X-Aglyn-Export-Rows'\n\n/** The result file's columns after the file's own: what happened to the row, why, and the record. */\nexport const TRANSFER_RESULT_COLUMNS = ['Outcome', 'Reason', 'Record ID'] as const\n\n/** One row of the result file, before it is written as CSV. */\nexport interface TransferResultFileRow {\n cells: string[]\n result: TransferRowResult | null\n}\n\n/** What the person decided for one record edited since the import. */\nexport type TransferUndoDecision = 'revert' | 'keep'\n\n/** `undo` with `action: 'plan'`: what undo would do. Writes nothing. */\nexport interface TransferUndoPlanRequest extends TransferJobRequest {\n action: 'plan'\n offset?: number\n limit?: number\n}\n\n/** A record edited since the import: what it holds now, and what undo would put back. */\nexport interface TransferUndoConflict {\n row: number\n recordId: string\n /** A name for the record, from what it holds now. */\n label?: string\n action: 'created' | 'updated'\n /** The fields edited since the import. */\n fields: string[]\n /** Those fields as the record holds them now. */\n current: Record<string, unknown>\n /** What undo would restore them to; empty for a created record (undo deletes it). */\n restore: Record<string, unknown>\n}\n\nexport interface TransferUndoPlanResponse {\n ok: true\n job: TransferJobRecord\n counts: TransferUndoCounts\n conflicts: TransferUndoConflict[]\n offset: number\n next: number | null\n expiresAt: number | null\n}\n\n/** `undo` with `action: 'apply'`: carry it out. Called again until `done`. */\nexport interface TransferUndoApplyRequest extends TransferJobRequest {\n action: 'apply'\n /** The person's decision per record id, for the conflicts the plan listed. */\n decisions?: Record<string, TransferUndoDecision>\n /** For an edited record the decisions do not name — one edited after the plan was read. */\n otherwise: TransferUndoDecision\n}\n\nexport interface TransferUndoApplyResponse {\n ok: true\n job: TransferJobRecord\n undo: TransferUndoState\n done: boolean\n}\n\n/**\n * `export`: the chosen fields of the chosen records, streamed. The answer is\n * the file (`text/csv`, `application/json` or `application/x-ndjson`) with\n * the rows it promises in {@link TRANSFER_EXPORT_ROWS_HEADER} whenever the\n * resource could count them before the first byte, or a\n * {@link TransferErrorResponse}.\n */\nexport interface TransferExportRequest extends TransferOrgRequest, TransferExportChoice {\n /** The site, for a host-scoped resource. */\n hostId?: string | null\n}\n\n/*------------------------------------------\n * Workspace packages (AGL-3535): `POST /api/transfer/package`\n *-----------------------------------------*/\n\n/** One item a workspace holds, for the export picker. */\nexport interface TransferPackageListItem {\n /** `<kind>/<id>`. */\n key: string\n kind: string\n id: string\n name?: string\n /** What it names, so \"include what they need\" can be shown before the file is made. */\n deps: PackageDependency[]\n}\n\n/** One package resource the workspace can move, and what it holds. */\nexport interface TransferPackageResourceList {\n key: string\n label: string\n pluginId: string\n description?: string\n items: TransferPackageListItem[]\n /** The resource's rules, shown with their reasons (an imported sequence is a draft). */\n rules: TransferPackageRuleView[]\n}\n\n/** A plugin's rule an import cannot change, with why. */\nexport interface TransferPackageRuleView {\n id: string\n label: string\n reason: string\n}\n\n/** `action: 'list'`: the package resources the workspace can move, and their items. */\nexport interface TransferPackageListRequest extends TransferOrgRequest {\n action: 'list'\n /** Only these resources; every one when absent. */\n resources?: string[]\n}\n\nexport interface TransferPackageListResponse {\n ok: true\n resources: TransferPackageResourceList[]\n}\n\n/**\n * `action: 'export'`: the package file. `items` names item keys; absent,\n * every item of `resources` (or of every resource). `dependencies` adds\n * what the chosen items name that the workspace holds as package items.\n */\nexport interface TransferPackageExportRequest extends TransferOrgRequest {\n action: 'export'\n items?: string[]\n resources?: string[]\n dependencies?: boolean\n}\n\nexport interface TransferPackageExportResponse {\n ok: true\n fileName: string\n package: TransferPackage\n}\n\n/**\n * `action: 'plan'`: what importing a package would do, writing nothing.\n * The first call carries `package` and makes the job; later calls name\n * `jobId` and carry the person's choices, and re-plan against the\n * workspace as it is then.\n */\nexport interface TransferPackagePlanRequest extends TransferOrgRequest {\n action: 'plan'\n jobId?: string\n /** The file's parsed JSON, on the first call. */\n package?: unknown\n fileName?: string\n decisions?: Record<string, PackageItemDecision>\n dependencyChoices?: Record<string, TransferPackageDependencyChoice>\n}\n\nexport interface TransferPackagePlanResponse {\n ok: true\n job: TransferJobRecord\n items: TransferPackagePlanItem[]\n references: TransferPackageReference[]\n unknownKinds: string[]\n summary: TransferPackageSummary\n blocking: string[]\n acknowledgementsRequired: TransferPackageWarningClass[]\n /** Each resource's label and rules, by resource key. */\n resources: Record<string, { label: string; rules: TransferPackageRuleView[] }>\n}\n\n/** `action: 'apply'`: write the planned items. Called again until `done`. */\nexport interface TransferPackageApplyRequest extends TransferOrgRequest {\n action: 'apply'\n jobId: string\n acknowledged?: TransferPackageWarningClass[]\n}\n\nexport interface TransferPackageApplyResponse {\n ok: true\n job: TransferJobRecord\n done: boolean\n results: TransferRowResult[]\n}\n\n/** What undo needs to reverse one package item. */\nexport interface TransferPackageUndoEntry {\n row: number\n key: string\n resource: string\n /** The id it was written under. */\n id: string\n name?: string\n action: 'created' | 'updated'\n /** A replaced item's content before the import. */\n previous?: unknown\n previousHash?: string\n /** The item's content hash once written, to tell a later edit from the import's own. */\n writtenHash?: string\n}\n\n/** An item edited since the import: undo asks before it overwrites or deletes it. */\nexport interface TransferPackageUndoConflict {\n row: number\n key: string\n resource: string\n id: string\n name?: string\n action: 'created' | 'updated'\n}\n\n/** `action: 'undoPlan'`: what undo would do. Writes nothing. */\nexport interface TransferPackageUndoPlanRequest extends TransferOrgRequest {\n action: 'undoPlan'\n jobId: string\n}\n\nexport interface TransferPackageUndoPlanResponse {\n ok: true\n job: TransferJobRecord\n counts: TransferUndoCounts\n conflicts: TransferPackageUndoConflict[]\n expiresAt: number | null\n}\n\n/** `action: 'undo'`: carry undo out, each edited item as decided (`otherwise` for the rest). */\nexport interface TransferPackageUndoRequest extends TransferOrgRequest {\n action: 'undo'\n jobId: string\n /** By item id. */\n decisions?: Record<string, TransferUndoDecision>\n otherwise: TransferUndoDecision\n}\n\nexport interface TransferPackageUndoResponse {\n ok: true\n job: TransferJobRecord\n undo: TransferUndoState\n done: boolean\n}\n\n/*------------------------------------------\n * The workspace's imports (AGL-3535): `POST /api/transfer/jobs`\n *-----------------------------------------*/\n\n/** The most jobs one page of the history holds. */\nexport const TRANSFER_JOBS_PAGE_MAX = 50\n\n/** `jobs`: the workspace's imports, newest first. */\nexport interface TransferJobsRequest extends TransferOrgRequest {\n limit?: number\n /** The `next` of the page before. */\n after?: number | null\n /** Also each site's package imports (`hosts/{hostId}/packageImports`), on the first page. */\n sitePackages?: boolean\n /**\n * Instead of the history, the caller's own unfinished imports of records\n * (AGL-3549): every job they started that has not written anything, the\n * ones the cleanup would expire — answered as `unfinished`.\n */\n unfinished?: boolean\n /** Throw away one unfinished import of the caller's (`jobId`) now; answered `{ ok: true }`. */\n action?: 'discard'\n jobId?: string\n}\n\n/** The most unfinished imports one answer lists. */\nexport const TRANSFER_UNFINISHED_IMPORTS_MAX = 50\n\n/** One of the caller's unfinished imports (AGL-3549), as Resume lists it. */\nexport interface TransferUnfinishedImportSummary {\n jobId: string\n /** The resource key, naming the instance for one declared with `instances`. */\n resource: string\n hostId: string | null\n fileName: string | null\n /** Where it stopped: before the dry run, after it, or failed before writing. */\n status: 'draft' | 'analyzed' | 'planned' | 'failed'\n /** When the person last worked on it, epoch ms. */\n updatedAt: number\n /** What the resource is called. */\n label: string\n}\n\n/** The answer to `{ unfinished: true }`. */\nexport interface TransferUnfinishedImportsResponse {\n ok: true\n unfinished: TransferUnfinishedImportSummary[]\n}\n\n/** The most package imports listed per site. */\nexport const TRANSFER_SITE_PACKAGE_IMPORTS_PER_SITE = 10\n\n/** One site package import (AGL-3533), as the hub's history lists it. */\nexport interface TransferSitePackageImportSummary {\n hostId: string\n hostName: string | null\n importId: string\n status: 'applying' | 'applied' | 'refused' | 'failed' | 'undone'\n actorEmail: string | null\n source: string | null\n startedAt: number\n appliedAt: number | null\n undoneAt: number | null\n /** Items by decision. */\n counts: Record<string, number>\n items: number\n undo: { available: boolean; expiresAt: number | null }\n error: string | null\n}\n\n/** One import as the hub's history lists it. */\nexport interface TransferJobSummary {\n id: string\n resource: string\n /** The resource's label, or \"Package\". */\n label: string\n kind: TransferJob['kind']\n status: TransferJobStatus\n hostId: string | null\n fileName: string | null\n createdAt: number\n updatedAt: number\n createdBy: string\n /** The member's address, when the workspace still has them. */\n createdByEmail: string | null\n rowCount: number\n results: TransferResultSummary | null\n appliedAt: number | null\n undo: {\n available: boolean\n expiresAt: number | null\n status: TransferUndoState['status'] | null\n }\n /** The per-row result file can be downloaded (`status` with `download: 'results'`). */\n resultFile: boolean\n error: string | null\n}\n\nexport interface TransferJobsResponse {\n ok: true\n jobs: TransferJobSummary[]\n /** With `sitePackages`: each site's latest package imports, newest first. */\n sitePackageImports?: TransferSitePackageImportSummary[]\n /** The `createdAt` to ask the next page `after`, or `null` after the last. */\n next: number | null\n}\n\n/** Why a request was refused. */\nexport type TransferErrorCode =\n | 'unauthenticated'\n | 'forbidden'\n | 'notFound'\n | 'invalid'\n | 'tooLarge'\n | 'unsupportedFormat'\n | 'rejectedFile'\n | 'state'\n | 'busy'\n | 'acknowledgementsMissing'\n | 'choicesNeeded'\n | 'undoExpired'\n | 'rateLimited'\n | 'unavailable'\n | 'failed'\n /**\n * The workspace's plan does not carry the resource's feature. Never on\n * the wire: the route answers a {@link TransferPlanRequiredResponse}, and\n * a client reads it as this code.\n */\n | 'planRequired'\n\n/** Every refusal's body. */\nexport interface TransferErrorResponse {\n error: string\n code: TransferErrorCode\n /** What the client needs to fix it: the missing acknowledgements, the mapping problems, the open picklist values. */\n details?: unknown\n}\n\n/** The `reason` of a refusal for the workspace's plan — the REST API's `error.type`. */\nexport const TRANSFER_PLAN_REQUIRED = 'plan_required'\n\n/**\n * The body of a refusal for the workspace's plan (AGL-3555), answered with\n * 403: the flat shape every plan-gated console route answers (the CRM's\n * `suite-gate.ts`), so a transfer route refuses a plan exactly as the\n * owning plugin's own routes do. `code` is the plan feature\n * (`TransferResourceDescriptor.featureFlag`, the REST `error.code`);\n * `error` the sentence a client relays unchanged.\n */\nexport interface TransferPlanRequiredResponse {\n error: string\n reason: typeof TRANSFER_PLAN_REQUIRED\n code: string\n}\n"],"names":["TRANSFER_API_ROUTES","fields","upload","analyze","plan","apply","status","undo","export","package","jobs","TRANSFER_UPLOAD_PART_MAX_BYTES","TRANSFER_UPLOAD_MAX_BYTES","TRANSFER_UPLOAD_MAX_PARTS","Math","ceil","TRANSFER_DEFAULT_MAX_ROWS","TRANSFER_ANALYZE_SAMPLE_ROWS","TRANSFER_PLAN_PAGE_MAX","TRANSFER_UNDO_PAGE_MAX","TRANSFER_PLAN_SAMPLE_PER_VERDICT","TRANSFER_PLAN_CONFLICTS_MAX","TRANSFER_MATCH_ROWS_PER_KIND","TRANSFER_REVIEW_SAMPLES","TRANSFER_DRAFT_RETENTION_MS","TRANSFER_PREFS_COLLECTION","TRANSFER_EXPORT_SELECTION_MAX","TRANSFER_EXPORT_PAGE_ROWS","TRANSFER_EXPORT_PREFETCH_ROWS","TRANSFER_EXPORT_ROWS_HEADER","TRANSFER_RESULT_COLUMNS","TRANSFER_JOBS_PAGE_MAX","TRANSFER_UNFINISHED_IMPORTS_MAX","TRANSFER_SITE_PACKAGE_IMPORTS_PER_SITE","TRANSFER_PLAN_REQUIRED"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CAyC2C,GA4C3C,gCAAgC,GAChC,OAAO,MAAMA,sBAAsB;IACjCC,QAAQ;IACRC,QAAQ;IACRC,SAAS;IACTC,MAAM;IACNC,OAAO;IACPC,QAAQ;IACRC,MAAM;IACNC,QAAQ;IACRC,SAAS;IACTC,MAAM;AACR,EAAU;AAIV;;;CAGC,GACD,OAAO,MAAMC,iCAAiC,QAAS;AAEvD,2FAA2F,GAC3F,OAAO,MAAMC,4BAA4B,SAAU;AAEnD,8CAA8C,GAC9C,OAAO,MAAMC,4BAA4BC,KAAKC,IAAI,CAACH,4BAA4BD,gCAA+B;AAE9G,+EAA+E,GAC/E,OAAO,MAAMK,4BAA4B,MAAM;AAE/C,0DAA0D,GAC1D,OAAO,MAAMC,+BAA+B,GAAE;AAE9C,mDAAmD,GACnD,OAAO,MAAMC,yBAAyB,IAAG;AAEzC,0DAA0D,GAC1D,OAAO,MAAMC,yBAAyB,IAAG;AAEzC,6GAA6G,GAC7G,OAAO,MAAMC,mCAAmC,GAAE;AAElD,oEAAoE,GACpE,OAAO,MAAMC,8BAA8B,IAAG;AAE9C,gFAAgF,GAChF,OAAO,MAAMC,+BAA+B,IAAG;AAE/C,mDAAmD,GACnD,OAAO,MAAMC,0BAA0B,EAAC;AAoCxC,uEAAuE,GACvE,OAAO,MAAMC,8BAA8B,IAAI,KAAK,KAAK,KAAK,KAAI;AAiOlE,4GAA4G,GAC5G,OAAO,MAAMC,4BAA4B,gBAAe;AAExD,4CAA4C,GAC5C,OAAO,MAAMC,gCAAgC,MAAM;AAEnD,uDAAuD,GACvD,OAAO,MAAMC,4BAA4B,IAAG;AAE5C;;;CAGC,GACD,OAAO,MAAMC,gCAAgC,KAAK;AA4QlD;;;CAGC,GACD,OAAO,MAAMC,8BAA8B,sBAAqB;AAEhE,mGAAmG,GACnG,OAAO,MAAMC,0BAA0B;IAAC;IAAW;IAAU;CAAY,CAAS;AAyOlF;;2CAE2C,GAE3C,iDAAiD,GACjD,OAAO,MAAMC,yBAAyB,GAAE;AAoBxC,kDAAkD,GAClD,OAAO,MAAMC,kCAAkC,GAAE;AAuBjD,8CAA8C,GAC9C,OAAO,MAAMC,yCAAyC,GAAE;AAyFxD,sFAAsF,GACtF,OAAO,MAAMC,yBAAyB,gBAAe"}
@@ -1196,9 +1196,40 @@ export interface AglynOrgBilling extends AglynDocument {
1196
1196
  * the section can grow without a rules change each time.
1197
1197
  */
1198
1198
  crm?: OrgCrmSettings;
1199
+ /**
1200
+ * The plan the platform team has asked this workspace to move to
1201
+ * (AGL-3466) — see `OrgUpgradeProposal`.
1202
+ *
1203
+ * SERVER-OWNED: written only by /api/admin/org-upgrade-proposal (staff,
1204
+ * audited) and cleared by `writeOrgBilling` once a subscription is live,
1205
+ * both Admin SDK. Denied to every client in the rules, staff included,
1206
+ * because `standingUpgradeProposal` reads it in `app-utils` and the
1207
+ * proposal is the platform's statement to the customer, not theirs.
1208
+ */
1209
+ upgradeProposal?: OrgUpgradeProposal;
1199
1210
  createdAt?: ITimestamp;
1200
1211
  updatedAt?: ITimestamp;
1201
1212
  }
1213
+ /**
1214
+ * The platform team's request that a workspace move to a paid plan
1215
+ * (AGL-3466).
1216
+ *
1217
+ * The separate step that follows an evaluation: a client handed a workspace
1218
+ * looks around first, on whatever the workspace already has, and is asked to
1219
+ * upgrade only once staff record this. While it stands and no subscription
1220
+ * is live, the workspace's billing managers see it on the org home and on
1221
+ * Billing, with the plan preselected. Nothing about it grants or bills.
1222
+ */
1223
+ export interface OrgUpgradeProposal {
1224
+ /** A paid plan the workspace can buy itself — never `free` or `enterprise`. */
1225
+ plan: OrgPlan;
1226
+ /** The staff member who proposed it. */
1227
+ proposedBy: string;
1228
+ /** Epoch millis, so every cache serialization reads it back unchanged. */
1229
+ proposedAt: number;
1230
+ /** An optional line from staff, shown with the proposal. */
1231
+ note?: string;
1232
+ }
1202
1233
  /**
1203
1234
  * What the CRM lets an organization decide for every site at once
1204
1235
  * (AGL-2613). Every key is optional and off when absent: a setting that
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/aglyn/src/lib/foundation/definitions/org-billing.types.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The org's billing/entitlement vocabulary (AGL-443 naming cleanup).\n *\n * HISTORY: these types were spelled `Tenant*` until AGL-444 — they\n * predate the organizations migration (AGL-232..238); the retired\n * `tenants/{uid}` collection's billing shape was mirrored ONTO the org\n * doc and the names came along. The alias is gone: everything here is\n * `Org*` and describes fields of `orgs/{orgId}`. The last persisted\n * tenant spellings (the `users.{uid}.tenants` map, Stripe\n * `metadata[tenantId]`, `host.tenantId`) were retired pre-launch in\n * AGL-445 — billing keys off `metadata[orgId]` now.\n *\n * Convention (see the docs-site glossary): \"organization/org\" is the\n * entity; \"workspace\" is the user-facing word for it; \"tenant\" is\n * reserved for the published-site runtime (`apps/tenant`,\n * `@aglyn/tenant-*` libs).\n */\n\nimport type { ITimestamp } from '@aglyn/shared-util-timestamp'\nimport type {\n AglynDocument,\n HostUid,\n OrgUid,\n UserUid,\n} from './platform.types'\n/*\n * The plugin half of the two composed shapes below (AGL-3124).\n *\n * Each is an EMPTY interface a plugin augments with `declare module`, so the\n * keys a plugin owns are part of `OrgEntitlements` and `OrgFeatureFlags`\n * without the core listing them. The AI credits, the CRM's one-to-one email\n * pace, the form-submission and workflow-run bands, the marketplace's take\n * rate and selling gate and commerce's three gated features are declared that\n * way, each in its plugin's `plan-entitlements` module (AGL-3080).\n *\n * `import type` and nothing else: TypeScript erases it, so the plugin-manager\n * module is not on this module's runtime graph and a published page that\n * reaches the billing types (through the foundation barrel it must not reach\n * anyway) carries nothing new.\n */\nimport type {\n PluginEntitlementFeatures,\n PluginEntitlementQuotas,\n} from '../../plugin-manager/plugin-entitlement-keys'\n\nexport type { OrgUid } from './platform.types'\n\n\n/** Hosted in master catalog */\n/**\n * SaaS subscription tiers (Tenant Billing & SaaS Plans, AGL-38..41).\n * Pricing v3 (2026-07) inserted `scale` between business and advanced to\n * fill the $139→$399 gap, and added `agency` above advanced for\n * high-volume multi-site orgs — see the Pricing Decision Log.\n *\n * `enterprise` (AGL-1118) is a REAL plan, not a display label: it tops the\n * ladder with unlimited capacity plus white-label and SSO, and it is the ONE\n * tier with no list price — it is staff-provisioned per deal (AGL-1110), never\n * self-serve. Surfaces that offer plans for sale iterate `SELF_SERVE_PLANS`,\n * which excludes it; surfaces that merely NAME the org's plan read\n * `PLAN_LABELS` and get \"Enterprise\" for free.\n */\nexport type OrgPlan =\n | 'free'\n | 'starter'\n | 'pro'\n | 'business'\n | 'scale'\n | 'advanced'\n | 'agency'\n | 'enterprise'\n\n/**\n * The CORE boolean feature gates per plan; quotas live beside them as\n * numbers. Read {@link OrgFeatureFlags} instead — this half exists so the\n * plan tables can stay exhaustive over the platform's own gates while a\n * plugin declares its own (AGL-3124).\n */\nexport interface CoreOrgFeatureFlags {\n /** A/B experiments (AGL-252); Business tier. */\n abTesting?: boolean\n versioning?: boolean\n reusableComponents?: boolean\n customDomain?: boolean\n /**\n * Send mail as a domain the CUSTOMER owns — `hello@acme.com` — by publishing\n * our SPF, DKIM and return-path records in their own zone.\n *\n * Distinct from `customDomain`, which is the site's public web address and\n * authorizes nothing about mail; and distinct from `whiteLabel`, which\n * replaces the Aglyn brand — product name, logo, colors, support URL,\n * console chrome, favicon — across every branded surface. Sending as your\n * own name is one narrow consequence of white-labeling rather than the\n * whole of it, so the two cannot share a flag without the wider capability\n * following the narrower one wherever it goes.\n *\n * It is also the CHEAP half of the sending model, which is why it sits low\n * on the ladder. A customer-owned domain costs the platform one provider\n * domain object and NOTHING in our own DNS zone, because the customer\n * publishes the records; the platform subdomain a site is issued costs a\n * provider slot, three records in our zone and a permanent place in the\n * re-verification sweep. Holding the free-to-us option behind the highest\n * paywall makes the expensive one the default at every tier that can send.\n *\n * Read through `checkEntitlement` at the two routes that write a sending\n * identity: the org's domain list, and the per-site selection.\n */\n customSendingDomain?: boolean\n /**\n * Hold a sending subdomain of the PLATFORM's own —\n * `hello@{label}.mail.aglyn.app` — provisioned for one site inside Aglyn's\n * mail apex.\n *\n * The EXPENSIVE half of the sending model, and the reason it is a separate\n * flag from `customSendingDomain`. A domain the customer owns costs one\n * provider domain object and nothing in our zone, because they publish the\n * records. One of these costs a provider slot, THREE records in our own\n * zone, and a permanent place in the re-verification sweep — per site,\n * forever. The two capabilities are decided against different resources, so\n * a shared flag would move a bounded one whenever the unbounded one is\n * re-cut.\n *\n * The flag is the SECOND of two conditions and never the only one. Nothing\n * provisions on entitlement alone: a merchant asks from the sending-identity\n * route, this says whether the org carries what they asked for, and the\n * provider ceiling still refuses beyond the account's allowance. A site\n * refused at any of the three sends on the shared pool rather than not at\n * all.\n *\n * Read through `checkEntitlement` at\n * `libs/aglyn/src/lib/app-utils/dedicated-sending-domain.ts`, which is the\n * only reader — the claim path in `host-sending-domain.ts` goes through it.\n */\n dedicatedSendingDomain?: boolean\n removeBranding?: boolean\n /** Schedule a version to publish at a date/time (tier above versioning). */\n scheduledPublishing?: boolean\n /** AI copy assist in the besigner (AGL-89). */\n aiAssist?: boolean\n /**\n * Generative building and automation (AGL-2896): pages, components,\n * layouts, templates, SEO, email, campaigns, A/B variants, analytics\n * insights, products, CRM records, onboarding and workflows produced by\n * the assistant rather than assembled by hand.\n *\n * Distinct from `aiAssist`, which is the chat guide, copy assist and\n * generate-section rung every tier from Pro up carries. This is the\n * expensive rung: a generated surface carries the node tree, the catalog\n * and the theme in and iterates, so it draws credits by the hundreds where\n * a question draws tens. No self-serve tier includes it; the Aglyn AI\n * add-on (`OrgSeatAddons.aiAddon`) switches it on and adds the credit band\n * that funds it, and `resolveOrgEntitlements` flips this flag together\n * with `aiAssist` when the add-on is present. Enterprise carries it in the\n * agreement.\n */\n aiGenerative?: boolean\n /** No-code workflow builder (AGL-101). */\n workflows?: boolean\n /** Datasets + repeatable components (AGL-102/103). */\n dataStore?: boolean\n /** Video/file uploads in the media manager (AGL-162). */\n videoMedia?: boolean\n /** Appointment bookings (AGL-159). */\n bookings?: boolean\n /**\n * THE CRM (AGL-2611): contacts, leads, companies, the deals pipeline,\n * tasks, reports and custom fields, the CRM automation steps, and the\n * `crm:*` REST resources.\n *\n * The Contacts section included (AGL-2851). Capture still writes a site's\n * audience into the contacts collection on every plan including Free,\n * banded by `contactsPerHost`, and exporting or erasing those people stays\n * on every plan in Settings → Privacy. What this flag gates is the CRM a\n * team works them in, and it is the upgrade motive from Free to the first\n * paid tier rather than a line on top of one: for the small-business buyer\n * the CRM is the reason to pick a platform over a page builder.\n *\n * Read by the console shell through the CRM extension's and its widgets'\n * `featureFlag` (so the shell refuses every CRM section and page before\n * one mounts), by the `crm/*` routes, by the automation executor before a\n * CRM step runs, by the REST dispatcher in front of the CRM resources,\n * contacts included, and, restated, by the Firestore rules on a client's\n * CRM writes. A per-org override on `entitlements.features.crm` works the\n * way every other flag's does.\n */\n crm?: boolean\n /**\n * SEQUENCES (AGL-2974): one-to-one, multi-step email sequences a rep sends\n * from their own connected mailbox, logged on the CRM's records. The key\n * below keeps the name the feature shipped under, because it is stored on\n * the org (AGL-3199).\n *\n * False on EVERY plan, Enterprise included. Which tiers carry sequences\n * and connected mailboxes, and at what caps, is a packaging decision that\n * has not been made, so no plan may claim it. An organization reaches it\n * only through the per-org override on `entitlements.features.outreach`.\n *\n * Read by the console shell through the Sequences extension's\n * `featureFlag`, by the plugin's routes, and, restated, by the Firestore\n * rules in front of the `outreach*` collections. The rules carry no plan\n * list for it because no plan grants it; when packaging lands, the plan\n * table and those rules change together.\n */\n outreach?: boolean\n /**\n * Basic presentational interactions (AGL-577): menu/drawer open-close,\n * element show/hide, class toggles, sticky nav, navigation, site\n * alerts. Included on ALL plans — pure client-side DOM with no server\n * cost. The `actions` flag below gates the powerful automation steps\n * (server dispatch, runJs, analytics, overlays, raw HTML).\n *\n * `true` on all eight tiers, and NO code gates on it — which makes it look\n * dead, and it has now been filed as dead once (AGL-2082). It is not.\n * `tools/marketing/build-pricing-tables.mts` reads its value to emit the\n * \"Interactions\" row on the public /pricing compare table, ticked on every\n * plan, and the Free plan card bullets it. That row is the claim that a\n * hover-to-open menu is not a paid feature — a real competitive statement,\n * and the reason `true` everywhere is a DECISION rather than a default.\n *\n * So: do not delete it as dead weight. Deleting it silently removes a\n * public pricing row while the Figma frames still carry it in all four\n * responsive variants, which is a pricing call and not a cleanup.\n */\n interactions?: boolean\n /** Event → action automation builder (AGL-148). */\n actions?: boolean\n /** Outbound/inbound webhooks (AGL-149). */\n webhooks?: boolean\n /** Customer REST API v1 + API keys (AGL-615); Business tier. */\n apiAccess?: boolean\n /** Whole-site export/backup + restore (AGL-163). */\n siteExport?: boolean\n /** Multilingual sites (AGL-164): locale variants + switcher. */\n multilingual?: boolean\n /** Event Calendar add-on (AGL-145); paid, not part of any base tier. */\n eventCalendar?: boolean\n /** URL redirects manager (AGL-154). */\n redirects?: boolean\n /** Per-screen traffic analytics (AGL-150). */\n screenAnalytics?: boolean\n /** CDN delivery + responsive image variants for media (AGL-175). */\n mediaCdn?: boolean\n /** Announcement bar + promotional popups (AGL-195/196). */\n marketingOverlays?: boolean\n /** Full storefront commerce: catalog, cart, checkout (AGL-278). */\n commerce?: boolean\n /** Console point-of-sale mode (AGL-312). */\n pos?: boolean\n /** Entitlement-gated screens/sections/video paywalls (AGL-309). */\n contentGating?: boolean\n /** Verified-buyer product reviews (AGL-324). */\n productReviews?: boolean\n /** Abandoned checkout recovery emails (AGL-323). */\n abandonedCart?: boolean\n /** Dropship supplier routing on paid orders (AGL-289). */\n dropshipRouting?: boolean\n /**\n * White-label the platform (White-Label Phase 1): replace the Aglyn brand\n * — product name, logo, colors, support URL, transactional email from-name\n * — with the org's own `brandingProfile` across every branded surface.\n * Agency ($799) and Enterprise carry it on the plan (AGL-1118); any other\n * tier needs a per-org `entitlements` override. Strictly broader than\n * `removeBranding`, which only drops the\n * \"Made with Aglyn\" badge on published sites; white-label REPLACES the\n * brand rather than merely hiding it. Every branded surface resolves the\n * effective brand through `resolveBrandingProfile` so it can never drift.\n */\n whiteLabel?: boolean\n /**\n * Enterprise SSO (AGL-1101): the org's console users sign in through the\n * org's own SAML/OIDC IdP, wired as a per-org GCIP tenant (`org.sso`).\n * Distinct from `whiteLabel` — an Agency org can have one without the other.\n * Carried by the `enterprise` plan (AGL-1118) and false on every other base\n * plan; a lower tier needs a per-org `entitlements` override, which is how\n * enterprise orgs provisioned before that plan existed still get it. Gates\n * the staff SSO-config card and the SSO sign-in path; a non-entitled org can\n * neither configure nor use SSO.\n */\n ssoEnabled?: boolean\n}\n\n/**\n * Boolean feature gates per plan, the platform's and its plugins' together.\n *\n * {@link CoreOrgFeatureFlags} is the platform's own set — what\n * `PLAN_ENTITLEMENTS` declares for every plan, and what\n * `Required<CoreOrgFeatureFlags>` keeps exhaustive.\n * {@link PluginEntitlementFeatures} is the plugin half (AGL-3124): a plugin\n * declares a gate of its own into it with `declare module`, and its answer\n * per plan comes from that plugin's `plan-entitlements` declaration or its\n * `registerPluginEntitlements` defaults rather than from a row core writes —\n * which is why a plugin's gate joins HERE and not above. Every reader keeps reading `OrgFeatureFlags`, and\n * `keyof OrgFeatureFlags` admits a plugin's key the moment it is declared.\n */\nexport interface OrgFeatureFlags\n extends CoreOrgFeatureFlags,\n PluginEntitlementFeatures {}\n\n/**\n * An org's white-label brand identity (White-Label Phase 1). Populated on\n * the org doc (`orgs/{orgId}.brandingProfile`) and applied ONLY when the org\n * carries the `whiteLabel` entitlement (Agency or Enterprise plan, or a\n * per-org override);\n * otherwise every surface falls back to the Aglyn defaults baked into\n * `resolveBrandingProfile`. Every field is optional — a partial profile\n * still resolves, with the Aglyn default filling each gap — so an agency can\n * set just a product name and from-name without supplying logos.\n */\nexport interface OrgBrandingProfile {\n /** Brand name shown in place of \"Aglyn\" (console chrome, emails, badges). */\n productName?: string\n /** Primary/full-color logo URL (light backgrounds, console chrome). */\n logoUrl?: string\n /**\n * The logo for DARK grounds (AGL-3406): the console chrome in dark mode and the\n * published site's attribution badge, which always sits on a dark pill.\n * Unset falls back to {@link logoUrl}.\n */\n logoDarkUrl?: string\n /** Favicon URL for branded surfaces. */\n faviconUrl?: string\n /**\n * The favicon for a browser whose tab strip is dark, emitted as a second\n * `<link rel=\"icon\">` under `(prefers-color-scheme: dark)`. Unset falls back\n * to {@link faviconUrl}.\n */\n faviconDarkUrl?: string\n /** Brand primary color as a CSS color (hex), e.g. `#1a73e8`. */\n primaryColor?: string\n /** Support/help destination linked from branded surfaces and emails. */\n supportUrl?: string\n /** Transactional email from-name (the display name before the address). */\n fromName?: string\n /** Logo URL specifically for the email header (often a hosted PNG). */\n emailLogoUrl?: string\n /** Custom console domain the agency serves the app on (Phase 4 wiring). */\n customConsoleDomain?: string\n}\n\n/**\n * An org's enterprise SSO configuration (AGL-1101, Phase 1). Applied ONLY when\n * the org carries the `ssoEnabled` entitlement. The actual IdP lives in a\n * per-org **GCIP tenant** (`tenantId`) with a SAML/OIDC **provider**\n * (`providerId`); this block is the org-doc mirror the console reads to route\n * sign-in and the staff card edits. A public `ssoDomains/{domain}` doc maps a\n * verified email domain → `{ orgId, tenantId, providerId }` so the\n * pre-auth sign-in page can resolve an SSO org without reading the org doc.\n *\n * Security: a domain reaches `domains[]` ONLY by passing DNS TXT verification\n * (AGL-1210). This is the account-takeover guard, and it is the whole reason\n * self-serve is safe: without it an org could claim a domain it does not own,\n * `ssoDomains/{domain}` would route that domain's sign-ins to its IdP, and it\n * would intercept another company's logins. Claims in flight live in\n * `orgs/{orgId}/ssoDomains/{domain}` (see `OrgSsoDomainClaim`) and are NOT\n * governed until verified.\n *\n * `enforced` blocks password/social login for the governed domains, so it is a\n * lockout risk and is rehearsed (`previewSsoEnforcement`) before it is applied.\n */\nexport interface OrgSsoConfig {\n /** GCIP tenant id that carries this org's IdP provider. */\n tenantId: string\n /** GCIP provider id, e.g. `saml.aglyn-workspace` or `oidc.acme`. */\n providerId: string\n /** IdP protocol. Phase 1 ships SAML; OIDC is Phase 2. */\n protocol: 'saml' | 'oidc'\n /** Human label for the IdP (shown on the SSO button + staff card). */\n displayName?: string\n /**\n * Email domains routed to this IdP (lowercased, no `@`). A domain is added\n * here only after its claim passes DNS TXT verification, and removed the\n * moment re-verification fails — so membership of this array IS the\n * \"ownership proven\" statement. Never write to it from anywhere but the\n * verification path.\n */\n domains: string[]\n /**\n * Retained for `sso-jit`, which gates on it. Kept in lockstep with\n * `domains.length > 0` by the verification path; it was a staff-attested\n * boolean before AGL-1210 and is now derived, never asserted by a human.\n */\n domainVerified: boolean\n /**\n * SAML metadata the CUSTOMER supplies about their IdP. Stored so the pool's\n * provider config can be rebuilt or re-applied without asking again. The\n * X.509 certificate is a public signing certificate, not a secret.\n */\n idp?: {\n entityId: string\n ssoUrl: string\n certificates: string[]\n }\n /**\n * Require SSO for the governed domains — disables password/social login for\n * them (Phase 2). Phase 1 keeps this false so users keep a fallback.\n */\n enforced: boolean\n /** Lifecycle: `configuring` (not live), `active`, or `disabled`. */\n status: 'configuring' | 'active' | 'disabled'\n /**\n * Uid + time of the last config change. Since AGL-1210 this is normally an\n * ORG ADMIN, not staff — the flow is self-serve end to end.\n */\n configuredBy?: string\n configuredAt?: ITimestamp\n}\n\n/**\n * A pending or proven claim on one email domain (AGL-1210), stored at\n * `orgs/{orgId}/ssoDomains/{domain}`.\n *\n * A subcollection rather than a map on the org doc because domains contain\n * dots, and a dotted key in a Firestore map is read as a nested field path by\n * every update helper — writing `sso.domainClaims[\"acme.com\"]` would silently\n * create `{acme: {com: …}}`. A document id has no such ambiguity.\n *\n * The claim is deliberately worthless on its own: holding one grants nothing.\n * Only `verified` moving to true adds the domain to `sso.domains`, and only\n * that array is consulted at sign-in.\n */\nexport interface OrgSsoDomainClaim {\n /** The domain being claimed (lowercased, no `@`); mirrors the document id. */\n domain: string\n /**\n * Random value the org must publish as a DNS TXT record at\n * `_aglyn-challenge.<domain>`. Per org+domain, so two orgs claiming the same\n * domain get different tokens and neither can pass on the other's record.\n */\n token: string\n /** True once a DNS lookup has actually seen `token` at the challenge host. */\n verified: boolean\n createdAt: ITimestamp\n verifiedAt?: ITimestamp\n /** Last lookup attempt, successful or not — drives re-verification. */\n lastCheckedAt?: ITimestamp\n /**\n * TXT records seen on the last FAILED lookup. Shown back to the customer,\n * because \"no record found\" and \"found the wrong value\" are different\n * mistakes and the fix differs.\n */\n lastRecords?: string[]\n}\n\n/**\n * Effective limits/gates for a tenant. Plan defaults come from\n * `PLAN_ENTITLEMENTS` (versioned with the app); per-tenant overrides can be\n * stored on the tenant doc and win over the plan defaults.\n *\n * The CORE half. Read {@link OrgEntitlements} instead: it is this plus\n * whatever the plugins declare. The keys spelled out below predate the seam\n * and move to their plugins with the rest of AGL-3080; every NEW key is\n * declared by whoever owns it.\n */\nexport interface CoreOrgEntitlements {\n hostLimit?: number\n screensPerHost?: number\n sharedLayoutsPerHost?: number\n /** Saved templates per host (AGL-666) — includes marketplace downloads. */\n templatesPerHost?: number\n // NOTE (AGL-658): \"add-on\" means two unrelated things in this codebase.\n // `seatAddons` below are BILLING capacity — extra managers, hosts, seats —\n // surfaced in the UI as \"plan add-ons\". The marketplace sense (installed\n // plugins, the `orgAddons` slot) is a different concept entirely. The\n // marketplace owns the bare word; billing copy always qualifies it.\n // Firestore and Stripe lookup keys stay as they are — they are persisted.\n storagePerHostMb?: number\n /**\n * RETIRED (AGL-2133). Enforced by nothing and unreachable by measurement —\n * see `RETIRED_ENTITLEMENT_KEYS`. It stays on the TYPE because live org\n * documents still carry staff overrides of it and `OrgEntitlements` is the\n * shape those documents are read through; `resolveOrgEntitlements` drops\n * it, and no plan declares it any more, so nothing can resolve a value.\n * @deprecated\n */\n totalSiteSizeMb?: number\n /**\n * Included per-site COLLABORATOR seats (`hosts/{id}/members`,\n * viewer/editor/admin) — console teammates scoped to one site, not\n * end-user member accounts (`siteMembers`), which are unlimited on\n * every plan (AGL-888/889). Legacy key name; persisted, do not rename.\n */\n membersPerHost?: number\n /** Seat model (AGL-112): included tenant-manager seats. */\n managersPerOrg?: number\n /** Hard seat caps incl. purchased addons; beyond these, upgrade the plan. */\n maxManagersPerOrg?: number\n /** Hard per-site collaborator cap incl. addons (see `membersPerHost`). */\n maxMembersPerHost?: number\n bandwidthGb?: number\n /**\n * Saved form DEFINITIONS per host — documents under\n * `hosts/{hostId}/forms`, the entity a submission's `formId` points at.\n *\n * Not the same dimension as `formSubmissionsPerMonth`, and the two are\n * routinely confused: this is how many distinct intake forms an author may\n * build, that is how many replies the site may accept. Only the second is\n * tiered. This one resolves to the same `FORMS_PER_HOST_CEILING` on every\n * plan that has the form entity at all — it bounds a collection rather than\n * selling a tier — and exists as an entitlement so that `checkQuota` can\n * refuse a create, and so a contract can override one org's number.\n *\n * Counts the CATALOG, never the traffic. A `Form` node drawn on a page and\n * left unbound submits without a definition and spends nothing here, which\n * is why a plan resolving to 0 still accepts submissions.\n */\n formsPerHost?: number\n /** Component-builder caps (AGL-99): host variables. */\n variablesPerHost?: number\n /** Component-builder caps (AGL-99): host functions. */\n functionsPerHost?: number\n /** Workflow builder cap (AGL-99/101). */\n workflowsPerHost?: number\n /** Bookable services per host (AGL-159). */\n servicesPerHost?: number\n /** Redirect rules per host (AGL-154). */\n redirectsPerHost?: number\n /**\n * The CRM RECORDS band (AGL-197, widened in AGL-2611): contacts, companies\n * and deals, counted together across the org. The persisted key still says\n * \"contacts\" because it is written on live org documents as a staff\n * override and read back through this type; every customer surface says\n * \"CRM records\". A tier with an `extraContactsUsdPer1k` rate meters past\n * it; a tier with none refuses the next record. Free has no CRM\n * (AGL-2851), so on Free the record its band refuses is a capture. Tasks\n * and activities are not counted — see `CRM_ACTIVITIES_PER_RECORD_CEILING`.\n */\n contactsPerHost?: number\n /**\n * CAMPAIGN emails sendable per calendar month (AGL-161), and campaign\n * emails only (AGL-1438).\n *\n * Transactional mail — password resets, invites, order confirmations,\n * booking reminders, workflow notifications — is never refused by this cap\n * at any tier. It still COUNTS toward the org's email cost meter; it simply\n * cannot be blocked, because a quota that can drop a password reset locks\n * somebody out of their own account and a dropped order confirmation reads\n * to the buyer as a failed order.\n *\n * The name is the narrow thing on purpose. A cap that means something\n * narrower than it says is how AGL-1438 came to exist, so every surface\n * that shows this number says \"campaign\".\n *\n * Sized against shared-domain reputation, which is a different principle\n * from the one behind `crmEmailsPerDay` — see there for why the two ladders\n * are not meant to track each other.\n */\n emailSendsPerMonth?: number\n /** Action runs per calendar month (AGL-148). */\n actionRunsPerMonth?: number\n /** Included customer REST API requests per calendar month (AGL-634);\n * Beyond it, metered overage per 1,000 where the plan prices it. Only\n * Business/Advanced carry `apiAccess`, so lower tiers are 0. */\n apiRequestsPerMonth?: number\n /** Dynamic data caps — org-scoped (AGL-239/240): datasets are shared\n * by every host in the org, so counts and size meter per org. */\n datasetsPerOrg?: number\n /** Hard dataset cap incl. addons (AGL-132/240); beyond it, upgrade. */\n maxDatasetsPerOrg?: number\n recordsPerDataset?: number\n /** Included aggregate dataset storage (MB) across the org (AGL-240);\n * Beyond it, metered overage per GB where the plan prices it. */\n dataStorageMbPerOrg?: number\n /** @deprecated Legacy host-keyed override (pre-AGL-240); resolved into\n * `datasetsPerOrg` by `resolveOrgEntitlements`. */\n datasetsPerHost?: number\n /** @deprecated Legacy host-keyed override (pre-AGL-240); resolved into\n * `maxDatasetsPerOrg` by `resolveOrgEntitlements`. */\n maxDatasetsPerHost?: number\n /** Catalog products per host (AGL-278). */\n productsPerHost?: number\n /** Inventory locations per host (AGL-286). */\n inventoryLocations?: number\n /** Concurrent POS registers (AGL-312); add-ons raise it (AGL-329). */\n posRegisters?: number\n /** Platform fee % on physical storefront sales (Connect app fee). */\n transactionFeePhysicalPct?: number\n /** Platform fee % on digital storefront sales (Connect app fee). */\n transactionFeeDigitalPct?: number\n features?: OrgFeatureFlags\n /**\n * A staff PLAN COMP (AGL-3034) — see `OrgPlanComp`. Not a quota: the\n * resolver's numeric loop skips it, and `ResolvedOrgEntitlements` omits\n * it. Read through `readOrgPlanComp`, never directly.\n */\n planComp?: OrgPlanComp\n}\n\n/**\n * Effective limits and gates for a tenant, the platform's and its plugins'\n * together (AGL-3124).\n *\n * {@link CoreOrgEntitlements} is what `PLAN_ENTITLEMENTS` declares per plan\n * and what `ResolvedOrgEntitlements` keeps exhaustive.\n * {@link PluginEntitlementQuotas} is the plugin half: a plugin declares a\n * key of its own into it with `declare module`, and its figure per plan comes\n * from that plugin's `plan-entitlements` declaration, compiled into\n * `PLAN_ENTITLEMENTS` (AGL-3080), widened by any seat add-on it declares. So\n * `resolveOrgEntitlements` carries a plugin's band, and `keyof\n * OrgEntitlements` admits its key, without this file naming the plugin's\n * domain.\n */\nexport interface OrgEntitlements\n extends CoreOrgEntitlements,\n PluginEntitlementQuotas {}\n\n/**\n * A plan staff GRANTED a workspace that no live subscription pays for\n * (AGL-3034), stored at `orgs/{orgId}.entitlements.planComp`.\n *\n * ## Why a marker of its own, and not the stored `plan`\n *\n * `plan: 'pro'` beside `billingStatus: 'canceled'` is what EVERY customer who\n * canceled can look like: Stripe's mirror is not always walked back to\n * `free`, and a staff override used to write `plan` directly. So a stored\n * paid plan must never outrank a dead subscription — `resolveEffectivePlan`\n * reads it as Free, which is AGL-247's rule and stays it. A comp is a\n * different fact with a different writer: only `/api/admin/org-override`\n * writes this, audited, with a reason. Nothing reads a comp out of `plan`.\n *\n * ## When it applies\n *\n * Only while the subscription is dead or absent — `resolvePlanComp`. A live\n * subscription (and any status the resolver does not read as dead) governs\n * the plan, and the comp waits, dormant, until staff remove it or the\n * subscription ends.\n *\n * ## Why under `entitlements`\n *\n * It sits beside the other staff overrides, on the one map the override\n * route already owns and the rules already deny to every client, staff\n * included (AGL-1795). A top-level key would have needed a new entry in all\n * three of the org rule's deny-lists, in a rules file at its size limit.\n *\n * ## What it is NOT\n *\n * Not a sale. A comp bills nothing, so every metered band on it is a wall —\n * `resolvePlanPricing` — and it is never a card on file or a paying\n * subscription: `isBillingSubscription` stays false, and the AI overage\n * charge path has no rate to bill it at.\n *\n * ## Capped or uncapped (AGL-3049)\n *\n * A comp is CAPPED unless staff lift it: every band is its plan's figure, a\n * hard limit, and a per-org quota override beside the comp raises one band.\n * An UNCAPPED comp (`uncapped: true`) reads every band and quota as\n * unlimited while it is in force — the shape an internal workspace needs —\n * and still bills nothing. The flag is part of the grant: the same route\n * writes it with its own reason and audit row, removing the comp removes it,\n * and a live subscription ignores it along with the rest of the comp.\n *\n * The route states it on every grant, `false` included. The org write is a\n * merge, which writes a nested map key by key, so a capped grant that left\n * the key out would keep the `true` of the uncapped comp it replaced.\n *\n * Typed loosely where foundation cannot import the vocabulary: `reason` is\n * an `OrgOverrideReasonCode` (`app-utils/org-override-reason`), narrowed by\n * `readOrgPlanComp`.\n */\nexport interface OrgPlanComp {\n /** The plan granted. A paid plan; a comp of `free` is not a comp. */\n plan: OrgPlan\n /**\n * Every band and quota reads as unlimited while the comp is in force\n * (AGL-3049). Only a literal `true` lifts them; absent means capped.\n */\n uncapped?: boolean\n /** Why, from the override's fixed reason set (AGL-1652). */\n reason: string\n /** Staff-only rationale; explicit `null` when none was given. */\n note: string | null\n /** The staff uid that granted it. */\n grantedBy: string\n /** When it was granted — the server's clock. */\n grantedAt: ITimestamp | null\n}\n\n/**\n * Paid addon quantities (AGL-112/524) purchased on top of the plan's\n * included allowances, billed as items on the org's Stripe subscription.\n * Seat/dataset kinds resolve as `included + purchased` clamped to the\n * plan's hard max — beyond the max the org must upgrade. Purchases only\n * count while the subscription is alive (they bill on it); staff grants\n * live on `entitlements` overrides instead, so the two never collide.\n */\nexport interface OrgSeatAddons {\n /** Extra tenant-manager seats. */\n managers?: number\n /**\n * Extra per-site collaborator seats. Legacy key name (AGL-888): the Stripe\n * price env suffix is still `EXTRA_MEMBER` and existing org docs carry this\n * key — persisted, do not rename.\n *\n * A POOL, not a raise (AGL-2439) — the same correction `posRegisters`\n * received in AGL-1775, applied to the key that never got it.\n * `membersPerHost` is enforced PER SITE, so folding this org-wide quantity\n * into the org-level entitlement handed every site the whole purchase: one\n * extra collaborator seat bought 20 collaborators on a 20-site org. Since\n * AGL-2439 the quantity here is the size of an org-level pool and\n * `collaboratorAllocations` says which site each purchased seat is assigned\n * to. Nothing reads this as a per-site number; use\n * `checkHostCollaboratorQuota` / `resolveHostCollaboratorCap`.\n */\n members?: number\n /** Extra org datasets (AGL-132/240); billed monthly per dataset. */\n datasets?: number\n /** Extra sites beyond the plan's `hostLimit` (AGL-68/524). */\n hosts?: number\n /**\n * Extra POS registers beyond the plan's `posRegisters` (AGL-329/524).\n *\n * A POOL, not a raise (AGL-1775). `posRegisters` is enforced PER SITE, so\n * folding this quantity into the org-level entitlement handed every site\n * the whole purchase — one $89/mo register bought 20 registers on a\n * 20-site org. Since AGL-1775 the quantity here is the size of an org-level\n * pool and `registerAllocations` says which site each purchased seat is\n * assigned to. Nothing reads this as a per-site number; use\n * `resolveHostRegisterCap`.\n */\n posRegisters?: number\n /** Event Calendar org-wide toggle, 0/1 (AGL-145/524). */\n eventCalendar?: number\n /**\n * Aglyn AI org-wide toggle, 0/1 (AGL-2896). One purchase covers every\n * host in the org, the way Event Calendar does: `resolveOrgEntitlements`\n * reads any quantity >= 1 as one, adds the plan's\n * `AI_ADDON_CREDITS_PER_MONTH` band to `assistCreditsPerMonth`, and\n * switches `features.aiGenerative` and `features.aiAssist` on. Priced per\n * plan at `PlanPricing.aiAddonMonthlyUsd`; `null` there means the plan\n * does not sell it.\n */\n aiAddon?: number\n}\n\n/**\n * Which SITE each purchased POS register seat is assigned to (AGL-1775):\n * `{ [hostId]: seats }`, drawn from the org-level pool\n * `seatAddons.posRegisters`.\n *\n * WHY IT LIVES ON THE ORG DOC. The pool is org-level and the entitlement it\n * modifies is resolved from this same document, so a caller that can resolve\n * entitlements at all already holds the allocation — there is no second read\n * to fail and no separate loading state in which a host could resolve to\n * something other than its plan cap. That property is the point: the\n * `checkQuota(undefined)` = Free-tier lesson inverted, where an absent\n * allocation must mean the PLAN's cap and never the pooled total.\n *\n * AN ENTITLEMENT INPUT, and therefore Admin-SDK-only — it is denied to every\n * client in `cloud/firebase-firestore.rules` alongside `seatAddons` and\n * `entitlements`. A client that could write this could assign itself the\n * whole pool on every site, which is the defect AGL-1775 exists to close.\n *\n * A host id absent from the map holds ZERO purchased seats and resolves to\n * the plan cap alone. Deleting a site deletes its key, which returns its\n * seats to the pool — the pool is `seatAddons.posRegisters` minus the sum of\n * this map, so a released key is available capacity by arithmetic rather\n * than by a separate counter that could drift.\n */\nexport type OrgRegisterAllocations = Record<string, number>\n\n/**\n * Which SITE each purchased COLLABORATOR seat is assigned to (AGL-2439):\n * `{ [hostId]: seats }`, drawn from the org-level pool `seatAddons.members`.\n *\n * The same object as `OrgRegisterAllocations` and for the same reasons —\n * see that type's block, all of which applies verbatim. It is a distinct type\n * because it is a distinct field on the document and the two pools are bought\n * separately; nothing may read one as the other.\n *\n * AN ENTITLEMENT INPUT, and therefore Admin-SDK-only — denied to every client\n * in `cloud/firebase-firestore.rules` alongside `seatAddons`,\n * `registerAllocations` and `entitlements`. A client that could write this\n * could assign itself the whole pool on every site, which is the defect the\n * pool exists to close.\n *\n * A host id absent from the map holds ZERO purchased seats and resolves to\n * the plan cap alone. Deleting a site deletes its key, returning its seats to\n * the pool by arithmetic rather than by a counter that could drift.\n *\n * ONE THING DIFFERS FROM REGISTERS and it is deliberate: collaborator seats\n * clamp to the plan's `maxMembersPerHost` band, because that is how they are\n * sold. Assigning ten pool seats to a Starter site cannot lift it past ten —\n * `resolveHostCollaboratorCap` applies the clamp, so the console must not\n * present an assignment past the band as capacity.\n */\nexport type OrgCollaboratorAllocations = Record<string, number>\n\n/**\n * The monthly storage-overage spend cap an org CHOSE for itself (AGL-1886,\n * corrected 2026-08-18).\n *\n * The ceiling on metered spend is the END USER's control: it exists to prevent\n * a surprise, not to gate the product on a customer's acknowledgement.\n *\n * So this document is **absent by default and absent is the normal state**.\n * Storage past a metered plan's included band bills without it; the customer\n * is warned by `usage-alerts` on approach and at the band. Writing a cap here\n * is a customer opting IN to being stopped, and it is the only thing that can\n * refuse an upload on a plan that meters.\n *\n * NOT a consent record. As an acknowledgement that must exist before a paying\n * org may store a byte past its band, it costs revenue and blocks customers\n * without preventing any bill — `report-usage` bills stored bytes and never\n * reads this document.\n *\n * Still an ENTITLEMENT INPUT in the security sense, in the other direction: a\n * member who could raise their own `capUsd` could raise their own spend\n * ceiling, so the rules deny it to every client and only\n * `/api/billing/storage-overage` (Admin SDK, `billing.manage`) writes it.\n *\n * @see apps/console/utils/storage-overage.ts for the model and what still\n * hard-bands.\n */\nexport interface OrgStorageOverage {\n /**\n * The monthly storage-overage spend the org will not go past. Present means\n * capped; absent means uncapped, which is the default. A present but\n * malformed value resolves to `STORAGE_CAP_FALLBACK_USD` rather than to \"no\n * cap\" — a customer who asked for a ceiling must not be billed past one\n * because the stored number was corrupt.\n */\n capUsd?: number\n /** When the cap was set, for the audit trail. */\n capSetAt?: ITimestamp | null\n /** The uid that set it. */\n capSetBy?: string | null\n /**\n * LEGACY (pre-2026-08-18): the acknowledged-consent pair. Read only, and\n * honoured as a cap of `monthlyCeilingUsd` so a ceiling somebody typed is\n * not silently raised. Never written again.\n */\n acknowledgedAt?: ITimestamp | null\n /** @deprecated legacy consent field; see `acknowledgedAt`. */\n acknowledgedBy?: string | null\n /** @deprecated legacy bound; read as a cap. See `capUsd`. */\n monthlyCeilingUsd?: number\n}\n\n/**\n * The org's own answer to what happens when Aglyn Assist reaches its included\n * band (AGL-2653).\n *\n * Absent by default, and absent is the normal state: assist past the band is\n * SOLD — `reserveAssistMessage` keeps reserving and `report-usage` bills the\n * credits past the band at `PLAN_PRICING.extraAssistCreditsUsdPer1k`. Writing\n * `hardCap: true` is a customer opting IN to being stopped at the band\n * instead, which is what the gate did for every org before the overage was\n * sold. The shape mirrors `OrgStorageOverage`, the same control on storage,\n * for the same reason: a ceiling on metered spend is the END USER's control,\n * offered rather than imposed.\n *\n * An ENTITLEMENT INPUT in the security sense, in both directions at once. A\n * client that could clear it would lift a ceiling the org chose; one that\n * could set it on another org would switch that org's assistant off at the\n * band. So the rules deny it to every client and only\n * `/api/billing/assist-overage` (Admin SDK, `billing.manage`) writes it. Read\n * through `resolveAssistHardCap` in the AI plugin's `usage/assist-credits`,\n * never directly.\n *\n * `report-usage` never reads this map — it bills whatever landed past the\n * band, the way storage bills stored bytes. With the cap on that is at most\n * the one exchange that crossed the line, because an exchange's cost is known\n * only after it is answered; with it off, it is the overage the org chose to\n * buy. Reading the switch at sweep time instead would let a flip on the 1st\n * erase a month's overage, the AGL-2399 shape on a different meter.\n *\n * On a plan whose `extraAssistCreditsUsdPer1k` is `null` the switch changes\n * nothing: there is no rate to sell past the band at, so the band stays the\n * wall it always was, on or off.\n *\n * ## The dollar ceiling beside the switch (AGL-2898)\n *\n * `capUsd` is the second control, and it answers a different question. The\n * switch asks \"sell past the band at all?\"; the ceiling asks \"and if so, how\n * much?\" — a monthly figure in USD of OVERAGE, priced at the plan's rate, past\n * which the assistant refuses for the rest of the month. It is the\n * `storageOverage.capUsd` of this meter: optional, absent by default, and the\n * customer's own number rather than a band the plan sold them. Read through\n * `resolveAssistOverageCapUsd`, and enforced by `assistOverageCapReached`\n * inside the same reservation transaction as the band.\n *\n * The two are independent. With the switch on the ceiling is never reached,\n * because nothing past the band is ever sold; with the switch off and no\n * ceiling the overage is open-ended, bounded only by the plan's message cap.\n * On a plan with no rate the ceiling, like the switch, changes nothing:\n * overage on such a plan prices to zero and zero never reaches a ceiling.\n */\nexport interface OrgAssistOverage {\n /** True stops assist at the included band; absent or false sells past it. */\n hardCap?: boolean\n /** When the switch was last written, for the audit trail. */\n hardCapSetAt?: ITimestamp | null\n /** The uid that wrote it. */\n hardCapSetBy?: string | null\n /**\n * The month's ceiling on AI overage, in USD at the plan's rate. `null` or\n * absent is no ceiling; anything that is not a finite positive number reads\n * as none too, so a corrupt value cannot become a wall of `NaN`.\n */\n capUsd?: number | null\n /** When the ceiling was last written or cleared, for the audit trail. */\n capSetAt?: ITimestamp | null\n /** The uid that wrote it. */\n capSetBy?: string | null\n}\n\n/**\n * A discount applied to the org's OWN Aglyn subscription (AGL-1105) — a\n * comped enterprise deal or a redeemed coupon, mirrored from a Stripe coupon\n * so `orgMonthlyRevenueUsd` can report net-of-discount MRR without a Stripe\n * round-trip. Exactly one of `percentOff` / `amountOffUsd` is set, matching\n * the Stripe coupon it points at (Stripe coupons are one or the other).\n * DISTINCT from a storefront/customer discount — this is Aglyn's own bill.\n */\nexport interface OrgDiscount {\n /** The Stripe coupon id (`co_…`) applied to the subscription. */\n couponId: string\n /** The Stripe promotion code id (`promo_…`), when the coupon has a code. */\n promotionCodeId?: string\n /** The human redemption code (e.g. `LAUNCH25`), when one exists. */\n code?: string\n /** Percentage off, 0–100 (mutually exclusive with `amountOffUsd`). */\n percentOff?: number\n /** Fixed USD off per invoice (mutually exclusive with `percentOff`). */\n amountOffUsd?: number\n /** The staff uid that applied it (audit trail; self-serve reads `system`). */\n appliedBy: UserUid\n /** Free-text why (the enterprise deal, the promotion) — staff-only. */\n reason?: string\n appliedAt: ITimestamp\n}\n\nexport interface OrgSubscription {\n status?:\n | 'active'\n | 'trialing'\n | 'past_due'\n | 'canceled'\n | 'incomplete'\n | 'unpaid'\n /**\n * WHY the subscription ended, mirrored from Stripe's\n * `cancellation_details.reason` on `customer.subscription.deleted`\n * (AGL-1877). `'payment_failed'` is Stripe having exhausted the dunning\n * retries; `'cancellation_requested'` is the customer leaving on purpose.\n *\n * The distinction is not cosmetic — it is the only thing that tells a\n * DELINQUENT workspace apart from one that simply left, and both arrive as\n * the same event with the same `status: 'canceled'` and the same\n * `plan: 'free'` mirror. Without it `shouldAutoLockOrgForBilling` had no\n * reachable delinquent state at all (see `billing-auto-lock.ts`), the\n * churn funnel counted a dunning failure as voluntary churn, and nobody\n * could be told which had happened.\n *\n * Absent on every org whose subscription ended before this shipped, and\n * every consumer must fail CLOSED on that — an unknown reason is not a\n * proven payment failure.\n */\n canceledReason?: string | null\n priceId?: string\n /**\n * Billing interval of the plan item (AGL-532), webhook-mirrored: the\n * Billing page initializes its monthly/annual toggle from it and plan\n * switches keep it unless the toggle says otherwise.\n */\n interval?: 'month' | 'year'\n currentPeriodEnd?: ITimestamp\n /**\n * A downgrade scheduled for the current period end (AGL-1862). Set by\n * `/api/billing/subscription` when a switch walks DOWN the self-serve\n * ladder — the Stripe subscription schedule owns the transition, this is\n * the manager-gated mirror the billing page renders. `null` (not absent)\n * once released, so a merge clears it. Rides inside `subscription` because\n * `pickOrgBillingFields` drops any other top-level key.\n */\n pendingDowngrade?: {\n plan: string\n interval: 'month' | 'year'\n /** ISO timestamp of the period end the schedule flips at. */\n effectiveAt: string | null\n scheduleId: string\n } | null\n /**\n * Negotiated custom price as a **monthly-normalized** USD figure (AGL-1110),\n * set when an enterprise org bills at an ad-hoc amount rather than a plan's\n * list price — e.g. `agency` capability at $2,730/mo. An annual custom deal\n * is stored ÷12 here (the yearly total lives on the Stripe price), so every\n * reader treats it as monthly and never re-divides. Webhook-mirrored from the\n * Stripe subscription's recurring price. When present it is the truth for\n * revenue — `orgListPriceMonthlyUsd`/`orgMonthlyRevenueUsd` use it instead of\n * the plan default (fixes the custom-price MRR under-report, AGL-1110).\n */\n customMonthlyUsd?: number\n}\n\n/**\n * The org's billing/entitlement doc shape — the view of `orgs/{orgId}`\n * that `useCurrentOrg()`, the plugin-page `org` prop, and the\n * entitlement resolvers carry. (Formerly `AglynTenant`; the alias was\n * removed in AGL-444.)\n */\n/**\n * `orgs/{orgId}.bandwidthCap` — the engaged free-plan bandwidth cap\n * (AGL-1967/2070/2155).\n *\n * STAMPED WITH THE MONTH IT IS FOR, and never cleared. A new month simply\n * stops matching, so the cap lifts on its own with no write: a marker whose\n * removal depends on a cron running is a marker that stays engaged when the\n * cron fails, which would take a customer's site down over an infrastructure\n * problem rather than over their traffic.\n *\n * Plain numbers rather than `ITimestamp`, matching `suspendedUntilMs`, so the\n * value survives the tenant's cache serialization unchanged.\n */\nexport interface OrgBandwidthCap {\n /** UTC `YYYY-MM` this cap was engaged for. Only the current month refuses. */\n month: string\n /** When the sweep engaged it. Diagnostic only; nothing gates on it. */\n engagedAt?: number\n /** The org-wide page views measured at engage time. Diagnostic. */\n pageViews?: number\n /** The band those page views were measured against. Diagnostic. */\n includedPageViews?: number\n}\n\n/**\n * The organization document.\n *\n * NOT composed from a plugin-augmented interface, deliberately (AGL-3124).\n * `org-write-deny-coverage.spec.ts` enumerates this interface's fields by\n * reading THIS FILE'S SOURCE, and checks every one of them against the\n * Firestore write-deny rules; a field a plugin declared from its own file\n * would be invisible to that sweep, so composing here would open a rules\n * coverage hole to save a plugin an already-solved problem. A plugin's own\n * settings block goes through `registerPluginConfigSchema` into\n * `pluginSettings/{pluginId}`, which is its own document under its own rule.\n */\nexport interface AglynOrgBilling extends AglynDocument {\n /** The document id, injected by the reader — never a stored field. */\n $id: OrgUid\n /**\n * The org's display name, written by `createOrganization` and renamed\n * through /api/orgs/settings (the name is denormalized onto every\n * membership row, so the rename fans out server-side). Client-writable by\n * an org admin on purpose — see `ORG_CLIENT_WRITABLE_FIELDS`.\n *\n * Spelled `displayName` here until AGL-1355; nothing ever read that, and\n * the document has always carried `name`. The coverage guard derives the\n * org's field set from this interface, so a field that does not exist\n * bought nothing but a blind spot.\n */\n name?: string\n /** Free-text workspace description; nothing gates on it. */\n description?: string\n /**\n * The IANA zone this workspace's published dates read in (AGL-3237) —\n * `America/Chicago`, `Europe/Berlin`. Absent is UTC.\n *\n * What it decides is which CALENDAR DAY an instant is attributed to on a\n * published site, which is an editorial fact about the publisher: a post\n * that went out at 19:30 in Chicago is dated the 21st here and was dated\n * the 22nd while UTC decided it. Sites inherit it; see\n * `resolveSiteTimeZone`.\n *\n * SERVER-OWNED: written through /api/orgs/settings and denied to the\n * client SDK, because `resolveSiteTimeZone` reads it in `app-utils` and\n * the write-deny guard treats a resolver input as server-owned.\n * Nothing bills, gates or routes on it, and it is validated at every read\n * through `isSupportedTimeZone`, so an unusable value renders as UTC rather\n * than throwing inside a page render.\n */\n timeZone?: string\n /**\n * The sites this organization declared ONE SENDER, `{ [groupId]: { name,\n * hostIds } }` (`consent-groups.ts`). Absent, every site is alone.\n *\n * SERVER-OWNED: denied to the client SDK, because `consentGroupForHost`\n * reads it in `app-utils` and decides who a marketing basis covers and whose\n * opt-outs hold a send. The Emails hub's consent group editor changes it\n * through `/api/orgs/consent-groups`, and the one write is the executor's\n * declare step, taken only after every separating site's refusals have\n * been carried both ways (`consent-group-change.ts`).\n */\n consentGroups?: Record<string, { name: string; hostIds: string[] }>\n /**\n * A consent group change in flight (AGL-3320): `{ changeId, phase,\n * hostIds, startedAtMs, declaredAtMs? }`, present exactly while one runs\n * and cleared when it finishes or is canceled. The job it names is\n * `orgs/{orgId}/consentGroupChanges/{changeId}`.\n *\n * SERVER-OWNED: written only by the executor, which relies on it as the\n * lock — a second change, and a deletion of a site it names, are refused\n * while it stands — so a client able to clear it could run two changes at\n * once, and one able to set it could block every change after it.\n */\n consentGroupsChange?: {\n changeId: string\n phase: 'carry' | 'rehome' | 'sweep'\n hostIds: string[]\n startedAtMs: number\n declaredAtMs?: number\n }\n /**\n * Whether a declared group's sites wait for each other's confirmation click\n * (AGL-3316). Absent or `false` is off: a pending confirmation holds only\n * the asking site's mail.\n *\n * SERVER-OWNED: written through /api/orgs/settings and denied to the client\n * SDK, for the reason `consentGroups` is — `consentGroupForHost` resolves it\n * into every group a send path reads.\n */\n consentGroupsAwaitConfirmation?: boolean\n /** The workspace URL segment, reserved through `orgSlugs/{slug}`. */\n slug?: string\n /**\n * The owner's uid. Spelled `ownerId` here until AGL-1355 — every reader\n * has always used `ownerUid` (the key `createOrganization` writes and the\n * rules deny), so the declared name was dead.\n */\n ownerUid?: UserUid\n hosts?: Record<HostUid, true>\n /** Subscription tier; missing/unknown plans resolve as `free`. */\n plan?: OrgPlan\n /** Per-org entitlement overrides (admin console); win over plan defaults. */\n entitlements?: OrgEntitlements\n /**\n * Per-org RELEASE-flag overrides (AGL-1635) — a different axis from\n * `entitlements`, which asks what the org's plan includes. These ask\n * whether an unreleased feature is switched on for this one customer, and\n * win over both the Remote Config value and the rollout bucket.\n *\n * Staff-only, and deliberately narrower than `entitlements`: super staff\n * alone may write it, matching the platform-wide flag editor\n * (`/api/admin/flags` is super-only), because forcing a flag on for an org\n * is the same class of act as flipping it for everyone — just scoped.\n * Read through `parseOrgReleaseFlagOverrides`, never directly.\n *\n * Typed loosely on purpose. The keys ARE `ReleaseFlagKey`, but that union\n * lives in `app-utils/release-flags` and foundation cannot import\n * app-utils (see `platform.types.ts`). Declaring the narrow type here\n * would also overstate what is on disk: this map outlives registry\n * renames, so a retired key is a thing that genuinely exists in Firestore.\n * `parseOrgReleaseFlagOverrides` is what narrows it, dropping unknown keys\n * and non-booleans.\n */\n releaseFlags?: Record<string, boolean>\n /**\n * White-label brand identity (White-Label Phase 1). Applied only when the\n * org carries the `whiteLabel` entitlement; read exclusively through\n * `resolveBrandingProfile`, never directly, so no surface diverges.\n */\n brandingProfile?: OrgBrandingProfile\n /**\n * Enterprise SSO config (AGL-1101). Applied only when the org carries the\n * `ssoEnabled` entitlement; the console routes sign-in through the org's\n * GCIP tenant/provider named here. See `OrgSsoConfig`.\n */\n sso?: OrgSsoConfig\n /** Per-org plugin switchboard (AGL-416); see plugin-manager/enabled-plugins. */\n enabledPlugins?: string[]\n /** Purchased addon seats (AGL-112); billed monthly per seat. */\n seatAddons?: OrgSeatAddons\n /**\n * POS register seats assigned out of the org pool (AGL-1775). An\n * ENTITLEMENT INPUT: it raises a per-site cap, so it is Admin-SDK-only.\n * @see OrgRegisterAllocations\n */\n registerAllocations?: OrgRegisterAllocations\n /**\n * Per-site collaborator seats assigned out of the org pool (AGL-2439). An\n * ENTITLEMENT INPUT: it raises a per-site cap, so it is Admin-SDK-only.\n * @see OrgCollaboratorAllocations\n */\n collaboratorAllocations?: OrgCollaboratorAllocations\n /**\n * The free plan's engaged BANDWIDTH CAP (AGL-1967/2070/2155), denormalized\n * onto this doc by the `usage-alerts` cron so the serving path can refuse a\n * capped site without a read of its own.\n *\n * An ENTITLEMENT INPUT in the strongest sense: it is the only thing standing\n * between a free site that has blown its band and unmetered egress, so a\n * client-writable value would let an org admin lift their own cap by\n * deleting one field. Admin-SDK only; denied in the rules.\n *\n * Read exclusively through `bandwidthCapEngaged` (`app-utils/bandwidth-cap`),\n * never directly — the marker on its own is not the answer. The resolver\n * re-derives the plan on every read, which is what lets an org that upgrades\n * mid-month start serving again without waiting for a cron to clear it.\n */\n bandwidthCap?: OrgBandwidthCap\n /**\n * The org's assist hard-cap switch (AGL-2653) — see `OrgAssistOverage`.\n * Admin-SDK-only, denied in the rules beside `storageOverage`; read\n * through `resolveAssistHardCap`.\n */\n assistOverage?: OrgAssistOverage\n stripeCustomerId?: string\n subscription?: OrgSubscription\n /**\n * Discount on the org's own Aglyn subscription (AGL-1105) — staff-applied\n * enterprise deal or a redeemed coupon. `orgMonthlyRevenueUsd` subtracts it\n * for net-of-discount MRR; `orgListPriceMonthlyUsd` ignores it (list price).\n */\n discount?: OrgDiscount\n /**\n * Explicit **comped enterprise** marker (AGL-1110). Makes the org read as\n * \"Enterprise\" (via `isEnterpriseOrg`) without a negotiated custom price —\n * for internal/dogfood accounts (e.g. Aglyn's own org) that carry full\n * Enterprise capability + SSO but are 100%-discounted, so they collect $0\n * while infra cost is still metered. Staff-set; distinct from a paying\n * custom-priced enterprise, which qualifies via `subscription.customMonthlyUsd`.\n */\n enterprise?: boolean\n /**\n * The bare `subscription.status` word the Stripe webhook mirrors back onto\n * this doc for the AGL-275 dunning banner and `resolveEffectivePlan`\n * (AGL-1028). An ENTITLEMENT INPUT: a dead subscription downgrades a paid\n * plan to free, so a client-writable value would restore the plan.\n */\n billingStatus?: string\n /** Staff suspension (AGL-202): set = all the org's sites serve 503. */\n suspendedAt?: ITimestamp | null\n suspendedReason?: string\n /**\n * Whether the suspension above is IN FORCE, stored so the staff\n * Organizations list can filter by it with an equality (AGL-3416). `false`\n * on every unsuspended org; written beside the `suspended*` family by the\n * lockdown core and cleared on a lapsed timed lock by\n * `settleLapsedSuspensions`. Never an enforcement input: every enforcing\n * reader asks the family itself.\n */\n suspended?: boolean\n /**\n * GDPR erasure request (AGL-206): hard deletion happens ONLY via `eraseOrg`\n * after a 7-day hold from this stamp — reached by the\n * `/api/admin/run-erasures` cron, or by hand with\n * tools/scripts/erase-tenant.mjs, which calls the same function (AGL-1481).\n */\n erasureRequestedAt?: ITimestamp | null\n /**\n * The CRM's organization-wide settings (AGL-2613), written from\n * CRM → Settings by an owner or admin with the client SDK — see\n * `ORG_CLIENT_WRITABLE_FIELDS`. One map rather than a key per setting, so\n * the section can grow without a rules change each time.\n */\n crm?: OrgCrmSettings\n createdAt?: ITimestamp\n updatedAt?: ITimestamp\n}\n\n/**\n * What the CRM lets an organization decide for every site at once\n * (AGL-2613). Every key is optional and off when absent: a setting that\n * has never been touched behaves as the product did before it existed.\n */\nexport interface OrgCrmSettings {\n /**\n * Create a company from a captured contact's work email domain when no\n * company the capturing site can see carries that domain. Off by default:\n * a company minted from every domain that ever submitted a form is a list\n * nobody asked for. Public mailbox domains never qualify either way.\n */\n autoCreateCompanies?: boolean\n /**\n * Per-site settings, keyed by host id (AGL-2618). On the ORG document\n * rather than on each host document because the reader is the org-level\n * assignment pass — one read of one document answers every site's default\n * — and because the writer is the same owner-or-admin the rest of this\n * map admits, through the same client branch. A host document would need\n * its own rules clause for a key only the CRM reads.\n */\n hosts?: Record<string, OrgCrmHostSettings>\n /**\n * The assignment rules, in the order they are tried (AGL-2618). The first\n * rule whose every condition holds names the owner; the site's default\n * owner is the fallback when none does. Bounded by\n * `CRM_ASSIGNMENT_RULES_MAX` in the section that writes it.\n */\n assignmentRules?: OrgCrmAssignmentRule[]\n /** The round-robin pool and its pointer — see `OrgCrmRoundRobin`. */\n roundRobin?: OrgCrmRoundRobin\n}\n\n/** What one site decides for records captured on it (AGL-2618). */\nexport interface OrgCrmHostSettings {\n /**\n * The member every record captured on this site is handed to when no\n * assignment rule claimed it. Absent: the record stays unassigned, which\n * is what the product did before the setting existed.\n */\n defaultOwnerUid?: string\n}\n\n/**\n * One assignment rule (AGL-2618): `when` every named condition holds for a\n * capture, `assign` the record this way. A condition left out is not a\n * condition — a rule naming only a source matches every capture from that\n * source — and a rule naming nothing matches every capture, which is how a\n * catch-all is written. `source` is a `ContactSource`, typed as a string\n * here because the foundation cannot import the capture vocabulary; the CRM\n * module narrows it.\n */\nexport interface OrgCrmAssignmentRule {\n id: string\n when: {\n source?: string\n formId?: string\n emailDomain?: string\n tag?: string\n }\n assign: { memberUid: string } | { roundRobin: true }\n}\n\n/**\n * The round-robin pool (AGL-2618): the members handed records in turn, and\n * WHO GOT THE LAST ONE. The pointer is a uid rather than an index so that\n * editing the pool — a member added, removed or moved — never skips or\n * repeats anybody: the next record goes to whoever follows the last\n * recipient in the pool as it stands, and to the first member when the last\n * recipient is no longer in it. Advanced only by the server, inside the\n * transaction that writes the owner, so two captures landing together take\n * two different members.\n */\nexport interface OrgCrmRoundRobin {\n memberUids?: string[]\n lastAssignedUid?: string\n}\n\n/**\n * WHO OWNS EACH FIELD OF `orgs/{orgId}` (AGL-1355).\n *\n * AGL-1354 found four server-owned keys — `brandingProfile`, `sso`,\n * `discount`, `enterprise` — that the Firestore rules' org-update key diff\n * had drifted past, so an org admin holding nothing but the Firebase client\n * SDK could write them on any plan. The keys are closed. The MECHANISM that\n * opened them was a hand-maintained deny-list with nothing checking it, and\n * that is what these two maps close.\n *\n * The rule is DEFAULT-DENY, and it is enforced from the interface above:\n * `org-write-deny-coverage.spec.ts` reads every field declared on\n * `AglynOrgBilling`, and one that appears in neither the rules' deny-list nor\n * one of these maps FAILS THE BUILD, naming the field. Adding a field to the\n * interface therefore forces the ownership decision here, at the declaration,\n * on the same commit — instead of shipping client-writable and staying that\n * way until the next audit.\n *\n * So: add a field above, and either add it to the `hasAny([...])` list in\n * `cloud/firebase-firestore.rules` under `match /orgs/{orgId}` (server-owned:\n * anything an entitlement, a price, a routing decision or a staff judgement\n * reads) or add it below WITH A REASON. When in doubt, deny it — a field the\n * server writes through an Admin-SDK route loses nothing by being denied to\n * the client, and that is true of every key AGL-1354 closed.\n *\n * The entries are the fields an org admin may set from the client SDK. The\n * reason is mandatory and is the whole value of the map: it records that\n * someone decided, rather than that someone forgot.\n */\nexport const ORG_CLIENT_WRITABLE_FIELDS: Readonly<Record<string, string>> = {\n name:\n 'The workspace name. Deliberately writable by an org admin — the rules ' +\n 'admit the rename branch and `firestore-rules.test.mjs` asserts it still ' +\n 'succeeds. Cosmetic: no entitlement, price or routing decision reads it. ' +\n '(The console renames through /api/orgs/settings anyway, because the name ' +\n 'is denormalized onto every membership row and the fan-out is a server ' +\n 'job — but the client write is allowed and must stay allowed.)',\n description:\n 'Free-text workspace blurb. Nothing resolves, gates or bills on it, and ' +\n 'no server route owns it.',\n createdAt:\n 'Creation stamp seeded by `createOrganization`. Nothing gates on it; the ' +\n 'staff org list only displays it. Denying it would buy nothing and would ' +\n 'break the merge-writes below, which stamp the pair together.',\n updatedAt:\n 'Last-write stamp. Every client write that IS allowed sets it in the same ' +\n '`setDoc(..., { merge: true })` — the staff suspension, erasure-request ' +\n 'and plan-override cards all do — so denying it would deny those writes.',\n crm:\n 'The CRM settings map (AGL-2613): `autoCreateCompanies`, the per-site ' +\n 'default owner, the assignment rules and the round-robin pool ' +\n '(AGL-2618), and whatever the CRM → Settings section adds beside them. ' +\n 'Deliberately writable by an org owner or admin — the section writes it ' +\n 'client-direct by dotted path. No entitlement, price, routing decision ' +\n 'or staff judgement reads it; its readers are the contact capture door ' +\n 'deciding whether to mint a company record from an email domain and ' +\n 'whom to hand a new record to, which are choices about the org\\'s own ' +\n \"data that the org's managers are the right people to make. The pool's \" +\n 'pointer (`crm.roundRobin.lastAssignedUid`) is advanced by the server ' +\n 'inside the assigning transaction; a client that moved it would only ' +\n 'change who is next, never what anybody may see.',\n}\n\n/**\n * Fields declared on `AglynOrgBilling` that are NOT stored on the document,\n * and so cannot be written by anyone. Separate from the map above because\n * \"the client may set this\" and \"this is not a field\" are different\n * statements, and collapsing them would let a genuinely client-writable field\n * hide behind a synthetic one.\n */\nexport const ORG_UNPERSISTED_FIELDS: Readonly<Record<string, string>> = {\n $id: 'The document id, injected by the reader. Never written as a field.',\n}\n\n"],"names":["ORG_CLIENT_WRITABLE_FIELDS","name","description","createdAt","updatedAt","crm","ORG_UNPERSISTED_FIELDS","$id"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;CAgBC,GA+xCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,MAAMA,6BAA+D;IAC1EC,MACE,2EACA,6EACA,6EACA,8EACA,2EACA;IACFC,aACE,4EACA;IACFC,WACE,6EACA,6EACA;IACFC,WACE,8EACA,4EACA;IACFC,KACE,0EACA,kEACA,2EACA,4EACA,2EACA,2EACA,wEACA,0EACA,2EACA,0EACA,yEACA;AACJ,EAAC;AAED;;;;;;CAMC,GACD,OAAO,MAAMC,yBAA2D;IACtEC,KAAK;AACP,EAAC"}
1
+ {"version":3,"sources":["../../../../../../../libs/aglyn/src/lib/foundation/definitions/org-billing.types.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The org's billing/entitlement vocabulary (AGL-443 naming cleanup).\n *\n * HISTORY: these types were spelled `Tenant*` until AGL-444 — they\n * predate the organizations migration (AGL-232..238); the retired\n * `tenants/{uid}` collection's billing shape was mirrored ONTO the org\n * doc and the names came along. The alias is gone: everything here is\n * `Org*` and describes fields of `orgs/{orgId}`. The last persisted\n * tenant spellings (the `users.{uid}.tenants` map, Stripe\n * `metadata[tenantId]`, `host.tenantId`) were retired pre-launch in\n * AGL-445 — billing keys off `metadata[orgId]` now.\n *\n * Convention (see the docs-site glossary): \"organization/org\" is the\n * entity; \"workspace\" is the user-facing word for it; \"tenant\" is\n * reserved for the published-site runtime (`apps/tenant`,\n * `@aglyn/tenant-*` libs).\n */\n\nimport type { ITimestamp } from '@aglyn/shared-util-timestamp'\nimport type {\n AglynDocument,\n HostUid,\n OrgUid,\n UserUid,\n} from './platform.types'\n/*\n * The plugin half of the two composed shapes below (AGL-3124).\n *\n * Each is an EMPTY interface a plugin augments with `declare module`, so the\n * keys a plugin owns are part of `OrgEntitlements` and `OrgFeatureFlags`\n * without the core listing them. The AI credits, the CRM's one-to-one email\n * pace, the form-submission and workflow-run bands, the marketplace's take\n * rate and selling gate and commerce's three gated features are declared that\n * way, each in its plugin's `plan-entitlements` module (AGL-3080).\n *\n * `import type` and nothing else: TypeScript erases it, so the plugin-manager\n * module is not on this module's runtime graph and a published page that\n * reaches the billing types (through the foundation barrel it must not reach\n * anyway) carries nothing new.\n */\nimport type {\n PluginEntitlementFeatures,\n PluginEntitlementQuotas,\n} from '../../plugin-manager/plugin-entitlement-keys'\n\nexport type { OrgUid } from './platform.types'\n\n\n/** Hosted in master catalog */\n/**\n * SaaS subscription tiers (Tenant Billing & SaaS Plans, AGL-38..41).\n * Pricing v3 (2026-07) inserted `scale` between business and advanced to\n * fill the $139→$399 gap, and added `agency` above advanced for\n * high-volume multi-site orgs — see the Pricing Decision Log.\n *\n * `enterprise` (AGL-1118) is a REAL plan, not a display label: it tops the\n * ladder with unlimited capacity plus white-label and SSO, and it is the ONE\n * tier with no list price — it is staff-provisioned per deal (AGL-1110), never\n * self-serve. Surfaces that offer plans for sale iterate `SELF_SERVE_PLANS`,\n * which excludes it; surfaces that merely NAME the org's plan read\n * `PLAN_LABELS` and get \"Enterprise\" for free.\n */\nexport type OrgPlan =\n | 'free'\n | 'starter'\n | 'pro'\n | 'business'\n | 'scale'\n | 'advanced'\n | 'agency'\n | 'enterprise'\n\n/**\n * The CORE boolean feature gates per plan; quotas live beside them as\n * numbers. Read {@link OrgFeatureFlags} instead — this half exists so the\n * plan tables can stay exhaustive over the platform's own gates while a\n * plugin declares its own (AGL-3124).\n */\nexport interface CoreOrgFeatureFlags {\n /** A/B experiments (AGL-252); Business tier. */\n abTesting?: boolean\n versioning?: boolean\n reusableComponents?: boolean\n customDomain?: boolean\n /**\n * Send mail as a domain the CUSTOMER owns — `hello@acme.com` — by publishing\n * our SPF, DKIM and return-path records in their own zone.\n *\n * Distinct from `customDomain`, which is the site's public web address and\n * authorizes nothing about mail; and distinct from `whiteLabel`, which\n * replaces the Aglyn brand — product name, logo, colors, support URL,\n * console chrome, favicon — across every branded surface. Sending as your\n * own name is one narrow consequence of white-labeling rather than the\n * whole of it, so the two cannot share a flag without the wider capability\n * following the narrower one wherever it goes.\n *\n * It is also the CHEAP half of the sending model, which is why it sits low\n * on the ladder. A customer-owned domain costs the platform one provider\n * domain object and NOTHING in our own DNS zone, because the customer\n * publishes the records; the platform subdomain a site is issued costs a\n * provider slot, three records in our zone and a permanent place in the\n * re-verification sweep. Holding the free-to-us option behind the highest\n * paywall makes the expensive one the default at every tier that can send.\n *\n * Read through `checkEntitlement` at the two routes that write a sending\n * identity: the org's domain list, and the per-site selection.\n */\n customSendingDomain?: boolean\n /**\n * Hold a sending subdomain of the PLATFORM's own —\n * `hello@{label}.mail.aglyn.app` — provisioned for one site inside Aglyn's\n * mail apex.\n *\n * The EXPENSIVE half of the sending model, and the reason it is a separate\n * flag from `customSendingDomain`. A domain the customer owns costs one\n * provider domain object and nothing in our zone, because they publish the\n * records. One of these costs a provider slot, THREE records in our own\n * zone, and a permanent place in the re-verification sweep — per site,\n * forever. The two capabilities are decided against different resources, so\n * a shared flag would move a bounded one whenever the unbounded one is\n * re-cut.\n *\n * The flag is the SECOND of two conditions and never the only one. Nothing\n * provisions on entitlement alone: a merchant asks from the sending-identity\n * route, this says whether the org carries what they asked for, and the\n * provider ceiling still refuses beyond the account's allowance. A site\n * refused at any of the three sends on the shared pool rather than not at\n * all.\n *\n * Read through `checkEntitlement` at\n * `libs/aglyn/src/lib/app-utils/dedicated-sending-domain.ts`, which is the\n * only reader — the claim path in `host-sending-domain.ts` goes through it.\n */\n dedicatedSendingDomain?: boolean\n removeBranding?: boolean\n /** Schedule a version to publish at a date/time (tier above versioning). */\n scheduledPublishing?: boolean\n /** AI copy assist in the besigner (AGL-89). */\n aiAssist?: boolean\n /**\n * Generative building and automation (AGL-2896): pages, components,\n * layouts, templates, SEO, email, campaigns, A/B variants, analytics\n * insights, products, CRM records, onboarding and workflows produced by\n * the assistant rather than assembled by hand.\n *\n * Distinct from `aiAssist`, which is the chat guide, copy assist and\n * generate-section rung every tier from Pro up carries. This is the\n * expensive rung: a generated surface carries the node tree, the catalog\n * and the theme in and iterates, so it draws credits by the hundreds where\n * a question draws tens. No self-serve tier includes it; the Aglyn AI\n * add-on (`OrgSeatAddons.aiAddon`) switches it on and adds the credit band\n * that funds it, and `resolveOrgEntitlements` flips this flag together\n * with `aiAssist` when the add-on is present. Enterprise carries it in the\n * agreement.\n */\n aiGenerative?: boolean\n /** No-code workflow builder (AGL-101). */\n workflows?: boolean\n /** Datasets + repeatable components (AGL-102/103). */\n dataStore?: boolean\n /** Video/file uploads in the media manager (AGL-162). */\n videoMedia?: boolean\n /** Appointment bookings (AGL-159). */\n bookings?: boolean\n /**\n * THE CRM (AGL-2611): contacts, leads, companies, the deals pipeline,\n * tasks, reports and custom fields, the CRM automation steps, and the\n * `crm:*` REST resources.\n *\n * The Contacts section included (AGL-2851). Capture still writes a site's\n * audience into the contacts collection on every plan including Free,\n * banded by `contactsPerHost`, and exporting or erasing those people stays\n * on every plan in Settings → Privacy. What this flag gates is the CRM a\n * team works them in, and it is the upgrade motive from Free to the first\n * paid tier rather than a line on top of one: for the small-business buyer\n * the CRM is the reason to pick a platform over a page builder.\n *\n * Read by the console shell through the CRM extension's and its widgets'\n * `featureFlag` (so the shell refuses every CRM section and page before\n * one mounts), by the `crm/*` routes, by the automation executor before a\n * CRM step runs, by the REST dispatcher in front of the CRM resources,\n * contacts included, and, restated, by the Firestore rules on a client's\n * CRM writes. A per-org override on `entitlements.features.crm` works the\n * way every other flag's does.\n */\n crm?: boolean\n /**\n * SEQUENCES (AGL-2974): one-to-one, multi-step email sequences a rep sends\n * from their own connected mailbox, logged on the CRM's records. The key\n * below keeps the name the feature shipped under, because it is stored on\n * the org (AGL-3199).\n *\n * False on EVERY plan, Enterprise included. Which tiers carry sequences\n * and connected mailboxes, and at what caps, is a packaging decision that\n * has not been made, so no plan may claim it. An organization reaches it\n * only through the per-org override on `entitlements.features.outreach`.\n *\n * Read by the console shell through the Sequences extension's\n * `featureFlag`, by the plugin's routes, and, restated, by the Firestore\n * rules in front of the `outreach*` collections. The rules carry no plan\n * list for it because no plan grants it; when packaging lands, the plan\n * table and those rules change together.\n */\n outreach?: boolean\n /**\n * Basic presentational interactions (AGL-577): menu/drawer open-close,\n * element show/hide, class toggles, sticky nav, navigation, site\n * alerts. Included on ALL plans — pure client-side DOM with no server\n * cost. The `actions` flag below gates the powerful automation steps\n * (server dispatch, runJs, analytics, overlays, raw HTML).\n *\n * `true` on all eight tiers, and NO code gates on it — which makes it look\n * dead, and it has now been filed as dead once (AGL-2082). It is not.\n * `tools/marketing/build-pricing-tables.mts` reads its value to emit the\n * \"Interactions\" row on the public /pricing compare table, ticked on every\n * plan, and the Free plan card bullets it. That row is the claim that a\n * hover-to-open menu is not a paid feature — a real competitive statement,\n * and the reason `true` everywhere is a DECISION rather than a default.\n *\n * So: do not delete it as dead weight. Deleting it silently removes a\n * public pricing row while the Figma frames still carry it in all four\n * responsive variants, which is a pricing call and not a cleanup.\n */\n interactions?: boolean\n /** Event → action automation builder (AGL-148). */\n actions?: boolean\n /** Outbound/inbound webhooks (AGL-149). */\n webhooks?: boolean\n /** Customer REST API v1 + API keys (AGL-615); Business tier. */\n apiAccess?: boolean\n /** Whole-site export/backup + restore (AGL-163). */\n siteExport?: boolean\n /** Multilingual sites (AGL-164): locale variants + switcher. */\n multilingual?: boolean\n /** Event Calendar add-on (AGL-145); paid, not part of any base tier. */\n eventCalendar?: boolean\n /** URL redirects manager (AGL-154). */\n redirects?: boolean\n /** Per-screen traffic analytics (AGL-150). */\n screenAnalytics?: boolean\n /** CDN delivery + responsive image variants for media (AGL-175). */\n mediaCdn?: boolean\n /** Announcement bar + promotional popups (AGL-195/196). */\n marketingOverlays?: boolean\n /** Full storefront commerce: catalog, cart, checkout (AGL-278). */\n commerce?: boolean\n /** Console point-of-sale mode (AGL-312). */\n pos?: boolean\n /** Entitlement-gated screens/sections/video paywalls (AGL-309). */\n contentGating?: boolean\n /** Verified-buyer product reviews (AGL-324). */\n productReviews?: boolean\n /** Abandoned checkout recovery emails (AGL-323). */\n abandonedCart?: boolean\n /** Dropship supplier routing on paid orders (AGL-289). */\n dropshipRouting?: boolean\n /**\n * White-label the platform (White-Label Phase 1): replace the Aglyn brand\n * — product name, logo, colors, support URL, transactional email from-name\n * — with the org's own `brandingProfile` across every branded surface.\n * Agency ($799) and Enterprise carry it on the plan (AGL-1118); any other\n * tier needs a per-org `entitlements` override. Strictly broader than\n * `removeBranding`, which only drops the\n * \"Made with Aglyn\" badge on published sites; white-label REPLACES the\n * brand rather than merely hiding it. Every branded surface resolves the\n * effective brand through `resolveBrandingProfile` so it can never drift.\n */\n whiteLabel?: boolean\n /**\n * Enterprise SSO (AGL-1101): the org's console users sign in through the\n * org's own SAML/OIDC IdP, wired as a per-org GCIP tenant (`org.sso`).\n * Distinct from `whiteLabel` — an Agency org can have one without the other.\n * Carried by the `enterprise` plan (AGL-1118) and false on every other base\n * plan; a lower tier needs a per-org `entitlements` override, which is how\n * enterprise orgs provisioned before that plan existed still get it. Gates\n * the staff SSO-config card and the SSO sign-in path; a non-entitled org can\n * neither configure nor use SSO.\n */\n ssoEnabled?: boolean\n}\n\n/**\n * Boolean feature gates per plan, the platform's and its plugins' together.\n *\n * {@link CoreOrgFeatureFlags} is the platform's own set — what\n * `PLAN_ENTITLEMENTS` declares for every plan, and what\n * `Required<CoreOrgFeatureFlags>` keeps exhaustive.\n * {@link PluginEntitlementFeatures} is the plugin half (AGL-3124): a plugin\n * declares a gate of its own into it with `declare module`, and its answer\n * per plan comes from that plugin's `plan-entitlements` declaration or its\n * `registerPluginEntitlements` defaults rather than from a row core writes —\n * which is why a plugin's gate joins HERE and not above. Every reader keeps reading `OrgFeatureFlags`, and\n * `keyof OrgFeatureFlags` admits a plugin's key the moment it is declared.\n */\nexport interface OrgFeatureFlags\n extends CoreOrgFeatureFlags,\n PluginEntitlementFeatures {}\n\n/**\n * An org's white-label brand identity (White-Label Phase 1). Populated on\n * the org doc (`orgs/{orgId}.brandingProfile`) and applied ONLY when the org\n * carries the `whiteLabel` entitlement (Agency or Enterprise plan, or a\n * per-org override);\n * otherwise every surface falls back to the Aglyn defaults baked into\n * `resolveBrandingProfile`. Every field is optional — a partial profile\n * still resolves, with the Aglyn default filling each gap — so an agency can\n * set just a product name and from-name without supplying logos.\n */\nexport interface OrgBrandingProfile {\n /** Brand name shown in place of \"Aglyn\" (console chrome, emails, badges). */\n productName?: string\n /** Primary/full-color logo URL (light backgrounds, console chrome). */\n logoUrl?: string\n /**\n * The logo for DARK grounds (AGL-3406): the console chrome in dark mode and the\n * published site's attribution badge, which always sits on a dark pill.\n * Unset falls back to {@link logoUrl}.\n */\n logoDarkUrl?: string\n /** Favicon URL for branded surfaces. */\n faviconUrl?: string\n /**\n * The favicon for a browser whose tab strip is dark, emitted as a second\n * `<link rel=\"icon\">` under `(prefers-color-scheme: dark)`. Unset falls back\n * to {@link faviconUrl}.\n */\n faviconDarkUrl?: string\n /** Brand primary color as a CSS color (hex), e.g. `#1a73e8`. */\n primaryColor?: string\n /** Support/help destination linked from branded surfaces and emails. */\n supportUrl?: string\n /** Transactional email from-name (the display name before the address). */\n fromName?: string\n /** Logo URL specifically for the email header (often a hosted PNG). */\n emailLogoUrl?: string\n /** Custom console domain the agency serves the app on (Phase 4 wiring). */\n customConsoleDomain?: string\n}\n\n/**\n * An org's enterprise SSO configuration (AGL-1101, Phase 1). Applied ONLY when\n * the org carries the `ssoEnabled` entitlement. The actual IdP lives in a\n * per-org **GCIP tenant** (`tenantId`) with a SAML/OIDC **provider**\n * (`providerId`); this block is the org-doc mirror the console reads to route\n * sign-in and the staff card edits. A public `ssoDomains/{domain}` doc maps a\n * verified email domain → `{ orgId, tenantId, providerId }` so the\n * pre-auth sign-in page can resolve an SSO org without reading the org doc.\n *\n * Security: a domain reaches `domains[]` ONLY by passing DNS TXT verification\n * (AGL-1210). This is the account-takeover guard, and it is the whole reason\n * self-serve is safe: without it an org could claim a domain it does not own,\n * `ssoDomains/{domain}` would route that domain's sign-ins to its IdP, and it\n * would intercept another company's logins. Claims in flight live in\n * `orgs/{orgId}/ssoDomains/{domain}` (see `OrgSsoDomainClaim`) and are NOT\n * governed until verified.\n *\n * `enforced` blocks password/social login for the governed domains, so it is a\n * lockout risk and is rehearsed (`previewSsoEnforcement`) before it is applied.\n */\nexport interface OrgSsoConfig {\n /** GCIP tenant id that carries this org's IdP provider. */\n tenantId: string\n /** GCIP provider id, e.g. `saml.aglyn-workspace` or `oidc.acme`. */\n providerId: string\n /** IdP protocol. Phase 1 ships SAML; OIDC is Phase 2. */\n protocol: 'saml' | 'oidc'\n /** Human label for the IdP (shown on the SSO button + staff card). */\n displayName?: string\n /**\n * Email domains routed to this IdP (lowercased, no `@`). A domain is added\n * here only after its claim passes DNS TXT verification, and removed the\n * moment re-verification fails — so membership of this array IS the\n * \"ownership proven\" statement. Never write to it from anywhere but the\n * verification path.\n */\n domains: string[]\n /**\n * Retained for `sso-jit`, which gates on it. Kept in lockstep with\n * `domains.length > 0` by the verification path; it was a staff-attested\n * boolean before AGL-1210 and is now derived, never asserted by a human.\n */\n domainVerified: boolean\n /**\n * SAML metadata the CUSTOMER supplies about their IdP. Stored so the pool's\n * provider config can be rebuilt or re-applied without asking again. The\n * X.509 certificate is a public signing certificate, not a secret.\n */\n idp?: {\n entityId: string\n ssoUrl: string\n certificates: string[]\n }\n /**\n * Require SSO for the governed domains — disables password/social login for\n * them (Phase 2). Phase 1 keeps this false so users keep a fallback.\n */\n enforced: boolean\n /** Lifecycle: `configuring` (not live), `active`, or `disabled`. */\n status: 'configuring' | 'active' | 'disabled'\n /**\n * Uid + time of the last config change. Since AGL-1210 this is normally an\n * ORG ADMIN, not staff — the flow is self-serve end to end.\n */\n configuredBy?: string\n configuredAt?: ITimestamp\n}\n\n/**\n * A pending or proven claim on one email domain (AGL-1210), stored at\n * `orgs/{orgId}/ssoDomains/{domain}`.\n *\n * A subcollection rather than a map on the org doc because domains contain\n * dots, and a dotted key in a Firestore map is read as a nested field path by\n * every update helper — writing `sso.domainClaims[\"acme.com\"]` would silently\n * create `{acme: {com: …}}`. A document id has no such ambiguity.\n *\n * The claim is deliberately worthless on its own: holding one grants nothing.\n * Only `verified` moving to true adds the domain to `sso.domains`, and only\n * that array is consulted at sign-in.\n */\nexport interface OrgSsoDomainClaim {\n /** The domain being claimed (lowercased, no `@`); mirrors the document id. */\n domain: string\n /**\n * Random value the org must publish as a DNS TXT record at\n * `_aglyn-challenge.<domain>`. Per org+domain, so two orgs claiming the same\n * domain get different tokens and neither can pass on the other's record.\n */\n token: string\n /** True once a DNS lookup has actually seen `token` at the challenge host. */\n verified: boolean\n createdAt: ITimestamp\n verifiedAt?: ITimestamp\n /** Last lookup attempt, successful or not — drives re-verification. */\n lastCheckedAt?: ITimestamp\n /**\n * TXT records seen on the last FAILED lookup. Shown back to the customer,\n * because \"no record found\" and \"found the wrong value\" are different\n * mistakes and the fix differs.\n */\n lastRecords?: string[]\n}\n\n/**\n * Effective limits/gates for a tenant. Plan defaults come from\n * `PLAN_ENTITLEMENTS` (versioned with the app); per-tenant overrides can be\n * stored on the tenant doc and win over the plan defaults.\n *\n * The CORE half. Read {@link OrgEntitlements} instead: it is this plus\n * whatever the plugins declare. The keys spelled out below predate the seam\n * and move to their plugins with the rest of AGL-3080; every NEW key is\n * declared by whoever owns it.\n */\nexport interface CoreOrgEntitlements {\n hostLimit?: number\n screensPerHost?: number\n sharedLayoutsPerHost?: number\n /** Saved templates per host (AGL-666) — includes marketplace downloads. */\n templatesPerHost?: number\n // NOTE (AGL-658): \"add-on\" means two unrelated things in this codebase.\n // `seatAddons` below are BILLING capacity — extra managers, hosts, seats —\n // surfaced in the UI as \"plan add-ons\". The marketplace sense (installed\n // plugins, the `orgAddons` slot) is a different concept entirely. The\n // marketplace owns the bare word; billing copy always qualifies it.\n // Firestore and Stripe lookup keys stay as they are — they are persisted.\n storagePerHostMb?: number\n /**\n * RETIRED (AGL-2133). Enforced by nothing and unreachable by measurement —\n * see `RETIRED_ENTITLEMENT_KEYS`. It stays on the TYPE because live org\n * documents still carry staff overrides of it and `OrgEntitlements` is the\n * shape those documents are read through; `resolveOrgEntitlements` drops\n * it, and no plan declares it any more, so nothing can resolve a value.\n * @deprecated\n */\n totalSiteSizeMb?: number\n /**\n * Included per-site COLLABORATOR seats (`hosts/{id}/members`,\n * viewer/editor/admin) — console teammates scoped to one site, not\n * end-user member accounts (`siteMembers`), which are unlimited on\n * every plan (AGL-888/889). Legacy key name; persisted, do not rename.\n */\n membersPerHost?: number\n /** Seat model (AGL-112): included tenant-manager seats. */\n managersPerOrg?: number\n /** Hard seat caps incl. purchased addons; beyond these, upgrade the plan. */\n maxManagersPerOrg?: number\n /** Hard per-site collaborator cap incl. addons (see `membersPerHost`). */\n maxMembersPerHost?: number\n bandwidthGb?: number\n /**\n * Saved form DEFINITIONS per host — documents under\n * `hosts/{hostId}/forms`, the entity a submission's `formId` points at.\n *\n * Not the same dimension as `formSubmissionsPerMonth`, and the two are\n * routinely confused: this is how many distinct intake forms an author may\n * build, that is how many replies the site may accept. Only the second is\n * tiered. This one resolves to the same `FORMS_PER_HOST_CEILING` on every\n * plan that has the form entity at all — it bounds a collection rather than\n * selling a tier — and exists as an entitlement so that `checkQuota` can\n * refuse a create, and so a contract can override one org's number.\n *\n * Counts the CATALOG, never the traffic. A `Form` node drawn on a page and\n * left unbound submits without a definition and spends nothing here, which\n * is why a plan resolving to 0 still accepts submissions.\n */\n formsPerHost?: number\n /** Component-builder caps (AGL-99): host variables. */\n variablesPerHost?: number\n /** Component-builder caps (AGL-99): host functions. */\n functionsPerHost?: number\n /** Workflow builder cap (AGL-99/101). */\n workflowsPerHost?: number\n /** Bookable services per host (AGL-159). */\n servicesPerHost?: number\n /** Redirect rules per host (AGL-154). */\n redirectsPerHost?: number\n /**\n * The CRM RECORDS band (AGL-197, widened in AGL-2611): contacts, companies\n * and deals, counted together across the org. The persisted key still says\n * \"contacts\" because it is written on live org documents as a staff\n * override and read back through this type; every customer surface says\n * \"CRM records\". A tier with an `extraContactsUsdPer1k` rate meters past\n * it; a tier with none refuses the next record. Free has no CRM\n * (AGL-2851), so on Free the record its band refuses is a capture. Tasks\n * and activities are not counted — see `CRM_ACTIVITIES_PER_RECORD_CEILING`.\n */\n contactsPerHost?: number\n /**\n * CAMPAIGN emails sendable per calendar month (AGL-161), and campaign\n * emails only (AGL-1438).\n *\n * Transactional mail — password resets, invites, order confirmations,\n * booking reminders, workflow notifications — is never refused by this cap\n * at any tier. It still COUNTS toward the org's email cost meter; it simply\n * cannot be blocked, because a quota that can drop a password reset locks\n * somebody out of their own account and a dropped order confirmation reads\n * to the buyer as a failed order.\n *\n * The name is the narrow thing on purpose. A cap that means something\n * narrower than it says is how AGL-1438 came to exist, so every surface\n * that shows this number says \"campaign\".\n *\n * Sized against shared-domain reputation, which is a different principle\n * from the one behind `crmEmailsPerDay` — see there for why the two ladders\n * are not meant to track each other.\n */\n emailSendsPerMonth?: number\n /** Action runs per calendar month (AGL-148). */\n actionRunsPerMonth?: number\n /** Included customer REST API requests per calendar month (AGL-634);\n * Beyond it, metered overage per 1,000 where the plan prices it. Only\n * Business/Advanced carry `apiAccess`, so lower tiers are 0. */\n apiRequestsPerMonth?: number\n /** Dynamic data caps — org-scoped (AGL-239/240): datasets are shared\n * by every host in the org, so counts and size meter per org. */\n datasetsPerOrg?: number\n /** Hard dataset cap incl. addons (AGL-132/240); beyond it, upgrade. */\n maxDatasetsPerOrg?: number\n recordsPerDataset?: number\n /** Included aggregate dataset storage (MB) across the org (AGL-240);\n * Beyond it, metered overage per GB where the plan prices it. */\n dataStorageMbPerOrg?: number\n /** @deprecated Legacy host-keyed override (pre-AGL-240); resolved into\n * `datasetsPerOrg` by `resolveOrgEntitlements`. */\n datasetsPerHost?: number\n /** @deprecated Legacy host-keyed override (pre-AGL-240); resolved into\n * `maxDatasetsPerOrg` by `resolveOrgEntitlements`. */\n maxDatasetsPerHost?: number\n /** Catalog products per host (AGL-278). */\n productsPerHost?: number\n /** Inventory locations per host (AGL-286). */\n inventoryLocations?: number\n /** Concurrent POS registers (AGL-312); add-ons raise it (AGL-329). */\n posRegisters?: number\n /** Platform fee % on physical storefront sales (Connect app fee). */\n transactionFeePhysicalPct?: number\n /** Platform fee % on digital storefront sales (Connect app fee). */\n transactionFeeDigitalPct?: number\n features?: OrgFeatureFlags\n /**\n * A staff PLAN COMP (AGL-3034) — see `OrgPlanComp`. Not a quota: the\n * resolver's numeric loop skips it, and `ResolvedOrgEntitlements` omits\n * it. Read through `readOrgPlanComp`, never directly.\n */\n planComp?: OrgPlanComp\n}\n\n/**\n * Effective limits and gates for a tenant, the platform's and its plugins'\n * together (AGL-3124).\n *\n * {@link CoreOrgEntitlements} is what `PLAN_ENTITLEMENTS` declares per plan\n * and what `ResolvedOrgEntitlements` keeps exhaustive.\n * {@link PluginEntitlementQuotas} is the plugin half: a plugin declares a\n * key of its own into it with `declare module`, and its figure per plan comes\n * from that plugin's `plan-entitlements` declaration, compiled into\n * `PLAN_ENTITLEMENTS` (AGL-3080), widened by any seat add-on it declares. So\n * `resolveOrgEntitlements` carries a plugin's band, and `keyof\n * OrgEntitlements` admits its key, without this file naming the plugin's\n * domain.\n */\nexport interface OrgEntitlements\n extends CoreOrgEntitlements,\n PluginEntitlementQuotas {}\n\n/**\n * A plan staff GRANTED a workspace that no live subscription pays for\n * (AGL-3034), stored at `orgs/{orgId}.entitlements.planComp`.\n *\n * ## Why a marker of its own, and not the stored `plan`\n *\n * `plan: 'pro'` beside `billingStatus: 'canceled'` is what EVERY customer who\n * canceled can look like: Stripe's mirror is not always walked back to\n * `free`, and a staff override used to write `plan` directly. So a stored\n * paid plan must never outrank a dead subscription — `resolveEffectivePlan`\n * reads it as Free, which is AGL-247's rule and stays it. A comp is a\n * different fact with a different writer: only `/api/admin/org-override`\n * writes this, audited, with a reason. Nothing reads a comp out of `plan`.\n *\n * ## When it applies\n *\n * Only while the subscription is dead or absent — `resolvePlanComp`. A live\n * subscription (and any status the resolver does not read as dead) governs\n * the plan, and the comp waits, dormant, until staff remove it or the\n * subscription ends.\n *\n * ## Why under `entitlements`\n *\n * It sits beside the other staff overrides, on the one map the override\n * route already owns and the rules already deny to every client, staff\n * included (AGL-1795). A top-level key would have needed a new entry in all\n * three of the org rule's deny-lists, in a rules file at its size limit.\n *\n * ## What it is NOT\n *\n * Not a sale. A comp bills nothing, so every metered band on it is a wall —\n * `resolvePlanPricing` — and it is never a card on file or a paying\n * subscription: `isBillingSubscription` stays false, and the AI overage\n * charge path has no rate to bill it at.\n *\n * ## Capped or uncapped (AGL-3049)\n *\n * A comp is CAPPED unless staff lift it: every band is its plan's figure, a\n * hard limit, and a per-org quota override beside the comp raises one band.\n * An UNCAPPED comp (`uncapped: true`) reads every band and quota as\n * unlimited while it is in force — the shape an internal workspace needs —\n * and still bills nothing. The flag is part of the grant: the same route\n * writes it with its own reason and audit row, removing the comp removes it,\n * and a live subscription ignores it along with the rest of the comp.\n *\n * The route states it on every grant, `false` included. The org write is a\n * merge, which writes a nested map key by key, so a capped grant that left\n * the key out would keep the `true` of the uncapped comp it replaced.\n *\n * Typed loosely where foundation cannot import the vocabulary: `reason` is\n * an `OrgOverrideReasonCode` (`app-utils/org-override-reason`), narrowed by\n * `readOrgPlanComp`.\n */\nexport interface OrgPlanComp {\n /** The plan granted. A paid plan; a comp of `free` is not a comp. */\n plan: OrgPlan\n /**\n * Every band and quota reads as unlimited while the comp is in force\n * (AGL-3049). Only a literal `true` lifts them; absent means capped.\n */\n uncapped?: boolean\n /** Why, from the override's fixed reason set (AGL-1652). */\n reason: string\n /** Staff-only rationale; explicit `null` when none was given. */\n note: string | null\n /** The staff uid that granted it. */\n grantedBy: string\n /** When it was granted — the server's clock. */\n grantedAt: ITimestamp | null\n}\n\n/**\n * Paid addon quantities (AGL-112/524) purchased on top of the plan's\n * included allowances, billed as items on the org's Stripe subscription.\n * Seat/dataset kinds resolve as `included + purchased` clamped to the\n * plan's hard max — beyond the max the org must upgrade. Purchases only\n * count while the subscription is alive (they bill on it); staff grants\n * live on `entitlements` overrides instead, so the two never collide.\n */\nexport interface OrgSeatAddons {\n /** Extra tenant-manager seats. */\n managers?: number\n /**\n * Extra per-site collaborator seats. Legacy key name (AGL-888): the Stripe\n * price env suffix is still `EXTRA_MEMBER` and existing org docs carry this\n * key — persisted, do not rename.\n *\n * A POOL, not a raise (AGL-2439) — the same correction `posRegisters`\n * received in AGL-1775, applied to the key that never got it.\n * `membersPerHost` is enforced PER SITE, so folding this org-wide quantity\n * into the org-level entitlement handed every site the whole purchase: one\n * extra collaborator seat bought 20 collaborators on a 20-site org. Since\n * AGL-2439 the quantity here is the size of an org-level pool and\n * `collaboratorAllocations` says which site each purchased seat is assigned\n * to. Nothing reads this as a per-site number; use\n * `checkHostCollaboratorQuota` / `resolveHostCollaboratorCap`.\n */\n members?: number\n /** Extra org datasets (AGL-132/240); billed monthly per dataset. */\n datasets?: number\n /** Extra sites beyond the plan's `hostLimit` (AGL-68/524). */\n hosts?: number\n /**\n * Extra POS registers beyond the plan's `posRegisters` (AGL-329/524).\n *\n * A POOL, not a raise (AGL-1775). `posRegisters` is enforced PER SITE, so\n * folding this quantity into the org-level entitlement handed every site\n * the whole purchase — one $89/mo register bought 20 registers on a\n * 20-site org. Since AGL-1775 the quantity here is the size of an org-level\n * pool and `registerAllocations` says which site each purchased seat is\n * assigned to. Nothing reads this as a per-site number; use\n * `resolveHostRegisterCap`.\n */\n posRegisters?: number\n /** Event Calendar org-wide toggle, 0/1 (AGL-145/524). */\n eventCalendar?: number\n /**\n * Aglyn AI org-wide toggle, 0/1 (AGL-2896). One purchase covers every\n * host in the org, the way Event Calendar does: `resolveOrgEntitlements`\n * reads any quantity >= 1 as one, adds the plan's\n * `AI_ADDON_CREDITS_PER_MONTH` band to `assistCreditsPerMonth`, and\n * switches `features.aiGenerative` and `features.aiAssist` on. Priced per\n * plan at `PlanPricing.aiAddonMonthlyUsd`; `null` there means the plan\n * does not sell it.\n */\n aiAddon?: number\n}\n\n/**\n * Which SITE each purchased POS register seat is assigned to (AGL-1775):\n * `{ [hostId]: seats }`, drawn from the org-level pool\n * `seatAddons.posRegisters`.\n *\n * WHY IT LIVES ON THE ORG DOC. The pool is org-level and the entitlement it\n * modifies is resolved from this same document, so a caller that can resolve\n * entitlements at all already holds the allocation — there is no second read\n * to fail and no separate loading state in which a host could resolve to\n * something other than its plan cap. That property is the point: the\n * `checkQuota(undefined)` = Free-tier lesson inverted, where an absent\n * allocation must mean the PLAN's cap and never the pooled total.\n *\n * AN ENTITLEMENT INPUT, and therefore Admin-SDK-only — it is denied to every\n * client in `cloud/firebase-firestore.rules` alongside `seatAddons` and\n * `entitlements`. A client that could write this could assign itself the\n * whole pool on every site, which is the defect AGL-1775 exists to close.\n *\n * A host id absent from the map holds ZERO purchased seats and resolves to\n * the plan cap alone. Deleting a site deletes its key, which returns its\n * seats to the pool — the pool is `seatAddons.posRegisters` minus the sum of\n * this map, so a released key is available capacity by arithmetic rather\n * than by a separate counter that could drift.\n */\nexport type OrgRegisterAllocations = Record<string, number>\n\n/**\n * Which SITE each purchased COLLABORATOR seat is assigned to (AGL-2439):\n * `{ [hostId]: seats }`, drawn from the org-level pool `seatAddons.members`.\n *\n * The same object as `OrgRegisterAllocations` and for the same reasons —\n * see that type's block, all of which applies verbatim. It is a distinct type\n * because it is a distinct field on the document and the two pools are bought\n * separately; nothing may read one as the other.\n *\n * AN ENTITLEMENT INPUT, and therefore Admin-SDK-only — denied to every client\n * in `cloud/firebase-firestore.rules` alongside `seatAddons`,\n * `registerAllocations` and `entitlements`. A client that could write this\n * could assign itself the whole pool on every site, which is the defect the\n * pool exists to close.\n *\n * A host id absent from the map holds ZERO purchased seats and resolves to\n * the plan cap alone. Deleting a site deletes its key, returning its seats to\n * the pool by arithmetic rather than by a counter that could drift.\n *\n * ONE THING DIFFERS FROM REGISTERS and it is deliberate: collaborator seats\n * clamp to the plan's `maxMembersPerHost` band, because that is how they are\n * sold. Assigning ten pool seats to a Starter site cannot lift it past ten —\n * `resolveHostCollaboratorCap` applies the clamp, so the console must not\n * present an assignment past the band as capacity.\n */\nexport type OrgCollaboratorAllocations = Record<string, number>\n\n/**\n * The monthly storage-overage spend cap an org CHOSE for itself (AGL-1886,\n * corrected 2026-08-18).\n *\n * The ceiling on metered spend is the END USER's control: it exists to prevent\n * a surprise, not to gate the product on a customer's acknowledgement.\n *\n * So this document is **absent by default and absent is the normal state**.\n * Storage past a metered plan's included band bills without it; the customer\n * is warned by `usage-alerts` on approach and at the band. Writing a cap here\n * is a customer opting IN to being stopped, and it is the only thing that can\n * refuse an upload on a plan that meters.\n *\n * NOT a consent record. As an acknowledgement that must exist before a paying\n * org may store a byte past its band, it costs revenue and blocks customers\n * without preventing any bill — `report-usage` bills stored bytes and never\n * reads this document.\n *\n * Still an ENTITLEMENT INPUT in the security sense, in the other direction: a\n * member who could raise their own `capUsd` could raise their own spend\n * ceiling, so the rules deny it to every client and only\n * `/api/billing/storage-overage` (Admin SDK, `billing.manage`) writes it.\n *\n * @see apps/console/utils/storage-overage.ts for the model and what still\n * hard-bands.\n */\nexport interface OrgStorageOverage {\n /**\n * The monthly storage-overage spend the org will not go past. Present means\n * capped; absent means uncapped, which is the default. A present but\n * malformed value resolves to `STORAGE_CAP_FALLBACK_USD` rather than to \"no\n * cap\" — a customer who asked for a ceiling must not be billed past one\n * because the stored number was corrupt.\n */\n capUsd?: number\n /** When the cap was set, for the audit trail. */\n capSetAt?: ITimestamp | null\n /** The uid that set it. */\n capSetBy?: string | null\n /**\n * LEGACY (pre-2026-08-18): the acknowledged-consent pair. Read only, and\n * honoured as a cap of `monthlyCeilingUsd` so a ceiling somebody typed is\n * not silently raised. Never written again.\n */\n acknowledgedAt?: ITimestamp | null\n /** @deprecated legacy consent field; see `acknowledgedAt`. */\n acknowledgedBy?: string | null\n /** @deprecated legacy bound; read as a cap. See `capUsd`. */\n monthlyCeilingUsd?: number\n}\n\n/**\n * The org's own answer to what happens when Aglyn Assist reaches its included\n * band (AGL-2653).\n *\n * Absent by default, and absent is the normal state: assist past the band is\n * SOLD — `reserveAssistMessage` keeps reserving and `report-usage` bills the\n * credits past the band at `PLAN_PRICING.extraAssistCreditsUsdPer1k`. Writing\n * `hardCap: true` is a customer opting IN to being stopped at the band\n * instead, which is what the gate did for every org before the overage was\n * sold. The shape mirrors `OrgStorageOverage`, the same control on storage,\n * for the same reason: a ceiling on metered spend is the END USER's control,\n * offered rather than imposed.\n *\n * An ENTITLEMENT INPUT in the security sense, in both directions at once. A\n * client that could clear it would lift a ceiling the org chose; one that\n * could set it on another org would switch that org's assistant off at the\n * band. So the rules deny it to every client and only\n * `/api/billing/assist-overage` (Admin SDK, `billing.manage`) writes it. Read\n * through `resolveAssistHardCap` in the AI plugin's `usage/assist-credits`,\n * never directly.\n *\n * `report-usage` never reads this map — it bills whatever landed past the\n * band, the way storage bills stored bytes. With the cap on that is at most\n * the one exchange that crossed the line, because an exchange's cost is known\n * only after it is answered; with it off, it is the overage the org chose to\n * buy. Reading the switch at sweep time instead would let a flip on the 1st\n * erase a month's overage, the AGL-2399 shape on a different meter.\n *\n * On a plan whose `extraAssistCreditsUsdPer1k` is `null` the switch changes\n * nothing: there is no rate to sell past the band at, so the band stays the\n * wall it always was, on or off.\n *\n * ## The dollar ceiling beside the switch (AGL-2898)\n *\n * `capUsd` is the second control, and it answers a different question. The\n * switch asks \"sell past the band at all?\"; the ceiling asks \"and if so, how\n * much?\" — a monthly figure in USD of OVERAGE, priced at the plan's rate, past\n * which the assistant refuses for the rest of the month. It is the\n * `storageOverage.capUsd` of this meter: optional, absent by default, and the\n * customer's own number rather than a band the plan sold them. Read through\n * `resolveAssistOverageCapUsd`, and enforced by `assistOverageCapReached`\n * inside the same reservation transaction as the band.\n *\n * The two are independent. With the switch on the ceiling is never reached,\n * because nothing past the band is ever sold; with the switch off and no\n * ceiling the overage is open-ended, bounded only by the plan's message cap.\n * On a plan with no rate the ceiling, like the switch, changes nothing:\n * overage on such a plan prices to zero and zero never reaches a ceiling.\n */\nexport interface OrgAssistOverage {\n /** True stops assist at the included band; absent or false sells past it. */\n hardCap?: boolean\n /** When the switch was last written, for the audit trail. */\n hardCapSetAt?: ITimestamp | null\n /** The uid that wrote it. */\n hardCapSetBy?: string | null\n /**\n * The month's ceiling on AI overage, in USD at the plan's rate. `null` or\n * absent is no ceiling; anything that is not a finite positive number reads\n * as none too, so a corrupt value cannot become a wall of `NaN`.\n */\n capUsd?: number | null\n /** When the ceiling was last written or cleared, for the audit trail. */\n capSetAt?: ITimestamp | null\n /** The uid that wrote it. */\n capSetBy?: string | null\n}\n\n/**\n * A discount applied to the org's OWN Aglyn subscription (AGL-1105) — a\n * comped enterprise deal or a redeemed coupon, mirrored from a Stripe coupon\n * so `orgMonthlyRevenueUsd` can report net-of-discount MRR without a Stripe\n * round-trip. Exactly one of `percentOff` / `amountOffUsd` is set, matching\n * the Stripe coupon it points at (Stripe coupons are one or the other).\n * DISTINCT from a storefront/customer discount — this is Aglyn's own bill.\n */\nexport interface OrgDiscount {\n /** The Stripe coupon id (`co_…`) applied to the subscription. */\n couponId: string\n /** The Stripe promotion code id (`promo_…`), when the coupon has a code. */\n promotionCodeId?: string\n /** The human redemption code (e.g. `LAUNCH25`), when one exists. */\n code?: string\n /** Percentage off, 0–100 (mutually exclusive with `amountOffUsd`). */\n percentOff?: number\n /** Fixed USD off per invoice (mutually exclusive with `percentOff`). */\n amountOffUsd?: number\n /** The staff uid that applied it (audit trail; self-serve reads `system`). */\n appliedBy: UserUid\n /** Free-text why (the enterprise deal, the promotion) — staff-only. */\n reason?: string\n appliedAt: ITimestamp\n}\n\nexport interface OrgSubscription {\n status?:\n | 'active'\n | 'trialing'\n | 'past_due'\n | 'canceled'\n | 'incomplete'\n | 'unpaid'\n /**\n * WHY the subscription ended, mirrored from Stripe's\n * `cancellation_details.reason` on `customer.subscription.deleted`\n * (AGL-1877). `'payment_failed'` is Stripe having exhausted the dunning\n * retries; `'cancellation_requested'` is the customer leaving on purpose.\n *\n * The distinction is not cosmetic — it is the only thing that tells a\n * DELINQUENT workspace apart from one that simply left, and both arrive as\n * the same event with the same `status: 'canceled'` and the same\n * `plan: 'free'` mirror. Without it `shouldAutoLockOrgForBilling` had no\n * reachable delinquent state at all (see `billing-auto-lock.ts`), the\n * churn funnel counted a dunning failure as voluntary churn, and nobody\n * could be told which had happened.\n *\n * Absent on every org whose subscription ended before this shipped, and\n * every consumer must fail CLOSED on that — an unknown reason is not a\n * proven payment failure.\n */\n canceledReason?: string | null\n priceId?: string\n /**\n * Billing interval of the plan item (AGL-532), webhook-mirrored: the\n * Billing page initializes its monthly/annual toggle from it and plan\n * switches keep it unless the toggle says otherwise.\n */\n interval?: 'month' | 'year'\n currentPeriodEnd?: ITimestamp\n /**\n * A downgrade scheduled for the current period end (AGL-1862). Set by\n * `/api/billing/subscription` when a switch walks DOWN the self-serve\n * ladder — the Stripe subscription schedule owns the transition, this is\n * the manager-gated mirror the billing page renders. `null` (not absent)\n * once released, so a merge clears it. Rides inside `subscription` because\n * `pickOrgBillingFields` drops any other top-level key.\n */\n pendingDowngrade?: {\n plan: string\n interval: 'month' | 'year'\n /** ISO timestamp of the period end the schedule flips at. */\n effectiveAt: string | null\n scheduleId: string\n } | null\n /**\n * Negotiated custom price as a **monthly-normalized** USD figure (AGL-1110),\n * set when an enterprise org bills at an ad-hoc amount rather than a plan's\n * list price — e.g. `agency` capability at $2,730/mo. An annual custom deal\n * is stored ÷12 here (the yearly total lives on the Stripe price), so every\n * reader treats it as monthly and never re-divides. Webhook-mirrored from the\n * Stripe subscription's recurring price. When present it is the truth for\n * revenue — `orgListPriceMonthlyUsd`/`orgMonthlyRevenueUsd` use it instead of\n * the plan default (fixes the custom-price MRR under-report, AGL-1110).\n */\n customMonthlyUsd?: number\n}\n\n/**\n * The org's billing/entitlement doc shape — the view of `orgs/{orgId}`\n * that `useCurrentOrg()`, the plugin-page `org` prop, and the\n * entitlement resolvers carry. (Formerly `AglynTenant`; the alias was\n * removed in AGL-444.)\n */\n/**\n * `orgs/{orgId}.bandwidthCap` — the engaged free-plan bandwidth cap\n * (AGL-1967/2070/2155).\n *\n * STAMPED WITH THE MONTH IT IS FOR, and never cleared. A new month simply\n * stops matching, so the cap lifts on its own with no write: a marker whose\n * removal depends on a cron running is a marker that stays engaged when the\n * cron fails, which would take a customer's site down over an infrastructure\n * problem rather than over their traffic.\n *\n * Plain numbers rather than `ITimestamp`, matching `suspendedUntilMs`, so the\n * value survives the tenant's cache serialization unchanged.\n */\nexport interface OrgBandwidthCap {\n /** UTC `YYYY-MM` this cap was engaged for. Only the current month refuses. */\n month: string\n /** When the sweep engaged it. Diagnostic only; nothing gates on it. */\n engagedAt?: number\n /** The org-wide page views measured at engage time. Diagnostic. */\n pageViews?: number\n /** The band those page views were measured against. Diagnostic. */\n includedPageViews?: number\n}\n\n/**\n * The organization document.\n *\n * NOT composed from a plugin-augmented interface, deliberately (AGL-3124).\n * `org-write-deny-coverage.spec.ts` enumerates this interface's fields by\n * reading THIS FILE'S SOURCE, and checks every one of them against the\n * Firestore write-deny rules; a field a plugin declared from its own file\n * would be invisible to that sweep, so composing here would open a rules\n * coverage hole to save a plugin an already-solved problem. A plugin's own\n * settings block goes through `registerPluginConfigSchema` into\n * `pluginSettings/{pluginId}`, which is its own document under its own rule.\n */\nexport interface AglynOrgBilling extends AglynDocument {\n /** The document id, injected by the reader — never a stored field. */\n $id: OrgUid\n /**\n * The org's display name, written by `createOrganization` and renamed\n * through /api/orgs/settings (the name is denormalized onto every\n * membership row, so the rename fans out server-side). Client-writable by\n * an org admin on purpose — see `ORG_CLIENT_WRITABLE_FIELDS`.\n *\n * Spelled `displayName` here until AGL-1355; nothing ever read that, and\n * the document has always carried `name`. The coverage guard derives the\n * org's field set from this interface, so a field that does not exist\n * bought nothing but a blind spot.\n */\n name?: string\n /** Free-text workspace description; nothing gates on it. */\n description?: string\n /**\n * The IANA zone this workspace's published dates read in (AGL-3237) —\n * `America/Chicago`, `Europe/Berlin`. Absent is UTC.\n *\n * What it decides is which CALENDAR DAY an instant is attributed to on a\n * published site, which is an editorial fact about the publisher: a post\n * that went out at 19:30 in Chicago is dated the 21st here and was dated\n * the 22nd while UTC decided it. Sites inherit it; see\n * `resolveSiteTimeZone`.\n *\n * SERVER-OWNED: written through /api/orgs/settings and denied to the\n * client SDK, because `resolveSiteTimeZone` reads it in `app-utils` and\n * the write-deny guard treats a resolver input as server-owned.\n * Nothing bills, gates or routes on it, and it is validated at every read\n * through `isSupportedTimeZone`, so an unusable value renders as UTC rather\n * than throwing inside a page render.\n */\n timeZone?: string\n /**\n * The sites this organization declared ONE SENDER, `{ [groupId]: { name,\n * hostIds } }` (`consent-groups.ts`). Absent, every site is alone.\n *\n * SERVER-OWNED: denied to the client SDK, because `consentGroupForHost`\n * reads it in `app-utils` and decides who a marketing basis covers and whose\n * opt-outs hold a send. The Emails hub's consent group editor changes it\n * through `/api/orgs/consent-groups`, and the one write is the executor's\n * declare step, taken only after every separating site's refusals have\n * been carried both ways (`consent-group-change.ts`).\n */\n consentGroups?: Record<string, { name: string; hostIds: string[] }>\n /**\n * A consent group change in flight (AGL-3320): `{ changeId, phase,\n * hostIds, startedAtMs, declaredAtMs? }`, present exactly while one runs\n * and cleared when it finishes or is canceled. The job it names is\n * `orgs/{orgId}/consentGroupChanges/{changeId}`.\n *\n * SERVER-OWNED: written only by the executor, which relies on it as the\n * lock — a second change, and a deletion of a site it names, are refused\n * while it stands — so a client able to clear it could run two changes at\n * once, and one able to set it could block every change after it.\n */\n consentGroupsChange?: {\n changeId: string\n phase: 'carry' | 'rehome' | 'sweep'\n hostIds: string[]\n startedAtMs: number\n declaredAtMs?: number\n }\n /**\n * Whether a declared group's sites wait for each other's confirmation click\n * (AGL-3316). Absent or `false` is off: a pending confirmation holds only\n * the asking site's mail.\n *\n * SERVER-OWNED: written through /api/orgs/settings and denied to the client\n * SDK, for the reason `consentGroups` is — `consentGroupForHost` resolves it\n * into every group a send path reads.\n */\n consentGroupsAwaitConfirmation?: boolean\n /** The workspace URL segment, reserved through `orgSlugs/{slug}`. */\n slug?: string\n /**\n * The owner's uid. Spelled `ownerId` here until AGL-1355 — every reader\n * has always used `ownerUid` (the key `createOrganization` writes and the\n * rules deny), so the declared name was dead.\n */\n ownerUid?: UserUid\n hosts?: Record<HostUid, true>\n /** Subscription tier; missing/unknown plans resolve as `free`. */\n plan?: OrgPlan\n /** Per-org entitlement overrides (admin console); win over plan defaults. */\n entitlements?: OrgEntitlements\n /**\n * Per-org RELEASE-flag overrides (AGL-1635) — a different axis from\n * `entitlements`, which asks what the org's plan includes. These ask\n * whether an unreleased feature is switched on for this one customer, and\n * win over both the Remote Config value and the rollout bucket.\n *\n * Staff-only, and deliberately narrower than `entitlements`: super staff\n * alone may write it, matching the platform-wide flag editor\n * (`/api/admin/flags` is super-only), because forcing a flag on for an org\n * is the same class of act as flipping it for everyone — just scoped.\n * Read through `parseOrgReleaseFlagOverrides`, never directly.\n *\n * Typed loosely on purpose. The keys ARE `ReleaseFlagKey`, but that union\n * lives in `app-utils/release-flags` and foundation cannot import\n * app-utils (see `platform.types.ts`). Declaring the narrow type here\n * would also overstate what is on disk: this map outlives registry\n * renames, so a retired key is a thing that genuinely exists in Firestore.\n * `parseOrgReleaseFlagOverrides` is what narrows it, dropping unknown keys\n * and non-booleans.\n */\n releaseFlags?: Record<string, boolean>\n /**\n * White-label brand identity (White-Label Phase 1). Applied only when the\n * org carries the `whiteLabel` entitlement; read exclusively through\n * `resolveBrandingProfile`, never directly, so no surface diverges.\n */\n brandingProfile?: OrgBrandingProfile\n /**\n * Enterprise SSO config (AGL-1101). Applied only when the org carries the\n * `ssoEnabled` entitlement; the console routes sign-in through the org's\n * GCIP tenant/provider named here. See `OrgSsoConfig`.\n */\n sso?: OrgSsoConfig\n /** Per-org plugin switchboard (AGL-416); see plugin-manager/enabled-plugins. */\n enabledPlugins?: string[]\n /** Purchased addon seats (AGL-112); billed monthly per seat. */\n seatAddons?: OrgSeatAddons\n /**\n * POS register seats assigned out of the org pool (AGL-1775). An\n * ENTITLEMENT INPUT: it raises a per-site cap, so it is Admin-SDK-only.\n * @see OrgRegisterAllocations\n */\n registerAllocations?: OrgRegisterAllocations\n /**\n * Per-site collaborator seats assigned out of the org pool (AGL-2439). An\n * ENTITLEMENT INPUT: it raises a per-site cap, so it is Admin-SDK-only.\n * @see OrgCollaboratorAllocations\n */\n collaboratorAllocations?: OrgCollaboratorAllocations\n /**\n * The free plan's engaged BANDWIDTH CAP (AGL-1967/2070/2155), denormalized\n * onto this doc by the `usage-alerts` cron so the serving path can refuse a\n * capped site without a read of its own.\n *\n * An ENTITLEMENT INPUT in the strongest sense: it is the only thing standing\n * between a free site that has blown its band and unmetered egress, so a\n * client-writable value would let an org admin lift their own cap by\n * deleting one field. Admin-SDK only; denied in the rules.\n *\n * Read exclusively through `bandwidthCapEngaged` (`app-utils/bandwidth-cap`),\n * never directly — the marker on its own is not the answer. The resolver\n * re-derives the plan on every read, which is what lets an org that upgrades\n * mid-month start serving again without waiting for a cron to clear it.\n */\n bandwidthCap?: OrgBandwidthCap\n /**\n * The org's assist hard-cap switch (AGL-2653) — see `OrgAssistOverage`.\n * Admin-SDK-only, denied in the rules beside `storageOverage`; read\n * through `resolveAssistHardCap`.\n */\n assistOverage?: OrgAssistOverage\n stripeCustomerId?: string\n subscription?: OrgSubscription\n /**\n * Discount on the org's own Aglyn subscription (AGL-1105) — staff-applied\n * enterprise deal or a redeemed coupon. `orgMonthlyRevenueUsd` subtracts it\n * for net-of-discount MRR; `orgListPriceMonthlyUsd` ignores it (list price).\n */\n discount?: OrgDiscount\n /**\n * Explicit **comped enterprise** marker (AGL-1110). Makes the org read as\n * \"Enterprise\" (via `isEnterpriseOrg`) without a negotiated custom price —\n * for internal/dogfood accounts (e.g. Aglyn's own org) that carry full\n * Enterprise capability + SSO but are 100%-discounted, so they collect $0\n * while infra cost is still metered. Staff-set; distinct from a paying\n * custom-priced enterprise, which qualifies via `subscription.customMonthlyUsd`.\n */\n enterprise?: boolean\n /**\n * The bare `subscription.status` word the Stripe webhook mirrors back onto\n * this doc for the AGL-275 dunning banner and `resolveEffectivePlan`\n * (AGL-1028). An ENTITLEMENT INPUT: a dead subscription downgrades a paid\n * plan to free, so a client-writable value would restore the plan.\n */\n billingStatus?: string\n /** Staff suspension (AGL-202): set = all the org's sites serve 503. */\n suspendedAt?: ITimestamp | null\n suspendedReason?: string\n /**\n * Whether the suspension above is IN FORCE, stored so the staff\n * Organizations list can filter by it with an equality (AGL-3416). `false`\n * on every unsuspended org; written beside the `suspended*` family by the\n * lockdown core and cleared on a lapsed timed lock by\n * `settleLapsedSuspensions`. Never an enforcement input: every enforcing\n * reader asks the family itself.\n */\n suspended?: boolean\n /**\n * GDPR erasure request (AGL-206): hard deletion happens ONLY via `eraseOrg`\n * after a 7-day hold from this stamp — reached by the\n * `/api/admin/run-erasures` cron, or by hand with\n * tools/scripts/erase-tenant.mjs, which calls the same function (AGL-1481).\n */\n erasureRequestedAt?: ITimestamp | null\n /**\n * The CRM's organization-wide settings (AGL-2613), written from\n * CRM → Settings by an owner or admin with the client SDK — see\n * `ORG_CLIENT_WRITABLE_FIELDS`. One map rather than a key per setting, so\n * the section can grow without a rules change each time.\n */\n crm?: OrgCrmSettings\n /**\n * The plan the platform team has asked this workspace to move to\n * (AGL-3466) — see `OrgUpgradeProposal`.\n *\n * SERVER-OWNED: written only by /api/admin/org-upgrade-proposal (staff,\n * audited) and cleared by `writeOrgBilling` once a subscription is live,\n * both Admin SDK. Denied to every client in the rules, staff included,\n * because `standingUpgradeProposal` reads it in `app-utils` and the\n * proposal is the platform's statement to the customer, not theirs.\n */\n upgradeProposal?: OrgUpgradeProposal\n createdAt?: ITimestamp\n updatedAt?: ITimestamp\n}\n\n/**\n * The platform team's request that a workspace move to a paid plan\n * (AGL-3466).\n *\n * The separate step that follows an evaluation: a client handed a workspace\n * looks around first, on whatever the workspace already has, and is asked to\n * upgrade only once staff record this. While it stands and no subscription\n * is live, the workspace's billing managers see it on the org home and on\n * Billing, with the plan preselected. Nothing about it grants or bills.\n */\nexport interface OrgUpgradeProposal {\n /** A paid plan the workspace can buy itself — never `free` or `enterprise`. */\n plan: OrgPlan\n /** The staff member who proposed it. */\n proposedBy: string\n /** Epoch millis, so every cache serialization reads it back unchanged. */\n proposedAt: number\n /** An optional line from staff, shown with the proposal. */\n note?: string\n}\n\n/**\n * What the CRM lets an organization decide for every site at once\n * (AGL-2613). Every key is optional and off when absent: a setting that\n * has never been touched behaves as the product did before it existed.\n */\nexport interface OrgCrmSettings {\n /**\n * Create a company from a captured contact's work email domain when no\n * company the capturing site can see carries that domain. Off by default:\n * a company minted from every domain that ever submitted a form is a list\n * nobody asked for. Public mailbox domains never qualify either way.\n */\n autoCreateCompanies?: boolean\n /**\n * Per-site settings, keyed by host id (AGL-2618). On the ORG document\n * rather than on each host document because the reader is the org-level\n * assignment pass — one read of one document answers every site's default\n * — and because the writer is the same owner-or-admin the rest of this\n * map admits, through the same client branch. A host document would need\n * its own rules clause for a key only the CRM reads.\n */\n hosts?: Record<string, OrgCrmHostSettings>\n /**\n * The assignment rules, in the order they are tried (AGL-2618). The first\n * rule whose every condition holds names the owner; the site's default\n * owner is the fallback when none does. Bounded by\n * `CRM_ASSIGNMENT_RULES_MAX` in the section that writes it.\n */\n assignmentRules?: OrgCrmAssignmentRule[]\n /** The round-robin pool and its pointer — see `OrgCrmRoundRobin`. */\n roundRobin?: OrgCrmRoundRobin\n}\n\n/** What one site decides for records captured on it (AGL-2618). */\nexport interface OrgCrmHostSettings {\n /**\n * The member every record captured on this site is handed to when no\n * assignment rule claimed it. Absent: the record stays unassigned, which\n * is what the product did before the setting existed.\n */\n defaultOwnerUid?: string\n}\n\n/**\n * One assignment rule (AGL-2618): `when` every named condition holds for a\n * capture, `assign` the record this way. A condition left out is not a\n * condition — a rule naming only a source matches every capture from that\n * source — and a rule naming nothing matches every capture, which is how a\n * catch-all is written. `source` is a `ContactSource`, typed as a string\n * here because the foundation cannot import the capture vocabulary; the CRM\n * module narrows it.\n */\nexport interface OrgCrmAssignmentRule {\n id: string\n when: {\n source?: string\n formId?: string\n emailDomain?: string\n tag?: string\n }\n assign: { memberUid: string } | { roundRobin: true }\n}\n\n/**\n * The round-robin pool (AGL-2618): the members handed records in turn, and\n * WHO GOT THE LAST ONE. The pointer is a uid rather than an index so that\n * editing the pool — a member added, removed or moved — never skips or\n * repeats anybody: the next record goes to whoever follows the last\n * recipient in the pool as it stands, and to the first member when the last\n * recipient is no longer in it. Advanced only by the server, inside the\n * transaction that writes the owner, so two captures landing together take\n * two different members.\n */\nexport interface OrgCrmRoundRobin {\n memberUids?: string[]\n lastAssignedUid?: string\n}\n\n/**\n * WHO OWNS EACH FIELD OF `orgs/{orgId}` (AGL-1355).\n *\n * AGL-1354 found four server-owned keys — `brandingProfile`, `sso`,\n * `discount`, `enterprise` — that the Firestore rules' org-update key diff\n * had drifted past, so an org admin holding nothing but the Firebase client\n * SDK could write them on any plan. The keys are closed. The MECHANISM that\n * opened them was a hand-maintained deny-list with nothing checking it, and\n * that is what these two maps close.\n *\n * The rule is DEFAULT-DENY, and it is enforced from the interface above:\n * `org-write-deny-coverage.spec.ts` reads every field declared on\n * `AglynOrgBilling`, and one that appears in neither the rules' deny-list nor\n * one of these maps FAILS THE BUILD, naming the field. Adding a field to the\n * interface therefore forces the ownership decision here, at the declaration,\n * on the same commit — instead of shipping client-writable and staying that\n * way until the next audit.\n *\n * So: add a field above, and either add it to the `hasAny([...])` list in\n * `cloud/firebase-firestore.rules` under `match /orgs/{orgId}` (server-owned:\n * anything an entitlement, a price, a routing decision or a staff judgement\n * reads) or add it below WITH A REASON. When in doubt, deny it — a field the\n * server writes through an Admin-SDK route loses nothing by being denied to\n * the client, and that is true of every key AGL-1354 closed.\n *\n * The entries are the fields an org admin may set from the client SDK. The\n * reason is mandatory and is the whole value of the map: it records that\n * someone decided, rather than that someone forgot.\n */\nexport const ORG_CLIENT_WRITABLE_FIELDS: Readonly<Record<string, string>> = {\n name:\n 'The workspace name. Deliberately writable by an org admin — the rules ' +\n 'admit the rename branch and `firestore-rules.test.mjs` asserts it still ' +\n 'succeeds. Cosmetic: no entitlement, price or routing decision reads it. ' +\n '(The console renames through /api/orgs/settings anyway, because the name ' +\n 'is denormalized onto every membership row and the fan-out is a server ' +\n 'job — but the client write is allowed and must stay allowed.)',\n description:\n 'Free-text workspace blurb. Nothing resolves, gates or bills on it, and ' +\n 'no server route owns it.',\n createdAt:\n 'Creation stamp seeded by `createOrganization`. Nothing gates on it; the ' +\n 'staff org list only displays it. Denying it would buy nothing and would ' +\n 'break the merge-writes below, which stamp the pair together.',\n updatedAt:\n 'Last-write stamp. Every client write that IS allowed sets it in the same ' +\n '`setDoc(..., { merge: true })` — the staff suspension, erasure-request ' +\n 'and plan-override cards all do — so denying it would deny those writes.',\n crm:\n 'The CRM settings map (AGL-2613): `autoCreateCompanies`, the per-site ' +\n 'default owner, the assignment rules and the round-robin pool ' +\n '(AGL-2618), and whatever the CRM → Settings section adds beside them. ' +\n 'Deliberately writable by an org owner or admin — the section writes it ' +\n 'client-direct by dotted path. No entitlement, price, routing decision ' +\n 'or staff judgement reads it; its readers are the contact capture door ' +\n 'deciding whether to mint a company record from an email domain and ' +\n 'whom to hand a new record to, which are choices about the org\\'s own ' +\n \"data that the org's managers are the right people to make. The pool's \" +\n 'pointer (`crm.roundRobin.lastAssignedUid`) is advanced by the server ' +\n 'inside the assigning transaction; a client that moved it would only ' +\n 'change who is next, never what anybody may see.',\n}\n\n/**\n * Fields declared on `AglynOrgBilling` that are NOT stored on the document,\n * and so cannot be written by anyone. Separate from the map above because\n * \"the client may set this\" and \"this is not a field\" are different\n * statements, and collapsing them would let a genuinely client-writable field\n * hide behind a synthetic one.\n */\nexport const ORG_UNPERSISTED_FIELDS: Readonly<Record<string, string>> = {\n $id: 'The document id, injected by the reader. Never written as a field.',\n}\n\n"],"names":["ORG_CLIENT_WRITABLE_FIELDS","name","description","createdAt","updatedAt","crm","ORG_UNPERSISTED_FIELDS","$id"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;CAgBC,GA+zCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,MAAMA,6BAA+D;IAC1EC,MACE,2EACA,6EACA,6EACA,8EACA,2EACA;IACFC,aACE,4EACA;IACFC,WACE,6EACA,6EACA;IACFC,WACE,8EACA,4EACA;IACFC,KACE,0EACA,kEACA,2EACA,4EACA,2EACA,2EACA,wEACA,0EACA,2EACA,0EACA,yEACA;AACJ,EAAC;AAED;;;;;;CAMC,GACD,OAAO,MAAMC,yBAA2D;IACtEC,KAAK;AACP,EAAC"}