@omega.js/desktop 0.51.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/README.md +18 -13
  2. package/dist/assets/css/core/_initialize.scss +1 -1
  3. package/dist/assets/js/core/app-shell.js +14 -15
  4. package/dist/assets/themes/_template/_theme.js +6 -6
  5. package/dist/assets/themes/base/_includes/global/sections/account.html +4 -4
  6. package/dist/assets/themes/base/_includes/global/sections/app-sidebar.html +5 -5
  7. package/dist/assets/themes/base/_layouts/frontend/pages/account/index.html +3 -3
  8. package/dist/assets/themes/base/_layouts/frontend/pages/alternatives/index.html +1 -1
  9. package/dist/assets/themes/base/_layouts/frontend/pages/blog/tags/tag.html +1 -1
  10. package/dist/assets/themes/base/_layouts/frontend/pages/payment/confirmation.html +1 -1
  11. package/dist/assets/themes/base/_sections/marketing/newsletter-cta/section.js +2 -3
  12. package/dist/assets/themes/base/_sections/verts/unit/section.html +1 -1
  13. package/dist/assets/themes/base/_sections/verts/unit/section.js +3 -4
  14. package/dist/assets/themes/base/_theme.js +4 -3
  15. package/dist/assets/themes/bootstrap/_theme.js +2 -2
  16. package/dist/assets/themes/bootstrap/overrides/_links.scss +1 -1
  17. package/dist/assets/themes/classy/_theme.js +8 -6
  18. package/dist/assets/themes/classy/css/marketing/_sections.scss +1 -2
  19. package/dist/assets/themes/classy/js/hero-demo-form.js +3 -2
  20. package/dist/assets/themes/neobrutalism/_theme.js +5 -5
  21. package/dist/assets/themes/neobrutalism/js/pages/test/libraries/layers/index.js +1 -1
  22. package/dist/assets/themes/newsflash/_theme.js +6 -6
  23. package/dist/assets/themes/newsflash/js/pages/test/libraries/layers/index.js +1 -1
  24. package/dist/build.js +87 -111
  25. package/dist/cli.js +1 -1
  26. package/dist/commands/build.js +2 -2
  27. package/dist/commands/cdp/capture.js +2 -2
  28. package/dist/commands/cdp/quit.js +2 -2
  29. package/dist/commands/cdp/relaunch.js +2 -2
  30. package/dist/commands/cdp/theme.js +1 -1
  31. package/dist/commands/clean.js +2 -2
  32. package/dist/commands/deploy.js +4 -4
  33. package/dist/commands/finalize-release.js +4 -4
  34. package/dist/commands/install.js +3 -3
  35. package/dist/commands/launch.js +2 -2
  36. package/dist/commands/lib/deploy-precheck.js +3 -3
  37. package/dist/commands/lib/ensure-target.js +6 -6
  38. package/dist/commands/package.js +2 -2
  39. package/dist/commands/publish.js +2 -2
  40. package/dist/commands/release.js +3 -3
  41. package/dist/commands/runner.js +145 -48
  42. package/dist/commands/sign-windows.js +5 -5
  43. package/dist/commands/test.js +4 -4
  44. package/dist/commands/update.js +2 -2
  45. package/dist/commands/validate-certs.js +4 -4
  46. package/dist/commands/version.js +3 -3
  47. package/dist/defaults/.github/workflows/build.yml +2 -0
  48. package/dist/defaults/AGENTS.md +17 -8
  49. package/dist/defaults/config/omega.json5 +7 -7
  50. package/dist/defaults/hooks/build/post.js +1 -1
  51. package/dist/defaults/hooks/build/pre.js +1 -1
  52. package/dist/defaults/hooks/deploy/pre.js +1 -1
  53. package/dist/defaults/hooks/release/post.js +1 -1
  54. package/dist/defaults/hooks/release/pre.js +1 -1
  55. package/dist/defaults/src/assets/js/components/about/index.js +3 -5
  56. package/dist/defaults/src/assets/js/components/main/index.js +3 -5
  57. package/dist/defaults/src/assets/js/components/settings/index.js +3 -5
  58. package/dist/defaults/src/integrations/context-menu/index.js +2 -2
  59. package/dist/defaults/src/integrations/menu/index.js +3 -3
  60. package/dist/defaults/src/integrations/tray/index.js +3 -3
  61. package/dist/defaults/src/main.js +3 -5
  62. package/dist/defaults/src/preload.js +3 -5
  63. package/dist/defaults/test/README.md +2 -2
  64. package/dist/gulp/main.js +9 -10
  65. package/dist/gulp/tasks/audit.js +7 -7
  66. package/dist/gulp/tasks/build-config.js +8 -8
  67. package/dist/gulp/tasks/bundle.js +16 -16
  68. package/dist/gulp/tasks/defaults.js +3 -3
  69. package/dist/gulp/tasks/distribute.js +2 -2
  70. package/dist/gulp/tasks/html.js +9 -9
  71. package/dist/gulp/tasks/package-quick.js +3 -3
  72. package/dist/gulp/tasks/package.js +3 -3
  73. package/dist/gulp/tasks/release.js +3 -3
  74. package/dist/gulp/tasks/sass.js +6 -6
  75. package/dist/gulp/tasks/serve.js +4 -4
  76. package/dist/hooks/notarize-artifacts.js +1 -1
  77. package/dist/hooks/notarize.js +1 -1
  78. package/dist/index.js +5 -8
  79. package/dist/lib/_environment-mixin.js +50 -0
  80. package/dist/lib/_lifecycle-mixin.js +45 -0
  81. package/dist/lib/analytics.js +33 -35
  82. package/dist/lib/app-state.js +14 -14
  83. package/dist/lib/auth-flow.js +18 -18
  84. package/dist/lib/auth-persistence.js +12 -12
  85. package/dist/lib/auth.js +421 -0
  86. package/dist/lib/auto-updater.js +49 -49
  87. package/dist/lib/context-menu.js +13 -13
  88. package/dist/lib/context.js +19 -19
  89. package/dist/lib/deep-link.js +34 -34
  90. package/dist/lib/fontawesome.js +5 -5
  91. package/dist/lib/ipc.js +4 -4
  92. package/dist/lib/menu.js +25 -25
  93. package/dist/lib/protocol.js +5 -5
  94. package/dist/lib/remote-config.js +22 -22
  95. package/dist/lib/remote-scripts.js +21 -21
  96. package/dist/lib/restart-manager/index.js +28 -28
  97. package/dist/lib/sign-helpers/exec-with-limit.js +1 -1
  98. package/dist/lib/sign-helpers/sign-events.js +1 -1
  99. package/dist/lib/startup.js +18 -13
  100. package/dist/lib/storage.js +10 -10
  101. package/dist/lib/templating.js +16 -16
  102. package/dist/lib/theme.js +10 -10
  103. package/dist/lib/tray.js +27 -27
  104. package/dist/lib/usage.js +11 -11
  105. package/dist/lib/window-manager.js +26 -26
  106. package/dist/main.js +398 -483
  107. package/dist/preload.js +236 -176
  108. package/dist/renderer.js +417 -391
  109. package/dist/runner/job-started.js +1 -1
  110. package/dist/test/fixtures/consumer-app/config/omega.json5 +1 -1
  111. package/dist/test/fixtures/consumer-app/src/assets/js/components/main/index.js +8 -11
  112. package/dist/test/fixtures/consumer-app/src/main.js +5 -7
  113. package/dist/test/fixtures/consumer-app/src/preload.js +2 -2
  114. package/dist/test/harness/boot-entry.js +22 -20
  115. package/dist/test/harness/main-entry.js +31 -30
  116. package/dist/test/harness/renderer-entry.js +5 -5
  117. package/dist/test/harness/renderer-preload.js +137 -141
  118. package/dist/test/index.js +10 -10
  119. package/dist/test/runner.js +2 -2
  120. package/dist/test/runners/boot.js +7 -6
  121. package/dist/test/runners/electron.js +3 -2
  122. package/dist/test/runners/render-event.js +2 -2
  123. package/dist/test/suites/boot/consumer-app-boots.test.js +57 -26
  124. package/dist/test/suites/boot/restart-manager.test.js +2 -2
  125. package/dist/test/suites/boot/storage-bundled.test.js +5 -5
  126. package/dist/test/suites/boot/theme.test.js +13 -13
  127. package/dist/test/suites/build/audit.test.js +1 -1
  128. package/dist/test/suites/build/auth-persistence-resolve.test.js +6 -6
  129. package/dist/test/suites/build/boot-fixture.test.js +2 -2
  130. package/dist/test/suites/build/boot-runner-timeout.test.js +6 -5
  131. package/dist/test/suites/build/brand-scss.test.js +1 -1
  132. package/dist/test/suites/build/build-json-bake.test.js +1 -1
  133. package/dist/test/suites/build/cli.test.js +2 -2
  134. package/dist/test/suites/build/config-schema.test.js +5 -5
  135. package/dist/test/suites/build/defaults-scaffold.test.js +2 -2
  136. package/dist/test/suites/build/deploy-hook.test.js +3 -3
  137. package/dist/test/suites/build/ensure-target.test.js +2 -2
  138. package/dist/test/suites/build/env-delivery.test.js +2 -2
  139. package/dist/test/suites/build/esm-only-dependency.test.js +2 -2
  140. package/dist/test/suites/build/exports.test.js +9 -8
  141. package/dist/test/suites/build/get-config.test.js +4 -4
  142. package/dist/test/suites/build/manifest-deps.test.js +1 -1
  143. package/dist/test/suites/build/merge-line-files.test.js +1 -1
  144. package/dist/test/suites/build/omega-shell.test.js +34 -2
  145. package/dist/test/suites/build/omega.test.js +350 -0
  146. package/dist/test/suites/build/renderer-auth-bridge.test.js +211 -78
  147. package/dist/test/suites/build/runner.test.js +612 -5
  148. package/dist/test/suites/build/sentry.test.js +2 -2
  149. package/dist/test/suites/build/sign-windows-e2e.test.js +2 -2
  150. package/dist/test/suites/build/templating.test.js +3 -3
  151. package/dist/test/suites/build/test-stealth.test.js +7 -9
  152. package/dist/test/suites/build/url-helpers.test.js +55 -56
  153. package/dist/test/suites/build/validate-config.test.js +2 -2
  154. package/dist/test/suites/build/wave5-pins.test.js +2 -2
  155. package/dist/test/suites/main/analytics.test.js +59 -59
  156. package/dist/test/suites/main/app-state.test.js +66 -66
  157. package/dist/test/suites/main/auth-flow.test.js +41 -41
  158. package/dist/test/suites/main/auth-persistence.test.js +54 -43
  159. package/dist/test/suites/main/{client-bridge.integration.test.js → auth.integration.test.js} +10 -9
  160. package/dist/test/suites/main/auth.test.js +336 -0
  161. package/dist/test/suites/main/auto-updater.test.js +134 -134
  162. package/dist/test/suites/main/boot-sequence.test.js +37 -49
  163. package/dist/test/suites/main/context-menu.test.js +51 -50
  164. package/dist/test/suites/main/context.test.js +25 -25
  165. package/dist/test/suites/main/deep-link.test.js +74 -74
  166. package/dist/test/suites/main/fontawesome.test.js +27 -27
  167. package/dist/test/suites/main/ipc.test.js +35 -35
  168. package/dist/test/suites/main/menu.test.js +101 -100
  169. package/dist/test/suites/main/protocol.test.js +19 -19
  170. package/dist/test/suites/main/remote-config.test.js +63 -63
  171. package/dist/test/suites/main/remote-scripts.test.js +103 -103
  172. package/dist/test/suites/main/request.test.js +71 -0
  173. package/dist/test/suites/main/restart-manager.test.js +33 -33
  174. package/dist/test/suites/main/startup-paths-and-ua.test.js +4 -4
  175. package/dist/test/suites/main/startup.test.js +26 -26
  176. package/dist/test/suites/main/stealth-window.test.js +1 -1
  177. package/dist/test/suites/main/storage.test.js +25 -25
  178. package/dist/test/suites/main/theme.test.js +34 -34
  179. package/dist/test/suites/main/tray.test.js +79 -79
  180. package/dist/test/suites/main/url-helpers.test.js +133 -133
  181. package/dist/test/suites/main/usage.test.js +25 -25
  182. package/dist/test/suites/main/window-bounds.test.js +27 -27
  183. package/dist/test/suites/main/window-manager.test.js +44 -44
  184. package/dist/test/suites/renderer/analytics-bridge.test.js +5 -5
  185. package/dist/test/suites/renderer/cross-context-helpers.test.js +37 -31
  186. package/dist/test/suites/renderer/round-trip.test.js +3 -3
  187. package/dist/test/suites/renderer/tooltips.test.js +15 -15
  188. package/dist/test/suites/renderer/{window-em-surface.test.js → window-desktop-surface.test.js} +27 -7
  189. package/dist/test/utils/extended-mode-warning.js +1 -1
  190. package/dist/utils/boot-harness.js +56 -0
  191. package/dist/utils/mode-helpers.js +2 -15
  192. package/dist/utils/ship-keys.js +3 -3
  193. package/dist/utils/signing-status.js +51 -0
  194. package/dist/utils/test-events.js +7 -0
  195. package/dist/utils/test-stealth.js +6 -6
  196. package/dist/utils/url-helpers.js +52 -42
  197. package/dist/utils/user-agent.js +44 -0
  198. package/dist/vendor/account/engine.js +3 -3
  199. package/dist/vendor/account/index.js +14 -45
  200. package/dist/vendor/account/resolve-account.js +44 -0
  201. package/dist/vendor/account/schema.js +1 -1
  202. package/dist/vendor/account/user.js +99 -0
  203. package/dist/vendor/config/client-config.js +1 -1
  204. package/dist/vendor/config/environment.js +11 -30
  205. package/dist/vendor/config/index.js +13 -12
  206. package/dist/vendor/config/load.js +1 -2
  207. package/dist/vendor/config/platforms.js +1 -1
  208. package/dist/vendor/config/repo.js +24 -0
  209. package/dist/vendor/config/retired-keys.js +2 -2
  210. package/dist/vendor/config/schema.js +5 -8
  211. package/dist/vendor/config/site-global.js +2 -3
  212. package/dist/vendor/config/validate.js +1 -2
  213. package/dist/vendor/config/winback.js +1 -1
  214. package/dist/vendor/devkit/actions-secrets.js +1 -1
  215. package/dist/vendor/devkit/attach-log-file.js +1 -1
  216. package/dist/vendor/devkit/brand-version.js +162 -1
  217. package/dist/vendor/devkit/build-json.js +1 -1
  218. package/dist/vendor/devkit/cli-router.js +3 -4
  219. package/dist/vendor/devkit/defaults-engine.js +9 -11
  220. package/dist/vendor/devkit/deploy.js +64 -4
  221. package/dist/vendor/devkit/git-remote.js +78 -1
  222. package/dist/vendor/devkit/local.js +53 -3
  223. package/dist/vendor/devkit/lockfile.js +127 -0
  224. package/dist/vendor/devkit/merge-line-files.js +2 -3
  225. package/dist/vendor/devkit/pack-local.js +4 -7
  226. package/dist/vendor/devkit/preludes/origin-heal.js +35 -49
  227. package/dist/vendor/devkit/target-secrets.js +34 -45
  228. package/dist/vendor/devkit/test/runner-core.js +6 -6
  229. package/dist/vendor/monitoring/env.js +2 -2
  230. package/dist/vendor/monitoring/index.js +1 -1
  231. package/dist/vendor/monitoring/main.js +1 -1
  232. package/dist/vendor/monitoring/preload.js +1 -1
  233. package/dist/vendor/monitoring/renderer.js +1 -1
  234. package/docs/analytics.md +8 -8
  235. package/docs/app-state.md +19 -19
  236. package/docs/audit.md +4 -4
  237. package/docs/{client-bridge.md → auth.md} +88 -73
  238. package/docs/auto-updater.md +8 -8
  239. package/docs/boot-sequence.md +11 -6
  240. package/docs/build-system.md +1 -1
  241. package/docs/cdp-debugging.md +1 -1
  242. package/docs/common-mistakes.md +7 -7
  243. package/docs/config-schema.md +1 -1
  244. package/docs/context-menu.md +13 -13
  245. package/docs/context.md +11 -11
  246. package/docs/css.md +3 -9
  247. package/docs/deep-link.md +25 -25
  248. package/docs/environment-detection.md +16 -16
  249. package/docs/fontawesome.md +7 -5
  250. package/docs/hooks.md +8 -8
  251. package/docs/index.md +39 -28
  252. package/docs/ipc.md +10 -10
  253. package/docs/lib-modules.md +9 -9
  254. package/docs/logging.md +12 -14
  255. package/docs/menu.md +18 -18
  256. package/docs/releasing.md +4 -2
  257. package/docs/remote-config.md +9 -9
  258. package/docs/remote-scripts.md +12 -12
  259. package/docs/restart-manager.md +8 -8
  260. package/docs/runner.md +12 -10
  261. package/docs/sentry.md +4 -4
  262. package/docs/shared/analytics.md +1 -1
  263. package/docs/shared/brands.md +1 -1
  264. package/docs/shared/breaking-changes.md +79 -13
  265. package/docs/shared/config.md +24 -21
  266. package/docs/shared/deploys.md +30 -7
  267. package/docs/shared/local-dev.md +5 -3
  268. package/docs/shared/logging.md +1 -1
  269. package/docs/shared/monitoring.md +5 -5
  270. package/docs/shared/testing.md +2 -2
  271. package/docs/shared/theming.md +1 -1
  272. package/docs/shared/translation.md +19 -10
  273. package/docs/signing.md +1 -1
  274. package/docs/startup.md +16 -16
  275. package/docs/storage.md +11 -11
  276. package/docs/templating.md +3 -3
  277. package/docs/test-boot-layer.md +12 -12
  278. package/docs/test-framework.md +17 -17
  279. package/docs/themes.md +7 -7
  280. package/docs/tooltips.md +2 -2
  281. package/docs/tray.md +31 -31
  282. package/docs/usage.md +7 -7
  283. package/docs/verts.md +1 -1
  284. package/docs/windows.md +22 -22
  285. package/package.json +3 -4
  286. package/dist/lib/client-bridge.js +0 -374
  287. package/dist/lib/logger.js +0 -4
  288. package/dist/test/suites/build/manager.test.js +0 -213
  289. package/dist/test/suites/main/client-bridge.test.js +0 -262
@@ -0,0 +1,421 @@
1
+ // Auth: main-process Firebase Auth, the source of truth every renderer syncs with.
2
+ //
3
+ // This is @omega.js/desktop's analogue of @omega.js/extension's background auth
4
+ // (its src/lib/background-auth.js). The pattern:
5
+ //
6
+ // 1. Main runs its own Firebase Auth instance and is the source of truth for the SESSION.
7
+ // 2. When a deep-link auth/token arrives, main calls signInWithCustomToken with that token,
8
+ // then BROADCASTS the token to all renderer windows so their @omega.js/client Firebase
9
+ // instances can sign in with the SAME token.
10
+ // 3. On every renderer load, the renderer asks main "I'm at UID X (or null)" via the
11
+ // desktop:auth:sync-request IPC. Main compares with its own UID and either does nothing,
12
+ // tells the renderer to sign out, or fetches a fresh custom token from /omega
13
+ // and sends it to the renderer.
14
+ // 4. Sign-out: any renderer can request sign-out via desktop:auth:sign-out. Main signs out
15
+ // its own Firebase + broadcasts desktop:auth:sign-out to all renderers.
16
+ // 5. The ACCOUNT crosses the other way: main cannot read Firestore, so every renderer
17
+ // pushes the account document its @omega.js/client resolved
18
+ // (desktop:auth:account-resolved, `{ uid, document, identity }`), and main builds its
19
+ // `user` from it when the uid is its own session's.
20
+ //
21
+ // Firebase is BUNDLED by esbuild from @omega.js/desktop's module context (@omega.js/client owns it
22
+ // in @omega.js/desktop's tree). If loading fails, auth stays in no-op mode and logs the reason.
23
+
24
+ const { User } = require('../vendor/account/index.js');
25
+ const LoggerLite = require('./logger-lite.js');
26
+ const authPersistence = require('./auth-persistence.js');
27
+ const { requiredPort } = require('../utils/url-helpers.js');
28
+ const sanitizeURL = require('../utils/sanitize-url.js');
29
+
30
+ const logger = new LoggerLite('auth');
31
+
32
+ const FIREBASE_APP_NAME = 'omega-auth';
33
+
34
+ const auth = {
35
+ _initialized: false,
36
+ _omega: null,
37
+ _firebase: null, // firebase/app
38
+ _firebaseAuth: null, // auth instance
39
+ _firebaseModule: null, // firebase/auth namespace (signInWithCustomToken, signOut, ...)
40
+ _callbacks: [], // listen() subscribers, each called with `{ user }`
41
+ _ipcRegistered: false,
42
+
43
+ // The account as one `User`, never null: signed out until a renderer pushes the
44
+ // account document of the uid this process's Firebase session holds, and signed
45
+ // out again the moment that session ends
46
+ user: new User(),
47
+
48
+ // The newest state, `{ user }`, the shape @omega.js/client's listeners get
49
+ state: null,
50
+
51
+ async initialize(omega) {
52
+ if (auth._initialized) {
53
+ return;
54
+ }
55
+
56
+ auth._omega = omega;
57
+ auth.state = { user: auth.user };
58
+
59
+ // Try to load firebase. If it's not installed, run in no-op mode.
60
+ const ok = await auth._tryLoadFirebase();
61
+ if (!ok) {
62
+ logger.warn('firebase not installed: auth running in no-op mode. `npm i firebase` to enable auth.');
63
+ }
64
+
65
+ auth._registerIpc();
66
+
67
+ if (ok) {
68
+ // Boot Firebase Auth. With a persistence strategy active (lib/auth-persistence.js,
69
+ // default safeStorage), the session survives restarts: the restore lands async
70
+ // via onAuthStateChanged shortly after boot.
71
+ try {
72
+ const strategy = await authPersistence.resolve(omega);
73
+ auth._firebaseAuth = auth._getFirebaseAuth(strategy);
74
+ // onAuthStateChanged is the source of truth listener.
75
+ auth._firebaseModule.onAuthStateChanged(auth._firebaseAuth, (user) => {
76
+ auth._handleAuthStateChange(user);
77
+ });
78
+ logger.log(`initialize: firebase loaded (persistence: ${strategy ? strategy.name : 'in-memory'})`);
79
+ } catch (e) {
80
+ logger.error('firebase init failed:', e.message);
81
+ }
82
+ }
83
+
84
+ auth._initialized = true;
85
+ },
86
+
87
+ // Load firebase. Returns true on success. BUNDLED by esbuild from @omega.js/desktop's module context
88
+ // (@omega.js/client owns firebase in @omega.js/desktop's tree), the same treatment as json5 in main.js. In the
89
+ // webpack era it was a webpackIgnore'd runtime import(), which resolves relative to the CONSUMER's
90
+ // main.bundle.js: that walk never reaches @omega.js/desktop's node_modules when @omega.js/desktop is symlinked
91
+ // (`mgr install dev`) and depends on npm hoisting when installed, so every dev app silently
92
+ // ran auth in no-op mode. Interop guards handle both namespace shapes (json5 precedent).
93
+ async _tryLoadFirebase() {
94
+ try {
95
+ const appMod = require('firebase/app');
96
+ const authMod = require('firebase/auth');
97
+
98
+ auth._firebase = appMod.initializeApp ? appMod : (appMod.default || appMod);
99
+ auth._firebaseModule = authMod.getAuth ? authMod : (authMod.default || authMod);
100
+ return true;
101
+ } catch (e) {
102
+ logger.warn('firebase load failed:', e.message);
103
+ return false;
104
+ }
105
+ },
106
+
107
+ _getFirebaseAuth(strategy) {
108
+ if (auth._firebaseAuth) return auth._firebaseAuth;
109
+
110
+ const firebaseConfig = auth._omega.config.cloud?.config;
111
+ if (!firebaseConfig || !Object.keys(firebaseConfig).length) {
112
+ throw new Error('cloud.config is empty: cannot initialize auth.');
113
+ }
114
+
115
+ const { initializeApp, getApp } = auth._firebase;
116
+ const { getAuth, initializeAuth } = auth._firebaseModule;
117
+
118
+ let app;
119
+ try { app = getApp(FIREBASE_APP_NAME); }
120
+ catch (e) { app = initializeApp(firebaseConfig, FIREBASE_APP_NAME); }
121
+
122
+ // With a storage strategy, boot auth on OUR persistence (session survives
123
+ // restarts); without one, firebase's Node default (in-memory) applies.
124
+ if (strategy && initializeAuth) {
125
+ auth._firebaseAuth = initializeAuth(app, {
126
+ persistence: [authPersistence.buildFirebasePersistence(strategy)],
127
+ });
128
+ } else {
129
+ auth._firebaseAuth = getAuth(app);
130
+ }
131
+
132
+ // A TESTING run talks to the LOCAL stack, never real auth: the same move
133
+ // getApiUrl() makes when it maps testing to localhost, and the same one
134
+ // @omega.js/extension's background SW makes for its emulator runs. Only an
135
+ // OMEGA_ENVIRONMENT=testing run reaches here; dev and production are untouched.
136
+ if (auth._omega.isTesting()) {
137
+ const { connectAuthEmulator } = auth._firebaseModule;
138
+ const port = auth._authEmulatorPort();
139
+ logger.log(`testing run: connecting auth to the emulator on :${port}`);
140
+ connectAuthEmulator(auth._firebaseAuth, `http://localhost:${port}`, { disableWarnings: true });
141
+ }
142
+
143
+ return auth._firebaseAuth;
144
+ },
145
+
146
+ // The auth emulator's port, on the same chain getApiUrl() walks: the
147
+ // resolved-port env channel (N7), then the `dev.ports` map the bundle baked
148
+ // into OMEGA_BUILD_JSON
149
+ // ([#745](https://github.com/Omega-JS-Stack/omega/issues/745)). The classic
150
+ // 9099 used to sit under those two as a last resort; it is gone (#834),
151
+ // because nothing identity-checks what answers on it and a neighbouring
152
+ // project's emulator reads as an auth mystery rather than a port problem.
153
+ _authEmulatorPort() {
154
+ return requiredPort(auth._omega, 'OMEGA_AUTH_PORT', 'auth');
155
+ },
156
+
157
+ _registerIpc() {
158
+ if (auth._ipcRegistered) return;
159
+ const ipc = auth._omega.ipc;
160
+
161
+ // Renderer asks main: "I'm at UID X (or null). Are we in sync?"
162
+ ipc.handle('desktop:auth:sync-request', async ({ contextUid }) => {
163
+ return auth._handleSyncRequest(contextUid);
164
+ });
165
+
166
+ // Renderer asks main to sign out (a window's `omega-signout` trigger, or
167
+ // its `omega.signOut()`): main signs out and broadcasts to every window.
168
+ ipc.handle('desktop:auth:sign-out', () => auth.signOut());
169
+
170
+ // Renderer asks main for its account, in the shape a renderer pushes one:
171
+ // the stored document of `user` plus the identity of main's own session
172
+ // (never a token field).
173
+ ipc.handle('desktop:auth:get-user', () => {
174
+ const identity = auth._identity(auth._firebaseAuth?.currentUser);
175
+ return { uid: identity ? identity.uid : null, document: auth.user.toJSON(), identity };
176
+ });
177
+
178
+ // Renderer pushes the account its @omega.js/client resolved. Main can't run
179
+ // Firestore, so this is how main's `user` learns the REAL account: browser
180
+ // contexts resolve, the authority caches.
181
+ ipc.handle('desktop:auth:account-resolved', (payload) => auth._handleAccountResolved(payload));
182
+
183
+ // A renderer's `omega-signin` trigger: the sign-in round trip in the user's browser
184
+ ipc.handle('desktop:auth:open-flow', () => auth._omega.openAuthFlow());
185
+
186
+ // A renderer's `omega-account` trigger: the brand site's /account page in the
187
+ // user's browser, on the host getWebsiteUrl() resolves (localhost in dev), the
188
+ // tray's "Visit Website" resolution
189
+ ipc.handle('desktop:auth:open-account', () => {
190
+ const safe = sanitizeURL(new URL('/account', auth._omega.getWebsiteUrl()).toString());
191
+ if (safe) require('electron').shell.openExternal(safe);
192
+ });
193
+
194
+ auth._ipcRegistered = true;
195
+ },
196
+
197
+ // UID-guarded: a push whose uid is not this session's (a stale push, or one from
198
+ // before main signed in) lands nothing. An accepted push whose account differs
199
+ // from the one `user` holds lands a new User and announces it.
200
+ _handleAccountResolved({ uid, document, identity } = {}) {
201
+ const mainUid = auth._firebaseAuth?.currentUser?.uid || null;
202
+ if (!uid || uid !== mainUid) {
203
+ return { accepted: false };
204
+ }
205
+
206
+ const next = new User(document, identity);
207
+
208
+ // The account a renderer already pushed: nothing to land or announce
209
+ if (auth.user.uid === next.uid && JSON.stringify(auth.user.toJSON()) === JSON.stringify(next.toJSON())) {
210
+ return { accepted: true };
211
+ }
212
+
213
+ logger.log(`account resolved: plan=${next.plan} active=${next.active}`);
214
+ auth._land(next);
215
+ auth._omega.ipc.broadcast('desktop:auth:plan-changed', { document: next.toJSON() });
216
+
217
+ return { accepted: true };
218
+ },
219
+
220
+ // Land a new User as the state and tell every listener; a throwing listener
221
+ // never stops the others
222
+ _land(user) {
223
+ auth.user = user;
224
+ auth.state = { user };
225
+
226
+ for (const callback of [...auth._callbacks]) {
227
+ try {
228
+ callback(auth.state);
229
+ } catch (e) {
230
+ logger.error('auth listener threw:', e);
231
+ }
232
+ }
233
+ },
234
+
235
+ _handleAuthStateChange(user) {
236
+ logger.log(`auth state → ${user ? user.email : 'signed out'}`);
237
+
238
+ // The pushed account belongs to ONE uid: a sign-out, or a different account
239
+ // signing in, drops it so `user` never claims a session main lost. Renderers
240
+ // re-resolve and push fresh via desktop:auth:account-resolved.
241
+ if (auth.user.authenticated && auth.user.uid !== (user?.uid || null)) {
242
+ auth._land(new User());
243
+ }
244
+
245
+ // Tell renderers about the change so they can update UI / re-push their account.
246
+ const identity = auth._identity(user);
247
+ auth._omega.ipc.broadcast('desktop:auth:state-changed', identity);
248
+
249
+ // Attribute Sentry events to the signed-in user (or clear on sign-out). When
250
+ // sentry is disabled (no DSN, dev mode, etc.) setUser is a documented no-op.
251
+ auth._omega.sentry.setUser(identity);
252
+ },
253
+
254
+ // A renderer reports its uid; main answers whether (and how) it must sync to main's user.
255
+ async _handleSyncRequest(contextUid) {
256
+ if (!auth._firebaseAuth) {
257
+ return { needsSync: false, reason: 'firebase-not-loaded' };
258
+ }
259
+
260
+ const bgUser = auth._firebaseAuth.currentUser;
261
+ const bgUid = bgUser?.uid || null;
262
+
263
+ // Already in sync.
264
+ if (contextUid === bgUid) {
265
+ return { needsSync: false };
266
+ }
267
+
268
+ // Renderer signed in but main signed out → tell renderer to sign out.
269
+ if (!bgUser && contextUid) {
270
+ return { needsSync: true, signOut: true };
271
+ }
272
+
273
+ // Main signed in, renderer not (or different user) → fetch a fresh custom token + send.
274
+ try {
275
+ const token = await auth._fetchCustomToken();
276
+ return {
277
+ needsSync: true,
278
+ customToken: token,
279
+ user: auth._identity(bgUser),
280
+ };
281
+ } catch (e) {
282
+ logger.error('sync-request: failed to fetch custom token:', e.message);
283
+ return { needsSync: false, error: e.message };
284
+ }
285
+ },
286
+
287
+ async _handleSignOut() {
288
+ try {
289
+ if (auth._firebaseAuth?.currentUser) {
290
+ await auth._firebaseModule.signOut(auth._firebaseAuth);
291
+ }
292
+ auth._omega.ipc.broadcast('desktop:auth:sign-out', {});
293
+ return { success: true };
294
+ } catch (e) {
295
+ logger.error('sign-out failed:', e.message);
296
+ return { success: false, error: e.message };
297
+ }
298
+ },
299
+
300
+ // Public API ──────────────────────────────────────────────────────────────────
301
+
302
+ /**
303
+ * Listen for state changes: every change after this call, plus a catch-up
304
+ * with the current state, delivered after listen() returns.
305
+ * @param {Function} callback - called with `{ user }`.
306
+ * @returns {Function} the unsubscribe.
307
+ */
308
+ listen(callback) {
309
+ auth._callbacks.push(callback);
310
+
311
+ const current = auth.state;
312
+ Promise.resolve().then(() => {
313
+ if (auth.state === current && auth._callbacks.includes(callback)) {
314
+ callback(auth.state);
315
+ }
316
+ });
317
+
318
+ return () => {
319
+ const index = auth._callbacks.indexOf(callback);
320
+ if (index > -1) {
321
+ auth._callbacks.splice(index, 1);
322
+ }
323
+ };
324
+ },
325
+
326
+ /**
327
+ * Sign in with a custom token and hand the same token to every renderer.
328
+ * Called by lib/deep-link's auth/token built-in.
329
+ * @param {string} token - the Firebase custom token.
330
+ * @returns {Promise<{ success: boolean, user?: object, reason?: string, error?: string }>}
331
+ */
332
+ async handleToken(token) {
333
+ if (!auth._firebaseAuth) {
334
+ logger.warn('handleToken called but firebase not loaded: ignoring.');
335
+ return { success: false, reason: 'firebase-not-loaded' };
336
+ }
337
+ if (!token) {
338
+ logger.warn('handleToken called with no token: ignoring.');
339
+ return { success: false, reason: 'no-token' };
340
+ }
341
+
342
+ try {
343
+ const cred = await auth._firebaseModule.signInWithCustomToken(auth._firebaseAuth, token);
344
+ logger.log(`signed in: ${cred.user.email || cred.user.uid}`);
345
+
346
+ // Broadcast the token to renderers so they can sign in with the SAME token.
347
+ // Tokens expire in 1 hour and aren't stored.
348
+ auth._omega.ipc.broadcast('desktop:auth:sign-in-with-token', { token });
349
+
350
+ return { success: true, user: auth._identity(cred.user) };
351
+ } catch (e) {
352
+ logger.error('signInWithCustomToken failed:', e.message);
353
+ return { success: false, error: e.message };
354
+ }
355
+ },
356
+
357
+ /**
358
+ * A fresh Firebase ID token for the signed-in session, for consumer main code
359
+ * calling authenticated backend routes (`Authorization: Bearer <token>`).
360
+ * @param {boolean} [force] - refresh the token even when the cached one is still valid.
361
+ * @returns {Promise<string|null>} the token, or null when signed out or Firebase is absent.
362
+ */
363
+ async getIdToken(force) {
364
+ const u = auth._firebaseAuth?.currentUser;
365
+ return u ? u.getIdToken(force) : null;
366
+ },
367
+
368
+ /**
369
+ * Sign main out and broadcast the sign-out to every renderer.
370
+ * @returns {Promise<{ success: boolean, error?: string }>}
371
+ */
372
+ async signOut() {
373
+ return auth._handleSignOut();
374
+ },
375
+
376
+ // ─── Internals ────────────────────────────────────────────────────────────
377
+
378
+ // The sign-in's view of a Firebase user, the identity a `User` is built from:
379
+ // no token field ever crosses the bridge
380
+ _identity(user) {
381
+ if (!user) return null;
382
+
383
+ return {
384
+ uid: user.uid,
385
+ email: user.email,
386
+ displayName: user.displayName,
387
+ photoURL: user.photoURL,
388
+ emailVerified: user.emailVerified,
389
+ };
390
+ },
391
+
392
+ // Fetch a fresh custom token for the currently-signed-in user through the
393
+ // instance's `omega.request()`: POST <apiUrl>/omega/user/token, with this
394
+ // session's Bearer token. The route defaults uid to the authenticated caller
395
+ // and responds { token } at the top level.
396
+ async _fetchCustomToken() {
397
+ const data = await auth._omega.request('/omega/user/token', { method: 'POST', body: {} });
398
+
399
+ const token = data?.token;
400
+ if (!token) {
401
+ throw new Error('@omega.js/backend response missing token.');
402
+ }
403
+ return token;
404
+ },
405
+
406
+ // Tear-down used by tests: signs out and drops the account and the listeners,
407
+ // but keeps registered IPC handlers (they're idempotent via `ipc.handle`
408
+ // duplicate-detection).
409
+ async _resetForTests() {
410
+ try {
411
+ if (auth._firebaseAuth?.currentUser && auth._firebaseModule?.signOut) {
412
+ await auth._firebaseModule.signOut(auth._firebaseAuth);
413
+ }
414
+ } catch (e) { /* ignore */ }
415
+ auth._callbacks = [];
416
+ auth.user = new User();
417
+ auth.state = { user: auth.user };
418
+ },
419
+ };
420
+
421
+ module.exports = auth;