@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.
- package/LICENSE +98 -0
- package/README.md +1053 -0
- package/bin/omega +2 -0
- package/bin/omega-backend +2 -0
- package/bin/omg +2 -0
- package/cli.js +3 -0
- package/dist/cli/command-table.js +268 -0
- package/dist/cli/commands/auth.js +259 -0
- package/dist/cli/commands/base-command.js +451 -0
- package/dist/cli/commands/build.js +27 -0
- package/dist/cli/commands/clean.js +13 -0
- package/dist/cli/commands/clear.js +11 -0
- package/dist/cli/commands/cwd.js +9 -0
- package/dist/cli/commands/deploy.js +205 -0
- package/dist/cli/commands/emulator-orphans.js +440 -0
- package/dist/cli/commands/emulator.js +1829 -0
- package/dist/cli/commands/firebase-init.js +101 -0
- package/dist/cli/commands/firestore.js +261 -0
- package/dist/cli/commands/indexes.js +51 -0
- package/dist/cli/commands/install.js +151 -0
- package/dist/cli/commands/logs.js +358 -0
- package/dist/cli/commands/mcp.js +41 -0
- package/dist/cli/commands/migrate-markers.js +176 -0
- package/dist/cli/commands/migrate-rules.js +90 -0
- package/dist/cli/commands/migrate.js +59 -0
- package/dist/cli/commands/serve.js +208 -0
- package/dist/cli/commands/setup-tests/base-test.js +174 -0
- package/dist/cli/commands/setup-tests/emulator-config.js +109 -0
- package/dist/cli/commands/setup-tests/env-runtime-config-deprecated.js +47 -0
- package/dist/cli/commands/setup-tests/firebase-admin.js +49 -0
- package/dist/cli/commands/setup-tests/firebase-auth.js +26 -0
- package/dist/cli/commands/setup-tests/firebase-cli.js +26 -0
- package/dist/cli/commands/setup-tests/firebase-functions.js +49 -0
- package/dist/cli/commands/setup-tests/firestore-indexes-file.js +72 -0
- package/dist/cli/commands/setup-tests/firestore-indexes-in-json.js +20 -0
- package/dist/cli/commands/setup-tests/firestore-indexes-required.js +108 -0
- package/dist/cli/commands/setup-tests/firestore-indexes-synced.js +236 -0
- package/dist/cli/commands/setup-tests/firestore-rules-file.js +115 -0
- package/dist/cli/commands/setup-tests/firestore-rules-in-json.js +49 -0
- package/dist/cli/commands/setup-tests/functions-package.js +44 -0
- package/dist/cli/commands/setup-tests/gcloud-cli.js +26 -0
- package/dist/cli/commands/setup-tests/gitignore.js +122 -0
- package/dist/cli/commands/setup-tests/helpers/merge-line-files.js +28 -0
- package/dist/cli/commands/setup-tests/helpers/required-indexes.js +109 -0
- package/dist/cli/commands/setup-tests/helpers/seed-campaigns.js +246 -0
- package/dist/cli/commands/setup-tests/helpers.js +72 -0
- package/dist/cli/commands/setup-tests/hosting-folder.js +23 -0
- package/dist/cli/commands/setup-tests/hosting-rewrites.js +52 -0
- package/dist/cli/commands/setup-tests/index.js +111 -0
- package/dist/cli/commands/setup-tests/is-firebase-project.js +21 -0
- package/dist/cli/commands/setup-tests/java-installed.js +27 -0
- package/dist/cli/commands/setup-tests/legacy-tests-cleanup.js +43 -0
- package/dist/cli/commands/setup-tests/marketing-campaigns-seeded.js +230 -0
- package/dist/cli/commands/setup-tests/node-version.js +60 -0
- package/dist/cli/commands/setup-tests/npm-project-scripts.js +42 -0
- package/dist/cli/commands/setup-tests/nvmrc-version.js +36 -0
- package/dist/cli/commands/setup-tests/omega-backend.js +42 -0
- package/dist/cli/commands/setup-tests/omega-config.js +84 -0
- package/dist/cli/commands/setup-tests/project-directories.js +34 -0
- package/dist/cli/commands/setup-tests/project-id-consistency.js +201 -0
- package/dist/cli/commands/setup-tests/public-html-files.js +22 -0
- package/dist/cli/commands/setup-tests/realtime-rules-file.js +69 -0
- package/dist/cli/commands/setup-tests/realtime-rules-in-json.js +20 -0
- package/dist/cli/commands/setup-tests/remoteconfig-template-file.js +32 -0
- package/dist/cli/commands/setup-tests/remoteconfig-template-in-json.js +31 -0
- package/dist/cli/commands/setup-tests/service-account.js +86 -0
- package/dist/cli/commands/setup-tests/storage-lifecycle-policy.js +81 -0
- package/dist/cli/commands/setup-tests/storage-rules-file.js +32 -0
- package/dist/cli/commands/setup-tests/storage-rules-in-json.js +20 -0
- package/dist/cli/commands/stripe.js +14 -0
- package/dist/cli/commands/test-lanes/stripe-live.js +440 -0
- package/dist/cli/commands/test.js +976 -0
- package/dist/cli/commands/update.js +27 -0
- package/dist/cli/commands/version.js +10 -0
- package/dist/cli/commands/watch.js +196 -0
- package/dist/cli/flags.js +13 -0
- package/dist/cli/index.js +173 -0
- package/dist/cli/run.js +33 -0
- package/dist/cli/utils/attach-log-file.js +4 -0
- package/dist/cli/utils/compile-rules.js +1155 -0
- package/dist/cli/utils/ensure-target.js +314 -0
- package/dist/cli/utils/project-type.js +140 -0
- package/dist/cli/utils/public-files.js +49 -0
- package/dist/cli/utils/safe-install.js +4 -0
- package/dist/cli/utils/spawn-shell.js +94 -0
- package/dist/cli/utils/stage-functions.js +338 -0
- package/dist/cli/utils/stage-local-packages.js +210 -0
- package/dist/cli/utils/target-checks.js +161 -0
- package/dist/cli/utils/target.js +24 -0
- package/dist/cli/utils/ui.js +292 -0
- package/dist/defaults/AGENTS.md +125 -0
- package/dist/defaults/CHANGELOG.md +15 -0
- package/dist/defaults/CLAUDE.md +1 -0
- package/dist/defaults/_.gitignore +65 -0
- package/dist/defaults/docs/README.md +17 -0
- package/dist/defaults/test/README.md +65 -0
- package/dist/defaults/test/_init.js +14 -0
- package/dist/defaults/test/helpers/connect-trap.js +72 -0
- package/dist/defaults/test/unit/registration.test.js +143 -0
- package/dist/defaults/test/unit/rules-posture.test.js +101 -0
- package/dist/defaults/test/unit/socket-free.test.js +43 -0
- package/dist/manager/events/auth/before-create.js +89 -0
- package/dist/manager/events/auth/before-signin.js +76 -0
- package/dist/manager/events/auth/on-create.js +112 -0
- package/dist/manager/events/auth/on-delete.js +141 -0
- package/dist/manager/events/auth/utils.js +93 -0
- package/dist/manager/events/cron/daily/blog-auto-publisher.js +287 -0
- package/dist/manager/events/cron/daily/data-requests.js +57 -0
- package/dist/manager/events/cron/daily/expire-paypal-cancellations.js +191 -0
- package/dist/manager/events/cron/daily/marketing-prune.js +486 -0
- package/dist/manager/events/cron/daily/reset-usage.js +188 -0
- package/dist/manager/events/cron/daily/trial-lapse-sweep.js +355 -0
- package/dist/manager/events/cron/daily.js +3 -0
- package/dist/manager/events/cron/frequent/abandoned-carts.js +204 -0
- package/dist/manager/events/cron/frequent/email-queue.js +71 -0
- package/dist/manager/events/cron/frequent/marketing-campaigns.js +463 -0
- package/dist/manager/events/cron/frequent/retry-failed-webhooks.js +70 -0
- package/dist/manager/events/cron/frequent.js +3 -0
- package/dist/manager/events/cron/runner.js +64 -0
- package/dist/manager/events/firestore/notifications/on-write.js +123 -0
- package/dist/manager/events/firestore/payments-disputes/on-write.js +256 -0
- package/dist/manager/events/firestore/payments-disputes/providers/stripe.js +218 -0
- package/dist/manager/events/firestore/payments-disputes/providers/test.js +231 -0
- package/dist/manager/events/firestore/payments-webhooks/analytics.js +687 -0
- package/dist/manager/events/firestore/payments-webhooks/on-write.js +1224 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/index.js +395 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +58 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +28 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-refunded.js +30 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/send-email.js +74 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-removed.js +14 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +28 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/checkout-declined.js +18 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +67 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +25 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +26 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-refunded.js +34 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +42 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +33 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-winback.js +17 -0
- package/dist/manager/functions/_legacy/actions/create-post-handler.js +188 -0
- package/dist/manager/functions/_legacy/actions/generate-uuid.js +63 -0
- package/dist/manager/functions/_legacy/actions/sign-up-handler.js +205 -0
- package/dist/manager/functions/_legacy/admin/create-post.js +207 -0
- package/dist/manager/functions/_legacy/admin/firestore-write.js +72 -0
- package/dist/manager/functions/_legacy/admin/get-stats.js +218 -0
- package/dist/manager/functions/_legacy/admin/query.js +198 -0
- package/dist/manager/functions/_legacy/admin/send-notification.js +206 -0
- package/dist/manager/functions/_legacy/template.js +33 -0
- package/dist/manager/functions/_legacy/test/authenticate.js +40 -0
- package/dist/manager/functions/_legacy/test/webhook.js +37 -0
- package/dist/manager/functions/wrappers/mailchimp/addToList.js +25 -0
- package/dist/manager/helpers/analytics.js +498 -0
- package/dist/manager/helpers/api-manager.js +313 -0
- package/dist/manager/helpers/backend-router.js +37 -0
- package/dist/manager/helpers/context/authenticate.js +215 -0
- package/dist/manager/helpers/context/client-info.js +91 -0
- package/dist/manager/helpers/context/index.js +228 -0
- package/dist/manager/helpers/context/logging.js +150 -0
- package/dist/manager/helpers/context/parse.js +160 -0
- package/dist/manager/helpers/context/respond.js +247 -0
- package/dist/manager/helpers/event-middleware.js +109 -0
- package/dist/manager/helpers/metadata.js +32 -0
- package/dist/manager/helpers/middleware.js +541 -0
- package/dist/manager/helpers/redact-secret.js +25 -0
- package/dist/manager/helpers/resolved-config.js +34 -0
- package/dist/manager/helpers/roles.js +69 -0
- package/dist/manager/helpers/safe-compare.js +31 -0
- package/dist/manager/helpers/schema-engine.js +314 -0
- package/dist/manager/helpers/schema-zod.js +203 -0
- package/dist/manager/helpers/settings.js +215 -0
- package/dist/manager/helpers/usage.js +541 -0
- package/dist/manager/helpers/user.js +57 -0
- package/dist/manager/helpers/utilities.js +565 -0
- package/dist/manager/index.js +1404 -0
- package/dist/manager/libraries/abandoned-cart-config.js +12 -0
- package/dist/manager/libraries/ai/index.js +321 -0
- package/dist/manager/libraries/ai/prompt.js +85 -0
- package/dist/manager/libraries/ai/providers/anthropic-format.js +297 -0
- package/dist/manager/libraries/ai/providers/anthropic.js +169 -0
- package/dist/manager/libraries/ai/providers/claude-code.js +183 -0
- package/dist/manager/libraries/ai/providers/openai.js +1272 -0
- package/dist/manager/libraries/ai/providers/test.js +257 -0
- package/dist/manager/libraries/ai/tokens.js +54 -0
- package/dist/manager/libraries/analytics/conversions.js +478 -0
- package/dist/manager/libraries/analytics/match-data.js +573 -0
- package/dist/manager/libraries/analytics/signup.js +120 -0
- package/dist/manager/libraries/auth-user.js +46 -0
- package/dist/manager/libraries/content/feed-parser.js +198 -0
- package/dist/manager/libraries/content/ghostii.js +191 -0
- package/dist/manager/libraries/content/source-resolver.js +629 -0
- package/dist/manager/libraries/email/constants.js +532 -0
- package/dist/manager/libraries/email/data/blocked-local-parts.json +55 -0
- package/dist/manager/libraries/email/data/blocked-local-patterns.js +14 -0
- package/dist/manager/libraries/email/data/corporate-domains.json +23 -0
- package/dist/manager/libraries/email/data/custom-disposable-domains.json +58 -0
- package/dist/manager/libraries/email/data/disposable-domains.json +8184 -0
- package/dist/manager/libraries/email/data/typo-domains.js +83 -0
- package/dist/manager/libraries/email/disposable-domains.js +92 -0
- package/dist/manager/libraries/email/generators/lib/filter.js +179 -0
- package/dist/manager/libraries/email/generators/lib/image-host.js +344 -0
- package/dist/manager/libraries/email/generators/lib/image-illustrator.js +154 -0
- package/dist/manager/libraries/email/generators/lib/markdown-renderer.js +294 -0
- package/dist/manager/libraries/email/generators/lib/mjml-template.js +120 -0
- package/dist/manager/libraries/email/generators/lib/structure.js +299 -0
- package/dist/manager/libraries/email/generators/lib/svg-illustrator.js +197 -0
- package/dist/manager/libraries/email/generators/lib/templates/base.js +229 -0
- package/dist/manager/libraries/email/generators/lib/templates/card.js +33 -0
- package/dist/manager/libraries/email/generators/lib/templates/classic-schema.js +59 -0
- package/dist/manager/libraries/email/generators/lib/templates/clean.js +82 -0
- package/dist/manager/libraries/email/generators/lib/templates/editorial/helpers.js +100 -0
- package/dist/manager/libraries/email/generators/lib/templates/editorial/index.js +318 -0
- package/dist/manager/libraries/email/generators/lib/templates/feedback.js +101 -0
- package/dist/manager/libraries/email/generators/lib/templates/field-report/helpers.js +138 -0
- package/dist/manager/libraries/email/generators/lib/templates/field-report/index.js +494 -0
- package/dist/manager/libraries/email/generators/lib/templates/index.js +47 -0
- package/dist/manager/libraries/email/generators/lib/templates/newsletter-shared.js +610 -0
- package/dist/manager/libraries/email/generators/lib/templates/order.js +461 -0
- package/dist/manager/libraries/email/generators/lib/templates/plain.js +83 -0
- package/dist/manager/libraries/email/generators/lib/templates/shared-campaign.js +64 -0
- package/dist/manager/libraries/email/generators/newsletter.js +899 -0
- package/dist/manager/libraries/email/index.js +146 -0
- package/dist/manager/libraries/email/marketing/index.js +754 -0
- package/dist/manager/libraries/email/prepare.js +365 -0
- package/dist/manager/libraries/email/providers/beehiiv.js +589 -0
- package/dist/manager/libraries/email/providers/sendgrid.js +845 -0
- package/dist/manager/libraries/email/transactional/index.js +510 -0
- package/dist/manager/libraries/email/utm.js +86 -0
- package/dist/manager/libraries/email/validation-provider-neverbounce.js +70 -0
- package/dist/manager/libraries/email/validation-provider-zerobounce.js +42 -0
- package/dist/manager/libraries/email/validation.js +255 -0
- package/dist/manager/libraries/env.js +198 -0
- package/dist/manager/libraries/infer-contact.js +101 -0
- package/dist/manager/libraries/load-provider.js +65 -0
- package/dist/manager/libraries/notification.js +256 -0
- package/dist/manager/libraries/openai.js +13 -0
- package/dist/manager/libraries/payment/discount-codes.js +204 -0
- package/dist/manager/libraries/payment/fetch-failure.js +56 -0
- package/dist/manager/libraries/payment/license.js +44 -0
- package/dist/manager/libraries/payment/order-id.js +57 -0
- package/dist/manager/libraries/payment/provider-errors.js +71 -0
- package/dist/manager/libraries/payment/providers/chargebee.js +817 -0
- package/dist/manager/libraries/payment/providers/coinbase.js +330 -0
- package/dist/manager/libraries/payment/providers/paypal.js +987 -0
- package/dist/manager/libraries/payment/providers/stripe.js +755 -0
- package/dist/manager/libraries/payment/providers/test.js +250 -0
- package/dist/manager/libraries/payment/refund-linkage.js +78 -0
- package/dist/manager/libraries/payment/refund-policy.js +113 -0
- package/dist/manager/libraries/payment/winback.js +55 -0
- package/dist/manager/libraries/prompts/infer-contact.md +78 -0
- package/dist/manager/libraries/rate-limits.js +17 -0
- package/dist/manager/libraries/recaptcha.js +58 -0
- package/dist/manager/libraries/user-doc.js +234 -0
- package/dist/manager/routes/admin/backup/post.js +106 -0
- package/dist/manager/routes/admin/cron/post.js +37 -0
- package/dist/manager/routes/admin/database/get.js +35 -0
- package/dist/manager/routes/admin/database/post.js +34 -0
- package/dist/manager/routes/admin/email/post.js +74 -0
- package/dist/manager/routes/admin/firestore/get.js +35 -0
- package/dist/manager/routes/admin/firestore/post.js +56 -0
- package/dist/manager/routes/admin/firestore/query/post.js +128 -0
- package/dist/manager/routes/admin/hook/post.js +104 -0
- package/dist/manager/routes/admin/infer-contact/post.js +35 -0
- package/dist/manager/routes/admin/notification/post.js +37 -0
- package/dist/manager/routes/admin/payment/post.js +56 -0
- package/dist/manager/routes/admin/post/deduplicate-image-alts.js +52 -0
- package/dist/manager/routes/admin/post/dispatch-deploy.js +36 -0
- package/dist/manager/routes/admin/post/post.js +487 -0
- package/dist/manager/routes/admin/post/put.js +146 -0
- package/dist/manager/routes/admin/post/templates/post.html +14 -0
- package/dist/manager/routes/admin/repo/content/post.js +106 -0
- package/dist/manager/routes/admin/stats/get.js +222 -0
- package/dist/manager/routes/admin/users/disable/post.js +50 -0
- package/dist/manager/routes/admin/users/list/get.js +110 -0
- package/dist/manager/routes/admin/users/sync/post.js +119 -0
- package/dist/manager/routes/brand/get.js +26 -0
- package/dist/manager/routes/content/post/get.js +110 -0
- package/dist/manager/routes/general/email/post.js +89 -0
- package/dist/manager/routes/general/email/templates/general/download-app-link.js +42 -0
- package/dist/manager/routes/general/uuid/post.js +31 -0
- package/dist/manager/routes/handler/post/post.js +142 -0
- package/dist/manager/routes/health/get.js +43 -0
- package/dist/manager/routes/index.js +11 -0
- package/dist/manager/routes/marketing/campaign/delete.js +45 -0
- package/dist/manager/routes/marketing/campaign/get.js +69 -0
- package/dist/manager/routes/marketing/campaign/post.js +124 -0
- package/dist/manager/routes/marketing/campaign/put.js +86 -0
- package/dist/manager/routes/marketing/campaign/utils.js +59 -0
- package/dist/manager/routes/marketing/contact/delete.js +100 -0
- package/dist/manager/routes/marketing/contact/post.js +149 -0
- package/dist/manager/routes/marketing/contact/put.js +37 -0
- package/dist/manager/routes/marketing/email-preferences/post.js +275 -0
- package/dist/manager/routes/marketing/webhook/forward/post.js +170 -0
- package/dist/manager/routes/marketing/webhook/post.js +123 -0
- package/dist/manager/routes/marketing/webhook/providers/beehiiv.js +197 -0
- package/dist/manager/routes/marketing/webhook/providers/sendgrid.js +194 -0
- package/dist/manager/routes/payments/cancel/_is-trialing.js +40 -0
- package/dist/manager/routes/payments/cancel/post.js +149 -0
- package/dist/manager/routes/payments/cancel/providers/chargebee.js +41 -0
- package/dist/manager/routes/payments/cancel/providers/paypal.js +56 -0
- package/dist/manager/routes/payments/cancel/providers/stripe.js +33 -0
- package/dist/manager/routes/payments/cancel/providers/test.js +118 -0
- package/dist/manager/routes/payments/discount/get.js +25 -0
- package/dist/manager/routes/payments/dispute-alert/post.js +117 -0
- package/dist/manager/routes/payments/dispute-alert/providers/chargeblast.js +55 -0
- package/dist/manager/routes/payments/intent/post.js +341 -0
- package/dist/manager/routes/payments/intent/providers/chargebee.js +197 -0
- package/dist/manager/routes/payments/intent/providers/coinbase.js +94 -0
- package/dist/manager/routes/payments/intent/providers/paypal.js +244 -0
- package/dist/manager/routes/payments/intent/providers/stripe.js +141 -0
- package/dist/manager/routes/payments/intent/providers/test.js +341 -0
- package/dist/manager/routes/payments/plan/post.js +147 -0
- package/dist/manager/routes/payments/plan/providers/chargebee.js +62 -0
- package/dist/manager/routes/payments/plan/providers/paypal.js +62 -0
- package/dist/manager/routes/payments/plan/providers/stripe.js +59 -0
- package/dist/manager/routes/payments/plan/providers/test.js +113 -0
- package/dist/manager/routes/payments/portal/post.js +113 -0
- package/dist/manager/routes/payments/portal/providers/chargebee.js +62 -0
- package/dist/manager/routes/payments/portal/providers/paypal.js +25 -0
- package/dist/manager/routes/payments/portal/providers/stripe.js +62 -0
- package/dist/manager/routes/payments/portal/providers/test.js +18 -0
- package/dist/manager/routes/payments/refund/post.js +207 -0
- package/dist/manager/routes/payments/refund/providers/chargebee.js +147 -0
- package/dist/manager/routes/payments/refund/providers/coinbase.js +40 -0
- package/dist/manager/routes/payments/refund/providers/paypal.js +239 -0
- package/dist/manager/routes/payments/refund/providers/stripe.js +160 -0
- package/dist/manager/routes/payments/refund/providers/test.js +204 -0
- package/dist/manager/routes/payments/trial-eligibility/get.js +29 -0
- package/dist/manager/routes/payments/uncancel/post.js +106 -0
- package/dist/manager/routes/payments/uncancel/providers/chargebee.js +30 -0
- package/dist/manager/routes/payments/uncancel/providers/paypal.js +25 -0
- package/dist/manager/routes/payments/uncancel/providers/stripe.js +27 -0
- package/dist/manager/routes/payments/uncancel/providers/test.js +114 -0
- package/dist/manager/routes/payments/webhook/post.js +157 -0
- package/dist/manager/routes/payments/webhook/providers/chargebee.js +218 -0
- package/dist/manager/routes/payments/webhook/providers/coinbase.js +91 -0
- package/dist/manager/routes/payments/webhook/providers/paypal.js +214 -0
- package/dist/manager/routes/payments/webhook/providers/stripe.js +182 -0
- package/dist/manager/routes/payments/webhook/providers/test.js +18 -0
- package/dist/manager/routes/payments/winback/post.js +276 -0
- package/dist/manager/routes/payments/winback/providers/chargebee.js +24 -0
- package/dist/manager/routes/payments/winback/providers/paypal.js +22 -0
- package/dist/manager/routes/payments/winback/providers/stripe.js +41 -0
- package/dist/manager/routes/payments/winback/providers/test.js +42 -0
- package/dist/manager/routes/restart/index.js +37 -0
- package/dist/manager/routes/special/electron-client/post.js +59 -0
- package/dist/manager/routes/test/authenticate/get.js +11 -0
- package/dist/manager/routes/test/health/get.js +8 -0
- package/dist/manager/routes/test/index.js +16 -0
- package/dist/manager/routes/test/lab/post.js +17 -0
- package/dist/manager/routes/test/redirect/get.js +27 -0
- package/dist/manager/routes/test/reset-account/post.js +79 -0
- package/dist/manager/routes/test/roster/get.js +40 -0
- package/dist/manager/routes/test/schema/post.js +20 -0
- package/dist/manager/routes/test/usage/post.js +44 -0
- package/dist/manager/routes/test/webhook/post.js +21 -0
- package/dist/manager/routes/user/api-keys/post.js +51 -0
- package/dist/manager/routes/user/connections/_context.js +194 -0
- package/dist/manager/routes/user/connections/_grant.js +247 -0
- package/dist/manager/routes/user/connections/_lease.js +184 -0
- package/dist/manager/routes/user/connections/_providers.js +191 -0
- package/dist/manager/routes/user/connections/_state.js +117 -0
- package/dist/manager/routes/user/connections/delete.js +55 -0
- package/dist/manager/routes/user/connections/get.js +167 -0
- package/dist/manager/routes/user/connections/post.js +370 -0
- package/dist/manager/routes/user/connections/providers/discord.js +41 -0
- package/dist/manager/routes/user/connections/providers/google.js +55 -0
- package/dist/manager/routes/user/connections/providers/kick.js +68 -0
- package/dist/manager/routes/user/connections/providers/spotify.js +40 -0
- package/dist/manager/routes/user/connections/providers/twitch.js +52 -0
- package/dist/manager/routes/user/data-request/delete.js +84 -0
- package/dist/manager/routes/user/data-request/get.js +229 -0
- package/dist/manager/routes/user/data-request/post.js +136 -0
- package/dist/manager/routes/user/delete.js +149 -0
- package/dist/manager/routes/user/feedback/post.js +81 -0
- package/dist/manager/routes/user/get.js +14 -0
- package/dist/manager/routes/user/orders/get.js +95 -0
- package/dist/manager/routes/user/sessions/delete.js +95 -0
- package/dist/manager/routes/user/sessions/get.js +40 -0
- package/dist/manager/routes/user/settings/validate/post.js +86 -0
- package/dist/manager/routes/user/signup/post.js +662 -0
- package/dist/manager/routes/user/subscription/get.js +77 -0
- package/dist/manager/routes/user/token/post.js +30 -0
- package/dist/manager/routes/verts/delete.js +43 -0
- package/dist/manager/routes/verts/get.js +49 -0
- package/dist/manager/routes/verts/post.js +61 -0
- package/dist/manager/routes/verts/put.js +75 -0
- package/dist/manager/routes/verts/redirect/get.js +36 -0
- package/dist/manager/routes/verts/serve/get.js +67 -0
- package/dist/manager/routes/verts/utils.js +436 -0
- package/dist/manager/schemas/admin/backup/post.js +8 -0
- package/dist/manager/schemas/admin/cron/post.js +8 -0
- package/dist/manager/schemas/admin/database/get.js +5 -0
- package/dist/manager/schemas/admin/database/post.js +6 -0
- package/dist/manager/schemas/admin/email/post.js +31 -0
- package/dist/manager/schemas/admin/firestore/get.js +5 -0
- package/dist/manager/schemas/admin/firestore/post.js +8 -0
- package/dist/manager/schemas/admin/firestore/query/post.js +5 -0
- package/dist/manager/schemas/admin/hook/post.js +8 -0
- package/dist/manager/schemas/admin/infer-contact/post.js +6 -0
- package/dist/manager/schemas/admin/notification/post.js +23 -0
- package/dist/manager/schemas/admin/payment/post.js +8 -0
- package/dist/manager/schemas/admin/post/post.js +23 -0
- package/dist/manager/schemas/admin/post/put.js +13 -0
- package/dist/manager/schemas/admin/repo/content/post.js +10 -0
- package/dist/manager/schemas/admin/stats/get.js +5 -0
- package/dist/manager/schemas/admin/users/disable/post.js +9 -0
- package/dist/manager/schemas/admin/users/list/get.js +10 -0
- package/dist/manager/schemas/admin/users/sync/post.js +7 -0
- package/dist/manager/schemas/brand/get.js +3 -0
- package/dist/manager/schemas/content/post/get.js +8 -0
- package/dist/manager/schemas/general/email/post.js +10 -0
- package/dist/manager/schemas/general/uuid/post.js +9 -0
- package/dist/manager/schemas/handler/post/post.js +13 -0
- package/dist/manager/schemas/health/get.js +3 -0
- package/dist/manager/schemas/marketing/campaign/delete.js +8 -0
- package/dist/manager/schemas/marketing/campaign/get.js +13 -0
- package/dist/manager/schemas/marketing/campaign/post.js +43 -0
- package/dist/manager/schemas/marketing/campaign/put.js +37 -0
- package/dist/manager/schemas/marketing/contact/delete.js +8 -0
- package/dist/manager/schemas/marketing/contact/post.js +14 -0
- package/dist/manager/schemas/marketing/contact/put.js +8 -0
- package/dist/manager/schemas/marketing/email-preferences/post.js +15 -0
- package/dist/manager/schemas/marketing/webhook/forward/post.js +8 -0
- package/dist/manager/schemas/marketing/webhook/post.js +7 -0
- package/dist/manager/schemas/payments/cancel/post.js +15 -0
- package/dist/manager/schemas/payments/discount/get.js +8 -0
- package/dist/manager/schemas/payments/dispute-alert/post.js +6 -0
- package/dist/manager/schemas/payments/intent/post.js +28 -0
- package/dist/manager/schemas/payments/plan/post.js +11 -0
- package/dist/manager/schemas/payments/portal/post.js +12 -0
- package/dist/manager/schemas/payments/refund/post.js +14 -0
- package/dist/manager/schemas/payments/trial-eligibility/get.js +7 -0
- package/dist/manager/schemas/payments/uncancel/post.js +9 -0
- package/dist/manager/schemas/payments/webhook/post.js +6 -0
- package/dist/manager/schemas/payments/winback/post.js +12 -0
- package/dist/manager/schemas/restart/index.js +5 -0
- package/dist/manager/schemas/special/electron-client/post.js +11 -0
- package/dist/manager/schemas/test/authenticate/get.js +3 -0
- package/dist/manager/schemas/test/health/get.js +3 -0
- package/dist/manager/schemas/test/index.js +5 -0
- package/dist/manager/schemas/test/lab/post.js +3 -0
- package/dist/manager/schemas/test/redirect/get.js +7 -0
- package/dist/manager/schemas/test/reset-account/post.js +3 -0
- package/dist/manager/schemas/test/roster/get.js +3 -0
- package/dist/manager/schemas/test/schema/post.js +70 -0
- package/dist/manager/schemas/test/usage/post.js +8 -0
- package/dist/manager/schemas/test/webhook/post.js +7 -0
- package/dist/manager/schemas/user/api-keys/post.js +6 -0
- package/dist/manager/schemas/user/connections/delete.js +8 -0
- package/dist/manager/schemas/user/connections/get.js +21 -0
- package/dist/manager/schemas/user/connections/post.js +12 -0
- package/dist/manager/schemas/user/data-request/delete.js +3 -0
- package/dist/manager/schemas/user/data-request/get.js +5 -0
- package/dist/manager/schemas/user/data-request/post.js +6 -0
- package/dist/manager/schemas/user/delete.js +6 -0
- package/dist/manager/schemas/user/feedback/post.js +8 -0
- package/dist/manager/schemas/user/get.js +3 -0
- package/dist/manager/schemas/user/orders/get.js +6 -0
- package/dist/manager/schemas/user/sessions/delete.js +6 -0
- package/dist/manager/schemas/user/sessions/get.js +6 -0
- package/dist/manager/schemas/user/settings/validate/post.js +8 -0
- package/dist/manager/schemas/user/signup/post.js +27 -0
- package/dist/manager/schemas/user/subscription/get.js +5 -0
- package/dist/manager/schemas/user/token/post.js +5 -0
- package/dist/manager/schemas/verts/delete.js +8 -0
- package/dist/manager/schemas/verts/get.js +9 -0
- package/dist/manager/schemas/verts/post.js +23 -0
- package/dist/manager/schemas/verts/put.js +20 -0
- package/dist/manager/schemas/verts/redirect/get.js +10 -0
- package/dist/manager/schemas/verts/serve/get.js +14 -0
- package/dist/manager/server-manager.js +69 -0
- package/dist/mcp/client.js +98 -0
- package/dist/mcp/handler.js +521 -0
- package/dist/mcp/index.js +157 -0
- package/dist/mcp/tools.js +555 -0
- package/dist/mcp/utils.js +108 -0
- package/dist/omega-bin.js +6 -0
- package/dist/require.js +3 -0
- package/dist/test/fixtures/firebase-project/.firebaserc +5 -0
- package/dist/test/fixtures/firebase-project/config/omega.json5 +131 -0
- package/dist/test/fixtures/firebase-project/database.rules.json +6 -0
- package/dist/test/fixtures/firebase-project/firebase.json +49 -0
- package/dist/test/fixtures/firebase-project/firestore.indexes.json +4 -0
- package/dist/test/fixtures/firebase-project/package.json +14 -0
- package/dist/test/fixtures/firebase-project/src/index.js +11 -0
- package/dist/test/fixtures/firebase-project/storage.rules +8 -0
- package/dist/test/parse-audit.js +25 -0
- package/dist/test/run-tests.js +90 -0
- package/dist/test/runner.js +982 -0
- package/dist/test/seed.js +242 -0
- package/dist/test/test-accounts.js +2660 -0
- package/dist/test/utils/assertions.js +189 -0
- package/dist/test/utils/email-capture.js +252 -0
- package/dist/test/utils/extended-mode-warning.js +11 -0
- package/dist/test/utils/firestore-rules-client.js +277 -0
- package/dist/test/utils/http-client.js +228 -0
- package/dist/test/utils/test-mode-file.js +192 -0
- package/dist/utils/merge-line-files.js +6 -0
- package/dist/utils/scaffold-defaults.js +71 -0
- package/dist/utils/test-lanes.js +62 -0
- package/dist/vendor/account/engine.js +182 -0
- package/dist/vendor/account/features.js +220 -0
- package/dist/vendor/account/index.js +53 -0
- package/dist/vendor/account/schema.js +272 -0
- package/dist/vendor/account/subscription.js +38 -0
- package/dist/vendor/analytics/adapters/ga4.js +26 -0
- package/dist/vendor/analytics/adapters/meta.js +26 -0
- package/dist/vendor/analytics/adapters/resolve.js +130 -0
- package/dist/vendor/analytics/adapters/tiktok.js +27 -0
- package/dist/vendor/analytics/catalog.js +908 -0
- package/dist/vendor/analytics/identity.js +136 -0
- package/dist/vendor/config/company.js +31 -0
- package/dist/vendor/config/defaults.js +173 -0
- package/dist/vendor/config/demo.js +18 -0
- package/dist/vendor/config/desktop-artifacts.js +110 -0
- package/dist/vendor/config/edit.js +769 -0
- package/dist/vendor/config/env-delivery.js +145 -0
- package/dist/vendor/config/env-rules.js +93 -0
- package/dist/vendor/config/env-schema.js +1078 -0
- package/dist/vendor/config/env.js +445 -0
- package/dist/vendor/config/hooks.js +97 -0
- package/dist/vendor/config/index.js +237 -0
- package/dist/vendor/config/instances.js +208 -0
- package/dist/vendor/config/load.js +490 -0
- package/dist/vendor/config/merge.js +68 -0
- package/dist/vendor/config/order.js +139 -0
- package/dist/vendor/config/ports.js +374 -0
- package/dist/vendor/config/providers.js +32 -0
- package/dist/vendor/config/repo.js +142 -0
- package/dist/vendor/config/retired-keys.js +430 -0
- package/dist/vendor/config/schema.js +1610 -0
- package/dist/vendor/config/secrets.js +50 -0
- package/dist/vendor/config/seed.js +34 -0
- package/dist/vendor/config/site-global.js +205 -0
- package/dist/vendor/config/validate.js +554 -0
- package/dist/vendor/config/winback.js +61 -0
- package/dist/vendor/devkit/attach-log-file.js +262 -0
- package/dist/vendor/devkit/bare-requires.js +153 -0
- package/dist/vendor/devkit/command-path.js +46 -0
- package/dist/vendor/devkit/defaults-engine.js +372 -0
- package/dist/vendor/devkit/deploy-record.js +180 -0
- package/dist/vendor/devkit/deploy.js +286 -0
- package/dist/vendor/devkit/license.js +155 -0
- package/dist/vendor/devkit/local-https.js +360 -0
- package/dist/vendor/devkit/local.js +1905 -0
- package/dist/vendor/devkit/logger.js +128 -0
- package/dist/vendor/devkit/merge-line-files.js +296 -0
- package/dist/vendor/devkit/npm-registry.js +52 -0
- package/dist/vendor/devkit/omega-bin.js +345 -0
- package/dist/vendor/devkit/parse-audit.js +74 -0
- package/dist/vendor/devkit/safe-install.js +18 -0
- package/dist/vendor/devkit/scaffold-guard.js +96 -0
- package/dist/vendor/devkit/stop-signals.js +28 -0
- package/dist/vendor/devkit/test/assert.js +120 -0
- package/dist/vendor/devkit/test/define-cases.js +104 -0
- package/dist/vendor/devkit/test/runner-core.js +554 -0
- package/dist/vendor/devkit/test/scope.js +162 -0
- package/dist/vendor/devkit/update.js +569 -0
- package/dist/vendor/monitoring/core.js +180 -0
- package/dist/vendor/monitoring/env.js +49 -0
- package/dist/vendor/monitoring/logger.js +39 -0
- package/dist/vendor/monitoring/node.js +72 -0
- package/docs/admin-post-route.md +57 -0
- package/docs/ai-library.md +171 -0
- package/docs/architecture.md +62 -0
- package/docs/audit.md +70 -0
- package/docs/auth-hooks.md +74 -0
- package/docs/build-system.md +65 -0
- package/docs/cdp-debugging.md +29 -0
- package/docs/cli-firestore-auth.md +85 -0
- package/docs/cli-logs.md +67 -0
- package/docs/cli-output.md +146 -0
- package/docs/code-patterns.md +77 -0
- package/docs/common-mistakes.md +12 -0
- package/docs/common-operations.md +60 -0
- package/docs/connections.md +212 -0
- package/docs/consent.md +362 -0
- package/docs/directory-structure.md +150 -0
- package/docs/email-system.md +459 -0
- package/docs/environment-detection.md +93 -0
- package/docs/file-naming.md +10 -0
- package/docs/firestore.md +134 -0
- package/docs/ghostii.md +240 -0
- package/docs/index.md +299 -0
- package/docs/key-files.md +37 -0
- package/docs/logging.md +60 -0
- package/docs/marketing-campaigns.md +407 -0
- package/docs/marketing-fields.md +25 -0
- package/docs/mcp.md +222 -0
- package/docs/migration.md +129 -0
- package/docs/payment-system.md +856 -0
- package/docs/paypal-sandbox-qa.md +105 -0
- package/docs/response-headers.md +9 -0
- package/docs/routes.md +211 -0
- package/docs/sanitization.md +71 -0
- package/docs/schemas.md +163 -0
- package/docs/shared/agent-docs.md +89 -0
- package/docs/shared/analytics.md +612 -0
- package/docs/shared/brands.md +51 -0
- package/docs/shared/breaking-changes.md +497 -0
- package/docs/shared/config.md +1387 -0
- package/docs/shared/deploys.md +215 -0
- package/docs/shared/icons.md +201 -0
- package/docs/shared/local-dev.md +147 -0
- package/docs/shared/logging.md +202 -0
- package/docs/shared/monitoring.md +153 -0
- package/docs/shared/publishing.md +183 -0
- package/docs/shared/rulings.md +34 -0
- package/docs/shared/testing.md +147 -0
- package/docs/shared/theming.md +604 -0
- package/docs/shared/translation.md +291 -0
- package/docs/shared/updates.md +61 -0
- package/docs/stripe-webhook-forwarding.md +20 -0
- package/docs/test-boot-layer.md +67 -0
- package/docs/test-framework.md +583 -0
- package/docs/usage-rate-limiting.md +121 -0
- package/docs/verts.md +29 -0
- package/package.json +143 -0
- package/templates/config/omega.json5 +322 -0
- package/templates/database.rules.json +82 -0
- package/templates/firebase.json +67 -0
- package/templates/firestore.framework.rules +191 -0
- package/templates/firestore.indexes.json +4 -0
- package/templates/firestore.rules +59 -0
- package/templates/index.js +12 -0
- package/templates/public/404.html +26 -0
- package/templates/public/index.html +24 -0
- package/templates/remoteconfig.template.json +1 -0
- package/templates/storage-lifecycle-config-1-day.json +9 -0
- package/templates/storage-lifecycle-config-30-days.json +9 -0
- package/templates/storage.rules +11 -0
- package/test/_init/accounts-validation.js +58 -0
- package/test/ai/tools-live.test.js +171 -0
- package/test/analytics/conversion-delivery.test.js +1013 -0
- package/test/analytics/match-normalization.test.js +238 -0
- package/test/analytics/signup-conversion.test.js +135 -0
- package/test/boot/cli-dispatch.test.js +142 -0
- package/test/boot/defaults-scaffold.test.js +248 -0
- package/test/boot/deploy-staging.test.js +446 -0
- package/test/boot/disposable-domains.test.js +154 -0
- package/test/boot/emulator-boots.test.js +39 -0
- package/test/boot/emulator-port-preflight.test.js +126 -0
- package/test/boot/emulator-ready-timeout.test.js +46 -0
- package/test/boot/project-id-refresh.test.js +73 -0
- package/test/boot/suite-portability.test.js +278 -0
- package/test/boot/update-command.test.js +63 -0
- package/test/cli/cross-platform.test.js +168 -0
- package/test/cli/custom-project-type.test.js +426 -0
- package/test/cli/deploy-license-env.test.js +100 -0
- package/test/cli/emulator-adoption.test.js +254 -0
- package/test/cli/emulator-orphans.test.js +658 -0
- package/test/cli/emulator-ownership.test.js +177 -0
- package/test/cli/emulator-port-retry.test.js +395 -0
- package/test/cli/emulator-shutdown.test.js +788 -0
- package/test/cli/emulator-stale-reap.test.js +574 -0
- package/test/cli/ensure-target.test.js +325 -0
- package/test/cli/flags.test.js +64 -0
- package/test/cli/https-trust.test.js +38 -0
- package/test/cli/install-update-dispatch.test.js +47 -0
- package/test/cli/lane-environments.test.js +159 -0
- package/test/cli/marketing-campaigns-seeded.test.js +194 -0
- package/test/cli/migrate-bare-requires.test.js +128 -0
- package/test/cli/migrate-markers.test.js +530 -0
- package/test/cli/required-indexes.test.js +64 -0
- package/test/cli/rules-compile.test.js +622 -0
- package/test/cli/rules-migration-deferral.test.js +259 -0
- package/test/cli/rules-version.test.js +104 -0
- package/test/cli/setup-load-files.test.js +66 -0
- package/test/cli/setup-offline-mode.test.js +276 -0
- package/test/cli/setup-retired.test.js +64 -0
- package/test/cli/setup-shared-project-indexes.test.js +262 -0
- package/test/cli/setup-tests-requires.test.js +45 -0
- package/test/cli/stage-env-compose.test.js +316 -0
- package/test/cli/stripe-live-lane.test.js +303 -0
- package/test/cli/target.test.js +182 -0
- package/test/cli/templates.test.js +66 -0
- package/test/cli/test-runner-env.test.js +115 -0
- package/test/cli/test-stack-shutdown.test.js +275 -0
- package/test/cli/test-target-no-match.test.js +128 -0
- package/test/cli/verb-logs.test.js +118 -0
- package/test/cli/version-dispatch.test.js +39 -0
- package/test/content/blog-generate.test.js +164 -0
- package/test/email/campaign-config-fault.test.js +376 -0
- package/test/email/campaign-cron-pipeline.test.js +536 -0
- package/test/email/campaign-send.test.js +47 -0
- package/test/email/consent-lifecycle.test.js +258 -0
- package/test/email/content-html-policy.test.js +270 -0
- package/test/email/feedback-and-plain-send.test.js +54 -0
- package/test/email/fixtures/clean.json +30 -0
- package/test/email/fixtures/editorial.json +30 -0
- package/test/email/fixtures/field-report.json +53 -0
- package/test/email/identity.test.js +321 -0
- package/test/email/marketing/consent-gate.test.js +265 -0
- package/test/email/marketing/custom-fields-catalog.test.js +259 -0
- package/test/email/marketing/prune-per-provider.test.js +762 -0
- package/test/email/marketing/remove-log-privacy.test.js +225 -0
- package/test/email/marketing-lifecycle.test.js +139 -0
- package/test/email/newsletter-generate.test.js +853 -0
- package/test/email/newsletter-svg-tokens.test.js +51 -0
- package/test/email/newsletter-templates.test.js +492 -0
- package/test/email/order-one-time-cta.test.js +97 -0
- package/test/email/render-content.test.js +248 -0
- package/test/email/safe-url.test.js +0 -0
- package/test/email/sanitize-images.test.js +66 -0
- package/test/email/send-log-privacy.test.js +107 -0
- package/test/email/templates.test.js +288 -0
- package/test/email/testing-capture.test.js +260 -0
- package/test/email/transactional-send.test.js +36 -0
- package/test/email/transactional.test.js +562 -0
- package/test/email/unsubscribe-groups.test.js +155 -0
- package/test/email/unsubscribe-key.test.js +91 -0
- package/test/email/validation-cases.test.js +152 -0
- package/test/email/validation.test.js +746 -0
- package/test/events/auth-delete-conversion.test.js +138 -0
- package/test/events/auth-delete-race.test.js +211 -0
- package/test/events/auth-on-create-log.test.js +164 -0
- package/test/events/auth-signup-conversion.test.js +141 -0
- package/test/events/auth-signup-limit.test.js +142 -0
- package/test/events/auth-trigger-log-privacy.test.js +256 -0
- package/test/events/cron-job-doc-shape.test.js +101 -0
- package/test/events/cron-reset-usage.test.js +116 -0
- package/test/events/notification-conversion.test.js +182 -0
- package/test/events/payments/_webhook-harness.js +307 -0
- package/test/events/payments/abandoned-cart-activity.test.js +135 -0
- package/test/events/payments/analytics-payment-events.test.js +945 -0
- package/test/events/payments/dispute-email-status.test.js +55 -0
- package/test/events/payments/journey-payments-abandoned.test.js +126 -0
- package/test/events/payments/journey-payments-cancel-endpoint.test.js +102 -0
- package/test/events/payments/journey-payments-cancel-no-order.test.js +76 -0
- package/test/events/payments/journey-payments-cancel.test.js +184 -0
- package/test/events/payments/journey-payments-decline.test.js +225 -0
- package/test/events/payments/journey-payments-discount.test.js +91 -0
- package/test/events/payments/journey-payments-dispute.test.js +190 -0
- package/test/events/payments/journey-payments-failure.test.js +148 -0
- package/test/events/payments/journey-payments-legacy-product.test.js +151 -0
- package/test/events/payments/journey-payments-one-time-decline.test.js +127 -0
- package/test/events/payments/journey-payments-one-time-failure.test.js +113 -0
- package/test/events/payments/journey-payments-one-time-refund.test.js +186 -0
- package/test/events/payments/journey-payments-one-time.test.js +175 -0
- package/test/events/payments/journey-payments-plan-change.test.js +146 -0
- package/test/events/payments/journey-payments-plan-switch-trial.test.js +161 -0
- package/test/events/payments/journey-payments-plan-switch.test.js +182 -0
- package/test/events/payments/journey-payments-refund-no-order.test.js +76 -0
- package/test/events/payments/journey-payments-refund-webhook.test.js +225 -0
- package/test/events/payments/journey-payments-suspend.test.js +183 -0
- package/test/events/payments/journey-payments-trial-cancel.test.js +132 -0
- package/test/events/payments/journey-payments-trial.test.js +173 -0
- package/test/events/payments/journey-payments-uid-resolution.test.js +134 -0
- package/test/events/payments/journey-payments-uncancel.test.js +157 -0
- package/test/events/payments/journey-payments-upgrade.test.js +137 -0
- package/test/events/payments/journey-payments-winback-decline.test.js +171 -0
- package/test/events/payments/journey-payments-winback.test.js +195 -0
- package/test/events/payments/paypal-expiry-cron.test.js +166 -0
- package/test/events/payments/purchase-failed-handler.test.js +80 -0
- package/test/events/payments/purchase-refunded-handler.test.js +108 -0
- package/test/events/payments/test-processor-doc-shape.test.js +162 -0
- package/test/events/payments/transition-order-emails.test.js +185 -0
- package/test/events/payments/transition-promo-lines.test.js +192 -0
- package/test/events/payments/transitions-detect.test.js +767 -0
- package/test/events/payments/trial-lapse-sweep-staleness.test.js +173 -0
- package/test/events/payments/trial-lapse-sweep.test.js +359 -0
- package/test/events/payments/webhook-atomic-writes.test.js +69 -0
- package/test/events/payments/webhook-chargebee-unreachable.test.js +106 -0
- package/test/events/payments/webhook-discount-clear.test.js +189 -0
- package/test/events/payments/webhook-failure-intent.test.js +93 -0
- package/test/events/payments/webhook-hosted-page-uid-trust.test.js +212 -0
- package/test/events/payments/webhook-ordering.test.js +326 -0
- package/test/events/payments/webhook-provider-lookup-trust.test.js +206 -0
- package/test/events/payments/webhook-refund-amount-trust.test.js +223 -0
- package/test/events/payments/webhook-refund-envelope-shape.test.js +148 -0
- package/test/events/payments/webhook-refund-linkage-trust.test.js +404 -0
- package/test/events/payments/webhook-refund-transaction-fallback.test.js +184 -0
- package/test/events/payments/webhook-refund-without-order.test.js +170 -0
- package/test/events/payments/webhook-refusal-reporting.test.js +194 -0
- package/test/events/payments/webhook-refused-intent.test.js +142 -0
- package/test/events/payments/webhook-retry-sweep.test.js +252 -0
- package/test/events/payments/webhook-transition-claim.test.js +389 -0
- package/test/events/payments/webhook-uid-trust.test.js +203 -0
- package/test/events/payments/webhook-user-without-auth.test.js +161 -0
- package/test/fixtures/chargebee/invoice-one-time.json +27 -0
- package/test/fixtures/chargebee/subscription-active.json +44 -0
- package/test/fixtures/chargebee/subscription-cancelled.json +42 -0
- package/test/fixtures/chargebee/subscription-in-trial.json +41 -0
- package/test/fixtures/chargebee/subscription-legacy-plan.json +41 -0
- package/test/fixtures/chargebee/subscription-non-renewing.json +41 -0
- package/test/fixtures/chargebee/subscription-paused.json +42 -0
- package/test/fixtures/chargebee/webhook-payment-failed.json +51 -0
- package/test/fixtures/chargebee/webhook-subscription-created.json +47 -0
- package/test/fixtures/coinbase/charge-confirmed.json +50 -0
- package/test/fixtures/coinbase/charge-failed.json +34 -0
- package/test/fixtures/coinbase/charge-pending.json +34 -0
- package/test/fixtures/migrate/ported-route.js.txt +11 -0
- package/test/fixtures/paypal/capture-completed.json +51 -0
- package/test/fixtures/paypal/capture-refunded.json +43 -0
- package/test/fixtures/paypal/order-approved.json +62 -0
- package/test/fixtures/paypal/order-completed.json +110 -0
- package/test/fixtures/paypal/sale-refunded.json +38 -0
- package/test/fixtures/paypal/subscription-active.json +76 -0
- package/test/fixtures/paypal/subscription-cancelled.json +50 -0
- package/test/fixtures/paypal/subscription-suspended.json +65 -0
- package/test/fixtures/stripe/checkout-session-completed.json +130 -0
- package/test/fixtures/stripe/invoice-payment-failed.json +148 -0
- package/test/fixtures/stripe/invoice-subscription-payment-failed.json +28 -0
- package/test/fixtures/stripe/invoice-subscription-payment-succeeded.json +28 -0
- package/test/fixtures/stripe/subscription-active.json +161 -0
- package/test/fixtures/stripe/subscription-canceled.json +161 -0
- package/test/fixtures/stripe/subscription-trialing.json +161 -0
- package/test/helpers/_shared-config.js +15 -0
- package/test/helpers/ai-request-payload.test.js +619 -0
- package/test/helpers/ai-schema-resolve.test.js +125 -0
- package/test/helpers/ai-test-provider.test.js +227 -0
- package/test/helpers/ai-token-accounting.test.js +150 -0
- package/test/helpers/ai-tools-format.test.js +384 -0
- package/test/helpers/analytics-no-id-notice.test.js +146 -0
- package/test/helpers/analytics-user-data.test.js +194 -0
- package/test/helpers/api-manager.test.js +311 -0
- package/test/helpers/backend-router.test.js +136 -0
- package/test/helpers/content/blog-auto-publisher.test.js +482 -0
- package/test/helpers/content/feed-parser.test.js +529 -0
- package/test/helpers/content/ghostii-blocks.test.js +135 -0
- package/test/helpers/content/ghostii-feed-integration.test.js +405 -0
- package/test/helpers/content/ghostii-write-article.test.js +244 -0
- package/test/helpers/dev-only-routes.test.js +225 -0
- package/test/helpers/env-reader.test.js +362 -0
- package/test/helpers/environment.test.js +263 -0
- package/test/helpers/event-middleware.test.js +407 -0
- package/test/helpers/infer-contact.test.js +157 -0
- package/test/helpers/lane-url.test.js +90 -0
- package/test/helpers/merge-line-files.test.js +272 -0
- package/test/helpers/metadata.test.js +122 -0
- package/test/helpers/middleware-request-log.test.js +134 -0
- package/test/helpers/middleware-user-log.test.js +124 -0
- package/test/helpers/payment/chargebee/parse-webhook.test.js +490 -0
- package/test/helpers/payment/chargebee/refund-details.test.js +198 -0
- package/test/helpers/payment/chargebee/to-unified-one-time.test.js +148 -0
- package/test/helpers/payment/chargebee/to-unified-subscription.test.js +649 -0
- package/test/helpers/payment/coinbase/create-intent.test.js +247 -0
- package/test/helpers/payment/coinbase/parse-webhook.test.js +195 -0
- package/test/helpers/payment/coinbase/refund-unsupported.test.js +141 -0
- package/test/helpers/payment/coinbase/to-unified-one-time.test.js +201 -0
- package/test/helpers/payment/discount-codes.test.js +141 -0
- package/test/helpers/payment/extract-resource.test.js +113 -0
- package/test/helpers/payment/fetch-failure.test.js +215 -0
- package/test/helpers/payment/license-gate.test.js +108 -0
- package/test/helpers/payment/order-id.test.js +99 -0
- package/test/helpers/payment/paypal/create-intent.test.js +382 -0
- package/test/helpers/payment/paypal/fetch-capture.test.js +123 -0
- package/test/helpers/payment/paypal/fetch-sale.test.js +183 -0
- package/test/helpers/payment/paypal/parse-webhook.test.js +678 -0
- package/test/helpers/payment/paypal/refund-details.test.js +202 -0
- package/test/helpers/payment/paypal/resolve-plan-id.test.js +108 -0
- package/test/helpers/payment/paypal/switch-plan.test.js +92 -0
- package/test/helpers/payment/paypal/to-unified-one-time.test.js +383 -0
- package/test/helpers/payment/paypal/to-unified-subscription.test.js +884 -0
- package/test/helpers/payment/stripe/fetch-charge.test.js +147 -0
- package/test/helpers/payment/stripe/parse-webhook.test.js +448 -0
- package/test/helpers/payment/stripe/refund-details.test.js +147 -0
- package/test/helpers/payment/stripe/to-unified-one-time.test.js +307 -0
- package/test/helpers/payment/stripe/to-unified-subscription.test.js +709 -0
- package/test/helpers/persona-domain.test.js +87 -0
- package/test/helpers/recaptcha.test.js +166 -0
- package/test/helpers/resolved-config.test.js +106 -0
- package/test/helpers/response-log-redaction.test.js +262 -0
- package/test/helpers/roles.test.js +155 -0
- package/test/helpers/route-context-debug-gate.test.js +118 -0
- package/test/helpers/route-context-logging.test.js +214 -0
- package/test/helpers/route-context.test.js +129 -0
- package/test/helpers/safe-compare.test.js +114 -0
- package/test/helpers/sanitize.test.js +223 -0
- package/test/helpers/schema-engine.test.js +537 -0
- package/test/helpers/schema-zod.test.js +554 -0
- package/test/helpers/seed-accounts-load.test.js +163 -0
- package/test/helpers/seed-google-personas.test.js +114 -0
- package/test/helpers/seeded-personas.test.js +823 -0
- package/test/helpers/settings.test.js +292 -0
- package/test/helpers/setup-engines-pin.test.js +68 -0
- package/test/helpers/setup-manifest-sync.test.js +116 -0
- package/test/helpers/slugify.test.js +395 -0
- package/test/helpers/storage.test.js +195 -0
- package/test/helpers/test-banner-latch.test.js +139 -0
- package/test/helpers/usage-consume.test.js +460 -0
- package/test/helpers/usage-log-privacy.test.js +121 -0
- package/test/helpers/user-doc-heal.test.js +540 -0
- package/test/helpers/user.test.js +771 -0
- package/test/helpers/webhook-forward.test.js +419 -0
- package/test/helpers/wipe-auth-project.test.js +143 -0
- package/test/mcp/discovery.test.js +52 -0
- package/test/mcp/oauth.test.js +160 -0
- package/test/mcp/protocol.test.js +278 -0
- package/test/mcp/roles.test.js +203 -0
- package/test/mcp/utils.test.js +245 -0
- package/test/notification/identity.test.js +155 -0
- package/test/routes/admin/create-post.test.js +362 -0
- package/test/routes/admin/database.test.js +133 -0
- package/test/routes/admin/deduplicate-image-alts.test.js +191 -0
- package/test/routes/admin/email-content-html.test.js +58 -0
- package/test/routes/admin/email-request-log.test.js +86 -0
- package/test/routes/admin/email.test.js +116 -0
- package/test/routes/admin/firestore-query.test.js +206 -0
- package/test/routes/admin/firestore.test.js +129 -0
- package/test/routes/admin/infer-contact.test.js +220 -0
- package/test/routes/admin/notification.test.js +199 -0
- package/test/routes/admin/post-convert-image.test.js +159 -0
- package/test/routes/admin/post-deploy-flag.test.js +81 -0
- package/test/routes/admin/post-download-error.test.js +90 -0
- package/test/routes/admin/post-resize-image.test.js +185 -0
- package/test/routes/admin/post.test.js +369 -0
- package/test/routes/admin/repo-content.test.js +223 -0
- package/test/routes/admin/stats.test.js +114 -0
- package/test/routes/admin/users-disable.test.js +63 -0
- package/test/routes/admin/users-list.test.js +71 -0
- package/test/routes/content/post.test.js +60 -0
- package/test/routes/general/uuid.test.js +133 -0
- package/test/routes/health.test.js +110 -0
- package/test/routes/marketing/campaign.test.js +184 -0
- package/test/routes/marketing/contact.test.js +416 -0
- package/test/routes/marketing/email-preferences.test.js +293 -0
- package/test/routes/marketing/push-send.test.js +34 -0
- package/test/routes/marketing/webhook-forward.test.js +63 -0
- package/test/routes/marketing/webhook.test.js +641 -0
- package/test/routes/payments/_route-harness.js +145 -0
- package/test/routes/payments/cancel-provider-errors.test.js +227 -0
- package/test/routes/payments/cancel-skip-guards.test.js +126 -0
- package/test/routes/payments/cancel-trialing.test.js +428 -0
- package/test/routes/payments/cancel.test.js +165 -0
- package/test/routes/payments/dedup-race.test.js +186 -0
- package/test/routes/payments/discount.test.js +82 -0
- package/test/routes/payments/dispute-alert.test.js +324 -0
- package/test/routes/payments/intent-discount-amounts.test.js +550 -0
- package/test/routes/payments/intent-discount-coupons.test.js +293 -0
- package/test/routes/payments/intent-one-time-metadata.test.js +122 -0
- package/test/routes/payments/intent-purchaser-guard.test.js +139 -0
- package/test/routes/payments/intent-zero-total.test.js +260 -0
- package/test/routes/payments/intent.test.js +413 -0
- package/test/routes/payments/plan.test.js +367 -0
- package/test/routes/payments/portal-return-url.test.js +107 -0
- package/test/routes/payments/portal.test.js +94 -0
- package/test/routes/payments/refund-one-time.test.js +254 -0
- package/test/routes/payments/refund-paypal-proration.test.js +190 -0
- package/test/routes/payments/refund.test.js +181 -0
- package/test/routes/payments/trial-eligibility.test.js +73 -0
- package/test/routes/payments/uncancel.test.js +270 -0
- package/test/routes/payments/webhook-stripe-invoice.test.js +129 -0
- package/test/routes/payments/webhook-stripe-refund-one-time.test.js +130 -0
- package/test/routes/payments/webhook-test-provider.test.js +156 -0
- package/test/routes/payments/webhook.test.js +115 -0
- package/test/routes/payments/winback-stripe-coupon.test.js +126 -0
- package/test/routes/payments/winback.test.js +597 -0
- package/test/routes/test/authenticate.test.js +79 -0
- package/test/routes/test/redirect.test.js +66 -0
- package/test/routes/test/reset-account.test.js +126 -0
- package/test/routes/test/roster.test.js +101 -0
- package/test/routes/test/schema.test.js +556 -0
- package/test/routes/test/usage.test.js +374 -0
- package/test/routes/user/api-keys.test.js +158 -0
- package/test/routes/user/connections-grant.test.js +759 -0
- package/test/routes/user/connections-identity.test.js +260 -0
- package/test/routes/user/connections-log-privacy.test.js +206 -0
- package/test/routes/user/connections-refresh-lease.test.js +696 -0
- package/test/routes/user/connections-return.test.js +261 -0
- package/test/routes/user/connections-uid.test.js +516 -0
- package/test/routes/user/delete.test.js +138 -0
- package/test/routes/user/feedback.test.js +106 -0
- package/test/routes/user/orders.test.js +233 -0
- package/test/routes/user/sessions.test.js +231 -0
- package/test/routes/user/settings-validate.test.js +84 -0
- package/test/routes/user/signup-emails.test.js +113 -0
- package/test/routes/user/signup-location.test.js +84 -0
- package/test/routes/user/signup-log-privacy.test.js +112 -0
- package/test/routes/user/signup.test.js +579 -0
- package/test/routes/user/subscription.test.js +101 -0
- package/test/routes/user/token.test.js +111 -0
- package/test/routes/user/user.test.js +158 -0
- package/test/routes/verts/cache.test.js +78 -0
- package/test/routes/verts/click-destination.test.js +76 -0
- package/test/routes/verts/crud.test.js +194 -0
- package/test/routes/verts/redirect.test.js +147 -0
- package/test/routes/verts/selection.test.js +237 -0
- package/test/routes/verts/serve.test.js +185 -0
- package/test/routes/verts/unit-document.test.js +117 -0
- package/test/rules/_environment.js +66 -0
- package/test/rules/brand-merge.test.js +195 -0
- package/test/rules/field-helpers.test.js +309 -0
- package/test/rules/notifications.test.js +577 -0
- package/test/rules/payments-carts.test.js +406 -0
- package/test/rules/sessions.test.js +170 -0
- package/test/rules/user.test.js +517 -0
- package/test/rules/verts.test.js +101 -0
- package/test/security/fetch-log-secrets.test.js +124 -0
- package/test/security/primitives.test.js +105 -0
- package/test/security/repo-pinning.test.js +77 -0
- 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 };
|