@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,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure Firebase Hosting serves the brand's API domains.
|
|
3
|
+
*
|
|
4
|
+
* The default hosting site ({projectId}) gets api.{domain} (Cloud Functions'
|
|
5
|
+
* public endpoint via Firebase Hosting rewrites; a brand whose api domain
|
|
6
|
+
* fronts a dedicated non-Firebase backend routes /omega through the
|
|
7
|
+
* cloudflare omega-api-proxy worker instead). ONE api domain, shared by every
|
|
8
|
+
* web instance the brand runs (#588). The main domain is NOT added — the
|
|
9
|
+
* website hosts elsewhere (GitHub Pages).
|
|
10
|
+
*
|
|
11
|
+
* Per domain, one reconciliation pass:
|
|
12
|
+
* verified → ensure the Cloudflare CNAME has the right proxy state:
|
|
13
|
+
* proxied when the zone's TLS coverage reaches the name.
|
|
14
|
+
* Universal SSL (every plan) covers the apex + one label;
|
|
15
|
+
* deeper names (api.{sub}.{zone} — subdomain projects) ask
|
|
16
|
+
* the zone what paid coverage it ACTUALLY has (Total TLS,
|
|
17
|
+
* Advanced certificate packs — plans differ per brand).
|
|
18
|
+
* Uncovered names stay DNS-only and Firebase serves the
|
|
19
|
+
* certificate instead
|
|
20
|
+
* pending → write Firebase's required DNS records (TXT ownership/ACME +
|
|
21
|
+
* unproxied CNAME); interactive runs then poll until Firebase
|
|
22
|
+
* verifies (writing any NEW records it demands mid-poll — the
|
|
23
|
+
* ACME challenge appears once ownership passes) and finish by
|
|
24
|
+
* setting the CNAME's final proxy state; non-interactive/dry
|
|
25
|
+
* runs report state, warned — rerun converges
|
|
26
|
+
* deleted → undelete, then treat as pending
|
|
27
|
+
* missing → create, then treat as pending
|
|
28
|
+
*
|
|
29
|
+
* DNS writes go through Cloudflare (context.cloudflareApi). No token → the
|
|
30
|
+
* required records are printed instead and the operation warns.
|
|
31
|
+
*/
|
|
32
|
+
const chalk = require('chalk').default;
|
|
33
|
+
const { pollWithSpinner } = require('../../../vendor/devkit/flows.js');
|
|
34
|
+
const { canPrompt, dryRunPlan } = require('../../../lib/run-gates.js');
|
|
35
|
+
|
|
36
|
+
module.exports = async function ensureHosting(context) {
|
|
37
|
+
const { firebaseApi: api, cloudflareApi, brandConfig, projectId, domain, apexDomain, isSubdomainProject, options = {} } = context;
|
|
38
|
+
|
|
39
|
+
if (brandConfig.cloud?.apiSubdomain === false) {
|
|
40
|
+
console.log(chalk.dim(' ⊘ API subdomain disabled in config, skipping'));
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Cloudflare zone for DNS writes (apex zone covers subdomain projects)
|
|
45
|
+
const zone = cloudflareApi ? await cloudflareApi.getZoneByName(apexDomain) : null;
|
|
46
|
+
if (cloudflareApi && !zone) {
|
|
47
|
+
console.log(` ${chalk.yellow('⚠')} No Cloudflare zone found for ${chalk.cyan(apexDomain)}`);
|
|
48
|
+
return { status: 'warned', reason: 'no Cloudflare zone for the apex domain', output: { hosting: { note: 'no Cloudflare zone' } } };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// API domains to ensure. For subdomain projects (app.brand.com) the DNS
|
|
52
|
+
// record names stay relative to the apex zone (api.app, not api).
|
|
53
|
+
const subdomainPrefix = isSubdomainProject ? domain.replace(`.${apexDomain}`, '') : '';
|
|
54
|
+
const apiDomains = [{
|
|
55
|
+
fullDomain: `api.${domain}`,
|
|
56
|
+
recordName: subdomainPrefix ? `api.${subdomainPrefix}` : 'api',
|
|
57
|
+
}];
|
|
58
|
+
|
|
59
|
+
// === Hosting site ===
|
|
60
|
+
const sites = await api.listHostingSites(projectId);
|
|
61
|
+
let site = sites.find((s) => s.name?.includes(projectId));
|
|
62
|
+
|
|
63
|
+
if (site) {
|
|
64
|
+
console.log(` ${chalk.green('✓')} Site exists: ${chalk.cyan(projectId)}`);
|
|
65
|
+
} else if (options.dryRun) {
|
|
66
|
+
return dryRunPlan(`create hosting site ${projectId}`, { output: { hosting: { planned: 'create-site' } } });
|
|
67
|
+
} else {
|
|
68
|
+
console.log(' Creating hosting site...');
|
|
69
|
+
try {
|
|
70
|
+
site = await api.createHostingSite(projectId, projectId);
|
|
71
|
+
console.log(` ${chalk.green('✓')} Created site: ${chalk.cyan(projectId)}`);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (!error.message?.includes('already exists')) {
|
|
74
|
+
console.log(` ${chalk.yellow('⚠')} Could not create site${chalk.dim(`: ${error.message}`)}`);
|
|
75
|
+
return { status: 'warned', reason: 'could not create the hosting site', output: { hosting: { error: error.message } } };
|
|
76
|
+
}
|
|
77
|
+
console.log(` ${chalk.green('✓')} Site exists: ${chalk.cyan(projectId)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// === Custom domains (one pass each) ===
|
|
82
|
+
const results = [];
|
|
83
|
+
for (const apiDomain of apiDomains) {
|
|
84
|
+
results.push(await ensureApiDomain(context, zone, apiDomain));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const pending = results.filter((r) => r.status !== 'verified');
|
|
88
|
+
return {
|
|
89
|
+
status: pending.length > 0 ? 'warned' : 'success',
|
|
90
|
+
...(pending.length > 0 ? { reason: `${pending.length} API domain(s) not verified yet` } : {}),
|
|
91
|
+
state: { hosting: { siteId: projectId, domains: results } },
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// ─── TLS coverage (proxy eligibility) ────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* TLS certificate-host matching: an exact host matches itself; a wildcard
|
|
99
|
+
* (`*.X`) covers exactly ONE label below X — standard certificate semantics,
|
|
100
|
+
* a wildcard never spans two levels.
|
|
101
|
+
*/
|
|
102
|
+
function certHostMatches(certHost, fullDomain) {
|
|
103
|
+
if (certHost === fullDomain) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
if (!certHost.startsWith('*.')) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const base = certHost.slice(2);
|
|
110
|
+
return fullDomain.endsWith(`.${base}`)
|
|
111
|
+
&& !fullDomain.slice(0, -(base.length + 1)).includes('.');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Cloudflare Universal SSL (every plan, free included) covers the apex and
|
|
116
|
+
* ONE label below it (*.zone) — never deeper.
|
|
117
|
+
*/
|
|
118
|
+
function universalSslCovers(fullDomain, zoneName) {
|
|
119
|
+
return fullDomain === zoneName || certHostMatches(`*.${zoneName}`, fullDomain);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Whether the zone can terminate TLS for fullDomain at the proxy. Universal
|
|
124
|
+
* SSL answers statically; deeper names (api.playground.zone) ask the zone
|
|
125
|
+
* what coverage it ACTUALLY has — plans differ per brand: Total TLS mints a
|
|
126
|
+
* certificate for every proxied hostname, and Advanced certificate packs can
|
|
127
|
+
* carry deeper wildcards. Lookup failures fall back to not-covered — a
|
|
128
|
+
* DNS-only record works on every plan, a wrongly-proxied one never does.
|
|
129
|
+
*
|
|
130
|
+
* @returns {{ covered: boolean, via: string|null }}
|
|
131
|
+
*/
|
|
132
|
+
async function zoneTlsCoverage(cloudflareApi, zone, fullDomain) {
|
|
133
|
+
if (universalSslCovers(fullDomain, zone.name)) {
|
|
134
|
+
return { covered: true, via: 'universal' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const data = await cloudflareApi.makeRequest(`/zones/${zone.id}/acm/total_tls`);
|
|
139
|
+
if (data.result?.enabled) {
|
|
140
|
+
return { covered: true, via: 'Total TLS' };
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// No ACM entitlement — the endpoint rejects on free zones
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const data = await cloudflareApi.makeRequest(`/zones/${zone.id}/ssl/certificate_packs`);
|
|
148
|
+
const packs = Array.isArray(data.result) ? data.result : [];
|
|
149
|
+
const pack = packs.find((p) => p.status === 'active'
|
|
150
|
+
&& (p.hosts || []).some((host) => certHostMatches(host, fullDomain)));
|
|
151
|
+
if (pack) {
|
|
152
|
+
return { covered: true, via: `the ${pack.type} certificate pack` };
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// Coverage unknown — treat as uncovered
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { covered: false, via: null };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* One reconciliation pass over a single API domain.
|
|
163
|
+
*/
|
|
164
|
+
async function ensureApiDomain(context, zone, apiDomain) {
|
|
165
|
+
const { firebaseApi: api, cloudflareApi, projectId, apexDomain, options = {} } = context;
|
|
166
|
+
const { fullDomain, recordName } = apiDomain;
|
|
167
|
+
|
|
168
|
+
// Can the proxy terminate TLS for this name? (No Cloudflare client → moot;
|
|
169
|
+
// the DNS helpers only print manual instructions.)
|
|
170
|
+
const tls = zone
|
|
171
|
+
? await zoneTlsCoverage(cloudflareApi, zone, fullDomain)
|
|
172
|
+
: { covered: true, via: null };
|
|
173
|
+
|
|
174
|
+
let status = await api.checkDomainStatus(projectId, projectId, fullDomain);
|
|
175
|
+
|
|
176
|
+
// Fully verified — just make sure the CNAME has the right proxy state
|
|
177
|
+
if (status.verified) {
|
|
178
|
+
console.log(` ${chalk.green('✓')} Domain verified: ${chalk.cyan(fullDomain)}`);
|
|
179
|
+
if (!tls.covered) {
|
|
180
|
+
console.log(` ${chalk.dim(`No certificate covers ${fullDomain} at the proxy (Universal SSL stops at *.${zone.name}; no Total TLS or deeper cert pack on this zone) — CNAME stays DNS-only, Firebase serves the certificate`)}`);
|
|
181
|
+
} else if (tls.via && tls.via !== 'universal') {
|
|
182
|
+
console.log(` ${chalk.dim(`${fullDomain} is deeper than *.${zone.name} but ${tls.via} covers it — proxying`)}`);
|
|
183
|
+
}
|
|
184
|
+
await ensureCname(context, zone, recordName, { proxied: tls.covered, tlsCovered: tls.covered });
|
|
185
|
+
return { domain: fullDomain, status: 'verified' };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Soft-deleted → undelete; missing → create. Both then converge as pending.
|
|
189
|
+
if (!status.exists) {
|
|
190
|
+
if (options.dryRun) {
|
|
191
|
+
return dryRunPlan(`${status.deleted ? 'restore' : 'add'} domain ${fullDomain}`, { domain: fullDomain, status: 'planned' });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
if (status.deleted) {
|
|
196
|
+
console.log(` Restoring deleted domain: ${chalk.cyan(fullDomain)}...`);
|
|
197
|
+
const claim = await api.undeleteCustomDomain(projectId, projectId, fullDomain);
|
|
198
|
+
console.log(` ${chalk.green('✓')} Restored domain${claim?.pending ? chalk.dim(' (claim pending DNS verification)') : ''}`);
|
|
199
|
+
} else {
|
|
200
|
+
console.log(` Adding domain: ${chalk.cyan(fullDomain)}...`);
|
|
201
|
+
// A claim still running is pending DNS verification, a normal state (#56)
|
|
202
|
+
const claim = await api.createCustomDomain(projectId, projectId, fullDomain);
|
|
203
|
+
console.log(` ${chalk.green('✓')} Added domain to Firebase${claim?.pending ? chalk.dim(' (claim pending DNS verification)') : ''}`);
|
|
204
|
+
}
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (!error.message?.includes('already exists')) {
|
|
207
|
+
console.log(` ${chalk.yellow('⚠')} Could not add domain${chalk.dim(`: ${error.message}`)}`);
|
|
208
|
+
return { domain: fullDomain, status: 'error', error: error.message };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
status = await api.checkDomainStatus(projectId, projectId, fullDomain);
|
|
213
|
+
} else {
|
|
214
|
+
console.log(` ${chalk.yellow('⏳')} Domain pending: ${chalk.cyan(fullDomain)} ${chalk.dim(`(${status.ownershipState || 'ownership pending'}, ${status.hostState || 'host pending'})`)}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Write the DNS records Firebase requires (TXT ownership + ACME challenge;
|
|
218
|
+
// A/AAAA skipped — the CNAME to {projectId}.web.app replaces them)
|
|
219
|
+
await ensureFirebaseDnsRecords(context, zone, status.requiredDnsUpdates, recordName);
|
|
220
|
+
await ensureCname(context, zone, recordName, { proxied: false, tlsCovered: tls.covered });
|
|
221
|
+
|
|
222
|
+
// Interactive runs wait for Firebase to verify (DNS propagation)
|
|
223
|
+
if (canPrompt(options)) {
|
|
224
|
+
const verified = await waitForVerification(context, zone, fullDomain, recordName, status);
|
|
225
|
+
if (verified) {
|
|
226
|
+
console.log(` ${chalk.green('✓')} Domain verified: ${chalk.cyan(fullDomain)}`);
|
|
227
|
+
await ensureCname(context, zone, recordName, { proxied: tls.covered, tlsCovered: tls.covered });
|
|
228
|
+
return { domain: fullDomain, status: 'verified' };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log(` ${chalk.dim('→')} Rerun after DNS propagates — verification converges on a later pass`);
|
|
233
|
+
return { domain: fullDomain, status: 'pending' };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Poll Firebase until the domain verifies. Firebase can demand NEW DNS
|
|
238
|
+
* records mid-verification (the ACME challenge appears once ownership
|
|
239
|
+
* passes) — those are written as they show up.
|
|
240
|
+
*
|
|
241
|
+
* @returns {boolean} - Whether the domain reached verified
|
|
242
|
+
*/
|
|
243
|
+
async function waitForVerification(context, zone, fullDomain, recordName, initialStatus) {
|
|
244
|
+
const { firebaseApi: api, projectId } = context;
|
|
245
|
+
let lastStates = '';
|
|
246
|
+
let lastDnsUpdateCount = (initialStatus.requiredDnsUpdates || []).length;
|
|
247
|
+
|
|
248
|
+
const result = await pollWithSpinner({
|
|
249
|
+
check: async () => {
|
|
250
|
+
const status = await api.checkDomainStatus(projectId, projectId, fullDomain);
|
|
251
|
+
|
|
252
|
+
if (status.verified) {
|
|
253
|
+
return { done: true };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const dnsUpdates = status.requiredDnsUpdates || [];
|
|
257
|
+
if (dnsUpdates.length > 0 && dnsUpdates.length !== lastDnsUpdateCount) {
|
|
258
|
+
console.log(` ${chalk.dim('→')} Firebase requires new DNS records — writing...`);
|
|
259
|
+
await ensureFirebaseDnsRecords(context, zone, dnsUpdates, recordName);
|
|
260
|
+
lastDnsUpdateCount = dnsUpdates.length;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const states = `${status.ownershipState || 'ownership pending'}, ${status.hostState || 'host pending'}${status.certState ? `, ${status.certState}` : ''}`;
|
|
264
|
+
if (states !== lastStates) {
|
|
265
|
+
console.log(` ${chalk.dim(`→ ${states}`)}`);
|
|
266
|
+
lastStates = states;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return { done: false };
|
|
270
|
+
},
|
|
271
|
+
intervalMs: 5000,
|
|
272
|
+
message: 'Waiting for DNS propagation',
|
|
273
|
+
indent: ' ',
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
return result.success;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ─── Cloudflare DNS helpers (via the zone-scoped REST endpoints) ─────────────
|
|
280
|
+
|
|
281
|
+
async function findDnsRecord(cloudflareApi, zoneId, name, type) {
|
|
282
|
+
const data = await cloudflareApi.makeRequest(`/zones/${zoneId}/dns_records?type=${type}&name=${encodeURIComponent(name)}`);
|
|
283
|
+
return data.result?.[0] || null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Write Firebase's requiredDnsUpdates into the zone (skipping A/AAAA).
|
|
288
|
+
* Without a Cloudflare client the records are printed for manual setup.
|
|
289
|
+
*/
|
|
290
|
+
async function ensureFirebaseDnsRecords(context, zone, requiredDnsUpdates, recordName) {
|
|
291
|
+
const { cloudflareApi, apexDomain, options = {} } = context;
|
|
292
|
+
const updates = Array.isArray(requiredDnsUpdates) ? requiredDnsUpdates : [];
|
|
293
|
+
|
|
294
|
+
for (const update of updates) {
|
|
295
|
+
const { domainName } = update;
|
|
296
|
+
const records = update.desired?.records || [];
|
|
297
|
+
|
|
298
|
+
for (const record of records) {
|
|
299
|
+
const { type, rdata } = record;
|
|
300
|
+
|
|
301
|
+
if (type === 'A' || type === 'AAAA') {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!cloudflareApi) {
|
|
306
|
+
console.log(` ${chalk.yellow('→')} Set manually: ${chalk.cyan(type)} ${chalk.cyan(domainName)} → ${rdata}`);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (options.dryRun) {
|
|
311
|
+
console.log(` ${chalk.dim(`⊘ Dry run — would ensure ${type} ${domainName}`)}`);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const comment = type === 'TXT' && domainName.includes('_acme-challenge')
|
|
316
|
+
? 'Firebase SSL certificate verification'
|
|
317
|
+
: 'Firebase Hosting';
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const existing = await findDnsRecord(cloudflareApi, zone.id, domainName, type);
|
|
321
|
+
|
|
322
|
+
if (existing) {
|
|
323
|
+
if (existing.content !== rdata) {
|
|
324
|
+
await cloudflareApi.makeRequest(`/zones/${zone.id}/dns_records/${existing.id}`, {
|
|
325
|
+
method: 'PATCH',
|
|
326
|
+
body: JSON.stringify({ content: rdata, comment, ttl: 60 }),
|
|
327
|
+
});
|
|
328
|
+
console.log(` ${chalk.green('✓')} Updated ${chalk.cyan(type)}: ${chalk.cyan(domainName)}`);
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
const name = domainName === apexDomain ? apexDomain : domainName.replace(`.${apexDomain}`, '');
|
|
332
|
+
await cloudflareApi.makeRequest(`/zones/${zone.id}/dns_records`, {
|
|
333
|
+
method: 'POST',
|
|
334
|
+
body: JSON.stringify({ type, name, content: rdata, comment, ttl: 60 }),
|
|
335
|
+
});
|
|
336
|
+
console.log(` ${chalk.green('✓')} Created ${chalk.cyan(type)}: ${chalk.cyan(domainName)}`);
|
|
337
|
+
}
|
|
338
|
+
} catch (error) {
|
|
339
|
+
console.log(` ${chalk.yellow('⚠')} ${chalk.cyan(type)} record error for ${chalk.cyan(domainName)}${chalk.dim(`: ${error.message}`)}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Ensure the CNAME to {projectId}.web.app exists with the desired proxy state
|
|
347
|
+
* (unproxied while verification is pending, proxied once verified).
|
|
348
|
+
*/
|
|
349
|
+
async function ensureCname(context, zone, recordName, { proxied, tlsCovered = true }) {
|
|
350
|
+
const { cloudflareApi, projectId, apexDomain, options = {} } = context;
|
|
351
|
+
const cnameTarget = `${projectId}.web.app`;
|
|
352
|
+
const fullRecordName = `${recordName}.${apexDomain}`;
|
|
353
|
+
|
|
354
|
+
if (!cloudflareApi) {
|
|
355
|
+
console.log(` ${chalk.yellow('→')} Set manually: ${chalk.cyan('CNAME')} ${chalk.cyan(fullRecordName)} → ${cnameTarget}`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const existing = await findDnsRecord(cloudflareApi, zone.id, fullRecordName, 'CNAME');
|
|
361
|
+
|
|
362
|
+
// Never un-proxy a working record (verify-cycle flapping) — EXCEPT when
|
|
363
|
+
// the zone's TLS coverage can't reach the name, where proxied can never
|
|
364
|
+
// complete a handshake and the record must come back down to DNS-only.
|
|
365
|
+
const needsUpdate = existing
|
|
366
|
+
&& (existing.content !== cnameTarget
|
|
367
|
+
|| (proxied === true && existing.proxied === false)
|
|
368
|
+
|| (proxied === false && existing.proxied === true && !tlsCovered));
|
|
369
|
+
|
|
370
|
+
if (existing && !needsUpdate) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (options.dryRun) {
|
|
375
|
+
console.log(` ${chalk.dim(`⊘ Dry run — would ${existing ? 'update' : 'create'} CNAME ${fullRecordName} → ${cnameTarget}`)}`);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (existing) {
|
|
380
|
+
await cloudflareApi.makeRequest(`/zones/${zone.id}/dns_records/${existing.id}`, {
|
|
381
|
+
method: 'PATCH',
|
|
382
|
+
body: JSON.stringify({ content: cnameTarget, comment: 'Firebase Hosting', ttl: 60, proxied }),
|
|
383
|
+
});
|
|
384
|
+
console.log(` ${chalk.green('✓')} Updated CNAME: ${chalk.cyan(recordName)} → ${chalk.cyan(cnameTarget)} ${chalk.dim(`(proxied: ${proxied})`)}`);
|
|
385
|
+
} else {
|
|
386
|
+
await cloudflareApi.makeRequest(`/zones/${zone.id}/dns_records`, {
|
|
387
|
+
method: 'POST',
|
|
388
|
+
body: JSON.stringify({ type: 'CNAME', name: recordName, content: cnameTarget, comment: 'Firebase Hosting', ttl: 60, proxied }),
|
|
389
|
+
});
|
|
390
|
+
console.log(` ${chalk.green('✓')} Created CNAME: ${chalk.cyan(recordName)} → ${chalk.cyan(cnameTarget)} ${chalk.dim(`(proxied: ${proxied})`)}`);
|
|
391
|
+
}
|
|
392
|
+
} catch (error) {
|
|
393
|
+
console.log(` ${chalk.yellow('⚠')} CNAME error for ${chalk.cyan(recordName)}${chalk.dim(`: ${error.message}`)}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
module.exports.universalSslCovers = universalSslCovers;
|
|
398
|
+
module.exports.certHostMatches = certHostMatches;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure the OAuth consent screen (IAP brand) exists: application title +
|
|
3
|
+
* support email (required for Google sign-in).
|
|
4
|
+
*
|
|
5
|
+
* No OAuth clients here — IAP-created clients are locked (no redirect URIs);
|
|
6
|
+
* Firebase auto-creates the client when Google sign-in is enabled.
|
|
7
|
+
*
|
|
8
|
+
* Google only accepts a supportEmail the AUTHORIZING USER owns (their own
|
|
9
|
+
* email or a Google Group they manage) — anything else is "Request contains
|
|
10
|
+
* an invalid argument" (friction #29; the old support@{domain} default could
|
|
11
|
+
* never work). `cloud.supportEmail` in config wins (the Google-Group
|
|
12
|
+
* case); the default is the authenticated user's own email via the
|
|
13
|
+
* userinfo.email scope. Neither available → warn with guidance, never send
|
|
14
|
+
* a doomed value.
|
|
15
|
+
*
|
|
16
|
+
* The screen's AUDIENCE is a manage-time STOPPER (#667): Internal means only
|
|
17
|
+
* the owning org's Workspace users may sign in, which is `Error 403:
|
|
18
|
+
* org_internal` for every Gmail account, and Google gives NO API write for
|
|
19
|
+
* it (proven live: PATCH is a 404, the create's orgInternalOnly is
|
|
20
|
+
* output-only, and an API-made brand is born Internal). So an interactive
|
|
21
|
+
* run STOPS on it — open the console page and poll until it flips — instead
|
|
22
|
+
* of attempting a write that cannot exist.
|
|
23
|
+
*
|
|
24
|
+
* The screen's BRANDING page (logo, home/privacy/terms links, authorized
|
|
25
|
+
* domains) has no API either, and nothing here can reconcile it — so the run
|
|
26
|
+
* NAMES it as a manual step with the one line that matters (#696).
|
|
27
|
+
*/
|
|
28
|
+
const chalk = require('chalk').default;
|
|
29
|
+
const { openBrowserAndPoll } = require('../../../vendor/devkit/flows.js');
|
|
30
|
+
const { canPrompt, dryRunPlan } = require('../../../lib/run-gates.js');
|
|
31
|
+
const { confirmSetup, readTriState } = require('../../../lib/config-flow.js');
|
|
32
|
+
|
|
33
|
+
// Where the gate's disable outcome lands the tri-state `false` (#608/#33).
|
|
34
|
+
// The AUDIENCE opts out on its own — a brand that accepts an org-only sign-in
|
|
35
|
+
// still wants every other cloud operation, so this is never cloud.enabled.
|
|
36
|
+
const AUDIENCE_PATH = 'cloud.consentAudience';
|
|
37
|
+
|
|
38
|
+
// The step-aside reason the run summary's warned breakdown reads (#643)
|
|
39
|
+
const INTERNAL_REASON = 'consent audience is Internal — Google sign-in fails outside the org';
|
|
40
|
+
|
|
41
|
+
// The console flip is a handful of clicks — poll faster than the 10s default
|
|
42
|
+
const AUDIENCE_POLL_MS = 5000;
|
|
43
|
+
|
|
44
|
+
/** The console page that owns the audience switch (the only path there is). */
|
|
45
|
+
function audienceUrl(projectId) {
|
|
46
|
+
return `https://console.cloud.google.com/auth/audience?project=${projectId}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Name the consent screen's BRANDING page as a manual step (#696).
|
|
51
|
+
*
|
|
52
|
+
* The logo, the home/privacy/terms links and the authorized domains have no
|
|
53
|
+
* API at all — so the walk cannot reconcile them, and by the automation ruling
|
|
54
|
+
* (#693) what cannot be automated gets NAMED instead of going unmentioned.
|
|
55
|
+
* The one-line checklist is the part that decides whether it is safe to touch
|
|
56
|
+
* the page mid-launch: a logo upload is the only entry that costs anything.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} projectId - The GCP project the consent screen belongs to.
|
|
59
|
+
*/
|
|
60
|
+
function logBrandingManualStep(projectId) {
|
|
61
|
+
console.log(` ${chalk.dim('→')} Branding is manual (no API): ${chalk.cyan(`https://console.cloud.google.com/auth/branding?project=${projectId}`)}`);
|
|
62
|
+
console.log(` ${chalk.dim('→')} ${chalk.dim("Links + authorized domains are safe anytime; a LOGO upload starts Google's verification review for External apps")}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Read the audience off the brand; Internal is a stopper, not a write.
|
|
67
|
+
*
|
|
68
|
+
* Interactive: the uniform three-outcome gate — Yes opens the console
|
|
69
|
+
* audience page and polls the brand read until `orgInternalOnly` flips,
|
|
70
|
+
* Skip (and `s` at the poll) warns and asks again next run, Disable lands
|
|
71
|
+
* `cloud.consentAudience: false` and nothing ever asks again.
|
|
72
|
+
* Non-interactive: the warn + the console URL, as before.
|
|
73
|
+
*
|
|
74
|
+
* @param {object} context - The handler context (firebaseApi, brandConfig,
|
|
75
|
+
* brandRoot, projectId, options).
|
|
76
|
+
* @param {object} brand - The IAP brand resource just read or created.
|
|
77
|
+
* @returns {Promise<{audience: string, status?: string, reason?: string}>} The resulting audience.
|
|
78
|
+
*/
|
|
79
|
+
async function ensureExternalAudience(context, brand) {
|
|
80
|
+
const { firebaseApi: api, brandConfig, projectId, options = {} } = context;
|
|
81
|
+
|
|
82
|
+
if (brand.orgInternalOnly !== true) {
|
|
83
|
+
console.log(` ${chalk.green('✓')} Consent audience: ${chalk.cyan('External')}`);
|
|
84
|
+
return { audience: 'External' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Recorded opt-out (#33): the brand said Internal is fine — never ask,
|
|
88
|
+
// never warn again, just say why nothing happened
|
|
89
|
+
if (readTriState(brandConfig, AUDIENCE_PATH).optedOut) {
|
|
90
|
+
console.log(` ${chalk.dim(`⊘ Consent audience: Internal — ${AUDIENCE_PATH}: false (delete the line in omega.json5 to be asked again)`)}`);
|
|
91
|
+
return { audience: 'Internal' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (options.dryRun) {
|
|
95
|
+
dryRunPlan('stop for the Internal → External consent-audience flip in the console');
|
|
96
|
+
return { audience: 'Internal' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const url = audienceUrl(projectId);
|
|
100
|
+
|
|
101
|
+
if (canPrompt(options)) {
|
|
102
|
+
const action = await confirmSetup(context, {
|
|
103
|
+
label: 'Consent audience (External)',
|
|
104
|
+
instructions: [
|
|
105
|
+
`${chalk.cyan(projectId)}'s consent screen is Internal — every Gmail account gets Error 403: org_internal`,
|
|
106
|
+
'Google has no API for the flip: the console switch is the only path there is',
|
|
107
|
+
],
|
|
108
|
+
disablePath: AUDIENCE_PATH,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Disable landed `false` (and said so) — this run is done with the audience
|
|
112
|
+
if (action === 'disable') {
|
|
113
|
+
return { audience: 'Internal' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (action === 'yes') {
|
|
117
|
+
const result = await openBrowserAndPoll({
|
|
118
|
+
url,
|
|
119
|
+
label: 'the consent audience page',
|
|
120
|
+
promptMessage: `Switch ${chalk.cyan(projectId)}'s audience to External (Audience → Publish app).`,
|
|
121
|
+
waitMessage: 'Waiting for the consent audience to flip to External',
|
|
122
|
+
check: async () => {
|
|
123
|
+
const [fresh] = await api.listBrands(projectId);
|
|
124
|
+
return fresh && fresh.orgInternalOnly !== true ? { done: true, result: fresh } : { done: false };
|
|
125
|
+
},
|
|
126
|
+
intervalMs: AUDIENCE_POLL_MS,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (result.success) {
|
|
130
|
+
console.log(` ${chalk.green('✓')} Consent audience: ${chalk.cyan('External')}`);
|
|
131
|
+
return { audience: 'External' };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log(` ${chalk.yellow('⚠')} ${chalk.cyan(projectId)}'s consent audience is Internal — Google sign-in fails outside the org`);
|
|
137
|
+
console.log(` ${chalk.dim('→')} Switch it to External: ${chalk.cyan(url)}`);
|
|
138
|
+
return { audience: 'Internal', status: 'warned', reason: INTERNAL_REASON };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Reconcile an existing brand's audience and report the screen as it stands.
|
|
143
|
+
* The handler return for every path that READ the brand off the account.
|
|
144
|
+
*
|
|
145
|
+
* @param {object} context - The handler context.
|
|
146
|
+
* @param {object} brand - The existing IAP brand resource.
|
|
147
|
+
* @returns {Promise<object>} The handler return ({ status?, state }).
|
|
148
|
+
*/
|
|
149
|
+
async function reportExistingBrand(context, brand) {
|
|
150
|
+
const audience = await ensureExternalAudience(context, brand);
|
|
151
|
+
|
|
152
|
+
logBrandingManualStep(context.projectId);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
...(audience.status ? { status: audience.status, reason: audience.reason } : {}),
|
|
156
|
+
state: {
|
|
157
|
+
oauthConsent: {
|
|
158
|
+
brandName: brand.name,
|
|
159
|
+
applicationTitle: brand.applicationTitle,
|
|
160
|
+
supportEmail: brand.supportEmail,
|
|
161
|
+
audience: audience.audience,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = async function ensureOAuthConsent(context) {
|
|
168
|
+
const { firebaseApi: api, brandConfig, projectId, options = {} } = context;
|
|
169
|
+
const brandName = brandConfig.brand?.name;
|
|
170
|
+
|
|
171
|
+
// === READ ===
|
|
172
|
+
const existingBrands = await api.listBrands(projectId);
|
|
173
|
+
|
|
174
|
+
if (existingBrands.length > 0) {
|
|
175
|
+
console.log(` ${chalk.green('✓')} OAuth consent screen exists`);
|
|
176
|
+
|
|
177
|
+
return reportExistingBrand(context, existingBrands[0]);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// === WRITE ===
|
|
181
|
+
if (options.dryRun) {
|
|
182
|
+
const planned = brandConfig.cloud?.supportEmail || "(authorizing user's email)";
|
|
183
|
+
return dryRunPlan(`create OAuth consent screen (${brandName}, ${planned})`, { output: { oauthConsent: { planned: 'create' } } });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const supportEmail = brandConfig.cloud?.supportEmail
|
|
187
|
+
|| await api.getAuthenticatedEmail();
|
|
188
|
+
|
|
189
|
+
if (!supportEmail) {
|
|
190
|
+
console.log(` ${chalk.yellow('⚠')} No usable support email — Google only accepts one the authorizing user OWNS`);
|
|
191
|
+
console.log(` ${chalk.dim('→')} Re-auth to grant the email scope (delete .omega/auth/google-tokens.json and rerun), or set cloud.supportEmail to a Google Group you own`);
|
|
192
|
+
return { status: 'warned', reason: 'no ownable support email — re-auth for the email scope or set cloud.supportEmail', output: { oauthConsent: { note: 'no ownable supportEmail available' } } };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.log(' Creating OAuth consent screen...');
|
|
196
|
+
try {
|
|
197
|
+
const brand = await api.createBrand(projectId, brandName, supportEmail);
|
|
198
|
+
console.log(` ${chalk.green('✓')} Created OAuth consent screen`);
|
|
199
|
+
console.log(` ${chalk.dim('→')} Application: ${chalk.cyan(brandName)}`);
|
|
200
|
+
console.log(` ${chalk.dim('→')} Support email: ${chalk.cyan(supportEmail)}`);
|
|
201
|
+
|
|
202
|
+
// An API-made brand is born INTERNAL, always (#667, proven live): the
|
|
203
|
+
// audience is READ off the new brand and taken through the same stopper
|
|
204
|
+
// as any other, never assumed
|
|
205
|
+
const audience = await ensureExternalAudience(context, brand);
|
|
206
|
+
|
|
207
|
+
logBrandingManualStep(projectId);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
...(audience.status ? { status: audience.status, reason: audience.reason } : {}),
|
|
211
|
+
state: {
|
|
212
|
+
oauthConsent: {
|
|
213
|
+
brandName: brand.name,
|
|
214
|
+
applicationTitle: brandName,
|
|
215
|
+
supportEmail,
|
|
216
|
+
audience: audience.audience,
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (error.message?.includes('already exists')) {
|
|
222
|
+
console.log(` ${chalk.green('✓')} OAuth consent screen already exists`);
|
|
223
|
+
|
|
224
|
+
// The list above missed it (IAP's list lags a create by another run, or
|
|
225
|
+
// a concurrent one won): read it back so this run still reconciles the
|
|
226
|
+
// audience and reports the screen, instead of saying nothing about it
|
|
227
|
+
const [existing] = await api.listBrands(projectId);
|
|
228
|
+
if (!existing) {
|
|
229
|
+
return {};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return reportExistingBrand(context, existing);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Brand creation is ORG-ONLY (#667, proven live): an org-less project
|
|
236
|
+
// answers 400 "Project must belong to an organization". A known cause
|
|
237
|
+
// gets named, never the generic could-not-create line
|
|
238
|
+
if (error.message?.includes('must belong to an organization')) {
|
|
239
|
+
console.log(` ${chalk.yellow('⚠')} ${chalk.cyan(projectId)} belongs to no organization — Google only creates a consent screen in an org-owned project`);
|
|
240
|
+
console.log(` ${chalk.dim('→')} Create it by hand: ${chalk.cyan(`https://console.cloud.google.com/apis/credentials/consent?project=${projectId}`)}`);
|
|
241
|
+
return { status: 'warned', reason: 'the project belongs to no organization — create the consent screen in the console', output: { oauthConsent: { note: 'org-less project — brand creation is org-only' } } };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
console.log(` ${chalk.yellow('⚠')} Could not create OAuth consent screen${chalk.dim(`: ${error.message}`)}`);
|
|
245
|
+
console.log(` ${chalk.dim('→')} Configure manually: ${chalk.cyan(`https://console.cloud.google.com/apis/credentials/consent?project=${projectId}`)}`);
|
|
246
|
+
return { status: 'warned', reason: 'could not create the OAuth consent screen', output: { oauthConsent: { error: error.message } } };
|
|
247
|
+
}
|
|
248
|
+
};
|