@aglyn/tenant-data-admin 1.0.0-beta.143

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/jest.integration.config.d.ts +39 -0
  4. package/jest.integration.setup.d.ts +17 -0
  5. package/package.json +51 -0
  6. package/src/index.d.ts +17 -0
  7. package/src/index.js +18 -0
  8. package/src/index.js.map +1 -0
  9. package/src/lib/render-cache.d.ts +184 -0
  10. package/src/lib/render-cache.js +198 -0
  11. package/src/lib/render-cache.js.map +1 -0
  12. package/src/lib/server/account-addresses.d.ts +160 -0
  13. package/src/lib/server/account-addresses.js +311 -0
  14. package/src/lib/server/account-addresses.js.map +1 -0
  15. package/src/lib/server/account-emails.d.ts +219 -0
  16. package/src/lib/server/account-emails.js +640 -0
  17. package/src/lib/server/account-emails.js.map +1 -0
  18. package/src/lib/server/admin-audit.d.ts +180 -0
  19. package/src/lib/server/admin-audit.js +233 -0
  20. package/src/lib/server/admin-audit.js.map +1 -0
  21. package/src/lib/server/analytics-retention.d.ts +63 -0
  22. package/src/lib/server/analytics-retention.js +66 -0
  23. package/src/lib/server/analytics-retention.js.map +1 -0
  24. package/src/lib/server/api-http.d.ts +159 -0
  25. package/src/lib/server/api-http.js +230 -0
  26. package/src/lib/server/api-http.js.map +1 -0
  27. package/src/lib/server/api-keys.d.ts +117 -0
  28. package/src/lib/server/api-keys.js +298 -0
  29. package/src/lib/server/api-keys.js.map +1 -0
  30. package/src/lib/server/auth-handoff.d.ts +165 -0
  31. package/src/lib/server/auth-handoff.js +482 -0
  32. package/src/lib/server/auth-handoff.js.map +1 -0
  33. package/src/lib/server/auth-pools.d.ts +246 -0
  34. package/src/lib/server/auth-pools.js +551 -0
  35. package/src/lib/server/auth-pools.js.map +1 -0
  36. package/src/lib/server/billing-addons.d.ts +171 -0
  37. package/src/lib/server/billing-addons.js +277 -0
  38. package/src/lib/server/billing-addons.js.map +1 -0
  39. package/src/lib/server/campaign-attribution-store.d.ts +55 -0
  40. package/src/lib/server/campaign-attribution-store.js +95 -0
  41. package/src/lib/server/campaign-attribution-store.js.map +1 -0
  42. package/src/lib/server/campaign-conversion-attribution.d.ts +205 -0
  43. package/src/lib/server/campaign-conversion-attribution.js +279 -0
  44. package/src/lib/server/campaign-conversion-attribution.js.map +1 -0
  45. package/src/lib/server/client-error-report.d.ts +301 -0
  46. package/src/lib/server/client-error-report.js +755 -0
  47. package/src/lib/server/client-error-report.js.map +1 -0
  48. package/src/lib/server/connect-account-status.d.ts +74 -0
  49. package/src/lib/server/connect-account-status.js +94 -0
  50. package/src/lib/server/connect-account-status.js.map +1 -0
  51. package/src/lib/server/connect-payout-failure.d.ts +103 -0
  52. package/src/lib/server/connect-payout-failure.js +127 -0
  53. package/src/lib/server/connect-payout-failure.js.map +1 -0
  54. package/src/lib/server/console-api-rate-limit.d.ts +81 -0
  55. package/src/lib/server/console-api-rate-limit.js +111 -0
  56. package/src/lib/server/console-api-rate-limit.js.map +1 -0
  57. package/src/lib/server/console-domains.d.ts +326 -0
  58. package/src/lib/server/console-domains.js +779 -0
  59. package/src/lib/server/console-domains.js.map +1 -0
  60. package/src/lib/server/consume-once.d.ts +91 -0
  61. package/src/lib/server/consume-once.js +96 -0
  62. package/src/lib/server/consume-once.js.map +1 -0
  63. package/src/lib/server/contact-company-link.d.ts +85 -0
  64. package/src/lib/server/contact-company-link.js +105 -0
  65. package/src/lib/server/contact-company-link.js.map +1 -0
  66. package/src/lib/server/contact-email-engagement.d.ts +32 -0
  67. package/src/lib/server/contact-email-engagement.js +145 -0
  68. package/src/lib/server/contact-email-engagement.js.map +1 -0
  69. package/src/lib/server/contact-email-index.d.ts +56 -0
  70. package/src/lib/server/contact-email-index.js +158 -0
  71. package/src/lib/server/contact-email-index.js.map +1 -0
  72. package/src/lib/server/contact-lifecycle-floor.d.ts +89 -0
  73. package/src/lib/server/contact-lifecycle-floor.js +88 -0
  74. package/src/lib/server/contact-lifecycle-floor.js.map +1 -0
  75. package/src/lib/server/contact-merge.d.ts +55 -0
  76. package/src/lib/server/contact-merge.js +279 -0
  77. package/src/lib/server/contact-merge.js.map +1 -0
  78. package/src/lib/server/contact-suppression.d.ts +153 -0
  79. package/src/lib/server/contact-suppression.js +259 -0
  80. package/src/lib/server/contact-suppression.js.map +1 -0
  81. package/src/lib/server/crm-booking-activity.d.ts +86 -0
  82. package/src/lib/server/crm-booking-activity.js +251 -0
  83. package/src/lib/server/crm-booking-activity.js.map +1 -0
  84. package/src/lib/server/crm-email-activity.d.ts +112 -0
  85. package/src/lib/server/crm-email-activity.js +143 -0
  86. package/src/lib/server/crm-email-activity.js.map +1 -0
  87. package/src/lib/server/crm-inbound-email.d.ts +145 -0
  88. package/src/lib/server/crm-inbound-email.js +287 -0
  89. package/src/lib/server/crm-inbound-email.js.map +1 -0
  90. package/src/lib/server/crm-next-activity.d.ts +74 -0
  91. package/src/lib/server/crm-next-activity.js +194 -0
  92. package/src/lib/server/crm-next-activity.js.map +1 -0
  93. package/src/lib/server/crm-records.d.ts +144 -0
  94. package/src/lib/server/crm-records.js +218 -0
  95. package/src/lib/server/crm-records.js.map +1 -0
  96. package/src/lib/server/csp-aggregate.d.ts +62 -0
  97. package/src/lib/server/csp-aggregate.js +276 -0
  98. package/src/lib/server/csp-aggregate.js.map +1 -0
  99. package/src/lib/server/data-storage-gate.d.ts +66 -0
  100. package/src/lib/server/data-storage-gate.js +48 -0
  101. package/src/lib/server/data-storage-gate.js.map +1 -0
  102. package/src/lib/server/dataset-live-pages.d.ts +179 -0
  103. package/src/lib/server/dataset-live-pages.js +421 -0
  104. package/src/lib/server/dataset-live-pages.js.map +1 -0
  105. package/src/lib/server/dns-probe.d.ts +53 -0
  106. package/src/lib/server/dns-probe.js +184 -0
  107. package/src/lib/server/dns-probe.js.map +1 -0
  108. package/src/lib/server/document-id.d.ts +29 -0
  109. package/src/lib/server/document-id.js +100 -0
  110. package/src/lib/server/document-id.js.map +1 -0
  111. package/src/lib/server/domain-provider-vercel.d.ts +33 -0
  112. package/src/lib/server/domain-provider-vercel.js +298 -0
  113. package/src/lib/server/domain-provider-vercel.js.map +1 -0
  114. package/src/lib/server/domain-provider-webhook.d.ts +73 -0
  115. package/src/lib/server/domain-provider-webhook.js +223 -0
  116. package/src/lib/server/domain-provider-webhook.js.map +1 -0
  117. package/src/lib/server/domain-provider-wildcard.d.ts +54 -0
  118. package/src/lib/server/domain-provider-wildcard.js +163 -0
  119. package/src/lib/server/domain-provider-wildcard.js.map +1 -0
  120. package/src/lib/server/domain-provider.d.ts +197 -0
  121. package/src/lib/server/domain-provider.js +221 -0
  122. package/src/lib/server/domain-provider.js.map +1 -0
  123. package/src/lib/server/duplicate-activity.d.ts +54 -0
  124. package/src/lib/server/duplicate-activity.js +67 -0
  125. package/src/lib/server/duplicate-activity.js.map +1 -0
  126. package/src/lib/server/duplicate-resource.d.ts +55 -0
  127. package/src/lib/server/duplicate-resource.js +474 -0
  128. package/src/lib/server/duplicate-resource.js.map +1 -0
  129. package/src/lib/server/dynamic-list-materialize.d.ts +153 -0
  130. package/src/lib/server/dynamic-list-materialize.js +581 -0
  131. package/src/lib/server/dynamic-list-materialize.js.map +1 -0
  132. package/src/lib/server/edit-access-authz.d.ts +56 -0
  133. package/src/lib/server/edit-access-authz.js +107 -0
  134. package/src/lib/server/edit-access-authz.js.map +1 -0
  135. package/src/lib/server/edit-access-token.d.ts +72 -0
  136. package/src/lib/server/edit-access-token.js +116 -0
  137. package/src/lib/server/edit-access-token.js.map +1 -0
  138. package/src/lib/server/edit-hint-token.d.ts +83 -0
  139. package/src/lib/server/edit-hint-token.js +119 -0
  140. package/src/lib/server/edit-hint-token.js.map +1 -0
  141. package/src/lib/server/email-delivery-log.d.ts +509 -0
  142. package/src/lib/server/email-delivery-log.js +1043 -0
  143. package/src/lib/server/email-delivery-log.js.map +1 -0
  144. package/src/lib/server/email-flow-gate.d.ts +80 -0
  145. package/src/lib/server/email-flow-gate.js +153 -0
  146. package/src/lib/server/email-flow-gate.js.map +1 -0
  147. package/src/lib/server/email-marketing-gate.d.ts +333 -0
  148. package/src/lib/server/email-marketing-gate.js +639 -0
  149. package/src/lib/server/email-marketing-gate.js.map +1 -0
  150. package/src/lib/server/email-metering.d.ts +293 -0
  151. package/src/lib/server/email-metering.js +359 -0
  152. package/src/lib/server/email-metering.js.map +1 -0
  153. package/src/lib/server/email-revenue-attribution.d.ts +185 -0
  154. package/src/lib/server/email-revenue-attribution.js +300 -0
  155. package/src/lib/server/email-revenue-attribution.js.map +1 -0
  156. package/src/lib/server/email-send-rate.d.ts +232 -0
  157. package/src/lib/server/email-send-rate.js +417 -0
  158. package/src/lib/server/email-send-rate.js.map +1 -0
  159. package/src/lib/server/email-sender-reputation.d.ts +317 -0
  160. package/src/lib/server/email-sender-reputation.js +464 -0
  161. package/src/lib/server/email-sender-reputation.js.map +1 -0
  162. package/src/lib/server/email-suppression.d.ts +490 -0
  163. package/src/lib/server/email-suppression.js +648 -0
  164. package/src/lib/server/email-suppression.js.map +1 -0
  165. package/src/lib/server/email-topic-confirmation.d.ts +150 -0
  166. package/src/lib/server/email-topic-confirmation.js +272 -0
  167. package/src/lib/server/email-topic-confirmation.js.map +1 -0
  168. package/src/lib/server/email-unsubscribe-link.d.ts +174 -0
  169. package/src/lib/server/email-unsubscribe-link.js +236 -0
  170. package/src/lib/server/email-unsubscribe-link.js.map +1 -0
  171. package/src/lib/server/erase-person.d.ts +116 -0
  172. package/src/lib/server/erase-person.js +272 -0
  173. package/src/lib/server/erase-person.js.map +1 -0
  174. package/src/lib/server/erase.d.ts +458 -0
  175. package/src/lib/server/erase.js +1506 -0
  176. package/src/lib/server/erase.js.map +1 -0
  177. package/src/lib/server/firebase-admin.d.ts +71 -0
  178. package/src/lib/server/firebase-admin.js +343 -0
  179. package/src/lib/server/firebase-admin.js.map +1 -0
  180. package/src/lib/server/form-dataset-binding-token.d.ts +35 -0
  181. package/src/lib/server/form-dataset-binding-token.js +107 -0
  182. package/src/lib/server/form-dataset-binding-token.js.map +1 -0
  183. package/src/lib/server/free-workspace-cap.d.ts +201 -0
  184. package/src/lib/server/free-workspace-cap.js +404 -0
  185. package/src/lib/server/free-workspace-cap.js.map +1 -0
  186. package/src/lib/server/ga4-measurement-protocol.d.ts +256 -0
  187. package/src/lib/server/ga4-measurement-protocol.js +558 -0
  188. package/src/lib/server/ga4-measurement-protocol.js.map +1 -0
  189. package/src/lib/server/host-email-tokens.d.ts +47 -0
  190. package/src/lib/server/host-email-tokens.js +72 -0
  191. package/src/lib/server/host-email-tokens.js.map +1 -0
  192. package/src/lib/server/host-memberships.d.ts +54 -0
  193. package/src/lib/server/host-memberships.js +194 -0
  194. package/src/lib/server/host-memberships.js.map +1 -0
  195. package/src/lib/server/host-sending-domain.d.ts +292 -0
  196. package/src/lib/server/host-sending-domain.js +451 -0
  197. package/src/lib/server/host-sending-domain.js.map +1 -0
  198. package/src/lib/server/host-visitor-records.d.ts +150 -0
  199. package/src/lib/server/host-visitor-records.js +262 -0
  200. package/src/lib/server/host-visitor-records.js.map +1 -0
  201. package/src/lib/server/id-token-refusal.d.ts +28 -0
  202. package/src/lib/server/id-token-refusal.js +111 -0
  203. package/src/lib/server/id-token-refusal.js.map +1 -0
  204. package/src/lib/server/legal-acceptance.d.ts +214 -0
  205. package/src/lib/server/legal-acceptance.js +305 -0
  206. package/src/lib/server/legal-acceptance.js.map +1 -0
  207. package/src/lib/server/list-members.d.ts +211 -0
  208. package/src/lib/server/list-members.js +176 -0
  209. package/src/lib/server/list-members.js.map +1 -0
  210. package/src/lib/server/live-page-usage.d.ts +202 -0
  211. package/src/lib/server/live-page-usage.js +300 -0
  212. package/src/lib/server/live-page-usage.js.map +1 -0
  213. package/src/lib/server/lockdown.d.ts +313 -0
  214. package/src/lib/server/lockdown.js +643 -0
  215. package/src/lib/server/lockdown.js.map +1 -0
  216. package/src/lib/server/media-cdn-rate-limit.d.ts +188 -0
  217. package/src/lib/server/media-cdn-rate-limit.js +204 -0
  218. package/src/lib/server/media-cdn-rate-limit.js.map +1 -0
  219. package/src/lib/server/media-delivery.d.ts +267 -0
  220. package/src/lib/server/media-delivery.js +485 -0
  221. package/src/lib/server/media-delivery.js.map +1 -0
  222. package/src/lib/server/media-download-tokens.d.ts +161 -0
  223. package/src/lib/server/media-download-tokens.js +386 -0
  224. package/src/lib/server/media-download-tokens.js.map +1 -0
  225. package/src/lib/server/media-quarantine.d.ts +127 -0
  226. package/src/lib/server/media-quarantine.js +170 -0
  227. package/src/lib/server/media-quarantine.js.map +1 -0
  228. package/src/lib/server/media-signing.d.ts +126 -0
  229. package/src/lib/server/media-signing.js +159 -0
  230. package/src/lib/server/media-signing.js.map +1 -0
  231. package/src/lib/server/media-storage-path.d.ts +96 -0
  232. package/src/lib/server/media-storage-path.js +111 -0
  233. package/src/lib/server/media-storage-path.js.map +1 -0
  234. package/src/lib/server/media-strong-digest.d.ts +85 -0
  235. package/src/lib/server/media-strong-digest.js +190 -0
  236. package/src/lib/server/media-strong-digest.js.map +1 -0
  237. package/src/lib/server/media-tombstone.d.ts +276 -0
  238. package/src/lib/server/media-tombstone.js +421 -0
  239. package/src/lib/server/media-tombstone.js.map +1 -0
  240. package/src/lib/server/media-variants.d.ts +309 -0
  241. package/src/lib/server/media-variants.js +428 -0
  242. package/src/lib/server/media-variants.js.map +1 -0
  243. package/src/lib/server/member-email-aliases.d.ts +180 -0
  244. package/src/lib/server/member-email-aliases.js +420 -0
  245. package/src/lib/server/member-email-aliases.js.map +1 -0
  246. package/src/lib/server/member-photo.d.ts +88 -0
  247. package/src/lib/server/member-photo.js +185 -0
  248. package/src/lib/server/member-photo.js.map +1 -0
  249. package/src/lib/server/membership-recover-throttle.d.ts +211 -0
  250. package/src/lib/server/membership-recover-throttle.js +231 -0
  251. package/src/lib/server/membership-recover-throttle.js.map +1 -0
  252. package/src/lib/server/notifications.d.ts +55 -0
  253. package/src/lib/server/notifications.js +134 -0
  254. package/src/lib/server/notifications.js.map +1 -0
  255. package/src/lib/server/org-billing.d.ts +97 -0
  256. package/src/lib/server/org-billing.js +236 -0
  257. package/src/lib/server/org-billing.js.map +1 -0
  258. package/src/lib/server/organizations.d.ts +837 -0
  259. package/src/lib/server/organizations.js +1814 -0
  260. package/src/lib/server/organizations.js.map +1 -0
  261. package/src/lib/server/password-reset-throttle.d.ts +84 -0
  262. package/src/lib/server/password-reset-throttle.js +102 -0
  263. package/src/lib/server/password-reset-throttle.js.map +1 -0
  264. package/src/lib/server/personal-data-export.d.ts +241 -0
  265. package/src/lib/server/personal-data-export.js +858 -0
  266. package/src/lib/server/personal-data-export.js.map +1 -0
  267. package/src/lib/server/platform-domain-names.d.ts +62 -0
  268. package/src/lib/server/platform-domain-names.js +197 -0
  269. package/src/lib/server/platform-domain-names.js.map +1 -0
  270. package/src/lib/server/platform-marketing-consent.d.ts +133 -0
  271. package/src/lib/server/platform-marketing-consent.js +209 -0
  272. package/src/lib/server/platform-marketing-consent.js.map +1 -0
  273. package/src/lib/server/rate-limit-store.d.ts +702 -0
  274. package/src/lib/server/rate-limit-store.js +1220 -0
  275. package/src/lib/server/rate-limit-store.js.map +1 -0
  276. package/src/lib/server/realm-plugins.d.ts +67 -0
  277. package/src/lib/server/realm-plugins.js +250 -0
  278. package/src/lib/server/realm-plugins.js.map +1 -0
  279. package/src/lib/server/recaptcha-allowlist.d.ts +90 -0
  280. package/src/lib/server/recaptcha-allowlist.js +409 -0
  281. package/src/lib/server/recaptcha-allowlist.js.map +1 -0
  282. package/src/lib/server/release-flags.d.ts +105 -0
  283. package/src/lib/server/release-flags.js +183 -0
  284. package/src/lib/server/release-flags.js.map +1 -0
  285. package/src/lib/server/replay-erasures.d.ts +126 -0
  286. package/src/lib/server/replay-erasures.js +217 -0
  287. package/src/lib/server/replay-erasures.js.map +1 -0
  288. package/src/lib/server/resolve-people.d.ts +66 -0
  289. package/src/lib/server/resolve-people.js +112 -0
  290. package/src/lib/server/resolve-people.js.map +1 -0
  291. package/src/lib/server/safe-equal.d.ts +45 -0
  292. package/src/lib/server/safe-equal.js +58 -0
  293. package/src/lib/server/safe-equal.js.map +1 -0
  294. package/src/lib/server/sending-domain-debt.d.ts +127 -0
  295. package/src/lib/server/sending-domain-debt.js +268 -0
  296. package/src/lib/server/sending-domain-debt.js.map +1 -0
  297. package/src/lib/server/sending-domain-recheck.d.ts +151 -0
  298. package/src/lib/server/sending-domain-recheck.js +253 -0
  299. package/src/lib/server/sending-domain-recheck.js.map +1 -0
  300. package/src/lib/server/sending-domains.d.ts +364 -0
  301. package/src/lib/server/sending-domains.js +683 -0
  302. package/src/lib/server/sending-domains.js.map +1 -0
  303. package/src/lib/server/serve-media-cdn.d.ts +433 -0
  304. package/src/lib/server/serve-media-cdn.js +1370 -0
  305. package/src/lib/server/serve-media-cdn.js.map +1 -0
  306. package/src/lib/server/serve-plugin-fetch.d.ts +61 -0
  307. package/src/lib/server/serve-plugin-fetch.js +294 -0
  308. package/src/lib/server/serve-plugin-fetch.js.map +1 -0
  309. package/src/lib/server/sms-keywords.d.ts +63 -0
  310. package/src/lib/server/sms-keywords.js +146 -0
  311. package/src/lib/server/sms-keywords.js.map +1 -0
  312. package/src/lib/server/sso-break-glass-owners.d.ts +91 -0
  313. package/src/lib/server/sso-break-glass-owners.js +227 -0
  314. package/src/lib/server/sso-break-glass-owners.js.map +1 -0
  315. package/src/lib/server/sso-domain-policy.d.ts +140 -0
  316. package/src/lib/server/sso-domain-policy.js +239 -0
  317. package/src/lib/server/sso-domain-policy.js.map +1 -0
  318. package/src/lib/server/sso-drift-logic.d.ts +106 -0
  319. package/src/lib/server/sso-drift-logic.js +95 -0
  320. package/src/lib/server/sso-drift-logic.js.map +1 -0
  321. package/src/lib/server/sso-enforcement.d.ts +204 -0
  322. package/src/lib/server/sso-enforcement.js +309 -0
  323. package/src/lib/server/sso-enforcement.js.map +1 -0
  324. package/src/lib/server/sso-provisioning.d.ts +320 -0
  325. package/src/lib/server/sso-provisioning.js +715 -0
  326. package/src/lib/server/sso-provisioning.js.map +1 -0
  327. package/src/lib/server/staff-alert-email.d.ts +41 -0
  328. package/src/lib/server/staff-alert-email.js +63 -0
  329. package/src/lib/server/staff-alert-email.js.map +1 -0
  330. package/src/lib/server/stripe-account-mode.d.ts +156 -0
  331. package/src/lib/server/stripe-account-mode.js +162 -0
  332. package/src/lib/server/stripe-account-mode.js.map +1 -0
  333. package/src/lib/server/tenant-write-lockdown.d.ts +131 -0
  334. package/src/lib/server/tenant-write-lockdown.js +176 -0
  335. package/src/lib/server/tenant-write-lockdown.js.map +1 -0
  336. package/src/lib/server/test-firestore.d.ts +45 -0
  337. package/src/lib/server/test-firestore.js +124 -0
  338. package/src/lib/server/test-firestore.js.map +1 -0
  339. package/src/lib/server/token-revocation.d.ts +140 -0
  340. package/src/lib/server/token-revocation.js +255 -0
  341. package/src/lib/server/token-revocation.js.map +1 -0
  342. package/src/lib/server/update-existing.d.ts +32 -0
  343. package/src/lib/server/update-existing.js +73 -0
  344. package/src/lib/server/update-existing.js.map +1 -0
  345. package/src/lib/server/upload-cors-reconcile.d.ts +118 -0
  346. package/src/lib/server/upload-cors-reconcile.js +234 -0
  347. package/src/lib/server/upload-cors-reconcile.js.map +1 -0
  348. package/src/lib/server/upsert-contact.d.ts +262 -0
  349. package/src/lib/server/upsert-contact.js +546 -0
  350. package/src/lib/server/upsert-contact.js.map +1 -0
  351. package/src/lib/server/usage-invoice.d.ts +203 -0
  352. package/src/lib/server/usage-invoice.js +397 -0
  353. package/src/lib/server/usage-invoice.js.map +1 -0
  354. package/src/lib/server/user-profiles.d.ts +151 -0
  355. package/src/lib/server/user-profiles.js +364 -0
  356. package/src/lib/server/user-profiles.js.map +1 -0
  357. package/src/lib/server/vercel-drain-signature.d.ts +30 -0
  358. package/src/lib/server/vercel-drain-signature.js +61 -0
  359. package/src/lib/server/vercel-drain-signature.js.map +1 -0
  360. package/src/lib/server/vercel-log-drain.d.ts +259 -0
  361. package/src/lib/server/vercel-log-drain.js +479 -0
  362. package/src/lib/server/vercel-log-drain.js.map +1 -0
  363. package/src/lib/server/verify-email-cooldown.d.ts +102 -0
  364. package/src/lib/server/verify-email-cooldown.js +114 -0
  365. package/src/lib/server/verify-email-cooldown.js.map +1 -0
  366. package/src/lib/server/visitor-write-rate-limit.d.ts +87 -0
  367. package/src/lib/server/visitor-write-rate-limit.js +117 -0
  368. package/src/lib/server/visitor-write-rate-limit.js.map +1 -0
  369. package/src/lib/server/workspace-domains.d.ts +156 -0
  370. package/src/lib/server/workspace-domains.js +271 -0
  371. package/src/lib/server/workspace-domains.js.map +1 -0
  372. package/src/tenancy.d.ts +115 -0
  373. package/src/tenancy.js +142 -0
  374. package/src/tenancy.js.map +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/sso-break-glass-owners.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 * Break-glass by way of an org owner who lives OUTSIDE the SSO pool\n * (AGL-1888, option (a) — decided).\n *\n * ## Why this exists\n *\n * `assessSsoLockoutRisk` only ever looked inside the org's GCIP tenant, and\n * nothing inside that tenant can hold a credential the IdP does not mediate:\n * `provisionSsoPool` creates the pool with `emailSignInConfig.enabled: false`,\n * `/api/orgs/members/password` refuses outright on `tenantId`, and a social\n * login cannot be linked to a governed account at all. So the pre-flight was\n * unsatisfiable and enforcement was unreachable for every org.\n *\n * The way out is the account the pre-flight was always described as wanting:\n * *\"at least one org owner holding a project-level credential\"*. A\n * project-pool account is not in the tenant, so the enforcement sweep cannot\n * see it, let alone strip it — the same invisibility that made SSO users\n * disappear from the staff console (AGL-1122) is what makes this work.\n *\n * ## The property being checked\n *\n * **Could this person still sign in if the org's IdP stopped answering?** A\n * lapsed certificate, a deleted SAML application, a revoked provider config.\n * Every condition below exists because failing it means the answer is no —\n * or means we cannot establish that the answer is yes, which for a control\n * whose failure mode is \"a paying customer loses its console permanently\" is\n * the same thing.\n *\n * 1. **Role `owner`, not `admin`.** The narrow reading of the decision, and\n * the strict one. An org whose only owner is an SSO account can promote a\n * project-level member to owner — a self-serve action — so requiring the\n * stronger role does not put anybody back in front of a support queue.\n * 2. **Present in the PROJECT pool**, resolved there directly rather than\n * through {@link findUserByUidAcrossPools}: the question is not \"where\n * does this uid live\", it is \"does a project-pool record exist\", and a\n * cross-pool resolver answering with the tenant record would silently\n * turn a pooled account into a qualifying one.\n * 3. **Not the emailless twin.** A cross-pool `signInWithCustomToken`\n * manufactures a project-pool record with no address and no providers\n * (AGL-1962/2005) — a record nobody can sign in as, and exactly the shape\n * a bare existence check accepts. There is no separate test for it here:\n * conditions (5) and (6) are each strictly stronger than\n * {@link isIdentifiedUserRecord}, so calling that as well would be a\n * guard no mutation can make fail — a control that reads as a check and\n * is not one. The twin is rejected, and the spec names which condition\n * does it.\n * 4. **NOT also in the org's tenant pool.** A uid in both pools is that same\n * collision seen from the other side, and it makes the identity ambiguous\n * at the moment it matters most. Refused rather than guessed.\n * 5. **Not disabled**, and **email verified**. The console refuses an\n * unverified session before it reaches any org setting\n * (`emailUnverifiedResponse`), so an unverified owner cannot turn\n * enforcement back off — which is the entire job of a break-glass\n * account. A credential that cannot be used is not a credential.\n * 6. **Holds a provider that is not the org's IdP.** Same rule the in-pool\n * assessment uses, applied to the project-pool record.\n * 7. **Not on a domain the operator REQUIRES SSO for**\n * ({@link evaluateSsoDomainPolicy}). This is the subtle one. A\n * project-pool `@aglyn.com` password is a perfectly good credential right\n * up until `AGLYN_SSO_DOMAIN_ENFORCEMENT` is switched on, at which point\n * that identity is refused at sign-in and the break-glass account\n * silently stops being one. `refused` is computed independently of the\n * switch by design, so this reads the rule rather than today's setting.\n *\n * Note what is deliberately NOT required: that the credential be a password.\n * A project-pool `google.com` link survives a lapsed SAML certificate just as\n * a password does — the org's IdP does not mediate it. The residual is that a\n * customer whose IdP and mailbox are the same vendor (Workspace SAML plus\n * Workspace Google sign-in) has a correlated failure if the whole vendor\n * account is deleted rather than the SAML app; that is a narrower failure\n * than the one this control exists for, and the docs say so.\n *\n * ## Failing safe\n *\n * Every error path yields **no owners**, which makes the assessment unsafe\n * and enforcement refuse. `unavailable` is carried alongside so the refusal\n * can say \"we could not check\" instead of \"you have nobody\" — a swallowed\n * query that renders as a measured zero is worse than an error, because\n * nothing looks wrong.\n */\n\nimport { authForPool } from './auth-pools'\nimport { evaluateSsoDomainPolicy } from './sso-domain-policy'\nimport firebaseAdmin from './firebase-admin'\n\n/**\n * ## One predicate, two callers\n *\n * The seven conditions live in {@link qualifiesAsBreakGlassOwner}, asked of\n * ONE uid, because two places need the same answer about different people:\n *\n * - {@link findBreakGlassOrgOwners} asks it of every owner on the roster,\n * which is what the enforcement pre-flight consumes;\n * - the ownership-transfer guard asks it of the person about to BECOME the\n * only owner, before the transfer happens (AGL-1888).\n *\n * The second caller exists because an org has exactly one owner at a time —\n * `transferOrgOwnership` moves the role rather than adding a second holder —\n * so a transfer can move the whole of an org's break-glass protection onto an\n * account inside the pool, after the pre-flight has already passed, with the\n * sweep long since finished. The pre-flight is a gate at one moment; this is\n * the same question asked again at the only other moment the answer can\n * change. Written as one function rather than two so the transfer guard\n * cannot drift into a second, laxer definition of who counts.\n */\n\n/** An org owner who would still be able to sign in without the org's IdP. */\nexport interface SsoBreakGlassOwner {\n uid: string\n email: string | null\n /** Project-pool sign-in methods, none of them the org's IdP. */\n providers: string[]\n}\n\nexport interface SsoBreakGlassOwnerLookup {\n owners: SsoBreakGlassOwner[]\n /**\n * True when a lookup failed for a reason that is not \"no such user\" — an\n * Auth or Firestore outage. The owner list is then INCOMPLETE, not empty,\n * and the caller must not report \"this org has nobody\" on the strength of\n * it.\n */\n unavailable: boolean\n}\n\n/** Owners walked in one lookup. An org with more has other problems. */\nconst OWNER_SCAN_CAP = 25\n\n/** Firebase's \"this pool does not have that uid\" — a real negative. */\nconst NOT_FOUND = 'auth/user-not-found'\n\nconst codeOf = (error: unknown): string =>\n String((error as { code?: unknown })?.code ?? '')\n\n/** Whether ONE account would still be a way in without the org's IdP. */\nexport interface SsoBreakGlassVerdict {\n /** The qualifying owner, or null when this account does not count. */\n owner: SsoBreakGlassOwner | null\n /**\n * A lookup failed for a reason that is not \"no such user\". `owner` is then\n * null because we could not establish that it should be anything else —\n * never because we established the account does not qualify. A caller must\n * refuse on this, not treat it as a clean negative.\n */\n unavailable: boolean\n}\n\nconst DOES_NOT_QUALIFY: SsoBreakGlassVerdict = {\n owner: null,\n unavailable: false,\n}\nconst COULD_NOT_CHECK: SsoBreakGlassVerdict = { owner: null, unavailable: true }\n\n/**\n * The seven conditions, asked of one uid.\n *\n * Callers must distinguish the two answers that both carry a null owner. A\n * null owner with `unavailable` false means \"we checked, and no\"; a null owner\n * with `unavailable` true means \"we could not check\". Collapsing them is how a\n * swallowed query starts rendering as a measured zero.\n *\n * @param uid - the account being judged. NOT checked for org membership here;\n * both callers establish that first, by different routes (a roster query,\n * and `transferOrgOwnership`'s own \"must already be a member\" transaction).\n * @param providerId - the org's SAML provider id, so a project-pool record\n * holding only that provider cannot qualify\n * @param tenantId - the org's GCIP pool, checked for the collision in (4)\n */\nexport async function qualifiesAsBreakGlassOwner(\n uid: string,\n providerId: string,\n tenantId: string,\n): Promise<SsoBreakGlassVerdict> {\n let record\n try {\n // Written inline rather than through a hoisted `projectPool` const so\n // the AGL-1122 guard can SEE that the receiver is pool-scoped: it\n // matches on the receiver text, and a variable named `projectPool` is\n // indistinguishable to it from a bare `auth().getUser`. A guard that\n // cannot read a legitimate call is one somebody widens, so the call\n // site moves instead of the guard.\n record = await authForPool(null).getUser(uid)\n } catch (error) {\n // Not in the project pool is the ORDINARY answer here — it means this\n // owner signs in through the IdP like everyone else. Anything else is\n // an outage, and an outage must not read as a clean negative.\n if (codeOf(error) !== NOT_FOUND) {\n console.error(`[sso] project-pool lookup failed for ${uid}`, error)\n return COULD_NOT_CHECK\n }\n return DOES_NOT_QUALIFY\n }\n // (5) Both halves. A disabled account is not a way in, and an unverified\n // address cannot reach an org setting to undo enforcement.\n //\n // These two also subsume (3): the emailless cross-pool artifact has no\n // address, so it never gets past the line below. That is why there is no\n // `isIdentifiedUserRecord` call here — it could never be the condition\n // that rejects anything, and a guard no mutation can break is not a\n // guard.\n if (record.disabled) return DOES_NOT_QUALIFY\n if (!record.email || !record.emailVerified) return DOES_NOT_QUALIFY\n // (6) The same rule the in-pool assessment applies. Also the second half\n // of (3): a record with no providers at all cannot qualify.\n const providers = (record.providerData ?? [])\n .map((info) => info?.providerId)\n .filter((id): id is string => Boolean(id) && id !== providerId)\n if (!providers.length) return DOES_NOT_QUALIFY\n // (7) A domain the operator requires SSO for. Read from the RULE, not\n // from whether the switch happens to be on today.\n if (evaluateSsoDomainPolicy({ email: record.email, tenantId: null }).refused) {\n return DOES_NOT_QUALIFY\n }\n // (4) Last, because it costs a call: the same uid in the org's own pool.\n try {\n await authForPool(tenantId).getUser(uid)\n // It answered — this uid names an account in the pool as well, so the\n // sweep will touch it and \"which record is this owner\" has no answer.\n return DOES_NOT_QUALIFY\n } catch (error) {\n if (codeOf(error) !== NOT_FOUND) {\n console.error(`[sso] tenant-pool lookup failed for ${uid}`, error)\n return COULD_NOT_CHECK\n }\n }\n return {\n owner: { uid, email: record.email ?? null, providers },\n unavailable: false,\n }\n}\n\n/**\n * Org owners who hold a credential the org's IdP does not mediate.\n *\n * @param orgId - the organization being enforced\n * @param providerId - the org's SAML provider id, so a project-pool record\n * holding only that provider cannot qualify\n * @param tenantId - the org's GCIP pool, checked for the collision in (4)\n */\nexport async function findBreakGlassOrgOwners(\n orgId: string,\n providerId: string,\n tenantId: string,\n): Promise<SsoBreakGlassOwnerLookup> {\n let ownerUids: string[]\n try {\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(orgId)\n .collection('members')\n .where('role', '==', 'owner')\n .get()\n ownerUids = snapshot.docs.slice(0, OWNER_SCAN_CAP).map((doc) => doc.id)\n } catch (error) {\n console.error(`[sso] owner roster read failed for ${orgId}`, error)\n return { owners: [], unavailable: true }\n }\n\n const owners: SsoBreakGlassOwner[] = []\n let unavailable = false\n\n for (const uid of ownerUids) {\n const verdict = await qualifiesAsBreakGlassOwner(uid, providerId, tenantId)\n // Accumulated rather than returned early: one unreachable owner must not\n // hide the qualifying one standing next to them, and it must not be\n // forgotten either. An org is safe if ANY owner qualifies, and the caller\n // still gets told the picture is incomplete.\n if (verdict.unavailable) unavailable = true\n if (verdict.owner) owners.push(verdict.owner)\n }\n\n return { owners, unavailable }\n}\n\nexport default findBreakGlassOrgOwners\n"],"names":["authForPool","evaluateSsoDomainPolicy","firebaseAdmin","OWNER_SCAN_CAP","NOT_FOUND","codeOf","error","String","code","DOES_NOT_QUALIFY","owner","unavailable","COULD_NOT_CHECK","qualifiesAsBreakGlassOwner","uid","providerId","tenantId","record","getUser","console","disabled","email","emailVerified","providers","providerData","map","info","filter","id","Boolean","length","refused","findBreakGlassOrgOwners","orgId","ownerUids","snapshot","app","firestore","collection","doc","where","get","docs","slice","owners","verdict","push"],"mappings":"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"}
@@ -0,0 +1,140 @@
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
+ export declare const SSO_REQUIRED_DOMAINS_ENV = "AGLYN_SSO_REQUIRED_DOMAINS";
18
+ export type SsoRequiredDomains = Readonly<Record<string, string>>;
19
+ /**
20
+ * Parse the configured domain→tenant map. Total: anything unparseable is
21
+ * dropped rather than throwing, because this sits on the authentication path
22
+ * and a malformed env var must not take sign-in down. A dropped entry fails
23
+ * OPEN (the domain is simply ungoverned), which is the safe direction for a
24
+ * rule whose only power is to refuse.
25
+ */
26
+ export declare function parseSsoRequiredDomains(raw: string | null | undefined): SsoRequiredDomains;
27
+ /**
28
+ * The operator's SSO-required domains. Empty unless configured — see above.
29
+ */
30
+ export declare function ssoRequiredDomains(env?: Record<string, string | undefined>): SsoRequiredDomains;
31
+ export type SsoDomainVerdict =
32
+ /** Domain is not governed. Any pool is fine — this is most of the world. */
33
+ 'allow-ungoverned'
34
+ /** Governed domain, signed in through the tenant we designated for it. */
35
+ | 'allow-designated-tenant'
36
+ /**
37
+ * Governed domain, signed in through SOME tenant, but not the designated
38
+ * one. Allowed — they did use SSO, and refusing would invent a restriction
39
+ * nobody asked for — but surfaced so a staff-access review can see it.
40
+ */
41
+ | 'allow-foreign-tenant'
42
+ /**
43
+ * Governed domain with NO tenant at all: a project-pool password or consumer
44
+ * Google sign-in. The only refusal this policy ever produces.
45
+ */
46
+ | 'refuse-sso-required';
47
+ export interface SsoDomainDecision {
48
+ verdict: SsoDomainVerdict;
49
+ /**
50
+ * Whether enforcement WOULD refuse this identity. Independent of whether
51
+ * enforcement is currently switched on — {@link ssoDomainEnforcementEnabled}
52
+ * decides that — so the same evaluation can drive both an audit in
53
+ * report-only mode and the refusal once the switch is flipped.
54
+ */
55
+ refused: boolean;
56
+ /** Lower-cased domain, or null when the identity has no email address. */
57
+ domain: string | null;
58
+ /** Tenant this domain is required to use, or null when ungoverned. */
59
+ requiredTenantId: string | null;
60
+ /** Tenant the identity actually signed in through; null = project pool. */
61
+ tenantId: string | null;
62
+ /** Worth a human look even though it is allowed (foreign-tenant case). */
63
+ reviewable: boolean;
64
+ }
65
+ export interface SsoDomainIdentity {
66
+ /** The identity's email address. Absent/empty is treated as ungoverned. */
67
+ email?: string | null;
68
+ /**
69
+ * GCIP tenant the identity signed in through — `decoded.firebase?.tenant`,
70
+ * or a `PooledUserRecord.tenantId`. Null/undefined means the project pool.
71
+ *
72
+ * Never inferred and never defaulted to a hard-coded tenant: a uid is only
73
+ * unique WITHIN a pool, and assuming one is how a lookup silently lands on
74
+ * the wrong record.
75
+ */
76
+ tenantId?: string | null;
77
+ }
78
+ /** The domain part of an address, lower-cased. Null when there isn't one. */
79
+ export declare function domainOf(email: string | null | undefined): string | null;
80
+ /**
81
+ * Decide the policy for one identity. Pure and total — no Firebase, no env —
82
+ * so a spec can drive every combination directly, and so the audit view and
83
+ * the enforcement path can never disagree about what the policy says.
84
+ */
85
+ export declare function evaluateSsoDomainPolicy(identity: SsoDomainIdentity, domains?: SsoRequiredDomains): SsoDomainDecision;
86
+ /**
87
+ * THE ENFORCEMENT SWITCH — ships OFF.
88
+ *
89
+ * Off means `evaluateSsoDomainPolicy` still computes `refused`, and callers
90
+ * still record it, but nobody is turned away. That is deliberate: the audit
91
+ * trail accumulates under the real rule before the rule bites, so flipping the
92
+ * switch holds no surprises.
93
+ *
94
+ * Opt-in by exact string. Anything else — unset, empty, `false`, `0`, a typo —
95
+ * is OFF. A security control that switches itself on through a misspelling is
96
+ * a control that can lock people out through a misspelling.
97
+ *
98
+ * Note both halves must be set for anything to happen: this switch AND a
99
+ * non-empty {@link SSO_REQUIRED_DOMAINS_ENV}. A self-host install that sets
100
+ * only this switch still governs nothing.
101
+ *
102
+ * **Precondition for flipping it — written down so it is not re-litigated:**
103
+ * 1. SSO staff access verified working through a REAL sign-in (not a token
104
+ * inspection): a staff account reaches `/admin/*` via the SAML tenant.
105
+ * 2. Every identity on a governed domain has been migrated into its tenant or
106
+ * retired. Measured on aglyn-main 2026-08-18: TWO would have been refused,
107
+ * neither of them staff — one on a consumer `google.com` provider, and the
108
+ * e2e smoke account on `password`. Flipping before migrating breaks the
109
+ * e2e smoke sign-in, which is the one that bites without warning: it is an
110
+ * AUTOMATION account, so nobody is watching a sign-in page when it fails.
111
+ *
112
+ * A second staff identity is NOT a precondition: the permanent break-glass
113
+ * account is on an ungoverned domain, so this rule can never refuse it.
114
+ */
115
+ export declare const SSO_DOMAIN_ENFORCEMENT_ENV = "AGLYN_SSO_DOMAIN_ENFORCEMENT";
116
+ export declare function ssoDomainEnforcementEnabled(env?: Record<string, string | undefined>): boolean;
117
+ /**
118
+ * What the refused person is told. Names the fix, not the mechanism.
119
+ *
120
+ * `PLATFORM_BRAND_NAME`, not a literal (AGL-2214). Everything else in this
121
+ * module is careful that the RULE is configuration rather than a compiled-in
122
+ * assumption about Aglyn — and then the one string a refused person actually
123
+ * reads named us anyway. A self-hoster who governs their own domain would
124
+ * have turned their own staff away in our name.
125
+ *
126
+ * Byte-identical for a deployment that has not renamed itself.
127
+ */
128
+ export declare const SSO_DOMAIN_REFUSAL_MESSAGE: string;
129
+ /**
130
+ * The refusal a caller should return, or null to carry on.
131
+ *
132
+ * Returns null whenever the switch is off, so wiring this into an
133
+ * authentication path is inert until the account owner flips it — the call site is live,
134
+ * the consequence is not.
135
+ */
136
+ export declare function ssoDomainRefusal(identity: SsoDomainIdentity, env?: Record<string, string | undefined>): {
137
+ decision: SsoDomainDecision;
138
+ response: Response;
139
+ } | null;
140
+ export default evaluateSsoDomainPolicy;
@@ -0,0 +1,239 @@
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
+ * Workspace-domain SSO policy (AGL-1993).
18
+ *
19
+ * **The rule is about a DOMAIN, not about staff.** An `@aglyn.com` address is a
20
+ * Google Workspace identity, and Workspace is what enforces MFA, disables an
21
+ * account at offboarding, and lets an admin revoke a session. An `@aglyn.com`
22
+ * address that signs in through a project-pool password or a consumer Google
23
+ * account has none of that: it looks like a company identity and is governed
24
+ * like a personal one. That gap is the whole reason this module exists.
25
+ *
26
+ * ## Staff is a SEPARATE axis and this module never reads it
27
+ *
28
+ * Staff may be granted to anybody — SSO or not, `@aglyn.com` or not, project
29
+ * pool or any GCIP tenant including a *customer's*. Nothing here consults the
30
+ * `staff` claim, and `sso-domain-policy.spec.ts` pins that by evaluating every
31
+ * staff permutation and asserting the verdict does not move. The two axes are
32
+ * independent; conflating them would turn a narrow domain control into an
33
+ * accidental SSO mandate for staff, which is explicitly not the policy.
34
+ *
35
+ * These all remain valid, and each has a test:
36
+ *
37
+ * | identity | staff | verdict |
38
+ * | ------------------------------------------ | ----- | ------------------------- |
39
+ * | `@aglyn.com` in the designated tenant | yes | allowed |
40
+ * | personal address, project pool (break-glass)| yes | allowed — permanent design|
41
+ * | personal address, project pool | no | allowed |
42
+ * | SSO identity in a CUSTOMER org's tenant | yes | allowed |
43
+ * | `@aglyn.com` in the PROJECT pool | any | **refused** when enforced |
44
+ *
45
+ * Only the last row is what the switch exists for.
46
+ *
47
+ * ## How this differs from `sso-enforcement.ts`
48
+ *
49
+ * {@link enforceSsoSignInMethods} (AGL-1128/1129) strips non-IdP providers from
50
+ * accounts that are ALREADY INSIDE a tenant. It cannot see an identity that
51
+ * never joined the tenant at all — a project-pool `@aglyn.com` account is
52
+ * invisible to every tenant sweep, by construction. Measured on aglyn-main on
53
+ * 2026-08-18: two such accounts existed, one of them holding a live
54
+ * `google.com` provider. This module covers that outside-the-tenant half.
55
+ *
56
+ * ## Shape note: deliberately NOT a discriminated union
57
+ *
58
+ * `strictNullChecks` is off repo-wide, which defeats boolean-discriminant
59
+ * narrowing — a spec can read the wrong arm of a union and pass silently. So
60
+ * the decision is ONE flat interface with a string `verdict`, and specs assert
61
+ * the literal. There is no arm to read by mistake.
62
+ */ /**
63
+ * ## Self-hosting: this policy is CONFIGURATION, never a compiled-in constant
64
+ *
65
+ * Aglyn ships in two shapes — the Aglyn-operated SaaS, and self-hosted installs
66
+ * on the operator's own Firebase project. `aglyn.com` is *our* Workspace
67
+ * domain; a self-hoster has their own domain, their own IdP, or none at all.
68
+ * Baking our domain in would make the rule dead weight for them at best and a
69
+ * lockout at worst, so the governed set is read from configuration and is
70
+ * **empty by default**.
71
+ *
72
+ * Empty means the rule does nothing. That is simultaneously the correct
73
+ * behaviour for a fresh self-host install and the correct OFF state for us —
74
+ * one default serving both, rather than a special case for either.
75
+ *
76
+ * Format — `domain=tenantId`, comma or whitespace separated:
77
+ *
78
+ * ```
79
+ * AGLYN_SSO_REQUIRED_DOMAINS="aglyn.com=aglyn-org-y5v14"
80
+ * ```
81
+ *
82
+ * A map rather than a bare list because "must use SSO" is meaningless without
83
+ * naming WHOSE SSO: the point is IdP lifecycle control, which only the tenant
84
+ * wired to that domain's SAML provider delivers.
85
+ */ import { PLATFORM_BRAND_NAME } from "@aglyn/aglyn/app-utils/platform-brand";
86
+ export const SSO_REQUIRED_DOMAINS_ENV = 'AGLYN_SSO_REQUIRED_DOMAINS';
87
+ /**
88
+ * Parse the configured domain→tenant map. Total: anything unparseable is
89
+ * dropped rather than throwing, because this sits on the authentication path
90
+ * and a malformed env var must not take sign-in down. A dropped entry fails
91
+ * OPEN (the domain is simply ungoverned), which is the safe direction for a
92
+ * rule whose only power is to refuse.
93
+ */ export function parseSsoRequiredDomains(raw) {
94
+ const map = {};
95
+ for (const pair of String(raw != null ? raw : '').split(/[,\s]+/)){
96
+ if (!pair) continue;
97
+ const eq = pair.indexOf('=');
98
+ if (eq <= 0) continue;
99
+ const domain = pair.slice(0, eq).trim().toLowerCase();
100
+ const tenantId = pair.slice(eq + 1).trim();
101
+ if (!domain || !tenantId) continue;
102
+ map[domain] = tenantId;
103
+ }
104
+ return map;
105
+ }
106
+ /**
107
+ * The operator's SSO-required domains. Empty unless configured — see above.
108
+ */ export function ssoRequiredDomains(env = process.env) {
109
+ return parseSsoRequiredDomains(env[SSO_REQUIRED_DOMAINS_ENV]);
110
+ }
111
+ /** The domain part of an address, lower-cased. Null when there isn't one. */ export function domainOf(email) {
112
+ const address = String(email != null ? email : '').trim().toLowerCase();
113
+ const at = address.lastIndexOf('@');
114
+ if (at < 0 || at === address.length - 1) return null;
115
+ return address.slice(at + 1);
116
+ }
117
+ /**
118
+ * Decide the policy for one identity. Pure and total — no Firebase, no env —
119
+ * so a spec can drive every combination directly, and so the audit view and
120
+ * the enforcement path can never disagree about what the policy says.
121
+ */ export function evaluateSsoDomainPolicy(identity, domains = ssoRequiredDomains()) {
122
+ var _identity_tenantId;
123
+ const domain = domainOf(identity.email);
124
+ const tenantId = (_identity_tenantId = identity.tenantId) != null ? _identity_tenantId : null;
125
+ // Exact-match lookup, never a suffix test: `notaglyn.com` ends with
126
+ // `aglyn.com`, and a suffix test would refuse an unrelated company's staff.
127
+ const requiredTenantId = domain ? Object.prototype.hasOwnProperty.call(domains, domain) ? domains[domain] : null : null;
128
+ if (!requiredTenantId) {
129
+ // Ungoverned: a personal address, a customer's address, anything not on
130
+ // the list. Includes the permanent break-glass account by construction
131
+ // rather than by special case — see the staff-access doc.
132
+ return {
133
+ verdict: 'allow-ungoverned',
134
+ refused: false,
135
+ domain,
136
+ requiredTenantId: null,
137
+ tenantId,
138
+ reviewable: false
139
+ };
140
+ }
141
+ if (tenantId === requiredTenantId) {
142
+ return {
143
+ verdict: 'allow-designated-tenant',
144
+ refused: false,
145
+ domain,
146
+ requiredTenantId,
147
+ tenantId,
148
+ reviewable: false
149
+ };
150
+ }
151
+ if (tenantId) {
152
+ // A governed address inside somebody else's tenant. They authenticated
153
+ // through an IdP, so the hole this policy closes is not open here — but
154
+ // the IdP is not one Aglyn controls, so it is flagged for review.
155
+ return {
156
+ verdict: 'allow-foreign-tenant',
157
+ refused: false,
158
+ domain,
159
+ requiredTenantId,
160
+ tenantId,
161
+ reviewable: true
162
+ };
163
+ }
164
+ return {
165
+ verdict: 'refuse-sso-required',
166
+ refused: true,
167
+ domain,
168
+ requiredTenantId,
169
+ tenantId: null,
170
+ reviewable: true
171
+ };
172
+ }
173
+ /**
174
+ * THE ENFORCEMENT SWITCH — ships OFF.
175
+ *
176
+ * Off means `evaluateSsoDomainPolicy` still computes `refused`, and callers
177
+ * still record it, but nobody is turned away. That is deliberate: the audit
178
+ * trail accumulates under the real rule before the rule bites, so flipping the
179
+ * switch holds no surprises.
180
+ *
181
+ * Opt-in by exact string. Anything else — unset, empty, `false`, `0`, a typo —
182
+ * is OFF. A security control that switches itself on through a misspelling is
183
+ * a control that can lock people out through a misspelling.
184
+ *
185
+ * Note both halves must be set for anything to happen: this switch AND a
186
+ * non-empty {@link SSO_REQUIRED_DOMAINS_ENV}. A self-host install that sets
187
+ * only this switch still governs nothing.
188
+ *
189
+ * **Precondition for flipping it — written down so it is not re-litigated:**
190
+ * 1. SSO staff access verified working through a REAL sign-in (not a token
191
+ * inspection): a staff account reaches `/admin/*` via the SAML tenant.
192
+ * 2. Every identity on a governed domain has been migrated into its tenant or
193
+ * retired. Measured on aglyn-main 2026-08-18: TWO would have been refused,
194
+ * neither of them staff — one on a consumer `google.com` provider, and the
195
+ * e2e smoke account on `password`. Flipping before migrating breaks the
196
+ * e2e smoke sign-in, which is the one that bites without warning: it is an
197
+ * AUTOMATION account, so nobody is watching a sign-in page when it fails.
198
+ *
199
+ * A second staff identity is NOT a precondition: the permanent break-glass
200
+ * account is on an ungoverned domain, so this rule can never refuse it.
201
+ */ export const SSO_DOMAIN_ENFORCEMENT_ENV = 'AGLYN_SSO_DOMAIN_ENFORCEMENT';
202
+ export function ssoDomainEnforcementEnabled(env = process.env) {
203
+ return env[SSO_DOMAIN_ENFORCEMENT_ENV] === 'on';
204
+ }
205
+ /**
206
+ * What the refused person is told. Names the fix, not the mechanism.
207
+ *
208
+ * `PLATFORM_BRAND_NAME`, not a literal (AGL-2214). Everything else in this
209
+ * module is careful that the RULE is configuration rather than a compiled-in
210
+ * assumption about Aglyn — and then the one string a refused person actually
211
+ * reads named us anyway. A self-hoster who governs their own domain would
212
+ * have turned their own staff away in our name.
213
+ *
214
+ * Byte-identical for a deployment that has not renamed itself.
215
+ */ export const SSO_DOMAIN_REFUSAL_MESSAGE = `Sign in with your ${PLATFORM_BRAND_NAME} Workspace account ` + '(Single sign-on). Accounts on this domain cannot use a password or ' + 'personal Google sign-in.';
216
+ /**
217
+ * The refusal a caller should return, or null to carry on.
218
+ *
219
+ * Returns null whenever the switch is off, so wiring this into an
220
+ * authentication path is inert until the account owner flips it — the call site is live,
221
+ * the consequence is not.
222
+ */ export function ssoDomainRefusal(identity, env = process.env) {
223
+ const decision = evaluateSsoDomainPolicy(identity, ssoRequiredDomains(env));
224
+ if (!decision.refused) return null;
225
+ if (!ssoDomainEnforcementEnabled(env)) return null;
226
+ return {
227
+ decision,
228
+ response: Response.json({
229
+ error: SSO_DOMAIN_REFUSAL_MESSAGE,
230
+ reason: 'sso-domain-required',
231
+ domain: decision.domain
232
+ }, {
233
+ status: 403
234
+ })
235
+ };
236
+ }
237
+ export default evaluateSsoDomainPolicy;
238
+
239
+ //# sourceMappingURL=sso-domain-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/sso-domain-policy.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 * Workspace-domain SSO policy (AGL-1993).\n *\n * **The rule is about a DOMAIN, not about staff.** An `@aglyn.com` address is a\n * Google Workspace identity, and Workspace is what enforces MFA, disables an\n * account at offboarding, and lets an admin revoke a session. An `@aglyn.com`\n * address that signs in through a project-pool password or a consumer Google\n * account has none of that: it looks like a company identity and is governed\n * like a personal one. That gap is the whole reason this module exists.\n *\n * ## Staff is a SEPARATE axis and this module never reads it\n *\n * Staff may be granted to anybody — SSO or not, `@aglyn.com` or not, project\n * pool or any GCIP tenant including a *customer's*. Nothing here consults the\n * `staff` claim, and `sso-domain-policy.spec.ts` pins that by evaluating every\n * staff permutation and asserting the verdict does not move. The two axes are\n * independent; conflating them would turn a narrow domain control into an\n * accidental SSO mandate for staff, which is explicitly not the policy.\n *\n * These all remain valid, and each has a test:\n *\n * | identity | staff | verdict |\n * | ------------------------------------------ | ----- | ------------------------- |\n * | `@aglyn.com` in the designated tenant | yes | allowed |\n * | personal address, project pool (break-glass)| yes | allowed — permanent design|\n * | personal address, project pool | no | allowed |\n * | SSO identity in a CUSTOMER org's tenant | yes | allowed |\n * | `@aglyn.com` in the PROJECT pool | any | **refused** when enforced |\n *\n * Only the last row is what the switch exists for.\n *\n * ## How this differs from `sso-enforcement.ts`\n *\n * {@link enforceSsoSignInMethods} (AGL-1128/1129) strips non-IdP providers from\n * accounts that are ALREADY INSIDE a tenant. It cannot see an identity that\n * never joined the tenant at all — a project-pool `@aglyn.com` account is\n * invisible to every tenant sweep, by construction. Measured on aglyn-main on\n * 2026-08-18: two such accounts existed, one of them holding a live\n * `google.com` provider. This module covers that outside-the-tenant half.\n *\n * ## Shape note: deliberately NOT a discriminated union\n *\n * `strictNullChecks` is off repo-wide, which defeats boolean-discriminant\n * narrowing — a spec can read the wrong arm of a union and pass silently. So\n * the decision is ONE flat interface with a string `verdict`, and specs assert\n * the literal. There is no arm to read by mistake.\n */\n\n/**\n * ## Self-hosting: this policy is CONFIGURATION, never a compiled-in constant\n *\n * Aglyn ships in two shapes — the Aglyn-operated SaaS, and self-hosted installs\n * on the operator's own Firebase project. `aglyn.com` is *our* Workspace\n * domain; a self-hoster has their own domain, their own IdP, or none at all.\n * Baking our domain in would make the rule dead weight for them at best and a\n * lockout at worst, so the governed set is read from configuration and is\n * **empty by default**.\n *\n * Empty means the rule does nothing. That is simultaneously the correct\n * behaviour for a fresh self-host install and the correct OFF state for us —\n * one default serving both, rather than a special case for either.\n *\n * Format — `domain=tenantId`, comma or whitespace separated:\n *\n * ```\n * AGLYN_SSO_REQUIRED_DOMAINS=\"aglyn.com=aglyn-org-y5v14\"\n * ```\n *\n * A map rather than a bare list because \"must use SSO\" is meaningless without\n * naming WHOSE SSO: the point is IdP lifecycle control, which only the tenant\n * wired to that domain's SAML provider delivers.\n */\nimport { PLATFORM_BRAND_NAME } from '@aglyn/aglyn/app-utils/platform-brand'\n\nexport const SSO_REQUIRED_DOMAINS_ENV = 'AGLYN_SSO_REQUIRED_DOMAINS'\n\nexport type SsoRequiredDomains = Readonly<Record<string, string>>\n\n/**\n * Parse the configured domain→tenant map. Total: anything unparseable is\n * dropped rather than throwing, because this sits on the authentication path\n * and a malformed env var must not take sign-in down. A dropped entry fails\n * OPEN (the domain is simply ungoverned), which is the safe direction for a\n * rule whose only power is to refuse.\n */\nexport function parseSsoRequiredDomains(\n raw: string | null | undefined,\n): SsoRequiredDomains {\n const map: Record<string, string> = {}\n for (const pair of String(raw ?? '').split(/[,\\s]+/)) {\n if (!pair) continue\n const eq = pair.indexOf('=')\n if (eq <= 0) continue\n const domain = pair.slice(0, eq).trim().toLowerCase()\n const tenantId = pair.slice(eq + 1).trim()\n if (!domain || !tenantId) continue\n map[domain] = tenantId\n }\n return map\n}\n\n/**\n * The operator's SSO-required domains. Empty unless configured — see above.\n */\nexport function ssoRequiredDomains(\n env: Record<string, string | undefined> = process.env,\n): SsoRequiredDomains {\n return parseSsoRequiredDomains(env[SSO_REQUIRED_DOMAINS_ENV])\n}\n\nexport type SsoDomainVerdict =\n /** Domain is not governed. Any pool is fine — this is most of the world. */\n | 'allow-ungoverned'\n /** Governed domain, signed in through the tenant we designated for it. */\n | 'allow-designated-tenant'\n /**\n * Governed domain, signed in through SOME tenant, but not the designated\n * one. Allowed — they did use SSO, and refusing would invent a restriction\n * nobody asked for — but surfaced so a staff-access review can see it.\n */\n | 'allow-foreign-tenant'\n /**\n * Governed domain with NO tenant at all: a project-pool password or consumer\n * Google sign-in. The only refusal this policy ever produces.\n */\n | 'refuse-sso-required'\n\nexport interface SsoDomainDecision {\n verdict: SsoDomainVerdict\n /**\n * Whether enforcement WOULD refuse this identity. Independent of whether\n * enforcement is currently switched on — {@link ssoDomainEnforcementEnabled}\n * decides that — so the same evaluation can drive both an audit in\n * report-only mode and the refusal once the switch is flipped.\n */\n refused: boolean\n /** Lower-cased domain, or null when the identity has no email address. */\n domain: string | null\n /** Tenant this domain is required to use, or null when ungoverned. */\n requiredTenantId: string | null\n /** Tenant the identity actually signed in through; null = project pool. */\n tenantId: string | null\n /** Worth a human look even though it is allowed (foreign-tenant case). */\n reviewable: boolean\n}\n\nexport interface SsoDomainIdentity {\n /** The identity's email address. Absent/empty is treated as ungoverned. */\n email?: string | null\n /**\n * GCIP tenant the identity signed in through — `decoded.firebase?.tenant`,\n * or a `PooledUserRecord.tenantId`. Null/undefined means the project pool.\n *\n * Never inferred and never defaulted to a hard-coded tenant: a uid is only\n * unique WITHIN a pool, and assuming one is how a lookup silently lands on\n * the wrong record.\n */\n tenantId?: string | null\n}\n\n/** The domain part of an address, lower-cased. Null when there isn't one. */\nexport function domainOf(email: string | null | undefined): string | null {\n const address = String(email ?? '').trim().toLowerCase()\n const at = address.lastIndexOf('@')\n if (at < 0 || at === address.length - 1) return null\n return address.slice(at + 1)\n}\n\n/**\n * Decide the policy for one identity. Pure and total — no Firebase, no env —\n * so a spec can drive every combination directly, and so the audit view and\n * the enforcement path can never disagree about what the policy says.\n */\nexport function evaluateSsoDomainPolicy(\n identity: SsoDomainIdentity,\n domains: SsoRequiredDomains = ssoRequiredDomains(),\n): SsoDomainDecision {\n const domain = domainOf(identity.email)\n const tenantId = identity.tenantId ?? null\n // Exact-match lookup, never a suffix test: `notaglyn.com` ends with\n // `aglyn.com`, and a suffix test would refuse an unrelated company's staff.\n const requiredTenantId = domain\n ? (Object.prototype.hasOwnProperty.call(domains, domain)\n ? domains[domain]\n : null)\n : null\n\n if (!requiredTenantId) {\n // Ungoverned: a personal address, a customer's address, anything not on\n // the list. Includes the permanent break-glass account by construction\n // rather than by special case — see the staff-access doc.\n return {\n verdict: 'allow-ungoverned',\n refused: false,\n domain,\n requiredTenantId: null,\n tenantId,\n reviewable: false,\n }\n }\n\n if (tenantId === requiredTenantId) {\n return {\n verdict: 'allow-designated-tenant',\n refused: false,\n domain,\n requiredTenantId,\n tenantId,\n reviewable: false,\n }\n }\n\n if (tenantId) {\n // A governed address inside somebody else's tenant. They authenticated\n // through an IdP, so the hole this policy closes is not open here — but\n // the IdP is not one Aglyn controls, so it is flagged for review.\n return {\n verdict: 'allow-foreign-tenant',\n refused: false,\n domain,\n requiredTenantId,\n tenantId,\n reviewable: true,\n }\n }\n\n return {\n verdict: 'refuse-sso-required',\n refused: true,\n domain,\n requiredTenantId,\n tenantId: null,\n reviewable: true,\n }\n}\n\n/**\n * THE ENFORCEMENT SWITCH — ships OFF.\n *\n * Off means `evaluateSsoDomainPolicy` still computes `refused`, and callers\n * still record it, but nobody is turned away. That is deliberate: the audit\n * trail accumulates under the real rule before the rule bites, so flipping the\n * switch holds no surprises.\n *\n * Opt-in by exact string. Anything else — unset, empty, `false`, `0`, a typo —\n * is OFF. A security control that switches itself on through a misspelling is\n * a control that can lock people out through a misspelling.\n *\n * Note both halves must be set for anything to happen: this switch AND a\n * non-empty {@link SSO_REQUIRED_DOMAINS_ENV}. A self-host install that sets\n * only this switch still governs nothing.\n *\n * **Precondition for flipping it — written down so it is not re-litigated:**\n * 1. SSO staff access verified working through a REAL sign-in (not a token\n * inspection): a staff account reaches `/admin/*` via the SAML tenant.\n * 2. Every identity on a governed domain has been migrated into its tenant or\n * retired. Measured on aglyn-main 2026-08-18: TWO would have been refused,\n * neither of them staff — one on a consumer `google.com` provider, and the\n * e2e smoke account on `password`. Flipping before migrating breaks the\n * e2e smoke sign-in, which is the one that bites without warning: it is an\n * AUTOMATION account, so nobody is watching a sign-in page when it fails.\n *\n * A second staff identity is NOT a precondition: the permanent break-glass\n * account is on an ungoverned domain, so this rule can never refuse it.\n */\nexport const SSO_DOMAIN_ENFORCEMENT_ENV = 'AGLYN_SSO_DOMAIN_ENFORCEMENT'\n\nexport function ssoDomainEnforcementEnabled(\n env: Record<string, string | undefined> = process.env,\n): boolean {\n return env[SSO_DOMAIN_ENFORCEMENT_ENV] === 'on'\n}\n\n/**\n * What the refused person is told. Names the fix, not the mechanism.\n *\n * `PLATFORM_BRAND_NAME`, not a literal (AGL-2214). Everything else in this\n * module is careful that the RULE is configuration rather than a compiled-in\n * assumption about Aglyn — and then the one string a refused person actually\n * reads named us anyway. A self-hoster who governs their own domain would\n * have turned their own staff away in our name.\n *\n * Byte-identical for a deployment that has not renamed itself.\n */\nexport const SSO_DOMAIN_REFUSAL_MESSAGE =\n `Sign in with your ${PLATFORM_BRAND_NAME} Workspace account ` +\n '(Single sign-on). Accounts on this domain cannot use a password or ' +\n 'personal Google sign-in.'\n\n/**\n * The refusal a caller should return, or null to carry on.\n *\n * Returns null whenever the switch is off, so wiring this into an\n * authentication path is inert until the account owner flips it — the call site is live,\n * the consequence is not.\n */\nexport function ssoDomainRefusal(\n identity: SsoDomainIdentity,\n env: Record<string, string | undefined> = process.env,\n): { decision: SsoDomainDecision; response: Response } | null {\n const decision = evaluateSsoDomainPolicy(identity, ssoRequiredDomains(env))\n if (!decision.refused) return null\n if (!ssoDomainEnforcementEnabled(env)) return null\n return {\n decision,\n response: Response.json(\n {\n error: SSO_DOMAIN_REFUSAL_MESSAGE,\n reason: 'sso-domain-required',\n domain: decision.domain,\n },\n { status: 403 },\n ),\n }\n}\n\nexport default evaluateSsoDomainPolicy\n"],"names":["PLATFORM_BRAND_NAME","SSO_REQUIRED_DOMAINS_ENV","parseSsoRequiredDomains","raw","map","pair","String","split","eq","indexOf","domain","slice","trim","toLowerCase","tenantId","ssoRequiredDomains","env","process","domainOf","email","address","at","lastIndexOf","length","evaluateSsoDomainPolicy","identity","domains","requiredTenantId","Object","prototype","hasOwnProperty","call","verdict","refused","reviewable","SSO_DOMAIN_ENFORCEMENT_ENV","ssoDomainEnforcementEnabled","SSO_DOMAIN_REFUSAL_MESSAGE","ssoDomainRefusal","decision","response","Response","json","error","reason","status"],"mappings":"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"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * Has an SSO domain stopped proving it belongs to the org routing it, and is
19
+ * that conclusion safe to act on? (AGL-1210)
20
+ *
21
+ * PURE, and in its own file with NO imports at all — not `dns`, not
22
+ * `firebase-admin`. `sso-provisioning.ts` re-exports everything here, so the
23
+ * public surface is unchanged, but the decision can be required by a test on
24
+ * either side of the app/lib boundary without dragging in the admin SDK. The
25
+ * console route's spec does exactly that: it fakes the I/O and runs the REAL
26
+ * decision, which is the only arrangement where "the route obeys the rule"
27
+ * means anything.
28
+ *
29
+ * The reasoning for the policy itself lives in `sso-provisioning.ts`, next to
30
+ * the probe it interprets. In one line: a DNS lookup cannot distinguish "the
31
+ * record is gone" from "nobody answered", so the third state below is what
32
+ * stops a resolver blip from being read as every customer deleting their TXT
33
+ * record at the same instant.
34
+ */
35
+ /**
36
+ * What a re-verification lookup actually established.
37
+ *
38
+ * The third state is the whole point. A resolver that did not answer has told
39
+ * us nothing, and "nothing" must not be counted as "gone".
40
+ */
41
+ export type DomainProbeStatus =
42
+ /** The record is there and matches this org's token. */
43
+ 'proven'
44
+ /** We got an ANSWER, and the token is not in it. Conclusive. */
45
+ | 'missing'
46
+ /** Nobody answered. Not evidence of anything, in either direction. */
47
+ | 'unreachable';
48
+ export interface DomainProbe {
49
+ status: DomainProbeStatus;
50
+ /** What the lookup saw — the difference between "gone" and "changed". */
51
+ records: string[];
52
+ }
53
+ /**
54
+ * Consecutive conclusive failures before a domain is REPORTED as drifted.
55
+ *
56
+ * Three, against a weekly sweep, is a three-week signal. Exported so the
57
+ * threshold is one named number rather than a literal buried in a loop, and so
58
+ * a test can state the boundary it is checking.
59
+ */
60
+ export declare const SSO_DRIFT_FAILURES_BEFORE_REPORT = 3;
61
+ /**
62
+ * And the wall-clock floor the same run must clear, independent of the count.
63
+ *
64
+ * Belt AND braces, because the two guard different mistakes. The count alone
65
+ * can be run up in three minutes by a staff member re-running the sweep by
66
+ * hand, or by a scheduler firing more often than anyone intended — and "we
67
+ * checked three times" reads as diligence while meaning nothing. The age alone
68
+ * would report a domain that failed once a month ago and has answered every
69
+ * time since. A report needs both.
70
+ */
71
+ export declare const SSO_DRIFT_MIN_AGE_MS: number;
72
+ /** The drift bookkeeping carried on a claim between sweeps. */
73
+ export interface DomainDriftState {
74
+ /** Conclusive failures in a row. Reset by any single `proven`. */
75
+ consecutiveFailures: number;
76
+ /** When the current run of failures began. Null when there is no run. */
77
+ firstFailureAtMs: number | null;
78
+ }
79
+ export type DomainDriftAction =
80
+ /** Proven. Any failure run ends here. */
81
+ 'clear'
82
+ /** Unreachable. Change nothing at all — this is not evidence. */
83
+ | 'hold'
84
+ /** Conclusive failure, but not yet enough of them for long enough. */
85
+ | 'count'
86
+ /** Enough, for long enough. Tell people. Still revoke nothing. */
87
+ | 'report';
88
+ export interface DomainDriftVerdict {
89
+ action: DomainDriftAction;
90
+ /** The run length AFTER this probe. Unchanged on `hold`. */
91
+ consecutiveFailures: number;
92
+ /** Start of the run after this probe, for the next sweep to carry. */
93
+ firstFailureAtMs: number | null;
94
+ }
95
+ /**
96
+ * Should this probe change anything, and has the domain drifted?
97
+ *
98
+ * `hold` is the load-bearing arm. An `unreachable` probe neither increments
99
+ * the count nor resets it — an outage must not manufacture evidence, and must
100
+ * not launder away evidence already gathered either. The run simply pauses,
101
+ * and the next conclusive answer continues it.
102
+ *
103
+ * Note what this function CANNOT return: there is no `revoke`. Detection is
104
+ * the whole of its vocabulary, and revocation stays a human act.
105
+ */
106
+ export declare function assessDomainDrift(probe: DomainProbe, prior: DomainDriftState, nowMs: number, failuresBeforeReport?: number, minAgeMs?: number): DomainDriftVerdict;