@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,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure the configured admin accounts exist in Firebase Auth with the
|
|
3
|
+
* resolved passwords (env var → owner hook → seed derivation, see
|
|
4
|
+
* lib/resolve-password.js), carry roles.admin + the highest plan on their
|
|
5
|
+
* Firestore user doc, and — the audit — that NO other user holds
|
|
6
|
+
* roles.admin. Unauthorized admins fail the service.
|
|
7
|
+
*
|
|
8
|
+
* Per entry:
|
|
9
|
+
* account: true → create if missing (then POST /user/signup to complete
|
|
10
|
+
* the flow), else converge the password; admin role +
|
|
11
|
+
* plan on the user doc; marketing sync when flagged
|
|
12
|
+
* marketing: true (only) → push the contact to marketing providers when
|
|
13
|
+
* the auth user exists, skip quietly when not
|
|
14
|
+
*/
|
|
15
|
+
const chalk = require('chalk').default;
|
|
16
|
+
|
|
17
|
+
module.exports = async function ensureUsers(context) {
|
|
18
|
+
const { authAdmin, firestore, accountBackend, admins, domain, apiKey, resolvePassword, brandConfig, options } = context;
|
|
19
|
+
const dryRun = options?.dryRun || false;
|
|
20
|
+
|
|
21
|
+
const counts = { ok: 0, created: 0, updated: 0, adminUpdated: 0, planned: 0, marketingSynced: 0 };
|
|
22
|
+
let warnings = 0;
|
|
23
|
+
|
|
24
|
+
for (const [index, entry] of admins.entries()) {
|
|
25
|
+
const { email } = entry;
|
|
26
|
+
const label = `[${index + 1}/${admins.length}]`;
|
|
27
|
+
|
|
28
|
+
if (!entry.account) {
|
|
29
|
+
if (!entry.marketing) {
|
|
30
|
+
console.log(` ${label} ${chalk.dim(`⊘ ${email} — nothing to manage (account: false, marketing: false)`)}`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Marketing-only — sync if the auth user exists, skip if not
|
|
35
|
+
const user = await authAdmin.getUserByEmail(email);
|
|
36
|
+
if (!user) {
|
|
37
|
+
console.log(` ${label} ${chalk.dim(`⊘ ${email} — no account (marketing-only, skipped)`)}`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(` ${label} ${chalk.cyan(email)} ${chalk.dim(user.uid)} — marketing-only`);
|
|
42
|
+
warnings += await syncMarketing(accountBackend, apiKey, user.uid, dryRun, counts);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// password null only in a dry run whose seed doesn't exist yet; the
|
|
47
|
+
// via-marker calls out the non-default channels (env pin, owner hook)
|
|
48
|
+
const { password, source } = await resolvePassword(email);
|
|
49
|
+
const via = source && source !== 'seed' ? ` ${chalk.dim(`· via ${source}`)}` : '';
|
|
50
|
+
const user = await authAdmin.getUserByEmail(email);
|
|
51
|
+
|
|
52
|
+
if (!user) {
|
|
53
|
+
if (dryRun) {
|
|
54
|
+
console.log(` ${label} ${chalk.cyan('[DRY RUN]')} Would create ${chalk.cyan(email)} + admin role + signup call${via}`);
|
|
55
|
+
counts.planned++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const created = await authAdmin.createUser({ email, password });
|
|
60
|
+
console.log(` ${label} ${chalk.green('✓')} ${chalk.cyan(email)} ${chalk.dim(created.uid)} — account created${via}`);
|
|
61
|
+
console.log(` ${chalk.red('✗')} Google provider not linked`);
|
|
62
|
+
counts.created++;
|
|
63
|
+
|
|
64
|
+
const adminResult = await ensureAdmin(firestore, created.uid, brandConfig, dryRun);
|
|
65
|
+
if (adminResult === 'updated') counts.adminUpdated++;
|
|
66
|
+
|
|
67
|
+
// Complete the signup flow (welcome email, marketing lists, contact inference)
|
|
68
|
+
if (!apiKey) {
|
|
69
|
+
console.log(` ${chalk.yellow('⚠')} Skipping signup call — no Firebase API key in firebase state`);
|
|
70
|
+
warnings++;
|
|
71
|
+
} else {
|
|
72
|
+
try {
|
|
73
|
+
await accountBackend.signup(created.uid);
|
|
74
|
+
console.log(` ${chalk.green('✓')} Signup completed`);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.log(` ${chalk.yellow('⚠')} Signup call failed: ${chalk.dim(error.message)}`);
|
|
77
|
+
warnings++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Existing account — converge the password
|
|
84
|
+
if (!password) {
|
|
85
|
+
// Dry run before the seed exists: it will be generated on the first real run
|
|
86
|
+
console.log(` ${label} ${chalk.cyan('[DRY RUN]')} ${chalk.cyan(email)} ${chalk.dim(user.uid)} — would set password (seed pending)`);
|
|
87
|
+
counts.planned++;
|
|
88
|
+
} else if (await accountBackend.verifyPassword(email, password)) {
|
|
89
|
+
console.log(` ${label} ${chalk.green('✓')} ${chalk.cyan(email)} ${chalk.dim(user.uid)} — password OK${via}`);
|
|
90
|
+
counts.ok++;
|
|
91
|
+
} else if (dryRun) {
|
|
92
|
+
console.log(` ${label} ${chalk.cyan('[DRY RUN]')} ${chalk.cyan(email)} ${chalk.dim(user.uid)} — would update password${via}`);
|
|
93
|
+
counts.planned++;
|
|
94
|
+
} else {
|
|
95
|
+
await authAdmin.updateUser(user.uid, { password });
|
|
96
|
+
console.log(` ${label} ${chalk.green('✓')} ${chalk.cyan(email)} ${chalk.dim(user.uid)} — password updated${via}`);
|
|
97
|
+
counts.updated++;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
logGoogleProvider(user);
|
|
101
|
+
|
|
102
|
+
const adminResult = await ensureAdmin(firestore, user.uid, brandConfig, dryRun);
|
|
103
|
+
if (adminResult === 'planned') counts.planned++;
|
|
104
|
+
if (adminResult === 'updated') counts.adminUpdated++;
|
|
105
|
+
|
|
106
|
+
if (entry.marketing) {
|
|
107
|
+
warnings += await syncMarketing(accountBackend, apiKey, user.uid, dryRun, counts);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Audit: only account entries may hold roles.admin
|
|
112
|
+
const allowedEmails = new Set(admins.filter((e) => e.account).map((e) => e.email));
|
|
113
|
+
const unauthorized = await auditAdmins(firestore, authAdmin, allowedEmails);
|
|
114
|
+
|
|
115
|
+
const output = { users: counts };
|
|
116
|
+
|
|
117
|
+
if (unauthorized.length > 0) {
|
|
118
|
+
return { output, status: 'error', error: `Unauthorized admin accounts found: ${unauthorized.join(', ')}` };
|
|
119
|
+
}
|
|
120
|
+
if (warnings > 0) {
|
|
121
|
+
return { output, status: 'warned', reason: `${warnings} account(s) had a signup or marketing warning` };
|
|
122
|
+
}
|
|
123
|
+
return { output };
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* roles.admin + the highest-tier plan (last entry in payment.products) on
|
|
128
|
+
* the Firestore user doc. Leaf-path merge write, so sibling fields survive.
|
|
129
|
+
* Brands with no products manage only the role (omega-manager issued an
|
|
130
|
+
* empty merge write every run in that case).
|
|
131
|
+
*
|
|
132
|
+
* @returns {'ok'|'planned'|'updated'}
|
|
133
|
+
*/
|
|
134
|
+
async function ensureAdmin(firestore, uid, brandConfig, dryRun) {
|
|
135
|
+
const doc = await firestore.getDoc(`users/${uid}`) || {};
|
|
136
|
+
|
|
137
|
+
const products = brandConfig.payment?.products || [];
|
|
138
|
+
const maxProduct = products[products.length - 1];
|
|
139
|
+
|
|
140
|
+
const hasAdmin = doc.roles?.admin === true;
|
|
141
|
+
const hasPlan = !maxProduct || doc.subscription?.product?.id === maxProduct.id;
|
|
142
|
+
|
|
143
|
+
if (hasAdmin && hasPlan) {
|
|
144
|
+
console.log(` ${chalk.green('✓')} Admin role set${maxProduct ? `, plan: ${chalk.cyan(maxProduct.id)}` : ''}`);
|
|
145
|
+
return 'ok';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (dryRun) {
|
|
149
|
+
const changes = [
|
|
150
|
+
...(!hasAdmin ? ['admin role'] : []),
|
|
151
|
+
...(!hasPlan ? [`plan ${maxProduct.id}`] : []),
|
|
152
|
+
];
|
|
153
|
+
console.log(` ${chalk.cyan('[DRY RUN]')} Would set ${changes.join(' + ')}`);
|
|
154
|
+
return 'planned';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const data = {};
|
|
158
|
+
const fieldPaths = [];
|
|
159
|
+
|
|
160
|
+
if (!hasAdmin) {
|
|
161
|
+
data.roles = { admin: true };
|
|
162
|
+
fieldPaths.push('roles.admin');
|
|
163
|
+
}
|
|
164
|
+
if (!hasPlan) {
|
|
165
|
+
data.subscription = {
|
|
166
|
+
product: { id: maxProduct.id, name: maxProduct.name },
|
|
167
|
+
status: 'active',
|
|
168
|
+
};
|
|
169
|
+
fieldPaths.push('subscription.product.id', 'subscription.product.name', 'subscription.status');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await firestore.patchDoc(`users/${uid}`, data, fieldPaths);
|
|
173
|
+
|
|
174
|
+
if (!hasAdmin) {
|
|
175
|
+
console.log(` ${chalk.green('✓')} Admin role set`);
|
|
176
|
+
}
|
|
177
|
+
if (!hasPlan) {
|
|
178
|
+
console.log(` ${chalk.green('✓')} Plan set to ${chalk.cyan(maxProduct.id)}`);
|
|
179
|
+
}
|
|
180
|
+
return 'updated';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* All users with roles.admin == true, checked against the allowed emails.
|
|
185
|
+
*
|
|
186
|
+
* @returns {Promise<string[]>} Unauthorized admin descriptions (empty = clean)
|
|
187
|
+
*/
|
|
188
|
+
async function auditAdmins(firestore, authAdmin, allowedEmails) {
|
|
189
|
+
const docs = await firestore.runQuery({
|
|
190
|
+
from: [{ collectionId: 'users' }],
|
|
191
|
+
where: {
|
|
192
|
+
fieldFilter: {
|
|
193
|
+
field: { fieldPath: 'roles.admin' },
|
|
194
|
+
op: 'EQUAL',
|
|
195
|
+
value: { booleanValue: true },
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const unauthorized = [];
|
|
201
|
+
|
|
202
|
+
for (const doc of docs) {
|
|
203
|
+
const user = await authAdmin.getUser(doc.id);
|
|
204
|
+
|
|
205
|
+
if (user && allowedEmails.has(user.email)) {
|
|
206
|
+
console.log(` ${chalk.green('✓')} Admin: ${chalk.cyan(user.email)} ${chalk.dim(doc.id)}`);
|
|
207
|
+
} else if (user) {
|
|
208
|
+
unauthorized.push(`${user.email || '?'} (${doc.id})`);
|
|
209
|
+
console.log(` ${chalk.red('✗')} Unauthorized admin: ${chalk.red(user.email || '?')} ${chalk.dim(doc.id)}`);
|
|
210
|
+
} else {
|
|
211
|
+
unauthorized.push(doc.id);
|
|
212
|
+
console.log(` ${chalk.red('✗')} Unauthorized admin (no auth record): ${chalk.red(doc.id)}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return unauthorized;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Whether the user has Google sign-in linked (informational — linking is a
|
|
221
|
+
* manual browser step).
|
|
222
|
+
*/
|
|
223
|
+
function logGoogleProvider(user) {
|
|
224
|
+
const hasGoogle = user.providerData.some((p) => p.providerId === 'google.com');
|
|
225
|
+
|
|
226
|
+
if (hasGoogle) {
|
|
227
|
+
console.log(` ${chalk.green('✓')} Google provider linked`);
|
|
228
|
+
} else {
|
|
229
|
+
console.log(` ${chalk.red('✗')} Google provider not linked`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Push the user's contact to the marketing providers via the backend.
|
|
235
|
+
*
|
|
236
|
+
* @returns {Promise<number>} Warning count contribution (0 or 1)
|
|
237
|
+
*/
|
|
238
|
+
async function syncMarketing(accountBackend, apiKey, uid, dryRun, counts) {
|
|
239
|
+
if (!apiKey) {
|
|
240
|
+
console.log(` ${chalk.dim('⊘ Marketing sync skipped (no Firebase API key in firebase state)')}`);
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (dryRun) {
|
|
245
|
+
console.log(` ${chalk.cyan('[DRY RUN]')} Would sync marketing contact`);
|
|
246
|
+
counts.planned++;
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
await accountBackend.syncMarketingContact(uid);
|
|
252
|
+
console.log(` ${chalk.green('✓')} Marketing contact synced`);
|
|
253
|
+
counts.marketingSynced++;
|
|
254
|
+
return 0;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.log(` ${chalk.yellow('⚠')} Marketing sync failed: ${chalk.dim(error.message)}`);
|
|
257
|
+
return 1;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account service — required accounts exist in the brand's Firebase Auth
|
|
3
|
+
* with the expected (derived) passwords, carry the admin role + highest
|
|
4
|
+
* plan on their Firestore user doc, and no one else holds the admin role.
|
|
5
|
+
* Runs after update (deploy) because signup/marketing calls hit the live
|
|
6
|
+
* brand backend.
|
|
7
|
+
*
|
|
8
|
+
* De-ITW'd from omega-manager: the account list was the hardcoded company
|
|
9
|
+
* ADMIN_EMAILS in config.js (personal Gmail addresses) — the port reads
|
|
10
|
+
* account.admins from config, defaulting to support@{domain} only (a
|
|
11
|
+
* company's own list lives in its company omega.json5 and replaces the
|
|
12
|
+
* default whole). The password formula was a company-specific scheme in
|
|
13
|
+
* code — passwords now resolve per account through the owner channels
|
|
14
|
+
* (OMEGA_ACCOUNT_PASSWORD__* env var → config/hooks/account/password.js →
|
|
15
|
+
* lazy ACCOUNT_PASSWORD_SEED derivation; see lib/resolve-password.js).
|
|
16
|
+
* firebase-admin is replaced by the Identity Toolkit REST API +
|
|
17
|
+
* FirestoreREST over the brand's own service account.
|
|
18
|
+
*/
|
|
19
|
+
const { join } = require('node:path');
|
|
20
|
+
const jetpack = require('fs-jetpack');
|
|
21
|
+
|
|
22
|
+
const { createServiceRunner } = require('../../lib/service-runner.js');
|
|
23
|
+
const { FirestoreREST, loadServiceAccount } = require('../../lib/firestore-rest.js');
|
|
24
|
+
const { createAuthAdmin } = require('../../lib/auth-admin.js');
|
|
25
|
+
const { createBackendClient } = require('./lib/backend-client.js');
|
|
26
|
+
const { createPasswordResolver } = require('./lib/resolve-password.js');
|
|
27
|
+
|
|
28
|
+
const SERVICE_ACCOUNT_PATH = join('.omega', 'secrets', 'service-account.json');
|
|
29
|
+
|
|
30
|
+
module.exports.run = createServiceRunner({
|
|
31
|
+
serviceDir: __dirname,
|
|
32
|
+
setup: (context) => {
|
|
33
|
+
const config = context.brandConfig.account;
|
|
34
|
+
|
|
35
|
+
if (config === false || config?.enabled === false) {
|
|
36
|
+
return { skip: true, reason: 'account.enabled = false' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const targets = context.brandConfig.targets || {};
|
|
40
|
+
if (!targets.backend) {
|
|
41
|
+
return { skip: true, reason: 'no backend target' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Shared Firebase project — the owning brand manages accounts
|
|
45
|
+
if (context.brandConfig.cloud?.shared === true) {
|
|
46
|
+
return { skip: true, reason: 'shared Firebase project' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const domain = (context.brandConfig.brand?.url || '').replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
50
|
+
if (!domain) {
|
|
51
|
+
return { skip: true, reason: 'no brand.url configured' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Resolve {domain} templates in the configured admin list
|
|
55
|
+
const admins = (config?.admins || []).map((entry) => ({
|
|
56
|
+
...entry,
|
|
57
|
+
email: entry.email.replace(/\{\s*domain\s*\}/g, domain),
|
|
58
|
+
}));
|
|
59
|
+
if (admins.length === 0) {
|
|
60
|
+
return { skip: true, reason: 'no account.admins configured' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Firebase web API key from config (the cloud service lands it there) —
|
|
64
|
+
// without it, password verification and backend calls degrade (handled
|
|
65
|
+
// per-operation)
|
|
66
|
+
const apiKey = context.brandConfig.cloud?.config?.apiKey || null;
|
|
67
|
+
const apiBaseUrl = `https://api.${domain}`;
|
|
68
|
+
|
|
69
|
+
// Tests inject fakes via context
|
|
70
|
+
let authAdmin = context.authAdmin;
|
|
71
|
+
let firestore = context.firestore;
|
|
72
|
+
let accountBackend = context.accountBackend;
|
|
73
|
+
if (!authAdmin) {
|
|
74
|
+
if (!jetpack.exists(join(context.brandRoot, SERVICE_ACCOUNT_PATH))) {
|
|
75
|
+
return { skip: true, reason: `no service account at ${SERVICE_ACCOUNT_PATH} (run the cloud service first)` };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const serviceAccount = loadServiceAccount(SERVICE_ACCOUNT_PATH, context.brandRoot);
|
|
79
|
+
authAdmin = createAuthAdmin(serviceAccount);
|
|
80
|
+
firestore = new FirestoreREST(serviceAccount);
|
|
81
|
+
accountBackend = createBackendClient({ authAdmin, apiKey, apiBaseUrl });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Lazy channels: nothing (hook load, seed generation) happens until an
|
|
85
|
+
// account actually needs a password — a skipped or fully env/hook-covered
|
|
86
|
+
// brand never grows a seed in its .env
|
|
87
|
+
const dryRun = context.options?.dryRun || false;
|
|
88
|
+
const resolvePassword = createPasswordResolver({
|
|
89
|
+
brandRoot: context.brandRoot,
|
|
90
|
+
domain,
|
|
91
|
+
brand: context.brandConfig.brand,
|
|
92
|
+
dryRun,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return { authAdmin, firestore, accountBackend, admins, domain, apiKey, resolvePassword };
|
|
96
|
+
},
|
|
97
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand-backend calls made AS a managed user — password verification plus
|
|
3
|
+
* the two @omega.js/backend endpoints the account service drives after auth changes:
|
|
4
|
+
* POST /user/signup (completes the signup flow: welcome email, marketing
|
|
5
|
+
* lists, contact inference) and PUT /marketing/contact (pushes the contact
|
|
6
|
+
* to every marketing provider in one call).
|
|
7
|
+
*
|
|
8
|
+
* Auth model: mint a custom token with the Admin credentials, exchange it
|
|
9
|
+
* for an ID token at the public Firebase Auth REST API (needs the web
|
|
10
|
+
* apiKey from firebase state), then call the backend as that user. Methods
|
|
11
|
+
* throw on failure; the handler downgrades failures to warnings because
|
|
12
|
+
* the backend may simply not be deployed yet on a fresh brand.
|
|
13
|
+
*/
|
|
14
|
+
const AUTH_API_BASE = 'https://identitytoolkit.googleapis.com/v1';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create the backend client.
|
|
18
|
+
*
|
|
19
|
+
* @param {Object} deps
|
|
20
|
+
* @param {Object} deps.authAdmin - Auth admin client (for custom tokens)
|
|
21
|
+
* @param {string|null} deps.apiKey - Firebase web API key (null → verify
|
|
22
|
+
* always false, signup/sync throw)
|
|
23
|
+
* @param {string} deps.apiBaseUrl - e.g. 'https://api.mybrand.com'
|
|
24
|
+
* @returns {Object} { verifyPassword, signup, syncMarketingContact }
|
|
25
|
+
*/
|
|
26
|
+
function createBackendClient({ authAdmin, apiKey, apiBaseUrl }) {
|
|
27
|
+
/**
|
|
28
|
+
* Exchange a custom token for an ID token.
|
|
29
|
+
*/
|
|
30
|
+
async function getIdToken(uid) {
|
|
31
|
+
const customToken = await authAdmin.createCustomToken(uid);
|
|
32
|
+
|
|
33
|
+
const response = await fetch(
|
|
34
|
+
`${AUTH_API_BASE}/accounts:signInWithCustomToken?key=${apiKey}`,
|
|
35
|
+
{
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38
|
+
body: JSON.stringify({ token: customToken, returnSecureToken: true }),
|
|
39
|
+
signal: AbortSignal.timeout(30000),
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
throw new Error(`Token exchange failed (${response.status})`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { idToken } = await response.json();
|
|
48
|
+
return idToken;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function callBackend(method, path, uid) {
|
|
52
|
+
const idToken = await getIdToken(uid);
|
|
53
|
+
|
|
54
|
+
const response = await fetch(`${apiBaseUrl}${path}`, {
|
|
55
|
+
method,
|
|
56
|
+
headers: {
|
|
57
|
+
'Content-Type': 'application/json',
|
|
58
|
+
'Authorization': `Bearer ${idToken}`,
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({ uid }),
|
|
61
|
+
signal: AbortSignal.timeout(60000),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
throw new Error(`${method} ${path} failed (${response.status})`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
/**
|
|
71
|
+
* True when the password signs in successfully (non-mutating check).
|
|
72
|
+
*/
|
|
73
|
+
async verifyPassword(email, password) {
|
|
74
|
+
if (!apiKey) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch(
|
|
80
|
+
`${AUTH_API_BASE}/accounts:signInWithPassword?key=${apiKey}`,
|
|
81
|
+
{
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
body: JSON.stringify({ email, password, returnSecureToken: false }),
|
|
85
|
+
signal: AbortSignal.timeout(30000),
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return response.ok;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/** Complete the signup flow for a freshly created user. */
|
|
96
|
+
signup(uid) {
|
|
97
|
+
return callBackend('POST', '/omega/user/signup', uid);
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
/** Push the user's contact data to all marketing providers. */
|
|
101
|
+
syncMarketingContact(uid) {
|
|
102
|
+
return callBackend('PUT', '/omega/marketing/contact', uid);
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { createBackendClient };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic admin-account passwords — HMAC-SHA256 of the account email +
|
|
3
|
+
* the brand's apex domain, keyed by the brand-local ACCOUNT_PASSWORD_SEED.
|
|
4
|
+
* Same inputs → same password on every run (the service converges Firebase
|
|
5
|
+
* Auth to it), different per account and per brand, and nothing stored
|
|
6
|
+
* anywhere but the seed in the gitignored brand .env.
|
|
7
|
+
*
|
|
8
|
+
* De-ITW'd from omega-manager, whose generate-password.js encoded the
|
|
9
|
+
* company's personal keyboard-column formula in code. Rotating the seed is
|
|
10
|
+
* safe but noisy: the next run updates every managed account's password.
|
|
11
|
+
*/
|
|
12
|
+
const { createHmac } = require('node:crypto');
|
|
13
|
+
|
|
14
|
+
const { getApexDomain } = require('../../../lib/domain-utils.js');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Derive the password for a managed account.
|
|
18
|
+
*
|
|
19
|
+
* The static prefix guarantees the uppercase/digit/symbol character classes
|
|
20
|
+
* regardless of what the digest contains; the 20 base64url chars carry the
|
|
21
|
+
* entropy.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} seed - ACCOUNT_PASSWORD_SEED from the brand .env
|
|
24
|
+
* @param {string} email - Account email address
|
|
25
|
+
* @param {string} domain - Brand domain (e.g. 'mybrand.com'); apex is used
|
|
26
|
+
* so subdomain changes in brand.url don't rotate passwords
|
|
27
|
+
* @returns {string} Derived password
|
|
28
|
+
*/
|
|
29
|
+
function derivePassword(seed, email, domain) {
|
|
30
|
+
const apex = getApexDomain(domain);
|
|
31
|
+
const digest = createHmac('sha256', seed).update(`${email}:${apex}`).digest('base64url');
|
|
32
|
+
|
|
33
|
+
return `A1!${digest.slice(0, 20)}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { derivePassword };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-account password resolution — the owner-controlled channels, strongest
|
|
3
|
+
* first (brand-account provisioning ownership, Ian 2026-07-10):
|
|
4
|
+
*
|
|
5
|
+
* 1. env — OMEGA_ACCOUNT_PASSWORD__<EMAIL> (email uppercased, runs of
|
|
6
|
+
* non-alphanumerics → _), e.g. support@acme.com →
|
|
7
|
+
* OMEGA_ACCOUNT_PASSWORD__SUPPORT_ACME_COM. Rides the D15
|
|
8
|
+
* cascade, so a fixed password can live in the brand or
|
|
9
|
+
* company .env.
|
|
10
|
+
* 2. hook — config/hooks/account/password.js (brand root, else company
|
|
11
|
+
* root via the company stamp): ({ email, domain, apex, brand })
|
|
12
|
+
* → password. A company-wide formula lives in the OWNER'S tree,
|
|
13
|
+
* never in framework source. A broken hook throws — an owner
|
|
14
|
+
* who wrote one never gets silent fallback.
|
|
15
|
+
* 3. seed — derivePassword(ACCOUNT_PASSWORD_SEED, email, domain), the
|
|
16
|
+
* generated default. The seed is resolved LAZILY: only when
|
|
17
|
+
* some account actually falls through to this channel does it
|
|
18
|
+
* get generated + persisted to the brand .env — a brand fully
|
|
19
|
+
* covered by env/hook never grows a seed.
|
|
20
|
+
*/
|
|
21
|
+
const { randomBytes } = require('node:crypto');
|
|
22
|
+
const chalk = require('chalk').default;
|
|
23
|
+
|
|
24
|
+
const { loadHook } = require('../../../vendor/config/index.js');
|
|
25
|
+
|
|
26
|
+
const { writeEnvValue } = require('../../../lib/env-secret.js');
|
|
27
|
+
const { getApexDomain } = require('../../../lib/domain-utils.js');
|
|
28
|
+
const { derivePassword } = require('./password.js');
|
|
29
|
+
|
|
30
|
+
const HOOK_POINT = 'account/password';
|
|
31
|
+
|
|
32
|
+
// Firebase Auth's own minimum — catch a broken hook here, not as an opaque
|
|
33
|
+
// Identity Toolkit 400 later.
|
|
34
|
+
const MIN_PASSWORD_LENGTH = 6;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The env var that pins a specific account's password.
|
|
38
|
+
* @param {string} email - Resolved account email (templates already applied)
|
|
39
|
+
* @returns {string} e.g. 'OMEGA_ACCOUNT_PASSWORD__SUPPORT_ACME_COM'
|
|
40
|
+
*/
|
|
41
|
+
function passwordEnvVar(email) {
|
|
42
|
+
const key = email.toUpperCase().replace(/[^A-Z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
43
|
+
return `OMEGA_ACCOUNT_PASSWORD__${key}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build the per-account password resolver for one brand run.
|
|
48
|
+
*
|
|
49
|
+
* @param {Object} spec
|
|
50
|
+
* @param {string} spec.brandRoot - Brand-monorepo root (hook + seed home)
|
|
51
|
+
* @param {string} spec.domain - Brand domain (hook context + seed derivation)
|
|
52
|
+
* @param {Object} spec.brand - brandConfig.brand (hook context: id, name, url)
|
|
53
|
+
* @param {boolean} spec.dryRun - Dry runs never generate/persist a seed
|
|
54
|
+
* @returns {(email: string) => Promise<{ password: string|null, source: string|null }>}
|
|
55
|
+
* password null only in a dry run that fell through to a not-yet-existing
|
|
56
|
+
* seed (the caller's "seed pending" path); source is 'env <VAR>',
|
|
57
|
+
* 'hook <file>', or 'seed'.
|
|
58
|
+
*/
|
|
59
|
+
function createPasswordResolver({ brandRoot, domain, brand, dryRun }) {
|
|
60
|
+
let hook; // undefined = not looked up yet; null = no hook file
|
|
61
|
+
let seed; // undefined = not resolved yet; null = dry run without one
|
|
62
|
+
|
|
63
|
+
return async function resolvePassword(email) {
|
|
64
|
+
// 1. Explicit env pin
|
|
65
|
+
const envVar = passwordEnvVar(email);
|
|
66
|
+
if (process.env[envVar]) {
|
|
67
|
+
return { password: process.env[envVar], source: `env ${envVar}` };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 2. Owner hook (loaded once; a broken hook throws out of the service)
|
|
71
|
+
if (hook === undefined) {
|
|
72
|
+
hook = loadHook(brandRoot, HOOK_POINT);
|
|
73
|
+
}
|
|
74
|
+
if (hook) {
|
|
75
|
+
const password = await hook.fn({ email, domain, apex: getApexDomain(domain), brand });
|
|
76
|
+
if (typeof password !== 'string' || password.length < MIN_PASSWORD_LENGTH) {
|
|
77
|
+
throw new Error(`Hook ${HOOK_POINT} (${hook.file}) returned an invalid password for ${email} — must be a string of at least ${MIN_PASSWORD_LENGTH} characters`);
|
|
78
|
+
}
|
|
79
|
+
return { password, source: `hook ${hook.file}` };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 3. Seed derivation — the seed materializes on first need
|
|
83
|
+
if (seed === undefined) {
|
|
84
|
+
seed = process.env.ACCOUNT_PASSWORD_SEED || null;
|
|
85
|
+
|
|
86
|
+
if (!seed && dryRun) {
|
|
87
|
+
console.log(` ${chalk.cyan('[DRY RUN]')} Would generate ACCOUNT_PASSWORD_SEED and save it to the brand .env`);
|
|
88
|
+
} else if (!seed) {
|
|
89
|
+
seed = randomBytes(24).toString('base64url');
|
|
90
|
+
writeEnvValue(brandRoot, 'ACCOUNT_PASSWORD_SEED', seed);
|
|
91
|
+
process.env.ACCOUNT_PASSWORD_SEED = seed;
|
|
92
|
+
console.log(` ${chalk.green('✓')} Generated ACCOUNT_PASSWORD_SEED and saved to the brand .env`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { password: seed ? derivePassword(seed, email, domain) : null, source: 'seed' };
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { createPasswordResolver, passwordEnvVar };
|