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