@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,1176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Users collection migration — converges user documents to the canonical
|
|
3
|
+
* @omega.js/backend user schema.
|
|
4
|
+
*
|
|
5
|
+
* Fixes:
|
|
6
|
+
* - Deletes orphaned user docs (no matching Firebase Auth user)
|
|
7
|
+
* - Renames `plan` → `subscription`
|
|
8
|
+
* - Transforms flat `subscription.id` (string) → `subscription.product` (object)
|
|
9
|
+
* - Renames `subscription.trial.activated` → `subscription.trial.claimed`
|
|
10
|
+
* - Moves `oauth2.<provider>` → `connections.<provider>`, stamping each
|
|
11
|
+
* record with `type: 'oauth2'`, and deletes the original (#788)
|
|
12
|
+
* - Removes deprecated payment/limits/trial/affiliate fields
|
|
13
|
+
* - Migrates legacy timestamps → metadata.* and reconciles metadata.created
|
|
14
|
+
* against Firebase Auth's canonical creation time
|
|
15
|
+
* - Backfills auth.uid/auth.email from Firebase Auth when missing
|
|
16
|
+
* - Backfills consent (implicit grant at signup) for existing users
|
|
17
|
+
* - Folds the legacy `attribution.utm` blob into `attribution.first`/`last`
|
|
18
|
+
* - Backfills all missing fields with defaults from the @omega.js/backend user schema
|
|
19
|
+
* - Generates dynamic values for affiliate.code, api.clientId, api.privateKey
|
|
20
|
+
* - Normalizes '' and old sentinels ('127.0.0.1', 'ZZ', 'Unknown') to null
|
|
21
|
+
* - Migrates usage.*.period → usage.*.monthly (+ daily backfill) and deletes
|
|
22
|
+
* zero-total usage placeholders
|
|
23
|
+
*
|
|
24
|
+
* De-ITW'd from omega-manager: the company instance's one-off damage-repair
|
|
25
|
+
* fixes (per-brand usage-key renames, stray-key rescue from prior bad runs,
|
|
26
|
+
* HTML-entity decoding from an old sanitize middleware, UID/base64 affiliate
|
|
27
|
+
* code repair, the somiibo keep-plan carve-out) stay in omega-manager —
|
|
28
|
+
* they repair one company's historical data, not the schema.
|
|
29
|
+
*
|
|
30
|
+
* Validates:
|
|
31
|
+
* - All fields match the expected schema
|
|
32
|
+
*/
|
|
33
|
+
const { randomUUID, randomBytes } = require('node:crypto');
|
|
34
|
+
|
|
35
|
+
const { runMigration, FieldValue } = require('../lib/migration-runner.js');
|
|
36
|
+
const { createMetadataFix } = require('../lib/ensure-metadata.js');
|
|
37
|
+
const { validateDocument } = require('../lib/schema-validator.js');
|
|
38
|
+
const { createSanitizeFix } = require('../lib/sanitize-strings.js');
|
|
39
|
+
const { createAttributionFoldFix } = require('../lib/attribution-touch.js');
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generate a random alphanumeric ID (matches nanoid with URL-safe alphabet minus _ and -)
|
|
43
|
+
*/
|
|
44
|
+
function generateId(size = 7) {
|
|
45
|
+
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
46
|
+
const bytes = randomBytes(size);
|
|
47
|
+
let result = '';
|
|
48
|
+
for (let i = 0; i < size; i++) {
|
|
49
|
+
result += alphabet[bytes[i] % alphabet.length];
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Default user structure from @omega.js/backend's user schema.
|
|
56
|
+
* Values here are used to backfill missing fields during migration.
|
|
57
|
+
*
|
|
58
|
+
* Dynamic fields (affiliate.code, api.clientId, api.privateKey) use empty strings here
|
|
59
|
+
* and are generated per-doc in the dynamic-values fix.
|
|
60
|
+
*/
|
|
61
|
+
const DEFAULT_USER = {
|
|
62
|
+
auth: {
|
|
63
|
+
uid: null,
|
|
64
|
+
email: null,
|
|
65
|
+
temporary: false,
|
|
66
|
+
},
|
|
67
|
+
subscription: {
|
|
68
|
+
product: {
|
|
69
|
+
id: 'basic',
|
|
70
|
+
name: 'Basic',
|
|
71
|
+
},
|
|
72
|
+
status: 'active',
|
|
73
|
+
expires: {
|
|
74
|
+
timestamp: '1970-01-01T00:00:00.000Z',
|
|
75
|
+
timestampUNIX: 0,
|
|
76
|
+
},
|
|
77
|
+
trial: {
|
|
78
|
+
claimed: false,
|
|
79
|
+
expires: {
|
|
80
|
+
timestamp: '1970-01-01T00:00:00.000Z',
|
|
81
|
+
timestampUNIX: 0,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
cancellation: {
|
|
85
|
+
pending: false,
|
|
86
|
+
date: {
|
|
87
|
+
timestamp: '1970-01-01T00:00:00.000Z',
|
|
88
|
+
timestampUNIX: 0,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
payment: {
|
|
92
|
+
provider: null,
|
|
93
|
+
orderId: null,
|
|
94
|
+
resourceId: null,
|
|
95
|
+
frequency: null,
|
|
96
|
+
price: 0,
|
|
97
|
+
startDate: {
|
|
98
|
+
timestamp: '1970-01-01T00:00:00.000Z',
|
|
99
|
+
timestampUNIX: 0,
|
|
100
|
+
},
|
|
101
|
+
updatedBy: {
|
|
102
|
+
event: {
|
|
103
|
+
name: null,
|
|
104
|
+
id: null,
|
|
105
|
+
},
|
|
106
|
+
date: {
|
|
107
|
+
timestamp: '1970-01-01T00:00:00.000Z',
|
|
108
|
+
timestampUNIX: 0,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
roles: {
|
|
114
|
+
admin: false,
|
|
115
|
+
betaTester: false,
|
|
116
|
+
developer: false,
|
|
117
|
+
},
|
|
118
|
+
flags: {
|
|
119
|
+
signupProcessed: false,
|
|
120
|
+
},
|
|
121
|
+
affiliate: {
|
|
122
|
+
code: '',
|
|
123
|
+
referrals: [],
|
|
124
|
+
},
|
|
125
|
+
metadata: {
|
|
126
|
+
created: {
|
|
127
|
+
timestamp: '1970-01-01T00:00:00.000Z',
|
|
128
|
+
timestampUNIX: 0,
|
|
129
|
+
},
|
|
130
|
+
updated: {
|
|
131
|
+
timestamp: '1970-01-01T00:00:00.000Z',
|
|
132
|
+
timestampUNIX: 0,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
activity: {
|
|
136
|
+
geolocation: {
|
|
137
|
+
ip: null,
|
|
138
|
+
continent: null,
|
|
139
|
+
country: null,
|
|
140
|
+
region: null,
|
|
141
|
+
city: null,
|
|
142
|
+
latitude: 0,
|
|
143
|
+
longitude: 0,
|
|
144
|
+
},
|
|
145
|
+
client: {
|
|
146
|
+
language: null,
|
|
147
|
+
mobile: false,
|
|
148
|
+
device: null,
|
|
149
|
+
platform: null,
|
|
150
|
+
browser: null,
|
|
151
|
+
vendor: null,
|
|
152
|
+
runtime: null,
|
|
153
|
+
userAgent: null,
|
|
154
|
+
url: null,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
api: {
|
|
158
|
+
clientId: '',
|
|
159
|
+
privateKey: '',
|
|
160
|
+
},
|
|
161
|
+
usage: {},
|
|
162
|
+
personal: {
|
|
163
|
+
birthday: {
|
|
164
|
+
timestamp: '1970-01-01T00:00:00.000Z',
|
|
165
|
+
timestampUNIX: 0,
|
|
166
|
+
},
|
|
167
|
+
gender: null,
|
|
168
|
+
location: {
|
|
169
|
+
country: null,
|
|
170
|
+
region: null,
|
|
171
|
+
city: null,
|
|
172
|
+
},
|
|
173
|
+
name: {
|
|
174
|
+
first: null,
|
|
175
|
+
last: null,
|
|
176
|
+
},
|
|
177
|
+
company: {
|
|
178
|
+
name: null,
|
|
179
|
+
position: null,
|
|
180
|
+
},
|
|
181
|
+
telephone: {
|
|
182
|
+
countryCode: 0,
|
|
183
|
+
national: 0,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
connections: {},
|
|
187
|
+
attribution: {
|
|
188
|
+
affiliate: {
|
|
189
|
+
code: null,
|
|
190
|
+
timestamp: null,
|
|
191
|
+
url: null,
|
|
192
|
+
page: null,
|
|
193
|
+
},
|
|
194
|
+
// The #384 touch model. `tags`/`clickIds` are deliberately absent from the
|
|
195
|
+
// default: a visit that carried none writes no key at all, so backfilling
|
|
196
|
+
// them here would re-inject the empty husks the fold refuses to write.
|
|
197
|
+
first: {
|
|
198
|
+
referrer: null,
|
|
199
|
+
url: null,
|
|
200
|
+
page: null,
|
|
201
|
+
timestamp: null,
|
|
202
|
+
},
|
|
203
|
+
last: {
|
|
204
|
+
referrer: null,
|
|
205
|
+
url: null,
|
|
206
|
+
page: null,
|
|
207
|
+
timestamp: null,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
consent: {
|
|
211
|
+
legal: {
|
|
212
|
+
status: 'revoked',
|
|
213
|
+
grantedAt: { timestamp: null, timestampUNIX: null, source: null, ip: null, text: null },
|
|
214
|
+
},
|
|
215
|
+
marketing: {
|
|
216
|
+
status: 'revoked',
|
|
217
|
+
grantedAt: { timestamp: null, timestampUNIX: null, source: null, ip: null, text: null },
|
|
218
|
+
revokedAt: { timestamp: null, timestampUNIX: null, source: null, ip: null, text: null },
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Users collection schema
|
|
225
|
+
* Based on @omega.js/backend's user schema
|
|
226
|
+
*/
|
|
227
|
+
const timestampSchema = {
|
|
228
|
+
type: 'object',
|
|
229
|
+
required: true,
|
|
230
|
+
properties: {
|
|
231
|
+
timestamp: { type: 'string', required: true },
|
|
232
|
+
timestampUNIX: { type: 'number', required: true },
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* One attribution touch — @omega.js/account's ATTRIBUTION_TOUCH, shared by
|
|
238
|
+
* `attribution.first` and `attribution.last`.
|
|
239
|
+
*
|
|
240
|
+
* `tags` and `clickIds` are optional because capture writes a key only when the
|
|
241
|
+
* visit carried one: an organic landing has neither, and demanding them here
|
|
242
|
+
* would flag every untagged user for a husk nothing should be writing.
|
|
243
|
+
*/
|
|
244
|
+
const attributionTouchSchema = {
|
|
245
|
+
type: 'object',
|
|
246
|
+
required: true,
|
|
247
|
+
properties: {
|
|
248
|
+
tags: { type: 'object', required: false },
|
|
249
|
+
clickIds: { type: 'object', required: false },
|
|
250
|
+
referrer: { type: 'string', required: true, nullable: true },
|
|
251
|
+
url: { type: 'string', required: true, nullable: true },
|
|
252
|
+
page: { type: 'string', required: true, nullable: true },
|
|
253
|
+
timestamp: { type: 'string', required: true, nullable: true },
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const schema = {
|
|
258
|
+
auth: {
|
|
259
|
+
type: 'object',
|
|
260
|
+
required: true,
|
|
261
|
+
properties: {
|
|
262
|
+
uid: { type: 'string', required: true, nullable: true },
|
|
263
|
+
email: { type: 'string', required: true, nullable: true },
|
|
264
|
+
temporary: { type: 'boolean', required: true },
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
subscription: {
|
|
268
|
+
type: 'object',
|
|
269
|
+
required: true,
|
|
270
|
+
properties: {
|
|
271
|
+
product: {
|
|
272
|
+
type: 'object',
|
|
273
|
+
required: true,
|
|
274
|
+
properties: {
|
|
275
|
+
id: { type: 'string', required: true },
|
|
276
|
+
name: { type: 'string', required: true },
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
status: { type: 'string', required: true },
|
|
280
|
+
expires: timestampSchema,
|
|
281
|
+
trial: {
|
|
282
|
+
type: 'object',
|
|
283
|
+
required: true,
|
|
284
|
+
properties: {
|
|
285
|
+
claimed: { type: 'boolean', required: true },
|
|
286
|
+
expires: timestampSchema,
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
cancellation: {
|
|
290
|
+
type: 'object',
|
|
291
|
+
required: true,
|
|
292
|
+
properties: {
|
|
293
|
+
pending: { type: 'boolean', required: true },
|
|
294
|
+
date: timestampSchema,
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
payment: {
|
|
298
|
+
type: 'object',
|
|
299
|
+
required: true,
|
|
300
|
+
properties: {
|
|
301
|
+
provider: { type: 'string', required: true, nullable: true },
|
|
302
|
+
orderId: { type: 'string', required: true, nullable: true },
|
|
303
|
+
resourceId: { type: 'string', required: true, nullable: true },
|
|
304
|
+
frequency: { type: 'string', required: true, nullable: true },
|
|
305
|
+
price: { type: 'number', required: true },
|
|
306
|
+
startDate: timestampSchema,
|
|
307
|
+
updatedBy: {
|
|
308
|
+
type: 'object',
|
|
309
|
+
required: true,
|
|
310
|
+
properties: {
|
|
311
|
+
event: {
|
|
312
|
+
type: 'object',
|
|
313
|
+
required: true,
|
|
314
|
+
properties: {
|
|
315
|
+
name: { type: 'string', required: true, nullable: true },
|
|
316
|
+
id: { type: 'string', required: true, nullable: true },
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
date: timestampSchema,
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
roles: {
|
|
327
|
+
type: 'object',
|
|
328
|
+
required: true,
|
|
329
|
+
properties: {
|
|
330
|
+
admin: { type: 'boolean', required: true },
|
|
331
|
+
betaTester: { type: 'boolean', required: true },
|
|
332
|
+
developer: { type: 'boolean', required: true },
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
flags: {
|
|
336
|
+
type: 'object',
|
|
337
|
+
required: true,
|
|
338
|
+
properties: {
|
|
339
|
+
signupProcessed: { type: 'boolean', required: true },
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
affiliate: {
|
|
343
|
+
type: 'object',
|
|
344
|
+
required: true,
|
|
345
|
+
properties: {
|
|
346
|
+
code: { type: 'string', required: true },
|
|
347
|
+
referrals: { type: 'array', required: true },
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
metadata: {
|
|
351
|
+
type: 'object',
|
|
352
|
+
required: true,
|
|
353
|
+
properties: {
|
|
354
|
+
created: timestampSchema,
|
|
355
|
+
updated: timestampSchema,
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
activity: {
|
|
359
|
+
type: 'object',
|
|
360
|
+
required: true,
|
|
361
|
+
properties: {
|
|
362
|
+
geolocation: {
|
|
363
|
+
type: 'object',
|
|
364
|
+
required: true,
|
|
365
|
+
properties: {
|
|
366
|
+
ip: { type: 'string', required: true, nullable: true },
|
|
367
|
+
continent: { type: 'string', required: true, nullable: true },
|
|
368
|
+
country: { type: 'string', required: true, nullable: true },
|
|
369
|
+
region: { type: 'string', required: true, nullable: true },
|
|
370
|
+
city: { type: 'string', required: true, nullable: true },
|
|
371
|
+
latitude: { type: 'number', required: true },
|
|
372
|
+
longitude: { type: 'number', required: true },
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
client: {
|
|
376
|
+
type: 'object',
|
|
377
|
+
required: true,
|
|
378
|
+
properties: {
|
|
379
|
+
language: { type: 'string', required: true, nullable: true },
|
|
380
|
+
mobile: { type: 'boolean', required: true },
|
|
381
|
+
device: { type: 'string', required: true, nullable: true },
|
|
382
|
+
platform: { type: 'string', required: true, nullable: true },
|
|
383
|
+
browser: { type: 'string', required: true, nullable: true },
|
|
384
|
+
vendor: { type: 'string', required: true, nullable: true },
|
|
385
|
+
runtime: { type: 'string', required: true, nullable: true },
|
|
386
|
+
userAgent: { type: 'string', required: true, nullable: true },
|
|
387
|
+
url: { type: 'string', required: true, nullable: true },
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
api: {
|
|
393
|
+
type: 'object',
|
|
394
|
+
required: true,
|
|
395
|
+
properties: {
|
|
396
|
+
clientId: { type: 'string', required: true },
|
|
397
|
+
privateKey: { type: 'string', required: true },
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
usage: {
|
|
401
|
+
type: 'object',
|
|
402
|
+
required: true,
|
|
403
|
+
},
|
|
404
|
+
personal: {
|
|
405
|
+
type: 'object',
|
|
406
|
+
required: true,
|
|
407
|
+
properties: {
|
|
408
|
+
birthday: timestampSchema,
|
|
409
|
+
gender: { type: 'string', required: true, nullable: true },
|
|
410
|
+
location: {
|
|
411
|
+
type: 'object',
|
|
412
|
+
required: true,
|
|
413
|
+
properties: {
|
|
414
|
+
country: { type: 'string', required: true, nullable: true },
|
|
415
|
+
region: { type: 'string', required: true, nullable: true },
|
|
416
|
+
city: { type: 'string', required: true, nullable: true },
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
name: {
|
|
420
|
+
type: 'object',
|
|
421
|
+
required: true,
|
|
422
|
+
properties: {
|
|
423
|
+
first: { type: 'string', required: true, nullable: true },
|
|
424
|
+
last: { type: 'string', required: true, nullable: true },
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
company: {
|
|
428
|
+
type: 'object',
|
|
429
|
+
required: true,
|
|
430
|
+
properties: {
|
|
431
|
+
name: { type: 'string', required: true, nullable: true },
|
|
432
|
+
position: { type: 'string', required: true, nullable: true },
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
telephone: {
|
|
436
|
+
type: 'object',
|
|
437
|
+
required: true,
|
|
438
|
+
properties: {
|
|
439
|
+
countryCode: { type: 'number', required: true },
|
|
440
|
+
national: { type: 'number', required: true },
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
connections: { type: 'object', required: true },
|
|
446
|
+
attribution: {
|
|
447
|
+
type: 'object',
|
|
448
|
+
required: true,
|
|
449
|
+
properties: {
|
|
450
|
+
affiliate: {
|
|
451
|
+
type: 'object',
|
|
452
|
+
required: true,
|
|
453
|
+
properties: {
|
|
454
|
+
code: { type: 'string', required: true, nullable: true },
|
|
455
|
+
timestamp: { type: 'string', required: true, nullable: true },
|
|
456
|
+
url: { type: 'string', required: true, nullable: true },
|
|
457
|
+
page: { type: 'string', required: true, nullable: true },
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
first: attributionTouchSchema,
|
|
461
|
+
last: attributionTouchSchema,
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
consent: {
|
|
465
|
+
type: 'object',
|
|
466
|
+
required: true,
|
|
467
|
+
properties: {
|
|
468
|
+
legal: {
|
|
469
|
+
type: 'object',
|
|
470
|
+
required: true,
|
|
471
|
+
properties: {
|
|
472
|
+
status: { type: 'string', required: true },
|
|
473
|
+
grantedAt: {
|
|
474
|
+
type: 'object',
|
|
475
|
+
required: true,
|
|
476
|
+
properties: {
|
|
477
|
+
timestamp: { type: 'string', required: true, nullable: true },
|
|
478
|
+
timestampUNIX: { type: 'number', required: true, nullable: true },
|
|
479
|
+
source: { type: 'string', required: true, nullable: true },
|
|
480
|
+
ip: { type: 'string', required: true, nullable: true },
|
|
481
|
+
text: { type: 'string', required: true, nullable: true },
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
marketing: {
|
|
487
|
+
type: 'object',
|
|
488
|
+
required: true,
|
|
489
|
+
properties: {
|
|
490
|
+
status: { type: 'string', required: true },
|
|
491
|
+
grantedAt: {
|
|
492
|
+
type: 'object',
|
|
493
|
+
required: true,
|
|
494
|
+
properties: {
|
|
495
|
+
timestamp: { type: 'string', required: true, nullable: true },
|
|
496
|
+
timestampUNIX: { type: 'number', required: true, nullable: true },
|
|
497
|
+
source: { type: 'string', required: true, nullable: true },
|
|
498
|
+
ip: { type: 'string', required: true, nullable: true },
|
|
499
|
+
text: { type: 'string', required: true, nullable: true },
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
revokedAt: {
|
|
503
|
+
type: 'object',
|
|
504
|
+
required: true,
|
|
505
|
+
properties: {
|
|
506
|
+
timestamp: { type: 'string', required: true, nullable: true },
|
|
507
|
+
timestampUNIX: { type: 'number', required: true, nullable: true },
|
|
508
|
+
source: { type: 'string', required: true, nullable: true },
|
|
509
|
+
ip: { type: 'string', required: true, nullable: true },
|
|
510
|
+
text: { type: 'string', required: true, nullable: true },
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Deep merge defaults under existing data (existing values take precedence)
|
|
521
|
+
*/
|
|
522
|
+
function deepMergeDefaults(existing, defaults) {
|
|
523
|
+
const result = { ...defaults };
|
|
524
|
+
|
|
525
|
+
for (const [key, value] of Object.entries(existing)) {
|
|
526
|
+
if (value && typeof value === 'object' && !Array.isArray(value)
|
|
527
|
+
&& defaults[key] && typeof defaults[key] === 'object' && !Array.isArray(defaults[key])) {
|
|
528
|
+
result[key] = deepMergeDefaults(value, defaults[key]);
|
|
529
|
+
} else {
|
|
530
|
+
result[key] = value;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return result;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Deep equality check (key-order insensitive)
|
|
539
|
+
*/
|
|
540
|
+
function deepEqual(a, b) {
|
|
541
|
+
if (a === b) {
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (a === null || b === null || typeof a !== typeof b) {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (Array.isArray(a)) {
|
|
550
|
+
if (!Array.isArray(b) || a.length !== b.length) {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
return a.every((item, i) => deepEqual(item, b[i]));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (typeof a === 'object') {
|
|
557
|
+
const keysA = Object.keys(a);
|
|
558
|
+
const keysB = Object.keys(b);
|
|
559
|
+
if (keysA.length !== keysB.length) {
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
return keysA.every((key) => key in b && deepEqual(a[key], b[key]));
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Capitalize first letter of a string
|
|
570
|
+
*/
|
|
571
|
+
function capitalize(str) {
|
|
572
|
+
if (!str) {
|
|
573
|
+
return '';
|
|
574
|
+
}
|
|
575
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Users collection migration handler.
|
|
580
|
+
*
|
|
581
|
+
* @param {Object} context - Handler context (authAdmin + firestore from setup)
|
|
582
|
+
* @returns {Object} Migration results
|
|
583
|
+
*/
|
|
584
|
+
module.exports = async function ensureUsers(context) {
|
|
585
|
+
const { brandId, brandConfig, authAdmin } = context;
|
|
586
|
+
|
|
587
|
+
// Cache auth lookups to avoid redundant API calls within a single migration run
|
|
588
|
+
const authCache = new Map();
|
|
589
|
+
|
|
590
|
+
// Cache affiliate code lookups: UID → affiliateCode or null
|
|
591
|
+
const affiliateCache = new Map();
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Resolve a UID to the user's affiliate.code via DB lookup (cached).
|
|
595
|
+
* Returns the affiliate code string, or null if user not found / has no code.
|
|
596
|
+
*/
|
|
597
|
+
async function resolveAffiliateCode(db, uid) {
|
|
598
|
+
if (affiliateCache.has(uid)) {
|
|
599
|
+
return affiliateCache.get(uid);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
const referrer = await db.getDoc(`users/${uid}`);
|
|
604
|
+
const code = referrer?.affiliate?.code || null;
|
|
605
|
+
affiliateCache.set(uid, code);
|
|
606
|
+
return code;
|
|
607
|
+
} catch {
|
|
608
|
+
affiliateCache.set(uid, null);
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return runMigration(context, {
|
|
614
|
+
collection: 'users',
|
|
615
|
+
|
|
616
|
+
fixes: [
|
|
617
|
+
// Fix 0: Delete orphaned user docs (no matching Firebase Auth user).
|
|
618
|
+
// Caches the user record (or null) so later fixes can read metadata.creationTime.
|
|
619
|
+
async (data, doc) => {
|
|
620
|
+
const uid = data.auth?.uid || doc.id;
|
|
621
|
+
|
|
622
|
+
if (authCache.has(uid)) {
|
|
623
|
+
return authCache.get(uid) ? null : { __delete__: true, reason: 'orphaned auth user' };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const MAX_RETRIES = 5;
|
|
627
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
628
|
+
try {
|
|
629
|
+
const userRecord = await authAdmin.getUser(uid);
|
|
630
|
+
authCache.set(uid, userRecord);
|
|
631
|
+
return userRecord ? null : { __delete__: true, reason: 'orphaned auth user' };
|
|
632
|
+
} catch (error) {
|
|
633
|
+
// Retry on transient errors; missing users are a null return, not a throw
|
|
634
|
+
if (attempt < MAX_RETRIES) {
|
|
635
|
+
await new Promise((r) => setTimeout(r, 1000 * attempt));
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
throw error;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
|
|
643
|
+
// Fix 1: plan → subscription
|
|
644
|
+
(data) => {
|
|
645
|
+
if (data.plan === undefined) {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const updates = { plan: FieldValue.delete() };
|
|
650
|
+
|
|
651
|
+
// Merge plan into subscription (subscription takes precedence if both exist)
|
|
652
|
+
if (data.subscription === undefined) {
|
|
653
|
+
updates.subscription = data.plan;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return updates;
|
|
657
|
+
},
|
|
658
|
+
|
|
659
|
+
// Fix 2: subscription.id (flat string) → subscription.product (object)
|
|
660
|
+
(data) => {
|
|
661
|
+
if (!data.subscription || typeof data.subscription.id !== 'string') {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const id = data.subscription.id;
|
|
666
|
+
const name = (typeof data.subscription.name === 'string')
|
|
667
|
+
? data.subscription.name
|
|
668
|
+
: capitalize(id);
|
|
669
|
+
|
|
670
|
+
const updates = {
|
|
671
|
+
'subscription.product': { id, name },
|
|
672
|
+
'subscription.id': FieldValue.delete(),
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
if (typeof data.subscription.name === 'string') {
|
|
676
|
+
updates['subscription.name'] = FieldValue.delete();
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return updates;
|
|
680
|
+
},
|
|
681
|
+
|
|
682
|
+
// Fix 3: trial.activated → trial.claimed
|
|
683
|
+
(data) => {
|
|
684
|
+
if (data.subscription?.trial?.activated === undefined) {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
return {
|
|
688
|
+
'subscription.trial.claimed': data.subscription.trial.activated,
|
|
689
|
+
'subscription.trial.activated': FieldValue.delete(),
|
|
690
|
+
};
|
|
691
|
+
},
|
|
692
|
+
|
|
693
|
+
// Fix 4: oauth2 → connections, each record stamped with its KIND
|
|
694
|
+
// ([#788](https://github.com/Omega-JS-Stack/omega/issues/788)), and every
|
|
695
|
+
// record carrying the `identity.id` the connections route matches on
|
|
696
|
+
// ([#793](https://github.com/Omega-JS-Stack/omega/issues/793)). The
|
|
697
|
+
// product concept is a CONNECTION and a connection will not always be an
|
|
698
|
+
// OAuth grant, so the field carries the product word and every record
|
|
699
|
+
// says which kind it is. The original is deleted in the same write (the
|
|
700
|
+
// standing ruling on moved fields). Both keys present keeps `connections`
|
|
701
|
+
// (written by current code, so newer) and drops the leftover, the same
|
|
702
|
+
// rule `payment-provider` follows.
|
|
703
|
+
//
|
|
704
|
+
// The BACKFILL runs over an existing `connections` map too, not only over
|
|
705
|
+
// what this run moves: the rename shipped before the id did, so a document
|
|
706
|
+
// moved by the #788 code has no `oauth2` left to trigger anything and
|
|
707
|
+
// would never gain one. A document that needs neither the move nor the
|
|
708
|
+
// backfill writes nothing at all.
|
|
709
|
+
(data) => {
|
|
710
|
+
const hasLegacy = data.oauth2 !== undefined;
|
|
711
|
+
const existing = (data.connections && typeof data.connections === 'object') ? data.connections : {};
|
|
712
|
+
|
|
713
|
+
if (!hasLegacy && Object.keys(existing).length === 0) {
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const legacy = (data.oauth2 && typeof data.oauth2 === 'object') ? data.oauth2 : {};
|
|
718
|
+
const moved = { ...existing };
|
|
719
|
+
|
|
720
|
+
for (const [provider, record] of Object.entries(legacy)) {
|
|
721
|
+
if (provider in moved) {
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
moved[provider] = (record && typeof record === 'object')
|
|
726
|
+
? { ...record, type: 'oauth2' }
|
|
727
|
+
: record;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// A record written before the id existed carries whatever its provider
|
|
731
|
+
// answered: Google's `sub`, Kick's numeric `user_id`. The id is filled
|
|
732
|
+
// in beside them — as a STRING, which is what the query compares — and
|
|
733
|
+
// nothing is deleted: the identity is stored as the provider gave it.
|
|
734
|
+
let backfilled = false;
|
|
735
|
+
|
|
736
|
+
for (const [provider, record] of Object.entries(moved)) {
|
|
737
|
+
if (!record || typeof record !== 'object') {
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const identity = record.identity;
|
|
742
|
+
|
|
743
|
+
if (!identity || typeof identity !== 'object' || typeof identity.id === 'string') {
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const stableId = identity.sub ?? identity.user_id ?? identity.id;
|
|
748
|
+
|
|
749
|
+
if (stableId === undefined || stableId === null || stableId === '') {
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// A COPY, never a mutation of the document data this audit is reading
|
|
754
|
+
moved[provider] = { ...record, identity: { ...identity, id: `${stableId}` } };
|
|
755
|
+
backfilled = true;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (!hasLegacy) {
|
|
759
|
+
return backfilled ? { connections: moved } : null;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return { connections: moved, oauth2: FieldValue.delete() };
|
|
763
|
+
},
|
|
764
|
+
|
|
765
|
+
// Fix 5: Remove deprecated fields
|
|
766
|
+
(data) => {
|
|
767
|
+
const updates = {};
|
|
768
|
+
let hasUpdates = false;
|
|
769
|
+
|
|
770
|
+
if (data.subscription?.payment?.active !== undefined) {
|
|
771
|
+
updates['subscription.payment.active'] = FieldValue.delete();
|
|
772
|
+
hasUpdates = true;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (data.subscription?.limits !== undefined) {
|
|
776
|
+
updates['subscription.limits'] = FieldValue.delete();
|
|
777
|
+
hasUpdates = true;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (data.subscription?.payment?.ignoreUntil !== undefined) {
|
|
781
|
+
updates['subscription.payment.ignoreUntil'] = FieldValue.delete();
|
|
782
|
+
hasUpdates = true;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (data.subscription?.payment?.method !== undefined) {
|
|
786
|
+
updates['subscription.payment.method'] = FieldValue.delete();
|
|
787
|
+
hasUpdates = true;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (data.subscription?.trial?.date !== undefined) {
|
|
791
|
+
updates['subscription.trial.date'] = FieldValue.delete();
|
|
792
|
+
hasUpdates = true;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (data.affiliate?.referredBy !== undefined) {
|
|
796
|
+
updates['affiliate.referredBy'] = FieldValue.delete();
|
|
797
|
+
hasUpdates = true;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return hasUpdates ? updates : null;
|
|
801
|
+
},
|
|
802
|
+
|
|
803
|
+
// Fix 6: Fix affiliate.referrals from object to array,
|
|
804
|
+
// and migrate affiliate.referrer → attribution.affiliate.code
|
|
805
|
+
// NOTE: affiliate.referrer stored the referrer's UID, not their affiliate code.
|
|
806
|
+
// We resolve the UID to the actual affiliate code via DB lookup.
|
|
807
|
+
async (data, doc, db) => {
|
|
808
|
+
const updates = {};
|
|
809
|
+
let hasUpdates = false;
|
|
810
|
+
|
|
811
|
+
if (data.affiliate?.referrals && !Array.isArray(data.affiliate.referrals)) {
|
|
812
|
+
updates['affiliate.referrals'] = [];
|
|
813
|
+
hasUpdates = true;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (data.affiliate?.referrer !== undefined) {
|
|
817
|
+
// Migrate to attribution.affiliate.code if not already set
|
|
818
|
+
if (data.affiliate.referrer && !data.attribution?.affiliate?.code) {
|
|
819
|
+
const resolved = await resolveAffiliateCode(db, data.affiliate.referrer);
|
|
820
|
+
if (resolved) {
|
|
821
|
+
updates['attribution.affiliate.code'] = resolved;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
updates['affiliate.referrer'] = FieldValue.delete();
|
|
825
|
+
hasUpdates = true;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return hasUpdates ? updates : null;
|
|
829
|
+
},
|
|
830
|
+
|
|
831
|
+
// Fix 7: Migrate legacy timestamps → metadata.created/updated
|
|
832
|
+
// Falls back to the document's server createTime/updateTime if no legacy fields exist
|
|
833
|
+
createMetadataFix({
|
|
834
|
+
legacyCreatedFields: ['activity.created', 'created'],
|
|
835
|
+
legacyUpdatedFields: ['activity.lastActivity', 'updated'],
|
|
836
|
+
}),
|
|
837
|
+
|
|
838
|
+
// Fix 8: Reconcile metadata.created against Firebase Auth's creationTime.
|
|
839
|
+
// Auth is the canonical source for account creation — if the doc's value differs,
|
|
840
|
+
// overwrite with auth's. The user record was cached in Fix 0 so no extra API call.
|
|
841
|
+
(data, doc) => {
|
|
842
|
+
const uid = data.auth?.uid || doc.id;
|
|
843
|
+
const userRecord = authCache.get(uid);
|
|
844
|
+
const creationTime = userRecord?.metadata?.creationTime;
|
|
845
|
+
if (!creationTime) {
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const authDate = new Date(creationTime);
|
|
850
|
+
const authUNIX = Math.round(authDate.getTime() / 1000);
|
|
851
|
+
const currentUNIX = data.metadata?.created?.timestampUNIX;
|
|
852
|
+
|
|
853
|
+
if (currentUNIX === authUNIX) {
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return {
|
|
858
|
+
'metadata.created.timestamp': authDate.toISOString(),
|
|
859
|
+
'metadata.created.timestampUNIX': authUNIX,
|
|
860
|
+
};
|
|
861
|
+
},
|
|
862
|
+
|
|
863
|
+
// Fix 9: Backfill auth.uid and auth.email from Firebase Auth when missing.
|
|
864
|
+
// Some users end up in Firestore without the signup trigger ever running
|
|
865
|
+
// (trigger silently dropped at create time, or the doc was incrementally
|
|
866
|
+
// built by middleware writes only). Firebase Auth is the canonical source —
|
|
867
|
+
// use the cached user record from Fix 0 to backfill.
|
|
868
|
+
(data, doc) => {
|
|
869
|
+
const uid = data.auth?.uid || doc.id;
|
|
870
|
+
const userRecord = authCache.get(uid);
|
|
871
|
+
if (!userRecord) {
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const updates = {};
|
|
876
|
+
if (!data.auth?.uid) {
|
|
877
|
+
updates['auth.uid'] = uid;
|
|
878
|
+
}
|
|
879
|
+
if (!data.auth?.email && userRecord.email) {
|
|
880
|
+
updates['auth.email'] = userRecord.email;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
return Object.keys(updates).length > 0 ? updates : null;
|
|
884
|
+
},
|
|
885
|
+
|
|
886
|
+
// Fix 10: Flatten personal.company from string to object
|
|
887
|
+
(data) => {
|
|
888
|
+
if (typeof data.personal?.company !== 'string') {
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
return {
|
|
892
|
+
personal: {
|
|
893
|
+
...data.personal,
|
|
894
|
+
company: { name: data.personal.company || null, position: null },
|
|
895
|
+
},
|
|
896
|
+
};
|
|
897
|
+
},
|
|
898
|
+
|
|
899
|
+
// Fix 11: Backfill consent for existing users (implicit grant at signup).
|
|
900
|
+
// Runs before the defaults backfill so derived values aren't overwritten
|
|
901
|
+
// by DEFAULT_USER nulls. metadata.created is finalized by Fix 7/8;
|
|
902
|
+
// activity.geolocation.ip is on the doc or null.
|
|
903
|
+
(data) => {
|
|
904
|
+
if (data.consent?.legal?.status === 'granted') {
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const createdTimestamp = data.metadata?.created?.timestamp || null;
|
|
909
|
+
const createdTimestampUNIX = data.metadata?.created?.timestampUNIX || null;
|
|
910
|
+
const ip = data.activity?.geolocation?.ip || null;
|
|
911
|
+
const brandName = brandConfig?.brand?.name || brandId;
|
|
912
|
+
const legalText = `I agree to ${brandName}'s Terms of Service and Privacy Policy.`;
|
|
913
|
+
const marketingText = `I agree to receive product updates, newsletters, and marketing communications from ${brandName}. You can unsubscribe anytime.`;
|
|
914
|
+
|
|
915
|
+
return {
|
|
916
|
+
'consent.legal.status': 'granted',
|
|
917
|
+
'consent.legal.grantedAt.timestamp': createdTimestamp,
|
|
918
|
+
'consent.legal.grantedAt.timestampUNIX': createdTimestampUNIX,
|
|
919
|
+
'consent.legal.grantedAt.source': 'signup',
|
|
920
|
+
'consent.legal.grantedAt.ip': ip,
|
|
921
|
+
'consent.legal.grantedAt.text': legalText,
|
|
922
|
+
'consent.marketing.status': 'granted',
|
|
923
|
+
'consent.marketing.grantedAt.timestamp': createdTimestamp,
|
|
924
|
+
'consent.marketing.grantedAt.timestampUNIX': createdTimestampUNIX,
|
|
925
|
+
'consent.marketing.grantedAt.source': 'signup',
|
|
926
|
+
'consent.marketing.grantedAt.ip': ip,
|
|
927
|
+
'consent.marketing.grantedAt.text': marketingText,
|
|
928
|
+
};
|
|
929
|
+
},
|
|
930
|
+
|
|
931
|
+
// Fix 12: Reconcile consent.{legal,marketing}.grantedAt against metadata.created.
|
|
932
|
+
// Existing granted consents were backfilled from whatever metadata.created was at
|
|
933
|
+
// the time — now that Fix 8 pulls the canonical creation time from Firebase Auth,
|
|
934
|
+
// any pre-existing grantedAt timestamps that don't match should be corrected.
|
|
935
|
+
(data) => {
|
|
936
|
+
const createdTimestamp = data.metadata?.created?.timestamp;
|
|
937
|
+
const createdTimestampUNIX = data.metadata?.created?.timestampUNIX;
|
|
938
|
+
if (!createdTimestamp || !createdTimestampUNIX) {
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const updates = {};
|
|
943
|
+
for (const kind of ['legal', 'marketing']) {
|
|
944
|
+
const grantedAt = data.consent?.[kind]?.grantedAt;
|
|
945
|
+
if (!grantedAt) {
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
if (grantedAt.timestampUNIX !== createdTimestampUNIX) {
|
|
949
|
+
updates[`consent.${kind}.grantedAt.timestamp`] = createdTimestamp;
|
|
950
|
+
updates[`consent.${kind}.grantedAt.timestampUNIX`] = createdTimestampUNIX;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
return Object.keys(updates).length > 0 ? updates : null;
|
|
955
|
+
},
|
|
956
|
+
|
|
957
|
+
// Fix 13: Fold the legacy attribution.utm blob → attribution.first/last.
|
|
958
|
+
// Runs before the defaults backfill for the same reason Fix 11 does: once
|
|
959
|
+
// the backfill has written the empty touches, the fold reads them as an
|
|
960
|
+
// earlier migration's work and drops the blob it should have folded.
|
|
961
|
+
createAttributionFoldFix(),
|
|
962
|
+
|
|
963
|
+
// Fix 14: Backfill all missing fields with defaults
|
|
964
|
+
(data) => {
|
|
965
|
+
const merged = deepMergeDefaults(data, DEFAULT_USER);
|
|
966
|
+
|
|
967
|
+
// Build flat update object for only the top-level keys that changed
|
|
968
|
+
// Uses deepEqual to avoid false positives from key-order differences
|
|
969
|
+
const updates = {};
|
|
970
|
+
for (const key of Object.keys(DEFAULT_USER)) {
|
|
971
|
+
if (!deepEqual(data[key], merged[key])) {
|
|
972
|
+
updates[key] = merged[key];
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
return Object.keys(updates).length > 0 ? updates : null;
|
|
977
|
+
},
|
|
978
|
+
|
|
979
|
+
// Fix 15: Generate dynamic values for empty fields
|
|
980
|
+
// The @omega.js/backend user schema generates these at signup: affiliate.code, api.clientId, api.privateKey
|
|
981
|
+
(data) => {
|
|
982
|
+
const updates = {};
|
|
983
|
+
let hasUpdates = false;
|
|
984
|
+
|
|
985
|
+
if (!data.affiliate?.code) {
|
|
986
|
+
updates['affiliate.code'] = generateId(7);
|
|
987
|
+
hasUpdates = true;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if (!data.api?.clientId) {
|
|
991
|
+
updates['api.clientId'] = randomUUID();
|
|
992
|
+
hasUpdates = true;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (!data.api?.privateKey) {
|
|
996
|
+
updates['api.privateKey'] = randomBytes(32).toString('hex');
|
|
997
|
+
hasUpdates = true;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return hasUpdates ? updates : null;
|
|
1001
|
+
},
|
|
1002
|
+
|
|
1003
|
+
// Fix 16: Normalize empty strings and old sentinel values to null
|
|
1004
|
+
// Old defaults used '' for unknown strings and '127.0.0.1'/'ZZ'/'Unknown' for geolocation
|
|
1005
|
+
(data) => {
|
|
1006
|
+
const NULLABLE_FIELDS = [
|
|
1007
|
+
'activity.geolocation.ip',
|
|
1008
|
+
'activity.geolocation.continent',
|
|
1009
|
+
'activity.geolocation.country',
|
|
1010
|
+
'activity.geolocation.region',
|
|
1011
|
+
'activity.geolocation.city',
|
|
1012
|
+
'activity.client.language',
|
|
1013
|
+
'activity.client.device',
|
|
1014
|
+
'activity.client.platform',
|
|
1015
|
+
'activity.client.browser',
|
|
1016
|
+
'activity.client.vendor',
|
|
1017
|
+
'activity.client.runtime',
|
|
1018
|
+
'activity.client.userAgent',
|
|
1019
|
+
'activity.client.url',
|
|
1020
|
+
'personal.gender',
|
|
1021
|
+
'personal.location.country',
|
|
1022
|
+
'personal.location.region',
|
|
1023
|
+
'personal.location.city',
|
|
1024
|
+
'personal.name.first',
|
|
1025
|
+
'personal.name.last',
|
|
1026
|
+
'personal.company.name',
|
|
1027
|
+
'personal.company.position',
|
|
1028
|
+
];
|
|
1029
|
+
|
|
1030
|
+
const SENTINEL_VALUES = new Set(['', '127.0.0.1', 'ZZ', 'Unknown']);
|
|
1031
|
+
|
|
1032
|
+
const updates = {};
|
|
1033
|
+
let hasUpdates = false;
|
|
1034
|
+
|
|
1035
|
+
for (const path of NULLABLE_FIELDS) {
|
|
1036
|
+
const parts = path.split('.');
|
|
1037
|
+
let value = data;
|
|
1038
|
+
for (const part of parts) {
|
|
1039
|
+
value = value?.[part];
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (typeof value === 'string' && SENTINEL_VALUES.has(value)) {
|
|
1043
|
+
updates[path] = null;
|
|
1044
|
+
hasUpdates = true;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return hasUpdates ? updates : null;
|
|
1049
|
+
},
|
|
1050
|
+
|
|
1051
|
+
// Fix 17: Recursively trim whitespace from all string values
|
|
1052
|
+
createSanitizeFix(),
|
|
1053
|
+
|
|
1054
|
+
// Fix 18: Reset null values to their correct defaults for non-nullable fields
|
|
1055
|
+
(data) => {
|
|
1056
|
+
const RESET_MAP = {
|
|
1057
|
+
// Timestamps should never be null — reset to epoch
|
|
1058
|
+
'personal.birthday.timestamp': '1970-01-01T00:00:00.000Z',
|
|
1059
|
+
'personal.birthday.timestampUNIX': 0,
|
|
1060
|
+
'metadata.created.timestamp': '1970-01-01T00:00:00.000Z',
|
|
1061
|
+
'metadata.created.timestampUNIX': 0,
|
|
1062
|
+
'metadata.updated.timestamp': '1970-01-01T00:00:00.000Z',
|
|
1063
|
+
'metadata.updated.timestampUNIX': 0,
|
|
1064
|
+
'subscription.expires.timestamp': '1970-01-01T00:00:00.000Z',
|
|
1065
|
+
'subscription.expires.timestampUNIX': 0,
|
|
1066
|
+
'subscription.trial.expires.timestamp': '1970-01-01T00:00:00.000Z',
|
|
1067
|
+
'subscription.trial.expires.timestampUNIX': 0,
|
|
1068
|
+
'subscription.cancellation.date.timestamp': '1970-01-01T00:00:00.000Z',
|
|
1069
|
+
'subscription.cancellation.date.timestampUNIX': 0,
|
|
1070
|
+
'subscription.payment.startDate.timestamp': '1970-01-01T00:00:00.000Z',
|
|
1071
|
+
'subscription.payment.startDate.timestampUNIX': 0,
|
|
1072
|
+
'subscription.payment.updatedBy.date.timestamp': '1970-01-01T00:00:00.000Z',
|
|
1073
|
+
'subscription.payment.updatedBy.date.timestampUNIX': 0,
|
|
1074
|
+
// Coordinates should never be null — reset to 0
|
|
1075
|
+
'activity.geolocation.latitude': 0,
|
|
1076
|
+
'activity.geolocation.longitude': 0,
|
|
1077
|
+
// Boolean fields should never be null — reset to false
|
|
1078
|
+
'activity.client.mobile': false,
|
|
1079
|
+
// Telephone fields should never be null — reset to 0
|
|
1080
|
+
'personal.telephone.countryCode': 0,
|
|
1081
|
+
'personal.telephone.national': 0,
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
const updates = {};
|
|
1085
|
+
let hasUpdates = false;
|
|
1086
|
+
|
|
1087
|
+
for (const [path, defaultValue] of Object.entries(RESET_MAP)) {
|
|
1088
|
+
const parts = path.split('.');
|
|
1089
|
+
let value = data;
|
|
1090
|
+
for (const part of parts) {
|
|
1091
|
+
value = value?.[part];
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
if (value === null || (typeof defaultValue === 'number' && value === '')) {
|
|
1095
|
+
updates[path] = defaultValue;
|
|
1096
|
+
hasUpdates = true;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return hasUpdates ? updates : null;
|
|
1101
|
+
},
|
|
1102
|
+
|
|
1103
|
+
// Fix 19: Migrate usage.*.period → usage.*.monthly + add usage.*.daily
|
|
1104
|
+
// Sets the whole usage.{metric} object to avoid dot-notation conflicts with Fix 20
|
|
1105
|
+
(data) => {
|
|
1106
|
+
if (!data.usage || typeof data.usage !== 'object') {
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const updates = {};
|
|
1111
|
+
let hasUpdates = false;
|
|
1112
|
+
|
|
1113
|
+
for (const [metric, values] of Object.entries(data.usage)) {
|
|
1114
|
+
if (!values || typeof values !== 'object') {
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const needsPeriodMigrate = values.period !== undefined;
|
|
1119
|
+
const needsDailyBackfill = values.daily === undefined;
|
|
1120
|
+
|
|
1121
|
+
if (!needsPeriodMigrate && !needsDailyBackfill) {
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// Build the corrected object (removing period, adding monthly/daily)
|
|
1126
|
+
const corrected = { ...values };
|
|
1127
|
+
|
|
1128
|
+
if (needsPeriodMigrate) {
|
|
1129
|
+
corrected.monthly = (corrected.monthly || 0) + (corrected.period || 0);
|
|
1130
|
+
delete corrected.period;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (needsDailyBackfill) {
|
|
1134
|
+
corrected.daily = 0;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
updates[`usage.${metric}`] = corrected;
|
|
1138
|
+
hasUpdates = true;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
return hasUpdates ? updates : null;
|
|
1142
|
+
},
|
|
1143
|
+
|
|
1144
|
+
// Fix 20: Delete any usage key where total == 0 (unused placeholder)
|
|
1145
|
+
// @omega.js/backend creates usage keys on first use — no need for zero-total placeholders.
|
|
1146
|
+
(data) => {
|
|
1147
|
+
if (!data.usage || typeof data.usage !== 'object') {
|
|
1148
|
+
return null;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const updates = {};
|
|
1152
|
+
let hasUpdates = false;
|
|
1153
|
+
|
|
1154
|
+
for (const [key, values] of Object.entries(data.usage)) {
|
|
1155
|
+
if (!values || typeof values !== 'object') {
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if ((values.total || 0) === 0) {
|
|
1160
|
+
updates[`usage.${key}`] = FieldValue.delete();
|
|
1161
|
+
hasUpdates = true;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
return hasUpdates ? updates : null;
|
|
1166
|
+
},
|
|
1167
|
+
],
|
|
1168
|
+
|
|
1169
|
+
validate: (data) => {
|
|
1170
|
+
return validateDocument(data, schema);
|
|
1171
|
+
},
|
|
1172
|
+
});
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
// Exported for tests — the canonical @omega.js/backend default user shape the backfill converges to
|
|
1176
|
+
module.exports.DEFAULT_USER = DEFAULT_USER;
|