@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,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DNS records helpers — pure logic for building the desired record set,
|
|
3
|
+
* diffing against existing records, normalizing TXT content (SPF/DMARC), and
|
|
4
|
+
* inferring canonical comments. Ported from omega-manager with the company-
|
|
5
|
+
* specific hardcodes moved to config:
|
|
6
|
+
*
|
|
7
|
+
* dns.spfIncludes — SPF include list (manager default: google + sendgrid)
|
|
8
|
+
* dns.dmarcReports — { rua: [...], ruf: [...] } report addresses (no default)
|
|
9
|
+
* dns.bimiLogo — BIMI logo URL (no default; record only emitted when set)
|
|
10
|
+
* dns.records — custom records (verification TXTs like Ahrefs go here)
|
|
11
|
+
*
|
|
12
|
+
* One block in the dns object is NOT config: `sendgrid` — { id, whitelabel },
|
|
13
|
+
* plus `linkBrandingValid` — is what the ensure handler read off SendGrid
|
|
14
|
+
* live and folded in, so the builder below stays pure
|
|
15
|
+
* ([#692](https://github.com/Omega-JS-Stack/omega/issues/692),
|
|
16
|
+
* [#646](https://github.com/Omega-JS-Stack/omega/issues/646)).
|
|
17
|
+
*/
|
|
18
|
+
const chalk = require('chalk').default;
|
|
19
|
+
const { resolveEmailProvider } = require('../../domain/lib/registrars.js');
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// BUILD REQUIRED STATIC RECORDS
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
function buildRequiredRecords(domain, dnsConfig, isSubdomainProject = false, emailProvider = null) {
|
|
26
|
+
const customRecords = [];
|
|
27
|
+
const defaultRecords = [];
|
|
28
|
+
|
|
29
|
+
// -------------------------------------------------------------------------
|
|
30
|
+
// CUSTOM RECORDS - Add any custom records from dns.records array
|
|
31
|
+
// -------------------------------------------------------------------------
|
|
32
|
+
if (dnsConfig?.records && Array.isArray(dnsConfig.records)) {
|
|
33
|
+
for (const record of dnsConfig.records) {
|
|
34
|
+
customRecords.push({
|
|
35
|
+
type: record.type,
|
|
36
|
+
name: record.name === '@' ? domain : (record.name.includes('.') ? record.name : `${record.name}.${domain}`),
|
|
37
|
+
content: record.content,
|
|
38
|
+
proxied: record.proxied !== undefined ? record.proxied : true,
|
|
39
|
+
comment: record.comment || '',
|
|
40
|
+
...(record.priority !== undefined && { priority: record.priority }),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const hasCustomOverride = (type, name) =>
|
|
46
|
+
customRecords.some((r) => r.type === type && r.name.toLowerCase() === name.toLowerCase());
|
|
47
|
+
|
|
48
|
+
// TXT records coexist at one name (SPF + a verification token at the apex is
|
|
49
|
+
// the normal case) — custom TXT records are ADDITIVE, never suppressing a
|
|
50
|
+
// default. Override suppression applies to the unique-ish types only.
|
|
51
|
+
const addDefault = (record) => {
|
|
52
|
+
if (record.type === 'TXT' || !hasCustomOverride(record.type, record.name)) {
|
|
53
|
+
defaultRecords.push(record);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// GitHub Pages A + AAAA for apex/subdomain
|
|
58
|
+
addDefault({ type: 'A', name: domain, content: '185.199.108.153', proxied: true, comment: 'GitHub Pages IP' });
|
|
59
|
+
addDefault({ type: 'AAAA', name: domain, content: '2606:50c0:8000::153', proxied: true, comment: 'GitHub Pages IP' });
|
|
60
|
+
|
|
61
|
+
// Subdomain projects: no email/verification records — return early
|
|
62
|
+
if (isSubdomainProject) {
|
|
63
|
+
return [...customRecords, ...defaultRecords];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// www CNAME -> root
|
|
67
|
+
addDefault({ type: 'CNAME', name: `www.${domain}`, content: domain, proxied: true, comment: 'Redirect www to root domain' });
|
|
68
|
+
|
|
69
|
+
// MX (email provider)
|
|
70
|
+
if (emailProvider === 'squarespace') {
|
|
71
|
+
addDefault({ type: 'MX', name: domain, content: 'mxa.mailgun.org', priority: 10, comment: 'Squarespace email forwarding' });
|
|
72
|
+
addDefault({ type: 'MX', name: domain, content: 'mxb.mailgun.org', priority: 10, comment: 'Squarespace email forwarding' });
|
|
73
|
+
} else if (emailProvider === 'privateemail') {
|
|
74
|
+
addDefault({ type: 'MX', name: domain, content: 'mx1.privateemail.com', priority: 10, comment: 'Private email forwarding (Namecheap)' });
|
|
75
|
+
addDefault({ type: 'MX', name: domain, content: 'mx2.privateemail.com', priority: 10, comment: 'Private email forwarding (Namecheap)' });
|
|
76
|
+
} else if (emailProvider === 'cloudflare') {
|
|
77
|
+
addDefault({ type: 'MX', name: domain, content: 'route1.mx.cloudflare.net', priority: 36, comment: 'Cloudflare Email Routing' });
|
|
78
|
+
addDefault({ type: 'MX', name: domain, content: 'route2.mx.cloudflare.net', priority: 4, comment: 'Cloudflare Email Routing' });
|
|
79
|
+
addDefault({ type: 'MX', name: domain, content: 'route3.mx.cloudflare.net', priority: 24, comment: 'Cloudflare Email Routing' });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// SPF
|
|
83
|
+
const spfIncludes = [...(dnsConfig?.spfIncludes || [])];
|
|
84
|
+
if (emailProvider === 'squarespace') spfIncludes.push('mailgun.org');
|
|
85
|
+
else if (emailProvider === 'privateemail') spfIncludes.push('spf.privateemail.com');
|
|
86
|
+
else if (emailProvider === 'cloudflare') spfIncludes.push('_spf.mx.cloudflare.net');
|
|
87
|
+
|
|
88
|
+
if (spfIncludes.length > 0) {
|
|
89
|
+
const spfEnforcement = dnsConfig?.spf === 'strict' ? '-all' : '~all';
|
|
90
|
+
const spfContent = `"v=spf1 include:${spfIncludes.join(' include:')} ${spfEnforcement}"`;
|
|
91
|
+
addDefault({ type: 'TXT', name: domain, content: spfContent, comment: 'SPF policy' });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// DMARC — report addresses (rua/ruf) only when configured
|
|
95
|
+
const dmarcParts = ['v=DMARC1', `p=${dnsConfig?.dmarcPolicy || 'quarantine'}`];
|
|
96
|
+
const reports = dnsConfig?.dmarcReports;
|
|
97
|
+
if (reports?.rua?.length) {
|
|
98
|
+
dmarcParts.push(`rua=${reports.rua.map((a) => `mailto:${a}`).join(',')}`);
|
|
99
|
+
}
|
|
100
|
+
if (reports?.ruf?.length) {
|
|
101
|
+
dmarcParts.push(`ruf=${reports.ruf.map((a) => `mailto:${a}`).join(',')}`);
|
|
102
|
+
}
|
|
103
|
+
dmarcParts.push('pct=100');
|
|
104
|
+
addDefault({
|
|
105
|
+
type: 'TXT', name: `_dmarc.${domain}`,
|
|
106
|
+
content: `"${dmarcParts.join('; ')}"`,
|
|
107
|
+
comment: 'DMARC policy',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// BIMI — only when a logo URL is configured
|
|
111
|
+
if (dnsConfig?.bimiLogo) {
|
|
112
|
+
addDefault({
|
|
113
|
+
type: 'TXT', name: `default._bimi.${domain}`,
|
|
114
|
+
content: `"v=BIMI1; l=${dnsConfig.bimiLogo}"`,
|
|
115
|
+
comment: 'BIMI logo',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// SendGrid domain auth — only when the live read answered with the account's
|
|
120
|
+
// values for this domain (#692)
|
|
121
|
+
const sendgrid = dnsConfig?.sendgrid;
|
|
122
|
+
if (sendgrid?.id && sendgrid?.whitelabel) {
|
|
123
|
+
const sgHost = `u${sendgrid.id}.${sendgrid.whitelabel}.sendgrid.net`;
|
|
124
|
+
addDefault({ type: 'CNAME', name: `emailauth.${domain}`, content: sgHost, proxied: false, comment: 'SendGrid email authentication' });
|
|
125
|
+
addDefault({ type: 'CNAME', name: `${sendgrid.id}.${domain}`, content: 'sendgrid.net', proxied: false, comment: 'SendGrid URL branding' });
|
|
126
|
+
// PROXIED, alone among the SendGrid records — but only ONCE SENDGRID SAYS
|
|
127
|
+
// THE BRANDED LINK IS VALID
|
|
128
|
+
// ([#646](https://github.com/Omega-JS-Stack/omega/issues/646)). SendGrid
|
|
129
|
+
// rewrites every link in a transactional email through this host, and it
|
|
130
|
+
// serves NO certificate of its own: `https://emailurl.<domain>` fails
|
|
131
|
+
// certificate validation on every legacy brand checked (2026-08-27), so a
|
|
132
|
+
// click could only ever land on `http://` first and the browser warned the
|
|
133
|
+
// recipient that the site is insecure. Cloudflare's proxy terminates TLS at
|
|
134
|
+
// the edge with the zone's own certificate and forwards to sendgrid.net, so
|
|
135
|
+
// the hop is HTTPS end to end.
|
|
136
|
+
//
|
|
137
|
+
// ORDER MATTERS on a NEW domain: SendGrid validates link branding by looking
|
|
138
|
+
// this record up as a CNAME to sendgrid.net, and a proxied record answers
|
|
139
|
+
// with Cloudflare's addresses instead — so flipping the proxy on before
|
|
140
|
+
// validation locks the branding out of ever validating. `linkBrandingValid`
|
|
141
|
+
// is the live answer the ensure handler reads off SendGrid's
|
|
142
|
+
// `GET /v3/whitelabel/links`: grey-cloud until that says `valid: true`, and
|
|
143
|
+
// the rerun converges once it does (docs/shared/breaking-changes.md).
|
|
144
|
+
addDefault({ type: 'CNAME', name: `emailurl.${domain}`, content: 'sendgrid.net', proxied: sendgrid.linkBrandingValid === true, comment: 'SendGrid URL tracking' });
|
|
145
|
+
addDefault({ type: 'CNAME', name: `s1._domainkey.${domain}`, content: `s1.domainkey.${sgHost}`, proxied: false, comment: 'Sendgrid DKIM key' });
|
|
146
|
+
addDefault({ type: 'CNAME', name: `s2._domainkey.${domain}`, content: `s2.domainkey.${sgHost}`, proxied: false, comment: 'Sendgrid DKIM key' });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return [...customRecords, ...defaultRecords];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// =============================================================================
|
|
153
|
+
// FIND OBSOLETE MX RECORDS
|
|
154
|
+
// =============================================================================
|
|
155
|
+
|
|
156
|
+
function findObsoleteMxRecords(existingRecords, domain, emailProvider = null) {
|
|
157
|
+
const toDelete = [];
|
|
158
|
+
|
|
159
|
+
const providerMxServers = {
|
|
160
|
+
squarespace: ['mailgun.org'],
|
|
161
|
+
privateemail: ['privateemail.com'],
|
|
162
|
+
cloudflare: ['cloudflare.net'],
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const obsoleteServers = [];
|
|
166
|
+
for (const [provider, servers] of Object.entries(providerMxServers)) {
|
|
167
|
+
if (provider !== emailProvider) obsoleteServers.push(...servers);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const record of existingRecords) {
|
|
171
|
+
if (record.type !== 'MX') continue;
|
|
172
|
+
if (record.name.toLowerCase() !== domain.toLowerCase()) continue;
|
|
173
|
+
|
|
174
|
+
const content = record.content.toLowerCase();
|
|
175
|
+
if (obsoleteServers.some((server) => content.includes(server))) {
|
|
176
|
+
console.log(` ${chalk.yellow('⚠')} Obsolete: MX ${chalk.cyan(record.name)} -> ${chalk.dim(record.content)} (wrong provider)`);
|
|
177
|
+
toDelete.push(record);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return toDelete;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// =============================================================================
|
|
185
|
+
// VALIDATE DYNAMIC RECORDS (warnings only)
|
|
186
|
+
// =============================================================================
|
|
187
|
+
|
|
188
|
+
function validateDynamicRecords(records, domain) {
|
|
189
|
+
const apiSubdomain = `api.${domain}`;
|
|
190
|
+
|
|
191
|
+
const apiCnameRecord = records.find((r) =>
|
|
192
|
+
r.type === 'CNAME' && r.name.toLowerCase() === apiSubdomain.toLowerCase(),
|
|
193
|
+
);
|
|
194
|
+
if (!apiCnameRecord) {
|
|
195
|
+
console.log(` ${chalk.yellow('⚠')} Missing: CNAME ${chalk.cyan(`api.${domain}`)} (created by Firebase service)`);
|
|
196
|
+
} else if (!apiCnameRecord.proxied) {
|
|
197
|
+
console.log(` ${chalk.yellow('⚠')} API CNAME not proxied (may need to enable after Firebase verification)`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const hasAcmeChallenge = records.some((r) =>
|
|
201
|
+
r.type === 'TXT' && r.name.toLowerCase().startsWith('_acme-challenge.api'),
|
|
202
|
+
);
|
|
203
|
+
if (!hasAcmeChallenge) {
|
|
204
|
+
console.log(` ${chalk.yellow('⚠')} Missing: TXT ${chalk.cyan(`_acme-challenge.api.${domain}`)} (created by Firebase service)`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const hasHostingSite = records.some((r) => {
|
|
208
|
+
if (r.type !== 'TXT' || r.name.toLowerCase() !== apiSubdomain.toLowerCase()) return false;
|
|
209
|
+
const content = r.content.replace(/^"|"$/g, '');
|
|
210
|
+
return content.toLowerCase().includes('hosting-site=');
|
|
211
|
+
});
|
|
212
|
+
if (!hasHostingSite) {
|
|
213
|
+
console.log(` ${chalk.yellow('⚠')} Missing: TXT ${chalk.cyan(`api.${domain}`)} hosting-site= (created by Firebase service)`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// =============================================================================
|
|
218
|
+
// DETERMINE COMMENT FOR EXISTING RECORD
|
|
219
|
+
// =============================================================================
|
|
220
|
+
|
|
221
|
+
function determineComment(record, domain) {
|
|
222
|
+
const name = record.name.toLowerCase();
|
|
223
|
+
const type = record.type.toLowerCase();
|
|
224
|
+
const content = record.content.toLowerCase();
|
|
225
|
+
|
|
226
|
+
if (type === 'a' && /^185\.199\.10[89]\.153$|^185\.199\.11[01]\.153$/.test(content)) {
|
|
227
|
+
return 'GitHub Pages IP';
|
|
228
|
+
}
|
|
229
|
+
if (type === 'aaaa' && /^2606:50c0:800[0-3]::153$/.test(content)) {
|
|
230
|
+
return 'GitHub Pages IP';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (type === 'cname') {
|
|
234
|
+
if (content.includes('sendgrid.net') && name.match(/^\d+\./)) return 'SendGrid URL branding';
|
|
235
|
+
if (content.includes('.web.app') || content.includes('.firebaseapp.com')) return 'Firebase hosting domain verification (quick setup mode)';
|
|
236
|
+
if (content.includes('sendgrid.net') && name.includes('emailauth')) return 'SendGrid email authentication';
|
|
237
|
+
if (content.includes('sendgrid.net') && name.includes('emailurl')) return 'SendGrid URL tracking';
|
|
238
|
+
if (name.includes('_domainkey') && content.includes('sendgrid.net')) return 'Sendgrid DKIM key';
|
|
239
|
+
if (name.includes('www.') && content === domain) return 'Redirect www to root domain';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (type === 'mx') {
|
|
243
|
+
if (content.includes('mailgun.org')) return 'Squarespace email forwarding';
|
|
244
|
+
if (content.includes('privateemail.com')) return 'Private email forwarding (Namecheap)';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (type === 'txt') {
|
|
248
|
+
if (name.includes('_acme-challenge')) return 'Firebase hosting domain verification (advanced setup mode)';
|
|
249
|
+
if (content.includes('hosting-site=')) return 'Firebase hosting domain verification (advanced setup mode)';
|
|
250
|
+
if (name.includes('_bimi')) return 'BIMI logo';
|
|
251
|
+
if (name.includes('_dmarc')) return 'DMARC policy';
|
|
252
|
+
if (name.includes('_github') || name.includes('-gh-')) return 'GitHub domain verification';
|
|
253
|
+
if (name.includes('_domainkey') && content.includes('k=rsa')) return 'Squarespace DKIM';
|
|
254
|
+
if (content.includes('ahrefs-site-verification')) return 'Ahrefs domain verification';
|
|
255
|
+
if (content.includes('google-site-verification')) return 'Google domain verification';
|
|
256
|
+
if (content.includes('v=spf1')) return 'SPF policy';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return record.comment;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// =============================================================================
|
|
263
|
+
// FIX QUOTES ON TXT RECORDS
|
|
264
|
+
// =============================================================================
|
|
265
|
+
|
|
266
|
+
function fixQuotes(record) {
|
|
267
|
+
if (record.type !== 'TXT') return record.content;
|
|
268
|
+
const content = record.content;
|
|
269
|
+
const startsWithQuote = content.startsWith('"');
|
|
270
|
+
const endsWithQuote = content.endsWith('"');
|
|
271
|
+
if (startsWithQuote && endsWithQuote) return content;
|
|
272
|
+
let fixed = content;
|
|
273
|
+
if (!startsWithQuote) fixed = `"${fixed}`;
|
|
274
|
+
if (!endsWithQuote) fixed = `${fixed}"`;
|
|
275
|
+
return fixed;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// =============================================================================
|
|
279
|
+
// ENFORCE SPF POLICY
|
|
280
|
+
// =============================================================================
|
|
281
|
+
|
|
282
|
+
function enforceSPF(record, content, brandConfig) {
|
|
283
|
+
if (record.type !== 'TXT') return content;
|
|
284
|
+
if (!content.toLowerCase().includes('v=spf1')) return content;
|
|
285
|
+
|
|
286
|
+
const spfSetting = brandConfig?.edge?.providers?.cloudflare?.dns?.spf;
|
|
287
|
+
if (!spfSetting) return content;
|
|
288
|
+
|
|
289
|
+
const spfQualifiers = { strict: '-all', soft: '~all', neutral: '?all', pass: '+all' };
|
|
290
|
+
const targetQualifier = spfQualifiers[spfSetting];
|
|
291
|
+
if (!targetQualifier) return content;
|
|
292
|
+
|
|
293
|
+
return content.replace(/([~\-+?])?all/g, targetQualifier);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// =============================================================================
|
|
297
|
+
// REORDER DMARC TAGS
|
|
298
|
+
// =============================================================================
|
|
299
|
+
|
|
300
|
+
// Stable rua ordering: configured report addresses first (in config order),
|
|
301
|
+
// everything else keeps its original position — so diffs don't churn on
|
|
302
|
+
// Cloudflare's arbitrary ordering (omega-manager hardcoded its own addresses
|
|
303
|
+
// here; the configured list is the general form)
|
|
304
|
+
function reorderRuaEmails(ruaValue, configuredRua = []) {
|
|
305
|
+
const emails = ruaValue.split(',').map((e) => e.trim());
|
|
306
|
+
if (configuredRua.length === 0) return emails.join(',');
|
|
307
|
+
|
|
308
|
+
const ranked = emails.map((email, index) => {
|
|
309
|
+
const rank = configuredRua.findIndex((addr) => email.toLowerCase().includes(addr.toLowerCase()));
|
|
310
|
+
return { email, rank: rank === -1 ? configuredRua.length : rank, index };
|
|
311
|
+
});
|
|
312
|
+
ranked.sort((a, b) => a.rank - b.rank || a.index - b.index);
|
|
313
|
+
return ranked.map((r) => r.email).join(',');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function reorderDMARC(record, content, brandConfig) {
|
|
317
|
+
if (record.type !== 'TXT') return content;
|
|
318
|
+
if (!content.toLowerCase().includes('v=dmarc1')) return content;
|
|
319
|
+
|
|
320
|
+
let dmarcContent = content;
|
|
321
|
+
const hasQuotes = dmarcContent.startsWith('"') && dmarcContent.endsWith('"');
|
|
322
|
+
if (hasQuotes) dmarcContent = dmarcContent.slice(1, -1);
|
|
323
|
+
|
|
324
|
+
const tags = {};
|
|
325
|
+
const tagPairs = dmarcContent.split(';').map((tag) => tag.trim()).filter(Boolean);
|
|
326
|
+
for (const pair of tagPairs) {
|
|
327
|
+
const [key, ...valueParts] = pair.split('=');
|
|
328
|
+
if (key && valueParts.length > 0) {
|
|
329
|
+
tags[key.trim()] = valueParts.join('=').trim();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const dnsConfig = brandConfig?.edge?.providers?.cloudflare?.dns;
|
|
334
|
+
const dmarcSetting = dnsConfig?.dmarcPolicy;
|
|
335
|
+
if (dmarcSetting && ['none', 'quarantine', 'reject'].includes(dmarcSetting)) {
|
|
336
|
+
tags.p = dmarcSetting;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (tags.rua) {
|
|
340
|
+
tags.rua = reorderRuaEmails(tags.rua, dnsConfig?.dmarcReports?.rua || []);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const orderedKeys = ['v', 'p', 'rua', 'ruf', 'pct', 'adkim', 'aspf', 'ri', 'fo', 'rf'];
|
|
344
|
+
const parts = [];
|
|
345
|
+
for (const key of orderedKeys) {
|
|
346
|
+
if (tags[key] !== undefined) parts.push(`${key}=${tags[key]}`);
|
|
347
|
+
}
|
|
348
|
+
for (const key in tags) {
|
|
349
|
+
if (!orderedKeys.includes(key)) parts.push(`${key}=${tags[key]}`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let result = parts.join('; ');
|
|
353
|
+
if (hasQuotes) result = `"${result}"`;
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// =============================================================================
|
|
358
|
+
// TXT IDENTITY
|
|
359
|
+
// =============================================================================
|
|
360
|
+
|
|
361
|
+
// Multiple TXT records live at one name, so "does this required TXT exist"
|
|
362
|
+
// can't be name+type alone (the apex SPF would match a verification token and
|
|
363
|
+
// never be created). Policy records match by kind — content drift is the
|
|
364
|
+
// update pass's job (enforceSPF/reorderDMARC); everything else matches by
|
|
365
|
+
// exact content.
|
|
366
|
+
function txtKind(content = '') {
|
|
367
|
+
const stripped = content.replace(/^"|"$/g, '');
|
|
368
|
+
const lower = stripped.toLowerCase();
|
|
369
|
+
if (lower.startsWith('v=spf1')) return 'spf';
|
|
370
|
+
if (lower.startsWith('v=dmarc1')) return 'dmarc';
|
|
371
|
+
if (lower.startsWith('v=bimi1')) return 'bimi';
|
|
372
|
+
return stripped;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// =============================================================================
|
|
376
|
+
// DIFF RECORDS - main entry point used by ensure handler
|
|
377
|
+
// =============================================================================
|
|
378
|
+
|
|
379
|
+
function diffRecords({ records, brandConfig, domain, isSubdomainProject, sendgrid = null, linkBrandingValid = false }) {
|
|
380
|
+
let dnsConfig = brandConfig?.edge?.providers?.cloudflare?.dns;
|
|
381
|
+
|
|
382
|
+
if (!dnsConfig && !isSubdomainProject) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// SendGrid's OWN facts ride in beside the config, never copied into it: the
|
|
387
|
+
// domain-auth values `{ id, whitelabel }` the ensure handler read off
|
|
388
|
+
// `GET /v3/whitelabel/domains` ([#692]) plus its word on the branded link
|
|
389
|
+
// ([#646]), as one block the record builder reads and stays pure.
|
|
390
|
+
//
|
|
391
|
+
// The key is REPLACED, never merged: a `sendgrid` block left behind in any
|
|
392
|
+
// config layer must never build the records the live read just declined to
|
|
393
|
+
// — that silent fallback is the whole bug #692 closes.
|
|
394
|
+
if (dnsConfig) {
|
|
395
|
+
dnsConfig = { ...dnsConfig, sendgrid: sendgrid ? { ...sendgrid, linkBrandingValid } : undefined };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// For subdomain projects, filter records to only those belonging to this subdomain
|
|
399
|
+
let relevantRecords = records;
|
|
400
|
+
if (isSubdomainProject) {
|
|
401
|
+
relevantRecords = records.filter((r) => {
|
|
402
|
+
const name = r.name.toLowerCase();
|
|
403
|
+
const subDomain = domain.toLowerCase();
|
|
404
|
+
return name === subDomain || name.endsWith(`.${subDomain}`);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const emailProvider = resolveEmailProvider(brandConfig);
|
|
409
|
+
const requiredRecords = buildRequiredRecords(domain, dnsConfig, isSubdomainProject, emailProvider);
|
|
410
|
+
const toCreate = [];
|
|
411
|
+
const configOverrides = [];
|
|
412
|
+
const uniqueRecordTypes = ['CNAME'];
|
|
413
|
+
|
|
414
|
+
for (const required of requiredRecords) {
|
|
415
|
+
const existing = relevantRecords.find((r) =>
|
|
416
|
+
r.name.toLowerCase() === required.name.toLowerCase() && r.type === required.type
|
|
417
|
+
&& (required.type !== 'TXT' || txtKind(r.content) === txtKind(required.content)),
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
if (!existing) {
|
|
421
|
+
console.log(` ${chalk.yellow('⚠')} Missing: ${chalk.cyan(`${required.type} ${required.name}`)}`);
|
|
422
|
+
toCreate.push(required);
|
|
423
|
+
} else if (uniqueRecordTypes.includes(required.type)) {
|
|
424
|
+
const contentDiff = existing.content !== required.content;
|
|
425
|
+
const proxiedDiff = required.proxied !== undefined && existing.proxied !== required.proxied;
|
|
426
|
+
if (contentDiff || proxiedDiff) {
|
|
427
|
+
const reasons = [];
|
|
428
|
+
if (contentDiff) reasons.push(`${chalk.dim(existing.content)} => ${chalk.cyan(required.content)}`);
|
|
429
|
+
if (proxiedDiff) reasons.push(`proxied: ${existing.proxied} => ${required.proxied}`);
|
|
430
|
+
console.log(` ${chalk.dim('·')} Override: ${chalk.cyan(`${required.type} ${required.name}`)} (${reasons.join(', ')})`);
|
|
431
|
+
configOverrides.push({
|
|
432
|
+
...existing,
|
|
433
|
+
content: contentDiff ? required.content : existing.content,
|
|
434
|
+
comment: required.comment || existing.comment,
|
|
435
|
+
proxied: required.proxied !== undefined ? required.proxied : existing.proxied,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const toDelete = isSubdomainProject ? [] : findObsoleteMxRecords(relevantRecords, domain, emailProvider);
|
|
442
|
+
|
|
443
|
+
const replaceableTypes = ['A', 'AAAA', 'CNAME'];
|
|
444
|
+
for (const existing of relevantRecords) {
|
|
445
|
+
if (!replaceableTypes.includes(existing.type)) continue;
|
|
446
|
+
const requiredForThisName = requiredRecords.filter((r) =>
|
|
447
|
+
r.type === existing.type && r.name.toLowerCase() === existing.name.toLowerCase(),
|
|
448
|
+
);
|
|
449
|
+
if (requiredForThisName.length > 0) {
|
|
450
|
+
const isInRequired = requiredForThisName.some((r) => r.content === existing.content);
|
|
451
|
+
if (!isInRequired) {
|
|
452
|
+
console.log(` ${chalk.dim('·')} Replace: ${chalk.cyan(`${existing.type} ${existing.name}`)} (${chalk.dim(existing.content)} not in config)`);
|
|
453
|
+
toDelete.push(existing);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const toUpdate = [...configOverrides];
|
|
459
|
+
const overrideIds = new Set(configOverrides.map((r) => r.id));
|
|
460
|
+
|
|
461
|
+
const requiredByKey = new Map(
|
|
462
|
+
requiredRecords.map((r) => [`${r.type}:${r.name.toLowerCase()}`, r]),
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
for (const existing of relevantRecords) {
|
|
466
|
+
if (toDelete.some((d) => d.id === existing.id) || overrideIds.has(existing.id)) continue;
|
|
467
|
+
|
|
468
|
+
const newComment = determineComment(existing, domain);
|
|
469
|
+
let newContent = fixQuotes(existing);
|
|
470
|
+
|
|
471
|
+
if (!isSubdomainProject) {
|
|
472
|
+
newContent = enforceSPF(existing, newContent, brandConfig);
|
|
473
|
+
newContent = reorderDMARC(existing, newContent, brandConfig);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const required = requiredByKey.get(`${existing.type}:${existing.name.toLowerCase()}`);
|
|
477
|
+
const expectedProxied = required?.proxied;
|
|
478
|
+
const proxiedChanged = expectedProxied !== undefined && existing.proxied !== expectedProxied;
|
|
479
|
+
|
|
480
|
+
const commentChanged = newComment !== existing.comment;
|
|
481
|
+
const contentChanged = newContent !== existing.content;
|
|
482
|
+
|
|
483
|
+
if (commentChanged || contentChanged || proxiedChanged) {
|
|
484
|
+
const changes = [];
|
|
485
|
+
if (commentChanged) changes.push(`comment: "${existing.comment || ''}" => "${newComment}"`);
|
|
486
|
+
if (contentChanged) changes.push('content changed');
|
|
487
|
+
if (proxiedChanged) changes.push(`proxied: ${existing.proxied} => ${expectedProxied}`);
|
|
488
|
+
console.log(` ${chalk.dim('·')} Update: ${chalk.cyan(`${existing.type} ${existing.name}`)} (${changes.join(', ')})`);
|
|
489
|
+
toUpdate.push({
|
|
490
|
+
...existing,
|
|
491
|
+
comment: newComment,
|
|
492
|
+
content: newContent,
|
|
493
|
+
...(proxiedChanged && { proxied: expectedProxied }),
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!isSubdomainProject) {
|
|
499
|
+
validateDynamicRecords(relevantRecords, domain);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (toCreate.length === 0 && toUpdate.length === 0 && toDelete.length === 0) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return { toCreate, toUpdate, toDelete };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
module.exports = {
|
|
510
|
+
buildRequiredRecords,
|
|
511
|
+
findObsoleteMxRecords,
|
|
512
|
+
validateDynamicRecords,
|
|
513
|
+
determineComment,
|
|
514
|
+
fixQuotes,
|
|
515
|
+
enforceSPF,
|
|
516
|
+
reorderDMARC,
|
|
517
|
+
diffRecords,
|
|
518
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache the result of a cloudflare read step to .omega/cache/cloudflare/{op}.json
|
|
3
|
+
* (omega-manager wrote to .output/{brandId}/cloudflare/ — in the brand-monorepo
|
|
4
|
+
* world the cache lives inside the brand's own gitignored .omega/).
|
|
5
|
+
*
|
|
6
|
+
* Debugging aid: inspect what the service saw without re-hitting the API.
|
|
7
|
+
*/
|
|
8
|
+
const { join } = require('node:path');
|
|
9
|
+
const jetpack = require('fs-jetpack');
|
|
10
|
+
|
|
11
|
+
function cacheRead(brandRoot, operationName, data) {
|
|
12
|
+
const filePath = join(brandRoot, '.omega', 'cache', 'cloudflare', `${operationName}.json`);
|
|
13
|
+
jetpack.write(filePath, { read: data });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { cacheRead };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for Cloudflare ruleset operations (cache, redirect,
|
|
3
|
+
* configuration, response-headers, security). Ported from omega-manager with
|
|
4
|
+
* one addition: applyRuleset honors dryRun (reports the planned write, makes
|
|
5
|
+
* no API call).
|
|
6
|
+
*
|
|
7
|
+
* Cloudflare's "rulesets" feature backs many of its policies, all reachable via:
|
|
8
|
+
* GET /zones/{id}/rulesets/phases/{phase}/entrypoint (fetch)
|
|
9
|
+
* POST /zones/{id}/rulesets (create when missing)
|
|
10
|
+
* PUT /zones/{id}/rulesets/{rulesetId} (update existing)
|
|
11
|
+
*
|
|
12
|
+
* If the phase has never been used, Cloudflare returns "could not find
|
|
13
|
+
* entrypoint ruleset" — detected here so the first write POSTs a new ruleset.
|
|
14
|
+
*/
|
|
15
|
+
const chalk = require('chalk').default;
|
|
16
|
+
const { dryRunPlan } = require('../../../lib/run-gates.js');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fetch the entrypoint ruleset for a phase. Returns { ruleset, needsCreate }.
|
|
20
|
+
*/
|
|
21
|
+
async function fetchRuleset(api, zoneId, phase) {
|
|
22
|
+
try {
|
|
23
|
+
const data = await api.makeRequest(`/zones/${zoneId}/rulesets/phases/${phase}/entrypoint`);
|
|
24
|
+
return { ruleset: data.result, needsCreate: false };
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error.message.includes('10003') || error.message.includes('could not find entrypoint ruleset')) {
|
|
27
|
+
return { ruleset: null, needsCreate: true };
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Apply rules to the zone — POST if the ruleset doesn't exist yet, PUT otherwise.
|
|
35
|
+
* Logs success/failure with `label` (e.g. "Cache rules").
|
|
36
|
+
*
|
|
37
|
+
* Returns { updated, planned?, error? } suitable for the output field.
|
|
38
|
+
*/
|
|
39
|
+
async function applyRuleset(api, zoneId, { ruleset, needsCreate, rules, phase, name, description, label, dryRun = false }) {
|
|
40
|
+
const output = { updated: false };
|
|
41
|
+
|
|
42
|
+
if (dryRun) {
|
|
43
|
+
dryRunPlan(`${needsCreate ? 'create' : 'update'} ${label} (${rules.length} rules)`);
|
|
44
|
+
output.planned = needsCreate ? 'create' : 'update';
|
|
45
|
+
return output;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
if (needsCreate) {
|
|
50
|
+
await api.makeRequest(`/zones/${zoneId}/rulesets`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
body: JSON.stringify({ name, description, kind: 'zone', phase, rules }),
|
|
53
|
+
});
|
|
54
|
+
output.updated = true;
|
|
55
|
+
console.log(` ${chalk.green('✓')} ${label} created`);
|
|
56
|
+
} else {
|
|
57
|
+
await api.makeRequest(`/zones/${zoneId}/rulesets/${ruleset.id}`, {
|
|
58
|
+
method: 'PUT',
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
rules,
|
|
61
|
+
description: ruleset.description || description,
|
|
62
|
+
kind: ruleset.kind,
|
|
63
|
+
name: ruleset.name,
|
|
64
|
+
phase: ruleset.phase,
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
output.updated = true;
|
|
68
|
+
console.log(` ${chalk.green('✓')} ${label} updated`);
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
output.error = error.message;
|
|
72
|
+
console.error(` ${chalk.red('✗')} ${label} failed${chalk.dim(`: ${error.message}`)}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return output;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the zone id for a handler: the zone operation's freshly-returned
|
|
80
|
+
* state (serviceData) wins over the setup-time lookup — so a zone created
|
|
81
|
+
* THIS run is visible to every later operation (omega-manager only saw it on
|
|
82
|
+
* the next run).
|
|
83
|
+
*/
|
|
84
|
+
function getZoneId(context) {
|
|
85
|
+
return context.serviceData?.zoneId ?? context.zoneId ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Gate for zone-scoped operations: no zoneId means the zone doesn't exist
|
|
90
|
+
* yet — reachable only in a dry-run (live runs create the zone first, its
|
|
91
|
+
* id flowing through serviceData; cp113 made that true for subdomain
|
|
92
|
+
* parents too), where the op plans instead of calling the API with
|
|
93
|
+
* zones/null. Returns the handler's early return, or null to proceed.
|
|
94
|
+
*/
|
|
95
|
+
function zoneGate(context, key) {
|
|
96
|
+
if (getZoneId(context)) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
if (context.options?.dryRun) {
|
|
100
|
+
return dryRunPlan(
|
|
101
|
+
`reconcile ${key} once zone ${context.zoneDomain} exists`,
|
|
102
|
+
{ status: 'success', output: { [key]: { planned: 'after-zone' } } },
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return { status: 'warned', reason: 'no Cloudflare zone available', output: { [key]: { note: 'no zone available' } } };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = { fetchRuleset, applyRuleset, getZoneId, zoneGate };
|