@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,127 @@
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
+ * Server half of asset quarantine (AGL-1512): the deny-list read that
19
+ * `serveMediaCdn` consults before any byte leaves Storage. The pure shape,
20
+ * key derivation and owner copy live in `@aglyn/aglyn`
21
+ * `app-utils/media-quarantine.ts`; this module adds the Admin-SDK read and
22
+ * the two properties only the server can hold.
23
+ *
24
+ * **One read for the whole feature.** The entire deny list is a single
25
+ * document, TTL-cached in-process, so a request for any asset — one, or the
26
+ * fifty tiles of a DAM grid, or every image on a published page — costs at
27
+ * most ONE Firestore read per process per {@link MEDIA_QUARANTINE_TTL_MS}.
28
+ * The alternative (a document per quarantined hash) would have made the
29
+ * cost scale with distinct assets requested, on the hottest unauthenticated
30
+ * path in the product. Same discipline as AGL-1302, same TTL as the
31
+ * lockdown verdict so both levers converge on the same clock.
32
+ *
33
+ * **Fail OPEN on infrastructure error**, matching the lockdown core: an
34
+ * unreachable Firestore is an outage, not a takedown, and a Firestore blip
35
+ * must not blank every customer image on every site. The residual risk is
36
+ * stated rather than traded away — during a Firestore outage a quarantined
37
+ * asset can serve. That is the same posture the platform already takes for
38
+ * a platform-wide security lockdown, and inverting it here would mean a
39
+ * database outage becomes a total media outage.
40
+ */
41
+ import { type MediaQuarantineState } from '@aglyn/aglyn/server';
42
+ /**
43
+ * Deny-list TTL. Deliberately the lockdown verdict's 15s: this is a
44
+ * takedown path, and 15s is the worst-case lag between staff pressing the
45
+ * button and a warm process refusing its next request for the asset. It is
46
+ * also the lag on a LIFT, which matters just as much — reversibility is the
47
+ * entire reason quarantine exists instead of deletion.
48
+ */
49
+ export declare const MEDIA_QUARANTINE_TTL_MS = 15000;
50
+ /**
51
+ * Drop the in-process deny list. The admin route calls it after a write so
52
+ * the process that took the action enforces immediately; every other
53
+ * process converges within the TTL. Tests need it between cases.
54
+ *
55
+ * Note what this canNOT do in production: the quarantine is written by the
56
+ * console app and enforced by the tenant app, different processes an
57
+ * in-process invalidation can never reach. The TTL is the real mechanism.
58
+ */
59
+ export declare function invalidateMediaQuarantineCache(): void;
60
+ /**
61
+ * Is this asset quarantined right now? Returns the state so a caller that
62
+ * wants to explain itself can (the console does); the CDN deliberately
63
+ * only uses the boolean, because its refusal must stay neutral.
64
+ *
65
+ * EVERY key is checked, and any one refusing is enough — see
66
+ * {@link mediaQuarantineKeys} for why none of them may be dropped. In
67
+ * short: the strong AGL-1614 `contentSha256`, the legacy truncated
68
+ * `contentHash`, and the per-asset fallback are all keys a quarantine in
69
+ * force may have been written under, and a takedown that stops biting
70
+ * because the document gained a better field would be the worst outcome
71
+ * this lever can produce. Checking all of them costs nothing: the deny list
72
+ * is already in memory.
73
+ */
74
+ export declare function getMediaQuarantine(asset: {
75
+ contentSha256?: string | null;
76
+ contentHash?: string | null;
77
+ scopeSegment?: string | null;
78
+ mediaId?: string | null;
79
+ }): Promise<MediaQuarantineState | null>;
80
+ /**
81
+ * Is ANYTHING quarantined right now? (AGL-1612.)
82
+ *
83
+ * The cheap pre-check that makes an owner-facing surface affordable. A
84
+ * console surface asking "are any of these fifty assets disabled?" cannot
85
+ * take the client's word for the digests — that would turn the staff-only
86
+ * deny list into an oracle anyone could probe by guessing hashes — so it has
87
+ * to derive the keys from the media documents server-side, which is a read
88
+ * per asset.
89
+ *
90
+ * That cost is only ever worth paying when the deny list has something in
91
+ * it, and on this platform it is empty essentially always. So the caller
92
+ * asks this first: one already-cached read, `false`, and zero media reads on
93
+ * the overwhelmingly common path.
94
+ */
95
+ export declare function hasMediaQuarantines(): Promise<boolean>;
96
+ /**
97
+ * INGESTION refusal (AGL-1613) — the half AGL-1512 did not land.
98
+ *
99
+ * AGL-1512 keys quarantine by content digest precisely so that re-uploading
100
+ * the same bytes stays quarantined, and then kept that promise at DELIVERY
101
+ * only: `serveMediaCdn` refused the re-uploaded copy while the upload itself
102
+ * succeeded. The practical outcome for a customer whose malicious PDF was
103
+ * disabled was that they could re-upload it, it landed in their DAM, it was
104
+ * BILLED to their storage quota, and it 410'd for every visitor with nothing
105
+ * anywhere to say why. The control technically held and the product lied
106
+ * about it.
107
+ *
108
+ * So the chokepoints consult the same deny list before the write. It is the
109
+ * same one-document, TTL-cached read as the CDN's — a warm process pays
110
+ * nothing for this, and even a cold one pays one Firestore read on a path
111
+ * that is already doing several.
112
+ *
113
+ * **Returns a refusal, and leaves the cleanup to the caller.** A `Response`
114
+ * cannot delete an orphaned Storage object, and exactly one of the three
115
+ * chokepoints has one to delete (signed-upload finalize, where the bytes are
116
+ * in the bucket before anything of ours can look at them). Every caller must
117
+ * return this BEFORE it saves bytes, writes the media document, or moves
118
+ * `counters/media` — that counter is a billing input, and a refused upload
119
+ * that still incremented it would bill a customer for a file the platform
120
+ * refuses to keep.
121
+ */
122
+ export declare function quarantinedUploadRefusal(asset: {
123
+ contentSha256?: string | null;
124
+ contentHash?: string | null;
125
+ scopeSegment?: string | null;
126
+ mediaId?: string | null;
127
+ }): Promise<Response | null>;
@@ -0,0 +1,170 @@
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
+ * Server half of asset quarantine (AGL-1512): the deny-list read that
18
+ * `serveMediaCdn` consults before any byte leaves Storage. The pure shape,
19
+ * key derivation and owner copy live in `@aglyn/aglyn`
20
+ * `app-utils/media-quarantine.ts`; this module adds the Admin-SDK read and
21
+ * the two properties only the server can hold.
22
+ *
23
+ * **One read for the whole feature.** The entire deny list is a single
24
+ * document, TTL-cached in-process, so a request for any asset — one, or the
25
+ * fifty tiles of a DAM grid, or every image on a published page — costs at
26
+ * most ONE Firestore read per process per {@link MEDIA_QUARANTINE_TTL_MS}.
27
+ * The alternative (a document per quarantined hash) would have made the
28
+ * cost scale with distinct assets requested, on the hottest unauthenticated
29
+ * path in the product. Same discipline as AGL-1302, same TTL as the
30
+ * lockdown verdict so both levers converge on the same clock.
31
+ *
32
+ * **Fail OPEN on infrastructure error**, matching the lockdown core: an
33
+ * unreachable Firestore is an outage, not a takedown, and a Firestore blip
34
+ * must not blank every customer image on every site. The residual risk is
35
+ * stated rather than traded away — during a Firestore outage a quarantined
36
+ * asset can serve. That is the same posture the platform already takes for
37
+ * a platform-wide security lockdown, and inverting it here would mean a
38
+ * database outage becomes a total media outage.
39
+ */ import { isMediaQuarantineActive, MEDIA_QUARANTINE_ENTRIES_FIELD, MEDIA_QUARANTINE_INDEX_DOC_ID, MEDIA_QUARANTINE_UPLOAD_STATUS, MEDIA_QUARANTINES_COLLECTION, mediaQuarantineKeys, mediaQuarantineRefusalBody, normalizeMediaQuarantine } from "@aglyn/aglyn/server";
40
+ import { firebaseAdmin } from "./firebase-admin.js";
41
+ /**
42
+ * Deny-list TTL. Deliberately the lockdown verdict's 15s: this is a
43
+ * takedown path, and 15s is the worst-case lag between staff pressing the
44
+ * button and a warm process refusing its next request for the asset. It is
45
+ * also the lag on a LIFT, which matters just as much — reversibility is the
46
+ * entire reason quarantine exists instead of deletion.
47
+ */ export const MEDIA_QUARANTINE_TTL_MS = 15000;
48
+ let indexCache;
49
+ let indexPending;
50
+ /**
51
+ * Drop the in-process deny list. The admin route calls it after a write so
52
+ * the process that took the action enforces immediately; every other
53
+ * process converges within the TTL. Tests need it between cases.
54
+ *
55
+ * Note what this canNOT do in production: the quarantine is written by the
56
+ * console app and enforced by the tenant app, different processes an
57
+ * in-process invalidation can never reach. The TTL is the real mechanism.
58
+ */ export function invalidateMediaQuarantineCache() {
59
+ indexCache = undefined;
60
+ indexPending = undefined;
61
+ }
62
+ /** The whole deny list, TTL-cached. `{}` on any error (fail open). */ async function readQuarantineIndex() {
63
+ if (indexCache && Date.now() - indexCache.at < MEDIA_QUARANTINE_TTL_MS) {
64
+ return indexCache.entries;
65
+ }
66
+ if (!indexPending) {
67
+ indexPending = (async ()=>{
68
+ let entries = {};
69
+ try {
70
+ const snapshot = await firebaseAdmin.app().firestore().collection(MEDIA_QUARANTINES_COLLECTION).doc(MEDIA_QUARANTINE_INDEX_DOC_ID).get();
71
+ const raw = snapshot.get(MEDIA_QUARANTINE_ENTRIES_FIELD);
72
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
73
+ entries = raw;
74
+ }
75
+ } catch (unused) {
76
+ // Fail open — see the module header. An outage is not a takedown.
77
+ }
78
+ indexCache = {
79
+ at: Date.now(),
80
+ entries
81
+ };
82
+ return entries;
83
+ })().finally(()=>{
84
+ indexPending = undefined;
85
+ });
86
+ }
87
+ return indexPending;
88
+ }
89
+ /**
90
+ * Is this asset quarantined right now? Returns the state so a caller that
91
+ * wants to explain itself can (the console does); the CDN deliberately
92
+ * only uses the boolean, because its refusal must stay neutral.
93
+ *
94
+ * EVERY key is checked, and any one refusing is enough — see
95
+ * {@link mediaQuarantineKeys} for why none of them may be dropped. In
96
+ * short: the strong AGL-1614 `contentSha256`, the legacy truncated
97
+ * `contentHash`, and the per-asset fallback are all keys a quarantine in
98
+ * force may have been written under, and a takedown that stops biting
99
+ * because the document gained a better field would be the worst outcome
100
+ * this lever can produce. Checking all of them costs nothing: the deny list
101
+ * is already in memory.
102
+ */ export async function getMediaQuarantine(asset) {
103
+ const keys = mediaQuarantineKeys(asset);
104
+ if (!keys.length) return null;
105
+ const entries = await readQuarantineIndex();
106
+ const nowMs = Date.now();
107
+ for (const key of keys){
108
+ const state = normalizeMediaQuarantine(entries[key], key);
109
+ if (isMediaQuarantineActive(state, nowMs)) return state;
110
+ }
111
+ return null;
112
+ }
113
+ /**
114
+ * Is ANYTHING quarantined right now? (AGL-1612.)
115
+ *
116
+ * The cheap pre-check that makes an owner-facing surface affordable. A
117
+ * console surface asking "are any of these fifty assets disabled?" cannot
118
+ * take the client's word for the digests — that would turn the staff-only
119
+ * deny list into an oracle anyone could probe by guessing hashes — so it has
120
+ * to derive the keys from the media documents server-side, which is a read
121
+ * per asset.
122
+ *
123
+ * That cost is only ever worth paying when the deny list has something in
124
+ * it, and on this platform it is empty essentially always. So the caller
125
+ * asks this first: one already-cached read, `false`, and zero media reads on
126
+ * the overwhelmingly common path.
127
+ */ export async function hasMediaQuarantines() {
128
+ const entries = await readQuarantineIndex();
129
+ return Object.keys(entries).length > 0;
130
+ }
131
+ /**
132
+ * INGESTION refusal (AGL-1613) — the half AGL-1512 did not land.
133
+ *
134
+ * AGL-1512 keys quarantine by content digest precisely so that re-uploading
135
+ * the same bytes stays quarantined, and then kept that promise at DELIVERY
136
+ * only: `serveMediaCdn` refused the re-uploaded copy while the upload itself
137
+ * succeeded. The practical outcome for a customer whose malicious PDF was
138
+ * disabled was that they could re-upload it, it landed in their DAM, it was
139
+ * BILLED to their storage quota, and it 410'd for every visitor with nothing
140
+ * anywhere to say why. The control technically held and the product lied
141
+ * about it.
142
+ *
143
+ * So the chokepoints consult the same deny list before the write. It is the
144
+ * same one-document, TTL-cached read as the CDN's — a warm process pays
145
+ * nothing for this, and even a cold one pays one Firestore read on a path
146
+ * that is already doing several.
147
+ *
148
+ * **Returns a refusal, and leaves the cleanup to the caller.** A `Response`
149
+ * cannot delete an orphaned Storage object, and exactly one of the three
150
+ * chokepoints has one to delete (signed-upload finalize, where the bytes are
151
+ * in the bucket before anything of ours can look at them). Every caller must
152
+ * return this BEFORE it saves bytes, writes the media document, or moves
153
+ * `counters/media` — that counter is a billing input, and a refused upload
154
+ * that still incremented it would bill a customer for a file the platform
155
+ * refuses to keep.
156
+ */ export async function quarantinedUploadRefusal(asset) {
157
+ const state = await getMediaQuarantine(asset);
158
+ if (!state) return null;
159
+ return Response.json(mediaQuarantineRefusalBody(state), {
160
+ status: MEDIA_QUARANTINE_UPLOAD_STATUS,
161
+ // A takedown is reversible and a refusal is per-request; nothing between
162
+ // here and the browser should be able to answer the next attempt from a
163
+ // cached copy of this one. Same reasoning as the CDN's `no-store`.
164
+ headers: {
165
+ 'cache-control': 'no-store'
166
+ }
167
+ });
168
+ }
169
+
170
+ //# sourceMappingURL=media-quarantine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/media-quarantine.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 * Server half of asset quarantine (AGL-1512): the deny-list read that\n * `serveMediaCdn` consults before any byte leaves Storage. The pure shape,\n * key derivation and owner copy live in `@aglyn/aglyn`\n * `app-utils/media-quarantine.ts`; this module adds the Admin-SDK read and\n * the two properties only the server can hold.\n *\n * **One read for the whole feature.** The entire deny list is a single\n * document, TTL-cached in-process, so a request for any asset — one, or the\n * fifty tiles of a DAM grid, or every image on a published page — costs at\n * most ONE Firestore read per process per {@link MEDIA_QUARANTINE_TTL_MS}.\n * The alternative (a document per quarantined hash) would have made the\n * cost scale with distinct assets requested, on the hottest unauthenticated\n * path in the product. Same discipline as AGL-1302, same TTL as the\n * lockdown verdict so both levers converge on the same clock.\n *\n * **Fail OPEN on infrastructure error**, matching the lockdown core: an\n * unreachable Firestore is an outage, not a takedown, and a Firestore blip\n * must not blank every customer image on every site. The residual risk is\n * stated rather than traded away — during a Firestore outage a quarantined\n * asset can serve. That is the same posture the platform already takes for\n * a platform-wide security lockdown, and inverting it here would mean a\n * database outage becomes a total media outage.\n */\n\nimport {\n isMediaQuarantineActive,\n MEDIA_QUARANTINE_ENTRIES_FIELD,\n MEDIA_QUARANTINE_INDEX_DOC_ID,\n MEDIA_QUARANTINE_UPLOAD_STATUS,\n MEDIA_QUARANTINES_COLLECTION,\n type MediaQuarantineEntry,\n type MediaQuarantineState,\n mediaQuarantineKeys,\n mediaQuarantineRefusalBody,\n normalizeMediaQuarantine,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from './firebase-admin'\n\n/**\n * Deny-list TTL. Deliberately the lockdown verdict's 15s: this is a\n * takedown path, and 15s is the worst-case lag between staff pressing the\n * button and a warm process refusing its next request for the asset. It is\n * also the lag on a LIFT, which matters just as much — reversibility is the\n * entire reason quarantine exists instead of deletion.\n */\nexport const MEDIA_QUARANTINE_TTL_MS = 15_000\n\ntype QuarantineIndex = Record<string, Partial<MediaQuarantineEntry>>\n\nlet indexCache: { at: number; entries: QuarantineIndex } | undefined\nlet indexPending: Promise<QuarantineIndex> | undefined\n\n/**\n * Drop the in-process deny list. The admin route calls it after a write so\n * the process that took the action enforces immediately; every other\n * process converges within the TTL. Tests need it between cases.\n *\n * Note what this canNOT do in production: the quarantine is written by the\n * console app and enforced by the tenant app, different processes an\n * in-process invalidation can never reach. The TTL is the real mechanism.\n */\nexport function invalidateMediaQuarantineCache(): void {\n indexCache = undefined\n indexPending = undefined\n}\n\n/** The whole deny list, TTL-cached. `{}` on any error (fail open). */\nasync function readQuarantineIndex(): Promise<QuarantineIndex> {\n if (indexCache && Date.now() - indexCache.at < MEDIA_QUARANTINE_TTL_MS) {\n return indexCache.entries\n }\n if (!indexPending) {\n indexPending = (async () => {\n let entries: QuarantineIndex = {}\n try {\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection(MEDIA_QUARANTINES_COLLECTION)\n .doc(MEDIA_QUARANTINE_INDEX_DOC_ID)\n .get()\n const raw = snapshot.get(MEDIA_QUARANTINE_ENTRIES_FIELD)\n if (raw && typeof raw === 'object' && !Array.isArray(raw)) {\n entries = raw as QuarantineIndex\n }\n } catch {\n // Fail open — see the module header. An outage is not a takedown.\n }\n indexCache = { at: Date.now(), entries }\n return entries\n })().finally(() => {\n indexPending = undefined\n })\n }\n return indexPending\n}\n\n/**\n * Is this asset quarantined right now? Returns the state so a caller that\n * wants to explain itself can (the console does); the CDN deliberately\n * only uses the boolean, because its refusal must stay neutral.\n *\n * EVERY key is checked, and any one refusing is enough — see\n * {@link mediaQuarantineKeys} for why none of them may be dropped. In\n * short: the strong AGL-1614 `contentSha256`, the legacy truncated\n * `contentHash`, and the per-asset fallback are all keys a quarantine in\n * force may have been written under, and a takedown that stops biting\n * because the document gained a better field would be the worst outcome\n * this lever can produce. Checking all of them costs nothing: the deny list\n * is already in memory.\n */\nexport async function getMediaQuarantine(asset: {\n contentSha256?: string | null\n contentHash?: string | null\n scopeSegment?: string | null\n mediaId?: string | null\n}): Promise<MediaQuarantineState | null> {\n const keys = mediaQuarantineKeys(asset)\n if (!keys.length) return null\n const entries = await readQuarantineIndex()\n const nowMs = Date.now()\n for (const key of keys) {\n const state = normalizeMediaQuarantine(entries[key], key)\n if (isMediaQuarantineActive(state, nowMs)) return state\n }\n return null\n}\n\n/**\n * Is ANYTHING quarantined right now? (AGL-1612.)\n *\n * The cheap pre-check that makes an owner-facing surface affordable. A\n * console surface asking \"are any of these fifty assets disabled?\" cannot\n * take the client's word for the digests — that would turn the staff-only\n * deny list into an oracle anyone could probe by guessing hashes — so it has\n * to derive the keys from the media documents server-side, which is a read\n * per asset.\n *\n * That cost is only ever worth paying when the deny list has something in\n * it, and on this platform it is empty essentially always. So the caller\n * asks this first: one already-cached read, `false`, and zero media reads on\n * the overwhelmingly common path.\n */\nexport async function hasMediaQuarantines(): Promise<boolean> {\n const entries = await readQuarantineIndex()\n return Object.keys(entries).length > 0\n}\n\n/**\n * INGESTION refusal (AGL-1613) — the half AGL-1512 did not land.\n *\n * AGL-1512 keys quarantine by content digest precisely so that re-uploading\n * the same bytes stays quarantined, and then kept that promise at DELIVERY\n * only: `serveMediaCdn` refused the re-uploaded copy while the upload itself\n * succeeded. The practical outcome for a customer whose malicious PDF was\n * disabled was that they could re-upload it, it landed in their DAM, it was\n * BILLED to their storage quota, and it 410'd for every visitor with nothing\n * anywhere to say why. The control technically held and the product lied\n * about it.\n *\n * So the chokepoints consult the same deny list before the write. It is the\n * same one-document, TTL-cached read as the CDN's — a warm process pays\n * nothing for this, and even a cold one pays one Firestore read on a path\n * that is already doing several.\n *\n * **Returns a refusal, and leaves the cleanup to the caller.** A `Response`\n * cannot delete an orphaned Storage object, and exactly one of the three\n * chokepoints has one to delete (signed-upload finalize, where the bytes are\n * in the bucket before anything of ours can look at them). Every caller must\n * return this BEFORE it saves bytes, writes the media document, or moves\n * `counters/media` — that counter is a billing input, and a refused upload\n * that still incremented it would bill a customer for a file the platform\n * refuses to keep.\n */\nexport async function quarantinedUploadRefusal(asset: {\n contentSha256?: string | null\n contentHash?: string | null\n scopeSegment?: string | null\n mediaId?: string | null\n}): Promise<Response | null> {\n const state = await getMediaQuarantine(asset)\n if (!state) return null\n return Response.json(mediaQuarantineRefusalBody(state), {\n status: MEDIA_QUARANTINE_UPLOAD_STATUS,\n // A takedown is reversible and a refusal is per-request; nothing between\n // here and the browser should be able to answer the next attempt from a\n // cached copy of this one. Same reasoning as the CDN's `no-store`.\n headers: { 'cache-control': 'no-store' },\n })\n}\n"],"names":["isMediaQuarantineActive","MEDIA_QUARANTINE_ENTRIES_FIELD","MEDIA_QUARANTINE_INDEX_DOC_ID","MEDIA_QUARANTINE_UPLOAD_STATUS","MEDIA_QUARANTINES_COLLECTION","mediaQuarantineKeys","mediaQuarantineRefusalBody","normalizeMediaQuarantine","firebaseAdmin","MEDIA_QUARANTINE_TTL_MS","indexCache","indexPending","invalidateMediaQuarantineCache","undefined","readQuarantineIndex","Date","now","at","entries","snapshot","app","firestore","collection","doc","get","raw","Array","isArray","finally","getMediaQuarantine","asset","keys","length","nowMs","key","state","hasMediaQuarantines","Object","quarantinedUploadRefusal","Response","json","status","headers"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GAED,SACEA,uBAAuB,EACvBC,8BAA8B,EAC9BC,6BAA6B,EAC7BC,8BAA8B,EAC9BC,4BAA4B,EAG5BC,mBAAmB,EACnBC,0BAA0B,EAC1BC,wBAAwB,QACnB,sBAAqB;AAC5B,SAASC,aAAa,QAAQ,sBAAkB;AAEhD;;;;;;CAMC,GACD,OAAO,MAAMC,0BAA0B,MAAM;AAI7C,IAAIC;AACJ,IAAIC;AAEJ;;;;;;;;CAQC,GACD,OAAO,SAASC;IACdF,aAAaG;IACbF,eAAeE;AACjB;AAEA,oEAAoE,GACpE,eAAeC;IACb,IAAIJ,cAAcK,KAAKC,GAAG,KAAKN,WAAWO,EAAE,GAAGR,yBAAyB;QACtE,OAAOC,WAAWQ,OAAO;IAC3B;IACA,IAAI,CAACP,cAAc;QACjBA,eAAe,AAAC,CAAA;YACd,IAAIO,UAA2B,CAAC;YAChC,IAAI;gBACF,MAAMC,WAAW,MAAMX,cACpBY,GAAG,GACHC,SAAS,GACTC,UAAU,CAAClB,8BACXmB,GAAG,CAACrB,+BACJsB,GAAG;gBACN,MAAMC,MAAMN,SAASK,GAAG,CAACvB;gBACzB,IAAIwB,OAAO,OAAOA,QAAQ,YAAY,CAACC,MAAMC,OAAO,CAACF,MAAM;oBACzDP,UAAUO;gBACZ;YACF,EAAE,eAAM;YACN,kEAAkE;YACpE;YACAf,aAAa;gBAAEO,IAAIF,KAAKC,GAAG;gBAAIE;YAAQ;YACvC,OAAOA;QACT,CAAA,IAAKU,OAAO,CAAC;YACXjB,eAAeE;QACjB;IACF;IACA,OAAOF;AACT;AAEA;;;;;;;;;;;;;CAaC,GACD,OAAO,eAAekB,mBAAmBC,KAKxC;IACC,MAAMC,OAAO1B,oBAAoByB;IACjC,IAAI,CAACC,KAAKC,MAAM,EAAE,OAAO;IACzB,MAAMd,UAAU,MAAMJ;IACtB,MAAMmB,QAAQlB,KAAKC,GAAG;IACtB,KAAK,MAAMkB,OAAOH,KAAM;QACtB,MAAMI,QAAQ5B,yBAAyBW,OAAO,CAACgB,IAAI,EAAEA;QACrD,IAAIlC,wBAAwBmC,OAAOF,QAAQ,OAAOE;IACpD;IACA,OAAO;AACT;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,eAAeC;IACpB,MAAMlB,UAAU,MAAMJ;IACtB,OAAOuB,OAAON,IAAI,CAACb,SAASc,MAAM,GAAG;AACvC;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;CAyBC,GACD,OAAO,eAAeM,yBAAyBR,KAK9C;IACC,MAAMK,QAAQ,MAAMN,mBAAmBC;IACvC,IAAI,CAACK,OAAO,OAAO;IACnB,OAAOI,SAASC,IAAI,CAAClC,2BAA2B6B,QAAQ;QACtDM,QAAQtC;QACR,yEAAyE;QACzE,wEAAwE;QACxE,mEAAmE;QACnEuC,SAAS;YAAE,iBAAiB;QAAW;IACzC;AACF"}
@@ -0,0 +1,126 @@
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
+ * Signed access to a PRIVATE media asset (AGL-1051).
19
+ *
20
+ * The rest of this project controls *discovery and use* — `visibleTo` says
21
+ * which sites may reference an asset. It cannot control secrecy, because
22
+ * the CDN route is unauthenticated by necessity (it serves images to
23
+ * anonymous visitors) and a URL, once shared, is a bearer capability with
24
+ * no revocation. An asset marked `private` opts out of that model: it gets
25
+ * no `cdnPath`, and the only way to fetch it is a signature that expires.
26
+ *
27
+ * Signed with the shared, fail-closed `TOKEN_SIGNING_SECRET`
28
+ * (docs/COMMERCE_TOKEN_SIGNING.md) rather than a new secret — it is already
29
+ * provisioned as one team-level variable on both the console and tenant
30
+ * projects, and a second secret would be a second thing to keep in step
31
+ * across two apps, which is the failure mode that doc exists to describe.
32
+ * The `media:` prefix namespaces this payload so a token minted here can
33
+ * never be replayed as a download or stream token.
34
+ */
35
+ export declare function tokenSigningSecret(): string;
36
+ /**
37
+ * Fifteen minutes, matching the gated-video stream token.
38
+ *
39
+ * The TTL is the only revocation this scheme has, so it is deliberately
40
+ * short: long enough to open a link and download the file, too short for a
41
+ * URL pasted into a chat to stay useful. Console previews re-mint per view.
42
+ */
43
+ export declare const MEDIA_SIGNATURE_TTL_MS: number;
44
+ /**
45
+ * How long one viewing session of a purchased, gated video stays playable
46
+ * (AGL-2814): four hours.
47
+ *
48
+ * The console's fifteen minutes is the wrong number for playback, because a
49
+ * video is not fetched once. Every seek, every resume after a pause and every
50
+ * buffer refill is another byte-range request under the same URL, so the
51
+ * signature has to outlive the sitting rather than the first request. Four
52
+ * hours covers a feature-length recording plus the breaks people take inside
53
+ * one sitting.
54
+ *
55
+ * It stays hours, not days, because the URL is a bearer capability. Anyone who
56
+ * copies it out of a buyer's network panel holds the whole film until it
57
+ * expires, and past four hours a pause is long enough that asking the stream
58
+ * endpoint again is the right answer anyway: that endpoint re-checks the
59
+ * entitlement before it signs another session, and the gated player asks it
60
+ * again on its own when a request under an expired URL fails.
61
+ */
62
+ export declare const GATED_VIDEO_SESSION_TTL_MS: number;
63
+ /**
64
+ * How long a paid download's link works (AGL-2847): one hour.
65
+ *
66
+ * A download is one transfer rather than a sitting, so it needs far less than
67
+ * a viewing session. It gets more than the console's fifteen minutes because a
68
+ * large file on a slow connection can take longer than that to arrive, and a
69
+ * download manager that resumes a broken transfer asks again under the same
70
+ * URL. A refused resume sends the buyer back to the receipt link, which counts
71
+ * another attempt against the download limit, so the window is sized to make
72
+ * that rare while a forwarded link still dies the same evening.
73
+ */
74
+ export declare const PAID_DOWNLOAD_LINK_TTL_MS: number;
75
+ /**
76
+ * The longest lifetime any signature may claim, enforced by the VERIFIER as
77
+ * well as the minter.
78
+ *
79
+ * `exp` is inside the signed payload, so nobody without the secret can stretch
80
+ * it. What this bounds is the platform's own mistakes: a minter that one day
81
+ * passes a day where it meant an hour would otherwise hand out links nothing
82
+ * downstream ever questions.
83
+ */
84
+ export declare const MEDIA_SIGNATURE_MAX_TTL_MS: number;
85
+ /**
86
+ * The signature over one asset at one expiry.
87
+ *
88
+ * `scope` is inside the payload, not just alongside it. Without it a
89
+ * signature minted for `org:{orgId}` would verify against
90
+ * `hosts/{hostId}` — different documents, same id space — so a caller
91
+ * authorized for one library could read the other's asset of the same id.
92
+ */
93
+ export declare function signMediaAccess(scope: string, mediaId: string, expiresAtMs: number): string;
94
+ export interface MediaSignature {
95
+ exp: number;
96
+ sig: string;
97
+ }
98
+ /**
99
+ * Whether a presented signature authorizes this asset right now.
100
+ *
101
+ * Compared with `safeEqual`, which is constant-time on equal-length inputs and
102
+ * length-checks first because `timingSafeEqual` THROWS on a length mismatch —
103
+ * and an attacker controls that length, so an exception here would be a 500
104
+ * where a 403 belongs. That dance was hand-written in six files before
105
+ * AGL-1902 lifted it into one.
106
+ */
107
+ export declare function verifyMediaAccess(scope: string, mediaId: string, presented: Partial<MediaSignature> | undefined, nowMs?: number): boolean;
108
+ /** The query string that carries a signature on a CDN URL. */
109
+ export declare function mediaSignatureQuery(signature: MediaSignature): string;
110
+ /**
111
+ * Throws unless `ttlMs` is a lifetime the platform issues: positive, and no
112
+ * longer than {@link MEDIA_SIGNATURE_MAX_TTL_MS}.
113
+ *
114
+ * It THROWS rather than clamping. The verifier refuses a longer link, so
115
+ * minting one would hand out a URL that is dead on arrival, and a quiet clamp
116
+ * would hide the caller asking for the wrong thing. Exported for the minters
117
+ * that sign something other than a CDN URL, such as a Storage read, so every
118
+ * expiring link the platform hands out obeys the same bound.
119
+ */
120
+ export declare function assertMediaSignatureTtl(ttlMs: number): void;
121
+ /**
122
+ * Mints a signature valid for `ttlMs` from now — {@link MEDIA_SIGNATURE_TTL_MS}
123
+ * unless the caller names a longer window, bounded by
124
+ * {@link assertMediaSignatureTtl}.
125
+ */
126
+ export declare function mintMediaSignature(scope: string, mediaId: string, nowMs?: number, ttlMs?: number): MediaSignature;
@@ -0,0 +1,159 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { createHmac } from "crypto";
17
+ import { safeEqual } from "./safe-equal.js";
18
+ /**
19
+ * Signed access to a PRIVATE media asset (AGL-1051).
20
+ *
21
+ * The rest of this project controls *discovery and use* — `visibleTo` says
22
+ * which sites may reference an asset. It cannot control secrecy, because
23
+ * the CDN route is unauthenticated by necessity (it serves images to
24
+ * anonymous visitors) and a URL, once shared, is a bearer capability with
25
+ * no revocation. An asset marked `private` opts out of that model: it gets
26
+ * no `cdnPath`, and the only way to fetch it is a signature that expires.
27
+ *
28
+ * Signed with the shared, fail-closed `TOKEN_SIGNING_SECRET`
29
+ * (docs/COMMERCE_TOKEN_SIGNING.md) rather than a new secret — it is already
30
+ * provisioned as one team-level variable on both the console and tenant
31
+ * projects, and a second secret would be a second thing to keep in step
32
+ * across two apps, which is the failure mode that doc exists to describe.
33
+ * The `media:` prefix namespaces this payload so a token minted here can
34
+ * never be replayed as a download or stream token.
35
+ */ export function tokenSigningSecret() {
36
+ const secret = process.env['TOKEN_SIGNING_SECRET'];
37
+ // No fallback, deliberately. AGL-509 was `STRIPE_SECRET_KEY ?? 'aglyn'`:
38
+ // every payload is built from public identifiers, so a default key makes
39
+ // signatures forgeable on any deploy missing the real one.
40
+ if (!secret) throw new Error('TOKEN_SIGNING_SECRET is not configured');
41
+ return secret;
42
+ }
43
+ /**
44
+ * Fifteen minutes, matching the gated-video stream token.
45
+ *
46
+ * The TTL is the only revocation this scheme has, so it is deliberately
47
+ * short: long enough to open a link and download the file, too short for a
48
+ * URL pasted into a chat to stay useful. Console previews re-mint per view.
49
+ */ export const MEDIA_SIGNATURE_TTL_MS = 15 * 60 * 1000;
50
+ /**
51
+ * How long one viewing session of a purchased, gated video stays playable
52
+ * (AGL-2814): four hours.
53
+ *
54
+ * The console's fifteen minutes is the wrong number for playback, because a
55
+ * video is not fetched once. Every seek, every resume after a pause and every
56
+ * buffer refill is another byte-range request under the same URL, so the
57
+ * signature has to outlive the sitting rather than the first request. Four
58
+ * hours covers a feature-length recording plus the breaks people take inside
59
+ * one sitting.
60
+ *
61
+ * It stays hours, not days, because the URL is a bearer capability. Anyone who
62
+ * copies it out of a buyer's network panel holds the whole film until it
63
+ * expires, and past four hours a pause is long enough that asking the stream
64
+ * endpoint again is the right answer anyway: that endpoint re-checks the
65
+ * entitlement before it signs another session, and the gated player asks it
66
+ * again on its own when a request under an expired URL fails.
67
+ */ export const GATED_VIDEO_SESSION_TTL_MS = 4 * 60 * 60 * 1000;
68
+ /**
69
+ * How long a paid download's link works (AGL-2847): one hour.
70
+ *
71
+ * A download is one transfer rather than a sitting, so it needs far less than
72
+ * a viewing session. It gets more than the console's fifteen minutes because a
73
+ * large file on a slow connection can take longer than that to arrive, and a
74
+ * download manager that resumes a broken transfer asks again under the same
75
+ * URL. A refused resume sends the buyer back to the receipt link, which counts
76
+ * another attempt against the download limit, so the window is sized to make
77
+ * that rare while a forwarded link still dies the same evening.
78
+ */ export const PAID_DOWNLOAD_LINK_TTL_MS = 60 * 60 * 1000;
79
+ /**
80
+ * The longest lifetime any signature may claim, enforced by the VERIFIER as
81
+ * well as the minter.
82
+ *
83
+ * `exp` is inside the signed payload, so nobody without the secret can stretch
84
+ * it. What this bounds is the platform's own mistakes: a minter that one day
85
+ * passes a day where it meant an hour would otherwise hand out links nothing
86
+ * downstream ever questions.
87
+ */ export const MEDIA_SIGNATURE_MAX_TTL_MS = GATED_VIDEO_SESSION_TTL_MS;
88
+ /**
89
+ * Allowance for the minting and verifying instances disagreeing about the
90
+ * time. The console signs and the tenant app verifies, on different machines.
91
+ */ const MEDIA_SIGNATURE_CLOCK_SKEW_MS = 60 * 1000;
92
+ /**
93
+ * The signature over one asset at one expiry.
94
+ *
95
+ * `scope` is inside the payload, not just alongside it. Without it a
96
+ * signature minted for `org:{orgId}` would verify against
97
+ * `hosts/{hostId}` — different documents, same id space — so a caller
98
+ * authorized for one library could read the other's asset of the same id.
99
+ */ export function signMediaAccess(scope, mediaId, expiresAtMs) {
100
+ return createHmac('sha256', tokenSigningSecret()).update(`media:${scope}:${mediaId}:${expiresAtMs}`).digest('hex').slice(0, 32);
101
+ }
102
+ /**
103
+ * Whether a presented signature authorizes this asset right now.
104
+ *
105
+ * Compared with `safeEqual`, which is constant-time on equal-length inputs and
106
+ * length-checks first because `timingSafeEqual` THROWS on a length mismatch —
107
+ * and an attacker controls that length, so an exception here would be a 500
108
+ * where a 403 belongs. That dance was hand-written in six files before
109
+ * AGL-1902 lifted it into one.
110
+ */ export function verifyMediaAccess(scope, mediaId, presented, nowMs = Date.now()) {
111
+ var _ref, _ref1;
112
+ const exp = Number((_ref = presented == null ? void 0 : presented.exp) != null ? _ref : 0);
113
+ const sig = String((_ref1 = presented == null ? void 0 : presented.sig) != null ? _ref1 : '');
114
+ if (!Number.isFinite(exp) || exp <= nowMs) return false;
115
+ // Refused whatever the signature says: no minter issues a lifetime this
116
+ // long, so a link that claims one was never meant to exist.
117
+ if (exp - nowMs > MEDIA_SIGNATURE_MAX_TTL_MS + MEDIA_SIGNATURE_CLOCK_SKEW_MS) {
118
+ return false;
119
+ }
120
+ let expected;
121
+ try {
122
+ expected = signMediaAccess(scope, mediaId, exp);
123
+ } catch (unused) {
124
+ // Secret missing — fail closed rather than serving the bytes.
125
+ return false;
126
+ }
127
+ return safeEqual(sig, expected);
128
+ }
129
+ /** The query string that carries a signature on a CDN URL. */ export function mediaSignatureQuery(signature) {
130
+ return `exp=${signature.exp}&sig=${encodeURIComponent(signature.sig)}`;
131
+ }
132
+ /**
133
+ * Throws unless `ttlMs` is a lifetime the platform issues: positive, and no
134
+ * longer than {@link MEDIA_SIGNATURE_MAX_TTL_MS}.
135
+ *
136
+ * It THROWS rather than clamping. The verifier refuses a longer link, so
137
+ * minting one would hand out a URL that is dead on arrival, and a quiet clamp
138
+ * would hide the caller asking for the wrong thing. Exported for the minters
139
+ * that sign something other than a CDN URL, such as a Storage read, so every
140
+ * expiring link the platform hands out obeys the same bound.
141
+ */ export function assertMediaSignatureTtl(ttlMs) {
142
+ if (!(ttlMs > 0) || ttlMs > MEDIA_SIGNATURE_MAX_TTL_MS) {
143
+ throw new RangeError(`A media link lifetime must be between 1 ms and ${MEDIA_SIGNATURE_MAX_TTL_MS} ms`);
144
+ }
145
+ }
146
+ /**
147
+ * Mints a signature valid for `ttlMs` from now — {@link MEDIA_SIGNATURE_TTL_MS}
148
+ * unless the caller names a longer window, bounded by
149
+ * {@link assertMediaSignatureTtl}.
150
+ */ export function mintMediaSignature(scope, mediaId, nowMs = Date.now(), ttlMs = MEDIA_SIGNATURE_TTL_MS) {
151
+ assertMediaSignatureTtl(ttlMs);
152
+ const exp = nowMs + ttlMs;
153
+ return {
154
+ exp,
155
+ sig: signMediaAccess(scope, mediaId, exp)
156
+ };
157
+ }
158
+
159
+ //# sourceMappingURL=media-signing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/media-signing.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\nimport { createHmac } from 'crypto'\nimport { safeEqual } from './safe-equal'\n\n/**\n * Signed access to a PRIVATE media asset (AGL-1051).\n *\n * The rest of this project controls *discovery and use* — `visibleTo` says\n * which sites may reference an asset. It cannot control secrecy, because\n * the CDN route is unauthenticated by necessity (it serves images to\n * anonymous visitors) and a URL, once shared, is a bearer capability with\n * no revocation. An asset marked `private` opts out of that model: it gets\n * no `cdnPath`, and the only way to fetch it is a signature that expires.\n *\n * Signed with the shared, fail-closed `TOKEN_SIGNING_SECRET`\n * (docs/COMMERCE_TOKEN_SIGNING.md) rather than a new secret — it is already\n * provisioned as one team-level variable on both the console and tenant\n * projects, and a second secret would be a second thing to keep in step\n * across two apps, which is the failure mode that doc exists to describe.\n * The `media:` prefix namespaces this payload so a token minted here can\n * never be replayed as a download or stream token.\n */\nexport function tokenSigningSecret(): string {\n const secret = process.env['TOKEN_SIGNING_SECRET']\n // No fallback, deliberately. AGL-509 was `STRIPE_SECRET_KEY ?? 'aglyn'`:\n // every payload is built from public identifiers, so a default key makes\n // signatures forgeable on any deploy missing the real one.\n if (!secret) throw new Error('TOKEN_SIGNING_SECRET is not configured')\n return secret\n}\n\n/**\n * Fifteen minutes, matching the gated-video stream token.\n *\n * The TTL is the only revocation this scheme has, so it is deliberately\n * short: long enough to open a link and download the file, too short for a\n * URL pasted into a chat to stay useful. Console previews re-mint per view.\n */\nexport const MEDIA_SIGNATURE_TTL_MS = 15 * 60 * 1000\n\n/**\n * How long one viewing session of a purchased, gated video stays playable\n * (AGL-2814): four hours.\n *\n * The console's fifteen minutes is the wrong number for playback, because a\n * video is not fetched once. Every seek, every resume after a pause and every\n * buffer refill is another byte-range request under the same URL, so the\n * signature has to outlive the sitting rather than the first request. Four\n * hours covers a feature-length recording plus the breaks people take inside\n * one sitting.\n *\n * It stays hours, not days, because the URL is a bearer capability. Anyone who\n * copies it out of a buyer's network panel holds the whole film until it\n * expires, and past four hours a pause is long enough that asking the stream\n * endpoint again is the right answer anyway: that endpoint re-checks the\n * entitlement before it signs another session, and the gated player asks it\n * again on its own when a request under an expired URL fails.\n */\nexport const GATED_VIDEO_SESSION_TTL_MS = 4 * 60 * 60 * 1000\n\n/**\n * How long a paid download's link works (AGL-2847): one hour.\n *\n * A download is one transfer rather than a sitting, so it needs far less than\n * a viewing session. It gets more than the console's fifteen minutes because a\n * large file on a slow connection can take longer than that to arrive, and a\n * download manager that resumes a broken transfer asks again under the same\n * URL. A refused resume sends the buyer back to the receipt link, which counts\n * another attempt against the download limit, so the window is sized to make\n * that rare while a forwarded link still dies the same evening.\n */\nexport const PAID_DOWNLOAD_LINK_TTL_MS = 60 * 60 * 1000\n\n/**\n * The longest lifetime any signature may claim, enforced by the VERIFIER as\n * well as the minter.\n *\n * `exp` is inside the signed payload, so nobody without the secret can stretch\n * it. What this bounds is the platform's own mistakes: a minter that one day\n * passes a day where it meant an hour would otherwise hand out links nothing\n * downstream ever questions.\n */\nexport const MEDIA_SIGNATURE_MAX_TTL_MS = GATED_VIDEO_SESSION_TTL_MS\n\n/**\n * Allowance for the minting and verifying instances disagreeing about the\n * time. The console signs and the tenant app verifies, on different machines.\n */\nconst MEDIA_SIGNATURE_CLOCK_SKEW_MS = 60 * 1000\n\n/**\n * The signature over one asset at one expiry.\n *\n * `scope` is inside the payload, not just alongside it. Without it a\n * signature minted for `org:{orgId}` would verify against\n * `hosts/{hostId}` — different documents, same id space — so a caller\n * authorized for one library could read the other's asset of the same id.\n */\nexport function signMediaAccess(\n scope: string,\n mediaId: string,\n expiresAtMs: number,\n): string {\n return createHmac('sha256', tokenSigningSecret())\n .update(`media:${scope}:${mediaId}:${expiresAtMs}`)\n .digest('hex')\n .slice(0, 32)\n}\n\nexport interface MediaSignature {\n exp: number\n sig: string\n}\n\n/**\n * Whether a presented signature authorizes this asset right now.\n *\n * Compared with `safeEqual`, which is constant-time on equal-length inputs and\n * length-checks first because `timingSafeEqual` THROWS on a length mismatch —\n * and an attacker controls that length, so an exception here would be a 500\n * where a 403 belongs. That dance was hand-written in six files before\n * AGL-1902 lifted it into one.\n */\nexport function verifyMediaAccess(\n scope: string,\n mediaId: string,\n presented: Partial<MediaSignature> | undefined,\n nowMs: number = Date.now(),\n): boolean {\n const exp = Number(presented?.exp ?? 0)\n const sig = String(presented?.sig ?? '')\n if (!Number.isFinite(exp) || exp <= nowMs) return false\n // Refused whatever the signature says: no minter issues a lifetime this\n // long, so a link that claims one was never meant to exist.\n if (exp - nowMs > MEDIA_SIGNATURE_MAX_TTL_MS + MEDIA_SIGNATURE_CLOCK_SKEW_MS) {\n return false\n }\n let expected: string\n try {\n expected = signMediaAccess(scope, mediaId, exp)\n } catch {\n // Secret missing — fail closed rather than serving the bytes.\n return false\n }\n return safeEqual(sig, expected)\n}\n\n/** The query string that carries a signature on a CDN URL. */\nexport function mediaSignatureQuery(signature: MediaSignature): string {\n return `exp=${signature.exp}&sig=${encodeURIComponent(signature.sig)}`\n}\n\n/**\n * Throws unless `ttlMs` is a lifetime the platform issues: positive, and no\n * longer than {@link MEDIA_SIGNATURE_MAX_TTL_MS}.\n *\n * It THROWS rather than clamping. The verifier refuses a longer link, so\n * minting one would hand out a URL that is dead on arrival, and a quiet clamp\n * would hide the caller asking for the wrong thing. Exported for the minters\n * that sign something other than a CDN URL, such as a Storage read, so every\n * expiring link the platform hands out obeys the same bound.\n */\nexport function assertMediaSignatureTtl(ttlMs: number): void {\n if (!(ttlMs > 0) || ttlMs > MEDIA_SIGNATURE_MAX_TTL_MS) {\n throw new RangeError(\n `A media link lifetime must be between 1 ms and ${MEDIA_SIGNATURE_MAX_TTL_MS} ms`,\n )\n }\n}\n\n/**\n * Mints a signature valid for `ttlMs` from now — {@link MEDIA_SIGNATURE_TTL_MS}\n * unless the caller names a longer window, bounded by\n * {@link assertMediaSignatureTtl}.\n */\nexport function mintMediaSignature(\n scope: string,\n mediaId: string,\n nowMs: number = Date.now(),\n ttlMs: number = MEDIA_SIGNATURE_TTL_MS,\n): MediaSignature {\n assertMediaSignatureTtl(ttlMs)\n const exp = nowMs + ttlMs\n return { exp, sig: signMediaAccess(scope, mediaId, exp) }\n}\n"],"names":["createHmac","safeEqual","tokenSigningSecret","secret","process","env","Error","MEDIA_SIGNATURE_TTL_MS","GATED_VIDEO_SESSION_TTL_MS","PAID_DOWNLOAD_LINK_TTL_MS","MEDIA_SIGNATURE_MAX_TTL_MS","MEDIA_SIGNATURE_CLOCK_SKEW_MS","signMediaAccess","scope","mediaId","expiresAtMs","update","digest","slice","verifyMediaAccess","presented","nowMs","Date","now","exp","Number","sig","String","isFinite","expected","mediaSignatureQuery","signature","encodeURIComponent","assertMediaSignatureTtl","ttlMs","RangeError","mintMediaSignature"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,QAAQ,SAAQ;AACnC,SAASC,SAAS,QAAQ,kBAAc;AAExC;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASC;IACd,MAAMC,SAASC,QAAQC,GAAG,CAAC,uBAAuB;IAClD,yEAAyE;IACzE,yEAAyE;IACzE,2DAA2D;IAC3D,IAAI,CAACF,QAAQ,MAAM,IAAIG,MAAM;IAC7B,OAAOH;AACT;AAEA;;;;;;CAMC,GACD,OAAO,MAAMI,yBAAyB,KAAK,KAAK,KAAI;AAEpD;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,MAAMC,6BAA6B,IAAI,KAAK,KAAK,KAAI;AAE5D;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,4BAA4B,KAAK,KAAK,KAAI;AAEvD;;;;;;;;CAQC,GACD,OAAO,MAAMC,6BAA6BF,2BAA0B;AAEpE;;;CAGC,GACD,MAAMG,gCAAgC,KAAK;AAE3C;;;;;;;CAOC,GACD,OAAO,SAASC,gBACdC,KAAa,EACbC,OAAe,EACfC,WAAmB;IAEnB,OAAOf,WAAW,UAAUE,sBACzBc,MAAM,CAAC,CAAC,MAAM,EAAEH,MAAM,CAAC,EAAEC,QAAQ,CAAC,EAAEC,aAAa,EACjDE,MAAM,CAAC,OACPC,KAAK,CAAC,GAAG;AACd;AAOA;;;;;;;;CAQC,GACD,OAAO,SAASC,kBACdN,KAAa,EACbC,OAAe,EACfM,SAA8C,EAC9CC,QAAgBC,KAAKC,GAAG,EAAE;;IAE1B,MAAMC,MAAMC,eAAOL,6BAAAA,UAAWI,GAAG,mBAAI;IACrC,MAAME,MAAMC,gBAAOP,6BAAAA,UAAWM,GAAG,oBAAI;IACrC,IAAI,CAACD,OAAOG,QAAQ,CAACJ,QAAQA,OAAOH,OAAO,OAAO;IAClD,wEAAwE;IACxE,4DAA4D;IAC5D,IAAIG,MAAMH,QAAQX,6BAA6BC,+BAA+B;QAC5E,OAAO;IACT;IACA,IAAIkB;IACJ,IAAI;QACFA,WAAWjB,gBAAgBC,OAAOC,SAASU;IAC7C,EAAE,eAAM;QACN,8DAA8D;QAC9D,OAAO;IACT;IACA,OAAOvB,UAAUyB,KAAKG;AACxB;AAEA,4DAA4D,GAC5D,OAAO,SAASC,oBAAoBC,SAAyB;IAC3D,OAAO,CAAC,IAAI,EAAEA,UAAUP,GAAG,CAAC,KAAK,EAAEQ,mBAAmBD,UAAUL,GAAG,GAAG;AACxE;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASO,wBAAwBC,KAAa;IACnD,IAAI,CAAEA,CAAAA,QAAQ,CAAA,KAAMA,QAAQxB,4BAA4B;QACtD,MAAM,IAAIyB,WACR,CAAC,+CAA+C,EAAEzB,2BAA2B,GAAG,CAAC;IAErF;AACF;AAEA;;;;CAIC,GACD,OAAO,SAAS0B,mBACdvB,KAAa,EACbC,OAAe,EACfO,QAAgBC,KAAKC,GAAG,EAAE,EAC1BW,QAAgB3B,sBAAsB;IAEtC0B,wBAAwBC;IACxB,MAAMV,MAAMH,QAAQa;IACpB,OAAO;QAAEV;QAAKE,KAAKd,gBAAgBC,OAAOC,SAASU;IAAK;AAC1D"}