@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,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Campaigns service (SendGrid provider) — the brand's email-marketing infrastructure: domain
|
|
3
|
+
* authentication (DKIM/SPF CNAMEs written via Cloudflare), link branding for
|
|
4
|
+
* the emailurl host (validated, then the CNAME flips proxied), a verified sender
|
|
5
|
+
* for Single Sends, the brand's marketing list, the unsubscribe groups
|
|
6
|
+
* @omega.js/backend sends through, @omega.js/backend's custom fields and
|
|
7
|
+
* segments (from @omega.js/backend's SSOT), and the account-global Event
|
|
8
|
+
* Webhook pointed at the parent @omega.js/backend's forwarder.
|
|
9
|
+
*
|
|
10
|
+
* Runs after cloudflare (the zone must exist for the DKIM records). The
|
|
11
|
+
* whole service reconciles one SendGrid account per brand-or-company — the
|
|
12
|
+
* groups/fields/segments/webhook operations converge to the same result when
|
|
13
|
+
* sibling brands share the account.
|
|
14
|
+
*
|
|
15
|
+
* Auth: SENDGRID_API_KEY in the brand .env; the event-webhook operation
|
|
16
|
+
* additionally needs OMEGA_WEBHOOK_KEY, which the setup MINTS through the
|
|
17
|
+
* shared contract (it is OMEGA's own key, #635). No API key → clean skip.
|
|
18
|
+
*/
|
|
19
|
+
const { chosenProvider } = require('../../vendor/config/index.js');
|
|
20
|
+
|
|
21
|
+
const { serviceInputSpec } = require('../../config.js');
|
|
22
|
+
const { createServiceRunner } = require('../../lib/service-runner.js');
|
|
23
|
+
const { requestServiceInput } = require('../../lib/service-input.js');
|
|
24
|
+
const { CloudflareAPI } = require('../edge/lib/cloudflare-api.js');
|
|
25
|
+
const { getApexDomain } = require('../../lib/domain-utils.js');
|
|
26
|
+
const { SendGridAPI } = require('./lib/sendgrid-api.js');
|
|
27
|
+
|
|
28
|
+
module.exports.run = createServiceRunner({
|
|
29
|
+
serviceDir: __dirname,
|
|
30
|
+
setup: async (context) => {
|
|
31
|
+
const campaigns = context.brandConfig.marketing?.campaigns || {};
|
|
32
|
+
|
|
33
|
+
if (campaigns.enabled === false) {
|
|
34
|
+
return { skip: true, reason: 'marketing.campaigns.enabled = false' };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The vendor is a KEY under marketing.campaigns.providers (#425) — no
|
|
38
|
+
// entry means none chosen and this service skips.
|
|
39
|
+
const provider = chosenProvider(campaigns.providers);
|
|
40
|
+
if (!provider) {
|
|
41
|
+
return { skip: true, reason: 'no marketing.campaigns.providers entry' };
|
|
42
|
+
}
|
|
43
|
+
if (provider !== 'sendgrid') {
|
|
44
|
+
return { skip: true, reason: `marketing.campaigns.providers.${provider} is not a known email-marketing provider` };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const domain = (context.brandConfig.brand?.url || '').replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
48
|
+
if (!domain) {
|
|
49
|
+
return { skip: true, reason: 'no brand.url configured' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!context.sendgridApi) {
|
|
53
|
+
const gate = await requestServiceInput(context, serviceInputSpec('campaigns', { names: ['SENDGRID_API_KEY'] }));
|
|
54
|
+
if (gate) return gate;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The webhook key is OMEGA's OWN (#635): the shared helper mints it in
|
|
58
|
+
// place when this service runs before the workspace one did, so the
|
|
59
|
+
// webhook operation can read it unconditionally.
|
|
60
|
+
const webhookKeyGate = await requestServiceInput(context, serviceInputSpec('campaigns', { names: ['OMEGA_WEBHOOK_KEY'] }));
|
|
61
|
+
if (webhookKeyGate) return webhookKeyGate;
|
|
62
|
+
|
|
63
|
+
// Tests inject fake clients via context.sendgridApi / context.cloudflareApi.
|
|
64
|
+
// Cloudflare is only needed while the domain still has to be DNS-verified
|
|
65
|
+
// (and for the link-branding proxy flip); without a token the domain-auth
|
|
66
|
+
// operation prints the records to add manually and link-branding prints the
|
|
67
|
+
// manual flip.
|
|
68
|
+
return {
|
|
69
|
+
sendgridApi: context.sendgridApi || new SendGridAPI(),
|
|
70
|
+
cloudflareApi: context.cloudflareApi
|
|
71
|
+
|| (process.env.CLOUDFLARE_TOKEN ? new CloudflareAPI() : null),
|
|
72
|
+
domain,
|
|
73
|
+
apexDomain: getApexDomain(domain),
|
|
74
|
+
// A subdomain project's apex records belong to the PARENT brand's walk
|
|
75
|
+
// (the same line the edge service draws) — link-branding skips on it.
|
|
76
|
+
isSubdomainProject: domain !== getApexDomain(domain),
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff-sync a SendGrid record set into the brand's Cloudflare apex zone — the
|
|
3
|
+
* ONE writer both whitelabel operations share (domain authentication's DKIM
|
|
4
|
+
* CNAMEs, link branding's two link CNAMEs).
|
|
5
|
+
*
|
|
6
|
+
* Exact match = untouched, wrong content = patched, missing = created. Every
|
|
7
|
+
* record lands UNPROXIED: SendGrid validates each of them by resolving the
|
|
8
|
+
* host as a plain CNAME, and a proxied record answers with Cloudflare's own
|
|
9
|
+
* addresses instead. No Cloudflare token (or no zone yet) prints the records
|
|
10
|
+
* to add by hand, so a manual fix converges on the rerun.
|
|
11
|
+
*/
|
|
12
|
+
const chalk = require('chalk').default;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {object|null} cloudflareApi - The Cloudflare client (null without a token)
|
|
16
|
+
* @param {string} apexDomain - The zone the records belong to
|
|
17
|
+
* @param {Array<{ key: string, record: object }>} records - SendGrid's own
|
|
18
|
+
* record set, each entry keyed by the name SendGrid gives it
|
|
19
|
+
* @returns {Promise<boolean>} - true when records were written via Cloudflare,
|
|
20
|
+
* false when they were printed for manual addition
|
|
21
|
+
*/
|
|
22
|
+
async function syncDnsRecords(cloudflareApi, apexDomain, records) {
|
|
23
|
+
if (!cloudflareApi) {
|
|
24
|
+
console.log(` ${chalk.yellow('⚠')} No CLOUDFLARE_TOKEN — add these records manually, then rerun:`);
|
|
25
|
+
logManualRecords(records);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const zone = await cloudflareApi.getZoneByName(apexDomain);
|
|
30
|
+
if (!zone) {
|
|
31
|
+
console.log(` ${chalk.yellow('⚠')} No Cloudflare zone for ${chalk.cyan(apexDomain)} — add these records manually (or rerun once the cloudflare service creates the zone):`);
|
|
32
|
+
logManualRecords(records);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const existing = await cloudflareApi.makeRequest(`/zones/${zone.id}/dns_records?type=CNAME&per_page=100`, { method: 'GET' });
|
|
37
|
+
const existingRecords = existing.result || [];
|
|
38
|
+
|
|
39
|
+
for (const { key, record } of records) {
|
|
40
|
+
const type = record.type.toUpperCase();
|
|
41
|
+
const name = record.host;
|
|
42
|
+
const content = record.data;
|
|
43
|
+
|
|
44
|
+
const match = existingRecords.find((r) => r.type === type && r.name === name);
|
|
45
|
+
|
|
46
|
+
if (match && match.content === content) {
|
|
47
|
+
console.log(` ${chalk.dim('→')} ${chalk.dim(key)}: ${chalk.cyan(name)} already in place`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (match) {
|
|
52
|
+
await cloudflareApi.makeRequest(`/zones/${zone.id}/dns_records/${match.id}`, {
|
|
53
|
+
method: 'PATCH',
|
|
54
|
+
body: JSON.stringify({ type, name, content, ttl: 1, proxied: false }),
|
|
55
|
+
});
|
|
56
|
+
console.log(` ${chalk.yellow('↻')} ${chalk.dim(key)}: updated ${chalk.cyan(name)}`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await cloudflareApi.makeRequest(`/zones/${zone.id}/dns_records`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
body: JSON.stringify({ type, name, content, ttl: 1, proxied: false, comment: `SendGrid ${key}` }),
|
|
63
|
+
});
|
|
64
|
+
console.log(` ${chalk.green('✓')} ${chalk.dim(key)}: created ${chalk.cyan(name)}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function logManualRecords(records) {
|
|
71
|
+
for (const { record } of records) {
|
|
72
|
+
console.log(` ${chalk.cyan(record.type.toUpperCase())} ${record.host} → ${record.data}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The manualRecords shape a warned return carries when nothing was written. */
|
|
77
|
+
function manualRecordList(records) {
|
|
78
|
+
return records.map(({ record }) => ({ type: record.type.toUpperCase(), name: record.host, content: record.data }));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { syncDnsRecords, manualRecordList };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @omega.js/backend segment conditions → SendGrid query_dsl (the SQL-like segment query
|
|
3
|
+
* language). Pure functions, ported behavior-for-behavior from omega-manager
|
|
4
|
+
* — the generated SQL is compared against live segments for staleness, so
|
|
5
|
+
* any change here re-syncs every segment on the next run.
|
|
6
|
+
*/
|
|
7
|
+
const { BACKEND_FIELDS_MAP } = require('../../../lib/backend-marketing.js');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Convert a @omega.js/backend field condition to a WHERE clause fragment. Custom fields
|
|
11
|
+
* are referenced by name (double-quoted) in query_dsl, not by internal ID.
|
|
12
|
+
*/
|
|
13
|
+
function fieldConditionToWhere(condition) {
|
|
14
|
+
const fieldDef = BACKEND_FIELDS_MAP[condition.field];
|
|
15
|
+
if (!fieldDef) {
|
|
16
|
+
throw new Error(`Unknown field "${condition.field}" in segment condition`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const col = `"${condition.field}"`;
|
|
20
|
+
|
|
21
|
+
switch (condition.op) {
|
|
22
|
+
case '==':
|
|
23
|
+
return `${col} = '${condition.value}'`;
|
|
24
|
+
case '!=':
|
|
25
|
+
return `${col} != '${condition.value}'`;
|
|
26
|
+
case 'within': {
|
|
27
|
+
const days = parseInt(condition.value, 10);
|
|
28
|
+
return `${col} > timestampadd(day, -${days}, CURRENT_TIMESTAMP)`;
|
|
29
|
+
}
|
|
30
|
+
case 'not_within': {
|
|
31
|
+
const days = parseInt(condition.value, 10);
|
|
32
|
+
return `${col} <= timestampadd(day, -${days}, CURRENT_TIMESTAMP)`;
|
|
33
|
+
}
|
|
34
|
+
default:
|
|
35
|
+
throw new Error(`Unsupported field condition op "${condition.op}"`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Contact conditions use built-in contact_data columns (email, first_name, …). */
|
|
40
|
+
function contactConditionToWhere(condition) {
|
|
41
|
+
switch (condition.op) {
|
|
42
|
+
case 'email_is':
|
|
43
|
+
return `email = '${condition.value}'`;
|
|
44
|
+
case 'email_like':
|
|
45
|
+
return `email LIKE '${condition.value}'`;
|
|
46
|
+
default:
|
|
47
|
+
throw new Error(`Unsupported contact condition op "${condition.op}"`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function conditionToWhere(condition) {
|
|
52
|
+
if (condition.type === 'engagement') {
|
|
53
|
+
return null; // Handled separately in buildQueryDsl
|
|
54
|
+
}
|
|
55
|
+
if (condition.type === 'contact') {
|
|
56
|
+
return contactConditionToWhere(condition);
|
|
57
|
+
}
|
|
58
|
+
return fieldConditionToWhere(condition);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build a complete query_dsl from @omega.js/backend segment conditions. Engagement
|
|
63
|
+
* conditions need event_data (JOIN or subqueries); field/contact conditions
|
|
64
|
+
* query contact_data directly.
|
|
65
|
+
*/
|
|
66
|
+
function buildQueryDsl(conditions, logic) {
|
|
67
|
+
const engagementConditions = conditions.filter((c) => c.type === 'engagement');
|
|
68
|
+
const nonEngagementConditions = conditions.filter((c) => c.type !== 'engagement');
|
|
69
|
+
const joiner = logic === 'or' ? ' OR ' : ' AND ';
|
|
70
|
+
|
|
71
|
+
const fieldWhereParts = nonEngagementConditions.map((c) => conditionToWhere(c)).filter(Boolean);
|
|
72
|
+
|
|
73
|
+
// No engagement — simple contact_data query
|
|
74
|
+
if (engagementConditions.length === 0) {
|
|
75
|
+
const whereClause = fieldWhereParts.length > 0
|
|
76
|
+
? ` WHERE ${fieldWhereParts.join(joiner)}`
|
|
77
|
+
: '';
|
|
78
|
+
return `SELECT contact_id, updated_at FROM contact_data${whereClause}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Positive engagement (opened_or_clicked) uses a JOIN
|
|
82
|
+
const positiveEngagement = engagementConditions.find((c) => c.op === 'opened_or_clicked');
|
|
83
|
+
if (positiveEngagement) {
|
|
84
|
+
const days = parseInt(positiveEngagement.value, 10);
|
|
85
|
+
const threshold = `timestampadd(day, -${days}, CURRENT_TIMESTAMP)`;
|
|
86
|
+
const engWhere = `(e.event_type = 'open' OR e.event_type = 'click') AND e.timestamp > ${threshold}`;
|
|
87
|
+
const allWhere = [...fieldWhereParts, engWhere];
|
|
88
|
+
return `SELECT c.contact_id, c.updated_at FROM contact_data c`
|
|
89
|
+
+ ` JOIN event_data e ON c.contact_id = e.contact_id`
|
|
90
|
+
+ ` WHERE ${allWhere.join(joiner)}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Negative/count engagement conditions use subqueries (combinable)
|
|
94
|
+
const engagementWhereParts = engagementConditions.map((engagement) => {
|
|
95
|
+
switch (engagement.op) {
|
|
96
|
+
case 'not_opened': {
|
|
97
|
+
const days = parseInt(engagement.value, 10);
|
|
98
|
+
const threshold = `timestampadd(day, -${days}, CURRENT_TIMESTAMP)`;
|
|
99
|
+
return `contact_id NOT IN (`
|
|
100
|
+
+ `SELECT contact_id FROM event_data WHERE event_type = 'open' AND timestamp > ${threshold}`
|
|
101
|
+
+ `)`;
|
|
102
|
+
}
|
|
103
|
+
case 'not_opened_or_clicked': {
|
|
104
|
+
const days = parseInt(engagement.value, 10);
|
|
105
|
+
const threshold = `timestampadd(day, -${days}, CURRENT_TIMESTAMP)`;
|
|
106
|
+
return `contact_id NOT IN (`
|
|
107
|
+
+ `SELECT contact_id FROM event_data WHERE (event_type = 'open' OR event_type = 'click') AND timestamp > ${threshold}`
|
|
108
|
+
+ `)`;
|
|
109
|
+
}
|
|
110
|
+
case 'received_gte': {
|
|
111
|
+
// SendGrid has no GROUP BY / HAVING — approximate >=N delivered events
|
|
112
|
+
// by requiring one in each of N distinct time windows. The window size
|
|
113
|
+
// comes from the sibling time-based engagement condition's period.
|
|
114
|
+
const minCount = parseInt(engagement.value, 10);
|
|
115
|
+
const timeEng = engagementConditions.find((c) => c.op !== 'received_gte');
|
|
116
|
+
const totalDays = timeEng ? parseInt(timeEng.value, 10) : 180;
|
|
117
|
+
const windowSize = Math.floor(totalDays / minCount);
|
|
118
|
+
|
|
119
|
+
return '(' + Array.from({ length: minCount }, (_, i) => {
|
|
120
|
+
const start = i * windowSize;
|
|
121
|
+
const end = (i + 1) * windowSize;
|
|
122
|
+
return `contact_id IN (`
|
|
123
|
+
+ `SELECT contact_id FROM event_data WHERE event_type = 'delivered'`
|
|
124
|
+
+ ` AND timestamp > timestampadd(day, -${end}, CURRENT_TIMESTAMP)`
|
|
125
|
+
+ ` AND timestamp <= timestampadd(day, -${start}, CURRENT_TIMESTAMP)`
|
|
126
|
+
+ `)`;
|
|
127
|
+
}).join(' AND ') + ')';
|
|
128
|
+
}
|
|
129
|
+
default:
|
|
130
|
+
throw new Error(`Unsupported engagement op "${engagement.op}"`);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const allWhere = [...fieldWhereParts, ...engagementWhereParts];
|
|
135
|
+
return `SELECT contact_id, updated_at FROM contact_data WHERE ${allWhere.join(joiner)}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { buildQueryDsl };
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SendGrid v3 API client — marketing (lists, custom fields, segments),
|
|
3
|
+
* unsubscribe groups, verified senders, domain authentication, and the
|
|
4
|
+
* account-global Event Webhook. Named-method surface so tests can fake it
|
|
5
|
+
* method-for-method.
|
|
6
|
+
*
|
|
7
|
+
* Auth: SENDGRID_API_KEY in the brand .env.
|
|
8
|
+
*/
|
|
9
|
+
const SENDGRID_API_BASE = 'https://api.sendgrid.com/v3';
|
|
10
|
+
|
|
11
|
+
class SendGridAPI {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.apiKey = options.apiKey || process.env.SENDGRID_API_KEY;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async makeRequest(endpoint, options = {}) {
|
|
17
|
+
const response = await fetch(`${SENDGRID_API_BASE}${endpoint}`, {
|
|
18
|
+
...options,
|
|
19
|
+
headers: {
|
|
20
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
...options.headers,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// 204 No Content / 202 Accepted come back bodyless
|
|
27
|
+
if (response.status === 204 || response.status === 202) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const text = await response.text();
|
|
32
|
+
if (!text) {
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new Error(`SendGrid API error (${response.status}): ${response.statusText}`);
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const data = JSON.parse(text);
|
|
40
|
+
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
const message = data.errors?.map((e) => e.message).join(', ')
|
|
43
|
+
|| data.message
|
|
44
|
+
|| data.error
|
|
45
|
+
|| response.statusText;
|
|
46
|
+
throw new Error(`SendGrid API error (${response.status}): ${message}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ========== Marketing lists ==========
|
|
53
|
+
|
|
54
|
+
/** All marketing lists (paginated). */
|
|
55
|
+
async getLists() {
|
|
56
|
+
const allLists = [];
|
|
57
|
+
let pageToken = null;
|
|
58
|
+
|
|
59
|
+
do {
|
|
60
|
+
const params = new URLSearchParams({ page_size: '100' });
|
|
61
|
+
if (pageToken) {
|
|
62
|
+
params.set('page_token', pageToken);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const data = await this.makeRequest(`/marketing/lists?${params}`);
|
|
66
|
+
allLists.push(...(data.result || []));
|
|
67
|
+
pageToken = data._metadata?.next;
|
|
68
|
+
} while (pageToken);
|
|
69
|
+
|
|
70
|
+
return allLists;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async getListByName(name) {
|
|
74
|
+
const lists = await this.getLists();
|
|
75
|
+
return lists.find((list) => list.name === name);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async getList(listId) {
|
|
79
|
+
try {
|
|
80
|
+
return await this.makeRequest(`/marketing/lists/${listId}`);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error.message.includes('404')) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async createList(name) {
|
|
90
|
+
return this.makeRequest('/marketing/lists', {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
body: JSON.stringify({ name }),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ========== Custom fields ==========
|
|
97
|
+
|
|
98
|
+
async getCustomFields() {
|
|
99
|
+
const data = await this.makeRequest('/marketing/field_definitions');
|
|
100
|
+
return data.custom_fields || [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** fieldType: 'Text' | 'Number' | 'Date' */
|
|
104
|
+
async createCustomField(name, fieldType) {
|
|
105
|
+
return this.makeRequest('/marketing/field_definitions', {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
body: JSON.stringify({ name, field_type: fieldType }),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async deleteCustomField(fieldId) {
|
|
112
|
+
await this.makeRequest(`/marketing/field_definitions/${fieldId}`, {
|
|
113
|
+
method: 'DELETE',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ========== Segments (v2 API) ==========
|
|
118
|
+
|
|
119
|
+
/** All segments (paginated). The list endpoint omits query_dsl — use getSegment for detail. */
|
|
120
|
+
async getSegments() {
|
|
121
|
+
const allSegments = [];
|
|
122
|
+
let pageToken = null;
|
|
123
|
+
|
|
124
|
+
do {
|
|
125
|
+
const params = new URLSearchParams({ page_size: '50' });
|
|
126
|
+
if (pageToken) {
|
|
127
|
+
params.set('page_token', pageToken);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const data = await this.makeRequest(`/marketing/segments/2.0?${params}`);
|
|
131
|
+
allSegments.push(...(data.results || []));
|
|
132
|
+
pageToken = data._metadata?.next;
|
|
133
|
+
} while (pageToken);
|
|
134
|
+
|
|
135
|
+
return allSegments;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async getSegment(segmentId) {
|
|
139
|
+
return this.makeRequest(`/marketing/segments/2.0/${segmentId}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async createSegment(name, queryDsl) {
|
|
143
|
+
return this.makeRequest('/marketing/segments/2.0', {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
body: JSON.stringify({ name, query_dsl: queryDsl }),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async updateSegment(segmentId, name, queryDsl) {
|
|
150
|
+
return this.makeRequest(`/marketing/segments/2.0/${segmentId}`, {
|
|
151
|
+
method: 'PATCH',
|
|
152
|
+
body: JSON.stringify({ name, query_dsl: queryDsl }),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async deleteSegment(segmentId) {
|
|
157
|
+
await this.makeRequest(`/marketing/segments/2.0/${segmentId}`, {
|
|
158
|
+
method: 'DELETE',
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ========== Unsubscribe groups (ASM) ==========
|
|
163
|
+
|
|
164
|
+
/** Every unsubscribe group on the ACCOUNT (ids are account-scoped, not brand-scoped). */
|
|
165
|
+
async getUnsubscribeGroups() {
|
|
166
|
+
return this.makeRequest('/asm/groups');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** description is recipient-facing — it renders on the preferences page. */
|
|
170
|
+
async createUnsubscribeGroup(name, description) {
|
|
171
|
+
return this.makeRequest('/asm/groups', {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
body: JSON.stringify({ name, description, is_default: false }),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ========== Verified senders ==========
|
|
178
|
+
|
|
179
|
+
async getVerifiedSenders() {
|
|
180
|
+
const data = await this.makeRequest('/verified_senders');
|
|
181
|
+
return data?.results || [];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Create a verified sender. Auto-verifies when domain authentication is
|
|
186
|
+
* valid (the domain-auth operation runs first for exactly that reason).
|
|
187
|
+
*
|
|
188
|
+
* @param {Object} sender - { nickname, fromEmail, fromName, replyToEmail, replyToName, address }
|
|
189
|
+
* address: { street, street2, city, state, zip, country } (CAN-SPAM requires it)
|
|
190
|
+
*/
|
|
191
|
+
async createVerifiedSender({ nickname, fromEmail, fromName, replyToEmail, replyToName, address }) {
|
|
192
|
+
return this.makeRequest('/verified_senders', {
|
|
193
|
+
method: 'POST',
|
|
194
|
+
body: JSON.stringify({
|
|
195
|
+
nickname,
|
|
196
|
+
from_email: fromEmail,
|
|
197
|
+
from_name: fromName,
|
|
198
|
+
reply_to: replyToEmail,
|
|
199
|
+
reply_to_name: replyToName,
|
|
200
|
+
address: address.street,
|
|
201
|
+
address2: address.street2 || '',
|
|
202
|
+
city: address.city,
|
|
203
|
+
state: address.state,
|
|
204
|
+
zip: address.zip,
|
|
205
|
+
country: address.country,
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async deleteVerifiedSender(senderId) {
|
|
211
|
+
await this.makeRequest(`/verified_senders/${senderId}`, {
|
|
212
|
+
method: 'DELETE',
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ========== Domain authentication ==========
|
|
217
|
+
|
|
218
|
+
async getAuthenticatedDomains() {
|
|
219
|
+
return this.makeRequest('/whitelabel/domains');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Creates the DKIM/SPF record set (automatic_security → 3 CNAMEs). */
|
|
223
|
+
async authenticateDomain(domain, subdomain = 'emailauth') {
|
|
224
|
+
return this.makeRequest('/whitelabel/domains', {
|
|
225
|
+
method: 'POST',
|
|
226
|
+
body: JSON.stringify({
|
|
227
|
+
domain,
|
|
228
|
+
subdomain,
|
|
229
|
+
automatic_security: true,
|
|
230
|
+
custom_spf: false,
|
|
231
|
+
}),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ========== Link branding ==========
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Every branded LINK host on the account — the `emailurl.<domain>` CNAME
|
|
239
|
+
* SendGrid rewrites transactional links through. Each entry carries its own
|
|
240
|
+
* `valid`, which is SendGrid's word on whether it has resolved the record
|
|
241
|
+
* (`{ domain, subdomain, valid, dns: { domain_cname, owner_cname } }`).
|
|
242
|
+
*/
|
|
243
|
+
async getBrandedLinks() {
|
|
244
|
+
return this.makeRequest('/whitelabel/links');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Creates the branded link record set (two CNAMEs under `<subdomain>.`). */
|
|
248
|
+
async createBrandedLink(domain, subdomain = 'emailurl') {
|
|
249
|
+
return this.makeRequest('/whitelabel/links', {
|
|
250
|
+
method: 'POST',
|
|
251
|
+
body: JSON.stringify({
|
|
252
|
+
domain,
|
|
253
|
+
subdomain,
|
|
254
|
+
default: false,
|
|
255
|
+
}),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Asks SendGrid to check the DNS records for a branded link. */
|
|
260
|
+
async validateBrandedLink(linkId) {
|
|
261
|
+
return this.makeRequest(`/whitelabel/links/${linkId}/validate`, {
|
|
262
|
+
method: 'POST',
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Asks SendGrid to check the DNS records for a domain authentication. */
|
|
267
|
+
async validateDomain(domainId) {
|
|
268
|
+
return this.makeRequest(`/whitelabel/domains/${domainId}/validate`, {
|
|
269
|
+
method: 'POST',
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ========== Event Webhook (account-global) ==========
|
|
274
|
+
|
|
275
|
+
async getEventWebhookSettings() {
|
|
276
|
+
return this.makeRequest('/user/webhooks/event/settings');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async updateEventWebhookSettings(settings) {
|
|
280
|
+
return this.makeRequest('/user/webhooks/event/settings', {
|
|
281
|
+
method: 'PATCH',
|
|
282
|
+
body: JSON.stringify(settings),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = { SendGridAPI };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate the brand's own classic reCAPTCHA keys.
|
|
3
|
+
*
|
|
4
|
+
* omega-manager only printed a checkmark outside onboarding; this port
|
|
5
|
+
* actually proves the secret: siteverify with a throwaway token answers
|
|
6
|
+
* invalid-input-response when the secret is valid and invalid-input-secret
|
|
7
|
+
* when it isn't. The key's domain list has no read API (classic reCAPTCHA),
|
|
8
|
+
* so domain membership can't be proven — interactive runs open the console
|
|
9
|
+
* and confirm once, stamping config (a confirm nothing can re-check is the
|
|
10
|
+
* one reconcile flag config keeps, #434 — same pattern as the ga-link
|
|
11
|
+
* confirm); non-interactive runs keep the printed guidance.
|
|
12
|
+
*
|
|
13
|
+
* The probe is a pure read (no assessment is created), so dry-run behaves
|
|
14
|
+
* identically to a normal run.
|
|
15
|
+
*/
|
|
16
|
+
const chalk = require('chalk').default;
|
|
17
|
+
const { openBrowserAndPoll } = require('../../../vendor/devkit/flows.js');
|
|
18
|
+
const { writeBrandConfig } = require('../../../lib/config-write.js');
|
|
19
|
+
const { canPrompt } = require('../../../lib/run-gates.js');
|
|
20
|
+
const { recaptchaConsoleUrl } = require('../lib/console-url.js');
|
|
21
|
+
|
|
22
|
+
const CONFIRMED_PATH = 'captcha.providers.recaptcha.domainsConfirmed';
|
|
23
|
+
|
|
24
|
+
// Throwaway token for the secret-validity probe — never a real assessment
|
|
25
|
+
const PROBE_TOKEN = 'omega-manager-secret-validation-probe';
|
|
26
|
+
|
|
27
|
+
module.exports = async function ensureSiteKey(context) {
|
|
28
|
+
const { recaptchaApi, siteKey, domain, brandConfig, options = {} } = context;
|
|
29
|
+
|
|
30
|
+
const consoleUrl = recaptchaConsoleUrl(brandConfig, siteKey);
|
|
31
|
+
|
|
32
|
+
console.log(` ${chalk.dim('→')} Validating reCAPTCHA secret key...`);
|
|
33
|
+
const verification = await recaptchaApi.verify(PROBE_TOKEN);
|
|
34
|
+
const errorCodes = verification['error-codes'] || [];
|
|
35
|
+
|
|
36
|
+
if (errorCodes.includes('invalid-input-secret')) {
|
|
37
|
+
console.log(` ${chalk.red('✗')} RECAPTCHA_SECRET_KEY in the brand .env is not a valid secret key`);
|
|
38
|
+
console.log(` ${chalk.dim('→')} Key settings: ${chalk.cyan(consoleUrl)}`);
|
|
39
|
+
throw new Error('RECAPTCHA_SECRET_KEY is invalid (siteverify answered invalid-input-secret) — fix it in the brand .env');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The bare domain only — the brand contract dropped the www twin, so
|
|
43
|
+
// recommending it would ask for a listing nothing serves
|
|
44
|
+
const domains = [domain];
|
|
45
|
+
const output = { siteKey: { secretValid: true, domains, consoleUrl } };
|
|
46
|
+
|
|
47
|
+
console.log(` ${chalk.green('✓')} reCAPTCHA secret key is valid`);
|
|
48
|
+
|
|
49
|
+
// The domain list has no read API — a one-time interactive confirm stamps
|
|
50
|
+
// config (#434); a changed brand domain naturally re-prompts. A pre-existing
|
|
51
|
+
// stamp that also carries the www twin still covers the bare domain, so it
|
|
52
|
+
// confirms without churn and the stamp narrows to what's recommended now
|
|
53
|
+
const confirmed = brandConfig.captcha?.providers?.recaptcha?.domainsConfirmed || [];
|
|
54
|
+
if (domains.every((entry) => confirmed.includes(entry))) {
|
|
55
|
+
// Narrow a stamp that carries extras (the retired www twin); an exact
|
|
56
|
+
// match stays disk-free
|
|
57
|
+
if (confirmed.length !== domains.length) {
|
|
58
|
+
writeBrandConfig(context, { [CONFIRMED_PATH]: domains });
|
|
59
|
+
}
|
|
60
|
+
console.log(` ${chalk.green('✓')} Domain list confirmed (${domains.join(' + ')})`);
|
|
61
|
+
return { output };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (canPrompt(options)) {
|
|
65
|
+
const result = await openBrowserAndPoll({
|
|
66
|
+
url: consoleUrl,
|
|
67
|
+
label: 'the reCAPTCHA console',
|
|
68
|
+
promptMessage: `Add ${chalk.cyan(domains.join(' + '))} to the reCAPTCHA key's domain list.`,
|
|
69
|
+
waitMessage: 'Waiting for the domains to be saved',
|
|
70
|
+
manualOnly: true,
|
|
71
|
+
indent: ' ',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (result.success) {
|
|
75
|
+
writeBrandConfig(context, { [CONFIRMED_PATH]: domains });
|
|
76
|
+
console.log(` ${chalk.green('✓')} Domain list confirmed`);
|
|
77
|
+
return { output };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(` ${chalk.dim(`→ The key's domain list has no read API — ensure ${domains.join(' + ')} is listed: ${consoleUrl}`)}`);
|
|
82
|
+
return { output };
|
|
83
|
+
};
|