@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
package/docs/analytics.md DELETED
@@ -1,140 +0,0 @@
1
- # Analytics
2
-
3
- GA4 Measurement Protocol with cross-platform identity. The same human gets unified events across desktop (@omega.js/desktop), web (UJM/@omega.js/client), and backend (@omega.js/backend) — provided all four reference the same Firebase project ID.
4
-
5
- ## How identity works
6
-
7
- Every event ships with two GA4 fields:
8
-
9
- - **`client_id`** — uniquely identifies a *desktop install*. Stable per-install, anonymous.
10
- - **`user_id`** — uniquely identifies a *human*. Set when the user is signed in via Firebase Auth. It rides **alongside** the `client_id`, never in place of it: GA stitches sessions by the client id.
11
-
12
- @omega.js/desktop derives both via `uuidv5(input, namespace)` where:
13
-
14
- - `namespace = uuidv5(cloud.config.projectId, uuidv5.URL)` — same projectId in @omega.js/backend/UJM/@omega.js/client → same namespace everywhere.
15
- - `client_id = uuidv5(deviceId, namespace)` — the `deviceId` comes from the ONE shared derivation, `@omega.js/analytics`' `deriveDeviceId` ([#396](https://github.com/Omega-JS-Stack/omega/issues/396)), with desktop injecting its storage and its MAC seed ([context.md](context.md)).
16
- - `user_id = uuidv5(firebaseUid, namespace)` — set automatically when `omega.onAuthChange` fires with a uid; cleared on logout.
17
-
18
- Why this matters: the same Firebase user signing into the desktop app, the web app, and triggering backend events produces **identical `user_id` values** in every Measurement Protocol call. GA4 stitches the events into one user journey across all surfaces.
19
-
20
- What does NOT cross surfaces is the machine. The desktop app's deviceId lives in electron-store and a browser's lives in that browser's localStorage, so one machine is two `client_id`s — the human is the link, and always was.
21
-
22
- ## Config
23
-
24
- ```jsonc
25
- analytics: {
26
- enabled: true, // default true
27
- providers: {
28
- google: {
29
- id: 'G-XXXXXXXXXX', // Measurement ID — REQUIRED
30
- },
31
- },
32
- }
33
- ```
34
-
35
- The API secret is read from `process.env.GOOGLE_ANALYTICS_SECRET` — never committed. Mirrors @omega.js/backend's convention.
36
-
37
- ### Local dev
38
-
39
- Add to `.env`:
40
-
41
- ```bash
42
- GOOGLE_ANALYTICS_SECRET=your_secret_here
43
- ```
44
-
45
- Mint the secret in GA4 Admin → Data Streams → your stream → **Measurement Protocol API secrets**.
46
-
47
- ### Production builds
48
-
49
- The `bundle` task's esbuild `define` bakes `process.env.GOOGLE_ANALYTICS_SECRET` into the bundled main process at build time, so packaged apps don't need `.env` at runtime. The build runs with the secret set (CI does this via the GitHub Actions secret `omega deploy`'s precheck pushes).
50
-
51
- ## API
52
-
53
- ```js
54
- omega.analytics.event('button_click', { button_id: 'cta' });
55
- omega.analytics.pageview('/settings');
56
- omega.analytics.screenview('SettingsScreen');
57
- omega.analytics.setUserProperties({ plan: 'premium', trial: false });
58
- omega.analytics.setUserId('firebase-uid-abc'); // usually wired automatically
59
- ```
60
-
61
- Same surface in renderer:
62
-
63
- ```js
64
- window.desktop.analytics.event('button_click', { button_id: 'cta' });
65
- window.desktop.analytics.pageview('/settings');
66
- window.desktop.analytics.setUserProperties({ plan: 'premium' });
67
- const status = await window.desktop.analytics.getStatus(); // { enabled, measurementId, clientId, userId, queueLength }
68
- ```
69
-
70
- The renderer surface is fire-and-forget IPC (`ipcRenderer.send`) for events; only `getStatus` round-trips via `invoke`.
71
-
72
- ## The renderer NEVER sends — it forwards ([#411](https://github.com/Omega-JS-Stack/omega/issues/411))
73
-
74
- A renderer's own `omega.analytics.event(...)` (the embedded @omega.js/client, the surface a vert click or a permission prompt fires through) routes to the bridge above and is delivered by the MAIN process's sender. There is exactly one sender per install:
75
-
76
- ```
77
- renderer: omega.analytics.event('vert_click', { … })
78
- → @omega.js/client reads config.analyticsBridge (src/renderer.js injects the
79
- preload's window.desktop.analytics when it boots the client)
80
- → ipcRenderer.send('desktop:analytics:event', { name, params })
81
- → main: analytics.event(name, params) → catalog → Measurement Protocol
82
- ```
83
-
84
- The bridge is **injected, never sniffed**: the client reads that one config key and no global, so a page that merely carries a `window.desktop` can never route a brand's analytics into a void.
85
-
86
- Main owns identity end to end: the device id from electron-store, the session id minted once per launch, the real engagement time, and the app's `page_location` / `page_title` (a renderer's `file://` href is not GA's business). Only the canonical name and the caller's params cross.
87
-
88
- What does NOT cross the bridge:
89
-
90
- - **The fire's `options`** (`{ eventId, providers }`) — they exist to deduplicate a browser pixel against its server-side half, and GA4 through main is the one lane a desktop window has.
91
- - **The page data** the web path merges in (`page_path` / `page_title` / `page_location`) — main supplies the app's own.
92
-
93
- What the bridged client does NOT do: mint a `client_id` of its own (no `localStorage._omega_device_id` in a desktop renderer), hold an api_secret, or fire `login` / `logout` — main's auth bridge already fires those off the same Firebase user, and a second pair would double-count every sign-in.
94
-
95
- An uncatalogued event name never leaves the renderer: the catalog check runs before the forward, so a typo throws at the call site in development (and is logged-and-skipped in a packaged app) instead of surfacing as an unattributable warning in main's `runtime.log`. A forward that IPC cannot carry (a non-cloneable param) is warned about, never thrown at the user mid-action.
96
-
97
- > 🚫 **Never inject `GOOGLE_ANALYTICS_SECRET` into a renderer's config.** Desktop's config carries the measurement id alone. A renderer with its own secret would become a second Measurement Protocol sender with its own device id and its own session id — one install counted as two GA clients ([#396](https://github.com/Omega-JS-Stack/omega/issues/396) class). The client backs the rule with a guard: a bridged renderer drops any secret handed to it.
98
-
99
- ## Auto-fired events
100
-
101
- | Event | When | Notes |
102
- |---|---|---|
103
- | `app_launch` | At end of `analytics.initialize()` (main process) | Fires once per launch |
104
- | `login` | On `omega.onAuthChange({uid: ...})` transition from null → uid | `params.method = providerId` |
105
- | `logout` | On `omega.onAuthChange({uid: null})` after a previous uid | — |
106
-
107
- ## Queueing
108
-
109
- Calls before init complete are queued (up to 200 events). On init, the queue is drained. After init, calls send immediately.
110
-
111
- ## Disabled paths
112
-
113
- `analytics._enabled = false` whenever:
114
-
115
- - `config.analytics.enabled === false`
116
- - No measurement ID configured
117
- - No `GOOGLE_ANALYTICS_SECRET` env var
118
-
119
- In all three cases, `event()` is a silent no-op (no throws, no warns past init).
120
-
121
- ## Event names come from the catalog
122
-
123
- `event(name, params)` takes a CANONICAL name from `@omega.js/analytics`' shared
124
- catalog — the same vocabulary the web pages, the extension and the Cloud
125
- Functions speak ([docs/shared/analytics.md](../../../docs/shared/analytics.md)).
126
- The catalog's GA4 mapping decides the native name and payload; this module keeps
127
- only what is desktop's: the Measurement Protocol transport, the pre-init queue,
128
- the session/engagement enrichment, and the IPC bridge (unchanged — the preload
129
- API is byte-compatible).
130
-
131
- A name no catalog entry declares is a programmer error: it throws in development
132
- and is logged-and-skipped in a packaged app, where a throw would take the user's
133
- action with it. Adding an event is one catalog entry plus its test, never a
134
- free-typed string here.
135
-
136
- ## Tests
137
-
138
- - `src/test/suites/main/analytics.test.js` — disabled paths, uuidv5 stability, the catalog contract (canonical name in, GA4 descriptor out; unknown names never post), queueing, auth-bridge wiring, IPC handlers, secret-not-leaked guard.
139
- - `src/test/suites/renderer/analytics-bridge.test.js`: renderer-side surface shape, `getStatus` round-trip, and the #411 pin: a renderer-originated `omega.analytics.event(...)` reaches main's sender exactly once, and the payload GA would receive carries main's `client_id` and main's session id, while the bridged client holds no secret and no device id of its own (the harness hands it credentials on purpose). The harness taps main's transport (`harness/main-entry.js`) so a renderer suite can read back what the sender was handed: a test run never reaches a real GA property. Harness fidelity, stated plainly: it drives the real client and the real IPC channel into the real main-process sender, but the client runs in the preload world holding the bridge object directly, not the contextBridge proxy a page bundle gets, the proxy hop is the one link this pin does not exercise.
140
- - `packages/client/test/analytics.test.js` — the client half: the bridge is the injected config value alone (a planted `window.desktop` bridges nothing), a bridged client drops credentials and mints no device id, an uncatalogued name never leaves the renderer, and a forward that throws never reaches the caller.
package/docs/app-state.md DELETED
@@ -1,92 +0,0 @@
1
- # App State
2
-
3
- Storage-backed launch flags + crash sentinel. Tells you *whether* this is the first launch ever, *how many* times the app has launched, *whether* the previous run crashed, and *whether* the version changed.
4
-
5
- ## Public API on `omega.appState`
6
-
7
- ```js
8
- omega.appState.isFirstLaunch() // boolean: true ONLY on the very first boot
9
- omega.appState.getLaunchCount() // number: total successful launches (including this one)
10
- omega.appState.getInstalledAt() // Date: first ever launch timestamp
11
- omega.appState.getLastLaunchAt() // Date | null: previous launch (null on first)
12
- omega.appState.getLastQuitAt() // Date | null: previous graceful quit; null if it crashed
13
- omega.appState.recoveredFromCrash() // boolean: previous run did not exit cleanly
14
-
15
- omega.appState.getVersion() // string | null: package version of this launch
16
- omega.appState.getPreviousVersion() // string | null: version before this launch
17
- omega.appState.wasUpgraded() // boolean: true if THIS launch's version differs from the prior
18
-
19
- omega.appState.launchedAtLogin() // boolean: OS booted us via openAtLogin
20
- omega.appState.launchedFromDeepLink() // boolean: argv had a deep-link payload (set by lib/deep-link)
21
-
22
- omega.appState.reset() // wipe persisted state (test helper / factory-reset command)
23
- ```
24
-
25
- ## Storage shape (key `appState`)
26
-
27
- ```js
28
- {
29
- installedAt: 1700000000000, // first ever boot timestamp (ms epoch)
30
- launchCount: 42,
31
- lastLaunchAt: 1700000123456, // THIS launch's timestamp; previous launch's value exposed via getLastLaunchAt()
32
- lastQuitAt: null, // null while running; set on graceful quit
33
- version: '1.2.3',
34
- previousVersion: '1.2.2', // preserved across no-change launches (see "Upgrade detection")
35
- sentinel: true // true while running, cleared on graceful quit
36
- }
37
- ```
38
-
39
- ## Crash detection
40
-
41
- - On every boot, `appState.initialize()` reads the previous `sentinel` and `lastQuitAt` values BEFORE overwriting them.
42
- - If `sentinel === true` AND `lastQuitAt === null`, the previous run never made it to `before-quit` / `will-quit` → it crashed. `recoveredFromCrash()` returns `true` for this launch only.
43
- - First launch is exempt (no prior state to compare).
44
- - Graceful-quit cleanup is wired up via `app.on('before-quit')` and `app.on('will-quit')`. Both fire under different shutdown paths; either one clears the sentinel and writes `lastQuitAt`.
45
-
46
- ## Upgrade detection
47
-
48
- `wasUpgraded()` is true **only** if THIS launch's version differs from the prior launch's version. Subsequent launches at the same version return `false`, but `getPreviousVersion()` keeps returning the historical value so you can show a "what's new" UI on the second launch too.
49
-
50
- ```js
51
- // install 1.0.0
52
- appState.wasUpgraded() // false (first launch)
53
- appState.getPreviousVersion() // null
54
-
55
- // upgrade to 1.0.1, launch again
56
- appState.wasUpgraded() // true
57
- appState.getPreviousVersion() // '1.0.0'
58
-
59
- // launch again at 1.0.1, no change
60
- appState.wasUpgraded() // false
61
- appState.getPreviousVersion() // '1.0.0' (preserved — useful for "what's new" UIs)
62
- ```
63
-
64
- ## Common patterns
65
-
66
- ### Show onboarding on first launch
67
-
68
- ```js
69
- if (omega.appState.isFirstLaunch()) {
70
- omega.windows.show('onboarding');
71
- }
72
- ```
73
-
74
- ### Crash report ping
75
-
76
- ```js
77
- if (omega.appState.recoveredFromCrash()) {
78
- omega.sentry.captureMessage('recovered from crash', 'warning');
79
- }
80
- ```
81
-
82
- ### What's new modal after upgrade
83
-
84
- ```js
85
- if (omega.appState.wasUpgraded()) {
86
- omega.windows.show('changelog');
87
- }
88
- ```
89
-
90
- ## Test helper
91
-
92
- `appState.reset()` wipes the persisted state and resets the in-memory snapshot. Useful in test suites and as a real `Settings → Reset to factory defaults` command.
package/docs/audit.md DELETED
@@ -1,69 +0,0 @@
1
- # Audit Workflow
2
-
3
- Full-project audit for @omega.js/desktop — runs against a CONSUMER project or the FRAMEWORK repo itself (scope auto-detected). Invoked via the `omega:desktop` skill (`/omega:desktop audit`) or any "audit this app/project" request.
4
-
5
- Every check has a stable ID, a severity, and a scope. Findings are reported as `ID @ file:line`, fixed one at a time, then re-verified. The tables below do NOT restate the rules — each check links to the doc that owns the rule and the fix.
6
-
7
- ## Protocol
8
-
9
- 1. **Detect scope** — read `package.json`: `name` is `@omega.js/desktop` → **framework audit** (U + @omega.js/desktop + F checks); `@omega.js/desktop` in (dev)dependencies → **consumer audit** (U + @omega.js/desktop checks).
10
- 2. **Run the catalog** — every check matching the scope. Search with Grep/Glob/Read over `src/` (+ `test/`, `config/`, `hooks/`); ALWAYS exclude `dist/`, `release/`, `node_modules/`, `_legacy/`, `_backup/`, `.cache/`. Record each finding as `ID @ file:line` + a one-line description.
11
- 3. **Persist the report** — write the findings list to `.temp/audit/claude-audit.md` (create the dir; add `.temp/` to `.gitignore` if missing) so a long fix loop survives session breaks. Summarize counts by severity in chat.
12
- 4. **Fix loop** — TodoWrite per finding, highest severity first, ONE at a time: mark in-progress → root cause → fix → verify → complete. Ask before structural or destructive fixes (file deletions, lib restructures, config reshapes).
13
- 5. **Re-verify** — re-run every check that produced findings until clean; finish with `npx omega test` (must be green).
14
- 6. **Doc parity** — if fixes changed behavior, update README / the framework guide / `docs/<topic>.md` / CHANGELOG in the same change set.
15
-
16
- Severity: **CRIT** security or broken functionality · **HIGH** hard-rule violation · **MED** convention drift · **LOW** optional improvement.
17
- Scope: **C** consumer · **F** framework repo · **B** both.
18
-
19
- ## Universal checks (U-xx)
20
-
21
- Mirrored across all four OMEGA frameworks (UJM / @omega.js/backend / BXM / @omega.js/desktop) — same ID means the same check everywhere.
22
-
23
- | ID | Sev | Scope | Check |
24
- |----|-----|-------|-------|
25
- | U-01 | HIGH | B | Every feature has tests at EVERY layer it surfaces (build / main / renderer / boot) — never mocked, real harness only ([test-framework.md](test-framework.md)) |
26
- | U-02 | HIGH | B | Test hygiene — real-external-API tests gated behind `TEST_EXTENDED_MODE` in-source (not mocked); no tests that assert nothing ([test-framework.md](test-framework.md)) |
27
- | U-03 | CRIT | B | XSS: renderer DOM sinks escape untrusted values inline via `omega.utilities.escapeHTML(value)` (+ `sanitizeURL` for URL sinks); zero local escape helpers (rules mirror @omega.js/web and @omega.js/extension `docs/xss-prevention.md`; see also DSK-01 for navigation sinks) |
28
- | U-04 | HIGH | B | @omega.js/client owns Firebase: never `require('firebase')`; renderers use `omega.auth` / `omega.firestore`, main uses `omega.auth` ([common-mistakes.md](common-mistakes.md), [auth.md](auth.md)) |
29
- | U-05 | HIGH | C | No @omega.js/desktop transitive deps installed in the consumer `package.json` (`firebase`, `@omega.js/client`, `fs-jetpack`, …) — the bundle task's framework-deps hook resolves them ([common-mistakes.md](common-mistakes.md)) |
30
- | U-06 | HIGH | B | Env behavior gated on the INTENTIONAL check: `isProduction()` or `isDevelopment() \|\| isTesting()`, never `!isDevelopment()`; no ad-hoc `process.env` reads where a helper exists ([environment-detection.md](environment-detection.md)) |
31
- | U-07 | HIGH | B | Config canon — `config/omega.json5` validates against the schema (boot validator green); canonical cross-framework blocks (`brand`, `app`, `cloud.{provider,config}`, `monitoring`, `analytics`, `payment`) not reinvented ([config-schema.md](config-schema.md)) |
32
- | U-08 | CRIT | B | No private credentials committed — signing certs (`config/certs/` gitignored), `.env` secrets, tokens, API secret keys ([signing.md](signing.md)). (The Firebase WEB `apiKey` is public by design — do NOT flag it.) |
33
- | U-09 | HIGH | B | Source discipline — nothing edited in `dist/` or generated files (`dist/electron-builder.yml`, entitlements plist); no live code referencing `_legacy/` / `_backup/` ([build-system.md](build-system.md), [common-mistakes.md](common-mistakes.md)) |
34
- | U-10 | MED | B | Doc parity — README / the framework guide / `docs/` / CHANGELOG match shipped behavior; the docs index lists every `docs/*.md`; no stale names for renamed commands/patterns |
35
- | U-11 | MED | B | SSOT/DRY — no duplicated constants/config/logic; one authoritative home per value, imported everywhere else |
36
- | U-12 | MED | B | JS conventions — file structure, JSDoc, short-circuit returns, leading logical operators, `fs-jetpack`, one `module.exports` per file (global `js:patterns` skill + [the framework guide](../../../docs/desktop/index.md) §File Conventions) |
37
- | U-13 | MED | B | Dead code & stale patterns — no orphaned `src/` files nothing imports; no unused views/components/integrations; inventory TODO/FIXME (report only) |
38
- | U-14 | LOW | B | Dependency health — review `npm outdated` / `npm audit`; apply fixes via the `general:update-packages` workflow (includes supply-chain checks) |
39
-
40
- ## desktop-specific checks
41
-
42
- | ID | Sev | Scope | Check |
43
- |----|-----|-------|-------|
44
- | DSK-01 | CRIT | B | Zero-trust URLs — every DYNAMIC URL is gated through `sanitize-url.js` before `shell.openExternal` / `BrowserWindow.loadURL` / `window.location.href =` (hardcoded internal-scheme URLs bypass) ([the framework guide](../../../docs/desktop/index.md) §File Conventions, [common-mistakes.md](common-mistakes.md)) |
45
- | DSK-02 | HIGH | B | Path resolution — `app.getAppPath()` / `utils/app-root.js`, never `process.cwd()`, in runtime code (it's `/` in packaged apps) ([common-mistakes.md](common-mistakes.md)) |
46
- | DSK-03 | HIGH | C | Windows — every `windows.create()` is `await`ed; the `main` window is ALWAYS created (even hidden launches, with `show: false`) so activate/second-instance can surface UI ([windows.md](windows.md), [common-mistakes.md](common-mistakes.md)) |
47
- | DSK-04 | HIGH | B | Zero-trust IPC: all channels go through `omega.ipc` (never raw `ipcMain`); handlers validate payload content before acting, especially in apps embedding remote web content ([ipc.md](ipc.md#zero-trust-payloads)) |
48
- | DSK-05 | MED | C | Icons — one native-size PNG per slot (no `@2x` siblings), macOS tray source named `tray.png` (@omega.js/desktop owns the `Template` rename), no `app.icons` config block ([icons.md](icons.md)) |
49
- | DSK-06 | HIGH | C | File-based integrations — tray/menu/context-menu logic lives in `src/integrations/<name>/index.js`, never expressed in config JSON ([tray.md](tray.md), [menu.md](menu.md), [context-menu.md](context-menu.md)) |
50
- | DSK-07 | HIGH | B | Presence-driven feature flags — credentials enable features (`monitoring.providers.sentry.dsn`, `analytics.providers.google.id`, `cloud.config`); no invented `enabled:` toggles ([config-schema.md](config-schema.md)) |
51
- | DSK-08 | MED | B | Accessibility basics in renderer views — meaningful `alt` text, labeled form fields, real `<button>`/`<a>` elements (no clickable `div`s) |
52
-
53
- ## Framework-repo checks (F-xx)
54
-
55
- Only when auditing the @omega.js/desktop repo itself. Mirrored across the four frameworks.
56
-
57
- | ID | Sev | Check |
58
- |----|-----|-------|
59
- | F-01 | MED | Sister parity — mirrored sections (config shapes, test contract, guide skeleton, shared env/test conventions) in sync with UJM / @omega.js/backend / BXM; deviations are deliberate and documented |
60
- | F-02 | HIGH | Consumer-shipped defaults in sync — what `ensureTarget()` scaffolds (`src/defaults/`, every verb) matches current conventions and docs |
61
- | F-03 | MED | Docs completeness — every `docs/*.md` indexed in the framework guide; every lib module has a doc; no "(planned)" links for things that have shipped |
62
- | F-04 | HIGH | `npx omega test mgr:` green before treating the audit as complete |
63
-
64
- ## See also
65
-
66
- - [common-mistakes.md](common-mistakes.md) — the canonical anti-pattern list behind several checks
67
- - [ipc.md](ipc.md#zero-trust-payloads) — the payload rules behind DSK-04
68
- - [config-schema.md](config-schema.md) — the validator behind U-07 / DSK-07
69
- - [test-framework.md](test-framework.md) — the layers behind U-01 / U-02
package/docs/auth.md DELETED
@@ -1,284 +0,0 @@
1
- # Auth: `omega.auth` and the state sync
2
-
3
- @omega.js/desktop keeps Firebase auth state in sync across all processes (main + every renderer window). **Main is the source of truth**, renderers reflect: the same pattern as @omega.js/extension's background and page contexts. In main the lib is `omega.auth` ([src/lib/auth.js](../src/lib/auth.js)); in a renderer `omega.auth` is @omega.js/client's Auth module. Both sides hold the account as one `User` (`@omega.js/account`), never null.
4
-
5
- ## Why this exists
6
-
7
- In Electron, you can't just initialize Firebase in the renderer and forget about it:
8
-
9
- - Multiple renderer windows would each have their own Firebase instance with no coordination.
10
- - Main-process code (tray, menu, deep-link routes) needs to know who's signed in.
11
- - A deep-link auth-token (`myapp://auth/token?token=...`) arrives in main, but the user's UI lives in the renderer — somebody has to bridge them.
12
-
13
- The bridge handles all three.
14
-
15
- ## How it works
16
-
17
- ```
18
- ┌─────────────────────────────────────────────────────────────┐
19
- │ MAIN (lib/auth.js, omega.auth) │
20
- │ - Owns Firebase Auth instance ("omega-auth" app) │
21
- │ - Source of truth for auth state │
22
- │ - Listens for desktop:auth:* IPC from renderers │
23
- │ - Broadcasts desktop:auth:* IPC to all renderers on changes │
24
- └─────────────────────────────────────────────────────────────┘
25
- ▲ │ broadcasts
26
- │ sync-request ▼
27
- ┌──────────────────────────┐ ┌──────────────────────────┐
28
- │ RENDERER (window 1) │ │ RENDERER (window 2) │
29
- │ @omega.js/client + Firebase │ │ @omega.js/client + Firebase │
30
- └──────────────────────────┘ └──────────────────────────┘
31
- ```
32
-
33
- ### Auth flow: deep-link → all processes signed in
34
-
35
- **Consumers never collect credentials.** There is no login form to build: call `omega.openAuthFlow()` (documented in `lib/auth-flow.js`), which opens the brand website's sign-in page in the user's browser and receives the result through the deep link below.
36
-
37
- 1. User signs in on the website. The website's token page mints a custom token and opens `myapp://auth/token?authToken=XYZ` (deep link).
38
- 2. @omega.js/desktop's deep-link `auth/token` built-in calls `omega.auth.handleToken(token)`.
39
- 3. Main calls `signInWithCustomToken(auth, token)` against its own Firebase Auth → main is now signed in.
40
- 4. Main broadcasts `desktop:auth:sign-in-with-token` IPC with the same token to all renderer windows.
41
- 5. Each renderer receives the broadcast, calls `omega.auth.signInWithCustomToken(token)` against its own (@omega.js/client-managed) Firebase Auth → all renderers signed in with the same user.
42
- 6. Tokens are NOT stored — they expire in 1 hour. Auth state persists via Firebase's built-in IndexedDB persistence.
43
-
44
- ### Auth flow: renderer load → sync with main
45
-
46
- When a renderer window opens (cold or warm), it asks main for the current state:
47
-
48
- 1. Renderer sends `desktop:auth:sync-request` IPC with its current UID (or null).
49
- 2. Main compares with its own UID:
50
- - **Same UID** → no sync needed, returns `{ needsSync: false }`.
51
- - **Main signed out, renderer signed in** → returns `{ needsSync: true, signOut: true }`. Renderer signs out.
52
- - **Main signed in, renderer not (or different user)** → main fetches a fresh custom token from `POST ${apiUrl}/omega/user/token` (responds `{ token }`) and returns `{ needsSync: true, customToken, user }`. Renderer signs in with that token.
53
-
54
- ### Sign-out flow
55
-
56
- Main code calls `omega.auth.signOut()`; a renderer calls `omega.signOut()`, which a click on any `.omega-signout` element runs (@omega.js/client's trigger: confirm, then `desktop:auth:sign-out` to main). Either way:
57
-
58
- 1. Main signs out its own Firebase.
59
- 2. Main broadcasts `desktop:auth:sign-out` IPC to all renderers.
60
- 3. Each renderer signs out its own Firebase, on that broadcast alone (the clicked window included), so nothing signs out twice.
61
-
62
- ## Public API
63
-
64
- ### Main process (`omega.auth`)
65
-
66
- ```js
67
- // The account, always a `User`: signed out until a renderer pushes the account
68
- // document of the uid main's session holds, and signed out again the moment that
69
- // session ends.
70
- omega.auth.user;
71
- // → User: .authenticated .uid .email .plan .active .trialing .cancelling .everPaid,
72
- // .roles, .subscription, ..., .profile { displayName, photoURL, emailVerified }
73
-
74
- // Subscribe to state changes (e.g. to refresh tray/menu items). Called with
75
- // `{ user }`, plus a catch-up with the current state after listen() returns.
76
- const off = omega.auth.listen(({ user }) => {
77
- omega.tray.refresh();
78
- omega.menu.refresh();
79
- });
80
- off(); // unsubscribe
81
-
82
- // Sign in via a custom token (called automatically by the auth/token deep-link route).
83
- await omega.auth.handleToken(token);
84
-
85
- // Fresh Firebase ID token for calling authenticated backend routes from main
86
- // (send as `Authorization: Bearer <token>`). null when signed out.
87
- await omega.auth.getIdToken();
88
-
89
- // Or let the instance fetch: @omega.js/client's request, built once on main,
90
- // attaches that token itself (the renderer's and the extension's shape).
91
- await omega.request('/notes', { method: 'POST', body: { text: 'hi' } });
92
-
93
- // Sign main out and broadcast the sign-out to every renderer.
94
- await omega.auth.signOut();
95
- ```
96
-
97
- Main can't run Firestore, so the account crosses from the renderers: each renderer runs @omega.js/client's full auth cycle and pushes the WHOLE stored document (`desktop:auth:account-resolved`, `{ uid, document, identity }`, uid-guarded), and main builds its `omega.auth.user` from it. `user.plan`, `user.active` and `user.roles.admin` read the same in main as in the renderer.
98
-
99
- ### Renderer process (the renderer's `omega`)
100
-
101
- ```js
102
- // The renderer's own account, from @omega.js/client
103
- omega.auth.user; // a User, the same class main holds
104
- omega.auth.listen(({ user, denied }) => { });
105
-
106
- // Main's authoritative account: { uid, document, identity }
107
- const main = await omega.getMainUser();
108
-
109
- // Sign out through main (broadcasts to every renderer). omega.auth.signOut()
110
- // signs out this renderer alone.
111
- await omega.signOut();
112
- ```
113
-
114
- The renderer's `omega.initialize()` automatically:
115
- - Boots @omega.js/client (so renderer-side Firebase is available).
116
- - Wires the auth bridge (`desktop:auth:sync-request` on load + listens for broadcasts).
117
- - Registers the auth click triggers the extension's pages carry, so a view signs in with markup
118
- alone: `.omega-signin` runs main's `omega.openAuthFlow()` (`desktop:auth:open-flow`),
119
- `.omega-account` opens the website's `/account` page in the user's browser
120
- (`desktop:auth:open-account`), and `.omega-signout` (@omega.js/client's) runs `omega.signOut()`,
121
- signing the whole app out through main (`desktop:auth:sign-out`).
122
- - Runs @omega.js/client's **full auth cycle** (`omega.auth.listen()`): waits for auth to settle,
123
- fetches the Firestore account, lands one `User`, and auto-populates the
124
- **`data-omega-bind` bindings**, so @omega.js/desktop app views use the same reactive HTML as
125
- every OMEGA browser surface (`@show auth.user.authenticated`, `@text auth.user.plan`,
126
- `@show auth.user.plan === 'premium'`, see @omega.js/client's docs/bindings.md). Each signed-in
127
- state pushes its account to main (`desktop:auth:account-resolved`) and re-offers it whenever
128
- main announces a state change, so a renderer that resolved before main signed in still delivers.
129
-
130
- You don't write any of this — it just works.
131
-
132
- ## Session persistence (main)
133
-
134
- Renderers persist their Firebase sessions in IndexedDB for free (browser contexts).
135
- Main is Node — Firebase defaults to in-memory there — so @omega.js/desktop plugs in its own vault:
136
- **`lib/auth-persistence.js`**, a PLUGGABLE strategy behind a custom Firebase
137
- `Persistence` (the `getReactNativePersistence()` shape).
138
-
139
- - **`safeStorage`** (default) — values encrypted via Electron `safeStorage` (macOS
140
- Keychain / Windows DPAPI / kwallet-gnome) before touching disk
141
- (`{userData}/omega-auth-session.json` holds base64 ciphertext only). This is the same
142
- os_crypt machinery Chromium uses for its cookie jar — stronger than browser
143
- IndexedDB/localStorage, which are plaintext LevelDB on disk.
144
- - **`none`** — explicit opt-out (in-memory, pre-1.12 behavior).
145
- - **Custom** — `require('@omega.js/desktop/lib/auth-persistence').register(name, { available, getItem, setItem, removeItem })` before `initialize()`, then select it via config.
146
-
147
- ```jsonc
148
- {
149
- "omega": {
150
- "authPersistence": "safeStorage" // 'safeStorage' (default) | 'none' | custom name
151
- }
152
- }
153
- ```
154
-
155
- A TEST RUN is always `none`, whatever this config says, on the main and boot layers
156
- alike: the harness never signs a real user in, so it never asks the OS keychain
157
- ([#907](https://github.com/Omega-JS-Stack/omega/issues/907), the mechanics in
158
- [test-framework.md](test-framework.md)).
159
-
160
- Storage only — distribution across processes stays the IPC sync protocol above.
161
- The session restores at boot (offline included: no network round trip), so a restart
162
- keeps the user signed in; renderers then re-resolve the account and re-push it.
163
-
164
- ## Config
165
-
166
- ```jsonc
167
- {
168
- "cloud": {
169
- "provider": "firebase",
170
- "config": {
171
- "apiKey": "...",
172
- "authDomain": "myapp.com",
173
- "projectId": "myapp",
174
- // ... etc.
175
- }
176
- }
177
- }
178
- ```
179
-
180
- If `cloud.config` is empty/missing, `omega.auth` logs a warning and runs in no-op mode (`user` stays the signed-out `User`, everything else returns harmless defaults).
181
-
182
- ## Firebase (bundled)
183
-
184
- Firebase is **bundled from @omega.js/desktop's module context** (@omega.js/client owns it in @omega.js/desktop's dependency tree), the same treatment `json5` gets in main.
185
-
186
- If you're building a no-auth Electron app, just leave `cloud.config` empty — the bridge is a clean no-op.
187
-
188
- In a TESTING run (`OMEGA_ENVIRONMENT=testing`) the bridge connects its auth instance to the local auth emulator, on the port it reads in three steps: `OMEGA_AUTH_PORT` when the CLI that booted the stack published one, then the `dev.ports.auth` value the bundle baked into `OMEGA_BUILD_JSON` (a packaged main process has no parent env, [#745](https://github.com/Omega-JS-Stack/omega/issues/745)), then the classic `9099`. Same chain `getApiUrl()` walks ([environment-detection.md](environment-detection.md)) and the same move it makes when it maps testing to localhost, and the same one @omega.js/extension's background worker makes for its emulator runs; development and production are untouched.
189
-
190
- ## Common patterns
191
-
192
- ### Refresh tray when auth state changes
193
-
194
- ```js
195
- // In src/integrations/tray/index.js, or anywhere main code reaches omega:
196
- omega.auth.listen(() => {
197
- omega.tray.refresh(); // re-evaluates dynamic labels
198
- });
199
- ```
200
-
201
- ```js
202
- // In src/integrations/tray/index.js:
203
- tray.item({
204
- label: () => {
205
- const { user } = omega.auth;
206
- return user.authenticated ? `Signed in as ${user.email}` : 'Sign in';
207
- },
208
- click: () => {
209
- if (omega.auth.user.authenticated) {
210
- omega.auth.signOut();
211
- } else {
212
- omega.openAuthFlow();
213
- }
214
- },
215
- });
216
- ```
217
-
218
- ### Gate a deep-link route on auth
219
-
220
- ```js
221
- omega.deepLink.on('user/profile/:id', (ctx) => {
222
- if (!omega.auth.user.authenticated) {
223
- omega.openAuthFlow();
224
- ctx.handled = true;
225
- return;
226
- }
227
- omega.windows.show('main');
228
- omega.windows.get('main').webContents.send('navigate', { to: `/profile/${ctx.params.id}` });
229
- });
230
- ```
231
-
232
- ### Sign-out button in a renderer
233
-
234
- ```html
235
- <button id="signout">Sign out</button>
236
- <script>
237
- document.getElementById('signout').addEventListener('click', async () => {
238
- await omega.signOut(); // goes through main, propagates everywhere
239
- });
240
- </script>
241
- ```
242
-
243
- ## IPC channels
244
-
245
- | Channel | Direction | Payload | Description |
246
- |---|---|---|---|
247
- | `desktop:auth:sync-request` | renderer → main | `{ contextUid }` | "I'm at this UID, are we in sync?" |
248
- | `desktop:auth:sign-out` | renderer → main | (none) | "Sign me (and everyone) out." The `.omega-signout` trigger and `omega.signOut()`; main runs `omega.auth.signOut()`. |
249
- | `desktop:auth:get-user` | renderer → main | (none) | Read main's account: `{ uid, document, identity }`. |
250
- | `desktop:auth:account-resolved` | renderer → main | `{ uid, document, identity }` | The account this renderer's client resolved; main builds `omega.auth.user` from it (uid-guarded). |
251
- | `desktop:auth:open-flow` | renderer → main | (none) | The `.omega-signin` trigger: main runs `omega.openAuthFlow()`. |
252
- | `desktop:auth:open-account` | renderer → main | (none) | The `.omega-account` trigger: main opens `<getWebsiteUrl()>/account` in the user's browser. |
253
- | `desktop:auth:plan-changed` | main → all renderers | `{ document }` | Main landed a new account. |
254
- | `desktop:auth:sign-in-with-token` | main → all renderers | `{ token }` | "Sign in with this custom token now." |
255
- | `desktop:auth:sign-out` | main → all renderers | `{}` | "Sign out now." |
256
- | `desktop:auth:state-changed` | main → all renderers | `{ uid, email, ... } \| null` | Auth state changed (informational). |
257
-
258
- ## Testing
259
-
260
- ### Unit tests (always run)
261
-
262
- `auth.test.js` covers the dispatch logic, IPC handler shape, sync-request comparison, and the `auth/token` deep-link integration, all without hitting Firebase.
263
-
264
- ### The real-surface e2e lane (monorepo root)
265
-
266
- `npm run test:e2e-desktop` ([scripts/e2e-desktop-auth.js](../../../scripts/e2e-desktop-auth.js)) boots a real Electron app against the backend emulator and delivers `<brand.id>://auth/token` from a SECOND instance — the OS-forwarded argv path — then asserts main AND the renderer both land on the emulator user. Offline; it is the lane that proves this whole chain end to end.
267
-
268
- ### Extended tests (skip without the opt-in)
269
-
270
- `auth.integration.test.js` talks to REAL Firebase, so it is gated behind extended mode (the cross-framework `TEST_EXTENDED_MODE` opt-in; see [test-framework.md](test-framework.md#extended-vs-normal-mode)):
271
-
272
- ```bash
273
- npx omega test --extended # or: TEST_EXTENDED_MODE=true npx omega test
274
- ```
275
-
276
- It asks for NO credential of its own. The SIGN-IN proof belongs to [#904](https://github.com/Omega-JS-Stack/omega/issues/904), which signs desktop in as a persona the backend emulator seeds, the same mechanism web and the extension use. Without the opt-in the suite skips cleanly with a reason, so CI stays green.
277
-
278
- ## Implementation notes
279
-
280
- - Firebase app name in main is `omega-auth` (avoids clashes if a consumer's main code also wants its own Firebase instance).
281
- - The bridge does NOT persist user info to @omega.js/desktop storage: the session vault (above) and the renderers' IndexedDB persistence handle session restoration, the same as @omega.js/extension.
282
- - Custom tokens are NEVER stored. Renderers receive them once via broadcast, sign in, discard. Fresh tokens are minted on demand from `POST /omega/user/token` through main's `omega.request()` (@omega.js/client's `createRequest`, built once on the instance), the same code path as the extension background's token sync.
283
- - `omega.getApiUrl()` returns the dev or prod URL, so the bridge automatically hits the right backend. Available on all three process instances (main / renderer / preload) via the shared `src/utils/url-helpers.js` module, the same code path everywhere. See the Cross-context helpers section of the framework guide ([docs/desktop/index.md](../../../docs/desktop/index.md)).
284
- - All sensitive Firebase user fields (`stsTokenManager`, `providerData`, etc.) are stripped before sending over IPC. Only the identity `{uid, email, displayName, photoURL, emailVerified}` and the stored account document cross the bridge.