@omega.js/desktop 0.52.0 → 0.54.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 (345) hide show
  1. package/README.md +53 -48
  2. package/dist/assets/css/core/_initialize.scss +1 -1
  3. package/dist/assets/js/core/app-shell.js +14 -15
  4. package/dist/assets/themes/_template/_theme.js +6 -6
  5. package/dist/assets/themes/base/_includes/global/sections/account.html +4 -4
  6. package/dist/assets/themes/base/_includes/global/sections/app-sidebar.html +5 -5
  7. package/dist/assets/themes/base/_layouts/frontend/pages/account/index.html +3 -3
  8. package/dist/assets/themes/base/_layouts/frontend/pages/alternatives/index.html +1 -1
  9. package/dist/assets/themes/base/_layouts/frontend/pages/blog/tags/tag.html +1 -1
  10. package/dist/assets/themes/base/_layouts/frontend/pages/payment/confirmation.html +1 -1
  11. package/dist/assets/themes/base/_sections/marketing/newsletter-cta/section.js +2 -3
  12. package/dist/assets/themes/base/_sections/verts/unit/section.html +1 -1
  13. package/dist/assets/themes/base/_sections/verts/unit/section.js +3 -4
  14. package/dist/assets/themes/base/_theme.js +4 -3
  15. package/dist/assets/themes/bootstrap/_theme.js +2 -2
  16. package/dist/assets/themes/bootstrap/overrides/_links.scss +1 -1
  17. package/dist/assets/themes/classy/_theme.js +8 -6
  18. package/dist/assets/themes/classy/css/marketing/_sections.scss +1 -2
  19. package/dist/assets/themes/classy/js/hero-demo-form.js +3 -2
  20. package/dist/assets/themes/neobrutalism/_theme.js +5 -5
  21. package/dist/assets/themes/neobrutalism/js/pages/test/libraries/layers/index.js +1 -1
  22. package/dist/assets/themes/newsflash/_theme.js +6 -6
  23. package/dist/assets/themes/newsflash/js/pages/test/libraries/layers/index.js +1 -1
  24. package/dist/build.js +87 -111
  25. package/dist/cli-run.js +4 -1
  26. package/dist/cli.js +3 -3
  27. package/dist/commands/build.js +2 -2
  28. package/dist/commands/cdp/capture.js +2 -2
  29. package/dist/commands/cdp/client.js +1 -1
  30. package/dist/commands/cdp/quit.js +2 -2
  31. package/dist/commands/cdp/relaunch.js +2 -2
  32. package/dist/commands/cdp/theme.js +1 -1
  33. package/dist/commands/cdp.js +1 -1
  34. package/dist/commands/clean.js +4 -5
  35. package/dist/commands/deploy.js +4 -4
  36. package/dist/commands/dev.js +25 -0
  37. package/dist/commands/finalize-release.js +4 -4
  38. package/dist/commands/launch.js +2 -2
  39. package/dist/commands/lib/deploy-precheck.js +3 -3
  40. package/dist/commands/lib/ensure-target.js +18 -23
  41. package/dist/commands/lib/migrate.js +17 -0
  42. package/dist/commands/logs.js +1 -1
  43. package/dist/commands/package.js +2 -2
  44. package/dist/commands/publish.js +2 -2
  45. package/dist/commands/release.js +4 -4
  46. package/dist/commands/runner.js +11 -11
  47. package/dist/commands/sign-windows.js +5 -5
  48. package/dist/commands/test.js +8 -8
  49. package/dist/commands/update.js +7 -6
  50. package/dist/commands/validate-certs.js +4 -4
  51. package/dist/commands/version.js +3 -3
  52. package/dist/defaults/.github/workflows/build.yml +18 -18
  53. package/dist/defaults/_.gitignore +0 -2
  54. package/dist/defaults/_mas/README.md +3 -3
  55. package/dist/defaults/config/certs/README.md +1 -1
  56. package/dist/defaults/config/omega.json5 +43 -43
  57. package/dist/defaults/docs/README.md +3 -3
  58. package/dist/defaults/gulpfile.js +1 -1
  59. package/dist/defaults/hooks/build/post.js +2 -2
  60. package/dist/defaults/hooks/build/pre.js +2 -2
  61. package/dist/defaults/hooks/deploy/pre.js +1 -1
  62. package/dist/defaults/hooks/notarize/post.js +2 -2
  63. package/dist/defaults/hooks/release/post.js +2 -2
  64. package/dist/defaults/hooks/release/pre.js +2 -2
  65. package/dist/defaults/src/assets/js/components/about/index.js +3 -5
  66. package/dist/defaults/src/assets/js/components/main/index.js +3 -5
  67. package/dist/defaults/src/assets/js/components/settings/index.js +3 -5
  68. package/dist/defaults/src/assets/scss/pages/about.scss +1 -1
  69. package/dist/defaults/src/assets/scss/pages/main.scss +1 -1
  70. package/dist/defaults/src/assets/scss/pages/settings.scss +1 -1
  71. package/dist/defaults/src/integrations/context-menu/index.js +13 -13
  72. package/dist/defaults/src/integrations/menu/index.js +8 -8
  73. package/dist/defaults/src/integrations/tray/index.js +12 -12
  74. package/dist/defaults/src/main.js +5 -7
  75. package/dist/defaults/src/preload.js +4 -6
  76. package/dist/defaults/test/README.md +5 -5
  77. package/dist/defaults/test/_init.js +1 -1
  78. package/dist/gulp/main.js +9 -10
  79. package/dist/gulp/tasks/audit.js +11 -14
  80. package/dist/gulp/tasks/build-config.js +8 -8
  81. package/dist/gulp/tasks/bundle.js +16 -16
  82. package/dist/gulp/tasks/defaults.js +3 -3
  83. package/dist/gulp/tasks/distribute.js +2 -2
  84. package/dist/gulp/tasks/html.js +9 -9
  85. package/dist/gulp/tasks/package-quick.js +3 -3
  86. package/dist/gulp/tasks/package.js +3 -3
  87. package/dist/gulp/tasks/release.js +3 -3
  88. package/dist/gulp/tasks/sass.js +6 -6
  89. package/dist/gulp/tasks/serve.js +4 -4
  90. package/dist/hooks/notarize-artifacts.js +1 -1
  91. package/dist/hooks/notarize.js +1 -1
  92. package/dist/index.js +5 -8
  93. package/dist/lib/_environment-mixin.js +50 -0
  94. package/dist/lib/_lifecycle-mixin.js +45 -0
  95. package/dist/lib/analytics.js +33 -35
  96. package/dist/lib/app-state.js +14 -14
  97. package/dist/lib/auth-flow.js +18 -18
  98. package/dist/lib/auth-persistence.js +12 -12
  99. package/dist/lib/auth.js +421 -0
  100. package/dist/lib/auto-updater.js +49 -49
  101. package/dist/lib/context-menu.js +13 -13
  102. package/dist/lib/context.js +19 -19
  103. package/dist/lib/deep-link.js +34 -34
  104. package/dist/lib/fontawesome.js +5 -5
  105. package/dist/lib/ipc.js +4 -4
  106. package/dist/lib/menu.js +25 -25
  107. package/dist/lib/protocol.js +5 -5
  108. package/dist/lib/remote-config.js +22 -22
  109. package/dist/lib/remote-scripts.js +21 -21
  110. package/dist/lib/restart-manager/index.js +29 -29
  111. package/dist/lib/restart-manager/install.js +1 -1
  112. package/dist/lib/restart-manager/protocol.js +1 -1
  113. package/dist/lib/sign-helpers/exec-with-limit.js +1 -1
  114. package/dist/lib/sign-helpers/sign-events.js +1 -1
  115. package/dist/lib/startup.js +18 -13
  116. package/dist/lib/storage.js +10 -10
  117. package/dist/lib/templating.js +16 -16
  118. package/dist/lib/theme.js +10 -10
  119. package/dist/lib/tray.js +27 -27
  120. package/dist/lib/usage.js +11 -11
  121. package/dist/lib/window-manager.js +26 -26
  122. package/dist/main.js +399 -483
  123. package/dist/preload.js +236 -176
  124. package/dist/renderer.js +417 -391
  125. package/dist/test/fixtures/consumer-app/config/omega.json5 +1 -1
  126. package/dist/test/fixtures/consumer-app/src/assets/js/components/main/index.js +8 -11
  127. package/dist/test/fixtures/consumer-app/src/main.js +5 -7
  128. package/dist/test/fixtures/consumer-app/src/preload.js +2 -2
  129. package/dist/test/harness/boot-entry.js +22 -20
  130. package/dist/test/harness/main-entry.js +31 -30
  131. package/dist/test/harness/renderer-entry.js +5 -5
  132. package/dist/test/harness/renderer-preload.js +137 -141
  133. package/dist/test/index.js +10 -10
  134. package/dist/test/runner.js +2 -2
  135. package/dist/test/runners/boot.js +7 -6
  136. package/dist/test/runners/electron.js +3 -2
  137. package/dist/test/runners/render-event.js +2 -2
  138. package/dist/test/suites/boot/consumer-app-boots.test.js +57 -26
  139. package/dist/test/suites/boot/restart-manager.test.js +2 -2
  140. package/dist/test/suites/boot/storage-bundled.test.js +5 -5
  141. package/dist/test/suites/boot/theme.test.js +13 -13
  142. package/dist/test/suites/build/audit.test.js +21 -8
  143. package/dist/test/suites/build/auth-persistence-resolve.test.js +6 -6
  144. package/dist/test/suites/build/boot-fixture.test.js +2 -2
  145. package/dist/test/suites/build/boot-runner-timeout.test.js +6 -5
  146. package/dist/test/suites/build/brand-scss.test.js +1 -1
  147. package/dist/test/suites/build/build-json-bake.test.js +1 -1
  148. package/dist/test/suites/build/build-workflow-jobs.test.js +2 -2
  149. package/dist/test/suites/build/cli.test.js +30 -2
  150. package/dist/test/suites/build/config-schema.test.js +5 -5
  151. package/dist/test/suites/build/defaults-em-dash.test.js +22 -0
  152. package/dist/test/suites/build/defaults-scaffold.test.js +21 -7
  153. package/dist/test/suites/build/deploy-direct.test.js +7 -5
  154. package/dist/test/suites/build/deploy-dispatch.test.js +2 -1
  155. package/dist/test/suites/build/deploy-hook.test.js +7 -5
  156. package/dist/test/suites/build/dev-verb.test.js +67 -0
  157. package/dist/test/suites/build/ensure-target.test.js +13 -5
  158. package/dist/test/suites/build/env-delivery.test.js +2 -2
  159. package/dist/test/suites/build/esm-only-dependency.test.js +2 -2
  160. package/dist/test/suites/build/exports.test.js +9 -8
  161. package/dist/test/suites/build/get-config.test.js +4 -4
  162. package/dist/test/suites/build/manifest-deps.test.js +1 -1
  163. package/dist/test/suites/build/merge-line-files.test.js +7 -7
  164. package/dist/test/suites/build/migrate.test.js +29 -0
  165. package/dist/test/suites/build/omega-shell.test.js +34 -2
  166. package/dist/test/suites/build/omega.test.js +350 -0
  167. package/dist/test/suites/build/project-scripts-deps.test.js +6 -10
  168. package/dist/test/suites/build/renderer-auth-bridge.test.js +211 -78
  169. package/dist/test/suites/build/runner-env-write.test.js +73 -0
  170. package/dist/test/suites/build/runner.test.js +11 -10
  171. package/dist/test/suites/build/sentry.test.js +2 -2
  172. package/dist/test/suites/build/setup-scripts.test.js +27 -0
  173. package/dist/test/suites/build/sign-windows-e2e.test.js +2 -2
  174. package/dist/test/suites/build/templating.test.js +3 -3
  175. package/dist/test/suites/build/test-stealth.test.js +7 -9
  176. package/dist/test/suites/build/url-helpers.test.js +55 -56
  177. package/dist/test/suites/build/validate-config.test.js +15 -4
  178. package/dist/test/suites/build/verb-logs.test.js +20 -0
  179. package/dist/test/suites/build/wave5-pins.test.js +2 -2
  180. package/dist/test/suites/main/analytics.test.js +59 -59
  181. package/dist/test/suites/main/app-state.test.js +66 -66
  182. package/dist/test/suites/main/auth-flow.test.js +41 -41
  183. package/dist/test/suites/main/auth-persistence.test.js +54 -43
  184. package/dist/test/suites/main/{client-bridge.integration.test.js → auth.integration.test.js} +10 -9
  185. package/dist/test/suites/main/auth.test.js +336 -0
  186. package/dist/test/suites/main/auto-updater.test.js +134 -134
  187. package/dist/test/suites/main/boot-sequence.test.js +37 -49
  188. package/dist/test/suites/main/context-menu.test.js +51 -50
  189. package/dist/test/suites/main/context.test.js +25 -25
  190. package/dist/test/suites/main/deep-link.test.js +74 -74
  191. package/dist/test/suites/main/fontawesome.test.js +27 -27
  192. package/dist/test/suites/main/ipc.test.js +35 -35
  193. package/dist/test/suites/main/menu.test.js +101 -100
  194. package/dist/test/suites/main/protocol.test.js +19 -19
  195. package/dist/test/suites/main/remote-config.test.js +63 -63
  196. package/dist/test/suites/main/remote-scripts.test.js +103 -103
  197. package/dist/test/suites/main/request.test.js +71 -0
  198. package/dist/test/suites/main/restart-manager.test.js +33 -33
  199. package/dist/test/suites/main/startup-paths-and-ua.test.js +4 -4
  200. package/dist/test/suites/main/startup.test.js +26 -26
  201. package/dist/test/suites/main/stealth-window.test.js +1 -1
  202. package/dist/test/suites/main/storage.test.js +25 -25
  203. package/dist/test/suites/main/theme.test.js +34 -34
  204. package/dist/test/suites/main/tray.test.js +79 -79
  205. package/dist/test/suites/main/url-helpers.test.js +133 -133
  206. package/dist/test/suites/main/usage.test.js +25 -25
  207. package/dist/test/suites/main/window-bounds.test.js +27 -27
  208. package/dist/test/suites/main/window-manager.test.js +44 -44
  209. package/dist/test/suites/renderer/analytics-bridge.test.js +5 -5
  210. package/dist/test/suites/renderer/cross-context-helpers.test.js +37 -31
  211. package/dist/test/suites/renderer/round-trip.test.js +3 -3
  212. package/dist/test/suites/renderer/tooltips.test.js +15 -15
  213. package/dist/test/suites/renderer/{window-em-surface.test.js → window-desktop-surface.test.js} +28 -8
  214. package/dist/test/utils/extended-mode-warning.js +1 -1
  215. package/dist/utils/boot-harness.js +56 -0
  216. package/dist/utils/build-pipeline.js +4 -4
  217. package/dist/utils/mode-helpers.js +2 -15
  218. package/dist/utils/runner-env.js +13 -28
  219. package/dist/utils/ship-keys.js +3 -3
  220. package/dist/utils/signing-status.js +51 -0
  221. package/dist/utils/test-events.js +7 -0
  222. package/dist/utils/test-stealth.js +6 -6
  223. package/dist/utils/url-helpers.js +52 -42
  224. package/dist/utils/user-agent.js +44 -0
  225. package/dist/vendor/account/engine.js +3 -3
  226. package/dist/vendor/account/index.js +14 -45
  227. package/dist/vendor/account/resolve-account.js +44 -0
  228. package/dist/vendor/account/schema.js +1 -1
  229. package/dist/vendor/account/user.js +99 -0
  230. package/dist/vendor/config/client-config.js +1 -1
  231. package/dist/vendor/config/company.js +46 -14
  232. package/dist/vendor/config/defaults.js +30 -7
  233. package/dist/vendor/config/edit.js +25 -3
  234. package/dist/vendor/config/env-delivery.js +1 -1
  235. package/dist/vendor/config/env-schema.js +3 -6
  236. package/dist/vendor/config/env.js +34 -22
  237. package/dist/vendor/config/environment.js +11 -30
  238. package/dist/vendor/config/index.js +21 -28
  239. package/dist/vendor/config/load.js +16 -9
  240. package/dist/vendor/config/platforms.js +1 -1
  241. package/dist/vendor/config/repo.js +10 -27
  242. package/dist/vendor/config/schema-client.js +64 -0
  243. package/dist/vendor/config/schema-cloud.js +38 -0
  244. package/dist/vendor/config/schema-manager.js +118 -0
  245. package/dist/vendor/config/schema-overrides.js +68 -0
  246. package/dist/vendor/config/schema.js +104 -160
  247. package/dist/vendor/config/site-global.js +2 -3
  248. package/dist/vendor/config/validate.js +97 -78
  249. package/dist/vendor/config/winback.js +1 -1
  250. package/dist/vendor/devkit/actions-secrets.js +1 -1
  251. package/dist/vendor/devkit/agents-md.js +233 -0
  252. package/dist/vendor/devkit/attach-log-file.js +16 -2
  253. package/dist/vendor/devkit/build-json.js +1 -1
  254. package/dist/vendor/devkit/ci-workflows.js +30 -30
  255. package/dist/vendor/devkit/cli-router.js +16 -11
  256. package/dist/vendor/devkit/defaults-engine.js +15 -51
  257. package/dist/vendor/devkit/deploy-snapshot.js +44 -9
  258. package/dist/vendor/devkit/env-lines.js +183 -0
  259. package/dist/vendor/devkit/local.js +64 -10
  260. package/dist/vendor/devkit/lockfile.js +32 -13
  261. package/dist/vendor/devkit/logger.js +7 -2
  262. package/dist/vendor/devkit/merge-line-files.js +219 -177
  263. package/dist/vendor/devkit/omega-bin.js +208 -111
  264. package/dist/vendor/devkit/preludes/docs-sync.js +52 -0
  265. package/dist/vendor/devkit/preludes/index.js +1 -0
  266. package/dist/vendor/devkit/target-picker.js +45 -0
  267. package/dist/vendor/devkit/test/dashed-files.js +37 -0
  268. package/dist/vendor/devkit/test/run-verb-under-tee.js +71 -0
  269. package/dist/vendor/devkit/test/runner-core.js +6 -6
  270. package/dist/vendor/devkit/update.js +15 -15
  271. package/dist/vendor/devkit/verb-scripts.js +40 -0
  272. package/dist/vendor/devkit/verbs.js +170 -0
  273. package/dist/vendor/monitoring/env.js +2 -2
  274. package/dist/vendor/monitoring/index.js +1 -1
  275. package/dist/vendor/monitoring/main.js +1 -1
  276. package/dist/vendor/monitoring/preload.js +1 -1
  277. package/dist/vendor/monitoring/renderer.js +1 -1
  278. package/package.json +19 -26
  279. package/dist/commands/install.js +0 -37
  280. package/dist/defaults/AGENTS.md +0 -110
  281. package/dist/defaults/CLAUDE.md +0 -1
  282. package/dist/lib/client-bridge.js +0 -374
  283. package/dist/lib/logger.js +0 -4
  284. package/dist/test/suites/build/manager.test.js +0 -213
  285. package/dist/test/suites/main/client-bridge.test.js +0 -262
  286. package/dist/vendor/config/env-retired.js +0 -137
  287. package/dist/vendor/config/retired-keys.js +0 -635
  288. package/docs/analytics.md +0 -140
  289. package/docs/app-state.md +0 -92
  290. package/docs/audit.md +0 -69
  291. package/docs/auto-updater.md +0 -243
  292. package/docs/boot-sequence.md +0 -39
  293. package/docs/build-system.md +0 -169
  294. package/docs/cdp-debugging.md +0 -169
  295. package/docs/client-bridge.md +0 -269
  296. package/docs/common-mistakes.md +0 -21
  297. package/docs/config-schema.md +0 -120
  298. package/docs/context-menu.md +0 -112
  299. package/docs/context.md +0 -81
  300. package/docs/css.md +0 -90
  301. package/docs/deep-link.md +0 -186
  302. package/docs/environment-detection.md +0 -112
  303. package/docs/fontawesome.md +0 -107
  304. package/docs/hooks.md +0 -89
  305. package/docs/icons.md +0 -79
  306. package/docs/index.md +0 -317
  307. package/docs/installer-options.md +0 -165
  308. package/docs/ipc.md +0 -61
  309. package/docs/lib-modules.md +0 -53
  310. package/docs/logging.md +0 -229
  311. package/docs/menu.md +0 -160
  312. package/docs/releasing.md +0 -239
  313. package/docs/remote-config.md +0 -118
  314. package/docs/remote-scripts.md +0 -144
  315. package/docs/restart-manager.md +0 -144
  316. package/docs/runner.md +0 -290
  317. package/docs/sentry.md +0 -97
  318. package/docs/shared/agent-docs.md +0 -89
  319. package/docs/shared/analytics.md +0 -612
  320. package/docs/shared/brands.md +0 -57
  321. package/docs/shared/breaking-changes.md +0 -851
  322. package/docs/shared/config.md +0 -1952
  323. package/docs/shared/deploys.md +0 -341
  324. package/docs/shared/icons.md +0 -219
  325. package/docs/shared/local-dev.md +0 -167
  326. package/docs/shared/logging.md +0 -205
  327. package/docs/shared/monitoring.md +0 -167
  328. package/docs/shared/publishing.md +0 -187
  329. package/docs/shared/rulings.md +0 -34
  330. package/docs/shared/testing.md +0 -147
  331. package/docs/shared/theming.md +0 -629
  332. package/docs/shared/translation.md +0 -333
  333. package/docs/shared/updates.md +0 -61
  334. package/docs/signing.md +0 -293
  335. package/docs/startup.md +0 -142
  336. package/docs/storage.md +0 -59
  337. package/docs/templating.md +0 -101
  338. package/docs/test-boot-layer.md +0 -157
  339. package/docs/test-framework.md +0 -362
  340. package/docs/themes.md +0 -149
  341. package/docs/tooltips.md +0 -99
  342. package/docs/tray.md +0 -164
  343. package/docs/usage.md +0 -58
  344. package/docs/verts.md +0 -62
  345. package/docs/windows.md +0 -149
@@ -1,1952 +0,0 @@
1
- # omega.json5 — the single OMEGA config
2
-
3
- One config file, identical shape, for every OMEGA project type. Owned by `@omega.js/config`
4
- (`packages/config`); frameworks vendor it at prepare time and read **only** this format —
5
- there is no dual-read of legacy files. Legacy brands migrate by converting their old config
6
- once (mapping tables below) and deleting the old file.
7
-
8
- Consistency between this file and what pages actually say — brand facts read from config instead of typed,
9
- one brand hex, the merge chain, secrets out of config — is the plugin's `omega:brandcheck` skill,
10
- [agent-plugins/claude/skills/brandcheck/SKILL.md](../../agent-plugins/claude/skills/brandcheck/SKILL.md);
11
- its quality hook fires on every `omega.json5` edit.
12
-
13
- ## Location
14
-
15
- | Project | File |
16
- |---|---|
17
- | Every project type (default) | `config/omega.json5` |
18
- | Standalone backend repo | `functions/config/omega.json5` |
19
- | Brand monorepo — brand level | `{brand}/config/omega.json5` |
20
- | Brand monorepo — local level | `{brand}/targets/{target}/config/omega.json5` |
21
-
22
- JSON5: comments, trailing commas, unquoted keys, single quotes all allowed.
23
-
24
- ## Shape
25
-
26
- ```json5
27
- {
28
- // SHARED sections — identical spelling in every project type.
29
- // (`SHARED_SECTIONS` in @omega.js/config is the authoritative list.)
30
- brand: { id, name, url, description, tagline, type, font, color, contact: { email, phone, person: {…}, carbonCopy: […] }, address: {…}, images: {…} }, // #524: type = the schema.org type the JSON-LD stamps ('Organization' unset), font = the display face the assets service renders the wordmark from. contact.person = the human who signs "personal" email (name, firstName, image, url, urlText), ASKED for by `omega onboard` ([#770](https://github.com/Omega-JS-Stack/omega/issues/770)) since nothing can derive a human name: the wizard prompt writes name + the optional image/url, `--contactName`/`--contactImage`/`--contactUrl` answer it non-interactively, and an unanswered brand gets no key at all; contact.carbonCopy = audit BCCs
31
- cloud: { provider: 'firebase', config: { apiKey, authDomain, databaseURL, projectId, storageBucket, messagingSenderId, appId, measurementId }, messaging: { vapidKey }, shared, supportEmail, consentAudience, apiSubdomain, organizationId, billingAccount, oauthRedirectsConfigured }, // ONE cloud home (#23): the app config PLUS the provisioning fields; projectId lives only at cloud.config.projectId. vapidKey: web-push public key (console → Cloud Messaging), public by design. shared: true limits the manage cycle to the per-brand operations AND skips the backend deploy whole (#882, deploys.md)
32
- repo: { provider: 'github', org }, // ONE block (#883): where the brand hosts its SOURCE. Presence enables the repo service, `provider` defaults to `github`, and `org` is the only typed value: every repo name derives from `<brand.id>-<role>` and visibility is the brand root package.json's `private` field, so neither is configurable here
33
- edge: { providers: { cloudflare: { enabled, zone, dns, settings, rules, cacheRules, speedTest, workers } } }, // rules.redirect: the ORDERED dynamic-redirect ruleset — [{ name, expression, statusCode, preserveQueryString, targetUrl, enabled }], `expression`/`targetUrl` in Cloudflare's own filter language. The ONE home for a TEMPLATED redirect, whose destination is computed from the request path (#466); the manager's edge service reconciles them by `name` (docs/manager/edge.md)
34
- captcha: { providers: { recaptcha: { project, siteKey, domainsConfirmed: [] } } }, // domainsConfirmed: machine-written — the classic key's domain list has no API, so the captcha service records the owner's confirmation here and stops asking
35
- search: { providers: { searchConsole: { enabled, submitSitemap, sitemapPaths, gaLinked } } }, // #546: each `enabled` is the service's own switch, default ON — false skips that whole service. gaLinked: machine-written — the Search Console ↔ GA association has no API, so the confirmation is the record
36
- forms: { providers: { slapform: { enabled, formId, templateFormId, updateFormInfo, plan } } },
37
- inbound: { chat: { providers: { chatsy: { enabled, agentId, accountId, templateAgentId, updateAgentInfo, plan, sponsorshipsUrl, settings } } },
38
- email: { providers: { replyify: { enabled, agentId, templateAgentId, updateAgentInfo, plan, discount } } } },
39
- analytics: { providers: { google: { id, propertyId, accountId }, meta: { id, accountId }, tiktok: { id, accountId, appId } } }, // #524: the pixel/measurement id is the RUNTIME value; propertyId/accountId are the platform ids the manager reconciles against (never secrets — tokens stay in .env). tiktok.appId: the DEVELOPER APP the token mint authorizes through (#448/#635) — public config; the app secret is pasted once and never saved
40
- advertising: { providers: { adsense: { client, displaySlot, inArticleSlot, inFeedSlot, multiplexSlot }, inhouse: { source } }, fallback, tags: [] }, // C4 cp105; inhouse source: 'self' | 'company' | full URL (ads spec). #527: `client` is the ONE adsense switch — its presence drives the managed account, the ad units and the ads.txt record together (no `units`, no `enabled`). Role-level: `fallback: 'inhouse'` is the lane a provider miss falls through to (false/absent ends at the built-in promo), `tags` are the brand's contextual targeting tags
41
- payment: { currency, providers: { stripe: { publishableKey }, paypal: { clientId }, chargebee: { site }, coinbase: { enabled } }, products: […], winback: { enabled, percent, amount, duration } }, // currency: the ISO 4217 code every price in `products` is quoted in, default `USD` (#850). One currency per brand, named by the pricing page's JSON-LD, the checkout and the backend's order history alike; the successor to the retired `targets.web.currency`. #642: coinbase (Coinbase Commerce, crypto, one-time purchases only) is the one provider switched by an explicit `enabled`, default OFF: its whole credential is the secret COINBASE_COMMERCE_API_KEY, so there is no public datum to gate on. winback = the cancel-flow save offer (#268), on by default at 50% off the next cycle; see below
42
- monitoring: { enabled, providers: { sentry: { org, dsn, environment, sampleRate, tracesSampleRate, replaysSessionSampleRate, replaysOnErrorSampleRate, scrubEmail, attachScreenshot, bundlePatterns: [] } } }, // #425: the monitor is a KEY under `providers`. dsn presence IS the runtime enable signal; environment unset = the host's gate names it; scrubEmail defaults true (email OFF), attachScreenshot is desktop-only, bundlePatterns + the two replay rates browser-only (replay defaults to 0 — opt-in, #485). docs/shared/monitoring.md
43
- connections: { <provider>: { enabled, scope: [], name, logo, description } }, // #771/#788: per-provider USER-CONNECTION settings, keyed by provider name; public values only (the credentials are the CONNECTIONS_<PROVIDER>_CLIENT_ID/_SECRET env pair). The provider set is OPEN — a brand ships its own as `targets/backend/src/connections/<name>.js` — so the section stays free-form. See below
44
- theme: { id, appearance }, // project-owned; seeded at onboarding
45
- translation: { enabled, default, languages: [], providers: { claude: {} } | { chatgpt: {} }, model, include: [] }, // presence picks the engine; absent = claude. `include` is the web route list (#858): globs with `!` negation, read in .gitignore order, default ['**', '!blog/**'], a brand list REPLACES it, and a page overrides it for itself with `translation.include: true`/`false` in frontmatter. docs/shared/translation.md
46
- socials: { twitter: 'somiibo', spotify: { handle, redirect } }, // platform → handle. The handle derives the profile URL every surface reads (JSON-LD sameAs, the footer row, omega_social) and @omega.js/web emits a shortlink redirect page at /<platform> per entry (#429); the object form adds a redirect target that WINS for the shortlink when it is not the profile URL. Blank handle = no entry, no page. Not a disperse-owned SHARED_SECTION — brand-level content, read by the web target
47
-
48
- // MANAGER-read brand-level sections (#277). Schema-known at the TOP level: the
49
- // manager loads the brand config unfolded, and a website-only brand has no
50
- // `targets.backend` to hold them (presence there would enable the target). A
51
- // `targets.backend.<same key>` block still overrides any of them.
52
- company: { id: '<parent brand.id>' | 'self', webhooks: true }, // #677: the ONE key joining a brand to its company, OUTSIDE `brand`. `id` and `webhooks` are the only typed keys; the loader FILLS the same key with { id, name, url, images: { wordmark }, webhooks } from the parent's own config, and a brand with no company resolves to its own name/url under a null id, so no reader carries a fallback. `webhooks: false` says the provider ACCOUNT (SendGrid Event Webhook, Beehiiv webhook) is somebody else's and the walk never repoints its one webhook. See "The company" below
53
- domain: { providers: { namecheap: {} }, email: { providers: { cloudflare: {} }, forwarding: [] } }, // TWO roles (#425): the REGISTRAR is the one key under `providers` (namecheap is the one the service drives by API; every other registrar gets manual instructions), the mailbox provider the one key under `email.providers`. Presence picks; no entry = nothing chosen, and the service skips
54
- certificates: { enabled, providers: { apple: { bundleIdPrefix, capabilities: [], profiles: [], certificates: [] } } }, // Apple signing for desktop/mobile targets. bundleIdPrefix is the brand's own answer ('com.mycompany' + brand.id composes the bundle id); the credentials live in .env (APPLE_API_ISSUER, APPLE_API_KEY_ID, APPLE_TEAM_ID)
55
- reviews: { enabled, sites: [] },
56
- marketing: { campaigns: { enabled, providers: { sendgrid: { listId, groups: { orders, hello, account, marketing, security, newsletter, internal } } } }, newsletter: { enabled, providers: { beehiiv: { publicationId } }, content: […] }, prune: { enabled } }, // #425: each role names its vendor as a KEY under `providers`; `enabled` and the newsletter `content` PIPELINE blob stay role-level. `prune` is ON by default (Ian 2026-08-22, #478) and per-brand disableable: packages/backend/docs/marketing-campaigns.md § Contact Pruning. `groups` holds the SendGrid unsubscribe (ASM) group ids — per ACCOUNT, so the campaigns service provisions them by name and writes the ids here (#649)
57
- blog: { /* AI blog-content settings (Ghostii pipeline) */ },
58
- devlog: { enabled, providers: { ghostii: { orgs, lookbackDays, … } } }, // commit-digest devlog (#553): `enabled: true` PUBLISHES AI-written posts to the live site, so it is case 3 — the literal true is the only ON, absence is off, and no default is materialized
59
- seo: { github: { content: [] } }, // the manager's parasite-SEO content repos; big blocks may live in the `config/seo.json5` sidecar. The site-wide SEARCH POSTURE is NOT here: it is `targets.web.meta.index`, the same name a page writes (#564)
60
- dataRequest: { /* GDPR/CCPA data-request query definitions */ },
61
- directory: { enabled }, // opt in to the manager's directory PUSH: this brand's entry into the COMPANY's brands collection (#246/#677); default off, public facts only. docs/manager/directory.md
62
- sponsorships: { acceptable: [], unacceptable: [], prices: { 'guest-post': 70, 'link-insertion': 50 } }, // sponsorship terms — the first directory BLOCK; `prices` is an open placement→USD map, not an enum
63
-
64
- // TARGET-scoped config. EVERY KEY IS A TARGET NAME and key presence =
65
- // "this brand enables a target by that name" (#886); the name is the folder
66
- // `targets/<name>`. Every entry MUST declare its `type`: the framework that
67
- // runs there, or `custom` (see Custom targets below). The keys below are
68
- // named for their type, which is the common case, not a rule: a second web
69
- // target is a sibling key with `type: 'web'` (see Targets below).
70
- targets: {
71
- web: { type: 'web', hosting: { provider: 'github' }, meta: { index }, imagemin, collections, client: { consent, … }, dev: { limitCollections } }, // `hosting` says who SERVES this target's built site (#883): web targets only, `github` by default, which publishes it to the target's own `<brand.id>-<name>` repo and serves it from Pages at the target's url. `meta` holds SITE-WIDE defaults for page-meta values, spelled exactly as a page spells them (#564; `index` is the only key today). No `redirects` key: #466 retired it; a TEMPLATED redirect is a Cloudflare redirect rule (edge.providers.cloudflare.rules.redirect), an enumerable one is a redirect PAGE (docs/web/index.md). client: the @omega.js/client runtime blob (auth, sentry, exitPopup, …): a settings bag the client normalizes; only the keys a BRAND authors are schema-known: `consent` (see "Consent" below), plus `auth.config.policy` ('authenticated' | 'unauthenticated' | 'disabled'; absent = no policy, and the auth/admin layouts set theirs in page frontmatter), `exitPopup.enabled` and `serviceWorker.enabled` (both default true, materialized) ([#650](https://github.com/Omega-JS-Stack/omega/issues/650)). collections: the brand's OWN content collections: name → { field, size, title, description, permalink }; documents live in `_<name>/` and the engine generates the listing + one page per category of `field` (#207). dev.limitCollections: dev-only collection sampling: collection name → max documents ({ posts: 50 }) plus `randomize: true`; development builds only, production always ships the whole site (#190)
72
- backend: { type: 'backend', projectType, auth: { signup: { maxPerIpPerDay } } }, // projectType: 'firebase' (default: Cloud Functions) | 'custom' (the same backend as its own server on PORT, for a container host: no Functions deploy, no emulator lane; see "Backend project type" below). auth.signup.maxPerIpPerDay: signups allowed per client IP per day, positive integer, default 2. Raise it for audiences behind shared egress (NAT/CGNAT, VPNs, offices)
73
- desktop: { type: 'desktop', omega: { authPersistence }, app: { appId, productName, copyright, category, languages, darkModeSupport },
74
- platforms: { mac, windows, linux },
75
- autoUpdate: { enabled, autoDownload, startupDelayMs, feedCheckIntervalMs, idleEvalIntervalMs, maxAgeMs },
76
- startup: { mode, openAtLogin: { enabled, mode } },
77
- releases, remoteConfig: { enabled, url }, remoteScripts, restartManager }, // platforms: what this app SHIPS plus each platform's install knobs (#867): `platforms.<mac|windows|linux>.formats.<dmg|nsis|deb|appimage|snap>`, presence = enabled, `false` drops a default, per-format settings inside the format (see "The shipping declaration" below)
78
- extension: { type: 'extension', platforms: { chrome, firefox, edge }, categories, listings }, // platforms: the SAME shipping declaration the desktop target carries (#867): `platforms.<chrome|firefox|edge>.formats.<zip|store>`, where the zip rides the release and the store is published to. categories: the AMO slugs a FIRST Firefox publish lists the add-on under (#884), default ['alerts-updates'] and validated against AMO's own set; the Firefox lane sends them with the summary (brand.description, cut to 250) and the target package.json `license` (UNLICENSED lists as all-rights-reserved). listings: per store, the `id` the store knows this extension by (#893: chrome/firefox/edge; the firefox one IS the manifest's gecko id) plus the `url` and `state` the site's /extension page reads (see "The site global" below)
79
- mobile: { type: 'mobile' /* RESERVED: MAM parked */ },
80
- community: { type: 'web', url: 'https://community.acme.com' }, // a SECOND web target → targets/community
81
- api: { type: 'custom' }, // a target no framework owns (#603): the manager drives it entirely through its own package.json scripts; see "Custom targets" below
82
- },
83
- }
84
- ```
85
-
86
- ## Resolution
87
-
88
- `loadConfig(projectDir, target, { defaults })` produces ONE resolved object per target:
89
-
90
- ```
91
- schema defaults ← framework defaults ← company ← company.<environment> ← brand shared ← brand targets[name] ← brand.<environment> ← local shared ← local targets[name] ← local.<environment>
92
- ```
93
-
94
- - **The bottom layer is the SCHEMA's own defaults** ([#478](https://github.com/Omega-JS-Stack/omega/issues/478)) —
95
- see [Defaults & self-healing](#defaults--self-healing) below. `options.defaults` sits directly
96
- above it and carries only what a framework does differently.
97
- - "shared" = the file minus its `targets` key. In a standalone repo only the local layers exist.
98
- - **Every layer is TWO files**: its `config/omega.json5` and an optional
99
- `config/omega.<environment>.json5` overlay beside it
100
- ([#856](https://github.com/Omega-JS-Stack/omega/issues/856)), the config mirror of the
101
- `.env` + `.env.<environment>` pair below. The overlay wins over its OWN base and still
102
- loses to the layer above, each file keeping the shared-then-`targets[name]` split. The
103
- three names are exactly what `envEnvironment()` returns, so the config overlay, the env
104
- overlay and the runtime's own answer are ONE vocabulary: exactly ONE environment's
105
- overlay composes, a missing overlay is nothing, and a file named anything else
106
- (`omega.staging.json5`) is not a layer at all, so nothing reads it and nothing warns
107
- about it. An overlay is a PARTIAL, holding only the overrides: the validator judges the
108
- merged result, never an overlay on its own. Nothing scaffolds one; a brand writes the
109
- file the day it has a public value that differs by environment (the case that named it:
110
- a `pk_test_` Stripe publishable key for the local checkout, the live key in the base).
111
- - **WHICH environment's overlay composes is the LANE's word, never the machine's**
112
- ([#856](https://github.com/Omega-JS-Stack/omega/issues/856)): `loadConfig` and
113
- `composeTargetConfig` take `options.environment`, and every lane that produces a
114
- production artifact names `production`, the way `stageFunctions({ environment })` already
115
- names one for the `.env` overlay beside it. So a production build never carries a
116
- development override, even though the machine that runs it answers `development` in a
117
- terminal: the web build and both web deploy lanes name it, the desktop and extension
118
- bakes name it under their own `OMEGA_BUILD_MODE` word, and the backend deploy stage names
119
- it for both halves of the upload at once. The ambient answer (`envEnvironment()`) is what
120
- a DEV boot and a deployed runtime take, which is the whole point of it; an environment
121
- outside the three names throws. A compose asks outright: told nothing,
122
- `composeTargetConfig` freezes the authored base layers and no overlay at all.
123
- - **The company layer** is the company's own `company/config/omega.json5`, found through the
124
- brand's `company: { id }` key and the machine registry (the same ONE resolver the `.env`
125
- cascade, owner hooks and the signing tree read, [#677](https://github.com/Omega-JS-Stack/omega/issues/677)
126
- see "The company" below). It layers exactly like the brand file (company shared <-
127
- company `targets[name]`). A brand naming no company has no company layer; the resolved
128
- result reports the file it used as `files.company`.
129
- - **`projectDir` may be one of a target's SUBDIRS** — `functions/` (@omega.js/backend's runtime cwd)
130
- or `dist/` (its staged build output, the view `omega test` loads): every walk (brand root,
131
- company marker, local-layer fallback, target name, compose, `resolveBrandRoot`) treats the target root
132
- as one level up, so `loadConfig(functionsDir, 'backend')`, `loadConfig(distDir, 'backend')` and
133
- `loadConfig(targetRoot, 'backend')` resolve identically. A staged `config/omega.json5` inside either
134
- subdir is the deployed runtime's own view, never an authored local layer.
135
- - **WHICH entry is the target layer comes from the DIR**: `targets/community` resolves
136
- `targets.community` (§ Targets). A standalone project has no such dir, so it is named by its
137
- own file: the SINGLE declared target of the framework's type (a deployed backend staged from
138
- `api: { type: 'backend' }` is still the `api` target, so its entry is the layer, `enabled` is
139
- true and its url derives), and the type word when the file declares none or declares two.
140
- - **Target sections overlay the TOP LEVEL**: `targets.desktop.platforms` resolves to
141
- `config.platforms`; frameworks never read through `config.targets.<name>.…`.
142
- - **Any shared key inside a target entry overrides it for that surface** — a desktop-only
143
- Sentry DSN is just `targets.desktop.monitoring.providers.sentry.dsn`; disabling any integration per-surface is
144
- uniformly `<key>: { enabled: false }`. One agnostic deep merge everywhere (objects merge,
145
- arrays/scalars replace, `null` replaces, `undefined` is skipped).
146
- - **A global value and its specific override share ONE name**: the standing rule is recorded in
147
- [docs/shared/rulings.md](rulings.md) (Ian 2026-09-09).
148
- - **@omega.js/web adds one MORE layer, per page** ([#607](https://github.com/Omega-JS-Stack/omega/issues/607)):
149
- a page's (or layout's) `config:` frontmatter block merges over the resolved config for that page
150
- alone, and templates read the result as `resolved.config.*` — the WHOLE merged config, never a
151
- subset. It is the same deep merge, one layer higher: `… ← local targets[name] ← page config:`.
152
- Nothing else in the config chain knows about it. The membership rule runs both ways: a page
153
- restating a config section BARE is a build error, and a key under `config:` that no omega.json5
154
- section answers to is a build error too. Page machinery — `meta`, `schema`, `layout`,
155
- `permalink` — is not config and has no home in this file at all
156
- ([docs/web/frontmatter.md](../web/frontmatter.md)).
157
- - The merged `targets` map rides along on the resolved config so enabled-target enumeration
158
- survives (`getEnabledTargets()`); the `enabled` flag on the result says whether the resolved
159
- NAME is listed, and the result carries that `name`.
160
- - **A dir whose entry runs another framework is a loud error**: `loadConfig(targets/backend,
161
- 'desktop')` throws `targets.backend is type backend; this project runs the desktop framework`
162
- rather than silently merging somebody else's layer.
163
- - No target argument → whole-file merge (the shape omega-manager's disperse works with).
164
-
165
- ## Targets: every key is a NAME (#886)
166
-
167
- `targets` is an object whose EVERY key is a target NAME. The name is the folder
168
- (`targets/<name>`), the `--target=<name>` word, and the derived-repo suffix. Every entry
169
- declares a `type`: the framework that runs there (`web`, `backend`, `desktop`, `extension`,
170
- `mobile`) or `custom`. A brand runs two web sites by declaring two names:
171
-
172
- ```json5
173
- targets: {
174
- web: { type: 'web', url: 'https://somiibo.com' },
175
- community: { type: 'web', url: 'https://community.somiibo.com' },
176
- backend: { type: 'backend' },
177
- desktop: { type: 'desktop' },
178
- docs: { type: 'custom' },
179
- }
180
- ```
181
-
182
- - **No id, no folder key, no name table.** The key IS the answer, so the dir walk and the path
183
- derivation are each one line: `targetPath(config, 'community')` → `targets/community`, and
184
- `targetNameFromDir(projectDir)` → the target root's basename inside a brand (null for a
185
- standalone project, whose dir name is arbitrary). `targetEntries(config)` is the ONE
186
- enumeration, `{ name, type, ...entry }` in config order; `targetsOfType(config, 'web')`
187
- filters it. Nothing derives a folder any other way.
188
- - **The merge chain is by NAME**: `defaults ← company ← brand shared ← brand targets.<name> ←
189
- local shared ← local targets.<name>`. A shared key inside an entry still overrides the shared
190
- value for that surface, exactly as before.
191
- - **The NAME is the subdomain** ([#588](https://github.com/Omega-JS-Stack/omega/issues/588),
192
- Ian 2026-09-01). `targetUrl(config, name)` is the one resolver: the entry's own `url`, else a
193
- target-scoped `brand.url`, else `brand.url` when the name IS its type (`web: { type: 'web' }`
194
- is the brand itself), else `https://<name>.<host of brand.url>`, so `community: { type:
195
- 'web' }` is a COMPLETE declaration. An explicit `url` overrides it for a custom host. The host
196
- is taken EXACTLY as `brand.url` states it (a `www.` brand derives `admin.www.acme.com`), and
197
- no usable `brand.url` derives nothing at all (null, never a half-built `https://admin.`).
198
- - **Validator rules**: a key must be a dir-safe slug (`/^[a-z][a-z0-9-]*$/`, because it is a
199
- folder name), an entry must be a plain object, and its `type` must be one of the six. An ARRAY
200
- is the retired multi-instance form and names its replacement, a sibling key
201
- ([breaking-changes.md](breaking-changes.md)). **More than one `backend` target is a WARNING**
202
- (`warnings` on the result): backend stays single in practice, one Cloud Functions surface per
203
- brand.
204
- - **Dev ports offset by the target's position among its OWN type**
205
- (`targetPortOffset(config, name)`): the first web target takes the classic base, the second
206
- takes base + 1, and a backend declared between them shifts nothing
207
- ([docs/shared/local-dev.md](local-dev.md)). The N7 bump-if-taken allocator still guarantees a
208
- free port.
209
- - **An override AT the target IS its url**, never a base to stack the name on: the entry's own
210
- `brand.url`, a `targets/<name>/config/omega.json5` naming `https://shop.acme.test`, or a dev
211
- layer naming `http://localhost:4000` are each the answer as written (no `shop.shop.acme.test`,
212
- no `https://admin.localhost:4000`). The derivation only runs while the resolved `brand.url` is
213
- still the brand layer's. A CUSTOM host belongs on the entry's `url`, never on a target-scoped
214
- `brand.url`: the authDomain check reads `brand.url`, so overriding it there makes that check
215
- compare against the custom host and fail.
216
- - **Brand-level facts stay brand-level.** `cloud.config.authDomain` compares against `brand.url`
217
- for every target (one Firebase project, one backend, one authDomain), and so does the persona
218
- domain the test lanes seed. Only a target's PUBLIC surface is per target: `site.url`, the
219
- gh-pages CNAME `omega deploy`/`omega build` write, and the deploy path prefix.
220
- - **One shared `api.<domain>`**: every web target talks to the same backend, so the manager's
221
- cloud hosting op ensures exactly one API domain no matter how many targets a brand runs
222
- ([docs/manager/cloud.md](../manager/cloud.md)).
223
- - **The resolved `url` is a declared key** (`packages/config/src/schema.js`), so a target load
224
- raises no undeclared-key warning for the url it just derived.
225
- - **Legacy `brand.subdomains` conversion rule**: each subdomain becomes its own web target,
226
- `["admin", "cdn"]` → `admin: { type: 'web' }, cdn: { type: 'web' }` beside the main site; the
227
- names carry the subdomains, so nothing else is written. The key itself is a retired path
228
- (below), so a config still carrying it fails validation with that recipe.
229
- - Non-goals: no cross-target shared builds, no per-target Firebase projects.
230
-
231
- ## The shipping declaration (`platforms`) and the format table (#867)
232
-
233
- Desktop and extension declare what they SHIP in the SAME shape, in the ONE vocabulary
234
- (@omega.js/client's `getPlatform()` / `getBrowser()`): platforms `mac`, `windows`,
235
- `linux`, `chrome`, `firefox`, `edge`; formats `dmg`, `nsis`, `deb`, `appimage`, `snap`,
236
- `zip`, `store`. It is `mac`, never `macos`, and `windows`, never `win`.
237
-
238
- ```json5
239
- targets: {
240
- desktop: {
241
- type: 'desktop',
242
- platforms: {
243
- mac: { formats: { dmg: {} }, arch: ['universal'] },
244
- windows: { formats: { nsis: {} }, oneClick: true, signing: { strategy: 'self-hosted' } },
245
- linux: { formats: { deb: {}, appimage: {}, snap: { channels: ['stable'] } } },
246
- },
247
- },
248
- extension: {
249
- type: 'extension',
250
- platforms: {
251
- chrome: { formats: { zip: {}, store: {} } },
252
- firefox: { formats: { zip: {}, store: { channel: 'listed' } } },
253
- edge: false, // this brand does not ship to Edge
254
- },
255
- },
256
- }
257
- ```
258
-
259
- - **Presence is the switch, and every platform and format defaults ON.** A brand drops one
260
- with the literal `false` (the `certificates: false` idiom), never by omitting it: a
261
- config that says nothing ships everything the framework can build.
262
- - **Per-format settings live INSIDE the format** (`linux.formats.snap.channels`,
263
- `firefox.formats.store.channel`), which is why the declaration is an object and not a
264
- list. The keys BESIDE `formats` are that platform's install knobs (arch, the NSIS
265
- installer flags, mac entitlements, the Windows signing strategy).
266
- - **The format table is [`platforms.js`](../../packages/config/src/platforms.js)**, the
267
- successor of `desktop-artifacts.js`: per target, platform and format it says what the
268
- format IS (`kind: 'asset'`, a file on the brand's releases repo with an `ext`; or
269
- `kind: 'store'`, a publish), the env keys it cannot ship without (`requires`) and the
270
- per-listing config paths a store needs before it can accept an upload (`listing`, the
271
- `listings.<browser>.id` of [#893](https://github.com/Omega-JS-Stack/omega/issues/893)),
272
- and, for a store, the `label` and `console` page a human creates that listing on.
273
- Everything derives from it: the electron-builder target lists, the site's
274
- `/download/<platform>/<format>` links and their versionless asset names, the manage
275
- walk's per-brand list of ship credentials and its Enter-to-open ask, and the manual step
276
- a publish prints for a listing that does not exist yet.
277
- - **A `listing` says what a store needs BEFORE it can accept an upload**, which is why the
278
- firefox store declares none: AMO takes the manifest's gecko id, so that publish always
279
- goes through, and the id is the brand's own derived value, pinned into the brand config as
280
- `listings.firefox.id` by the extension's local scaffold (never by a publish, which runs on
281
- a throwaway checkout). Chrome and Edge assign theirs in a dashboard, so those two are
282
- the ids the walk asks for, with a "not yet" skip.
283
- - **`requires` names ENV keys, and the env schema owns them.** A key with a `requiredWhen`
284
- applies only when that rule holds, so the Windows nsis format asks for the credentials of
285
- the strategy the brand actually picked and no other's. The schema is also where each key's
286
- `label`, `url` (the page that mints it) and `hint` live: one home for what a key is and
287
- where it comes from (see "The env schema" below).
288
- - **The one translation points.** Node's `darwin`/`win32` becomes OMEGA's word in
289
- @omega.js/desktop's `utils/platform.js`; electron-builder's `mac`/`win`/`AppImage`/`nsis`
290
- is spelled only in its `gulp/tasks/build-config.js`; GitHub's runner labels only in the
291
- generated workflow. `APPLE_*` and `SNAPCRAFT_*` env names stay as they are: they name
292
- vendors, not platforms.
293
- - **Retired, with a migration**: `platforms.win` and `platforms.linux.snap` are registered
294
- retired paths, so a brand still carrying either fails validation naming its replacement.
295
- `npx omega manage --migration=platform-names --execute` performs the rewrite at the brand
296
- root (every omega.json5 it owns, brand file and target files alike) and renames
297
- `config/icons/macos/` to `config/icons/mac/` with it. Run it BEFORE `omega migrate`, which
298
- DELETES a retired key rather than moving it. `snap.enabled: false` becomes
299
- `formats.snap: false`, because presence is the switch now.
300
-
301
- ## Custom targets (#603)
302
-
303
- A brand also runs targets no framework owns — a Render API, a worker, a script. They are
304
- declared like any other target, with `type: 'custom'`:
305
-
306
- ```json5
307
- targets: {
308
- web: { type: 'web' },
309
- api: { type: 'custom' }, // → targets/api
310
- jobs: { type: 'custom' }, // → targets/jobs
311
- nightly: { type: 'custom' }, // → targets/nightly
312
- }
313
- ```
314
-
315
- - **The type is the declaration**, and it is the ONLY thing checked: the key is a name, so a
316
- name no framework owns is a deliberate brand choice rather than a typo to guess at.
317
- - **Its verbs are its own package.json scripts**: `start`, `build`, `test`, `deploy`, `clean`.
318
- The manager runs each through `npm run <verb>` when the script is present and skips it
319
- loudly when it is absent — nothing is inferred or defaulted.
320
- - **No framework service reconciles it.** The only manage op that sees a custom target is the
321
- workspace service (structure, agent docs, settings). Nothing is composed into a `.env` of its
322
- own; it INHERITS the brand keys — the manager loads the env chain into `process.env` before it
323
- spawns anything, so a custom target started by `omega dev`/`omega deploy` has them. A standalone
324
- run inside the target dir does not (there is no `@omega.js/config` in there to walk the cascade).
325
- Full contract: [docs/manager/index.md](../manager/index.md) § Custom targets.
326
-
327
- ## Backend project type (#584)
328
-
329
- `targets.backend.projectType` says how the backend RUNS, and it is the only switch:
330
-
331
- ```json5
332
- targets: {
333
- backend: { projectType: 'custom' }, // default is 'firebase'
334
- }
335
- ```
336
-
337
- - **`'firebase'` (default)** — the backend exports Cloud Functions, deploys with `firebase deploy`,
338
- and runs locally on the emulator suite. Everything OMEGA does today.
339
- - **`'custom'`** — the SAME backend (same routes, same schemas, same auth middleware, same
340
- helpers, same `.env`) served by its Express app on `process.env.PORT`, for a container host
341
- (Render & co). `Manager.init()` reads the mode off this key, so a brand's `src/index.js` is
342
- unchanged; an explicit `init` option still wins.
343
- - **What custom mode removes is the Firebase LANE, not Firebase**: no Functions deploy, no
344
- emulator, no emulator test run — those four verbs refuse loudly and name their replacement
345
- ([docs/backend/index.md](../backend/index.md)). `firebase-admin` still loads, so a custom
346
- server that reads Firestore or verifies an ID token works exactly as before.
347
- - **Not to be confused with a custom TARGET** (above): that is a target no framework owns.
348
- This one IS the `@omega.js/backend` target, with a different artifact. Same-type duplicates
349
- are sibling keys (§ Targets).
350
- - The brand-root behavior — deploy through the target's own `deploy` script, `omega dev` booting
351
- the server instead of the emulator: [docs/manager/index.md](../manager/index.md).
352
-
353
- ## Hard rules
354
-
355
- - **Secrets NEVER live in omega.json5** — they live in `.env`. `loadConfig` throws on any
356
- key matching `/(secret|privateKey|apiSecret)$/i` in any section of a raw file, before any
357
- merge. Public credentials (`publishableKey`, `clientId`, `cloud.config.apiKey`) pass by
358
- design.
359
- - **The legacy `targets` ARRAY form throws**: `targets` is an object keyed by target name,
360
- and a per-target ARRAY (the retired multi-instance form) is a validation error naming its
361
- replacement, a sibling key.
362
- - Schema findings (required/type/min/match/enum) come back as `errors`, not throws — build-time
363
- audit throws on them, boot warns/fails per framework policy.
364
-
365
- ## The company (`company: { id }`), #677
366
-
367
- ONE key joins a brand to its company, OUTSIDE `brand` (Ian 2026-09-12: "brand key is for
368
- things about this brand, and the parent/company/organization key is OUTSIDE of that"):
369
-
370
- ```json5
371
- company: { id: 'itw-creative-works' }, // a sub-brand: the parent's brand.id
372
- company: { id: 'self' }, // the company brand itself, kept visible on purpose
373
- ```
374
-
375
- - **The `id` is the only key a consumer types.** The loader FILLS the same object at load,
376
- from the parent's own config: `company: { id, name, url, images: { wordmark } }`. One name,
377
- one shape, at every level.
378
- - **A brand with no `company` key resolves to `{ id: null, name: brand.name, url: brand.url,
379
- images: {} }`**, so no reader anywhere needs a fallback: the footer credit, the email
380
- wordmark and the in-house ads api all read one shape. `company.name` / `company.url` /
381
- `company.images` are RESOLVED, never typed: an authored one fails the load naming the key.
382
- - **The company's shared files live in `company/` inside the company brand's own repo**
383
- (`company/config/omega.json5`, `company/.env`, `company/.omega/certificates/apple/`), and
384
- inheritance is ONE rule: a brand-level file the child lacks resolves from there at the same
385
- relative path (`resolveCompany(brandRoot).file(relPath)`). A new kind of inherited file
386
- costs zero code. The manage walk's preflight SENDS an operator there
387
- ([#910](https://github.com/Omega-JS-Stack/omega/issues/910)): once a company resolves,
388
- every fix line it prints for a missing key names that `company/.env` by path, with the
389
- brand `.env` as the override, so a credential every sibling brand needs is pasted once.
390
- - **The doctrine line: a brand with a company reuses the company tree first, always.**
391
- - **WHERE that repo is on this machine is a cache, never configuration**: `~/.omega/brands.json`
392
- (`OMEGA_HOME` moves it), keyed by `brand.id`, which every `loadConfig()` refreshes for its
393
- own brand, and nobody maintains it. A company that has never been loaded here prints ONE line
394
- per run (`Company <id> is not on this machine: inheritance off…`) and the run continues with
395
- no company layer.
396
- - **Off-laptop, nothing ever reads `company/`**: the machine that dispatches a deploy resolves
397
- it and writes `config/company-resolved.json5` beside the brand config (`{ config, company }`),
398
- which the mirror push carries and then removes; the loader reads that generated layer when the
399
- registry has no line. The env half needs nothing: the deploy precheck's secrets push sends the COMPOSED target
400
- env (company ← brand ← target) as that repo's own secrets.
401
-
402
- Full contract, and the `omega company init` half: [../manager/company.md](../manager/company.md).
403
-
404
- ## Config or env? (#893)
405
-
406
- One value, one home, and the line between the two files is what it IS, not who
407
- reads it:
408
-
409
- - **config/omega.json5 holds what is PUBLIC by design**: a value a store URL
410
- carries, a browser bundle ships, or a binary prints. A store item id, a
411
- reCAPTCHA site key, a PayPal client id, a Chargebee site name, `cloud.config.apiKey`,
412
- a Stripe `publishableKey`: anyone can read them off the shipped product, so
413
- hiding them buys nothing and splitting them across two files costs a drift.
414
- - **`.env` holds secrets, the login coordinates that only travel WITH a secret,
415
- and machine paths**: an API key or token; the username, OAuth client id,
416
- Apple issuer / key id / team id that are useless without the secret beside
417
- them and are asked for in the same breath (Ian 2026-09-12: the Apple triple
418
- and the Namecheap username stay in env); and a path that is true of one
419
- laptop (`OMEGA_FONTAWESOME_ROOT`, `machineLocal`).
420
- - The validator enforces the first half mechanically: a secret-shaped key in
421
- omega.json5 hard-fails the load (Hard rules above). The second half is this
422
- rule plus the retired-key register below.
423
- - **A public value in config needs no CI delivery.** It rides the snapshot a
424
- deploy pushes like every other config value, so it is never a repo secret and
425
- never a `${{ secrets.KEY }}` line.
426
-
427
- ### Retired env keys (`src/env-retired.js`)
428
-
429
- Six public identifiers moved out of `.env` with #893. There is no dual-read, so
430
- a stale line is a value nothing consults: every `.env` layer read
431
- (`parseEnvFile`, which both the process cascade and the artifact composer go
432
- through) FAILS on one, naming the move.
433
-
434
- | Retired `.env` key | Its config home |
435
- |---|---|
436
- | `CHROME_EXTENSION_ID` | `targets.<name>.listings.chrome.id` |
437
- | `FIREFOX_EXTENSION_ID` | `targets.<name>.listings.firefox.id` |
438
- | `EDGE_PRODUCT_ID` | `targets.<name>.listings.edge.id` |
439
- | `RECAPTCHA_SITE_KEY` | `captcha.providers.recaptcha.siteKey` |
440
- | `PAYPAL_CLIENT_ID` | `payment.providers.paypal.clientId` |
441
- | `CHARGEBEE_SITE` | `payment.providers.chargebee.site` |
442
-
443
- The by-hand move for a brand that still carries one:
444
- [breaking-changes.md](breaking-changes.md). The SECRET halves are untouched:
445
- `RECAPTCHA_SECRET_KEY`, `PAYPAL_CLIENT_SECRET`, `CHARGEBEE_API_KEY` and every
446
- store API credential stay in `.env`.
447
-
448
- The same register carries the keys that stayed secrets and only changed NAME
449
- ([#845](https://github.com/Omega-JS-Stack/omega/issues/845)). The refusal is
450
- identical, and so is the silence it prevents: the env schema does not know the
451
- old name, so the value reaches nothing. The fix is renaming the line, not
452
- deleting it. Rows are exact names, one per provider.
453
-
454
- | Renamed `.env` key | Its new `.env` name |
455
- |---|---|
456
- | `OAUTH2_GOOGLE_CLIENT_ID` | `CONNECTIONS_GOOGLE_CLIENT_ID` |
457
- | `OAUTH2_GOOGLE_CLIENT_SECRET` | `CONNECTIONS_GOOGLE_CLIENT_SECRET` |
458
-
459
- And it carries the keys retired OUTRIGHT
460
- ([#819](https://github.com/Omega-JS-Stack/omega/issues/819), Ian 2026-09-13): no config
461
- path and no new env name, because a MECHANISM took the key's place rather than another
462
- value. Such a row declares `replacement: null` and its refusal reads as a deletion with
463
- nowhere to move the value to. The test-lane pair is the whole set today: web, desktop and
464
- extension each test their own sign-in against a persona the backend emulator seeds
465
- ([#904](https://github.com/Omega-JS-Stack/omega/issues/904)), so no suite holds a
466
- credential of its own and the `testing` schema group is gone with them.
467
-
468
- | Retired `.env` key | What replaced it |
469
- |---|---|
470
- | `OMEGA_TEST_FIREBASE_ADMIN_KEY` | Nothing to move: delete the line. The seeded persona (#904) replaces the minted custom token |
471
- | `OMEGA_TEST_USER_UID` | Nothing to move: delete the line. The emulator's roster names the persona (#904) |
472
-
473
- ## The .env cascade (secrets) — D15
474
-
475
- Secrets mirror the config hierarchy (`src/env.js`), weakest → strongest:
476
-
477
- ```
478
- company .env ← brand .env ← local .env ← shell env
479
- ```
480
-
481
- Every layer is TWO files: its `.env`, and the `.env.<environment>` overlay that wins
482
- over it.
483
-
484
- ```
485
- .env ← .env.development | .env.testing | .env.production
486
- ```
487
-
488
- - **Same walk as the config cascade**: `{brand}/targets/{target}` layers the brand root's `.env`
489
- under the target's; a brand naming a company (`company: { id }`) layers that company's
490
- `company/.env` underneath that (#677). `findBrandRoot` in `load.js` is the ONE definition
491
- of the walk; both cascades use it.
492
- - **`.env.<environment>` overlays the `.env` beside it**
493
- ([#586](https://github.com/Omega-JS-Stack/omega/issues/586)) — the widespread standard
494
- (Next.js, Vite, Rails dotenv, dotenv-flow). The three names are exactly what
495
- `envEnvironment()` returns (`development` | `testing` | `production`), so the file name
496
- and the runtime's own answer are ONE vocabulary; only the RUNNING environment's overlay
497
- is read, and every key in it is equal — whatever it holds wins, values are TRUSTED, and
498
- no key gets special treatment. The composed artifact is flat and single-environment: a
499
- deploy composes base + production, the emulator base + development, a test lane base +
500
- testing, and no other environment's file ever rides along. Onboard scaffolds all three
501
- beside the brand `.env`, empty but for a header comment (`.env.*` is gitignored).
502
- - **A key of your own in the brand root `.env` reaches every target**
503
- ([#835](https://github.com/Omega-JS-Stack/omega/issues/835)). The schema filters the
504
- company and brand layers by TARGET, and a key it never declared names no target, so it
505
- composes everywhere (base `.env` and `.env.production` alike) and rides the same pipeline
506
- to GitHub Actions as every declared one. Want it on one target only? Put it in that
507
- target's own `.env`, the per-key override that was always unfiltered.
508
- - **Precedence via dotenv's no-override semantics**: files load strongest-first and never
509
- overwrite keys already set, so the shell always wins and local beats brand beats company.
510
- - **A RELOAD honors edits, because ownership is remembered**
511
- ([#724](https://github.com/Omega-JS-Stack/omega/issues/724)). After a boot load every key
512
- is "already set", so presence can no longer tell a shell value from a file value — which
513
- is why `loadEnv` alone can never deliver an edit. The first chain load in a process
514
- snapshots what `process.env` carried before any file was read (shell-owned, forever) and
515
- records what each file layer delivers (file-owned). `reloadEnv(startDir, options?)` drops
516
- the file-owned keys, then loads again: a NEW key and an EDITED value both land, a key
517
- dropped from the file is dropped from the process, and a shell-set value is never touched.
518
- The dev lanes' `.env` watchers ([#681](https://github.com/Omega-JS-Stack/omega/issues/681))
519
- are its one caller.
520
- - **Empty file values never claim a key (cp95a, friction #20)**: `KEY=` / `KEY=""` in any
521
- `.env` FILE means "documented here, value supplied by another layer" — a scaffolded local
522
- file full of placeholders can't shadow the brand root's real values. Only the shell can
523
- deliberately set a key to empty. The brand root's `.env` stub ships `# KEY=` commented
524
- placeholders, rendered from the env schema (the merge protocol keeps set values on their
525
- line, converges empties to the placeholder); no framework scaffolds a target `.env` at all.
526
- - **Defined at the source, resolved at runtime/build**: a brand-wide `GH_TOKEN` lives once
527
- in the brand `.env`; every framework CLI/build resolves the chain at boot
528
- (`loadEnv(process.cwd())` in the web/desktop/extension CLIs + gulp pipelines,
529
- `loadEnv(functionsDir)` in the @omega.js/backend CLI and runtime). Nothing is copied
530
- between `.env` files just to be visible.
531
- - **The brand root's `.env` is the ONE file humans and the manager edit**
532
- ([#678](https://github.com/Omega-JS-Stack/omega/issues/678)). A target's own `.env` is
533
- optional and overrides PER KEY, by hand; no machine ever writes one.
534
- - **Backend's local layer is the target-root `.env`** (`targets/backend/.env`) — the layer a
535
- human uses to override one key for that surface. What physically ships is the STAGED
536
- `dist/.env`: it rides the Firebase deploy artifact (the cloud can't walk up), so every verb
537
- that produces one (`omega build`, `dev`, `test`, `deploy`) composes it from the file layers,
538
- filtered by the env schema. In the cloud the walk finds no brand/company and behavior
539
- is identical to plain dotenv.
540
- - **The brand-generated keys are the manager's to mint** — `OMEGA_ADMIN_KEY`,
541
- `OMEGA_WEBHOOK_KEY`, `OMEGA_NAMESPACE` and `UNSUBSCRIBE_HMAC_KEY` have no dashboard
542
- behind them, so the onboard stub writes them for a fresh brand and the workspace
543
- service's `env-keys` step mints any the cascade doesn't serve on every manage
544
- ([#569](https://github.com/Omega-JS-Stack/omega/issues/569)). The env schema below is
545
- the one list feeding both, a company-served value is never shadowed by a brand-level
546
- one, and nothing prints a minted value.
547
- - Missing files and unreadable/stale markers skip silently — `loadEnv` never throws for
548
- an absent layer.
549
-
550
- ## The env schema (`src/env-schema.js`) — #581
551
-
552
- The omega.json5 schema's sibling: ONE inventory of the env keys OMEGA needs — who owns
553
- each, which targets read it, whether OMEGA mints it or a human pastes it from a third
554
- party, whether it is required, and what it does. Everything that used to hand-keep its
555
- own list derives from it, so a new key is **one entry**, never four edits.
556
-
557
- ```js
558
- {
559
- name: 'OMEGA_ADMIN_KEY', // the env var (SCREAMING_SNAKE)
560
- match: /^CONNECTIONS_.+$/, // …or a pattern, for dynamic families
561
- owner: 'workspace', // the manager service that owns it
562
- // ('backend' = the framework itself)
563
- targets: ['backend'], // the targets whose runtime READS it
564
- group: 'omega', // its ENV_GROUPS bucket (.env file order)
565
- generated: () => randomBytes(32)…, // the function that MINTS a value
566
- default: 'value', // …or a static default, where one applies
567
- secret: true, // never printed, never in omega.json5
568
- required: true, // absent = the backend refuses to boot
569
- delivery: { backend: 'env' }, // per target, HOW the value gets there
570
- requiredWhen: 'captcha.providers…', // non-empty when this config path is truthy
571
- publicAtRest: true, // sanctions a 'bake' (readable in the artifact)
572
- machineLocal: true, // this machine's fact — never published to CI
573
- label: 'Snap Store credentials', // the human name an ask opens with
574
- url: 'https://snapcraft.io/account', // the page that MINTS it
575
- hint: 'Run `snapcraft export-login -`', // how to get it there
576
- description: 'What the key drives.',
577
- }
578
- ```
579
-
580
- - **`label`, `url` and `hint` are the human half, and this schema is their one home**
581
- ([#867](https://github.com/Omega-JS-Stack/omega/issues/867)). The manager's REQUIRES
582
- registry used to carry a second copy per service, which is how every desktop signing key
583
- and every extension store key ended up with a mint page in NEITHER place: no service
584
- declared them, so nobody could be asked for them. `serviceInputSpec` fills each ask from
585
- here now, and the registry keeps only its own fields (which service asks, whether the ask
586
- is interactive, where Disable writes `false`). `url: null` is a deliberate statement that
587
- no page mints the value, and such an entry owes a `hint` saying where it does come from.
588
- The sweep test (`packages/manager/test/service-input.test.js`) holds the line: no
589
- label/url/hint in the registry, a label on every pasted key, and a url (or an explicit
590
- null plus a hint) on every one that is not machine-local.
591
-
592
- - **`generated:` is the mint switch.** Those keys have no dashboard behind them, so the
593
- manager writes them into a brand `.env` — at onboard, and on every manage that finds
594
- one missing. Everything else is a credential a human provides.
595
- - **Only a key OMEGA can produce may be `required`.** Refusing every boot over a secret
596
- nobody can mint would be a hostage note, not a guard — the config test pins it.
597
- - **`targets:` is the composition domain.** Every verb composes its target's RUNTIME env
598
- (the backend's staged `dist/.env` — the only artifact that ships and so cannot walk up to
599
- the brand layer) from the file layers, taking the entries whose `targets` name that
600
- target. The schema is the only filter on DECLARED keys: no hand list, and PATTERN entries
601
- (`match:`, e.g. the `CONNECTIONS_*` family) compose exactly like named ones. A key the schema
602
- names for other targets never reaches this one, so a desktop signing key stays out of the
603
- functions upload. A key the schema does not know AT ALL is the consumer's own
604
- ([#835](https://github.com/Omega-JS-Stack/omega/issues/835)): it names no target to be
605
- filtered by, so a custom line in the brand root `.env` (or its `.env.production` overlay)
606
- composes for EVERY target, and a consumer who wants one on a single target puts it in
607
- that target's own `.env`, which passes unfiltered anyway. Other targets read brand values through the cascade above at runtime,
608
- so nothing is written for them.
609
- - **`deliverAs:` renames on delivery**: the entry's brand-level name is what the cascade
610
- carries (`GOOGLE_ANALYTICS_SECRET_BACKEND`), and the target receives it under the name
611
- its own code reads (`GOOGLE_ANALYTICS_SECRET`). One entry, both names.
612
- - **`delivery:` says HOW a value reaches each target**
613
- ([#627](https://github.com/Omega-JS-Stack/omega/issues/627)): `'env'` (read from the
614
- composed `.env` at runtime — the backend), `'ci'` (the generated workflow injects it
615
- into the runner env for the build step), or `'bake'` (the build writes it into the
616
- shipped artifact, because the installed app runs with no `.env`). A bake implies the
617
- CI injection — the workflow delivers the value the build then bakes. One renderer in
618
- `@omega.js/config/env-delivery` derives everything from these declarations: each
619
- target's workflow secrets block, its bake list, and its publish-step secret set. No
620
- hand-kept `${{ secrets.KEY }}` list survives anywhere.
621
- - **The delivered set is the schema PLUS what only the composed env can name**
622
- ([#835](https://github.com/Omega-JS-Stack/omega/issues/835),
623
- [#876](https://github.com/Omega-JS-Stack/omega/issues/876)). `deliveredKeys(target, modes,
624
- { values })` is the ONE primitive every list derives from, and the composed production
625
- values it takes add two kinds of key the schema cannot name: a `match` FAMILY member (the
626
- schema knows `CONNECTIONS_<PROVIDER>_CLIENT_ID` as a shape, so only a brand's own `.env`
627
- says which providers exist), and a CUSTOM key the schema never declared. A custom key
628
- travels in its target's FILE mode: on the backend it is written into the `.env` the runner
629
- builds, like an `env` delivery; on web, desktop and the extension it reaches the runner env
630
- only, like a `ci` one. `machineLocal` keys and declared keys this target does not deliver
631
- stay home exactly as before, and neither a `WORKFLOW_OWNED_KEYS` name (the template
632
- declares those itself) nor a `GITHUB_`-prefixed one (GitHub refuses that prefix as a
633
- secret name) is ever taken from a composed set. Both halves of the backend workflow (the
634
- injected block and the `.env` writer's key list) render from that one set, so they cannot
635
- disagree inside a run.
636
- - **`env` on a NON-backend target declares the laptop's composed `.env` as the channel**
637
- ([#819](https://github.com/Omega-JS-Stack/omega/issues/819)). The workflow renderer walks
638
- `ci` and `bake` only, so such a key renders no workflow line, reaches no runner and is
639
- published as no repo secret: it is read where the developer runs the build.
640
- `OPENAI_API_KEY` is the case that made it explicit (`{ web: 'env', backend: 'env',
641
- extension: 'env' }`), because translation runs locally and CI reads the committed cache
642
- ([#905](https://github.com/Omega-JS-Stack/omega/issues/905)).
643
- - **A workflow block carries `ci` + `bake`, except the backend's, which carries `env` too**
644
- ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)). Everywhere else the artifact holds its values inside itself and reads
645
- no env file, so the CI half is the whole block. The backend's deployed artifact ships a
646
- COMPOSED `.env`, and its deploy runs on a runner with no brand checkout to compose one
647
- from, so its workflow writes that file out of the runner env: every `env` delivery has to
648
- be up there to be written. `WORKFLOW_MODES` in `env-delivery.js` is the one home of that
649
- exception; `DEFAULT_WORKFLOW_MODES` is every other target. Riding with it is
650
- **`OMEGA_SERVICE_ACCOUNT_JSON`** (`delivery: { backend: 'ci' }`), the deploy credential as
651
- the key file's own CONTENTS: it is a FILE on every other lane (minted into the brand's
652
- `.omega/secrets/`, staged into `dist/`), so it renders no brand `.env` line, and the
653
- backend's precheck values it from that authored chain instead of from a composed env.
654
- Riding beside it: **`OMEGA_LICENSE_KEY`** (`ci` on all four targets now), because the
655
- license check runs inside the deploy and the backend's deploy runs on a runner too.
656
- - **`ci` is what keeps a key out of an artifact's `.env`.** The two lanes that write a
657
- backend `.env` read that one declaration: `envFileKeys('backend')` (the generated key
658
- list the workflow's writer reads out of the runner env) takes the `env` deliveries only,
659
- and `artifactEnvValues('backend', values)` strips every `ci` key out of the COMPOSED
660
- values before the stage serializes `dist/.env`. The composer itself resolves a value for
661
- everything the target claims, `ci` included, because the secrets publisher has to value
662
- what it publishes from the same cascade as everything else: the artifact is the narrower
663
- half, and those two functions are where that is said.
664
- - **A baked key is public at rest.** Anyone who unpacks the app can read it, so a
665
- `secret: true` entry may only bake when it also declares `publicAtRest: true` — the
666
- renderer THROWS otherwise, on every lane, so a real credential can never reach an
667
- artifact by accident. The GA Measurement Protocol secrets are the sanctioned baked keys.
668
- - **`machineLocal: true` marks this machine's own facts** (`OMEGA_FONTAWESOME_ROOT`):
669
- composed locally like any key, but filtered out of every rendered block and every
670
- published secret set — a laptop path has no business in CI.
671
- - **`requiredWhen: '<config path>'` is the conditional presence rule**
672
- ([#626](https://github.com/Omega-JS-Stack/omega/issues/626)): when the resolved config
673
- path is truthy, the key must be non-empty. Presence only, never a value-shape check,
674
- and one-directional. One checker, `checkEnvRules()` in `@omega.js/config/env-rules`,
675
- answers it for every consumer: the backend boot (production refuses, development warns
676
- once), the desktop and extension build bakes (build mode throws, development warns),
677
- and the manager's manage walk (warns per enabled target, never fails). A TARGET-LESS
678
- entry (`targets: []` — a SERVICE's own key, like `SENTRY_AUTH_TOKEN`) is owed when its
679
- path is truthy in the brand root **or in ANY enabled target's resolved config**
680
- ([#683](https://github.com/Omega-JS-Stack/omega/issues/683)): the service writes its
681
- values where the target lives — monitoring lands one Sentry DSN per surface and leaves
682
- the shared slot null — so a root-only read never fired on the shape brands carry.
683
- Violations name the BRAND-level key — the one a human sets in the brand `.env`.
684
- - **Nothing is CLEARED, because nothing is written into a hand file**
685
- ([#636](https://github.com/Omega-JS-Stack/omega/issues/636),
686
- [#678](https://github.com/Omega-JS-Stack/omega/issues/678)): the composed artifact is
687
- rebuilt from the cascade every verb, so a credential the brand root retires stops being
688
- served the moment it is dropped.
689
- - **`group:` picks the .env section**, and `ENV_GROUPS` owns the file order plus each
690
- section's comment. A group marked `file: false` (the `runtime` group) never reaches a
691
- brand `.env` at all — those keys resolve some other way (from config at boot, from the
692
- developer's own shell), so they are neither rendered as placeholders nor composed.
693
- - **Runtime/platform vars are NOT in the schema**: `FIREBASE_CONFIG`,
694
- `FUNCTIONS_EMULATOR`, `GCLOUD_PROJECT`, the `OMEGA_*_PORT` map, the test-mode
695
- switches. They are the runtime's facts about itself, not a brand's credentials, and
696
- the frameworks read them directly.
697
-
698
- ### One key, one entry — the environment supplies the value — #586
699
-
700
- A key whose value must differ between a local run and a deployed one is **not a second
701
- schema entry**. Public payment keys already split per machine through the config merge
702
- chain's local layer; secrets now split the same way through the `.env` cascade's
703
- environment overlay above — the brand puts the test credential in `.env.development`
704
- under the SAME name. The backend uses whatever key the env chain resolves, nothing more:
705
- the rule, and the `.env.development` advice that follows from it, live in
706
- [docs/backend/index.md](../backend/index.md) (the payment-keys paragraph).
707
-
708
- The schema declares WHAT a brand supplies, never which environment supplies it: `env.js`
709
- owns that. So `STRIPE_SECRET_KEY`, `PAYPAL_CLIENT_SECRET`, `CHARGEBEE_API_KEY` and
710
- `COINBASE_COMMERCE_API_KEY` (asked for through the setup contract, #608, only when
711
- `payment.providers.coinbase.enabled` is on) are one entry each, every provider library
712
- reads the one name, and there is no `<KEY>_DEV` twin, no live-shape guard, and no
713
- payment-specific rule anywhere (ruled 2026-08-26; the twin system was replaced before it shipped, so
714
- nothing migrates).
715
-
716
- ### One key per AI provider — #639
717
-
718
- `OPENAI_API_KEY` and `ANTHROPIC_API_KEY`. There is no second name for either.
719
-
720
- | Key | Owner | Read by | Asked at |
721
- |---|---|---|---|
722
- | `OPENAI_API_KEY` | `backend` | Contact inference, content + newsletter generation, the `chatgpt` translation provider | `manage` — the `ai` service's setup gate |
723
- | `ANTHROPIC_API_KEY` | `backend` | The backend's Anthropic provider (SVG generation, tool loops) | `manage` — the `ai` service's setup gate |
724
-
725
- - **The company-wide fallback is the COMPANY LAYER, never a second key** (Ian
726
- 2026-08-27). The legacy pair (`BACKEND_MANAGER_OPENAI_API_KEY`, then
727
- `OMEGA_OPENAI_API_KEY`) existed so one company key could serve every brand; the
728
- `.env` cascade already does that — put the value in the company `.env` and every
729
- brand under it resolves it, with a brand `.env` overriding. The prefixed names are
730
- gone from the schema and every reader; migration row in
731
- [breaking-changes.md](breaking-changes.md).
732
- - **Both are optional and neither gates a run.** The `ai` service declares them
733
- `gates: false`, so preflight never blocks on them and a brand that calls one
734
- provider is never nagged about the other. `ai.enabled: false` (what the gate's
735
- Disable lands) stops the ask for good.
736
-
737
- Who derives from it:
738
-
739
- | Lane | What it takes |
740
- |---|---|
741
- | `@omega.js/manager` workspace `env-keys` + the onboard `.env` stub | `generatedEnvKeys()` — name → the function that mints a value |
742
- | `@omega.js/manager` `lib/env-order.js` (canonical .env order) | `envFileGroups()` + `envKeysByGroup()` — the sections, their comments, their keys |
743
- | `@omega.js/config` `composeTargetEnv()` (the delivery composition every verb runs) | `ENV_SCHEMA` + `envFileGroups()`: a DECLARED brand key rides down when its entry claims it (by `name` or by `match`), its `targets` include the target, and its group renders into a file; a key no entry knows at all is the consumer's own and rides down to every target ([#835](https://github.com/Omega-JS-Stack/omega/issues/835)); `deliverAs` is applied on arrival, and each layer's `.env.<environment>` overlay composes above its own base (#586) |
744
- | `@omega.js/config` `envKeysForTarget(target)` (the rendering lane's list) | `ENV_SCHEMA` + `envFileGroups()` — the NAMED keys a target reads, which placeholders a brand `.env` carries |
745
- | `@omega.js/backend` `libraries/env.js` (the one reader) | `envSchemaEntry()` for every read, `requiredEnvKeys('backend')` for the boot guard, and `getEnvironment()` re-exported under its own name ([docs/backend/index.md](../backend/index.md)) |
746
- | `@omega.js/manager` `lib/scaffold.js` (the onboard stub) + `lib/gitignore.js` (the heal) | `ENV_ENVIRONMENTS` — one empty `.env.<environment>` per name, and the `.env.*` ignore |
747
- | `@omega.js/config` `env-delivery.js` (the one delivery renderer) | `delivery` + `deliverAs` + `machineLocal` + `publicAtRest`, plus the target's COMPOSED production values for the keys the schema cannot name (`match` families and custom keys, #835/#876): each target's workflow secrets block, bake list, and publish-step secret set; web and extension render their workflow token from it, desktop's ensure-target template pass does the same, backend's composed `deploy.yml` renders both its secrets block and the KEY LIST its node `.env` writer reads out of the runner env ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)), and all secret publishers send exactly its set |
748
- | `@omega.js/config` `env-rules.js` (the one presence checker) | `required` + `requiredWhen` — the violations the backend boot, the desktop/extension bakes, and the manager's manage walk act on, each at its own severity |
749
-
750
- ## The environment (`src/environment.js`), #817
751
-
752
- `src/environment.js` is the ONE environment module every OMEGA target answers
753
- from ([#817](https://github.com/Omega-JS-Stack/omega/issues/817)). Four calls,
754
- one implementation, one call form everywhere: `getEnvironment()` plus
755
- `isDevelopment()` / `isProduction()` / `isTesting()`, mixed into a framework's
756
- Manager with `attachTo()` (exported from the package index as
757
- `attachEnvironment`). It requires NOTHING, so it is bundled into browser
758
- artifacts (the desktop renderer, every extension bundle) and vendored into
759
- `@omega.js/client`, which reaches the same four through its own methods.
760
-
761
- **ONE INPUT.** On Node it is `process.env.OMEGA_ENVIRONMENT`. In a browser
762
- context, which can read neither env nor files, it is `config.environment`, the
763
- build fact every surface already bakes into `OMEGA_BUILD_JSON.config`
764
- ([#896](https://github.com/Omega-JS-Stack/omega/issues/896)), reached as
765
- `this.config.environment` off the Manager the call is made on. Nothing else is
766
- consulted: no `app.isPackaged`, no `manifest.update_url`, no `NODE_ENV`, no
767
- terminal sniffing.
768
-
769
- **NO DEFAULT.** A context with no input throws and names the variable. The four
770
- framework copies this replaced each had their own default and they DISAGREED:
771
- desktop answered `production` with no signal while the extension answered
772
- `development`, so a desktop `npm start` bundled itself as a production artifact.
773
- @omega.js/client seeded `environment: 'production'` into its own config defaults,
774
- and web's browser Manager defaulted to `development`.
775
-
776
- **`setEnvironment(value)` is the only writer**, so a fourth word can never reach
777
- the variable. Who calls it, and with what:
778
-
779
- | Lane | Names |
780
- |---|---|
781
- | `@omega.js/backend`'s boot (`Manager.init`, right after the `.env` cascade loads) | `envEnvironment()`, the AMBIENT answer (below) |
782
- | `@omega.js/desktop` / `@omega.js/extension` `src/build.js`, at load | `buildLaneEnvironment(Manager.isBuildMode())`: `production` under `OMEGA_BUILD_MODE` (which WINS over an inherited value, so a production build spawned from a test run still bakes production), else an already-named value, else `development`. That rule is ONE exported helper beside `setEnvironment`, not a copy of the expression per framework, and those two build lanes are its only callers |
783
- | `@omega.js/desktop`'s main process, at `initialize()` | the `environment` its baked config carries, for a packaged app that has no parent lane |
784
- | `@omega.js/web`'s verbs | `production` for `omega build`, `development` for `omega dev`, `testing` for `omega test` |
785
- | the desktop test runners, the extension `test` verb | `testing` |
786
-
787
- `envEnvironment()` (in `src/env.js`) is the PRODUCER of that input, never a
788
- second reader of it: it is the sniff a lane runs ONCE to decide what to set, and
789
- the default for the two overlay selections (`.env.<environment>` and
790
- `config/omega.<environment>.json5`). An already-set `OMEGA_ENVIRONMENT` wins
791
- inside it too, so the producer and the reader can never disagree in one process.
792
-
793
- ## Owner hooks (`config/hooks/`) — cp91
794
-
795
- `src/hooks.js` — owner-supplied code the frameworks call at named hook points, so
796
- company-specific logic lives in the OWNER'S tree, never in framework source. Layout is
797
- **nested, mirroring the call site** (Ian's directive): the account service's password
798
- step loads `config/hooks/account/password.js`; a future onboarding hook would live under
799
- `config/hooks/onboard/…` — one file per hook point, path = the invoking structure.
800
-
801
- - **Home = `config/`, versioned by default** (Ian 2026-07-11): hooks are AUTHORED code
802
- and sit with the other owner-authored omega inputs (omega.json5, seo.json5, chatsy.md,
803
- …) — never under machine-owned, gitignored `.omega/`, where a hook lost on a fresh
804
- clone would silently change behavior (passwords falling back to the seed channel and
805
- rotating). Secrets still belong in `.env` — a hook that needs one reads `process.env`;
806
- to keep a hook out of git anyway, add your own `config/hooks/` ignore line.
807
- - **Resolution order**: the brand root's own `config/hooks/<point>.js`, else the company
808
- tree's `company/config/hooks/<point>.js` (via `company: { id }`, #677): a company-wide
809
- hook covers every brand, a single brand can still override it. It is the ONE inheritance
810
- rule applied to one more relative path, and costs the hook loader no code of its own.
811
- - **Contract**: plain CJS, `module.exports = ({ … }) => …` (async fine). Each call site
812
- documents its hook's signature/return. Absent hook → `loadHook` returns null and the
813
- caller uses its default behavior; a hook that EXISTS but is broken (unloadable,
814
- non-function export, bad return) throws — an owner who wrote a hook never gets silent
815
- fallback.
816
- - **First (and so far only) hook point**: `account/password` —
817
- `({ email, domain, apex, brand }) => password` (string ≥ 6 chars), letting a company
818
- formula generate per-brand passwords without ever living in a repo the framework ships.
819
-
820
- ## Port auto-allocation (N7)
821
-
822
- `src/ports.js` — classic defaults, probe at boot, per-port +1 bump only when taken, so
823
- multiple brands run dev stacks concurrently. Single-brand dev with free defaults is
824
- byte-identical to the pre-N7 behavior (no bumping, no artifacts).
825
-
826
- - **`CLASSIC_PORTS`** — the historical defaults (functions 5001, hosting 5002, firestore
827
- 8080, auth 9099, database 9000, storage 9199, pubsub 8085, ui 4050, website 4000,
828
- livereload 35729, cdp 9222). **This map is the ONLY source of those numbers**
829
- ([#834](https://github.com/Omega-JS-Stack/omega/issues/834)): @omega.js/client
830
- carried four of them plus a classic dev origin, @omega.js/desktop's url-helpers
831
- and client-bridge three more, and @omega.js/extension's url-helpers and
832
- background worker two, each as a last-resort fallback "for a build made with no
833
- stack up". Every one of those is gone. The numbers reach a browser exactly one
834
- way: each surface bakes the map into its build json with the classics as the
835
- FLOOR and the live stack's resolved (possibly bumped) numbers over them, so a
836
- dev artifact always carries a COMPLETE map and the classic dev ORIGIN rides it
837
- the same way. A read that finds no map throws `devFactMissing()`
838
- (`src/dev-facts.js`, the one home of that error), naming the fact it wanted and
839
- the build step that writes it: a missing map is a broken build, and nothing
840
- anywhere assumes a port again. Nothing identity-checks what answers on a
841
- classic port, which is why an assumed one failed as an auth mystery or a silent
842
- connection refusal rather than as a port problem.
843
- - **`resolvePorts({ wanted, pins, claimed })`** — each wanted port keeps its value when
844
- free, bumps +1 until free when taken (shared `claimed` set prevents two names landing
845
- on one port). `pins` (the config `ports` section) never bump — a busy pin throws.
846
- The free-check is a QUADRUPLE bind-probe (127.0.0.1, ::1 when the host has IPv6,
847
- the IPv4 wildcard 0.0.0.0, and the `::` wildcard) — on macOS/BSD, wildcard and
848
- specific-address listeners COEXIST on one port, and the two wildcard FAMILIES
849
- coexist with each other, so any single-surface probe false-positives against a
850
- sibling brand's stack and the real bind crashes later (found live at cp186: the
851
- playground's https proxy holds IPv6 `*:5002`; a 127.0.0.1-only probe handed 5002
852
- to the second brand's functions emulator — and again at #345: a foreign `0.0.0.0`
853
- squatter read free to the `::` probe and the auth emulator died with no bump).
854
- - **Ports file** — `writePortsFile(projectDir, ports, facts)` /
855
- `readPortsFile/clearPortsFile(projectDir)`: `<projectDir>/.temp/ports.json`
856
- (pid-stamped; readers ignore dead-pid leftovers). The allocator (the backend emulator
857
- boot) writes it; siblings of the same brand (`omega test` against a running emulator,
858
- the e2e harness) read it; cleared on clean shutdown. `facts` publishes the resolved
859
- NON-port facts beside the map — today `origin`, the website's dev origin
860
- ([#262](https://github.com/Omega-JS-Stack/omega/issues/262)).
861
- A consumer process targets `https`, the PUBLIC origin under the local certificate,
862
- and never `hosting`, the internal plain-http port the mkcert proxy forwards to; every
863
- `omega dev` leg trusts that certificate through `NODE_EXTRA_CA_CERTS` ([#795](https://github.com/Omega-JS-Stack/omega/issues/795)).
864
- - **Sibling map** — `readSiblingPorts(targetDir)` merges every OTHER target's live ports file
865
- in the same brand (a running backend's emulator map) for the target that asks;
866
- `readSiblingOrigin(targetDir)` reads the published dev website origin the same way. Read
867
- at USE time, never cached: the file appears when the backend boots and changes when it
868
- restarts.
869
- - **Env channel** — `portsToEnv(ports)` → `OMEGA_<NAME>_PORT` vars injected into spawned
870
- children; `envPort(name)` reads one, `envPorts(env)` reads the whole map back out.
871
- URL getters resolve env → classic default.
872
- - **Browser channel (cp89, [#300](https://github.com/Omega-JS-Stack/omega/issues/300))** —
873
- browser code can read neither env nor files, so a surface BAKES the map into its
874
- client config, under the same key on all three (`OMEGA_BUILD_JSON.config.dev`, #894):
875
- `omega dev` writes that line into the page chrome
876
- PER RENDER (its resolved website port with the sibling backend's map merged over it),
877
- and its auth-emulator proxy resolves the target port per REQUEST. The render-time
878
- bake is ADVISORY ([#346](https://github.com/Omega-JS-Stack/omega/issues/346)): the
879
- dev server REWRITES that chrome in every HTML response as it serves it, resolving the
880
- sibling maps per request, because the normal boot order builds the whole page fleet in
881
- under a second while the emulator suite seeds for minutes — nothing under `src/`
882
- changes when it lands, so no page would ever re-render onto it. Mid-session emulator
883
- restarts onto bumped numbers ride the same lane. Built `dist/` output is untouched on
884
- disk. Desktop (its renderer bundle) and extension (every bundle) bake the same map at
885
- the same key at build time, from the sibling file plus the env channel; production
886
- builds bake none. Drivers serving a
887
- STATIC build set `window.__OMEGA_DEV_PORTS__` (the devkit e2e harness — the site
888
- builds before the emulator boots), which is a FALLBACK: it fills only what a page's
889
- chrome omits, so a side channel no real browser has can never hide a broken real one.
890
- The dev WEBSITE ORIGIN rides the same map as one more resolved fact
891
- ([#262](https://github.com/Omega-JS-Stack/omega/issues/262)): `omega dev` publishes
892
- `dev.origin` (protocol AND port — the mkcert proxy fronts the public port by default,
893
- so a port number alone cannot say the scheme) into the chrome and into its ports file,
894
- and desktop/extension bake it from that file on their existing lanes, with
895
- `CLASSIC_DEV_ORIGIN` as the floor under it (#834) so the key is always present.
896
- The extension manifest's `externally_connectable` dev entry resolves from it at
897
- package time; nothing hardcodes a dev origin any more.
898
- `@omega.js/client`'s `getDevWebsiteOrigin()` is the one getter that answers it,
899
- and it THROWS when the artifact carries none rather than assuming
900
- `https://localhost:4000`, because a wrong dev origin fails as a silent
901
- connection refusal.
902
- `@omega.js/client` resolves chrome `dev.ports` → runtime global, and THROWS when
903
- neither answers ([#834](https://github.com/Omega-JS-Stack/omega/issues/834)):
904
- there is no classic-defaults step under them any more, and no
905
- "assuming the classic …" warning, because nothing is assumed. Its dev
906
- `getApiUrl` speaks plain http to a mapped `hosting` (the emulator serves http)
907
- and https to a mapped `https` (`mgr serve`'s mkcert proxy); neither mapped is
908
- the same loud error. Dev mode
909
- resolves the LOCAL stack for every source, including `source: 'company'` —
910
- `company.url` is a production concept, and dev deliberately makes no live server
911
- hits (ratified, Ian 2026-08-03, [#34](https://github.com/Omega-JS-Stack/omega/issues/34)).
912
- - **Website port (cp89)** — `omega dev` allocates through the same model: classic
913
- **4000** (pre-N7 it defaulted to 8080, colliding with the SAME brand's firestore
914
- emulator), bump when taken, `--port` flag or config `ports.website` pins; publishes
915
- its own ports file in the website target dir.
916
- - **Serve + per-target ports (cp90)** — `mgr serve` allocates through the same model
917
- (`--port` pins, taken bumps — the old kill-the-incumbent check is gone) and PUBLISHES
918
- its map: `https` (the mkcert proxy) + `hosting` (the internal plain-http
919
- firebase-serve port), so `omega dev` bakes even a bumped serve into the chrome and
920
- Stripe webhook forwarding targets the port that actually speaks http (it used to aim
921
- plain http at the TLS proxy). Desktop + extension `serve` allocate `livereload` — two
922
- targets of one brand land on distinct ports — and desktop allocates `cdp` when
923
- requested (`OMEGA_CDP_PORT` set); desktop URL getters mirror the backend's
924
- env-channel reads (`https` → mkcert, `hosting` → plain http, classic otherwise).
925
- The manager's Google-OAuth loopback binds an EPHEMERAL port (`listen(0)`, RFC 8252)
926
- instead of pinning 9876. The backend's `getWebsiteUrl` returns
927
- `http://localhost:4000` unless its own mkcert proxy is up (plain http on the public
928
- port 307s to https, so the link lands either way); desktop's reads the whole dev
929
- ORIGIN instead (baked `dev.origin`, then `OMEGA_WEBSITE_PORT` composed over https,
930
- then the classic `https://localhost:4000`), matching the browser-side answer scheme
931
- and all ([#747](https://github.com/Omega-JS-Stack/omega/issues/747)). The BROWSER-side answer
932
- is `getDevWebsiteOrigin()`, which needs the exact origin and takes it from the
933
- resolved map (#262). Packaged
934
- extension/desktop artifacts keep BUILD-TIME-BAKED ports by design (a shipped
935
- extension can't probe); the extension manifest's dev-website origin documents that
936
- inline.
937
- - **Config `ports` section** (schema, optional object) — explicit pins for any port name;
938
- unset = auto-allocate.
939
- - When a boot bumps emulator ports, the backend CLI materializes
940
- `firebase.resolved.json` next to firebase.json (same dir, so relative paths keep
941
- resolving) and boots firebase-tools with `--config`; the committed firebase.json never
942
- changes. Gitignored; removed on shutdown.
943
-
944
- Design + slice plan: [_attic/plans/archive/n7-port-allocation.md](../../_attic/plans/archive/n7-port-allocation.md).
945
-
946
- ## Validation
947
-
948
- `validateConfig(config, { target })` = shared schema + that target's refinements
949
- (`TARGET_SCHEMAS[target]`), run against the RESOLVED config. `brand.id` (URL-scheme-safe
950
- slug) and `brand.name` are the only universally required fields.
951
-
952
- **Undeclared keys WARN** ([#636](https://github.com/Omega-JS-Stack/omega/issues/636)):
953
- every leaf path of the resolved config no rule declares comes back as ONE warning naming
954
- them — never an error, because a brand config that outlives a framework version must still
955
- build. A rule of type `object`/`array` declares its whole subtree (a brand's postal
956
- address, an open provider map), and the `targets` namespace is exempt: those keys belong to
957
- a framework, or to a custom target. A finding is a hole to fill — either the key is dead,
958
- or the schema owes it a rule.
959
-
960
- **authDomain is the brand's own host** (cp268): when `cloud.config.authDomain` is set it
961
- must equal the BRAND host, `brand.url`, for every target a brand runs
962
- ([#588](https://github.com/Omega-JS-Stack/omega/issues/588)): one Firebase project, one
963
- backend, one authDomain. A target's own `url` is not read here (a
964
- `targets/admin` load would otherwise fail its own brand's authDomain), and the
965
- top-level `url` is only the fallback for a config carrying no `brand.url` at all. A
966
- `*.firebaseapp.com` value hard-fails (self-hosted `/__/auth/*` on the brand
967
- host is what keeps redirect sign-in working under browser storage partitioning; the web
968
- build emits those helper files), and any other mismatch fails naming both values. Absent
969
- passes, and `demo-*` (emulator-only) projects are exempt.
970
-
971
- **A product price is a bare NUMBER** ([#674](https://github.com/Omega-JS-Stack/omega/issues/674)):
972
- every entry of `payment.products[].prices` must be a number — `once: 49.99`, never
973
- `once: { amount: 49.99 }` — and the object shape is a config ERROR naming the product and
974
- the key. The two sides did not read it the same: the checkout page's resolver unwrapped
975
- `{ amount: N }` while the backend's confirmation URL and all three provider libraries took
976
- the bare number, so an object-shaped price rendered a correct order summary and reached
977
- the confirmation URL as `[object Object]`. There is no shared resolver to settle it in —
978
- the browser bundle cannot reach a build-time package, and the deployed backend runtime
979
- carries none either — so the SHAPE is settled here, in the one place both sides' catalog
980
- comes from. (`prices.amount` as a KEY is a different thing, a legacy one-time spelling the
981
- checkout still reads; its value is a number like every other.)
982
-
983
- ## The features catalog (`features`) and a product's values — #647
984
-
985
- A feature is **defined once**, at the top level, and a product names only its **value**.
986
- The two halves cannot disagree, because there is only one place a name, an icon or a
987
- definition can be written:
988
-
989
- ```json5
990
- {
991
- features: {
992
- saves: {
993
- name: 'Saves',
994
- icon: 'fa-solid fa-feather',
995
- definition: 'Notes, clips, and pages you can save per month.',
996
- usage: { pace: 'daily', mirror: ['teams'] },
997
- },
998
- templates: { name: 'Page templates', icon: 'fa-solid fa-palette', usage: { pace: false } },
999
- support: { name: 'Priority support', icon: 'fa-solid fa-headset', definition: 'Your tickets jump the queue.' },
1000
- },
1001
-
1002
- payment: {
1003
- products: [
1004
- { id: 'basic', name: 'Basic', features: { saves: 100 } },
1005
- { id: 'premium', name: 'Premium', features: { saves: 10000, templates: 40, support: true } },
1006
- { id: 'pro', name: 'Pro', features: { saves: -1, templates: 120, support: true } },
1007
- ],
1008
- },
1009
- }
1010
- ```
1011
-
1012
- ### The catalog
1013
-
1014
- | Key | Type | What it does |
1015
- |-----|------|--------------|
1016
- | `features.<id>.name` | string, **required** | The label every surface prints: pricing rows, the comparison matrix, the account's usage bars |
1017
- | `features.<id>.icon` | string | Font Awesome icon name ([icons.md](icons.md)) |
1018
- | `features.<id>.definition` | string | The dotted-underline tooltip. Authored ONCE — every card, row and bar renders this one |
1019
- | `features.<id>.usage` | object | Its presence makes the feature **counted** (metered per user). Absent = a **perk**, never counted |
1020
- | `features.<id>.usage.pace` | `'daily'` \| `false` | Day pacing is the DEFAULT. `false` opts out to a plain monthly counter |
1021
- | `features.<id>.usage.mirror` | string[] | Document KINDS this feature's counters also land on, resolved from `user.owns.<kind>` — declared here, never at a call site |
1022
-
1023
- **Key order is row order.** The pricing page's rows, the comparison matrix and the
1024
- account's usage bars all render the catalog in the order it is written.
1025
-
1026
- ### A product's values
1027
-
1028
- `payment.products[].features` is a MAP of `<catalog id>: value`:
1029
-
1030
- | The feature is | Its value is | Renders as |
1031
- |---|---|---|
1032
- | Counted | a number — the **monthly limit** | `100 Saves` |
1033
- | Counted | `-1` — unlimited | `Unlimited Saves` |
1034
- | Perk | `true` | a check, name only |
1035
- | Perk | a string | `24/7 Support` |
1036
- | Either | `false` (or absent) | nothing — the tier does not include it |
1037
-
1038
- The validator fails **a number on a perk** (it would draw a usage bar against a limit no
1039
- gate enforces) and **a perk value on a counted feature** (the gate would read it as zero,
1040
- so the plan advertises the feature and every call refuses it). It also fails a value on an
1041
- id the catalog does not define, because nothing reads it — the same silence a retired key
1042
- used to buy.
1043
-
1044
- ### What this replaces
1045
-
1046
- `payment.products[].limits`, the per-product `features` ARRAY, and the product-wide
1047
- `rateLimit` are **retired** — all three are validation errors naming their replacement.
1048
- The cross-product definition BACKFILL retires with them: nothing repeats, so nothing needs
1049
- unifying. The mapping is in [breaking-changes.md](breaking-changes.md).
1050
-
1051
- ### Top-level `usage` is reserved
1052
-
1053
- `usage` at the top level is reserved for **counting settings** (an anonymous-store mode, a
1054
- reset hour) and carries **no key today**. It is not a second home for the catalog. The
1055
- backend's gate reads `features`; how a user's counters behave is
1056
- [packages/backend/docs/usage-rate-limiting.md](../../packages/backend/docs/usage-rate-limiting.md).
1057
-
1058
- ## User connections (`connections`) — #771, #788, #792, #793
1059
-
1060
- Every key under `connections` is a PROVIDER a brand's users may link from the account page
1061
- — `google`, `discord`, `spotify`, `twitch`, `kick` ship with `@omega.js/backend`, and
1062
- any other one is a file the brand writes at `targets/backend/src/connections/<name>.js`
1063
- (the lane loads that directory before its own). So the section is free-form by design;
1064
- nothing here is required, and these are the keys an entry may carry:
1065
-
1066
- | Key | What it does |
1067
- |---|---|
1068
- | `enabled` | Whether the connection is offered on the account page. The five PACKAGED providers are `false` in the framework defaults — turning one on is the brand's act, since a card with no `CONNECTIONS_<PROVIDER>_CLIENT_ID` behind it could connect nothing. A brand's OWN provider is on unless this is `false` |
1069
- | `scope` | An array that WINS over the provider module's default scope |
1070
- | `name` | The card's title on the account page |
1071
- | `logo` | What the card draws: the NAME of a mark `@omega.js/web` ships (`core/logos/brandmarks/original/<name>.svg`, drawn inline), or a full URL (drawn as an `<img>`). A value carrying a `/` or a `:` is a URL; anything else is a mark name |
1072
- | `description` | The line under the title |
1073
-
1074
- Every key is a PROVIDER NAME, so it is strictly `[a-z0-9-]` — the same rule the
1075
- backend's confined loader enforces. A key outside it can never resolve to a
1076
- provider, and the account page says so on the card instead of offering a
1077
- connection that could only fail.
1078
-
1079
- **This section is the ONLY card list** ([#792](https://github.com/Omega-JS-Stack/omega/issues/792)):
1080
- every entry carrying a `name` and a `logo` renders a card, and the account layout's old
1081
- `connections:` frontmatter rows — which shadowed a brand's own entry and pointed at CDN
1082
- files that 404 — are gone. The framework's defaults carry `name`, `logo` and `description`
1083
- for the five packaged providers, so `google: { enabled: true }` is a complete card and any
1084
- key a brand writes wins through the ordinary merge chain. Those defaults RESOLVE only
1085
- (`materialize: false`, above): they are never copied into a brand's file. An enabled
1086
- provider with no `name` + `logo` after the merge gets the "unsupported connection" card,
1087
- which says exactly that.
1088
-
1089
- Secrets never live here: the credentials are the `CONNECTIONS_<PROVIDER>_CLIENT_ID` /
1090
- `CONNECTIONS_<PROVIDER>_CLIENT_SECRET` pair in the `.env` (the provider name uppercased,
1091
- dashes as underscores). The full provider contract — the module shape, the ONE context
1092
- every step takes, `pkce: 'S256'`, the route-owned identity uniqueness, and the `type` every
1093
- stored record carries — is `packages/backend/docs/connections.md`.
1094
-
1095
- ## The cancel-flow save offer (`payment.winback`) — #268
1096
-
1097
- When a customer starts cancelling a PAID subscription, the billing card pitches a
1098
- discount on the next cycle before it asks them why they are leaving. Accepting applies
1099
- the discount through the provider's own coupon plumbing, calls the cancel off, and
1100
- leaves the saving on the card (what comes off, and which bills it comes off — #325);
1101
- declining opens the cancellation questionnaire unchanged.
1102
-
1103
- The pitch is made per cancel ATTEMPT, never once per session (#324): a customer who
1104
- declines, closes the questionnaire and comes back to cancel meets the offer again,
1105
- because nothing about their subscription changed. Only a CLAIM (the discount is applied,
1106
- and the backend refuses a second one) or a refusal no retry fixes ends it.
1107
-
1108
- An accepted offer is recorded in TWO places, on purpose (#325). `payments-orders/{orderId}`
1109
- `.requests.winback` is the offer's MEMORY — what a second accept is refused against — and
1110
- the account carries the discount ITSELF at `subscription.discount`, shaped like every other
1111
- discount in the payment stack (`{ valid, code, percent | amount, duration }`) plus a
1112
- `source`. That is what the billing card renders on a later visit; without it the saving
1113
- disappeared on the next page load. `source` is the whole reason it is safe to read as a
1114
- claim: today only the winback claim writes this node, and `source` is what keeps the
1115
- read safe when checkout discounts start writing it too, because only
1116
- `source: 'winback'` says this customer already took the save offer.
1117
-
1118
- **The claim ends with the subscription it was made on (#333).** The account's node also
1119
- carries `resourceId`, the subscription the discount was applied to, stamped at claim
1120
- time. The unified webhook write carries no discount key, so a merge would otherwise keep
1121
- the node forever: a customer who churned and resubscribed carried a spent claim into the
1122
- NEW subscription, where `source: 'winback'` reads as "already claimed" and the save offer
1123
- is silently never pitched again. Every subscription-resource webhook compares the stamp against
1124
- the subscription the event is about and CLEARS the node on a mismatch (a new subscription
1125
- is a clean slate), while same-subscription traffic (renewals, cancellations, plan
1126
- changes) leaves the saving exactly as claimed. A node with no stamp predates it and
1127
- nothing can prove it belongs to an older subscription, so it is read as riding the one it
1128
- is found on and stamped there: no live discount is taken away on a guess, and it clears
1129
- on the next resubscribe like any other. CONSUMPTION is not cleared: a spent `once` coupon
1130
- still reads as applied until the subscription changes, because no provider's unified
1131
- shape reports whether the coupon is still attached (#333).
1132
-
1133
- The offer is the brand's, and **a brand that writes nothing gets one anyway**: 50% off
1134
- the next cycle, that cycle only.
1135
-
1136
- | Key | Type | Default | What it does |
1137
- |-----|------|---------|--------------|
1138
- | `payment.winback.enabled` | boolean | `true` | The whole off switch. `false` skips the pitch, and the cancel meets the data-retention warning instead (#341) |
1139
- | `payment.winback.percent` | integer 1-100 | `50` | Whole percentage off. Mutually exclusive with `amount` |
1140
- | `payment.winback.amount` | number > 0 | — | Flat amount off in `payment.currency`'s major unit (`10` = $10). Mutually exclusive with `percent` |
1141
- | `payment.winback.duration` | `'once'` \| `'forever'` | `'once'` | `once` discounts the next cycle only; `forever` is a permanent price cut |
1142
-
1143
- Setting **both** `percent` and `amount` is a validation error: a coupon is one shape or
1144
- the other everywhere in the payment stack, and two shapes on one offer has no honest
1145
- reading.
1146
-
1147
- `resolveWinbackOffer(payment)` is the ONE home of these defaults. The backend's
1148
- `POST /payments/winback` route resolves the brand's section through it, and the web
1149
- build bakes the same call into the client blob (`site.client.payment.winback`), so the
1150
- dialog the customer reads and the coupon the provider creates can never name different
1151
- numbers — the browser never applies a default of its own.
1152
-
1153
- Not every provider can discount a subscription that is already running. Stripe can
1154
- (the coupon plumbing the checkout already uses); PayPal has no discount object at all
1155
- and Chargebee has no way to reach a live subscription with one through existing
1156
- plumbing. Those two refuse with `not-supported-by-provider`, and the billing card
1157
- retires the offer for the session and opens the questionnaire — a subscriber can always
1158
- still cancel.
1159
-
1160
- ## Consent (`client.consent`) — #383
1161
-
1162
- The consent banner is a real GATE, so its config is schema-known even though the rest of
1163
- the `client` blob is not: a typo that silently disabled it would ship a site with no
1164
- consent surface and no error.
1165
-
1166
- ```json5
1167
- targets: {
1168
- web: {
1169
- client: {
1170
- consent: {
1171
- enabled: true, // default true; false ships NO banner
1172
- config: {
1173
- position: 'bottom-left', // bottom-left | bottom-right | bottom
1174
- content: {
1175
- message: 'We use cookies … See our { terms }.', // the banner face
1176
- panelIntro: 'We and our partners … See our { cookies } and { terms }.',
1177
- accept: 'Accept', // the big grant
1178
- customize: 'Customize', // opens the panel
1179
- acceptAll: 'Accept all', // the panel's pair
1180
- acceptNone: 'Accept none',
1181
- },
1182
- // `{terms}` and `{cookies}` link the terms and cookie-policy pages.
1183
- // `save` retired with the Save button (#391) — a config still setting
1184
- // it is ignored, not an error.
1185
- },
1186
- },
1187
- },
1188
- },
1189
- }
1190
- ```
1191
-
1192
- Two things are deliberately NOT config:
1193
-
1194
- - **The regime.** The visitor's browser timezone picks it — the strict opt-in roster (plus an
1195
- unplaceable timezone) gets opt-in, where no provider script loads until they accept; everywhere
1196
- else gets opt-out, where the scripts load and a first visit sees only the Cookies Settings
1197
- tab ([#391](https://github.com/Omega-JS-Stack/omega/issues/391)). There is no key that
1198
- forces one, because the answer is legal, not stylistic.
1199
- - **The colors.** The panel paints itself from the `--omega-*` token sheet, which is the
1200
- only way it is correct in both color modes. The old `palette`/`theme` keys are gone.
1201
-
1202
- `enabled: false` is legal only for a site that loads no analytics or marketing provider
1203
- at all — the gate and the banner are the same switch.
1204
-
1205
- ## Feature gating polarity (#527)
1206
-
1207
- Ratified 2026-08-24 (Ian). A feature has ONE switch with ONE polarity, and which
1208
- polarity it is follows from what the feature needs — never from taste at the read
1209
- site. Three cases:
1210
-
1211
- | Case | The switch | The read | Examples |
1212
- |------|-----------|----------|----------|
1213
- | **1. Data-bearing** — the feature cannot run without a value only the brand can supply | that DATA's presence | `if (value)` | `advertising.providers.adsense.client`, `monitoring.providers.sentry.dsn`, `analytics.providers.*.id`, `edge.providers.cloudflare.zone`, `advertising.providers.inhouse.source` |
1214
- | **2. Zero-data**: the framework can run it for every brand with no input | `enabled`, default ON | `value !== false` | `forms.providers.slapform.enabled`, `inbound.chat.providers.chatsy.enabled`, `search.providers.searchConsole.enabled`, `edge.providers.cloudflare.enabled`, `targets.web.meta.index` |
1215
- | **3. Consequential** — it costs money, publishes to the world, or is irreversible | `enabled`, default OFF | `value === true` | `directory.enabled`, `devlog.enabled`, `targets.desktop.platforms.mac.mas.enabled` |
1216
-
1217
- - **A block by itself NEVER enables.** Authoring `providers: { adsense: {} }` is an opt-IN
1218
- to being asked, not an ON — the case-1 data or the case-2/3 `enabled` still decides.
1219
- - **Never a second switch on one feature.** Two switches let a config say ON to one half
1220
- of the stack and OFF to the other: adsense carried exactly that (the manager gated on
1221
- `enabled`, the site on `client` + `units`), so `{ client, enabled: false }` stopped the
1222
- account being managed while the site kept serving ads off it. #527 collapsed adsense to
1223
- case 1 and deleted both extra gates. A state that needs a second switch to express
1224
- (managed account, ad-free site) is deliberately inexpressible.
1225
- - **Every code-read switch has a schema rule** ([#546](https://github.com/Omega-JS-Stack/omega/issues/546)):
1226
- an undeclared key validates clean, so a typo (`enbaled: false`) silently reads as ON and
1227
- the validator cannot document what the key does. The rule carries the `default:` that
1228
- states the polarity — except where the SECTION is presence-gated (`advertising`), because
1229
- a materialized default would write the section into every brand and switch the feature on
1230
- for brands that configured none; there the ON answer lives at the read site.
1231
-
1232
- ## Tri-state provisioning values (#33)
1233
-
1234
- Provisioning-flow keys (org, billing account, service/agent ids — anything a manage
1235
- flow can set up interactively) follow ONE contract, enforced by the manager's
1236
- config-flow engine (`packages/manager/src/lib/config-flow.js`):
1237
-
1238
- | Value | Meaning |
1239
- |-------|---------|
1240
- | missing / `null` | ASK in an interactive run — the answer lands in omega.json5; without a TTY: warn + skip, aggregated in the run summary |
1241
- | `false` | The user opted OUT — silent skip, never prompt or warn again. `false` on an ancestor section (`inbound.chat.providers.chatsy: false`) opts out every key under it |
1242
- | anything else | Use it |
1243
-
1244
- Every ask offers the opt-out (the gate's "Disable" and, in selection flows, an inline
1245
- "No …" choice), so `false` is always reachable; delete the line to be asked again.
1246
- First consumers: `cloud.organizationId` (asked at project creation — pick an org or
1247
- create standalone) and `cloud.billingAccount` (pick/create a billing account or stay
1248
- on Spark). ONE cloud home (#23, reversing the old `gcp`-vs-`firebase` split): the
1249
- platform-level org and billing account, the provisioning switches (`cloud.shared`,
1250
- `cloud.supportEmail`, `cloud.apiSubdomain`) and the app config (`cloud.provider`,
1251
- `cloud.config`) are all `cloud.*`, and the project id has exactly one address —
1252
- `cloud.config.projectId`. `cloud.supportEmail`'s null auto-derives the authorizing
1253
- user's email instead of asking.
1254
-
1255
- ## The site global: the curated targets view (#85, #610)
1256
-
1257
- `toSiteGlobal()` (the web build's `site.*`) strips the raw `targets` machinery and
1258
- replaces it with a CURATED `site.targets` — an allow-list of display-safe facts per
1259
- declared target, never a spread of the raw config: every entry carries `enabled: true`,
1260
- desktop adds a derived `releasesUrl` plus the per-artifact `downloads` map once releases
1261
- are opted in, extension adds its store `listings`.
1262
-
1263
- It is the ONE home of those facts
1264
- ([#610](https://github.com/Omega-JS-Stack/omega/issues/610)). The legacy UJM-shaped
1265
- `targets.web.download` / `targets.web.extension` page maps — and the `site.download` /
1266
- `site.extension` data they filled — are GONE: a brand still carrying either key fails
1267
- validation naming the block it derives from, and `omega migrate` drops it with a note.
1268
-
1269
- - **The desktop derivation is OPT-IN (#124)**: `releasesUrl` appears only when
1270
- `targets.desktop.releases` is present (its
1271
- `enabled` defaults true when the block exists); `releases.enabled: false` always
1272
- suppresses, and a bare desktop target with no `releases` block derives nothing —
1273
- declaring the target does not mean a release exists yet. `releases.enabled` is ONE
1274
- switch for the whole release surface: the desktop build also reads it
1275
- (electron-builder publish config), so `false` turns off desktop publishing too — and
1276
- there it defaults true even with no `releases` block.
1277
- - **Desktop releases URL**: `https://github.com/<owner>/<name>/releases/latest`, where
1278
- owner and name come from `releasesRepo(config)` in
1279
- [repo.js](../../packages/config/src/repo.js): the brand's ONE public releases repo
1280
- ([#799](https://github.com/Omega-JS-Stack/omega/issues/799),
1281
- [#883](https://github.com/Omega-JS-Stack/omega/issues/883)). It is always
1282
- `<brand.id>-releases` under `repo.org`, with no override key left anywhere;
1283
- `targets.desktop.releases` presence is only the opt-in. Nothing addressable, no URL
1284
- (no org, no brand id). That helper is the ONE
1285
- home of the address: @omega.js/desktop's electron-builder publish block, its releases-repo
1286
- provisioning and its `finalize-release` uploads read the same call, so the feed a shipped
1287
- app polls and the link a download button carries cannot disagree.
1288
- - **Desktop direct downloads ([#620](https://github.com/Omega-JS-Stack/omega/issues/620),
1289
- [#867](https://github.com/Omega-JS-Stack/omega/issues/867))**:
1290
- `downloads.<platform>.<format>` = `<releasesUrl>/download/<asset>`, one per format the
1291
- brand SHIPS (`mac.dmg`, `windows.nsis`, `linux.deb`, `linux.appimage`, in offer order),
1292
- plus `linux.snap`, which is the Snap Store listing because the store holds that file.
1293
- The format keys and the asset names are `platforms.js`'s: the SAME table
1294
- @omega.js/desktop's `build-config` writes into `electron-builder.yml`, so a button
1295
- hands over the file and never lands on a GitHub page. They carry no version, which is
1296
- what keeps `/releases/latest/download/<asset>` pointing at the newest build forever:
1297
- releasing a desktop version never touches the website. Derived from
1298
- `targets.desktop.app.productName` → `brand.name`; no product name, no `downloads` (a
1299
- guessed filename is a dead button). A platform or format the brand dropped is absent
1300
- here too, so the site never renders a button for something nobody builds.
1301
- - **Extension listings**: `targets.extension.listings.<store>.{url,state}` for the six
1302
- stores the theme renders (chrome, firefox, edge, opera, safari, brave) —
1303
- schema-declared; url must be http(s). Entries with neither url nor state stay absent.
1304
- The `id` beside them (#893) is the publish lane's, not the page's: it never
1305
- reaches the curated view, because a listing with no url has nothing to link.
1306
- - **Idempotent by contract**: the web build applies `toSiteGlobal` twice (loadSiteData,
1307
- then configureOmega) — a curated `releasesUrl` and its `downloads` survive the second
1308
- pass unchanged.
1309
- - **The curated view is keyed by target NAME, and every fact derives from the entry's TYPE**
1310
- ([#886](https://github.com/Omega-JS-Stack/omega/issues/886)): a brand that names its desktop
1311
- target `app` gets `site.targets.app.releasesUrl` and `site.targets.app.downloads`. The name is
1312
- never the test. A target whose type derives nothing is a presence-only entry.
1313
-
1314
- Three consumers read the curated view and nothing else: the `/download` page (every
1315
- desktop button → `site.targets.desktop.downloads[platform][artifact]`), the `/extension` page
1316
- (`site.targets.extension.listings[browser].url`), and the shortlink generator
1317
- (`src/target-shortlinks.js`, #561), which publishes `/download/<platform>[/<artifact>]`
1318
- and `/extension/<store>` off the same facts. Mobile derives nothing while MAM is
1319
- parked, so the mobile band stays on its notify form.
1320
-
1321
- ## The browser subset: one `client` flag, one `OMEGA_BUILD_JSON`, one `build.js` (#894, #743)
1322
-
1323
- Three surfaces hand a config to a browser: web's pages and its service worker, desktop's
1324
- renderer windows, and every extension page plus its service worker. What may go in is ONE
1325
- decision, declared once and made once, and it is DELIVERED one way:
1326
-
1327
- - **The declaration** is `CLIENT_SECTIONS` in [src/schema.js](../../packages/config/src/schema.js):
1328
- a per-SECTION `client` flag. `true` ships the whole section (its keys are public by
1329
- design, and a secret-shaped key can never be in the file at all); an ARRAY of dot-paths
1330
- ships only that section's public half, for a section whose other keys are
1331
- provisioning-only. No row means the browser never sees it. Today: `advertising`,
1332
- `analytics`, `app`, `brand`, `captcha`, `client`, `company`, `connections`, `features`,
1333
- `listings`, `monitoring`, `payment`, `theme` ride whole; `cloud` rides
1334
- `['config', 'messaging.vapidKey']` (the Firebase WEB config is public, the GCP account
1335
- facts beside it are not); `forms` rides `['providers.slapform.formId']` and `inbound`
1336
- the three chatsy leaves the widget reads. A NEW section defaults to invisible, which
1337
- fails as a missing feature rather than as a leak.
1338
- - **The function** is `clientConfig(resolved)`
1339
- ([src/client-config.js](../../packages/config/src/client-config.js)): the flagged
1340
- sections, plus the build FACTS a surface composes onto the config before baking it
1341
- (`runtime`, `environment`, `version`, `buildTime`, `target`, `url`, `dev`). `runtime` is
1342
- baked on every surface, including desktop (#896): a packaged Electron renderer is a
1343
- browser with no Electron globals of its own, so nothing can sniff it from inside.
1344
- `payment` comes out with its winback offer RESOLVED, so the dialog a customer reads and
1345
- the coupon the backend creates name the same numbers. A secret-shaped key inside a
1346
- section about to ship THROWS: that config should never have loaded, and this is the last
1347
- place before it is published. A value a build is sanctioned to bake (`publicAtRest`,
1348
- "Config or env?" above) is added by that build AFTER this gate, never carried through it.
1349
- - **The wrapper** is the same name and the same five keys on every surface:
1350
- `OMEGA_BUILD_JSON = { config, package, mode, license, builtAt }`, reachable as
1351
- `self.OMEGA_BUILD_JSON` in a window and in a worker alike. `config` is the subset; the
1352
- rest are facts about the BUILD and never part of the client contract. `mode` is the same
1353
- THREE keys everywhere, `{ environment, build, publish }`: the build's verdict, whether it
1354
- was a build rather than a dev/watch run, and whether it publishes. A surface's own extra
1355
- verdicts (desktop's `server`) stay inside that surface's Manager.
1356
- - **The delivery** is ONE FILE, `build.js` at the artifact's web root
1357
- ([#743](https://github.com/Omega-JS-Stack/omega/issues/743), Ian 2026-09-12: "make it
1358
- same shape and consumption everywhere as much as possible"). Two plain statements, written
1359
- by `composeBuildJson()` + `writeBuildJs()` in
1360
- [@omega.js/devkit/build-json](../../packages/devkit/src/build-json.js):
1361
-
1362
- ```js
1363
- self.OMEGA_BUILD_JSON = { config, package, mode, license, builtAt };
1364
- self.OMEGA_BUILD_JSON.config.dev = { … } | null;
1365
- ```
1366
-
1367
- Every HTML shell loads it with one script tag, FIRST (web's `core/_includes/core/head.html`
1368
- at `/build.js`, desktop's page template at `../../build.js`, the extension's at
1369
- `/build.js`), and every worker with one `importScripts('/build.js')` line (web's
1370
- `sw/manager.js`, the extension's `background.js`). No bundle carries a copy: the esbuild
1371
- banners and defines the browser bundles grew are retired, and so is web's inline foot
1372
- script. The `dev` map is its own statement because `omega dev` REWRITES that one line per
1373
- request (#346), so a site built before the emulator came up still serves the map of the
1374
- stack running right now.
1375
-
1376
- Desktop's MAIN and PRELOAD bundles are the one exception, and not to the file: they are
1377
- Node, they boot from the WHOLE resolved config inside the asar, and they keep carrying it
1378
- as a define plus a banner.
1379
-
1380
- A web page whose own `config:` block (#607) changes what a runtime reader sees emits ONE
1381
- more line after the loader tag, `Object.assign(self.OMEGA_BUILD_JSON.config, <delta>)`,
1382
- naming only the sections that actually differ.
1383
- - **`@omega.js/client` is handed `OMEGA_BUILD_JSON.config`** by the same call on all three
1384
- surfaces, and it owns the mapping onto its own contract: the `client` blob IS its top
1385
- level, `cloud.config` is the Firebase home it boots from, and
1386
- `monitoring.providers.sentry` is the one error-reporting switch. No surface composes a
1387
- second home for any of those.
1388
-
1389
- The backend is deliberately outside this: it stages the WHOLE resolved config to
1390
- `dist/config/omega.json5` and reads it with the loader, which is right for a server.
1391
-
1392
- ## Consumer access
1393
-
1394
- Each framework exposes the vendored loader — desktop: `require('@omega.js/desktop/config')`,
1395
- extension: `require('@omega.js/extension/config')` → `{ loadConfig, validateConfig, … }`.
1396
- Consumer workflows use this instead of raw JSON5 reads so brand-monorepo resolution
1397
- always applies.
1398
-
1399
- **Derived values reach brands as VALUES, never as a recipe to re-run**
1400
- ([#290](https://github.com/Omega-JS-Stack/omega/issues/290)). A brand target cannot require
1401
- this private package at runtime, so a framework that owns a derivation publishes its
1402
- ANSWER on the runtime config object the target already holds, under `resolved.*`: the
1403
- backend's `Manager.config.resolved.sourceRepo` carries `{ owner, name, slug }`, the brand's
1404
- SOURCE repo as one finished value. The derivations themselves stay here (`src/repo.js`):
1405
- one implementation, called by the framework, so no brand re-implements the rules and
1406
- drifts from them. New derived values join a framework's `resolved` group as real brand
1407
- needs surface.
1408
-
1409
- ## The repo block and the repos it derives (#883)
1410
-
1411
- ONE block says where a brand hosts its code, and it is two keys:
1412
-
1413
- ```json5
1414
- repo: { provider: 'github', org: 'Acme-Org' }
1415
- ```
1416
-
1417
- - **Presence is the switch.** A brand with no `repo` block hosts its source somewhere the
1418
- manager does not touch, and the repo service skips, exactly as a missing target key
1419
- means that target is not enabled. There is no `enabled` key.
1420
- - **`provider`** defaults to `github` and must be one of `REPO_PROVIDERS` (`['github']`).
1421
- A second provider is one table row plus its own helper, never a reshape of the block.
1422
- - **`org`** is the only typed value, and it is required whenever the block is present: it
1423
- owns every repo the brand's names derive under.
1424
-
1425
- **Every repo NAME derives from the `<brand.id>-<role>` rule** (Ian 2026-09-07,
1426
- [#809](https://github.com/Omega-JS-Stack/omega/issues/809)), one function per role in
1427
- [repo.js](../../packages/config/src/repo.js), which every package reads. No override key
1428
- of any kind exists: a repo name that must differ is a brand id that must differ.
1429
-
1430
- | Role | Derivation | Visibility | Holds |
1431
- |---|---|---|---|
1432
- | Source monorepo | `sourceRepo(config)` → `<repo.org>/<brand.id>-omega` | the brand root package.json `private` field | the brand's code, its scaffolded workflows and their Actions secrets, and the CMS's content commits. Every dispatch addresses this one |
1433
- | Releases | `releasesRepo(config)` → `<repo.org>/<brand.id>-releases` | always public (the desktop updater polls it with no token) | every target's built artifacts: desktop installers, the extension's zips, tagged per target |
1434
- | Website, one per GitHub-hosted web target | `websiteRepo(config, name)` → `<repo.org>/<brand.id>-<target name>` | private only when the brand is private AND the org's plan allows Pages from a private repo, else public, and the manage walk says which | the BUILT site only, one force-orphan commit on `gh-pages`, served by Pages at the target's url |
1435
-
1436
- **`repoDrift(originSlug, config)`** is the one comparison of a checkout's `origin` with
1437
- the source repo ([#934](https://github.com/Omega-JS-Stack/omega/issues/934)): the whole
1438
- slug, case-insensitive, null when they agree or the config derives no source repo, else
1439
- the line `origin is <slug> but config derives <derived>: fix repo.org in
1440
- config/omega.json5 or move the repo`. The boot prelude prints it; `omega manage` and
1441
- `omega deploy` refuse with it ([deploys.md](deploys.md#the-origin-gate-934)).
1442
-
1443
- **Visibility lives in the brand root's `package.json`**, never in omega.json5
1444
- (`brandVisibility(brandRoot)`): `private: true` or the field ABSENT is a private brand
1445
- (every brand monorepo is private by default, Ian 2026-09-11), and only a literal `false`
1446
- is a public one. The manage walk reconciles the repo to it in both directions.
1447
-
1448
- **`targets.<name>.hosting.provider`** says who SERVES a web target's built site, on web
1449
- targets only (a non-web target carrying `hosting` is a validation error). It defaults to
1450
- `github`, must be one of `HOSTING_PROVIDERS` (`['github']`), and is what `websiteRepo`
1451
- reads: another provider means there is no GitHub repo to address at all.
1452
-
1453
- **The Pages custom domain is ONE derivation**, `pagesHost(config, name)`: the bare host of
1454
- that target's `targetUrl`, and an empty string when the url names a default Pages address
1455
- (`*.github.io` is an ADDRESS, never a domain, [#366](https://github.com/Omega-JS-Stack/omega/issues/366)).
1456
- The manage walk sets the domain on the repo from it and the web deploy writes its `CNAME`
1457
- file from it, so the two can never claim different domains for one site.
1458
-
1459
- The by-hand steps for a brand still on the old shape are the register's
1460
- [2026-09-11 section](breaking-changes.md#2026-09-11-one-repo-block-and-the-website-repo-883).
1461
-
1462
- ## Defaults & self-healing
1463
-
1464
- Ian's ruling (2026-08-22, [#478](https://github.com/Omega-JS-Stack/omega/issues/478)): the config
1465
- stays complete and current. A subsystem that exists has its config structure IN the file —
1466
- visible and editable — instead of an invisible framework fallback, and every default has ONE
1467
- home.
1468
-
1469
- **The schema is that home.** A `schema.js` entry carries its own `default:` beside its type and
1470
- description; `schemaDefaults(target)` builds them into the merge chain's lowest layer, so nothing
1471
- else re-states a default. A key with no sane framework answer carries none: owner decisions,
1472
- tri-states that mean "ask" (`cloud.billingAccount`), ids the services provision
1473
- (`forms.providers.slapform.formId`), anything secret-shaped. The other standing exclusion is the
1474
- **presence gate** ([#425](https://github.com/Omega-JS-Stack/omega/issues/425)), which in practice
1475
- means monitoring: that service reads `monitoring.providers.sentry` presence as the pick of
1476
- Sentry, so its SDK knobs (`sampleRate`, `scrubEmail`, …) carry no default and keep their home in
1477
- the package that reads them — and `advertising`, whose adsense `client` id is the whole switch
1478
- ([#527](https://github.com/Omega-JS-Stack/omega/issues/527)), so a materialized block would turn
1479
- the web build's automatic ad placements on for a brand that configured none. `repo` is the third
1480
- ([#883](https://github.com/Omega-JS-Stack/omega/issues/883)): the BLOCK's presence enables the
1481
- repo service, so `repo.provider` carries no default either and the value a missing provider reads
1482
- lives beside the derivation, in `src/repo.js`. Blocks whose services
1483
- gate on `enabled` or provisioned ids rather than presence (cloudflare, searchConsole,
1484
- slapform, chatsy, replyify) DO carry defaults — see the polarity doctrine above.
1485
- Role-level switches beside any providers block
1486
- (`monitoring.enabled`, `marketing.campaigns.enabled`) are nobody's pick and always may.
1487
-
1488
- **`omega manage` materializes what a brand lacks.** The workspace service's `defaults` operation
1489
- (right after the `config` health check) diffs the brand's own `config/omega.json5` against the
1490
- schema defaults and writes the missing blocks through the comment-preserving editor, each key
1491
- documented with the schema's own description. The rules:
1492
-
1493
- - **Never an overwrite.** Only keys the brand has NOT authored are written — a `false` a brand
1494
- set (or a section it deliberately switched off) is a decision, and the heal never dives into it.
1495
- - **Highest missing path, once.** A brand with no `marketing` at all gets one `marketing` block,
1496
- not one edit per key inside it.
1497
- - **Idempotent.** A converged config leaves the file byte-identical; a dry run reports the blocks
1498
- and writes nothing.
1499
- - **New subsystems arrive on the next run.** Adding a `default:` to the schema is all it takes for
1500
- every consumer's config to grow the block the next time manage runs.
1501
-
1502
- `marketing.prune.enabled` is the first ruling this carries: pruning is ON by default and lands in
1503
- every brand config as an editable switch (Ian 2026-08-22, closing the
1504
- [#422](https://github.com/Omega-JS-Stack/omega/issues/422) follow-up).
1505
-
1506
- **`materialize: false` — a default that resolves but is never written**
1507
- ([#793](https://github.com/Omega-JS-Stack/omega/issues/793)). A rule may carry the flag beside its
1508
- `default:`, and then `schemaDefaults()` still puts the value at the merge chain's lowest layer —
1509
- every reader resolves it — while `missingDefaults()` and `defaultComments()` skip it, so the manage
1510
- walk never writes it into `config/omega.json5`. The line for when to use it: **is this value an
1511
- OWNER's decision, or the framework's own fact?** A decision belongs in the brand's file, where it is
1512
- visible and editable (that is every ordinary default). A framework fact a brand may override and
1513
- rarely does — presentation the framework owns — belongs at the layer that owns it, because a copy in
1514
- every brand config is a copy that drifts from the thing it came from. The `connections` section is
1515
- the first: the framework ships the five packaged providers' `name`, `logo` and `description`, a
1516
- brand overrides any key through the ordinary merge chain, and nothing is copied into a brand file to
1517
- go stale ([packages/backend/docs/connections.md](../../packages/backend/docs/connections.md)).
1518
-
1519
- ## Writeback (comment-preserving edits)
1520
-
1521
- omega.json5 is hand-edited — comments, key order, and quote style carry meaning — so
1522
- programmatic writes are surgical text edits, not a re-stringify (omega-manager's
1523
- serializer rewrote the whole file in canonical order and lost comments; this replaces
1524
- it). The manager's services use it to land resolved IDs in config: the SendGrid list,
1525
- the Beehiiv publication, Stripe/PayPal product IDs, the Firebase SDK config.
1526
-
1527
- `applyConfigEdits(source, edits, { comments }?)` applies `{ 'dot.path': value }` edits to JSON5 text:
1528
- existing leaves get their value span replaced; missing branches insert as one property
1529
- before the containing object's closing brace (matching indent; house style: unquoted
1530
- keys, JSON.stringify strings, trailing commas). Every byte outside the edited spans
1531
- survives. Paths take dots, numeric array indexes, and `[key=value]` matchers that select
1532
- an array element by its own key — `payment.products[id=plus].stripe.productId` — so
1533
- writes self-locate in the file being edited instead of trusting an index computed from a
1534
- merged config. Array elements are never created.
1535
-
1536
- `comments` (dot-path → text) documents INSERTED keys only — the comment lands above the key its
1537
- path names, wherever inside the inserted block that is, wrapped at that key's indent. A path that
1538
- already exists keeps whatever the brand wrote above it. That is how the manage-run heal
1539
- ([Defaults & self-healing](#defaults--self-healing)) lands each materialized block with the
1540
- schema's own guidance beside it.
1541
-
1542
- Guarantees: edits whose value already matches are skipped entirely (reruns are
1543
- byte-identical); after every edit the result must JSON5-parse and hold the requested
1544
- value at the requested path, or the call throws and nothing is returned — a corrupted
1545
- config can't land on disk.
1546
-
1547
- `writeConfigValues(projectDir, edits, { dryRun }?)` is the file-level form: resolves the
1548
- standard locations, skips the write when nothing changes, and returns
1549
- `{ path, changed, applied }` (`applied` = the paths that actually differed). The manager
1550
- wraps it in `lib/config-write.js` (`writeBrandConfig(context, edits)`) for the uniform
1551
- dry-run gate + logging.
1552
-
1553
- **Deleting** is the same surgery in reverse ([#612](https://github.com/Omega-JS-Stack/omega/issues/612)):
1554
- `applyConfigRemovals(source, paths)` cuts the property a dot-path names — its key, its whole
1555
- subtree, the comma that separated it, and the `//` comment block documenting it, because that
1556
- comment describes the key being deleted and would otherwise dangle over the next one. A property
1557
- sharing its line (`{ a: 1, b: 2 }`) takes only itself and its separator. Absent paths are skipped,
1558
- so reruns are byte-identical, and each removal is verified (parses, path gone) or the call throws.
1559
- Array ELEMENTS are never removed, the mirror of never creating them. `removeConfigValues(projectDir,
1560
- paths, { dryRun }?)` is the file-level form → `{ path, changed, removed }`. Unlike a write it does
1561
- NOT normalize top-level key order: a deletion is surgical, and re-sorting the file around it would
1562
- bury the one line the caller means to report. The consumer is `omega migrate` at a brand root
1563
- ([manager/index.md](../manager/index.md)).
1564
-
1565
- ## Migration — legacy configs → omega.json5
1566
-
1567
- No framework reads the legacy files anymore. Convert once, delete the old file. General
1568
- recipe: shared-looking sections move to the TOP LEVEL (brand, analytics, payment, theme;
1569
- `oauth2` lands as `connections` (#788); `firebaseConfig` becomes `cloud: { provider: 'firebase', config: {…} }` and
1570
- `sentry` becomes `monitoring: { providers: { sentry: {…} } }`); everything framework-specific
1571
- moves under `targets.<name>`.
1572
-
1573
- ### The targets shape rename (#886)
1574
-
1575
- An OMEGA-era brand converts once: every entry declares its `type`, and the KEY is the folder.
1576
- The by-hand steps (and the rest of the register) are in
1577
- [breaking-changes.md](breaking-changes.md#2026-09-11-targets-keyed-by-name-886).
1578
-
1579
- | Legacy | New |
1580
- |---|---|
1581
- | `targets/website` (the canonical web folder) | **`targets/web`**: the folder is the key, and the key is `web` |
1582
- | `targets/website-admin` (an instance's suffixed folder) | **`targets/admin`**: the sibling key names its own folder |
1583
- | `web: [{ id: 'main' }, { id: 'admin' }]` (the instance ARRAY) | **sibling keys**: `web: { type: 'web' }, admin: { type: 'web' }` |
1584
- | `--target=website` | **`--target=web`**: the flag takes the NAME |
1585
-
1586
- ### The one repo block (#883)
1587
-
1588
- The brand's repo hosting used to be spelled in four places: `repo.providers.github`, a
1589
- separate top-level `github` identity, a `targets.<name>.github.repo` override, and the
1590
- desktop releases owner/repo. One block says it now (`repo: { provider, org }`), every repo
1591
- name derives from `<brand.id>-<role>`, and visibility is the brand root package.json's
1592
- `private` field. Each retired key is a registered PATH, so a brand still carrying one
1593
- fails validation instead of silently addressing a repo nothing publishes to. The by-hand
1594
- steps are the register's
1595
- [2026-09-11 section](breaking-changes.md#2026-09-11-one-repo-block-and-the-website-repo-883).
1596
-
1597
- | Retired path | New home |
1598
- |---|---|
1599
- | `repo.providers.github.org` | **`repo.org`** |
1600
- | `repo.providers.github.repo` | nothing: the source repo IS `<brand.id>-omega`. A name that must differ is a brand id that must differ |
1601
- | `repo.providers.github.private` | the brand root `package.json` `private` field (absent = private) |
1602
- | `repo.providers.github.shared` | nothing: an org may host many brands, and no brand rewrites an org profile |
1603
- | `repo.providers.github.enabled` | the PRESENCE of the `repo` block |
1604
- | `github.user` | nothing: the org is `repo.org` |
1605
- | `github.website` | nothing: a web target's site repo is `<brand.id>-<target name>` |
1606
- | `targets.<name>.github.repo` (every type) | nothing: the CMS commits to the SOURCE repo `<brand.id>-omega` |
1607
- | `targets.desktop.releases.owner` | nothing: `<brand.id>-releases` under `repo.org` |
1608
- | `targets.desktop.releases.repo` | nothing: `<brand.id>-releases` under `repo.org` (`releases: {}` stays the presence switch) |
1609
-
1610
- **Retired keys fail loudly** ([#142](https://github.com/Omega-JS-Stack/omega/issues/142)):
1611
- a name that was renamed OUTRIGHT is a validation error wherever it sits — shared level,
1612
- inside a `targets.<name>` entry, naming its replacement and
1613
- pointing back here. Today that is `web_manager` → `client`, `firebaseConfig` → `cloud`,
1614
- `cookieConsent` → `client.consent`, `subdomains` → `targets.web` and `oauth2` → `connections` (`src/retired-keys.js` is the list). Without the guard the old key validated clean and
1615
- everything under it vanished, since nothing dual-reads it. Names that live on as legitimate
1616
- keys elsewhere stay out of the list — the rows below are their only guide. `sentry` is the one
1617
- that reads like a contradiction and is not: its NEW home is itself a `sentry` key
1618
- (`monitoring.providers.sentry`), so a name test would fire on the very shape it is steering
1619
- people toward. Same for `google`/`meta` under `analytics.providers`.
1620
-
1621
- **`omega migrate` at the brand root deletes them** ([#612](https://github.com/Omega-JS-Stack/omega/issues/612)): both halves of the
1622
- list, from the file as AUTHORED, through the comment-preserving editor (`removeConfigValues`) —
1623
- the key, its subtree, and the comment documenting it, with every other byte untouched. One line
1624
- per key naming its replacement, `--dry-run` for the plan, idempotent (a converged brand's rerun
1625
- is byte-identical). It removes the dead key; moving the setting into the home named in the tables
1626
- below is still by hand, EXCEPT where a row carries a converter
1627
- ([#858](https://github.com/Omega-JS-Stack/omega/issues/858)): then the new value is written
1628
- first, through the same comment-preserving editor, and the old key is deleted in the same
1629
- run, so a brand never sits between the two shapes. A dry run prints both halves.
1630
-
1631
- `subdomains` is the newest name ([#588](https://github.com/Omega-JS-Stack/omega/issues/588),
1632
- Ian 2026-09-01). The key was READ by exactly one thing, the cloud hosting op, which ensured an
1633
- `api.{sub}.{domain}` per entry, and DECLARED by nothing: no schema rule, no default, never
1634
- materialized. The fact it was reaching for is a web target, so a target is its home now:
1635
- the NAME is the subdomain, and every subdomain shares one `api.<domain>`. It is a NAME test by the
1636
- rule above (`subdomains` exists nowhere else in the schema), so it fires wherever a brand wrote
1637
- it, including down inside a target entry's own `brand` block.
1638
-
1639
- | Retired key | New home |
1640
- |---|---|
1641
- | `subdomains` | **`targets`** as sibling web targets: `["admin", "cdn"]` becomes `admin: { type: 'web' }, cdn: { type: 'web' }` beside the main site (§ Targets). The NAME is the subdomain (`https://admin.<brand host>`), an entry's own `url` overrides it for a custom host, and the targets share ONE `api.<domain>` |
1642
-
1643
- `oauth2` is the newest name ([#788](https://github.com/Omega-JS-Stack/omega/issues/788),
1644
- Ian 2026-09-03). The product concept is a CONNECTION, and a connection will not always be an
1645
- OAuth grant — an API key or a bot token is one too — so the whole feature carries the product
1646
- word (the section, the route, the user-record field, the env prefix, the brand provider folder,
1647
- the callback URL) and each stored record names its own kind with `type: 'oauth2'`. A NAME test
1648
- by the rule above: `oauth2` exists nowhere else in the schema. The by-hand steps a carrying
1649
- brand still owes — the env rename, the provider-console redirect URI, the provider folder move —
1650
- are in [breaking-changes.md](breaking-changes.md#the-user-connection-feature-is-connections-788).
1651
-
1652
- | Retired key | New home |
1653
- |---|---|
1654
- | `oauth2` | **`connections`** — the per-provider block is unchanged. The credentials are the `CONNECTIONS_<PROVIDER>_CLIENT_ID`/`_SECRET` pair now, a brand's own provider module lives at `targets/backend/src/connections/<name>.js`, the route is `/omega/user/connections`, and the redirect URI to register is `<websiteUrl>/connections/callback` |
1655
-
1656
- The de-branding rekey ([#23](https://github.com/Omega-JS-Stack/omega/issues/23)) adds a
1657
- second, PATH-based half in the same file (`RETIRED_PATHS`): keys whose provider keeps its
1658
- own name one level down inside the new home, so a name test would false-positive. Each
1659
- entry matches ONE exact path from the root, array positions ignored, and the error names the
1660
- real path, index and all
1661
- ([#732](https://github.com/Omega-JS-Stack/omega/issues/732)). A `targets.<name>` row names the
1662
- TYPE, and the key is a NAME, so the row fires on EVERY target of that type: with `community:
1663
- { type: 'web' }` declared, `targets.community.meta.title` bounces against the
1664
- `targets.web.meta.title` row and the error names the real path
1665
- ([#886](https://github.com/Omega-JS-Stack/omega/issues/886)):
1666
-
1667
- | Retired path | New home |
1668
- |---|---|
1669
- | `slapform` | **`forms.providers.slapform`** |
1670
- | `chatsy` | **`inbound.chat.providers.chatsy`** (the widget `settings` moved here too — one home) |
1671
- | `replyify` | **`inbound.email.providers.replyify`** |
1672
- | `cloudflare` | **`edge.providers.cloudflare`** |
1673
- | `recaptcha` | **`captcha.providers.recaptcha`** (`site-key` → `siteKey`) |
1674
- | `searchConsole` | **`search.providers.searchConsole`** (`seo` already means the parasite-SEO content feature) |
1675
- | `gcp` | **`cloud`** (`cloud.organizationId`, `cloud.billingAccount`) |
1676
- | `firebase` | **`cloud`** (`cloud.shared`, `cloud.supportEmail`, `cloud.apiSubdomain`; projectId only at `cloud.config.projectId`) |
1677
- | `advertising.providers.google-adsense` | **`advertising.providers.adsense`** + camelCase slots |
1678
- | `github` | **`repo`** ([#883](https://github.com/Omega-JS-Stack/omega/issues/883)): the separate identity block is gone, and `github.user` / `github.website` are registered retired PATHS now, so a carrying brand hears it from the validator instead of this table |
1679
-
1680
- The one-provider-shape normalization ([#425](https://github.com/Omega-JS-Stack/omega/issues/425))
1681
- adds its own rows to the same `RETIRED_PATHS` half — every role names its vendors
1682
- `role.providers.<provider>` now, so the flat picks, the bare vendor key and payment's
1683
- fourth word are all retired. Key PRESENCE is the pick; `false` is the deliberate off
1684
- switch; no entry at all is "none chosen" (what a null provider meant). `cloud` stays the
1685
- ratified exception. Full rationale + the by-hand step per row:
1686
- [breaking-changes.md](breaking-changes.md#one-provider-shape--roleprovidersprovider-425).
1687
-
1688
- | Retired path | New home |
1689
- |---|---|
1690
- | `payment.providers` | **`payment.providers`** — contents identical. The SINGULAR `provider` (Firestore document fields, the intent schema, the payments route params, the email merge field, `libraries/payment/providers/`) is a live data contract and is unchanged |
1691
- | `certificates.apple` | **`certificates.providers.apple`** — no bare vendor keys; Windows signing sits beside it later |
1692
- | `domain.provider` | **`domain.providers.<registrar>`** — `{ namecheap: {} }` / `{ squarespace: {} }` |
1693
- | `domain.email.provider` | **`domain.email.providers.<provider>`** — `domain.email.forwarding` stays role-level (provider-agnostic) |
1694
- | `translation.provider` | **`translation.providers.<name>`** — `{ claude: {} }` / `{ chatgpt: {} }`; an absent block still means claude. `translation.model` stays role-level |
1695
- | `devlog.provider` + `devlog.{lookbackDays,orgs,excludeRepos,excludeCommits,excludeTopics,includePrivate,postPath,destinations,overrides}` | **`devlog.providers.ghostii.<same key>`** — the writer is the KEY, its settings live inside it. `devlog.enabled` stays role-level |
1696
- | `monitoring.provider` + `monitoring.{org,dsn,environment,sampleRate,tracesSampleRate,scrubEmail,attachScreenshot,bundlePatterns}` | **`monitoring.providers.sentry.<same key>`**: the monitor is the KEY, every SDK-facing knob lives inside it. `monitoring.enabled` stays role-level, and per-surface DSNs are `targets.<name>.monitoring.providers.sentry.dsn` |
1697
- | `marketing.campaigns.provider` + `marketing.campaigns.listId` | **`marketing.campaigns.providers.sendgrid.listId`** — `marketing.campaigns.enabled` stays role-level |
1698
- | `marketing.newsletter.provider` + `marketing.newsletter.publicationId` | **`marketing.newsletter.providers.beehiiv.publicationId`** — `marketing.newsletter.enabled` AND `marketing.newsletter.content` stay role-level: content configures @omega.js/backend's newsletter generator, not Beehiiv |
1699
-
1700
- The retired-key sweep only ever runs over an omega.json5: the `omega migrate` converter
1701
- READS legacy files as input and emits the new names, and only its output is validated.
1702
-
1703
- AdSense's one-switch collapse ([#527](https://github.com/Omega-JS-Stack/omega/issues/527))
1704
- adds the last two rows ([#628](https://github.com/Omega-JS-Stack/omega/issues/628)). Both
1705
- were unregistered until then, so a brand still carrying the deleted gate validated CLEAN
1706
- while the account it meant to leave alone started being managed — the key reading exactly
1707
- like it still worked. `omega migrate` drops both with the note.
1708
-
1709
- | Retired path | New home |
1710
- |---|---|
1711
- | `advertising.providers.adsense.enabled` | **`advertising.providers.adsense`** — `client` presence is the ONE switch (managed account + rendered units + the ads.txt record). `advertising.providers.adsense: false` opts the provider out; there is no second gate |
1712
- | `advertising.providers.adsense.units` | **`advertising.providers.adsense`** — the render-only gate #527 refused. A managed-but-ad-free brand omits the block and manages the account by hand |
1713
-
1714
- The `meta` section is the other registered path ([#607](https://github.com/Omega-JS-Stack/omega/issues/607),
1715
- Ian 2026-08-26 — meta never exists in two places). It shipped for one wave beside the
1716
- bare `meta:` a page and a layout already wrote, which is two homes for one fact; deleting
1717
- the config half leaves page frontmatter as the only meta, with `brand.name` /
1718
- `brand.description` as the site-wide default the head falls back to. Registered at its
1719
- authored path AND at the `targets.web` overlay — never by key NAME, because
1720
- `analytics.providers.meta` is a legitimate key one level down.
1721
-
1722
- | Retired path | New home |
1723
- |---|---|
1724
- | `meta.title`, `meta.description` (and the same two under `targets.web.meta`) | **`brand.name` / `brand.description`** for the site-wide default, and that page's own `meta:` frontmatter for anything per-page ([docs/web/frontmatter.md](../web/frontmatter.md)) |
1725
- | `seo.index` | **`targets.web.meta.index`** ([#564](https://github.com/Omega-JS-Stack/omega/issues/564), Ian's same-name ruling 2026-09-09): the site-wide default and the page override are ONE name at both levels, so `meta.index` is what a page writes and `targets.web.meta.index` is what the site writes. `targets.web.meta` itself is LIVE for that key; only `title` and `description` are retired under it |
1726
-
1727
- `translation.exclude` is the same ruling's second fold ([#858](https://github.com/Omega-JS-Stack/omega/issues/858),
1728
- Ian 2026-09-13). The key named what NOT to translate and defaulted to nothing, so a brand
1729
- that never thought about it paid a provider for every post it had, and a page had no way to
1730
- say anything at all. The list says what to TRANSLATE now: globs on page routes with `!`
1731
- negation, read in `.gitignore` order, defaulting to `['**', '!blog/**']` in the DEFAULTS
1732
- layer, and a page overrides it for itself with the SAME key name one level down
1733
- (`translation.include: true`/`false` in its own frontmatter). Registered at its authored
1734
- path AND at the `targets.web` overlay, the two places a brand can write it, never by key
1735
- NAME: `exclude` is a legitimate key elsewhere. This row carries a CONVERTER, so
1736
- `omega migrate` MOVES the setting instead of only deleting it.
1737
-
1738
- | Retired path | New home |
1739
- |---|---|
1740
- | `translation.exclude` (and the same key under `targets.web.translation`) | **`translation.include`**: the route list says what to translate, so `exclude: ['docs']` becomes `include: ['**', '!docs']` and `omega migrate` writes exactly that before deleting the old key ([translation.md](translation.md), [docs/web/frontmatter.md](../web/frontmatter.md)) |
1741
-
1742
- `targets.web.redirects` is the newest registered path ([#466](https://github.com/Omega-JS-Stack/omega/issues/466)).
1743
- It shipped in 0.45.0 and was withdrawn: static hosting has no server, so the map could only
1744
- ever be answered CLIENT-side off the built 404 page, and a search engine saw a 404 that
1745
- redirects rather than a move. Redirects are not web config at all now — a TEMPLATED
1746
- redirect needs edge computing, an enumerable one is a page. Registered at its authored
1747
- path, the one place a carrying brand has it.
1748
-
1749
- | Retired path | New home |
1750
- |---|---|
1751
- | `targets.web.redirects` | **`edge.providers.cloudflare.rules.redirect`** for a templated redirect (`/c/:id` → `/code?id=:id`, the DashQR pattern) — the manager's edge service reconciles the ruleset ([docs/manager/edge.md](../manager/edge.md)). A redirect whose URLs can be ENUMERATED is a redirect PAGE instead: `redirect.url` in frontmatter on the `modules/utilities/redirect` layout ([docs/web/index.md](../web/index.md)) |
1752
-
1753
- The four schema-less web sections are registered paths too
1754
- ([#850](https://github.com/Omega-JS-Stack/omega/issues/850), Ian 2026-09-09:
1755
- everything the build processes has a schema home). They were legacy UJM
1756
- presentation blocks the converter wrote under `targets.web` with no schema rule
1757
- anywhere, and @omega.js/web carried a PRIVATE list (`WEB_ONLY_SECTIONS`) purely
1758
- so its own `config:` guard would pass them. That list is gone: each key is a
1759
- registered path now, so a brand still carrying one hears it from the validator
1760
- instead of authoring a value nothing reads. Registered at their authored path,
1761
- the one place a carrying brand has them, and `omega migrate` drops the three
1762
- with a note and moves the currency.
1763
-
1764
- | Retired path | New home |
1765
- |---|---|
1766
- | `targets.web.favicon` | nothing for the path: the favicon set is MINTED from the brand images (manager assets service) and bridged to `/assets/images/favicon`, so an override pointed the whole site at an unminted folder. Change the source image, **`brand.images.favicon`**. The `theme-color` half is **`brand.color`**, the one place a brand states its hex |
1767
- | `targets.web.manifest` | nothing: no reader anywhere in @omega.js/web. The web app manifest that ships is the minted set's own `site.webmanifest` |
1768
- | `targets.web.icons` | **the `icon` on the link itself**, as Font Awesome classes (`icon: 'fa-brands fa-github'`): one icon mechanism ([#619](https://github.com/Omega-JS-Stack/omega/issues/619), [icons.md](icons.md)), so there is no name-to-markup map |
1769
- | `targets.web.currency` | **`payment.currency`**: the price currency belongs to the payment section every other surface reads it from, and the pricing JSON-LD reads it there |
1770
-
1771
- `targets.desktop.downloads.*` are the newest registered paths ([#799](https://github.com/Omega-JS-Stack/omega/issues/799)).
1772
- The `download-server` mirror gave marketing a fixed filename, which the versionless artifact
1773
- names ([#620](https://github.com/Omega-JS-Stack/omega/issues/620)) made free: the site links
1774
- the ONE public releases repo directly and reads nothing from the mirror, so #799 deleted the
1775
- lane. Without these rows a brand carrying the block validates clean (it sits inside the
1776
- exempt `targets` namespace), gets a second repo provisioned and nothing published to it.
1777
- Registered per KEY at its authored path, never by name: the curated
1778
- `site.targets.desktop.downloads` map is a legitimate `downloads` one level down.
1779
-
1780
- | Retired path | New home |
1781
- |---|---|
1782
- | `targets.desktop.downloads.enabled` | **`targets.desktop.releases`**: one public releases repo per brand, and its versionless assets ARE the permanent download links |
1783
- | `targets.desktop.downloads.owner` | nothing: there is no second repo to own, and the releases repo derives as `<brand.id>-releases` under `repo.org` (#883) |
1784
- | `targets.desktop.downloads.repo` | nothing: the releases repo derives as `<brand.id>-releases` (#883) |
1785
- | `targets.desktop.downloads.tag` | nothing: `/releases/latest/download/<asset>` is what the stable mirror tag was for |
1786
-
1787
- The company is ONE key now ([#677](https://github.com/Omega-JS-Stack/omega/issues/677)),
1788
- outside `brand`, and everything else about the company is RESOLVED at load. The typed
1789
- display name and the typed parent wordmark are registered retired PATHS; the typed
1790
- `company.url` (and any `company.name` / `company.images`) fails the LOAD instead, naming
1791
- the file, because the resolver fills those same keys and a retired-key sweep over a
1792
- resolved config would fire on its own answer. The by-hand step, and the `parent` /
1793
- `.omega/company.json` half, are in
1794
- [breaking-changes.md](breaking-changes.md#2026-09-12-one-company-key-and-a-company-tree-inside-the-parent-677).
1795
-
1796
- | Retired path | New home |
1797
- |---|---|
1798
- | `brand.company` | **`company.name`**, resolved from `company: { id: '<parent brand.id>' }`: the parent's name is the parent's to state |
1799
- | `brand.images.companyWordmark` | **`company.images.wordmark`**, resolved from the parent's own `brand.images.wordmark` |
1800
- | `company.url` (typed) | **`company.url`** (resolved): type `company: { id }` and the loader fills it; an authored one fails the load |
1801
- | `parent` (every value) | The key is RETIRED outright ([#677](https://github.com/Omega-JS-Stack/omega/issues/677)): the topology is **`company: { id: 'self' }`** / **`company: { id: '<parent brand.id>' }`**, and the opt-out `parent: false` is **`company: { webhooks: false }`**. A config still carrying `parent` fails the load naming `company.webhooks` |
1802
-
1803
- ### electron-manager (`config/electron-manager.json` → `config/omega.json5`) — DONE (checkpoint 18)
1804
-
1805
- | Legacy | New |
1806
- |---|---|
1807
- | `brand`, `analytics`, `payment`, `theme` | top level, unchanged |
1808
- | `firebaseConfig` | **`cloud: { provider: 'firebase', config: {…} }`** (D12) |
1809
- | `sentry` | **`monitoring: { providers: { sentry: { dsn } } }`** ([#425](https://github.com/Omega-JS-Stack/omega/issues/425)) |
1810
- | `app` | `targets.desktop.app` |
1811
- | `targets.mac` / `targets.win` / `targets.linux` (per-OS) | `targets.desktop.platforms.mac` / `.windows` / `.linux` ([#867](https://github.com/Omega-JS-Stack/omega/issues/867): the vocabulary is `mac`, `windows`, `linux`, and what each ships is `platforms.<platform>.formats.<format>`; `platforms.linux.snap` moved inside as `platforms.linux.formats.snap`, its `enabled` flag replaced by presence) |
1812
- | `autoUpdate`, `startup`, `releases`, `remoteConfig`, `restartManager` | `targets.desktop.<same key>` |
1813
- | `electronBuilder` overrides | `targets.desktop.electronBuilder` |
1814
- | `cdp` | `targets.desktop.cdp` |
1815
- | `windows` (optional) | `targets.desktop.windows` |
1816
- | `fileAssociations`, `protocols` | `targets.desktop.<same key>` |
1817
-
1818
- ### backend-manager (`functions/backend-manager-config.json` → `functions/config/omega.json5`) — DONE (checkpoint 19)
1819
-
1820
- | Legacy | New |
1821
- |---|---|
1822
- | `brand`, `analytics`, `payment` | top level, unchanged |
1823
- | `oauth2` | **`connections`** — the per-provider block is unchanged ([#788](https://github.com/Omega-JS-Stack/omega/issues/788)); the credentials become the `CONNECTIONS_<PROVIDER>_CLIENT_ID`/`_SECRET` pair |
1824
- | `firebaseConfig` | **`cloud: { provider: 'firebase', config: {…} }`** (D12) |
1825
- | `sentry` | **`monitoring: { providers: { sentry: { dsn } } }`** ([#425](https://github.com/Omega-JS-Stack/omega/issues/425)) |
1826
- | custom keys (`omega`, `mcp`, …) | top level, unchanged |
1827
- | `github`, `reviews`, `marketing`, `blog`, `dataRequest` | top level, unchanged ([#277](https://github.com/Omega-JS-Stack/omega/issues/277)): shared keys the manager reads brand-level, so a website-only brand has a home for them; `targets.backend.<same key>` still overrides |
1828
- | `parent` | RETIRED outright ([#677](https://github.com/Omega-JS-Stack/omega/issues/677)): a URL or `'self'` becomes **`company: { id }`**, and `parent: false` becomes **`company: { webhooks: false }`**. The key itself fails the load now, naming its new home |
1829
-
1830
- Notes: @omega.js/backend's framework-defaults layer is `templates/config/omega.json5` resolved through
1831
- the same loader and passed as `options.defaults`; `Manager.init()`'s
1832
- `backendManagerConfigPath` option is gone (the loader discovers the file); boot warns on
1833
- schema findings, `npx omega test`'s target checks are the hard audit. The sandbox brand dogfoods the full
1834
- hierarchy: shared sections live in `brands/sandbox-brand/config/omega.json5` (brand level),
1835
- the backend target's local file carries only `targets.backend`.
1836
-
1837
- ### browser-extension-manager (`config/browser-extension-manager.json` → `config/omega.json5`) — DONE (checkpoint 20)
1838
-
1839
- | Legacy | New |
1840
- |---|---|
1841
- | `brand`, `analytics`, `theme` | top level, unchanged |
1842
- | `firebaseConfig` | **`cloud: { provider: 'firebase', config: {…} }`** (D12) |
1843
- | `sentry` | **`monitoring: { providers: { sentry: { dsn } } }`** ([#425](https://github.com/Omega-JS-Stack/omega/issues/425)) |
1844
- | custom keys (`liveReloadPort`, …) | top level, unchanged |
1845
- | `analytics.providers.google.secret` | **`.env` → `GOOGLE_ANALYTICS_SECRET`** (secrets never in omega.json5; loader hard-fails) |
1846
- | *(no extension-specific keys yet)* | `targets.extension: {}` — presence = enabled; extension-specific settings land here |
1847
-
1848
- Notes: `Manager.getConfig()` returns the RESOLVED config (missing file → `{}`; schema
1849
- findings warn once per process — BXM has no separate audit surface). The build snapshot
1850
- (`OMEGA_BUILD_JSON`, the artifact's one `build.js`) bakes `GOOGLE_ANALYTICS_SECRET` from the environment at
1851
- build time, same value flow as before. `bxm setup` scaffolds + merges `config/omega.json5`
1852
- (the defaults merge now preserves consumer-only keys at every level — it previously
1853
- dropped them).
1854
-
1855
- ### ultimate-jekyll-manager (`src/_config.yml` + `config/ultimate-jekyll-manager.json`) — `omega migrate` (B4, checkpoint 32)
1856
-
1857
- One command converts the consumer in place (and `--check` previews without
1858
- writing). The converted file is validated through `loadConfig(root, 'web')`
1859
- before the report prints.
1860
-
1861
- | Legacy | New |
1862
- |---|---|
1863
- | `url` | `brand.url` (site.url derives; empty `baseurl` dropped) |
1864
- | `brand`, `theme` | top level, verbatim |
1865
- | `oauth2` | **`connections`** — same block, new name ([#788](https://github.com/Omega-JS-Stack/omega/issues/788)) |
1866
- | `analytics.{google,meta,tiktok}` (flat scalars) | `analytics.providers.<p>.id` — the unified spelling; the web chrome emits the client's flat shape from it |
1867
- | `web_manager.firebase.app.config` | **`cloud: { provider: 'firebase', config: {…} }`** (top level); the engine composes `cloud.config` back into `client.firebase.app.config` at build |
1868
- | `web_manager.payment` | **`payment`** (top level); composed back into `client.payment` (pricing layouts + the client read it there); credential keys set to `false` (legacy "disabled") are dropped |
1869
- | `web_manager` (rest: auth, exitPopup, …) | **`targets.web.client`** — the client-runtime settings blob, whole, under its new name (#1: `web_manager` → `client`, since it configures `@omega.js/client`; WebManager is not an OMEGA concept). No dual-read: the old key name is not honored anywhere |
1870
- | `web_manager.sentry` (`{ enabled, config: {…} }`) | **`monitoring: { enabled, providers: { sentry: {…} } }`** (top level) — the SDK knobs inside `config` ARE the provider block ([#485](https://github.com/Omega-JS-Stack/omega/issues/485)). Leaving them in the client blob meant the converted brand had no `monitoring` key at all, so the manager's monitoring service skipped every run and no Sentry project was ever reconciled |
1871
- | `web_manager.cookieConsent` (incl. `palette`, `theme`, `type`, `content.dismiss`) | **`targets.web.client.consent`** — the block that became a real gate (#383). `palette`/`theme` are gone (the panel paints from the `--omega-*` tokens); `type` is gone (the visitor's region picks opt-in vs opt-out); `content.dismiss` is now `content.accept`, beside `content.customize`, `content.panelIntro`, `content.acceptAll` and `content.acceptNone` (#391 retired `content.save` with the Save button) |
1872
- | `web_manager.chatsy` (agentId + widget settings) | **`inbound.chat.providers.chatsy`** — the chat widget left the client blob for the one chat home the manager also provisions (#23) |
1873
- | `socials` | **`socials`** (top level) — a SHARED_SCHEMA key, so the root is its home and the scaffold emits it there ([#483](https://github.com/Omega-JS-Stack/omega/issues/483)). A web load resolves `targets.web.socials` identically, which is why the converter used to leave it in the target; the handles are brand identity, read past the website too |
1874
- | `translation` | **`translation`** (top level): a SHARED_SECTIONS key, so the root is its home and the scaffold emits it there ([#526](https://github.com/Omega-JS-Stack/omega/issues/526)). A web load resolves `targets.web.translation` identically, which is why the converter used to leave it in the target; but disperse copies the SHARED sections, so a target-scoped engine config is invisible to every other target that translates (the extension's `_locales`). `translation.include` stays a web-only key at that shared home, and the legacy `exclude` list CONVERTS to it on the way through ([#858](https://github.com/Omega-JS-Stack/omega/issues/858)): `exclude: ['docs']` becomes `include: ['**', '!docs']`, one note, so the brand keeps translating exactly what it was |
1875
- | `meta` | **`targets.web.meta.index`, and nothing else**: the title/description half is DROPPED ([#607](https://github.com/Omega-JS-Stack/omega/issues/607), Ian 2026-08-26: meta never exists in two places): the site-wide defaults are `brand.name` / `brand.description` (what @omega.js/web's head falls back to) and everything per-page is that page's own `meta:` frontmatter ([docs/web/frontmatter.md](../web/frontmatter.md)). The `index` half lives on under the SAME name a page writes ([#564](https://github.com/Omega-JS-Stack/omega/issues/564), Ian 2026-09-09). `omega migrate` drops the legacy title/description with a note, and a config still carrying `meta.title` / `meta.description` is a retired-key error |
1876
- | `download`, `extension`, `favicon`, `manifest`, `icons`, `currency` | **DROPPED with a note, every one** (nothing lands under `targets.web`): `download` / `extension` derive from the desktop and extension targets ([#610](https://github.com/Omega-JS-Stack/omega/issues/610)); `favicon`, `manifest` and `icons` retire with [#850](https://github.com/Omega-JS-Stack/omega/issues/850) (the favicon set is minted and `theme-color` comes from `brand.color`, nothing reads a manifest block, and a link carries its own `fa-*` classes). `currency` MOVES to **`payment.currency`**. Each one is a registered retired path, so carrying it forward would write a config the validator refuses |
1877
- | `recaptcha` (incl. `site-key`) | **`captcha.providers.recaptcha`** (`siteKey` — every key is camelCase, #23) |
1878
- | `cloudflare` (the purge `zone`) | **`edge.providers.cloudflare`** — one cloudflare home, shared with the manager's zone reconciliation (#23) |
1879
- | `advertising.google-adsense` (flat or under `providers`) | **`advertising.providers.adsense`** with camelCase slots (`displaySlot`, `inArticleSlot`, `inFeedSlot`, `multiplexSlot`) — provider ids drop the vendor prefix (#23). An `enabled` or `units` gate beside them is DROPPED with a note ([#628](https://github.com/Omega-JS-Stack/omega/issues/628)): `client` presence is the one switch, so carrying the gate forward would validate as retired while the id turned the account and the units back on |
1880
- | `permalink`, `pagination`, `collections`, `defaults`, `generators` | `targets.web.<same key>` (codemod rule 8's home — engine consumption of custom collections rides the consumer-theme waves) |
1881
- | UJM-json `distribute`, `sass.purgecss`, `imagemin`, `github.workflows` | `targets.web.{distribute,purgecss,imagemin,workflows}` — `imagemin` is LIVE (schema-known; `enabled: false` ships images verbatim, otherwise the build-time 320/640/1024 + webp matrix runs), and `purgecss.safelist` is LIVE too ([#250](https://github.com/Omega-JS-Stack/omega/issues/250)): schema-known (`{ standard, deep, greedy, keyframes }` arrays, or a bare array for `standard`), it merges over the framework's built-in safelist in the purge pass |
1882
- | UJM-json `webpack`, `gems`; `_config.yml` Jekyll machinery (`plugins`, `exclude`, …) | dropped, noted in the report |
1883
- | secret-shaped keys anywhere | dropped + warned — move to `.env` |
1884
-
1885
- Beyond config: the codemod rule table runs over `src/**` templates, the seed
1886
- `src/assets/js/main.js` is deleted (core main + boot runtime replace it;
1887
- customized ones are flagged with the port recipe), `main.scss`'s
1888
- `@use 'ultimate-jekyll-manager' with (…)` becomes `@use 'omega:main' with (…)`,
1889
- page-css self-`@use` lines are dropped, and Gemfile/Gemfile.lock/the legacy
1890
- configs are removed.
1891
-
1892
- ### omega-manager brand configs (`.brands/{id}/config.json`)
1893
-
1894
- The brand-config `targets` ARRAY's role is absorbed by key presence in the omega.json5
1895
- `targets` object. omega-manager's disperse writes omega.json5 from brand config + state at
1896
- its Phase-3 cutover (enumerating `SHARED_SECTIONS`, per-surface values into
1897
- `targets.<name>` overrides).
1898
-
1899
- ## Package API (quick reference)
1900
-
1901
- ```js
1902
- const {
1903
- loadConfig, // (projectDir, target?, { defaults }?) → { config, errors, warnings, enabled, name, files }
1904
- composeTargetConfig, // (projectDir, target) → { config, files } — company+brand+local frozen into ONE self-contained file (deploy upload boundary, #31)
1905
- hasOmegaConfig, // (projectDir) → boolean — "is this project migrated?"
1906
- resolveConfigPath, // (projectDir) → abs path | null
1907
- getEnabledTargets, // (config) → ['web', 'backend', …]
1908
- findBrandRoot, // (projectDir) → brand root | null — CLASSIFIES one target dir (THE hierarchy rule)
1909
- findBrandConfigPath, // (projectDir) → the BRAND layer's omega.json5 | null — the file a target with no local-layer file rides
1910
- resolveBrandRoot, // (startDir) → brand root | null — SEARCHES upward from anywhere (standalone → itself), bounded at the nearest .git
1911
- loadEnv, // (startDir) → { chain, loaded } — resolve + load the .env cascade
1912
- reloadEnv, // (startDir, options?) → same — drops the FILE-owned keys, then loads again, so an EDITED value lands and the shell still wins (#724)
1913
- resolveEnvChain, // (startDir) → { local, brand, company } .env paths (no loading)
1914
- loadEnvChain, // (paths) → loaded[] — dotenv strongest-first, nulls/missing skip
1915
- resolveCompany, // (brandRoot) → { id, name, url, images, root, dir, file(relPath), config }: the ONE company resolver (#677)
1916
- recordBrand, // ({ id, root, name, url }) → wrote?: the machine registry line every loadConfig refreshes
1917
- readRegistry, // () → { <brand.id>: { root, name, url, updatedAt } }: ~/.omega/brands.json (OMEGA_HOME moves it)
1918
- COMPANY_RESOLVED_FILE, // 'config/company-resolved.json5': the generated layer a deploy hands a runner
1919
- resolveHook, // (startRoot, 'account/password') → hook file | null (brand → company)
1920
- loadHook, // (startRoot, hookPath) → { fn, file } | null — broken hooks THROW
1921
- validateConfig, // (config, { target }?) → { errors, warnings }
1922
- runSchema, // low-level rule walker (EM's proven engine)
1923
- formatErrors, // errors → numbered block
1924
- resolvedBrandHost, // (config) → the BRAND's own host (`brand.url`, top-level `url` only as fallback), lowercased | '' — authDomain validation AND every persona address (#708)
1925
- findSecretKeys, // (object) → dot-paths of secret-shaped keys
1926
- findRetiredKeys, // (object) → [{ path, key, replacement, why }] — renamed-outright keys (#142)
1927
- chosenProvider, // (role.providers) → the picked provider name | null — presence is the pick, `false` the off switch (#425)
1928
- backendProjectType, // (targets.backend | resolved backend config) → 'firebase' | 'custom' — how the backend runs (#584)
1929
- BACKEND_PROJECT_TYPES,
1930
- applyConfigEdits, // (source, edits, { comments }?) → edited source — comment-preserving (see Writeback)
1931
- writeConfigValues, // (projectDir, edits, { dryRun, comments }?) → { path, changed, applied }
1932
- applyConfigRemovals, // (source, paths) → edited source — deletes a key, its subtree and its own comment (#612)
1933
- removeConfigValues, // (projectDir, paths, { dryRun }?) → { path, changed, removed } — absent paths skip, so reruns are byte-identical
1934
- schemaDefaults, // (target?) → the schema's own defaults, the merge chain's lowest layer (#478)
1935
- missingDefaults, // (rawConfig, target?) → [{ path, value }] — the blocks a brand file lacks (the manage heal's list)
1936
- defaultComments, // (target?) → { 'dot.path': description } — the guiding comments a materialized block carries
1937
- deepMerge, // agnostic layer merge
1938
- // The targets map (targets.js: every key is a NAME, #886)
1939
- targetEntries, // (config) → [{ name, type, …entry }] in config order; throws on a missing/unknown type
1940
- targetsOfType, // (config, 'web') → the same entries, that type only
1941
- targetPath, // (config, 'community') → 'targets/community'; an undeclared name throws with the declared list
1942
- targetNameFromDir, // (projectDir) → the target root's basename inside a brand (functions//dist/ normalize up); null standalone
1943
- targetUrl, // (config, name) → entry url → entry brand.url → brand.url when name IS the type → https://<name>.<brand host> | null
1944
- targetPortOffset, // (config, name) → position among the SAME-type targets (dev-port offsets)
1945
- brandHost, // (url) → the host exactly as brand.url states it (www kept, path dropped, port kept)
1946
- TARGET_NAME_PATTERN, TARGET_TYPES, // the name slug rule and [web, backend, desktop, extension, mobile, custom]
1947
- // The browser subset (#894): what a browser surface may see, and the one wrapper it is baked in
1948
- clientConfig, // (resolved + this build's facts) → the blob every browser surface bakes as OMEGA_BUILD_JSON.config
1949
- CLIENT_FACT_KEYS, // the build facts that ride beside the client sections (runtime, environment, version, buildTime, target, url, dev)
1950
- TARGETS, SHARED_SECTIONS, CLIENT_SECTIONS, SHARED_SCHEMA, TARGET_SCHEMAS,
1951
- } = require('@omega.js/config');
1952
- ```