@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,558 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { sanitizeEventParams } from "@aglyn/aglyn/app-utils/analytics-events";
18
+ // One definition for a pair of strings that has to agree with a setting in the
19
+ // GA UI nothing here can typecheck against (AGL-1582/AGL-2064). Four surfaces
20
+ // stamp them now — the console, the tenant runtime, the docs site, and this
21
+ // one — and a second spelling would read as a different dimension in GA, so
22
+ // the filter would catch three of them and look like it was working.
23
+ import { INTERNAL_TRAFFIC_PARAM, INTERNAL_TRAFFIC_VALUE } from "@aglyn/aglyn/app-utils/internal-traffic";
24
+ /**
25
+ * Server-to-server GA4 delivery via the Measurement Protocol (AGL-1561) —
26
+ * `purchase`, and the one activation event no browser can witness.
27
+ *
28
+ * ## Why revenue is sent from the server and nothing else is
29
+ *
30
+ * The obvious alternative is a client-side `purchase` on the post-checkout
31
+ * return page. It is simpler, and it is wrong for money:
32
+ *
33
+ * - **The return page is not reached reliably.** Stripe redirects back after
34
+ * payment, and a customer who closes the tab, loses signal, or gets a bank
35
+ * 3DS interstitial that lands somewhere else has still paid us. Their
36
+ * revenue would simply never appear.
37
+ * - **Ad blockers drop it**, and they are over-represented in exactly the
38
+ * developer audience Aglyn sells to. Under-reported revenue is worse than
39
+ * no revenue reporting, because it looks like data.
40
+ * - **`?status=success` carries no amount and no session id** (verified: the
41
+ * hosted checkout return URL sets only `status`, and nothing in the app
42
+ * reads it), so a client event could not state what was actually charged.
43
+ * It would also re-fire on a refresh or a back-navigation.
44
+ * - The authoritative amount, currency and transaction id already exist,
45
+ * server-side, in the Stripe webhook — the same place the subscription
46
+ * mirror is written. Reporting revenue from anywhere else means reporting
47
+ * a second, weaker version of a number we already hold.
48
+ *
49
+ * `begin_checkout` stays client-side, because intent genuinely is a browser
50
+ * event and its loss to an ad blocker costs a funnel step, not a dollar.
51
+ *
52
+ * ## The second sender: `site_published` from a scheduled publish (AGL-1589)
53
+ *
54
+ * Everything above argues for keeping the server-side surface as small as
55
+ * possible, and {@link sendGa4SitePublished} is the one other event that has
56
+ * no browser to fire it: a SCHEDULED publish is applied by a cron beat (or by
57
+ * an ISR regeneration) at a moment when the author is asleep. Until AGL-1589
58
+ * that path could not make a page reachable at all, so it was not an
59
+ * activation and nothing was sent; now that it registers the routing entry, a
60
+ * scheduled FIRST publish is a route going live and `publishScreenRoute`'s
61
+ * client-side `site_published` — the headline activation metric — would miss
62
+ * it. It is sent HERE for the same reason revenue is: the alternative is not
63
+ * a weaker event, it is no event.
64
+ *
65
+ * The rest of the activation funnel stays client-side. This is not a general
66
+ * server-side analytics channel and should not become one.
67
+ *
68
+ * ## The `client_id` problem, stated plainly
69
+ *
70
+ * The Measurement Protocol REQUIRES a `client_id`, and a server has no way to
71
+ * know the browser's. Whatever is supplied decides whether the purchase joins
72
+ * the user's existing GA session — and therefore whether revenue can be
73
+ * attributed to the campaign that produced it, which is the entire point of
74
+ * the exercise.
75
+ *
76
+ * So the browser's real `client_id` is captured when checkout STARTS and
77
+ * carried on the Stripe object's metadata (`ga_client_id`), and read back
78
+ * here. When it is present, the purchase lands on the same GA user and
79
+ * session as the marketing click that produced it.
80
+ *
81
+ * When it is absent — an older checkout session, a customer whose consent or
82
+ * ad blocker meant gtag never ran, or a subscription renewal months later
83
+ * with no browser involved at all — {@link sendGa4Purchase} falls back to a
84
+ * synthesized id derived from the Stripe customer. That records the revenue
85
+ * correctly but attributes it to a synthetic, sessionless user: the money is
86
+ * right, the channel is unknown. This is a deliberate trade (revenue truth
87
+ * beats attribution completeness) and NOT a silent one — the fallback is
88
+ * counted in the returned result so it can be alarmed on if it ever becomes
89
+ * the common case.
90
+ *
91
+ * ## Failure posture
92
+ *
93
+ * Fire-and-forget, swallowing everything. The webhook route claims each
94
+ * Stripe event in a `stripeEvents` doc BEFORE running handlers and deletes
95
+ * the claim on any throw, so a thrown error here would un-claim the event and
96
+ * make Stripe redeliver it — turning a missing analytics hit into a repeated
97
+ * billing side effect. Analytics must never be able to do that.
98
+ */ const GA4_ENDPOINT = 'https://www.google-analytics.com/mp/collect';
99
+ const SEND_TIMEOUT_MS = 3000;
100
+ /**
101
+ * A stable, opaque stand-in for a browser client id.
102
+ *
103
+ * GA expects the `<random>.<timestamp>` shape. Determinism matters more than
104
+ * realism: the same `seed` must map to the same synthetic user every time, or
105
+ * one paying customer becomes a crowd of one-purchase strangers and ARPA is
106
+ * nonsense. Derived by a cheap non-cryptographic hash — this is a bucketing
107
+ * key, not a secret, and the seed it comes from never leaves this function.
108
+ *
109
+ * The seed is whatever identifies the actor the events belong to on the
110
+ * surface doing the sending: the Stripe customer for revenue, the host id for
111
+ * a scheduled publish (AGL-1589). Both are opaque to GA — only the hash is
112
+ * ever transmitted.
113
+ */ export function synthesizeClientId(seed) {
114
+ let hash = 0;
115
+ for(let i = 0; i < seed.length; i += 1){
116
+ hash = hash * 31 + seed.charCodeAt(i) >>> 0;
117
+ }
118
+ // Fixed second component: a real timestamp would make the id move between
119
+ // renewals, which is precisely what determinism is here to prevent.
120
+ return `${hash}.1000000000`;
121
+ }
122
+ /**
123
+ * Absent config is the normal state on self-hosted deployments and in
124
+ * development — not an error, and not worth a log line per event.
125
+ *
126
+ * ⚠️ It is NO LONGER the state of our own production deployments, and this
127
+ * comment saying otherwise is how that fact got lost twice (AGL-2327).
128
+ * `GA4_MEASUREMENT_ID` and `GA4_API_SECRET` are present on the production
129
+ * deployments of `aglyn-console`, `aglyn-tenant` AND `aglyn-docs` since
130
+ * **2026-08-17 12:15 UTC** — verified against `GET /v13/deployments/{id}`,
131
+ * which returns the env key list a running lambda actually has, with an older
132
+ * deployment as a negative control. `vercel env ls` and the project's own env
133
+ * list cannot answer this question; only the deployment can.
134
+ *
135
+ * So a server-side event missing from GA4 today is **not** explained by
136
+ * credentials, and must not be written off as "the transport is dead". A
137
+ * 2026-08-19 smoke pass reached exactly that conclusion off the stale version
138
+ * of this note, four days after the flip landed. Look at dimension
139
+ * registration (AGL-1637 item 3) and at how the beacon is SCHEDULED
140
+ * (AGL-2346/AGL-2327 — a bare `void` in a route handler never runs) instead.
141
+ *
142
+ * See docs/ANALYTICS.md §"Environment variables".
143
+ */ function ga4Credentials() {
144
+ const measurementId = process.env.GA4_MEASUREMENT_ID || '';
145
+ const apiSecret = process.env.GA4_API_SECRET || '';
146
+ if (!measurementId || !apiSecret) return null;
147
+ return {
148
+ measurementId,
149
+ apiSecret
150
+ };
151
+ }
152
+ /**
153
+ * POST one already-sanitized event. Never throws — see the module comment on
154
+ * failure posture; the callers run inside a Stripe webhook claim and inside a
155
+ * publish executor, and in both a throw would have a side effect far worse
156
+ * than a missing analytics hit.
157
+ */ async function postGa4Event(options) {
158
+ const { credentials, eventName, params, clientId, userId, log } = options;
159
+ // Only ever ADDED, never set to `false`. GA4's internal-traffic filter
160
+ // matches on the parameter being present with this value; a `traffic_type:
161
+ // 'external'` on every real hit would be a second dimension nobody filters
162
+ // on, and one bad `internal` computation away from erasing paying customers
163
+ // from every report — which a data filter cannot undo.
164
+ const stamped = options.internal ? _extends({}, params, {
165
+ [INTERNAL_TRAFFIC_PARAM]: INTERNAL_TRAFFIC_VALUE
166
+ }) : params;
167
+ try {
168
+ const response = await fetch(`${GA4_ENDPOINT}?measurement_id=${encodeURIComponent(credentials.measurementId)}&api_secret=${encodeURIComponent(credentials.apiSecret)}`, {
169
+ method: 'POST',
170
+ headers: {
171
+ 'Content-Type': 'application/json'
172
+ },
173
+ body: JSON.stringify(_extends({
174
+ client_id: clientId
175
+ }, userId ? {
176
+ user_id: userId
177
+ } : {}, {
178
+ // Keeps THIS path out of ads personalization, and it is the only
179
+ // thing that does. The property runs Google Signals and ads
180
+ // personalization ON in every region, so a server event sent without
181
+ // this flag would join the advertising audiences the browser tags
182
+ // build — carrying a `client_id` that may be synthesized from a
183
+ // Stripe customer id, which is a stronger identifier than any cookie
184
+ // the browser path has. A per-hit assertion rather than a property
185
+ // setting, so no dashboard change can quietly opt these events in.
186
+ non_personalized_ads: true,
187
+ events: [
188
+ {
189
+ name: eventName,
190
+ params: stamped
191
+ }
192
+ ]
193
+ })),
194
+ signal: AbortSignal.timeout(SEND_TIMEOUT_MS)
195
+ });
196
+ if (!response.ok) {
197
+ console.warn(JSON.stringify(_extends({}, log, {
198
+ status: response.status
199
+ })));
200
+ return {
201
+ sent: false,
202
+ reason: `http-${response.status}`
203
+ };
204
+ }
205
+ return {
206
+ sent: true
207
+ };
208
+ } catch (error) {
209
+ console.warn(JSON.stringify(_extends({}, log, {
210
+ error: error instanceof Error ? error.message : 'unknown'
211
+ })));
212
+ return {
213
+ sent: false,
214
+ reason: 'network'
215
+ };
216
+ }
217
+ }
218
+ /**
219
+ * Send one `purchase` to GA4. Never throws.
220
+ *
221
+ * Returns a result rather than void so the caller can log the fallback rate;
222
+ * nothing about the return value should change billing behaviour.
223
+ */ export async function sendGa4Purchase(input) {
224
+ const credentials = ga4Credentials();
225
+ if (!credentials) {
226
+ return {
227
+ sent: false,
228
+ synthesizedClientId: false,
229
+ reason: 'not-configured'
230
+ };
231
+ }
232
+ const clientId = input.clientId || (input.stripeCustomerId ? synthesizeClientId(input.stripeCustomerId) : '');
233
+ if (!clientId) {
234
+ return {
235
+ sent: false,
236
+ synthesizedClientId: false,
237
+ reason: 'no-client-id'
238
+ };
239
+ }
240
+ const synthesizedClientId = !input.clientId;
241
+ // The same sanitizer the browser path uses, applied again here. The inputs
242
+ // are Stripe-derived rather than hand-written, and a Stripe product name is
243
+ // one `metadata` edit away from carrying a customer's name.
244
+ const params = sanitizeEventParams(_extends({
245
+ transaction_id: input.transactionId,
246
+ currency: String(input.currency || 'USD').toUpperCase(),
247
+ value: input.value
248
+ }, input.billingInterval ? {
249
+ billing_interval: input.billingInterval
250
+ } : {}, {
251
+ items: input.items
252
+ }));
253
+ // Swallows everything — see the module comment: a throw here would un-claim
254
+ // the Stripe event and cause a redelivery.
255
+ const result = await postGa4Event({
256
+ credentials,
257
+ eventName: 'purchase',
258
+ params,
259
+ clientId,
260
+ userId: input.userId,
261
+ log: {
262
+ tag: 'AGL-1561:ga4-purchase',
263
+ transactionId: input.transactionId
264
+ },
265
+ internal: input.internal
266
+ });
267
+ return _extends({}, result, {
268
+ synthesizedClientId
269
+ });
270
+ }
271
+ /**
272
+ * Send one GA4 `refund` — revenue that came back (AGL-1850). Never throws.
273
+ *
274
+ * Both `purchase` senders were one-directional, so GA revenue could only
275
+ * ever go UP: a refunded invoice or marketplace sale stayed on the books
276
+ * forever, drifting GA away from Stripe by exactly the refund volume. GA4's
277
+ * native `refund` event is built for this — sent with the ORIGINAL
278
+ * purchase's `transaction_id`, GA nets the two against each other in every
279
+ * revenue report.
280
+ *
281
+ * `transaction_id` is therefore the one field that carries the whole
282
+ * mechanism: the invoice id for a subscription refund, the checkout session
283
+ * id for a marketplace one — whatever the matching `purchase` reported,
284
+ * never the refund's own charge or event id.
285
+ *
286
+ * `value` is what actually came back, in the SAME accounting the purchase
287
+ * used. For a subscription that is `amount_refunded` (the purchase reported
288
+ * `amount_paid`, all ours); for a marketplace sale it is the platform-net
289
+ * share (the purchase reported our net per AGL-1639, so refunding the gross
290
+ * would net MORE than the sale ever reported). The caller owns that
291
+ * arithmetic because the caller holds the ledger; this sender ships whatever
292
+ * number it is handed.
293
+ *
294
+ * Same claiming rules as `purchase` (the caller discriminates, this module
295
+ * transmits), same client-id capture-or-synthesize fallback, same
296
+ * fire-and-forget posture — a throw would un-claim the Stripe event.
297
+ */ export async function sendGa4Refund(input) {
298
+ const credentials = ga4Credentials();
299
+ if (!credentials) {
300
+ return {
301
+ sent: false,
302
+ synthesizedClientId: false,
303
+ reason: 'not-configured'
304
+ };
305
+ }
306
+ const clientId = input.clientId || (input.stripeCustomerId ? synthesizeClientId(input.stripeCustomerId) : '');
307
+ if (!clientId) {
308
+ return {
309
+ sent: false,
310
+ synthesizedClientId: false,
311
+ reason: 'no-client-id'
312
+ };
313
+ }
314
+ const synthesizedClientId = !input.clientId;
315
+ const params = sanitizeEventParams(_extends({
316
+ transaction_id: input.transactionId,
317
+ currency: String(input.currency || 'USD').toUpperCase(),
318
+ value: input.value
319
+ }, input.billingInterval ? {
320
+ billing_interval: input.billingInterval
321
+ } : {}, {
322
+ items: input.items
323
+ }));
324
+ const result = await postGa4Event({
325
+ credentials,
326
+ eventName: 'refund',
327
+ params,
328
+ clientId,
329
+ userId: input.userId,
330
+ log: {
331
+ tag: 'AGL-1850:ga4-refund',
332
+ transactionId: input.transactionId
333
+ },
334
+ internal: input.internal
335
+ });
336
+ return _extends({}, result, {
337
+ synthesizedClientId
338
+ });
339
+ }
340
+ /**
341
+ * Send one `subscription_cancelled` — the churn half of the funnel
342
+ * (AGL-1851). Never throws.
343
+ *
344
+ * Every event instruments the way INTO revenue and nothing the way out, so
345
+ * GA could not answer churn rate, plan-tier churn mix or tenure at
346
+ * cancellation. Sent server-side for the same reason `purchase` is: a
347
+ * subscription ends with no browser present — portal cancellations at
348
+ * period end, dunning exhaustion, staff action — so a client event is not a
349
+ * weaker option, it is no option.
350
+ *
351
+ * A CUSTOM name, deliberately. GA4's only subscription-cancel vocabulary
352
+ * (`app_store_subscription_cancel`) is reserved for app-store data sources
353
+ * and a web hit wearing it is dropped, which is silence, not pollution —
354
+ * the worse failure. The params need dimension registration to report on
355
+ * (AGL-1637): `plan` — the plan being LEFT, never the `free` the org mirror
356
+ * writes; `billing_interval` — the churn half of the AGL-1640 annual-mix
357
+ * metric; `tenure_days` — whole days from the subscription's `created` to
358
+ * when it actually ended, the "how long do they stay" number.
359
+ */ export async function sendGa4SubscriptionCancelled(input) {
360
+ const credentials = ga4Credentials();
361
+ if (!credentials) {
362
+ return {
363
+ sent: false,
364
+ synthesizedClientId: false,
365
+ reason: 'not-configured'
366
+ };
367
+ }
368
+ const clientId = input.clientId || (input.stripeCustomerId ? synthesizeClientId(input.stripeCustomerId) : '');
369
+ if (!clientId) {
370
+ return {
371
+ sent: false,
372
+ synthesizedClientId: false,
373
+ reason: 'no-client-id'
374
+ };
375
+ }
376
+ const synthesizedClientId = !input.clientId;
377
+ const params = sanitizeEventParams(_extends({
378
+ plan: input.plan
379
+ }, input.billingInterval ? {
380
+ billing_interval: input.billingInterval
381
+ } : {}, {
382
+ // The sanitizer drops `undefined`, which keeps "not determined" distinct
383
+ // from a real zero-day tenure.
384
+ tenure_days: input.tenureDays
385
+ }));
386
+ const result = await postGa4Event({
387
+ credentials,
388
+ eventName: 'subscription_cancelled',
389
+ params,
390
+ clientId,
391
+ userId: input.userId,
392
+ log: {
393
+ tag: 'AGL-1851:ga4-subscription-cancelled'
394
+ },
395
+ internal: input.internal
396
+ });
397
+ return _extends({}, result, {
398
+ synthesizedClientId
399
+ });
400
+ }
401
+ /**
402
+ * Send one `site_published` for a route that went live with no browser
403
+ * involved (AGL-1589). Never throws.
404
+ *
405
+ * ## Why this is allowed to be a synthetic user
406
+ *
407
+ * The client id is derived from the HOST — the site whose route went live —
408
+ * and not from the author, because the author is not present: a scheduled
409
+ * publish is applied by the cron beat minutes or hours after anybody clicked
410
+ * anything, and the browser that wrote the schedule is long gone. There is no
411
+ * session to join and nothing to attribute.
412
+ *
413
+ * Deriving it from the host rather than randomly is what keeps the metric
414
+ * honest in the one direction it can be dishonest. "% who publish a site"
415
+ * counts USERS, so a random id per publish would let one site scheduling ten
416
+ * publishes look like ten activated customers — the same inflation
417
+ * `publishScreenRoute` avoids by firing only when a route goes live. One host
418
+ * is one synthetic user, forever, however many of its screens go live on a
419
+ * timer.
420
+ *
421
+ * ## One param, deliberately
422
+ *
423
+ * `first_publish` and nothing else, matching the browser event (AGL-1588).
424
+ * The host id is a resource identifier bought for nothing, and the rest of the
425
+ * metric is answered by the event alone; the id is used only as the hash seed
426
+ * and never transmitted.
427
+ *
428
+ * The caller decides `first_publish` from the routing map it read before
429
+ * registering the entry, through the same `isFirstPublishedRoute` the console
430
+ * uses — a dimension is only worth registering if every sender means the same
431
+ * thing by it, and this sender is the one nobody can watch happen.
432
+ */ export async function sendGa4SitePublished(input) {
433
+ const credentials = ga4Credentials();
434
+ if (!credentials) {
435
+ return {
436
+ sent: false,
437
+ synthesizedClientId: false,
438
+ reason: 'not-configured'
439
+ };
440
+ }
441
+ if (!input.hostId) {
442
+ return {
443
+ sent: false,
444
+ synthesizedClientId: false,
445
+ reason: 'no-client-id'
446
+ };
447
+ }
448
+ const result = await postGa4Event({
449
+ credentials,
450
+ eventName: 'site_published',
451
+ // Sanitized like every other payload even though it carries one boolean —
452
+ // the guarantee should hold because the sanitizer ran, not because the
453
+ // author of this call happened to pass nothing identifying. It also drops
454
+ // `first_publish` when it is undefined, which is how "not determined"
455
+ // stays distinct from `false`.
456
+ params: sanitizeEventParams({
457
+ first_publish: input.firstPublish
458
+ }),
459
+ clientId: synthesizeClientId(input.hostId),
460
+ log: {
461
+ tag: 'AGL-1589:ga4-site-published'
462
+ },
463
+ internal: input.internal
464
+ });
465
+ // Always synthetic here: there is no browser client id to have missed.
466
+ return _extends({}, result, {
467
+ synthesizedClientId: true
468
+ });
469
+ }
470
+ /**
471
+ * Send one `stripe_connected` for an account that became able to take
472
+ * payments with no browser present (AGL-1580). Never throws.
473
+ *
474
+ * ## Why this needs a server sender at all
475
+ *
476
+ * There are two client-side emitters for this event — the commerce payments
477
+ * card and the marketplace seller panel — and both are gated the same way:
478
+ *
479
+ * if (!chargesEnabled) trackEvent('stripe_connected')
480
+ *
481
+ * where `chargesEnabled` is the state read from the merchant's profile BEFORE
482
+ * the connect request. The gate is right in intent: the connect handler is a
483
+ * re-entrant create-or-resume that answers `chargesEnabled: true` on every
484
+ * later visit, so a bare event there would count one merchant once per click
485
+ * forever.
486
+ *
487
+ * What defeats it is AGL-1997. Stripe emits `account.updated` the moment
488
+ * hosted onboarding completes — while the merchant is still on Stripe's
489
+ * domain, before the `return_url` bounces them back — and
490
+ * {@link syncConnectAccountStatus} mirrors `charges_enabled` onto the profile
491
+ * from that webhook. So by the time the merchant lands back in the console
492
+ * the flag they are supposed to transition FROM already reads `true`, the
493
+ * client guard is false, and the event never fires. It is not a race that
494
+ * usually goes the wrong way; the webhook wins by an entire page load.
495
+ *
496
+ * ## The two senders cannot double-count
497
+ *
498
+ * Both key off the SAME stored flag, from opposite sides of it:
499
+ *
500
+ * - merchant clicks first → the connect route writes `true` → the later
501
+ * `account.updated` sees no transition → only the CLIENT event fires;
502
+ * - webhook lands first → the transition is here → the merchant's later click
503
+ * reads a pre-state of `true` → only the SERVER event fires.
504
+ *
505
+ * Exactly one of the two guards can be open for a given account, which is why
506
+ * this is added alongside the client emitters rather than replacing them —
507
+ * a deployment with no Connect webhook destination (the self-host default)
508
+ * still reports through the browser.
509
+ *
510
+ * ## One synthetic user per connected account
511
+ *
512
+ * The client id is hashed from the Stripe account id for the reason
513
+ * {@link sendGa4SitePublished} hashes the host: "% who connect Stripe" counts
514
+ * MERCHANTS, and a random id per hit would let one account that reconnects
515
+ * look like several activated customers. The account id is the hash seed and
516
+ * is never transmitted.
517
+ *
518
+ * ## No parameters, deliberately
519
+ *
520
+ * `stripe_connected` is declared as `Record<string, never>` in the client
521
+ * event contract, and a server sender that invented a param would create a
522
+ * dimension only half the hits carry.
523
+ */ export async function sendGa4StripeConnected(input) {
524
+ const credentials = ga4Credentials();
525
+ if (!credentials) {
526
+ return {
527
+ sent: false,
528
+ synthesizedClientId: false,
529
+ reason: 'not-configured'
530
+ };
531
+ }
532
+ if (!input.accountId) {
533
+ return {
534
+ sent: false,
535
+ synthesizedClientId: false,
536
+ reason: 'no-client-id'
537
+ };
538
+ }
539
+ const result = await postGa4Event({
540
+ credentials,
541
+ eventName: 'stripe_connected',
542
+ // Sanitized despite being empty, so the guarantee holds because the
543
+ // sanitizer ran rather than because this call site happened to pass
544
+ // nothing.
545
+ params: sanitizeEventParams({}),
546
+ clientId: synthesizeClientId(input.accountId),
547
+ log: {
548
+ tag: 'AGL-1580:ga4-stripe-connected'
549
+ },
550
+ internal: input.internal
551
+ });
552
+ // Always synthetic here: there is no browser client id to have missed.
553
+ return _extends({}, result, {
554
+ synthesizedClientId: true
555
+ });
556
+ }
557
+
558
+ //# sourceMappingURL=ga4-measurement-protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/ga4-measurement-protocol.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 { sanitizeEventParams } from '@aglyn/aglyn/app-utils/analytics-events'\n// One definition for a pair of strings that has to agree with a setting in the\n// GA UI nothing here can typecheck against (AGL-1582/AGL-2064). Four surfaces\n// stamp them now — the console, the tenant runtime, the docs site, and this\n// one — and a second spelling would read as a different dimension in GA, so\n// the filter would catch three of them and look like it was working.\nimport {\n INTERNAL_TRAFFIC_PARAM,\n INTERNAL_TRAFFIC_VALUE,\n} from '@aglyn/aglyn/app-utils/internal-traffic'\n\n/**\n * Server-to-server GA4 delivery via the Measurement Protocol (AGL-1561) —\n * `purchase`, and the one activation event no browser can witness.\n *\n * ## Why revenue is sent from the server and nothing else is\n *\n * The obvious alternative is a client-side `purchase` on the post-checkout\n * return page. It is simpler, and it is wrong for money:\n *\n * - **The return page is not reached reliably.** Stripe redirects back after\n * payment, and a customer who closes the tab, loses signal, or gets a bank\n * 3DS interstitial that lands somewhere else has still paid us. Their\n * revenue would simply never appear.\n * - **Ad blockers drop it**, and they are over-represented in exactly the\n * developer audience Aglyn sells to. Under-reported revenue is worse than\n * no revenue reporting, because it looks like data.\n * - **`?status=success` carries no amount and no session id** (verified: the\n * hosted checkout return URL sets only `status`, and nothing in the app\n * reads it), so a client event could not state what was actually charged.\n * It would also re-fire on a refresh or a back-navigation.\n * - The authoritative amount, currency and transaction id already exist,\n * server-side, in the Stripe webhook — the same place the subscription\n * mirror is written. Reporting revenue from anywhere else means reporting\n * a second, weaker version of a number we already hold.\n *\n * `begin_checkout` stays client-side, because intent genuinely is a browser\n * event and its loss to an ad blocker costs a funnel step, not a dollar.\n *\n * ## The second sender: `site_published` from a scheduled publish (AGL-1589)\n *\n * Everything above argues for keeping the server-side surface as small as\n * possible, and {@link sendGa4SitePublished} is the one other event that has\n * no browser to fire it: a SCHEDULED publish is applied by a cron beat (or by\n * an ISR regeneration) at a moment when the author is asleep. Until AGL-1589\n * that path could not make a page reachable at all, so it was not an\n * activation and nothing was sent; now that it registers the routing entry, a\n * scheduled FIRST publish is a route going live and `publishScreenRoute`'s\n * client-side `site_published` — the headline activation metric — would miss\n * it. It is sent HERE for the same reason revenue is: the alternative is not\n * a weaker event, it is no event.\n *\n * The rest of the activation funnel stays client-side. This is not a general\n * server-side analytics channel and should not become one.\n *\n * ## The `client_id` problem, stated plainly\n *\n * The Measurement Protocol REQUIRES a `client_id`, and a server has no way to\n * know the browser's. Whatever is supplied decides whether the purchase joins\n * the user's existing GA session — and therefore whether revenue can be\n * attributed to the campaign that produced it, which is the entire point of\n * the exercise.\n *\n * So the browser's real `client_id` is captured when checkout STARTS and\n * carried on the Stripe object's metadata (`ga_client_id`), and read back\n * here. When it is present, the purchase lands on the same GA user and\n * session as the marketing click that produced it.\n *\n * When it is absent — an older checkout session, a customer whose consent or\n * ad blocker meant gtag never ran, or a subscription renewal months later\n * with no browser involved at all — {@link sendGa4Purchase} falls back to a\n * synthesized id derived from the Stripe customer. That records the revenue\n * correctly but attributes it to a synthetic, sessionless user: the money is\n * right, the channel is unknown. This is a deliberate trade (revenue truth\n * beats attribution completeness) and NOT a silent one — the fallback is\n * counted in the returned result so it can be alarmed on if it ever becomes\n * the common case.\n *\n * ## Failure posture\n *\n * Fire-and-forget, swallowing everything. The webhook route claims each\n * Stripe event in a `stripeEvents` doc BEFORE running handlers and deletes\n * the claim on any throw, so a thrown error here would un-claim the event and\n * make Stripe redeliver it — turning a missing analytics hit into a repeated\n * billing side effect. Analytics must never be able to do that.\n */\n\nconst GA4_ENDPOINT = 'https://www.google-analytics.com/mp/collect'\nconst SEND_TIMEOUT_MS = 3000\n\nexport interface Ga4PurchaseItem {\n item_id: string\n item_name: string\n item_category?: string\n price?: number\n quantity?: number\n}\n\nexport interface Ga4PurchaseInput {\n /** Stripe object id — invoice or checkout session. GA de-duplicates on it. */\n transactionId: string\n /** Whole currency units, NOT cents. */\n value: number\n /** ISO 4217, upper-cased by the caller or here. */\n currency: string\n items: Ga4PurchaseItem[]\n billingInterval?: 'monthly' | 'annual'\n /**\n * The browser's GA `client_id`, captured at checkout start and carried on\n * Stripe metadata. Absent for renewals and for customers whose gtag never\n * ran; see the module comment.\n */\n clientId?: string | null\n /** Opaque Firebase uid. The one identifier GA is allowed to hold. */\n userId?: string | null\n /** Stripe customer id — used ONLY to synthesize a fallback client id. */\n stripeCustomerId?: string | null\n\n /**\n * Whether this transaction is OURS — a staff or rehearsal purchase — rather\n * than a customer's (AGL-1582). Carried from the browser that started\n * checkout on Stripe metadata, the same way `clientId` is.\n *\n * Opt-in only, never inferred: wrongly flagging a real customer erases their\n * revenue from every report, and a GA4 data filter is not retroactive.\n */\n internal?: boolean\n}\n\nexport interface Ga4SendResult {\n sent: boolean\n /** True when no real browser client id was available. */\n synthesizedClientId: boolean\n reason?: string\n}\n\n/**\n * A stable, opaque stand-in for a browser client id.\n *\n * GA expects the `<random>.<timestamp>` shape. Determinism matters more than\n * realism: the same `seed` must map to the same synthetic user every time, or\n * one paying customer becomes a crowd of one-purchase strangers and ARPA is\n * nonsense. Derived by a cheap non-cryptographic hash — this is a bucketing\n * key, not a secret, and the seed it comes from never leaves this function.\n *\n * The seed is whatever identifies the actor the events belong to on the\n * surface doing the sending: the Stripe customer for revenue, the host id for\n * a scheduled publish (AGL-1589). Both are opaque to GA — only the hash is\n * ever transmitted.\n */\nexport function synthesizeClientId(seed: string): string {\n let hash = 0\n for (let i = 0; i < seed.length; i += 1) {\n hash = (hash * 31 + seed.charCodeAt(i)) >>> 0\n }\n // Fixed second component: a real timestamp would make the id move between\n // renewals, which is precisely what determinism is here to prevent.\n return `${hash}.1000000000`\n}\n\ninterface Ga4Credentials {\n measurementId: string\n apiSecret: string\n}\n\n/**\n * Absent config is the normal state on self-hosted deployments and in\n * development — not an error, and not worth a log line per event.\n *\n * ⚠️ It is NO LONGER the state of our own production deployments, and this\n * comment saying otherwise is how that fact got lost twice (AGL-2327).\n * `GA4_MEASUREMENT_ID` and `GA4_API_SECRET` are present on the production\n * deployments of `aglyn-console`, `aglyn-tenant` AND `aglyn-docs` since\n * **2026-08-17 12:15 UTC** — verified against `GET /v13/deployments/{id}`,\n * which returns the env key list a running lambda actually has, with an older\n * deployment as a negative control. `vercel env ls` and the project's own env\n * list cannot answer this question; only the deployment can.\n *\n * So a server-side event missing from GA4 today is **not** explained by\n * credentials, and must not be written off as \"the transport is dead\". A\n * 2026-08-19 smoke pass reached exactly that conclusion off the stale version\n * of this note, four days after the flip landed. Look at dimension\n * registration (AGL-1637 item 3) and at how the beacon is SCHEDULED\n * (AGL-2346/AGL-2327 — a bare `void` in a route handler never runs) instead.\n *\n * See docs/ANALYTICS.md §\"Environment variables\".\n */\nfunction ga4Credentials(): Ga4Credentials | null {\n const measurementId = process.env.GA4_MEASUREMENT_ID || ''\n const apiSecret = process.env.GA4_API_SECRET || ''\n if (!measurementId || !apiSecret) return null\n return { measurementId, apiSecret }\n}\n\n/**\n * POST one already-sanitized event. Never throws — see the module comment on\n * failure posture; the callers run inside a Stripe webhook claim and inside a\n * publish executor, and in both a throw would have a side effect far worse\n * than a missing analytics hit.\n */\nasync function postGa4Event(options: {\n credentials: Ga4Credentials\n eventName: string\n params: Record<string, unknown>\n clientId: string\n userId?: string | null\n /** Structured-log fields for the two failure lines (tag + subject). */\n log: Record<string, unknown>\n /**\n * Whether this hit is OURS rather than a customer's (AGL-1582).\n *\n * Applied here, centrally, rather than by each sender: a fifth sender added\n * later would otherwise be unstamped by default, and \"unstamped by default\"\n * is how our own rehearsal revenue ends up in the real reports.\n */\n internal?: boolean\n}): Promise<{ sent: boolean; reason?: string }> {\n const { credentials, eventName, params, clientId, userId, log } = options\n // Only ever ADDED, never set to `false`. GA4's internal-traffic filter\n // matches on the parameter being present with this value; a `traffic_type:\n // 'external'` on every real hit would be a second dimension nobody filters\n // on, and one bad `internal` computation away from erasing paying customers\n // from every report — which a data filter cannot undo.\n const stamped = options.internal\n ? { ...params, [INTERNAL_TRAFFIC_PARAM]: INTERNAL_TRAFFIC_VALUE }\n : params\n try {\n const response = await fetch(\n `${GA4_ENDPOINT}?measurement_id=${encodeURIComponent(\n credentials.measurementId,\n )}&api_secret=${encodeURIComponent(credentials.apiSecret)}`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n client_id: clientId,\n // Opaque uid only — it is what stitches this event to the\n // console's client-side events, which set the same user_id.\n ...(userId ? { user_id: userId } : {}),\n // Keeps THIS path out of ads personalization, and it is the only\n // thing that does. The property runs Google Signals and ads\n // personalization ON in every region, so a server event sent without\n // this flag would join the advertising audiences the browser tags\n // build — carrying a `client_id` that may be synthesized from a\n // Stripe customer id, which is a stronger identifier than any cookie\n // the browser path has. A per-hit assertion rather than a property\n // setting, so no dashboard change can quietly opt these events in.\n non_personalized_ads: true,\n events: [{ name: eventName, params: stamped }],\n }),\n signal: AbortSignal.timeout(SEND_TIMEOUT_MS),\n },\n )\n if (!response.ok) {\n console.warn(JSON.stringify({ ...log, status: response.status }))\n return { sent: false, reason: `http-${response.status}` }\n }\n return { sent: true }\n } catch (error) {\n console.warn(\n JSON.stringify({\n ...log,\n error: error instanceof Error ? error.message : 'unknown',\n }),\n )\n return { sent: false, reason: 'network' }\n }\n}\n\n/**\n * Send one `purchase` to GA4. Never throws.\n *\n * Returns a result rather than void so the caller can log the fallback rate;\n * nothing about the return value should change billing behaviour.\n */\nexport async function sendGa4Purchase(\n input: Ga4PurchaseInput,\n): Promise<Ga4SendResult> {\n const credentials = ga4Credentials()\n if (!credentials) {\n return { sent: false, synthesizedClientId: false, reason: 'not-configured' }\n }\n const clientId =\n input.clientId ||\n (input.stripeCustomerId ? synthesizeClientId(input.stripeCustomerId) : '')\n if (!clientId) {\n return { sent: false, synthesizedClientId: false, reason: 'no-client-id' }\n }\n const synthesizedClientId = !input.clientId\n\n // The same sanitizer the browser path uses, applied again here. The inputs\n // are Stripe-derived rather than hand-written, and a Stripe product name is\n // one `metadata` edit away from carrying a customer's name.\n const params = sanitizeEventParams({\n transaction_id: input.transactionId,\n currency: String(input.currency || 'USD').toUpperCase(),\n value: input.value,\n ...(input.billingInterval\n ? { billing_interval: input.billingInterval }\n : {}),\n items: input.items,\n })\n\n // Swallows everything — see the module comment: a throw here would un-claim\n // the Stripe event and cause a redelivery.\n const result = await postGa4Event({\n credentials,\n eventName: 'purchase',\n params,\n clientId,\n userId: input.userId,\n log: {\n tag: 'AGL-1561:ga4-purchase',\n transactionId: input.transactionId,\n },\n internal: input.internal,\n })\n return { ...result, synthesizedClientId }\n}\n\n/**\n * Send one GA4 `refund` — revenue that came back (AGL-1850). Never throws.\n *\n * Both `purchase` senders were one-directional, so GA revenue could only\n * ever go UP: a refunded invoice or marketplace sale stayed on the books\n * forever, drifting GA away from Stripe by exactly the refund volume. GA4's\n * native `refund` event is built for this — sent with the ORIGINAL\n * purchase's `transaction_id`, GA nets the two against each other in every\n * revenue report.\n *\n * `transaction_id` is therefore the one field that carries the whole\n * mechanism: the invoice id for a subscription refund, the checkout session\n * id for a marketplace one — whatever the matching `purchase` reported,\n * never the refund's own charge or event id.\n *\n * `value` is what actually came back, in the SAME accounting the purchase\n * used. For a subscription that is `amount_refunded` (the purchase reported\n * `amount_paid`, all ours); for a marketplace sale it is the platform-net\n * share (the purchase reported our net per AGL-1639, so refunding the gross\n * would net MORE than the sale ever reported). The caller owns that\n * arithmetic because the caller holds the ledger; this sender ships whatever\n * number it is handed.\n *\n * Same claiming rules as `purchase` (the caller discriminates, this module\n * transmits), same client-id capture-or-synthesize fallback, same\n * fire-and-forget posture — a throw would un-claim the Stripe event.\n */\nexport async function sendGa4Refund(\n input: Ga4PurchaseInput,\n): Promise<Ga4SendResult> {\n const credentials = ga4Credentials()\n if (!credentials) {\n return { sent: false, synthesizedClientId: false, reason: 'not-configured' }\n }\n const clientId =\n input.clientId ||\n (input.stripeCustomerId ? synthesizeClientId(input.stripeCustomerId) : '')\n if (!clientId) {\n return { sent: false, synthesizedClientId: false, reason: 'no-client-id' }\n }\n const synthesizedClientId = !input.clientId\n const params = sanitizeEventParams({\n transaction_id: input.transactionId,\n currency: String(input.currency || 'USD').toUpperCase(),\n value: input.value,\n ...(input.billingInterval\n ? { billing_interval: input.billingInterval }\n : {}),\n items: input.items,\n })\n const result = await postGa4Event({\n credentials,\n eventName: 'refund',\n params,\n clientId,\n userId: input.userId,\n log: {\n tag: 'AGL-1850:ga4-refund',\n transactionId: input.transactionId,\n },\n internal: input.internal,\n })\n return { ...result, synthesizedClientId }\n}\n\n/**\n * Send one `subscription_cancelled` — the churn half of the funnel\n * (AGL-1851). Never throws.\n *\n * Every event instruments the way INTO revenue and nothing the way out, so\n * GA could not answer churn rate, plan-tier churn mix or tenure at\n * cancellation. Sent server-side for the same reason `purchase` is: a\n * subscription ends with no browser present — portal cancellations at\n * period end, dunning exhaustion, staff action — so a client event is not a\n * weaker option, it is no option.\n *\n * A CUSTOM name, deliberately. GA4's only subscription-cancel vocabulary\n * (`app_store_subscription_cancel`) is reserved for app-store data sources\n * and a web hit wearing it is dropped, which is silence, not pollution —\n * the worse failure. The params need dimension registration to report on\n * (AGL-1637): `plan` — the plan being LEFT, never the `free` the org mirror\n * writes; `billing_interval` — the churn half of the AGL-1640 annual-mix\n * metric; `tenure_days` — whole days from the subscription's `created` to\n * when it actually ended, the \"how long do they stay\" number.\n */\nexport async function sendGa4SubscriptionCancelled(input: {\n /** The plan tier the org is LEAVING (metadata/price-derived, not `free`). */\n plan: string\n /** `monthly` | `annual`, when the subscription's price interval said. */\n billingInterval?: 'monthly' | 'annual'\n /** Whole days the subscription lived; omitted when timestamps are absent. */\n tenureDays?: number\n /** Same capture-or-synthesize contract as `purchase`. */\n clientId?: string | null\n userId?: string | null\n stripeCustomerId?: string | null\n /** Ours rather than a customer's (AGL-1582) — see `Ga4PurchaseInput`. */\n internal?: boolean\n}): Promise<Ga4SendResult> {\n const credentials = ga4Credentials()\n if (!credentials) {\n return { sent: false, synthesizedClientId: false, reason: 'not-configured' }\n }\n const clientId =\n input.clientId ||\n (input.stripeCustomerId ? synthesizeClientId(input.stripeCustomerId) : '')\n if (!clientId) {\n return { sent: false, synthesizedClientId: false, reason: 'no-client-id' }\n }\n const synthesizedClientId = !input.clientId\n const params = sanitizeEventParams({\n plan: input.plan,\n ...(input.billingInterval\n ? { billing_interval: input.billingInterval }\n : {}),\n // The sanitizer drops `undefined`, which keeps \"not determined\" distinct\n // from a real zero-day tenure.\n tenure_days: input.tenureDays,\n })\n const result = await postGa4Event({\n credentials,\n eventName: 'subscription_cancelled',\n params,\n clientId,\n userId: input.userId,\n log: { tag: 'AGL-1851:ga4-subscription-cancelled' },\n internal: input.internal,\n })\n return { ...result, synthesizedClientId }\n}\n\n/**\n * Send one `site_published` for a route that went live with no browser\n * involved (AGL-1589). Never throws.\n *\n * ## Why this is allowed to be a synthetic user\n *\n * The client id is derived from the HOST — the site whose route went live —\n * and not from the author, because the author is not present: a scheduled\n * publish is applied by the cron beat minutes or hours after anybody clicked\n * anything, and the browser that wrote the schedule is long gone. There is no\n * session to join and nothing to attribute.\n *\n * Deriving it from the host rather than randomly is what keeps the metric\n * honest in the one direction it can be dishonest. \"% who publish a site\"\n * counts USERS, so a random id per publish would let one site scheduling ten\n * publishes look like ten activated customers — the same inflation\n * `publishScreenRoute` avoids by firing only when a route goes live. One host\n * is one synthetic user, forever, however many of its screens go live on a\n * timer.\n *\n * ## One param, deliberately\n *\n * `first_publish` and nothing else, matching the browser event (AGL-1588).\n * The host id is a resource identifier bought for nothing, and the rest of the\n * metric is answered by the event alone; the id is used only as the hash seed\n * and never transmitted.\n *\n * The caller decides `first_publish` from the routing map it read before\n * registering the entry, through the same `isFirstPublishedRoute` the console\n * uses — a dimension is only worth registering if every sender means the same\n * thing by it, and this sender is the one nobody can watch happen.\n */\nexport async function sendGa4SitePublished(input: {\n /** Host uid the route went live on — hashed into a client id, never sent. */\n hostId: string\n /**\n * Whether the host had no live route at all before this one. Omit when it\n * genuinely cannot be determined — the param is optional and an absent\n * breakdown value beats an invented one.\n */\n firstPublish?: boolean\n /**\n * Ours rather than a customer's (AGL-1582). A scheduled publish of our own\n * marketing site or a demo host is an activation event for nobody, and\n * `site_published` is the headline activation metric.\n */\n internal?: boolean\n}): Promise<Ga4SendResult> {\n const credentials = ga4Credentials()\n if (!credentials) {\n return { sent: false, synthesizedClientId: false, reason: 'not-configured' }\n }\n if (!input.hostId) {\n return { sent: false, synthesizedClientId: false, reason: 'no-client-id' }\n }\n const result = await postGa4Event({\n credentials,\n eventName: 'site_published',\n // Sanitized like every other payload even though it carries one boolean —\n // the guarantee should hold because the sanitizer ran, not because the\n // author of this call happened to pass nothing identifying. It also drops\n // `first_publish` when it is undefined, which is how \"not determined\"\n // stays distinct from `false`.\n params: sanitizeEventParams({ first_publish: input.firstPublish }),\n clientId: synthesizeClientId(input.hostId),\n log: { tag: 'AGL-1589:ga4-site-published' },\n internal: input.internal,\n })\n // Always synthetic here: there is no browser client id to have missed.\n return { ...result, synthesizedClientId: true }\n}\n\n/**\n * Send one `stripe_connected` for an account that became able to take\n * payments with no browser present (AGL-1580). Never throws.\n *\n * ## Why this needs a server sender at all\n *\n * There are two client-side emitters for this event — the commerce payments\n * card and the marketplace seller panel — and both are gated the same way:\n *\n * if (!chargesEnabled) trackEvent('stripe_connected')\n *\n * where `chargesEnabled` is the state read from the merchant's profile BEFORE\n * the connect request. The gate is right in intent: the connect handler is a\n * re-entrant create-or-resume that answers `chargesEnabled: true` on every\n * later visit, so a bare event there would count one merchant once per click\n * forever.\n *\n * What defeats it is AGL-1997. Stripe emits `account.updated` the moment\n * hosted onboarding completes — while the merchant is still on Stripe's\n * domain, before the `return_url` bounces them back — and\n * {@link syncConnectAccountStatus} mirrors `charges_enabled` onto the profile\n * from that webhook. So by the time the merchant lands back in the console\n * the flag they are supposed to transition FROM already reads `true`, the\n * client guard is false, and the event never fires. It is not a race that\n * usually goes the wrong way; the webhook wins by an entire page load.\n *\n * ## The two senders cannot double-count\n *\n * Both key off the SAME stored flag, from opposite sides of it:\n *\n * - merchant clicks first → the connect route writes `true` → the later\n * `account.updated` sees no transition → only the CLIENT event fires;\n * - webhook lands first → the transition is here → the merchant's later click\n * reads a pre-state of `true` → only the SERVER event fires.\n *\n * Exactly one of the two guards can be open for a given account, which is why\n * this is added alongside the client emitters rather than replacing them —\n * a deployment with no Connect webhook destination (the self-host default)\n * still reports through the browser.\n *\n * ## One synthetic user per connected account\n *\n * The client id is hashed from the Stripe account id for the reason\n * {@link sendGa4SitePublished} hashes the host: \"% who connect Stripe\" counts\n * MERCHANTS, and a random id per hit would let one account that reconnects\n * look like several activated customers. The account id is the hash seed and\n * is never transmitted.\n *\n * ## No parameters, deliberately\n *\n * `stripe_connected` is declared as `Record<string, never>` in the client\n * event contract, and a server sender that invented a param would create a\n * dimension only half the hits carry.\n */\nexport async function sendGa4StripeConnected(input: {\n /** Stripe account id — hashed into a client id, never sent. */\n accountId: string\n /**\n * Ours rather than a customer's (AGL-1582). Omitted by the webhook caller\n * for the same reason the publish executor omits it: no server-side signal\n * distinguishes our own connected account from a customer's, and guessing\n * would stamp real merchants as internal — which a data filter then\n * permanently discards.\n */\n internal?: boolean\n}): Promise<Ga4SendResult> {\n const credentials = ga4Credentials()\n if (!credentials) {\n return { sent: false, synthesizedClientId: false, reason: 'not-configured' }\n }\n if (!input.accountId) {\n return { sent: false, synthesizedClientId: false, reason: 'no-client-id' }\n }\n const result = await postGa4Event({\n credentials,\n eventName: 'stripe_connected',\n // Sanitized despite being empty, so the guarantee holds because the\n // sanitizer ran rather than because this call site happened to pass\n // nothing.\n params: sanitizeEventParams({}),\n clientId: synthesizeClientId(input.accountId),\n log: { tag: 'AGL-1580:ga4-stripe-connected' },\n internal: input.internal,\n })\n // Always synthetic here: there is no browser client id to have missed.\n return { ...result, synthesizedClientId: true }\n}\n"],"names":["sanitizeEventParams","INTERNAL_TRAFFIC_PARAM","INTERNAL_TRAFFIC_VALUE","GA4_ENDPOINT","SEND_TIMEOUT_MS","synthesizeClientId","seed","hash","i","length","charCodeAt","ga4Credentials","measurementId","process","env","GA4_MEASUREMENT_ID","apiSecret","GA4_API_SECRET","postGa4Event","options","credentials","eventName","params","clientId","userId","log","stamped","internal","response","fetch","encodeURIComponent","method","headers","body","JSON","stringify","client_id","user_id","non_personalized_ads","events","name","signal","AbortSignal","timeout","ok","console","warn","status","sent","reason","error","Error","message","sendGa4Purchase","input","synthesizedClientId","stripeCustomerId","transaction_id","transactionId","currency","String","toUpperCase","value","billingInterval","billing_interval","items","result","tag","sendGa4Refund","sendGa4SubscriptionCancelled","plan","tenure_days","tenureDays","sendGa4SitePublished","hostId","first_publish","firstPublish","sendGa4StripeConnected","accountId"],"mappings":"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"}