@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,298 @@
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
+ * Customer API keys (AGL-616). Keys authenticate the public REST API
18
+ * (`/api/v1/**`). Only a SHA-256 hash of the key is ever stored — the raw
19
+ * token is shown to the user exactly once, at creation. The Firestore doc id
20
+ * IS the hash, so verification is an O(1) `get()` and the hash never leaves
21
+ * the server (listings return metadata only). The `apiKeys` collection is
22
+ * deny-all in the security rules; all access is Admin-SDK only.
23
+ *
24
+ * The token/hash/format/scope helpers are pure and unit-tested; the storage
25
+ * functions wrap the Admin SDK.
26
+ */ import { createHash, randomBytes } from "node:crypto";
27
+ import { firebaseAdmin } from "./firebase-admin.js";
28
+ /**
29
+ * Scopes a key can be granted, as `resource:action`. Every entry must be
30
+ * enforced by a real endpoint — a mintable scope that grants nothing reads
31
+ * as a broken permission (AGL-899 removed `contacts:write` on those grounds,
32
+ * and AGL-2276 re-added it in the same change that shipped the writes, which
33
+ * is the condition AGL-899 set).
34
+ */ export const API_SCOPES = [
35
+ 'datasets:read',
36
+ 'datasets:write',
37
+ 'contacts:read',
38
+ // AGL-2276. Contacts used to arrive only through the four site capture
39
+ // points, so an integration that OWNS the customer list upstream — a CRM,
40
+ // a mailing tool, a migration off another platform — could read the
41
+ // audience and never add to it. The write is metered against the same
42
+ // audience bands capture is (AGL-890): `checkContactQuota` on the create,
43
+ // and the monthly rollup counts `orgs/{orgId}/contacts` whatever wrote
44
+ // them, so an API-created contact bills exactly like a form-captured one.
45
+ // The scope covers the create, the tag/notes edit the console's Contacts
46
+ // page already makes, and the delete — never `email`, which is the dedupe
47
+ // key, and never `sources`/`interactions`, which are provenance.
48
+ 'contacts:write',
49
+ 'sites:read',
50
+ // AGL-2462. An integration could write a dataset record over /v1 and had no
51
+ // way to make a live page show it: `getDatasets` caches for 60s behind
52
+ // `tenant-data:{hostId}` and the catch-all page is `revalidate = 60`, and
53
+ // time-based ISR is stale-while-revalidate — so the visitor AFTER the window
54
+ // can still get the old copy. A cache expiring is not a publish.
55
+ //
56
+ // Its OWN scope rather than a `sites:write` covering two very different
57
+ // powers. Publishing is not a site edit: it changes no stored field, and it
58
+ // is the one /v1 call whose cost lands on a different service — up to 250
59
+ // dropped pages, each re-rendering at roughly 40 Firestore reads. A key that
60
+ // syncs a catalogue should not thereby be able to spend that, and an
61
+ // integration that only needs to flush its own writes should not have to
62
+ // hold a scope that could rename the site. Bounded by a per-HOST hourly
63
+ // budget on top of the per-key request limit, because one call and 250
64
+ // dropped pages are not the same unit of cost.
65
+ 'sites:publish',
66
+ // AGL-2465. Everything downstream of a site was automatable and creating
67
+ // the site was not: `handleSites` 405'd every non-GET, so an agency
68
+ // onboarding a client could push its datasets, contacts and media and still
69
+ // had to open a browser to make the site those things go in.
70
+ //
71
+ // CREATE ONLY — no rename, no delete, and the omissions are deliberate
72
+ // rather than unfinished. `hosts/rename` authorizes off the host projection
73
+ // alone — the member's own entry in `memberRoles`, which must equal
74
+ // `admin` — and an API key is an ORG credential with no uid, so that gate
75
+ // has no translation at all; giving it one is a permission decision, not a
76
+ // port. `hosts/delete` runs
77
+ // `recursiveDelete` over the whole site plus a storage prefix wipe with no
78
+ // hold and no soft-delete, and the type-the-name confirmation that guards it
79
+ // is console UI only — a one-field irreversible delete is the last thing to
80
+ // put behind a bearer token, and whether it should exist at all is a product
81
+ // decision that has not been made.
82
+ //
83
+ // Replay-safe, which is what actually blocked this: site creation is the
84
+ // most expensive object here to duplicate — it takes a `hostLimit` slot,
85
+ // writes `hostIndex` and runs `syncOrgAuthProjections` across every member —
86
+ // and `/v1` publishes idempotency as a contract. A POST that succeeded
87
+ // server-side but lost its response now replays the original site rather
88
+ // than minting a second one.
89
+ 'sites:write',
90
+ 'forms:read',
91
+ // AGL-2127. A lead sync could READ a submission and never record that it
92
+ // had — so it re-pushed the same lead on every poll, or kept its own
93
+ // high-water mark against a list ordered by document id rather than by
94
+ // time. `read` is the flag the console already toggles on the same
95
+ // document, so the scope grants exactly the state the integration needs
96
+ // (plus the delete that lets it purge after export), and nothing that
97
+ // rewrites what a visitor actually typed.
98
+ 'forms:write',
99
+ // Commerce and media reads (AGL-1928). Each is separate from `sites:read`
100
+ // for the same reason `forms:read` is: a key handed to a fulfilment or
101
+ // accounting integration needs the order history and nothing else, and a
102
+ // key that syncs a catalogue into a PIM should not thereby be able to read
103
+ // every form submission a site has ever taken. Read-only on purpose —
104
+ // writes here move money or stock and want their own decision, in the same
105
+ // change that ships them (the AGL-899 rule for `contacts:write`).
106
+ 'orders:read',
107
+ // AGL-2461. The read-only note above says commerce writes wait for "their
108
+ // own decision, in the same change that ships them" because they "move
109
+ // money or stock". Recording a shipment does NEITHER — `fulfill-order.ts`'s
110
+ // own header says so: "a forward status flip plus a timeline entry, no
111
+ // stock moved, no money moved" — so it is precisely the write that rule
112
+ // admits, and it is the day-one commerce automation: a 3PL, ShipStation, or
113
+ // a warehouse's own system marking an order shipped with a tracking number.
114
+ // A public API that can take an order and never ship it is a fulfilment
115
+ // dead end.
116
+ //
117
+ // Fulfilment ONLY. `cancelled` and `refunded` are refused by name with a
118
+ // 400: those move stock and money through their own transactions, and this
119
+ // scope must never become the door around them. The transition rule itself
120
+ // is not re-implemented here at all — the write goes through the commerce
121
+ // plugin's own transaction via the order-fulfilment capability registry,
122
+ // so the API can never write a status the console forbids.
123
+ 'orders:write',
124
+ 'products:read',
125
+ 'media:read',
126
+ // AGL-2463. `media:read` was the only media scope, so an agency onboarding a
127
+ // client site could automate everything about that site except putting its
128
+ // images in it — an integration could LIST the library and never add to it,
129
+ // which stops the asset pipeline at the one step that matters.
130
+ //
131
+ // Create only: no replace, no delete, no edit of an existing file's metadata.
132
+ // A key handed to a migration tool should be able to fill a library and not
133
+ // to empty one, and the destructive half wants its own decision in the change
134
+ // that ships it — the same rule that kept this scope out until now.
135
+ //
136
+ // Charged, not free: the create goes through `mediaStorageGate`, the same
137
+ // helper the console's four ingress routes use, and consumes the existing
138
+ // per-GB storage dimension rather than inventing a per-upload one.
139
+ 'media:write',
140
+ // AGL-2606. The CRM v2 collections (AGL-2595) — companies, pipelines,
141
+ // deals, tasks and activities — sit beside `contacts` and were reachable
142
+ // only from the console, so the integration that owns the sales process
143
+ // upstream (a CRM sync, a dialer logging calls, a billing system closing
144
+ // deals) could read the people and nothing they were doing with them.
145
+ //
146
+ // ONE pair for the five resources rather than five, because a key that
147
+ // needs any of them needs the set: a deal names a company and a contact, a
148
+ // task names a deal, an activity names all three, and a key that could
149
+ // read deals but not the pipelines whose stages they cite would hand back
150
+ // stage ids it cannot explain. The contact's own CRM profile — phone, job
151
+ // title, company, owner, lifecycle stage — stays under `contacts:*`, since
152
+ // it is a field of the contact and reads with the contact.
153
+ //
154
+ // Reads are org-wide, as every read on this surface is: a key is an
155
+ // organization credential. Writes name the site they belong to and are
156
+ // stamped in that site's scope (`crmCreateStamp`), so an API-created deal
157
+ // lands exactly where a console-created one would, and never wider.
158
+ 'crm:read',
159
+ 'crm:write'
160
+ ];
161
+ /** Live-key token prefix. A `_test_` variant can follow the same shape later. */ export const API_KEY_TOKEN_PREFIX = 'aglyn_sk_';
162
+ /** Top-level collection; doc id = the token hash. */ export const API_KEYS_COLLECTION = 'apiKeys';
163
+ /** How much of the raw token is retained (plaintext) for display/identification. */ const DISPLAY_BODY_CHARS = 6;
164
+ // ── Pure helpers (unit-tested; no I/O) ──────────────────────────────────────
165
+ /** True if `value` is one of the known scopes. */ export function isApiScope(value) {
166
+ return API_SCOPES.includes(value);
167
+ }
168
+ /** Keep only valid, de-duplicated scopes, preserving `API_SCOPES` order. */ export function normalizeScopes(scopes) {
169
+ const set = new Set(scopes.filter(isApiScope));
170
+ return API_SCOPES.filter((scope)=>set.has(scope));
171
+ }
172
+ /** SHA-256 hex of a raw token — the Firestore doc id and lookup key. */ export function hashApiKey(token) {
173
+ return createHash('sha256').update(token, 'utf8').digest('hex');
174
+ }
175
+ /** Structural check before hashing/looking up — cheap reject of junk input. */ export function isValidApiKeyFormat(token) {
176
+ return typeof token === 'string' && token.startsWith(API_KEY_TOKEN_PREFIX) && /^[A-Za-z0-9_-]{24,}$/.test(token.slice(API_KEY_TOKEN_PREFIX.length));
177
+ }
178
+ /**
179
+ * Generate a fresh token plus the derived hash and display prefix. The raw
180
+ * `token` is the only time the secret exists in plaintext — persist the hash,
181
+ * hand the token to the user once.
182
+ */ export function generateApiKeyToken() {
183
+ const body = randomBytes(24).toString('base64url');
184
+ const token = `${API_KEY_TOKEN_PREFIX}${body}`;
185
+ return {
186
+ token,
187
+ hash: hashApiKey(token),
188
+ keyPrefix: `${API_KEY_TOKEN_PREFIX}${body.slice(0, DISPLAY_BODY_CHARS)}…`
189
+ };
190
+ }
191
+ /** True when the granted scopes include the one an operation requires. */ export function hasScope(granted, required) {
192
+ return granted.includes(required);
193
+ }
194
+ /** A random public key id (`key_…`), distinct from the secret and its hash. */ export function generateKeyId() {
195
+ return `key_${randomBytes(9).toString('base64url')}`;
196
+ }
197
+ // ── Storage (Admin SDK) ─────────────────────────────────────────────────────
198
+ function collection() {
199
+ return firebaseAdmin.app().firestore().collection(API_KEYS_COLLECTION);
200
+ }
201
+ const tsToIso = (value)=>value ? value.toDate().toISOString() : null;
202
+ /** Public projection of a stored key — never includes the hash/doc id. */ export function toPublicApiKey(doc) {
203
+ var _doc_scopes;
204
+ return {
205
+ keyId: doc.keyId,
206
+ name: doc.name,
207
+ keyPrefix: doc.keyPrefix,
208
+ scopes: (_doc_scopes = doc.scopes) != null ? _doc_scopes : [],
209
+ createdAt: tsToIso(doc.createdAt),
210
+ lastUsedAt: tsToIso(doc.lastUsedAt),
211
+ revokedAt: tsToIso(doc.revokedAt),
212
+ expiresAt: tsToIso(doc.expiresAt)
213
+ };
214
+ }
215
+ /**
216
+ * Create a key for an org. Returns the raw `token` (show once) and the public
217
+ * metadata. Invalid scopes are dropped; at least one valid scope is required.
218
+ */ export async function mintApiKey(input) {
219
+ const scopes = normalizeScopes(input.scopes);
220
+ if (scopes.length === 0) {
221
+ throw new Error('An API key needs at least one valid scope');
222
+ }
223
+ const { Timestamp, FieldValue } = firebaseAdmin.firestore;
224
+ const { token, hash, keyPrefix } = generateApiKeyToken();
225
+ const keyId = generateKeyId();
226
+ const expiresAt = input.expiresAtMs != null ? Timestamp.fromMillis(input.expiresAtMs) : null;
227
+ const doc = {
228
+ keyId,
229
+ orgId: input.orgId,
230
+ name: input.name.slice(0, 120),
231
+ keyPrefix,
232
+ scopes,
233
+ createdBy: input.createdBy,
234
+ createdAt: FieldValue.serverTimestamp(),
235
+ lastUsedAt: null,
236
+ revokedAt: null,
237
+ expiresAt
238
+ };
239
+ // `create` (not set) so a hash collision can never overwrite another key.
240
+ await collection().doc(hash).create(doc);
241
+ return {
242
+ token,
243
+ key: {
244
+ keyId,
245
+ name: doc.name,
246
+ keyPrefix,
247
+ scopes,
248
+ createdAt: new Date().toISOString(),
249
+ lastUsedAt: null,
250
+ revokedAt: null,
251
+ expiresAt: expiresAt ? expiresAt.toDate().toISOString() : null
252
+ }
253
+ };
254
+ }
255
+ /**
256
+ * Resolve a raw token to its org/scopes, or `null` if it is malformed,
257
+ * unknown, revoked, or expired. Refreshes `lastUsedAt` at most once a minute
258
+ * (fire-and-forget) to avoid a write on every request.
259
+ */ export async function verifyApiKey(token) {
260
+ var _data_scopes, _data_name;
261
+ if (!isValidApiKeyFormat(token)) return null;
262
+ const snap = await collection().doc(hashApiKey(token)).get();
263
+ if (!snap.exists) return null;
264
+ const data = snap.data();
265
+ if (data.revokedAt) return null;
266
+ const now = Date.now();
267
+ if (data.expiresAt && data.expiresAt.toMillis() <= now) return null;
268
+ const lastUsedMs = data.lastUsedAt ? data.lastUsedAt.toMillis() : 0;
269
+ if (now - lastUsedMs > 60000) {
270
+ snap.ref.update({
271
+ lastUsedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp()
272
+ }).catch(()=>undefined);
273
+ }
274
+ return {
275
+ orgId: data.orgId,
276
+ keyId: data.keyId,
277
+ scopes: (_data_scopes = data.scopes) != null ? _data_scopes : [],
278
+ name: String((_data_name = data.name) != null ? _data_name : '')
279
+ };
280
+ }
281
+ /** All keys for an org, newest first (metadata only). */ export async function listApiKeys(orgId) {
282
+ const snap = await collection().where('orgId', '==', orgId).get();
283
+ return snap.docs.map((doc)=>toPublicApiKey(doc.data())).sort((a, b)=>{
284
+ var _b_createdAt, _a_createdAt;
285
+ return ((_b_createdAt = b.createdAt) != null ? _b_createdAt : '').localeCompare((_a_createdAt = a.createdAt) != null ? _a_createdAt : '');
286
+ });
287
+ }
288
+ /** Revoke a key by its public id. Returns false if not found/already revoked. */ export async function revokeApiKey(orgId, keyId) {
289
+ const snap = await collection().where('orgId', '==', orgId).get();
290
+ const match = snap.docs.find((doc)=>doc.data().keyId === keyId);
291
+ if (!match || match.data().revokedAt) return false;
292
+ await match.ref.update({
293
+ revokedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp()
294
+ });
295
+ return true;
296
+ }
297
+
298
+ //# sourceMappingURL=api-keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/api-keys.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 * Customer API keys (AGL-616). Keys authenticate the public REST API\n * (`/api/v1/**`). Only a SHA-256 hash of the key is ever stored — the raw\n * token is shown to the user exactly once, at creation. The Firestore doc id\n * IS the hash, so verification is an O(1) `get()` and the hash never leaves\n * the server (listings return metadata only). The `apiKeys` collection is\n * deny-all in the security rules; all access is Admin-SDK only.\n *\n * The token/hash/format/scope helpers are pure and unit-tested; the storage\n * functions wrap the Admin SDK.\n */\nimport { createHash, randomBytes } from 'node:crypto'\nimport { firebaseAdmin } from './firebase-admin'\n\n/**\n * Scopes a key can be granted, as `resource:action`. Every entry must be\n * enforced by a real endpoint — a mintable scope that grants nothing reads\n * as a broken permission (AGL-899 removed `contacts:write` on those grounds,\n * and AGL-2276 re-added it in the same change that shipped the writes, which\n * is the condition AGL-899 set).\n */\nexport const API_SCOPES = [\n 'datasets:read',\n 'datasets:write',\n 'contacts:read',\n // AGL-2276. Contacts used to arrive only through the four site capture\n // points, so an integration that OWNS the customer list upstream — a CRM,\n // a mailing tool, a migration off another platform — could read the\n // audience and never add to it. The write is metered against the same\n // audience bands capture is (AGL-890): `checkContactQuota` on the create,\n // and the monthly rollup counts `orgs/{orgId}/contacts` whatever wrote\n // them, so an API-created contact bills exactly like a form-captured one.\n // The scope covers the create, the tag/notes edit the console's Contacts\n // page already makes, and the delete — never `email`, which is the dedupe\n // key, and never `sources`/`interactions`, which are provenance.\n 'contacts:write',\n 'sites:read',\n // AGL-2462. An integration could write a dataset record over /v1 and had no\n // way to make a live page show it: `getDatasets` caches for 60s behind\n // `tenant-data:{hostId}` and the catch-all page is `revalidate = 60`, and\n // time-based ISR is stale-while-revalidate — so the visitor AFTER the window\n // can still get the old copy. A cache expiring is not a publish.\n //\n // Its OWN scope rather than a `sites:write` covering two very different\n // powers. Publishing is not a site edit: it changes no stored field, and it\n // is the one /v1 call whose cost lands on a different service — up to 250\n // dropped pages, each re-rendering at roughly 40 Firestore reads. A key that\n // syncs a catalogue should not thereby be able to spend that, and an\n // integration that only needs to flush its own writes should not have to\n // hold a scope that could rename the site. Bounded by a per-HOST hourly\n // budget on top of the per-key request limit, because one call and 250\n // dropped pages are not the same unit of cost.\n 'sites:publish',\n // AGL-2465. Everything downstream of a site was automatable and creating\n // the site was not: `handleSites` 405'd every non-GET, so an agency\n // onboarding a client could push its datasets, contacts and media and still\n // had to open a browser to make the site those things go in.\n //\n // CREATE ONLY — no rename, no delete, and the omissions are deliberate\n // rather than unfinished. `hosts/rename` authorizes off the host projection\n // alone — the member's own entry in `memberRoles`, which must equal\n // `admin` — and an API key is an ORG credential with no uid, so that gate\n // has no translation at all; giving it one is a permission decision, not a\n // port. `hosts/delete` runs\n // `recursiveDelete` over the whole site plus a storage prefix wipe with no\n // hold and no soft-delete, and the type-the-name confirmation that guards it\n // is console UI only — a one-field irreversible delete is the last thing to\n // put behind a bearer token, and whether it should exist at all is a product\n // decision that has not been made.\n //\n // Replay-safe, which is what actually blocked this: site creation is the\n // most expensive object here to duplicate — it takes a `hostLimit` slot,\n // writes `hostIndex` and runs `syncOrgAuthProjections` across every member —\n // and `/v1` publishes idempotency as a contract. A POST that succeeded\n // server-side but lost its response now replays the original site rather\n // than minting a second one.\n 'sites:write',\n 'forms:read',\n // AGL-2127. A lead sync could READ a submission and never record that it\n // had — so it re-pushed the same lead on every poll, or kept its own\n // high-water mark against a list ordered by document id rather than by\n // time. `read` is the flag the console already toggles on the same\n // document, so the scope grants exactly the state the integration needs\n // (plus the delete that lets it purge after export), and nothing that\n // rewrites what a visitor actually typed.\n 'forms:write',\n // Commerce and media reads (AGL-1928). Each is separate from `sites:read`\n // for the same reason `forms:read` is: a key handed to a fulfilment or\n // accounting integration needs the order history and nothing else, and a\n // key that syncs a catalogue into a PIM should not thereby be able to read\n // every form submission a site has ever taken. Read-only on purpose —\n // writes here move money or stock and want their own decision, in the same\n // change that ships them (the AGL-899 rule for `contacts:write`).\n 'orders:read',\n // AGL-2461. The read-only note above says commerce writes wait for \"their\n // own decision, in the same change that ships them\" because they \"move\n // money or stock\". Recording a shipment does NEITHER — `fulfill-order.ts`'s\n // own header says so: \"a forward status flip plus a timeline entry, no\n // stock moved, no money moved\" — so it is precisely the write that rule\n // admits, and it is the day-one commerce automation: a 3PL, ShipStation, or\n // a warehouse's own system marking an order shipped with a tracking number.\n // A public API that can take an order and never ship it is a fulfilment\n // dead end.\n //\n // Fulfilment ONLY. `cancelled` and `refunded` are refused by name with a\n // 400: those move stock and money through their own transactions, and this\n // scope must never become the door around them. The transition rule itself\n // is not re-implemented here at all — the write goes through the commerce\n // plugin's own transaction via the order-fulfilment capability registry,\n // so the API can never write a status the console forbids.\n 'orders:write',\n 'products:read',\n 'media:read',\n // AGL-2463. `media:read` was the only media scope, so an agency onboarding a\n // client site could automate everything about that site except putting its\n // images in it — an integration could LIST the library and never add to it,\n // which stops the asset pipeline at the one step that matters.\n //\n // Create only: no replace, no delete, no edit of an existing file's metadata.\n // A key handed to a migration tool should be able to fill a library and not\n // to empty one, and the destructive half wants its own decision in the change\n // that ships it — the same rule that kept this scope out until now.\n //\n // Charged, not free: the create goes through `mediaStorageGate`, the same\n // helper the console's four ingress routes use, and consumes the existing\n // per-GB storage dimension rather than inventing a per-upload one.\n 'media:write',\n // AGL-2606. The CRM v2 collections (AGL-2595) — companies, pipelines,\n // deals, tasks and activities — sit beside `contacts` and were reachable\n // only from the console, so the integration that owns the sales process\n // upstream (a CRM sync, a dialer logging calls, a billing system closing\n // deals) could read the people and nothing they were doing with them.\n //\n // ONE pair for the five resources rather than five, because a key that\n // needs any of them needs the set: a deal names a company and a contact, a\n // task names a deal, an activity names all three, and a key that could\n // read deals but not the pipelines whose stages they cite would hand back\n // stage ids it cannot explain. The contact's own CRM profile — phone, job\n // title, company, owner, lifecycle stage — stays under `contacts:*`, since\n // it is a field of the contact and reads with the contact.\n //\n // Reads are org-wide, as every read on this surface is: a key is an\n // organization credential. Writes name the site they belong to and are\n // stamped in that site's scope (`crmCreateStamp`), so an API-created deal\n // lands exactly where a console-created one would, and never wider.\n 'crm:read',\n 'crm:write',\n] as const\n\nexport type ApiScope = (typeof API_SCOPES)[number]\n\n/** Live-key token prefix. A `_test_` variant can follow the same shape later. */\nexport const API_KEY_TOKEN_PREFIX = 'aglyn_sk_'\n\n/** Top-level collection; doc id = the token hash. */\nexport const API_KEYS_COLLECTION = 'apiKeys'\n\n/** How much of the raw token is retained (plaintext) for display/identification. */\nconst DISPLAY_BODY_CHARS = 6\n\n/** Stored document. `createdAt`/`lastUsedAt`/etc. are Firestore Timestamps. */\nexport interface ApiKeyDocument {\n /** Public, stable id shown in the UI and used for revoke (never the hash). */\n keyId: string\n orgId: string\n name: string\n /** e.g. `aglyn_sk_ab12cd…` — enough to recognize a key, not to use it. */\n keyPrefix: string\n scopes: ApiScope[]\n createdBy: string\n createdAt: FirebaseFirestore.Timestamp\n lastUsedAt: FirebaseFirestore.Timestamp | null\n revokedAt: FirebaseFirestore.Timestamp | null\n expiresAt: FirebaseFirestore.Timestamp | null\n}\n\n/** Metadata safe to return to a client (no hash, no raw token). */\nexport interface PublicApiKey {\n keyId: string\n name: string\n keyPrefix: string\n scopes: ApiScope[]\n createdAt: string | null\n lastUsedAt: string | null\n revokedAt: string | null\n expiresAt: string | null\n}\n\n/** The resolved principal after a successful key verification. */\nexport interface VerifiedApiKey {\n orgId: string\n keyId: string\n scopes: ApiScope[]\n /**\n * The name the organization gave the key — what a feed attributes the\n * key's writes to (AGL-2632). Every minted key carries one; empty only\n * for a document written before the field was required.\n */\n name: string\n}\n\n// ── Pure helpers (unit-tested; no I/O) ──────────────────────────────────────\n\n/** True if `value` is one of the known scopes. */\nexport function isApiScope(value: string): value is ApiScope {\n return (API_SCOPES as readonly string[]).includes(value)\n}\n\n/** Keep only valid, de-duplicated scopes, preserving `API_SCOPES` order. */\nexport function normalizeScopes(scopes: readonly string[]): ApiScope[] {\n const set = new Set(scopes.filter(isApiScope) as ApiScope[])\n return API_SCOPES.filter((scope) => set.has(scope))\n}\n\n/** SHA-256 hex of a raw token — the Firestore doc id and lookup key. */\nexport function hashApiKey(token: string): string {\n return createHash('sha256').update(token, 'utf8').digest('hex')\n}\n\n/** Structural check before hashing/looking up — cheap reject of junk input. */\nexport function isValidApiKeyFormat(token: string): boolean {\n return (\n typeof token === 'string' &&\n token.startsWith(API_KEY_TOKEN_PREFIX) &&\n /^[A-Za-z0-9_-]{24,}$/.test(token.slice(API_KEY_TOKEN_PREFIX.length))\n )\n}\n\n/**\n * Generate a fresh token plus the derived hash and display prefix. The raw\n * `token` is the only time the secret exists in plaintext — persist the hash,\n * hand the token to the user once.\n */\nexport function generateApiKeyToken(): {\n token: string\n hash: string\n keyPrefix: string\n} {\n const body = randomBytes(24).toString('base64url')\n const token = `${API_KEY_TOKEN_PREFIX}${body}`\n return {\n token,\n hash: hashApiKey(token),\n keyPrefix: `${API_KEY_TOKEN_PREFIX}${body.slice(0, DISPLAY_BODY_CHARS)}…`,\n }\n}\n\n/** True when the granted scopes include the one an operation requires. */\nexport function hasScope(\n granted: readonly ApiScope[],\n required: ApiScope,\n): boolean {\n return granted.includes(required)\n}\n\n/** A random public key id (`key_…`), distinct from the secret and its hash. */\nexport function generateKeyId(): string {\n return `key_${randomBytes(9).toString('base64url')}`\n}\n\n// ── Storage (Admin SDK) ─────────────────────────────────────────────────────\n\nfunction collection() {\n return firebaseAdmin.app().firestore().collection(API_KEYS_COLLECTION)\n}\n\nconst tsToIso = (\n value: FirebaseFirestore.Timestamp | null | undefined,\n): string | null => (value ? value.toDate().toISOString() : null)\n\n/** Public projection of a stored key — never includes the hash/doc id. */\nexport function toPublicApiKey(doc: ApiKeyDocument): PublicApiKey {\n return {\n keyId: doc.keyId,\n name: doc.name,\n keyPrefix: doc.keyPrefix,\n scopes: doc.scopes ?? [],\n createdAt: tsToIso(doc.createdAt),\n lastUsedAt: tsToIso(doc.lastUsedAt),\n revokedAt: tsToIso(doc.revokedAt),\n expiresAt: tsToIso(doc.expiresAt),\n }\n}\n\nexport interface MintApiKeyInput {\n orgId: string\n name: string\n scopes: readonly string[]\n createdBy: string\n /** Optional absolute expiry, epoch ms. */\n expiresAtMs?: number | null\n}\n\n/**\n * Create a key for an org. Returns the raw `token` (show once) and the public\n * metadata. Invalid scopes are dropped; at least one valid scope is required.\n */\nexport async function mintApiKey(\n input: MintApiKeyInput,\n): Promise<{ token: string; key: PublicApiKey }> {\n const scopes = normalizeScopes(input.scopes)\n if (scopes.length === 0) {\n throw new Error('An API key needs at least one valid scope')\n }\n const { Timestamp, FieldValue } = firebaseAdmin.firestore\n const { token, hash, keyPrefix } = generateApiKeyToken()\n const keyId = generateKeyId()\n const expiresAt =\n input.expiresAtMs != null ? Timestamp.fromMillis(input.expiresAtMs) : null\n\n const doc = {\n keyId,\n orgId: input.orgId,\n name: input.name.slice(0, 120),\n keyPrefix,\n scopes,\n createdBy: input.createdBy,\n createdAt: FieldValue.serverTimestamp(),\n lastUsedAt: null,\n revokedAt: null,\n expiresAt,\n }\n // `create` (not set) so a hash collision can never overwrite another key.\n await collection().doc(hash).create(doc)\n\n return {\n token,\n key: {\n keyId,\n name: doc.name,\n keyPrefix,\n scopes,\n createdAt: new Date().toISOString(),\n lastUsedAt: null,\n revokedAt: null,\n expiresAt: expiresAt ? expiresAt.toDate().toISOString() : null,\n },\n }\n}\n\n/**\n * Resolve a raw token to its org/scopes, or `null` if it is malformed,\n * unknown, revoked, or expired. Refreshes `lastUsedAt` at most once a minute\n * (fire-and-forget) to avoid a write on every request.\n */\nexport async function verifyApiKey(\n token: string,\n): Promise<VerifiedApiKey | null> {\n if (!isValidApiKeyFormat(token)) return null\n const snap = await collection().doc(hashApiKey(token)).get()\n if (!snap.exists) return null\n const data = snap.data() as ApiKeyDocument\n\n if (data.revokedAt) return null\n const now = Date.now()\n if (data.expiresAt && data.expiresAt.toMillis() <= now) return null\n\n const lastUsedMs = data.lastUsedAt ? data.lastUsedAt.toMillis() : 0\n if (now - lastUsedMs > 60_000) {\n snap.ref\n .update({ lastUsedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp() })\n .catch(() => undefined)\n }\n\n return {\n orgId: data.orgId,\n keyId: data.keyId,\n scopes: data.scopes ?? [],\n name: String(data.name ?? ''),\n }\n}\n\n/** All keys for an org, newest first (metadata only). */\nexport async function listApiKeys(orgId: string): Promise<PublicApiKey[]> {\n const snap = await collection().where('orgId', '==', orgId).get()\n return snap.docs\n .map((doc) => toPublicApiKey(doc.data() as ApiKeyDocument))\n .sort((a, b) => (b.createdAt ?? '').localeCompare(a.createdAt ?? ''))\n}\n\n/** Revoke a key by its public id. Returns false if not found/already revoked. */\nexport async function revokeApiKey(\n orgId: string,\n keyId: string,\n): Promise<boolean> {\n const snap = await collection().where('orgId', '==', orgId).get()\n const match = snap.docs.find(\n (doc) => (doc.data() as ApiKeyDocument).keyId === keyId,\n )\n if (!match || (match.data() as ApiKeyDocument).revokedAt) return false\n await match.ref.update({\n revokedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n })\n return true\n}\n"],"names":["createHash","randomBytes","firebaseAdmin","API_SCOPES","API_KEY_TOKEN_PREFIX","API_KEYS_COLLECTION","DISPLAY_BODY_CHARS","isApiScope","value","includes","normalizeScopes","scopes","set","Set","filter","scope","has","hashApiKey","token","update","digest","isValidApiKeyFormat","startsWith","test","slice","length","generateApiKeyToken","body","toString","hash","keyPrefix","hasScope","granted","required","generateKeyId","collection","app","firestore","tsToIso","toDate","toISOString","toPublicApiKey","doc","keyId","name","createdAt","lastUsedAt","revokedAt","expiresAt","mintApiKey","input","Error","Timestamp","FieldValue","expiresAtMs","fromMillis","orgId","createdBy","serverTimestamp","create","key","Date","verifyApiKey","data","snap","get","exists","now","toMillis","lastUsedMs","ref","catch","undefined","String","listApiKeys","where","docs","map","sort","a","b","localeCompare","revokeApiKey","match","find"],"mappings":"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"}
@@ -0,0 +1,165 @@
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
+ /** Firestore collection holding pending/authorized handoffs. */
18
+ export declare const AUTH_HANDOFFS_COLLECTION = "authHandoffs";
19
+ /** Name of the host-only verifier cookie the custom domain sets. */
20
+ export declare const HANDOFF_VERIFIER_COOKIE = "__aglyn_handoff";
21
+ /** initiate → authorize. Generous; a pending record grants nothing. */
22
+ export declare const HANDOFF_PENDING_TTL_MS: number;
23
+ /** authorize → redeem. One navigation and one same-origin POST. */
24
+ export declare const HANDOFF_AUTHORIZED_TTL_MS: number;
25
+ export type HandoffStatus = 'pending' | 'authorized' | 'redeemed';
26
+ /** SHA-256 hex, the same shape `hashApiKey` stores. */
27
+ export declare function hashHandoffSecret(value: string): string;
28
+ /**
29
+ * A `continuePath` that cannot leave the custom domain.
30
+ *
31
+ * The handoff lands the user somewhere immediately after signing them in,
32
+ * which is the worst possible moment to hand control to an attacker-chosen
33
+ * origin — so this defers to {@link safeSameOriginPath}, which resolves the
34
+ * value and compares origins instead of testing its shape (AGL-1881).
35
+ *
36
+ * This function used to reject `//evil.example` and `/\evil.example` by
37
+ * inspecting the characters. That list read as complete and was not: the URL
38
+ * parser deletes tab, LF and CR before parsing, so `/<TAB>/evil.example`
39
+ * contains neither `//` nor `\` and still resolves to `https://evil.example/`.
40
+ * Enumerating tricks is the bug; asking the parser is the fix.
41
+ */
42
+ export declare function safeContinuePath(input: string | null | undefined): string;
43
+ export interface StartedHandoff {
44
+ requestId: string;
45
+ /** `V` — goes in the custom domain's own host-only cookie. */
46
+ verifier: string;
47
+ expiresAtMs: number;
48
+ }
49
+ /**
50
+ * Create a `pending` handoff for `targetHost`, returning the id and verifier.
51
+ *
52
+ * Called on the custom domain, unauthenticated: nothing about a pending record
53
+ * is a capability. It names the host the request actually arrived on — never a
54
+ * host from a parameter — which is what makes the `targetHost` check at
55
+ * redemption mean anything.
56
+ */
57
+ export declare function startConsoleHandoff(options: {
58
+ targetHost: string;
59
+ orgSlug: string | null;
60
+ continuePath?: string | null;
61
+ nowMs?: number;
62
+ }): Promise<StartedHandoff | null>;
63
+ export type AuthorizeRefusal = 'unknown-request' | 'not-pending' | 'expired' | 'host-mismatch' | 'domain-inactive' | 'not-a-member' | 'impersonation';
64
+ export type AuthorizeResult = {
65
+ ok: true;
66
+ /** `S` — goes in the return URL's FRAGMENT, never its query. */
67
+ secret: string;
68
+ targetHost: string;
69
+ continuePath: string;
70
+ } | {
71
+ ok: false;
72
+ reason: AuthorizeRefusal;
73
+ orgSlug: string | null;
74
+ };
75
+ /**
76
+ * Authorize a pending handoff, on the AUTH HOST, for a signed-in user.
77
+ *
78
+ * `isMember` is injected rather than resolved here so this module stays free
79
+ * of the membership model, and so the test can exercise the refusal without a
80
+ * membership fixture. It receives the target host as well as the slug because
81
+ * the ORG ID lives on the claim and `resolveConsoleDomain` deliberately never
82
+ * returns it — a verdict is read by an unauthenticated route. It is called
83
+ * only after the domain has been shown to be live, so a refusal never reveals
84
+ * whether an org exists.
85
+ *
86
+ * The write flips `pending → authorized` inside {@link consumeOnce}, so a
87
+ * second authorize of the same id finds `not-pending`. That matters: without
88
+ * it, an attacker who obtained a request id could keep re-authorizing it and
89
+ * mint a fresh secret each time the victim signed in.
90
+ */
91
+ export declare function authorizeConsoleHandoff(options: {
92
+ requestId: string;
93
+ uid: string;
94
+ tenantId?: string | null;
95
+ impersonated?: boolean;
96
+ isMember: (context: {
97
+ targetHost: string;
98
+ orgSlug: string | null;
99
+ }) => Promise<boolean>;
100
+ nowMs?: number;
101
+ }): Promise<AuthorizeResult>;
102
+ export type RedeemRefusal = 'unknown-request' | 'already-redeemed' | 'not-authorized' | 'expired' | 'host-mismatch' | 'bad-secret' | 'bad-verifier' | 'domain-inactive' | 'revoked';
103
+ export type RedeemResult = {
104
+ ok: true;
105
+ uid: string;
106
+ tenantId: string | null;
107
+ continuePath: string;
108
+ } | {
109
+ ok: false;
110
+ reason: RedeemRefusal;
111
+ };
112
+ /**
113
+ * Consume an authorized handoff, on the CUSTOM DOMAIN.
114
+ *
115
+ * Both secrets are required and both are checked INSIDE the transaction, so
116
+ * the checks cannot be raced against the consume.
117
+ *
118
+ * `verifiers` is every value of the `__aglyn_handoff` cookie on the request,
119
+ * not one. A compromised sibling host under the customer's own apex can set a
120
+ * `Domain=.acme-agency.com` cookie of the same name, which SHADOWS the
121
+ * host-only one in the `Cookie` header with no way to tell them apart — the
122
+ * precise failure AGL-1259 hit with duplicate `__session` cookies. Hashing
123
+ * every value and accepting if any matches is safe by construction (only the
124
+ * real `V` hashes to `verifierHash`) and turns a hijack attempt into a no-op
125
+ * rather than a denial of service.
126
+ */
127
+ export declare function redeemConsoleHandoff(options: {
128
+ requestId: string;
129
+ secret: string;
130
+ verifiers: string[];
131
+ requestHost: string;
132
+ nowMs?: number;
133
+ }): Promise<RedeemResult>;
134
+ /**
135
+ * `consoleDomains/{host}.sessionEpoch`, or `0` when there is no usable one.
136
+ *
137
+ * `0` is returned for every unreadable shape — an unnormalizable host, an
138
+ * absent document, a Firestore error, a non-finite or non-positive value — and
139
+ * every caller reads `0` as "this control has nothing to say", i.e. FAILS OPEN.
140
+ * That is deliberate and matches every other console-domain lookup: the Vercel
141
+ * allowlist is the boundary and this is defence in depth, so a Firestore blip
142
+ * must not lock every custom domain out of its own session.
143
+ *
144
+ * Note the split with {@link consoleSessionEpochRefuses}: failing open on a
145
+ * missing EPOCH is not the same as failing open on a missing CREDENTIAL DATE.
146
+ * A cookie whose `iat` cannot be read is refused, because an undateable
147
+ * credential cannot be shown to postdate a revocation.
148
+ */
149
+ export declare function readConsoleSessionEpoch(host: string): Promise<number>;
150
+ /**
151
+ * The revocation epoch on a console domain (AGL-1353 D6).
152
+ *
153
+ * A session cookie minted before the epoch is dead. Bumping `sessionEpoch` —
154
+ * which `suspendOnDowngrade` and `releaseConsoleDomain` already do — therefore
155
+ * invalidates every outstanding cookie for that host instantly, at our
156
+ * boundary. Our boundary is the ONLY place a Firebase session cookie has any
157
+ * value: it is opaque and can only be cashed by `verifySessionCookie` under
158
+ * our Admin credentials.
159
+ *
160
+ * Fails OPEN on a read error, matching every other console-domain lookup.
161
+ */
162
+ export declare function consoleSessionEpochRefuses(options: {
163
+ host: string;
164
+ cookieIssuedAtMs: number | null;
165
+ }): Promise<boolean>;