@omega.js/backend 0.1.0

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 (993) hide show
  1. package/LICENSE +98 -0
  2. package/README.md +1053 -0
  3. package/bin/omega +2 -0
  4. package/bin/omega-backend +2 -0
  5. package/bin/omg +2 -0
  6. package/cli.js +3 -0
  7. package/dist/cli/command-table.js +268 -0
  8. package/dist/cli/commands/auth.js +259 -0
  9. package/dist/cli/commands/base-command.js +451 -0
  10. package/dist/cli/commands/build.js +27 -0
  11. package/dist/cli/commands/clean.js +13 -0
  12. package/dist/cli/commands/clear.js +11 -0
  13. package/dist/cli/commands/cwd.js +9 -0
  14. package/dist/cli/commands/deploy.js +205 -0
  15. package/dist/cli/commands/emulator-orphans.js +440 -0
  16. package/dist/cli/commands/emulator.js +1829 -0
  17. package/dist/cli/commands/firebase-init.js +101 -0
  18. package/dist/cli/commands/firestore.js +261 -0
  19. package/dist/cli/commands/indexes.js +51 -0
  20. package/dist/cli/commands/install.js +151 -0
  21. package/dist/cli/commands/logs.js +358 -0
  22. package/dist/cli/commands/mcp.js +41 -0
  23. package/dist/cli/commands/migrate-markers.js +176 -0
  24. package/dist/cli/commands/migrate-rules.js +90 -0
  25. package/dist/cli/commands/migrate.js +59 -0
  26. package/dist/cli/commands/serve.js +208 -0
  27. package/dist/cli/commands/setup-tests/base-test.js +174 -0
  28. package/dist/cli/commands/setup-tests/emulator-config.js +109 -0
  29. package/dist/cli/commands/setup-tests/env-runtime-config-deprecated.js +47 -0
  30. package/dist/cli/commands/setup-tests/firebase-admin.js +49 -0
  31. package/dist/cli/commands/setup-tests/firebase-auth.js +26 -0
  32. package/dist/cli/commands/setup-tests/firebase-cli.js +26 -0
  33. package/dist/cli/commands/setup-tests/firebase-functions.js +49 -0
  34. package/dist/cli/commands/setup-tests/firestore-indexes-file.js +72 -0
  35. package/dist/cli/commands/setup-tests/firestore-indexes-in-json.js +20 -0
  36. package/dist/cli/commands/setup-tests/firestore-indexes-required.js +108 -0
  37. package/dist/cli/commands/setup-tests/firestore-indexes-synced.js +236 -0
  38. package/dist/cli/commands/setup-tests/firestore-rules-file.js +115 -0
  39. package/dist/cli/commands/setup-tests/firestore-rules-in-json.js +49 -0
  40. package/dist/cli/commands/setup-tests/functions-package.js +44 -0
  41. package/dist/cli/commands/setup-tests/gcloud-cli.js +26 -0
  42. package/dist/cli/commands/setup-tests/gitignore.js +122 -0
  43. package/dist/cli/commands/setup-tests/helpers/merge-line-files.js +28 -0
  44. package/dist/cli/commands/setup-tests/helpers/required-indexes.js +109 -0
  45. package/dist/cli/commands/setup-tests/helpers/seed-campaigns.js +246 -0
  46. package/dist/cli/commands/setup-tests/helpers.js +72 -0
  47. package/dist/cli/commands/setup-tests/hosting-folder.js +23 -0
  48. package/dist/cli/commands/setup-tests/hosting-rewrites.js +52 -0
  49. package/dist/cli/commands/setup-tests/index.js +111 -0
  50. package/dist/cli/commands/setup-tests/is-firebase-project.js +21 -0
  51. package/dist/cli/commands/setup-tests/java-installed.js +27 -0
  52. package/dist/cli/commands/setup-tests/legacy-tests-cleanup.js +43 -0
  53. package/dist/cli/commands/setup-tests/marketing-campaigns-seeded.js +230 -0
  54. package/dist/cli/commands/setup-tests/node-version.js +60 -0
  55. package/dist/cli/commands/setup-tests/npm-project-scripts.js +42 -0
  56. package/dist/cli/commands/setup-tests/nvmrc-version.js +36 -0
  57. package/dist/cli/commands/setup-tests/omega-backend.js +42 -0
  58. package/dist/cli/commands/setup-tests/omega-config.js +84 -0
  59. package/dist/cli/commands/setup-tests/project-directories.js +34 -0
  60. package/dist/cli/commands/setup-tests/project-id-consistency.js +201 -0
  61. package/dist/cli/commands/setup-tests/public-html-files.js +22 -0
  62. package/dist/cli/commands/setup-tests/realtime-rules-file.js +69 -0
  63. package/dist/cli/commands/setup-tests/realtime-rules-in-json.js +20 -0
  64. package/dist/cli/commands/setup-tests/remoteconfig-template-file.js +32 -0
  65. package/dist/cli/commands/setup-tests/remoteconfig-template-in-json.js +31 -0
  66. package/dist/cli/commands/setup-tests/service-account.js +86 -0
  67. package/dist/cli/commands/setup-tests/storage-lifecycle-policy.js +81 -0
  68. package/dist/cli/commands/setup-tests/storage-rules-file.js +32 -0
  69. package/dist/cli/commands/setup-tests/storage-rules-in-json.js +20 -0
  70. package/dist/cli/commands/stripe.js +14 -0
  71. package/dist/cli/commands/test-lanes/stripe-live.js +440 -0
  72. package/dist/cli/commands/test.js +976 -0
  73. package/dist/cli/commands/update.js +27 -0
  74. package/dist/cli/commands/version.js +10 -0
  75. package/dist/cli/commands/watch.js +196 -0
  76. package/dist/cli/flags.js +13 -0
  77. package/dist/cli/index.js +173 -0
  78. package/dist/cli/run.js +33 -0
  79. package/dist/cli/utils/attach-log-file.js +4 -0
  80. package/dist/cli/utils/compile-rules.js +1155 -0
  81. package/dist/cli/utils/ensure-target.js +314 -0
  82. package/dist/cli/utils/project-type.js +140 -0
  83. package/dist/cli/utils/public-files.js +49 -0
  84. package/dist/cli/utils/safe-install.js +4 -0
  85. package/dist/cli/utils/spawn-shell.js +94 -0
  86. package/dist/cli/utils/stage-functions.js +338 -0
  87. package/dist/cli/utils/stage-local-packages.js +210 -0
  88. package/dist/cli/utils/target-checks.js +161 -0
  89. package/dist/cli/utils/target.js +24 -0
  90. package/dist/cli/utils/ui.js +292 -0
  91. package/dist/defaults/AGENTS.md +125 -0
  92. package/dist/defaults/CHANGELOG.md +15 -0
  93. package/dist/defaults/CLAUDE.md +1 -0
  94. package/dist/defaults/_.gitignore +65 -0
  95. package/dist/defaults/docs/README.md +17 -0
  96. package/dist/defaults/test/README.md +65 -0
  97. package/dist/defaults/test/_init.js +14 -0
  98. package/dist/defaults/test/helpers/connect-trap.js +72 -0
  99. package/dist/defaults/test/unit/registration.test.js +143 -0
  100. package/dist/defaults/test/unit/rules-posture.test.js +101 -0
  101. package/dist/defaults/test/unit/socket-free.test.js +43 -0
  102. package/dist/manager/events/auth/before-create.js +89 -0
  103. package/dist/manager/events/auth/before-signin.js +76 -0
  104. package/dist/manager/events/auth/on-create.js +112 -0
  105. package/dist/manager/events/auth/on-delete.js +141 -0
  106. package/dist/manager/events/auth/utils.js +93 -0
  107. package/dist/manager/events/cron/daily/blog-auto-publisher.js +287 -0
  108. package/dist/manager/events/cron/daily/data-requests.js +57 -0
  109. package/dist/manager/events/cron/daily/expire-paypal-cancellations.js +191 -0
  110. package/dist/manager/events/cron/daily/marketing-prune.js +486 -0
  111. package/dist/manager/events/cron/daily/reset-usage.js +188 -0
  112. package/dist/manager/events/cron/daily/trial-lapse-sweep.js +355 -0
  113. package/dist/manager/events/cron/daily.js +3 -0
  114. package/dist/manager/events/cron/frequent/abandoned-carts.js +204 -0
  115. package/dist/manager/events/cron/frequent/email-queue.js +71 -0
  116. package/dist/manager/events/cron/frequent/marketing-campaigns.js +463 -0
  117. package/dist/manager/events/cron/frequent/retry-failed-webhooks.js +70 -0
  118. package/dist/manager/events/cron/frequent.js +3 -0
  119. package/dist/manager/events/cron/runner.js +64 -0
  120. package/dist/manager/events/firestore/notifications/on-write.js +123 -0
  121. package/dist/manager/events/firestore/payments-disputes/on-write.js +256 -0
  122. package/dist/manager/events/firestore/payments-disputes/providers/stripe.js +218 -0
  123. package/dist/manager/events/firestore/payments-disputes/providers/test.js +231 -0
  124. package/dist/manager/events/firestore/payments-webhooks/analytics.js +687 -0
  125. package/dist/manager/events/firestore/payments-webhooks/on-write.js +1224 -0
  126. package/dist/manager/events/firestore/payments-webhooks/transitions/index.js +395 -0
  127. package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +58 -0
  128. package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +28 -0
  129. package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-refunded.js +30 -0
  130. package/dist/manager/events/firestore/payments-webhooks/transitions/send-email.js +74 -0
  131. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-removed.js +14 -0
  132. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +28 -0
  133. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/checkout-declined.js +18 -0
  134. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +67 -0
  135. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +25 -0
  136. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +26 -0
  137. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-refunded.js +34 -0
  138. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +42 -0
  139. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +33 -0
  140. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-winback.js +17 -0
  141. package/dist/manager/functions/_legacy/actions/create-post-handler.js +188 -0
  142. package/dist/manager/functions/_legacy/actions/generate-uuid.js +63 -0
  143. package/dist/manager/functions/_legacy/actions/sign-up-handler.js +205 -0
  144. package/dist/manager/functions/_legacy/admin/create-post.js +207 -0
  145. package/dist/manager/functions/_legacy/admin/firestore-write.js +72 -0
  146. package/dist/manager/functions/_legacy/admin/get-stats.js +218 -0
  147. package/dist/manager/functions/_legacy/admin/query.js +198 -0
  148. package/dist/manager/functions/_legacy/admin/send-notification.js +206 -0
  149. package/dist/manager/functions/_legacy/template.js +33 -0
  150. package/dist/manager/functions/_legacy/test/authenticate.js +40 -0
  151. package/dist/manager/functions/_legacy/test/webhook.js +37 -0
  152. package/dist/manager/functions/wrappers/mailchimp/addToList.js +25 -0
  153. package/dist/manager/helpers/analytics.js +498 -0
  154. package/dist/manager/helpers/api-manager.js +313 -0
  155. package/dist/manager/helpers/backend-router.js +37 -0
  156. package/dist/manager/helpers/context/authenticate.js +215 -0
  157. package/dist/manager/helpers/context/client-info.js +91 -0
  158. package/dist/manager/helpers/context/index.js +228 -0
  159. package/dist/manager/helpers/context/logging.js +150 -0
  160. package/dist/manager/helpers/context/parse.js +160 -0
  161. package/dist/manager/helpers/context/respond.js +247 -0
  162. package/dist/manager/helpers/event-middleware.js +109 -0
  163. package/dist/manager/helpers/metadata.js +32 -0
  164. package/dist/manager/helpers/middleware.js +541 -0
  165. package/dist/manager/helpers/redact-secret.js +25 -0
  166. package/dist/manager/helpers/resolved-config.js +34 -0
  167. package/dist/manager/helpers/roles.js +69 -0
  168. package/dist/manager/helpers/safe-compare.js +31 -0
  169. package/dist/manager/helpers/schema-engine.js +314 -0
  170. package/dist/manager/helpers/schema-zod.js +203 -0
  171. package/dist/manager/helpers/settings.js +215 -0
  172. package/dist/manager/helpers/usage.js +541 -0
  173. package/dist/manager/helpers/user.js +57 -0
  174. package/dist/manager/helpers/utilities.js +565 -0
  175. package/dist/manager/index.js +1404 -0
  176. package/dist/manager/libraries/abandoned-cart-config.js +12 -0
  177. package/dist/manager/libraries/ai/index.js +321 -0
  178. package/dist/manager/libraries/ai/prompt.js +85 -0
  179. package/dist/manager/libraries/ai/providers/anthropic-format.js +297 -0
  180. package/dist/manager/libraries/ai/providers/anthropic.js +169 -0
  181. package/dist/manager/libraries/ai/providers/claude-code.js +183 -0
  182. package/dist/manager/libraries/ai/providers/openai.js +1272 -0
  183. package/dist/manager/libraries/ai/providers/test.js +257 -0
  184. package/dist/manager/libraries/ai/tokens.js +54 -0
  185. package/dist/manager/libraries/analytics/conversions.js +478 -0
  186. package/dist/manager/libraries/analytics/match-data.js +573 -0
  187. package/dist/manager/libraries/analytics/signup.js +120 -0
  188. package/dist/manager/libraries/auth-user.js +46 -0
  189. package/dist/manager/libraries/content/feed-parser.js +198 -0
  190. package/dist/manager/libraries/content/ghostii.js +191 -0
  191. package/dist/manager/libraries/content/source-resolver.js +629 -0
  192. package/dist/manager/libraries/email/constants.js +532 -0
  193. package/dist/manager/libraries/email/data/blocked-local-parts.json +55 -0
  194. package/dist/manager/libraries/email/data/blocked-local-patterns.js +14 -0
  195. package/dist/manager/libraries/email/data/corporate-domains.json +23 -0
  196. package/dist/manager/libraries/email/data/custom-disposable-domains.json +58 -0
  197. package/dist/manager/libraries/email/data/disposable-domains.json +8184 -0
  198. package/dist/manager/libraries/email/data/typo-domains.js +83 -0
  199. package/dist/manager/libraries/email/disposable-domains.js +92 -0
  200. package/dist/manager/libraries/email/generators/lib/filter.js +179 -0
  201. package/dist/manager/libraries/email/generators/lib/image-host.js +344 -0
  202. package/dist/manager/libraries/email/generators/lib/image-illustrator.js +154 -0
  203. package/dist/manager/libraries/email/generators/lib/markdown-renderer.js +294 -0
  204. package/dist/manager/libraries/email/generators/lib/mjml-template.js +120 -0
  205. package/dist/manager/libraries/email/generators/lib/structure.js +299 -0
  206. package/dist/manager/libraries/email/generators/lib/svg-illustrator.js +197 -0
  207. package/dist/manager/libraries/email/generators/lib/templates/base.js +229 -0
  208. package/dist/manager/libraries/email/generators/lib/templates/card.js +33 -0
  209. package/dist/manager/libraries/email/generators/lib/templates/classic-schema.js +59 -0
  210. package/dist/manager/libraries/email/generators/lib/templates/clean.js +82 -0
  211. package/dist/manager/libraries/email/generators/lib/templates/editorial/helpers.js +100 -0
  212. package/dist/manager/libraries/email/generators/lib/templates/editorial/index.js +318 -0
  213. package/dist/manager/libraries/email/generators/lib/templates/feedback.js +101 -0
  214. package/dist/manager/libraries/email/generators/lib/templates/field-report/helpers.js +138 -0
  215. package/dist/manager/libraries/email/generators/lib/templates/field-report/index.js +494 -0
  216. package/dist/manager/libraries/email/generators/lib/templates/index.js +47 -0
  217. package/dist/manager/libraries/email/generators/lib/templates/newsletter-shared.js +610 -0
  218. package/dist/manager/libraries/email/generators/lib/templates/order.js +461 -0
  219. package/dist/manager/libraries/email/generators/lib/templates/plain.js +83 -0
  220. package/dist/manager/libraries/email/generators/lib/templates/shared-campaign.js +64 -0
  221. package/dist/manager/libraries/email/generators/newsletter.js +899 -0
  222. package/dist/manager/libraries/email/index.js +146 -0
  223. package/dist/manager/libraries/email/marketing/index.js +754 -0
  224. package/dist/manager/libraries/email/prepare.js +365 -0
  225. package/dist/manager/libraries/email/providers/beehiiv.js +589 -0
  226. package/dist/manager/libraries/email/providers/sendgrid.js +845 -0
  227. package/dist/manager/libraries/email/transactional/index.js +510 -0
  228. package/dist/manager/libraries/email/utm.js +86 -0
  229. package/dist/manager/libraries/email/validation-provider-neverbounce.js +70 -0
  230. package/dist/manager/libraries/email/validation-provider-zerobounce.js +42 -0
  231. package/dist/manager/libraries/email/validation.js +255 -0
  232. package/dist/manager/libraries/env.js +198 -0
  233. package/dist/manager/libraries/infer-contact.js +101 -0
  234. package/dist/manager/libraries/load-provider.js +65 -0
  235. package/dist/manager/libraries/notification.js +256 -0
  236. package/dist/manager/libraries/openai.js +13 -0
  237. package/dist/manager/libraries/payment/discount-codes.js +204 -0
  238. package/dist/manager/libraries/payment/fetch-failure.js +56 -0
  239. package/dist/manager/libraries/payment/license.js +44 -0
  240. package/dist/manager/libraries/payment/order-id.js +57 -0
  241. package/dist/manager/libraries/payment/provider-errors.js +71 -0
  242. package/dist/manager/libraries/payment/providers/chargebee.js +817 -0
  243. package/dist/manager/libraries/payment/providers/coinbase.js +330 -0
  244. package/dist/manager/libraries/payment/providers/paypal.js +987 -0
  245. package/dist/manager/libraries/payment/providers/stripe.js +755 -0
  246. package/dist/manager/libraries/payment/providers/test.js +250 -0
  247. package/dist/manager/libraries/payment/refund-linkage.js +78 -0
  248. package/dist/manager/libraries/payment/refund-policy.js +113 -0
  249. package/dist/manager/libraries/payment/winback.js +55 -0
  250. package/dist/manager/libraries/prompts/infer-contact.md +78 -0
  251. package/dist/manager/libraries/rate-limits.js +17 -0
  252. package/dist/manager/libraries/recaptcha.js +58 -0
  253. package/dist/manager/libraries/user-doc.js +234 -0
  254. package/dist/manager/routes/admin/backup/post.js +106 -0
  255. package/dist/manager/routes/admin/cron/post.js +37 -0
  256. package/dist/manager/routes/admin/database/get.js +35 -0
  257. package/dist/manager/routes/admin/database/post.js +34 -0
  258. package/dist/manager/routes/admin/email/post.js +74 -0
  259. package/dist/manager/routes/admin/firestore/get.js +35 -0
  260. package/dist/manager/routes/admin/firestore/post.js +56 -0
  261. package/dist/manager/routes/admin/firestore/query/post.js +128 -0
  262. package/dist/manager/routes/admin/hook/post.js +104 -0
  263. package/dist/manager/routes/admin/infer-contact/post.js +35 -0
  264. package/dist/manager/routes/admin/notification/post.js +37 -0
  265. package/dist/manager/routes/admin/payment/post.js +56 -0
  266. package/dist/manager/routes/admin/post/deduplicate-image-alts.js +52 -0
  267. package/dist/manager/routes/admin/post/dispatch-deploy.js +36 -0
  268. package/dist/manager/routes/admin/post/post.js +487 -0
  269. package/dist/manager/routes/admin/post/put.js +146 -0
  270. package/dist/manager/routes/admin/post/templates/post.html +14 -0
  271. package/dist/manager/routes/admin/repo/content/post.js +106 -0
  272. package/dist/manager/routes/admin/stats/get.js +222 -0
  273. package/dist/manager/routes/admin/users/disable/post.js +50 -0
  274. package/dist/manager/routes/admin/users/list/get.js +110 -0
  275. package/dist/manager/routes/admin/users/sync/post.js +119 -0
  276. package/dist/manager/routes/brand/get.js +26 -0
  277. package/dist/manager/routes/content/post/get.js +110 -0
  278. package/dist/manager/routes/general/email/post.js +89 -0
  279. package/dist/manager/routes/general/email/templates/general/download-app-link.js +42 -0
  280. package/dist/manager/routes/general/uuid/post.js +31 -0
  281. package/dist/manager/routes/handler/post/post.js +142 -0
  282. package/dist/manager/routes/health/get.js +43 -0
  283. package/dist/manager/routes/index.js +11 -0
  284. package/dist/manager/routes/marketing/campaign/delete.js +45 -0
  285. package/dist/manager/routes/marketing/campaign/get.js +69 -0
  286. package/dist/manager/routes/marketing/campaign/post.js +124 -0
  287. package/dist/manager/routes/marketing/campaign/put.js +86 -0
  288. package/dist/manager/routes/marketing/campaign/utils.js +59 -0
  289. package/dist/manager/routes/marketing/contact/delete.js +100 -0
  290. package/dist/manager/routes/marketing/contact/post.js +149 -0
  291. package/dist/manager/routes/marketing/contact/put.js +37 -0
  292. package/dist/manager/routes/marketing/email-preferences/post.js +275 -0
  293. package/dist/manager/routes/marketing/webhook/forward/post.js +170 -0
  294. package/dist/manager/routes/marketing/webhook/post.js +123 -0
  295. package/dist/manager/routes/marketing/webhook/providers/beehiiv.js +197 -0
  296. package/dist/manager/routes/marketing/webhook/providers/sendgrid.js +194 -0
  297. package/dist/manager/routes/payments/cancel/_is-trialing.js +40 -0
  298. package/dist/manager/routes/payments/cancel/post.js +149 -0
  299. package/dist/manager/routes/payments/cancel/providers/chargebee.js +41 -0
  300. package/dist/manager/routes/payments/cancel/providers/paypal.js +56 -0
  301. package/dist/manager/routes/payments/cancel/providers/stripe.js +33 -0
  302. package/dist/manager/routes/payments/cancel/providers/test.js +118 -0
  303. package/dist/manager/routes/payments/discount/get.js +25 -0
  304. package/dist/manager/routes/payments/dispute-alert/post.js +117 -0
  305. package/dist/manager/routes/payments/dispute-alert/providers/chargeblast.js +55 -0
  306. package/dist/manager/routes/payments/intent/post.js +341 -0
  307. package/dist/manager/routes/payments/intent/providers/chargebee.js +197 -0
  308. package/dist/manager/routes/payments/intent/providers/coinbase.js +94 -0
  309. package/dist/manager/routes/payments/intent/providers/paypal.js +244 -0
  310. package/dist/manager/routes/payments/intent/providers/stripe.js +141 -0
  311. package/dist/manager/routes/payments/intent/providers/test.js +341 -0
  312. package/dist/manager/routes/payments/plan/post.js +147 -0
  313. package/dist/manager/routes/payments/plan/providers/chargebee.js +62 -0
  314. package/dist/manager/routes/payments/plan/providers/paypal.js +62 -0
  315. package/dist/manager/routes/payments/plan/providers/stripe.js +59 -0
  316. package/dist/manager/routes/payments/plan/providers/test.js +113 -0
  317. package/dist/manager/routes/payments/portal/post.js +113 -0
  318. package/dist/manager/routes/payments/portal/providers/chargebee.js +62 -0
  319. package/dist/manager/routes/payments/portal/providers/paypal.js +25 -0
  320. package/dist/manager/routes/payments/portal/providers/stripe.js +62 -0
  321. package/dist/manager/routes/payments/portal/providers/test.js +18 -0
  322. package/dist/manager/routes/payments/refund/post.js +207 -0
  323. package/dist/manager/routes/payments/refund/providers/chargebee.js +147 -0
  324. package/dist/manager/routes/payments/refund/providers/coinbase.js +40 -0
  325. package/dist/manager/routes/payments/refund/providers/paypal.js +239 -0
  326. package/dist/manager/routes/payments/refund/providers/stripe.js +160 -0
  327. package/dist/manager/routes/payments/refund/providers/test.js +204 -0
  328. package/dist/manager/routes/payments/trial-eligibility/get.js +29 -0
  329. package/dist/manager/routes/payments/uncancel/post.js +106 -0
  330. package/dist/manager/routes/payments/uncancel/providers/chargebee.js +30 -0
  331. package/dist/manager/routes/payments/uncancel/providers/paypal.js +25 -0
  332. package/dist/manager/routes/payments/uncancel/providers/stripe.js +27 -0
  333. package/dist/manager/routes/payments/uncancel/providers/test.js +114 -0
  334. package/dist/manager/routes/payments/webhook/post.js +157 -0
  335. package/dist/manager/routes/payments/webhook/providers/chargebee.js +218 -0
  336. package/dist/manager/routes/payments/webhook/providers/coinbase.js +91 -0
  337. package/dist/manager/routes/payments/webhook/providers/paypal.js +214 -0
  338. package/dist/manager/routes/payments/webhook/providers/stripe.js +182 -0
  339. package/dist/manager/routes/payments/webhook/providers/test.js +18 -0
  340. package/dist/manager/routes/payments/winback/post.js +276 -0
  341. package/dist/manager/routes/payments/winback/providers/chargebee.js +24 -0
  342. package/dist/manager/routes/payments/winback/providers/paypal.js +22 -0
  343. package/dist/manager/routes/payments/winback/providers/stripe.js +41 -0
  344. package/dist/manager/routes/payments/winback/providers/test.js +42 -0
  345. package/dist/manager/routes/restart/index.js +37 -0
  346. package/dist/manager/routes/special/electron-client/post.js +59 -0
  347. package/dist/manager/routes/test/authenticate/get.js +11 -0
  348. package/dist/manager/routes/test/health/get.js +8 -0
  349. package/dist/manager/routes/test/index.js +16 -0
  350. package/dist/manager/routes/test/lab/post.js +17 -0
  351. package/dist/manager/routes/test/redirect/get.js +27 -0
  352. package/dist/manager/routes/test/reset-account/post.js +79 -0
  353. package/dist/manager/routes/test/roster/get.js +40 -0
  354. package/dist/manager/routes/test/schema/post.js +20 -0
  355. package/dist/manager/routes/test/usage/post.js +44 -0
  356. package/dist/manager/routes/test/webhook/post.js +21 -0
  357. package/dist/manager/routes/user/api-keys/post.js +51 -0
  358. package/dist/manager/routes/user/connections/_context.js +194 -0
  359. package/dist/manager/routes/user/connections/_grant.js +247 -0
  360. package/dist/manager/routes/user/connections/_lease.js +184 -0
  361. package/dist/manager/routes/user/connections/_providers.js +191 -0
  362. package/dist/manager/routes/user/connections/_state.js +117 -0
  363. package/dist/manager/routes/user/connections/delete.js +55 -0
  364. package/dist/manager/routes/user/connections/get.js +167 -0
  365. package/dist/manager/routes/user/connections/post.js +370 -0
  366. package/dist/manager/routes/user/connections/providers/discord.js +41 -0
  367. package/dist/manager/routes/user/connections/providers/google.js +55 -0
  368. package/dist/manager/routes/user/connections/providers/kick.js +68 -0
  369. package/dist/manager/routes/user/connections/providers/spotify.js +40 -0
  370. package/dist/manager/routes/user/connections/providers/twitch.js +52 -0
  371. package/dist/manager/routes/user/data-request/delete.js +84 -0
  372. package/dist/manager/routes/user/data-request/get.js +229 -0
  373. package/dist/manager/routes/user/data-request/post.js +136 -0
  374. package/dist/manager/routes/user/delete.js +149 -0
  375. package/dist/manager/routes/user/feedback/post.js +81 -0
  376. package/dist/manager/routes/user/get.js +14 -0
  377. package/dist/manager/routes/user/orders/get.js +95 -0
  378. package/dist/manager/routes/user/sessions/delete.js +95 -0
  379. package/dist/manager/routes/user/sessions/get.js +40 -0
  380. package/dist/manager/routes/user/settings/validate/post.js +86 -0
  381. package/dist/manager/routes/user/signup/post.js +662 -0
  382. package/dist/manager/routes/user/subscription/get.js +77 -0
  383. package/dist/manager/routes/user/token/post.js +30 -0
  384. package/dist/manager/routes/verts/delete.js +43 -0
  385. package/dist/manager/routes/verts/get.js +49 -0
  386. package/dist/manager/routes/verts/post.js +61 -0
  387. package/dist/manager/routes/verts/put.js +75 -0
  388. package/dist/manager/routes/verts/redirect/get.js +36 -0
  389. package/dist/manager/routes/verts/serve/get.js +67 -0
  390. package/dist/manager/routes/verts/utils.js +436 -0
  391. package/dist/manager/schemas/admin/backup/post.js +8 -0
  392. package/dist/manager/schemas/admin/cron/post.js +8 -0
  393. package/dist/manager/schemas/admin/database/get.js +5 -0
  394. package/dist/manager/schemas/admin/database/post.js +6 -0
  395. package/dist/manager/schemas/admin/email/post.js +31 -0
  396. package/dist/manager/schemas/admin/firestore/get.js +5 -0
  397. package/dist/manager/schemas/admin/firestore/post.js +8 -0
  398. package/dist/manager/schemas/admin/firestore/query/post.js +5 -0
  399. package/dist/manager/schemas/admin/hook/post.js +8 -0
  400. package/dist/manager/schemas/admin/infer-contact/post.js +6 -0
  401. package/dist/manager/schemas/admin/notification/post.js +23 -0
  402. package/dist/manager/schemas/admin/payment/post.js +8 -0
  403. package/dist/manager/schemas/admin/post/post.js +23 -0
  404. package/dist/manager/schemas/admin/post/put.js +13 -0
  405. package/dist/manager/schemas/admin/repo/content/post.js +10 -0
  406. package/dist/manager/schemas/admin/stats/get.js +5 -0
  407. package/dist/manager/schemas/admin/users/disable/post.js +9 -0
  408. package/dist/manager/schemas/admin/users/list/get.js +10 -0
  409. package/dist/manager/schemas/admin/users/sync/post.js +7 -0
  410. package/dist/manager/schemas/brand/get.js +3 -0
  411. package/dist/manager/schemas/content/post/get.js +8 -0
  412. package/dist/manager/schemas/general/email/post.js +10 -0
  413. package/dist/manager/schemas/general/uuid/post.js +9 -0
  414. package/dist/manager/schemas/handler/post/post.js +13 -0
  415. package/dist/manager/schemas/health/get.js +3 -0
  416. package/dist/manager/schemas/marketing/campaign/delete.js +8 -0
  417. package/dist/manager/schemas/marketing/campaign/get.js +13 -0
  418. package/dist/manager/schemas/marketing/campaign/post.js +43 -0
  419. package/dist/manager/schemas/marketing/campaign/put.js +37 -0
  420. package/dist/manager/schemas/marketing/contact/delete.js +8 -0
  421. package/dist/manager/schemas/marketing/contact/post.js +14 -0
  422. package/dist/manager/schemas/marketing/contact/put.js +8 -0
  423. package/dist/manager/schemas/marketing/email-preferences/post.js +15 -0
  424. package/dist/manager/schemas/marketing/webhook/forward/post.js +8 -0
  425. package/dist/manager/schemas/marketing/webhook/post.js +7 -0
  426. package/dist/manager/schemas/payments/cancel/post.js +15 -0
  427. package/dist/manager/schemas/payments/discount/get.js +8 -0
  428. package/dist/manager/schemas/payments/dispute-alert/post.js +6 -0
  429. package/dist/manager/schemas/payments/intent/post.js +28 -0
  430. package/dist/manager/schemas/payments/plan/post.js +11 -0
  431. package/dist/manager/schemas/payments/portal/post.js +12 -0
  432. package/dist/manager/schemas/payments/refund/post.js +14 -0
  433. package/dist/manager/schemas/payments/trial-eligibility/get.js +7 -0
  434. package/dist/manager/schemas/payments/uncancel/post.js +9 -0
  435. package/dist/manager/schemas/payments/webhook/post.js +6 -0
  436. package/dist/manager/schemas/payments/winback/post.js +12 -0
  437. package/dist/manager/schemas/restart/index.js +5 -0
  438. package/dist/manager/schemas/special/electron-client/post.js +11 -0
  439. package/dist/manager/schemas/test/authenticate/get.js +3 -0
  440. package/dist/manager/schemas/test/health/get.js +3 -0
  441. package/dist/manager/schemas/test/index.js +5 -0
  442. package/dist/manager/schemas/test/lab/post.js +3 -0
  443. package/dist/manager/schemas/test/redirect/get.js +7 -0
  444. package/dist/manager/schemas/test/reset-account/post.js +3 -0
  445. package/dist/manager/schemas/test/roster/get.js +3 -0
  446. package/dist/manager/schemas/test/schema/post.js +70 -0
  447. package/dist/manager/schemas/test/usage/post.js +8 -0
  448. package/dist/manager/schemas/test/webhook/post.js +7 -0
  449. package/dist/manager/schemas/user/api-keys/post.js +6 -0
  450. package/dist/manager/schemas/user/connections/delete.js +8 -0
  451. package/dist/manager/schemas/user/connections/get.js +21 -0
  452. package/dist/manager/schemas/user/connections/post.js +12 -0
  453. package/dist/manager/schemas/user/data-request/delete.js +3 -0
  454. package/dist/manager/schemas/user/data-request/get.js +5 -0
  455. package/dist/manager/schemas/user/data-request/post.js +6 -0
  456. package/dist/manager/schemas/user/delete.js +6 -0
  457. package/dist/manager/schemas/user/feedback/post.js +8 -0
  458. package/dist/manager/schemas/user/get.js +3 -0
  459. package/dist/manager/schemas/user/orders/get.js +6 -0
  460. package/dist/manager/schemas/user/sessions/delete.js +6 -0
  461. package/dist/manager/schemas/user/sessions/get.js +6 -0
  462. package/dist/manager/schemas/user/settings/validate/post.js +8 -0
  463. package/dist/manager/schemas/user/signup/post.js +27 -0
  464. package/dist/manager/schemas/user/subscription/get.js +5 -0
  465. package/dist/manager/schemas/user/token/post.js +5 -0
  466. package/dist/manager/schemas/verts/delete.js +8 -0
  467. package/dist/manager/schemas/verts/get.js +9 -0
  468. package/dist/manager/schemas/verts/post.js +23 -0
  469. package/dist/manager/schemas/verts/put.js +20 -0
  470. package/dist/manager/schemas/verts/redirect/get.js +10 -0
  471. package/dist/manager/schemas/verts/serve/get.js +14 -0
  472. package/dist/manager/server-manager.js +69 -0
  473. package/dist/mcp/client.js +98 -0
  474. package/dist/mcp/handler.js +521 -0
  475. package/dist/mcp/index.js +157 -0
  476. package/dist/mcp/tools.js +555 -0
  477. package/dist/mcp/utils.js +108 -0
  478. package/dist/omega-bin.js +6 -0
  479. package/dist/require.js +3 -0
  480. package/dist/test/fixtures/firebase-project/.firebaserc +5 -0
  481. package/dist/test/fixtures/firebase-project/config/omega.json5 +131 -0
  482. package/dist/test/fixtures/firebase-project/database.rules.json +6 -0
  483. package/dist/test/fixtures/firebase-project/firebase.json +49 -0
  484. package/dist/test/fixtures/firebase-project/firestore.indexes.json +4 -0
  485. package/dist/test/fixtures/firebase-project/package.json +14 -0
  486. package/dist/test/fixtures/firebase-project/src/index.js +11 -0
  487. package/dist/test/fixtures/firebase-project/storage.rules +8 -0
  488. package/dist/test/parse-audit.js +25 -0
  489. package/dist/test/run-tests.js +90 -0
  490. package/dist/test/runner.js +982 -0
  491. package/dist/test/seed.js +242 -0
  492. package/dist/test/test-accounts.js +2660 -0
  493. package/dist/test/utils/assertions.js +189 -0
  494. package/dist/test/utils/email-capture.js +252 -0
  495. package/dist/test/utils/extended-mode-warning.js +11 -0
  496. package/dist/test/utils/firestore-rules-client.js +277 -0
  497. package/dist/test/utils/http-client.js +228 -0
  498. package/dist/test/utils/test-mode-file.js +192 -0
  499. package/dist/utils/merge-line-files.js +6 -0
  500. package/dist/utils/scaffold-defaults.js +71 -0
  501. package/dist/utils/test-lanes.js +62 -0
  502. package/dist/vendor/account/engine.js +182 -0
  503. package/dist/vendor/account/features.js +220 -0
  504. package/dist/vendor/account/index.js +53 -0
  505. package/dist/vendor/account/schema.js +272 -0
  506. package/dist/vendor/account/subscription.js +38 -0
  507. package/dist/vendor/analytics/adapters/ga4.js +26 -0
  508. package/dist/vendor/analytics/adapters/meta.js +26 -0
  509. package/dist/vendor/analytics/adapters/resolve.js +130 -0
  510. package/dist/vendor/analytics/adapters/tiktok.js +27 -0
  511. package/dist/vendor/analytics/catalog.js +908 -0
  512. package/dist/vendor/analytics/identity.js +136 -0
  513. package/dist/vendor/config/company.js +31 -0
  514. package/dist/vendor/config/defaults.js +173 -0
  515. package/dist/vendor/config/demo.js +18 -0
  516. package/dist/vendor/config/desktop-artifacts.js +110 -0
  517. package/dist/vendor/config/edit.js +769 -0
  518. package/dist/vendor/config/env-delivery.js +145 -0
  519. package/dist/vendor/config/env-rules.js +93 -0
  520. package/dist/vendor/config/env-schema.js +1078 -0
  521. package/dist/vendor/config/env.js +445 -0
  522. package/dist/vendor/config/hooks.js +97 -0
  523. package/dist/vendor/config/index.js +237 -0
  524. package/dist/vendor/config/instances.js +208 -0
  525. package/dist/vendor/config/load.js +490 -0
  526. package/dist/vendor/config/merge.js +68 -0
  527. package/dist/vendor/config/order.js +139 -0
  528. package/dist/vendor/config/ports.js +374 -0
  529. package/dist/vendor/config/providers.js +32 -0
  530. package/dist/vendor/config/repo.js +142 -0
  531. package/dist/vendor/config/retired-keys.js +430 -0
  532. package/dist/vendor/config/schema.js +1610 -0
  533. package/dist/vendor/config/secrets.js +50 -0
  534. package/dist/vendor/config/seed.js +34 -0
  535. package/dist/vendor/config/site-global.js +205 -0
  536. package/dist/vendor/config/validate.js +554 -0
  537. package/dist/vendor/config/winback.js +61 -0
  538. package/dist/vendor/devkit/attach-log-file.js +262 -0
  539. package/dist/vendor/devkit/bare-requires.js +153 -0
  540. package/dist/vendor/devkit/command-path.js +46 -0
  541. package/dist/vendor/devkit/defaults-engine.js +372 -0
  542. package/dist/vendor/devkit/deploy-record.js +180 -0
  543. package/dist/vendor/devkit/deploy.js +286 -0
  544. package/dist/vendor/devkit/license.js +155 -0
  545. package/dist/vendor/devkit/local-https.js +360 -0
  546. package/dist/vendor/devkit/local.js +1905 -0
  547. package/dist/vendor/devkit/logger.js +128 -0
  548. package/dist/vendor/devkit/merge-line-files.js +296 -0
  549. package/dist/vendor/devkit/npm-registry.js +52 -0
  550. package/dist/vendor/devkit/omega-bin.js +345 -0
  551. package/dist/vendor/devkit/parse-audit.js +74 -0
  552. package/dist/vendor/devkit/safe-install.js +18 -0
  553. package/dist/vendor/devkit/scaffold-guard.js +96 -0
  554. package/dist/vendor/devkit/stop-signals.js +28 -0
  555. package/dist/vendor/devkit/test/assert.js +120 -0
  556. package/dist/vendor/devkit/test/define-cases.js +104 -0
  557. package/dist/vendor/devkit/test/runner-core.js +554 -0
  558. package/dist/vendor/devkit/test/scope.js +162 -0
  559. package/dist/vendor/devkit/update.js +569 -0
  560. package/dist/vendor/monitoring/core.js +180 -0
  561. package/dist/vendor/monitoring/env.js +49 -0
  562. package/dist/vendor/monitoring/logger.js +39 -0
  563. package/dist/vendor/monitoring/node.js +72 -0
  564. package/docs/admin-post-route.md +57 -0
  565. package/docs/ai-library.md +171 -0
  566. package/docs/architecture.md +62 -0
  567. package/docs/audit.md +70 -0
  568. package/docs/auth-hooks.md +74 -0
  569. package/docs/build-system.md +65 -0
  570. package/docs/cdp-debugging.md +29 -0
  571. package/docs/cli-firestore-auth.md +85 -0
  572. package/docs/cli-logs.md +67 -0
  573. package/docs/cli-output.md +146 -0
  574. package/docs/code-patterns.md +77 -0
  575. package/docs/common-mistakes.md +12 -0
  576. package/docs/common-operations.md +60 -0
  577. package/docs/connections.md +212 -0
  578. package/docs/consent.md +362 -0
  579. package/docs/directory-structure.md +150 -0
  580. package/docs/email-system.md +459 -0
  581. package/docs/environment-detection.md +93 -0
  582. package/docs/file-naming.md +10 -0
  583. package/docs/firestore.md +134 -0
  584. package/docs/ghostii.md +240 -0
  585. package/docs/index.md +299 -0
  586. package/docs/key-files.md +37 -0
  587. package/docs/logging.md +60 -0
  588. package/docs/marketing-campaigns.md +407 -0
  589. package/docs/marketing-fields.md +25 -0
  590. package/docs/mcp.md +222 -0
  591. package/docs/migration.md +129 -0
  592. package/docs/payment-system.md +856 -0
  593. package/docs/paypal-sandbox-qa.md +105 -0
  594. package/docs/response-headers.md +9 -0
  595. package/docs/routes.md +211 -0
  596. package/docs/sanitization.md +71 -0
  597. package/docs/schemas.md +163 -0
  598. package/docs/shared/agent-docs.md +89 -0
  599. package/docs/shared/analytics.md +612 -0
  600. package/docs/shared/brands.md +51 -0
  601. package/docs/shared/breaking-changes.md +497 -0
  602. package/docs/shared/config.md +1387 -0
  603. package/docs/shared/deploys.md +215 -0
  604. package/docs/shared/icons.md +201 -0
  605. package/docs/shared/local-dev.md +147 -0
  606. package/docs/shared/logging.md +202 -0
  607. package/docs/shared/monitoring.md +153 -0
  608. package/docs/shared/publishing.md +183 -0
  609. package/docs/shared/rulings.md +34 -0
  610. package/docs/shared/testing.md +147 -0
  611. package/docs/shared/theming.md +604 -0
  612. package/docs/shared/translation.md +291 -0
  613. package/docs/shared/updates.md +61 -0
  614. package/docs/stripe-webhook-forwarding.md +20 -0
  615. package/docs/test-boot-layer.md +67 -0
  616. package/docs/test-framework.md +583 -0
  617. package/docs/usage-rate-limiting.md +121 -0
  618. package/docs/verts.md +29 -0
  619. package/package.json +143 -0
  620. package/templates/config/omega.json5 +322 -0
  621. package/templates/database.rules.json +82 -0
  622. package/templates/firebase.json +67 -0
  623. package/templates/firestore.framework.rules +191 -0
  624. package/templates/firestore.indexes.json +4 -0
  625. package/templates/firestore.rules +59 -0
  626. package/templates/index.js +12 -0
  627. package/templates/public/404.html +26 -0
  628. package/templates/public/index.html +24 -0
  629. package/templates/remoteconfig.template.json +1 -0
  630. package/templates/storage-lifecycle-config-1-day.json +9 -0
  631. package/templates/storage-lifecycle-config-30-days.json +9 -0
  632. package/templates/storage.rules +11 -0
  633. package/test/_init/accounts-validation.js +58 -0
  634. package/test/ai/tools-live.test.js +171 -0
  635. package/test/analytics/conversion-delivery.test.js +1013 -0
  636. package/test/analytics/match-normalization.test.js +238 -0
  637. package/test/analytics/signup-conversion.test.js +135 -0
  638. package/test/boot/cli-dispatch.test.js +142 -0
  639. package/test/boot/defaults-scaffold.test.js +248 -0
  640. package/test/boot/deploy-staging.test.js +446 -0
  641. package/test/boot/disposable-domains.test.js +154 -0
  642. package/test/boot/emulator-boots.test.js +39 -0
  643. package/test/boot/emulator-port-preflight.test.js +126 -0
  644. package/test/boot/emulator-ready-timeout.test.js +46 -0
  645. package/test/boot/project-id-refresh.test.js +73 -0
  646. package/test/boot/suite-portability.test.js +278 -0
  647. package/test/boot/update-command.test.js +63 -0
  648. package/test/cli/cross-platform.test.js +168 -0
  649. package/test/cli/custom-project-type.test.js +426 -0
  650. package/test/cli/deploy-license-env.test.js +100 -0
  651. package/test/cli/emulator-adoption.test.js +254 -0
  652. package/test/cli/emulator-orphans.test.js +658 -0
  653. package/test/cli/emulator-ownership.test.js +177 -0
  654. package/test/cli/emulator-port-retry.test.js +395 -0
  655. package/test/cli/emulator-shutdown.test.js +788 -0
  656. package/test/cli/emulator-stale-reap.test.js +574 -0
  657. package/test/cli/ensure-target.test.js +325 -0
  658. package/test/cli/flags.test.js +64 -0
  659. package/test/cli/https-trust.test.js +38 -0
  660. package/test/cli/install-update-dispatch.test.js +47 -0
  661. package/test/cli/lane-environments.test.js +159 -0
  662. package/test/cli/marketing-campaigns-seeded.test.js +194 -0
  663. package/test/cli/migrate-bare-requires.test.js +128 -0
  664. package/test/cli/migrate-markers.test.js +530 -0
  665. package/test/cli/required-indexes.test.js +64 -0
  666. package/test/cli/rules-compile.test.js +622 -0
  667. package/test/cli/rules-migration-deferral.test.js +259 -0
  668. package/test/cli/rules-version.test.js +104 -0
  669. package/test/cli/setup-load-files.test.js +66 -0
  670. package/test/cli/setup-offline-mode.test.js +276 -0
  671. package/test/cli/setup-retired.test.js +64 -0
  672. package/test/cli/setup-shared-project-indexes.test.js +262 -0
  673. package/test/cli/setup-tests-requires.test.js +45 -0
  674. package/test/cli/stage-env-compose.test.js +316 -0
  675. package/test/cli/stripe-live-lane.test.js +303 -0
  676. package/test/cli/target.test.js +182 -0
  677. package/test/cli/templates.test.js +66 -0
  678. package/test/cli/test-runner-env.test.js +115 -0
  679. package/test/cli/test-stack-shutdown.test.js +275 -0
  680. package/test/cli/test-target-no-match.test.js +128 -0
  681. package/test/cli/verb-logs.test.js +118 -0
  682. package/test/cli/version-dispatch.test.js +39 -0
  683. package/test/content/blog-generate.test.js +164 -0
  684. package/test/email/campaign-config-fault.test.js +376 -0
  685. package/test/email/campaign-cron-pipeline.test.js +536 -0
  686. package/test/email/campaign-send.test.js +47 -0
  687. package/test/email/consent-lifecycle.test.js +258 -0
  688. package/test/email/content-html-policy.test.js +270 -0
  689. package/test/email/feedback-and-plain-send.test.js +54 -0
  690. package/test/email/fixtures/clean.json +30 -0
  691. package/test/email/fixtures/editorial.json +30 -0
  692. package/test/email/fixtures/field-report.json +53 -0
  693. package/test/email/identity.test.js +321 -0
  694. package/test/email/marketing/consent-gate.test.js +265 -0
  695. package/test/email/marketing/custom-fields-catalog.test.js +259 -0
  696. package/test/email/marketing/prune-per-provider.test.js +762 -0
  697. package/test/email/marketing/remove-log-privacy.test.js +225 -0
  698. package/test/email/marketing-lifecycle.test.js +139 -0
  699. package/test/email/newsletter-generate.test.js +853 -0
  700. package/test/email/newsletter-svg-tokens.test.js +51 -0
  701. package/test/email/newsletter-templates.test.js +492 -0
  702. package/test/email/order-one-time-cta.test.js +97 -0
  703. package/test/email/render-content.test.js +248 -0
  704. package/test/email/safe-url.test.js +0 -0
  705. package/test/email/sanitize-images.test.js +66 -0
  706. package/test/email/send-log-privacy.test.js +107 -0
  707. package/test/email/templates.test.js +288 -0
  708. package/test/email/testing-capture.test.js +260 -0
  709. package/test/email/transactional-send.test.js +36 -0
  710. package/test/email/transactional.test.js +562 -0
  711. package/test/email/unsubscribe-groups.test.js +155 -0
  712. package/test/email/unsubscribe-key.test.js +91 -0
  713. package/test/email/validation-cases.test.js +152 -0
  714. package/test/email/validation.test.js +746 -0
  715. package/test/events/auth-delete-conversion.test.js +138 -0
  716. package/test/events/auth-delete-race.test.js +211 -0
  717. package/test/events/auth-on-create-log.test.js +164 -0
  718. package/test/events/auth-signup-conversion.test.js +141 -0
  719. package/test/events/auth-signup-limit.test.js +142 -0
  720. package/test/events/auth-trigger-log-privacy.test.js +256 -0
  721. package/test/events/cron-job-doc-shape.test.js +101 -0
  722. package/test/events/cron-reset-usage.test.js +116 -0
  723. package/test/events/notification-conversion.test.js +182 -0
  724. package/test/events/payments/_webhook-harness.js +307 -0
  725. package/test/events/payments/abandoned-cart-activity.test.js +135 -0
  726. package/test/events/payments/analytics-payment-events.test.js +945 -0
  727. package/test/events/payments/dispute-email-status.test.js +55 -0
  728. package/test/events/payments/journey-payments-abandoned.test.js +126 -0
  729. package/test/events/payments/journey-payments-cancel-endpoint.test.js +102 -0
  730. package/test/events/payments/journey-payments-cancel-no-order.test.js +76 -0
  731. package/test/events/payments/journey-payments-cancel.test.js +184 -0
  732. package/test/events/payments/journey-payments-decline.test.js +225 -0
  733. package/test/events/payments/journey-payments-discount.test.js +91 -0
  734. package/test/events/payments/journey-payments-dispute.test.js +190 -0
  735. package/test/events/payments/journey-payments-failure.test.js +148 -0
  736. package/test/events/payments/journey-payments-legacy-product.test.js +151 -0
  737. package/test/events/payments/journey-payments-one-time-decline.test.js +127 -0
  738. package/test/events/payments/journey-payments-one-time-failure.test.js +113 -0
  739. package/test/events/payments/journey-payments-one-time-refund.test.js +186 -0
  740. package/test/events/payments/journey-payments-one-time.test.js +175 -0
  741. package/test/events/payments/journey-payments-plan-change.test.js +146 -0
  742. package/test/events/payments/journey-payments-plan-switch-trial.test.js +161 -0
  743. package/test/events/payments/journey-payments-plan-switch.test.js +182 -0
  744. package/test/events/payments/journey-payments-refund-no-order.test.js +76 -0
  745. package/test/events/payments/journey-payments-refund-webhook.test.js +225 -0
  746. package/test/events/payments/journey-payments-suspend.test.js +183 -0
  747. package/test/events/payments/journey-payments-trial-cancel.test.js +132 -0
  748. package/test/events/payments/journey-payments-trial.test.js +173 -0
  749. package/test/events/payments/journey-payments-uid-resolution.test.js +134 -0
  750. package/test/events/payments/journey-payments-uncancel.test.js +157 -0
  751. package/test/events/payments/journey-payments-upgrade.test.js +137 -0
  752. package/test/events/payments/journey-payments-winback-decline.test.js +171 -0
  753. package/test/events/payments/journey-payments-winback.test.js +195 -0
  754. package/test/events/payments/paypal-expiry-cron.test.js +166 -0
  755. package/test/events/payments/purchase-failed-handler.test.js +80 -0
  756. package/test/events/payments/purchase-refunded-handler.test.js +108 -0
  757. package/test/events/payments/test-processor-doc-shape.test.js +162 -0
  758. package/test/events/payments/transition-order-emails.test.js +185 -0
  759. package/test/events/payments/transition-promo-lines.test.js +192 -0
  760. package/test/events/payments/transitions-detect.test.js +767 -0
  761. package/test/events/payments/trial-lapse-sweep-staleness.test.js +173 -0
  762. package/test/events/payments/trial-lapse-sweep.test.js +359 -0
  763. package/test/events/payments/webhook-atomic-writes.test.js +69 -0
  764. package/test/events/payments/webhook-chargebee-unreachable.test.js +106 -0
  765. package/test/events/payments/webhook-discount-clear.test.js +189 -0
  766. package/test/events/payments/webhook-failure-intent.test.js +93 -0
  767. package/test/events/payments/webhook-hosted-page-uid-trust.test.js +212 -0
  768. package/test/events/payments/webhook-ordering.test.js +326 -0
  769. package/test/events/payments/webhook-provider-lookup-trust.test.js +206 -0
  770. package/test/events/payments/webhook-refund-amount-trust.test.js +223 -0
  771. package/test/events/payments/webhook-refund-envelope-shape.test.js +148 -0
  772. package/test/events/payments/webhook-refund-linkage-trust.test.js +404 -0
  773. package/test/events/payments/webhook-refund-transaction-fallback.test.js +184 -0
  774. package/test/events/payments/webhook-refund-without-order.test.js +170 -0
  775. package/test/events/payments/webhook-refusal-reporting.test.js +194 -0
  776. package/test/events/payments/webhook-refused-intent.test.js +142 -0
  777. package/test/events/payments/webhook-retry-sweep.test.js +252 -0
  778. package/test/events/payments/webhook-transition-claim.test.js +389 -0
  779. package/test/events/payments/webhook-uid-trust.test.js +203 -0
  780. package/test/events/payments/webhook-user-without-auth.test.js +161 -0
  781. package/test/fixtures/chargebee/invoice-one-time.json +27 -0
  782. package/test/fixtures/chargebee/subscription-active.json +44 -0
  783. package/test/fixtures/chargebee/subscription-cancelled.json +42 -0
  784. package/test/fixtures/chargebee/subscription-in-trial.json +41 -0
  785. package/test/fixtures/chargebee/subscription-legacy-plan.json +41 -0
  786. package/test/fixtures/chargebee/subscription-non-renewing.json +41 -0
  787. package/test/fixtures/chargebee/subscription-paused.json +42 -0
  788. package/test/fixtures/chargebee/webhook-payment-failed.json +51 -0
  789. package/test/fixtures/chargebee/webhook-subscription-created.json +47 -0
  790. package/test/fixtures/coinbase/charge-confirmed.json +50 -0
  791. package/test/fixtures/coinbase/charge-failed.json +34 -0
  792. package/test/fixtures/coinbase/charge-pending.json +34 -0
  793. package/test/fixtures/migrate/ported-route.js.txt +11 -0
  794. package/test/fixtures/paypal/capture-completed.json +51 -0
  795. package/test/fixtures/paypal/capture-refunded.json +43 -0
  796. package/test/fixtures/paypal/order-approved.json +62 -0
  797. package/test/fixtures/paypal/order-completed.json +110 -0
  798. package/test/fixtures/paypal/sale-refunded.json +38 -0
  799. package/test/fixtures/paypal/subscription-active.json +76 -0
  800. package/test/fixtures/paypal/subscription-cancelled.json +50 -0
  801. package/test/fixtures/paypal/subscription-suspended.json +65 -0
  802. package/test/fixtures/stripe/checkout-session-completed.json +130 -0
  803. package/test/fixtures/stripe/invoice-payment-failed.json +148 -0
  804. package/test/fixtures/stripe/invoice-subscription-payment-failed.json +28 -0
  805. package/test/fixtures/stripe/invoice-subscription-payment-succeeded.json +28 -0
  806. package/test/fixtures/stripe/subscription-active.json +161 -0
  807. package/test/fixtures/stripe/subscription-canceled.json +161 -0
  808. package/test/fixtures/stripe/subscription-trialing.json +161 -0
  809. package/test/helpers/_shared-config.js +15 -0
  810. package/test/helpers/ai-request-payload.test.js +619 -0
  811. package/test/helpers/ai-schema-resolve.test.js +125 -0
  812. package/test/helpers/ai-test-provider.test.js +227 -0
  813. package/test/helpers/ai-token-accounting.test.js +150 -0
  814. package/test/helpers/ai-tools-format.test.js +384 -0
  815. package/test/helpers/analytics-no-id-notice.test.js +146 -0
  816. package/test/helpers/analytics-user-data.test.js +194 -0
  817. package/test/helpers/api-manager.test.js +311 -0
  818. package/test/helpers/backend-router.test.js +136 -0
  819. package/test/helpers/content/blog-auto-publisher.test.js +482 -0
  820. package/test/helpers/content/feed-parser.test.js +529 -0
  821. package/test/helpers/content/ghostii-blocks.test.js +135 -0
  822. package/test/helpers/content/ghostii-feed-integration.test.js +405 -0
  823. package/test/helpers/content/ghostii-write-article.test.js +244 -0
  824. package/test/helpers/dev-only-routes.test.js +225 -0
  825. package/test/helpers/env-reader.test.js +362 -0
  826. package/test/helpers/environment.test.js +263 -0
  827. package/test/helpers/event-middleware.test.js +407 -0
  828. package/test/helpers/infer-contact.test.js +157 -0
  829. package/test/helpers/lane-url.test.js +90 -0
  830. package/test/helpers/merge-line-files.test.js +272 -0
  831. package/test/helpers/metadata.test.js +122 -0
  832. package/test/helpers/middleware-request-log.test.js +134 -0
  833. package/test/helpers/middleware-user-log.test.js +124 -0
  834. package/test/helpers/payment/chargebee/parse-webhook.test.js +490 -0
  835. package/test/helpers/payment/chargebee/refund-details.test.js +198 -0
  836. package/test/helpers/payment/chargebee/to-unified-one-time.test.js +148 -0
  837. package/test/helpers/payment/chargebee/to-unified-subscription.test.js +649 -0
  838. package/test/helpers/payment/coinbase/create-intent.test.js +247 -0
  839. package/test/helpers/payment/coinbase/parse-webhook.test.js +195 -0
  840. package/test/helpers/payment/coinbase/refund-unsupported.test.js +141 -0
  841. package/test/helpers/payment/coinbase/to-unified-one-time.test.js +201 -0
  842. package/test/helpers/payment/discount-codes.test.js +141 -0
  843. package/test/helpers/payment/extract-resource.test.js +113 -0
  844. package/test/helpers/payment/fetch-failure.test.js +215 -0
  845. package/test/helpers/payment/license-gate.test.js +108 -0
  846. package/test/helpers/payment/order-id.test.js +99 -0
  847. package/test/helpers/payment/paypal/create-intent.test.js +382 -0
  848. package/test/helpers/payment/paypal/fetch-capture.test.js +123 -0
  849. package/test/helpers/payment/paypal/fetch-sale.test.js +183 -0
  850. package/test/helpers/payment/paypal/parse-webhook.test.js +678 -0
  851. package/test/helpers/payment/paypal/refund-details.test.js +202 -0
  852. package/test/helpers/payment/paypal/resolve-plan-id.test.js +108 -0
  853. package/test/helpers/payment/paypal/switch-plan.test.js +92 -0
  854. package/test/helpers/payment/paypal/to-unified-one-time.test.js +383 -0
  855. package/test/helpers/payment/paypal/to-unified-subscription.test.js +884 -0
  856. package/test/helpers/payment/stripe/fetch-charge.test.js +147 -0
  857. package/test/helpers/payment/stripe/parse-webhook.test.js +448 -0
  858. package/test/helpers/payment/stripe/refund-details.test.js +147 -0
  859. package/test/helpers/payment/stripe/to-unified-one-time.test.js +307 -0
  860. package/test/helpers/payment/stripe/to-unified-subscription.test.js +709 -0
  861. package/test/helpers/persona-domain.test.js +87 -0
  862. package/test/helpers/recaptcha.test.js +166 -0
  863. package/test/helpers/resolved-config.test.js +106 -0
  864. package/test/helpers/response-log-redaction.test.js +262 -0
  865. package/test/helpers/roles.test.js +155 -0
  866. package/test/helpers/route-context-debug-gate.test.js +118 -0
  867. package/test/helpers/route-context-logging.test.js +214 -0
  868. package/test/helpers/route-context.test.js +129 -0
  869. package/test/helpers/safe-compare.test.js +114 -0
  870. package/test/helpers/sanitize.test.js +223 -0
  871. package/test/helpers/schema-engine.test.js +537 -0
  872. package/test/helpers/schema-zod.test.js +554 -0
  873. package/test/helpers/seed-accounts-load.test.js +163 -0
  874. package/test/helpers/seed-google-personas.test.js +114 -0
  875. package/test/helpers/seeded-personas.test.js +823 -0
  876. package/test/helpers/settings.test.js +292 -0
  877. package/test/helpers/setup-engines-pin.test.js +68 -0
  878. package/test/helpers/setup-manifest-sync.test.js +116 -0
  879. package/test/helpers/slugify.test.js +395 -0
  880. package/test/helpers/storage.test.js +195 -0
  881. package/test/helpers/test-banner-latch.test.js +139 -0
  882. package/test/helpers/usage-consume.test.js +460 -0
  883. package/test/helpers/usage-log-privacy.test.js +121 -0
  884. package/test/helpers/user-doc-heal.test.js +540 -0
  885. package/test/helpers/user.test.js +771 -0
  886. package/test/helpers/webhook-forward.test.js +419 -0
  887. package/test/helpers/wipe-auth-project.test.js +143 -0
  888. package/test/mcp/discovery.test.js +52 -0
  889. package/test/mcp/oauth.test.js +160 -0
  890. package/test/mcp/protocol.test.js +278 -0
  891. package/test/mcp/roles.test.js +203 -0
  892. package/test/mcp/utils.test.js +245 -0
  893. package/test/notification/identity.test.js +155 -0
  894. package/test/routes/admin/create-post.test.js +362 -0
  895. package/test/routes/admin/database.test.js +133 -0
  896. package/test/routes/admin/deduplicate-image-alts.test.js +191 -0
  897. package/test/routes/admin/email-content-html.test.js +58 -0
  898. package/test/routes/admin/email-request-log.test.js +86 -0
  899. package/test/routes/admin/email.test.js +116 -0
  900. package/test/routes/admin/firestore-query.test.js +206 -0
  901. package/test/routes/admin/firestore.test.js +129 -0
  902. package/test/routes/admin/infer-contact.test.js +220 -0
  903. package/test/routes/admin/notification.test.js +199 -0
  904. package/test/routes/admin/post-convert-image.test.js +159 -0
  905. package/test/routes/admin/post-deploy-flag.test.js +81 -0
  906. package/test/routes/admin/post-download-error.test.js +90 -0
  907. package/test/routes/admin/post-resize-image.test.js +185 -0
  908. package/test/routes/admin/post.test.js +369 -0
  909. package/test/routes/admin/repo-content.test.js +223 -0
  910. package/test/routes/admin/stats.test.js +114 -0
  911. package/test/routes/admin/users-disable.test.js +63 -0
  912. package/test/routes/admin/users-list.test.js +71 -0
  913. package/test/routes/content/post.test.js +60 -0
  914. package/test/routes/general/uuid.test.js +133 -0
  915. package/test/routes/health.test.js +110 -0
  916. package/test/routes/marketing/campaign.test.js +184 -0
  917. package/test/routes/marketing/contact.test.js +416 -0
  918. package/test/routes/marketing/email-preferences.test.js +293 -0
  919. package/test/routes/marketing/push-send.test.js +34 -0
  920. package/test/routes/marketing/webhook-forward.test.js +63 -0
  921. package/test/routes/marketing/webhook.test.js +641 -0
  922. package/test/routes/payments/_route-harness.js +145 -0
  923. package/test/routes/payments/cancel-provider-errors.test.js +227 -0
  924. package/test/routes/payments/cancel-skip-guards.test.js +126 -0
  925. package/test/routes/payments/cancel-trialing.test.js +428 -0
  926. package/test/routes/payments/cancel.test.js +165 -0
  927. package/test/routes/payments/dedup-race.test.js +186 -0
  928. package/test/routes/payments/discount.test.js +82 -0
  929. package/test/routes/payments/dispute-alert.test.js +324 -0
  930. package/test/routes/payments/intent-discount-amounts.test.js +550 -0
  931. package/test/routes/payments/intent-discount-coupons.test.js +293 -0
  932. package/test/routes/payments/intent-one-time-metadata.test.js +122 -0
  933. package/test/routes/payments/intent-purchaser-guard.test.js +139 -0
  934. package/test/routes/payments/intent-zero-total.test.js +260 -0
  935. package/test/routes/payments/intent.test.js +413 -0
  936. package/test/routes/payments/plan.test.js +367 -0
  937. package/test/routes/payments/portal-return-url.test.js +107 -0
  938. package/test/routes/payments/portal.test.js +94 -0
  939. package/test/routes/payments/refund-one-time.test.js +254 -0
  940. package/test/routes/payments/refund-paypal-proration.test.js +190 -0
  941. package/test/routes/payments/refund.test.js +181 -0
  942. package/test/routes/payments/trial-eligibility.test.js +73 -0
  943. package/test/routes/payments/uncancel.test.js +270 -0
  944. package/test/routes/payments/webhook-stripe-invoice.test.js +129 -0
  945. package/test/routes/payments/webhook-stripe-refund-one-time.test.js +130 -0
  946. package/test/routes/payments/webhook-test-provider.test.js +156 -0
  947. package/test/routes/payments/webhook.test.js +115 -0
  948. package/test/routes/payments/winback-stripe-coupon.test.js +126 -0
  949. package/test/routes/payments/winback.test.js +597 -0
  950. package/test/routes/test/authenticate.test.js +79 -0
  951. package/test/routes/test/redirect.test.js +66 -0
  952. package/test/routes/test/reset-account.test.js +126 -0
  953. package/test/routes/test/roster.test.js +101 -0
  954. package/test/routes/test/schema.test.js +556 -0
  955. package/test/routes/test/usage.test.js +374 -0
  956. package/test/routes/user/api-keys.test.js +158 -0
  957. package/test/routes/user/connections-grant.test.js +759 -0
  958. package/test/routes/user/connections-identity.test.js +260 -0
  959. package/test/routes/user/connections-log-privacy.test.js +206 -0
  960. package/test/routes/user/connections-refresh-lease.test.js +696 -0
  961. package/test/routes/user/connections-return.test.js +261 -0
  962. package/test/routes/user/connections-uid.test.js +516 -0
  963. package/test/routes/user/delete.test.js +138 -0
  964. package/test/routes/user/feedback.test.js +106 -0
  965. package/test/routes/user/orders.test.js +233 -0
  966. package/test/routes/user/sessions.test.js +231 -0
  967. package/test/routes/user/settings-validate.test.js +84 -0
  968. package/test/routes/user/signup-emails.test.js +113 -0
  969. package/test/routes/user/signup-location.test.js +84 -0
  970. package/test/routes/user/signup-log-privacy.test.js +112 -0
  971. package/test/routes/user/signup.test.js +579 -0
  972. package/test/routes/user/subscription.test.js +101 -0
  973. package/test/routes/user/token.test.js +111 -0
  974. package/test/routes/user/user.test.js +158 -0
  975. package/test/routes/verts/cache.test.js +78 -0
  976. package/test/routes/verts/click-destination.test.js +76 -0
  977. package/test/routes/verts/crud.test.js +194 -0
  978. package/test/routes/verts/redirect.test.js +147 -0
  979. package/test/routes/verts/selection.test.js +237 -0
  980. package/test/routes/verts/serve.test.js +185 -0
  981. package/test/routes/verts/unit-document.test.js +117 -0
  982. package/test/rules/_environment.js +66 -0
  983. package/test/rules/brand-merge.test.js +195 -0
  984. package/test/rules/field-helpers.test.js +309 -0
  985. package/test/rules/notifications.test.js +577 -0
  986. package/test/rules/payments-carts.test.js +406 -0
  987. package/test/rules/sessions.test.js +170 -0
  988. package/test/rules/user.test.js +517 -0
  989. package/test/rules/verts.test.js +101 -0
  990. package/test/security/fetch-log-secrets.test.js +124 -0
  991. package/test/security/primitives.test.js +105 -0
  992. package/test/security/repo-pinning.test.js +77 -0
  993. package/test/stripe-live/subscription-lifecycle.test.js +141 -0
@@ -0,0 +1,1013 @@
1
+ /**
2
+ * Test: server conversion delivery — the match data every platform receives
3
+ * ([#385](https://github.com/Omega-JS-Stack/omega/issues/385), stage D of
4
+ * [#302](https://github.com/Omega-JS-Stack/omega/issues/302)).
5
+ *
6
+ * The point of the stage is that a server conversion can be MATCHED to the human
7
+ * who clicked the ad. So these tests read the actual bodies that would go on the
8
+ * wire — built by the real adapters from `@omega.js/analytics`' catalog, fed by
9
+ * the real match-data builders — from a realistic order: attribution with both
10
+ * touches, the platform cookies the checkout collected, the request pair the
11
+ * intent captured, and the tracking-consent snapshot.
12
+ *
13
+ * Four contracts under test:
14
+ * 1. Match data — hashed email/phone (raw PII never leaves), external_id, the
15
+ * click ids and cookies in each platform's own slot, IP + user agent.
16
+ * 2. The consent gate — an explicit `false` blocks its category's providers;
17
+ * an ABSENT snapshot (legacy orders, the raw-API recovery lane) allows all.
18
+ * 3. Absence never throws — an order with no attribution and no request still
19
+ * delivers, on external_id alone.
20
+ * 4. The environment gate — only PRODUCTION reaches a platform; a dev or
21
+ * emulator run resolves the whole walk and delivers nothing
22
+ * ([#464](https://github.com/Omega-JS-Stack/omega/issues/464)).
23
+ *
24
+ * Zero I/O: nothing here reaches a platform. Most assertions are on the bodies
25
+ * the builders return; the two that read the WIRE drive a copy of the delivery
26
+ * module whose transport is a recorder (`fireOnRecordedWire`), so a send is
27
+ * observed without a byte leaving the process.
28
+ *
29
+ * Run: npx omega test framework:analytics/conversion-delivery
30
+ */
31
+ const crypto = require('crypto');
32
+ const path = require('path');
33
+ const conversions = require('../../dist/manager/libraries/analytics/conversions.js');
34
+ const matchData = require('../../dist/manager/libraries/analytics/match-data.js');
35
+ const defineCases = require('../../dist/vendor/devkit/test/define-cases.js');
36
+
37
+ // A uid the way one actually looks — Firebase's alphabet is case-SENSITIVE —
38
+ // carrying the padding a stored id can pick up. A lowercased or untrimmed
39
+ // digest is a different digest, so a fixture that was already trim and
40
+ // lowercase would have let either drift through unnoticed.
41
+ const UID = ' AYVZezRcE9CsyI ';
42
+ const UID_TRIMMED = 'AYVZezRcE9CsyI';
43
+ // TikTok's Events API documents external_id as "SHA-256 hashing is required",
44
+ // while Meta documents it as "Hashing recommended" and its own Pixel example
45
+ // sends a raw id ([#410](https://github.com/Omega-JS-Stack/omega/issues/410)).
46
+ // So the two platforms take the SAME uid in two shapes, and the digest is
47
+ // hard-coded (shasum of the TRIMMED, case-preserved uid) rather than computed
48
+ // here: a hash that stops matching the browser half's `ttq.identify()` fails
49
+ // here instead of on a live pixel.
50
+ const UID_DIGEST = '12266491d7fe2f07c833999f4d011444564df71cfd7aaafb346f0edf473572b4';
51
+ const EMAIL = 'Buyer@Example.COM';
52
+ const IP = '203.0.113.7';
53
+ const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)';
54
+
55
+ // An account doc with everything the schema can hold — what the ONE shared
56
+ // reader (`readUserMatchFields`) turns into the full match block (#577).
57
+ const FULL_USER_DOC = {
58
+ auth: { uid: UID_TRIMMED, email: 'Ada.Lovelace@Example.COM' },
59
+ personal: {
60
+ name: { first: 'Ada', last: 'Lovelace' },
61
+ birthday: { timestamp: '1985-02-16T00:00:00.000Z', timestampUNIX: 477273600 },
62
+ gender: 'female',
63
+ location: { country: 'US', region: 'California', city: 'San Diego', postalCode: '94035' },
64
+ telephone: { countryCode: 1, national: 5550102030 },
65
+ },
66
+ };
67
+
68
+ function sha256(value) {
69
+ return crypto.createHash('sha256').update(value).digest('hex');
70
+ }
71
+
72
+ // The attribution shape the landing capture writes and the intent fold carries:
73
+ // an organic first touch, a paid last touch, plus the cookies read at checkout.
74
+ function fullAttribution() {
75
+ return {
76
+ first: {
77
+ tags: { utm_source: 'newsletter', utm_medium: 'email' },
78
+ referrer: 'https://news.ycombinator.com/',
79
+ url: 'https://brand.test/',
80
+ page: '/',
81
+ timestamp: '2026-06-01T00:00:00.000Z',
82
+ },
83
+ last: {
84
+ tags: { utm_source: 'meta', utm_medium: 'cpc', utm_campaign: 'launch', utm_term: 'omega', utm_content: 'hero' },
85
+ clickIds: { fbclid: 'FB-CLICK', gclid: 'G-CLICK', ttclid: 'TT-CLICK' },
86
+ referrer: 'https://facebook.com/',
87
+ url: 'https://brand.test/pricing?utm_source=meta',
88
+ page: '/pricing',
89
+ timestamp: '2026-08-01T00:00:00.000Z',
90
+ },
91
+ affiliate: { code: 'IAN7', timestamp: '2026-08-01T00:00:00.000Z', url: 'https://brand.test/', page: '/' },
92
+ cookies: { fbc: 'fb.1.1754006400000.REAL-COOKIE', fbp: 'fb.1.1754006400000.987654321', ttp: 'TTP-COOKIE' },
93
+ };
94
+ }
95
+
96
+ const PURCHASE_PARAMS = {
97
+ transaction_id: '_test-sub-conversion',
98
+ value: 9.99,
99
+ currency: 'USD',
100
+ items: [{ item_id: 'premium', item_name: 'Premium', price: 9.99, quantity: 1 }],
101
+ payment_provider: 'stripe',
102
+ payment_frequency: 'monthly',
103
+ is_trial: false,
104
+ is_recurring: false,
105
+ };
106
+
107
+ /** Deliver one event exactly as the payment webhook would, and hand back the per-provider results. */
108
+ function deliver({ ctx, Manager, event = 'purchase', params = PURCHASE_PARAMS, attribution = fullAttribution(), trackingConsent = null, providers, identity }) {
109
+ return conversions.deliverConversion({
110
+ event: event,
111
+ params: params,
112
+ providers: providers,
113
+ attribution: matchData.buildAttributionContext(attribution),
114
+ identity: identity || matchData.buildIdentity({
115
+ uid: UID,
116
+ email: EMAIL,
117
+ telephone: { countryCode: 1, national: 5550102030 },
118
+ request: { ip: IP, userAgent: USER_AGENT },
119
+ }),
120
+ trackingConsent: trackingConsent,
121
+ eventId: `${event}._test-webhook-event`,
122
+ ctx: ctx,
123
+ Manager: Manager,
124
+ });
125
+ }
126
+
127
+ function descriptorFor(results, provider) {
128
+ return results.find((result) => result.provider === provider)?.descriptor;
129
+ }
130
+
131
+ function outcomeFor(results, provider) {
132
+ return results.find((result) => result.provider === provider)?.outcome;
133
+ }
134
+
135
+ // The pixel ids a brand configures, in the shape the config resolves them from
136
+ // (`analytics.providers.<provider>.id`).
137
+ const META_PIXEL = '_TEST_META_PIXEL';
138
+ const TIKTOK_PIXEL = '_TEST_TIKTOK_PIXEL';
139
+
140
+ /**
141
+ * Fire one purchase at the two ad transports with the wire RECORDED.
142
+ *
143
+ * The delivery module is re-required with its `wonderful-fetch` replaced by a
144
+ * recorder, so the real `sendMeta`/`sendTikTok` run — config read, body built,
145
+ * POST issued — and the test reads exactly what a platform would have received.
146
+ * The module and the cache entry are both restored, so every other test in this
147
+ * file keeps the real transport.
148
+ *
149
+ * GA4 is out of the fire: its transport is the Manager's own Measurement
150
+ * Protocol helper rather than this module's `post`, and the server half of a
151
+ * purchase-shaped fire names the two providers that dedupe anyway.
152
+ *
153
+ * @param {object} options
154
+ * @param {boolean} options.production - What the ctx answers for `isProduction()`.
155
+ * @returns {{results: object[], calls: object[], lines: string[]}}
156
+ */
157
+ function fireOnRecordedWire({ production }) {
158
+ const modulePath = require.resolve('../../dist/manager/libraries/analytics/conversions.js');
159
+ const fetchPath = require.resolve('wonderful-fetch', { paths: [path.dirname(modulePath)] });
160
+ const realFetch = require.cache[fetchPath];
161
+ const savedEnv = { META_ACCESS_TOKEN: process.env.META_ACCESS_TOKEN, TIKTOK_ACCESS_TOKEN: process.env.TIKTOK_ACCESS_TOKEN };
162
+ const calls = [];
163
+ const lines = [];
164
+ const record = (...args) => lines.push(args.map((arg) => String(arg)).join(' '));
165
+
166
+ require.cache[fetchPath] = {
167
+ id: fetchPath,
168
+ filename: fetchPath,
169
+ loaded: true,
170
+ exports: (url, options) => {
171
+ calls.push({ url: url, options: options });
172
+ return Promise.resolve({});
173
+ },
174
+ };
175
+ delete require.cache[modulePath];
176
+
177
+ // A configured brand is the only one that can send at all — without a token
178
+ // both transports answer `skipped (not configured)` and prove nothing.
179
+ process.env.META_ACCESS_TOKEN = '_test-meta-token';
180
+ process.env.TIKTOK_ACCESS_TOKEN = '_test-tiktok-token';
181
+
182
+ try {
183
+ const results = require(modulePath).deliverConversion({
184
+ event: 'purchase',
185
+ params: PURCHASE_PARAMS,
186
+ attribution: matchData.buildAttributionContext(fullAttribution()),
187
+ identity: matchData.buildIdentity({ uid: UID, email: EMAIL, request: { ip: IP, userAgent: USER_AGENT } }),
188
+ providers: ['meta', 'tiktok'],
189
+ eventId: 'purchase._test-wire-event',
190
+ ctx: {
191
+ log: record,
192
+ error: record,
193
+ warn: record,
194
+ isProduction: () => production,
195
+ isDevelopment: () => !production,
196
+ },
197
+ Manager: { config: { analytics: { providers: { meta: { id: META_PIXEL }, tiktok: { id: TIKTOK_PIXEL } } } } },
198
+ });
199
+
200
+ return { results: results, calls: calls, lines: lines };
201
+ } finally {
202
+ for (const [key, value] of Object.entries(savedEnv)) {
203
+ if (typeof value === 'undefined') {
204
+ delete process.env[key];
205
+ } else {
206
+ process.env[key] = value;
207
+ }
208
+ }
209
+
210
+ if (realFetch) {
211
+ require.cache[fetchPath] = realFetch;
212
+ } else {
213
+ delete require.cache[fetchPath];
214
+ }
215
+ delete require.cache[modulePath];
216
+ require(modulePath);
217
+ }
218
+ }
219
+
220
+ module.exports = defineCases({
221
+ description: 'Server conversion delivery: match data, consent gating, absent-data tolerance',
222
+ type: 'group',
223
+
224
+ tests: [
225
+ {
226
+ name: 'meta-user-data-carries-the-full-match-block',
227
+ auth: 'none',
228
+
229
+ async run({ assert, ctx, Manager }) {
230
+ const results = deliver({ ctx, Manager });
231
+ const body = conversions.buildMetaBody({
232
+ descriptor: descriptorFor(results, 'meta'),
233
+ identity: matchData.buildIdentity({
234
+ uid: UID,
235
+ email: EMAIL,
236
+ telephone: { countryCode: 1, national: 5550102030 },
237
+ request: { ip: IP, userAgent: USER_AGENT },
238
+ }),
239
+ eventId: 'purchase._test-webhook-event',
240
+ });
241
+
242
+ const event = body.data[0];
243
+
244
+ assert.equal(event.event_name, 'Purchase', 'the catalog names the Meta event');
245
+ assert.equal(event.event_id, 'purchase._test-webhook-event', 'the dedupe id rides the event');
246
+ assert.equal(event.action_source, 'website');
247
+
248
+ // Identity — hashed, never raw
249
+ assert.equal(event.user_data.em, sha256('buyer@example.com'), 'email is SHA256 of the trimmed lowercase address');
250
+ assert.equal(event.user_data.ph, sha256('15550102030'), 'phone is SHA256 of the digits, country code included');
251
+ assert.equal(event.user_data.external_id, UID, 'Meta only RECOMMENDS hashing external_id, and its Pixel half sends the raw uid (#410)');
252
+ assert.equal(JSON.stringify(event).includes(EMAIL), false, 'the raw email must never appear anywhere in the body');
253
+ assert.equal(JSON.stringify(event).includes('5550102030'), false, 'the raw phone number must never appear anywhere in the body');
254
+
255
+ // Same-device match
256
+ assert.equal(event.user_data.client_ip_address, IP);
257
+ assert.equal(event.user_data.client_user_agent, USER_AGENT);
258
+ assert.equal(event.user_data.fbc, 'fb.1.1754006400000.REAL-COOKIE', 'the real cookie wins over any construction');
259
+ assert.equal(event.user_data.fbp, 'fb.1.1754006400000.987654321');
260
+
261
+ // Commerce, in Meta's own vocabulary (the catalog's map)
262
+ assert.equal(event.custom_data.value, 9.99);
263
+ assert.equal(event.custom_data.currency, 'USD');
264
+ assert.deepEqual(event.custom_data.content_ids, ['premium']);
265
+ assert.equal(event.custom_data.fbc, undefined, 'match data never rides the custom data');
266
+ },
267
+ },
268
+
269
+ {
270
+ // Meta REQUIRES `action_source` and makes its accuracy a term of use, and
271
+ // its enum names this exact case: `system_generated`, "for example, a
272
+ // subscription renewal that's set to auto-pay each month". Every server
273
+ // fire claimed `website`, which said a person had been on the site for a
274
+ // charge their card took on its own
275
+ // ([#498](https://github.com/Omega-JS-Stack/omega/issues/498)). The
276
+ // catalog decides it per event; the body is where it has to show up.
277
+ name: 'a-billed-charge-tells-meta-system-generated-and-a-checkout-website',
278
+ auth: 'none',
279
+
280
+ async run({ assert, ctx, Manager }) {
281
+ const identity = matchData.buildIdentity({ uid: UID, email: EMAIL, request: { ip: IP, userAgent: USER_AGENT } });
282
+ const actionSourceFor = (event) => conversions.buildMetaBody({
283
+ descriptor: descriptorFor(deliver({ ctx, Manager, event: event }), 'meta'),
284
+ identity: identity,
285
+ eventId: `${event}._test-webhook-event`,
286
+ }).data[0].action_source;
287
+
288
+ for (const event of ['subscription_renew', 'payment_recovered', 'trial_convert']) {
289
+ assert.equal(actionSourceFor(event), 'system_generated', `${event} bills a card with nobody present`);
290
+ }
291
+
292
+ for (const event of ['purchase', 'trial_start', 'subscription_cancel']) {
293
+ assert.equal(actionSourceFor(event), 'website', `${event} is something a person did on the site`);
294
+ }
295
+ },
296
+ },
297
+
298
+ {
299
+ // The match block a real account can fill
300
+ // ([#577](https://github.com/Omega-JS-Stack/omega/issues/577)): the doc
301
+ // already held the name, birthday, gender and location, and Meta accepts
302
+ // all four — so the body carries every parameter, not just `em` +
303
+ // `external_id`. The normalization each one needs is pinned value by
304
+ // value in analytics/match-normalization.test.js; what this asserts is
305
+ // that the BODY carries them, from the one shared reader.
306
+ name: 'a-full-account-fills-every-meta-parameter',
307
+ auth: 'none',
308
+
309
+ async run({ assert, ctx, Manager }) {
310
+ const identity = matchData.buildIdentity({
311
+ uid: UID,
312
+ user: FULL_USER_DOC,
313
+ request: { ip: IP, userAgent: USER_AGENT },
314
+ });
315
+
316
+ const results = deliver({ ctx, Manager, identity });
317
+ const userData = conversions.buildMetaBody({
318
+ descriptor: descriptorFor(results, 'meta'),
319
+ identity: identity,
320
+ eventId: 'purchase._test-webhook-event',
321
+ }).data[0].user_data;
322
+
323
+ assert.deepEqual(
324
+ Object.keys(userData).sort(),
325
+ ['client_ip_address', 'client_user_agent', 'country', 'ct', 'db', 'em', 'external_id', 'fbc', 'fbp', 'fn', 'ge', 'ln', 'ph', 'st', 'zp'],
326
+ 'every parameter Meta accepts from this account rides the body',
327
+ );
328
+
329
+ // The email and phone still come off the SAME reader — a call site that
330
+ // passes a doc must not lose the two keys it always had.
331
+ assert.equal(userData.em, sha256('ada.lovelace@example.com'), 'the doc\'s email is read and normalized like any other');
332
+ assert.equal(userData.ph, sha256('15550102030'), 'and its telephone pair');
333
+ assert.equal(JSON.stringify(userData).includes('Lovelace'), false, 'no raw personal value anywhere in the block');
334
+ },
335
+ },
336
+
337
+ {
338
+ // The fire log is how a brand SEES its match quality without opening an
339
+ // ads manager: per provider, the keys that went and the accepted keys
340
+ // that were empty. KEY NAMES ONLY — a match value, raw or hashed, must
341
+ // never reach a log line (#577).
342
+ name: 'the-fire-log-names-the-match-keys-sent-and-the-ones-that-were-empty',
343
+ auth: 'none',
344
+
345
+ async run({ assert, Manager }) {
346
+ const lines = [];
347
+ const record = (...args) => lines.push(args.map((arg) => String(arg)).join(' '));
348
+ const identity = matchData.buildIdentity({ uid: UID, email: EMAIL, request: { ip: IP, userAgent: USER_AGENT } });
349
+
350
+ conversions.deliverConversion({
351
+ event: 'purchase',
352
+ params: PURCHASE_PARAMS,
353
+ attribution: matchData.buildAttributionContext({}),
354
+ identity: identity,
355
+ providers: ['meta'],
356
+ eventId: 'purchase._test-fire-log',
357
+ ctx: { log: record, error: record, warn: record, isProduction: () => false, isDevelopment: () => true },
358
+ Manager: Manager,
359
+ });
360
+
361
+ const summary = lines.find((line) => line.startsWith('deliverConversion: purchase →'));
362
+
363
+ assert.equal(!!summary, true, `the walk logs one summary line: ${lines.join(' | ')}`);
364
+ assert.equal(summary.includes('meta blocked (dev) sent em,external_id,client_ip_address,client_user_agent;'), true, `the line names what went: ${summary}`);
365
+ assert.equal(summary.includes('empty ph,fn,ln,ct,st,zp,country,db,ge,fbc,fbp'), true, `and what the platform accepts but this fire had nothing for: ${summary}`);
366
+ assert.equal(summary.includes(EMAIL), false, 'never the raw value');
367
+ assert.equal(summary.includes(identity.emailHash), false, 'and never the hashed one either');
368
+ },
369
+ },
370
+
371
+ {
372
+ name: 'meta-constructs-fbc-from-the-captured-fbclid',
373
+ auth: 'none',
374
+
375
+ async run({ assert, ctx, Manager }) {
376
+ // The cookie never made it to checkout (cleared jar, ITP, another device)
377
+ const attribution = fullAttribution();
378
+ delete attribution.cookies.fbc;
379
+
380
+ const results = deliver({ ctx, Manager, attribution });
381
+ const descriptor = descriptorFor(results, 'meta');
382
+ const capturedMs = Date.parse('2026-08-01T00:00:00.000Z');
383
+
384
+ assert.equal(descriptor.userData.fbc, `fb.1.${capturedMs}.FB-CLICK`, 'fbc is built per Meta spec from the click id, stamped when it was captured');
385
+ assert.equal(descriptor.userData.fbp, 'fb.1.1754006400000.987654321', 'the browser cookie is untouched by the construction');
386
+
387
+ // Nothing to build from is not an error — the fire still goes with what it has
388
+ const noClick = fullAttribution();
389
+ delete noClick.cookies.fbc;
390
+ delete noClick.last.clickIds.fbclid;
391
+
392
+ const bare = descriptorFor(deliver({ ctx, Manager, attribution: noClick }), 'meta');
393
+ assert.equal(bare.userData.fbc, undefined, 'no cookie and no fbclid means no fbc, never a malformed one');
394
+ },
395
+ },
396
+
397
+ {
398
+ // Events API 2.0 FLATTENED the match block: what used to sit under
399
+ // `data[].context` (`context.user`, `context.ad.callback`, `context.ip`,
400
+ // `context.user_agent`) is one `data[].user` object, with the click id as
401
+ // a plain `user.ttclid` and the phone under `phone`, not `phone_number`
402
+ // ([#482](https://github.com/Omega-JS-Stack/omega/issues/482)). An
403
+ // off-spec key does not error — it matches nobody, silently.
404
+ name: 'tiktok-user-block-carries-the-full-match-block',
405
+ auth: 'none',
406
+
407
+ async run({ assert, ctx, Manager }) {
408
+ const results = deliver({ ctx, Manager });
409
+ const body = conversions.buildTikTokBody({
410
+ descriptor: descriptorFor(results, 'tiktok'),
411
+ identity: matchData.buildIdentity({
412
+ uid: UID,
413
+ email: EMAIL,
414
+ telephone: { countryCode: 1, national: 5550102030 },
415
+ request: { ip: IP, userAgent: USER_AGENT },
416
+ }),
417
+ eventId: 'purchase._test-webhook-event',
418
+ pixelCode: '_TEST_PIXEL',
419
+ });
420
+
421
+ const event = body.data[0];
422
+
423
+ assert.equal(event.event, 'Purchase', 'the catalog names the TikTok event — Purchase, since CompletePayment was retired ([#652])');
424
+ assert.equal(event.event_id, 'purchase._test-webhook-event');
425
+ assert.equal(event.user.email, sha256('buyer@example.com'), 'TikTok takes the email hashed too');
426
+ assert.equal(event.user.phone, sha256('+15550102030'), 'TikTok\'s spec is the E.164 number, plus included');
427
+ assert.equal(event.user.external_id, UID_DIGEST, 'TikTok REQUIRES external_id hashed, and a raw one matches nobody (#410)');
428
+ assert.equal(JSON.stringify(event).includes(UID_TRIMMED), false, 'the raw uid must not ride a TikTok body under any key');
429
+ assert.equal(event.user.ttp, 'TTP-COOKIE');
430
+ assert.equal(event.user.ttclid, 'TT-CLICK', 'the click id is a match key like any other in 2.0');
431
+ assert.equal(event.user.ip, IP);
432
+ assert.equal(event.user.user_agent, USER_AGENT);
433
+ assert.equal(event.properties.contents[0].content_id, 'premium', 'TikTok commerce rides its documented `contents` array ([#652])');
434
+ assert.equal(event.properties.value, 9.99);
435
+
436
+ // The v1.2 nesting is GONE, not duplicated alongside — a body carrying
437
+ // both shapes is how the two drift apart on the next edit.
438
+ assert.equal(Object.hasOwn(event, 'context'), false, 'the v1.2 context block never rides along');
439
+ assert.equal(JSON.stringify(event).includes('phone_number'), false, 'nor its key names');
440
+ },
441
+ },
442
+
443
+ {
444
+ // `timestamp` was an ISO string; 2.0's REQUIRED field is `event_time`, in
445
+ // Unix SECONDS — the same stamp Meta's body has always carried. An ISO
446
+ // string here is either a rejection or an event attributed to the wrong
447
+ // day, and neither shows up anywhere but the platform's own dashboard
448
+ // ([#482](https://github.com/Omega-JS-Stack/omega/issues/482)).
449
+ name: 'the-tiktok-item-stamps-event-time-in-unix-seconds',
450
+ auth: 'none',
451
+
452
+ async run({ assert, ctx, Manager }) {
453
+ const before = Math.floor(Date.now() / 1000);
454
+ const results = deliver({ ctx, Manager });
455
+ const event = conversions.buildTikTokBody({
456
+ descriptor: descriptorFor(results, 'tiktok'),
457
+ identity: matchData.buildIdentity({ uid: UID }),
458
+ eventId: 'purchase._test-webhook-event',
459
+ pixelCode: TIKTOK_PIXEL,
460
+ }).data[0];
461
+
462
+ assert.equal(Number.isInteger(event.event_time), true, `event_time is an integer of seconds, not ${JSON.stringify(event.event_time)}`);
463
+ assert.equal(event.event_time >= before, true, 'stamped at build time');
464
+ assert.equal(event.event_time <= Math.floor(Date.now() / 1000), true, 'and never in the future');
465
+ // Milliseconds pass Number.isInteger too, and are the failure the unit
466
+ // gets wrong silently: a 13-digit stamp reads as the year 46,000.
467
+ assert.equal(String(event.event_time).length, 10, 'seconds, not milliseconds');
468
+ assert.equal(Object.hasOwn(event, 'timestamp'), false, 'the v1.2 ISO field is replaced, not kept beside it');
469
+ assert.equal(JSON.stringify(event).includes('T00:00:00'), false, 'no ISO stamp rides the item under any key');
470
+ },
471
+ },
472
+
473
+ {
474
+ // TikTok's Events API validates the DESTINATION at the top level of the
475
+ // request: `event_source` and `event_source_id`. The pixel id used to ride
476
+ // as `data[].pixel_code` instead, so every live fire came back
477
+ // `{"code":40002,"message":"Invalid value for event_source_id: not a valid
478
+ // string."}` — the field TikTok validates was never sent, while the id sat
479
+ // correctly in config the whole time
480
+ // ([#465](https://github.com/Omega-JS-Stack/omega/issues/465)).
481
+ name: 'the-tiktok-body-names-the-pixel-in-event-source-id',
482
+ auth: 'none',
483
+
484
+ async run({ assert, ctx, Manager }) {
485
+ const results = deliver({ ctx, Manager });
486
+ const body = conversions.buildTikTokBody({
487
+ descriptor: descriptorFor(results, 'tiktok'),
488
+ identity: matchData.buildIdentity({ uid: UID }),
489
+ eventId: 'purchase._test-webhook-event',
490
+ pixelCode: TIKTOK_PIXEL,
491
+ });
492
+
493
+ assert.equal(body.event_source_id, TIKTOK_PIXEL, 'the configured pixel id is the event source id TikTok validates');
494
+ assert.equal(body.event_source, 'web', 'a conversion for a website is the web source');
495
+ assert.equal(body.data[0].pixel_code, undefined, 'one home for the id — a second copy in the data item is what drifted');
496
+ },
497
+ },
498
+
499
+ {
500
+ name: 'the-tiktok-send-puts-the-configured-pixel-on-the-wire',
501
+ auth: 'none',
502
+
503
+ async run({ assert }) {
504
+ // The body test above proves the SHAPE; this one proves the id actually
505
+ // reaches it — the resolution step (#465) is where the send went wrong.
506
+ const { calls, lines } = fireOnRecordedWire({ production: true });
507
+ const tiktok = calls.find((call) => call.url.includes('business-api.tiktok.com'));
508
+
509
+ assert.equal(!!tiktok, true, `a configured brand in production must reach TikTok: ${lines.join(' | ')}`);
510
+ assert.equal(tiktok.options.body.event_source_id, TIKTOK_PIXEL, 'the id on the wire is the one config resolved');
511
+ assert.equal(tiktok.options.body.event_source, 'web');
512
+ assert.equal(JSON.stringify(tiktok.options.body).includes('pixel_code'), false, 'the v1.2 field never rides along');
513
+
514
+ // The other transport is the control: it was delivering all along.
515
+ const meta = calls.find((call) => call.url.includes('graph.facebook.com'));
516
+
517
+ assert.equal(!!meta, true, `Meta must still be reached: ${lines.join(' | ')}`);
518
+ assert.equal(meta.url.includes(`/${META_PIXEL}/events`), true, 'Meta names its pixel in the path');
519
+ },
520
+ },
521
+
522
+ {
523
+ // A server conversion has no page of its own — a webhook fires from
524
+ // Stripe's servers — so the only URL it can honestly claim is the one the
525
+ // attribution touch captured. It used to be dropped on the floor by
526
+ // `buildAttributionContext()`, so TikTok's data item carried no `page` and
527
+ // Meta's event no `event_source_url`, and both platforms scored the match
528
+ // lower for it ([#497](https://github.com/Omega-JS-Stack/omega/issues/497)).
529
+ name: 'the-page-the-conversion-happened-on-rides-both-ad-bodies',
530
+ auth: 'none',
531
+
532
+ async run({ assert, ctx, Manager }) {
533
+ const context = matchData.buildAttributionContext(fullAttribution());
534
+
535
+ assert.equal(context.url, 'https://brand.test/pricing?utm_source=meta', 'the CREDITED touch\'s url reaches the adapters');
536
+ assert.equal(context.referrer, 'https://facebook.com/', 'and the referrer it landed from');
537
+
538
+ const results = deliver({ ctx, Manager });
539
+ const identity = matchData.buildIdentity({ uid: UID });
540
+
541
+ const meta = conversions.buildMetaBody({
542
+ descriptor: descriptorFor(results, 'meta'),
543
+ identity: identity,
544
+ eventId: 'purchase._test-webhook-event',
545
+ }).data[0];
546
+
547
+ const tiktok = conversions.buildTikTokBody({
548
+ descriptor: descriptorFor(results, 'tiktok'),
549
+ identity: identity,
550
+ eventId: 'purchase._test-webhook-event',
551
+ pixelCode: TIKTOK_PIXEL,
552
+ }).data[0];
553
+
554
+ assert.equal(meta.event_source_url, 'https://brand.test/pricing?utm_source=meta', 'Meta reads the conversion\'s page as event_source_url');
555
+ assert.deepEqual(tiktok.page, { url: 'https://brand.test/pricing?utm_source=meta', referrer: 'https://facebook.com/' }, 'TikTok 2.0 reads it as the item\'s page object');
556
+
557
+ // It is context, not commerce and not a match key — a url in either
558
+ // block is an off-spec field the platform reads as neither.
559
+ assert.equal(meta.custom_data.url, undefined, 'the page never rides Meta\'s custom data');
560
+ assert.equal(tiktok.properties.url, undefined, 'nor TikTok\'s properties');
561
+ assert.equal(tiktok.user.url, undefined, 'nor either match block');
562
+ assert.equal(meta.user_data.url, undefined);
563
+
564
+ // GA4's Measurement Protocol gets no invented slot: the campaign params
565
+ // are what its half of this attribution has always been.
566
+ assert.equal(descriptorFor(results, 'ga4').payload.url, undefined, 'GA4 gains no url param');
567
+ },
568
+ },
569
+
570
+ {
571
+ name: 'a-direct-landing-sends-a-page-of-the-url-alone',
572
+ auth: 'none',
573
+
574
+ async run({ assert, ctx, Manager }) {
575
+ // The landing capture writes `referrer: null` for a visit that came from
576
+ // no page at all (a typed url, an app link). An empty referrer key is a
577
+ // field we tried and failed to send.
578
+ const attribution = fullAttribution();
579
+ attribution.last.referrer = null;
580
+
581
+ const results = deliver({ ctx, Manager, attribution });
582
+ const identity = matchData.buildIdentity({ uid: UID });
583
+
584
+ const meta = conversions.buildMetaBody({
585
+ descriptor: descriptorFor(results, 'meta'),
586
+ identity: identity,
587
+ eventId: 'purchase._test-webhook-event',
588
+ }).data[0];
589
+
590
+ const tiktok = conversions.buildTikTokBody({
591
+ descriptor: descriptorFor(results, 'tiktok'),
592
+ identity: identity,
593
+ eventId: 'purchase._test-webhook-event',
594
+ pixelCode: TIKTOK_PIXEL,
595
+ }).data[0];
596
+
597
+ assert.equal(meta.event_source_url, 'https://brand.test/pricing?utm_source=meta', 'the url still goes — a referrer was never part of Meta\'s field');
598
+ assert.deepEqual(tiktok.page, { url: 'https://brand.test/pricing?utm_source=meta' }, 'the page is the url alone');
599
+ assert.equal(Object.hasOwn(tiktok.page, 'referrer'), false, 'never an empty referrer beside it');
600
+ },
601
+ },
602
+
603
+ {
604
+ name: 'no-touch-url-means-no-page-and-no-event-source-url',
605
+ auth: 'none',
606
+
607
+ async run({ assert, ctx, Manager }) {
608
+ // The raw-API recovery lane: no browser was ever involved, so there is
609
+ // no touch and no url. An invented one is worse than an absent one —
610
+ // the brand's home page would credit every recovered order to a page
611
+ // nobody converted on.
612
+ const results = deliver({ ctx, Manager, attribution: {}, identity: matchData.buildIdentity({ uid: UID }) });
613
+ const identity = matchData.buildIdentity({ uid: UID });
614
+
615
+ const meta = conversions.buildMetaBody({
616
+ descriptor: descriptorFor(results, 'meta'),
617
+ identity: identity,
618
+ eventId: 'purchase._test-webhook-event',
619
+ }).data[0];
620
+
621
+ const tiktok = conversions.buildTikTokBody({
622
+ descriptor: descriptorFor(results, 'tiktok'),
623
+ identity: identity,
624
+ eventId: 'purchase._test-webhook-event',
625
+ pixelCode: TIKTOK_PIXEL,
626
+ }).data[0];
627
+
628
+ assert.equal(Object.hasOwn(meta, 'event_source_url'), false, 'Meta gets no key at all, never an empty string');
629
+ assert.equal(Object.hasOwn(tiktok, 'page'), false, 'and TikTok no empty page object');
630
+ },
631
+ },
632
+
633
+ {
634
+ name: 'ga4-gains-the-campaign-params-and-gclid',
635
+ auth: 'none',
636
+
637
+ async run({ assert, ctx, Manager }) {
638
+ const descriptor = descriptorFor(deliver({ ctx, Manager }), 'ga4');
639
+
640
+ assert.equal(descriptor.name, 'purchase');
641
+ assert.equal(descriptor.payload.source, 'meta', 'last touch is the credited one');
642
+ assert.equal(descriptor.payload.medium, 'cpc');
643
+ assert.equal(descriptor.payload.campaign, 'launch');
644
+ assert.equal(descriptor.payload.term, 'omega');
645
+ assert.equal(descriptor.payload.content, 'hero');
646
+ assert.equal(descriptor.payload.gclid, 'G-CLICK', 'the Google click id rides as an event param');
647
+ assert.equal(descriptor.payload.value, 9.99, 'the commerce params are untouched');
648
+ assert.equal(descriptor.payload.fbc, undefined, 'another platform\'s match data never rides GA4');
649
+ },
650
+ },
651
+
652
+ {
653
+ name: 'campaign-falls-back-to-the-first-touch',
654
+ auth: 'none',
655
+
656
+ async run({ assert, ctx, Manager }) {
657
+ // A user who arrived on a tagged link and never came back through one:
658
+ // last touch is only ever written by a tagged visit (#384).
659
+ const attribution = fullAttribution();
660
+ delete attribution.last;
661
+
662
+ const descriptor = descriptorFor(deliver({ ctx, Manager, attribution }), 'ga4');
663
+
664
+ assert.equal(descriptor.payload.source, 'newsletter', 'first touch credits the conversion when there is no last');
665
+ assert.equal(descriptor.payload.medium, 'email');
666
+ assert.equal(descriptor.payload.campaign, undefined, 'a param the touch never carried is absent, not empty');
667
+ },
668
+ },
669
+
670
+ {
671
+ name: 'marketing-declined-blocks-meta-and-tiktok-but-not-ga4',
672
+ auth: 'none',
673
+
674
+ async run({ assert, ctx, Manager }) {
675
+ const results = deliver({
676
+ ctx,
677
+ Manager,
678
+ trackingConsent: { analytics: true, marketing: false, region: 'opt-in', version: 1 },
679
+ });
680
+
681
+ assert.equal(outcomeFor(results, 'meta'), 'skipped (consent: marketing)', 'an opted-out user reaches no ad platform');
682
+ assert.equal(outcomeFor(results, 'tiktok'), 'skipped (consent: marketing)');
683
+ assert.equal(descriptorFor(results, 'meta'), undefined, 'a blocked provider never even resolves its payload');
684
+ assert.equal(outcomeFor(results, 'ga4').startsWith('skipped (consent'), false, 'analytics was granted, so GA4 still counts it');
685
+ },
686
+ },
687
+
688
+ {
689
+ name: 'analytics-declined-blocks-ga4-but-not-the-ad-platforms',
690
+ auth: 'none',
691
+
692
+ async run({ assert, ctx, Manager }) {
693
+ const results = deliver({
694
+ ctx,
695
+ Manager,
696
+ trackingConsent: { analytics: false, marketing: true, region: 'opt-in', version: 1 },
697
+ });
698
+
699
+ assert.equal(outcomeFor(results, 'ga4'), 'skipped (consent: analytics)');
700
+ assert.equal(outcomeFor(results, 'meta').startsWith('skipped (consent'), false, 'the two categories gate independently');
701
+ },
702
+ },
703
+
704
+ {
705
+ name: 'an-absent-snapshot-is-not-a-denial',
706
+ auth: 'none',
707
+
708
+ async run({ assert, ctx, Manager }) {
709
+ // A legacy order, or one minted by the raw-API recovery lane, carries no
710
+ // snapshot at all. Blocking it would be a silent revenue hole.
711
+ for (const snapshot of [null, undefined, {}]) {
712
+ const granted = conversions.resolveTrackingConsent(snapshot);
713
+
714
+ assert.equal(granted.analytics, true, `${JSON.stringify(snapshot)} allows analytics`);
715
+ assert.equal(granted.marketing, true, `${JSON.stringify(snapshot)} allows marketing`);
716
+ }
717
+
718
+ const results = deliver({ ctx, Manager, trackingConsent: null });
719
+
720
+ for (const provider of ['ga4', 'meta', 'tiktok']) {
721
+ assert.equal(outcomeFor(results, provider).startsWith('skipped (consent'), false, `${provider} fires without a snapshot`);
722
+ }
723
+ },
724
+ },
725
+
726
+ {
727
+ name: 'an-order-with-no-attribution-still-fires-on-external-id',
728
+ auth: 'none',
729
+
730
+ async run({ assert, ctx, Manager }) {
731
+ // The raw-API recovery lane: no browser was ever involved, so there is no
732
+ // attribution, no cookies and no request pair.
733
+ const results = deliver({
734
+ ctx,
735
+ Manager,
736
+ attribution: {},
737
+ identity: matchData.buildIdentity({ uid: UID }),
738
+ });
739
+
740
+ const body = conversions.buildMetaBody({
741
+ descriptor: descriptorFor(results, 'meta'),
742
+ identity: matchData.buildIdentity({ uid: UID }),
743
+ eventId: 'purchase._test-webhook-event',
744
+ });
745
+
746
+ assert.deepEqual(body.data[0].user_data, { external_id: UID }, 'external_id alone — no empty match keys invented');
747
+
748
+ // The lane where external_id is the ONLY match key is exactly where its
749
+ // shape decides whether the fire matches anybody at all (#410).
750
+ const tiktok = conversions.buildTikTokBody({
751
+ descriptor: descriptorFor(results, 'tiktok'),
752
+ identity: matchData.buildIdentity({ uid: UID }),
753
+ eventId: 'purchase._test-webhook-event',
754
+ pixelCode: '_TEST_PIXEL',
755
+ });
756
+
757
+ assert.deepEqual(tiktok.data[0].user, { external_id: UID_DIGEST }, 'TikTok\'s lone match key is the digest its Events API requires');
758
+
759
+ assert.equal(descriptorFor(results, 'ga4').payload.source, undefined, 'no campaign params either');
760
+ assert.equal(outcomeFor(results, 'meta').startsWith('skipped (consent'), false, 'missing match data never blocks the fire');
761
+ },
762
+ },
763
+
764
+ {
765
+ // `sign_up` is a `placement: 'both'` event, and its two halves do NOT all
766
+ // deduplicate: Meta and TikTok merge on event_id, GA4 has no cross-source
767
+ // dedupe at all — a Measurement Protocol sign_up beside the browser's gtag
768
+ // one is simply two registrations. So the server half names the providers
769
+ // it owns, and GA4 stays mapped in the catalog for the client.
770
+ name: 'a-provider-the-other-half-owns-is-never-fired',
771
+ auth: 'none',
772
+
773
+ async run({ assert, ctx, Manager }) {
774
+ const results = deliver({
775
+ ctx,
776
+ Manager,
777
+ event: 'sign_up',
778
+ params: { method: 'email', user_id: UID },
779
+ providers: ['meta', 'tiktok'],
780
+ });
781
+
782
+ assert.equal(outcomeFor(results, 'ga4'), 'skipped (not selected)', 'GA4 must not be fired by a half that cannot dedupe against the other');
783
+ assert.equal(descriptorFor(results, 'ga4'), undefined, 'an unselected provider never even resolves its payload');
784
+
785
+ // The two that DO dedupe are resolved and handed to their transport.
786
+ for (const provider of ['meta', 'tiktok']) {
787
+ assert.equal(descriptorFor(results, provider).name, 'CompleteRegistration', `${provider} resolves its native event`);
788
+ assert.equal(outcomeFor(results, provider).startsWith('skipped (not selected)'), false, `${provider} is this half's to fire`);
789
+ }
790
+
791
+ // Unrestricted stays unrestricted — the seam is opt-in per fire.
792
+ const everyProvider = deliver({ ctx, Manager, event: 'sign_up', params: { method: 'email', user_id: UID } });
793
+
794
+ assert.equal(outcomeFor(everyProvider, 'ga4').startsWith('skipped (not selected)'), false, 'a fire that names no providers reaches every mapped one');
795
+ },
796
+ },
797
+
798
+ {
799
+ name: 'an-account-with-no-phone-sends-no-phone-hash',
800
+ auth: 'none',
801
+
802
+ async run({ assert }) {
803
+ // The account schema stores the pair as NUMBERS defaulting to 0. Hashing
804
+ // that would give every phoneless account the same match key — junk the
805
+ // platforms would happily match strangers to each other on.
806
+ for (const hash of [matchData.hashPhone, matchData.hashPhoneE164]) {
807
+ assert.equal(hash({ countryCode: 0, national: 0 }), null, 'the schema default is no phone');
808
+ assert.equal(hash(undefined), null);
809
+ }
810
+
811
+ assert.equal(matchData.hashPhone({ countryCode: 1, national: 5550102030 }), sha256('15550102030'), 'a real number still hashes');
812
+
813
+ // Firebase Auth hands the trigger an E.164 string instead of the pair.
814
+ assert.equal(matchData.hashPhone('+1 555-010-2030'), sha256('15550102030'), 'the Auth record shape hashes to the same digest');
815
+
816
+ const identity = matchData.buildIdentity({ uid: UID, telephone: { countryCode: 0, national: 0 } });
817
+
818
+ assert.equal(Object.hasOwn(identity, 'metaPhoneHash'), false, 'an unresolvable match key is absent, never empty');
819
+ assert.equal(Object.hasOwn(identity, 'tiktokPhoneHash'), false, 'and neither normalization invents one');
820
+ },
821
+ },
822
+
823
+ {
824
+ // ONE hash used to go to every provider, and the platforms do not agree on
825
+ // what to hash ([#392](https://github.com/Omega-JS-Stack/omega/issues/392)):
826
+ // Meta's advanced matching wants bare digits, TikTok's Events API the E.164
827
+ // form WITH the plus. Same person, two digests — an off-spec key does not
828
+ // error, it just silently matches nobody.
829
+ name: 'the-phone-hash-is-normalized-per-provider-spec',
830
+ auth: 'none',
831
+
832
+ async run({ assert, ctx, Manager }) {
833
+ const telephone = { countryCode: 1, national: 5550102030 };
834
+ const identity = matchData.buildIdentity({ uid: UID, telephone: telephone });
835
+
836
+ assert.equal(identity.metaPhoneHash, sha256('15550102030'), 'Meta hashes the bare digits');
837
+ assert.equal(identity.tiktokPhoneHash, sha256('+15550102030'), 'TikTok hashes the E.164 number');
838
+ assert.equal(identity.metaPhoneHash === identity.tiktokPhoneHash, false, 'two specs, two digests — never one hash for both');
839
+
840
+ // The same normalizers the browser half uses (`@omega.js/analytics/identity`),
841
+ // so a server conversion and a pixel event carry the SAME match key.
842
+ const results = deliver({ ctx, Manager, identity });
843
+
844
+ const meta = conversions.buildMetaBody({ descriptor: descriptorFor(results, 'meta'), identity: identity, eventId: 'purchase._test-webhook-event' });
845
+ const tiktok = conversions.buildTikTokBody({ descriptor: descriptorFor(results, 'tiktok'), identity: identity, eventId: 'purchase._test-webhook-event', pixelCode: '_TEST_PIXEL' });
846
+
847
+ assert.equal(meta.data[0].user_data.ph, sha256('15550102030'), 'the Meta body carries the digits-only digest');
848
+ assert.equal(tiktok.data[0].user.phone, sha256('+15550102030'), 'the TikTok body carries the E.164 digest');
849
+ },
850
+ },
851
+
852
+ {
853
+ name: 'an-event-name-the-catalog-does-not-know-is-loud-and-harmless',
854
+ auth: 'none',
855
+
856
+ async run({ assert, ctx, Manager }) {
857
+ // A typo in a canonical name used to be SILENT: every adapter returned
858
+ // null, and the fire read exactly like `subscription_plan_change` on Meta — a
859
+ // deliberate non-mapping. So a webhook could stop reporting revenue and the logs
860
+ // would say nothing was wrong. The outcome is now its own word, and the
861
+ // walk still cannot take the webhook down with it.
862
+ const warnings = [];
863
+ const record = (line) => warnings.push(String(line));
864
+ const guarded = { ...ctx, log: record, error: record, warn: record };
865
+
866
+ const results = conversions.deliverConversion({
867
+ event: 'purchsae',
868
+ params: PURCHASE_PARAMS,
869
+ eventId: 'purchsae._test-typo',
870
+ ctx: guarded,
871
+ Manager: Manager,
872
+ });
873
+
874
+ for (const provider of ['ga4', 'meta', 'tiktok']) {
875
+ assert.equal(outcomeFor(results, provider), 'skipped (unknown event)', `${provider} must say the NAME was wrong, not that the provider declined to map a real event`);
876
+ }
877
+
878
+ const loud = warnings.filter((line) => line.includes('[unknown canonical event]'));
879
+
880
+ assert.equal(loud.length, 1, `exactly one loud line, not one per provider: ${warnings.join(' | ')}`);
881
+ assert.equal(loud[0].includes('purchsae'), true, `the line must name the offender: ${loud[0]}`);
882
+ },
883
+ },
884
+
885
+ {
886
+ name: 'the-outcomes-an-ad-platform-has-no-use-for-reach-ga4-only',
887
+ auth: 'none',
888
+
889
+ async run({ assert, ctx, Manager }) {
890
+ // The catalog maps none of these on Meta or TikTok — an unmapped provider
891
+ // is a decision, and the adapter skips rather than inventing an event.
892
+ // `trial_lapse` is the deliberate omission of the exclusion lane below
893
+ // ([#415](https://github.com/Omega-JS-Stack/omega/issues/415)): a lapsed
894
+ // trialist is a win-back audience to retarget, not one to hide ads from.
895
+ // The other two are [#407](https://github.com/Omega-JS-Stack/omega/issues/407)'s
896
+ // lifecycle additions, dark to an ad platform because no money moved.
897
+ for (const event of ['subscription_uncancel', 'subscription_plan_change', 'trial_lapse']) {
898
+ const results = deliver({ ctx, Manager, event });
899
+
900
+ assert.equal(descriptorFor(results, 'ga4').name, event, `${event} resolves on GA4 under its own name`);
901
+ assert.equal(outcomeFor(results, 'meta'), 'skipped (no mapping)', `${event} has no Meta mapping`);
902
+ assert.equal(outcomeFor(results, 'tiktok'), 'skipped (no mapping)', `${event} has no TikTok mapping`);
903
+ }
904
+ },
905
+ },
906
+
907
+ {
908
+ name: 'a-cancellation-and-a-refund-build-exclusion-audiences-worth-zero',
909
+ auth: 'none',
910
+
911
+ async run({ assert, ctx, Manager }) {
912
+ // The churn moments an ad platform CAN use
913
+ // ([#415](https://github.com/Omega-JS-Stack/omega/issues/415)): neither
914
+ // Meta nor TikTok can subtract revenue and neither optimizes against an
915
+ // event, but both can build an exclusion audience from a custom one. So
916
+ // each webhook fire that already reaches GA4 now resolves on the two
917
+ // platforms as well — at value ZERO, because a refund carrying its
918
+ // amount would ADD to the return their ads manager reports.
919
+ for (const [event, native] of [['subscription_cancel', 'SubscriptionCancel'], ['refund', 'Refund']]) {
920
+ const results = deliver({ ctx, Manager, event });
921
+
922
+ const google = descriptorFor(results, 'ga4');
923
+ const meta = descriptorFor(results, 'meta');
924
+ const tiktok = descriptorFor(results, 'tiktok');
925
+
926
+ assert.equal(google.name, event, `${event} still resolves on GA4 under its own name`);
927
+ assert.equal(google.payload.value, 9.99, 'GA4 keeps the real number — its revenue report is where a refund nets out');
928
+
929
+ assert.equal(meta.name, native, `${event} reaches Meta as ${native}`);
930
+ assert.equal(meta.kind, 'custom', 'Meta defines no churn event, so the pixel command is trackCustom');
931
+ assert.equal(meta.payload.value, 0, 'a churn signal must never book revenue in an ad account');
932
+ assert.deepEqual(meta.payload.content_ids, ['premium'], 'the plan is what makes the audience worth building');
933
+
934
+ assert.equal(tiktok.name, native, `${event} reaches TikTok as ${native}`);
935
+ assert.equal(tiktok.kind, 'custom');
936
+ assert.equal(tiktok.payload.value, 0, 'TikTok cannot subtract revenue either');
937
+ assert.equal(tiktok.payload.contents[0].content_id, 'premium');
938
+ assert.equal(tiktok.payload.contents[0].price, undefined, 'the per-item price would put the amount back on the wire');
939
+ assert.equal(tiktok.payload.contents[0].quantity, undefined, 'so it comes off with the value, like Meta\'s signal');
940
+
941
+ // The bodies that would go on the wire carry the same zero.
942
+ const body = conversions.buildMetaBody({ descriptor: meta, identity: {}, eventId: `${event}._test-webhook-event` });
943
+ const tiktokBody = conversions.buildTikTokBody({ descriptor: tiktok, identity: {}, eventId: `${event}._test-webhook-event`, pixelCode: '_TEST_PIXEL' });
944
+
945
+ assert.equal(body.data[0].event_name, native);
946
+ assert.equal(body.data[0].custom_data.value, 0);
947
+ assert.equal(tiktokBody.data[0].event, native);
948
+ assert.equal(tiktokBody.data[0].properties.value, 0);
949
+ assert.equal(JSON.stringify(tiktokBody).includes('9.99'), false, 'no amount rides a churn body under ANY key');
950
+ }
951
+ },
952
+ },
953
+
954
+ {
955
+ name: 'a-trial-conversion-reaches-all-three-as-real-revenue',
956
+ auth: 'none',
957
+
958
+ async run({ assert, ctx, Manager }) {
959
+ // The one [#407] addition where money actually moved, so unlike the four
960
+ // above it carries a full mapping: GA4's standard purchase (revenue
961
+ // belongs in the revenue report) and each ad platform's Purchase, which
962
+ // is what every real charge is to them (Ian's money-event table, [#652]).
963
+ const results = deliver({
964
+ ctx,
965
+ Manager,
966
+ event: 'trial_convert',
967
+ params: { ...PURCHASE_PARAMS, is_trial: true, is_recurring: false },
968
+ });
969
+
970
+ const google = descriptorFor(results, 'ga4');
971
+
972
+ assert.equal(google.name, 'purchase', 'the first real charge lands in GA4 revenue');
973
+ assert.equal(google.payload.value, 9.99);
974
+ assert.equal(google.payload.is_trial, true, 'the flag pair is what marks it inside GA4 purchase');
975
+ assert.equal(google.payload.is_recurring, false);
976
+
977
+ assert.equal(descriptorFor(results, 'meta').name, 'Purchase');
978
+ assert.equal(descriptorFor(results, 'tiktok').name, 'Purchase');
979
+ },
980
+ },
981
+
982
+ {
983
+ // An emulator boot seeds personas, every seeded account fired the server
984
+ // half of `sign_up`, and Meta DELIVERED dozens of fake registrations to the
985
+ // brand's live pixel — real ad data polluted by a dev run
986
+ // ([#464](https://github.com/Omega-JS-Stack/omega/issues/464)). The gate is
987
+ // the one `@omega.js/monitoring` and the GA4 Measurement Protocol helper
988
+ // already use: outside production, nothing goes out.
989
+ name: 'a-dev-run-resolves-the-whole-walk-and-delivers-nothing',
990
+ auth: 'none',
991
+
992
+ async run({ assert }) {
993
+ const { results, calls, lines } = fireOnRecordedWire({ production: false });
994
+
995
+ assert.equal(calls.length, 0, `an emulator run must reach no platform: ${calls.map((call) => call.url).join(', ')}`);
996
+
997
+ for (const [provider, native] of [['meta', 'Purchase'], ['tiktok', 'Purchase']]) {
998
+ assert.equal(outcomeFor(results, provider), 'blocked (dev)', `${provider} says it was blocked, not that it sent`);
999
+ assert.equal(descriptorFor(results, provider).name, native, `${provider} still resolves — the dev trace names what WOULD have fired`);
1000
+
1001
+ const blocked = lines.filter((line) => line.includes(`[${provider}]`) && line.includes('blocked (dev)'));
1002
+
1003
+ assert.equal(blocked.length, 1, `exactly one blocked line for ${provider}: ${lines.join(' | ')}`);
1004
+ assert.equal(blocked[0].includes('purchase'), true, `the line names the event: ${blocked[0]}`);
1005
+ }
1006
+
1007
+ // And the same fire in production is the proof the gate reads the
1008
+ // environment rather than simply never sending.
1009
+ assert.equal(fireOnRecordedWire({ production: true }).calls.length, 2, 'production still delivers to both platforms');
1010
+ },
1011
+ },
1012
+ ],
1013
+ });