@omega.js/manager 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/.claude-plugin/marketplace.json +14 -0
- package/LICENSE +98 -0
- package/README.md +165 -0
- package/bin/omega +2 -0
- package/bin/omg +2 -0
- package/claude-plugin/.claude-plugin/plugin.json +11 -0
- package/claude-plugin/.mcp.json +9 -0
- package/claude-plugin/README.md +139 -0
- package/claude-plugin/hooks/gate/mark.sh +58 -0
- package/claude-plugin/hooks/gate/run.sh +97 -0
- package/claude-plugin/hooks/guard/run.sh +249 -0
- package/claude-plugin/hooks/hooks.json +63 -0
- package/claude-plugin/hooks/inject/run.sh +127 -0
- package/claude-plugin/hooks/lib/omega-gate.sh +20 -0
- package/claude-plugin/hooks/lib/omega-scope.sh +41 -0
- package/claude-plugin/hooks/lib/omega-skills.sh +209 -0
- package/claude-plugin/hooks/quality/run.sh +132 -0
- package/claude-plugin/hooks/shape/run.sh +45 -0
- package/claude-plugin/mcp-router-launch.js +29 -0
- package/claude-plugin/skills/README.md +48 -0
- package/claude-plugin/skills/accessibility/SKILL.md +31 -0
- package/claude-plugin/skills/analytics/SKILL.md +30 -0
- package/claude-plugin/skills/backend/SKILL.md +25 -0
- package/claude-plugin/skills/brandcheck/SKILL.md +29 -0
- package/claude-plugin/skills/browser/SKILL.md +64 -0
- package/claude-plugin/skills/client/SKILL.md +24 -0
- package/claude-plugin/skills/desktop/SKILL.md +25 -0
- package/claude-plugin/skills/extension/SKILL.md +25 -0
- package/claude-plugin/skills/main/SKILL.md +54 -0
- package/claude-plugin/skills/manager/SKILL.md +25 -0
- package/claude-plugin/skills/seo/SKILL.md +36 -0
- package/claude-plugin/skills/theme/SKILL.md +33 -0
- package/claude-plugin/skills/web/SKILL.md +29 -0
- package/dist/cli-run.js +34 -0
- package/dist/cli.js +29 -0
- package/dist/commands/build.js +17 -0
- package/dist/commands/clean.js +16 -0
- package/dist/commands/company.js +46 -0
- package/dist/commands/deploy.js +143 -0
- package/dist/commands/dev.js +437 -0
- package/dist/commands/devlog.js +25 -0
- package/dist/commands/help.js +48 -0
- package/dist/commands/manage.js +53 -0
- package/dist/commands/migrate.js +76 -0
- package/dist/commands/onboard.js +31 -0
- package/dist/commands/pipeline.js +319 -0
- package/dist/commands/test.js +380 -0
- package/dist/commands/update.js +128 -0
- package/dist/commands/version.js +8 -0
- package/dist/company-init.js +167 -0
- package/dist/company.js +283 -0
- package/dist/config.js +1359 -0
- package/dist/devlog/index.js +226 -0
- package/dist/devlog/lib/collect-commits.js +136 -0
- package/dist/devlog/lib/generate-post.js +201 -0
- package/dist/devlog/lib/ghostii.js +122 -0
- package/dist/devlog/lib/project-map.js +98 -0
- package/dist/devlog/lib/publish-website.js +88 -0
- package/dist/index.js +45 -0
- package/dist/lib/agents-md.js +201 -0
- package/dist/lib/analytics-secret.js +22 -0
- package/dist/lib/argv.js +21 -0
- package/dist/lib/auth-admin.js +120 -0
- package/dist/lib/automation-client.js +210 -0
- package/dist/lib/backend-marketing.js +49 -0
- package/dist/lib/brand.js +240 -0
- package/dist/lib/bundle-id.js +53 -0
- package/dist/lib/claude-settings.js +125 -0
- package/dist/lib/company-scaffold.js +213 -0
- package/dist/lib/company.js +227 -0
- package/dist/lib/config-flow.js +350 -0
- package/dist/lib/config-write.js +41 -0
- package/dist/lib/custom-target.js +75 -0
- package/dist/lib/domain-utils.js +19 -0
- package/dist/lib/duration.js +39 -0
- package/dist/lib/env-order.js +236 -0
- package/dist/lib/env-secret.js +78 -0
- package/dist/lib/firestore-rest.js +326 -0
- package/dist/lib/framework-bin.js +156 -0
- package/dist/lib/gitignore.js +63 -0
- package/dist/lib/google-auth.js +495 -0
- package/dist/lib/google-token.js +66 -0
- package/dist/lib/jwt.js +31 -0
- package/dist/lib/legacy-oauth.js +59 -0
- package/dist/lib/node-version.js +127 -0
- package/dist/lib/owner-plan.js +78 -0
- package/dist/lib/package-scripts.js +152 -0
- package/dist/lib/preflight.js +470 -0
- package/dist/lib/product-create.js +144 -0
- package/dist/lib/run-command.js +74 -0
- package/dist/lib/run-gates.js +70 -0
- package/dist/lib/run-output.js +42 -0
- package/dist/lib/run-summary.js +384 -0
- package/dist/lib/scaffold.js +427 -0
- package/dist/lib/service-input.js +207 -0
- package/dist/lib/service-runner.js +466 -0
- package/dist/lib/stale.js +24 -0
- package/dist/lib/target-selection.js +155 -0
- package/dist/lib/verb-fanout.js +128 -0
- package/dist/lib/verify-live.js +188 -0
- package/dist/manage.js +311 -0
- package/dist/omega-bin.js +7 -0
- package/dist/onboard.js +532 -0
- package/dist/services/account/ensure/users.js +259 -0
- package/dist/services/account/index.js +97 -0
- package/dist/services/account/lib/backend-client.js +107 -0
- package/dist/services/account/lib/password.js +36 -0
- package/dist/services/account/lib/resolve-password.js +100 -0
- package/dist/services/advertising/ensure/sites.js +101 -0
- package/dist/services/advertising/index.js +101 -0
- package/dist/services/advertising/lib/adsense-api.js +41 -0
- package/dist/services/ai/ensure/keys.js +21 -0
- package/dist/services/ai/index.js +36 -0
- package/dist/services/analytics/ensure/google-firebase-link.js +230 -0
- package/dist/services/analytics/ensure/google-streams.js +256 -0
- package/dist/services/analytics/ensure/meta-pixel.js +22 -0
- package/dist/services/analytics/ensure/tiktok-pixel.js +22 -0
- package/dist/services/analytics/index.js +134 -0
- package/dist/services/analytics/lib/analytics-api.js +156 -0
- package/dist/services/analytics/lib/meta-api.js +83 -0
- package/dist/services/analytics/lib/pixel-account.js +78 -0
- package/dist/services/analytics/lib/pixel-provision.js +152 -0
- package/dist/services/analytics/lib/pixel-specs.js +57 -0
- package/dist/services/analytics/lib/pixel-token.js +88 -0
- package/dist/services/analytics/lib/property-flow.js +77 -0
- package/dist/services/analytics/lib/tiktok-api.js +157 -0
- package/dist/services/analytics/lib/tiktok-auth.js +153 -0
- package/dist/services/assets/ensure/logo-gen.js +68 -0
- package/dist/services/assets/index.js +109 -0
- package/dist/services/assets/lib/assets-config.js +168 -0
- package/dist/services/assets/lib/brandmark-api.js +160 -0
- package/dist/services/assets/lib/derived.js +113 -0
- package/dist/services/assets/lib/font-loader.js +67 -0
- package/dist/services/assets/lib/reconcile.js +122 -0
- package/dist/services/assets/lib/reset.js +118 -0
- package/dist/services/assets/lib/svg-logo-generator.js +154 -0
- package/dist/services/assets/lib/svg-to-black.js +32 -0
- package/dist/services/assets/write/favicons.js +82 -0
- package/dist/services/assets/write/icons.js +64 -0
- package/dist/services/assets/write/process.js +96 -0
- package/dist/services/assets/write/reconcile.js +30 -0
- package/dist/services/assets/write/social-icons.js +108 -0
- package/dist/services/assets/write/templates.js +345 -0
- package/dist/services/bookmark/ensure/sync.js +255 -0
- package/dist/services/bookmark/index.js +17 -0
- package/dist/services/campaigns/ensure/contact-person.js +40 -0
- package/dist/services/campaigns/ensure/custom-fields.js +84 -0
- package/dist/services/campaigns/ensure/domain-auth.js +92 -0
- package/dist/services/campaigns/ensure/event-webhook.js +89 -0
- package/dist/services/campaigns/ensure/link-branding.js +187 -0
- package/dist/services/campaigns/ensure/list.js +50 -0
- package/dist/services/campaigns/ensure/segments.js +109 -0
- package/dist/services/campaigns/ensure/sender-identity.js +153 -0
- package/dist/services/campaigns/ensure/unsubscribe-groups.js +116 -0
- package/dist/services/campaigns/index.js +79 -0
- package/dist/services/campaigns/lib/dns-sync.js +81 -0
- package/dist/services/campaigns/lib/segment-query.js +138 -0
- package/dist/services/campaigns/lib/sendgrid-api.js +287 -0
- package/dist/services/captcha/ensure/site-key.js +83 -0
- package/dist/services/captcha/index.js +78 -0
- package/dist/services/captcha/lib/console-url.js +45 -0
- package/dist/services/captcha/lib/recaptcha-api.js +36 -0
- package/dist/services/certificates/ensure/api-key.js +22 -0
- package/dist/services/certificates/ensure/bundle-ids.js +115 -0
- package/dist/services/certificates/ensure/certificates.js +187 -0
- package/dist/services/certificates/ensure/profiles.js +143 -0
- package/dist/services/certificates/index.js +225 -0
- package/dist/services/certificates/lib/apple-api.js +183 -0
- package/dist/services/certificates/lib/certificate-manager.js +252 -0
- package/dist/services/certificates/lib/identifier-manager.js +107 -0
- package/dist/services/certificates/lib/keychain.js +135 -0
- package/dist/services/certificates/lib/manual-walkthrough.js +165 -0
- package/dist/services/certificates/lib/profile-manager.js +115 -0
- package/dist/services/chat/data/baseline-knowledge.md +96 -0
- package/dist/services/chat/ensure/chat.js +106 -0
- package/dist/services/chat/ensure/user.js +45 -0
- package/dist/services/chat/index.js +139 -0
- package/dist/services/chat/lib/baseline-knowledge.js +137 -0
- package/dist/services/cloud/ensure/authentication.js +267 -0
- package/dist/services/cloud/ensure/billing.js +120 -0
- package/dist/services/cloud/ensure/cloud-messaging.js +105 -0
- package/dist/services/cloud/ensure/database.js +60 -0
- package/dist/services/cloud/ensure/firestore.js +79 -0
- package/dist/services/cloud/ensure/functions.js +49 -0
- package/dist/services/cloud/ensure/hosting.js +398 -0
- package/dist/services/cloud/ensure/oauth-consent.js +248 -0
- package/dist/services/cloud/ensure/project-settings.js +72 -0
- package/dist/services/cloud/ensure/sdk-config.js +95 -0
- package/dist/services/cloud/ensure/service-account.js +152 -0
- package/dist/services/cloud/ensure/services.js +103 -0
- package/dist/services/cloud/ensure/storage.js +47 -0
- package/dist/services/cloud/index.js +130 -0
- package/dist/services/cloud/lib/access-heal.js +130 -0
- package/dist/services/cloud/lib/firebase-api.js +662 -0
- package/dist/services/cloud/lib/project-flow.js +102 -0
- package/dist/services/directory/ensure/entry.js +48 -0
- package/dist/services/directory/index.js +64 -0
- package/dist/services/directory/lib/blocks.js +104 -0
- package/dist/services/disperse/index.js +45 -0
- package/dist/services/disperse/write/certs.js +117 -0
- package/dist/services/domain/ensure/nameservers.js +179 -0
- package/dist/services/domain/index.js +69 -0
- package/dist/services/domain/lib/namecheap-api.js +167 -0
- package/dist/services/domain/lib/registrars.js +46 -0
- package/dist/services/domain/lib/whitelist-walkthrough.js +76 -0
- package/dist/services/edge/ensure/cache-rules.js +105 -0
- package/dist/services/edge/ensure/dns-records.js +304 -0
- package/dist/services/edge/ensure/email-routing.js +292 -0
- package/dist/services/edge/ensure/rules-configuration.js +98 -0
- package/dist/services/edge/ensure/rules-managed-transforms.js +101 -0
- package/dist/services/edge/ensure/rules-redirect.js +107 -0
- package/dist/services/edge/ensure/rules-response-headers.js +90 -0
- package/dist/services/edge/ensure/rules-security.js +142 -0
- package/dist/services/edge/ensure/speed-scheduled-tests.js +98 -0
- package/dist/services/edge/ensure/workers.js +219 -0
- package/dist/services/edge/ensure/zone-settings.js +108 -0
- package/dist/services/edge/ensure/zone.js +183 -0
- package/dist/services/edge/index.js +73 -0
- package/dist/services/edge/lib/cloudflare-api.js +84 -0
- package/dist/services/edge/lib/dns-records-helpers.js +518 -0
- package/dist/services/edge/lib/read-cache.js +16 -0
- package/dist/services/edge/lib/ruleset-helper.js +108 -0
- package/dist/services/edge/workers/omega-api-proxy.js +43 -0
- package/dist/services/email/data/baseline-filter.md +4 -0
- package/dist/services/email/data/baseline-knowledge.md +86 -0
- package/dist/services/email/ensure/agent.js +107 -0
- package/dist/services/email/ensure/user.js +45 -0
- package/dist/services/email/index.js +138 -0
- package/dist/services/email/lib/baseline.js +82 -0
- package/dist/services/email/lib/knowledge-file.js +53 -0
- package/dist/services/forms/ensure/form.js +46 -0
- package/dist/services/forms/ensure/user.js +45 -0
- package/dist/services/forms/index.js +138 -0
- package/dist/services/migrations/ensure/notifications.js +189 -0
- package/dist/services/migrations/ensure/orders.js +34 -0
- package/dist/services/migrations/ensure/payment-provider.js +132 -0
- package/dist/services/migrations/ensure/payments-intents.js +34 -0
- package/dist/services/migrations/ensure/state-retirement.js +266 -0
- package/dist/services/migrations/ensure/targets-rename.js +124 -0
- package/dist/services/migrations/ensure/users.js +1176 -0
- package/dist/services/migrations/index.js +101 -0
- package/dist/services/migrations/lib/attribution-touch.js +64 -0
- package/dist/services/migrations/lib/ensure-metadata.js +144 -0
- package/dist/services/migrations/lib/migration-runner.js +473 -0
- package/dist/services/migrations/lib/sanitize-strings.js +112 -0
- package/dist/services/migrations/lib/schema-validator.js +154 -0
- package/dist/services/monitoring/ensure/dsn.js +54 -0
- package/dist/services/monitoring/ensure/projects.js +135 -0
- package/dist/services/monitoring/index.js +54 -0
- package/dist/services/monitoring/lib/sentry-api.js +94 -0
- package/dist/services/newsletter/ensure/custom-fields.js +90 -0
- package/dist/services/newsletter/ensure/publication.js +94 -0
- package/dist/services/newsletter/ensure/segments.js +140 -0
- package/dist/services/newsletter/ensure/webhook.js +108 -0
- package/dist/services/newsletter/index.js +64 -0
- package/dist/services/newsletter/lib/beehiiv-api.js +124 -0
- package/dist/services/newsletter/lib/segment-automation.js +577 -0
- package/dist/services/payment/ensure/chargebee-account.js +30 -0
- package/dist/services/payment/ensure/chargebee-products.js +359 -0
- package/dist/services/payment/ensure/chargebee-webhook.js +113 -0
- package/dist/services/payment/ensure/paypal-account.js +36 -0
- package/dist/services/payment/ensure/paypal-products.js +374 -0
- package/dist/services/payment/ensure/paypal-webhook.js +128 -0
- package/dist/services/payment/ensure/stripe-account.js +93 -0
- package/dist/services/payment/ensure/stripe-disputes.js +56 -0
- package/dist/services/payment/ensure/stripe-products.js +245 -0
- package/dist/services/payment/ensure/stripe-radar.js +74 -0
- package/dist/services/payment/ensure/stripe-webhook.js +130 -0
- package/dist/services/payment/index.js +163 -0
- package/dist/services/payment/lib/chargebee-api.js +338 -0
- package/dist/services/payment/lib/payment-utils.js +116 -0
- package/dist/services/payment/lib/paypal-api.js +354 -0
- package/dist/services/payment/lib/provider-setup.js +134 -0
- package/dist/services/payment/lib/stripe-api.js +152 -0
- package/dist/services/repo/ensure/org.js +70 -0
- package/dist/services/repo/ensure/pages.js +72 -0
- package/dist/services/repo/ensure/repo.js +90 -0
- package/dist/services/repo/index.js +52 -0
- package/dist/services/repo/lib/github-api.js +196 -0
- package/dist/services/search/ensure/ga-link.js +57 -0
- package/dist/services/search/ensure/property.js +137 -0
- package/dist/services/search/ensure/sitemaps.js +70 -0
- package/dist/services/search/index.js +61 -0
- package/dist/services/search/lib/search-console-api.js +92 -0
- package/dist/services/seo/ensure/github-repos.js +271 -0
- package/dist/services/seo/index.js +55 -0
- package/dist/services/seo/lib/gh-api.js +161 -0
- package/dist/services/seo/templates/developer-tool/.github/workflows/maintenance.yml +25 -0
- package/dist/services/seo/templates/developer-tool/.nvmrc +1 -0
- package/dist/services/seo/templates/developer-tool/_README.md.js +121 -0
- package/dist/services/seo/templates/developer-tool/_package.json.js +33 -0
- package/dist/services/seo/templates/developer-tool/src/index.js +120 -0
- package/dist/services/seo/templates/index.js +57 -0
- package/dist/services/server/ensure/brands.js +63 -0
- package/dist/services/server/index.js +42 -0
- package/dist/services/testing/ensure/target-checks.js +97 -0
- package/dist/services/testing/index.js +11 -0
- package/dist/services/testing/lib/checks.js +467 -0
- package/dist/services/update/index.js +20 -0
- package/dist/services/update/lib/cache.js +59 -0
- package/dist/services/update/lib/fingerprint.js +157 -0
- package/dist/services/update/write/targets.js +206 -0
- package/dist/services/workspace/ensure/agents.js +42 -0
- package/dist/services/workspace/ensure/claude-settings.js +34 -0
- package/dist/services/workspace/ensure/config.js +54 -0
- package/dist/services/workspace/ensure/defaults.js +60 -0
- package/dist/services/workspace/ensure/env-keys.js +55 -0
- package/dist/services/workspace/ensure/env-order.js +66 -0
- package/dist/services/workspace/ensure/env-rules.js +97 -0
- package/dist/services/workspace/ensure/gitignore.js +18 -0
- package/dist/services/workspace/ensure/scripts.js +126 -0
- package/dist/services/workspace/ensure/structure.js +97 -0
- package/dist/services/workspace/ensure/translation-sdk.js +148 -0
- package/dist/services/workspace/ensure/workflows.js +54 -0
- package/dist/services/workspace/index.js +8 -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/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/certs.js +199 -0
- package/dist/vendor/devkit/ci-workflows.js +520 -0
- package/dist/vendor/devkit/cli-router.js +156 -0
- package/dist/vendor/devkit/command-path.js +46 -0
- package/dist/vendor/devkit/deploy-record.js +180 -0
- package/dist/vendor/devkit/flows.js +317 -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/omega-bin.js +345 -0
- package/dist/vendor/devkit/prompt.js +187 -0
- package/dist/vendor/devkit/safe-install.js +18 -0
- package/dist/vendor/devkit/stop-signals.js +28 -0
- package/dist/vendor/devkit/test/scope.js +162 -0
- package/dist/vendor/devkit/translate/cache.js +84 -0
- package/dist/vendor/devkit/translate/engine.js +243 -0
- package/dist/vendor/devkit/translate/index.js +49 -0
- package/dist/vendor/devkit/translate/languages.js +134 -0
- package/dist/vendor/devkit/translate/providers.js +188 -0
- package/dist/vendor/devkit/update.js +569 -0
- package/docs/AGENTS.md +200 -0
- package/docs/account.md +45 -0
- package/docs/advertising.md +46 -0
- package/docs/ai.md +34 -0
- package/docs/analytics.md +69 -0
- package/docs/assets.md +57 -0
- package/docs/bookmark.md +26 -0
- package/docs/brand.md +151 -0
- package/docs/campaigns.md +96 -0
- package/docs/captcha.md +46 -0
- package/docs/certificates.md +68 -0
- package/docs/chat.md +47 -0
- package/docs/cloud.md +110 -0
- package/docs/company.md +64 -0
- package/docs/directory.md +140 -0
- package/docs/disperse.md +46 -0
- package/docs/domain.md +56 -0
- package/docs/edge.md +234 -0
- package/docs/email.md +42 -0
- package/docs/forms.md +48 -0
- package/docs/index.md +109 -0
- package/docs/migration.md +203 -0
- package/docs/migrations.md +56 -0
- package/docs/monitoring.md +41 -0
- package/docs/newsletter.md +48 -0
- package/docs/payment.md +83 -0
- package/docs/repo.md +48 -0
- package/docs/search.md +44 -0
- package/docs/seo.md +34 -0
- package/docs/server.md +37 -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/testing.md +34 -0
- package/docs/update.md +36 -0
- package/docs/workspace.md +64 -0
- package/package.json +88 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure DNS records match `edge.providers.cloudflare.dns` plus the platform record set
|
|
3
|
+
* (GitHub Pages, www, email-provider MX/SPF, DMARC; BIMI only when
|
|
4
|
+
* configured — see lib/dns-records-helpers.js).
|
|
5
|
+
*
|
|
6
|
+
* Subdomain projects only touch records belonging to the subdomain (apex
|
|
7
|
+
* records are owned by the parent brand).
|
|
8
|
+
*
|
|
9
|
+
* The whole SendGrid set is not config at all — this handler asks SendGrid
|
|
10
|
+
* before it diffs: `GET /v3/whitelabel/domains` for the `u<id>.<whitelabel>`
|
|
11
|
+
* host the domain-auth records are built from ([#692]), and
|
|
12
|
+
* `GET /v3/whitelabel/links` for whether `emailurl.<domain>` may be proxied
|
|
13
|
+
* yet ([#646]).
|
|
14
|
+
*/
|
|
15
|
+
const chalk = require('chalk').default;
|
|
16
|
+
const { pollWithSpinner } = require('../../../vendor/devkit/flows.js');
|
|
17
|
+
const { cacheRead } = require('../lib/read-cache.js');
|
|
18
|
+
const { getZoneId, zoneGate } = require('../lib/ruleset-helper.js');
|
|
19
|
+
const { diffRecords } = require('../lib/dns-records-helpers.js');
|
|
20
|
+
const { SendGridAPI } = require('../../campaigns/lib/sendgrid-api.js');
|
|
21
|
+
const { canPrompt, dryRunPlan } = require('../../../lib/run-gates.js');
|
|
22
|
+
|
|
23
|
+
const LINK_BRANDING_PENDING_REASON = 'SendGrid has not validated the branded link — the emailurl CNAME stays unproxied';
|
|
24
|
+
|
|
25
|
+
module.exports = async function ensureDnsRecords(context) {
|
|
26
|
+
const { cloudflareApi: api, brandRoot, brandConfig, domain, isSubdomainProject, options = {} } = context;
|
|
27
|
+
const zoneId = getZoneId(context);
|
|
28
|
+
const gated = zoneGate(context, 'dnsRecords');
|
|
29
|
+
if (gated) return gated;
|
|
30
|
+
|
|
31
|
+
// === READ ===
|
|
32
|
+
const data = await api.makeRequest(`/zones/${zoneId}/dns_records`);
|
|
33
|
+
const records = data.result;
|
|
34
|
+
console.log(` ${chalk.green('✓')} Read ${chalk.dim(`(${records.length} items)`)}`);
|
|
35
|
+
cacheRead(brandRoot, 'dns-records', { count: records.length, records });
|
|
36
|
+
|
|
37
|
+
// === DIFF ===
|
|
38
|
+
// Tests inject a fake client via context.sendgridApi
|
|
39
|
+
const sendgridApi = context.sendgridApi || (process.env.SENDGRID_API_KEY ? new SendGridAPI() : null);
|
|
40
|
+
const sendgrid = await resolveDomainAuth(context, sendgridApi);
|
|
41
|
+
const { valid: linkBrandingValid, pending: linkBrandingPending } = await resolveLinkBranding(context, sendgridApi, sendgrid);
|
|
42
|
+
const diff = diffRecords({ records, brandConfig, domain, isSubdomainProject, sendgrid, linkBrandingValid });
|
|
43
|
+
if (!diff) {
|
|
44
|
+
console.log(` ${chalk.dim('⊘ No changes needed')}`);
|
|
45
|
+
if (linkBrandingPending) {
|
|
46
|
+
return { status: 'warned', reason: LINK_BRANDING_PENDING_REASON };
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (options.dryRun) {
|
|
52
|
+
// The SendGrid host is READ, never written, so a dry run reads it too and
|
|
53
|
+
// names what it found — the plan covers the same records a real run does
|
|
54
|
+
// ([#692](https://github.com/Omega-JS-Stack/omega/issues/692)).
|
|
55
|
+
const sendgridPlan = sendgrid ? ` (SendGrid domain auth read live: u${sendgrid.id}.${sendgrid.whitelabel}.sendgrid.net)` : '';
|
|
56
|
+
dryRunPlan(`create ${diff.toCreate.length}, update ${diff.toUpdate.length}, delete ${diff.toDelete.length}${sendgridPlan}`);
|
|
57
|
+
return {
|
|
58
|
+
status: 'success',
|
|
59
|
+
output: { dns: { planned: { create: diff.toCreate.length, update: diff.toUpdate.length, delete: diff.toDelete.length } } },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// === WRITE ===
|
|
64
|
+
const output = { created: [], updated: [], deleted: [], errors: [] };
|
|
65
|
+
|
|
66
|
+
for (const record of diff.toCreate) {
|
|
67
|
+
try {
|
|
68
|
+
await api.makeRequest(`/zones/${zoneId}/dns_records`, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
type: record.type,
|
|
72
|
+
name: record.name,
|
|
73
|
+
content: record.content,
|
|
74
|
+
ttl: record.ttl || 1,
|
|
75
|
+
proxied: record.proxied ?? false,
|
|
76
|
+
priority: record.priority,
|
|
77
|
+
comment: record.comment,
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
output.created.push({ name: record.name, type: record.type });
|
|
81
|
+
console.log(` ${chalk.green('✓')} Created ${chalk.cyan(`${record.type} ${record.name}`)}`);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
output.errors.push({ name: record.name, type: record.type, error: error.message });
|
|
84
|
+
console.error(` ${chalk.red('✗')} Failed to create ${chalk.cyan(`${record.type} ${record.name}`)}${chalk.dim(`: ${error.message}`)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const record of diff.toUpdate) {
|
|
89
|
+
try {
|
|
90
|
+
await api.makeRequest(`/zones/${zoneId}/dns_records/${record.id}`, {
|
|
91
|
+
method: 'PATCH',
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
type: record.type,
|
|
94
|
+
name: record.name,
|
|
95
|
+
content: record.content,
|
|
96
|
+
ttl: record.ttl,
|
|
97
|
+
proxied: record.proxied,
|
|
98
|
+
comment: record.comment,
|
|
99
|
+
priority: record.priority,
|
|
100
|
+
}),
|
|
101
|
+
});
|
|
102
|
+
output.updated.push({ name: record.name, type: record.type });
|
|
103
|
+
console.log(` ${chalk.green('✓')} Updated ${chalk.cyan(`${record.type} ${record.name}`)}`);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
output.errors.push({ name: record.name, type: record.type, error: error.message });
|
|
106
|
+
console.error(` ${chalk.red('✗')} Failed to update ${chalk.cyan(`${record.type} ${record.name}`)}${chalk.dim(`: ${error.message}`)}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const record of diff.toDelete) {
|
|
111
|
+
try {
|
|
112
|
+
await api.makeRequest(`/zones/${zoneId}/dns_records/${record.id}`, { method: 'DELETE' });
|
|
113
|
+
output.deleted.push({ name: record.name, type: record.type, content: record.content });
|
|
114
|
+
console.log(` ${chalk.green('✓')} Deleted ${chalk.cyan(`${record.type} ${record.name}`)} (${chalk.dim(record.content)})`);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
output.errors.push({ name: record.name, type: record.type, error: error.message });
|
|
117
|
+
console.error(` ${chalk.red('✗')} Failed to delete ${chalk.cyan(`${record.type} ${record.name}`)}${chalk.dim(`: ${error.message}`)}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const parts = [];
|
|
122
|
+
if (output.created.length > 0) parts.push(`${chalk.bold(output.created.length)} created`);
|
|
123
|
+
if (output.updated.length > 0) parts.push(`${chalk.bold(output.updated.length)} updated`);
|
|
124
|
+
if (output.deleted.length > 0) parts.push(`${chalk.bold(output.deleted.length)} deleted`);
|
|
125
|
+
if (output.errors.length > 0) parts.push(`${chalk.bold(output.errors.length)} errors`);
|
|
126
|
+
if (parts.length > 0) {
|
|
127
|
+
console.log(` Summary: ${parts.join(', ')}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const reasons = [];
|
|
131
|
+
if (output.errors.length > 0) {
|
|
132
|
+
reasons.push(`${output.errors.length} record(s) failed: ${output.errors.map((e) => e.name).join(', ')}`);
|
|
133
|
+
}
|
|
134
|
+
if (linkBrandingPending) {
|
|
135
|
+
reasons.push(LINK_BRANDING_PENDING_REASON);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
status: reasons.length > 0 ? 'warned' : 'success',
|
|
140
|
+
...(reasons.length > 0 ? { reason: reasons.join('; ') } : {}),
|
|
141
|
+
output: { dns: output },
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The `u<id>.<whitelabel>` host every SendGrid domain-auth record is built
|
|
147
|
+
* from, read LIVE off the account.
|
|
148
|
+
*
|
|
149
|
+
* These are SendGrid's OWN observed facts about the domain, not choices a
|
|
150
|
+
* brand makes, so they are referenced at the source instead of copied into
|
|
151
|
+
* config — a `dns.sendgrid` block drifts the moment the account
|
|
152
|
+
* re-authenticates the domain, and nothing ever wrote it back
|
|
153
|
+
* ([#692](https://github.com/Omega-JS-Stack/omega/issues/692)).
|
|
154
|
+
* `GET /v3/whitelabel/domains` answers with the authentication's own record
|
|
155
|
+
* set, whose `mail_cname` points at exactly the host the record builder
|
|
156
|
+
* rebuilds (`u<id>.<whitelabel>.sendgrid.net`).
|
|
157
|
+
*
|
|
158
|
+
* Every no-answer SKIPS the SendGrid records and says why: no key, no
|
|
159
|
+
* authentication for this domain (the campaigns service creates it), an
|
|
160
|
+
* unreachable API, or a host shape this parser does not know. A subdomain
|
|
161
|
+
* project has no SendGrid records to build at all — the apex record set
|
|
162
|
+
* belongs to the parent brand — so it never asks.
|
|
163
|
+
*
|
|
164
|
+
* @param {object} context - The service context
|
|
165
|
+
* @param {object|null} api - The SendGrid client (null without an API key)
|
|
166
|
+
* @returns {Promise<{ id: string, whitelabel: string }|null>}
|
|
167
|
+
*/
|
|
168
|
+
async function resolveDomainAuth({ domain, isSubdomainProject }, api) {
|
|
169
|
+
if (isSubdomainProject) return null;
|
|
170
|
+
|
|
171
|
+
if (!api) {
|
|
172
|
+
console.log(` ${chalk.yellow('⚠')} SendGrid records skipped — no ${chalk.cyan('SENDGRID_API_KEY')} to read the domain authentication with`);
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let domains;
|
|
177
|
+
try {
|
|
178
|
+
domains = await api.getAuthenticatedDomains();
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.log(` ${chalk.yellow('⚠')} SendGrid records skipped — could not read the domain authentication${chalk.dim(`: ${error.message}`)}`);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const auth = (domains || []).find((entry) => (entry.domain || '').toLowerCase() === domain.toLowerCase());
|
|
185
|
+
if (!auth) {
|
|
186
|
+
console.log(` ${chalk.yellow('⚠')} SendGrid records skipped — no authenticated domain for ${chalk.cyan(domain)} in SendGrid (the campaigns service creates it)`);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const host = auth.dns?.mail_cname?.data;
|
|
191
|
+
const parsed = /^u([^.]+)\.([^.]+)\.sendgrid\.net$/i.exec(host || '');
|
|
192
|
+
if (!parsed) {
|
|
193
|
+
console.log(` ${chalk.yellow('⚠')} SendGrid records skipped — domain-auth host ${chalk.cyan(host || '(none)')} is not ${chalk.dim('u<id>.<whitelabel>.sendgrid.net')}`);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { id: parsed[1], whitelabel: parsed[2] };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Has SendGrid VALIDATED the branded link host for this brand's domain?
|
|
202
|
+
*
|
|
203
|
+
* SendGrid validates `emailurl.<domain>` by resolving it as a CNAME to
|
|
204
|
+
* sendgrid.net, and a Cloudflare-proxied record answers with the edge's own
|
|
205
|
+
* addresses instead — so proxying it BEFORE validation locks the branding out
|
|
206
|
+
* of ever validating, and every emailed link stays broken. This is the read
|
|
207
|
+
* that keeps the flip in order: grey-cloud until `GET /v3/whitelabel/links`
|
|
208
|
+
* reports `valid: true` for the host
|
|
209
|
+
* ([#646](https://github.com/Omega-JS-Stack/omega/issues/646)).
|
|
210
|
+
*
|
|
211
|
+
* An interactive run WAITS for that answer rather than leaving the flip to a
|
|
212
|
+
* later walk ([#662](https://github.com/Omega-JS-Stack/omega/issues/662)):
|
|
213
|
+
* `pollWithSpinner` re-asks SendGrid until the branding validates and the
|
|
214
|
+
* diff below proxies the CNAME in this same walk. Skipping the wait (or a
|
|
215
|
+
* non-interactive run) keeps the record grey and the rerun message.
|
|
216
|
+
*
|
|
217
|
+
* An unreachable SendGrid answers NO: the unproxied record is the safe half of
|
|
218
|
+
* the pair (a plain-HTTP hop, which is where every brand already is), while a
|
|
219
|
+
* wrong YES is unrecoverable without a hand edit. No API key never reaches
|
|
220
|
+
* here — the domain-auth read skipped the whole set first.
|
|
221
|
+
*
|
|
222
|
+
* NO ENTRY is the one NO that is not pending: the campaigns service creates
|
|
223
|
+
* the branding later in this same walk
|
|
224
|
+
* ([#693](https://github.com/Omega-JS-Stack/omega/issues/693)), so waiting on
|
|
225
|
+
* it here would never end. That case says so and moves on.
|
|
226
|
+
*
|
|
227
|
+
* @param {object} context - The service context
|
|
228
|
+
* @param {object|null} api - The SendGrid client (null without an API key)
|
|
229
|
+
* @param {object|null} sendgrid - The live domain-auth values, when there are
|
|
230
|
+
* SendGrid records to gate at all
|
|
231
|
+
* @returns {Promise<{ valid: boolean, pending: boolean }>} - `pending` is a
|
|
232
|
+
* live branding that did not validate: the warned reason's condition.
|
|
233
|
+
*/
|
|
234
|
+
async function resolveLinkBranding(context, api, sendgrid) {
|
|
235
|
+
const { domain, options = {} } = context;
|
|
236
|
+
|
|
237
|
+
// No SendGrid records to gate: the domain-auth read found none (and already
|
|
238
|
+
// said why), or a subdomain project, whose record set stops at the GitHub
|
|
239
|
+
// Pages pair (the apex belongs to the parent).
|
|
240
|
+
if (!sendgrid) {
|
|
241
|
+
return { valid: false, pending: false };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const host = `emailurl.${domain}`;
|
|
245
|
+
|
|
246
|
+
const branding = await readBrandedLink(api, host, true);
|
|
247
|
+
|
|
248
|
+
// Nothing to WAIT for HERE: the account has no link branding for this host
|
|
249
|
+
// yet, and the campaigns service creates it LATER IN THIS SAME WALK
|
|
250
|
+
// ([#693](https://github.com/Omega-JS-Stack/omega/issues/693)) — waiting on
|
|
251
|
+
// it before that would never end. The record still lands (grey), which is
|
|
252
|
+
// exactly the state SendGrid validates against.
|
|
253
|
+
if (branding === null) {
|
|
254
|
+
console.log(` ${chalk.yellow('⚠')} ${chalk.cyan(`CNAME ${host}`)} stays unproxied — SendGrid has no link branding for that host yet. The campaigns service creates and validates it later in this run (when campaigns is enabled for this brand), then flips ${chalk.cyan(`CNAME ${host}`)} to proxied.`);
|
|
255
|
+
return { valid: false, pending: false };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let valid = branding?.valid === true;
|
|
259
|
+
|
|
260
|
+
// Wait it out: SendGrid validates minutes after the CNAME lands, and the
|
|
261
|
+
// proxy flip belongs to THIS walk (#662). Ticks stay quiet — the first read
|
|
262
|
+
// already reported an unreachable API.
|
|
263
|
+
if (!valid && canPrompt(options)) {
|
|
264
|
+
const result = await pollWithSpinner({
|
|
265
|
+
check: async () => ((await readBrandedLink(api, host))?.valid === true ? { done: true } : { done: false }),
|
|
266
|
+
intervalMs: 10000,
|
|
267
|
+
message: `Waiting for SendGrid to validate ${host}`,
|
|
268
|
+
indent: ' ',
|
|
269
|
+
});
|
|
270
|
+
valid = result.success;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!valid) {
|
|
274
|
+
console.log(` ${chalk.yellow('⚠')} ${chalk.cyan(`CNAME ${host}`)} stays unproxied — SendGrid has not validated the branded link. Rerun ${chalk.cyan('omega manage')} once it has, and the record flips to proxied so emailed links land on HTTPS.`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { valid, pending: !valid };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* One read of SendGrid's branded-link list: this host's entry.
|
|
282
|
+
*
|
|
283
|
+
* The caller needs all three answers apart, because only ONE of them is worth
|
|
284
|
+
* waiting on: an entry that exists and is not valid yet is minutes away, while
|
|
285
|
+
* a missing entry arrives from the campaigns service later in the walk, not
|
|
286
|
+
* from any wait here.
|
|
287
|
+
*
|
|
288
|
+
* @param {object} api - The SendGrid client
|
|
289
|
+
* @param {string} host - The branded link host (`emailurl.<domain>`)
|
|
290
|
+
* @param {boolean} [logError] - Report an unreachable API (the first read only)
|
|
291
|
+
* @returns {Promise<object|null|undefined>} The branding entry; `null` when the
|
|
292
|
+
* account has none for this host; `undefined` when the read itself failed.
|
|
293
|
+
*/
|
|
294
|
+
async function readBrandedLink(api, host, logError = false) {
|
|
295
|
+
try {
|
|
296
|
+
const links = await api.getBrandedLinks();
|
|
297
|
+
return (links || []).find((link) => `${link.subdomain}.${link.domain}`.toLowerCase() === host.toLowerCase()) || null;
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (logError) {
|
|
300
|
+
console.log(` ${chalk.yellow('⚠')} Could not read SendGrid link branding${chalk.dim(`: ${error.message}`)}`);
|
|
301
|
+
}
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure Cloudflare Email Routing is enabled and forwarding rules are synced.
|
|
3
|
+
*
|
|
4
|
+
* Only runs when domain.email.providers names cloudflare; rules come from
|
|
5
|
+
* domain.email.forwarding [{ from: 'support' | '*', to: 'inbox@…' }].
|
|
6
|
+
*
|
|
7
|
+
* Unverified destination addresses: a verification email is sent; interactive
|
|
8
|
+
* runs open the dashboard and retry the rule write until the address verifies,
|
|
9
|
+
* non-interactive/dry runs return warned with the dashboard URL — rerun after
|
|
10
|
+
* verifying.
|
|
11
|
+
*/
|
|
12
|
+
const chalk = require('chalk').default;
|
|
13
|
+
const { openBrowserAndPoll } = require('../../../vendor/devkit/flows.js');
|
|
14
|
+
const { cacheRead } = require('../lib/read-cache.js');
|
|
15
|
+
const { getZoneId, zoneGate } = require('../lib/ruleset-helper.js');
|
|
16
|
+
const { resolveEmailProvider } = require('../../domain/lib/registrars.js');
|
|
17
|
+
const { canPrompt, dryRunPlan } = require('../../../lib/run-gates.js');
|
|
18
|
+
|
|
19
|
+
function isUnverifiedError(error) {
|
|
20
|
+
return error.message.includes('2054') || error.message.toLowerCase().includes('not verified');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function getAccountId(api, zoneId) {
|
|
24
|
+
const zone = await api.makeRequest(`/zones/${zoneId}`);
|
|
25
|
+
return zone.result.account.id;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Send a verification email for the destination; interactive runs then open
|
|
30
|
+
* the dashboard and retry the rule write until the address verifies.
|
|
31
|
+
*
|
|
32
|
+
* @returns {boolean} - true when the rule landed after verification
|
|
33
|
+
*/
|
|
34
|
+
async function handleUnverified(api, zoneId, destination, retryWrite, options = {}) {
|
|
35
|
+
console.log(` ${chalk.yellow('⚠')} Destination ${chalk.cyan(destination)} is not verified in Cloudflare`);
|
|
36
|
+
|
|
37
|
+
const accountId = await getAccountId(api, zoneId);
|
|
38
|
+
try {
|
|
39
|
+
await api.makeRequest(`/accounts/${accountId}/email/routing/addresses`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
body: JSON.stringify({ email: destination }),
|
|
42
|
+
});
|
|
43
|
+
console.log(` ${chalk.dim('→')} Verification email sent to ${chalk.cyan(destination)}`);
|
|
44
|
+
} catch (verifyError) {
|
|
45
|
+
if (verifyError.message.includes('already exists')) {
|
|
46
|
+
console.log(` ${chalk.dim('→')} Verification already pending for ${chalk.cyan(destination)}`);
|
|
47
|
+
} else {
|
|
48
|
+
console.log(` ${chalk.yellow('⚠')} Could not send verification email${chalk.dim(`: ${verifyError.message}`)}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const dashboardUrl = `https://dash.cloudflare.com/${accountId}/email-routing/destination-addresses`;
|
|
53
|
+
|
|
54
|
+
if (canPrompt(options)) {
|
|
55
|
+
const result = await openBrowserAndPoll({
|
|
56
|
+
url: dashboardUrl,
|
|
57
|
+
label: 'the Cloudflare destination-addresses page',
|
|
58
|
+
promptMessage: `Verify ${chalk.cyan(destination)} in Cloudflare Email Routing (check the inbox).`,
|
|
59
|
+
waitMessage: 'Checking verification',
|
|
60
|
+
indent: ' ',
|
|
61
|
+
check: async () => {
|
|
62
|
+
try {
|
|
63
|
+
await retryWrite();
|
|
64
|
+
return { done: true };
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (isUnverifiedError(error)) {
|
|
67
|
+
return { done: false };
|
|
68
|
+
}
|
|
69
|
+
return { done: true, error: error.message };
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (result.success) {
|
|
75
|
+
console.log(` ${chalk.green('✓')} Verified — rule created`);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (result.error) {
|
|
79
|
+
console.log(` ${chalk.yellow('⚠')} Rule write failed${chalk.dim(`: ${result.error}`)}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(` ${chalk.dim(`Verify at: ${dashboardUrl} — then rerun`)}`);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = async function ensureEmailRouting(context) {
|
|
88
|
+
const { cloudflareApi: api, brandRoot, domain, brandConfig, options = {} } = context;
|
|
89
|
+
const zoneId = getZoneId(context);
|
|
90
|
+
const gated = zoneGate(context, 'emailRouting');
|
|
91
|
+
if (gated) return gated;
|
|
92
|
+
const emailConfig = brandConfig.domain?.email;
|
|
93
|
+
|
|
94
|
+
if (resolveEmailProvider(brandConfig) !== 'cloudflare') {
|
|
95
|
+
console.log(` ${chalk.dim('⊘ Email provider is not cloudflare — nothing to route')}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// === READ: routing status ===
|
|
100
|
+
let routing = null;
|
|
101
|
+
try {
|
|
102
|
+
const resp = await api.makeRequest(`/zones/${zoneId}/email/routing`);
|
|
103
|
+
routing = resp.result;
|
|
104
|
+
} catch {
|
|
105
|
+
if (options.dryRun) {
|
|
106
|
+
return dryRunPlan('enable email routing', { status: 'success', output: { emailRouting: { planned: 'enable' } } });
|
|
107
|
+
}
|
|
108
|
+
console.log(` ${chalk.dim('→')} Enabling email routing...`);
|
|
109
|
+
await api.makeRequest(`/zones/${zoneId}/email/routing/enable`, { method: 'POST' });
|
|
110
|
+
const resp = await api.makeRequest(`/zones/${zoneId}/email/routing`);
|
|
111
|
+
routing = resp.result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (routing?.enabled) {
|
|
115
|
+
console.log(` ${chalk.green('✓')} Email routing enabled`);
|
|
116
|
+
} else {
|
|
117
|
+
console.log(` ${chalk.yellow('⚠')} Email routing not yet active (may need DNS propagation)`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const desiredRules = emailConfig.forwarding || [];
|
|
121
|
+
if (desiredRules.length === 0) {
|
|
122
|
+
console.log(` ${chalk.dim('→')} No forwarding rules configured`);
|
|
123
|
+
return { output: { emailRouting: { enabled: true, rules: 0 } } };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Split catch-all from regular rules — Cloudflare uses a separate endpoint for catch-all
|
|
127
|
+
const desiredCatchAll = desiredRules.find((r) => r.from === '*');
|
|
128
|
+
const desiredRegular = desiredRules.filter((r) => r.from !== '*');
|
|
129
|
+
|
|
130
|
+
const existingResp = await api.makeRequest(`/zones/${zoneId}/email/routing/rules`);
|
|
131
|
+
const existingRules = existingResp.result || [];
|
|
132
|
+
cacheRead(brandRoot, 'email-routing', { routing, rules: existingRules });
|
|
133
|
+
|
|
134
|
+
let created = 0;
|
|
135
|
+
let updated = 0;
|
|
136
|
+
let skipped = 0;
|
|
137
|
+
let deleted = 0;
|
|
138
|
+
let unverified = 0;
|
|
139
|
+
let planned = 0;
|
|
140
|
+
|
|
141
|
+
// --- Catch-all rule (separate API endpoint) ---
|
|
142
|
+
if (desiredCatchAll) {
|
|
143
|
+
const catchAllResp = await api.makeRequest(`/zones/${zoneId}/email/routing/rules/catch_all`);
|
|
144
|
+
const currentCatchAll = catchAllResp.result;
|
|
145
|
+
const currentAction = currentCatchAll?.actions?.[0];
|
|
146
|
+
const alreadyCorrect = currentCatchAll?.enabled
|
|
147
|
+
&& currentAction?.type === 'forward'
|
|
148
|
+
&& currentAction?.value?.[0] === desiredCatchAll.to;
|
|
149
|
+
|
|
150
|
+
if (alreadyCorrect) {
|
|
151
|
+
console.log(` ${chalk.green('✓')} ${chalk.cyan('*')} → ${chalk.dim(desiredCatchAll.to)}`);
|
|
152
|
+
skipped++;
|
|
153
|
+
} else if (options.dryRun) {
|
|
154
|
+
dryRunPlan(`set catch-all → ${desiredCatchAll.to}`);
|
|
155
|
+
planned++;
|
|
156
|
+
} else {
|
|
157
|
+
console.log(` ${chalk.dim('→')} Updating catch-all: ${chalk.cyan('*')} → ${chalk.dim(desiredCatchAll.to)}`);
|
|
158
|
+
const putCatchAll = () => api.makeRequest(`/zones/${zoneId}/email/routing/rules/catch_all`, {
|
|
159
|
+
method: 'PUT',
|
|
160
|
+
body: JSON.stringify({
|
|
161
|
+
matchers: [{ type: 'all' }],
|
|
162
|
+
actions: [{ type: 'forward', value: [desiredCatchAll.to] }],
|
|
163
|
+
enabled: true,
|
|
164
|
+
name: 'Catch-all',
|
|
165
|
+
}),
|
|
166
|
+
});
|
|
167
|
+
try {
|
|
168
|
+
await putCatchAll();
|
|
169
|
+
console.log(` ${chalk.green('✓')} Updated`);
|
|
170
|
+
updated++;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (!isUnverifiedError(error)) throw error;
|
|
173
|
+
const resolved = await handleUnverified(api, zoneId, desiredCatchAll.to, putCatchAll, options);
|
|
174
|
+
if (resolved) {
|
|
175
|
+
updated++;
|
|
176
|
+
} else {
|
|
177
|
+
unverified++;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// --- Regular rules ---
|
|
184
|
+
for (const desired of desiredRegular) {
|
|
185
|
+
const matcherValue = `${desired.from}@${domain}`;
|
|
186
|
+
|
|
187
|
+
const exists = existingRules.find((rule) => {
|
|
188
|
+
if (!rule.matchers || !rule.actions) return false;
|
|
189
|
+
const matcher = rule.matchers[0];
|
|
190
|
+
const action = rule.actions[0];
|
|
191
|
+
return matcher.type === 'literal'
|
|
192
|
+
&& matcher.value === matcherValue
|
|
193
|
+
&& action.value?.[0] === desired.to;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (exists) {
|
|
197
|
+
console.log(` ${chalk.green('✓')} ${chalk.cyan(matcherValue)} → ${chalk.dim(desired.to)}`);
|
|
198
|
+
skipped++;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (options.dryRun) {
|
|
203
|
+
dryRunPlan(`create ${matcherValue} → ${desired.to}`);
|
|
204
|
+
planned++;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log(` ${chalk.dim('→')} Creating rule: ${chalk.cyan(matcherValue)} → ${chalk.dim(desired.to)}`);
|
|
209
|
+
|
|
210
|
+
const postRule = () => api.makeRequest(`/zones/${zoneId}/email/routing/rules`, {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
body: JSON.stringify({
|
|
213
|
+
matchers: [{ type: 'literal', field: 'to', value: matcherValue }],
|
|
214
|
+
actions: [{ type: 'forward', value: [desired.to] }],
|
|
215
|
+
enabled: true,
|
|
216
|
+
name: `Forward ${matcherValue}`,
|
|
217
|
+
}),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
await postRule();
|
|
222
|
+
console.log(` ${chalk.green('✓')} Created`);
|
|
223
|
+
created++;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (!isUnverifiedError(error)) throw error;
|
|
226
|
+
const resolved = await handleUnverified(api, zoneId, desired.to, postRule, options);
|
|
227
|
+
if (resolved) {
|
|
228
|
+
created++;
|
|
229
|
+
} else {
|
|
230
|
+
unverified++;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Delete regular rules in Cloudflare that aren't in config (skip catch-all — managed separately)
|
|
236
|
+
for (const existing of existingRules) {
|
|
237
|
+
if (!existing.matchers || !existing.actions || !existing.enabled) continue;
|
|
238
|
+
|
|
239
|
+
const matcher = existing.matchers[0];
|
|
240
|
+
const action = existing.actions[0];
|
|
241
|
+
if (action?.type !== 'forward') continue;
|
|
242
|
+
if (matcher.type === 'all') continue;
|
|
243
|
+
|
|
244
|
+
const isDesired = desiredRegular.some((desired) =>
|
|
245
|
+
matcher.type === 'literal'
|
|
246
|
+
&& matcher.value === `${desired.from}@${domain}`
|
|
247
|
+
&& action.value?.[0] === desired.to,
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
if (!isDesired) {
|
|
251
|
+
if (options.dryRun) {
|
|
252
|
+
dryRunPlan(`remove ${matcher.value} → ${action.value?.[0]}`);
|
|
253
|
+
planned++;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
console.log(` ${chalk.dim('→')} Removing rule: ${chalk.cyan(matcher.value)} → ${chalk.dim(action.value?.[0])}`);
|
|
257
|
+
await api.makeRequest(`/zones/${zoneId}/email/routing/rules/${existing.tag}`, { method: 'DELETE' });
|
|
258
|
+
console.log(` ${chalk.green('✓')} Removed`);
|
|
259
|
+
deleted++;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Disable catch-all if not in config but currently active
|
|
264
|
+
if (!desiredCatchAll) {
|
|
265
|
+
const catchAllResp = await api.makeRequest(`/zones/${zoneId}/email/routing/rules/catch_all`);
|
|
266
|
+
const currentCatchAll = catchAllResp.result;
|
|
267
|
+
if (currentCatchAll?.enabled && currentCatchAll?.actions?.[0]?.type === 'forward') {
|
|
268
|
+
if (options.dryRun) {
|
|
269
|
+
dryRunPlan('disable catch-all (not in config)');
|
|
270
|
+
planned++;
|
|
271
|
+
} else {
|
|
272
|
+
console.log(` ${chalk.dim('→')} Disabling catch-all (not in config)`);
|
|
273
|
+
await api.makeRequest(`/zones/${zoneId}/email/routing/rules/catch_all`, {
|
|
274
|
+
method: 'PUT',
|
|
275
|
+
body: JSON.stringify({
|
|
276
|
+
matchers: [{ type: 'all' }],
|
|
277
|
+
actions: [{ type: 'drop' }],
|
|
278
|
+
enabled: false,
|
|
279
|
+
}),
|
|
280
|
+
});
|
|
281
|
+
console.log(` ${chalk.green('✓')} Disabled`);
|
|
282
|
+
deleted++;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
status: unverified > 0 ? 'warned' : 'success',
|
|
289
|
+
...(unverified > 0 ? { reason: `${unverified} destination address(es) unverified` } : {}),
|
|
290
|
+
output: { emailRouting: { enabled: true, created, updated, skipped, deleted, unverified, ...(planned > 0 && { planned }) } },
|
|
291
|
+
};
|
|
292
|
+
};
|