@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,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure SendGrid link branding for the brand's domain — the `emailurl.`
|
|
3
|
+
* host every transactional link is rewritten through.
|
|
4
|
+
*
|
|
5
|
+
* A valid branding is converged proof. A missing one is created in one pass,
|
|
6
|
+
* exactly like its domain-auth sibling: brand the links in SendGrid (two
|
|
7
|
+
* CNAMEs), diff-sync those records into the Cloudflare apex zone UNPROXIED,
|
|
8
|
+
* then validate — interactive runs poll until SendGrid confirms;
|
|
9
|
+
* non-interactive/dry runs validate ONCE, report warned, and the rerun
|
|
10
|
+
* converges.
|
|
11
|
+
*
|
|
12
|
+
* The records are written HERE and not left to the edge service: on a fresh
|
|
13
|
+
* brand's FIRST walk, edge's live SendGrid read finds no authenticated domain
|
|
14
|
+
* at all, so it writes none of the SendGrid set — and a branding with no CNAME
|
|
15
|
+
* behind it can never validate.
|
|
16
|
+
*
|
|
17
|
+
* Once SendGrid says valid, the record may finally ride Cloudflare's proxy —
|
|
18
|
+
* so this operation patches it to proxied on the spot instead of leaving the
|
|
19
|
+
* flip to a later walk ([#693](https://github.com/Omega-JS-Stack/omega/issues/693)).
|
|
20
|
+
*/
|
|
21
|
+
const chalk = require('chalk').default;
|
|
22
|
+
const { pollWithSpinner } = require('../../../vendor/devkit/flows.js');
|
|
23
|
+
const { canPrompt, dryRunPlan } = require('../../../lib/run-gates.js');
|
|
24
|
+
const { syncDnsRecords, manualRecordList } = require('../lib/dns-sync.js');
|
|
25
|
+
|
|
26
|
+
// MUST match the name the edge record set builds (`emailurl.<domain>` in
|
|
27
|
+
// services/edge/lib/dns-records-helpers.js) — the branding SendGrid validates
|
|
28
|
+
// and the CNAME Cloudflare serves are the same host or neither works.
|
|
29
|
+
const SUBDOMAIN = 'emailurl';
|
|
30
|
+
|
|
31
|
+
// The record set SendGrid returns for a branded link
|
|
32
|
+
const DNS_KEYS = ['domain_cname', 'owner_cname'];
|
|
33
|
+
|
|
34
|
+
module.exports = async function ensureLinkBranding(context) {
|
|
35
|
+
const { sendgridApi: api, cloudflareApi, domain, apexDomain, isSubdomainProject, options = {} } = context;
|
|
36
|
+
|
|
37
|
+
// A subdomain project owns no apex records — the parent brand's walk does,
|
|
38
|
+
// which is why the edge service skips the whole SendGrid set for one. A
|
|
39
|
+
// branding created here would wait forever on CNAMEs this brand never writes.
|
|
40
|
+
if (isSubdomainProject) {
|
|
41
|
+
console.log(` ${chalk.dim(`⊘ Subdomain project — the parent brand owns the link branding for ${apexDomain}`)}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const host = `${SUBDOMAIN}.${domain}`;
|
|
46
|
+
|
|
47
|
+
// === READ: is the host already branded and valid? ===
|
|
48
|
+
const links = await api.getBrandedLinks();
|
|
49
|
+
let branding = findBranding(links, host);
|
|
50
|
+
|
|
51
|
+
if (branding?.valid) {
|
|
52
|
+
console.log(` ${chalk.green('✓')} Link branding ${chalk.cyan(host)} validated`);
|
|
53
|
+
// Catch-up flip only: a branding THIS run did not validate is the edge
|
|
54
|
+
// service's to reconcile (its live read already desired the record
|
|
55
|
+
// proxied), so a grey record here is a raced walk, not a pending step —
|
|
56
|
+
// patch it when Cloudflare is reachable, and never warn about it.
|
|
57
|
+
if (cloudflareApi) {
|
|
58
|
+
await proxyRecord(cloudflareApi, apexDomain, host);
|
|
59
|
+
}
|
|
60
|
+
return { output: { linkBranding: { id: branding.id, valid: true } } };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (options.dryRun) {
|
|
64
|
+
const planned = branding ? ['dns-records', 'validate', 'proxy-record'] : ['brand-links', 'dns-records', 'validate', 'proxy-record'];
|
|
65
|
+
return dryRunPlan(`${planned.join(' → ')}`, { output: { linkBranding: { planned } } });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// === Create the branding when missing ===
|
|
69
|
+
if (!branding) {
|
|
70
|
+
branding = await api.createBrandedLink(domain, SUBDOMAIN);
|
|
71
|
+
console.log(` ${chalk.green('✓')} Created link branding for ${chalk.cyan(host)}`);
|
|
72
|
+
} else {
|
|
73
|
+
console.log(` ${chalk.yellow('↻')} Link branding ${chalk.cyan(host)} exists but is not validated — reconciling DNS`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const records = DNS_KEYS
|
|
77
|
+
.map((key) => ({ key, record: branding.dns?.[key] }))
|
|
78
|
+
.filter(({ record }) => record);
|
|
79
|
+
|
|
80
|
+
if (records.length === 0) {
|
|
81
|
+
throw new Error('SendGrid returned no DNS records for the link branding');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// === DNS via Cloudflare (or manual guidance) ===
|
|
85
|
+
// Written UNPROXIED, which is the only state SendGrid can validate: it
|
|
86
|
+
// resolves the host as a plain CNAME to sendgrid.net, and a proxied record
|
|
87
|
+
// answers with Cloudflare's own addresses instead.
|
|
88
|
+
const synced = await syncDnsRecords(cloudflareApi, apexDomain, records);
|
|
89
|
+
|
|
90
|
+
// === Validate — DNS may need a minute; interactive runs wait it out ===
|
|
91
|
+
const isValid = (validation) => validation?.valid === true;
|
|
92
|
+
|
|
93
|
+
let valid = isValid(await api.validateBrandedLink(branding.id));
|
|
94
|
+
|
|
95
|
+
if (!valid && canPrompt(options)) {
|
|
96
|
+
const result = await pollWithSpinner({
|
|
97
|
+
check: async () => {
|
|
98
|
+
try {
|
|
99
|
+
return isValid(await api.validateBrandedLink(branding.id)) ? { done: true } : { done: false };
|
|
100
|
+
} catch {
|
|
101
|
+
return { done: false };
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
intervalMs: 10000,
|
|
105
|
+
message: `Validating link branding (waiting for SendGrid to resolve ${host})`,
|
|
106
|
+
indent: ' ',
|
|
107
|
+
});
|
|
108
|
+
valid = result.success;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!valid) {
|
|
112
|
+
console.log(` ${chalk.yellow('⚠')} Validation pending — DNS is likely still propagating, rerun in a few minutes`);
|
|
113
|
+
return {
|
|
114
|
+
status: 'warned',
|
|
115
|
+
reason: 'link branding validation pending — the emailurl CNAME stays unproxied',
|
|
116
|
+
output: { linkBranding: { id: branding.id, valid: false, ...(synced ? {} : { manualRecords: manualRecordList(records) }) } },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(` ${chalk.green('✓')} Link branding ${chalk.cyan(host)} validated`);
|
|
121
|
+
|
|
122
|
+
// === The flip: a validated branding may ride Cloudflare's proxy ===
|
|
123
|
+
// Edge stays the reconciler — its next live read sees `valid: true` and
|
|
124
|
+
// DESIRES the record proxied, so both services agree on the same state and
|
|
125
|
+
// the rerun is a no-op either way.
|
|
126
|
+
const proxied = await proxyRecord(cloudflareApi, apexDomain, host);
|
|
127
|
+
|
|
128
|
+
if (!proxied) {
|
|
129
|
+
return {
|
|
130
|
+
status: 'warned',
|
|
131
|
+
reason: 'link branding validated but the emailurl CNAME could not be proxied',
|
|
132
|
+
output: { linkBranding: { id: branding.id, valid: true, proxied: false } },
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { output: { linkBranding: { id: branding.id, valid: true, proxied: true } } };
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** The account's branding entry for this host (ids are account-scoped). */
|
|
140
|
+
function findBranding(links, host) {
|
|
141
|
+
return (links || []).find((link) => `${link.subdomain}.${link.domain}`.toLowerCase() === host.toLowerCase()) || null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Patch the branded-link CNAME to proxied.
|
|
146
|
+
*
|
|
147
|
+
* @returns {boolean} - true when the record rides the proxy, false when the
|
|
148
|
+
* manual line was printed instead
|
|
149
|
+
*/
|
|
150
|
+
async function proxyRecord(cloudflareApi, apexDomain, host) {
|
|
151
|
+
if (!cloudflareApi) {
|
|
152
|
+
logManualFlip('No CLOUDFLARE_TOKEN', host);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const zone = await cloudflareApi.getZoneByName(apexDomain);
|
|
157
|
+
if (!zone) {
|
|
158
|
+
logManualFlip(`No Cloudflare zone for ${apexDomain}`, host);
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const existing = await cloudflareApi.makeRequest(`/zones/${zone.id}/dns_records?type=CNAME&per_page=100`, { method: 'GET' });
|
|
163
|
+
const match = (existing.result || []).find((r) => r.type === 'CNAME' && r.name === host);
|
|
164
|
+
|
|
165
|
+
// The DNS sync above writes this record, so reaching here means the zone
|
|
166
|
+
// answered without it (a hand-deleted record, or a read that raced a write).
|
|
167
|
+
if (!match) {
|
|
168
|
+
console.log(` ${chalk.yellow('⚠')} No ${chalk.cyan(`CNAME ${host}`)} record in the zone — add ${chalk.cyan(`${host} → sendgrid.net`)} first, then rerun ${chalk.cyan('omega manage')} and it flips to proxied.`);
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (match.proxied === true) {
|
|
173
|
+
console.log(` ${chalk.dim('→')} ${chalk.cyan(host)} already proxied`);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
await cloudflareApi.makeRequest(`/zones/${zone.id}/dns_records/${match.id}`, {
|
|
178
|
+
method: 'PATCH',
|
|
179
|
+
body: JSON.stringify({ type: 'CNAME', name: host, content: match.content, ttl: 1, proxied: true, comment: match.comment }),
|
|
180
|
+
});
|
|
181
|
+
console.log(` ${chalk.green('✓')} Proxied ${chalk.cyan(host)} — emailed links now land on HTTPS`);
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function logManualFlip(why, host) {
|
|
186
|
+
console.log(` ${chalk.yellow('⚠')} ${why} — turn the proxy ON for ${chalk.cyan(`CNAME ${host}`)} manually (orange cloud), or rerun ${chalk.cyan('omega manage')} once Cloudflare is reachable.`);
|
|
187
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure the brand's SendGrid marketing list exists.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order: marketing.campaigns.providers.sendgrid.listId from
|
|
5
|
+
* config → exact-name lookup → create. The resolved id is written back into
|
|
6
|
+
* omega.json5 (marketing.campaigns.providers.sendgrid.listId, its ONE
|
|
7
|
+
* authoritative home — comment-preserving).
|
|
8
|
+
*/
|
|
9
|
+
const chalk = require('chalk').default;
|
|
10
|
+
const { writeBrandConfig } = require('../../../lib/config-write.js');
|
|
11
|
+
const { dryRunPlan } = require('../../../lib/run-gates.js');
|
|
12
|
+
|
|
13
|
+
module.exports = async function ensureList(context) {
|
|
14
|
+
const { sendgridApi: api, brandConfig, options = {} } = context;
|
|
15
|
+
|
|
16
|
+
const listName = brandConfig.brand.name;
|
|
17
|
+
const configuredId = brandConfig.marketing?.campaigns?.providers?.sendgrid?.listId;
|
|
18
|
+
|
|
19
|
+
// 1. Configured id — verify it still exists
|
|
20
|
+
if (configuredId) {
|
|
21
|
+
const list = await api.getList(configuredId);
|
|
22
|
+
if (list) {
|
|
23
|
+
console.log(` ${chalk.green('✓')} List ${chalk.cyan(`"${list.name}"`)} ${chalk.dim(`(${list.id})`)}`);
|
|
24
|
+
if (list.id !== configuredId) {
|
|
25
|
+
writeBrandConfig(context, { 'marketing.campaigns.providers.sendgrid.listId': list.id });
|
|
26
|
+
}
|
|
27
|
+
return { state: { listId: list.id, listName: list.name } };
|
|
28
|
+
}
|
|
29
|
+
console.log(` ${chalk.yellow('↻')} Configured listId ${chalk.cyan(configuredId)} no longer exists — falling back to name lookup`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 2. Exact-name lookup
|
|
33
|
+
const existing = await api.getListByName(listName);
|
|
34
|
+
if (existing) {
|
|
35
|
+
console.log(` ${chalk.green('✓')} Matched list ${chalk.cyan(`"${listName}"`)} ${chalk.dim(`(${existing.id})`)}`);
|
|
36
|
+
writeBrandConfig(context, { 'marketing.campaigns.providers.sendgrid.listId': existing.id });
|
|
37
|
+
return { state: { listId: existing.id, listName } };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 3. Create
|
|
41
|
+
if (options.dryRun) {
|
|
42
|
+
return dryRunPlan(`create list "${listName}"`, { output: { list: { planned: 'create', listName } } });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const created = await api.createList(listName);
|
|
46
|
+
console.log(` ${chalk.green('✓')} Created list ${chalk.cyan(`"${listName}"`)} ${chalk.dim(`(${created.id})`)}`);
|
|
47
|
+
writeBrandConfig(context, { 'marketing.campaigns.providers.sendgrid.listId': created.id });
|
|
48
|
+
|
|
49
|
+
return { state: { listId: created.id, listName } };
|
|
50
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure @omega.js/backend's segments exist in SendGrid with the right query_dsl.
|
|
3
|
+
*
|
|
4
|
+
* The segment list comes from @omega.js/backend's marketing SSOT; each one's
|
|
5
|
+
* query_dsl is rebuilt from its conditions and compared against the live
|
|
6
|
+
* segment (the list endpoint omits query_dsl, so present segments cost one
|
|
7
|
+
* detail read each). Stale segments are PATCHed in place, falling back to
|
|
8
|
+
* delete + recreate when SendGrid rejects the PATCH. Orphaned `__temp_`
|
|
9
|
+
* segments (leaked by @omega.js/backend's brand-scoped campaign sends on crash) are swept.
|
|
10
|
+
* Segments @omega.js/backend doesn't own are never touched.
|
|
11
|
+
*/
|
|
12
|
+
const chalk = require('chalk').default;
|
|
13
|
+
const { segmentsFor } = require('../../../lib/backend-marketing.js');
|
|
14
|
+
const { buildQueryDsl } = require('../lib/segment-query.js');
|
|
15
|
+
const { dryRunPlan } = require('../../../lib/run-gates.js');
|
|
16
|
+
|
|
17
|
+
const SENDGRID_SEGMENTS = segmentsFor('sendgrid');
|
|
18
|
+
|
|
19
|
+
module.exports = async function ensureSegments(context) {
|
|
20
|
+
const { sendgridApi: api, options = {} } = context;
|
|
21
|
+
|
|
22
|
+
const existing = await api.getSegments();
|
|
23
|
+
|
|
24
|
+
const orphans = existing.filter((s) => s.name.startsWith('__temp_'));
|
|
25
|
+
const existingByName = Object.fromEntries(
|
|
26
|
+
existing.filter((s) => !s.name.startsWith('__temp_')).map((s) => [s.name, s]),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Rebuild every segment's query and diff against the live detail
|
|
30
|
+
const missing = [];
|
|
31
|
+
const stale = [];
|
|
32
|
+
|
|
33
|
+
for (const segment of SENDGRID_SEGMENTS) {
|
|
34
|
+
const queryDsl = buildQueryDsl(segment.conditions, segment.logic);
|
|
35
|
+
const sgSegment = existingByName[segment.name];
|
|
36
|
+
|
|
37
|
+
if (!sgSegment) {
|
|
38
|
+
missing.push({ ...segment, queryDsl });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const full = await api.getSegment(sgSegment.id);
|
|
43
|
+
if (full.query_dsl !== queryDsl) {
|
|
44
|
+
stale.push({ ...segment, queryDsl, existingId: sgSegment.id });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (orphans.length === 0 && missing.length === 0 && stale.length === 0) {
|
|
49
|
+
console.log(` ${chalk.green('✓')} All ${SENDGRID_SEGMENTS.length} segments exist`);
|
|
50
|
+
return { output: { segments: { total: SENDGRID_SEGMENTS.length, created: 0, updated: 0, orphansSwept: 0 } } };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (options.dryRun) {
|
|
54
|
+
const planned = {
|
|
55
|
+
create: missing.map((s) => s.name),
|
|
56
|
+
update: stale.map((s) => s.name),
|
|
57
|
+
sweepOrphans: orphans.length,
|
|
58
|
+
};
|
|
59
|
+
return dryRunPlan(`create ${planned.create.length}, update ${planned.update.length}, sweep ${planned.sweepOrphans} orphan(s)`, { output: { segments: { planned } } });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Sweep leaked __temp_ segments
|
|
63
|
+
for (const orphan of orphans) {
|
|
64
|
+
await api.deleteSegment(orphan.id);
|
|
65
|
+
}
|
|
66
|
+
if (orphans.length > 0) {
|
|
67
|
+
console.log(` ${chalk.yellow('↻')} Swept ${orphans.length} orphaned __temp_ segment(s)`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Stale: PATCH in place, fall back to delete + recreate
|
|
71
|
+
let updated = 0;
|
|
72
|
+
for (const segment of stale) {
|
|
73
|
+
try {
|
|
74
|
+
await api.updateSegment(segment.existingId, segment.name, segment.queryDsl);
|
|
75
|
+
console.log(` ${chalk.green('↻')} Updated ${chalk.cyan(segment.display)} in place`);
|
|
76
|
+
updated += 1;
|
|
77
|
+
} catch (patchError) {
|
|
78
|
+
await api.deleteSegment(segment.existingId);
|
|
79
|
+
missing.push(segment);
|
|
80
|
+
console.log(` ${chalk.yellow('↻')} Recreating ${chalk.cyan(segment.display)} ${chalk.dim(`(PATCH rejected: ${patchError.message})`)}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const segment of missing) {
|
|
85
|
+
const created = await api.createSegment(segment.name, segment.queryDsl);
|
|
86
|
+
console.log(` ${chalk.green('✓')} Created ${chalk.cyan(segment.display)} ${chalk.dim(`(${created.id})`)}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Verify everything actually persisted
|
|
90
|
+
const verify = await api.getSegments();
|
|
91
|
+
const verifyByName = new Set(verify.map((s) => s.name));
|
|
92
|
+
const stillMissing = SENDGRID_SEGMENTS.filter((s) => !verifyByName.has(s.name));
|
|
93
|
+
|
|
94
|
+
if (stillMissing.length > 0) {
|
|
95
|
+
console.log(` ${chalk.yellow('⚠')} ${stillMissing.length} segment(s) failed to persist: ${chalk.cyan(stillMissing.map((s) => s.display).join(', '))}`);
|
|
96
|
+
return { status: 'warned', reason: `${stillMissing.length} segment(s) failed to persist: ${stillMissing.map((s) => s.name).join(', ')}`, output: { segments: { total: SENDGRID_SEGMENTS.length, failed: stillMissing.map((s) => s.name) } } };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
output: {
|
|
101
|
+
segments: {
|
|
102
|
+
total: SENDGRID_SEGMENTS.length,
|
|
103
|
+
created: missing.length,
|
|
104
|
+
updated,
|
|
105
|
+
orphansSwept: orphans.length,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure a verified SendGrid sender identity for the brand.
|
|
3
|
+
*
|
|
4
|
+
* Single Sends require a registered sender (not an inline from address);
|
|
5
|
+
* each brand gets one: offers@{contact-email domain} with the brand's name.
|
|
6
|
+
* It auto-verifies because domain-auth runs first. @omega.js/backend resolves the sender
|
|
7
|
+
* id at runtime by from_email, so nothing lands in state.
|
|
8
|
+
*
|
|
9
|
+
* CAN-SPAM requires a physical mailing address — brand.address in
|
|
10
|
+
* omega.json5. omega-manager silently stamped the company's address on
|
|
11
|
+
* every brand; the port ASKS for the brand's own (#635) and warns only when
|
|
12
|
+
* nobody can be asked.
|
|
13
|
+
*/
|
|
14
|
+
const chalk = require('chalk').default;
|
|
15
|
+
const { input } = require('../../../vendor/devkit/prompt.js');
|
|
16
|
+
const { canPrompt, dryRunPlan } = require('../../../lib/run-gates.js');
|
|
17
|
+
const { confirmSetup, landValue } = require('../../../lib/config-flow.js');
|
|
18
|
+
|
|
19
|
+
// The five fields brand.address carries. Only the three SendGrid cannot
|
|
20
|
+
// create a sender without are required — an address with no region or postal
|
|
21
|
+
// code is a real address in plenty of countries.
|
|
22
|
+
const ADDRESS_FIELDS = [
|
|
23
|
+
{ key: 'line1', message: 'Street address:', required: true },
|
|
24
|
+
{ key: 'city', message: 'City:', required: true },
|
|
25
|
+
{ key: 'region', message: 'State / region:' },
|
|
26
|
+
{ key: 'postalCode', message: 'Postal code:' },
|
|
27
|
+
{ key: 'country', message: 'Country:', required: true },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Ask for the brand's mailing address and land it as ONE brand.address object
|
|
32
|
+
* (#635). Five fields behind ONE gate — asking the Provide / Skip / Disable
|
|
33
|
+
* question per field would be the same question five times. The campaigns
|
|
34
|
+
* service already gated on its API key, so this gate is about the address.
|
|
35
|
+
*
|
|
36
|
+
* @param {Object} context - Handler context.
|
|
37
|
+
* @returns {Promise<Object|null>} The landed address, or null when the user
|
|
38
|
+
* stepped aside or left a required field empty.
|
|
39
|
+
*/
|
|
40
|
+
async function askBrandAddress(context) {
|
|
41
|
+
const action = await confirmSetup(context, {
|
|
42
|
+
label: 'Mailing address (CAN-SPAM)',
|
|
43
|
+
instructions: ['Every marketing email must carry a physical mailing address — it lands in brand.address'],
|
|
44
|
+
disablePath: 'marketing.campaigns.enabled',
|
|
45
|
+
});
|
|
46
|
+
if (action !== 'yes') {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const address = {};
|
|
51
|
+
for (const field of ADDRESS_FIELDS) {
|
|
52
|
+
const value = (await input({ message: field.message }) || '').trim();
|
|
53
|
+
if (!value) {
|
|
54
|
+
if (field.required) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
address[field.key] = value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
landValue(context, 'brand.address', address);
|
|
63
|
+
return address;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = async function ensureSenderIdentity(context) {
|
|
67
|
+
const { sendgridApi: api, brandConfig, options = {} } = context;
|
|
68
|
+
|
|
69
|
+
const brandName = brandConfig.brand.name;
|
|
70
|
+
const contactEmail = brandConfig.brand?.contact?.email;
|
|
71
|
+
const contactDomain = contactEmail?.split('@')[1];
|
|
72
|
+
|
|
73
|
+
if (!contactDomain) {
|
|
74
|
+
console.log(chalk.dim(' ⊘ No brand.contact.email configured — nothing to register'));
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const fromEmail = `offers@${contactDomain}`;
|
|
79
|
+
|
|
80
|
+
const senders = await api.getVerifiedSenders();
|
|
81
|
+
const match = senders.find((s) => s.from_email === fromEmail);
|
|
82
|
+
// The nickname is the managed handle (unique per SendGrid account) — a
|
|
83
|
+
// sender with this brand's nickname but another address is a stale
|
|
84
|
+
// derivation (contact.email changed) and would 400 the create below
|
|
85
|
+
const stale = senders.find((s) => s.nickname === brandName && s.from_email !== fromEmail);
|
|
86
|
+
|
|
87
|
+
if (match && match.verified !== false) {
|
|
88
|
+
console.log(` ${chalk.green('✓')} Verified sender ${chalk.cyan(fromEmail)} ${chalk.dim(`(id: ${match.id})`)}`);
|
|
89
|
+
return { output: { senderIdentity: { id: match.id, fromEmail, verified: true } } };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The physical address is required for creation — and it must be the
|
|
93
|
+
// brand's own (no company default to fall back on), so a run that can ask
|
|
94
|
+
// ASKS for it here rather than warning and stepping aside (#635).
|
|
95
|
+
const complete = (candidate) => Boolean(candidate?.line1 && candidate?.city && candidate?.country);
|
|
96
|
+
|
|
97
|
+
let address = brandConfig.brand?.address;
|
|
98
|
+
if (!complete(address) && canPrompt(options)) {
|
|
99
|
+
address = await askBrandAddress(context);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!complete(address)) {
|
|
103
|
+
console.log(` ${chalk.yellow('⚠')} No sender ${chalk.cyan(fromEmail)} yet, and CAN-SPAM requires a physical mailing address`);
|
|
104
|
+
console.log(` ${chalk.dim('→')} Set brand.address (line1, city, region, postalCode, country) in omega.json5, then rerun`);
|
|
105
|
+
return { status: 'warned', reason: 'no sender yet — brand.address is required by CAN-SPAM', output: { senderIdentity: { fromEmail, missingAddress: true } } };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (options.dryRun) {
|
|
109
|
+
const planned = match ? 'recreate-unverified-sender' : stale ? 'replace-stale-nickname-sender' : 'create-sender';
|
|
110
|
+
return dryRunPlan(`${planned} (${fromEmail})`, { output: { senderIdentity: { planned, fromEmail } } });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// An unverified leftover can't be verified retroactively — recreate it
|
|
114
|
+
// cleanly now that domain authentication is in place
|
|
115
|
+
if (match) {
|
|
116
|
+
await api.deleteVerifiedSender(match.id);
|
|
117
|
+
console.log(` ${chalk.yellow('↻')} Deleted unverified sender ${chalk.cyan(fromEmail)} for recreation`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (stale) {
|
|
121
|
+
if (stale.verified === false) {
|
|
122
|
+
await api.deleteVerifiedSender(stale.id);
|
|
123
|
+
console.log(` ${chalk.yellow('↻')} Deleted stale unverified sender ${chalk.cyan(stale.from_email)} — the contact domain changed`);
|
|
124
|
+
} else {
|
|
125
|
+
console.log(` ${chalk.yellow('⚠')} Verified sender ${chalk.cyan(stale.from_email)} already uses the nickname ${chalk.cyan(brandName)} — delete it in SendGrid or align brand.contact.email, then rerun`);
|
|
126
|
+
return { status: 'warned', reason: 'a verified sender already uses the brand nickname', output: { senderIdentity: { fromEmail, staleNickname: stale.from_email } } };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const sender = await api.createVerifiedSender({
|
|
131
|
+
nickname: brandName,
|
|
132
|
+
fromEmail,
|
|
133
|
+
fromName: brandName,
|
|
134
|
+
replyToEmail: contactEmail,
|
|
135
|
+
replyToName: brandName,
|
|
136
|
+
address: {
|
|
137
|
+
street: address.line1,
|
|
138
|
+
street2: address.line2 || '',
|
|
139
|
+
city: address.city,
|
|
140
|
+
state: address.region,
|
|
141
|
+
zip: address.postalCode,
|
|
142
|
+
country: address.country,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (sender.verified === false) {
|
|
147
|
+
console.log(` ${chalk.yellow('⚠')} Created sender ${chalk.cyan(fromEmail)} but it did not auto-verify — ensure domain authentication is valid, then rerun`);
|
|
148
|
+
return { status: 'warned', reason: 'the created sender did not auto-verify', output: { senderIdentity: { id: sender.id, fromEmail, verified: false } } };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(` ${chalk.green('✓')} Created verified sender ${chalk.cyan(fromEmail)} ${chalk.dim(`(id: ${sender.id})`)}`);
|
|
152
|
+
return { output: { senderIdentity: { id: sender.id, fromEmail, verified: true } } };
|
|
153
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure the SendGrid unsubscribe groups (ASM) @omega.js/backend sends through
|
|
3
|
+
* exist, and land their ids in config.
|
|
4
|
+
*
|
|
5
|
+
* The KEYS come from @omega.js/backend's email SSOT — its send path names a
|
|
6
|
+
* group by key — and the recipient-facing name + description of each live
|
|
7
|
+
* HERE, the provider side. Groups are matched by NAME, never by id: ASM ids
|
|
8
|
+
* are per SendGrid ACCOUNT, so sibling brands sharing one account converge on
|
|
9
|
+
* the same seven groups and land the same ids.
|
|
10
|
+
*
|
|
11
|
+
* Each resolved id is written into omega.json5 at
|
|
12
|
+
* marketing.campaigns.providers.sendgrid.groups.<key>, its ONE authoritative
|
|
13
|
+
* home (comment-preserving) — @omega.js/backend reads it there and fails loudly
|
|
14
|
+
* at send time when one is missing
|
|
15
|
+
* ([#649](https://github.com/Omega-JS-Stack/omega/issues/649)).
|
|
16
|
+
*/
|
|
17
|
+
const chalk = require('chalk').default;
|
|
18
|
+
const { BEM_GROUP_KEYS } = require('../../../lib/backend-marketing.js');
|
|
19
|
+
const { writeBrandConfig } = require('../../../lib/config-write.js');
|
|
20
|
+
const { dryRunPlan } = require('../../../lib/run-gates.js');
|
|
21
|
+
|
|
22
|
+
// The recipient-facing group name + description, one row per @omega.js/backend
|
|
23
|
+
// group key. Brand-neutral on purpose: the NAME is the match key, so two
|
|
24
|
+
// brands on one SendGrid account resolve the same group instead of each
|
|
25
|
+
// creating their own.
|
|
26
|
+
const GROUP_DEFINITIONS = {
|
|
27
|
+
orders: {
|
|
28
|
+
name: 'OMEGA - Order Updates',
|
|
29
|
+
description: 'Receipts, renewals, refunds, and changes to your plan.',
|
|
30
|
+
},
|
|
31
|
+
hello: {
|
|
32
|
+
name: 'OMEGA - Onboarding',
|
|
33
|
+
description: 'Welcome emails and getting-started tips for your first weeks.',
|
|
34
|
+
},
|
|
35
|
+
account: {
|
|
36
|
+
name: 'OMEGA - Account',
|
|
37
|
+
description: 'Account actions you asked for: deletions, data requests, and confirmations.',
|
|
38
|
+
},
|
|
39
|
+
marketing: {
|
|
40
|
+
name: 'OMEGA - Marketing & Promotions',
|
|
41
|
+
description: 'Offers, discounts, product news, and reminders.',
|
|
42
|
+
},
|
|
43
|
+
security: {
|
|
44
|
+
name: 'OMEGA - Security',
|
|
45
|
+
description: 'Password resets, sign-in codes, and security alerts.',
|
|
46
|
+
},
|
|
47
|
+
newsletter: {
|
|
48
|
+
name: 'OMEGA - Newsletter',
|
|
49
|
+
description: 'The regular newsletter: feature announcements and industry news.',
|
|
50
|
+
},
|
|
51
|
+
internal: {
|
|
52
|
+
name: 'OMEGA - Internal Alerts',
|
|
53
|
+
description: 'Operational alerts sent to the team.',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
module.exports = async function ensureUnsubscribeGroups(context) {
|
|
58
|
+
const { sendgridApi: api, options = {} } = context;
|
|
59
|
+
|
|
60
|
+
// A key @omega.js/backend added without a row here would send through a group
|
|
61
|
+
// this service never provisions — loud, at the top, not at send time.
|
|
62
|
+
const undefinedKeys = BEM_GROUP_KEYS.filter((key) => !GROUP_DEFINITIONS[key]);
|
|
63
|
+
if (undefinedKeys.length > 0) {
|
|
64
|
+
throw new Error(`@omega.js/backend group key(s) with no name+description row here: ${undefinedKeys.join(', ')}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const existing = await api.getUnsubscribeGroups();
|
|
68
|
+
const existingByName = Object.fromEntries(existing.map((group) => [group.name, group]));
|
|
69
|
+
|
|
70
|
+
const missing = BEM_GROUP_KEYS.filter((key) => !existingByName[GROUP_DEFINITIONS[key].name]);
|
|
71
|
+
|
|
72
|
+
if (missing.length > 0 && options.dryRun) {
|
|
73
|
+
return dryRunPlan(
|
|
74
|
+
`create ${missing.length} unsubscribe group(s): ${missing.map((key) => GROUP_DEFINITIONS[key].name).join(', ')}`,
|
|
75
|
+
{ output: { unsubscribeGroups: { planned: { create: missing.map((key) => GROUP_DEFINITIONS[key].name) } } } },
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const ids = {};
|
|
80
|
+
|
|
81
|
+
for (const key of BEM_GROUP_KEYS) {
|
|
82
|
+
const { name, description } = GROUP_DEFINITIONS[key];
|
|
83
|
+
const found = existingByName[name];
|
|
84
|
+
|
|
85
|
+
if (found) {
|
|
86
|
+
ids[key] = found.id;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const created = await api.createUnsubscribeGroup(name, description);
|
|
91
|
+
console.log(` ${chalk.green('✓')} Created group ${chalk.cyan(`"${name}"`)} ${chalk.dim(`(${created.id})`)}`);
|
|
92
|
+
ids[key] = created.id;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (missing.length === 0) {
|
|
96
|
+
console.log(` ${chalk.green('✓')} All ${BEM_GROUP_KEYS.length} unsubscribe groups exist`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Already-equal ids are skipped by the editor, so a converged brand leaves
|
|
100
|
+
// omega.json5 byte-identical.
|
|
101
|
+
const written = writeBrandConfig(context, Object.fromEntries(
|
|
102
|
+
BEM_GROUP_KEYS.map((key) => [`marketing.campaigns.providers.sendgrid.groups.${key}`, ids[key]]),
|
|
103
|
+
));
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
output: {
|
|
107
|
+
unsubscribeGroups: {
|
|
108
|
+
total: BEM_GROUP_KEYS.length,
|
|
109
|
+
created: missing.length,
|
|
110
|
+
written: written.length,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
module.exports.GROUP_DEFINITIONS = GROUP_DEFINITIONS;
|