@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,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google OAuth2 client — omega-manager's shared Google auth handler, ported.
|
|
3
|
+
* Handles token storage, refresh, and the one-time browser authorization flow
|
|
4
|
+
* (localhost callback server; interactive runs gate the browser open behind
|
|
5
|
+
* Enter — the house rule — with the printed URL as the always-there fallback).
|
|
6
|
+
*
|
|
7
|
+
* ONE Google identity for the whole manager (legacy parity): every service
|
|
8
|
+
* authorizes GOOGLE_SCOPES — the union of everything the manager touches —
|
|
9
|
+
* against ONE token store, so the operator consents ONCE. The store records
|
|
10
|
+
* which scopes it was granted; a cached token missing any requested scope
|
|
11
|
+
* re-consents once instead of 403ing forever (adding a service = one re-ask).
|
|
12
|
+
*
|
|
13
|
+
* Credentials come from GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET in the brand
|
|
14
|
+
* .env; tokens cache to the brand's gitignored .omega/auth/ directory.
|
|
15
|
+
*/
|
|
16
|
+
const { createServer } = require('node:http');
|
|
17
|
+
const { emitKeypressEvents } = require('node:readline');
|
|
18
|
+
const { dirname, join } = require('node:path');
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const chalk = require('chalk').default;
|
|
21
|
+
|
|
22
|
+
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
23
|
+
const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|
24
|
+
|
|
25
|
+
// The manager's full Google surface. Firebase Management + Cloud Platform
|
|
26
|
+
// (IAM, Billing, Service Usage), userinfo.email (consent-screen supportEmail
|
|
27
|
+
// defaults to the AUTHORIZING user — #29), Search Console + site
|
|
28
|
+
// verification, GA4 admin, AdSense (read-only).
|
|
29
|
+
const GOOGLE_SCOPES = [
|
|
30
|
+
'https://www.googleapis.com/auth/firebase',
|
|
31
|
+
'https://www.googleapis.com/auth/cloud-platform',
|
|
32
|
+
'https://www.googleapis.com/auth/cloud-billing',
|
|
33
|
+
'https://www.googleapis.com/auth/userinfo.email',
|
|
34
|
+
'https://www.googleapis.com/auth/webmasters',
|
|
35
|
+
'https://www.googleapis.com/auth/siteverification',
|
|
36
|
+
'https://www.googleapis.com/auth/analytics.edit',
|
|
37
|
+
'https://www.googleapis.com/auth/adsense.readonly',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// A headless consent gate is a PENDING HUMAN STEP, not a failure (#228): the
|
|
41
|
+
// throw carries this code so the walk classes it by code — never by sniffing
|
|
42
|
+
// the message — and steps the operation aside into the run summary's ⚑ list
|
|
43
|
+
// instead of failing a service (and, downstream, a whole `omega dev` boot).
|
|
44
|
+
const CONSENT_REQUIRED = 'OMEGA_CONSENT_REQUIRED';
|
|
45
|
+
|
|
46
|
+
// How long a printed consent url stays clickable (the loopback listener's life)
|
|
47
|
+
const AUTH_WINDOW_MS = 300000;
|
|
48
|
+
|
|
49
|
+
// How often the open flow reprints its url (see startAuthUrlReprint)
|
|
50
|
+
const AUTH_REPRINT_MS = 60000;
|
|
51
|
+
|
|
52
|
+
// This run's consent sessions, keyed by (client id, token store, scopes).
|
|
53
|
+
// Every service shares the token store, so a run that starts with an empty
|
|
54
|
+
// one would otherwise open a NEW browser flow per service, each minting a
|
|
55
|
+
// fresh loopback url and killing the previous listener (#56: three urls in
|
|
56
|
+
// six minutes, and the human was still reading the first). Concurrent and
|
|
57
|
+
// later callers join the session already open, or the one that already
|
|
58
|
+
// failed; a session that SUCCEEDS drops out (its tokens are in the shared
|
|
59
|
+
// store from then on, so nothing reaches here until a token goes bad, and
|
|
60
|
+
// that case deserves a real re-consent).
|
|
61
|
+
const authSessions = new Map();
|
|
62
|
+
|
|
63
|
+
/** The ONE brand-local token store every Google service shares. */
|
|
64
|
+
function googleTokenStorePath(brandRoot) {
|
|
65
|
+
return join(brandRoot, '.omega', 'auth', 'google-tokens.json');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Keep the LIVE consent url in view: reprint it on a stable line every
|
|
70
|
+
* intervalMs until the flow settles (#56). A run's other output scrolls the
|
|
71
|
+
* url away, and a human reading the log clicks the first link they see, which
|
|
72
|
+
* by then belongs to a dead listener.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} url - The url the open flow is waiting on
|
|
75
|
+
* @param {Object} [options] - intervalMs, expiresAt (ms epoch), log (sink)
|
|
76
|
+
* @returns {Function} stop - Clears the timer; call on settle.
|
|
77
|
+
*/
|
|
78
|
+
function startAuthUrlReprint(url, options = {}) {
|
|
79
|
+
const intervalMs = options.intervalMs || AUTH_REPRINT_MS;
|
|
80
|
+
const log = options.log || console.log;
|
|
81
|
+
const { expiresAt } = options;
|
|
82
|
+
|
|
83
|
+
const timer = setInterval(() => {
|
|
84
|
+
const minutesLeft = expiresAt ? Math.max(0, Math.ceil((expiresAt - Date.now()) / 60000)) : null;
|
|
85
|
+
log('');
|
|
86
|
+
log(` ${chalk.dim('→')} Still waiting for Google consent${minutesLeft === null ? '' : ` (~${minutesLeft} min left)`}. This is the live url; any earlier one is dead:`);
|
|
87
|
+
log(` ${chalk.cyan(url)}`);
|
|
88
|
+
}, intervalMs);
|
|
89
|
+
// Never hold the event loop open on the reprint alone (the loopback server
|
|
90
|
+
// is what keeps a waiting run alive)
|
|
91
|
+
timer.unref();
|
|
92
|
+
|
|
93
|
+
return () => clearInterval(timer);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
class GoogleOAuth2Client {
|
|
97
|
+
constructor(options = {}) {
|
|
98
|
+
this.clientId = options.clientId;
|
|
99
|
+
this.clientSecret = options.clientSecret;
|
|
100
|
+
this.scopes = options.scopes || [];
|
|
101
|
+
this.tokenStorePath = options.tokenStorePath || null;
|
|
102
|
+
|
|
103
|
+
this.accessToken = null;
|
|
104
|
+
this.refreshToken = null;
|
|
105
|
+
this.tokenExpiry = null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
loadStoredTokens() {
|
|
109
|
+
if (!this.tokenStorePath || !fs.existsSync(this.tokenStorePath)) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(fs.readFileSync(this.tokenStorePath, 'utf8'));
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
saveTokens(tokens) {
|
|
121
|
+
if (!this.tokenStorePath) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Owner-only dir + file — the store holds a cloud-platform-scoped refresh token
|
|
126
|
+
fs.mkdirSync(dirname(this.tokenStorePath), { recursive: true, mode: 0o700 });
|
|
127
|
+
// Record the granted scopes — getAccessToken() uses them to decide
|
|
128
|
+
// whether a cached token can serve a client or needs one re-consent.
|
|
129
|
+
// MERGE over the existing store: refresh-path saves carry only the token
|
|
130
|
+
// triple and must not drop sidecar fields (account_email).
|
|
131
|
+
const existing = this.loadStoredTokens() || {};
|
|
132
|
+
fs.writeFileSync(this.tokenStorePath, JSON.stringify({ ...existing, ...tokens, scopes: this.scopes }, null, 2), { mode: 0o600 });
|
|
133
|
+
// mode on mkdir/writeFileSync only applies at creation — tighten pre-existing paths too
|
|
134
|
+
fs.chmodSync(dirname(this.tokenStorePath), 0o700);
|
|
135
|
+
fs.chmodSync(this.tokenStorePath, 0o600);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Best-effort: which Google account these tokens act as. Cached in the
|
|
140
|
+
* token store (account_email) after the first userinfo lookup — the
|
|
141
|
+
* userinfo.email scope is part of GOOGLE_SCOPES. Returns null when
|
|
142
|
+
* unknowable; diagnostics must never throw over the error they decorate.
|
|
143
|
+
*/
|
|
144
|
+
async getAccountEmail() {
|
|
145
|
+
const stored = this.loadStoredTokens();
|
|
146
|
+
if (stored?.account_email) {
|
|
147
|
+
return stored.account_email;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const token = this.accessToken || await this.getAccessToken();
|
|
151
|
+
const response = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
|
|
152
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
153
|
+
});
|
|
154
|
+
const data = await response.json();
|
|
155
|
+
if (data.email) {
|
|
156
|
+
this.saveTokens({ account_email: data.email });
|
|
157
|
+
}
|
|
158
|
+
return data.email || null;
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get a valid access token (from cache, refresh, or new auth)
|
|
166
|
+
*/
|
|
167
|
+
async getAccessToken() {
|
|
168
|
+
const storedTokens = this.loadStoredTokens();
|
|
169
|
+
|
|
170
|
+
// A cached token only counts when it was granted every scope this client
|
|
171
|
+
// needs — otherwise API calls 403 with no re-consent trigger. Stores from
|
|
172
|
+
// before scope tracking (no `scopes` array) are treated as insufficient:
|
|
173
|
+
// one union re-consent upgrades them.
|
|
174
|
+
const grantedScopes = Array.isArray(storedTokens?.scopes) ? storedTokens.scopes : [];
|
|
175
|
+
const scopesSufficient = this.scopes.every((scope) => grantedScopes.includes(scope));
|
|
176
|
+
|
|
177
|
+
if (storedTokens && scopesSufficient) {
|
|
178
|
+
this.accessToken = storedTokens.access_token;
|
|
179
|
+
this.refreshToken = storedTokens.refresh_token;
|
|
180
|
+
this.tokenExpiry = storedTokens.expiry;
|
|
181
|
+
} else if (storedTokens) {
|
|
182
|
+
console.log(` ${chalk.dim('→')} Cached Google token is missing newly required scopes — one re-consent upgrades it`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Return cached token if still valid (with 5 min buffer)
|
|
186
|
+
if (this.accessToken && this.tokenExpiry && Date.now() < this.tokenExpiry - 300000) {
|
|
187
|
+
return this.accessToken;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (this.refreshToken) {
|
|
191
|
+
try {
|
|
192
|
+
return await this.refreshAccessToken();
|
|
193
|
+
} catch {
|
|
194
|
+
console.log(` ${chalk.yellow('⚠')} ${chalk.yellow('Token refresh failed, need to re-authenticate')}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return await this.authorizeOnce();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Join this run's consent session for these credentials, or open it (#56).
|
|
203
|
+
* One session per run: see the authSessions comment above.
|
|
204
|
+
*/
|
|
205
|
+
authorizeOnce() {
|
|
206
|
+
const key = [this.clientId, this.tokenStorePath, this.scopes.join(' ')].join('|');
|
|
207
|
+
const open = authSessions.get(key);
|
|
208
|
+
|
|
209
|
+
if (open) {
|
|
210
|
+
return open;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const session = this.performOAuth2Flow();
|
|
214
|
+
authSessions.set(key, session);
|
|
215
|
+
// Release the slot on success only; the caller's own await owns the
|
|
216
|
+
// rejection, so the catch here just keeps this branch from surfacing one.
|
|
217
|
+
session.then(() => authSessions.delete(key)).catch(() => {});
|
|
218
|
+
|
|
219
|
+
return session;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async refreshAccessToken() {
|
|
223
|
+
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
headers: {
|
|
226
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
227
|
+
},
|
|
228
|
+
body: new URLSearchParams({
|
|
229
|
+
client_id: this.clientId,
|
|
230
|
+
client_secret: this.clientSecret,
|
|
231
|
+
refresh_token: this.refreshToken,
|
|
232
|
+
grant_type: 'refresh_token',
|
|
233
|
+
}),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const data = await response.json();
|
|
237
|
+
|
|
238
|
+
if (data.error) {
|
|
239
|
+
throw new Error(`Token refresh failed: ${data.error_description || data.error}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this.accessToken = data.access_token;
|
|
243
|
+
this.tokenExpiry = Date.now() + (data.expires_in * 1000);
|
|
244
|
+
|
|
245
|
+
this.saveTokens({
|
|
246
|
+
access_token: this.accessToken,
|
|
247
|
+
refresh_token: this.refreshToken,
|
|
248
|
+
expiry: this.tokenExpiry,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return this.accessToken;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Perform the full OAuth2 authorization flow: print the auth URL, receive
|
|
256
|
+
* the callback on a temporary localhost server, exchange the code.
|
|
257
|
+
*/
|
|
258
|
+
async performOAuth2Flow() {
|
|
259
|
+
// Headless runs (pipeline, cron, background shells) must never open a
|
|
260
|
+
// consent flow or camp on the 5-minute callback wait — fail fast with
|
|
261
|
+
// the seeding instruction instead. stdout-TTY still counts as "a human
|
|
262
|
+
// is watching" (#25), but OMEGA_NON_INTERACTIVE=1 beats everything.
|
|
263
|
+
const { isInteractive } = require('../vendor/devkit/prompt.js');
|
|
264
|
+
if (process.env.OMEGA_NON_INTERACTIVE === '1' || (!isInteractive() && !process.stdout.isTTY)) {
|
|
265
|
+
const error = new Error(`Google consent required (scopes: ${this.scopes.join(', ')}) — run the service once interactively to grant it; headless runs work from the cached token after that`);
|
|
266
|
+
error.code = CONSENT_REQUIRED;
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// The Enter-gate keypress listener must tear down however the flow
|
|
271
|
+
// settles (Enter pressed, URL clicked directly, timeout, error) — a
|
|
272
|
+
// pending listener holds stdin and zombies the process at exit. The
|
|
273
|
+
// url reprint tears down on the same settle.
|
|
274
|
+
let disarm = null;
|
|
275
|
+
let stopReprint = null;
|
|
276
|
+
|
|
277
|
+
const flow = new Promise((resolve, reject) => {
|
|
278
|
+
// RFC 8252 §7.3 loopback: bind an EPHEMERAL port (listen(0)) and read
|
|
279
|
+
// the real one at listen time — Google's desktop-app client type
|
|
280
|
+
// accepts any localhost port, so nothing pins 9876 (which anything
|
|
281
|
+
// else could be holding; N7 removes fixed dev ports). `redirectUri`
|
|
282
|
+
// is assigned in the listen callback, before the server is reachable,
|
|
283
|
+
// so the request handler never sees it null.
|
|
284
|
+
let redirectUri = null;
|
|
285
|
+
|
|
286
|
+
const server = createServer(async (req, res) => {
|
|
287
|
+
const reqUrl = new URL(req.url, 'http://localhost');
|
|
288
|
+
|
|
289
|
+
if (reqUrl.pathname !== '/callback') {
|
|
290
|
+
res.writeHead(404);
|
|
291
|
+
res.end('Not found');
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const code = reqUrl.searchParams.get('code');
|
|
296
|
+
const error = reqUrl.searchParams.get('error');
|
|
297
|
+
|
|
298
|
+
if (error) {
|
|
299
|
+
res.writeHead(400);
|
|
300
|
+
res.end(`Authorization failed: ${error}`);
|
|
301
|
+
server.close();
|
|
302
|
+
reject(new Error(`Authorization failed: ${error}`));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!code) {
|
|
307
|
+
res.writeHead(400);
|
|
308
|
+
res.end('No authorization code received');
|
|
309
|
+
server.close();
|
|
310
|
+
reject(new Error('No authorization code received'));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const tokenResponse = await fetch(GOOGLE_TOKEN_URL, {
|
|
316
|
+
method: 'POST',
|
|
317
|
+
headers: {
|
|
318
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
319
|
+
},
|
|
320
|
+
body: new URLSearchParams({
|
|
321
|
+
client_id: this.clientId,
|
|
322
|
+
client_secret: this.clientSecret,
|
|
323
|
+
code,
|
|
324
|
+
grant_type: 'authorization_code',
|
|
325
|
+
redirect_uri: redirectUri,
|
|
326
|
+
}),
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const tokenData = await tokenResponse.json();
|
|
330
|
+
|
|
331
|
+
if (tokenData.error) {
|
|
332
|
+
throw new Error(`Token exchange failed: ${tokenData.error_description || tokenData.error}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
this.accessToken = tokenData.access_token;
|
|
336
|
+
this.refreshToken = tokenData.refresh_token;
|
|
337
|
+
this.tokenExpiry = Date.now() + (tokenData.expires_in * 1000);
|
|
338
|
+
|
|
339
|
+
this.saveTokens({
|
|
340
|
+
access_token: this.accessToken,
|
|
341
|
+
refresh_token: this.refreshToken,
|
|
342
|
+
expiry: this.tokenExpiry,
|
|
343
|
+
// A full re-consent may be a DIFFERENT Google account — the
|
|
344
|
+
// cached identity is unknown until the next userinfo lookup
|
|
345
|
+
account_email: null,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
349
|
+
res.end('<html><body><h1>Authentication successful!</h1><p>You can close this window.</p></body></html>');
|
|
350
|
+
server.close();
|
|
351
|
+
resolve(this.accessToken);
|
|
352
|
+
} catch (err) {
|
|
353
|
+
res.writeHead(500);
|
|
354
|
+
res.end(`Error: ${err.message}`);
|
|
355
|
+
server.close();
|
|
356
|
+
reject(err);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
server.listen(0, () => {
|
|
361
|
+
const port = server.address().port;
|
|
362
|
+
redirectUri = `http://localhost:${port}/callback`;
|
|
363
|
+
|
|
364
|
+
const authUrl = new URL(GOOGLE_AUTH_URL);
|
|
365
|
+
authUrl.searchParams.set('client_id', this.clientId);
|
|
366
|
+
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
367
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
368
|
+
authUrl.searchParams.set('scope', this.scopes.join(' '));
|
|
369
|
+
authUrl.searchParams.set('access_type', 'offline');
|
|
370
|
+
authUrl.searchParams.set('prompt', 'consent');
|
|
371
|
+
|
|
372
|
+
console.log('');
|
|
373
|
+
console.log(` ${chalk.dim('→')} Google authentication required:`);
|
|
374
|
+
console.log(` ${chalk.cyan(authUrl.toString())}`);
|
|
375
|
+
|
|
376
|
+
const { isInteractive: interactiveNow, openInBrowser } = require('../vendor/devkit/prompt.js');
|
|
377
|
+
if (interactiveNow()) {
|
|
378
|
+
// House rule (cp101d): browser opens are gated behind Enter. Raw
|
|
379
|
+
// keypress instead of a prompt — the consent can also complete via
|
|
380
|
+
// a clicked URL, and the listener tears down on settle.
|
|
381
|
+
disarm = this._armEnterToOpen(authUrl.toString());
|
|
382
|
+
} else if (process.stdout.isTTY && openInBrowser(authUrl.toString())) {
|
|
383
|
+
// stdout-TTY with piped stdin (npu wrapper, tee — #25): a human is
|
|
384
|
+
// watching but nobody can press Enter, so auto-open remains.
|
|
385
|
+
console.log(` ${chalk.dim('→')} Opening your browser... (use the URL above if nothing appears)`);
|
|
386
|
+
}
|
|
387
|
+
console.log('');
|
|
388
|
+
|
|
389
|
+
stopReprint = startAuthUrlReprint(authUrl.toString(), { expiresAt: Date.now() + AUTH_WINDOW_MS });
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Timeout after 5 minutes (was 2 — humans relaying URLs need slack,
|
|
393
|
+
// #25). unref + clear on settle: a finished flow must never hold the
|
|
394
|
+
// event loop (a live run idled minutes after its summary — the zombie
|
|
395
|
+
// timer from a SUCCESSFUL auth).
|
|
396
|
+
const timeout = setTimeout(() => {
|
|
397
|
+
server.close();
|
|
398
|
+
reject(new Error('Authentication timed out'));
|
|
399
|
+
}, AUTH_WINDOW_MS);
|
|
400
|
+
timeout.unref();
|
|
401
|
+
server.on('close', () => clearTimeout(timeout));
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
return flow.finally(() => {
|
|
405
|
+
if (disarm) {
|
|
406
|
+
disarm();
|
|
407
|
+
}
|
|
408
|
+
if (stopReprint) {
|
|
409
|
+
stopReprint();
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* "Press Enter to open" on a raw keypress listener rather than a prompt:
|
|
416
|
+
* the consent flow can settle without Enter ever being pressed (clicked
|
|
417
|
+
* URL, timeout, error), and an abandoned prompt would pin stdin open.
|
|
418
|
+
*
|
|
419
|
+
* @returns {Function} disarm - Restores the input stream; call on settle.
|
|
420
|
+
*/
|
|
421
|
+
_armEnterToOpen(url) {
|
|
422
|
+
const { getPromptStreams, openInBrowser } = require('../vendor/devkit/prompt.js');
|
|
423
|
+
const { input, output } = getPromptStreams();
|
|
424
|
+
output.write(` ${chalk.dim('→')} Press ${chalk.bold('Enter')} to open the Google consent page in your browser...\n`);
|
|
425
|
+
|
|
426
|
+
emitKeypressEvents(input);
|
|
427
|
+
input.setRawMode?.(true);
|
|
428
|
+
input.resume();
|
|
429
|
+
|
|
430
|
+
let opened = false;
|
|
431
|
+
const handler = (ch, key) => {
|
|
432
|
+
if (key?.ctrl && key?.name === 'c') {
|
|
433
|
+
process.exit();
|
|
434
|
+
}
|
|
435
|
+
if (key?.name === 'return' && !opened) {
|
|
436
|
+
opened = true;
|
|
437
|
+
openInBrowser(url);
|
|
438
|
+
output.write(` ${chalk.dim('→')} Opening your browser... (use the URL above if nothing appears)\n`);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
input.on('keypress', handler);
|
|
442
|
+
|
|
443
|
+
return () => {
|
|
444
|
+
input.removeListener('keypress', handler);
|
|
445
|
+
input.setRawMode?.(false);
|
|
446
|
+
input.pause();
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Make an authenticated request to any Google API
|
|
452
|
+
*/
|
|
453
|
+
async makeRequest(url, options = {}) {
|
|
454
|
+
const token = await this.getAccessToken();
|
|
455
|
+
|
|
456
|
+
const response = await fetch(url, {
|
|
457
|
+
...options,
|
|
458
|
+
headers: {
|
|
459
|
+
'Authorization': `Bearer ${token}`,
|
|
460
|
+
'Content-Type': 'application/json',
|
|
461
|
+
...options.headers,
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// Some endpoints return empty body on success (204)
|
|
466
|
+
if (response.status === 204 || response.headers.get('content-length') === '0') {
|
|
467
|
+
return { success: true };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const data = await response.json();
|
|
471
|
+
|
|
472
|
+
if (!response.ok) {
|
|
473
|
+
const errorMessage = data.error?.message || JSON.stringify(data);
|
|
474
|
+
let message = `Google API Error: ${errorMessage}`;
|
|
475
|
+
// Permission failures are an IDENTITY seam, not a transient fault
|
|
476
|
+
// (found live 2026-07-19: the manage identity had no role on a
|
|
477
|
+
// hand-minted project and the raw 403 diagnosed nothing) — name the
|
|
478
|
+
// acting account and both remedies right in the error.
|
|
479
|
+
if (response.status === 403 || data.error?.status === 'PERMISSION_DENIED') {
|
|
480
|
+
const email = await this.getAccountEmail();
|
|
481
|
+
const project = url.match(/\/projects\/([a-zA-Z0-9-]+)/)?.[1];
|
|
482
|
+
message += `\n → acting Google identity: ${email || 'unknown'}${this.tokenStorePath ? ` (token store: ${this.tokenStorePath})` : ''}`;
|
|
483
|
+
message += `\n → grant it access${project ? ` — a project owner runs: gcloud projects add-iam-policy-binding ${project} --member=user:${email || '<email>'} --role=roles/owner` : ''} — or delete the token store and rerun interactively to consent as an owning account`;
|
|
484
|
+
}
|
|
485
|
+
const error = new Error(message);
|
|
486
|
+
error.details = data.error?.details || null;
|
|
487
|
+
error.status = data.error?.status || response.status;
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return data;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
module.exports = { GoogleOAuth2Client, GOOGLE_SCOPES, googleTokenStorePath, startAuthUrlReprint, CONSENT_REQUIRED };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google service-account access tokens — the RS256 JWT-bearer grant shared
|
|
3
|
+
* by every Google REST surface the manager talks to (Firestore via
|
|
4
|
+
* firestore-rest.js, Firebase Auth admin via the account service). Each
|
|
5
|
+
* surface needs its own OAuth scope, so callers create one provider per
|
|
6
|
+
* (service account, scope) pair; the token is cached until shortly before
|
|
7
|
+
* expiry.
|
|
8
|
+
*/
|
|
9
|
+
const { signJwt } = require('./jwt.js');
|
|
10
|
+
|
|
11
|
+
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create a cached access-token provider for a service account + scope.
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} serviceAccount - Parsed service-account JSON (client_email, private_key)
|
|
17
|
+
* @param {string} scope - OAuth scope, e.g. 'https://www.googleapis.com/auth/datastore'
|
|
18
|
+
* @returns {{ getAccessToken: () => Promise<string> }}
|
|
19
|
+
*/
|
|
20
|
+
function createTokenProvider(serviceAccount, scope) {
|
|
21
|
+
let accessToken = null;
|
|
22
|
+
let tokenExpiry = 0;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
async getAccessToken() {
|
|
26
|
+
if (accessToken && Date.now() < tokenExpiry - 60000) {
|
|
27
|
+
return accessToken;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const now = Math.floor(Date.now() / 1000);
|
|
31
|
+
const assertion = signJwt(
|
|
32
|
+
{ alg: 'RS256', typ: 'JWT' },
|
|
33
|
+
{
|
|
34
|
+
iss: serviceAccount.client_email,
|
|
35
|
+
scope,
|
|
36
|
+
aud: GOOGLE_TOKEN_URL,
|
|
37
|
+
iat: now,
|
|
38
|
+
exp: now + 3600,
|
|
39
|
+
},
|
|
40
|
+
serviceAccount.private_key,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
46
|
+
body: new URLSearchParams({
|
|
47
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
|
48
|
+
assertion,
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const data = await response.json();
|
|
53
|
+
|
|
54
|
+
if (data.error) {
|
|
55
|
+
throw new Error(`Service-account auth failed: ${data.error_description || data.error}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
accessToken = data.access_token;
|
|
59
|
+
tokenExpiry = Date.now() + (data.expires_in * 1000);
|
|
60
|
+
|
|
61
|
+
return accessToken;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { createTokenProvider, GOOGLE_TOKEN_URL };
|
package/dist/lib/jwt.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal JWT signing over node:crypto — no jsonwebtoken dependency. Two
|
|
3
|
+
* consumers: the Firestore REST client (RS256 service-account bearer
|
|
4
|
+
* grants) and the App Store Connect client (ES256 with a `kid` header).
|
|
5
|
+
* ES256 signatures must use the raw JOSE r||s format, not ASN.1/DER —
|
|
6
|
+
* hence the ieee-p1363 dsaEncoding (ignored for RSA keys, so it's safe
|
|
7
|
+
* to set unconditionally).
|
|
8
|
+
*/
|
|
9
|
+
const { createSign } = require('node:crypto');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Sign a compact JWT.
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} header - JOSE header ({ alg, typ, kid? })
|
|
15
|
+
* @param {Object} payload - JWT claims
|
|
16
|
+
* @param {string} privateKey - PEM private key (RSA for RS256, EC P-256 for ES256)
|
|
17
|
+
* @returns {string} - Compact JWT (header.payload.signature)
|
|
18
|
+
*/
|
|
19
|
+
function signJwt(header, payload, privateKey) {
|
|
20
|
+
const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url');
|
|
21
|
+
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
22
|
+
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
23
|
+
|
|
24
|
+
const signer = createSign('SHA256');
|
|
25
|
+
signer.update(signingInput);
|
|
26
|
+
const signature = signer.sign({ key: privateKey, dsaEncoding: 'ieee-p1363' }, 'base64url');
|
|
27
|
+
|
|
28
|
+
return `${signingInput}.${signature}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { signJwt };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one-time legacy OAuth-secret conversion, run at PORT time
|
|
3
|
+
* ([#501](https://github.com/Omega-JS-Stack/omega/issues/501)).
|
|
4
|
+
*
|
|
5
|
+
* omega-manager kept the brand's Google OAuth client in `oauth.json` with the
|
|
6
|
+
* keys `{ googleClientId, googleClientSecret }`. OMEGA's home is
|
|
7
|
+
* `.omega/secrets/google-oauth.json` with `{ clientId, clientSecret }`
|
|
8
|
+
* (services/cloud/ensure/authentication.js), so a hand-carried legacy file sat
|
|
9
|
+
* inert beside it — the client only became real again once a live `omega
|
|
10
|
+
* manage` re-derived it from Firebase.
|
|
11
|
+
*
|
|
12
|
+
* Per the standing ruling (Ian 2026-08-21) no framework run dual-reads an old
|
|
13
|
+
* shape: onboarding CONVERTS the carried file once and removes it, and the
|
|
14
|
+
* authentication service never learns the legacy filename or key names.
|
|
15
|
+
*/
|
|
16
|
+
const { join } = require('node:path');
|
|
17
|
+
const jetpack = require('fs-jetpack');
|
|
18
|
+
|
|
19
|
+
const LEGACY_FILE = 'oauth.json';
|
|
20
|
+
const CANONICAL_FILE = 'google-oauth.json';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Convert a carried legacy `oauth.json` into the canonical
|
|
24
|
+
* `google-oauth.json`, then delete the legacy file (the conversion is
|
|
25
|
+
* one-time, so a rerun finds nothing and reports a clean no-op).
|
|
26
|
+
*
|
|
27
|
+
* @param {string} brandRoot - The brand root directory.
|
|
28
|
+
* @returns {{ converted: boolean, reason?: string }} `reason` names why a
|
|
29
|
+
* present legacy file was left alone.
|
|
30
|
+
*/
|
|
31
|
+
function convertLegacyOAuthSecret(brandRoot) {
|
|
32
|
+
const secretsDir = join(brandRoot, '.omega', 'secrets');
|
|
33
|
+
const legacyPath = join(secretsDir, LEGACY_FILE);
|
|
34
|
+
|
|
35
|
+
if (!jetpack.exists(legacyPath)) {
|
|
36
|
+
return { converted: false };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (jetpack.exists(join(secretsDir, CANONICAL_FILE))) {
|
|
40
|
+
return { converted: false, reason: `${CANONICAL_FILE} already exists — ${LEGACY_FILE} left for you to compare and delete` };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A hand-carried file is external input: an unreadable or unrecognized one
|
|
44
|
+
// is reported, never guessed at
|
|
45
|
+
const legacy = jetpack.read(legacyPath, 'json');
|
|
46
|
+
if (!legacy?.googleClientId || !legacy?.googleClientSecret) {
|
|
47
|
+
return { converted: false, reason: `${LEGACY_FILE} carries no googleClientId/googleClientSecret pair` };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
jetpack.write(join(secretsDir, CANONICAL_FILE), {
|
|
51
|
+
clientId: legacy.googleClientId,
|
|
52
|
+
clientSecret: legacy.googleClientSecret,
|
|
53
|
+
});
|
|
54
|
+
jetpack.remove(legacyPath);
|
|
55
|
+
|
|
56
|
+
return { converted: true };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { convertLegacyOAuthSecret };
|