@omega.js/desktop 0.1.0 → 0.51.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 (267) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/css/tokens/_index.scss +1 -1
  3. package/dist/assets/themes/base/_includes/frontend/sections/account-section-header.html +4 -1
  4. package/dist/assets/themes/base/_includes/frontend/sections/footer.html +13 -6
  5. package/dist/assets/themes/base/_includes/frontend/sections/nav.html +10 -8
  6. package/dist/assets/themes/base/_includes/global/sections/account.html +3 -1
  7. package/dist/assets/themes/base/_includes/global/sections/app-sidebar.html +12 -8
  8. package/dist/assets/themes/base/_includes/global/sections/app-topbar.html +10 -6
  9. package/dist/assets/themes/base/_includes/global/sections/page-header.html +8 -4
  10. package/dist/assets/themes/base/_layouts/backend/pages/dashboard/index.html +24 -24
  11. package/dist/assets/themes/base/_layouts/frontend/pages/about.html +10 -10
  12. package/dist/assets/themes/base/_layouts/frontend/pages/account/index.html +35 -35
  13. package/dist/assets/themes/base/_layouts/frontend/pages/alternatives/index.html +4 -4
  14. package/dist/assets/themes/base/_layouts/frontend/pages/auth/signin.html +1 -1
  15. package/dist/assets/themes/base/_layouts/frontend/pages/auth/signup.html +4 -4
  16. package/dist/assets/themes/base/_layouts/frontend/pages/contact.html +3 -3
  17. package/dist/assets/themes/base/_layouts/frontend/pages/download.html +34 -33
  18. package/dist/assets/themes/base/_layouts/frontend/pages/extension/index.html +11 -11
  19. package/dist/assets/themes/base/_layouts/frontend/pages/legal/document.html +1 -1
  20. package/dist/assets/themes/base/_layouts/frontend/pages/status.html +1 -1
  21. package/dist/assets/themes/base/_layouts/frontend/pages/team/index.html +9 -7
  22. package/dist/assets/themes/base/_layouts/frontend/pages/team/member.html +5 -3
  23. package/dist/assets/themes/base/_sections/about/letter/section.html +1 -1
  24. package/dist/assets/themes/base/_sections/about/letter/section.json5 +4 -4
  25. package/dist/assets/themes/base/_sections/marketing/bento/section.html +1 -1
  26. package/dist/assets/themes/base/_sections/marketing/bento/section.json5 +15 -15
  27. package/dist/assets/themes/base/_sections/marketing/cta/section.json5 +1 -1
  28. package/dist/assets/themes/base/_sections/marketing/hero/section.html +6 -6
  29. package/dist/assets/themes/base/_sections/marketing/hero/section.json5 +10 -10
  30. package/dist/assets/themes/base/_sections/marketing/product-demo/section.html +1 -1
  31. package/dist/assets/themes/base/_sections/marketing/product-demo/section.json5 +2 -2
  32. package/dist/assets/themes/base/_sections/marketing/stats/section.html +1 -1
  33. package/dist/assets/themes/base/_sections/marketing/stats/section.json5 +5 -5
  34. package/dist/assets/themes/base/_sections/marketing/trusted-by/section.html +1 -1
  35. package/dist/assets/themes/base/_sections/marketing/trusted-by/section.json5 +2 -2
  36. package/dist/assets/themes/neobrutalism/_layouts/frontend/pages/index.html +10 -10
  37. package/dist/assets/themes/newsflash/_layouts/frontend/pages/index.html +10 -10
  38. package/dist/assets/themes/newsflash/_sections/marketing/desks/section.html +2 -2
  39. package/dist/assets/themes/newsflash/_sections/marketing/desks/section.json5 +4 -4
  40. package/dist/build.js +69 -28
  41. package/dist/cli-run.js +20 -13
  42. package/dist/cli.js +12 -7
  43. package/dist/commands/build.js +0 -2
  44. package/dist/commands/deploy.js +116 -44
  45. package/dist/commands/finalize-release.js +5 -4
  46. package/dist/commands/launch.js +12 -7
  47. package/dist/commands/lib/deploy-precheck.js +66 -34
  48. package/dist/commands/lib/ensure-target.js +71 -11
  49. package/dist/commands/package.js +0 -1
  50. package/dist/commands/publish.js +11 -4
  51. package/dist/commands/release.js +99 -252
  52. package/dist/commands/runner.js +42 -5
  53. package/dist/commands/sign-windows.js +46 -15
  54. package/dist/commands/test.js +4 -3
  55. package/dist/commands/validate-certs.js +291 -115
  56. package/dist/config/page-template.html +4 -0
  57. package/dist/defaults/.github/workflows/build.yml +144 -64
  58. package/dist/defaults/AGENTS.md +3 -2
  59. package/dist/defaults/_.gitignore +4 -2
  60. package/dist/defaults/config/certs/README.md +25 -45
  61. package/dist/defaults/config/omega.json5 +66 -32
  62. package/dist/defaults/hooks/deploy/pre.js +10 -0
  63. package/dist/defaults/src/assets/scss/main.scss +12 -2
  64. package/dist/defaults/src/integrations/tray/index.js +1 -1
  65. package/dist/gulp/main.js +13 -25
  66. package/dist/gulp/tasks/audit.js +18 -6
  67. package/dist/gulp/tasks/build-config.js +181 -60
  68. package/dist/gulp/tasks/bundle.js +108 -36
  69. package/dist/gulp/tasks/release.js +86 -4
  70. package/dist/gulp/tasks/sass.js +11 -0
  71. package/dist/hooks/lib/notarize-tools.js +137 -0
  72. package/dist/hooks/notarize-artifacts.js +57 -0
  73. package/dist/hooks/notarize.js +59 -8
  74. package/dist/lib/auth-persistence.js +25 -0
  75. package/dist/lib/client-bridge.js +11 -8
  76. package/dist/lib/deep-link.js +15 -8
  77. package/dist/lib/protocol.js +7 -1
  78. package/dist/lib/restart-manager/install.js +6 -3
  79. package/dist/lib/sign-helpers/auto-unlock.js +65 -27
  80. package/dist/lib/sign-helpers/console-lock.js +34 -0
  81. package/dist/lib/sign-helpers/exec-with-limit.js +68 -0
  82. package/dist/lib/sign-helpers/resolve-icons.js +6 -4
  83. package/dist/lib/tray.js +7 -6
  84. package/dist/main.js +16 -0
  85. package/dist/preload.js +8 -0
  86. package/dist/renderer.js +16 -4
  87. package/dist/runner/job-started.js +104 -0
  88. package/dist/test/fixtures/consumer-app/config/omega.json5 +7 -0
  89. package/dist/test/fixtures/consumer-app/package.json +1 -1
  90. package/dist/test/fixtures/consumer-app/src/assets/js/components/main/index.js +7 -1
  91. package/dist/test/harness/main-entry.js +2 -1
  92. package/dist/test/harness/renderer-preload.js +12 -3
  93. package/dist/test/runners/boot.js +101 -11
  94. package/dist/test/runners/electron.js +1 -1
  95. package/dist/test/suites/boot/consumer-app-boots.test.js +54 -0
  96. package/dist/test/suites/build/audit.test.js +45 -6
  97. package/dist/test/suites/build/auth-persistence-resolve.test.js +119 -0
  98. package/dist/test/suites/build/auto-unlock.test.js +105 -0
  99. package/dist/test/suites/build/boot-runner-timeout.test.js +253 -0
  100. package/dist/test/suites/build/brand-scss.test.js +106 -0
  101. package/dist/test/suites/build/build-config.test.js +146 -36
  102. package/dist/test/suites/build/build-json-bake.test.js +245 -0
  103. package/dist/test/suites/build/build-verbs.test.js +48 -15
  104. package/dist/test/suites/build/build-workflow-jobs.test.js +190 -0
  105. package/dist/test/suites/build/cli.test.js +32 -10
  106. package/dist/test/suites/build/config-schema.test.js +4 -4
  107. package/dist/test/suites/build/console-lock.test.js +51 -0
  108. package/dist/test/suites/build/defaults-scaffold.test.js +71 -2
  109. package/dist/test/suites/build/deploy-direct.test.js +241 -0
  110. package/dist/test/suites/build/deploy-dispatch.test.js +287 -0
  111. package/dist/test/suites/build/deploy-hook.test.js +169 -0
  112. package/dist/test/suites/build/ensure-target.test.js +14 -2
  113. package/dist/test/suites/build/env-delivery.test.js +19 -10
  114. package/dist/test/suites/build/env-watch.test.js +18 -7
  115. package/dist/test/suites/build/esm-only-dependency.test.js +127 -0
  116. package/dist/test/suites/build/exec-with-limit.test.js +53 -0
  117. package/dist/test/suites/build/finalize-release.test.js +2 -2
  118. package/dist/test/suites/build/get-config.test.js +120 -8
  119. package/dist/test/suites/build/github-utils.test.js +12 -6
  120. package/dist/test/suites/build/license-stamp.test.js +6 -4
  121. package/dist/test/suites/build/manager.test.js +88 -68
  122. package/dist/test/suites/build/manifest-deps.test.js +116 -0
  123. package/dist/test/suites/build/merge-line-files.test.js +27 -4
  124. package/dist/test/suites/build/notarize-artifacts.test.js +135 -0
  125. package/dist/test/suites/build/notarize-tools.test.js +38 -0
  126. package/dist/test/suites/build/notarize.test.js +207 -0
  127. package/dist/test/suites/build/release-pipeline.test.js +38 -60
  128. package/dist/test/suites/build/release-skipped-upload.test.js +107 -0
  129. package/dist/test/suites/build/resolve-icons.test.js +35 -35
  130. package/dist/test/suites/build/runner-job-guard.test.js +182 -0
  131. package/dist/test/suites/build/runner.test.js +25 -2
  132. package/dist/test/suites/build/sentry.test.js +8 -3
  133. package/dist/test/suites/build/setup-scripts.test.js +3 -0
  134. package/dist/test/suites/build/sign-windows.test.js +92 -8
  135. package/dist/test/suites/build/test-stealth.test.js +17 -11
  136. package/dist/test/suites/build/url-helpers.test.js +37 -17
  137. package/dist/test/suites/build/validate-certs.test.js +428 -55
  138. package/dist/test/suites/build/validate-config.test.js +3 -3
  139. package/dist/test/suites/main/auth-flow.test.js +12 -0
  140. package/dist/test/suites/main/auth-persistence.test.js +14 -17
  141. package/dist/test/suites/main/auto-updater.test.js +2 -2
  142. package/dist/test/suites/main/boot-sequence.test.js +1 -1
  143. package/dist/test/suites/main/client-bridge.integration.test.js +5 -82
  144. package/dist/test/suites/main/client-bridge.test.js +19 -6
  145. package/dist/test/suites/main/deep-link.test.js +54 -0
  146. package/dist/test/suites/main/startup-paths-and-ua.test.js +1 -1
  147. package/dist/test/suites/main/url-helpers.test.js +81 -72
  148. package/dist/test/suites/renderer/cross-context-helpers.test.js +19 -16
  149. package/dist/utils/build-pipeline.js +10 -10
  150. package/dist/utils/github.js +12 -51
  151. package/dist/utils/load-env.js +66 -0
  152. package/dist/utils/mode-helpers.js +43 -111
  153. package/dist/utils/platform.js +37 -0
  154. package/dist/utils/runner-env.js +2 -1
  155. package/dist/utils/runner-job-guard.js +149 -0
  156. package/dist/utils/ship-keys.js +52 -0
  157. package/dist/utils/test-stealth.js +5 -3
  158. package/dist/utils/url-helpers.js +33 -17
  159. package/dist/vendor/config/bundle-id.js +53 -0
  160. package/dist/vendor/config/client-config.js +141 -0
  161. package/dist/vendor/config/company.js +334 -15
  162. package/dist/vendor/config/dev-facts.js +48 -0
  163. package/dist/vendor/config/env-delivery.js +231 -9
  164. package/dist/vendor/config/env-retired.js +137 -0
  165. package/dist/vendor/config/env-rules.js +22 -3
  166. package/dist/vendor/config/env-schema.js +234 -117
  167. package/dist/vendor/config/env.js +55 -26
  168. package/dist/vendor/config/environment.js +189 -0
  169. package/dist/vendor/config/hooks.js +13 -11
  170. package/dist/vendor/config/index.js +121 -44
  171. package/dist/vendor/config/load.js +366 -115
  172. package/dist/vendor/config/merge.js +2 -2
  173. package/dist/vendor/config/order.js +3 -3
  174. package/dist/vendor/config/platforms.js +276 -0
  175. package/dist/vendor/config/repo.js +226 -104
  176. package/dist/vendor/config/retired-keys.js +232 -27
  177. package/dist/vendor/config/schema.js +525 -99
  178. package/dist/vendor/config/site-global.js +63 -53
  179. package/dist/vendor/config/targets.js +187 -0
  180. package/dist/vendor/config/validate.js +119 -59
  181. package/dist/vendor/devkit/argv.js +118 -0
  182. package/dist/vendor/devkit/attach-log-file.js +21 -13
  183. package/dist/vendor/devkit/brand-tokens.js +278 -0
  184. package/dist/vendor/devkit/brand-version.js +264 -0
  185. package/dist/vendor/devkit/build-json.js +91 -0
  186. package/dist/vendor/devkit/bundle.js +48 -0
  187. package/dist/vendor/devkit/certificate-expiry.js +108 -0
  188. package/dist/vendor/devkit/certs.js +16 -194
  189. package/dist/vendor/devkit/ci-workflows.js +124 -8
  190. package/dist/vendor/devkit/cli-router.js +3 -3
  191. package/dist/vendor/devkit/defaults-engine.js +69 -7
  192. package/dist/vendor/devkit/deploy-follow.js +297 -0
  193. package/dist/vendor/devkit/deploy-precheck.js +23 -8
  194. package/dist/vendor/devkit/deploy-record.js +11 -27
  195. package/dist/vendor/devkit/deploy-snapshot.js +661 -0
  196. package/dist/vendor/devkit/deploy.js +445 -75
  197. package/dist/vendor/devkit/git-auth.js +73 -0
  198. package/dist/vendor/devkit/git-remote.js +95 -0
  199. package/dist/vendor/devkit/github-repo.js +290 -0
  200. package/dist/vendor/devkit/local.js +47 -0
  201. package/dist/vendor/devkit/merge-line-files.js +23 -16
  202. package/dist/vendor/devkit/omega-bin.js +18 -3
  203. package/dist/vendor/devkit/pack-local.js +391 -0
  204. package/dist/vendor/devkit/preludes/index.js +120 -0
  205. package/dist/vendor/devkit/preludes/origin-heal.js +156 -0
  206. package/dist/vendor/devkit/service-account.js +43 -0
  207. package/dist/vendor/devkit/ship-plan.js +112 -0
  208. package/dist/vendor/devkit/signing-env.js +180 -0
  209. package/dist/vendor/devkit/signing-tree.js +92 -0
  210. package/dist/vendor/devkit/target-seams.js +142 -0
  211. package/dist/vendor/devkit/target-secrets.js +235 -50
  212. package/dist/vendor/devkit/test/esm-only-fixture.js +48 -0
  213. package/dist/vendor/devkit/test/fixtures/esm-only-package/browser.js +19 -0
  214. package/dist/vendor/devkit/test/fixtures/esm-only-package/index.js +20 -0
  215. package/dist/vendor/devkit/test/fixtures/esm-only-package/package.json +13 -0
  216. package/dist/vendor/monitoring/env.js +20 -10
  217. package/dist/vendor/monitoring/main.js +1 -1
  218. package/dist/vendor/monitoring/preload.js +1 -1
  219. package/dist/vendor/monitoring/renderer.js +1 -1
  220. package/docs/analytics.md +1 -1
  221. package/docs/auto-updater.md +5 -5
  222. package/docs/boot-sequence.md +1 -1
  223. package/docs/build-system.md +15 -7
  224. package/docs/client-bridge.md +9 -7
  225. package/docs/config-schema.md +4 -4
  226. package/docs/css.md +8 -2
  227. package/docs/deep-link.md +12 -4
  228. package/docs/environment-detection.md +32 -24
  229. package/docs/hooks.md +3 -1
  230. package/docs/icons.md +7 -7
  231. package/docs/index.md +61 -23
  232. package/docs/installer-options.md +24 -21
  233. package/docs/logging.md +5 -5
  234. package/docs/releasing.md +25 -17
  235. package/docs/runner.md +40 -5
  236. package/docs/shared/brands.md +12 -6
  237. package/docs/shared/breaking-changes.md +375 -21
  238. package/docs/shared/config.md +757 -199
  239. package/docs/shared/deploys.md +194 -91
  240. package/docs/shared/icons.md +18 -0
  241. package/docs/shared/local-dev.md +24 -6
  242. package/docs/shared/logging.md +9 -6
  243. package/docs/shared/monitoring.md +27 -13
  244. package/docs/shared/publishing.md +19 -15
  245. package/docs/shared/rulings.md +2 -2
  246. package/docs/shared/testing.md +1 -1
  247. package/docs/shared/theming.md +26 -1
  248. package/docs/shared/translation.md +49 -7
  249. package/docs/shared/updates.md +3 -3
  250. package/docs/signing.md +59 -33
  251. package/docs/test-framework.md +10 -5
  252. package/docs/themes.md +15 -1
  253. package/package.json +18 -12
  254. package/bin/omega-desktop +0 -2
  255. package/dist/commands/push-secrets.js +0 -141
  256. package/dist/test/suites/build/deliver-certs.test.js +0 -95
  257. package/dist/test/suites/build/derive-signing-env.test.js +0 -122
  258. package/dist/test/suites/build/push-secrets.test.js +0 -226
  259. package/dist/test/suites/build/resolve-signing-cert.test.js +0 -342
  260. package/dist/utils/deliver-certs.js +0 -69
  261. package/dist/utils/derive-signing-env.js +0 -56
  262. package/dist/utils/resolve-signing-cert.js +0 -175
  263. package/dist/vendor/config/desktop-artifacts.js +0 -110
  264. package/dist/vendor/config/instances.js +0 -208
  265. /package/dist/defaults/config/icons/{macos → mac}/dmg.png +0 -0
  266. /package/dist/defaults/config/icons/{macos → mac}/icon.png +0 -0
  267. /package/dist/defaults/config/icons/{macos → mac}/tray.png +0 -0
@@ -9,17 +9,32 @@
9
9
  // APPLE_API_KEY_ID — 10-char Key ID (matches the XXXXXXXXXX in the filename)
10
10
  // APPLE_API_ISSUER — issuer UUID from App Store Connect → Users and Access → Keys
11
11
  //
12
- // Skipped automatically when:
13
- // - building for a non-darwin platform
14
- // - any of the env vars above is missing (warns, doesn't fail — useful for dev builds without certs)
12
+ // Skipped ONLY when building for a non-darwin platform. Every other state is a
13
+ // THROW ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)): a mac
14
+ // build that reaches this hook is a mac build this framework ships, and a build
15
+ // that cannot be signed and notarized must not become a release.
16
+ // - missing APPLE_API_KEY / _ID / _ISSUER: nothing can notarize it
17
+ // - no Developer ID signature (#872): electron-builder SKIPS signing when it
18
+ // finds no identity and leaves an ad-hoc signature behind, which notarytool
19
+ // rejects ("Failed to codesign your application"). That state used to warn
20
+ // and carry on, and the release published an app Gatekeeper refuses on every
21
+ // other Mac.
22
+ //
23
+ // After notarization the ticket is STAPLED and the result PROVED (stapler
24
+ // validate + spctl --assess): the hook returns only when the .app on disk is
25
+ // one a user's Mac will open offline. The DMG gets the same treatment in its
26
+ // own hook (notarize-artifacts.js, artifactBuildCompleted), because the ticket
27
+ // stapled to the app inside an image is not stapled to the image.
15
28
 
16
29
  const path = require('path');
17
30
  const fs = require('fs');
31
+ const { execute } = require('node-powertools');
18
32
  const Logger = require('../lib/logger');
33
+ const { stapleAndProve, toolRunner } = require('./lib/notarize-tools.js');
19
34
 
20
35
  const logger = new Logger('notarize');
21
36
 
22
- module.exports = async function notarize(context) {
37
+ module.exports = async function notarize(context, { run = toolRunner() } = {}) {
23
38
  const { electronPlatformName, appOutDir } = context;
24
39
 
25
40
  if (electronPlatformName !== 'darwin') {
@@ -31,8 +46,7 @@ module.exports = async function notarize(context) {
31
46
  const appleApiIssuer = process.env.APPLE_API_ISSUER;
32
47
 
33
48
  if (!appleApiKey || !appleApiKeyId || !appleApiIssuer) {
34
- logger.warn('Skipping — set APPLE_API_KEY, APPLE_API_KEY_ID, APPLE_API_ISSUER to notarize.');
35
- return;
49
+ throw new Error('[notarize] APPLE_API_KEY, APPLE_API_KEY_ID and APPLE_API_ISSUER are required to notarize a mac build. Run `omega deploy` (its precheck pushes them for CI) or set them in the target .env.');
36
50
  }
37
51
 
38
52
  const apiKeyPath = path.isAbsolute(appleApiKey)
@@ -43,10 +57,18 @@ module.exports = async function notarize(context) {
43
57
  throw new Error(`[notarize] APPLE_API_KEY file not found at ${apiKeyPath}`);
44
58
  }
45
59
 
46
- const { notarize } = require('@electron/notarize');
47
60
  const appName = context.packager.appInfo.productFilename;
48
61
  const appPath = path.join(appOutDir, `${appName}.app`);
49
62
 
63
+ // Notarization verifies a Developer ID signature; an ad-hoc one (what
64
+ // electron-builder leaves when it finds no identity) fails the submission
65
+ // instead ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)).
66
+ if (!isDeveloperIdSigned(await readCodesign(appPath, run))) {
67
+ throw new Error(`[notarize] ${appName}.app is NOT Developer ID signed, so it cannot be notarized and every other Mac would refuse it. Set CSC_LINK + CSC_KEY_PASSWORD (or install a Developer ID Application identity) and build again.`);
68
+ }
69
+
70
+ const { notarize } = require('@electron/notarize');
71
+
50
72
  logger.log(`Notarizing ${appName} via App Store Connect API key (${appleApiKeyId})...`);
51
73
  const start = Date.now();
52
74
 
@@ -59,7 +81,12 @@ module.exports = async function notarize(context) {
59
81
  });
60
82
 
61
83
  const duration = Math.round((Date.now() - start) / 1000);
62
- logger.log(`Done in ${duration}s.`);
84
+ logger.log(`Notarized in ${duration}s.`);
85
+
86
+ // The ticket, stapled and PROVED: a notarized app whose ticket never landed
87
+ // still fails on a Mac that is offline when the user opens it (#891).
88
+ await stapleAndProve({ filePath: appPath, kind: 'app', run });
89
+ logger.log(`Stapled and verified: stapler validate + spctl --assess both accept ${appName}.app.`);
63
90
 
64
91
  // After @omega.js/desktop's real notarization, optionally invoke the consumer's hooks/notarize/post.js as
65
92
  // an extension point. The consumer hook can do post-notarize work (custom stapling,
@@ -68,3 +95,27 @@ module.exports = async function notarize(context) {
68
95
  const runConsumerHook = require('../utils/run-consumer-hook.js');
69
96
  await runConsumerHook('notarize/post', context);
70
97
  };
98
+
99
+ // Pure: does codesign's own report describe a REAL Developer ID signature? An
100
+ // unsigned app has no report at all and an ad-hoc one ("Signature=adhoc",
101
+ // "TeamIdentifier=not set") carries no Authority line, so the one line
102
+ // notarization actually depends on is the one that is read.
103
+ function isDeveloperIdSigned(codesignOutput) {
104
+ return /^Authority=Developer ID Application/m.test(String(codesignOutput || ''));
105
+ }
106
+
107
+ // codesign writes its report to STDERR and exits non-zero for an unsigned app,
108
+ // so the report is folded into stdout and a failure is read as the answer it is.
109
+ async function readCodesign(appPath, exec) {
110
+ const run = exec || ((cmd) => execute(cmd, { log: false }));
111
+
112
+ try {
113
+ return String(await run(`codesign -dv --verbose=2 "${appPath}" 2>&1`) || '');
114
+ } catch (e) {
115
+ return String(e.message || '');
116
+ }
117
+ }
118
+
119
+ // Exported for tests.
120
+ module.exports.isDeveloperIdSigned = isDeveloperIdSigned;
121
+ module.exports.readCodesign = readCodesign;
@@ -25,6 +25,10 @@
25
25
  // than "store it in a browser session".
26
26
  // 'none' — explicit opt-out; Firebase stays in-memory (pre-1.12 behavior).
27
27
  //
28
+ // A TEST RUN is always 'none', whatever the config says: the test lanes never sign a
29
+ // real user in, so the harness never touches the OS keychain. Both lanes, on the one
30
+ // signal isTestRun() reads below ([#907](https://github.com/Omega-JS-Stack/omega/issues/907)).
31
+ //
28
32
  // Select via config `omega.authPersistence` ('safeStorage' | 'none' | a custom
29
33
  // registered name). Register custom strategies BEFORE manager.initialize():
30
34
  // require('@omega.js/desktop/lib/auth-persistence').register('keytar', {...})
@@ -110,6 +114,15 @@ const noneStrategy = {
110
114
  async removeItem() {},
111
115
  };
112
116
 
117
+ // Is this process a TEST RUN, in either lane that reaches this lib? The manager
118
+ // answers it, in both: each lane spawns its child with `OMEGA_ENVIRONMENT=testing`
119
+ // and the baked word never writes over it
120
+ // ([#925](https://github.com/Omega-JS-Stack/omega/issues/925)), so a booted
121
+ // production artifact reports the lane it is running in too.
122
+ function isTestRun(manager) {
123
+ return !!manager?.isTesting?.();
124
+ }
125
+
113
126
  const authPersistence = {
114
127
  _initialized: false,
115
128
  _manager: null,
@@ -140,6 +153,18 @@ const authPersistence = {
140
153
  // Pick + availability-check the configured strategy. Returns the active strategy
141
154
  // or null (→ caller falls back to firebase's in-memory default).
142
155
  async resolve(manager) {
156
+ // A TEST RUN is `none`, before any config is read: the test lanes never sign a
157
+ // real user in, so the OS keychain is never asked. This overrides an explicit
158
+ // consumer value too, because the boot layer boots the brand's REAL config through
159
+ // the staged test app, and an unsigned Electron binary asking macOS for a keychain
160
+ // item parks the whole run behind a SecurityAgent prompt
161
+ // ([#907](https://github.com/Omega-JS-Stack/omega/issues/907)).
162
+ if (isTestRun(manager)) {
163
+ logger.log('auth persistence: none (test mode)');
164
+ authPersistence._active = null;
165
+ return null;
166
+ }
167
+
143
168
  const wanted = manager.config?.omega?.authPersistence || 'safeStorage';
144
169
  const strategy = authPersistence._strategies[wanted];
145
170
 
@@ -18,7 +18,7 @@
18
18
 
19
19
  const LoggerLite = require('./logger-lite.js');
20
20
  const authPersistence = require('./auth-persistence.js');
21
- const { localPort } = require('../utils/url-helpers.js');
21
+ const { requiredPort } = require('../utils/url-helpers.js');
22
22
 
23
23
  const logger = new LoggerLite('client-bridge');
24
24
 
@@ -117,8 +117,8 @@ const bridge = {
117
117
 
118
118
  // A TESTING run talks to the LOCAL stack, never real auth — the same move
119
119
  // getApiUrl() makes when it maps testing to localhost, and the same one
120
- // @omega.js/extension's background SW makes for its emulator runs. Only
121
- // OMEGA_TEST_MODE=true reaches here; dev and production are untouched.
120
+ // @omega.js/extension's background SW makes for its emulator runs. Only an
121
+ // OMEGA_ENVIRONMENT=testing run reaches here; dev and production are untouched.
122
122
  if (bridge._manager.isTesting()) {
123
123
  const { connectAuthEmulator } = bridge._firebaseModule;
124
124
  const port = bridge._authEmulatorPort();
@@ -129,12 +129,15 @@ const bridge = {
129
129
  return bridge._firebaseAuth;
130
130
  },
131
131
 
132
- // The auth emulator's port, on the same three-step chain getApiUrl() walks:
133
- // the resolved-port env channel (N7), then the `dev.ports` map the bundle
134
- // baked into OMEGA_BUILD_JSON, then the classic 9099
135
- // ([#745](https://github.com/Omega-JS-Stack/omega/issues/745)).
132
+ // The auth emulator's port, on the same chain getApiUrl() walks: the
133
+ // resolved-port env channel (N7), then the `dev.ports` map the bundle baked
134
+ // into OMEGA_BUILD_JSON
135
+ // ([#745](https://github.com/Omega-JS-Stack/omega/issues/745)). The classic
136
+ // 9099 used to sit under those two as a last resort; it is gone (#834),
137
+ // because nothing identity-checks what answers on it and a neighbouring
138
+ // project's emulator reads as an auth mystery rather than a port problem.
136
139
  _authEmulatorPort() {
137
- return localPort(bridge._manager, 'OMEGA_AUTH_PORT', 'auth') || 9099;
140
+ return requiredPort(bridge._manager, 'OMEGA_AUTH_PORT', 'auth');
138
141
  },
139
142
 
140
143
  _registerIpc() {
@@ -3,8 +3,9 @@
3
3
  // Cross-platform plumbing handled internally:
4
4
  // - macOS: the OS routes `<scheme>://...` URLs via `app.on('open-url')`. Cold-start opens may queue
5
5
  // them before whenReady; we drain them after.
6
- // - Windows/Linux: deep links arrive as the LAST entry in `process.argv` on cold-start, and as the
7
- // `argv` arg of `app.on('second-instance')` on warm-start.
6
+ // - Windows/Linux: deep links arrive as the LAST entry in `process.argv` on cold-start, and on
7
+ // warm-start via `app.on('second-instance')` (read from the `additionalData` argument,
8
+ // which carries the duplicate's real argv; the event's own `argv` is Chromium-processed).
8
9
  //
9
10
  // Public API on manager.deepLink:
10
11
  //
@@ -23,8 +24,8 @@
23
24
  // ctx.params // { id: '42' }
24
25
  // ctx.query // { ref: 'abc' }
25
26
  // ctx.source // 'cold-start' | 'warm-start' | 'manual'
26
- // ctx.argv // process.argv (cold) or second-instance argv
27
- // ctx.cwd // process.cwd() or second-instance cwd
27
+ // ctx.argv // process.argv (cold) or the duplicate's real argv (warm, via additionalData)
28
+ // ctx.cwd // process.cwd() or the duplicate's cwd (warm)
28
29
  // ctx.handled // mutate: set true to suppress fall-through to '*' catch-all
29
30
  // });
30
31
  //
@@ -117,8 +118,14 @@ const deepLink = {
117
118
  // Windows/Linux warm-start: another instance launched (e.g. user double-clicked the
118
119
  // app icon while it's already running, or a deep link arrived). The OS gave us the
119
120
  // lock, killed the duplicate, and forwarded its argv here.
120
- app.on('second-instance', (_event, argv, cwd) => {
121
- logger.log(`second-instance — argv=${JSON.stringify(argv)} cwd=${cwd}`);
121
+ app.on('second-instance', (_event, argv, cwd, additionalData) => {
122
+ // The event's own argv is Chromium-processed (switches first, the values detached
123
+ // at the end), so the duplicate's REAL command line rides along in the object it
124
+ // passed to requestSingleInstanceLock (#921). The fallback covers a duplicate
125
+ // started by a build from before that object existed.
126
+ const realArgv = Array.isArray(additionalData?.argv) ? additionalData.argv : argv;
127
+ const realCwd = additionalData?.cwd || cwd;
128
+ logger.log(`second-instance argv=${JSON.stringify(realArgv)} eventArgv=${JSON.stringify(argv)} cwd=${realCwd}`);
122
129
 
123
130
  // Surface the main window. With hidden-mode apps, the consumer typically created
124
131
  // `main` with `show: false` at boot, so it's in the registry but invisible — we
@@ -134,9 +141,9 @@ const deepLink = {
134
141
  logger.log('second-instance — no main window in registry; ignoring');
135
142
  }
136
143
 
137
- const url = deepLink._extractUrlFromArgv(argv);
144
+ const url = deepLink._extractUrlFromArgv(realArgv);
138
145
  if (url) {
139
- deepLink._handle(url, 'warm-start', { argv, cwd });
146
+ deepLink._handle(url, 'warm-start', { argv: realArgv, cwd: realCwd });
140
147
  }
141
148
  });
142
149
  },
@@ -41,7 +41,13 @@ const protocol = {
41
41
  // initialize() and quits the duplicate WITHOUT running the rest of the boot
42
42
  // (two live instances would fight over storage, servers, and — for agents like
43
43
  // restart-manager — shared runtime files).
44
- protocol._hasLock = app.requestSingleInstanceLock();
44
+ //
45
+ // The object rides along because the event's OWN argv is Chromium-processed
46
+ // (switches first, Chromium's own switches spliced in, the values detached at
47
+ // the end), so a brand parsing it reads its flags empty (#921). `additionalData`
48
+ // arrives untouched as the fourth argument of second-instance, so every brand
49
+ // gets the duplicate's real command line, in order, for free.
50
+ protocol._hasLock = app.requestSingleInstanceLock({ argv: process.argv, cwd: process.cwd() });
45
51
  if (!protocol._hasLock) {
46
52
  logger.warn('single-instance lock lost — another copy of the app is running.');
47
53
  } else {
@@ -94,7 +94,8 @@ function parseFeed(ymlText) {
94
94
  /**
95
95
  * Pick the install artifact for a platform from a parsed feed.
96
96
  * darwin — zip, preferring `-<arch>-mac.zip` → `-universal-mac.zip` →
97
- * `-mac-universal.zip` (the versionless form, #620) → `-mac.zip`
97
+ * `-mac-universal.zip` (the #620 name) → `-mac.zip` (the name
98
+ * @omega.js/desktop packages the auto-update zip under since #867)
98
99
  * win32 — the NSIS setup .exe (run silently with /S; NSIS is what lets RM
99
100
  * self-update via electron-updater on Windows)
100
101
  * linux — the .AppImage entry
@@ -111,8 +112,10 @@ function pickArtifact(feed, platform, arch) {
111
112
  if (platform === 'darwin') {
112
113
  picked = names.find((n) => n.endsWith(`-${arch}-mac.zip`))
113
114
  || names.find((n) => n.endsWith('-universal-mac.zip'))
114
- // The name @omega.js/desktop packages under since #620 — a feed published
115
- // before it still answers on one of the forms above.
115
+ // A feed published before #867 still carries the arch-shaped name; the
116
+ // last form is what @omega.js/desktop packages the mac auto-update zip
117
+ // under now. This reads the RM app's OWN feed, so every published form
118
+ // stays answerable.
116
119
  || names.find((n) => n.endsWith('-mac-universal.zip'))
117
120
  || names.find((n) => n.endsWith('-mac.zip'));
118
121
  } else if (platform === 'win32') {
@@ -3,8 +3,11 @@
3
3
  //
4
4
  // Strategy: in parallel with the signtool invocation, poll the Win32 window
5
5
  // list (via automately.getWindows() — a single native call, ~1-5ms) looking
6
- // for a "Token Logon" window. When detected, focus it and type the password
7
- // + Enter via automately (a maintained nutjs fork).
6
+ // for a "Token Logon" window. When detected, type the password + Enter via
7
+ // automately (a maintained nutjs fork), then watch the dialog for a few seconds:
8
+ // a dialog that stays open after typing means the keystrokes did not reach it
9
+ // (a locked console does exactly that, [#864](https://github.com/Omega-JS-Stack/omega/issues/864)),
10
+ // and that is said out loud instead of leaving signtool's silence unexplained.
8
11
  //
9
12
  // Falls back to a no-op if:
10
13
  // - automately isn't installed (optional dep, may have failed to compile)
@@ -20,30 +23,44 @@ try {
20
23
  automately = null;
21
24
  }
22
25
 
23
- const POLL_INTERVAL_MS = 500; // automately.getWindows() is fast — poll twice a second
24
- const POLL_TIMEOUT_MS = 60000;
25
- const PRE_TYPE_DELAY_MS = 2000; // safety wait after detecting dialog before typing
26
- const PER_CHAR_DELAY_MS = 60; // small delay between keystrokes — some dialogs drop chars on fast input
26
+ const TIMINGS = {
27
+ pollIntervalMs: 500, // automately.getWindows() is fast — poll twice a second
28
+ pollTimeoutMs: 60000,
29
+ preTypeDelayMs: 2000, // safety wait after detecting dialog before typing
30
+ perCharDelayMs: 60, // small delay between keystrokes — some dialogs drop chars on fast input
31
+ confirmMs: 10000, // how long a typed-into dialog gets to close before that is reported
32
+ };
27
33
 
28
34
  function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
29
35
 
30
- async function findTokenLogonWindow() {
31
- const windows = await automately.getWindows();
32
- for (const w of windows) {
33
- let title = '';
34
- try { title = await w.title; } catch (e) { continue; }
35
- if (title && title.startsWith('Token Logon')) return title;
36
- }
37
- return null;
36
+ // The two things the helper does to the desktop, behind one seam so the loop
37
+ // above them is provable without a desktop.
38
+ function automatelyDriver() {
39
+ return {
40
+ findTokenLogonWindow: async () => {
41
+ const windows = await automately.getWindows();
42
+ for (const w of windows) {
43
+ let title = '';
44
+ try { title = await w.title; } catch (e) { continue; }
45
+ if (title && title.startsWith('Token Logon')) return title;
46
+ }
47
+ return null;
48
+ },
49
+ typeText: (text) => automately.keyboard.type(text),
50
+ pressEnter: () => automately.keyboard.type(automately.Key.Enter),
51
+ };
38
52
  }
39
53
 
40
- function startAutoUnlock({ password, logger }) {
41
- if (process.platform !== 'win32') return { stop: () => {} };
54
+ function startAutoUnlock({ password, logger, driver, timings, platform }) {
55
+ platform = platform || process.platform;
56
+ if (platform !== 'win32') return { stop: () => {} };
42
57
  if (!password) return { stop: () => {} };
43
- if (!automately) {
58
+ if (!driver && !automately) {
44
59
  if (logger) logger.warn('automately not installed — SafeNet password prompt will need manual entry. Run `npm install` in the @omega.js/desktop repo to enable auto-unlock.');
45
60
  return { stop: () => {} };
46
61
  }
62
+ driver = driver || automatelyDriver();
63
+ timings = { ...TIMINGS, ...(timings || {}) };
47
64
 
48
65
  let stopped = false;
49
66
  let typed = false;
@@ -51,42 +68,63 @@ function startAutoUnlock({ password, logger }) {
51
68
 
52
69
  (async function poll() {
53
70
  let attempt = 0;
54
- while (!stopped && !typed && Date.now() - startedAt < POLL_TIMEOUT_MS) {
71
+ while (!stopped && !typed && Date.now() - startedAt < timings.pollTimeoutMs) {
55
72
  attempt += 1;
56
73
  const elapsed = Math.round((Date.now() - startedAt) / 1000);
57
74
  const pollStart = Date.now();
58
75
  let title = null;
59
76
  try {
60
- title = await findTokenLogonWindow();
77
+ title = await driver.findTokenLogonWindow();
61
78
  } catch (e) {
62
79
  if (logger) logger.log(`auto-unlock: poll ${attempt} (${elapsed}s) — getWindows error: ${e.message}`);
63
80
  }
64
81
  const pollMs = Date.now() - pollStart;
65
82
 
66
83
  if (title) {
67
- if (logger) logger.log(`auto-unlock: SafeNet Token Logon dialog detected (attempt ${attempt}, ${elapsed}s, poll took ${pollMs}ms) — title: "${title}" — waiting ${PRE_TYPE_DELAY_MS}ms before typing...`);
68
- await sleep(PRE_TYPE_DELAY_MS);
84
+ if (logger) logger.log(`auto-unlock: SafeNet Token Logon dialog detected (attempt ${attempt}, ${elapsed}s, poll took ${pollMs}ms) — title: "${title}" — waiting ${timings.preTypeDelayMs}ms before typing...`);
85
+ await sleep(timings.preTypeDelayMs);
69
86
 
70
- if (logger) logger.log(`auto-unlock: typing password (${password.length} chars, ~${PER_CHAR_DELAY_MS}ms each)...`);
87
+ if (logger) logger.log(`auto-unlock: typing password (${password.length} chars, ~${timings.perCharDelayMs}ms each)...`);
71
88
  for (const ch of password) {
72
89
  if (stopped) return;
73
- await automately.keyboard.type(ch);
74
- await sleep(PER_CHAR_DELAY_MS);
90
+ await driver.typeText(ch);
91
+ await sleep(timings.perCharDelayMs);
75
92
  }
76
- await automately.keyboard.type(automately.Key.Enter);
93
+ await driver.pressEnter();
77
94
  typed = true;
78
95
  if (logger) logger.log('auto-unlock: password typed + Enter pressed.');
96
+ await confirmDialogClosed();
79
97
  return;
80
98
  }
81
99
 
82
100
  if (logger) logger.log(`auto-unlock: poll ${attempt} (${elapsed}s, took ${pollMs}ms) — no Token Logon window`);
83
- await sleep(POLL_INTERVAL_MS);
101
+ await sleep(timings.pollIntervalMs);
84
102
  }
85
- if (!typed && !stopped && logger) logger.warn(`auto-unlock: timed out after ${Math.round(POLL_TIMEOUT_MS / 1000)}s without seeing the Token Logon dialog`);
103
+ if (!typed && !stopped && logger) logger.warn(`auto-unlock: timed out after ${Math.round(timings.pollTimeoutMs / 1000)}s without seeing the Token Logon dialog`);
86
104
  })().catch((e) => {
87
105
  if (logger) logger.warn(`auto-unlock: poll error: ${e.message}`);
88
106
  });
89
107
 
108
+ // The dialog closing is the only proof the keystrokes landed in it.
109
+ async function confirmDialogClosed() {
110
+ const typedAt = Date.now();
111
+ while (!stopped && Date.now() - typedAt < timings.confirmMs) {
112
+ await sleep(timings.pollIntervalMs);
113
+ let title = null;
114
+ try {
115
+ title = await driver.findTokenLogonWindow();
116
+ } catch (e) {
117
+ if (logger) logger.log(`auto-unlock: confirm poll — getWindows error: ${e.message}`);
118
+ title = 'unknown';
119
+ }
120
+ if (!title) {
121
+ if (logger) logger.log('auto-unlock: Token Logon dialog closed.');
122
+ return;
123
+ }
124
+ }
125
+ if (!stopped && logger) logger.warn(`auto-unlock: the Token Logon dialog is still open ${Math.round(timings.confirmMs / 1000)}s after typing — the keystrokes did not reach it. Is the console session locked?`);
126
+ }
127
+
90
128
  return {
91
129
  stop: () => { stopped = true; },
92
130
  };
@@ -0,0 +1,34 @@
1
+ // Is the box's console session locked? The Token Logon dialog the SafeNet
2
+ // driver raises renders on the interactive desktop, and the auto-unlock helper
3
+ // types the PIN into whatever holds keyboard focus there. Behind a lock screen
4
+ // the dialog still exists (the window poll enumerates it) but every keystroke
5
+ // lands on the lock screen, so signtool waits for a PIN that never arrives
6
+ // ([#864](https://github.com/Omega-JS-Stack/omega/issues/864): a Windows Update
7
+ // restart auto-signed the box in and locked it, and the sign job hung six hours).
8
+ //
9
+ // Windows shows the lock screen through LogonUI.exe, which exits on unlock, so
10
+ // its presence in the process list IS the locked state.
11
+
12
+ const { execute } = require('node-powertools');
13
+
14
+ const TASKLIST_CMD = 'tasklist /fi "IMAGENAME eq LogonUI.exe" /fo csv /nh';
15
+
16
+ // Pure: read tasklist's CSV for the lock-screen process.
17
+ function isConsoleLocked(tasklistOutput) {
18
+ return /"LogonUI\.exe"/i.test(String(tasklistOutput || ''));
19
+ }
20
+
21
+ // Throw, naming the cause and the fix, when the console is locked. Off Windows
22
+ // there is no lock screen to check.
23
+ async function assertConsoleUnlocked(options) {
24
+ options = options || {};
25
+ const platform = options.platform || process.platform;
26
+ const exec = options.exec || ((cmd) => execute(cmd, { log: false }));
27
+ if (platform !== 'win32') return;
28
+
29
+ if (isConsoleLocked(await exec(TASKLIST_CMD))) {
30
+ throw new Error('The console session is locked (LogonUI.exe is running), so the PIN cannot reach the Token Logon dialog. Unlock the box (a Windows Update restart leaves it locked) and re-run.');
31
+ }
32
+ }
33
+
34
+ module.exports = { isConsoleLocked, assertConsoleUnlocked };
@@ -0,0 +1,68 @@
1
+ // Run a shell command with a hard time limit. node-powertools' `execute` is the
2
+ // runner every sign call already uses (shell, piped stdio, rejects with stderr),
3
+ // and it settles only on the child's `close` event. So the limit lives on a
4
+ // timer of its own: when it fires the attempt is rejected THEN, whatever the
5
+ // termination below manages, instead of holding the job until GitHub's six-hour
6
+ // default does it ([#864](https://github.com/Omega-JS-Stack/omega/issues/864)).
7
+ //
8
+ // `execute` spawns through a shell, so the program is the shell's child. On
9
+ // Windows `taskkill /T` takes the tree, the same call `runner.js` makes on a
10
+ // listener. Elsewhere the child is spawned detached, which puts the shell at
11
+ // the head of its own process group, and the negative pid signals the group.
12
+
13
+ const { spawnSync } = require('child_process');
14
+ const { execute } = require('node-powertools');
15
+
16
+ const logger = new (require('../../build.js'))().logger('exec-with-limit');
17
+
18
+ // A kill that failed is exactly the state the operator needs to see, so every
19
+ // failure is said, never swallowed.
20
+ function terminateTree(child, platform) {
21
+ if (platform === 'win32') {
22
+ const k = spawnSync('taskkill', ['/F', '/PID', String(child.pid), '/T'], { encoding: 'utf8' });
23
+ if (k.status !== 0) logger.warn(`taskkill /PID ${child.pid} /T exited ${k.status}: ${String(k.stderr || k.stdout || '').trim()}`);
24
+ return;
25
+ }
26
+ try {
27
+ process.kill(-child.pid, 'SIGKILL');
28
+ } catch (e) {
29
+ logger.warn(`kill of process group ${child.pid} failed: ${e.message}`);
30
+ }
31
+ }
32
+
33
+ // Resolves with the command's stdout; rejects with the command's own error, or
34
+ // with one naming the limit when the limit fired first.
35
+ function execWithLimit(cmd, options) {
36
+ options = options || {};
37
+ const { limitMs, label = 'command' } = options;
38
+ if (!(limitMs > 0)) throw new Error(`execWithLimit needs a positive limitMs (got ${limitMs})`);
39
+
40
+ const platform = process.platform;
41
+ let child;
42
+ let timedOut = false;
43
+
44
+ // `detached` on Windows would open a console window for the child; on POSIX
45
+ // it is the process group the kill above needs. `execute` runs the setup
46
+ // callback synchronously, so `child` is set before this returns.
47
+ const run = execute(cmd, { log: false, config: { detached: platform !== 'win32' } }, (c) => { child = c; });
48
+
49
+ return new Promise((resolve, reject) => {
50
+ const timer = setTimeout(() => {
51
+ timedOut = true;
52
+ reject(new Error(`${label} produced no verdict within ${Math.round(limitMs / 1000)}s and was terminated`));
53
+ terminateTree(child, platform);
54
+ }, limitMs);
55
+
56
+ run.then(
57
+ (output) => { clearTimeout(timer); resolve(output); },
58
+ (e) => {
59
+ clearTimeout(timer);
60
+ // After the limit fired, the child's own verdict is the termination we
61
+ // asked for; the rejection above already said what happened.
62
+ if (!timedOut) reject(e);
63
+ },
64
+ );
65
+ });
66
+ }
67
+
68
+ module.exports = { execWithLimit };
@@ -14,7 +14,7 @@
14
14
  // Retina (@2x) variants are DERIVED. Consumers ship ONE file at the @2x (native) size.
15
15
  // @omega.js/desktop downscales it to write the @1x sibling. So `tray.png` is the 32x32 native retina
16
16
  // source (consumer-facing input name), and @omega.js/desktop emits both `trayTemplate.png` (16x16)
17
- // and `trayTemplate@2x.png` (32x32) into `dist/config/icons/macos/`. The output name
17
+ // and `trayTemplate@2x.png` (32x32) into `dist/config/icons/mac/`. The output name
18
18
  // diverges from the input on macOS because the `Template` suffix is a system magic
19
19
  // marker that triggers auto-inversion in dark mode. Same retina derivation for
20
20
  // `dmg.png` (1080x760 native → emits 540x380 + 1080x760, no name change).
@@ -28,7 +28,7 @@ const jetpack = require('fs-jetpack');
28
28
  const sharp = require('sharp');
29
29
 
30
30
  // Slot defs: per platform, a list of { slot, file, outFile, retina } entries.
31
- // slot = key under resolved[platform] (e.g. 'app' → resolved.macos.app)
31
+ // slot = key under resolved[platform] (e.g. 'app' → resolved.mac.app)
32
32
  // file = INPUT filename — what consumers ship in config/icons/<platform>/ (or global/)
33
33
  // outFile = OUTPUT filename written into dist/config/icons/<platform>/.
34
34
  // Defaults to `file`. Diverges only when the on-disk runtime name
@@ -37,8 +37,10 @@ const sharp = require('sharp');
37
37
  // @omega.js/desktop owns that detail so consumers can just call it `tray.png`).
38
38
  // retina = if set, the SOURCE file is treated as @2x (native). @omega.js/desktop downscales it
39
39
  // to produce the @1x sibling. Both files are written into dist/.
40
+ // The platform keys are OMEGA's vocabulary (#867): `mac`, never `macos`, both
41
+ // as the consumer's `config/icons/<platform>/` dir and as the dist output dir.
40
42
  const SLOTS = {
41
- macos: [
43
+ mac: [
42
44
  { slot: 'app', file: 'icon.png' },
43
45
  { slot: 'tray', file: 'tray.png', outFile: 'trayTemplate.png', retina: true },
44
46
  { slot: 'dmg', file: 'dmg.png', retina: true },
@@ -123,7 +125,7 @@ async function resolveAndCopy({ config, projectRoot, distRoot, emDefaultsRoot })
123
125
  const distIconsRoot = path.join(distRoot, 'config', 'icons');
124
126
  jetpack.dir(distIconsRoot);
125
127
 
126
- const resolved = { macos: {}, windows: {}, linux: {} };
128
+ const resolved = { mac: {}, windows: {}, linux: {} };
127
129
 
128
130
  // First pass: resolve each platform/slot. For retina slots, source file is treated as
129
131
  // @2x; @omega.js/desktop emits both <slot>.png (downscaled) and <slot>@2x.png (the source) into dist.
package/dist/lib/tray.js CHANGED
@@ -166,15 +166,16 @@ const tray = {
166
166
  //
167
167
  // Returns null if nothing is found (caller logs).
168
168
  _defaultIconPath() {
169
- const platform = process.platform === 'darwin' ? 'macos'
170
- : process.platform === 'win32' ? 'windows'
171
- : 'linux';
169
+ // The icon dirs are OMEGA's vocabulary (mac/windows/linux), so the OS is
170
+ // read through the one translation point (#867). Electron runs nowhere else,
171
+ // so an unknown OS falling back to linux is the safe read.
172
+ const platform = require('../utils/platform.js').desktopPlatform() || 'linux';
172
173
  // INPUT name (what consumers ship in config/icons/<platform>/ or global/) is always tray.png.
173
- // OUTPUT name (what gulp/build-config writes into dist/config/icons/macos/) is
174
- // trayTemplate.png on mac — the `Template` suffix is a macOS magic marker that
174
+ // OUTPUT name (what gulp/build-config writes into dist/config/icons/mac/) is
175
+ // trayTemplate.png on mac: the `Template` suffix is a macOS magic marker that
175
176
  // makes the OS auto-invert the icon in dark mode.
176
177
  const inFile = 'tray.png';
177
- const outFile = platform === 'macos' ? 'trayTemplate.png' : 'tray.png';
178
+ const outFile = platform === 'mac' ? 'trayTemplate.png' : 'tray.png';
178
179
  const fallbackFile = 'icon.png'; // tray → app icon if absent
179
180
 
180
181
  const projectRoot = require('../utils/app-root.js')();
package/dist/main.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // Boot sequence below — each step delegates to a `lib/*.js` module. Stubs today, real impls land in pass 2.
4
4
 
5
5
  const LoggerLite = require('./lib/logger-lite.js');
6
+ const { setEnvironment, ENVIRONMENT_VAR } = require('./vendor/config/environment.js');
6
7
 
7
8
  const storage = require('./lib/storage.js');
8
9
  const theme = require('./lib/theme.js');
@@ -146,6 +147,21 @@ Manager.prototype.initialize = async function (consumerConfig, options) {
146
147
  self.config = consumerConfig || {};
147
148
  self._options = options || {};
148
149
 
150
+ // The environment's ONE input is the `OMEGA_ENVIRONMENT` variable
151
+ // ([#817](https://github.com/Omega-JS-Stack/omega/issues/817)), and a lane
152
+ // that named one is NEVER overridden here
153
+ // ([#925](https://github.com/Omega-JS-Stack/omega/issues/925)). The baked word
154
+ // is the FALLBACK, for the one case with no parent lane to inherit from: a
155
+ // packaged app, where this is the input every later read answers (the
156
+ // context-free ones, like test-stealth's, included). A dev boot inherits the
157
+ // word from the gulp lane that spawned this electron, and the test lanes spawn
158
+ // their child with `testing` while booting a PRODUCTION artifact, so writing
159
+ // the baked word over theirs turned every isTesting() gate in a booted app
160
+ // silently off.
161
+ if (!process.env[ENVIRONMENT_VAR] && self.config.environment) {
162
+ setEnvironment(self.config.environment);
163
+ }
164
+
149
165
  {
150
166
  const fs = require('fs');
151
167
  const _path = require('path');