@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,853 @@
1
+ /**
2
+ * Newsletter generation iteration test.
3
+ *
4
+ * Two modes:
5
+ *
6
+ * 1. FIXTURE MODE (default — runs in every test invocation, no env required)
7
+ * Loads a hand-crafted structure JSON from test/email/fixtures/<template>.json
8
+ * and renders it through the active template. No AI, no source fetching,
9
+ * no images, ~25-50ms render time, $0 cost. This is what runs in CI and
10
+ * what you iterate against for layout/CSS changes.
11
+ *
12
+ * Fixture-name resolution priority:
13
+ * 1. NEWSLETTER_FIXTURE=<name> (explicit override)
14
+ * 2. NEWSLETTER_TEMPLATE=<name> (use the fixture for the override template)
15
+ * 3. config.marketing.newsletter.content.template (use the active brand's template)
16
+ * 4. 'clean' (universal fallback)
17
+ *
18
+ * Add a new template? Drop a matching JSON in test/email/fixtures/<name>.json.
19
+ * The fixture's content shape MUST match the template's `schema` export.
20
+ *
21
+ * 2. AI PIPELINE MODE (set TEST_EXTENDED_MODE=1)
22
+ * Pulls real sources from the parent @omega.js/backend server, runs them through the
23
+ * structure → SVG → MJML pipeline, and writes a preview HTML. Same code
24
+ * path the daily pre-generation cron uses. Costs money (AI tokens). Use
25
+ * this when you want to evaluate prompt quality against real sources.
26
+ *
27
+ * Output (both modes): <projectRoot>/.temp/newsletter/run-<timestamp>/ (one level above functions/)
28
+ *
29
+ * Run from somiibo-backend/functions:
30
+ * npx omega test project:marketing/newsletter-generate.js # fixture (fast, free)
31
+ * NEWSLETTER_FIXTURE=editorial npx omega test project:marketing/newsletter-generate.js
32
+ * TEST_EXTENDED_MODE=1 npx omega test project:marketing/newsletter-generate.js # AI pipeline
33
+ *
34
+ * --- Env vars (most apply to AI mode only) ---
35
+ * NEWSLETTER_FIXTURE=<name> Load and render a specific fixture (fixture mode).
36
+ * NEWSLETTER_TEMPLATE=<name> Override the layout template for this run.
37
+ * NEWSLETTER_THEME_ONLY=1 Reuse the most recent AI run's structure.json + PNGs
38
+ * and re-render. Different from FIXTURE: this loads
39
+ * prior AI output, FIXTURE loads hand-crafted JSON.
40
+ * NEWSLETTER_REUSE_RUN=<dir> Pair with THEME_ONLY: reuse a specific run dir.
41
+ * NEWSLETTER_OPEN=1 Auto-open the rendered preview in the default browser (macOS only).
42
+ *
43
+ * --- AI-mode-only env vars (require TEST_EXTENDED_MODE=1) ---
44
+ * NEWSLETTER_SOURCE=<type> Override the source type for this run. Bypasses parent-server
45
+ * fetch and lets the generator's built-in resolver run.
46
+ * Values: '$feed:<url>', '$parent'. When set, sources[] in the
47
+ * config is replaced with this single source.
48
+ * Example: NEWSLETTER_SOURCE='$feed:https://feeds.arstechnica.com/arstechnica/index'
49
+ * NEWSLETTER_PEEK=1 Fetch + list ready sources, do not claim, exit.
50
+ * NEWSLETTER_SOURCE_ID=<id> Generate from one specific source WITHOUT claiming it.
51
+ * NEWSLETTER_LIMIT=10 Sources per category for PEEK (default 10).
52
+ * NEWSLETTER_CLAIM=1 CLAIM the fetched sources (claimFor=brandId), consuming them so the
53
+ * real newsletter won't reuse them. OFF by default — the test fetches
54
+ * without claiming so runs are repeatable and non-destructive.
55
+ * NEWSLETTER_RELEASE=1 Reset locally-tracked claimed sources back to 'ready'.
56
+ * NEWSLETTER_NO_IMAGES=1 Skip SVG/PNG generation (fast iteration on copy only).
57
+ * NEWSLETTER_CREATE_ARTICLE=1 PUBLISH the linked blog article to the website repo (Ghostii → admin/post → GitHub).
58
+ * The article is always GENERATED when the brand's
59
+ * marketing.newsletter.content.article.enabled is on (exercises the Ghostii write +
60
+ * URL/CTA path); this flag only controls whether it's actually committed.
61
+ * OFF by default — a newsletter test run never commits a real post.
62
+ * NEWSLETTER_PROVIDER_STRUCTURE=X Override structure provider (openai|anthropic).
63
+ * NEWSLETTER_PROVIDER_SVG=X Override SVG provider (openai|anthropic).
64
+ * NEWSLETTER_CAMPAIGN_ID=<id> Override the auto-generated campaign ID (folder name in newsletter-assets).
65
+ *
66
+ * In EXTENDED mode, the test ALWAYS uploads PNGs + newsletter.html to GitHub
67
+ * and creates a Beehiiv draft — same side effects as the production cron.
68
+ * No opt-out, no per-side-effect flag. EXTENDED = production-equivalent run.
69
+ *
70
+ * --- THEME_ONLY-mode-only env vars ---
71
+ * NEWSLETTER_BEEHIIV_UPLOAD=1 After re-rendering, upload as a new Beehiiv draft (rare).
72
+ *
73
+ * AI mode requires:
74
+ * OMEGA_ADMIN_KEY — authenticates with parent as admin
75
+ * OPENAI_API_KEY — structure provider
76
+ * ANTHROPIC_API_KEY — SVG provider
77
+ * PARENT_API_URL — or set `parent` in config/omega.json5
78
+ *
79
+ * Fixture mode requires: nothing.
80
+ */
81
+ const path = require('path');
82
+ const { execSync } = require('child_process');
83
+ const fetch = require('wonderful-fetch');
84
+ const jetpack = require('fs-jetpack');
85
+ const defineCases = require('../../dist/vendor/devkit/test/define-cases.js');
86
+
87
+ module.exports = defineCases({
88
+ description: 'Generate a newsletter preview (fixture by default, full AI pipeline with TEST_EXTENDED_MODE)',
89
+ auth: 'none',
90
+ timeout: 300000, // 5 min — AI structure + image generation + GitHub upload + Beehiiv draft
91
+ // The test ALWAYS runs. By default it renders a hand-crafted fixture for the
92
+ // active template (fast, free, deterministic — catches layout regressions in
93
+ // CI). Set TEST_EXTENDED_MODE=1 to switch to the full AI pipeline that fetches
94
+ // real sources, calls the structure + SVG providers, and writes a preview.
95
+ // Other modes (FIXTURE, THEME_ONLY, RELEASE, PEEK) are also opt-in via env.
96
+ async run({ assert, config, Manager, ctx, skip }) {
97
+ const env = process.env;
98
+
99
+ // --- Apply env overrides into newsletterConfig ---
100
+ // Newsletter config lives under marketing.newsletter.content (array or object).
101
+ const rawContent = config.marketing?.newsletter?.content;
102
+ const contentEntry = Array.isArray(rawContent) ? rawContent[0] : rawContent;
103
+ const newsletterConfig = JSON.parse(JSON.stringify(contentEntry || {}));
104
+
105
+ if (env.NEWSLETTER_PROVIDER_STRUCTURE) {
106
+ newsletterConfig.provider = { ...(newsletterConfig.provider || {}), structure: env.NEWSLETTER_PROVIDER_STRUCTURE };
107
+ }
108
+ if (env.NEWSLETTER_PROVIDER_SVG) {
109
+ newsletterConfig.provider = { ...(newsletterConfig.provider || {}), svg: env.NEWSLETTER_PROVIDER_SVG };
110
+ }
111
+ if (env.NEWSLETTER_TEMPLATE) {
112
+ newsletterConfig.template = env.NEWSLETTER_TEMPLATE;
113
+ }
114
+
115
+ // --- Build the output dir ---
116
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-').replace(/-(\d{3})Z$/, '');
117
+ // .temp lives at the CONSUMER PROJECT ROOT (e.g. somiibo-backend/.temp), not
118
+ // inside functions/. Matches the convention used by UJM, BXM, electron-manager,
119
+ // etc. — every project's transient cache directory sits at the repo root.
120
+ const outRoot = path.join(process.cwd(), '..', '.temp', 'newsletter');
121
+ const runDir = path.join(outRoot, `run-${stamp}`);
122
+ const claimedFile = path.join(outRoot, '.claimed.json');
123
+
124
+ jetpack.dir(runDir);
125
+
126
+ // --- Release mode (early return) ---
127
+ if (env.NEWSLETTER_RELEASE) {
128
+ const released = await releaseAll(claimedFile);
129
+ console.log(`Released ${released} previously claimed source(s) back to ready`);
130
+ return;
131
+ }
132
+
133
+ // --- Fixture mode ---
134
+ // Default behavior of this test. Loads a hand-crafted structure JSON from
135
+ // test/email/fixtures/{name}.json and renders it directly. No AI, no
136
+ // source fetching, no images. Used as the deterministic "predictable
137
+ // preview" loop AND as the default test-suite render.
138
+ //
139
+ // Fixture-name resolution priority:
140
+ // 1. NEWSLETTER_FIXTURE=<name> (explicit override)
141
+ // 2. NEWSLETTER_TEMPLATE=<name> (use the fixture for the override template)
142
+ // 3. newsletterConfig.template (use the fixture for the active brand's template)
143
+ // 4. 'clean' (universal fallback)
144
+ //
145
+ // This means a fresh `npx omega test` against any consumer project picks the
146
+ // brand's configured template, renders the matching fixture, and produces
147
+ // a deterministic preview HTML — no AI, no money spent.
148
+ //
149
+ // The full AI pipeline only runs when TEST_EXTENDED_MODE=1 is set (below).
150
+ // Explicit NEWSLETTER_FIXTURE wins even in EXTENDED mode (you can ask for
151
+ // a fixture render mid-AI-iteration without re-cycling envs).
152
+ // NEWSLETTER_THEME_ONLY takes precedence below — it's a no-AI mode that
153
+ // reuses prior AI output, distinct from fixture mode.
154
+ if (env.NEWSLETTER_THEME_ONLY) {
155
+ // Fall through to the theme-only block below.
156
+ } else if (!env.TEST_EXTENDED_MODE || env.NEWSLETTER_FIXTURE) {
157
+ const requestedFixture = (env.NEWSLETTER_FIXTURE || env.NEWSLETTER_TEMPLATE || newsletterConfig.template || 'clean')
158
+ .replace(/^fixture:/, '');
159
+ const fixturePath = path.join(__dirname, 'fixtures', `${requestedFixture}.json`);
160
+
161
+ assert.ok(jetpack.exists(fixturePath),
162
+ `Fixture not found: ${fixturePath}. Available fixtures: ${jetpack.list(path.join(__dirname, 'fixtures')).filter((f) => f.endsWith('.json')).join(', ')}. Every registered template should ship a matching fixture in test/email/fixtures/.`);
163
+
164
+ const structure = jetpack.read(fixturePath, 'json');
165
+
166
+ assert.ok(structure, `Fixture ${requestedFixture}.json failed to parse as JSON`);
167
+
168
+ // Pin the template to the fixture name — fixtures are authored for a
169
+ // specific template's content shape, so this prevents the obvious bug
170
+ // of "render the field-report fixture through the clean template".
171
+ // Explicit NEWSLETTER_TEMPLATE override is still honored above.
172
+ if (!env.NEWSLETTER_TEMPLATE) {
173
+ newsletterConfig.template = requestedFixture;
174
+ }
175
+
176
+ const { renderNewsletter } = require('../../dist/manager/libraries/email/generators/lib/mjml-template.js');
177
+ const { renderMarkdown } = require('../../dist/manager/libraries/email/generators/lib/markdown-renderer.js');
178
+
179
+ const renderStart = Date.now();
180
+ const { html, mjml, template: templateName } = await renderNewsletter({
181
+ brand: config.brand,
182
+ newsletterConfig,
183
+ structure,
184
+ imagePaths: [],
185
+ campaign: `fixture-${requestedFixture}`,
186
+ });
187
+ // Force the template metadata onto structure so renderMarkdown picks
188
+ // the right body strategy (fixtures don't carry _meta on their own).
189
+ Object.defineProperty(structure, '_meta', {
190
+ enumerable: false,
191
+ configurable: true,
192
+ value: { template: templateName },
193
+ });
194
+ const markdown = renderMarkdown({
195
+ structure,
196
+ brand: config.brand,
197
+ imagePaths: [],
198
+ });
199
+ const renderMs = Date.now() - renderStart;
200
+
201
+ const previewPath = path.join(runDir, 'newsletter.html');
202
+ jetpack.write(previewPath, html);
203
+ jetpack.write(path.join(runDir, 'newsletter.md'), markdown);
204
+ if (structure.summary) {
205
+ jetpack.write(path.join(runDir, 'summary.md'), structure.summary + '\n');
206
+ }
207
+ jetpack.write(path.join(runDir, 'newsletter.mjml'), mjml || '');
208
+ jetpack.write(path.join(runDir, 'structure.json'), JSON.stringify(structure, null, 2));
209
+ jetpack.write(path.join(runDir, 'metadata.json'), JSON.stringify({
210
+ mode: 'fixture',
211
+ fixture: requestedFixture,
212
+ template: templateName,
213
+ renderMs,
214
+ tags: structure.tags || [],
215
+ timestamp: new Date().toISOString(),
216
+ }, null, 2));
217
+
218
+ console.log(`\n[fixture=${requestedFixture}] Rendered in ${renderMs}ms using template: ${templateName}`);
219
+ console.log(`[fixture=${requestedFixture}] Preview: ${previewPath}`);
220
+ console.log(`[fixture=${requestedFixture}] Markdown: ${path.join(runDir, 'newsletter.md')}`);
221
+ console.log(`[fixture=${requestedFixture}] (Set TEST_EXTENDED_MODE=1 to switch to the full AI pipeline.)`);
222
+
223
+ if (env.NEWSLETTER_OPEN === '1' && process.platform === 'darwin') {
224
+ try { execSync(`open "${previewPath}"`); } catch (e) { /* no-op */ }
225
+ }
226
+
227
+ assert.ok(html.includes('<html'), 'Rendered HTML');
228
+ assert.ok(markdown.includes('# '), 'Rendered markdown has a heading');
229
+ return;
230
+ }
231
+
232
+ // --- Theme-only mode (early return) ---
233
+ // Reuse a previous run's structure.json + PNGs to re-render MJML/HTML only.
234
+ // Sub-second iteration on layout / theme tokens with no AI cost.
235
+ if (env.NEWSLETTER_THEME_ONLY) {
236
+ const sourceRun = resolveReuseRun({
237
+ outRoot,
238
+ explicit: env.NEWSLETTER_REUSE_RUN,
239
+ });
240
+
241
+ assert.ok(sourceRun, 'Found a previous run to reuse (set NEWSLETTER_REUSE_RUN=run-<stamp> if needed)');
242
+
243
+ console.log(`[theme-only] reusing structure + images from: ${path.basename(sourceRun)}`);
244
+
245
+ const structure = jetpack.read(path.join(sourceRun, 'structure.json'), 'json');
246
+
247
+ assert.ok(structure?.sections?.length, 'Previous run has a valid structure.json');
248
+
249
+ // Copy section PNGs from previous run into this run's dir so paths resolve
250
+ const imagePaths = [];
251
+
252
+ for (let i = 0; i < structure.sections.length; i++) {
253
+ const filename = `section-${i + 1}.png`;
254
+ const srcPath = path.join(sourceRun, filename);
255
+
256
+ if (jetpack.exists(srcPath)) {
257
+ jetpack.copy(srcPath, path.join(runDir, filename));
258
+ imagePaths.push(`./${filename}`);
259
+ } else {
260
+ imagePaths.push(null);
261
+ }
262
+ }
263
+
264
+ const { renderNewsletter } = require('../../dist/manager/libraries/email/generators/lib/mjml-template.js');
265
+ const { renderMarkdown } = require('../../dist/manager/libraries/email/generators/lib/markdown-renderer.js');
266
+
267
+ const renderStart = Date.now();
268
+ const { html, mjml, template: templateName } = await renderNewsletter({
269
+ brand: config.brand,
270
+ newsletterConfig,
271
+ structure,
272
+ imagePaths,
273
+ campaign: `theme-only-${stamp}`,
274
+ });
275
+ // Force the template metadata onto structure so renderMarkdown picks
276
+ // the right body strategy (reused structures may have lost _meta).
277
+ Object.defineProperty(structure, '_meta', {
278
+ enumerable: false,
279
+ configurable: true,
280
+ value: { template: templateName },
281
+ });
282
+ const markdown = renderMarkdown({
283
+ structure,
284
+ brand: config.brand,
285
+ imagePaths,
286
+ });
287
+ const renderMs = Date.now() - renderStart;
288
+
289
+ const previewPath = path.join(runDir, 'newsletter.html');
290
+ jetpack.write(previewPath, html);
291
+ jetpack.write(path.join(runDir, 'newsletter.md'), markdown);
292
+ if (structure.summary) {
293
+ jetpack.write(path.join(runDir, 'summary.md'), structure.summary + '\n');
294
+ }
295
+ jetpack.write(path.join(runDir, 'newsletter.mjml'), mjml || '');
296
+ jetpack.write(path.join(runDir, 'structure.json'), JSON.stringify(structure, null, 2));
297
+ jetpack.write(path.join(runDir, 'metadata.json'), JSON.stringify({
298
+ mode: 'theme-only',
299
+ reusedFrom: path.basename(sourceRun),
300
+ template: templateName,
301
+ renderMs,
302
+ tags: structure.tags || [],
303
+ timestamp: new Date().toISOString(),
304
+ }, null, 2));
305
+
306
+ console.log(`\n[theme-only] Rendered in ${renderMs}ms using template: ${templateName}`);
307
+ console.log(`[theme-only] Preview: ${previewPath}`);
308
+
309
+ // Optional: upload to Beehiiv as a draft
310
+ if (env.NEWSLETTER_BEEHIIV_UPLOAD) {
311
+ await uploadDraftToBeehiiv({
312
+ html,
313
+ structure,
314
+ config,
315
+ ctx: console,
316
+ runDir,
317
+ });
318
+ }
319
+
320
+ if (env.NEWSLETTER_OPEN === '1' && process.platform === 'darwin') {
321
+ try { execSync(`open "${previewPath}"`); } catch (e) { /* no-op */ }
322
+ }
323
+
324
+ assert.ok(html.includes('<html'), 'Rendered HTML');
325
+ return;
326
+ }
327
+
328
+ // --- AI pipeline path (TEST_EXTENDED_MODE) ---
329
+ // Everything below this point talks to the real parent server and the AI
330
+ // providers. The parent URL is required for any of it.
331
+ // Use Manager.getParentApiUrl() — same helper the production newsletter
332
+ // generator uses. config.parent stores the parent's brand URL WITHOUT the
333
+ // `api.` subdomain (e.g. 'https://itwcreativeworks.com'); the helper
334
+ // inserts `api.` at call time. PARENT_API_URL env override is honored
335
+ // verbatim for one-off testing against a different parent.
336
+ // When NEWSLETTER_SOURCE is set, skip parent URL checks and source fetching —
337
+ // the generator's built-in resolver (resolveSources) handles everything.
338
+ let sources = [];
339
+
340
+ if (!env.NEWSLETTER_SOURCE) {
341
+ const parentUrl = env.PARENT_API_URL || Manager.getParentApiUrl();
342
+ assert.ok(parentUrl, 'PARENT_API_URL (env) or parent (config) must be set for the AI pipeline. Set TEST_EXTENDED_MODE=1 to run it, or omit TEST_EXTENDED_MODE for the fast fixture preview.');
343
+
344
+ // --- Peek mode (early return) ---
345
+ if (env.NEWSLETTER_PEEK) {
346
+ const peeked = await peekSources({
347
+ parentUrl,
348
+ categories: newsletterConfig.categories || [],
349
+ limit: parseInt(env.NEWSLETTER_LIMIT, 10) || 10,
350
+ key: env.OMEGA_ADMIN_KEY,
351
+ });
352
+
353
+ console.log(`\nPeek mode — ${peeked.length} ready source(s):\n`);
354
+ peeked.forEach((s, i) => {
355
+ const raw = s.source || {};
356
+ const cats = (s.categories || []).join(', ') || s.category || '(none)';
357
+ console.log(`[${i + 1}] ${s.id}`);
358
+ console.log(` Categories: ${cats}`);
359
+ console.log(` From: ${raw.from || s.from || '(unknown)'}`);
360
+ console.log(` Subject: ${raw.subject || s.subject || '(none)'}`);
361
+ console.log(` Headline: ${s.ai?.headline || '(none — not yet AI-processed)'}`);
362
+ console.log('');
363
+ });
364
+
365
+ assert.ok(true, `Peeked ${peeked.length} sources`);
366
+ return;
367
+ }
368
+
369
+ // --- Fetch sources ---
370
+ const claim = !!env.NEWSLETTER_CLAIM;
371
+ sources = await fetchSourcesForRun({
372
+ parentUrl,
373
+ newsletterConfig,
374
+ brandId: config.brand?.id,
375
+ sourceId: env.NEWSLETTER_SOURCE_ID,
376
+ key: env.OMEGA_ADMIN_KEY,
377
+ claim,
378
+ });
379
+
380
+ if (sources.length === 0) {
381
+ return skip('No ready newsletter sources available on parent server (environmental)');
382
+ }
383
+
384
+ if (claim && !env.NEWSLETTER_SOURCE_ID) {
385
+ appendClaimed(claimedFile, sources.map((s) => s.id));
386
+ }
387
+ }
388
+
389
+ // Force `newsletter.enabled: true` and inject the per-run newsletter config
390
+ // overrides onto Manager.config. The iteration test IS the explicit trigger
391
+ // — we're not checking whether newsletter is configured for prod use, we're
392
+ // driving the generator directly. Mutating Manager.config is fine here
393
+ // because this is a `type: 'standalone'` test (one test per process — no
394
+ // cross-test config leakage).
395
+ Manager.config.marketing = {
396
+ ...(Manager.config.marketing || {}),
397
+ newsletter: {
398
+ ...(Manager.config.marketing?.newsletter || {}),
399
+ enabled: true,
400
+ content: newsletterConfig,
401
+ },
402
+ };
403
+
404
+ // --- Run the production generator with the local-persist image hook ---
405
+ const generator = require('../../dist/manager/libraries/email/generators/newsletter.js');
406
+
407
+ // EXTENDED mode mirrors the production cron's newsletter side effects:
408
+ // GH upload always happens (PNGs + newsletter.html), Beehiiv draft upload
409
+ // always happens (governed inside newsletter.js by newsletter.enabled, which
410
+ // we force true above). If you don't want the side effects, run fixture
411
+ // mode instead.
412
+ //
413
+ // The ONE deliberate exception is PUBLISHING the linked blog article: the
414
+ // article is still generated (so the test exercises the Ghostii write + CTA
415
+ // path), but it's NOT committed to the website repo unless you opt in with
416
+ // NEWSLETTER_CREATE_ARTICLE=1 (publishArticle below). Committing a real post
417
+ // is out of scope for a routine newsletter test.
418
+ //
419
+ // persistImage is a side-effect callback that writes PNG+SVG to runDir for
420
+ // local preview / debug. Its return value is ignored when imageHost: 'github'
421
+ // because the generator uses the uploaded CDN URLs in the rendered HTML.
422
+ const persistImage = async (image, idx) => {
423
+ const filename = `section-${idx + 1}.png`;
424
+
425
+ jetpack.write(path.join(runDir, filename), image.png);
426
+ if (image.svg) {
427
+ jetpack.write(path.join(runDir, `section-${idx + 1}.svg`), image.svg);
428
+ }
429
+
430
+ return `./${filename}`;
431
+ };
432
+
433
+ // The campaignId becomes the GitHub folder name. Either pinned via env
434
+ // (for repeatable iteration on the same folder) or auto-generated by
435
+ // newsletter.generate() in Firestore auto-ID shape.
436
+ const campaignId = env.NEWSLETTER_CAMPAIGN_ID || undefined;
437
+
438
+ console.log(`[extended] uploading to itw-creative-works/newsletter-assets/${config.brand?.id}/${campaignId || '<auto-id>'}/ + Beehiiv draft`);
439
+
440
+ // NEWSLETTER_SOURCE overrides the configured sources[] and lets the
441
+ // generator's built-in resolver run (resolveSources — the unified
442
+ // blog/newsletter resolver). Without this, the test always passes
443
+ // pre-fetched parent sources via opts.sources, bypassing resolution.
444
+ // NEWSLETTER_SOURCE='$feed:https://feeds.arstechnica.com/arstechnica/index'
445
+ // NEWSLETTER_SOURCE='$parent' (default behavior, explicit)
446
+ const useResolverSources = !!env.NEWSLETTER_SOURCE;
447
+
448
+ if (useResolverSources) {
449
+ newsletterConfig.sources = [env.NEWSLETTER_SOURCE];
450
+ Manager.config.marketing.newsletter.content = newsletterConfig;
451
+ }
452
+
453
+ const result = await generator.generate(
454
+ Manager,
455
+ ctx,
456
+ { name: `${config.brand?.name || 'Brand'} Newsletter — Iteration ${stamp}` },
457
+ {
458
+ sources: useResolverSources ? undefined : sources,
459
+ skipImages: !!env.NEWSLETTER_NO_IMAGES,
460
+ // The article is GENERATED whenever the brand's config.article.enabled is on
461
+ // (exercises the Ghostii write + URL/CTA path), but only PUBLISHED to the
462
+ // website repo when you opt in with NEWSLETTER_CREATE_ARTICLE=1. Default
463
+ // test run generates but does not commit a real post.
464
+ publishArticle: !!env.NEWSLETTER_CREATE_ARTICLE,
465
+ // Local disk persistence runs unconditionally (for preview/debug)
466
+ persistImage,
467
+ // EXTENDED always uploads to GitHub — mirrors production cron exactly
468
+ imageHost: 'github',
469
+ campaignId,
470
+ }
471
+ );
472
+
473
+ assert.ok(result, 'Generator returned a result');
474
+ assert.ok(result.contentHtml, 'Generator returned contentHtml');
475
+ assert.ok(result.contentMarkdown, 'Generator returned contentMarkdown');
476
+ assert.ok(result.structure?.sections?.length >= 2, 'Has at least 2 sections');
477
+
478
+ // Regression guard: with NEWSLETTER_CREATE_ARTICLE=1 the linked article
479
+ // must actually PUBLISH (commit to the website repo) and the CTA must be
480
+ // injected. Catches publish-path failures that generate-only runs mask
481
+ // (e.g. the v5.11.0 `sources` scope bug that silently failed every
482
+ // production publish inside buildLinkedArticle's try/catch).
483
+ if (env.NEWSLETTER_CREATE_ARTICLE && result.article) {
484
+ assert.ok(result.article.published === true,
485
+ `Linked article must be published when NEWSLETTER_CREATE_ARTICLE=1 (url: ${result.article.url})`);
486
+ assert.ok(result.structure.sections[0]?.cta?.url === result.article.url,
487
+ 'Lead section CTA must point at the published article');
488
+ }
489
+
490
+ // --- Write outputs ---
491
+ const previewPath = path.join(runDir, 'newsletter.html');
492
+ jetpack.write(previewPath, result.contentHtml);
493
+ jetpack.write(path.join(runDir, 'newsletter.md'), result.contentMarkdown);
494
+ if (result.summary) {
495
+ jetpack.write(path.join(runDir, 'summary.md'), result.summary + '\n');
496
+ }
497
+ jetpack.write(path.join(runDir, 'structure.json'), JSON.stringify(result.structure, null, 2));
498
+ jetpack.write(path.join(runDir, 'newsletter.mjml'), result.mjml || '');
499
+ jetpack.write(path.join(runDir, 'metadata.json'), JSON.stringify({
500
+ ...(result.meta || {}),
501
+ tags: result.tags || [],
502
+ assets: result.assets || null,
503
+ }, null, 2));
504
+
505
+ // Linked blog article (when content.article.enabled). Save the full Ghostii
506
+ // output + the computed URL so you can review what would be published — both
507
+ // the raw JSON and a readable markdown view of the article body.
508
+ if (result.article?.article) {
509
+ const a = result.article.article;
510
+ jetpack.write(path.join(runDir, 'article.json'), JSON.stringify(result.article, null, 2));
511
+ jetpack.write(path.join(runDir, 'article.md'), [
512
+ `# ${a.title || ''}`,
513
+ '',
514
+ `> ${a.description || ''}`,
515
+ '',
516
+ `**URL:** ${result.article.url || '(none)'}`,
517
+ `**Published:** ${result.article.published ? 'yes' : 'no (generate-only)'}`,
518
+ a.headerImageUrl ? `**Header image:** ${a.headerImageUrl}` : '',
519
+ a.categories?.length ? `**Categories:** ${a.categories.join(', ')}` : '',
520
+ a.keywords?.length ? `**Keywords:** ${a.keywords.join(', ')}` : '',
521
+ '',
522
+ '---',
523
+ '',
524
+ a.body || '',
525
+ ].filter(line => line !== undefined).join('\n'));
526
+ }
527
+
528
+ console.log(`\nNewsletter preview written: ${previewPath}`);
529
+ console.log(`Subject: ${result.subject}`);
530
+ console.log(`Preheader: ${result.preheader}`);
531
+ console.log(`Sections: ${result.structure.sections.length}`);
532
+ if (result.article?.article) {
533
+ console.log(`Article: "${result.article.article.title}" → ${result.article.url} (${result.article.published ? 'published' : 'generate-only'})`);
534
+ console.log(` ${path.join(runDir, 'article.md')}`);
535
+ }
536
+ if (result.meta?.totals) {
537
+ const t = result.meta.totals;
538
+ console.log(`\nRun summary:`);
539
+ console.log(` Total duration: ${(result.meta.totalDurationMs / 1000).toFixed(1)}s`);
540
+ console.log(` AI calls: ${t.aiCalls}`);
541
+ console.log(` Tokens: ${t.inputTokens} in / ${t.outputTokens} out (${t.totalTokens} total)`);
542
+ console.log(` Cost: $${t.totalCostUSD}`);
543
+ console.log(` Filter: provider=${result.meta.steps.filter?.provider} model=${result.meta.steps.filter?.model} (${result.meta.steps.filter?.durationMs}ms)`);
544
+ console.log(` Structure: provider=${result.meta.steps.structure?.provider} model=${result.meta.steps.structure?.model} (${result.meta.steps.structure?.durationMs}ms)`);
545
+ for (const img of result.meta.steps.images || []) {
546
+ console.log(` Image ${img.section}: provider=${img.provider} model=${img.model} (${img.durationMs}ms${img.fallback ? ', FALLBACK' : ''})`);
547
+ }
548
+ }
549
+
550
+ // Beehiiv draft upload already happened inside generator.generate() —
551
+ // newsletter.js calls beehiiv.createPost(draft) after the GH HTML upload.
552
+ // Look for assets.beehiivPostId in the result (null on free plan; non-null
553
+ // once the Beehiiv account is on Enterprise).
554
+ if (result.assets?.beehiivPostId) {
555
+ console.log(`Beehiiv: draft post created — id=${result.assets.beehiivPostId}`);
556
+ }
557
+
558
+ // --- Auto-open in browser (macOS) ---
559
+ if (env.NEWSLETTER_OPEN === '1' && process.platform === 'darwin') {
560
+ try {
561
+ execSync(`open "${previewPath}"`);
562
+ } catch (e) {
563
+ console.warn('Failed to auto-open preview:', e.message);
564
+ }
565
+ }
566
+ },
567
+ });
568
+
569
+ /**
570
+ * Upload the rendered HTML to Beehiiv as a draft post (never sends). Uses the
571
+ * v2 Posts API directly so it works against the test's stub Manager.
572
+ *
573
+ * Writes the Beehiiv response to {runDir}/beehiiv-upload.json for inspection.
574
+ *
575
+ * Required env: BEEHIIV_API_KEY
576
+ * Required config: marketing.newsletter.providers.beehiiv.publicationId (or we fuzzy-match by brand name)
577
+ */
578
+ async function uploadDraftToBeehiiv({ html, structure, config, runDir }) {
579
+ const apiKey = process.env.BEEHIIV_API_KEY;
580
+
581
+ if (!apiKey) {
582
+ console.warn('[beehiiv] Skipping upload — BEEHIIV_API_KEY not set');
583
+ return;
584
+ }
585
+
586
+ const BASE_URL = 'https://api.beehiiv.com/v2';
587
+ const headers = { 'Authorization': `Bearer ${apiKey}` };
588
+
589
+ // Resolve publication ID — config first, then fuzzy-match by brand name
590
+ let publicationId = config?.marketing?.newsletter?.providers?.beehiiv?.publicationId;
591
+ const brandName = config?.brand?.name;
592
+
593
+ if (!publicationId && brandName) {
594
+ try {
595
+ const pubs = await fetch(`${BASE_URL}/publications?limit=100`, {
596
+ response: 'json',
597
+ headers,
598
+ timeout: 10000,
599
+ });
600
+ const brandLower = brandName.toLowerCase();
601
+ const matched = (pubs.data || []).find((p) =>
602
+ p.name.toLowerCase() === brandLower
603
+ || p.name.toLowerCase().includes(brandLower)
604
+ || brandLower.includes(p.name.toLowerCase())
605
+ );
606
+ publicationId = matched?.id;
607
+ } catch (e) {
608
+ console.error('[beehiiv] Publication lookup failed:', e.message);
609
+ }
610
+ }
611
+
612
+ if (!publicationId) {
613
+ console.warn(`[beehiiv] Skipping upload — could not resolve publication ID (brand=${brandName})`);
614
+ return;
615
+ }
616
+
617
+ // Build the post — always draft from the test (never sends)
618
+ const body = {
619
+ title: structure.subject || `Newsletter ${new Date().toISOString()}`,
620
+ status: 'draft',
621
+ body_content: html,
622
+ email_settings: {
623
+ subject_line: structure.subject,
624
+ preview_text: structure.preheader || '',
625
+ },
626
+ };
627
+
628
+ console.log(`[beehiiv] Uploading draft to publication ${publicationId}...`);
629
+
630
+ try {
631
+ const data = await fetch(`${BASE_URL}/publications/${publicationId}/posts`, {
632
+ method: 'post',
633
+ response: 'json',
634
+ headers,
635
+ timeout: 30000,
636
+ body,
637
+ });
638
+
639
+ if (data.data?.id) {
640
+ const editUrl = `https://app.beehiiv.com/posts/${data.data.id}`;
641
+ console.log(`[beehiiv] ✓ Draft uploaded: ${data.data.id}`);
642
+ console.log(`[beehiiv] Edit at: ${editUrl}`);
643
+ jetpack.write(path.join(runDir, 'beehiiv-upload.json'), JSON.stringify({
644
+ publicationId,
645
+ postId: data.data.id,
646
+ editUrl,
647
+ status: 'draft',
648
+ uploadedAt: new Date().toISOString(),
649
+ response: data.data,
650
+ }, null, 2));
651
+ } else {
652
+ console.error('[beehiiv] Upload returned no post ID:', JSON.stringify(data));
653
+ }
654
+ } catch (e) {
655
+ console.error('[beehiiv] Upload failed:', e.message);
656
+ }
657
+ }
658
+
659
+ // --- Helpers ---
660
+
661
+ /**
662
+ * GET /newsletter/sources without claiming (claimFor omitted).
663
+ */
664
+ async function peekSources({ parentUrl, categories, limit, key }) {
665
+ if (!categories.length) {
666
+ // Dual-send like source-resolver: header for new-stack parents, legacy
667
+ // query field for BEM parents
668
+ const data = await fetch(`${parentUrl}/newsletter-sources`, {
669
+ method: 'get',
670
+ response: 'json',
671
+ timeout: 15000,
672
+ headers: { 'omega-admin-key': key },
673
+ query: { limit, backendManagerKey: key },
674
+ });
675
+
676
+ return data.sources || [];
677
+ }
678
+
679
+ const all = [];
680
+ for (const category of categories) {
681
+ const data = await fetch(`${parentUrl}/newsletter-sources`, {
682
+ method: 'get',
683
+ response: 'json',
684
+ timeout: 15000,
685
+ headers: { 'omega-admin-key': key },
686
+ query: { category, limit, backendManagerKey: key },
687
+ });
688
+
689
+ all.push(...(data.sources || []));
690
+ }
691
+
692
+ return all;
693
+ }
694
+
695
+ /**
696
+ * Fetch sources for an actual generation run. Either:
697
+ * - A specific source by id — preview only, NO claim (iterate repeatedly on the same source)
698
+ * - Or N per category. Fetches WITHOUT claiming by default (claim=false), so runs are
699
+ * repeatable; pass claim=true (NEWSLETTER_CLAIM=1) to atomically claim/consume them.
700
+ *
701
+ * When NEWSLETTER_SOURCE_ID is set, we look the source up in any status
702
+ * (ready, claimed, used) so you can keep iterating on it across runs without
703
+ * the parent server's claim mechanism marking it consumed.
704
+ */
705
+ async function fetchSourcesForRun({ parentUrl, newsletterConfig, brandId, sourceId, key, claim = false }) {
706
+ if (sourceId) {
707
+ // Peek across ALL ready sources (no claim). Search broadly first, then
708
+ // fall back to any-status if needed. We never call claimFor with sourceId
709
+ // so the source stays available for future runs.
710
+ const all = await peekSources({
711
+ parentUrl,
712
+ categories: newsletterConfig.categories || [],
713
+ limit: 100,
714
+ key,
715
+ });
716
+
717
+ const match = all.find((s) => s.id === sourceId);
718
+
719
+ if (match) {
720
+ console.log(`[source-id-mode] Found ${sourceId} in ready pool, using WITHOUT claiming (iteration mode)`);
721
+ return [match];
722
+ }
723
+
724
+ // Not in ready pool — could be already claimed/used. Try the broad endpoint without category filter.
725
+ const broad = await peekSources({
726
+ parentUrl,
727
+ categories: [],
728
+ limit: 100,
729
+ key,
730
+ });
731
+ const matchBroad = broad.find((s) => s.id === sourceId);
732
+
733
+ if (matchBroad) {
734
+ console.log(`[source-id-mode] Found ${sourceId} (outside configured categories), using WITHOUT claiming`);
735
+ return [matchBroad];
736
+ }
737
+
738
+ throw new Error(`Source ${sourceId} not found. It may have been used (status: used) or never existed.`);
739
+ }
740
+
741
+ // Normal: claim N per category
742
+ const categories = newsletterConfig.categories || [];
743
+
744
+ if (!categories.length) {
745
+ throw new Error('marketing.newsletter.content.categories must have at least one entry');
746
+ }
747
+
748
+ const all = [];
749
+ for (const category of categories) {
750
+ // claimFor is what tells the parent server to mark sources consumed. Omit it
751
+ // (claim=false) to fetch the same sources without claiming them.
752
+ const query = { category, limit: 3, backendManagerKey: key };
753
+ if (claim) {
754
+ query.claimFor = brandId;
755
+ }
756
+
757
+ const data = await fetch(`${parentUrl}/newsletter-sources`, {
758
+ method: 'get',
759
+ response: 'json',
760
+ timeout: 15000,
761
+ headers: { 'omega-admin-key': key },
762
+ query,
763
+ });
764
+ all.push(...(data.sources || []));
765
+ }
766
+
767
+ return all;
768
+ }
769
+
770
+ /**
771
+ * Find a previous run directory to reuse for theme-only iteration.
772
+ * Either honors NEWSLETTER_REUSE_RUN explicitly, or picks the most recent
773
+ * run-* directory under outRoot. Skips the current in-progress run dir.
774
+ */
775
+ function resolveReuseRun({ outRoot, explicit }) {
776
+ if (explicit) {
777
+ const candidate = path.join(outRoot, explicit);
778
+ return jetpack.exists(candidate) ? candidate : null;
779
+ }
780
+
781
+ if (!jetpack.exists(outRoot)) {
782
+ return null;
783
+ }
784
+
785
+ const entries = jetpack.list(outRoot) || [];
786
+ const runs = entries
787
+ .filter((name) => name.startsWith('run-'))
788
+ .map((name) => ({ name, full: path.join(outRoot, name) }))
789
+ .filter((r) => {
790
+ // Must contain a structure.json to be reusable
791
+ return jetpack.exists(path.join(r.full, 'structure.json')) === 'file';
792
+ })
793
+ .sort((a, b) => (a.name < b.name ? 1 : -1));
794
+
795
+ return runs[0]?.full || null;
796
+ }
797
+
798
+ function appendClaimed(claimedFile, ids) {
799
+ const existing = jetpack.exists(claimedFile) ? jetpack.read(claimedFile, 'json') || [] : [];
800
+ const stamped = ids.map((id) => ({ id, claimedAt: Math.round(Date.now() / 1000) }));
801
+ jetpack.write(claimedFile, [...existing, ...stamped]);
802
+ }
803
+
804
+ async function releaseAll(claimedFile) {
805
+ if (!jetpack.exists(claimedFile)) {
806
+ return 0;
807
+ }
808
+
809
+ const entries = jetpack.read(claimedFile, 'json') || [];
810
+
811
+ if (!entries.length) {
812
+ return 0;
813
+ }
814
+
815
+ const saPath = process.env.PARENT_SERVICE_ACCOUNT_PATH;
816
+
817
+ if (!saPath) {
818
+ throw new Error('PARENT_SERVICE_ACCOUNT_PATH env var is required for release. Point it at the parent project service-account.json.');
819
+ }
820
+
821
+ // Lazy-require firebase-admin
822
+ const admin = require('firebase-admin');
823
+
824
+ if (!admin.apps.length) {
825
+ admin.initializeApp({
826
+ credential: admin.credential.cert(require(saPath)),
827
+ });
828
+ }
829
+
830
+ const db = admin.firestore();
831
+ let released = 0;
832
+
833
+ for (const entry of entries) {
834
+ try {
835
+ await db.collection('newsletter-sources').doc(entry.id).update({
836
+ status: 'ready',
837
+ usedBy: null,
838
+ claimedBy: null,
839
+ usedAt: null,
840
+ claimedAt: null,
841
+ });
842
+ released++;
843
+ } catch (e) {
844
+ console.warn(`Failed to release ${entry.id}: ${e.message}`);
845
+ }
846
+ }
847
+
848
+ // Clear the tracker
849
+ jetpack.write(claimedFile, []);
850
+
851
+ return released;
852
+ }
853
+