@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,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Validator for Firestore Documents
|
|
3
|
+
*
|
|
4
|
+
* Validates documents against a schema definition.
|
|
5
|
+
* Used by migration handlers to check data integrity.
|
|
6
|
+
*
|
|
7
|
+
* Schema format:
|
|
8
|
+
* {
|
|
9
|
+
* fieldName: {
|
|
10
|
+
* type: 'string' | 'number' | 'boolean' | 'object' | 'array',
|
|
11
|
+
* required: true | false,
|
|
12
|
+
* nullable: true | false, // if true, null is valid even if type doesn't match
|
|
13
|
+
* itemType: 'string', // for arrays, the expected type of items
|
|
14
|
+
* properties: { ... }, // for objects, nested schema
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the type of a value (more specific than typeof)
|
|
21
|
+
*
|
|
22
|
+
* @param {*} value - Value to check
|
|
23
|
+
* @returns {string} Type string: 'null', 'array', 'object', 'string', 'number', 'boolean', 'undefined'
|
|
24
|
+
*/
|
|
25
|
+
function getType(value) {
|
|
26
|
+
if (value === null) {
|
|
27
|
+
return 'null';
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
return 'array';
|
|
31
|
+
}
|
|
32
|
+
return typeof value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Format expected type string for error messages
|
|
37
|
+
*
|
|
38
|
+
* @param {Object} fieldSchema - Schema for the field
|
|
39
|
+
* @returns {string} Human-readable type string
|
|
40
|
+
*/
|
|
41
|
+
function formatExpectedType(fieldSchema) {
|
|
42
|
+
let expected = fieldSchema.type;
|
|
43
|
+
|
|
44
|
+
if (fieldSchema.nullable) {
|
|
45
|
+
expected += '|null';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (fieldSchema.type === 'array' && fieldSchema.itemType) {
|
|
49
|
+
expected = `array<${fieldSchema.itemType}>`;
|
|
50
|
+
if (fieldSchema.nullable) {
|
|
51
|
+
expected += '|null';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return expected;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate a single field against its schema
|
|
60
|
+
*
|
|
61
|
+
* @param {*} value - The value to validate
|
|
62
|
+
* @param {Object} fieldSchema - Schema for this field
|
|
63
|
+
* @param {string} fieldPath - Dot-notation path for error messages
|
|
64
|
+
* @returns {Array} Array of error objects
|
|
65
|
+
*/
|
|
66
|
+
function validateField(value, fieldSchema, fieldPath) {
|
|
67
|
+
const errors = [];
|
|
68
|
+
const actualType = getType(value);
|
|
69
|
+
|
|
70
|
+
// Check if field is missing
|
|
71
|
+
if (actualType === 'undefined') {
|
|
72
|
+
if (fieldSchema.required) {
|
|
73
|
+
errors.push({
|
|
74
|
+
field: fieldPath,
|
|
75
|
+
expected: formatExpectedType(fieldSchema),
|
|
76
|
+
actual: 'undefined',
|
|
77
|
+
message: 'Missing required field',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return errors;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check nullable
|
|
84
|
+
if (actualType === 'null') {
|
|
85
|
+
if (!fieldSchema.nullable) {
|
|
86
|
+
errors.push({
|
|
87
|
+
field: fieldPath,
|
|
88
|
+
expected: formatExpectedType(fieldSchema),
|
|
89
|
+
actual: 'null',
|
|
90
|
+
message: `Expected ${fieldSchema.type}, got null (field is not nullable)`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return errors;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check type match
|
|
97
|
+
if (actualType !== fieldSchema.type) {
|
|
98
|
+
errors.push({
|
|
99
|
+
field: fieldPath,
|
|
100
|
+
expected: formatExpectedType(fieldSchema),
|
|
101
|
+
actual: actualType,
|
|
102
|
+
message: `Expected ${fieldSchema.type}, got ${actualType}`,
|
|
103
|
+
});
|
|
104
|
+
return errors; // Don't check nested if type doesn't match
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// For arrays, check item types
|
|
108
|
+
if (fieldSchema.type === 'array' && fieldSchema.itemType) {
|
|
109
|
+
for (let i = 0; i < value.length; i++) {
|
|
110
|
+
const itemType = getType(value[i]);
|
|
111
|
+
if (itemType !== fieldSchema.itemType) {
|
|
112
|
+
errors.push({
|
|
113
|
+
field: `${fieldPath}[${i}]`,
|
|
114
|
+
expected: fieldSchema.itemType,
|
|
115
|
+
actual: itemType,
|
|
116
|
+
message: `Array item expected ${fieldSchema.itemType}, got ${itemType}`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// For objects with properties schema, validate nested fields
|
|
123
|
+
if (fieldSchema.type === 'object' && fieldSchema.properties) {
|
|
124
|
+
for (const [propName, propSchema] of Object.entries(fieldSchema.properties)) {
|
|
125
|
+
const propErrors = validateField(value[propName], propSchema, `${fieldPath}.${propName}`);
|
|
126
|
+
errors.push(...propErrors);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return errors;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Validate a document against a schema
|
|
135
|
+
*
|
|
136
|
+
* @param {Object} doc - Document data to validate
|
|
137
|
+
* @param {Object} schema - Schema definition
|
|
138
|
+
* @returns {Object} { valid: boolean, errors: Array<{ field, expected, actual, message }> }
|
|
139
|
+
*/
|
|
140
|
+
function validateDocument(doc, schema) {
|
|
141
|
+
const errors = [];
|
|
142
|
+
|
|
143
|
+
for (const [fieldName, fieldSchema] of Object.entries(schema)) {
|
|
144
|
+
const fieldErrors = validateField(doc[fieldName], fieldSchema, fieldName);
|
|
145
|
+
errors.push(...fieldErrors);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
valid: errors.length === 0,
|
|
150
|
+
errors,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = { validateDocument };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Land each target project's DSN in
|
|
3
|
+
* targets.<type>.monitoring.providers.sentry.dsn (#425) — the
|
|
4
|
+
* per-surface override every framework reads through the config merge
|
|
5
|
+
* chain. DSNs are public by design (schema-pinned), so config is their
|
|
6
|
+
* home; the writeback is comment-preserving and a converged rerun leaves
|
|
7
|
+
* the file byte-identical. The resolved value always wins: a hand-set
|
|
8
|
+
* stale DSN is drift and gets patched.
|
|
9
|
+
*/
|
|
10
|
+
const chalk = require('chalk').default;
|
|
11
|
+
const { writeBrandConfig } = require('../../../lib/config-write.js');
|
|
12
|
+
|
|
13
|
+
module.exports = async function ensureDsn(context) {
|
|
14
|
+
const { sentryApi: api, brandConfig, serviceData } = context;
|
|
15
|
+
|
|
16
|
+
const org = serviceData.org;
|
|
17
|
+
const projects = serviceData.projectMap || {};
|
|
18
|
+
const targets = Object.keys(projects);
|
|
19
|
+
|
|
20
|
+
if (!org || targets.length === 0) {
|
|
21
|
+
console.log(` ${chalk.dim('⊘ No projects resolved — nothing to land')}`);
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const edits = {};
|
|
26
|
+
const dsns = {};
|
|
27
|
+
|
|
28
|
+
for (const target of targets) {
|
|
29
|
+
const { slug } = projects[target];
|
|
30
|
+
const keys = await api.getProjectKeys(org, slug);
|
|
31
|
+
const active = (keys || []).find((key) => key.isActive !== false && key.dsn?.public);
|
|
32
|
+
|
|
33
|
+
if (!active) {
|
|
34
|
+
console.log(` ${chalk.yellow('⚠')} ${chalk.cyan(slug)} has no active client key — create one in Sentry, then rerun`);
|
|
35
|
+
return { status: 'warned', reason: `${slug} has no active client key — create one in Sentry`, output: { dsn: { missingKey: slug } } };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const dsn = active.dsn.public;
|
|
39
|
+
dsns[target] = dsn;
|
|
40
|
+
|
|
41
|
+
const current = brandConfig.targets?.[target]?.monitoring?.providers?.sentry?.dsn;
|
|
42
|
+
if (current === dsn) {
|
|
43
|
+
console.log(` ${chalk.green('✓')} ${chalk.cyan(target)} DSN in place`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
edits[`targets.${target}.monitoring.providers.sentry.dsn`] = dsn;
|
|
48
|
+
console.log(` ${chalk.yellow('↻')} ${chalk.cyan(target)} DSN ${current ? 'drifted — updating' : 'landing'}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
writeBrandConfig(context, edits);
|
|
52
|
+
|
|
53
|
+
return { output: { dsn: { landed: Object.keys(edits).length, targets: dsns } } };
|
|
54
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure the org, the brand's team, and one Sentry project per enabled
|
|
3
|
+
* target. Org resolution: monitoring.providers.sentry.org from config wins;
|
|
4
|
+
* otherwise the token's single org is used and written back there
|
|
5
|
+
* (self-heal), and a token seeing SEVERAL orgs is asked which one (#635). The team is the brand id; projects are `{brand.id}-{target}`
|
|
6
|
+
* with the target's Sentry platform. Existing projects (matched by slug)
|
|
7
|
+
* are converged proof — nothing is renamed or deleted here.
|
|
8
|
+
*/
|
|
9
|
+
const chalk = require('chalk').default;
|
|
10
|
+
const { dryRunPlan } = require('../../../lib/run-gates.js');
|
|
11
|
+
const { writeBrandConfig } = require('../../../lib/config-write.js');
|
|
12
|
+
const { resolveConfigValue } = require('../../../lib/config-flow.js');
|
|
13
|
+
|
|
14
|
+
// Sentry platform slugs per target type — validated server-side against the
|
|
15
|
+
// integration-docs index (Project.is_valid_platform), so only ids that exist
|
|
16
|
+
// there work: Electron's is the top-level 'electron', NOT the frontend-UI
|
|
17
|
+
// alias 'javascript-electron' (400 Invalid platform — cp136b, live find).
|
|
18
|
+
// Unlisted target types are not monitored.
|
|
19
|
+
const TARGET_PLATFORMS = {
|
|
20
|
+
web: 'javascript',
|
|
21
|
+
backend: 'node',
|
|
22
|
+
desktop: 'electron',
|
|
23
|
+
extension: 'javascript',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
module.exports = async function ensureProjects(context) {
|
|
27
|
+
const { sentryApi: api, brandConfig, options = {} } = context;
|
|
28
|
+
const brandId = brandConfig.brand.id;
|
|
29
|
+
|
|
30
|
+
// === Resolve the org (config wins; a lone token org self-heals into config) ===
|
|
31
|
+
const orgs = await api.getOrganizations();
|
|
32
|
+
const configured = brandConfig.monitoring.providers.sentry.org;
|
|
33
|
+
let org = configured
|
|
34
|
+
? orgs.find((candidate) => candidate.slug === configured)
|
|
35
|
+
: (orgs.length === 1 ? orgs[0] : null);
|
|
36
|
+
|
|
37
|
+
// Ambiguous: the token sees several orgs and config names none. That is a
|
|
38
|
+
// question, not a warning (#635) — the list IS the answer, so ASK it. Zero
|
|
39
|
+
// orgs offers no choice, and a configured-but-invisible org is a different
|
|
40
|
+
// problem, so both keep their warn below. The service already gated on its
|
|
41
|
+
// token, hence `gate: false`.
|
|
42
|
+
if (!org && !configured && orgs.length > 0) {
|
|
43
|
+
const picked = await resolveConfigValue(context, {
|
|
44
|
+
path: 'monitoring.providers.sentry.org',
|
|
45
|
+
label: 'Sentry organization',
|
|
46
|
+
choices: async () => orgs,
|
|
47
|
+
getName: (candidate) => candidate.slug,
|
|
48
|
+
getValue: (candidate) => candidate.slug,
|
|
49
|
+
disablePath: 'monitoring.enabled',
|
|
50
|
+
gate: false,
|
|
51
|
+
});
|
|
52
|
+
org = picked ? orgs.find((candidate) => candidate.slug === picked) : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!org) {
|
|
56
|
+
if (configured) {
|
|
57
|
+
console.log(` ${chalk.yellow('⚠')} monitoring.providers.sentry.org '${configured}' is not visible to this token ${chalk.dim(`(it sees: ${orgs.map((candidate) => candidate.slug).join(', ') || 'none'})`)}`);
|
|
58
|
+
} else {
|
|
59
|
+
console.log(` ${chalk.yellow('⚠')} The token sees ${orgs.length} orgs — set monitoring.providers.sentry.org in omega.json5 to pick one`);
|
|
60
|
+
}
|
|
61
|
+
return { status: 'warned', reason: 'no Sentry org resolved — set monitoring.providers.sentry.org', output: { projects: { orgUnresolved: true } } };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Multi-region SaaS: all org-scoped calls go to the org's home region
|
|
65
|
+
api.setRegionUrl(org.links?.regionUrl);
|
|
66
|
+
|
|
67
|
+
// Self-heal — skipped when the ask above already landed the pick (it patches
|
|
68
|
+
// the in-memory config too, so this reads the CURRENT state, not `configured`)
|
|
69
|
+
if (!brandConfig.monitoring.providers.sentry.org) {
|
|
70
|
+
writeBrandConfig(context, { 'monitoring.providers.sentry.org': org.slug });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const targets = Object.keys(brandConfig.targets || {}).filter((target) => TARGET_PLATFORMS[target]);
|
|
74
|
+
if (targets.length === 0) {
|
|
75
|
+
console.log(` ${chalk.dim('⊘ No monitorable targets enabled')}`);
|
|
76
|
+
return { state: { org: org.slug, projectMap: {} } };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const existing = await api.getProjects(org.slug);
|
|
80
|
+
|
|
81
|
+
const projects = {};
|
|
82
|
+
const planned = [];
|
|
83
|
+
let created = 0;
|
|
84
|
+
let synced = 0;
|
|
85
|
+
let teamEnsured = false;
|
|
86
|
+
|
|
87
|
+
for (const target of targets) {
|
|
88
|
+
const slug = `${brandId}-${target}`;
|
|
89
|
+
console.log(` ${chalk.dim('•')} ${chalk.cyan(slug)} ${chalk.dim(`(${TARGET_PLATFORMS[target]})`)}`);
|
|
90
|
+
|
|
91
|
+
const match = existing.find((project) => project.slug === slug);
|
|
92
|
+
if (match) {
|
|
93
|
+
console.log(` ${chalk.green('✓')} Exists`);
|
|
94
|
+
projects[target] = { id: match.id, slug: match.slug };
|
|
95
|
+
synced++;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (options.dryRun) {
|
|
100
|
+
dryRunPlan(`create project ${slug}`);
|
|
101
|
+
planned.push(slug);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The brand's team carries its projects — ensured once, on first need
|
|
106
|
+
if (!teamEnsured) {
|
|
107
|
+
const teams = await api.getTeams(org.slug);
|
|
108
|
+
if (!teams.find((team) => team.slug === brandId)) {
|
|
109
|
+
await api.createTeam(org.slug, brandId);
|
|
110
|
+
console.log(` ${chalk.green('✓')} Created team ${chalk.cyan(brandId)}`);
|
|
111
|
+
}
|
|
112
|
+
teamEnsured = true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const project = await api.createProject(org.slug, brandId, {
|
|
116
|
+
name: slug,
|
|
117
|
+
slug,
|
|
118
|
+
platform: TARGET_PLATFORMS[target],
|
|
119
|
+
});
|
|
120
|
+
console.log(` ${chalk.green('✓')} Created`);
|
|
121
|
+
projects[target] = { id: project.id, slug: project.slug };
|
|
122
|
+
created++;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const summary = planned.length > 0
|
|
126
|
+
? { synced, planned }
|
|
127
|
+
: { synced, created };
|
|
128
|
+
|
|
129
|
+
// State key is projectMap (certificateMap-style) — the output summary key
|
|
130
|
+
// `projects` would clobber it in the merged serviceData the dsn op reads.
|
|
131
|
+
return {
|
|
132
|
+
state: { org: org.slug, projectMap: projects },
|
|
133
|
+
output: { projects: summary },
|
|
134
|
+
};
|
|
135
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monitoring service (Sentry provider) — one error-monitoring project per enabled target
|
|
3
|
+
* (web/backend/desktop/extension), with each project's DSN landed in
|
|
4
|
+
* targets.<type>.monitoring.providers.sentry.dsn via the comment-preserving
|
|
5
|
+
* writeback — the exact key every framework's runtime reads through the config
|
|
6
|
+
* merge chain. DSNs are public by design (schema-pinned); only the auth token
|
|
7
|
+
* is a secret.
|
|
8
|
+
*
|
|
9
|
+
* Gates: `monitoring.providers.sentry` present (the monitor is a KEY under
|
|
10
|
+
* `providers` since #425 — no entry means none chosen and this service skips,
|
|
11
|
+
* exactly what a null provider meant) + SENTRY_AUTH_TOKEN in the brand .env —
|
|
12
|
+
* a PERSONAL auth token with org:read, project:read, project:write, team:read,
|
|
13
|
+
* team:write (Sentry's "organization tokens" are CI-scoped and cannot create
|
|
14
|
+
* teams or projects, cp136). The projects operation resolves the org (config
|
|
15
|
+
* wins, else the token's lone visible org) and writes it back to
|
|
16
|
+
* monitoring.providers.sentry.org.
|
|
17
|
+
*/
|
|
18
|
+
const { chosenProvider } = require('../../vendor/config/index.js');
|
|
19
|
+
|
|
20
|
+
const { serviceInputSpec } = require('../../config.js');
|
|
21
|
+
const { createServiceRunner } = require('../../lib/service-runner.js');
|
|
22
|
+
const { requestServiceInput } = require('../../lib/service-input.js');
|
|
23
|
+
const { SentryAPI } = require('./lib/sentry-api.js');
|
|
24
|
+
|
|
25
|
+
module.exports.run = createServiceRunner({
|
|
26
|
+
serviceDir: __dirname,
|
|
27
|
+
setup: async (context) => {
|
|
28
|
+
const monitoring = context.brandConfig.monitoring;
|
|
29
|
+
|
|
30
|
+
if (monitoring === false || monitoring?.enabled === false) {
|
|
31
|
+
return { skip: true, reason: 'monitoring disabled' };
|
|
32
|
+
}
|
|
33
|
+
if (!monitoring) {
|
|
34
|
+
return { skip: true, reason: 'no monitoring config' };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const provider = chosenProvider(monitoring.providers);
|
|
38
|
+
if (!provider) {
|
|
39
|
+
return { skip: true, reason: 'no monitoring.providers entry' };
|
|
40
|
+
}
|
|
41
|
+
if (provider !== 'sentry') {
|
|
42
|
+
return { skip: true, reason: `monitoring.providers.${provider} is not a known monitor` };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!context.sentryApi) {
|
|
46
|
+
const gate = await requestServiceInput(context, serviceInputSpec('monitoring'));
|
|
47
|
+
if (gate) return gate;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
sentryApi: context.sentryApi || new SentryAPI(),
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentry API client (api/0) — organizations, teams, projects, client keys.
|
|
3
|
+
* Named-method surface so tests can fake it method-for-method.
|
|
4
|
+
*
|
|
5
|
+
* Auth: SENTRY_AUTH_TOKEN in the brand .env (a PERSONAL auth token with
|
|
6
|
+
* project+team write scopes — org tokens are CI-scoped and cannot create
|
|
7
|
+
* teams/projects, cp136). Multi-region SaaS: every org carries
|
|
8
|
+
* links.regionUrl; setRegionUrl() repoints the client so all org-scoped
|
|
9
|
+
* calls hit the org's home region.
|
|
10
|
+
*/
|
|
11
|
+
const SENTRY_API_BASE = 'https://sentry.io/api/0';
|
|
12
|
+
|
|
13
|
+
class SentryAPI {
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.authToken = options.authToken || process.env.SENTRY_AUTH_TOKEN;
|
|
16
|
+
this.apiBase = SENTRY_API_BASE;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Repoint org-scoped calls at the org's home region (multi-region SaaS). */
|
|
20
|
+
setRegionUrl(regionUrl) {
|
|
21
|
+
if (regionUrl) {
|
|
22
|
+
this.apiBase = `${regionUrl.replace(/\/$/, '')}/api/0`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async makeRequest(endpoint, options = {}) {
|
|
27
|
+
const response = await fetch(`${this.apiBase}${endpoint}`, {
|
|
28
|
+
...options,
|
|
29
|
+
headers: {
|
|
30
|
+
'Authorization': `Bearer ${this.authToken}`,
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
...options.headers,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (response.status === 204) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const text = await response.text();
|
|
41
|
+
const data = text ? JSON.parse(text) : null;
|
|
42
|
+
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
// DRF validation errors have no detail/message — they're field-keyed
|
|
45
|
+
// ({"platform": ["Invalid platform"]}); serialize so the failure names
|
|
46
|
+
// its field instead of reading as a bare statusText.
|
|
47
|
+
const message = data?.detail
|
|
48
|
+
|| data?.message
|
|
49
|
+
|| (data ? JSON.stringify(data) : response.statusText);
|
|
50
|
+
throw new Error(`Sentry API error (${response.status}): ${message}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Organizations the token can see (an org token sees exactly one). */
|
|
57
|
+
async getOrganizations() {
|
|
58
|
+
return this.makeRequest('/organizations/');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Teams in the organization. */
|
|
62
|
+
async getTeams(orgSlug) {
|
|
63
|
+
return this.makeRequest(`/organizations/${orgSlug}/teams/`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async createTeam(orgSlug, slug) {
|
|
67
|
+
return this.makeRequest(`/organizations/${orgSlug}/teams/`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
body: JSON.stringify({ slug }),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Projects in the organization (first 100 — slugs are matched exactly,
|
|
75
|
+
* and a colliding create on a beyond-page project 409s honestly).
|
|
76
|
+
*/
|
|
77
|
+
async getProjects(orgSlug) {
|
|
78
|
+
return this.makeRequest(`/organizations/${orgSlug}/projects/?per_page=100`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async createProject(orgSlug, teamSlug, { name, slug, platform }) {
|
|
82
|
+
return this.makeRequest(`/teams/${orgSlug}/${teamSlug}/projects/`, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
body: JSON.stringify({ name, slug, platform }),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Client keys for a project (a fresh project always has one). */
|
|
89
|
+
async getProjectKeys(orgSlug, projectSlug) {
|
|
90
|
+
return this.makeRequest(`/projects/${orgSlug}/${projectSlug}/keys/`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { SentryAPI, SENTRY_API_BASE };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure @omega.js/backend's custom fields exist on the Beehiiv publication with the
|
|
3
|
+
* right kinds.
|
|
4
|
+
*
|
|
5
|
+
* The field list comes from @omega.js/backend's marketing SSOT (honoring each
|
|
6
|
+
* field's provider skip list — Beehiiv needs first/last name as custom
|
|
7
|
+
* fields, but tracks country and UTM source natively). Beehiiv matches
|
|
8
|
+
* subscriber values by display name, so fields are diffed by `display`. A
|
|
9
|
+
* kind mismatch can't be patched — the field is deleted and recreated.
|
|
10
|
+
* Fields @omega.js/backend doesn't own are never touched.
|
|
11
|
+
*/
|
|
12
|
+
const chalk = require('chalk').default;
|
|
13
|
+
const { fieldsFor } = require('../../../lib/backend-marketing.js');
|
|
14
|
+
const { dryRunPlan } = require('../../../lib/run-gates.js');
|
|
15
|
+
|
|
16
|
+
const BEEHIIV_FIELDS = fieldsFor('beehiiv');
|
|
17
|
+
|
|
18
|
+
// @omega.js/backend type → Beehiiv kind
|
|
19
|
+
const KIND_MAP = { text: 'string', number: 'integer', date: 'datetime' };
|
|
20
|
+
|
|
21
|
+
module.exports = async function ensureCustomFields(context) {
|
|
22
|
+
const { beehiivApi: api, serviceData, options = {} } = context;
|
|
23
|
+
|
|
24
|
+
const publicationId = serviceData.publicationId;
|
|
25
|
+
if (!publicationId) {
|
|
26
|
+
console.log(chalk.dim(' ⊘ No publication yet — nothing to provision fields on'));
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const existing = await api.getCustomFields(publicationId);
|
|
31
|
+
const existingByDisplay = Object.fromEntries(existing.map((f) => [f.display, f]));
|
|
32
|
+
|
|
33
|
+
const missing = [];
|
|
34
|
+
const mismatched = [];
|
|
35
|
+
|
|
36
|
+
for (const field of BEEHIIV_FIELDS) {
|
|
37
|
+
const bhField = existingByDisplay[field.display];
|
|
38
|
+
|
|
39
|
+
if (!bhField) {
|
|
40
|
+
missing.push(field);
|
|
41
|
+
} else if (bhField.kind !== KIND_MAP[field.type]) {
|
|
42
|
+
mismatched.push({ ...field, existingId: bhField.id, existingKind: bhField.kind });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (missing.length === 0 && mismatched.length === 0) {
|
|
47
|
+
console.log(` ${chalk.green('✓')} All ${BEEHIIV_FIELDS.length} custom fields exist`);
|
|
48
|
+
return { output: { customFields: { total: BEEHIIV_FIELDS.length, created: 0, recreated: 0 } } };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (options.dryRun) {
|
|
52
|
+
const planned = {
|
|
53
|
+
create: missing.map((f) => f.display),
|
|
54
|
+
recreate: mismatched.map((f) => `${f.display} (${f.existingKind} → ${KIND_MAP[f.type]})`),
|
|
55
|
+
};
|
|
56
|
+
return dryRunPlan(`create ${planned.create.length}, recreate ${planned.recreate.length} field(s)`, { output: { customFields: { planned } } });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Kind mismatches: delete, then recreate alongside the missing ones
|
|
60
|
+
for (const field of mismatched) {
|
|
61
|
+
await api.deleteCustomField(publicationId, field.existingId);
|
|
62
|
+
console.log(` ${chalk.yellow('↻')} Deleted ${chalk.cyan(field.display)} ${chalk.dim(`(was ${field.existingKind}, need ${KIND_MAP[field.type]})`)}`);
|
|
63
|
+
missing.push(field);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const field of missing) {
|
|
67
|
+
await api.createCustomField(publicationId, field.name, field.display, KIND_MAP[field.type]);
|
|
68
|
+
console.log(` ${chalk.green('✓')} Created ${chalk.cyan(field.display)}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Verify everything actually persisted
|
|
72
|
+
const verify = await api.getCustomFields(publicationId);
|
|
73
|
+
const verifyByDisplay = new Set(verify.map((f) => f.display));
|
|
74
|
+
const stillMissing = BEEHIIV_FIELDS.filter((f) => !verifyByDisplay.has(f.display));
|
|
75
|
+
|
|
76
|
+
if (stillMissing.length > 0) {
|
|
77
|
+
console.log(` ${chalk.yellow('⚠')} ${stillMissing.length} field(s) failed to persist: ${chalk.cyan(stillMissing.map((f) => f.display).join(', '))}`);
|
|
78
|
+
return { status: 'warned', reason: `${stillMissing.length} field(s) failed to persist: ${stillMissing.map((f) => f.display).join(', ')}`, output: { customFields: { total: BEEHIIV_FIELDS.length, failed: stillMissing.map((f) => f.display) } } };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
output: {
|
|
83
|
+
customFields: {
|
|
84
|
+
total: BEEHIIV_FIELDS.length,
|
|
85
|
+
created: missing.length - mismatched.length,
|
|
86
|
+
recreated: mismatched.length,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure the brand's Beehiiv publication is configured and accessible.
|
|
3
|
+
*
|
|
4
|
+
* Resolution: marketing.newsletter.providers.beehiiv.publicationId from
|
|
5
|
+
* config → auto-match by brand name/id across the account's publications.
|
|
6
|
+
* Publications can't be created via API — when nothing matches, the exact
|
|
7
|
+
* values to copy into the dashboard are printed, and interactive runs open
|
|
8
|
+
* the create page and poll until the new publication auto-matches;
|
|
9
|
+
* non-interactive/dry runs warn instead. The resolved id is written back
|
|
10
|
+
* into omega.json5 (marketing.newsletter.providers.beehiiv.publicationId —
|
|
11
|
+
* comment-preserving), its ONE authoritative home.
|
|
12
|
+
*/
|
|
13
|
+
const chalk = require('chalk').default;
|
|
14
|
+
const { openBrowserAndPoll } = require('../../../vendor/devkit/flows.js');
|
|
15
|
+
const { writeBrandConfig } = require('../../../lib/config-write.js');
|
|
16
|
+
const { canPrompt } = require('../../../lib/run-gates.js');
|
|
17
|
+
|
|
18
|
+
const CREATE_URL = 'https://app.beehiiv.com/settings/workspace/overview?create_publication=true';
|
|
19
|
+
|
|
20
|
+
module.exports = async function ensurePublication(context) {
|
|
21
|
+
const { beehiivApi: api, brandConfig } = context;
|
|
22
|
+
|
|
23
|
+
const brandName = brandConfig.brand?.name || '';
|
|
24
|
+
const brandId = brandConfig.brand?.id || '';
|
|
25
|
+
const configuredId = brandConfig.marketing?.newsletter?.providers?.beehiiv?.publicationId;
|
|
26
|
+
|
|
27
|
+
// 1. Configured id — verify access
|
|
28
|
+
if (configuredId) {
|
|
29
|
+
const publication = await api.getPublication(configuredId);
|
|
30
|
+
|
|
31
|
+
if (!publication) {
|
|
32
|
+
console.log(` ${chalk.yellow('⚠')} Publication ${chalk.cyan(configuredId)} is not accessible with this API key — fix the id or the key, then rerun`);
|
|
33
|
+
return { status: 'warned', reason: 'the configured publication is not accessible with this API key', output: { publication: { publicationId: configuredId, accessible: false } } };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(` ${chalk.green('✓')} ${chalk.cyan(publication.name)} ${chalk.dim(`(${publication.id})`)}`);
|
|
37
|
+
if (publication.id !== configuredId) {
|
|
38
|
+
writeBrandConfig(context, { 'marketing.newsletter.providers.beehiiv.publicationId': publication.id });
|
|
39
|
+
}
|
|
40
|
+
return { state: { publicationId: publication.id, publicationName: publication.name } };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2. Auto-match by brand name/id
|
|
44
|
+
const nameLower = brandName.toLowerCase();
|
|
45
|
+
const idLower = brandId.toLowerCase();
|
|
46
|
+
const matchesBrand = (pub) => {
|
|
47
|
+
const pubName = (pub.name || '').toLowerCase();
|
|
48
|
+
return pubName.includes(nameLower)
|
|
49
|
+
|| pubName.includes(idLower)
|
|
50
|
+
|| nameLower.includes(pubName)
|
|
51
|
+
|| idLower.includes(pubName.replace(/\s+/g, '-'));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const publications = await api.listPublications();
|
|
55
|
+
const match = publications.find(matchesBrand);
|
|
56
|
+
|
|
57
|
+
if (match) {
|
|
58
|
+
console.log(` ${chalk.green('✓')} Auto-matched ${chalk.cyan(match.name)} ${chalk.dim(`(${match.id})`)}`);
|
|
59
|
+
writeBrandConfig(context, { 'marketing.newsletter.providers.beehiiv.publicationId': match.id });
|
|
60
|
+
return { state: { publicationId: match.id, publicationName: match.name } };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 3. Nothing matches — publications are dashboard-only, print the values to copy
|
|
64
|
+
console.log(` ${chalk.yellow('⚠')} No publication matches ${chalk.cyan(brandName)} — create one at ${chalk.cyan(CREATE_URL)}:`);
|
|
65
|
+
console.log(` ${chalk.dim('Publication name:')} ${chalk.cyan(brandName)}`);
|
|
66
|
+
console.log(` ${chalk.dim('This publication is...:')} ${chalk.cyan(brandConfig.brand?.description || `News and updates from ${brandName}`)}`);
|
|
67
|
+
console.log(` ${chalk.dim('Subdomain:')} ${chalk.cyan(brandId)}`);
|
|
68
|
+
|
|
69
|
+
// Interactive runs: open the dashboard and poll until the publication
|
|
70
|
+
// appears (same auto-match), then land it in omega.json5
|
|
71
|
+
if (canPrompt(context.options)) {
|
|
72
|
+
const result = await openBrowserAndPoll({
|
|
73
|
+
url: CREATE_URL,
|
|
74
|
+
label: 'the Beehiiv workspace settings',
|
|
75
|
+
promptMessage: `Create the publication for ${chalk.cyan(brandName)} with the values above.`,
|
|
76
|
+
waitMessage: 'Waiting for the publication to appear',
|
|
77
|
+
check: async () => {
|
|
78
|
+
const fresh = (await api.listPublications()).find(matchesBrand);
|
|
79
|
+
return fresh ? { done: true, result: fresh } : { done: false };
|
|
80
|
+
},
|
|
81
|
+
intervalMs: 10000,
|
|
82
|
+
indent: ' ',
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (result.success && result.result) {
|
|
86
|
+
const created = result.result;
|
|
87
|
+
console.log(` ${chalk.green('✓')} ${chalk.cyan(created.name)} ${chalk.dim(`(${created.id})`)}`);
|
|
88
|
+
writeBrandConfig(context, { 'marketing.newsletter.providers.beehiiv.publicationId': created.id });
|
|
89
|
+
return { state: { publicationId: created.id, publicationName: created.name } };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { status: 'warned', reason: 'no publication yet — create one in the beehiiv dashboard', output: { publication: { missing: true, suggestedName: brandName } } };
|
|
94
|
+
};
|