@omega.js/desktop 0.50.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 +3 -3
  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 +1 -1
  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
@@ -43,15 +43,15 @@ module.exports = defineCases({
43
43
  run: async (ctx) => {
44
44
  const { root, projectRoot, distRoot, emDefaultsRoot } = stage();
45
45
  try {
46
- fakePng(path.join(emDefaultsRoot, 'icons', 'macos', 'icon.png'));
46
+ fakePng(path.join(emDefaultsRoot, 'icons', 'mac', 'icon.png'));
47
47
  fakePng(path.join(emDefaultsRoot, 'icons', 'windows', 'icon.png'));
48
48
 
49
49
  const { resolveAndCopy } = require(path.join(__dirname, '..', '..', '..', 'lib', 'sign-helpers', 'resolve-icons.js'));
50
50
  const out = await resolveAndCopy({ config: {}, projectRoot, distRoot, emDefaultsRoot });
51
51
 
52
- ctx.expect(out.macos.app).toBe(path.join(distRoot, 'config', 'icons', 'macos', 'icon.png'));
52
+ ctx.expect(out.mac.app).toBe(path.join(distRoot, 'config', 'icons', 'mac', 'icon.png'));
53
53
  ctx.expect(out.windows.app).toBe(path.join(distRoot, 'config', 'icons', 'windows', 'icon.png'));
54
- ctx.expect(jetpack.exists(out.macos.app)).toBeTruthy();
54
+ ctx.expect(jetpack.exists(out.mac.app)).toBeTruthy();
55
55
  ctx.expect(jetpack.exists(out.windows.app)).toBeTruthy();
56
56
  } finally {
57
57
  fs.rmSync(root, { recursive: true, force: true });
@@ -63,15 +63,15 @@ module.exports = defineCases({
63
63
  run: async (ctx) => {
64
64
  const { root, projectRoot, distRoot, emDefaultsRoot } = stage();
65
65
  try {
66
- fakePng(path.join(emDefaultsRoot, 'icons', 'macos', 'icon.png'));
67
- const consumerIcon = path.join(projectRoot, 'config', 'icons', 'macos', 'icon.png');
66
+ fakePng(path.join(emDefaultsRoot, 'icons', 'mac', 'icon.png'));
67
+ const consumerIcon = path.join(projectRoot, 'config', 'icons', 'mac', 'icon.png');
68
68
  fakePng(consumerIcon);
69
69
  fs.writeFileSync(consumerIcon, Buffer.from('consumer-bytes'));
70
70
 
71
71
  const { resolveAndCopy } = require(path.join(__dirname, '..', '..', '..', 'lib', 'sign-helpers', 'resolve-icons.js'));
72
72
  const out = await resolveAndCopy({ config: {}, projectRoot, distRoot, emDefaultsRoot });
73
73
 
74
- const bytes = fs.readFileSync(out.macos.app);
74
+ const bytes = fs.readFileSync(out.mac.app);
75
75
  ctx.expect(bytes.toString()).toBe('consumer-bytes');
76
76
  } finally {
77
77
  fs.rmSync(root, { recursive: true, force: true });
@@ -84,7 +84,7 @@ module.exports = defineCases({
84
84
  const { root, projectRoot, distRoot, emDefaultsRoot } = stage();
85
85
  try {
86
86
  // Bundled defaults exist for all three platforms — but consumer-global wins.
87
- fakePng(path.join(emDefaultsRoot, 'icons', 'macos', 'icon.png'));
87
+ fakePng(path.join(emDefaultsRoot, 'icons', 'mac', 'icon.png'));
88
88
  fakePng(path.join(emDefaultsRoot, 'icons', 'windows', 'icon.png'));
89
89
 
90
90
  // Consumer ships ONE file at the global path. All three platforms pick it up.
@@ -95,7 +95,7 @@ module.exports = defineCases({
95
95
  const { resolveAndCopy } = require(path.join(__dirname, '..', '..', '..', 'lib', 'sign-helpers', 'resolve-icons.js'));
96
96
  const out = await resolveAndCopy({ config: {}, projectRoot, distRoot, emDefaultsRoot });
97
97
 
98
- for (const platform of ['macos', 'windows', 'linux']) {
98
+ for (const platform of ['mac', 'windows', 'linux']) {
99
99
  ctx.expect(out[platform].app).toBeDefined();
100
100
  const bytes = fs.readFileSync(out[platform].app);
101
101
  ctx.expect(bytes.toString()).toBe('global-bytes');
@@ -118,14 +118,14 @@ module.exports = defineCases({
118
118
  fs.writeFileSync(globalIcon, Buffer.from('global-bytes'));
119
119
 
120
120
  // Mac-specific override exists — should win for macOS.
121
- const macIcon = path.join(projectRoot, 'config', 'icons', 'macos', 'icon.png');
121
+ const macIcon = path.join(projectRoot, 'config', 'icons', 'mac', 'icon.png');
122
122
  fakePng(macIcon);
123
123
  fs.writeFileSync(macIcon, Buffer.from('mac-specific-bytes'));
124
124
 
125
125
  const { resolveAndCopy } = require(path.join(__dirname, '..', '..', '..', 'lib', 'sign-helpers', 'resolve-icons.js'));
126
126
  const out = await resolveAndCopy({ config: {}, projectRoot, distRoot, emDefaultsRoot });
127
127
 
128
- ctx.expect(fs.readFileSync(out.macos.app).toString()).toBe('mac-specific-bytes');
128
+ ctx.expect(fs.readFileSync(out.mac.app).toString()).toBe('mac-specific-bytes');
129
129
  ctx.expect(fs.readFileSync(out.windows.app).toString()).toBe('global-bytes');
130
130
  ctx.expect(fs.readFileSync(out.linux.app).toString()).toBe('global-bytes');
131
131
  } finally {
@@ -200,37 +200,37 @@ module.exports = defineCases({
200
200
  run: async (ctx) => {
201
201
  const { root, projectRoot, distRoot, emDefaultsRoot } = stage();
202
202
  try {
203
- fakePng(path.join(emDefaultsRoot, 'icons', 'macos', 'icon.png'));
203
+ fakePng(path.join(emDefaultsRoot, 'icons', 'mac', 'icon.png'));
204
204
  // Input source files at retina (native) size. @omega.js/desktop should emit both @1x + @2x in dist.
205
205
  // Tray input is `tray.png`; @omega.js/desktop renames the dist output to `trayTemplate.png` (macOS magic).
206
- await realPng(path.join(emDefaultsRoot, 'icons', 'macos', 'tray.png'), 32, 32);
207
- await realPng(path.join(emDefaultsRoot, 'icons', 'macos', 'dmg.png'), 1080, 760);
206
+ await realPng(path.join(emDefaultsRoot, 'icons', 'mac', 'tray.png'), 32, 32);
207
+ await realPng(path.join(emDefaultsRoot, 'icons', 'mac', 'dmg.png'), 1080, 760);
208
208
 
209
209
  const { resolveAndCopy } = require(path.join(__dirname, '..', '..', '..', 'lib', 'sign-helpers', 'resolve-icons.js'));
210
210
  const out = await resolveAndCopy({ config: {}, projectRoot, distRoot, emDefaultsRoot });
211
211
 
212
212
  // Both @1x and @2x slots populated.
213
- ctx.expect(out.macos.tray).toBeDefined();
214
- ctx.expect(out.macos.tray2x).toBeDefined();
215
- ctx.expect(out.macos.dmg).toBeDefined();
216
- ctx.expect(out.macos.dmg2x).toBeDefined();
213
+ ctx.expect(out.mac.tray).toBeDefined();
214
+ ctx.expect(out.mac.tray2x).toBeDefined();
215
+ ctx.expect(out.mac.dmg).toBeDefined();
216
+ ctx.expect(out.mac.dmg2x).toBeDefined();
217
217
  // app slot has no retina flag.
218
- ctx.expect(out.macos.app2x).toBeUndefined();
218
+ ctx.expect(out.mac.app2x).toBeUndefined();
219
219
 
220
220
  // Tray output filename is trayTemplate.png (renamed for macOS auto-inversion magic).
221
- ctx.expect(path.basename(out.macos.tray)).toBe('trayTemplate.png');
222
- ctx.expect(path.basename(out.macos.tray2x)).toBe('trayTemplate@2x.png');
221
+ ctx.expect(path.basename(out.mac.tray)).toBe('trayTemplate.png');
222
+ ctx.expect(path.basename(out.mac.tray2x)).toBe('trayTemplate@2x.png');
223
223
 
224
224
  // @1x derived at half the @2x dimensions.
225
- const tray1xMeta = await sharp(out.macos.tray).metadata();
226
- const tray2xMeta = await sharp(out.macos.tray2x).metadata();
225
+ const tray1xMeta = await sharp(out.mac.tray).metadata();
226
+ const tray2xMeta = await sharp(out.mac.tray2x).metadata();
227
227
  ctx.expect(tray1xMeta.width).toBe(16);
228
228
  ctx.expect(tray1xMeta.height).toBe(16);
229
229
  ctx.expect(tray2xMeta.width).toBe(32);
230
230
  ctx.expect(tray2xMeta.height).toBe(32);
231
231
 
232
- const dmg1xMeta = await sharp(out.macos.dmg).metadata();
233
- const dmg2xMeta = await sharp(out.macos.dmg2x).metadata();
232
+ const dmg1xMeta = await sharp(out.mac.dmg).metadata();
233
+ const dmg2xMeta = await sharp(out.mac.dmg2x).metadata();
234
234
  ctx.expect(dmg1xMeta.width).toBe(540);
235
235
  ctx.expect(dmg1xMeta.height).toBe(380);
236
236
  ctx.expect(dmg2xMeta.width).toBe(1080);
@@ -245,20 +245,20 @@ module.exports = defineCases({
245
245
  run: async (ctx) => {
246
246
  const { root, projectRoot, distRoot, emDefaultsRoot } = stage();
247
247
  try {
248
- fakePng(path.join(emDefaultsRoot, 'icons', 'macos', 'icon.png'));
249
- await realPng(path.join(emDefaultsRoot, 'icons', 'macos', 'tray.png'), 32, 32);
248
+ fakePng(path.join(emDefaultsRoot, 'icons', 'mac', 'icon.png'));
249
+ await realPng(path.join(emDefaultsRoot, 'icons', 'mac', 'tray.png'), 32, 32);
250
250
  // Consumer ships ONE file as `tray.png` (at @2x native size).
251
- const consumerTray = path.join(projectRoot, 'config', 'icons', 'macos', 'tray.png');
251
+ const consumerTray = path.join(projectRoot, 'config', 'icons', 'mac', 'tray.png');
252
252
  await realPng(consumerTray, 32, 32);
253
253
 
254
254
  const { resolveAndCopy } = require(path.join(__dirname, '..', '..', '..', 'lib', 'sign-helpers', 'resolve-icons.js'));
255
255
  const out = await resolveAndCopy({ config: {}, projectRoot, distRoot, emDefaultsRoot });
256
256
 
257
- ctx.expect(out.macos.tray).toBeDefined();
258
- ctx.expect(out.macos.tray2x).toBeDefined();
257
+ ctx.expect(out.mac.tray).toBeDefined();
258
+ ctx.expect(out.mac.tray2x).toBeDefined();
259
259
  // Output is still trayTemplate.png regardless of input name.
260
- ctx.expect(path.basename(out.macos.tray)).toBe('trayTemplate.png');
261
- const tray1xMeta = await sharp(out.macos.tray).metadata();
260
+ ctx.expect(path.basename(out.mac.tray)).toBe('trayTemplate.png');
261
+ const tray1xMeta = await sharp(out.mac.tray).metadata();
262
262
  ctx.expect(tray1xMeta.width).toBe(16);
263
263
  } finally {
264
264
  fs.rmSync(root, { recursive: true, force: true });
@@ -270,7 +270,7 @@ module.exports = defineCases({
270
270
  run: async (ctx) => {
271
271
  const { root, projectRoot, distRoot, emDefaultsRoot } = stage();
272
272
  try {
273
- fakePng(path.join(emDefaultsRoot, 'icons', 'macos', 'icon.png'));
273
+ fakePng(path.join(emDefaultsRoot, 'icons', 'mac', 'icon.png'));
274
274
  fakePng(path.join(emDefaultsRoot, 'icons', 'windows', 'icon.png'));
275
275
 
276
276
  // Consumer ships ONE tray file under global/.
@@ -279,13 +279,13 @@ module.exports = defineCases({
279
279
  const { resolveAndCopy } = require(path.join(__dirname, '..', '..', '..', 'lib', 'sign-helpers', 'resolve-icons.js'));
280
280
  const out = await resolveAndCopy({ config: {}, projectRoot, distRoot, emDefaultsRoot });
281
281
 
282
- ctx.expect(out.macos.tray).toBeDefined();
283
- ctx.expect(out.macos.tray2x).toBeDefined();
282
+ ctx.expect(out.mac.tray).toBeDefined();
283
+ ctx.expect(out.mac.tray2x).toBeDefined();
284
284
  ctx.expect(out.windows.tray).toBeDefined();
285
285
  ctx.expect(out.linux.tray).toBeDefined();
286
286
 
287
287
  // macOS @1x derived at half size from the global @2x source.
288
- const macTrayMeta = await sharp(out.macos.tray).metadata();
288
+ const macTrayMeta = await sharp(out.mac.tray).metadata();
289
289
  ctx.expect(macTrayMeta.width).toBe(16);
290
290
  } finally {
291
291
  fs.rmSync(root, { recursive: true, force: true });
@@ -0,0 +1,182 @@
1
+ // The self-hosted signer's job guard
2
+ // ([#875](https://github.com/Omega-JS-Stack/omega/issues/875)): the hook the
3
+ // runner runs before a job's first step, and the three files the runner install
4
+ // writes beside the runner so it has an allow list to read.
5
+ //
6
+ // The hook is run here the way the runner runs it (a node process with the
7
+ // job's environment and nothing else), because the EXIT CODE is the whole
8
+ // mechanism: nonzero and the job dies with the script's output, before the
9
+ // checkout, before npm, before signtool ever sees the token.
10
+ //
11
+ // Nothing in this file touches a real box: every write goes to a scratch home
12
+ // under the OS temp dir, and no verb of `commands/runner.js` is called at all.
13
+
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+ const os = require('os');
17
+ const { spawnSync } = require('child_process');
18
+ const defineCases = require('../../../vendor/devkit/test/define-cases.js');
19
+
20
+ const guard = require('../../../utils/runner-job-guard.js');
21
+ const hook = require('../../../runner/job-started.js');
22
+ const { ensureRunnerDirEnv } = require('../../../utils/runner-env.js');
23
+
24
+ const scratchHome = () => fs.mkdtempSync(path.join(os.tmpdir(), 'omega-job-guard-'));
25
+
26
+ // One job, as the runner delivers it: the three names the hook reads, and no
27
+ // inherited GITHUB_* (a run of this suite inside a job carries its own).
28
+ function runHook(hookPath, job) {
29
+ return spawnSync(process.execPath, [hookPath], {
30
+ env: { PATH: process.env.PATH, SystemRoot: process.env.SystemRoot || '', ...job },
31
+ encoding: 'utf8',
32
+ });
33
+ }
34
+
35
+ module.exports = defineCases({
36
+ type: 'suite',
37
+ layer: 'build',
38
+ description: 'runner job guard (#875): the box refuses any job that is not a trusted dispatch',
39
+ tests: [
40
+ {
41
+ name: 'a dispatch from a listed repo by a listed actor is allowed, and every other job is refused with one line naming the check and the value',
42
+ run: (ctx) => {
43
+ const home = scratchHome();
44
+ const { hook: hookPath } = guard.ensureRunnerJobGuard(home, { orgs: ['Omega-JS-Stack'], actor: 'ianwieds' });
45
+
46
+ // The passing case, on the ONE trusted event (#923).
47
+ const ok = runHook(hookPath, { GITHUB_EVENT_NAME: 'workflow_dispatch', GITHUB_REPOSITORY: 'Omega-JS-Stack/omega-omega', GITHUB_ACTOR: 'ianwieds' });
48
+ ctx.expect(ok.status).toBe(0);
49
+ ctx.expect(ok.stdout.trim()).toBe('Allowing this job: workflow_dispatch on Omega-JS-Stack/omega-omega by ianwieds.');
50
+
51
+ // Each failing check: nonzero, and ONE line carrying the env name that
52
+ // failed and the value seen.
53
+ const cases = [
54
+ [{ GITHUB_EVENT_NAME: 'pull_request', GITHUB_REPOSITORY: 'Omega-JS-Stack/omega-omega', GITHUB_ACTOR: 'ianwieds' }, 'GITHUB_EVENT_NAME', 'pull_request'],
55
+ [{ GITHUB_EVENT_NAME: 'repository_dispatch', GITHUB_REPOSITORY: 'Omega-JS-Stack/omega-omega', GITHUB_ACTOR: 'ianwieds' }, 'GITHUB_EVENT_NAME', 'repository_dispatch'],
56
+ [{ GITHUB_EVENT_NAME: 'push', GITHUB_REPOSITORY: 'Omega-JS-Stack/omega-omega', GITHUB_ACTOR: 'ianwieds' }, 'GITHUB_EVENT_NAME', 'push'],
57
+ [{ GITHUB_EVENT_NAME: 'workflow_dispatch', GITHUB_REPOSITORY: 'someone-else/their-app', GITHUB_ACTOR: 'ianwieds' }, 'GITHUB_REPOSITORY', 'someone-else/their-app'],
58
+ [{ GITHUB_EVENT_NAME: 'workflow_dispatch', GITHUB_REPOSITORY: 'Omega-JS-Stack/omega-omega', GITHUB_ACTOR: 'mallory' }, 'GITHUB_ACTOR', 'mallory'],
59
+ ];
60
+
61
+ for (const [job, name, value] of cases) {
62
+ const refused = runHook(hookPath, job);
63
+ ctx.expect(refused.status).toBe(1);
64
+ ctx.expect(refused.stdout.trim().split('\n').length).toBe(1);
65
+ ctx.expect(refused.stdout).toContain(name);
66
+ ctx.expect(refused.stdout).toContain(value);
67
+ }
68
+
69
+ fs.rmSync(home, { recursive: true, force: true });
70
+ },
71
+ },
72
+ {
73
+ name: 'a job with no environment at all is refused, and so is one whose allow list is missing',
74
+ run: (ctx) => {
75
+ const home = scratchHome();
76
+ const { hook: hookPath, repos } = guard.ensureRunnerJobGuard(home, { orgs: ['Omega-JS-Stack'], actor: 'ianwieds' });
77
+
78
+ // The runner always sets these; a hook run without them is a hook run
79
+ // that cannot answer the question, which is a refusal.
80
+ ctx.expect(runHook(hookPath, {}).status).toBe(1);
81
+
82
+ // An allow list that is not there allows nothing: the guard never opens
83
+ // the box up by failing to read its own configuration.
84
+ fs.rmSync(repos, { force: true });
85
+ const refused = runHook(hookPath, { GITHUB_EVENT_NAME: 'workflow_dispatch', GITHUB_REPOSITORY: 'Omega-JS-Stack/omega-omega', GITHUB_ACTOR: 'ianwieds' });
86
+ ctx.expect(refused.status).toBe(1);
87
+ ctx.expect(refused.stdout).toContain('allowed-repos.txt');
88
+
89
+ fs.rmSync(home, { recursive: true, force: true });
90
+ },
91
+ },
92
+ {
93
+ name: 'the allow list reads `owner/*` and single repos alike, case-insensitively, and comments and blanks are not entries',
94
+ run: (ctx) => {
95
+ const lists = { repos: ['Omega-JS-Stack/*', 'other-org/one-app'], actors: ['ianwieds'], reposFile: 'allowed-repos.txt', actorsFile: 'allowed-actors.txt' };
96
+ const dispatch = (repository) => hook.checkJob({ GITHUB_EVENT_NAME: 'workflow_dispatch', GITHUB_REPOSITORY: repository, GITHUB_ACTOR: 'IanWieds' }, lists).allowed;
97
+
98
+ ctx.expect(dispatch('omega-js-stack/anything')).toBe(true);
99
+ ctx.expect(dispatch('other-org/one-app')).toBe(true);
100
+ ctx.expect(dispatch('other-org/another-app')).toBe(false);
101
+ // `owner/*` is one owner, never a prefix of another owner's name.
102
+ ctx.expect(dispatch('Omega-JS-Stack-evil/app')).toBe(false);
103
+
104
+ const home = scratchHome();
105
+ const file = path.join(home, 'allowed-repos.txt');
106
+ fs.writeFileSync(file, '# a comment\n\n Omega-JS-Stack/omega-omega # trailing\n');
107
+ ctx.expect(hook.readList(file)).toEqual(['Omega-JS-Stack/omega-omega']);
108
+
109
+ fs.rmSync(home, { recursive: true, force: true });
110
+ },
111
+ },
112
+ {
113
+ name: 'the setup writes the hook and both allow lists beside the runner, and rerunning writes the same bytes',
114
+ run: (ctx) => {
115
+ const home = scratchHome();
116
+ const first = guard.ensureRunnerJobGuard(home, { orgs: ['Omega-JS-Stack'], actor: 'ianwieds' });
117
+
118
+ ctx.expect(first.hook).toBe(path.join(home, 'job-started.js'));
119
+ ctx.expect(first.repos).toBe(path.join(home, 'allowed-repos.txt'));
120
+ ctx.expect(first.actors).toBe(path.join(home, 'allowed-actors.txt'));
121
+ ctx.expect(first.added.repos).toEqual(['Omega-JS-Stack/*']);
122
+ ctx.expect(first.added.actors).toEqual(['ianwieds']);
123
+
124
+ // The hook on the box is this package's file, so a framework update
125
+ // reaches the box on its next install or start.
126
+ ctx.expect(fs.readFileSync(first.hook, 'utf8')).toBe(fs.readFileSync(path.join(__dirname, '..', '..', '..', 'runner', 'job-started.js'), 'utf8'));
127
+
128
+ const bytes = () => [first.hook, first.repos, first.actors].map((file) => fs.readFileSync(file, 'utf8'));
129
+ const before = bytes();
130
+ const again = guard.ensureRunnerJobGuard(home, { orgs: ['Omega-JS-Stack'], actor: 'ianwieds' });
131
+ ctx.expect(again.added).toEqual({ repos: [], actors: [] });
132
+ ctx.expect(bytes()).toEqual(before);
133
+
134
+ // A second org registered later gets its line; the first one is not
135
+ // duplicated and the file the operator reads keeps its header.
136
+ const third = guard.ensureRunnerJobGuard(home, { orgs: ['Omega-JS-Stack', 'ITW-Creative-Works'], actor: 'ianwieds' });
137
+ ctx.expect(third.added.repos).toEqual(['ITW-Creative-Works/*']);
138
+ ctx.expect(hook.readList(third.repos)).toEqual(['Omega-JS-Stack/*', 'ITW-Creative-Works/*']);
139
+ ctx.expect(fs.readFileSync(third.repos, 'utf8').startsWith('# The repositories this box will sign for')).toBe(true);
140
+
141
+ fs.rmSync(home, { recursive: true, force: true });
142
+ },
143
+ },
144
+ {
145
+ name: 'an allow list narrowed by hand is never widened back by a later setup',
146
+ run: (ctx) => {
147
+ const home = scratchHome();
148
+ const { repos } = guard.ensureRunnerJobGuard(home, { orgs: ['Omega-JS-Stack'], actor: 'ianwieds' });
149
+
150
+ // The operator narrows the org wildcard to the one repo that signs.
151
+ fs.writeFileSync(repos, '# mine\nOmega-JS-Stack/omega-omega\n');
152
+ const again = guard.ensureRunnerJobGuard(home, { orgs: ['Omega-JS-Stack'], actor: 'ianwieds' });
153
+
154
+ ctx.expect(again.added.repos).toEqual([]);
155
+ ctx.expect(hook.readList(repos)).toEqual(['Omega-JS-Stack/omega-omega']);
156
+
157
+ fs.rmSync(home, { recursive: true, force: true });
158
+ },
159
+ },
160
+ {
161
+ name: "the hook path reaches the listener through the runner dir's own .env, beside HOME",
162
+ run: (ctx) => {
163
+ const home = scratchHome();
164
+ const runnerDir = path.join(home, 'actions-runner-omega-js-stack');
165
+ const { hook: hookPath } = guard.ensureRunnerJobGuard(home, { orgs: ['Omega-JS-Stack'], actor: 'ianwieds' });
166
+
167
+ // The file GitHub documents for runner env, which the listener reads at
168
+ // startup and applies to every job: the same one the private HOME
169
+ // travels in (#807), written once for both.
170
+ const file = ensureRunnerDirEnv(runnerDir, { HOME: path.join(home, 'home'), ACTIONS_RUNNER_HOOK_JOB_STARTED: hookPath });
171
+ const lines = fs.readFileSync(file, 'utf8').trim().split(/\r?\n/);
172
+
173
+ ctx.expect(lines).toContain(`ACTIONS_RUNNER_HOOK_JOB_STARTED=${hookPath}`);
174
+ ctx.expect(lines).toContain(`HOME=${path.join(home, 'home')}`);
175
+ // Bare, never quoted: the listener takes the rest of the line verbatim.
176
+ ctx.expect(fs.readFileSync(file, 'utf8')).not.toContain('"');
177
+
178
+ fs.rmSync(home, { recursive: true, force: true });
179
+ },
180
+ },
181
+ ],
182
+ });
@@ -1155,11 +1155,23 @@ module.exports = defineCases({
1155
1155
  jetpack.write(path.join(home, 'actions-runner-foo', 'config.cmd'), 'rem foo');
1156
1156
  jetpack.write(path.join(home, '_template', 'config.cmd'), 'rem template');
1157
1157
 
1158
+ // The job guard's allow lists are configuration too
1159
+ // ([#875](https://github.com/Omega-JS-Stack/omega/issues/875)): a list
1160
+ // narrowed by hand outlives an uninstall exactly as the `.env` does.
1161
+ // The hook SCRIPT is install state, and the next install writes it back.
1162
+ jetpack.write(path.join(home, 'allowed-repos.txt'), 'Omega-JS-Stack/omega-omega\n');
1163
+ jetpack.write(path.join(home, 'allowed-actors.txt'), 'ianwieds\n');
1164
+ jetpack.write(path.join(home, 'job-started.js'), '// stale copy');
1165
+
1158
1166
  await runner.removeRunnerHomeWithRetry(new Set(), home);
1159
1167
 
1160
- ctx.expect(jetpack.list(home).sort()).toEqual(['.env', 'logs']);
1168
+ ctx.expect(jetpack.list(home).sort()).toEqual(['.env', 'allowed-actors.txt', 'allowed-repos.txt', 'logs']);
1161
1169
  ctx.expect(jetpack.read(path.join(home, '.env'))).toBe('WIN_CSC_KEY_PASSWORD=keep-me');
1162
1170
  ctx.expect(jetpack.read(runnerEnv.runnerLogFile(home))).toBe('# omega log\n');
1171
+ ctx.expect(jetpack.read(path.join(home, 'allowed-repos.txt'))).toBe('Omega-JS-Stack/omega-omega\n');
1172
+
1173
+ jetpack.remove(path.join(home, 'allowed-repos.txt'));
1174
+ jetpack.remove(path.join(home, 'allowed-actors.txt'));
1163
1175
 
1164
1176
  // A kept org dir (its deregistration failed) survives beside them.
1165
1177
  jetpack.write(path.join(home, 'actions-runner-bar', 'config.cmd'), 'rem bar');
@@ -2584,10 +2596,21 @@ module.exports = defineCases({
2584
2596
  });
2585
2597
 
2586
2598
  ctx.expect(fs.readFileSync(path.join(home, 'home', '.gitconfig'), 'utf8')).toBe('[core]\n\tautocrlf = false\n');
2599
+ // The job guard is healed on the same pass
2600
+ // ([#875](https://github.com/Omega-JS-Stack/omega/issues/875)): a box
2601
+ // that installed before it would otherwise come online signing for
2602
+ // anything. Both lines, in one `<runner dir>\.env`.
2603
+ const hook = path.join(home, 'job-started.js');
2604
+ ctx.expect(jetpack.exists(hook)).toBe('file');
2587
2605
  for (const org of orgs) {
2588
- ctx.expect(fs.readFileSync(path.join(home, `actions-runner-${org}`, '.env'), 'utf8')).toBe(`HOME=${path.join(home, 'home')}\n`);
2606
+ ctx.expect(fs.readFileSync(path.join(home, `actions-runner-${org}`, '.env'), 'utf8')).toBe(`HOME=${path.join(home, 'home')}\nACTIONS_RUNNER_HOOK_JOB_STARTED=${hook}\n`);
2589
2607
  ctx.expect(logged.join('\n')).toContain(`HOME for ${org}: ${path.join(home, 'home')}`);
2608
+ ctx.expect(logged.join('\n')).toContain(`Job guard for ${org}: ${hook}`);
2590
2609
  }
2610
+ // `start` asks GitHub nothing, so the actor list is the operator's to
2611
+ // fill: the file is there with its header, and an empty list refuses.
2612
+ ctx.expect(jetpack.exists(path.join(home, 'allowed-repos.txt'))).toBe('file');
2613
+ ctx.expect(jetpack.exists(path.join(home, 'allowed-actors.txt'))).toBe('file');
2591
2614
  } finally {
2592
2615
  if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
2593
2616
  for (const file of shortcuts) jetpack.remove(file);
@@ -63,15 +63,20 @@ module.exports = defineCases({
63
63
  // in the sibling `package` key.
64
64
  name: 'the bundle bake folds the app version into the config the renderer hands the client',
65
65
  run: (ctx) => {
66
- const { composeBuildConfig } = require(path.join(FRAMEWORK_ROOT, 'src', 'gulp', 'tasks', 'bundle.js'));
66
+ const { composeBuildConfig, buildFacts } = require(path.join(FRAMEWORK_ROOT, 'src', 'gulp', 'tasks', 'bundle.js'));
67
67
 
68
- const packaged = composeBuildConfig({ brand: { id: 'paperloom' } }, null, { version: '3.1.4' });
68
+ const pkg = { version: '3.1.4' };
69
+ const mode = { environment: 'production', build: true, publish: false };
70
+ const packaged = composeBuildConfig({ brand: { id: 'paperloom' } }, buildFacts({ pkg, mode, dev: null }));
69
71
  ctx.expect(packaged.version).toBe('3.1.4');
70
72
  ctx.expect(packaged.brand.id).toBe('paperloom');
71
73
  ctx.expect(packaged.dev).toBeUndefined();
72
74
 
73
75
  // A dev build carries the resolved local-stack map beside it (#300).
74
- const local = composeBuildConfig({ brand: { id: 'paperloom' } }, { ports: { auth: 9099 } }, { version: '3.1.4' });
76
+ const local = composeBuildConfig(
77
+ { brand: { id: 'paperloom' } },
78
+ buildFacts({ pkg, mode: { ...mode, environment: 'development', build: false }, dev: { ports: { auth: 9099 } } }),
79
+ );
75
80
  ctx.expect(local.version).toBe('3.1.4');
76
81
  ctx.expect(local.dev.ports.auth).toBe(9099);
77
82
  },
@@ -72,6 +72,9 @@ module.exports = defineCases({
72
72
  const written = JSON.parse(contents);
73
73
  ctx.expect(written.scripts.build).toBe('omega build');
74
74
  ctx.expect(written.private).toBe(true);
75
+ // Every OMEGA target states its license when it states none of its
76
+ // own (#884): npm's word for closed-source commercial code
77
+ ctx.expect(written.license).toBe('UNLICENSED');
75
78
  ctx.expect(written.main).toBe('dist/main.bundle.js');
76
79
  ctx.expect(contents).toContain('\n "name": "staged-app"');
77
80
  });
@@ -14,7 +14,7 @@ module.exports = defineCases({
14
14
  description: 'sign-windows — signtool command construction + retry policy',
15
15
  tests: [
16
16
  {
17
- name: 'this suite can never sign for real: every signWithRetry call injects its exec',
17
+ name: 'this suite can never sign for real: every call that reaches signtool injects its exec',
18
18
  run: (ctx) => {
19
19
  // The box runs this lane too. `signWithRetry` shells out to signtool by
20
20
  // DEFAULT, so a case that forgets `exec:` would drive the real EV token
@@ -22,13 +22,17 @@ module.exports = defineCases({
22
22
  // lives in sign-windows-e2e.test.js, gated. This pins the invariant.
23
23
  const fs = require('fs');
24
24
  const source = fs.readFileSync(__filename, 'utf8');
25
- // The needle is assembled so this case's own text is not one of its hits.
26
- const calls = source.split(`signWithRetry${'('}`).slice(1);
27
- ctx.expect(calls.length).toBeGreaterThan(0);
28
- for (const call of calls) {
29
- const body = call.slice(0, call.indexOf('});'));
30
- if (!body.includes('exec:')) {
31
- throw new Error(`a signWithRetry call in this suite does not inject exec — it would run the real signtool:\n${body.slice(0, 200)}`);
25
+ // Both entry points reach signtool: the retry wrapper directly, and the
26
+ // signing loop through it plus its own verify call.
27
+ for (const entry of ['signWithRetry', 'signWithSigntool']) {
28
+ // The needle is assembled so this case's own text is not one of its hits.
29
+ const calls = source.split(`${entry}${'('}`).slice(1);
30
+ ctx.expect(calls.length).toBeGreaterThan(0);
31
+ for (const call of calls) {
32
+ const body = call.slice(0, call.indexOf('});'));
33
+ if (!body.includes('exec:')) {
34
+ throw new Error(`a ${entry} call in this suite does not inject exec, it would run the real signtool:\n${body.slice(0, 200)}`);
35
+ }
32
36
  }
33
37
  }
34
38
  },
@@ -143,6 +147,59 @@ module.exports = defineCases({
143
147
  .toBe('"signtool" verify /pa "C:\\out\\App.exe"');
144
148
  },
145
149
  },
150
+ {
151
+ name: 'a signed artifact prints the proof line in the mac shape, after verify passes (#918)',
152
+ run: async (ctx) => {
153
+ // The mac lane ends with `Stapled and verified: ...` (hooks/notarize.js).
154
+ // Windows runs the same kind of proof (`signtool verify /pa`, a failure
155
+ // aborts the job), so it says so in the same shape, naming the check it
156
+ // ran. Driven here through the injected signtool: the ORDER (sign, then
157
+ // verify, THEN the line) is what makes the line true.
158
+ const os = require('os');
159
+ const jetpack = require('fs-jetpack');
160
+
161
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-sign-proof-'));
162
+ const inDir = path.join(tmp, 'release');
163
+ const outDir = path.join(inDir, 'signed');
164
+ const target = path.join(inDir, 'App-windows.msi');
165
+ // .msi on purpose: it needs no latest.yml, so the loop is the whole test.
166
+ jetpack.write(target, 'unsigned bytes');
167
+
168
+ const commands = [];
169
+ const lines = [];
170
+ const fakeLogger = {
171
+ log: (m) => lines.push(String(m)),
172
+ warn: (m) => lines.push(String(m)),
173
+ error: (m) => lines.push(String(m)),
174
+ };
175
+
176
+ const origToken = process.env.WIN_EV_TOKEN_PATH;
177
+ const origPassword = process.env.WIN_CSC_KEY_PASSWORD;
178
+ try {
179
+ // Thumbprint mode with no PIN: SafeNet owns it, so no console-lock check.
180
+ process.env.WIN_EV_TOKEN_PATH = 'f'.repeat(40);
181
+ delete process.env.WIN_CSC_KEY_PASSWORD;
182
+
183
+ await signWindows.signWithSigntool([target], inDir, outDir, {
184
+ exec: (cmd) => { commands.push(cmd); return Promise.resolve('Successfully signed'); },
185
+ logger: fakeLogger,
186
+ });
187
+ } finally {
188
+ if (origToken === undefined) delete process.env.WIN_EV_TOKEN_PATH; else process.env.WIN_EV_TOKEN_PATH = origToken;
189
+ if (origPassword === undefined) delete process.env.WIN_CSC_KEY_PASSWORD; else process.env.WIN_CSC_KEY_PASSWORD = origPassword;
190
+ jetpack.remove(tmp);
191
+ }
192
+
193
+ ctx.expect(commands.length).toBe(2);
194
+ ctx.expect(commands[0]).toContain(' sign /sha1 ');
195
+ ctx.expect(commands[1]).toContain('verify /pa');
196
+ // The proof line is the LAST thing this file's turn prints, and it names
197
+ // the check as well as the artifact: mac punctuation, basename, period.
198
+ ctx.expect(lines[lines.length - 1]).toBe('Signed and verified: signtool verify /pa accepts App-windows.msi.');
199
+ // The old line claimed only a signature, which is what sent the owner asking.
200
+ ctx.expect(lines.some((line) => line.includes('Signed: '))).toBe(false);
201
+ },
202
+ },
146
203
  {
147
204
  name: 'WIN_EV_TOKEN_PATH is the ONE cert-reference key — WIN_CSC_LINK is not read',
148
205
  run: (ctx) => {
@@ -309,5 +366,32 @@ module.exports = defineCases({
309
366
  ctx.expect(signWindows.isTransientSignFailure(new Error('The specified network password is not correct'))).toBe(false);
310
367
  },
311
368
  },
369
+ {
370
+ name: 'a terminated attempt reads as transient, so the next one gets a fresh PIN watcher',
371
+ run: async (ctx) => {
372
+ // The wording is execWithLimit's (exec-with-limit.test.js proves the
373
+ // mechanism); here it only matters that nothing classifies it as final.
374
+ const limitError = new Error('signtool produced no verdict within 180s and was terminated');
375
+ ctx.expect(signWindows.isTransientSignFailure(limitError)).toBe(true);
376
+ ctx.expect(signWindows.SIGN_TIME_LIMIT_MS).toBe(3 * 60 * 1000);
377
+
378
+ let watchers = 0;
379
+ let calls = 0;
380
+ let threw;
381
+ try {
382
+ await signWindows.signWithRetry('signtool sign ...', {
383
+ file: 'App.exe',
384
+ exec: () => { calls += 1; throw limitError; },
385
+ sleep: () => Promise.resolve(),
386
+ emit: () => {},
387
+ startUnlock: () => { watchers += 1; return { stop: () => {} }; },
388
+ });
389
+ } catch (e) { threw = e; }
390
+
391
+ ctx.expect(calls).toBe(3);
392
+ ctx.expect(watchers).toBe(3);
393
+ ctx.expect(threw.message).toMatch(/no verdict within 180s/);
394
+ },
395
+ },
312
396
  ],
313
397
  });
@@ -2,13 +2,15 @@
2
2
  // invisible and non-intrusive". Consumed by window-manager (stealth surfacing),
3
3
  // main.js (macOS app-activation suppression), and the test harness.
4
4
  //
5
- // Build layer runs in plain Node, so the env-var contract (OMEGA_TEST_MODE /
5
+ // Build layer runs in plain Node, so the env-var contract (OMEGA_ENVIRONMENT,
6
+ // the ONE environment input since
7
+ // [#817](https://github.com/Omega-JS-Stack/omega/issues/817), plus
6
8
  // OMEGA_TEST_SHOW) is exercised directly with save/restore around each case.
7
9
 
8
10
  const isTestStealth = require('../../../utils/test-stealth.js');
9
11
  const defineCases = require('../../../vendor/devkit/test/define-cases.js');
10
12
 
11
- // Run fn with OMEGA_TEST_MODE / OMEGA_TEST_SHOW set to the given values (undefined = unset),
13
+ // Run fn with OMEGA_ENVIRONMENT / OMEGA_TEST_SHOW set to the given values (undefined = unset),
12
14
  // restoring the real environment afterwards so other build suites are unaffected.
13
15
  function withEnv(vars, fn) {
14
16
  const saved = {};
@@ -41,7 +43,7 @@ module.exports = defineCases({
41
43
  {
42
44
  name: 'true in testing mode without OMEGA_TEST_SHOW',
43
45
  run: (ctx) => {
44
- withEnv({ OMEGA_TEST_MODE: 'true', OMEGA_TEST_SHOW: undefined }, () => {
46
+ withEnv({ OMEGA_ENVIRONMENT: 'testing', OMEGA_TEST_SHOW: undefined }, () => {
45
47
  ctx.expect(isTestStealth()).toBe(true);
46
48
  });
47
49
  },
@@ -49,7 +51,7 @@ module.exports = defineCases({
49
51
  {
50
52
  name: 'OMEGA_TEST_SHOW=1 opts out even in testing mode',
51
53
  run: (ctx) => {
52
- withEnv({ OMEGA_TEST_MODE: 'true', OMEGA_TEST_SHOW: '1' }, () => {
54
+ withEnv({ OMEGA_ENVIRONMENT: 'testing', OMEGA_TEST_SHOW: '1' }, () => {
53
55
  ctx.expect(isTestStealth()).toBe(false);
54
56
  });
55
57
  },
@@ -57,24 +59,28 @@ module.exports = defineCases({
57
59
  {
58
60
  name: 'false outside testing mode regardless of OMEGA_TEST_SHOW',
59
61
  run: (ctx) => {
60
- withEnv({ OMEGA_TEST_MODE: undefined, OMEGA_TEST_SHOW: undefined, OMEGA_BUILD_MODE: undefined, NODE_ENV: 'development' }, () => {
62
+ withEnv({ OMEGA_ENVIRONMENT: 'development', OMEGA_TEST_SHOW: undefined }, () => {
63
+ ctx.expect(isTestStealth()).toBe(false);
64
+ });
65
+ withEnv({ OMEGA_ENVIRONMENT: 'production', OMEGA_TEST_SHOW: '1' }, () => {
61
66
  ctx.expect(isTestStealth()).toBe(false);
62
67
  });
63
68
  },
64
69
  },
65
70
  {
66
- name: 'a provided manager is authoritative over the env fallback',
71
+ name: 'a provided manager answers from the config IT was baked with',
67
72
  run: (ctx) => {
68
- // Build-time Manager carries the same mode-helpers mixin as the runtime
69
- // Managers — config.em.environment overrides the env detection.
73
+ // The build-time Manager carries the same mixin as the runtime Managers,
74
+ // and since #817 they all read the ONE input: the process variable, or
75
+ // the baked `config.environment` when the process has none.
70
76
  const Manager = require('../../../build.js');
71
- withEnv({ OMEGA_TEST_MODE: undefined, OMEGA_TEST_SHOW: undefined }, () => {
77
+ withEnv({ OMEGA_ENVIRONMENT: undefined, OMEGA_TEST_SHOW: undefined }, () => {
72
78
  const testing = new Manager();
73
- testing.config = { em: { environment: 'testing' } };
79
+ testing.config = { environment: 'testing' };
74
80
  ctx.expect(isTestStealth(testing)).toBe(true);
75
81
 
76
82
  const production = new Manager();
77
- production.config = { em: { environment: 'production' } };
83
+ production.config = { environment: 'production' };
78
84
  ctx.expect(isTestStealth(production)).toBe(false);
79
85
  });
80
86
  },