@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,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical .env ordering — the .env sibling of @omega.js/config's
|
|
3
|
+
* applyCanonicalOrder (cp137, Ian: "reorder our env files like the config
|
|
4
|
+
* files... moving comments too?").
|
|
5
|
+
*
|
|
6
|
+
* The grouping, the group comments, and the key inventory all live in
|
|
7
|
+
* @omega.js/config's env schema (#581): canonicalEnvGroups() is that schema
|
|
8
|
+
* rendered as .env sections, the scaffold stub renders from it and every
|
|
9
|
+
* reorder renders from it, so a freshly scaffolded .env and a years-old
|
|
10
|
+
* converged one have the same shape — and a key the composer knows can never
|
|
11
|
+
* be missing from the stub, because both derive from the same entries.
|
|
12
|
+
*
|
|
13
|
+
* Machine-owned comments — the boxed `# ── … ──`
|
|
14
|
+
* group headers, `# KEY=` placeholders, the SSOT note lines — are
|
|
15
|
+
* REGENERATED on every render (stale text self-heals); hand-written
|
|
16
|
+
* comments travel verbatim with the key directly below them; the file's
|
|
17
|
+
* leading comment block stays at the top as the header. Unknown keys keep
|
|
18
|
+
* their relative order in an "Other" section after the known groups.
|
|
19
|
+
*
|
|
20
|
+
* Duplicate keys collapse to the LAST occurrence — dotenv.parse's winner,
|
|
21
|
+
* so the effective value can never change — and hand comments from dropped
|
|
22
|
+
* occurrences move to the kept one.
|
|
23
|
+
*
|
|
24
|
+
* Loss-proof by construction: a structural pre-check accepts only comment /
|
|
25
|
+
* blank / KEY=value lines (anything else — multi-line quoted values,
|
|
26
|
+
* `export` forms — declines untouched), kept lines are carried VERBATIM,
|
|
27
|
+
* and a key→value equality post-check (last-wins, both sides) refuses to
|
|
28
|
+
* return a reorder that changed any effective value.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const { envFileGroups, envKeysByGroup, envLine } = require('../vendor/config/index.js');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The canonical .env sections: every schema group that renders into a file,
|
|
35
|
+
* in schema order, with its keys. Derived on every call so a key added to the
|
|
36
|
+
* schema needs no edit here (and the group's own `notes` travel with it).
|
|
37
|
+
*
|
|
38
|
+
* @returns {Array<{comment: string, notes?: string[], keys: string[]}>}
|
|
39
|
+
*/
|
|
40
|
+
function canonicalEnvGroups() {
|
|
41
|
+
const byGroup = envKeysByGroup();
|
|
42
|
+
|
|
43
|
+
return envFileGroups()
|
|
44
|
+
.map((group) => ({ comment: group.comment, notes: group.notes, keys: byGroup[group.id] || [] }))
|
|
45
|
+
.filter((group) => group.keys.length > 0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const OTHER_COMMENT = 'Other keys (not in the canonical groups)';
|
|
49
|
+
|
|
50
|
+
// Machine-owned comment lines are regenerated from the SSOT on every render,
|
|
51
|
+
// so the parser drops them: boxed group headers (current or stale text),
|
|
52
|
+
// `# KEY=` placeholders, the groups' own note lines, and the legacy scaffold
|
|
53
|
+
// trailer (pre-cp137 stubs mentioned the auto-generated keys in prose).
|
|
54
|
+
const MACHINE_COMMENT_PATTERNS = [
|
|
55
|
+
/^# ── .* ──$/,
|
|
56
|
+
/^# [A-Za-z_][A-Za-z0-9_]*=$/,
|
|
57
|
+
];
|
|
58
|
+
const MACHINE_COMMENT_EXACT = [
|
|
59
|
+
'# Auto-generated and persisted here on the first real run — leave unset:',
|
|
60
|
+
'# ACCOUNT_PASSWORD_SEED, CSC_KEY_PASSWORD',
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
function isMachineComment(line, groups) {
|
|
64
|
+
return MACHINE_COMMENT_EXACT.includes(line)
|
|
65
|
+
|| groups.some((group) => (group.notes || []).some((note) => `# ${note}` === line))
|
|
66
|
+
|| MACHINE_COMMENT_PATTERNS.some((pattern) => pattern.test(line));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const KEY_LINE = /^([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse a simple .env file into header, keyed entries, and stray comment
|
|
73
|
+
* blocks. Returns { unparseable: reason } when any line is neither a
|
|
74
|
+
* comment, blank, nor single-line KEY=value — the caller declines to
|
|
75
|
+
* reorder rather than risk a legal-but-exotic dotenv construct.
|
|
76
|
+
*/
|
|
77
|
+
function parseEnvFile(content) {
|
|
78
|
+
const groups = canonicalEnvGroups();
|
|
79
|
+
const header = [];
|
|
80
|
+
const entries = new Map(); // key → { raw, handComments: string[], order }
|
|
81
|
+
const strays = [];
|
|
82
|
+
let pending = [];
|
|
83
|
+
let sawKey = false;
|
|
84
|
+
let order = 0;
|
|
85
|
+
|
|
86
|
+
const flushStandalone = () => {
|
|
87
|
+
if (pending.length === 0) return;
|
|
88
|
+
if (!sawKey && header.length === 0) {
|
|
89
|
+
header.push(...pending);
|
|
90
|
+
} else {
|
|
91
|
+
strays.push(pending);
|
|
92
|
+
}
|
|
93
|
+
pending = [];
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const lines = content.split('\n');
|
|
97
|
+
// A trailing newline yields one empty last element — structural, not a line
|
|
98
|
+
if (lines[lines.length - 1] === '') lines.pop();
|
|
99
|
+
|
|
100
|
+
for (let i = 0; i < lines.length; i++) {
|
|
101
|
+
const line = lines[i];
|
|
102
|
+
if (line.trim() === '') {
|
|
103
|
+
flushStandalone();
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (line.trimStart().startsWith('#')) {
|
|
107
|
+
if (!isMachineComment(line, groups)) pending.push(line);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const match = KEY_LINE.exec(line);
|
|
111
|
+
if (!match) {
|
|
112
|
+
return { unparseable: `line ${i + 1} is not a comment, blank, or single-line KEY=value` };
|
|
113
|
+
}
|
|
114
|
+
sawKey = true;
|
|
115
|
+
const key = match[1];
|
|
116
|
+
const existing = entries.get(key);
|
|
117
|
+
if (existing) {
|
|
118
|
+
// Last occurrence wins (dotenv.parse semantics) — its raw line is
|
|
119
|
+
// kept verbatim; hand comments from every occurrence merge onto it
|
|
120
|
+
existing.raw = line;
|
|
121
|
+
existing.handComments.push(...pending);
|
|
122
|
+
} else {
|
|
123
|
+
entries.set(key, { raw: line, handComments: pending, order: order++ });
|
|
124
|
+
}
|
|
125
|
+
pending = [];
|
|
126
|
+
}
|
|
127
|
+
flushStandalone();
|
|
128
|
+
|
|
129
|
+
return { header, entries, strays };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Render the canonical .env layout: header, every group (real lines
|
|
134
|
+
* verbatim where present, `# KEY=` placeholders otherwise), then unknown
|
|
135
|
+
* keys and stray comment blocks under an "Other" section.
|
|
136
|
+
*
|
|
137
|
+
* @param {object} input
|
|
138
|
+
* @param {string[]} [input.header] - Verbatim comment lines for the top.
|
|
139
|
+
* @param {Map<string, {raw: string, handComments?: string[]}>|object} [input.entries] -
|
|
140
|
+
* key → entry; `raw` is the exact line to emit for that key.
|
|
141
|
+
* @param {Array<string[]>} [input.strays] - Standalone comment blocks.
|
|
142
|
+
* @returns {string} File content, trailing newline included.
|
|
143
|
+
*/
|
|
144
|
+
function renderCanonicalEnv({ header = [], entries = new Map(), strays = [] } = {}) {
|
|
145
|
+
const entryMap = entries instanceof Map ? entries : new Map(Object.entries(entries));
|
|
146
|
+
const groups = canonicalEnvGroups();
|
|
147
|
+
const knownKeys = new Set(groups.flatMap((group) => group.keys));
|
|
148
|
+
const out = [];
|
|
149
|
+
|
|
150
|
+
if (header.length > 0) {
|
|
151
|
+
out.push(...header, '');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const group of groups) {
|
|
155
|
+
out.push(`# ── ${group.comment} ──`);
|
|
156
|
+
for (const note of group.notes || []) {
|
|
157
|
+
out.push(`# ${note}`);
|
|
158
|
+
}
|
|
159
|
+
for (const key of group.keys) {
|
|
160
|
+
const entry = entryMap.get(key);
|
|
161
|
+
if (entry) {
|
|
162
|
+
out.push(...(entry.handComments || []), entry.raw);
|
|
163
|
+
} else {
|
|
164
|
+
out.push(`# ${key}=`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
out.push('');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const unknown = [...entryMap.entries()]
|
|
171
|
+
.filter(([key]) => !knownKeys.has(key))
|
|
172
|
+
.sort(([, a], [, b]) => (a.order ?? 0) - (b.order ?? 0));
|
|
173
|
+
if (unknown.length > 0 || strays.length > 0) {
|
|
174
|
+
out.push(`# ── ${OTHER_COMMENT} ──`);
|
|
175
|
+
for (const [, entry] of unknown) {
|
|
176
|
+
out.push(...(entry.handComments || []), entry.raw);
|
|
177
|
+
}
|
|
178
|
+
for (const block of strays) {
|
|
179
|
+
out.push(...block);
|
|
180
|
+
}
|
|
181
|
+
out.push('');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return `${out.join('\n').replace(/\n+$/, '')}\n`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** key → verbatim value string, last occurrence wins — the equality probe. */
|
|
188
|
+
function effectiveValues(content) {
|
|
189
|
+
const values = new Map();
|
|
190
|
+
for (const line of content.split('\n')) {
|
|
191
|
+
const match = KEY_LINE.exec(line);
|
|
192
|
+
if (match) values.set(match[1], match[2]);
|
|
193
|
+
}
|
|
194
|
+
return values;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Reorder .env content into the canonical layout, loss-proof.
|
|
199
|
+
*
|
|
200
|
+
* @param {string} content - Current file content.
|
|
201
|
+
* @param {object} [options]
|
|
202
|
+
* @param {string[]} [options.defaultHeader] - Header comment lines used only
|
|
203
|
+
* when the file has none of its own.
|
|
204
|
+
* @returns {{ content: string, changed: boolean, skipped?: string, duplicatesCollapsed?: number }}
|
|
205
|
+
* `skipped` names why the content was left untouched (unrecognized
|
|
206
|
+
* structure, or the paranoia check tripping).
|
|
207
|
+
*/
|
|
208
|
+
function applyEnvOrder(content, { defaultHeader = [] } = {}) {
|
|
209
|
+
const parsed = parseEnvFile(content);
|
|
210
|
+
if (parsed.unparseable) {
|
|
211
|
+
return { content, changed: false, skipped: parsed.unparseable };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const keyLineCount = content.split('\n').filter((line) => KEY_LINE.test(line)).length;
|
|
215
|
+
const duplicatesCollapsed = keyLineCount - parsed.entries.size;
|
|
216
|
+
|
|
217
|
+
const rendered = renderCanonicalEnv({
|
|
218
|
+
header: parsed.header.length > 0 ? parsed.header : defaultHeader,
|
|
219
|
+
entries: parsed.entries,
|
|
220
|
+
strays: parsed.strays,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Paranoia post-check: every key's effective value must survive verbatim
|
|
224
|
+
const before = effectiveValues(content);
|
|
225
|
+
const after = effectiveValues(rendered);
|
|
226
|
+
if (before.size !== after.size || [...before].some(([key, value]) => after.get(key) !== value)) {
|
|
227
|
+
return { content, changed: false, skipped: 'reorder would change effective values (bug guard)' };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return { content: rendered, changed: rendered !== content, duplicatesCollapsed };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// `envLine` is @omega.js/config's serializer, re-exported so the manager's
|
|
234
|
+
// writebacks keep one import — the escaping rules live there, with the
|
|
235
|
+
// composer that has to read them back.
|
|
236
|
+
module.exports = { canonicalEnvGroups, envLine, renderCanonicalEnv, applyEnvOrder };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand .env writeback — persist a secret so every future run (and CI)
|
|
3
|
+
* sees the same value. Used for machine-generated secrets that only have
|
|
4
|
+
* to stay stable (CSC_KEY_PASSWORD, ACCOUNT_PASSWORD_SEED) and for
|
|
5
|
+
* interactively-entered ones (the payment provider-setup flows). Replaces
|
|
6
|
+
* the variable's line in place when it already exists, appends otherwise —
|
|
7
|
+
* then applies the canonical ordering (env-order.js, cp137), the same way
|
|
8
|
+
* writeConfigValues applies the omega.json5 canonical order on every
|
|
9
|
+
* writeback. The order pass is a polish step: if it declines (exotic file
|
|
10
|
+
* structure), the plain replace/append result is written unchanged.
|
|
11
|
+
*
|
|
12
|
+
* mintGeneratedKey is the ONE mint for the keys OMEGA generates for itself
|
|
13
|
+
* (the env schema's `generated:` set): the workspace service's env-keys op
|
|
14
|
+
* does it for the whole set up front, the setup contract (lib/service-input.js)
|
|
15
|
+
* does it for a single key a service reached first — same write, same export,
|
|
16
|
+
* same success line, so a brand cannot tell which one got there first.
|
|
17
|
+
*/
|
|
18
|
+
const { join } = require('node:path');
|
|
19
|
+
const chalk = require('chalk').default;
|
|
20
|
+
const jetpack = require('fs-jetpack');
|
|
21
|
+
|
|
22
|
+
const { generatedEnvKeys } = require('../vendor/config/index.js');
|
|
23
|
+
const { applyEnvOrder, envLine } = require('./env-order.js');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Write NAME="value" into the brand .env (replace-or-append).
|
|
27
|
+
*
|
|
28
|
+
* @param {string} brandRoot - Brand-monorepo root (its .env is the target)
|
|
29
|
+
* @param {string} name - Env var name, e.g. 'CSC_KEY_PASSWORD'
|
|
30
|
+
* @param {string} value - Value to persist
|
|
31
|
+
*/
|
|
32
|
+
function writeEnvValue(brandRoot, name, value) {
|
|
33
|
+
const envPath = join(brandRoot, '.env');
|
|
34
|
+
// envLine escapes \ " and newlines (env-order.js's serializer — the SSOT);
|
|
35
|
+
// the replacer FUNCTION keeps $& / $1 in a secret from being expanded by String.replace
|
|
36
|
+
const line = envLine(name, value);
|
|
37
|
+
const pattern = new RegExp(`^${name}\\s*=.*$`, 'm');
|
|
38
|
+
|
|
39
|
+
let envContent = jetpack.exists(envPath) ? jetpack.read(envPath) : '';
|
|
40
|
+
if (pattern.test(envContent)) {
|
|
41
|
+
envContent = envContent.replace(pattern, () => line);
|
|
42
|
+
} else {
|
|
43
|
+
// Terminate any existing content with exactly one newline, then append
|
|
44
|
+
envContent = envContent === '' ? '' : envContent.replace(/\n*$/, '\n');
|
|
45
|
+
envContent += `${line}\n`;
|
|
46
|
+
}
|
|
47
|
+
jetpack.write(envPath, applyEnvOrder(envContent).content);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Mint one of OMEGA's own generated keys into the brand .env and into THIS
|
|
52
|
+
* process's env, so the same run that minted it can use it. Values are never
|
|
53
|
+
* printed — the line names the key only.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} brandRoot - Brand-monorepo root (its .env is the target)
|
|
56
|
+
* @param {string} name - An env-schema `generated:` key, e.g. 'OMEGA_WEBHOOK_KEY'
|
|
57
|
+
* @param {object} [options]
|
|
58
|
+
* @param {string} [options.indent] - Leading spaces for the success line, so a
|
|
59
|
+
* service-level caller and an operation-level one both sit at their own depth
|
|
60
|
+
* @returns {string} The minted value
|
|
61
|
+
*/
|
|
62
|
+
function mintGeneratedKey(brandRoot, name, options = {}) {
|
|
63
|
+
const generate = generatedEnvKeys()[name];
|
|
64
|
+
if (!generate) {
|
|
65
|
+
// Programmer error: only the env schema says what OMEGA can mint, so a
|
|
66
|
+
// name that is not in that set has no generator and never will
|
|
67
|
+
throw new Error(`${name} is not a generated env key — nothing can mint it`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const value = generate();
|
|
71
|
+
writeEnvValue(brandRoot, name, value);
|
|
72
|
+
process.env[name] = value;
|
|
73
|
+
console.log(`${options.indent ?? ' '}${chalk.green('✓')} ${name} minted into the brand .env`);
|
|
74
|
+
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { writeEnvValue, mintGeneratedKey };
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firestore REST client authenticated with a Google service account —
|
|
3
|
+
* the non-interactive replacement for omega-manager's firebase-admin
|
|
4
|
+
* `getFirestore(brandId)` (which read `.output/{brandId}/secrets/`).
|
|
5
|
+
*
|
|
6
|
+
* Used by the services that write into an external product's Firestore
|
|
7
|
+
* (forms, and chat/email when they port): the operator provides a
|
|
8
|
+
* service-account JSON for that product's Firebase project via an env var
|
|
9
|
+
* holding a file path (absolute, or relative to the brand root — e.g.
|
|
10
|
+
* `.omega/secrets/slapform-service-account.json`).
|
|
11
|
+
*
|
|
12
|
+
* Auth is a plain RS256 JWT-bearer grant (node:crypto — no SDK dependency).
|
|
13
|
+
* Documents cross the API as plain JS objects; the Firestore typed-value
|
|
14
|
+
* encoding is internal. Handlers call named methods (getDoc/patchDoc/setDoc)
|
|
15
|
+
* so tests fake this surface method-for-method.
|
|
16
|
+
*/
|
|
17
|
+
const { isAbsolute, join } = require('node:path');
|
|
18
|
+
const fs = require('node:fs');
|
|
19
|
+
|
|
20
|
+
const { createTokenProvider } = require('./google-token.js');
|
|
21
|
+
|
|
22
|
+
const FIRESTORE_API_BASE = 'https://firestore.googleapis.com/v1';
|
|
23
|
+
const SCOPE = 'https://www.googleapis.com/auth/datastore';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Load and validate a service-account JSON from an env-provided path.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} envValue - Path from the env var (absolute or brand-root-relative)
|
|
29
|
+
* @param {string} brandRoot - Brand-monorepo root for resolving relative paths
|
|
30
|
+
* @returns {Object} Parsed service-account credentials
|
|
31
|
+
*/
|
|
32
|
+
function loadServiceAccount(envValue, brandRoot) {
|
|
33
|
+
const path = isAbsolute(envValue) ? envValue : join(brandRoot, envValue);
|
|
34
|
+
|
|
35
|
+
if (!fs.existsSync(path)) {
|
|
36
|
+
throw new Error(`Service-account file not found at ${path}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let serviceAccount;
|
|
40
|
+
try {
|
|
41
|
+
serviceAccount = JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new Error(`Service-account file at ${path} is not valid JSON: ${error.message}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!serviceAccount.project_id || !serviceAccount.client_email || !serviceAccount.private_key) {
|
|
47
|
+
throw new Error(`Service-account file at ${path} is missing project_id/client_email/private_key`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return serviceAccount;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Encode a plain JS value as a Firestore typed value.
|
|
55
|
+
*/
|
|
56
|
+
function encodeValue(value) {
|
|
57
|
+
if (value === null || value === undefined) {
|
|
58
|
+
return { nullValue: null };
|
|
59
|
+
}
|
|
60
|
+
if (typeof value === 'boolean') {
|
|
61
|
+
return { booleanValue: value };
|
|
62
|
+
}
|
|
63
|
+
if (typeof value === 'number') {
|
|
64
|
+
// REST integers are string-encoded (int64)
|
|
65
|
+
return Number.isInteger(value) ? { integerValue: String(value) } : { doubleValue: value };
|
|
66
|
+
}
|
|
67
|
+
if (typeof value === 'string') {
|
|
68
|
+
return { stringValue: value };
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
return { arrayValue: { values: value.map(encodeValue) } };
|
|
72
|
+
}
|
|
73
|
+
if (typeof value === 'object') {
|
|
74
|
+
return { mapValue: { fields: encodeFields(value) } };
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`Cannot encode value of type ${typeof value} for Firestore`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function encodeFields(obj) {
|
|
80
|
+
const fields = {};
|
|
81
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
82
|
+
fields[key] = encodeValue(value);
|
|
83
|
+
}
|
|
84
|
+
return fields;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Decode a Firestore typed value back to plain JS.
|
|
89
|
+
*/
|
|
90
|
+
function decodeValue(typed) {
|
|
91
|
+
if ('nullValue' in typed) return null;
|
|
92
|
+
if ('booleanValue' in typed) return typed.booleanValue;
|
|
93
|
+
if ('integerValue' in typed) return parseInt(typed.integerValue, 10);
|
|
94
|
+
if ('doubleValue' in typed) return typed.doubleValue;
|
|
95
|
+
if ('stringValue' in typed) return typed.stringValue;
|
|
96
|
+
if ('timestampValue' in typed) return typed.timestampValue;
|
|
97
|
+
if ('referenceValue' in typed) return typed.referenceValue;
|
|
98
|
+
if ('arrayValue' in typed) return (typed.arrayValue.values || []).map(decodeValue);
|
|
99
|
+
if ('mapValue' in typed) return decodeFields(typed.mapValue.fields || {});
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function decodeFields(fields) {
|
|
104
|
+
const obj = {};
|
|
105
|
+
for (const [key, typed] of Object.entries(fields)) {
|
|
106
|
+
obj[key] = decodeValue(typed);
|
|
107
|
+
}
|
|
108
|
+
return obj;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
class FirestoreREST {
|
|
112
|
+
constructor(serviceAccount) {
|
|
113
|
+
this.serviceAccount = serviceAccount;
|
|
114
|
+
this.projectId = serviceAccount.project_id;
|
|
115
|
+
this.tokenProvider = createTokenProvider(serviceAccount, SCOPE);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Access token for the datastore scope (cached by the shared provider).
|
|
120
|
+
*/
|
|
121
|
+
getAccessToken() {
|
|
122
|
+
return this.tokenProvider.getAccessToken();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async request(method, docPath, { query, body } = {}) {
|
|
126
|
+
const token = await this.getAccessToken();
|
|
127
|
+
const url = new URL(`${FIRESTORE_API_BASE}/projects/${this.projectId}/databases/(default)/documents/${docPath}`);
|
|
128
|
+
|
|
129
|
+
for (const [key, values] of Object.entries(query || {})) {
|
|
130
|
+
for (const value of [].concat(values)) {
|
|
131
|
+
url.searchParams.append(key, value);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const response = await fetch(url, {
|
|
136
|
+
method,
|
|
137
|
+
headers: {
|
|
138
|
+
'Authorization': `Bearer ${token}`,
|
|
139
|
+
'Content-Type': 'application/json',
|
|
140
|
+
},
|
|
141
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
142
|
+
signal: AbortSignal.timeout(30000),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (response.status === 404) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const data = await response.json();
|
|
150
|
+
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
throw new Error(`Firestore API error: ${data.error?.message || JSON.stringify(data)}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return data;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* POST to a colon method on the documents root (documents:runQuery,
|
|
160
|
+
* documents:runAggregationQuery) — those can't go through request()'s
|
|
161
|
+
* docPath URL builder.
|
|
162
|
+
*/
|
|
163
|
+
async rootRequest(method, body) {
|
|
164
|
+
const token = await this.getAccessToken();
|
|
165
|
+
const url = `${FIRESTORE_API_BASE}/projects/${this.projectId}/databases/(default)/documents:${method}`;
|
|
166
|
+
|
|
167
|
+
const response = await fetch(url, {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
headers: {
|
|
170
|
+
'Authorization': `Bearer ${token}`,
|
|
171
|
+
'Content-Type': 'application/json',
|
|
172
|
+
},
|
|
173
|
+
body: JSON.stringify(body),
|
|
174
|
+
signal: AbortSignal.timeout(30000),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const data = await response.json();
|
|
178
|
+
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
throw new Error(`Firestore API error: ${data.error?.message || JSON.stringify(data)}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return data;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Read a document as a plain JS object. Missing document → null.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} docPath - e.g. 'forms/abc123'
|
|
190
|
+
*/
|
|
191
|
+
async getDoc(docPath) {
|
|
192
|
+
const doc = await this.request('GET', docPath);
|
|
193
|
+
return doc ? decodeFields(doc.fields || {}) : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Read a document with its server metadata — the REST stand-in for the
|
|
198
|
+
* admin SDK's DocumentSnapshot (create/update times are ISO strings).
|
|
199
|
+
* Missing document → null.
|
|
200
|
+
*
|
|
201
|
+
* @param {string} docPath - e.g. 'users/uid123'
|
|
202
|
+
* @returns {Promise<{ id, createTime, updateTime, data }|null>}
|
|
203
|
+
*/
|
|
204
|
+
async getDocWithMeta(docPath) {
|
|
205
|
+
const doc = await this.request('GET', docPath);
|
|
206
|
+
if (!doc) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
id: doc.name.split('/').pop(),
|
|
211
|
+
createTime: doc.createTime,
|
|
212
|
+
updateTime: doc.updateTime,
|
|
213
|
+
data: decodeFields(doc.fields || {}),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* List one page of a collection's documents (ordered by name — Firestore's
|
|
219
|
+
* listDocuments default), with server metadata like getDocWithMeta.
|
|
220
|
+
*
|
|
221
|
+
* @param {string} collectionPath - e.g. 'users'
|
|
222
|
+
* @param {Object} [options]
|
|
223
|
+
* @param {number} [options.pageSize=500]
|
|
224
|
+
* @param {string} [options.pageToken] - From the previous page's nextPageToken
|
|
225
|
+
* @returns {Promise<{ docs: Array<{ id, createTime, updateTime, data }>, nextPageToken: string|null }>}
|
|
226
|
+
*/
|
|
227
|
+
async listDocs(collectionPath, { pageSize = 500, pageToken } = {}) {
|
|
228
|
+
const query = { pageSize };
|
|
229
|
+
if (pageToken) {
|
|
230
|
+
query.pageToken = pageToken;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const data = (await this.request('GET', collectionPath, { query })) || {};
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
docs: (data.documents || []).map((doc) => ({
|
|
237
|
+
id: doc.name.split('/').pop(),
|
|
238
|
+
createTime: doc.createTime,
|
|
239
|
+
updateTime: doc.updateTime,
|
|
240
|
+
data: decodeFields(doc.fields || {}),
|
|
241
|
+
})),
|
|
242
|
+
nextPageToken: data.nextPageToken || null,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Count a collection's documents server-side (aggregation query — no
|
|
248
|
+
* document reads billed beyond the aggregation).
|
|
249
|
+
*
|
|
250
|
+
* @param {string} collectionId - Top-level collection id, e.g. 'users'
|
|
251
|
+
* @returns {Promise<number>}
|
|
252
|
+
*/
|
|
253
|
+
async countDocs(collectionId) {
|
|
254
|
+
const data = await this.rootRequest('runAggregationQuery', {
|
|
255
|
+
structuredAggregationQuery: {
|
|
256
|
+
structuredQuery: { from: [{ collectionId }] },
|
|
257
|
+
aggregations: [{ alias: 'count', count: {} }],
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const entry = (data || []).find((e) => e.result);
|
|
262
|
+
return entry ? parseInt(entry.result.aggregateFields.count.integerValue, 10) : 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Delete a document. Deleting a missing document is a no-op (Firestore
|
|
267
|
+
* returns success either way).
|
|
268
|
+
*
|
|
269
|
+
* @param {string} docPath - e.g. 'users/uid123'
|
|
270
|
+
*/
|
|
271
|
+
async deleteDoc(docPath) {
|
|
272
|
+
return this.request('DELETE', docPath);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Merge-write specific fields of a document (created if missing) — the
|
|
277
|
+
* REST equivalent of the admin SDK's `set(data, { merge: true })`:
|
|
278
|
+
* updateMask carries the LEAF field paths so sibling fields the caller
|
|
279
|
+
* doesn't mention survive untouched.
|
|
280
|
+
*
|
|
281
|
+
* @param {string} docPath - e.g. 'users/uid123'
|
|
282
|
+
* @param {Object} data - Plain nested object with the desired values
|
|
283
|
+
* @param {string[]} fieldPaths - Dotted leaf paths to write (the updateMask)
|
|
284
|
+
*/
|
|
285
|
+
async patchDoc(docPath, data, fieldPaths) {
|
|
286
|
+
return this.request('PATCH', docPath, {
|
|
287
|
+
query: { 'updateMask.fieldPaths': fieldPaths },
|
|
288
|
+
body: { fields: encodeFields(data) },
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Replace-write a whole document (created if missing) — the REST
|
|
294
|
+
* equivalent of the admin SDK's `set(data, { merge: false })`: no
|
|
295
|
+
* updateMask, so fields absent from `data` are removed from the document.
|
|
296
|
+
*
|
|
297
|
+
* @param {string} docPath - e.g. 'brands/my-brand'
|
|
298
|
+
* @param {Object} data - Plain nested object; becomes the ENTIRE document
|
|
299
|
+
*/
|
|
300
|
+
async setDoc(docPath, data) {
|
|
301
|
+
return this.request('PATCH', docPath, {
|
|
302
|
+
body: { fields: encodeFields(data) },
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Run a structured query and return matching documents as plain objects.
|
|
308
|
+
*
|
|
309
|
+
* @param {Object} structuredQuery - Firestore StructuredQuery, e.g.
|
|
310
|
+
* { from: [{ collectionId: 'users' }], where: { fieldFilter: ... } }
|
|
311
|
+
* @returns {Promise<Array<{ id: string, data: Object }>>}
|
|
312
|
+
*/
|
|
313
|
+
async runQuery(structuredQuery) {
|
|
314
|
+
const data = await this.rootRequest('runQuery', { structuredQuery });
|
|
315
|
+
|
|
316
|
+
// Streamed response: one entry per result, some carry only readTime
|
|
317
|
+
return data
|
|
318
|
+
.filter((entry) => entry.document)
|
|
319
|
+
.map((entry) => ({
|
|
320
|
+
id: entry.document.name.split('/').pop(),
|
|
321
|
+
data: decodeFields(entry.document.fields || {}),
|
|
322
|
+
}));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = { FirestoreREST, loadServiceAccount, encodeFields, decodeFields };
|