@omega.js/desktop 0.53.0 → 0.54.1

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 (161) hide show
  1. package/README.md +38 -38
  2. package/dist/cli-run.js +4 -1
  3. package/dist/cli.js +2 -2
  4. package/dist/commands/cdp/client.js +1 -1
  5. package/dist/commands/cdp.js +1 -1
  6. package/dist/commands/clean.js +2 -3
  7. package/dist/commands/dev.js +25 -0
  8. package/dist/commands/lib/ensure-target.js +12 -17
  9. package/dist/commands/lib/migrate.js +17 -0
  10. package/dist/commands/logs.js +1 -1
  11. package/dist/commands/release.js +1 -1
  12. package/dist/commands/test.js +4 -4
  13. package/dist/commands/update.js +5 -4
  14. package/dist/defaults/.github/workflows/build.yml +18 -18
  15. package/dist/defaults/_.gitignore +0 -2
  16. package/dist/defaults/_mas/README.md +3 -3
  17. package/dist/defaults/config/certs/README.md +1 -1
  18. package/dist/defaults/config/omega.json5 +36 -36
  19. package/dist/defaults/docs/README.md +3 -3
  20. package/dist/defaults/gulpfile.js +1 -1
  21. package/dist/defaults/hooks/build/post.js +1 -1
  22. package/dist/defaults/hooks/build/pre.js +1 -1
  23. package/dist/defaults/hooks/notarize/post.js +2 -2
  24. package/dist/defaults/hooks/release/post.js +1 -1
  25. package/dist/defaults/hooks/release/pre.js +1 -1
  26. package/dist/defaults/src/assets/scss/pages/about.scss +1 -1
  27. package/dist/defaults/src/assets/scss/pages/main.scss +1 -1
  28. package/dist/defaults/src/assets/scss/pages/settings.scss +1 -1
  29. package/dist/defaults/src/integrations/context-menu/index.js +11 -11
  30. package/dist/defaults/src/integrations/menu/index.js +5 -5
  31. package/dist/defaults/src/integrations/tray/index.js +9 -9
  32. package/dist/defaults/src/main.js +2 -2
  33. package/dist/defaults/src/preload.js +1 -1
  34. package/dist/defaults/test/README.md +3 -3
  35. package/dist/defaults/test/_init.js +1 -1
  36. package/dist/gulp/tasks/audit.js +5 -8
  37. package/dist/lib/restart-manager/index.js +1 -1
  38. package/dist/lib/restart-manager/install.js +1 -1
  39. package/dist/lib/restart-manager/protocol.js +1 -1
  40. package/dist/main.js +4 -3
  41. package/dist/preload.js +1 -1
  42. package/dist/test/suites/build/audit.test.js +20 -7
  43. package/dist/test/suites/build/build-workflow-jobs.test.js +2 -2
  44. package/dist/test/suites/build/cli.test.js +28 -0
  45. package/dist/test/suites/build/defaults-em-dash.test.js +22 -0
  46. package/dist/test/suites/build/defaults-scaffold.test.js +19 -5
  47. package/dist/test/suites/build/deploy-direct.test.js +7 -5
  48. package/dist/test/suites/build/deploy-dispatch.test.js +2 -1
  49. package/dist/test/suites/build/deploy-hook.test.js +4 -2
  50. package/dist/test/suites/build/dev-verb.test.js +67 -0
  51. package/dist/test/suites/build/ensure-target.test.js +11 -3
  52. package/dist/test/suites/build/merge-line-files.test.js +6 -6
  53. package/dist/test/suites/build/migrate.test.js +29 -0
  54. package/dist/test/suites/build/project-scripts-deps.test.js +6 -10
  55. package/dist/test/suites/build/runner-env-write.test.js +73 -0
  56. package/dist/test/suites/build/runner.test.js +9 -8
  57. package/dist/test/suites/build/setup-scripts.test.js +27 -0
  58. package/dist/test/suites/build/validate-config.test.js +13 -2
  59. package/dist/test/suites/build/verb-logs.test.js +20 -0
  60. package/dist/test/suites/renderer/window-desktop-surface.test.js +1 -1
  61. package/dist/utils/build-pipeline.js +4 -4
  62. package/dist/utils/runner-env.js +13 -28
  63. package/dist/vendor/config/company.js +46 -14
  64. package/dist/vendor/config/defaults.js +30 -7
  65. package/dist/vendor/config/edit.js +25 -3
  66. package/dist/vendor/config/env-delivery.js +1 -1
  67. package/dist/vendor/config/env-schema.js +3 -6
  68. package/dist/vendor/config/env.js +34 -22
  69. package/dist/vendor/config/index.js +13 -17
  70. package/dist/vendor/config/load.js +15 -7
  71. package/dist/vendor/config/repo.js +10 -27
  72. package/dist/vendor/config/schema-client.js +64 -0
  73. package/dist/vendor/config/schema-cloud.js +38 -0
  74. package/dist/vendor/config/schema-manager.js +118 -0
  75. package/dist/vendor/config/schema-overrides.js +68 -0
  76. package/dist/vendor/config/schema.js +99 -152
  77. package/dist/vendor/config/validate.js +97 -77
  78. package/dist/vendor/devkit/agents-md.js +233 -0
  79. package/dist/vendor/devkit/attach-log-file.js +15 -1
  80. package/dist/vendor/devkit/ci-workflows.js +30 -30
  81. package/dist/vendor/devkit/cli-router.js +13 -7
  82. package/dist/vendor/devkit/defaults-engine.js +9 -43
  83. package/dist/vendor/devkit/deploy-snapshot.js +44 -9
  84. package/dist/vendor/devkit/env-lines.js +183 -0
  85. package/dist/vendor/devkit/local.js +62 -10
  86. package/dist/vendor/devkit/lockfile.js +32 -13
  87. package/dist/vendor/devkit/logger.js +7 -2
  88. package/dist/vendor/devkit/merge-line-files.js +219 -176
  89. package/dist/vendor/devkit/omega-bin.js +208 -111
  90. package/dist/vendor/devkit/preludes/docs-sync.js +52 -0
  91. package/dist/vendor/devkit/preludes/index.js +1 -0
  92. package/dist/vendor/devkit/target-picker.js +45 -0
  93. package/dist/vendor/devkit/test/dashed-files.js +37 -0
  94. package/dist/vendor/devkit/test/run-verb-under-tee.js +71 -0
  95. package/dist/vendor/devkit/update.js +15 -15
  96. package/dist/vendor/devkit/verb-scripts.js +40 -0
  97. package/dist/vendor/devkit/verbs.js +170 -0
  98. package/package.json +18 -24
  99. package/dist/commands/install.js +0 -37
  100. package/dist/defaults/AGENTS.md +0 -119
  101. package/dist/defaults/CLAUDE.md +0 -1
  102. package/dist/vendor/config/env-retired.js +0 -137
  103. package/dist/vendor/config/retired-keys.js +0 -635
  104. package/docs/analytics.md +0 -140
  105. package/docs/app-state.md +0 -92
  106. package/docs/audit.md +0 -69
  107. package/docs/auth.md +0 -284
  108. package/docs/auto-updater.md +0 -243
  109. package/docs/boot-sequence.md +0 -44
  110. package/docs/build-system.md +0 -169
  111. package/docs/cdp-debugging.md +0 -169
  112. package/docs/common-mistakes.md +0 -21
  113. package/docs/config-schema.md +0 -120
  114. package/docs/context-menu.md +0 -112
  115. package/docs/context.md +0 -81
  116. package/docs/css.md +0 -84
  117. package/docs/deep-link.md +0 -186
  118. package/docs/environment-detection.md +0 -112
  119. package/docs/fontawesome.md +0 -109
  120. package/docs/hooks.md +0 -89
  121. package/docs/icons.md +0 -79
  122. package/docs/index.md +0 -328
  123. package/docs/installer-options.md +0 -165
  124. package/docs/ipc.md +0 -61
  125. package/docs/lib-modules.md +0 -53
  126. package/docs/logging.md +0 -227
  127. package/docs/menu.md +0 -160
  128. package/docs/releasing.md +0 -239
  129. package/docs/remote-config.md +0 -118
  130. package/docs/remote-scripts.md +0 -144
  131. package/docs/restart-manager.md +0 -144
  132. package/docs/runner.md +0 -290
  133. package/docs/sentry.md +0 -97
  134. package/docs/shared/agent-docs.md +0 -89
  135. package/docs/shared/analytics.md +0 -612
  136. package/docs/shared/brands.md +0 -57
  137. package/docs/shared/breaking-changes.md +0 -917
  138. package/docs/shared/config.md +0 -1948
  139. package/docs/shared/deploys.md +0 -341
  140. package/docs/shared/icons.md +0 -219
  141. package/docs/shared/local-dev.md +0 -167
  142. package/docs/shared/logging.md +0 -205
  143. package/docs/shared/monitoring.md +0 -167
  144. package/docs/shared/publishing.md +0 -187
  145. package/docs/shared/rulings.md +0 -34
  146. package/docs/shared/testing.md +0 -147
  147. package/docs/shared/theming.md +0 -629
  148. package/docs/shared/translation.md +0 -342
  149. package/docs/shared/updates.md +0 -61
  150. package/docs/signing.md +0 -293
  151. package/docs/startup.md +0 -142
  152. package/docs/storage.md +0 -59
  153. package/docs/templating.md +0 -101
  154. package/docs/test-boot-layer.md +0 -157
  155. package/docs/test-framework.md +0 -362
  156. package/docs/themes.md +0 -149
  157. package/docs/tooltips.md +0 -99
  158. package/docs/tray.md +0 -164
  159. package/docs/usage.md +0 -58
  160. package/docs/verts.md +0 -62
  161. package/docs/windows.md +0 -149
@@ -1,917 +0,0 @@
1
- # Breaking changes — legacy → OMEGA
2
-
3
- The register of every contract that changed SHAPE between a legacy framework and
4
- its OMEGA successor, with the by-hand migration step for each. Read it once per
5
- legacy brand: work down the sections that apply and the brand lands on the new
6
- contracts in one pass.
7
-
8
- **No framework dual-reads a legacy form.** An old key, an old name, an old file
9
- is not "deprecated but accepted" — it is unknown, and the loud ones fail
10
- validation ([#142](https://github.com/Omega-JS-Stack/omega/issues/142)). The only
11
- sanctioned legacy-reading paths are the one-time converters: `npx omega migrate`
12
- for a UJM website, and the mapping tables in
13
- [config.md](config.md#migration--legacy-configs--omegajson5) for every other
14
- target.
15
-
16
- **OMEGA-to-OMEGA changes are by hand, and they live here** (Ian 2026-09-11,
17
- [#885](https://github.com/Omega-JS-Stack/omega/issues/885)): `omega migrate` converts a
18
- LEGACY brand only. A shape OMEGA changes its own mind about between two versions is a
19
- dated section in this register with its by-hand step, never a migrate rule; the few
20
- brands in the in-between state convert by hand. [#888](https://github.com/Omega-JS-Stack/omega/issues/888)
21
- makes `omega update` print the sections due since the installed version.
22
-
23
- **Boundaries.** What never got PORTED is [#78](https://github.com/Omega-JS-Stack/omega/issues/78)'s
24
- gap tables, not this file. Converter TOOLING is
25
- [#40](https://github.com/Omega-JS-Stack/omega/issues/40) — these rows are its
26
- input, not its implementation. The legacy repos stay read-only reference
27
- (AGENTS.md HARD RULE 1): nothing here asks you to change them.
28
-
29
- ## 2026-09-25: one runtime shape on every package ([#945](https://github.com/Omega-JS-Stack/omega/issues/945))
30
-
31
- Every package's default export is ONE ready-made instance, `omega`, and a consumer never
32
- writes `new`. `initialize(options)` returns that instance on every surface; on the async
33
- surfaces `omega.ready` is the same promise, so a module that did not call it can still await
34
- it. The class is exported by name (`Omega`) for tests only. Accessors are properties, never
35
- zero-arg methods. `auth.user` is always a `User` from `@omega.js/account`: the stored account
36
- document as own fields, the derived facts as getters, and a signed-out `User` (`authenticated`
37
- false, `plan` basic) instead of null. The `@omega.js/manager` package, its `omega manage` verb
38
- and its banners keep their names.
39
-
40
- | Contract | Old form | New form | Manual migration step |
41
- |---|---|---|---|
42
- | Backend consumer entry (`src/index.js`) | `const Manager = (new (require('@omega.js/backend'))).init(exports, { … });` then `const { functions } = Manager.libraries;` | `const omega = require('@omega.js/backend');`, then `omega.initialize({ … });`, and `module.exports = omega.functions;` as the file's last line | Rewrite the two lines and add the export line. A function of your own joins the map before it: `omega.functions.items = omega.firebase.functions.region(omega.project.resourceZone).https.onRequest((req, res) => omega.routes.run('items', { req, res }));` |
43
- | Desktop consumer entries (`src/main.js`, `src/preload.js`, each `src/assets/js/components/<view>/index.js`) | `new (require('@omega.js/desktop/main'))().initialize()`, or a `manager` instance destructured after `initialize()` | `const omega = require('@omega.js/desktop/main');` (or `/preload`, `/renderer`), then `omega.initialize().then(() => { const { logger, windows } = omega; })` | Rewrite each entry; every `manager.` becomes `omega.` |
44
- | Extension contexts (`background`, `popup`, `sidepanel`, `options`, `page`, `content`, `offscreen`) | `import Manager from '@omega.js/extension/popup'; const manager = new Manager(); await manager.initialize();`, with the client read as `manager.omega` | `import omega from '@omega.js/extension/popup'; await omega.initialize();`: the instance IS the runtime, and the four page contexts carry the client's modules on it (`omega.auth`, `omega.storage`, `omega.bindings`) | Rewrite each context's entry and drop the `omega` destructure. The bare `@omega.js/extension` import is gone: import the context entry |
45
- | Web service worker (`src/service-worker.js`) | `import Manager from '@omega.js/web/service-worker'; const manager = new Manager(); manager.initialize()` | `import omega from '@omega.js/web/service-worker'; omega.initialize()`; the framework file is `sw/omega.js` | Rewrite the three lines |
46
- | Web page, layout, section and global modules | `export default ({ manager, options }) => { }`, and `import omega from '@omega.js/client'` for the client | `export default async ({ omega, options }) => { }`; a module that needs the instance outside that call imports `@omega.js/web/runtime` | Rename the argument, and repoint every `@omega.js/client` default import at `@omega.js/web/runtime` |
47
- | A theme's `_theme.js` | A side-effect module: its initializers ran on import | `export default async ({ omega, options }) => { }`, which the host calls with the booted instance | Move the theme's initializers into the default export |
48
- | Web page features | `omega.library().showExitPopup()` and the rest of the `omega.library()` / `omega._library` bag | Properties of the web instance: `omega.appearance`, `omega.shell`, `omega.motion`, `omega.exitPopup.show()` (`exitPopup` is null when the popup is off) | Rewrite each `omega.library()` read to the property |
49
- | Client accessors | `omega.auth()`, `omega.utilities().escapeHTML(x)`, `omega.firestore()`, `omega.bindings()`, `omega.storage()`, `omega.dom()`, `omega.sentry()`, and the rest | `omega.auth`, `omega.utilities.escapeHTML(x)`, `omega.firestore`, `omega.bindings`, `omega.storage`, `omega.dom`, `omega.sentry` | Drop the `()` after every module name; a missed site throws `is not a function` |
50
- | `@omega.js/client` default export | A live singleton, imported anywhere | The base class `Omega` and no instance: web, the extension page contexts and the desktop renderer each export the one instance | Import the instance from the host framework (`@omega.js/web/runtime`, the extension context, `@omega.js/desktop/renderer`), never from `@omega.js/client` |
51
- | The signed-in user | `auth.getUser()` (the Firebase profile, or null), `auth.isAuthenticated()`, `auth.resolveSubscription(account)` | `auth.user`, a `User`: `authenticated`, `uid`, `email`, `plan`, `active`, `trialing`, `cancelling`, `everPaid`, the stored fields (`user.roles`, `user.subscription`), and `user.profile.{displayName,photoURL,emailVerified}` | Replace each call with the property. A `if (user)` check reads `user.authenticated`, since `auth.user` is never null |
52
- | The auth listener state | `auth.listen((state) => …)` with `{ user, account, resolved, accountDenied }` | `{ user, denied }`: `user` is the one `User`, `denied` is true only when rules refused the account read. `auth.reload()` re-reads the account and resolves with the new state | Read `state.account.*` and `state.resolved.*` off `state.user`, and `state.accountDenied` as `state.denied` |
53
- | Bindings | Three roots: `auth.user` (the Firebase profile), `auth.account.*` and `auth.resolved.*` | ONE root, `auth.user`: `auth.user.plan`, `auth.user.active`, `auth.user.roles.admin`, `auth.user.profile.displayName`, `auth.user.profile.photoURL`. `usage` stays its own root | Rewrite each `data-omega-bind`: `auth.account.x` and `auth.resolved.x` read `auth.user.x`, `auth.user.displayName` reads `auth.user.profile.displayName`, and `@show auth.user` reads `@show auth.user.authenticated` |
54
- | `FormManager` | `new FormManager('#form', options)`, the class importing the singleton | `new FormManager(omega, '#form', options)` | Pass the instance first |
55
- | Click triggers | `registerTrigger('name', handler)` from `@omega.js/client/modules/triggers.js` | `omega.triggers.register('name', handler)` | Rewrite each registration; the class stays `omega-<name>` |
56
- | Extension auth page and messaging | `openAuthPage()`; `messenger.onMessage = (message, sender, sendResponse) => { }`; `messenger` absent in background and offscreen | `omega.auth.openPage()`; `messenger.onMessage(handler)`, which returns the unsubscribe; `messenger` on every context, and background's `omega.auth.user` built from the whole account document | Rewrite the call and every `onMessage` assignment |
57
- | Desktop main auth | `manager.omega` (the client bridge): `getCurrentUser()`, `onAuthChange(fn)`, `getResolvedPlan()`, `getResolvedRoles()` | `omega.auth`: `.user` (a `User` built from the whole account document), `.listen(fn)`, `.signOut()`, `.getIdToken()`, `.handleToken()` | `getCurrentUser()` reads `omega.auth.user`, `onAuthChange` is `listen`, and the plan and roles read `omega.auth.user.plan` and `omega.auth.user.roles` |
58
- | Desktop renderer | `manager.omega` for the client; `manager.storage` and `window.desktop.*` for the main process | The renderer instance extends the client (`omega.auth`, `omega.firestore`); everything that crosses to main is `omega.desktop.{ipc,storage,theme,fontawesome,autoUpdater,analytics,context,usage,remoteConfig}`. `omega.storage` is the page store; the app store is `omega.desktop.storage` | Read the client off the instance, and the app store as `omega.desktop.storage` |
59
- | Desktop integrations (`src/integrations/<tray,menu,context-menu>/index.js`) | `module.exports = ({ manager, tray }) => { }` | `module.exports = ({ omega, tray }) => { }` (menu and context menu likewise) | Rename the argument |
60
- | Desktop test harness | Boot `inspect` bodies received `{ manager, expect, … }`; the harness wrote `__EM_TEST__` lines on stdout | `{ omega, expect, … }`; the prefix is `__OMEGA_TEST__` | Rename the argument, and any grep of a run's stdout |
61
- | Desktop logger export | `require('@omega.js/desktop/lib/logger')` | `require('@omega.js/desktop/build').logger(name)` at build time, `omega.logger` at runtime | Repoint the require |
62
- | Build modules (`@omega.js/desktop/build`, `@omega.js/extension/build`) | `const Manager = new (require('@omega.js/extension/build')); Manager.getConfig();`; hook context `{ manager, projectRoot, mode }` | `const build = require('@omega.js/extension/build'); build.getConfig();` (no class, no `new`); hook context `{ build, projectRoot, mode }` | Rewrite each hook's require and its destructure |
63
- | Backend route handler | `async ({ Manager, ctx, analytics, usage, user, settings, libraries, utilities }) => { }` | `async ({ ctx, omega, user, data, usage, analytics }) => { }`; everything in the list is also on `ctx` | Rename `settings` to `data`, `Manager` to `omega`; `libraries.admin` reads `omega.firebase.admin`, `utilities` reads `omega.utilities` |
64
- | Backend event and cron handlers | Events `({ Manager, ctx, libraries, user, context, change, snapshot })`; cron `({ Manager, ctx, context, libraries })` | Events `({ ctx, omega, user, context, change, snapshot })`; cron `({ ctx, omega, context })` | Same renames as a route |
65
- | Backend `ctx` | `RouteContext`: `ctx.Manager`, `ctx.getUser()`, `ctx.request.user`, `ctx.resolvedUser`, `ctx.settings`, `ctx.ref`, `ctx.constant.pastTime` | `Context`: `ctx.omega`, `ctx.user` (a `User`), `ctx.data`, `ctx.req` / `ctx.res`; request services built on first read: `ctx.usage`, `ctx.analytics`, `ctx.email`, `ctx.ai`, `ctx.metadata(doc)` | Rewrite each read. `ctx.constant.pastTime` has no replacement |
66
- | Backend instance | `Manager.libraries.admin` / `.functions`, the factories (`Manager.User()`, `Manager.Usage()`, `Manager.Analytics()`, `Manager.Settings()`, `Manager.Utilities()`, `Manager.AI()`, `Manager.Email()`, `Manager.Metadata()`), `Manager.Middleware(req, res).run('items', options)` | `omega.firebase.admin` / `omega.firebase.functions` (the SDK; `omega.functions` is the exported map); the request services on `ctx` and the process services on `omega` (`omega.utilities`, `omega.email`, `omega.ai`, `omega.storage({ name })`); `omega.routes.run('items', { req, res }, options)` | A route never constructs a service: read it off `ctx` or `omega` |
67
- | Backend events from a consumer's own trigger | `Manager.EventMiddleware(payload).run('users/on-create')` | `omega.events.run('users/on-create', payload)`: the dispatcher the framework's own triggers use; a relative name loads `<cwd>/events/<name>.js`, a leading `/` is verbatim | Rewrite each trigger: `.onCreate((user, context) => omega.events.run('users/on-create', { user, context }))` |
68
- | Backend consumer routes | A consumer route rode `omega_api` at `/omega/<name>` and won over a framework route of the same name; `omega.run(name, req, res, options)` and `omega.runEvent(name, payload)` | `omega_api` and `/omega/*` serve only the framework's routes and the MCP endpoint. A consumer route is its own function, `omega.routes.run(name, { req, res }, options)` in the region `omega.project.resourceZone`, behind its own hosting rewrite at its own path (`/notes`); `omega.events.run(name, payload)` runs a trigger's handler. A consumer MCP tool's `path` is the path as served (`/notes`) | Give each route its own function and a `{ "source": "{/notes,/notes/**}", "function": "notes" }` rewrite after the `omega_api` one, repoint every caller from `/omega/<name>` to `/<name>`, prefix each consumer tool `path` with `/`, and rename `omega.run`/`omega.runEvent` |
69
- | Backend `initialize()` options | `setupFunctionsIdentity`, and the test-runner switches `initialize`, `setupFunctions`, `setupServer`, `log` | `identity`; the switches are gone (`OMEGA_TEST_RUNNER` decides) | Rename `setupFunctionsIdentity`; delete the switches |
70
- | Backend pipeline options | `setupSettings`, `includeNonSchemaSettings`, `parseMultipartFormData` | `validate`, `includeUnknown`, `parseMultipart` | Rename each option passed to `omega.routes.run()` |
71
- | Config environment mixin | `attachTo(Manager)` / `attachEnvironment` from `@omega.js/config` | Each instance calls `getEnvironment()` and the three checks directly | Delete the mixin call |
72
- | Removed outright (backend) | `functions/_legacy/` and the `setupFunctionsLegacy` option, `functions/wrappers/mailchimp/addToList.js`, `helpers/api-manager.js` (`ApiManager`), `helpers/roles.js` (`Roles`), `Manager.install()`, `Manager.debug()`, `self.interface`, `libraries.localDatabase` and `initializeLocalStorage`, `fetchStats`, `server-manager.js` | Gone | Delete every reference; `ctx.usage` covers what `ApiManager` counted |
73
- | The `/backend-manager/*` URL alias | The router stripped the prefix, the edge worker forwarded it, and the `omega_api` hosting rewrite listed `/backend-manager` and `/backend-manager/**` | Gone: a call to the old prefix answers 404 | Repoint every caller at `/omega/*`, and drop the two sources from the brand's `firebase.json` rewrite |
74
- | Removed outright (frontends) | Client `omega.library()` / `omega._library`; extension `openAuthPage()`, the root `.` export, the `attachTo` mixins; desktop `manager.omega`, `wmBridge`, `./lib/logger`, `__EM_TEST__` | Gone; each row above names its replacement | Rewrite per the rows above |
75
-
76
- ## 2026-09-25: one request-schema system ([#823](https://github.com/Omega-JS-Stack/omega/issues/823))
77
-
78
- A route's schema file exports a function of the request and returns a plain field
79
- declaration. ONE adapter turns that declaration into a zod schema and validates with it, so
80
- zod is the only validator underneath and a schema stays a plain object until it runs: a split
81
- on the plan, the query, the path or any other request fact is ordinary code
82
- (`if (user.plan === 'pro') fields.limit.max = 200;`). Defaults still coerce and never reject.
83
- The hand-rolled engine and the zod builder layer are both gone. The full vocabulary and the
84
- split kinds: [schemas.md](../../packages/backend/docs/schemas.md).
85
-
86
- | Contract | Old form | New form | Manual migration step |
87
- |---|---|---|---|
88
- | The schema function's argument | `({ ctx, user, data, method, headers, geolocation, client })` | `({ user, body, query, path, method, headers, geolocation })`: `user` is the caller's `User`, `body` and `query` are the raw parts (the route still receives their validated merge as `data`) | Read `data.x` as `body.x` or `query.x`, `user.subscription.product.id` as `user.plan`; a schema no longer reaches `ctx` or `client` |
89
- | A field | `{ types: ['string'], default: '' }`, or a builder: `f.string({ default: '' })`, `f.number(…)`, `f.array(…)`, `f.passthrough(…)`, `f.multi(['string', 'number'], …)`, `f.any(…)` | `{ type: 'string', default: '' }`; `type` is one of `string number boolean array object any`, or a list of them | Rewrite each field; `f.passthrough` is `type: 'object'`, `f.multi(list)` is `type: list` |
90
- | Nesting | A nested plain object, or `f.object({ … })` around the whole schema | The function returns the declaration itself; a nested object is `{ type: 'object', fields: { … } }`, array items are `of: { … }` | Return the map directly and wrap each nested group in `fields` |
91
- | `required` | A boolean or a function (`required: () => isPremium`) | A boolean computed in the schema function (`required: isPremium`), never paired with `default` | Compute the condition before the declaration and assign the boolean |
92
- | Path ids | A `default` computed from the request path, plus `min: 1` | `{ type: 'string', path: true }`, filled from the request path's trailing segments in declaration order | Replace the computed default with `path: true` |
93
- | Field keys | `types`, `default`, `value`, `min`, `max`, `required`, `clean`, `sanitize`, `enum` | `type`, `default`, `value`, `min`, `max`, `required`, `clean`, `sanitize`, `enum`, plus `pattern` (a RegExp a sent value must match), `path`, `of` and `fields`; any other key throws, naming its dot-path | Rename `types` to `type`; nothing else to migrate |
94
- | Removed | `helpers/schema-engine.js`, `helpers/schema-zod.js` and its `fields` builders, and a schema file exporting raw zod | Gone: `helpers/schema.js` is the one adapter | Delete the builder require from every schema file |
95
-
96
- ## 2026-09-14: the companion extension leaves `@omega.js/manager` ([#927](https://github.com/Omega-JS-Stack/omega/issues/927))
97
-
98
- The companion extension lived at `@omega.js/manager`'s `extension/`, a whole extension
99
- project inside a published package: it carried another product's identity, shipped to no
100
- store, and was cut from the npm tarball, which meant the router's `omega-extension` upstream
101
- could not start on any install but a monorepo checkout. It is the **OMEGA Companion** now,
102
- the OMEGA brand's own extension target (`omega-omega/targets/extension`, Chrome Web Store
103
- lane), and its MCP bridge ships inside `@omega.js/mcp-router` at `servers/omega-extension/`.
104
- The WebSocket contract is unchanged: port 9876, `OMEGA_EXTENSION_PORT` on the server side.
105
-
106
- | Contract | Old form | New form | Manual migration step |
107
- |---|---|---|---|
108
- | Where the companion extension comes from | `node_modules/@omega.js/manager/extension/`, built and loaded unpacked from inside the package | The OMEGA Companion on the Chrome Web Store, or `omega-omega/targets/extension/packaged/chrome/raw/` loaded unpacked | Load the new path (or the store listing once it exists) at `chrome://extensions`, and drop any script that built the manager's `extension/` tree. The `omega-extension` MCP upstream needs no manager on disk at all now |
109
-
110
- ## 2026-09-14: desktop and extension take their accent from `brand.color` ([#912](https://github.com/Omega-JS-Stack/omega/issues/912))
111
-
112
- The scaffolded `main.scss` carried a literal `$primary: #2563EB`, so recoloring a
113
- desktop app or an extension meant editing css a brand had already forgotten about,
114
- and neither target emitted the runtime `--omega-accent` ramp web has emitted since
115
- [#272](https://github.com/Omega-JS-Stack/omega/issues/272). The sass task now renders
116
- a partial from the resolved `brand.color` before every compile (`dist/assets/scss/_brand.scss`
117
- on desktop, `dist/assets/css/_brand.scss` on the extension) carrying `$primary` plus a
118
- `ramp` mixin of the accent family, and the scaffold reads both.
119
-
120
- | Contract | Old form | New form | Manual migration step |
121
- |---|---|---|---|
122
- | The accent in a target's `main.scss` | `@use 'omega-desktop' as * with ($primary: #2563EB, …)` (and the `omega-extension` twin) | `@use 'brand';` above the framework import, `$primary: brand.$primary` inside `with (...)`, `@include brand.ramp;` below it | Replace the literal in `src/assets/scss/main.scss` (desktop) or `src/assets/css/main.scss` (extension), or keep the literal for an accent that DIVERGES from `brand.color` on purpose |
123
-
124
- ## 2026-09-14: one shape for a link's icon key ([#903](https://github.com/Omega-JS-Stack/omega/issues/903))
125
-
126
- The base theme spelled a link's `icon` two ways: the footer took the full Font Awesome
127
- class string and emitted it as-is (the one icon mechanism,
128
- [#619](https://github.com/Omega-JS-Stack/omega/issues/619)), while the rest of the chrome
129
- took a bare NAME and wrapped it as `fa-solid fa-<name>`, which cannot express a brand mark
130
- at all. Every chrome site now emits the authored string verbatim, so one key has one shape
131
- and any family the brand's set carries is reachable from data.
132
-
133
- | Contract | Old form | New form | Manual migration step |
134
- |---|---|---|---|
135
- | A link's icon key in nav, app sidebar, app topbar, page-header, account-dropdown and account-section-header data | `icon: github` (a bare name, wrapped as `fa-solid`) | `icon: fa-brands fa-github` (the full class string) | Prefix every bare name in your `_data` and config with `fa-solid` (or `fa-brands` for a brand mark); `grep -rn "icon:" src/_data config/` |
136
-
137
- The same wrap survived one layer down, and went the same way on 2026-09-14
138
- ([#929](https://github.com/Omega-JS-Stack/omega/issues/929)): every `{% section %}` arg,
139
- every page-layout arg, the footer's social row and the runtime builders (account sessions,
140
- the usage bars, the admin calendar) now emit the authored string verbatim too. A section
141
- arg can name a brand mark at last, and the account page's device marks (`apple`,
142
- `windows`, `linux`) render for the first time: the solid family never carried them.
143
-
144
- | Contract | Old form | New form | Manual migration step |
145
- |---|---|---|---|
146
- | A section or page-layout `icon` arg (`marketing/hero` buttons and cards, `marketing/bento`, `marketing/stats`, `marketing/trusted-by`, `marketing/product-demo`, `about/letter`, newsflash `marketing/desks`, and the download / contact / extension / auth / team / alternatives / account / dashboard layouts) | `icon: bolt` (a bare name, wrapped as `fa-solid`) | `icon: fa-solid fa-bolt` (the full class string) | Prefix every bare name in your pages and section calls: `grep -rn "icon:" src/` |
147
- | The feature catalog's `icon` (`features.<id>.icon` in omega.json5) | `icon: 'feather'` | `icon: 'fa-solid fa-feather'` | Prefix each catalog entry in `config/omega.json5` |
148
- | The footer's social row and a team member's link `id` | a platform key wrapped as `fa-solid fa-<platform>` | unchanged: still the platform key | Nothing to do. A social profile is always a brand mark, so the family is derived for you (`website` stays the solid globe) |
149
-
150
- ## 2026-09-14: ONE deploy branch, and the deploy stops committing your tree ([#915](https://github.com/Omega-JS-Stack/omega/issues/915))
151
-
152
- A deploy used to COMMIT AND PUSH the developer's own branch first (`git add -A`, message
153
- `Deploy`), which swept whatever the tree happened to hold into the brand's history. Ian,
154
- 2026-09-14: "I DO NOT WANT the entire local files committed+pushed to main"; "if all we are
155
- pushing to main is the gh workflow files that's fine". So the push lane is retired: every
156
- brand takes the snapshot lane, CI only ever builds `omega-deploy`, and the default branch
157
- receives the composed workflow files alone, in a `chore(ci): compose <names>` commit made
158
- through the git data api and only when one differs. The developer commits their own work on
159
- their own word, as they always should have.
160
-
161
- | Contract | Old form | New form | Manual migration step |
162
- |---|---|---|---|
163
- | The lane a deploy takes | `push` for a plain brand (its current branch), `snapshot` on `main` for a nested one, `snapshot` on `omega-deploy` for a linked one | ONE lane: `snapshot` on `omega-deploy` for every brand with a repo (`dispatch` for a brand outside git) | None. The first deploy after this creates the branch and pushes the workflows |
164
- | Committing the tree | the lane's own `git add -A` + commit + push | nothing: a deploy never commits | Commit what you mean to keep before deploying (the snapshot still carries the tree as it stands, committed or not) |
165
- | `--no-sync` | skipped that commit and push on any target verb and at the brand root | retired: the flag no longer exists (passing it is an unknown flag) | Drop it from any script or muscle memory that passes it |
166
- | A stale checkout | force-pushed the deploy branch anyway | REFUSED, naming `git pull` | Pull before deploying, which is the whole step |
167
- | Where the admin publish builds from | the repo's default branch ([#919](https://github.com/Omega-JS-Stack/omega/issues/919)) | `omega-deploy`, written and dispatched there | Run `omega deploy` once from the brand so the branch exists; until then the publish says so and dispatches nothing |
168
-
169
- ## 2026-09-14: the `Default Values` block is completely managed ([#926](https://github.com/Omega-JS-Stack/omega/issues/926))
170
-
171
- The marker merge used to MOVE any Default-block line the new framework block no longer
172
- carried into the consumer's `Custom Values` section, so a rule the framework retired or
173
- rewrote (the `config/certs/` case, [#913](https://github.com/Omega-JS-Stack/omega/issues/913))
174
- survived in every existing target as if the consumer had typed it, and defeated the new
175
- rule. The merged Default block is now exactly the new framework block: a retired line and
176
- a line a user typed inside the framework block cannot be told apart, and the block header
177
- has always said it is overwritten on every setup.
178
-
179
- | Contract | Old form | New form | Manual migration step |
180
- |---|---|---|---|
181
- | A line inside the `Default Values` block of a target's `.gitignore` or `AGENTS.md` that the framework does not carry | Moved down into `Custom Values` on the next setup | Dropped on the next setup | Move any line of your own into the `Custom Values` section before the next verb run; `.env` values are unaffected (a retired key holding a real value still migrates, only an empty one drops) |
182
-
183
- ## 2026-09-13: the translation route list, and the page's catalog keys ([#858](https://github.com/Omega-JS-Stack/omega/issues/858))
184
-
185
- Ian's same-name ruling (2026-09-09) applied to the last two pairs that named one thing two
186
- ways. Both are OMEGA-to-OMEGA, so both are by hand, except the config half, which
187
- `omega migrate` now CONVERTS rather than merely deleting.
188
-
189
- `translation.exclude` said what NOT to translate and defaulted to nothing, so a brand that
190
- never thought about it paid a provider for every post it had, and a page could not say
191
- anything at all. `translation.include` says what TO translate: route globs with `!`
192
- negation, read in `.gitignore` order, defaulting to `['**', '!blog/**']`, with a brand list
193
- REPLACING the default and a page overriding it for itself under the same key name
194
- ([translation.md](translation.md)).
195
-
196
- The page keys `search.include` / `search.category` named the page's entry in `pages.json`
197
- while the config `search` section means Search Console. The page half is `catalog` now,
198
- page-only, with no site-wide default.
199
-
200
- | Contract | Old form | New form | Manual migration step |
201
- |---|---|---|---|
202
- | The translation route list | `translation: { exclude: ['docs', 'changelog'] }` in omega.json5 | `translation: { include: ['**', '!docs', '!changelog'] }` | Run `npx omega migrate` at the brand root: it writes the converted list and deletes the old key in one run (`--dry-run` prints the plan). A brand that wants the new default instead writes `['**', '!blog/**']` by hand |
203
- | The page's catalog entry | `search: { include: false, category: 'Docs' }` in page frontmatter | `catalog: { include: false, category: 'Docs' }` | Rename the block in each page that carries it. `search:` is a bare config section now, so a page still writing it fails the build naming the file |
204
- | A page's translation opt-out | nothing existed | `translation: { include: false }` in page frontmatter | Nothing to migrate: pages that were named in the old `exclude` list ride the converted include list |
205
-
206
- ## 2026-09-13: the two test-lane env keys retire ([#819](https://github.com/Omega-JS-Stack/omega/issues/819))
207
-
208
- A test lane never asks a brand for a credential. Ian, 2026-09-13: "we need to make it so
209
- that extension and desktop don't need any of those test keys and user IDs ... gets whatever
210
- it needs from the back end". Web, desktop and extension each test their own sign-in against
211
- a persona the backend emulator seeds, so the pair that fed desktop's custom-token
212
- integration case is retired outright ([#904](https://github.com/Omega-JS-Stack/omega/issues/904)
213
- owns the mechanism that replaces it). They are `env-retired.js` rows now, with no
214
- replacement of any kind, so a `.env` layer still declaring one FAILS the load telling you to
215
- delete the line. The `testing` schema group went with them, and the two `${{ secrets.* }}`
216
- lines came off every generated web and backend workflow.
217
-
218
- | Contract | Old form | New form | Manual migration step |
219
- |---|---|---|---|
220
- | The test-lane sign-in credentials | `OMEGA_TEST_FIREBASE_ADMIN_KEY` and `OMEGA_TEST_USER_UID` in `.env` (+ repo secrets on the web and backend repos) | Nothing: each suite signs in as a persona the backend emulator seeds (#904) | Delete both lines from every `.env` layer that carries them, and delete the two repo secrets. No value moves anywhere |
221
-
222
- ## 2026-09-12: one `build.js` on every browser surface ([#743](https://github.com/Omega-JS-Stack/omega/issues/743))
223
-
224
- Every browser artifact OMEGA builds now delivers its snapshot the same way: ONE `build.js`
225
- at the artifact's web root, loaded by each HTML shell's first script tag and by each worker's
226
- `importScripts` line. The bundle banners and defines the browser bundles carried retire with
227
- it, and so does web's inline foot script.
228
-
229
- | Contract | Old form | New form | Manual migration step |
230
- |---|---|---|---|
231
- | Web `/build.js` | The SERVICE WORKER's own transport, written by `writeBuildMeta`: the build MANIFEST (`brand`, `cacheBreaker`, `firebase`, `assets`, `commit`) as `self.OMEGA_BUILD_JSON = {…}` | The one OMEGA wrapper, written by the engine: `self.OMEGA_BUILD_JSON = { config, package, mode, license, builtAt }` plus its `config.dev` line. The service worker reads `config.brand.id`, `config.environment`, `config.buildTime` and `config.cloud.config` off it | Nothing for a page (the head loads it for you). Custom service-worker code reading the old flat keys moves onto `OMEGA_BUILD_JSON.config.*`. `/build.json` is unchanged and still carries the manifest fields (commit, packages, assets, theme) |
232
- | Web page bake | `core/_includes/core/foot.html` emitted the whole wrapper inline in every page | The head loads `/build.js`; a page emits at most one `Object.assign(self.OMEGA_BUILD_JSON.config, <delta>)` line for its own `config:` block | Nothing, unless you overrode `foot.html` and copied the inline bake: delete it, and make sure your `head.html` override keeps the loader tag first |
233
- | Desktop renderer | The snapshot rode in the renderer bundle (esbuild `define` + banner), so consumer renderer code could read the bare `OMEGA_BUILD_JSON` identifier | The view's shell loads `../../build.js`; renderer code reads `window.OMEGA_BUILD_JSON` | Rename a bare `OMEGA_BUILD_JSON` read in your own renderer code to `window.OMEGA_BUILD_JSON`. Main and preload are unchanged |
234
-
235
- ## 2026-09-12: the web `Configuration` global retires ([#894](https://github.com/Omega-JS-Stack/omega/issues/894))
236
-
237
- Every browser surface bakes ONE snapshot under one name now: `OMEGA_BUILD_JSON`, wrapping
238
- `{ config, package, mode, license, builtAt }`, with `config` the browser subset
239
- @omega.js/config decides (its guide's "The browser subset"). Web's page chrome emitted a
240
- `Configuration` global instead, the legacy UJM name, composed key by key in
241
- `core/_includes/core/foot.html` from a subset `src/engine.js` wired together.
242
-
243
- | Contract | Old form | New form | Manual migration step |
244
- |---|---|---|---|
245
- | The page global | `window.Configuration` (a `var Configuration = {…}` literal the foot include composed) | `window.OMEGA_BUILD_JSON.config`, the same read desktop's renderer and every extension context already used | Rename every read. There is NO alias: a theme, page script or consumer module still reading `Configuration` gets `undefined` and throws on the first property. `grep -rn 'Configuration' src/` in your brand's web target |
246
- | The client blob's keys | The `client` block's keys were SPREAD onto the payload's top level, so a page read `Configuration.consent` | The `client` section rides under its own key, and @omega.js/client flattens it onto its own contract: `omega.config.consent` reads the same as before | Nothing to do if you read config through `omega.config.*`. A template or script reading the BAKE directly spells it `OMEGA_BUILD_JSON.config.client.consent` |
247
- | The composed bridges | The engine composed `cloud.config` → `client.firebase.app.config`, `payment` → `client.payment`, `features` → `client.features`, and the foot composed `monitoring.providers.sentry` → `sentry` | Each rides at its canonical home, and @omega.js/client does the mapping for all three surfaces | A template reading `resolved.config.client.firebase.app.config` reads `resolved.config.cloud.config` instead; `client.payment` / `client.features` become `payment` / `features` |
248
- | What may reach a browser | Desktop baked the WHOLE resolved config; the extension kept a hand-written allow list; web's engine wired its own subset | One per-section `client` flag in the schema, read by all three bakes | Nothing to do for a brand. A framework-level addition is a schema row, not a third list. A section with no row does not reach the browser: if a brand value your page needs is missing, it needs the flag |
249
-
250
- ## 2026-09-12: one company key, and a company/ tree inside the parent ([#677](https://github.com/Omega-JS-Stack/omega/issues/677))
251
-
252
- A brand used to state its relationship to its company in FOUR places: `brand.company`
253
- (the parent's display name, typed by hand), `company.url` (typed by hand),
254
- `parent` (the webhook topology), and the machine-local `.omega/company.json`
255
- stamp `omega company adopt` wrote. One key says it now, OUTSIDE `brand` (Ian
256
- 2026-09-12: "brand key is for things about this brand, and the
257
- parent/company/organization key is OUTSIDE of that"):
258
-
259
- ```json5
260
- company: { id: 'itw-creative-works' }, // a sub-brand: the parent's brand.id
261
- company: { id: 'self' }, // the company brand itself
262
- ```
263
-
264
- The loader FILLS that same key at load: `company: { id, name, url, images: { wordmark } }`,
265
- read from the parent's own config. A brand with no `company` key resolves to
266
- `{ id: null, name: brand.name, url: brand.url, images: {} }`, so no reader needs a
267
- fallback. The company's shared files live in a `company/` folder INSIDE the parent
268
- brand's repo (`company/config/omega.json5`, `company/.env`,
269
- `company/.omega/certificates/apple/`), and a brand-level file the child lacks resolves
270
- from there at the same relative path. WHERE that repo is on a given machine comes from
271
- `~/.omega/brands.json`, which every `loadConfig()` refreshes for its own brand: nobody
272
- maintains it, and a parent that has never been loaded on this machine is inheritance-off
273
- with one loud line. Full contract: [../manager/company.md](../manager/company.md).
274
-
275
- | Contract | Old form | New form | Manual migration step |
276
- |---|---|---|---|
277
- | Naming the company | `brand.company: "ITW Creative Works"` + `company: { url: 'https://itwcreativeworks.com' }` | `company: { id: 'itw-creative-works' }` (the parent's `brand.id`) | Add the one key, delete `brand.company` and the typed `company.url`. Both are validation errors now, and a typed `company.name`/`company.url`/`company.images` in a brand file fails the load naming the key |
278
- | The parent's wordmark in email | `brand.images.companyWordmark` | `company.images.wordmark`, resolved from the parent's own `brand.images.wordmark` | Delete the key; set the wordmark once, in the COMPANY brand's own config |
279
- | The webhook topology | `parent: 'self'` / `parent: 'https://parent.example.com'` | `company: { id: 'self' }` / `company: { id: '<parent brand.id>' }` | Delete `parent` and name the company. The key itself is a retired-key error now, whatever its value |
280
- | The webhook OPT-OUT | `parent: false` | `company: { webhooks: false }` ([#677](https://github.com/Omega-JS-Stack/omega/issues/677)) | Move it into the company block. Typed boolean, default `true`, and the only thing it says is "the provider ACCOUNT is shared and its one account-level webhook is owned elsewhere". The campaigns and newsletter services are its only readers, and the resolved `company` section carries it so neither needs a fallback |
281
- | Signing material in a target | The manager's disperse `certs` operation and every desktop verb COPIED the tree into `targets/<name>/config/certs/`, and `CSC_LINK`/`APPLE_API_KEY` were derived from those copies | The tree is READ IN PLACE, company tier first, and both paths derive ONCE at the desktop env load as ABSOLUTE paths into it ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)) | Delete the copies under `targets/<name>/config/certs/` (a build never reads them again). Nothing else: the derivation finds the tree. On a RUNNER that directory is still the decode target for the pushed secrets |
282
- | The company folder | A separate company workspace repo, joined by `omega company adopt` writing `.omega/company.json` | `company/` inside the parent brand's repo, joined by `company: { id }` | Move the workspace's `config/omega.json5`, `.env` and `.omega/certificates/` into `<parent brand>/company/`, then `rm .omega/company.json` in every brand. The stamp is read by nothing (a run that finds one says so once) |
283
- | Where a brand is, on this machine | The stamp's absolute path | `~/.omega/brands.json`, written by every run | Nothing: run any omega verb inside the parent once and the line appears |
284
- | The directory push's relationship | `parent` names it | `company: { id }` names it | Delete `parent`; a brand with no company skips the push, exactly as no parent did |
285
- | Off-laptop | The runner had no company at all | The dispatching machine resolves and writes `config/company-resolved.json5` beside the brand config; the deploy snapshot carries it | Nothing: it is generated per deploy and removed right after the push. No runner ever reads `company/` |
286
-
287
- ## 2026-09-11: targets keyed by name ([#886](https://github.com/Omega-JS-Stack/omega/issues/886))
288
-
289
- `targets` used to be keyed by TYPE, with a canonical folder per type
290
- (`targets/website` for web) and an ARRAY of id'd instances when a brand ran two of
291
- one type. Every key is a NAME now, the name IS the folder, and every entry declares
292
- its `type`:
293
-
294
- ```json5
295
- targets: {
296
- web: { type: 'web', url: 'https://somiibo.com' },
297
- community: { type: 'web', url: 'https://community.somiibo.com' },
298
- backend: { type: 'backend' },
299
- desktop: { type: 'desktop' },
300
- docs: { type: 'custom' },
301
- }
302
- ```
303
-
304
- The name is the folder `targets/<name>`, the `--target=<name>` word, and the derived-repo
305
- suffix. There is no id, no folder key and no name table left: a second web target is a
306
- sibling key, and its NAME is its subdomain (`https://community.<brand host>` unless the
307
- entry names its own `url`). Full contract: [config.md](config.md#targets-every-key-is-a-name-886).
308
-
309
- | Contract | Old form | New form | Manual migration step |
310
- |---|---|---|---|
311
- | The web target's folder | `targets/website` | `targets/web` | `git mv targets/website targets/web` |
312
- | A target entry | `web: { … }` (the key was the type) | `web: { type: 'web', … }` (the key is the name) | Add `type` to every entry under `targets`; the validator names any entry still missing it |
313
- | A second target of one type | `web: [{ id: 'main' }, { id: 'admin' }]`, folder `targets/website-admin` | `web: { type: 'web' }, admin: { type: 'web' }`, folder `targets/admin` | Turn the array into sibling keys (the ids become the names, `url:` only where the host is not `<name>.<brand host>`), then `git mv targets/website-admin targets/admin`. An array left behind is a validation error naming this row |
314
- | The target picker | `--target=website` | `--target=web` | Update every script, workflow and alias that passes the flag: it takes the NAME |
315
- | Scaffolded workflows + README | Written from the type-keyed folders | Written from the names | Rerun `npx omega manage` at the brand root so the workflows and the README regenerate against the new folders |
316
- | A deploy record from the array form | `.omega/state.json` `deploy` keyed `<type>:<id>` (`web:admin`) | Keyed by the target NAME (`admin`) | Nothing to do: the old key is not carried over, so that target reads "not deployed yet" once and adopts its record on the next live check |
317
-
318
- ## 2026-09-11: one repo block and the website repo ([#883](https://github.com/Omega-JS-Stack/omega/issues/883))
319
-
320
- A brand's repo hosting was spelled in four places (`repo.providers.github`, a separate
321
- top-level `github` identity, a `targets.<name>.github.repo` override, the desktop releases
322
- owner/repo), and the website published to the SOURCE repo's `gh-pages`, so a free org had
323
- to make the whole brand public to serve a site. One block says it now, every repo name
324
- derives, and the built site gets a repo of its own:
325
-
326
- ```json5
327
- repo: { provider: 'github', org: 'Acme-Org' }
328
- ```
329
-
330
- | Role | Repo | Visibility |
331
- |---|---|---|
332
- | Source monorepo | `<repo.org>/<brand.id>-omega` | the brand root `package.json` `private` field (absent = private) |
333
- | Releases | `<repo.org>/<brand.id>-releases` | always public |
334
- | Website, one per GitHub-hosted web target | `<repo.org>/<brand.id>-<target name>` | private only when the brand is private AND the org's plan allows private Pages, else public |
335
-
336
- Full contract: [config.md](config.md#the-repo-block-and-the-repos-it-derives-883).
337
-
338
- | Contract | Old form | New form | Manual migration step |
339
- |---|---|---|---|
340
- | The repo block | `repo: { providers: { github: { enabled, org, repo, shared, private } } }` | `repo: { provider: 'github', org }` | Replace the block: keep the org, drop the rest. Presence is the switch, so a brand the manager should not touch deletes the block |
341
- | The GitHub identity block | `github: { user, website }` | nothing | Delete it. Nothing read `user`, and the website has its own derived repo now |
342
- | A per-target repo override | `targets.backend.github.repo` (the CMS content repo) | nothing | Delete it: the CMS commits to the source repo `<brand.id>-omega` |
343
- | The releases repo | `targets.desktop.releases: { owner, repo }` | nothing | Delete both keys. `releases: {}` stays the presence switch for the site's download links |
344
- | Repo visibility | `repo.providers.github.private` | the brand root `package.json` `private` field | Make sure the brand root says `private: true` (or `false` on purpose): the manage walk reconciles the repo to it in both directions |
345
- | Where a web target is served from | implicit (Pages on the source repo) | `targets.<name>.hosting.provider`, default `github`, web targets only | Nothing to write unless a target is hosted elsewhere later; a non-web target carrying `hosting` is a validation error |
346
- | The website's home | the source repo's `gh-pages` branch + its custom domain | the target's own `<brand.id>-<name>` repo, `gh-pages` force-orphan, Pages with the target's url as the custom domain | 1) `omega manage`: it creates `<brand.id>-<name>` and reports that Pages is still on the home repo. 2) On the HOME repo, remove the custom domain and disable Pages (`gh api -X DELETE repos/<org>/<brand.id>-omega/pages`), then delete its `gh-pages` branch. ORDER MATTERS: a custom domain can be claimed by only one repo, so the home repo has to let go before the website repo can take it. 3) `omega deploy --direct` in `targets/<name>` (or dispatch) so the new repo gets its `gh-pages` branch. 4) `omega manage` again to set the Pages source and the domain |
347
- | The cross-repo token | `GH_TOKEN` on the source repo | unchanged | Rotate nothing: the same secret already has the scope the website and releases pushes need |
348
- | The backend resolved repo (`config.resolved.github`) | `{ owner, name, repo }` with empty strings when unset | `sourceRepo(config)`: `{ owner, name, slug }` or `null` | A consumer reading `.repo` reads `.slug`; a brand with no `repo` block gets `null`, so a route that needs the repo guards on it and names `repo.org` |
349
- | The extension release channel | a release on the SOURCE repo, uploaded with the same-repo Actions token | `<brand.id>-releases`, tag `<target name>-v<version>`, uploaded with `GH_TOKEN` | Nothing to write. The composed `publish.yml` regenerates on the next deploy dry run; delete the old `extension-v*` releases on the home repo when convenient |
350
-
351
- ## `ultimate-jekyll-manager` → `@omega.js/web`
352
-
353
- | Contract | Old form | New form | Manual migration step |
354
- |---|---|---|---|
355
- | Build engine | Jekyll 4 + Ruby/Bundler (`Gemfile`, `Gemfile.lock`, `src/_config.yml`) | Eleventy 3 + LiquidJS, Node only | Run `npx omega migrate --check`, then `npx omega migrate` in the website target: it converts the config, runs the codemod, and deletes `src/_config.yml`, `config/ultimate-jekyll-manager.json`, `Gemfile`, `Gemfile.lock`, `.ruby-version`. Drop the Ruby toolchain from CI |
356
- | Package + CLI | `ultimate-jekyll-manager` dependency; `uj` / `ujm` / `ultimate-jekyll` / `mgr` bins | `@omega.js/web`; `omega` / `omg` / `mgr` | Swap the dependency; replace `npx mgr <verb>` with `npx omega <verb>` in every npm script and workflow |
357
- | Config | `src/_config.yml` + `config/ultimate-jekyll-manager.json` | `config/omega.json5` | Key-by-key table in [config.md](config.md#ultimate-jekyll-manager-src_configyml--configultimate-jekyll-managerjson--omega-migrate-b4-checkpoint-32); `omega migrate` writes it for you |
358
- | Client-runtime config key | `web_manager: { … }` | `client` (`targets.web.client`) | Codemod rule `client-frontmatter` renames page-frontmatter blocks; config blocks relocate per the UJM mapping table in [config.md](config.md#migration--legacy-configs--omegajson5) (the SSOT for the key-by-key moves). The old name is a validation error, not a silent no-op |
359
- | Framework file delivery | `distribute.js` COPIED framework layouts/includes/assets into the consumer repo | Layered resolution (consumer → active theme → base → core), first-layer-wins per relative path, zero copying | Delete every copied framework file from the repo; keep only files you truly override, at the same relative path. `npx omega customize --list` prints the override map |
360
- | Layout values | `layout: themes/[ site.theme.id ]/frontend/pages/blog` (or hardcoded `themes/classy/…`) | The plain layout name: `layout: frontend/pages/blog` | Codemod rule `bracket-layout`, or strip the `themes/<id>/` prefix by hand. An unmigrated value fails the build loudly ("Problem creating an Eleventy Layout") — the engine no longer aliases the old spellings |
361
- | Frontmatter refs | Bracket interpolation — `title: [ site.brand.name ]` | Real Liquid — `title: {{ resolved.config.brand.name }}` | Rewrite `[ … ]` to `{{ … }}` in frontmatter by hand (the codemod covers only `layout:` lines). An unmigrated value ships the literal brackets into the output — nothing resolves them anymore |
362
- | Page data reads | `page.<frontmatterKey>`, `page.content`, `page.slug`, `page.resolved.*`, `page.canonical.url` | Bare `<key>`, `content`, `page.fileSlug`, `resolved.*`, `{{ site.url }}{{ page.url }}` | Codemod rules `page-props`, `page-resolved`, `canonical-url`. `page.next` / `page.previous` / `page.collection` have NO mechanical equivalent — port them by hand to the Eleventy collections API |
363
- | Page frontmatter scope | Any frontmatter key fed the templates | META-ONLY allow-list — every key it holds is [docs/web/frontmatter.md](../web/frontmatter.md); other content keys are stripped with a build warning | Move page content out of frontmatter into `{% section %}` entries or a collection document (collection entries and layouts are exempt — their frontmatter IS the document) |
364
- | Config namespace | A page restated omega.json5 keys BARE (`theme:`, `client:`, `inbound:`) and every template read config off the site global (`{{ site.brand.name }}`, `{{ resolved.theme.id }}`) | Three namespaces ([#607](https://github.com/Omega-JS-Stack/omega/issues/607)): a page's overrides go under a `config:` parent, templates read `resolved.config.<section>` (the WHOLE merged config), `meta` is PAGE machinery keeping its bare/flat spelling, and `site.*` is BUILD FACTS only (collections, `omega`, `time`, `pricing`, `targets`, …) | Codemod rules `config-parent` (the frontmatter keys) and `config-reads` (the reads, bodies + frontmatter values + section `.json` descriptors, [#611](https://github.com/Omega-JS-Stack/omega/issues/611)) — `omega migrate` runs both. Nothing dual-reads, and the `config:` rule runs both ways: a bare config section fails the build naming the key, a non-section key under `config:` fails the same way, and a `site.<section>` read fails naming the expression |
365
- | Site-wide page meta | A `meta` block in the config (`_config.yml`, then omega.json5's `meta` section) set the default title/description/index for every page | NO config meta section at all ([#607](https://github.com/Omega-JS-Stack/omega/issues/607), Ian 2026-08-26 — meta never exists in two places). Page frontmatter `meta:` is the only meta; the site-wide default is `brand.name` / `brand.description` ([docs/web/frontmatter.md](../web/frontmatter.md)) | `omega migrate` drops the legacy block with a note; a converted omega.json5 still carrying `meta` (or `targets.web.meta`) is a retired-key error naming the move. Move a real site-wide title/description into `brand.name` / `brand.description`, and anything per-page into that page's own `meta:` |
366
- | Redirects | Hand-maintained redirect pages under `src/defaults/dist/redirects/**` on UJM's `redirects` layout, and TEMPLATED ones (`/c/:id` → `/code?id=:id`, DashQR's QR codes) hand-authored in the Cloudflare dashboard where nothing declared them | Two declared mechanisms, split by whether the URLs can be enumerated ([#466](https://github.com/Omega-JS-Stack/omega/issues/466)): a redirect PAGE — `redirect.url` in frontmatter on the `modules/utilities/redirect` layout, and the build emits meta-refresh + canonical + noindex — and `edge.providers.cloudflare.rules.redirect`, the zone's dynamic-redirect ruleset `omega manage` reconciles | Redirect pages port as pages (the layout name is the only change). Move every dashboard-authored redirect rule into `edge.providers.cloudflare.rules.redirect` so the zone has a declared source; `omega manage` then owns the ruleset and REMOVES any rule config does not name. There is no web-config redirect key: `targets.web.redirects` existed only in 0.45.0–0.49.0 and is now a retired-key error naming this move |
367
- | `append: true` frontmatter | A page body rendered BELOW the layout's default `{% composition %}` when the page declared the flag | Gone ([#607](https://github.com/Omega-JS-Stack/omega/issues/607)) — a page body always REPLACES the composition | Codemod rule `append-flag` drops the key with a finding. To keep the default bands, run `omega customize <url>` to materialize them into the page, then edit them |
368
- | Nested loops | `forloop.parentloop.<prop>` | A hoisted `{% assign omega_parentloop<depth>_<prop> = forloop.<prop> %}` after the parent `{% for %}` (LiquidJS has no parentloop) | Codemod rule `parentloop`; it flags the cases it will not touch (multi-level, same-line) for hand-hoisting |
369
- | Interpolated tag args | `{% uj_icon "{{ page.icon }}" %}` — Jekyll silently no-op'd it | `{% capture omega_migrate_arg_1 %}…{% endcapture %}` hoisted above, the variable passed as the arg | Codemod rule `tag-arg-interpolation`; verify each reported line that it could not rewrite |
370
- | Icons | The `uj_icon`/`omega_icon` TAG, plus a `prerender_icons` frontmatter list that drew hidden copies for JS to clone | Native Font Awesome markup, `<i class="fa-solid fa-rocket"></i>`, everywhere ([#619](https://github.com/Omega-JS-Stack/omega/issues/619)): the build inlines every icon the rendered page names, and a runtime watcher upgrades whatever JS creates afterwards, from `/assets/icons/<style>/<name>.svg`. Country flags are the second namespace, `<i class="omega-flag omega-flag-us">` | Codemod rule `icon-tag-markup` converts every icon tag (both spellings) to markup; drop `prerender_icons` from frontmatter and delete any `getPrerenderedIcon` call in favour of an `<i class="fa-…">` string. A `label=` option has no native equivalent and is dropped — hand-write `role="img" aria-label` where an icon really carries meaning. Contract: [docs/shared/icons.md](icons.md) |
371
- | Includes | `{% include /components/x.html %}` | `{% include components/x.html %}` (no leading slash) | Codemod rule `include-slash` |
372
- | Analytics template reads | `site.analytics.google` | `resolved.config.analytics.providers.google.id` | Codemod rules `analytics-shape` (the provider shape) then `config-reads` (the namespace) |
373
- | Classy gradient utilities | `.gradient-animated` (gradient shimmer) + `.gradient-grain` (noise overlay) on hero markup | `omega-dotgrid` — the masked dot backdrop v2 puts behind every hero — plus `data-omega-dotfield` where the animation was. Classy v2 ships zero gradients, so the old classes are silent no-ops | Codemod rule `gradient-utilities` converts both once ([#296](https://github.com/Omega-JS-Stack/omega/issues/296)); the pair on one element collapses to ONE `omega-dotgrid`. `.bg-gradient-*` names are NOT touched — v2 still neutralizes those to flat token paint |
374
- | Sass entry | `@use 'ultimate-jekyll-manager' as * with (…)` in `src/assets/css/main.scss`; page CSS files `@use`-ing themselves | `@use 'omega:main' with (…)`; the self-`@use` lines are gone (every layer's page sheet already loads, [#624](https://github.com/Omega-JS-Stack/omega/issues/624)) | `omega migrate`'s consumer-assets pass rewrites both; by hand it is a one-line edit plus deleting the self-`@use` lines |
375
- | JS entry | Seeded `src/assets/js/main.js` bootstrapping the manager | Deleted — core main + the boot runtime own it | `omega migrate` deletes an untouched seed and FLAGS a customized one; port custom logic into a page or section module |
376
- | Client bootstrap | `import webManager from 'web-manager'`; `window.Manager` global | `import omega from '@omega.js/web/runtime'`, the web instance; no window global | Replace the import in every module and delete `window.Manager` references. The accessor and auth shapes are in [one runtime shape](#2026-09-25-one-runtime-shape-on-every-package-945) |
377
- | Deploy | `npu sync --message='Deploy'` shell-out | `omega deploy` (plain git sync + `workflow_dispatch`, or the direct lane) | Replace the script; contract in [deploys.md](deploys.md) |
378
- | Version maintenance | Setup-time `ensureManagerVersion()` + peer-dependency auto-install | The explicit `omega update` verb | Stop expecting self-updates; run `npx omega update` (`--apply` to install) — [updates.md](updates.md) |
379
- | Charts | A page imported `chart.js` bare (it was a `@omega.js/web` dependency) and built its own `new Chart(canvas, config)` | `chart.js` is gone — the framework draws with TanStack Charts ([#772](https://github.com/Omega-JS-Stack/omega/issues/772)), reached ONLY through `__main_assets__/js/libs/charts.js` (`loadCharts`, `chartSlot`, `barChart`/`stackedBarChart`/`doughnutChart`/`lineChart`) | Replace the bare import and the hand-built config with the helpers, and the page's `<canvas>` with `chartSlot`'s markup (an SVG chart has no canvas). A page that genuinely needs the raw grammar imports `@tanstack/charts` bare instead — same framework-resolution rule, new name |
380
-
381
- ## `backend-manager` → `@omega.js/backend`
382
-
383
- | Contract | Old form | New form | Manual migration step |
384
- |---|---|---|---|
385
- | Package + init | `require('backend-manager')`; `Manager.init({ backendManagerConfigPath: 'backend-manager-config.json' })` | `const omega = require('@omega.js/backend');`, then `omega.initialize({ … });` and `module.exports = omega.functions;`; the config loader discovers the file, so there is no config-path option | Swap the dependency, rewrite the entry to that shape and delete the `backendManagerConfigPath` option. The handler and `ctx` shapes are in [one runtime shape](#2026-09-25-one-runtime-shape-on-every-package-945) |
386
- | Config | `functions/backend-manager-config.json` | `functions/config/omega.json5` | Key-by-key table in [config.md](config.md#backend-manager-functionsbackend-manager-configjson--functionsconfigomegajson5--done-checkpoint-19) |
387
- | Exported Cloud Functions | `bm_api`, `bm_signUpHandler`, `bm_createPost`, `bm_cronDaily`, … | `omega_api`, `omega_signUpHandler`, `omega_createPost`, `omega_cronDaily`, … | Deploy the new names, repoint every trigger/scheduler/webhook that names a function, then DELETE the orphaned `bm_*` functions from the Firebase project (a rename leaves the old ones running) |
388
- | Hosting rewrite | `{ source: '/backend-manager/**', function: 'bm_api' }` | `{ source: '{/omega,/omega/**,/mcp,/mcp/**,/.well-known/oauth-protected-resource,/.well-known/oauth-authorization-server,/authorize,/token,/register}', function: 'omega_api' }` | The next verb's `ensureTarget()` writes it (and removes duplicates); by hand, replace the rewrite and keep it FIRST in the list |
389
- | API dispatch | Command-based: `POST /backend-manager` with `{ command: 'user:sign-up', payload: {…} }` | REST: `POST /omega/user/sign-up` with the payload AS the body | Rewrite each caller: the command's `:` becomes a path segment, `payload` becomes the body. On the client, `omega.request('/omega/user/sign-up', { method: 'POST', body: {…} })` |
390
- | URL prefix | `/backend-manager/*` | `/omega/*` (also `/omega_api/*` on the direct function URL) | Repoint every caller: the old prefix answers 404 |
391
- | Per-request object | `BackendAssistant`; handler signature `module.exports = async ({ assistant, settings, analytics }) => …` | `Context` (`ctx`); handler signature `module.exports = async ({ ctx, omega, user, data, usage, analytics }) => …` | Rename the destructured argument and every `assistant.` call site (`ctx.respond`, `ctx.log`, `ctx.request`) in each custom route, event, and cron handler; `settings` is `data` |
392
- | Environment | `BACKEND_MANAGER_KEY`, `BACKEND_MANAGER_WEBHOOK_KEY`, `BEM_TEST_RUNNER`, `BEM_HTTPS_PORT` | `OMEGA_ADMIN_KEY`, `OMEGA_WEBHOOK_KEY`, `OMEGA_TEST_RUNNER`, `OMEGA_HTTPS_PORT` | Rename in `.env`, in CI secrets, and in anything that reads them. Values carry over unchanged |
393
- | AI provider keys | TWO names per provider: `BACKEND_MANAGER_OPENAI_API_KEY` (the company-wide fallback) beside a bare `OPENAI_API_KEY` (the brand's own), and the same pair for Anthropic. The provider preferred the bare one and fell back to the prefixed; `inferContact` read ONLY the prefixed one | ONE name per provider: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY` ([#639](https://github.com/Omega-JS-Stack/omega/issues/639)). Every reader takes the bare name through the one env reader; the OMEGA-era `OMEGA_OPENAI_API_KEY` / `OMEGA_ANTHROPIC_API_KEY` twins are removed outright, with no dual-read | Rename `BACKEND_MANAGER_OPENAI_API_KEY` (or `OMEGA_OPENAI_API_KEY`) to `OPENAI_API_KEY` in `.env` and CI secrets, same for Anthropic, and delete the old rows. A key that served EVERY brand moves to the COMPANY `.env` under that same bare name — the cascade is the fallback, never a second key. An interactive `npx omega manage` asks for both once (the `ai` service) and writes them for you |
394
- | CLI | `bm` / `bem` / `backend-manager` / `mgr` bins | `omega` / `omg` / `mgr` (one dispatcher; a backend's `functions/` dir resolves to the backend CLI) | Replace the bin name in npm scripts and workflows |
395
- | `test/*` routes | Every route under `routes/test/` served at its production URL | The whole `test/` route folder 404s outside dev/testing, with zero carve-outs; contract in the backend's `docs/routes.md` ([#238](https://github.com/Omega-JS-Stack/omega/issues/238)) | Use `/omega/health` for liveness (a real route, public, no input echoed) — not `/omega/test/health`. A brand whose own `routes/test/*` route must serve in production moves it out of the `test/` folder; debug routes belong in `test/` and are gated by default |
396
- | Test discovery | The BEM runner discovered plain `.js` files under `test/` | `discoverTests()` matches `*.test.js` only (`packages/backend/src/test/runner.js`) — a `test/<name>.js` file is invisible, and the run reports zero tests with no error ([#481](https://github.com/Omega-JS-Stack/omega/issues/481)) | Rename every ported test file to `<name>.test.js`; a suite left on the old spelling goes silently dark |
397
- | Marketing prune cron | Ran daily unless `marketing.prune.enabled: false` — a brand with NO `marketing.prune` block was pruning | ON by default again: the config schema's `marketing.prune.enabled: true` default rides the resolved chain ([#478](https://github.com/Omega-JS-Stack/omega/issues/478), superseding #422's opt-in interlude); only an explicit `false` stops it, and the per-provider safety floors bound what a run may delete | A brand that must NOT prune sets `marketing.prune.enabled: false`; everyone else does nothing — matching the legacy default |
398
- | Firestore rules file | `firestore.rules` carried a framework-managed `// ========== OMEGA Rules (vX.Y.Z) ==========` block that `omega setup` regenerated wholesale on every run | `firestore.rules` is the brand's SOURCE — pure rules, no managed block. The framework half ships inside `@omega.js/backend` and compiles in; `firebase.json` points the emulator and `firebase deploy` at the generated `dist/firestore.rules` ([#255](https://github.com/Omega-JS-Stack/omega/issues/255)) | `npx omega migrate:rules` converts the file ONCE (custom region preserved) and retargets `firebase.json`. It is a RUN-ALONE verb, never a side effect of another one ([#522](https://github.com/Omega-JS-Stack/omega/issues/522)) — while `firebase.json` still names your own rules file, the verbs report the deferral and change nothing, because adopting the compiled artifact changes what the LIVE project enforces (see the two rows below). But per-field protection a legacy BEM brand added by HAND-EDITING the managed block is not carried over, because the block is gone: re-declare those keys in your own `match /users/{uid}` block, which merges into the framework's (row below). Keys are TOP-LEVEL — protecting `xp.total` means listing `'xp'` |
399
- | Firestore rules hooks (0.36.0 only) | The compiled model shipped with two brand HOOKS the framework called: `protectedFields()` (a list folded into the user write rule) and `canWriteUser()` (a condition ANDed into it), linted and re-seeded by the compiler | Merge-by-match: a brand match block whose path names a framework block's is MERGED into it, ANDing the brand's condition onto every op both declare. No hooks, no lint, no injection. Rules schema v2.0.0 → v3.0.0 ([#353](https://github.com/Omega-JS-Stack/omega/issues/353)) | `npx omega migrate:rules` migrates ONCE: a hook still carrying its shipped default body is deleted, a CUSTOMIZED one is kept as an ordinary function and reported — nothing calls it any more, so move what it enforced into `match /users/{uid} { allow create, update: if …; }` in your own file and delete it. `protectedFields()` becomes `allow create, update: if !isWritingAny(['xp', …]);` in that block |
400
- | Firestore rules helpers | `belongsTo(identity)`, `emailVerified()`, `isWritingProtectedUserField()`, `authUid()`, `authEmail()`, `existingData()`, `incomingData()`; `existingData()` was `resource.data`, so every field helper ERRORED on a create (which denied the write) | One naming convention, `is*` for predicates and `get*` for values: `isUser(identity)`, `isEmailVerified()`, `isWritingFrameworkField()`, `getAuthUid()`, `getAuthEmail()`, `getExistingData()`, `getIncomingData()`, plus new `isWritingAny(fields)` and `isOwner()`. `getRoles()` keeps its name and stays the one helper that bills a document read. `getExistingData()` reads an absent document as `{}`, so the field helpers mean the same thing on create and update ([#353](https://github.com/Omega-JS-Stack/omega/issues/353)) | The `npx omega migrate:rules` migration renames every one of those calls inside your rules file. Three behaviour changes to know: a signed-in client may now CREATE its own `users/{uid}` document as long as it carries no framework-owned key (before, every client create was denied by the error); `isUser()`'s EMAIL arm now requires a verified token, so an unverified signup claiming an address no longer matches a document keyed by it (the uid arm is unchanged); and `plan` came off the framework's protected key list — nothing in the stack reads or writes `users/{uid}.plan`, and a brand that still stores one protects it in its own merged block |
401
- | `isEmailVerified()` source of truth | Read the STORED `users/{uid}.verifications.email` through `getVerifications()`, at one billed document read per call | Reads the AUTH TOKEN: `request.auth != null && request.auth.token.get('email_verified', false) == true`. `getVerifications()` is REMOVED, and `verifications` joins the framework-owned key list ([#353](https://github.com/Omega-JS-Stack/omega/issues/353)) | Nothing to rename — the helper keeps its name and gets honest: nothing in the stack has ever WRITTEN `verifications`, so the old gate was satisfiable only by a client planting the field on its own user document. Rules of yours that called `getVerifications()` must stop (it is gone), and a client write to `users/{uid}.verifications` is denied from now on |
402
- | Framework `match /users/{uid}` ops | `allow read` + `allow write` — and `write` covers delete, where `request.resource` is null, so the field guard ERRORED and an owner deleting their own user document was denied by that error | `allow read` + `allow create, update`. Delete falls through to the admin catch-all and is denied by the RULE ([#353](https://github.com/Omega-JS-Stack/omega/issues/353)) | Only if you TIGHTEN that block from your own `firestore.rules`: ops pair by NAME, so change your `allow write:` to `allow create, update:` or your condition appends as a widening instead of ANDing on. The compiler reports the mismatch loudly rather than letting it look like a tightening |
403
- | Pre-family markers | Four marker shapes with no common grammar: the hand-written `{{ backend-manager }}` placeholder (`firestore.rules`, `database.rules.json`), the `# BEM>>>` … `# <<<BEM` block in `.gitignore`, and the `///---backend-manager---///` … `///---------end---------///` rules block (plus its short-lived `///---omega---///` OMEGA-era flavor) | ONE family grammar, `<comment> ========== <Label> ==========`, everywhere a marker is machine-parsed. Every evergreen verb reads ONLY the family, so a pre-family file converges at no verb | `npx omega migrate:markers` converts all four ONCE, run alone: the `# BEM>>>` block is deleted whole (its lines were framework-owned and the Default zone re-writes them), `firestore.rules` lands on the compiled-rules source shape with your own rules kept, and `database.rules.json`'s block is re-cut to the family markers with its rules untouched. The target checks DEFER on a pre-family file — reported as a warning (not a failure, so nothing auto-fixes it), naming this verb, tree untouched — and the compiler REFUSES to write `dist/firestore.rules` from a source still carrying one, rather than splicing the marker in as if it were a rule ([#40](https://github.com/Omega-JS-Stack/omega/issues/40)) |
404
- | Storage rules scaffold | `templates/storage.rules` granted the whole bucket to any signed-in user (`allow read, write: if request.auth!=null`) | Deny-all (`allow read, write: if false`) — a brand that serves files from Storage opts in per path it actually exposes ([#278](https://github.com/Omega-JS-Stack/omega/issues/278)) | Nothing is rewritten in place: the scaffold writes `storage.rules` only when the file is missing, so a migrated brand keeps whatever it arrived with. Read that file once and narrow it by hand — a bucket-wide grant carried over from the old scaffold stays live until you do |
405
-
406
- ## `electron-manager` → `@omega.js/desktop`
407
-
408
- | Contract | Old form | New form | Manual migration step |
409
- |---|---|---|---|
410
- | Package + entries | `require('electron-manager/main' \| '/preload' \| '/renderer' \| '/gulp')` (the scaffold README named a `/test/assert` entry the package never exported) | `require('@omega.js/desktop/main' \| '/preload' \| '/renderer' \| '/gulp')`; no assert entry: a test's `run`/`inspect` receives `expect` on its context ([#812](https://github.com/Omega-JS-Stack/omega/issues/812)) | Swap the dependency and every require path in `src/main.js`, `src/preload.js`, the renderer components, and `gulpfile.js`; in the test files, drop the assert require and use `ctx.expect` |
411
- | CLI | `em` / `electron-manager` / `mgr` bins | `omega` / `omg` / `mgr` | Replace the bin name in npm scripts and workflows |
412
- | Config | `config/electron-manager.json` | `config/omega.json5` | Key-by-key table in [config.md](config.md#electron-manager-configelectron-managerjson--configomegajson5--done-checkpoint-18) — note the per-OS move: `targets.mac` / `.win` / `.linux` → `targets.desktop.platforms.mac` / `.win` / `.linux` |
413
- | Apple signing material | The legacy company store, `omega-manager/.output/_shared/certificates/apple/` (one set for every brand, on one machine) | The TWO-tier signing tree ([#892](https://github.com/Omega-JS-Stack/omega/issues/892)): `<company brand>/company/.omega/certificates/apple/` read FIRST, the brand's own `.omega/certificates/apple/` second, and every WRITE into the company tree when the brand names a company. The layout is byte-for-byte the legacy one | **By hand, once**: copy `AuthKey_*.p8`, `certificates/*.cer` and `csr/*/{private.key,request.csr}` from the legacy store into the company tree, skipping `_backups` and the legacy `.p12` files (the walk re-exports those with the password it mints). The `csr/*/private.key` half is the load-bearing one: without it the walk ERRORS per type instead of minting a second certificate over a valid one. Step 5 of the [migration playbook](../manager/migration.md) |
414
- | Unsigned mac builds | Every rung warned and carried on, so a mac release could publish unsigned and unnotarized on a green run | Each rung is an ERROR that stops the run ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)): the certificates walk, `validate-certs` (strict in the deploy precheck and in `publish`), the precheck's secrets push (`fatal` on all four frameworks), the workflow's mac steps (`exit 1`), and the notarize hooks (which now staple and PROVE with `stapler validate` + `spctl --assess`). There is no per-brand "unsigned mac" switch | Set `CSC_KEY_PASSWORD` and the `APPLE_*` three in the brand or company `.env` and run `omega manage --service certificates`; `CSC_LINK` and `APPLE_API_KEY` are DERIVED from the signing tree, never typed. The env schema marks the set required once `certificates.providers.apple` is declared, and the deploy precheck refuses to publish a half set |
415
- | Windows signing strategy | `config/electron-manager.json` → `signing.windows.strategy` | `config/omega.json5` → `targets.desktop.platforms.win.signing.strategy` | Move the key; the `.env` credential slots (`CSC_LINK`, signtool path, cloud-provider creds) keep their names |
416
- | Windows runner logon account | `WIN_RUNNER_LOGON_ACCOUNT` + `WIN_RUNNER_LOGON_PASSWORD`, plus a DPAPI-encrypted `runner-logon.json` and an `omega runner set-credentials` subcommand, named the account the runner's Windows service ran as | GONE ([#337](https://github.com/Omega-JS-Stack/omega/issues/337)). The signing runner is a Startup-folder `.cmd` in the logged-in user's own session — there is no service and no scheduled task, so there is no second account to name. Nothing read either key | **On the signing box, by hand**: delete `WIN_RUNNER_LOGON_ACCOUNT` and `WIN_RUNNER_LOGON_PASSWORD` from its `.env` (and from GitHub Actions secrets if they were ever pushed), and delete `%APPDATA%\@omega.js/desktop\runner-logon.json`. Nothing replaces them |
417
- | Windows EV cert reference | `WIN_EV_TOKEN_PATH`, with `WIN_CSC_LINK` silently accepted as a fallback alias by the signer and by `validate-certs` | `WIN_EV_TOKEN_PATH` only — the env-schema name, and the ONE name ([#337](https://github.com/Omega-JS-Stack/omega/issues/337)). Nothing reads `WIN_CSC_LINK`, so a box that only sets it now fails loudly naming `WIN_EV_TOKEN_PATH` | **On the signing box, by hand**: rename the key in its `.env` (and in any GitHub Actions secret feeding the `windows-sign` job) from `WIN_CSC_LINK` to `WIN_EV_TOKEN_PATH`. The value — a SHA1 thumbprint or a `.pfx` path — is unchanged. electron-builder's own `WIN_CSC_LINK` is a different variable and is untouched |
418
- | Windows signing runner install | `em runner`: home `%LOCALAPPDATA%\em-runner` (`C:\actions-runners` before v1.2.36), Startup shortcuts and GitHub-side runners named `em-runner-<host>-<org>` | `omega runner`: home `%LOCALAPPDATA%\omega-runner`, shortcuts and runners named `omega-runner-<host>-<org>` ([#337](https://github.com/Omega-JS-Stack/omega/issues/337)). The legacy install is detected: `status` names it, `uninstall` deregisters and removes it, `install` tears it down first | **On the signing box**: `npx omega runner install` with `GH_TOKEN` set (`admin:org`). Nothing to move by hand — the old registrations come off GitHub through their own `config.cmd remove`; if one does not, the summary names it and a later `uninstall` retries |
419
- | Config validation | EM's local validation util | The shared `@omega.js/config` schema, at boot and in the audit task | Nothing to move — but expect boot to report schema findings a legacy config silently carried, and fix them |
420
- | Preload global | `contextBridge.exposeInMainWorld('em', …)` → `window.em` | `window.desktop` | Rename every `window.em.*` call in renderer code |
421
- | Theme controls | `data-em-theme-set="system\|light\|dark"` | `data-omega-theme-set="system\|light\|dark"` | Rename the attribute in every view |
422
- | Client bridge | `web-manager-bridge.js`; `manager.webManager` in renderer entries | The renderer instance extends `@omega.js/client` (`omega.auth`, `omega.firestore`); main reads the account as `omega.auth` | Rewrite every `webManager.` call to `omega.` on the instance; the desktop shapes are in [one runtime shape](#2026-09-25-one-runtime-shape-on-every-package-945) |
423
- | Environment | `BACKEND_MANAGER_KEY` in the app's `.env` | `OMEGA_ADMIN_KEY`, resolved through the `.env` cascade (shell > local > brand root > company) | Rename the key; in a brand monorepo put the value at the brand root and leave the target's placeholder commented |
424
- | Bundler overrides | `config.em.webpack.externals`: an array of extra module names the desktop webpack build marked `commonjs2` external | GONE ([#737](https://github.com/Omega-JS-Stack/omega/issues/737)): the bundler is esbuild and there is no consumer-facing override key. The externals set is the framework's native-module list plus what the consumer's own `package.json` declares from it | Delete the key. Nothing replaces it: ESM-only dependencies BUNDLE (a CommonJS bundle keeps `import.meta.url`, [#906](https://github.com/Omega-JS-Stack/omega/issues/906)), and native modules stay external through the framework's own list. A consumer with a genuinely native module the list does not name raises it upstream: the list lives in `src/gulp/tasks/bundle.js` (`nativeExternals`) and grows there, so every brand gets the fix |
425
- | Gulp task name | `webpack` — `npm run gulp -- webpack`, and `[@omega.js/desktop:webpack]` in the logs | `bundle` — `npm run gulp -- bundle`, `[@omega.js/desktop:bundle]` ([#737](https://github.com/Omega-JS-Stack/omega/issues/737)) | Nothing for a normal consumer: the `build` / `package` / `publish` verbs are unchanged and nobody's npm scripts name the sub-task. Rename it in anything that invokes the gulp task directly, or greps `build.log` for the old tag |
426
- | Downloads mirror | `targets.desktop.downloads: { enabled, owner, repo, tag }`: a second public repo (`download-server`) holding one `installer` tag of fixed-name copies of every artifact, written by the `mirror-downloads` gulp task and by `finalize-release` | GONE ([#799](https://github.com/Omega-JS-Stack/omega/issues/799)): ONE public releases repo per brand, `<brand.id>-releases` by default, whose assets already carry no version ([#620](https://github.com/Omega-JS-Stack/omega/issues/620)), so `/releases/latest/download/<asset>` is the permanent link the mirror existed to provide. All four `downloads.*` keys are retired keys a carrying config now fails validation on | Delete the `downloads` block. Nothing replaces it: the site already links the releases repo, and `targets.desktop.releases` (both keys optional) addresses it. If a published link points at the old mirror repo, redirect it or re-point it at `https://github.com/<owner>/<brand.id>-releases/releases/latest/download/<asset>` |
427
- | Startup config | `startup.openAtLogin: true\|false` — a bare boolean | `startup: { openAtLogin: { enabled, mode } }` (`startup.mode` is a SEPARATE knob: the user-launch mode) | Rewrite the boolean as the object under the same `openAtLogin` key. The boolean is TEMPORARILY still read: the config schema declares only `startup.mode`, so it cannot reject the old shape yet — the acceptance leg retires with the schema entry ([#148](https://github.com/Omega-JS-Stack/omega/issues/148) trail) |
428
-
429
- ## `browser-extension-manager` → `@omega.js/extension`
430
-
431
- | Contract | Old form | New form | Manual migration step |
432
- |---|---|---|---|
433
- | Package + entries | `require('browser-extension-manager/build')` (the scaffold README named a `/test/assert` entry the package never exported) | `require('@omega.js/extension/build')`; no assert entry: a test's `run`/`inspect` receives `expect` on its context ([#812](https://github.com/Omega-JS-Stack/omega/issues/812)) | Swap the dependency and the require paths in `hooks/build/pre.js`, `hooks/build/post.js`, and `gulpfile.js`; in the tests, drop the assert require and use `ctx.expect` |
434
- | CLI | `xm` / `bxm` / `ext` / `browser-extension-manager` / `mgr` bins | `omega` / `omg` / `mgr` | Replace the bin name in npm scripts and workflows |
435
- | Config | `config/browser-extension-manager.json` | `config/omega.json5` (`targets.extension: {}` — key presence enables the target) | Key-by-key table in [config.md](config.md#browser-extension-manager-configbrowser-extension-managerjson--configomegajson5--done-checkpoint-20) |
436
- | Analytics secret | `analytics.providers.google.secret` in the config file | `.env` → `GOOGLE_ANALYTICS_SECRET` (the loader hard-fails secret-shaped config keys) | Move the value to `.env`; the build snapshot bakes it exactly as before |
437
- | Runtime singleton | `import webManager from 'web-manager'`; `manager.webManager` | `import omega from '@omega.js/extension/<context>'`: the context's instance carries the client's modules | Swap the import in every context (background, popup, options, sidepanel, content scripts) and read the client off `omega`; the extension shapes are in [one runtime shape](#2026-09-25-one-runtime-shape-on-every-package-945) |
438
- | DOM bindings | `data-wm-bind` | `data-omega-bind` | Rename the attribute in every view |
439
- | Cross-context messages | `{ command: 'bxm:syncAuth' }`, `{ command: 'bxm:signOut' }` | `{ command: 'omega:syncAuth' }`, `{ command: 'omega:signOut' }` | Rename in any custom `runtime.onMessage` handler or sender the extension ships |
440
- | Bundler | webpack 5 + `babel-loader` + `@babel/preset-env`, with per-lane code splitting (`*.chunk.<hash>.js` plus `.LICENSE.txt` sidecars beside every bundle) | esbuild through @omega.js/devkit's `bundle()` wrapper ([#738](https://github.com/Omega-JS-Stack/omega/issues/738)). No consumer-facing bundler override key existed and none was added; there are no chunks and no license sidecars — every entry is ONE self-contained iife — and the syntax floor is esbuild's `target`, `chrome88, firefox91` (the MV3 minimums), not a browserslist guess | Nothing in a normal project: entry filenames (`assets/js/components/<name>.bundle.js`) are unchanged, and the manifest / views ask for the same paths. Two things to check: anything that referenced a chunk file BY NAME (nothing generated does), and any code that imported a package @omega.js/extension merely carries TRANSITIVELY — only the framework's DECLARED dependencies resolve from the framework now, so declare that package yourself or raise it upstream ([#87](https://github.com/Omega-JS-Stack/omega/issues/87)) |
441
- | Gulp task name | `webpack` — `npm run gulp -- webpack`, and `[@omega.js/extension:webpack]` in the logs | `bundle` — `npm run gulp -- bundle`, `[@omega.js/extension:bundle]` ([#738](https://github.com/Omega-JS-Stack/omega/issues/738)) | Nothing for a normal consumer: the `build` / `publish` verbs are unchanged and nobody's npm scripts name the sub-task. Rename it in anything that invokes the gulp task directly, or greps `build.log` for the old tag |
442
- | Build snapshot delivery | `packaged/<browser>/raw/build.js`: a JSONP file of BXM's own shape, plus a `build.json` sidecar beside it | `packaged/<browser>/raw/build.js` again, but the ONE OMEGA wrapper and the one writer every surface uses: `self.OMEGA_BUILD_JSON = { config, package, mode, license, builtAt };` plus its `config.dev` line ([#743](https://github.com/Omega-JS-Stack/omega/issues/743)). The page template loads it first, `background.js` importScripts it first, and no bundle carries a copy. The `build.json` sidecar is gone | Nothing to do if you read `window.OMEGA_BUILD_JSON` / `self.OMEGA_BUILD_JSON`: the wrapper answers as before, and a `<script src="/build.js">` tag or `importScripts('/build.js')` in a template or worker you overrode is CORRECT again. The payload is the OMEGA subset, so a read of BXM's old flat keys moves (`omega.cache_breaker` → `config.buildTime`, `omega.environment` → `config.environment`, `omega.liveReloadPort` → `config.dev.liveReloadPort`). Tooling that read `build.json` runs `build.js` instead (`readBakedBuildJson()` in `src/gulp/tasks/utils/build-json.js`) |
443
-
444
- ## `web-manager` → `@omega.js/client`
445
-
446
- | Contract | Old form | New form | Manual migration step |
447
- |---|---|---|---|
448
- | Package + import | `import webManager from 'web-manager'` (npm `web-manager`) | `@omega.js/client`, the browser base class `Omega`; each host framework exports the one instance | Swap the dependency and import the host's instance. The modules read as properties (`omega.auth`, `omega.firestore`, `omega.bindings`), and `initialize(configuration)` carries over: [one runtime shape](#2026-09-25-one-runtime-shape-on-every-package-945) |
449
- | Global | `window.Manager` / a page-attached `webManager` | No window global: import the host framework's instance wherever it is needed | Delete the window assignments and the code that reads them |
450
- | DOM bindings | `data-wm-bind` | `data-omega-bind` | Codemod rule `client-markup` (templates, consumer JS, and section `.json` descriptors alike) |
451
- | Sign-out hook | `.auth-signout-btn` class, hardcoded in the auth module | The generic `omega-signout` click trigger (`registerTrigger('signout', …)` → class `omega-signout`) | Codemod rule `client-markup` renames the class everywhere it appears; custom behavior registers its own trigger instead of patching auth |
452
- | Device module | `webManager.usage()` | `omega.device` | Rename the call sites (`usage.js` became `device.js` verbatim) |
453
- | Configuration payload | The site emitted a `web_manager` block into `window.Configuration` | The `client` section of `OMEGA_BUILD_JSON.config` (#894, the row above) | Config-side rename: see the UJM row and [config.md](config.md); nothing dual-reads the old key |
454
- | Version-check manifest | The client probed `/build.json` then `/@output/build/build.json`, reading `data.timestamp` OR `data['npm-build'].timestamp` | One fetch of the site's `build.json` — mounted under the page's `data-omega-path-prefix` stamp on a site served from a URL path ([#364](https://github.com/Omega-JS-Stack/omega/issues/364)), `/build.json` at the domain root — and one read of `data.timestamp` (the omega web build emits exactly this) | Nothing to do on a migrated site. A site still serving the old path or the `npm-build` wrapper logs "No timestamp found in build.json" and never auto-reloads on a new deploy |
455
-
456
- ## `jekyll-uj-powertools` → `@omega.js/template-kit`
457
-
458
- | Contract | Old form | New form | Manual migration step |
459
- |---|---|---|---|
460
- | Delivery | Ruby gem `jekyll-uj-powertools` in the `Gemfile` | JS package `@omega.js/template-kit`, vendored into `@omega.js/web` | Remove the gem line (the whole `Gemfile` goes — see the UJM engine row); nothing to install, the filters and tags are registered by the web engine |
461
- | Tag + filter names | `uj_icon`, `uj_image`, `uj_readtime`, `uj_liquify`, … (`iftruthy`, `iffalsy`, `iffile`, `urlmatches` were already unprefixed) | `omega_image`, `omega_readtime`, `omega_liquify`, … — the four unprefixed names are unchanged, and `uj_icon` becomes MARKUP rather than a renamed tag (the icons row above) | Codemod rules `legacy-prefix` (every registered name) and `icon-tag-markup` (the icon tag); there are NO aliases, so a missed `uj_*` is an undefined tag/filter. Inventory: [docs/web/template-kit.md](../web/template-kit.md) |
462
- | Site namespace | `site.uj.*` (`cache_breaker`, `date.year`, `date.iso`, `placeholder.src`); Jekyll's `site.time` | `site.omega.*`; the build stamp is `site.omega.date.iso`. `site.time` stays, set by the engine to the same instant ([#613](https://github.com/Omega-JS-Stack/omega/issues/613)) | Codemod rule `legacy-prefix` handles `site.uj`; `site.time` needs nothing |
463
- | Markup hooks | `uj-password-show`, `uj-password-hide`, `uj-language-flag`, `uj-language-dropdown`, `uj-schema-*`, `data-uj-no-translate` | `omega-password-show`, `omega-password-hide`, `omega-language-flag`, `omega-language-dropdown`, `omega-schema-*`, `data-omega-no-translate` | Codemod rule `legacy-prefix` renames all of them; check hand-written CSS/JS that selects on the old names |
464
- | Ruby generators + hooks | `variable_resolver.rb`, `blog-taxonomy.rb`, `inject-properties.rb`, `dynamic-pages.rb`, `limit-collections.rb`, `markdown-images.rb`, `parallel-build.rb` | Engine features of `@omega.js/web` (frontmatter Liquid, collections + pagination, the `resolved` cascade) — not template functions | Nothing to migrate: they were never consumer-callable. Which of them exist today and which never got ported is [#78](https://github.com/Omega-JS-Stack/omega/issues/78)'s table, not this register |
465
-
466
- ## `omega-manager` → `@omega.js/manager`
467
-
468
- | Contract | Old form | New form | Manual migration step |
469
- |---|---|---|---|
470
- | Brand config home | Central: `omega-manager/.brands/<id>/config.json` (one repo describing every brand) | The brand's OWN repo: `config/omega.json5` | Convert the brand's config file into the brand repo using the tables in [config.md](config.md) (shared sections at the top level, per-surface settings under `targets.<name>`), then retire the `.brands/<id>/` entry |
471
- | Enabled targets | `targets: ['website', 'backend']` — an array | `targets: { web: {}, backend: {} }` — an object where key PRESENCE enables the target | Rewrite the array as object keys; note the rename `website` → `web` |
472
- | Repo topology | One clone per surface, located by `local.folder` + `github.orgMain` / `orgWebsite` | ONE brand monorepo: npm workspaces, `targets/<target>/` per enabled target ([docs/manager/brand.md](../manager/brand.md)) | Merge the per-surface repos into one brand repo as `targets/web`, `targets/backend`, `targets/desktop`, `targets/extension`; the brand root holds `config/omega.json5`, `.env`, `assets/`, and the workspace `package.json` |
473
- | Durable state | `omega-manager/.output/<id>/state.json` | No durable-state CACHE at all — every provisioned fact lands in the brand's `config/omega.json5`, every secret in its `.env` ([#434](https://github.com/Omega-JS-Stack/omega/issues/434)); the brand's own `.omega/state.json` keeps only per-machine records (the deploy stamps, [#479](https://github.com/Omega-JS-Stack/omega/issues/479)) | Nothing to copy: a manage run resolves the ids from the platform and writes them into their real homes. Never commit `.omega/` |
474
- | Service names (the last eight) | Services still carried their PROVIDER's name: `github` / `cloudflare` / `recaptcha` / `search-console` / `adsense` / `slapform` / `chatsy` / `replyify` ([#418](https://github.com/Omega-JS-Stack/omega/issues/418)) | Every service now matches its config ROLE key: `repo` / `edge` / `captcha` / `search` / `advertising` / `forms` / `chat` / `email`. The provider KEYS underneath are unchanged (`repo.providers.github`, `edge.providers.cloudflare`, `captcha.providers.recaptcha`, `search.providers.searchConsole`, `advertising.providers.adsense`, `forms.providers.slapform`, `inbound.chat.providers.chatsy`, `inbound.email.providers.replyify`) — the service is the role, the provider is a value | Update `--service=<name>` in any script or cron (`--service=cloudflare` is now `--service=edge`, …) and expect the walk's `[NAME]` log tag to change with it |
475
- | AGENTS.md healing | Line 1 imported `@node_modules/@omega.js/manager/AGENTS.md`; the walk rewrote that target and scrubbed the cp244 marker/skeleton lines | Line 1 imports `@node_modules/@omega.js/AGENTS.md`; the walk only prepends a missing current import, never rewrites retired lines | Delete the old import line and any cp244 marker/skeleton line by hand — left in place they survive as consumer content (a dangling duplicate import) |
476
- | package.json script healing | Script values leading with the `omega-manager` bin token were healed to `omega` on every walk | Missing `manage: "omega"` and `deploy: "omega deploy"` scripts are minted, and exactly one value migrates: the legacy `start: "omega"` becomes `start: "omega dev"` ([#227](https://github.com/Omega-JS-Stack/omega/issues/227)); every other script value, the `omega-manager` token included, is never rewritten | Replace the leading `omega-manager` token with `omega` by hand (arguments unchanged) — unedited, the script fails at run time with command-not-found: no `omega-manager` bin exists (Ian retired the vestigial shim, 2026-08-05) |
477
- | Secrets store | `omega-manager/.output/<id>/secrets/*.json` | The brand's `.omega/secrets/*` plus the `.env` cascade (local → brand → company) | Move the files into the brand's `.omega/secrets/`; secret VALUES belong in `.env`, never in `omega.json5`. The Google OAuth client changed shape (`oauth.json` `{ googleClientId, googleClientSecret }` → `google-oauth.json` `{ clientId, clientSecret }`): carry the legacy file in and `npx omega onboard` converts it ONCE and removes it ([#501](https://github.com/Omega-JS-Stack/omega/issues/501)) — nothing dual-reads the old name |
478
- | Brand assets | `omega-manager/.brands/<id>/assets/` | The brand repo's `assets/` (logo sources, templates); derived variants land in the gitignored `.omega/assets/` | Copy the source assets into the brand repo; a manage cycle regenerates the derived set |
479
- | Entry point | `npm start` inside the omega-manager repo, all brands at once, `--brand <id>` to narrow | `npx omega` (or `npm run manage`) inside the BRAND root — one brand, always; `--service=<name>` still narrows to one service | Run the manage cycle from the brand repo; there is no cross-brand run |
480
- | Retired flags | `--bump`, `--build`, `--sync`, `--deploy`, `--exec`, `--dirty`, `--force-recreate` | Gone with the multi-repo-clone model | Use the per-target verbs instead: `omega deploy` at the brand root fans out (backend → web → extension/desktop), `omega update` handles version bumps. The drop calls are recorded in [#78](https://github.com/Omega-JS-Stack/omega/issues/78) |
481
-
482
- ## Cross-cutting
483
-
484
- | Contract | Old form | New form | Manual migration step |
485
- |---|---|---|---|
486
- | Config keys (every framework) | Per-framework key names and homes | The omega.json5 schema | **Do not re-derive them here** — the key-by-key mapping tables are in [config.md](config.md#migration--legacy-configs--omegajson5), one table per legacy framework, and they are the SSOT |
487
- | Config file | One JSON file per framework (`ultimate-jekyll-manager.json`, `backend-manager-config.json`, `electron-manager.json`, `browser-extension-manager.json`) | ONE format everywhere: `config/omega.json5`, merged `defaults ← company ← brand shared ← brand targets.<name> ← local shared ← local targets.<name>` | Convert once, delete the old file. In a brand monorepo the targets carry no config of their own: the brand root's file owns everything |
488
- | Retired key names | Renamed keys used to validate clean and their contents vanished | Retired names FAIL validation wherever they sit, naming their replacement (`web_manager` → `client`, `firebaseConfig` → `cloud`), plus path-based retirements (`slapform` → `forms.providers.slapform`, `cloudflare` → `edge.providers.cloudflare`, …) | Fix what the validator names; the full retired list is [config.md](config.md#migration--legacy-configs--omegajson5) |
489
- | Secrets | Secret values sat in the framework config files | Config hard-fails secret-shaped keys; secrets live in `.env`, resolved through the cascade shell > local > brand root > company | Move every secret out of config into `.env`, brand-root first so the targets inherit it |
490
- | CLI bins | EVERY legacy framework shipped a `mgr` bin (plus `uj`/`bm`/`em`/`bxm`), so in a multi-target repo whichever npm hoisted won | One context-aware dispatcher: `omega` / `omg` / `mgr`, all identical — the nearest `package.json` walking up from cwd names the framework whose CLI runs | Replace legacy bin names in npm scripts and CI; run the verb from the target dir that owns it |
491
- | CLI default | A bare `omega` / `mgr` at a brand root RAN the whole service walk (omega-manager's default command) | Every verb is named: `omega manage` is the walk (one name, no alias), `omega dev` the local stack, `omega deploy` the publish. A bare `omega` prints help and touches nothing ([#229](https://github.com/Omega-JS-Stack/omega/issues/229)) | Replace bare `omega`/`mgr` invocations with `omega manage` in scripts, cron, and CI. A brand still carrying `manage: 'omega'` must be walked ONCE by hand — `npx omega manage` — because its own `npm run manage` would print help; that walk heals the script to `omega manage` |
492
- | Per-target setup | `omega setup`, run BY HAND in every target (`cd targets/<dir> && npx omega setup`), and the default command a bare `omega` ran | The command is gone. Its LOCAL half — node check, defaults scaffold, `package.json` scripts sync, peer-dependency check, locality check — is `ensureTarget()`, which every verb (`dev`/`build`/`test`/`deploy`) runs first, idempotently and offline. Its NETWORK half — secret publication, cert validation, repo provisioning, framework freshness — is a precheck inside `omega deploy`, opted out with `--no-secrets` on all three frameworks (desktop's `--quick` is gone). A bare `omega` prints help ([#675](https://github.com/Omega-JS-Stack/omega/issues/675)) | Delete `npx omega setup` from npm scripts, CI workflows and runbooks — the verb beside it already does it. A scaffolded workflow's `npx omega setup && npm run build` becomes `npm run build`. Extension consumers on a pre-2.0.0 hook layout run the one-time `npx omega migrate` once |
493
- | Package script spelling | Backend, desktop and extension wrote `npx omega <verb>` into every framework-owned target script (web already spelled it bare) | Bare `omega <verb>` on all four frameworks ([#748](https://github.com/Omega-JS-Stack/omega/issues/748)) — npm puts `node_modules/.bin` on the path inside a script, so the prefix bought nothing there; `npx omega` stays canonical for docs and the terminal | Nothing by hand: the next verb's `ensureTarget()` rewrites every framework-owned key in place and a second run is a no-op — commit the one-line diff. A script key the framework never declares is yours and is never touched, and `npx omega <verb>` keeps working wherever you spell it yourself |
494
- | Publish | Per-framework release scripts and `npu sync` | `omega deploy` on every target — deliberate, never triggered by a push; at a brand root it fans out (backend → web → extension/desktop) | Replace publish scripts with `omega deploy`; contract in [deploys.md](deploys.md) |
495
- | Dependency updates | Framework self-update + peer-dependency auto-install at setup | The explicit `omega update` verb (report first, `--apply` installs, majors opt-in). The peer-dependency install still rides every verb's `ensureTarget()` — a satisfied target installs nothing — and the framework self-update moved to the `omega deploy` precheck ([#675](https://github.com/Omega-JS-Stack/omega/issues/675)) | Run it deliberately — [updates.md](updates.md) |
496
- | Environment prefixes | `BACKEND_MANAGER_*`, `BEM_*`, framework-specific names | `OMEGA_*` — except a THIRD-PARTY credential, which keeps the vendor's own name (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `STRIPE_SECRET_KEY`); `OMEGA_*` is for the keys OMEGA itself mints or owns | Rename in `.env`, CI secrets, and every reader. `@omega.js/config`'s env schema is the current list — the brand root's `.env` stub renders from it |
497
- | Default/Custom file markers | The marker grammar in `_.gitignore`, `AGENTS.md` (no framework scaffolds a target `.env` any more) | **UNCHANGED — this is not legacy.** The Default/Custom marker grammar is the live defaults-engine mechanism that merges framework-owned lines into consumer-owned files | Leave the markers alone in migrated files; every verb's `ensureTarget()` rewrites the Default block and preserves everything under Custom |
498
-
499
- ## One provider shape — `role.providers.<provider>` ([#425](https://github.com/Omega-JS-Stack/omega/issues/425))
500
-
501
- Not a legacy→OMEGA row: this is an OMEGA-internal config rename, made
502
- deliberately before the first npm publish closes the window. Config sections
503
- carried four shapes for the same idea — a `providers` block, a singular pick
504
- (`provider: 'sentry'`), a bare vendor key (`certificates.apple`) and a fourth
505
- word (`payment.processors`) — so a consumer had to memorise which section used
506
- which. Every role now names its vendors ONE way: `role.providers.<provider>`,
507
- where key PRESENCE is the pick and `false` is the deliberate off switch. The
508
- null = unset / false = disabled tri-state is unchanged; it just moved into the
509
- key. `cloud` is the ratified exception (`cloud.provider` + `cloud.config` — its
510
- discriminator is read by 34 runtime files across every framework's bootstrap
511
- and already delivers no-rename-on-a-second-provider).
512
-
513
- Every row below is enforced: the old path FAILS validation naming its
514
- replacement, so nothing is silently lost. There is no dual-read.
515
-
516
- | Contract | Old form | New form | Manual migration step |
517
- |---|---|---|---|
518
- | Payment providers | `payment.processors.{stripe,paypal,chargebee,coinbase}` | `payment.providers.{stripe,paypal,chargebee,coinbase}` — contents identical | Rename the one key in `config/omega.json5`. The singular `processor` followed one issue later — the whole word is `provider` now, stored data included ([#428](#one-word--provider-everywhere-428) below) |
519
- | Apple signing | `certificates.apple.{bundleIdPrefix,capabilities,profiles,certificates}` | `certificates.providers.apple.{…}` — contents identical | Nest the `apple` block one level under `providers`. Windows signing will sit beside it rather than adding a second bare vendor key. Nothing on disk moves: the signing tree stays `{companyRoot\|\|brandRoot}/.omega/certificates/apple/` |
520
- | Domain registrar | `domain.provider: 'namecheap' \| 'squarespace' \| null` | `domain.providers.<registrar>` — e.g. `providers: { namecheap: {} }` | Replace the string with a keyed entry. **No entry = none chosen** and the domain service skips, exactly what `null` meant |
521
- | Mailbox provider | `domain.email.provider: 'cloudflare' \| 'squarespace' \| 'privateemail' \| null` | `domain.email.providers.<provider>` | Same edit one level down. `domain.email.forwarding` stays role-level — it is provider-agnostic |
522
- | Translation engine | `translation.provider: 'claude' \| 'chatgpt'` | `translation.providers.<name>` — `{ claude: {} }` or `{ chatgpt: {} }` | Replace the string with a keyed entry. An absent block still means `claude` (the no-API-key default), so a brand that never set `provider` needs no edit. `translation.model` stays role-level — it overrides whichever engine is chosen |
523
- | Devlog writer | `devlog.provider: 'ghostii'` plus its settings flat on `devlog` (`lookbackDays`, `orgs`, `excludeRepos`, `excludeCommits`, `excludeTopics`, `includePrivate`, `postPath`, `destinations`, `overrides`) | `devlog.providers.ghostii.{lookbackDays,orgs,excludeRepos,excludeCommits,excludeTopics,includePrivate,postPath,destinations,overrides}` | Move the nine provider-hung keys inside `providers.ghostii` and drop the `provider` string (the key IS the writer). `devlog.enabled` stays role-level — it is the pipeline's switch, not the writer's |
524
- | Error monitoring | `monitoring.provider: 'sentry'` plus its settings flat on `monitoring` (`org`, `dsn`, `environment`, `sampleRate`, `tracesSampleRate`, `scrubEmail`, `attachScreenshot`, `bundlePatterns`) | `monitoring.providers.sentry.{org,dsn,environment,sampleRate,tracesSampleRate,scrubEmail,attachScreenshot,bundlePatterns}` | Move the eight knobs inside `providers.sentry` and drop the `provider` string (the key IS the monitor). `monitoring.enabled` stays role-level. **Per-surface DSNs move too**: `targets.<name>.monitoring.dsn` → `targets.<name>.monitoring.providers.sentry.dsn`: the manager's `monitoring/dsn` operation writes the new path, so a rerun re-lands them. DSN presence is still the runtime enable signal; nothing on Sentry's side changes (same projects, same keys, same release tags) |
525
- | Email marketing | `marketing.campaigns.provider: 'sendgrid'` + `marketing.campaigns.listId` | `marketing.campaigns.providers.sendgrid.listId` | Nest `listId` under `providers.sendgrid` and drop the `provider` string. `marketing.campaigns.enabled` stays role-level. The list itself is untouched — the id is the same SendGrid UUID, and the campaigns service writes the new path on its next run |
526
- | Newsletter | `marketing.newsletter.provider: 'beehiiv'` + `marketing.newsletter.publicationId` | `marketing.newsletter.providers.beehiiv.publicationId` | Nest `publicationId` under `providers.beehiiv` and drop the `provider` string. **`marketing.newsletter.enabled` AND `marketing.newsletter.content` stay role-level** — `content` (sources, categories, tone, template, theme, sponsorships) configures @omega.js/backend's newsletter GENERATOR, not Beehiiv, so it does not move. Beehiiv holds no copy of the id: inbound webhooks are matched against config, so there is no data migration |
527
- | AI blog | `blog.provider: 'ghostii'` | `blog.providers.ghostii` (key presence chooses the writer) | Replace the string with an (empty) `providers.ghostii` entry, or omit the block — an absent `providers` still defaults to ghostii. `blog.enabled` and `blog.content` stay role-level (content is pipeline config) |
528
-
529
- Per-target overrides convert on the same terms: a `targets.<name>` block
530
- carrying any of these keys is resolved at the top level, so it fails validation
531
- with the same message and takes the same edit.
532
-
533
- Two words the brand files carried but nothing read are simply gone with the
534
- edit: `marketing.campaigns.platform` / `marketing.newsletter.platform` (a
535
- legacy-BEM spelling of the same pick) BECOME the provider key. They are not in
536
- the retired-path guard — they were already dead config, so nothing was lost
537
- before or after — but leaving one behind now buys nothing.
538
-
539
- ## One word — `provider` everywhere ([#428](https://github.com/Omega-JS-Stack/omega/issues/428))
540
-
541
- The other half of #425, and the same window: #425 normalized the config SHAPE
542
- and deliberately left the singular runtime word `processor` standing, which
543
- meant a brand read `payment.providers` in config and wrote `processor` on every
544
- document it produced. Ian's ruling (2026-08-21): consistency wins — ONE word,
545
- `provider`, in code, on the API, and in stored data. Everything below changed
546
- together; there is no dual-read on any of it.
547
-
548
- Third-party vocabulary is untouched: where a field name belongs to Stripe,
549
- PayPal, Chargebee or Chargeblast, it is still read verbatim (Chargeblast's
550
- alert payload still sends `processor`, and the route normalizes it onto our
551
- `provider` on the way in).
552
-
553
- | Contract | Old form | New form | Manual migration step |
554
- |---|---|---|---|
555
- | Payments API param | `processor` on all eight payments routes (`intent`, `cancel`, `plan`, `portal`, `refund`, `uncancel`, `webhook`, `winback`) — body field or `?processor=` query, per route | `provider` in the same position | Rename it in every caller. A first-party brand gets this from the framework's own frontend; a CUSTOM caller (a mobile client, a server integration, a saved webhook URL at Stripe/PayPal/Chargebee) must be repointed — the webhook and dispute-alert routes read it from the QUERY STRING, so the URL registered at the provider changes too. `npx omega manage` rewrites the webhook URLs it owns; anything registered by hand is a by-hand edit. An unrenamed param is an `Unknown provider: undefined` 400, never a silent default |
556
- | Checkout dev param | `?_dev_cardProcessor=test\|stripe\|chargebee` | `?_dev_cardProvider=…` | Update saved QA links and any script driving a dev checkout. The old param is ignored (the palette control reads only the new name) |
557
- | Email merge field | `user_subscription_payment_processor` (path `subscription.payment.processor`) | `user_subscription_payment_provider` (path `subscription.payment.provider`) | Rename the token in every custom email template. An unrenamed token resolves to nothing — the merge-field table no longer declares the old name |
558
- | Analytics param | `payment_processor` on every commerce event (`purchase`, `refund`, plan changes, trial events) | `payment_provider` | Rename it in saved GA4 explorations, custom dimensions, and any dashboard filtered on the old key. Registered GA4 custom dimensions are per-name: register `payment_provider` alongside, and historical rows keep the old name |
559
- | Stored Firestore field | `processor` on `payments-orders` / `payments-intents` / `payments-webhooks`, `alert.processor` on `payments-disputes`, and `subscription.payment.processor` on `users` | `provider` / `alert.provider` / `subscription.payment.provider` | **This one has DATA.** Deploy the new backend, then run the backfill against the brand's Firestore: `npx omega manage --migration=payment-provider` audits (prints per-collection would-change counts and writes nothing), and `--migration=payment-provider --execute` performs it. Idempotent and re-runnable: a doc already on the new field is skipped, a doc carrying both keeps `provider` and drops the leftover. Order matters only in that the sweep is safe either side of the deploy — a doc written by the new code needs no fix |
560
- | Firestore composite index | `subscription.payment.processor` ASC + `subscription.cancellation.pending` ASC (the PayPal expiry cron's query) | Same index on `subscription.payment.provider` | The next verb's `ensureTarget()` rewrites `firestore.indexes.json`; deploy indexes before the backfill so the cron's query has one when the data lands. The old index is orphaned — delete it in the Firebase console once nothing queries the old field |
561
- | Payment module directory | `libraries/payment/processors/<vendor>.js`, the per-route `<route>/processors/` folders, and `libraries/load-processor.js` (`loadProcessor()`) | `libraries/payment/providers/`, `<route>/providers/`, `libraries/load-provider.js` (`loadProvider()`) | Only a brand that requires a framework payment module by path (a custom route, a custom cron) is affected: repoint the require and rename the call |
562
- | Admin payment sub-handler dir | `<brandRoot>/payment-processors/<productId>.js` — the per-product handler `POST /omega/admin/payment` loads | `<brandRoot>/payment-providers/<productId>.js` | Rename the directory. Nothing else about the handler contract changed; a brand with no such directory (almost all) has nothing to do |
563
-
564
- ## Two homes, not three — the `.omega/state.json` CACHE retired ([#434](https://github.com/Omega-JS-Stack/omega/issues/434))
565
-
566
- omega-manager's three-bucket principle came across intact: user choices in
567
- config, durable derived data in `.omega/state.json`, per-run transients in
568
- `.omega/runs/`. The middle bucket did not earn its keep. Almost everything in
569
- it was a CACHE of what each idempotent ensure re-reads from the platform on
570
- every run anyway, and the handful of facts that were genuinely durable were
571
- sitting in a gitignored per-machine file instead of the two homes the
572
- frameworks actually read — so a fresh clone silently lost them, and a
573
- brand's `omega.json5` could stay `null` for months next to a state file that
574
- had the answer.
575
-
576
- The rule now: **a provisioned fact lands in `config/omega.json5`, a secret
577
- lands in the brand `.env`, and everything else re-derives.** The handler
578
- return key `state` survives as the WITHIN-RUN carry (how the zone operation
579
- hands its zone id to the operations after it) and is written to no file.
580
-
581
- `.omega/runs/{ts}.json` is unchanged, and so is `.omega/state.json` — what
582
- retired is its CONTENT, not the file. It lives on as the ONE per-machine
583
- RECORD file, sectioned per fact kind
584
- ([#479](https://github.com/Omega-JS-Stack/omega/issues/479)): its `deploy`
585
- section is the record `@omega.js/devkit/deploy-record` writes on every
586
- successful deploy verb, and future record kinds join it as sibling top-level
587
- sections. The state-retirement migration owns only the service-keyed sections
588
- it retires and leaves every record section alone; deploy-record owns only
589
- `deploy` and passes every other section through verbatim, so an unmigrated
590
- brand's records are read in place beside its config-shaped leftovers. The one
591
- record move that still happens is the interim `.omega/deploys.json` the
592
- records spent 0.45.0 in
593
- ([#449](https://github.com/Omega-JS-Stack/omega/issues/449)): it needs no
594
- migration step — deploy-record folds it into state.json on its first read or
595
- write, says so in one line, and removes the old file.
596
-
597
- | Contract | Old form | New form | Manual migration step |
598
- |---|---|---|---|
599
- | The state file | `.omega/state.json`, per-service keyed, written after every service | The service-keyed sections are gone. The migration moves each fact to its home and deletes the file — or trims it to its machine records (`deploy`, and any future record section) when the brand has any | **This one has DATA.** `npx omega manage --migration=state-retirement` audits (prints every planned move and writes nothing); `--migration=state-retirement --execute` performs it. Idempotent: the executed run leaves nothing to move, so a re-run is a clean no-op. It is the ONE `local: true` migration — no backend target, no service account, no Firestore |
600
- | GA4 Measurement Protocol secrets | `analytics.streams.{target}.apiSecret` in state; the disperse service read it from there | `GOOGLE_ANALYTICS_SECRET_{TARGET}` in the brand `.env`, written by the analytics service; the schema's `deliverAs` composes each target's own `GOOGLE_ANALYTICS_SECRET` from it on every verb | The migration moves them. A brand with no state file re-resolves them from GA on the next `npx omega manage` |
601
- | VAPID key pair | `cloud.cloudMessaging.{vapidPublicKey,vapidPrivateKey}` in state | Public half → `cloud.messaging.vapidKey` in omega.json5 (it ships to every browser); private half → `VAPID_PRIVATE_KEY` in the brand `.env` | The migration splits them. Lose them and there is no API to re-read them: an interactive run re-prompts for the pair from the Firebase console |
602
- | Cloudflare zone id | `edge.zoneId` in state (read by nothing across runs) | `edge.providers.cloudflare.zone` in omega.json5 — the id `omega purge` targets from a build, where no Cloudflare token is in play | The migration moves it, and the edge service now keeps it current: the resolved id always wins over a stale hand-set one |
603
- | Reconcile confirmations | `search.gaLinked`, `payment.{radarConfirmed,disputesConfirmed}`, `cloud.authentication.oauthRedirectsConfigured`, `captcha.domainsConfirmed` in state | `search.providers.searchConsole.gaLinked`, `payment.providers.stripe.{radarConfirmed,disputesConfirmed}`, `cloud.oauthRedirectsConfigured`, `captcha.providers.recaptcha.domainsConfirmed` in omega.json5 | The migration moves them. These are the only reconcile flags config keeps — each is a human console action with no read API on either side, so nothing can re-check it. Every OTHER flag was dropped: the ensure re-reads the platform |
604
- | Everything else | Repo identity, Search Console property URL, GA stream ids and URIs, Stripe account id, provider product ids, Apple certificates/profiles/bundle id, Sentry project map, the Firebase SDK config, billing/firestore/database/storage/hosting/functions status, SendGrid + Beehiiv names | Not persisted anywhere | Nothing to do — every one of them is re-read from its platform (or re-derived from config) by the idempotent ensure that owned it. The provider product ids and the SDK config already had their config home; state was a duplicate |
605
- | `@omega.js/manager/state` subpath export | `require('@omega.js/manager/state')` → `readState` / `writeState` / `statePath` / `STATE_DIR` | Removed. `writeRunOutput` moved to `src/lib/run-output.js` and is still exported from the package root | Delete the import. Nothing in the monorepo or any brand used it |
606
-
607
- ## One vocabulary — a brand's surfaces live in `targets/` ([#443](https://github.com/Omega-JS-Stack/omega/issues/443))
608
-
609
- Config has always called them `targets`; the folder they lived in said `apps/`,
610
- and this monorepo's own `apps/` meant something else again — the in-repo test
611
- BRANDS. Three meanings for two words. Ian's ruling (2026-08-21): one
612
- vocabulary. A brand monorepo's surfaces live under `targets/`, and this
613
- monorepo's brands live under `brands/`. Same behavior, new names — discovery,
614
- scaffolding, the FILE_MAP semantics, disperse, the CI workflow templates and
615
- every doc changed together, and nothing dual-reads `apps/`.
616
-
617
- | Contract | Old form | New form | Manual migration step |
618
- |---|---|---|---|
619
- | Brand folder | `<brandRoot>/apps/<target>` — `apps/website`, `apps/backend`, `apps/desktop`, `apps/extension`, `apps/website-admin` | `<brandRoot>/targets/<target>` — the dir names inside are unchanged | **Run the migration ONCE, per brand**: `npx omega manage --migration=targets-rename --execute` (bare, without `--execute`, prints the plan and moves nothing), then `npm install` at the brand root. Nothing heals this inside a normal run — every other verb FAILS LOUD on the old shape and points here. Idempotent — a migrated brand re-runs as a no-op. A brand carrying BOTH folders is a half-done migration and fails loudly instead of guessing: merge them into `targets/` by hand, delete `apps/`, run again |
620
- | Root workspaces glob | `"workspaces": ["apps/*"]` | `"workspaces": ["targets/*"]` | The same migration flips the entry (every other entry is preserved), and heals it on its own for a folder you renamed by hand. Run `npm install` at the brand root afterwards so npm re-links `node_modules/<app>` at the new path |
621
- | Paths in YOUR files | `apps/website/...` in the brand's own scripts, CI workflows, editor config, READMEs, `.env` comments | `targets/web/...` | By hand: the migration moves the folder, never your text. `npx omega manage` rewrites the workflow templates it owns; anything you authored is yours to grep |
622
- | This monorepo's brand folder | `apps/sandbox-brand`, `apps/omega-playground`, `apps/newsflash-brand` | `brands/<same>` | Monorepo-internal — nothing for a consumer brand to do |
623
- | CI base-path helper ([#455](https://github.com/Omega-JS-Stack/omega/issues/455)) | `require('@omega.js/web/deploy').appPathPrefix()` — called by name from the scaffolded `.github/workflows/build.yml` | `targetPathPrefix()` — same signature, same behavior | A brand whose workflow still calls the old name re-scaffolds it (`npx omega manage` rewrites the workflows it owns) at its migration session; no alias exists |
624
- | Config merge-layer word ([#455](https://github.com/Omega-JS-Stack/omega/issues/455)) | The per-target-dir layer was the **app layer**: docs said `… ← app shared ← app targets.<name>`, `loadConfig()`/`composeTargetConfig()` returned `files.app`, and `resolveEnvChain()` returned `{ app, brand, company }` | The **local layer**: `… ← local shared ← local targets.<name>`, `files.local`, `{ local, brand, company }` | Nothing in an authored `omega.json5` changes (the layer is positional, no `app:` key ever existed). Code reading `files.app` or `chain.app` renames the key; no alias exists |
625
-
626
- ## One DNS flip — `emailurl.<domain>` rides the Cloudflare proxy ([#646](https://github.com/Omega-JS-Stack/omega/issues/646))
627
-
628
- Links inside a transactional email opened with the browser's insecure-site
629
- warning on every brand not yet on OMEGA. The cause, confirmed live on
630
- 2026-08-27: SendGrid rewrites every link through the branded link host
631
- `emailurl.<domain>`, that host serves NO certificate of its own
632
- (`https://emailurl.<legacy domain>` fails certificate validation, while plain
633
- HTTP answers), and the account's link branding carries no SSL setting at all —
634
- every one of its 36 entries is `valid: true`, `legacy: false`, with no `ssl`
635
- field. So a click could only ever land on an `http://` hop first, and Chrome
636
- said so. The edge service's DNS default now creates that one record
637
- **proxied**, so Cloudflare terminates TLS at the edge with the zone's own
638
- certificate and forwards to sendgrid.net.
639
-
640
- **The flip is ORDERED, and the DNS ensure enforces the order itself.** SendGrid
641
- validates a branded link by resolving `emailurl.<domain>` as a CNAME to
642
- sendgrid.net, and a proxied record answers with Cloudflare's addresses instead
643
- — so proxying first locks a NEW domain's branding out of ever validating. Every
644
- run the edge service reads `GET /v3/whitelabel/links` and only writes
645
- `proxied: true` when SendGrid reports that host `valid: true`; until then the
646
- record stays grey-clouded and the run warns, naming the record. Nothing to
647
- sequence by hand: since
648
- [#693](https://github.com/Omega-JS-Stack/omega/issues/693) the campaigns
649
- service CREATES the branding when the account has none, writes both of its
650
- CNAMEs, waits for the validation later in the same walk, and flips that record
651
- to proxied itself —
652
- the edge service's next read agrees, because a valid branding desires a proxied
653
- record.
654
-
655
- | Contract | Old form | New form | Manual migration step |
656
- |---|---|---|---|
657
- | `emailurl.<domain>` CNAME → `sendgrid.net` | Grey-clouded (`proxied: false`), so the click resolved straight to SendGrid over HTTP | Orange-clouded (`proxied: true`) once SendGrid reports the link branding valid — the only SendGrid record that is; `emailauth`, the `<sendgrid id>` owner CNAME and both DKIM keys stay grey, because SendGrid validates those by CNAME lookup | **Per legacy brand, once**: run `npx omega manage` for a brand the edge service owns — an already-validated branding (every live ITW brand) flips on that run; a branding still pending stays grey with a warning, and the rerun after SendGrid validates flips it. By hand elsewhere: validate the branded link in SendGrid FIRST, then turn the proxy ON for `emailurl.<domain>` in Cloudflare |
658
-
659
- ## Unsubscribe-group ids leave the code — config carries them ([#649](https://github.com/Omega-JS-Stack/omega/issues/649))
660
-
661
- Every transactional send attaches a SendGrid unsubscribe (ASM) group, and BEM
662
- hardcoded the seven ids in `constants.js`. Those ids belong to the SendGrid
663
- ACCOUNT they were created in — ITW's — so a brand on any other account either
664
- sent under a group that does not exist there or under a stranger's. Ids are
665
- account data, never code: the campaigns service now provisions the seven groups
666
- by NAME and writes each id into the brand's own `config/omega.json5`, and the
667
- backend keeps only the KEYS (`GROUP_KEYS` in `constants.js`).
668
-
669
- Matching by name is what makes sibling brands sharing one SendGrid account
670
- converge: the second brand's run finds the groups the first one created and
671
- lands the SAME ids.
672
-
673
- **A missing id fails the send LOUDLY** (coded 400, naming the config path). It
674
- is a programmer error — the manage walk never ran for this brand — and a
675
- fallback would be the exact bug this removes.
676
-
677
- | Contract | Old form | New form | Manual migration step |
678
- |---|---|---|---|
679
- | ASM group ids | `GROUPS` in `packages/backend/src/manager/libraries/email/constants.js` — seven literal ids compiled into the framework | `marketing.campaigns.providers.sendgrid.groups.<key>` in the brand's `config/omega.json5`, one integer per key (`orders`, `hello`, `account`, `marketing`, `security`, `newsletter`, `internal`) | **Per brand, once**: run `npx omega manage` (or `--service=campaigns`) — the campaigns service creates any group the account is missing and writes all seven ids into `config/omega.json5`. By hand: read the ids from SendGrid → Suppressions → Unsubscribe Groups and write the block yourself. The ensure matches by NAME, so on an account whose groups carry legacy-prefixed names ("BEM - Order Updates"), RENAME them to the canonical "OMEGA - " names FIRST (ids never change, so legacy sends keep working) — otherwise the ensure creates a duplicate set and unsubscribe state splits (this happened once on the shared account, repaired 2026-08-29). A brand on its own fresh account just gets its own groups. Until the block exists, every send throws instead of mailing under a foreign group |
680
- | `prepare.resolveSender()` signature | `resolveSender({ sender, from, group }, brand, brandDomain)` | `resolveSender({ sender, from, group }, brand, brandDomain, omega)`: the fourth argument carries the config the ids live in | Only affects code calling `prepare.js` directly: pass `omega` as the fourth argument. `group:` still accepts a raw numeric id AND now accepts a group KEY, which resolves through config |
681
-
682
- ## `brand.subdomains` becomes web targets ([#588](https://github.com/Omega-JS-Stack/omega/issues/588))
683
-
684
- A legacy brand running sibling sites off one domain declared them as a
685
- `brand.subdomains` list (soundgrail: `[app, music, exhale]`). Nothing in OMEGA
686
- DECLARED that key (no schema rule, no default, never materialized) and one
687
- thing read it: the cloud hosting op, which ensured an `api.{sub}.{domain}`
688
- Firebase Hosting domain per entry. Ian's 2026-09-01 call: each subdomain shares
689
- the ONE backend, and the fact the list was reaching for is a web TARGET.
690
-
691
- So a target is the home. The target's NAME IS the subdomain, so a bare
692
- `admin: { type: 'web' }` resolves to `https://admin.<brand host>`; an entry's own
693
- `url` overrides it for a custom host, and every target shares one
694
- `api.<domain>`. `subdomains` is a retired KEY now (a name test, so it fires at
695
- every depth): a config still carrying the list fails validation with the recipe
696
- instead of silently steering the hosting op.
697
-
698
- The brand-level facts stay brand-level. `cloud.config.authDomain` is compared
699
- against `brand.url` for every target (one Firebase project, one backend, one
700
- authDomain), and so is the persona domain the test lanes seed. What IS per
701
- target is the public surface: `site.url`, the gh-pages CNAME, and the deploy
702
- path prefix, so `targets/admin` publishes to admin.acme.test instead of
703
- over the main site.
704
-
705
- | Contract | Old form | New form | Manual migration step |
706
- |---|---|---|---|
707
- | Sibling sites of one brand | `brand: { subdomains: ["admin", "cdn"] }`, unvalidated, read only by the cloud hosting op | `targets: { web: { type: 'web' }, admin: { type: 'web' }, cdn: { type: 'web' } }` ([config.md](config.md#targets-every-key-is-a-name-886)); each target lives in `targets/<name>` | **Per brand, once, by hand**: delete the `brand.subdomains` list and write the sibling keys (the names are the subdomains verbatim; add `url:` only where the host is NOT `<name>.<brand host>`). Create each `targets/<name>` dir; `npx omega manage` names the missing ones. No converter: the validator refuses the old key and names the replacement |
708
- | Firebase Hosting API domains | `api.{domain}` **plus** `api.{sub}.{domain}` per subdomain entry | `api.{domain}` alone: one backend, one api host, however many targets | **Nothing to write**: the next `npx omega manage` reconciles the default site with the single domain. Any `api.{sub}.{domain}` a previous run created stays in Firebase and Cloudflare until removed BY HAND, because nothing deletes a live domain, so drop the custom domain in the Firebase console and its Cloudflare CNAME once no client calls it |
709
-
710
- ## Plan limits become the features catalog ([#647](https://github.com/Omega-JS-Stack/omega/issues/647))
711
-
712
- A metered feature used to be spelled TWICE on every product — a number in
713
- `limits` and a display row in the `features` array — and its display copy was
714
- unified across products by a BACKFILL rule (a definition on any one product's
715
- copy explained every other). Its PACING was a third thing again: a product-wide
716
- `rateLimit` that no single feature could opt out of.
717
-
718
- So a feature is defined ONCE now, in a top-level `features` catalog (name, icon,
719
- definition, and the `usage` block that meters it), and each product names only
720
- its VALUE. A limit and the row that renders it can no longer disagree, because
721
- there is only one place each fact is written. The backfill retires with the
722
- duplication it existed to paper over: nothing repeats, so nothing needs
723
- unifying.
724
-
725
- The counting side changes with it. `Usage.init()` / `validate()` / `increment()`
726
- / `set()` / `update()` / `setUser()` / `addMirror()` / `setMirrors()` are gone,
727
- replaced by ONE call — `await ctx.usage.consume('<feature>')` — which checks
728
- both counters, refuses with a 429 naming which one hit, else counts and writes
729
- ([packages/backend/docs/usage-rate-limiting.md](../../packages/backend/docs/usage-rate-limiting.md)).
730
- The hCaptcha over-limit fallback `validate()` carried goes with it: it had no
731
- caller (`marketing/contact`, its only user, passed
732
- `useCaptchaResponse: false`), and a captcha is a bot check, not a quota.
733
-
734
- | Contract | Old form | New form | Manual migration step |
735
- |---|---|---|---|
736
- | A metered feature's limit | `payment.products[].limits: { requests: 100 }` | `payment.products[].features: { requests: 100 }`, against a `features.requests` catalog entry carrying a `usage` block | **Per brand, once, by hand**: move each `limits` key into the product's `features` map (same number, same meaning — a MONTHLY limit, `-1` unlimited), and give every id a `features.<id>` catalog entry with at least a `name`. The validator refuses the old key and names the replacement |
737
- | A feature's display copy | `payment.products[].features: [{ id, name, icon, definition, value }]`, repeated per product, definitions unified by a cross-product backfill | `features.<id>: { name, icon, definition }` once, and `payment.products[].features: { <id>: value }` | **Per brand, once, by hand**: lift the name/icon/definition of each entry into the catalog (authored once, in the order you want the rows rendered), and leave only `<id>: value` on each product. `value: true` stays `true`; a feature a tier does NOT include becomes `false` or is simply omitted. The array shape is a validation error naming the map |
738
- | Pacing | `payment.products[].rateLimit: 'daily' \| 'monthly'`, one shape for every metric on the product | `features.<id>.usage.pace: 'daily' \| false`, per feature | **Per brand, once, by hand**: delete `rateLimit`. Day pacing is now the DEFAULT on every counted feature, so a product that carried `rateLimit: 'daily'` needs nothing; one that carried `'monthly'` sets `usage: { pace: false }` on each of its features' catalog entries |
739
- | Mirrored counters | `usage.addMirror('agents/x')` / `setMirrors([...])` at the call site, per request, writing the WHOLE usage object | `features.<id>.usage.mirror: ['agents']` in config, resolved from `user.owns.agents`, writing only the touched feature's counters | **Per brand, once, by hand**: delete the call-site mirror calls, name the document KINDS on the catalog entry, and make sure the code that creates an owned document records its id on the owner's `owns.<kind>` array (a framework field — server-written only) |
740
- | Proxy billing | `await usage.setUser(ownerUid)` swapped the counter's target user mid-request | Removed. A route that must bill another account resolves that account itself and counts against the mirror the catalog declares | **Per brand, by hand**: rework any proxy-billing route around `consume` + a catalog mirror. There is no replacement for silently re-pointing a counter at another user |
741
- | The counting call | `await usage.validate(m); usage.increment(m); await usage.update();` | `await ctx.usage.consume(m)` | **Per brand, by hand**: replace the three-call dance (and the hand-rolled `getUsage() >= LIMIT` gates that grew around it) with one `consume`. It THROWS the 429 — catch it only to reshape the message |
742
- | Anonymous counting | `Usage().init(ctx, { key: ip })` — an explicit key silently switched a signed-in user's storage too | `ctx.usage.forKey(ip)` — a SEPARATE counter | **Per brand, by hand**: replace each keyed `init` with `forKey`. A signed-in caller's own counters can no longer be redirected by passing a key |
743
- | Per-user extra credits | Not expressible — a limit was the plan's number, full stop | `user.usage.overrides.<feature>`, a number that wins over the plan's | **Nothing to migrate**: new capability. Admin-written only (`usage` is a rules-protected framework field), and the reset cron never touches it |
744
- | A page's own `features:` block | A page or layout could name a top-level `features:` frontmatter block of its own content (a marketing "Features" band) and read it as `resolved.features` | `features` is a CONFIG SECTION now, so a page key may not shadow it — rename the block (the framework's own `/download`, `/extension` and the neobrutalism index call theirs `highlights`) | **Per brand, by hand**: rename any top-level `features:` frontmatter block and its `resolved.features.*` reads. A section restated bare in `config:` is a build error naming the key, and a `site.features` read is one too, so an unmigrated page fails loudly rather than rendering an empty band |
745
-
746
- ## The user-connection feature is `connections` ([#788](https://github.com/Omega-JS-Stack/omega/issues/788))
747
-
748
- The lane a brand's users link third-party accounts through was named `oauth2`
749
- in the API, the user record, the config section, the env prefix, the brand
750
- provider folder and the callback URL, while the UI called it "Connections".
751
- Ian's 2026-09-03 ruling: the product concept is a CONNECTION, and a connection
752
- will not always be an OAuth grant — an API key or a bot token is one too — so
753
- the whole feature carries the product word, and each stored record names its
754
- own kind with a `type` field (`'oauth2'` today, the only kind that exists).
755
- OAuth's own version is not the reason; OAuth 2.1 still calls itself OAuth 2.
756
-
757
- Nothing dual-reads the old names. The user DOCUMENTS move in one step of the
758
- brand migration — `npx omega manage --migration=users --execute`, whose `users`
759
- migration writes `connections.<provider>` with its `type` and deletes `oauth2`
760
- in the same write ([manager/migrations.md](../manager/migrations.md)) — and the
761
- config key is a retired-key error naming its replacement. Everything else in
762
- the table is by hand, once per brand. One transient at the deploy itself: a
763
- connect attempt whose provider login was already open fails its callback once
764
- (the state cipher and the session key changed with the name) and succeeds on
765
- retry; its stale `usage/{uid}.oauth2` session clears with the daily clean.
766
-
767
- | Contract | Old form | New form | Manual migration step |
768
- |---|---|---|---|
769
- | User record | `users/{uid}.oauth2.{provider}` | `users/{uid}.connections.{provider}`, each record carrying `type: 'oauth2'` | **The migration moves them**: `npx omega manage --migration=users` audits, `--execute` writes. Idempotent — a document already moved is a no-op, and a document carrying both keeps `connections` and drops the leftover |
770
- | Route | `GET \| POST \| DELETE /omega/user/oauth2` | `/omega/user/connections`; the actions (`authorize`, `status`, `tokenize`, `refresh`) and the delete keep their names, but `authorize` and `tokenize` no longer accept an admin `uid` — a passed one answers 400 naming the argument, since both need the connecting user's own browser. `status`, `refresh` and the delete still take one as an admin (each acts at the provider on that user's behalf), and a trusted read of a stored token is `GET /omega/admin/firestore?path=users/<uid>` with the admin key ([#782](https://github.com/Omega-JS-Stack/omega/issues/782)) | Repoint any caller of your own that names the path. The framework's own callers (the account page, the callback page) move with the framework. A server-side caller that passed a `uid` to `authorize` or `tokenize` moves that call to the user's OWN browser session — no admin key can stand in for it — and reads the resulting token through the admin firestore route |
771
- | Config section | `oauth2: { <provider>: {…} }` | `connections: { <provider>: {…} }` — the per-provider block is byte-identical, with ONE change of meaning ([#793](https://github.com/Omega-JS-Stack/omega/issues/793)): a PACKAGED provider (google, discord, spotify, twitch, kick) is `enabled: false` in the framework defaults, so an entry that relied on "absent means enabled" is now off | Rename the key in `config/omega.json5`, and add `enabled: true` to each packaged provider's block that does not already carry it — a brand's OWN provider is unchanged (present unless `enabled: false`). A config still carrying `oauth2` is a retired-key error naming the move ([config.md](config.md#retired-keys-fail-loudly)) |
772
- | Credentials | `OAUTH2_<PROVIDER>_CLIENT_ID` / `OAUTH2_<PROVIDER>_CLIENT_SECRET` | `CONNECTIONS_<PROVIDER>_CLIENT_ID` / `CONNECTIONS_<PROVIDER>_CLIENT_SECRET` | **Rename the pair by hand** in the brand `.env` (and every `.env.<environment>` overlay), and in the CI secrets any workflow injects them from. A layer still carrying the Google pair FAILS the load naming the new name ([#845](https://github.com/Omega-JS-Stack/omega/issues/845)): they are registered retired env names ([config.md](config.md#retired-env-keys-srcenv-retiredjs)), spelled out one provider at a time. For any other provider there is no check, and the env schema does not know the old name, so the provider reads an empty client id and its authorize leg 500s |
773
- | Provider console redirect URI | `<websiteUrl>/oauth2` | `<websiteUrl>/connections/callback` | **Register the new URI at every provider** whose block the brand declares (Google Cloud console, Discord developer portal, Spotify dashboard, …). Add it BESIDE the old one, deploy, then remove the old one — a redirect URI is matched exactly, so a deploy ahead of the console edit breaks every link attempt |
774
- | Brand provider module | `targets/backend/src/oauth2/<name>.js` | `targets/backend/src/connections/<name>.js` | Move the directory. The lane resolves `${omega.cwd}/connections/` first and the package's own second, exactly as before |
775
- | Website callback page | the `/oauth2` default page (`blueprint/auth/oauth2`) | `/connections/callback` (`blueprint/connections/callback`) | **Nothing for a brand that never overrode it.** A brand carrying its own copy moves it to the new path and layout name; `/connections` itself stays free for a future listing page |
776
- | Provider module shape ([#793](https://github.com/Omega-JS-Stack/omega/issues/793)) | `verifyIdentity(tokenizeResult, Manager, ctx, uid)`, `buildAuthorizeUrl(context)`, `revokeToken(token, {…})`, `verifyConnection(refreshToken, {…})`, `authParams`, `urls.tokenize` + `urls.refresh` + `urls.status`, and a copy of the "already connected" query inside `verifyIdentity` | ONE context object for every step, called as a method on the module: `identity(context)` (required, → `{ id, … }`), `authorize`, `exchange`, `refresh`, `revoke`, `status`; `params` for extra authorize params; ONE `urls.token`; `urls.revoke` optional; `urls.status` deleted | **Per provider module, by hand** (switchboard's YouTube provider is the known one outside this repo): rename the six members, fold `urls.tokenize`/`urls.refresh` into `urls.token`, rename `authParams` → `params`, drop `urls.status`, read `tokenizeResult`/`Manager`/`ctx`/`uid`/`clientId` off the one `context` argument, and DELETE the uniqueness query — the route owns it now and matches on `identity.id`, so the step just answers the identity, with a string `id`. A module missing `identity()` or `urls.token` throws at load naming the file |
777
-
778
- ## One target picker — `--target=` on every brand-root verb ([#780](https://github.com/Omega-JS-Stack/omega/issues/780))
779
-
780
- The brand-root fan-outs picked targets with `--only=<a,b>` / `--except=<a,b>`
781
- while `omega test` picked with `--target=<a,b>`. One picker now, one spelling on
782
- all six verbs (`deploy`, `build`, `clean`, `dev`, `update`, `test`): the tokens
783
- are unchanged (a target key like `web`, or a target dir name like `website`), and
784
- a token matching nothing stops the run: never a run-everything fallback, and
785
- never the matched subset either.
786
-
787
- Beyond parity, `--only` was also firebase's own flag on the backend target:
788
- `firebase deploy --only hosting` picks a SERVICE, so one flag name meant two
789
- things on one verb. That pass-through is untouched and stays a target-level
790
- flag, run from `targets/backend`.
791
-
792
- | Contract | Old form | New form | Manual migration step |
793
- |---|---|---|---|
794
- | The brand-root target picker | `omega deploy --only=web,backend`, and the same flag on `build`, `clean`, `dev`, `update` | `omega deploy --target=web,backend` on every one of them | **Per brand, by hand**: respell `--only=` as `--target=` in npm scripts, CI workflows and shell aliases. Passing `--only` now fails loudly naming `--target=`, so nothing runs the wrong set silently |
795
- | Subtracting from the set | `--except=<a,b>` (the default set minus these) | No subtractive form: name the exact set with `--target=` | **Per brand, by hand**: replace `omega dev --except=backend` with `omega dev --target=web`. A GUI/watcher target (desktop, extension) still has to be named to boot |
796
-
797
- ## The brand SOURCE repo derives as `<brand.id>-omega` ([#809](https://github.com/Omega-JS-Stack/omega/issues/809))
798
-
799
- Brand repo names grew inconsistent (`clockii-omega`, `omega-playground`,
800
- `studymonkey-website`), so Ian ruled one rule on 2026-09-07: every repo a brand owns is
801
- `<brand.id>-<role>`. The releases repo already derived that way (`<brand.id>-releases`);
802
- the SOURCE repo's default was the bare `brand.id` and is now `<brand.id>-omega`. Nothing
803
- else moved: the typed `repo.providers.github.repo` slug still wins, and the owner half is
804
- unchanged.
805
-
806
- The blast radius is every reader of `brandRepo()`: web's `omega deploy --direct` (its
807
- Pages project address included), the manager's github service (org, repo and Pages
808
- reconciliation), the backend's `resolved.github` CMS commits, and the release dispatch.
809
-
810
- | Contract | Old form | New form | Manual migration step |
811
- |---|---|---|---|
812
- | The brand source repo's default name | `brand.id` verbatim (`acme` → `Acme-Org/acme`) | `<brand.id>-omega` (`acme` → `Acme-Org/acme-omega`) | **Per brand, by hand, and only if the brand relied on the default**: either declare the existing name once with `repo.providers.github.repo: "<name>"` (a bare name, or an `owner/name` slug when the repo sits under another org), or rename the GitHub repo to `<brand.id>-omega` and let the default derive it. A brand that already types the slug is untouched |
813
-
814
- ## Public identifiers leave `.env` for config ([#893](https://github.com/Omega-JS-Stack/omega/issues/893))
815
-
816
- Six values were declared in both homes at once. Ian, 2026-09-12: "it seems our custom is
817
- to put IDs in config. should we do that? seems like env is standard to be secrets only."
818
- The rule that settles it is in [config.md](config.md) ("Config or env?"): config holds what
819
- is PUBLIC by design (visible in a store URL, shipped to browsers, printed in a binary),
820
- `.env` holds secrets, the login coordinates that only travel with a secret, and machine
821
- paths. Nothing about the SECRET halves changed: `RECAPTCHA_SECRET_KEY`,
822
- `PAYPAL_CLIENT_SECRET`, `CHARGEBEE_API_KEY` and every store API credential stay in `.env`.
823
-
824
- There is no dual-read, so a `.env` layer that still declares one of the six FAILS the load
825
- naming the move (`@omega.js/config`'s `env-retired.js`, checked in the ONE place a `.env`
826
- layer is parsed). The three listing ids also left the extension's CI secrets block: a
827
- public id rides the config snapshot a deploy pushes, like every other config value.
828
-
829
- | Contract | Old form | New form | Manual migration step |
830
- |---|---|---|---|
831
- | The Chrome Web Store item id | `CHROME_EXTENSION_ID` in `.env` (+ a repo secret) | `targets.<name>.listings.chrome.id` | Move the value into config beside that listing's `url`, delete the `.env` line, and delete the repo secret |
832
- | The AMO add-on id | `FIREFOX_EXTENSION_ID` in `.env` (+ a repo secret) | `targets.<name>.listings.firefox.id`, which IS the manifest's `browser_specific_settings.gecko.id` | Move the value into config, delete the `.env` line and the repo secret. A manifest declaring a different gecko id now fails the package: delete that key, or make the two match. A brand that never published gets the derived id pinned into its config by the extension's local scaffold, on the first verb run in the target |
833
- | The Edge Partner Center product id | `EDGE_PRODUCT_ID` in `.env` (+ a repo secret) | `targets.<name>.listings.edge.id` | Move the value into config, delete the `.env` line and the repo secret |
834
- | The reCAPTCHA site key | `RECAPTCHA_SITE_KEY` in `.env` (+ a web repo secret) | `captcha.providers.recaptcha.siteKey` (already declared) | Delete the `.env` line; most brands already carry the value in config. The manage walk's captcha service asks for it there now |
835
- | The PayPal client id | `PAYPAL_CLIENT_ID` in `.env`, bridged into `process.env` at backend boot | `payment.providers.paypal.clientId` (already declared), read directly by the provider library | Delete the `.env` line; the config key is where it was already |
836
- | The Chargebee site name | `CHARGEBEE_SITE` in `.env`, bridged into `process.env` at backend boot | `payment.providers.chargebee.site` (already declared), read directly by the provider library | Delete the `.env` line; the config key is where it was already |
837
-
838
- ## The four schema-less web sections retire ([#850](https://github.com/Omega-JS-Stack/omega/issues/850))
839
-
840
- `favicon`, `manifest`, `icons` and `currency` were legacy UJM presentation blocks the
841
- config converter wrote under `targets.web`, with no schema rule anywhere: @omega.js/web
842
- kept a private list so its own `config:` guard would pass them. Ian, 2026-09-09:
843
- everything the build processes needs a schema definition, favicons stay auto-generated
844
- with no consumer config, and any new web-only shape lives under `targets.web`, never at
845
- the root. So the list is gone, `CONFIG_SECTIONS` is exactly the schema's list, and each of
846
- the four is a registered retired path ([config.md](config.md#retired-keys-fail-loudly)):
847
- a brand still carrying one fails validation naming what answers it now. `omega migrate`
848
- drops the three and moves the currency, with a note each.
849
-
850
- | Contract | Old form | New form | Manual migration step |
851
- |---|---|---|---|
852
- | The favicon folder | `targets.web.favicon.path` pointed the head's icon links at a folder of your own | The MINTED set only: the manager's assets service mints it from the brand images and the build bridges it to `/assets/images/favicon` (plus a root `favicon.ico`) | Delete the key. Put your source image at `brand.images.favicon` if the mint should start from a different one; there is no path to override |
853
- | The browser chrome color | `targets.web.favicon.theme-color` fed the `theme-color` meta | **`brand.color`**, the one place a brand states its hex (the accent ramps derive from it too, [theming.md](theming.md)) | Delete the key. Set `brand.color` if it is not set already; a brand with no color emits no `theme-color` meta at all |
854
- | The web app manifest block | `targets.web.manifest` | Nothing: no reader exists, and the manifest that ships is the minted set's own `site.webmanifest` | Delete the key. Every value under it was already being ignored |
855
- | The footer link icons | `targets.web.icons`, a name-to-markup map the footer looked each `link.icon` up in | The `icon` on the link itself, as Font Awesome classes: `icon: 'fa-brands fa-github'` ([#619](https://github.com/Omega-JS-Stack/omega/issues/619), [icons.md](icons.md)) | Delete the key and spell each link's icon as its own `fa-*` classes in your `src/_includes/frontend/sections/footer.json`. The legacy block only ever held a `style`, which the lookup could never resolve, so those icons were rendering empty already |
856
- | The price currency | `targets.web.currency` (or a root `currency`), read by the pricing JSON-LD | **`payment.currency`**, beside the providers that charge in it | Move the value. `omega migrate` does it for you; by hand it is one key |
857
-
858
- ## One platform vocabulary, and one shipping declaration ([#867](https://github.com/Omega-JS-Stack/omega/issues/867))
859
-
860
- OMEGA said `mac` in a login record, `macos` in an icon dir, `win` in a config key and
861
- `chromium` in a build dir, all for the same three or four things. Ian, 2026-09-10: ONE
862
- vocabulary, the client's (`getPlatform()` / `getBrowser()`), everywhere OMEGA speaks for
863
- itself: platforms `mac`, `windows`, `linux`, `chrome`, `firefox`, `edge`; formats `dmg`,
864
- `nsis`, `deb`, `appimage`, `snap`, `zip`, `store`. Node's `darwin`/`win32`,
865
- electron-builder's `mac`/`win`/`AppImage` and GitHub's runner labels are foreign
866
- vocabularies, each translated at one point; `APPLE_*` and `SNAPCRAFT_*` env names stay,
867
- because they name vendors.
868
-
869
- With it, what a target SHIPS became one declaration in one shape on both targets:
870
- `targets.<name>.platforms.<platform>.formats.<format>`, presence = enabled, every platform
871
- and format on by default, `false` to drop one, per-format settings inside the format.
872
-
873
- **The config half is a migration, not hand-editing**:
874
- `npx omega manage --migration=platform-names --execute` at the brand root rewrites every
875
- omega.json5 the brand owns and renames its icon dirs. Run it BEFORE `omega migrate`, which
876
- deletes a retired key rather than moving it.
877
-
878
- | Contract | Old form | New form | Manual migration step |
879
- |---|---|---|---|
880
- | The Windows platform block | `targets.desktop.platforms.win` | `targets.desktop.platforms.windows` (installer knobs and `signing` unchanged inside it) | Run the migration; it moves the block with every setting |
881
- | What the desktop ships | implicit (deb + AppImage always, snap behind `platforms.linux.snap.enabled`) | `platforms.<platform>.formats.<format>` | Run the migration: `linux.snap.*` moves into `linux.formats.snap.*` and its `enabled` flag becomes presence (`enabled: false` becomes `formats.snap: false`) |
882
- | What the extension ships | implicit (every browser built, a store published when its API credentials happened to be set) | `platforms.<chrome|firefox|edge>.formats.<zip|store>` | Nothing to move: declare the block only to DROP something (`edge: false`) |
883
- | The mac icon dir | `config/icons/macos/` | `config/icons/mac/` | Run the migration; it renames the dir at the brand root and in every target |
884
- | The chromium build dir | `packaged/chromium/raw/` + `packaged/chromium/extension.zip` | `packaged/chrome/raw/` + `packaged/chrome/extension.zip` | Nothing to move (the dir is generated): re-point a "Load unpacked" bookmark and any local script |
885
- | The opera extension build | `packaged/opera/` (a third build, published nowhere: the Opera Add-ons have no API) | gone | Nothing. Opera installs the chrome build like every other Chromium browser |
886
- | The site's download links | `/download/mac/universal`, `/download/windows/universal`, `/download/linux/debian` | `/download/mac/dmg`, `/download/windows/nsis`, `/download/linux/deb` | Nothing: the three old segments keep their pages as redirects to the same file. `/download/linux/snap` now goes to the brand's Snap Store listing |
887
- | The desktop asset names | `<Product>-mac-universal.dmg`, `<Product>-windows-universal.exe`, `<Product>-linux-debian.deb` | `<Product>-mac-dmg.dmg`, `<Product>-windows-nsis.exe`, `<Product>-linux-deb.deb` | Nothing in the brand. The next release publishes the new names, and the site links them from the same table; a link typed by hand somewhere outside OMEGA needs retyping |
888
- | The `platforms` workflow input | accepted `darwin`, `win`, `ubuntu` as spellings | `mac`, `windows`, `linux` (or `all`) | Retype any saved dispatch that used an alias. `mac,darwin` used to turn the Windows leg on, because `darwin` contains `win` |
889
- | Each env key's mint page | the manager's REQUIRES registry, per service | the env schema's `label` / `url` / `hint` | Nothing in a brand: the walk asks from the schema now, which is why the signing and store credentials can be asked for at all |
890
-
891
- ## One dispatch trigger on every workflow ([#923](https://github.com/Omega-JS-Stack/omega/issues/923))
892
-
893
- All four templates accepted a second trigger, `repository_dispatch: [omega-deploy]`,
894
- and the HTTP surface advertised it as a way in. It never worked the way it read:
895
- an event TYPE is not a ref, so such a run checks out the DEFAULT branch, which
896
- since [#915](https://github.com/Omega-JS-Stack/omega/issues/915) never carries
897
- the deploy tree. Nothing in OMEGA ever sent one, so the trigger is gone rather
898
- than pinned.
899
-
900
- | Contract | Old form | New form | Manual migration step |
901
- |---|---|---|---|
902
- | The workflow dispatch channel | `POST /repos/<o>/<r>/dispatches` with `{"event_type":"omega-deploy"}` | `POST /repos/<o>/<r>/actions/workflows/<file>/dispatches` with `{"ref":"omega-deploy"}` | Retype any caller outside OMEGA that sent the event type. A brand's own verbs already dispatched this way, so nothing in the brand changes; its next omega verb rewrites the composed workflow |
903
-
904
- ## Deliberate compatibility that REMAINS
905
-
906
- Old forms the new system still speaks ON PURPOSE, because a party outside this
907
- ecosystem still sends them. They are NOT accommodations to clean up: each one
908
- retires when its named condition is met, and never unilaterally.
909
-
910
- | Compatibility | Who still speaks it | Retirement condition |
911
- |---|---|---|
912
- | `backendManagerKey` sent in outbound request bodies (`process.env.OMEGA_ADMIN_KEY` under the OLD field name) | Legacy-BEM parent deployments, the Ghostii API, and ITW's `wrapper` Cloud Function — all still reading that field | Each upstream migrates to the new stack and accepts the `omega-admin-key` header; fix per upstream, never unilaterally |
913
- | The Ghostii flat-article response fallback | api.ghostii.ai, whose production backend runs legacy BEM and can return the flat field shape | Ghostii returns only the structured shape |
914
- | The `gatherings/online` sign-out leg | Old somiibo / electron-manager desktop clients that still write that RTDB path | Those app versions are out of circulation |
915
- | `legacyProductIds` / `legacyPlanIds` matching in the PayPal and Chargebee providers | Currently-billing subscribers on plan/product IDs created before the current catalog | The last subscription on a legacy ID ends or is migrated |
916
- | The legacy desktop deep-link param translation in web core auth (`?destination=&source=app&signout=&cb=` → `authReturnUrl` / `authSignout`, chained through `/token`) | Shipped legacy desktop apps whose auth links are baked into installed binaries | Those app versions are out of circulation |
917
- | Fixed legacy download filenames in the desktop mirror-downloads task (`Somiibo.dmg`, `Somiibo-Setup.exe`, `somiibo_amd64.deb`) | Every published download link and site pointing at the stable, version-less filename | No published link depends on the stable filename |