@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,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure one GA4 web data stream per enabled target.
|
|
3
|
+
*
|
|
4
|
+
* Streams are found by URI (the source of truth — each target gets a unique
|
|
5
|
+
* subdomain of the brand domain, virtual for app targets): create when
|
|
6
|
+
* missing, rename on displayName drift, enhanced measurement DIFFED before
|
|
7
|
+
* patching (omega-manager blind-PATCHed it every run), and a clean
|
|
8
|
+
* Measurement Protocol secret per stream (no dashes/underscores — regenerated
|
|
9
|
+
* until clean). Every value is re-resolved from GA on each run — the stream
|
|
10
|
+
* ids and URIs are pure derivation, so nothing about them is persisted.
|
|
11
|
+
*
|
|
12
|
+
* GA's "User Data Collection Acknowledgement" gate on secret creation has no
|
|
13
|
+
* API — interactive runs open the settings page (Enter-gated, house rule)
|
|
14
|
+
* and retry-poll until the acknowledgement lands, so ONE run finishes the
|
|
15
|
+
* job; non-interactive runs warn with the URL and the rerun converges.
|
|
16
|
+
* Acknowledging is per-property, so the first stream's prompt clears the
|
|
17
|
+
* gate for every stream after it.
|
|
18
|
+
*
|
|
19
|
+
* The two halves land in their two homes as they resolve. Each stream's
|
|
20
|
+
* measurement ID goes to CONFIG, at
|
|
21
|
+
* `targets.{target}.analytics.providers.google.id` — the per-surface
|
|
22
|
+
* override every framework reads through the merge chain, the same shape the
|
|
23
|
+
* monitoring service uses for its per-target DSNs (#417). Each stream's
|
|
24
|
+
* Measurement Protocol secret goes to the brand .env, under
|
|
25
|
+
* `GOOGLE_ANALYTICS_SECRET_{TARGET}` (#434) — the delivery step composes it
|
|
26
|
+
* into each target's runtime env as GOOGLE_ANALYTICS_SECRET, so the pair
|
|
27
|
+
* travels together.
|
|
28
|
+
*/
|
|
29
|
+
const chalk = require('chalk').default;
|
|
30
|
+
const { pressEnterToOpen } = require('../../../vendor/devkit/prompt.js');
|
|
31
|
+
const { pollWithSpinner } = require('../../../vendor/devkit/flows.js');
|
|
32
|
+
const { writeBrandConfig } = require('../../../lib/config-write.js');
|
|
33
|
+
const { writeEnvValue } = require('../../../lib/env-secret.js');
|
|
34
|
+
const { streamSecretEnvName } = require('../../../lib/analytics-secret.js');
|
|
35
|
+
const { canPrompt, dryRunPlan } = require('../../../lib/run-gates.js');
|
|
36
|
+
|
|
37
|
+
// target → stream URI subdomain (null = the root domain) + display name.
|
|
38
|
+
// Subdomains for app targets are virtual — they exist only to give each
|
|
39
|
+
// target a distinct stream URI. Unlisted targets default to their own name.
|
|
40
|
+
const STREAM_TARGETS = {
|
|
41
|
+
web: { displayName: 'Website', subdomain: null },
|
|
42
|
+
backend: { displayName: 'Backend', subdomain: 'api' },
|
|
43
|
+
desktop: { displayName: 'Desktop App', subdomain: 'desktop' },
|
|
44
|
+
extension: { displayName: 'Browser Extension', subdomain: 'extension' },
|
|
45
|
+
mobile: { displayName: 'Mobile App', subdomain: 'mobile' },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const SECRET_NAME = 'Secret';
|
|
49
|
+
const MAX_SECRET_ATTEMPTS = 20;
|
|
50
|
+
|
|
51
|
+
// Secrets get embedded in .env files and URLs downstream — dashes and
|
|
52
|
+
// underscores have burned enough tooling that omega-manager regenerated
|
|
53
|
+
// until the value was clean; the port keeps that rule
|
|
54
|
+
function isCleanSecret(secret) {
|
|
55
|
+
return !secret.includes('-') && !secret.includes('_');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = async function ensureGoogleStreams(context) {
|
|
59
|
+
const { analyticsApi: api, propertyId, accountId, domain, brand, brandConfig, brandRoot, options = {} } = context;
|
|
60
|
+
|
|
61
|
+
// Config drift check: a propertyId pointing at a deleted/inaccessible
|
|
62
|
+
// property should say so instead of erroring cryptically per stream
|
|
63
|
+
const property = await api.getProperty(propertyId);
|
|
64
|
+
if (!property) {
|
|
65
|
+
console.log(` ${chalk.yellow('⚠')} GA property ${chalk.cyan(propertyId)} not found (deleted, or the authed account has no access)`);
|
|
66
|
+
return { status: 'warned', reason: `GA property ${propertyId} not found`, output: { streams: { error: `property ${propertyId} not found` } } };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const existing = await api.listDataStreams(propertyId);
|
|
70
|
+
console.log(` ${chalk.dim('→')} Property ${chalk.cyan(propertyId)}: ${existing.length} existing stream(s)`);
|
|
71
|
+
|
|
72
|
+
const enhancedMeasurement = brandConfig.analytics?.providers?.google?.enhancedMeasurement || {};
|
|
73
|
+
|
|
74
|
+
const streams = {};
|
|
75
|
+
// What warned, by name (#643) — joined into the operation's reason
|
|
76
|
+
const warnings = [];
|
|
77
|
+
const planned = [];
|
|
78
|
+
const configEdits = {};
|
|
79
|
+
|
|
80
|
+
for (const target of brand.enabledTargets) {
|
|
81
|
+
const spec = STREAM_TARGETS[target] || { displayName: target, subdomain: target };
|
|
82
|
+
const uri = spec.subdomain ? `https://${spec.subdomain}.${domain}` : `https://${domain}`;
|
|
83
|
+
const displayName = `${brandConfig.brand.name} - ${spec.displayName}`;
|
|
84
|
+
|
|
85
|
+
let stream = existing.find((s) => s.type === 'WEB_DATA_STREAM' && s.webStreamData?.defaultUri === uri);
|
|
86
|
+
|
|
87
|
+
if (!stream) {
|
|
88
|
+
if (options.dryRun) {
|
|
89
|
+
dryRunPlan(`create ${target} stream (${uri})`);
|
|
90
|
+
planned.push({ target, action: 'create', uri });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
console.log(` ${chalk.dim('→')} Creating ${chalk.cyan(target)} stream (${uri})...`);
|
|
94
|
+
stream = await api.createWebDataStream(propertyId, { defaultUri: uri, displayName });
|
|
95
|
+
console.log(` ${chalk.green('✓')} ${chalk.cyan(target)}: created`);
|
|
96
|
+
} else {
|
|
97
|
+
console.log(` ${chalk.green('✓')} ${chalk.cyan(target)}: found by URI`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const streamId = stream.name.split('/').pop();
|
|
101
|
+
|
|
102
|
+
// Rename on drift only
|
|
103
|
+
if (stream.displayName !== displayName) {
|
|
104
|
+
if (options.dryRun) {
|
|
105
|
+
planned.push({ target, action: 'rename', to: displayName });
|
|
106
|
+
} else {
|
|
107
|
+
await api.updateDataStream(propertyId, streamId, { displayName });
|
|
108
|
+
console.log(` ${chalk.green('✓')} Renamed to "${chalk.cyan(displayName)}"`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Enhanced measurement — diff the configured keys against the live
|
|
113
|
+
// settings; omega-manager PATCHed unconditionally on every run
|
|
114
|
+
if (Object.keys(enhancedMeasurement).length > 0) {
|
|
115
|
+
const current = await api.getEnhancedMeasurementSettings(propertyId, streamId);
|
|
116
|
+
const drifted = Object.keys(enhancedMeasurement)
|
|
117
|
+
.filter((key) => current?.[key] !== enhancedMeasurement[key]);
|
|
118
|
+
if (drifted.length > 0) {
|
|
119
|
+
if (options.dryRun) {
|
|
120
|
+
planned.push({ target, action: 'enhanced-measurement', drifted });
|
|
121
|
+
} else {
|
|
122
|
+
await api.updateEnhancedMeasurementSettings(propertyId, streamId, enhancedMeasurement);
|
|
123
|
+
console.log(` ${chalk.green('✓')} Enhanced measurement updated (${drifted.join(', ')})`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Measurement Protocol secret — the brand .env holds the last resolved
|
|
129
|
+
// value, so a failed listing below degrades to it instead of to nothing
|
|
130
|
+
const secretEnvName = streamSecretEnvName(target);
|
|
131
|
+
let apiSecret = process.env[secretEnvName] || null;
|
|
132
|
+
try {
|
|
133
|
+
const secrets = await api.listMeasurementProtocolSecrets(propertyId, streamId);
|
|
134
|
+
const found = secrets.find((s) => s.displayName === SECRET_NAME);
|
|
135
|
+
|
|
136
|
+
if (found && isCleanSecret(found.secretValue)) {
|
|
137
|
+
apiSecret = found.secretValue;
|
|
138
|
+
} else if (options.dryRun) {
|
|
139
|
+
planned.push({ target, action: found ? 'replace-unclean-secret' : 'create-secret' });
|
|
140
|
+
} else {
|
|
141
|
+
if (found) {
|
|
142
|
+
await api.deleteMeasurementProtocolSecret(propertyId, streamId, found.name.split('/').pop());
|
|
143
|
+
console.log(` ${chalk.dim('↻')} Deleted unclean secret`);
|
|
144
|
+
}
|
|
145
|
+
apiSecret = await createCleanSecret(api, propertyId, streamId);
|
|
146
|
+
if (apiSecret) {
|
|
147
|
+
console.log(` ${chalk.green('✓')} API secret created`);
|
|
148
|
+
} else {
|
|
149
|
+
console.log(` ${chalk.yellow('⚠')} Could not generate a clean secret in ${MAX_SECRET_ATTEMPTS} attempts`);
|
|
150
|
+
warnings.push('could not generate a clean API secret');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (error.message.includes('User Data Collection Acknowledgement')) {
|
|
155
|
+
// No API for the acknowledgement — point at the stream settings page
|
|
156
|
+
// (the account-scoped deep link needs accountId; fall back to the app)
|
|
157
|
+
const settingsUrl = accountId
|
|
158
|
+
? `https://analytics.google.com/analytics/web/#/a${accountId}p${propertyId}/admin/streams/table/${streamId}`
|
|
159
|
+
: 'https://analytics.google.com/analytics/web/';
|
|
160
|
+
console.log(` ${chalk.yellow('⚠')} Data collection acknowledgement required before secrets can be created`);
|
|
161
|
+
|
|
162
|
+
if (canPrompt(options)) {
|
|
163
|
+
await pressEnterToOpen(settingsUrl, 'the data-collection acknowledgement page');
|
|
164
|
+
const poll = await pollWithSpinner({
|
|
165
|
+
check: async () => {
|
|
166
|
+
try {
|
|
167
|
+
const secret = await createCleanSecret(api, propertyId, streamId);
|
|
168
|
+
return { done: true, result: secret };
|
|
169
|
+
} catch (retryError) {
|
|
170
|
+
if (retryError.message.includes('User Data Collection Acknowledgement')) {
|
|
171
|
+
return { done: false };
|
|
172
|
+
}
|
|
173
|
+
return { done: true, error: retryError.message };
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
intervalMs: 5000,
|
|
177
|
+
message: 'Waiting for the acknowledgement',
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (poll.success && poll.result) {
|
|
181
|
+
apiSecret = poll.result;
|
|
182
|
+
console.log(` ${chalk.green('✓')} API secret created`);
|
|
183
|
+
} else {
|
|
184
|
+
console.log(` ${chalk.dim('→')} Still gated${poll.error ? chalk.dim(` (${poll.error})`) : ''} — rerun converges once acknowledged`);
|
|
185
|
+
warnings.push('API secret still gated — acknowledge it in the GA console');
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
console.log(` ${chalk.dim('→')} Acknowledge at: ${chalk.cyan(settingsUrl)} — then rerun`);
|
|
189
|
+
warnings.push('API secret still gated — acknowledge it in the GA console');
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
console.log(` ${chalk.yellow('⚠')} API secret${chalk.dim(`: ${error.message}`)}`);
|
|
193
|
+
warnings.push('could not read or create the API secret');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const measurementId = stream.webStreamData?.measurementId || null;
|
|
198
|
+
|
|
199
|
+
streams[target] = { streamId, measurementId };
|
|
200
|
+
|
|
201
|
+
// The resolved measurement id always wins — a hand-set stale value is
|
|
202
|
+
// drift and gets patched (the monitoring service's DSN rule)
|
|
203
|
+
if (measurementId && brandConfig.targets?.[target]?.analytics?.providers?.google?.id !== measurementId) {
|
|
204
|
+
configEdits[`targets.${target}.analytics.providers.google.id`] = measurementId;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// The secret's home is the brand .env — written only on drift, so a
|
|
208
|
+
// converged rerun leaves the file alone
|
|
209
|
+
if (apiSecret && !options.dryRun && process.env[secretEnvName] !== apiSecret) {
|
|
210
|
+
writeEnvValue(brandRoot, secretEnvName, apiSecret);
|
|
211
|
+
process.env[secretEnvName] = apiSecret;
|
|
212
|
+
console.log(` ${chalk.green('✓')} .env ← ${chalk.cyan(secretEnvName)}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Only touch omega.json5 when something actually drifted — a converged
|
|
217
|
+
// brand never opens the file
|
|
218
|
+
if (Object.keys(configEdits).length > 0) {
|
|
219
|
+
writeBrandConfig(context, configEdits);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// A Firebase web app measures through its OWN measurementId — if this GA
|
|
223
|
+
// property has no stream carrying it, Firebase is linked to a different
|
|
224
|
+
// property (expected for shared projects, a real misconfig otherwise)
|
|
225
|
+
const firebaseMeasurementId = brandConfig.cloud?.config?.measurementId;
|
|
226
|
+
if (firebaseMeasurementId && brandConfig.cloud?.shared !== true) {
|
|
227
|
+
const covered = existing.some((s) => s.webStreamData?.measurementId === firebaseMeasurementId)
|
|
228
|
+
|| Object.values(streams).some((s) => s.measurementId === firebaseMeasurementId);
|
|
229
|
+
if (!covered) {
|
|
230
|
+
console.log(` ${chalk.yellow('⚠')} Firebase SDK measures via ${chalk.cyan(firebaseMeasurementId)}, but property ${chalk.cyan(propertyId)} has no stream with that ID`);
|
|
231
|
+
console.log(` ${chalk.dim('→')} Firebase is likely linked to a different GA property — unlink in Firebase Console and rerun`);
|
|
232
|
+
warnings.push('the Firebase measurement ID has no stream on this property');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const result = { output: { streams: { resolved: streams } } };
|
|
237
|
+
if (planned.length > 0) {
|
|
238
|
+
result.output.streams.planned = planned;
|
|
239
|
+
}
|
|
240
|
+
if (warnings.length > 0) {
|
|
241
|
+
result.status = 'warned';
|
|
242
|
+
result.reason = warnings.join('; ');
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
async function createCleanSecret(api, propertyId, streamId) {
|
|
248
|
+
for (let attempt = 1; attempt <= MAX_SECRET_ATTEMPTS; attempt++) {
|
|
249
|
+
const created = await api.createMeasurementProtocolSecret(propertyId, streamId, SECRET_NAME);
|
|
250
|
+
if (isCleanSecret(created.secretValue)) {
|
|
251
|
+
return created.secretValue;
|
|
252
|
+
}
|
|
253
|
+
await api.deleteMeasurementProtocolSecret(propertyId, streamId, created.name.split('/').pop());
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure the Meta Pixel EXISTS and is configured with a Conversions API
|
|
3
|
+
* access token (META_ACCESS_TOKEN in the brand .env — the name
|
|
4
|
+
* @omega.js/backend reads, and the same system-user token the Marketing API
|
|
5
|
+
* create authenticates with).
|
|
6
|
+
*
|
|
7
|
+
* Near-zero input (#417, Ian 2026-08-21): one interactive pass acquires the
|
|
8
|
+
* token (Enter-gated open of the system-users page, then a paste-in),
|
|
9
|
+
* discovers the ad account that token can see (auto-selected when there's
|
|
10
|
+
* exactly one, landed in `analytics.providers.meta.accountId`), creates the
|
|
11
|
+
* pixel on it, and lands its id — so a brand configures Meta by saying Yes.
|
|
12
|
+
* `analytics.providers.meta: false` turns the whole thing off.
|
|
13
|
+
*/
|
|
14
|
+
const { ensurePixelToken } = require('../lib/pixel-token.js');
|
|
15
|
+
const { provisionPixel } = require('../lib/pixel-provision.js');
|
|
16
|
+
const { META_PIXEL } = require('../lib/pixel-specs.js');
|
|
17
|
+
|
|
18
|
+
module.exports = async function ensureMetaPixel(context) {
|
|
19
|
+
const ended = await provisionPixel(context, META_PIXEL);
|
|
20
|
+
|
|
21
|
+
return ended || ensurePixelToken(context, META_PIXEL);
|
|
22
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure the TikTok Pixel EXISTS (created on `analytics.providers.tiktok
|
|
3
|
+
* .accountId` — the advertiser account — when config has no id yet) and is
|
|
4
|
+
* configured with an Events API access token (TIKTOK_ACCESS_TOKEN in the
|
|
5
|
+
* brand .env — the name @omega.js/backend reads, and the same token the
|
|
6
|
+
* Business API create authenticates with).
|
|
7
|
+
*
|
|
8
|
+
* SCAFFOLD (#417, Ian 2026-08-21): the wiring is complete and the token is
|
|
9
|
+
* the gate, but TikTok's side is not provisioned — no brand carries the key,
|
|
10
|
+
* so the create path has never run against the live API and this pass always
|
|
11
|
+
* ends at the warned where-to-get guidance. lib/tiktok-api.js carries the
|
|
12
|
+
* call shapes it will use on the first real run.
|
|
13
|
+
*/
|
|
14
|
+
const { ensurePixelToken } = require('../lib/pixel-token.js');
|
|
15
|
+
const { provisionPixel } = require('../lib/pixel-provision.js');
|
|
16
|
+
const { TIKTOK_PIXEL } = require('../lib/pixel-specs.js');
|
|
17
|
+
|
|
18
|
+
module.exports = async function ensureTikTokPixel(context) {
|
|
19
|
+
const ended = await provisionPixel(context, TIKTOK_PIXEL);
|
|
20
|
+
|
|
21
|
+
return ended || ensurePixelToken(context, TIKTOK_PIXEL);
|
|
22
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics service — reconciles the brand's analytics providers to
|
|
3
|
+
* `analytics: {}` in omega.json5: one GA4 web data stream per enabled target
|
|
4
|
+
* (with enhanced measurement + a Measurement Protocol secret each), the
|
|
5
|
+
* GA property ↔ Firebase project link, and the Meta/TikTok pixels — created
|
|
6
|
+
* on their ad account when config has no id yet (#417), then checked for
|
|
7
|
+
* their access token.
|
|
8
|
+
*
|
|
9
|
+
* Google auth: GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET in the brand .env
|
|
10
|
+
* (OAuth2; tokens cache to .omega/auth/google-analytics-tokens.json — the
|
|
11
|
+
* analytics.edit scope is separate from the cloud service's tokens).
|
|
12
|
+
* The GA4 property itself is required config (`analytics.providers.google.
|
|
13
|
+
* propertyId`): interactive runs offer the account + property
|
|
14
|
+
* selection/creation flow (lib/property-flow.js) and land the ids in
|
|
15
|
+
* omega.json5. Without a propertyId (non-interactive/skipped)
|
|
16
|
+
* or without Google credentials the two google operations are filtered out.
|
|
17
|
+
*
|
|
18
|
+
* Pixel auth: META_ACCESS_TOKEN / TIKTOK_ACCESS_TOKEN in the brand .env (the
|
|
19
|
+
* names @omega.js/backend reads — one token per platform serves both the
|
|
20
|
+
* create and the conversions sender). A pixel operation runs whenever its
|
|
21
|
+
* provider has an id (token check) or an accountId to create under, and —
|
|
22
|
+
* Meta, whose token names its own ad accounts — whenever the token is in hand
|
|
23
|
+
* or the run can ask for one (lib/pixel-provision.js hasPixelWork). A missing
|
|
24
|
+
* token warns with guidance and never fails; `analytics.providers.{provider}:
|
|
25
|
+
* false` disables the provider and stops every ask.
|
|
26
|
+
*/
|
|
27
|
+
const { serviceInputSpec } = require('../../config.js');
|
|
28
|
+
const { googleTokenStorePath } = require('../../lib/google-auth.js');
|
|
29
|
+
const chalk = require('chalk').default;
|
|
30
|
+
const { createServiceRunner } = require('../../lib/service-runner.js');
|
|
31
|
+
const { requestServiceInput } = require('../../lib/service-input.js');
|
|
32
|
+
const { GoogleAnalyticsAPI } = require('./lib/analytics-api.js');
|
|
33
|
+
const { MetaMarketingAPI } = require('./lib/meta-api.js');
|
|
34
|
+
const { TikTokBusinessAPI } = require('./lib/tiktok-api.js');
|
|
35
|
+
const { resolveGoogleProperty } = require('./lib/property-flow.js');
|
|
36
|
+
const { hasPixelWork } = require('./lib/pixel-provision.js');
|
|
37
|
+
const { META_PIXEL, TIKTOK_PIXEL } = require('./lib/pixel-specs.js');
|
|
38
|
+
|
|
39
|
+
// Operations that need the GA Admin API (and therefore propertyId + creds)
|
|
40
|
+
const GOOGLE_OPERATIONS = new Set(['google-streams', 'google-firebase-link']);
|
|
41
|
+
|
|
42
|
+
module.exports.run = createServiceRunner({
|
|
43
|
+
serviceDir: __dirname,
|
|
44
|
+
setup: async (context) => {
|
|
45
|
+
const analytics = context.brandConfig.analytics || {};
|
|
46
|
+
|
|
47
|
+
if (analytics.enabled === false) {
|
|
48
|
+
return { skip: true, reason: 'analytics.enabled = false' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const domain = (context.brandConfig.brand?.url || '').replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
52
|
+
if (!domain) {
|
|
53
|
+
return { skip: true, reason: 'no brand.url configured' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The shared setup contract (#608) for the GOOGLE half only, and only
|
|
57
|
+
// once the brand has DECLARED it wants GA4 (a configured propertyId — the
|
|
58
|
+
// same gate REQUIRES.analytics.when reads): the pixel tokens are optional
|
|
59
|
+
// beside it and each pixel operation asks for its own in place
|
|
60
|
+
// (lib/pixel-token.js), so a pixel-only brand is never asked for a Google
|
|
61
|
+
// credential it has no use for.
|
|
62
|
+
if (!context.analyticsApi && analytics.providers?.google?.propertyId) {
|
|
63
|
+
await requestServiceInput(context, serviceInputSpec('analytics', {
|
|
64
|
+
names: ['GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET'],
|
|
65
|
+
label: 'Google Analytics',
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const haveGoogleAuth = Boolean(
|
|
70
|
+
context.analyticsApi
|
|
71
|
+
|| (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Missing property + creds available → offer the interactive account +
|
|
75
|
+
// property selection/creation flow (lands both ids in omega.json5, and
|
|
76
|
+
// in the in-memory config re-read just below)
|
|
77
|
+
if (!analytics.providers?.google?.propertyId && haveGoogleAuth) {
|
|
78
|
+
const flowApi = context.analyticsApi || new GoogleAnalyticsAPI({
|
|
79
|
+
tokenStorePath: googleTokenStorePath(context.brandRoot),
|
|
80
|
+
});
|
|
81
|
+
await resolveGoogleProperty(context, flowApi);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const providers = context.brandConfig.analytics?.providers || {};
|
|
85
|
+
const google = providers.google || {};
|
|
86
|
+
const meta = providers.meta || {};
|
|
87
|
+
const tiktok = providers.tiktok || {};
|
|
88
|
+
|
|
89
|
+
// A pixel half has work when its id is configured (token check), an
|
|
90
|
+
// account id names where to create it, or — Meta — a token in hand (or an
|
|
91
|
+
// interactive run that can ask for one) can discover the account itself
|
|
92
|
+
const metaWork = hasPixelWork(context.brandConfig, META_PIXEL, context.options);
|
|
93
|
+
const tiktokWork = hasPixelWork(context.brandConfig, TIKTOK_PIXEL, context.options);
|
|
94
|
+
const havePixelWork = metaWork || tiktokWork;
|
|
95
|
+
|
|
96
|
+
if (!google.propertyId && !havePixelWork) {
|
|
97
|
+
return { skip: true, reason: 'no analytics providers configured (analytics.providers.google.propertyId / meta.{id,accountId} / tiktok.{id,accountId})' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Decide whether the google operations can run this pass
|
|
101
|
+
let operations = context.operations;
|
|
102
|
+
if (!google.propertyId) {
|
|
103
|
+
console.log(chalk.dim(' ⊘ google operations skipped — no analytics.providers.google.propertyId (rerun interactively to select/create the property)'));
|
|
104
|
+
operations = operations.filter((op) => !GOOGLE_OPERATIONS.has(op.name));
|
|
105
|
+
} else if (!haveGoogleAuth) {
|
|
106
|
+
if (!havePixelWork) {
|
|
107
|
+
return { skip: true, reason: 'no GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET configured (set them in the brand .env)' };
|
|
108
|
+
}
|
|
109
|
+
console.log(chalk.dim(' ⊘ google operations skipped — no GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET in the brand .env'));
|
|
110
|
+
operations = operations.filter((op) => !GOOGLE_OPERATIONS.has(op.name));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const needsGoogleApi = operations.some((op) => GOOGLE_OPERATIONS.has(op.name));
|
|
114
|
+
|
|
115
|
+
// A pixel client is built only for a provisioning pass — a brand whose
|
|
116
|
+
// pixel id is already config never touches the platform's API
|
|
117
|
+
const needsPixelApi = (work, provider) => Boolean(work && !provider.id);
|
|
118
|
+
|
|
119
|
+
// Tests inject fake clients via context.analyticsApi/metaApi/tiktokApi
|
|
120
|
+
return {
|
|
121
|
+
analyticsApi: needsGoogleApi
|
|
122
|
+
? (context.analyticsApi || new GoogleAnalyticsAPI({
|
|
123
|
+
tokenStorePath: googleTokenStorePath(context.brandRoot),
|
|
124
|
+
}))
|
|
125
|
+
: null,
|
|
126
|
+
metaApi: context.metaApi || (needsPixelApi(metaWork, meta) ? new MetaMarketingAPI() : null),
|
|
127
|
+
tiktokApi: context.tiktokApi || (needsPixelApi(tiktokWork, tiktok) ? new TikTokBusinessAPI() : null),
|
|
128
|
+
domain,
|
|
129
|
+
propertyId: google.propertyId || null,
|
|
130
|
+
accountId: google.accountId || null,
|
|
131
|
+
operations,
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Analytics Admin API client (v1beta, plus the v1alpha enhanced-
|
|
3
|
+
* measurement endpoints that never made it into beta). Named-method surface
|
|
4
|
+
* over the shared GoogleOAuth2Client so tests can fake it method-for-method.
|
|
5
|
+
*
|
|
6
|
+
* Auth: GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET in the brand .env; the ONE
|
|
7
|
+
* shared token store (GOOGLE_SCOPES union — one consent covers everything).
|
|
8
|
+
*/
|
|
9
|
+
const { GoogleOAuth2Client, GOOGLE_SCOPES } = require('../../../lib/google-auth.js');
|
|
10
|
+
|
|
11
|
+
const API_BASE = 'https://analyticsadmin.googleapis.com/v1beta';
|
|
12
|
+
const API_BASE_ALPHA = 'https://analyticsadmin.googleapis.com/v1alpha';
|
|
13
|
+
|
|
14
|
+
class GoogleAnalyticsAPI {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.auth = new GoogleOAuth2Client({
|
|
17
|
+
clientId: options.clientId || process.env.GOOGLE_CLIENT_ID,
|
|
18
|
+
clientSecret: options.clientSecret || process.env.GOOGLE_CLIENT_SECRET,
|
|
19
|
+
tokenStorePath: options.tokenStorePath,
|
|
20
|
+
scopes: GOOGLE_SCOPES,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async makeRequest(endpoint, options = {}) {
|
|
25
|
+
return this.auth.makeRequest(`${API_BASE}${endpoint}`, options);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ========== Accounts / properties ==========
|
|
29
|
+
|
|
30
|
+
async listAccounts() {
|
|
31
|
+
const data = await this.makeRequest('/accounts');
|
|
32
|
+
return data.accounts || [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async listProperties(accountId) {
|
|
36
|
+
const data = await this.makeRequest(`/properties?filter=parent:accounts/${accountId}`);
|
|
37
|
+
return data.properties || [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Create a GA4 property under an account; returns the property resource */
|
|
41
|
+
async createProperty(accountId, { displayName, timeZone, currencyCode }) {
|
|
42
|
+
return this.makeRequest('/properties', {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
parent: `accounts/${accountId}`,
|
|
46
|
+
displayName,
|
|
47
|
+
timeZone,
|
|
48
|
+
currencyCode,
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async getProperty(propertyId) {
|
|
54
|
+
try {
|
|
55
|
+
return await this.makeRequest(`/properties/${propertyId}`);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error.message.includes('404')) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ========== Data streams ==========
|
|
65
|
+
|
|
66
|
+
async listDataStreams(propertyId) {
|
|
67
|
+
const data = await this.makeRequest(`/properties/${propertyId}/dataStreams`);
|
|
68
|
+
return data.dataStreams || [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async createWebDataStream(propertyId, { defaultUri, displayName }) {
|
|
72
|
+
return await this.makeRequest(`/properties/${propertyId}/dataStreams`, {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
type: 'WEB_DATA_STREAM',
|
|
76
|
+
webStreamData: { defaultUri },
|
|
77
|
+
displayName: displayName || 'Web Stream',
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Update a data stream. Nested paths (e.g. 'webStreamData.defaultUri') need
|
|
84
|
+
* an explicit update mask — Object.keys would produce just 'webStreamData'.
|
|
85
|
+
*/
|
|
86
|
+
async updateDataStream(propertyId, dataStreamId, updates, updateMaskFields) {
|
|
87
|
+
const updateMask = (updateMaskFields || Object.keys(updates)).join(',');
|
|
88
|
+
return await this.makeRequest(
|
|
89
|
+
`/properties/${propertyId}/dataStreams/${dataStreamId}?updateMask=${updateMask}`,
|
|
90
|
+
{ method: 'PATCH', body: JSON.stringify(updates) },
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ========== Enhanced measurement (v1alpha only) ==========
|
|
95
|
+
|
|
96
|
+
async getEnhancedMeasurementSettings(propertyId, dataStreamId) {
|
|
97
|
+
return this.auth.makeRequest(
|
|
98
|
+
`${API_BASE_ALPHA}/properties/${propertyId}/dataStreams/${dataStreamId}/enhancedMeasurementSettings`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async updateEnhancedMeasurementSettings(propertyId, dataStreamId, settings) {
|
|
103
|
+
const updateMask = Object.keys(settings).join(',');
|
|
104
|
+
return this.auth.makeRequest(
|
|
105
|
+
`${API_BASE_ALPHA}/properties/${propertyId}/dataStreams/${dataStreamId}/enhancedMeasurementSettings?updateMask=${updateMask}`,
|
|
106
|
+
{ method: 'PATCH', body: JSON.stringify(settings) },
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ========== Measurement Protocol secrets ==========
|
|
111
|
+
|
|
112
|
+
async listMeasurementProtocolSecrets(propertyId, dataStreamId) {
|
|
113
|
+
const data = await this.makeRequest(
|
|
114
|
+
`/properties/${propertyId}/dataStreams/${dataStreamId}/measurementProtocolSecrets`,
|
|
115
|
+
);
|
|
116
|
+
return data.measurementProtocolSecrets || [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async createMeasurementProtocolSecret(propertyId, dataStreamId, displayName) {
|
|
120
|
+
return await this.makeRequest(
|
|
121
|
+
`/properties/${propertyId}/dataStreams/${dataStreamId}/measurementProtocolSecrets`,
|
|
122
|
+
{ method: 'POST', body: JSON.stringify({ displayName }) },
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async deleteMeasurementProtocolSecret(propertyId, dataStreamId, secretId) {
|
|
127
|
+
await this.makeRequest(
|
|
128
|
+
`/properties/${propertyId}/dataStreams/${dataStreamId}/measurementProtocolSecrets/${secretId}`,
|
|
129
|
+
{ method: 'DELETE' },
|
|
130
|
+
);
|
|
131
|
+
return { deleted: true };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ========== Firebase links ==========
|
|
135
|
+
|
|
136
|
+
async listFirebaseLinks(propertyId) {
|
|
137
|
+
const data = await this.makeRequest(`/properties/${propertyId}/firebaseLinks`);
|
|
138
|
+
return data.firebaseLinks || [];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async createFirebaseLink(propertyId, firebaseProjectId) {
|
|
142
|
+
return await this.makeRequest(`/properties/${propertyId}/firebaseLinks`, {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
body: JSON.stringify({ project: `projects/${firebaseProjectId}` }),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async deleteFirebaseLink(propertyId, firebaseLinkId) {
|
|
149
|
+
await this.makeRequest(`/properties/${propertyId}/firebaseLinks/${firebaseLinkId}`, {
|
|
150
|
+
method: 'DELETE',
|
|
151
|
+
});
|
|
152
|
+
return { deleted: true };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = { GoogleAnalyticsAPI };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meta Marketing API client (Graph API) — the ad account's pixels/datasets.
|
|
3
|
+
* Named-method surface over fetch so tests can fake it method-for-method,
|
|
4
|
+
* normalized to the shared pixel shape ({ id, name }) the provisioning flow
|
|
5
|
+
* consumes (lib/pixel-provision.js).
|
|
6
|
+
*
|
|
7
|
+
* Auth: META_ACCESS_TOKEN in the brand .env — a Business Manager SYSTEM-USER
|
|
8
|
+
* token with ads_management on the ad account. ONE Meta token per brand: the
|
|
9
|
+
* same key @omega.js/backend's Conversions API sender reads, on the same
|
|
10
|
+
* Graph API version.
|
|
11
|
+
*/
|
|
12
|
+
const META_API_VERSION = 'v21.0'; // matches @omega.js/backend's conversions sender
|
|
13
|
+
const API_BASE = `https://graph.facebook.com/${META_API_VERSION}`;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Ad-account ids are configured BARE (the number Ads Manager shows); the
|
|
17
|
+
* Graph edge is `act_<id>`, and a value pasted with the prefix still works.
|
|
18
|
+
*/
|
|
19
|
+
function adAccountRef(adAccountId) {
|
|
20
|
+
const id = String(adAccountId);
|
|
21
|
+
return id.startsWith('act_') ? id : `act_${id}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class MetaMarketingAPI {
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
// Null unless passed explicitly: the env is read at REQUEST time, because
|
|
27
|
+
// an interactive run pastes META_ACCESS_TOKEN after this client was built
|
|
28
|
+
this.accessToken = options.accessToken || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async makeRequest(endpoint, options = {}) {
|
|
32
|
+
const response = await fetch(`${API_BASE}${endpoint}`, {
|
|
33
|
+
...options,
|
|
34
|
+
headers: {
|
|
35
|
+
'Authorization': `Bearer ${this.accessToken || process.env.META_ACCESS_TOKEN}`,
|
|
36
|
+
'Content-Type': 'application/json',
|
|
37
|
+
...options.headers,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const text = await response.text();
|
|
42
|
+
const data = text ? JSON.parse(text) : null;
|
|
43
|
+
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
// Graph errors are { error: { message, type, code } } and the message
|
|
46
|
+
// is the human half ("(#200) Requires ads_management permission")
|
|
47
|
+
throw new Error(`Meta API error (${response.status}): ${data?.error?.message || response.statusText}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return data;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Ad accounts this token can act on — the auto-discovery source (#417).
|
|
55
|
+
* `id` comes back as `act_<number>` and `account_id` as the bare number
|
|
56
|
+
* config carries, so the normalized id is the one a human reads in Ads
|
|
57
|
+
* Manager.
|
|
58
|
+
*/
|
|
59
|
+
async listAdAccounts() {
|
|
60
|
+
const data = await this.makeRequest('/me/adaccounts?fields=id,name,account_id&limit=100');
|
|
61
|
+
return (data?.data || []).map((account) => ({
|
|
62
|
+
id: account.account_id || String(account.id).replace(/^act_/, ''),
|
|
63
|
+
name: account.name,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Pixels on the ad account — id + name only (name is the match key). */
|
|
68
|
+
async listPixels(adAccountId) {
|
|
69
|
+
const data = await this.makeRequest(`/${adAccountRef(adAccountId)}/adspixels?fields=id,name&limit=100`);
|
|
70
|
+
return (data?.data || []).map((pixel) => ({ id: pixel.id, name: pixel.name }));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Create a pixel/dataset on the ad account (the create echoes id only). */
|
|
74
|
+
async createPixel(adAccountId, name) {
|
|
75
|
+
const created = await this.makeRequest(`/${adAccountRef(adAccountId)}/adspixels`, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
body: JSON.stringify({ name }),
|
|
78
|
+
});
|
|
79
|
+
return { id: created?.id, name };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { MetaMarketingAPI };
|