@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.
Files changed (416) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/LICENSE +98 -0
  3. package/README.md +165 -0
  4. package/bin/omega +2 -0
  5. package/bin/omg +2 -0
  6. package/claude-plugin/.claude-plugin/plugin.json +11 -0
  7. package/claude-plugin/.mcp.json +9 -0
  8. package/claude-plugin/README.md +139 -0
  9. package/claude-plugin/hooks/gate/mark.sh +58 -0
  10. package/claude-plugin/hooks/gate/run.sh +97 -0
  11. package/claude-plugin/hooks/guard/run.sh +249 -0
  12. package/claude-plugin/hooks/hooks.json +63 -0
  13. package/claude-plugin/hooks/inject/run.sh +127 -0
  14. package/claude-plugin/hooks/lib/omega-gate.sh +20 -0
  15. package/claude-plugin/hooks/lib/omega-scope.sh +41 -0
  16. package/claude-plugin/hooks/lib/omega-skills.sh +209 -0
  17. package/claude-plugin/hooks/quality/run.sh +132 -0
  18. package/claude-plugin/hooks/shape/run.sh +45 -0
  19. package/claude-plugin/mcp-router-launch.js +29 -0
  20. package/claude-plugin/skills/README.md +48 -0
  21. package/claude-plugin/skills/accessibility/SKILL.md +31 -0
  22. package/claude-plugin/skills/analytics/SKILL.md +30 -0
  23. package/claude-plugin/skills/backend/SKILL.md +25 -0
  24. package/claude-plugin/skills/brandcheck/SKILL.md +29 -0
  25. package/claude-plugin/skills/browser/SKILL.md +64 -0
  26. package/claude-plugin/skills/client/SKILL.md +24 -0
  27. package/claude-plugin/skills/desktop/SKILL.md +25 -0
  28. package/claude-plugin/skills/extension/SKILL.md +25 -0
  29. package/claude-plugin/skills/main/SKILL.md +54 -0
  30. package/claude-plugin/skills/manager/SKILL.md +25 -0
  31. package/claude-plugin/skills/seo/SKILL.md +36 -0
  32. package/claude-plugin/skills/theme/SKILL.md +33 -0
  33. package/claude-plugin/skills/web/SKILL.md +29 -0
  34. package/dist/cli-run.js +34 -0
  35. package/dist/cli.js +29 -0
  36. package/dist/commands/build.js +17 -0
  37. package/dist/commands/clean.js +16 -0
  38. package/dist/commands/company.js +46 -0
  39. package/dist/commands/deploy.js +143 -0
  40. package/dist/commands/dev.js +437 -0
  41. package/dist/commands/devlog.js +25 -0
  42. package/dist/commands/help.js +48 -0
  43. package/dist/commands/manage.js +53 -0
  44. package/dist/commands/migrate.js +76 -0
  45. package/dist/commands/onboard.js +31 -0
  46. package/dist/commands/pipeline.js +319 -0
  47. package/dist/commands/test.js +380 -0
  48. package/dist/commands/update.js +128 -0
  49. package/dist/commands/version.js +8 -0
  50. package/dist/company-init.js +167 -0
  51. package/dist/company.js +283 -0
  52. package/dist/config.js +1359 -0
  53. package/dist/devlog/index.js +226 -0
  54. package/dist/devlog/lib/collect-commits.js +136 -0
  55. package/dist/devlog/lib/generate-post.js +201 -0
  56. package/dist/devlog/lib/ghostii.js +122 -0
  57. package/dist/devlog/lib/project-map.js +98 -0
  58. package/dist/devlog/lib/publish-website.js +88 -0
  59. package/dist/index.js +45 -0
  60. package/dist/lib/agents-md.js +201 -0
  61. package/dist/lib/analytics-secret.js +22 -0
  62. package/dist/lib/argv.js +21 -0
  63. package/dist/lib/auth-admin.js +120 -0
  64. package/dist/lib/automation-client.js +210 -0
  65. package/dist/lib/backend-marketing.js +49 -0
  66. package/dist/lib/brand.js +240 -0
  67. package/dist/lib/bundle-id.js +53 -0
  68. package/dist/lib/claude-settings.js +125 -0
  69. package/dist/lib/company-scaffold.js +213 -0
  70. package/dist/lib/company.js +227 -0
  71. package/dist/lib/config-flow.js +350 -0
  72. package/dist/lib/config-write.js +41 -0
  73. package/dist/lib/custom-target.js +75 -0
  74. package/dist/lib/domain-utils.js +19 -0
  75. package/dist/lib/duration.js +39 -0
  76. package/dist/lib/env-order.js +236 -0
  77. package/dist/lib/env-secret.js +78 -0
  78. package/dist/lib/firestore-rest.js +326 -0
  79. package/dist/lib/framework-bin.js +156 -0
  80. package/dist/lib/gitignore.js +63 -0
  81. package/dist/lib/google-auth.js +495 -0
  82. package/dist/lib/google-token.js +66 -0
  83. package/dist/lib/jwt.js +31 -0
  84. package/dist/lib/legacy-oauth.js +59 -0
  85. package/dist/lib/node-version.js +127 -0
  86. package/dist/lib/owner-plan.js +78 -0
  87. package/dist/lib/package-scripts.js +152 -0
  88. package/dist/lib/preflight.js +470 -0
  89. package/dist/lib/product-create.js +144 -0
  90. package/dist/lib/run-command.js +74 -0
  91. package/dist/lib/run-gates.js +70 -0
  92. package/dist/lib/run-output.js +42 -0
  93. package/dist/lib/run-summary.js +384 -0
  94. package/dist/lib/scaffold.js +427 -0
  95. package/dist/lib/service-input.js +207 -0
  96. package/dist/lib/service-runner.js +466 -0
  97. package/dist/lib/stale.js +24 -0
  98. package/dist/lib/target-selection.js +155 -0
  99. package/dist/lib/verb-fanout.js +128 -0
  100. package/dist/lib/verify-live.js +188 -0
  101. package/dist/manage.js +311 -0
  102. package/dist/omega-bin.js +7 -0
  103. package/dist/onboard.js +532 -0
  104. package/dist/services/account/ensure/users.js +259 -0
  105. package/dist/services/account/index.js +97 -0
  106. package/dist/services/account/lib/backend-client.js +107 -0
  107. package/dist/services/account/lib/password.js +36 -0
  108. package/dist/services/account/lib/resolve-password.js +100 -0
  109. package/dist/services/advertising/ensure/sites.js +101 -0
  110. package/dist/services/advertising/index.js +101 -0
  111. package/dist/services/advertising/lib/adsense-api.js +41 -0
  112. package/dist/services/ai/ensure/keys.js +21 -0
  113. package/dist/services/ai/index.js +36 -0
  114. package/dist/services/analytics/ensure/google-firebase-link.js +230 -0
  115. package/dist/services/analytics/ensure/google-streams.js +256 -0
  116. package/dist/services/analytics/ensure/meta-pixel.js +22 -0
  117. package/dist/services/analytics/ensure/tiktok-pixel.js +22 -0
  118. package/dist/services/analytics/index.js +134 -0
  119. package/dist/services/analytics/lib/analytics-api.js +156 -0
  120. package/dist/services/analytics/lib/meta-api.js +83 -0
  121. package/dist/services/analytics/lib/pixel-account.js +78 -0
  122. package/dist/services/analytics/lib/pixel-provision.js +152 -0
  123. package/dist/services/analytics/lib/pixel-specs.js +57 -0
  124. package/dist/services/analytics/lib/pixel-token.js +88 -0
  125. package/dist/services/analytics/lib/property-flow.js +77 -0
  126. package/dist/services/analytics/lib/tiktok-api.js +157 -0
  127. package/dist/services/analytics/lib/tiktok-auth.js +153 -0
  128. package/dist/services/assets/ensure/logo-gen.js +68 -0
  129. package/dist/services/assets/index.js +109 -0
  130. package/dist/services/assets/lib/assets-config.js +168 -0
  131. package/dist/services/assets/lib/brandmark-api.js +160 -0
  132. package/dist/services/assets/lib/derived.js +113 -0
  133. package/dist/services/assets/lib/font-loader.js +67 -0
  134. package/dist/services/assets/lib/reconcile.js +122 -0
  135. package/dist/services/assets/lib/reset.js +118 -0
  136. package/dist/services/assets/lib/svg-logo-generator.js +154 -0
  137. package/dist/services/assets/lib/svg-to-black.js +32 -0
  138. package/dist/services/assets/write/favicons.js +82 -0
  139. package/dist/services/assets/write/icons.js +64 -0
  140. package/dist/services/assets/write/process.js +96 -0
  141. package/dist/services/assets/write/reconcile.js +30 -0
  142. package/dist/services/assets/write/social-icons.js +108 -0
  143. package/dist/services/assets/write/templates.js +345 -0
  144. package/dist/services/bookmark/ensure/sync.js +255 -0
  145. package/dist/services/bookmark/index.js +17 -0
  146. package/dist/services/campaigns/ensure/contact-person.js +40 -0
  147. package/dist/services/campaigns/ensure/custom-fields.js +84 -0
  148. package/dist/services/campaigns/ensure/domain-auth.js +92 -0
  149. package/dist/services/campaigns/ensure/event-webhook.js +89 -0
  150. package/dist/services/campaigns/ensure/link-branding.js +187 -0
  151. package/dist/services/campaigns/ensure/list.js +50 -0
  152. package/dist/services/campaigns/ensure/segments.js +109 -0
  153. package/dist/services/campaigns/ensure/sender-identity.js +153 -0
  154. package/dist/services/campaigns/ensure/unsubscribe-groups.js +116 -0
  155. package/dist/services/campaigns/index.js +79 -0
  156. package/dist/services/campaigns/lib/dns-sync.js +81 -0
  157. package/dist/services/campaigns/lib/segment-query.js +138 -0
  158. package/dist/services/campaigns/lib/sendgrid-api.js +287 -0
  159. package/dist/services/captcha/ensure/site-key.js +83 -0
  160. package/dist/services/captcha/index.js +78 -0
  161. package/dist/services/captcha/lib/console-url.js +45 -0
  162. package/dist/services/captcha/lib/recaptcha-api.js +36 -0
  163. package/dist/services/certificates/ensure/api-key.js +22 -0
  164. package/dist/services/certificates/ensure/bundle-ids.js +115 -0
  165. package/dist/services/certificates/ensure/certificates.js +187 -0
  166. package/dist/services/certificates/ensure/profiles.js +143 -0
  167. package/dist/services/certificates/index.js +225 -0
  168. package/dist/services/certificates/lib/apple-api.js +183 -0
  169. package/dist/services/certificates/lib/certificate-manager.js +252 -0
  170. package/dist/services/certificates/lib/identifier-manager.js +107 -0
  171. package/dist/services/certificates/lib/keychain.js +135 -0
  172. package/dist/services/certificates/lib/manual-walkthrough.js +165 -0
  173. package/dist/services/certificates/lib/profile-manager.js +115 -0
  174. package/dist/services/chat/data/baseline-knowledge.md +96 -0
  175. package/dist/services/chat/ensure/chat.js +106 -0
  176. package/dist/services/chat/ensure/user.js +45 -0
  177. package/dist/services/chat/index.js +139 -0
  178. package/dist/services/chat/lib/baseline-knowledge.js +137 -0
  179. package/dist/services/cloud/ensure/authentication.js +267 -0
  180. package/dist/services/cloud/ensure/billing.js +120 -0
  181. package/dist/services/cloud/ensure/cloud-messaging.js +105 -0
  182. package/dist/services/cloud/ensure/database.js +60 -0
  183. package/dist/services/cloud/ensure/firestore.js +79 -0
  184. package/dist/services/cloud/ensure/functions.js +49 -0
  185. package/dist/services/cloud/ensure/hosting.js +398 -0
  186. package/dist/services/cloud/ensure/oauth-consent.js +248 -0
  187. package/dist/services/cloud/ensure/project-settings.js +72 -0
  188. package/dist/services/cloud/ensure/sdk-config.js +95 -0
  189. package/dist/services/cloud/ensure/service-account.js +152 -0
  190. package/dist/services/cloud/ensure/services.js +103 -0
  191. package/dist/services/cloud/ensure/storage.js +47 -0
  192. package/dist/services/cloud/index.js +130 -0
  193. package/dist/services/cloud/lib/access-heal.js +130 -0
  194. package/dist/services/cloud/lib/firebase-api.js +662 -0
  195. package/dist/services/cloud/lib/project-flow.js +102 -0
  196. package/dist/services/directory/ensure/entry.js +48 -0
  197. package/dist/services/directory/index.js +64 -0
  198. package/dist/services/directory/lib/blocks.js +104 -0
  199. package/dist/services/disperse/index.js +45 -0
  200. package/dist/services/disperse/write/certs.js +117 -0
  201. package/dist/services/domain/ensure/nameservers.js +179 -0
  202. package/dist/services/domain/index.js +69 -0
  203. package/dist/services/domain/lib/namecheap-api.js +167 -0
  204. package/dist/services/domain/lib/registrars.js +46 -0
  205. package/dist/services/domain/lib/whitelist-walkthrough.js +76 -0
  206. package/dist/services/edge/ensure/cache-rules.js +105 -0
  207. package/dist/services/edge/ensure/dns-records.js +304 -0
  208. package/dist/services/edge/ensure/email-routing.js +292 -0
  209. package/dist/services/edge/ensure/rules-configuration.js +98 -0
  210. package/dist/services/edge/ensure/rules-managed-transforms.js +101 -0
  211. package/dist/services/edge/ensure/rules-redirect.js +107 -0
  212. package/dist/services/edge/ensure/rules-response-headers.js +90 -0
  213. package/dist/services/edge/ensure/rules-security.js +142 -0
  214. package/dist/services/edge/ensure/speed-scheduled-tests.js +98 -0
  215. package/dist/services/edge/ensure/workers.js +219 -0
  216. package/dist/services/edge/ensure/zone-settings.js +108 -0
  217. package/dist/services/edge/ensure/zone.js +183 -0
  218. package/dist/services/edge/index.js +73 -0
  219. package/dist/services/edge/lib/cloudflare-api.js +84 -0
  220. package/dist/services/edge/lib/dns-records-helpers.js +518 -0
  221. package/dist/services/edge/lib/read-cache.js +16 -0
  222. package/dist/services/edge/lib/ruleset-helper.js +108 -0
  223. package/dist/services/edge/workers/omega-api-proxy.js +43 -0
  224. package/dist/services/email/data/baseline-filter.md +4 -0
  225. package/dist/services/email/data/baseline-knowledge.md +86 -0
  226. package/dist/services/email/ensure/agent.js +107 -0
  227. package/dist/services/email/ensure/user.js +45 -0
  228. package/dist/services/email/index.js +138 -0
  229. package/dist/services/email/lib/baseline.js +82 -0
  230. package/dist/services/email/lib/knowledge-file.js +53 -0
  231. package/dist/services/forms/ensure/form.js +46 -0
  232. package/dist/services/forms/ensure/user.js +45 -0
  233. package/dist/services/forms/index.js +138 -0
  234. package/dist/services/migrations/ensure/notifications.js +189 -0
  235. package/dist/services/migrations/ensure/orders.js +34 -0
  236. package/dist/services/migrations/ensure/payment-provider.js +132 -0
  237. package/dist/services/migrations/ensure/payments-intents.js +34 -0
  238. package/dist/services/migrations/ensure/state-retirement.js +266 -0
  239. package/dist/services/migrations/ensure/targets-rename.js +124 -0
  240. package/dist/services/migrations/ensure/users.js +1176 -0
  241. package/dist/services/migrations/index.js +101 -0
  242. package/dist/services/migrations/lib/attribution-touch.js +64 -0
  243. package/dist/services/migrations/lib/ensure-metadata.js +144 -0
  244. package/dist/services/migrations/lib/migration-runner.js +473 -0
  245. package/dist/services/migrations/lib/sanitize-strings.js +112 -0
  246. package/dist/services/migrations/lib/schema-validator.js +154 -0
  247. package/dist/services/monitoring/ensure/dsn.js +54 -0
  248. package/dist/services/monitoring/ensure/projects.js +135 -0
  249. package/dist/services/monitoring/index.js +54 -0
  250. package/dist/services/monitoring/lib/sentry-api.js +94 -0
  251. package/dist/services/newsletter/ensure/custom-fields.js +90 -0
  252. package/dist/services/newsletter/ensure/publication.js +94 -0
  253. package/dist/services/newsletter/ensure/segments.js +140 -0
  254. package/dist/services/newsletter/ensure/webhook.js +108 -0
  255. package/dist/services/newsletter/index.js +64 -0
  256. package/dist/services/newsletter/lib/beehiiv-api.js +124 -0
  257. package/dist/services/newsletter/lib/segment-automation.js +577 -0
  258. package/dist/services/payment/ensure/chargebee-account.js +30 -0
  259. package/dist/services/payment/ensure/chargebee-products.js +359 -0
  260. package/dist/services/payment/ensure/chargebee-webhook.js +113 -0
  261. package/dist/services/payment/ensure/paypal-account.js +36 -0
  262. package/dist/services/payment/ensure/paypal-products.js +374 -0
  263. package/dist/services/payment/ensure/paypal-webhook.js +128 -0
  264. package/dist/services/payment/ensure/stripe-account.js +93 -0
  265. package/dist/services/payment/ensure/stripe-disputes.js +56 -0
  266. package/dist/services/payment/ensure/stripe-products.js +245 -0
  267. package/dist/services/payment/ensure/stripe-radar.js +74 -0
  268. package/dist/services/payment/ensure/stripe-webhook.js +130 -0
  269. package/dist/services/payment/index.js +163 -0
  270. package/dist/services/payment/lib/chargebee-api.js +338 -0
  271. package/dist/services/payment/lib/payment-utils.js +116 -0
  272. package/dist/services/payment/lib/paypal-api.js +354 -0
  273. package/dist/services/payment/lib/provider-setup.js +134 -0
  274. package/dist/services/payment/lib/stripe-api.js +152 -0
  275. package/dist/services/repo/ensure/org.js +70 -0
  276. package/dist/services/repo/ensure/pages.js +72 -0
  277. package/dist/services/repo/ensure/repo.js +90 -0
  278. package/dist/services/repo/index.js +52 -0
  279. package/dist/services/repo/lib/github-api.js +196 -0
  280. package/dist/services/search/ensure/ga-link.js +57 -0
  281. package/dist/services/search/ensure/property.js +137 -0
  282. package/dist/services/search/ensure/sitemaps.js +70 -0
  283. package/dist/services/search/index.js +61 -0
  284. package/dist/services/search/lib/search-console-api.js +92 -0
  285. package/dist/services/seo/ensure/github-repos.js +271 -0
  286. package/dist/services/seo/index.js +55 -0
  287. package/dist/services/seo/lib/gh-api.js +161 -0
  288. package/dist/services/seo/templates/developer-tool/.github/workflows/maintenance.yml +25 -0
  289. package/dist/services/seo/templates/developer-tool/.nvmrc +1 -0
  290. package/dist/services/seo/templates/developer-tool/_README.md.js +121 -0
  291. package/dist/services/seo/templates/developer-tool/_package.json.js +33 -0
  292. package/dist/services/seo/templates/developer-tool/src/index.js +120 -0
  293. package/dist/services/seo/templates/index.js +57 -0
  294. package/dist/services/server/ensure/brands.js +63 -0
  295. package/dist/services/server/index.js +42 -0
  296. package/dist/services/testing/ensure/target-checks.js +97 -0
  297. package/dist/services/testing/index.js +11 -0
  298. package/dist/services/testing/lib/checks.js +467 -0
  299. package/dist/services/update/index.js +20 -0
  300. package/dist/services/update/lib/cache.js +59 -0
  301. package/dist/services/update/lib/fingerprint.js +157 -0
  302. package/dist/services/update/write/targets.js +206 -0
  303. package/dist/services/workspace/ensure/agents.js +42 -0
  304. package/dist/services/workspace/ensure/claude-settings.js +34 -0
  305. package/dist/services/workspace/ensure/config.js +54 -0
  306. package/dist/services/workspace/ensure/defaults.js +60 -0
  307. package/dist/services/workspace/ensure/env-keys.js +55 -0
  308. package/dist/services/workspace/ensure/env-order.js +66 -0
  309. package/dist/services/workspace/ensure/env-rules.js +97 -0
  310. package/dist/services/workspace/ensure/gitignore.js +18 -0
  311. package/dist/services/workspace/ensure/scripts.js +126 -0
  312. package/dist/services/workspace/ensure/structure.js +97 -0
  313. package/dist/services/workspace/ensure/translation-sdk.js +148 -0
  314. package/dist/services/workspace/ensure/workflows.js +54 -0
  315. package/dist/services/workspace/index.js +8 -0
  316. package/dist/vendor/account/engine.js +182 -0
  317. package/dist/vendor/account/features.js +220 -0
  318. package/dist/vendor/account/index.js +53 -0
  319. package/dist/vendor/account/schema.js +272 -0
  320. package/dist/vendor/account/subscription.js +38 -0
  321. package/dist/vendor/config/company.js +31 -0
  322. package/dist/vendor/config/defaults.js +173 -0
  323. package/dist/vendor/config/demo.js +18 -0
  324. package/dist/vendor/config/desktop-artifacts.js +110 -0
  325. package/dist/vendor/config/edit.js +769 -0
  326. package/dist/vendor/config/env-delivery.js +145 -0
  327. package/dist/vendor/config/env-rules.js +93 -0
  328. package/dist/vendor/config/env-schema.js +1078 -0
  329. package/dist/vendor/config/env.js +445 -0
  330. package/dist/vendor/config/hooks.js +97 -0
  331. package/dist/vendor/config/index.js +237 -0
  332. package/dist/vendor/config/instances.js +208 -0
  333. package/dist/vendor/config/load.js +490 -0
  334. package/dist/vendor/config/merge.js +68 -0
  335. package/dist/vendor/config/order.js +139 -0
  336. package/dist/vendor/config/ports.js +374 -0
  337. package/dist/vendor/config/providers.js +32 -0
  338. package/dist/vendor/config/repo.js +142 -0
  339. package/dist/vendor/config/retired-keys.js +430 -0
  340. package/dist/vendor/config/schema.js +1610 -0
  341. package/dist/vendor/config/secrets.js +50 -0
  342. package/dist/vendor/config/seed.js +34 -0
  343. package/dist/vendor/config/site-global.js +205 -0
  344. package/dist/vendor/config/validate.js +554 -0
  345. package/dist/vendor/config/winback.js +61 -0
  346. package/dist/vendor/devkit/attach-log-file.js +262 -0
  347. package/dist/vendor/devkit/certs.js +199 -0
  348. package/dist/vendor/devkit/ci-workflows.js +520 -0
  349. package/dist/vendor/devkit/cli-router.js +156 -0
  350. package/dist/vendor/devkit/command-path.js +46 -0
  351. package/dist/vendor/devkit/deploy-record.js +180 -0
  352. package/dist/vendor/devkit/flows.js +317 -0
  353. package/dist/vendor/devkit/local-https.js +360 -0
  354. package/dist/vendor/devkit/local.js +1905 -0
  355. package/dist/vendor/devkit/logger.js +128 -0
  356. package/dist/vendor/devkit/omega-bin.js +345 -0
  357. package/dist/vendor/devkit/prompt.js +187 -0
  358. package/dist/vendor/devkit/safe-install.js +18 -0
  359. package/dist/vendor/devkit/stop-signals.js +28 -0
  360. package/dist/vendor/devkit/test/scope.js +162 -0
  361. package/dist/vendor/devkit/translate/cache.js +84 -0
  362. package/dist/vendor/devkit/translate/engine.js +243 -0
  363. package/dist/vendor/devkit/translate/index.js +49 -0
  364. package/dist/vendor/devkit/translate/languages.js +134 -0
  365. package/dist/vendor/devkit/translate/providers.js +188 -0
  366. package/dist/vendor/devkit/update.js +569 -0
  367. package/docs/AGENTS.md +200 -0
  368. package/docs/account.md +45 -0
  369. package/docs/advertising.md +46 -0
  370. package/docs/ai.md +34 -0
  371. package/docs/analytics.md +69 -0
  372. package/docs/assets.md +57 -0
  373. package/docs/bookmark.md +26 -0
  374. package/docs/brand.md +151 -0
  375. package/docs/campaigns.md +96 -0
  376. package/docs/captcha.md +46 -0
  377. package/docs/certificates.md +68 -0
  378. package/docs/chat.md +47 -0
  379. package/docs/cloud.md +110 -0
  380. package/docs/company.md +64 -0
  381. package/docs/directory.md +140 -0
  382. package/docs/disperse.md +46 -0
  383. package/docs/domain.md +56 -0
  384. package/docs/edge.md +234 -0
  385. package/docs/email.md +42 -0
  386. package/docs/forms.md +48 -0
  387. package/docs/index.md +109 -0
  388. package/docs/migration.md +203 -0
  389. package/docs/migrations.md +56 -0
  390. package/docs/monitoring.md +41 -0
  391. package/docs/newsletter.md +48 -0
  392. package/docs/payment.md +83 -0
  393. package/docs/repo.md +48 -0
  394. package/docs/search.md +44 -0
  395. package/docs/seo.md +34 -0
  396. package/docs/server.md +37 -0
  397. package/docs/shared/agent-docs.md +89 -0
  398. package/docs/shared/analytics.md +612 -0
  399. package/docs/shared/brands.md +51 -0
  400. package/docs/shared/breaking-changes.md +497 -0
  401. package/docs/shared/config.md +1387 -0
  402. package/docs/shared/deploys.md +215 -0
  403. package/docs/shared/icons.md +201 -0
  404. package/docs/shared/local-dev.md +147 -0
  405. package/docs/shared/logging.md +202 -0
  406. package/docs/shared/monitoring.md +153 -0
  407. package/docs/shared/publishing.md +183 -0
  408. package/docs/shared/rulings.md +34 -0
  409. package/docs/shared/testing.md +147 -0
  410. package/docs/shared/theming.md +604 -0
  411. package/docs/shared/translation.md +291 -0
  412. package/docs/shared/updates.md +61 -0
  413. package/docs/testing.md +34 -0
  414. package/docs/update.md +36 -0
  415. package/docs/workspace.md +64 -0
  416. 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;