@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,899 @@
1
+ /**
2
+ * Newsletter generator — pulls content from parent server and assembles a branded newsletter.
3
+ *
4
+ * Called by the frequent cron (and the iteration test) when a campaign has
5
+ * `generator: 'newsletter'`. Produces a fully rendered email-safe HTML newsletter ready
6
+ * to ship to Beehiiv / SendGrid.
7
+ *
8
+ * Pipeline:
9
+ * 1. Read newsletter categories from Manager.config.marketing.newsletter.content.categories
10
+ * 2. Fetch ready sources from parent server (no claiming — child tracks locally)
11
+ * 3. structure.js → AI authors subject, preheader, intro, sections, signoff
12
+ * 4. image-illustrator.js → AI generates one flat-vector PNG per section via
13
+ * gpt-image-2 (default). Set content.method.image = 'svg' to use the legacy
14
+ * svg-illustrator.js (AI writes an <svg>, resvg rasterizes it) instead.
15
+ * 5. mjml-template.js → compile MJML → email-safe HTML
16
+ * 6. Persist PNGs (caller-provided via opts.persistImage or default no-op)
17
+ * 7. Mark sources as used on parent server
18
+ *
19
+ * Returns { subject, preheader, content, contentHtml, structure, images } so the
20
+ * marketing library can either send the HTML directly (contentHtml) or fall back to
21
+ * the markdown pipeline (content) if needed.
22
+ */
23
+ const fetch = require('wonderful-fetch');
24
+ const powertools = require('node-powertools');
25
+
26
+ const { filterSources } = require('./lib/filter.js');
27
+ const { generateStructure } = require('./lib/structure.js');
28
+ const { generateSectionImage: generateImageSection } = require('./lib/image-illustrator.js');
29
+ const { generateSectionImage: generateSvgSection } = require('./lib/svg-illustrator.js');
30
+ const { renderNewsletter } = require('./lib/mjml-template.js');
31
+
32
+ // Default illustration method. 'image' = gpt-image-2 flat-vector PNGs (default).
33
+ // 'svg' = legacy AI-authored SVG rasterized via resvg. Selected per-brand via
34
+ // marketing.newsletter.content.method.image.
35
+ const DEFAULT_IMAGE_METHOD = 'image';
36
+
37
+ function resolveSectionImageFn(newsletterConfig) {
38
+ const method = newsletterConfig?.method?.image || DEFAULT_IMAGE_METHOD;
39
+ return method === 'svg' ? generateSvgSection : generateImageSection;
40
+ }
41
+ const { renderMarkdown } = require('./lib/markdown-renderer.js');
42
+ const { escapeHtml, safeUrl } = require('../constants.js');
43
+ const { uploadAssets, USE_CDN_URLS, REPO_OWNER, REPO_NAME, RAW_BASE } = require('./lib/image-host.js');
44
+ const { buildPublicConfig } = require('../../../routes/brand/get.js');
45
+ const { writeArticle, publishArticle } = require('../../../libraries/content/ghostii.js');
46
+ const { trackContentSource, contentSourceHash, resolveSources } = require('../../../libraries/content/source-resolver.js');
47
+
48
+ /**
49
+ * Generate newsletter content from parent server sources.
50
+ *
51
+ * @param {object} Manager - @omega.js/backend Manager instance
52
+ * @param {object} ctx - @omega.js/backend ctx instance
53
+ * @param {object} settings - Campaign settings from the recurring template
54
+ * @param {object} [opts] - Optional overrides used by the iteration test
55
+ * @param {function} [opts.persistImage] - async (image, idx) => imagePath (URL or relative path).
56
+ * When provided, takes precedence over imageHost — used by the
57
+ * iteration test to write PNGs locally for preview.
58
+ * @param {'github'|'local'} [opts.imageHost] - Where to host images for the final HTML.
59
+ * Defaults to 'github' (uploads to itw-creative-works/newsletter-assets).
60
+ * Local skips upload — only useful if persistImage is also set.
61
+ * @param {string} [opts.campaignId] - Stable ID used as the folder name in newsletter-assets.
62
+ * Defaults to the Firestore campaign doc ID if available.
63
+ * @param {object[]} [opts.sources] - Pre-fetched sources (bypasses parent server claim)
64
+ * @param {boolean} [opts.skipImages] - Skip SVG/PNG generation (use placeholders)
65
+ * @param {boolean} [opts.publishArticle] - When the linked-article build runs (config.article.enabled),
66
+ * actually COMMIT the post to the website repo via admin/post.
67
+ * When false (default), the article is still generated and its
68
+ * URL computed + injected as the CTA, but nothing is committed.
69
+ * Production cron passes true; the iteration test leaves it false.
70
+ * @returns {object|null} Updated settings with content filled in, or null if unavailable
71
+ */
72
+ async function generate(Manager, ctx, settings, opts = {}) {
73
+ // Same convention as the marketing providers (email/marketing/index.js):
74
+ // default test runs never hit the real pipeline (AI calls + GitHub commit +
75
+ // Beehiiv post + report email). TEST_EXTENDED_MODE opts into
76
+ // production-equivalent side effects.
77
+ if (ctx.isTesting() && !process.env.TEST_EXTENDED_MODE) {
78
+ ctx.log('Newsletter generator: skipped in test mode (set TEST_EXTENDED_MODE to run the real pipeline)');
79
+ return null;
80
+ }
81
+
82
+ const newsletterRoleConfig = Manager.config?.marketing?.newsletter;
83
+ const rawContent = newsletterRoleConfig?.content;
84
+
85
+ if (!newsletterRoleConfig?.enabled) {
86
+ ctx.log('Newsletter generator: newsletter disabled in config');
87
+ return null;
88
+ }
89
+
90
+ if (!rawContent) {
91
+ ctx.log('Newsletter generator: no marketing.newsletter.content config block');
92
+ return null;
93
+ }
94
+
95
+ // Content can be an array or single object (matches blog config shape)
96
+ const contentArray = powertools.arrayify(rawContent);
97
+ const config = contentArray[0];
98
+
99
+ if (!config) {
100
+ ctx.log('Newsletter generator: empty content array');
101
+ return null;
102
+ }
103
+
104
+ // Default sources to ['$parent'] if not specified
105
+ const contentSources = config.sources || ['$parent'];
106
+
107
+ // Either use pre-fetched sources (iteration test) or resolve from config
108
+ let sources = opts.sources;
109
+
110
+ if (!sources) {
111
+ const categories = config.categories || [];
112
+
113
+ if (!categories.length) {
114
+ ctx.log('Newsletter generator: no categories configured');
115
+ return null;
116
+ }
117
+
118
+ const admin = Manager.libraries?.admin;
119
+
120
+ // Unified resolver — same picking/fallback/dedup as the blog pipeline.
121
+ // config.sourceCount controls how many sources feed the issue (default 6).
122
+ const resolved = await resolveSources({
123
+ sources: contentSources,
124
+ count: config.sourceCount || 6,
125
+ categories,
126
+ admin,
127
+ Manager,
128
+ ctx,
129
+ });
130
+
131
+ sources = resolved
132
+ .filter((r) => r.type === 'feed' || r.type === 'parent')
133
+ .map((r) => toNewsletterSource(r, categories));
134
+ }
135
+
136
+ if (!sources?.length) {
137
+ ctx.log('Newsletter generator: no sources available');
138
+ return null;
139
+ }
140
+
141
+ const brand = Manager.config?.brand;
142
+ const ai = Manager.AI(ctx);
143
+ const pipelineStart = Date.now();
144
+
145
+ // 1. Filter — drop sources that don't fit the brand
146
+ const { kept: filteredSources, scores, meta: filterMeta } = await filterSources({
147
+ sources,
148
+ brand,
149
+ newsletterConfig: config,
150
+ ai,
151
+ ctx,
152
+ threshold: opts.fitThreshold,
153
+ });
154
+
155
+ if (!filteredSources.length) {
156
+ ctx.log('Newsletter generator: no sources passed brand-fit filter, skipping');
157
+ return null;
158
+ }
159
+
160
+ ctx.log(`Newsletter generator: assembling from ${filteredSources.length} brand-fit sources (out of ${sources.length})`);
161
+
162
+ // 2. Structure
163
+ const structure = await generateStructure({
164
+ sources: filteredSources,
165
+ brand,
166
+ newsletterConfig: config,
167
+ ai,
168
+ ctx,
169
+ });
170
+
171
+ ctx.log(`Newsletter generator: structure ready (${structure.sections.length} sections)`);
172
+
173
+ // Asset hosting target — controls what URLs end up in <img src=...> AND
174
+ // whether the rendered HTML gets uploaded too. Two values:
175
+ // - 'github': upload PNGs + newsletter.html to itw-creative-works/newsletter-assets,
176
+ // embed raw.githubusercontent.com URLs. The production cron path.
177
+ // - 'local': use whatever persistImage returns (iteration test writes to disk).
178
+ // Defaults to 'github' so production cron path "just works" without flag fiddling.
179
+ const host = opts.imageHost || 'github';
180
+
181
+ // CDN base URL — resolves from the parent config.
182
+ // parent: 'https://itwcreativeworks.com' → cdn.itwcreativeworks.com
183
+ // parent: 'self' / '' / null → falls back to brand.url
184
+ let cdnBase = null;
185
+ if (USE_CDN_URLS && host === 'github') {
186
+ const parentUrl = Manager.config?.parent;
187
+ let cdnDomain;
188
+ if (parentUrl && parentUrl !== 'self' && parentUrl !== '$self' && parentUrl.startsWith('http')) {
189
+ cdnDomain = new URL(parentUrl).hostname;
190
+ } else {
191
+ cdnDomain = brand?.url ? new URL(brand.url).hostname : null;
192
+ }
193
+ if (cdnDomain) {
194
+ cdnBase = `https://cdn.${cdnDomain}/newsletters`;
195
+ }
196
+ }
197
+
198
+ // campaignId — used as the GitHub folder name (and as the doc ID in production).
199
+ // Resolution priority (most-specific first):
200
+ // 1. opts.campaignId — explicit override (test runs, cron paths)
201
+ // 2. settings.id — marketing-campaigns/{id} doc ID in production
202
+ // 3. sources[0].id — when exactly one source is being processed
203
+ // (iteration test pinning to a single source —
204
+ // means re-running against the same source
205
+ // overwrites the same folder, no churn)
206
+ // 4. generatePushId() — final fallback for ad-hoc runs with no anchor
207
+ const campaignId = opts.campaignId
208
+ || settings?.id
209
+ || (sources.length === 1 ? sources[0].id : null)
210
+ || generatePushId();
211
+
212
+ // 2. SVG illustrations + (optional) linked blog article — run CONCURRENTLY.
213
+ // Both are slow AI calls. The image build produces the section image URLs;
214
+ // the article build expands the lead section into a full blog post and
215
+ // returns its public URL, which we inject as a "Read more" CTA before render.
216
+ let imagePaths = [];
217
+ let generatedImages = null;
218
+
219
+ const buildImages = async () => {
220
+ if (opts.skipImages) {
221
+ return;
222
+ }
223
+
224
+ const generateSectionImage = resolveSectionImageFn(config);
225
+ generatedImages = await Promise.all(
226
+ structure.sections.map((s) => generateSectionImage({
227
+ imagePrompt: s.image_prompt,
228
+ brand,
229
+ newsletterConfig: config,
230
+ ai,
231
+ ctx,
232
+ }))
233
+ );
234
+
235
+ // Run persistImage side-effects (writes to disk for iteration test).
236
+ const persistedPaths = typeof opts.persistImage === 'function'
237
+ ? await Promise.all(generatedImages.map((img, i) => opts.persistImage(img, i)))
238
+ : null;
239
+
240
+ if (host === 'github') {
241
+ // Compute image URLs from the deterministic path — no upload needed yet.
242
+ // The URLs are based on branch + path, not commit SHA, so they're valid
243
+ // before the commit exists. Everything uploads in one atomic commit later.
244
+ const imgBase = cdnBase ? `${cdnBase}/${brand?.id}` : `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${brand?.id}`;
245
+ imagePaths = generatedImages.map((_, i) => `${imgBase}/content/${campaignId}/section-${i + 1}.png`);
246
+ } else {
247
+ imagePaths = persistedPaths || generatedImages.map((_, i) => `about:blank#section-${i + 1}`);
248
+ }
249
+
250
+ ctx.log(`Newsletter generator: ${generatedImages.length} images rendered`);
251
+ opts._lastImages = generatedImages;
252
+ };
253
+
254
+ // The linked-article build is gated by config.article.enabled and needs a lead
255
+ // section to expand. Wrapped so a failure here NEVER blocks the newsletter —
256
+ // it resolves to null and the CTA simply isn't injected.
257
+ //
258
+ // Two phases, independently controllable:
259
+ // - GENERATE: always runs when article.enabled is on. Calls Ghostii to write
260
+ // the article + hero image and computes the public URL it WOULD live at.
261
+ // - PUBLISH: commits the post to the website repo via admin/post. Only when
262
+ // opts.publishArticle is true. The production cron passes true; the
263
+ // iteration test leaves it false (so it exercises generation without
264
+ // committing a real post) unless NEWSLETTER_CREATE_ARTICLE=1 is set.
265
+ //
266
+ // The CTA URL is derived from the article title (same slugify admin/post uses),
267
+ // so the newsletter links correctly even in generate-only mode.
268
+ const wantArticle = !!config.article?.enabled
269
+ && Array.isArray(structure.sections)
270
+ && structure.sections.length > 0;
271
+
272
+ const buildArticle = async () => {
273
+ if (!wantArticle) {
274
+ return null;
275
+ }
276
+
277
+ return buildLinkedArticle({
278
+ Manager,
279
+ ctx,
280
+ brand,
281
+ config,
282
+ structure,
283
+ sources: filteredSources,
284
+ publish: !!opts.publishArticle,
285
+ }).catch((e) => {
286
+ ctx.error(`Newsletter generator: linked article failed — ${e.message}`);
287
+ return null;
288
+ });
289
+ };
290
+
291
+ const [, articleResult] = await Promise.all([buildImages(), buildArticle()]);
292
+
293
+ // Inject the "Read the full article" CTA onto the lead section BEFORE render,
294
+ // but ONLY if the article was actually published. A CTA pointing to a
295
+ // non-existent post is worse than no CTA at all.
296
+ if (articleResult?.url && articleResult.published) {
297
+ structure.sections[0].cta = { label: 'Read the full article', url: articleResult.url };
298
+ ctx.log(`Newsletter generator: linked article published — ${articleResult.url}`);
299
+ } else if (articleResult?.url) {
300
+ ctx.log(`Newsletter generator: linked article generated (not published, CTA omitted) — ${articleResult.url}`);
301
+ }
302
+
303
+ // 3. MJML → HTML
304
+ // Sponsorships precedence: opts.sponsorships > campaign settings.sponsorships > config.sponsorships
305
+ const sponsorships = opts.sponsorships
306
+ || settings?.sponsorships
307
+ || config?.sponsorships
308
+ || [];
309
+
310
+ const { html, mjml } = await renderNewsletter({
311
+ brand,
312
+ newsletterConfig: config,
313
+ structure,
314
+ imagePaths,
315
+ campaign: settings?.name || 'newsletter',
316
+ sponsorships,
317
+ });
318
+
319
+ // 3a. Programmatic markdown view — same `structure`, walked by code (no AI).
320
+ // The markdown is split into ## blocks per section so it can be pasted
321
+ // into Beehiiv's block editor with ad blocks inserted between dispatches.
322
+ // `summary` is a separate short editorial recap, written by the AI
323
+ // during the structure step.
324
+ const markdown = renderMarkdown({
325
+ structure,
326
+ brand,
327
+ imagePaths,
328
+ sponsorships,
329
+ });
330
+
331
+ const summaryText = (structure.summary || '').trim();
332
+
333
+ // 3b. Upload EVERYTHING to GitHub in one atomic commit — images + HTML +
334
+ // markdown + summary. Image URLs were pre-computed from the deterministic
335
+ // path, so the HTML already embeds the correct URLs before the commit exists.
336
+ let assetsFolderUrl = null;
337
+ let htmlUrl = null;
338
+ let previewUrl = null;
339
+ let markdownUrl = null;
340
+ let summaryUrl = null;
341
+
342
+ if (host === 'github') {
343
+ try {
344
+ const upload = await uploadAssets({
345
+ images: generatedImages,
346
+ html,
347
+ markdown,
348
+ summary: summaryText || undefined,
349
+ brandId: brand?.id,
350
+ campaignId,
351
+ subject: structure.subject,
352
+ cdnBase,
353
+ ctx,
354
+ });
355
+ assetsFolderUrl = upload.folderUrl;
356
+ htmlUrl = upload.htmlUrl;
357
+ previewUrl = upload.previewUrl || null;
358
+ markdownUrl = upload.markdownUrl || null;
359
+ summaryUrl = upload.summaryUrl || null;
360
+ } catch (e) {
361
+ ctx.error(`Newsletter generator: asset upload failed — ${e.message}`);
362
+ }
363
+ }
364
+
365
+ // 3c. Upload to Beehiiv. Published in production, forced to draft in
366
+ // testing so real subscribers never receive test newsletters. Failure
367
+ // is logged, never thrown — the rest of the pipeline (GH archive,
368
+ // Firestore doc) succeeds regardless.
369
+ let beehiivPostId = null;
370
+ let beehiivFailureReason = null;
371
+
372
+ if (host === 'github' && newsletterRoleConfig?.enabled) {
373
+ try {
374
+ const beehiivProvider = require('../providers/beehiiv.js');
375
+ const result = await beehiivProvider.createPost({
376
+ publicationId: newsletterRoleConfig.providers?.beehiiv?.publicationId,
377
+ title: structure.subject,
378
+ subject: structure.subject,
379
+ preheader: structure.preheader,
380
+ content: html,
381
+ contentTags: Array.isArray(structure.tags) ? structure.tags : [],
382
+ status: ctx.isTesting() ? 'draft' : 'confirmed',
383
+ });
384
+
385
+ if (result?.success && result.id) {
386
+ beehiivPostId = result.id;
387
+ ctx.log(`Newsletter generator: Beehiiv ${ctx.isTesting() ? 'draft' : 'post'} created — ${beehiivPostId}`);
388
+ } else {
389
+ beehiivFailureReason = result?.error || 'unknown error';
390
+ ctx.log(`Newsletter generator: Beehiiv upload failed — ${beehiivFailureReason}`);
391
+ }
392
+ } catch (e) {
393
+ beehiivFailureReason = e.message;
394
+ ctx.error(`Newsletter generator: Beehiiv draft upload threw — ${e.message}`);
395
+ }
396
+ }
397
+
398
+ // 3d. Newsletter report email — always sent to the brand's internal alerts
399
+ // inbox after generation completes. Contains everything needed for
400
+ // review and manual Beehiiv upload if needed.
401
+ await sendNewsletterReportEmail(Manager, ctx, {
402
+ brand,
403
+ subject: structure.subject,
404
+ preheader: structure.preheader,
405
+ tags: Array.isArray(structure.tags) ? structure.tags : [],
406
+ htmlUrl,
407
+ previewUrl,
408
+ markdownUrl,
409
+ summaryUrl,
410
+ folderUrl: assetsFolderUrl,
411
+ beehiivPostId,
412
+ beehiivFailureReason,
413
+ articles: (articleResult?.published) ? [{ title: articleResult.article?.title || structure.sections?.[0]?.title, url: articleResult.url }] : [],
414
+ sources: sources.map(s => ({
415
+ title: s.title || '',
416
+ url: (s.url && s.url.startsWith('http')) ? s.url : '',
417
+ from: s.source?.from || '',
418
+ })),
419
+ });
420
+
421
+ // 4. Mark all sources used in the unified content-sources collection —
422
+ // only NOW that the newsletter actually generated successfully.
423
+ // trackingData comes from the resolver; pre-fetched test sources
424
+ // without it get a best-effort fallback.
425
+ const admin = Manager.libraries?.admin;
426
+ if (admin) {
427
+ for (const source of sources) {
428
+ const tracking = source.trackingData || {
429
+ url: source.url || source.id,
430
+ origin: '$parent',
431
+ itemId: source.id,
432
+ itemTitle: source.title || '',
433
+ };
434
+
435
+ await trackContentSource(admin, {
436
+ ...tracking,
437
+ usedBy: 'newsletter',
438
+ brandId: brand?.id || '',
439
+ }).catch((e) => {
440
+ ctx.error(`Newsletter generator: Error tracking content source (non-fatal): ${e.message}`);
441
+ });
442
+ }
443
+ }
444
+
445
+ // Aggregate per-step metadata for telemetry / cost tracking
446
+ const meta = {
447
+ timestamp: new Date().toISOString(),
448
+ totalDurationMs: Date.now() - pipelineStart,
449
+ brand: { id: brand?.id, name: brand?.name },
450
+ config: {
451
+ categories: config.categories,
452
+ tone: config.tone,
453
+ template: config.template,
454
+ },
455
+ sources: {
456
+ total: sources.length,
457
+ filtered: filteredSources.length,
458
+ scores,
459
+ },
460
+ steps: {
461
+ filter: filterMeta || null,
462
+ structure: structure._meta || null,
463
+ images: (opts._lastImages || []).map((img, i) => ({
464
+ section: i + 1,
465
+ ...(img.meta || {}),
466
+ })),
467
+ },
468
+ totals: aggregateTotals(filterMeta, structure._meta, opts._lastImages),
469
+ // Linked blog article (when config.article.enabled is on). null if disabled or failed.
470
+ // `published` is false when the article was generated but not committed (e.g. test mode).
471
+ article: articleResult
472
+ ? { url: articleResult.url, slug: articleResult.slug, path: articleResult.path, published: !!articleResult.published }
473
+ : null,
474
+ };
475
+
476
+ // Public asset URLs — stamped onto the generated campaign doc by the cron
477
+ // so you can find the GitHub folder + downloadable HTML + image URLs without
478
+ // re-deriving them. Null entries mean GitHub upload was skipped (local mode)
479
+ // or failed (errors already logged above). beehiivPostId is the ID of the
480
+ // draft post created on Beehiiv (or null if creation failed — expected until
481
+ // we move off the free Beehiiv plan).
482
+ const assets = host === 'github' ? {
483
+ campaignId,
484
+ folderUrl: assetsFolderUrl,
485
+ htmlUrl,
486
+ previewUrl,
487
+ markdownUrl,
488
+ summaryUrl,
489
+ imageUrls: imagePaths,
490
+ beehiivPostId,
491
+ articleUrl: articleResult?.url || null, // linked blog post (config.article.enabled), null otherwise
492
+ tags: Array.isArray(structure.tags) ? structure.tags : [],
493
+ } : null;
494
+
495
+ return {
496
+ ...settings,
497
+ subject: structure.subject,
498
+ preheader: structure.preheader,
499
+ content: '', // legacy markdown field, unused when contentHtml is set
500
+ contentHtml: html, // pre-rendered email-safe HTML
501
+ contentMarkdown: markdown, // programmatic markdown view (per-section blocks for Beehiiv paste)
502
+ summary: summaryText, // editorial recap (separate from preheader)
503
+ tags: Array.isArray(structure.tags) ? structure.tags : [],
504
+ structure, // structured copy for debugging / migration
505
+ mjml, // raw MJML for debugging
506
+ images: opts._lastImages || [], // image buffers for the iteration test to persist locally
507
+ assets, // GitHub asset URLs (folder, html, md, summary, images) — null in local mode
508
+ meta, // per-step provider/model/cost/timing telemetry
509
+ article: articleResult || null, // full linked-article result { url, slug, path, published, article: {title, body, headerImageUrl, ...} } — null when disabled/failed
510
+ };
511
+ }
512
+
513
+ /**
514
+ * Expand the newsletter's lead section into a full blog article via Ghostii and
515
+ * (optionally) publish it to the brand's website repo. Returns the post's public
516
+ * URL so the newsletter can link to it via a "Read the full article" CTA.
517
+ *
518
+ * The lead section (structure.sections[0]) is the same topic the newsletter
519
+ * leads with, so the resulting article is the long-form version.
520
+ *
521
+ * Two phases:
522
+ * - GENERATE (always): Ghostii writes the article + hero image. We then compute
523
+ * the public URL the post WOULD live at, using the same title→slug rule
524
+ * admin/post applies. This URL is valid for the CTA whether or not we publish.
525
+ * - PUBLISH (only when `publish`): commit the post to the website repo via
526
+ * admin/post (GitHub). When `publish` is false, nothing is committed.
527
+ *
528
+ * Gated upstream by config.article.enabled. Any failure is caught by the caller
529
+ * and the CTA is simply omitted — the newsletter never depends on this.
530
+ *
531
+ * @param {object} args
532
+ * @param {object} args.Manager
533
+ * @param {object} args.ctx
534
+ * @param {object} args.brand - { id, name, url, ... }
535
+ * @param {object} args.config - marketing.newsletter.content (tone, instructions, article.author)
536
+ * @param {object} args.structure - newsletter structure (sections[0] is the lead)
537
+ * @param {object[]} [args.sources] - The brand-fit sources that fed the issue. Their URLs are
538
+ * passed to admin/post as the article's `source` attribution.
539
+ * @param {boolean} [args.publish] - Commit the post to GitHub via admin/post. Default false.
540
+ * @returns {Promise<{url, slug, path, published}|null>}
541
+ */
542
+ async function buildLinkedArticle({ Manager, ctx, brand, config, structure, sources, publish }) {
543
+ const lead = structure.sections[0] || {};
544
+ const publicConfig = buildPublicConfig(Manager.config);
545
+
546
+ // Build the article brief from the lead section, folded with the shared
547
+ // editorial steer (tone + instructions) so the blog post matches the
548
+ // newsletter's voice. Ghostii expands this into a full article + hero image.
549
+ const briefLines = [
550
+ `Company: ${brand?.name || ''}: ${brand?.description || ''}`,
551
+ config?.tone ? `Tone: ${config.tone}` : '',
552
+ config?.instructions ? `Instructions: ${config.instructions}` : '',
553
+ '',
554
+ `Write a full blog article expanding on this topic:`,
555
+ `Title: ${lead.title || ''}`,
556
+ `Summary: ${lead.body || ''}`,
557
+ ].filter(Boolean).join('\n');
558
+
559
+ ctx.log(`Newsletter generator: building linked article for "${lead.title}"`);
560
+
561
+ // Phase 1 — GENERATE (always)
562
+ const article = await writeArticle({
563
+ brand: publicConfig,
564
+ description: briefLines,
565
+ });
566
+
567
+ // Compute the public URL the post WOULD live at. admin/post slugifies the
568
+ // title (after stripping a `blog/` prefix that titles never have), so we
569
+ // mirror that here to derive the same slug without needing to publish.
570
+ const slug = Manager.Utilities().slugify(article.title || '');
571
+ const url = slug
572
+ ? `${(publicConfig.brand?.url || '').replace(/\/$/, '')}/blog/${slug}`
573
+ : null;
574
+
575
+ // Phase 2 — PUBLISH (gated). Commit to the website repo only when asked.
576
+ if (!publish) {
577
+ ctx.log(`Newsletter generator: article generated but NOT published (publish=false) — would live at ${url}`);
578
+ return { url, slug, path: null, published: false, article };
579
+ }
580
+
581
+ try {
582
+ const sourceUrls = (sources || []).map(s => s.url || s.id).filter(Boolean);
583
+ const result = await publishArticle(ctx, {
584
+ brand: publicConfig,
585
+ article,
586
+ id: Math.round(Date.now() / 1000),
587
+ author: config?.article?.author,
588
+ postPath: 'newsletter',
589
+ source: sourceUrls.join(', '),
590
+ });
591
+
592
+ if (result?.path) {
593
+ return { url: result.url || url, slug: result.slug || slug, path: result.path, published: true, article };
594
+ }
595
+
596
+ ctx.log(`Newsletter generator: publishArticle returned no path — treating as unpublished`);
597
+ return { url, slug, path: null, published: false, article };
598
+ } catch (e) {
599
+ ctx.error(`Newsletter generator: publishArticle failed — ${e.message}`);
600
+ return { url, slug, path: null, published: false, article };
601
+ }
602
+ }
603
+
604
+ /**
605
+ * Send an internal alert email when Beehiiv draft creation fails so the
606
+ * brand team knows there's a ready newsletter waiting for manual upload.
607
+ *
608
+ * Uses `sender: 'internal'` which auto-resolves to `alerts@{brandDomain}` via
609
+ * the SENDERS table in email/constants.js. Recipient is the same alerts@
610
+ * address — a self-addressed operational alert, no human inbox involved.
611
+ *
612
+ * Errors here are logged but never thrown — the alert is best-effort. If
613
+ * brand.url is unset, the email is skipped entirely.
614
+ *
615
+ * @param {object} Manager
616
+ * @param {object} ctx
617
+ * @param {object} args
618
+ * @param {object} args.brand
619
+ * @param {string} args.subject
620
+ * @param {string} args.preheader
621
+ * @param {string[]} args.tags
622
+ * @param {string} [args.htmlUrl]
623
+ * @param {string} [args.markdownUrl]
624
+ * @param {string} [args.previewUrl]
625
+ * @param {string} [args.summaryUrl]
626
+ * @param {string} [args.folderUrl]
627
+ * @param {string} [args.beehiivPostId] - Beehiiv post ID (if upload succeeded)
628
+ * @param {string} [args.beehiivFailureReason] - Why Beehiiv upload failed (null if succeeded)
629
+ * @param {object[]} [args.articles] - Published linked articles
630
+ * @param {object[]} [args.sources] - Content sources used for generation
631
+ */
632
+ async function sendNewsletterReportEmail(Manager, ctx, args) {
633
+ const brandDomain = Manager.config?.brand?.contact?.email?.split('@')[1];
634
+
635
+ if (!brandDomain) {
636
+ ctx.log('Newsletter generator: report email skipped — no brand.contact.email');
637
+ return;
638
+ }
639
+
640
+ const alertsEmail = `alerts@${brandDomain}`;
641
+ const beehiivOk = !!args.beehiivPostId;
642
+
643
+ try {
644
+ const email = Manager.Email(ctx);
645
+
646
+ // The report body is hand-built markup (trustedContent below). Everything it
647
+ // interpolates is AI-authored (subject, preheader, tags) or scraped from a source
648
+ // (titles, URLs), so it gets escaped on the way in.
649
+ const messageLines = [];
650
+
651
+ // --- Status ---
652
+ if (beehiivOk) {
653
+ messageLines.push(`The newsletter has been generated and uploaded to Beehiiv.`);
654
+ } else {
655
+ messageLines.push(`The newsletter has been generated but Beehiiv upload failed. It needs to be manually uploaded.`);
656
+ messageLines.push('');
657
+ messageLines.push('<strong>Beehiiv failure reason</strong>');
658
+ messageLines.push('<ul>');
659
+ messageLines.push(`<li>${escapeHtml(args.beehiivFailureReason || 'unknown')}</li>`);
660
+ messageLines.push('</ul>');
661
+ }
662
+ messageLines.push('');
663
+
664
+ // --- Preview button (big, prominent) ---
665
+ if (args.previewUrl) {
666
+ messageLines.push(`<p style="text-align:center;margin:16px 0;"><a href="${safeUrl(args.previewUrl)}" style="display:inline-block;padding:14px 32px;background:#1a1a2e;color:#ffffff;text-decoration:none;border-radius:6px;font-weight:bold;font-size:16px;">Preview Newsletter</a></p>`);
667
+ messageLines.push('');
668
+ }
669
+
670
+ // --- Details ---
671
+ messageLines.push('<strong>Newsletter details</strong>');
672
+ messageLines.push('<ul>');
673
+ messageLines.push(`<li>Subject: ${escapeHtml(args.subject)}</li>`);
674
+ messageLines.push(`<li>Preheader: ${escapeHtml(args.preheader || '(none)')}</li>`);
675
+ if (args.tags?.length) {
676
+ messageLines.push(`<li>Tags: ${escapeHtml(args.tags.join(', '))}</li>`);
677
+ }
678
+ messageLines.push('</ul>');
679
+ messageLines.push('');
680
+
681
+ // --- Full HTML ---
682
+ if (args.htmlUrl) {
683
+ messageLines.push('<strong>Full HTML</strong> — one-shot paste into Beehiiv');
684
+ messageLines.push('<ul>');
685
+ messageLines.push(`<li><a href="${safeUrl(args.htmlUrl)}">View raw HTML</a></li>`);
686
+ messageLines.push('</ul>');
687
+ messageLines.push('');
688
+ }
689
+
690
+ // --- Markdown ---
691
+ if (args.markdownUrl) {
692
+ messageLines.push('<strong>Per-section markdown</strong> — paste as separate blocks, ads between');
693
+ messageLines.push('<ul>');
694
+ messageLines.push(`<li><a href="${safeUrl(args.markdownUrl)}">View markdown</a></li>`);
695
+ messageLines.push('</ul>');
696
+ messageLines.push('');
697
+ }
698
+
699
+ // --- Summary ---
700
+ if (args.summaryUrl) {
701
+ messageLines.push('<strong>Summary</strong> — 2-3 sentence recap');
702
+ messageLines.push('<ul>');
703
+ messageLines.push(`<li><a href="${safeUrl(args.summaryUrl)}">View summary</a></li>`);
704
+ messageLines.push('</ul>');
705
+ messageLines.push('');
706
+ }
707
+
708
+ // --- Linked articles (only published ones) ---
709
+ if (args.articles?.length) {
710
+ messageLines.push('<strong>Linked articles</strong>');
711
+ messageLines.push('<ul>');
712
+ for (const article of args.articles) {
713
+ messageLines.push(`<li><a href="${safeUrl(article.url)}">${escapeHtml(article.title || 'Article')}</a></li>`);
714
+ }
715
+ messageLines.push('</ul>');
716
+ messageLines.push('');
717
+ }
718
+
719
+ // --- Sources ---
720
+ if (args.sources?.length) {
721
+ messageLines.push(`<strong>Sources</strong> — ${args.sources.length} used`);
722
+ messageLines.push('<ul>');
723
+ for (const source of args.sources) {
724
+ const label = escapeHtml(source.title || source.url || 'Untitled');
725
+ let fromLabel = '';
726
+ if (source.from) {
727
+ try { fromLabel = ` (${escapeHtml(new URL(source.from).hostname)})`; } catch { fromLabel = ` (${escapeHtml(source.from)})`; }
728
+ }
729
+ if (source.url) {
730
+ messageLines.push(`<li><a href="${safeUrl(source.url)}">${label}</a>${fromLabel}</li>`);
731
+ } else {
732
+ messageLines.push(`<li>${label}${fromLabel}</li>`);
733
+ }
734
+ }
735
+ messageLines.push('</ul>');
736
+ messageLines.push('');
737
+ }
738
+
739
+ // --- All assets ---
740
+ if (args.folderUrl) {
741
+ messageLines.push('<strong>All assets</strong> — full GitHub archive');
742
+ messageLines.push('<ul>');
743
+ messageLines.push(`<li><a href="${safeUrl(args.folderUrl)}">Browse folder</a></li>`);
744
+ messageLines.push('</ul>');
745
+ }
746
+
747
+ const subjectLine = beehiivOk
748
+ ? `Newsletter generated: "${args.subject}"`
749
+ : `Newsletter ready for manual upload: "${args.subject}"`;
750
+
751
+ await email.send({
752
+ sender: 'internal',
753
+ to: alertsEmail,
754
+ copy: false,
755
+ subject: subjectLine,
756
+ template: 'card',
757
+ categories: ['marketing/newsletter-report'],
758
+ // Body is first-party markup built above; its AI/scraped values are escaped.
759
+ trustedContent: true,
760
+ data: {
761
+ email: {
762
+ preview: beehiivOk
763
+ ? `Newsletter generated and uploaded to Beehiiv`
764
+ : `Beehiiv upload failed — newsletter awaiting manual upload`,
765
+ },
766
+ content: {
767
+ title: beehiivOk ? 'Newsletter Generated' : 'Newsletter Ready for Manual Upload',
768
+ message: messageLines.join('\n'),
769
+ },
770
+ },
771
+ });
772
+
773
+ ctx.log(`Newsletter generator: report email sent to ${alertsEmail}`);
774
+ } catch (e) {
775
+ ctx.error(`Newsletter generator: report email failed — ${e.message}`);
776
+ }
777
+ }
778
+
779
+ /**
780
+ * Sum tokens + cost across all steps (filter, structure, all SVGs).
781
+ */
782
+ function aggregateTotals(filterMeta, structureMeta, images) {
783
+ let inputTokens = 0;
784
+ let outputTokens = 0;
785
+ let totalCost = 0;
786
+ let aiCalls = 0;
787
+
788
+ const collect = (m) => {
789
+ if (!m?.tokens) return;
790
+ inputTokens += m.tokens.input?.count || 0;
791
+ outputTokens += m.tokens.output?.count || 0;
792
+ totalCost += m.tokens.total?.price || 0;
793
+ aiCalls++;
794
+ };
795
+
796
+ collect(filterMeta);
797
+ collect(structureMeta);
798
+
799
+ for (const img of images || []) {
800
+ collect(img.meta);
801
+ }
802
+
803
+ return {
804
+ aiCalls,
805
+ inputTokens,
806
+ outputTokens,
807
+ totalTokens: inputTokens + outputTokens,
808
+ totalCostUSD: Number(totalCost.toFixed(4)),
809
+ };
810
+ }
811
+
812
+
813
+ /**
814
+ * Map a resolved source (from resolveSources) to the newsletter item shape
815
+ * the structure generator expects. Parent items already arrive in that shape
816
+ * from the parent server; feed items get normalized.
817
+ *
818
+ * @param {object} resolved - resolved source ({ type, id, title, url, content, feedUrl?, raw, trackingData })
819
+ * @param {string[]} categories - the entry's configured categories
820
+ * @returns {object} newsletter source record (+ trackingData for used-marking)
821
+ */
822
+ function toNewsletterSource(resolved, categories) {
823
+ if (resolved.type === 'parent') {
824
+ return { ...resolved.raw, trackingData: resolved.trackingData };
825
+ }
826
+
827
+ // feed item
828
+ let hostname = '';
829
+ try { hostname = new URL(resolved.feedUrl).hostname; } catch (e) { /* ignore */ }
830
+
831
+ return {
832
+ id: resolved.id,
833
+ title: resolved.title,
834
+ subject: resolved.title,
835
+ category: (categories && categories[0]) || '',
836
+ categories: categories || [],
837
+ url: resolved.url,
838
+ source: {
839
+ from: hostname,
840
+ subject: resolved.title,
841
+ content: resolved.content,
842
+ },
843
+ ai: null,
844
+ trackingData: resolved.trackingData,
845
+ };
846
+ }
847
+
848
+ /**
849
+ * Generate a 20-character Firebase push ID (RTDB-style).
850
+ *
851
+ * Format: 20 chars, starts with a timestamp-encoded prefix, lexicographically
852
+ * sortable by creation time. Matches the ID scheme used by Firebase Realtime
853
+ * Database `.push()` and by ITW's `newsletter-sources/{id}`.
854
+ *
855
+ * Used when no real `marketing-campaigns/{id}` doc exists yet — typically only
856
+ * the iteration test. Production cron passes the actual Firestore ID.
857
+ *
858
+ * Algorithm reference: https://gist.github.com/mikelehen/3596a30bd69384624c11
859
+ */
860
+ let _lastPushTime = 0;
861
+ const _lastRandChars = [];
862
+ const PUSH_CHARS = '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz';
863
+
864
+ function generatePushId() {
865
+ let now = Date.now();
866
+ const duplicateTime = (now === _lastPushTime);
867
+ _lastPushTime = now;
868
+
869
+ const timeStampChars = new Array(8);
870
+ for (let i = 7; i >= 0; i--) {
871
+ timeStampChars[i] = PUSH_CHARS.charAt(now % 64);
872
+ now = Math.floor(now / 64);
873
+ }
874
+
875
+ let id = timeStampChars.join('');
876
+
877
+ if (!duplicateTime) {
878
+ for (let i = 0; i < 12; i++) {
879
+ _lastRandChars[i] = Math.floor(Math.random() * 64);
880
+ }
881
+ } else {
882
+ // Increment to ensure monotonicity within the same millisecond
883
+ let i = 11;
884
+ for (; i >= 0 && _lastRandChars[i] === 63; i--) {
885
+ _lastRandChars[i] = 0;
886
+ }
887
+ if (i >= 0) {
888
+ _lastRandChars[i]++;
889
+ }
890
+ }
891
+
892
+ for (let i = 0; i < 12; i++) {
893
+ id += PUSH_CHARS.charAt(_lastRandChars[i]);
894
+ }
895
+
896
+ return id;
897
+ }
898
+
899
+ module.exports = { generate, generatePushId };