@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
@@ -69,14 +69,15 @@ const NO_ENV = {
69
69
  module.exports = defineCases({
70
70
  type: 'group', // independent tests; run all even on failure
71
71
  layer: 'build',
72
- description: 'utils/url-helpers: every local helper resolves env → baked dev.ports → classic',
72
+ description: 'utils/url-helpers: every local helper resolves env, then the baked dev.ports, then throws',
73
73
  tests: [
74
74
  {
75
- name: 'exports { attachTo, getApiUrl, localPort }',
75
+ name: 'exports { attachTo, getApiUrl, localPort, requiredPort }',
76
76
  run: (ctx) => {
77
77
  ctx.expect(typeof helpers.attachTo).toBe('function');
78
78
  ctx.expect(typeof helpers.getApiUrl).toBe('function');
79
79
  ctx.expect(typeof helpers.localPort).toBe('function');
80
+ ctx.expect(typeof helpers.requiredPort).toBe('function');
80
81
  },
81
82
  },
82
83
  {
@@ -107,11 +108,19 @@ module.exports = defineCases({
107
108
  },
108
109
  },
109
110
  {
110
- name: 'neither channel → the classic hosting port 5002',
111
+ // The classic 5002 used to answer here (#834). It is gone: a dev build
112
+ // always bakes the resolved map, so neither channel answering means the
113
+ // artifact is broken, and a wrong API base is a silent connection refusal
114
+ // or a hit on a neighbouring project's stack.
115
+ name: 'neither channel → a throw naming the port and the bundle task',
111
116
  run: (ctx) => {
112
117
  withEnv(NO_ENV, () => {
113
- ctx.expect(helpers.getApiUrl.call(fakeManager('testing'))).toBe('http://localhost:5002');
114
- ctx.expect(helpers.getApiUrl.call(fakeManager('development'))).toBe('http://localhost:5002');
118
+ for (const environment of ['testing', 'development']) {
119
+ ctx.expect(() => helpers.getApiUrl.call(fakeManager(environment)))
120
+ .toThrow(/dev port for `hosting`/);
121
+ ctx.expect(() => helpers.getApiUrl.call(fakeManager(environment)))
122
+ .toThrow(/bundle task/);
123
+ }
115
124
  });
116
125
  },
117
126
  },
@@ -163,11 +172,11 @@ module.exports = defineCases({
163
172
  },
164
173
  },
165
174
  {
166
- name: 'getFunctionsUrl: neither channel → the classic functions port 5001',
175
+ name: 'getFunctionsUrl: neither channel → a throw naming the port (#834)',
167
176
  run: (ctx) => {
168
177
  withEnv(NO_ENV, () => {
169
- ctx.expect(helpers.getFunctionsUrl.call(fakeManager('development')))
170
- .toBe('http://localhost:5001/demo-app/us-central1');
178
+ ctx.expect(() => helpers.getFunctionsUrl.call(fakeManager('development')))
179
+ .toThrow(/dev port for `functions`/);
171
180
  });
172
181
  },
173
182
  },
@@ -200,10 +209,11 @@ module.exports = defineCases({
200
209
  },
201
210
  },
202
211
  {
203
- name: 'getWebsiteUrl: neither channel → the classic dev origin https://localhost:4000',
212
+ name: 'getWebsiteUrl: neither channel → a throw naming the port (#834)',
204
213
  run: (ctx) => {
205
214
  withEnv(NO_ENV, () => {
206
- ctx.expect(helpers.getWebsiteUrl.call(fakeManager('development'))).toBe('https://localhost:4000');
215
+ ctx.expect(() => helpers.getWebsiteUrl.call(fakeManager('development')))
216
+ .toThrow(/dev port for `website`/);
207
217
  });
208
218
  },
209
219
  },
@@ -228,12 +238,18 @@ module.exports = defineCases({
228
238
  },
229
239
  },
230
240
  {
231
- // The lockstep the renderer bundle forces: url-helpers rides the browser
232
- // bundle, so it cannot require @omega.js/config (fs/net/json5) and mirrors
233
- // the constant instead. This Node-side case is what keeps the copy honest.
234
- name: 'CLASSIC_DEV_ORIGIN is in lockstep with @omega.js/config',
241
+ // There is no lockstep copy to keep honest any more (#834): the classic
242
+ // numbers live ONLY in @omega.js/config, the bundle task bakes them into
243
+ // the artifact as the floor of the resolved map, and this module reads
244
+ // that map. A copy here would be the exact defect the issue closed.
245
+ name: 'no classic constants live in this module (#834)',
235
246
  run: (ctx) => {
236
- ctx.expect(helpers.CLASSIC_DEV_ORIGIN).toBe(require('../../../vendor/config/index.js').CLASSIC_DEV_ORIGIN);
247
+ ctx.expect(helpers.CLASSIC_DEV_ORIGIN).toBe(undefined);
248
+ const source = require('fs').readFileSync(require.resolve('../../../utils/url-helpers.js'), 'utf8');
249
+ const code = source.split('\n').filter((line) => !line.trim().startsWith('//')).join('\n');
250
+ for (const classic of ['4000', '5001', '5002', '9099']) {
251
+ ctx.expect(code.includes(classic)).toBe(false);
252
+ }
237
253
  },
238
254
  },
239
255
  {
@@ -244,7 +260,7 @@ module.exports = defineCases({
244
260
  withEnv(NO_ENV, () => {
245
261
  // getAuthUrl calls `this.getWebsiteUrl()`, which attachTo() supplies on a
246
262
  // real Manager — hand the stand-in the same helper.
247
- const manager = fakeManager('development');
263
+ const manager = fakeManager('development', { website: 4000 });
248
264
  manager.getWebsiteUrl = helpers.getWebsiteUrl;
249
265
  const url = new URL(helpers.getAuthUrl.call(manager));
250
266
  ctx.expect(url.origin).toBe('https://localhost:4000');
@@ -274,8 +290,12 @@ module.exports = defineCases({
274
290
  withBridgeManager(fakeManager('testing', { auth: 9109 }), () => {
275
291
  ctx.expect(clientBridge._authEmulatorPort()).toBe(9109);
276
292
  });
293
+ // The classic 9099 used to answer here (#834): nothing identity-checks
294
+ // what holds that port, so a neighbouring project's emulator read as
295
+ // an auth mystery instead of a port problem.
277
296
  withBridgeManager(fakeManager('testing'), () => {
278
- ctx.expect(clientBridge._authEmulatorPort()).toBe(9099);
297
+ ctx.expect(() => clientBridge._authEmulatorPort()).toThrow(/dev port for `auth`/);
298
+ ctx.expect(() => clientBridge._authEmulatorPort()).toThrow(/bundle task/);
279
299
  });
280
300
  });
281
301
  withEnv({ OMEGA_AUTH_PORT: '9119' }, () => {
@@ -1,100 +1,473 @@
1
- // Build-layer tests for commands/validate-certs.js — provisioning profile parsing logic.
1
+ // Build-layer tests for commands/validate-certs.js: what CSC_LINK is allowed to
2
+ // carry, the three validity rungs every surface shares
3
+ // ([#892](https://github.com/Omega-JS-Stack/omega/issues/892)), and the rung an
4
+ // UNSET key is now ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)):
5
+ // the step READS the env the boot derived, and the `config/certs/` scan that
6
+ // used to stop a deploy the build would have signed fine is gone.
2
7
  // The full validate flow (Keychain query, env vars) is exercised via real `npx omega deploy` prechecks.
3
8
 
4
9
  const path = require('path');
5
10
  const fs = require('fs');
6
11
  const os = require('os');
12
+ const { execFileSync } = require('child_process');
7
13
 
8
14
  const validateCerts = require(path.join(__dirname, '..', '..', '..', 'commands', 'validate-certs.js'));
9
15
  const defineCases = require('../../../vendor/devkit/test/define-cases.js');
10
16
 
11
- // A minimal mock provisioning profile (CMS-wrapped XML plist payload).
12
- function makeMockProvision({ appId, expirationDate }) {
13
- const appIdLine = appId
14
- ? `\n <key>application-identifier</key>\n <string>${appId}</string>`
15
- : '';
16
- const expLine = expirationDate
17
- ? `\n <key>ExpirationDate</key>\n <date>${expirationDate.toISOString()}</date>`
18
- : '';
19
-
20
- // The function strips outer CMS wrapping by regex-matching the inner plist; we just
21
- // wrap our test XML in some CMS-like junk so the regex still works.
22
- const inner = `<?xml version="1.0" encoding="UTF-8"?>
23
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
24
- <plist version="1.0">
25
- <dict>${appIdLine}${expLine}
26
- </dict>
27
- </plist>`;
28
-
29
- return `XX-CMS-WRAPPER-XX${inner}YY-CMS-WRAPPER-YY`;
17
+ // A REAL signing certificate: a self-signed cert exported to a .p12 with a
18
+ // password, so the expiry read is the same openssl path a build takes.
19
+ function stageSigningCert({ days = 365 } = {}) {
20
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-p12-'));
21
+ const keyPath = path.join(dir, 'private.key');
22
+ const certPath = path.join(dir, 'cert.cer');
23
+ const p12Path = path.join(dir, 'dev-id.p12');
24
+ const password = 'fixture-pass';
25
+
26
+ execFileSync('openssl', [
27
+ 'req', '-x509', '-nodes', '-newkey', 'rsa:2048', '-keyout', keyPath, '-out', certPath,
28
+ '-days', String(days), '-subj', '/CN=Developer ID Application: Fixture/C=US', '-outform', 'DER',
29
+ ], { stdio: 'pipe' });
30
+ execFileSync('openssl', [
31
+ 'pkcs12', '-export', '-legacy', '-inkey', keyPath, '-in', certPath, '-out', p12Path,
32
+ '-passout', 'env:OMEGA_P12_PASSWORD',
33
+ ], { stdio: 'pipe', env: { ...process.env, OMEGA_P12_PASSWORD: password } });
34
+
35
+ return { dir, p12Path, password };
36
+ }
37
+
38
+ // Run one call with exactly these env vars set (undefined deletes the key), then
39
+ // put the environment back the way it was. An ASYNC call holds the window until
40
+ // it settles: checkMac awaits the Keychain query, and a restore that fired at
41
+ // the first await would hand the rest of the run this machine's real signing env.
42
+ function withEnv(vars, run) {
43
+ const previous = {};
44
+ for (const [key, value] of Object.entries(vars)) {
45
+ previous[key] = process.env[key];
46
+ if (value === undefined) {
47
+ delete process.env[key];
48
+ } else {
49
+ process.env[key] = value;
50
+ }
51
+ }
52
+
53
+ const restore = () => {
54
+ for (const [key, value] of Object.entries(previous)) {
55
+ if (value === undefined) {
56
+ delete process.env[key];
57
+ } else {
58
+ process.env[key] = value;
59
+ }
60
+ }
61
+ };
62
+
63
+ let result;
64
+ try {
65
+ result = run();
66
+ } catch (error) {
67
+ restore();
68
+ throw error;
69
+ }
70
+
71
+ if (result && typeof result.then === 'function') {
72
+ return result.finally(restore);
73
+ }
74
+
75
+ restore();
76
+ return result;
77
+ }
78
+
79
+ // Run one call as if this leg were `platform`, then put the real value back.
80
+ // The mac rungs are darwin-only (#891), so a case about them has to say which
81
+ // leg it is rather than inherit the machine the suite happens to run on.
82
+ function withPlatform(platform, run) {
83
+ const descriptor = Object.getOwnPropertyDescriptor(process, 'platform');
84
+ Object.defineProperty(process, 'platform', { value: platform, configurable: true });
85
+
86
+ const restore = () => Object.defineProperty(process, 'platform', descriptor);
87
+
88
+ let result;
89
+ try {
90
+ result = run();
91
+ } catch (error) {
92
+ restore();
93
+ throw error;
94
+ }
95
+
96
+ if (result && typeof result.then === 'function') {
97
+ return result.finally(restore);
98
+ }
99
+
100
+ restore();
101
+ return result;
30
102
  }
31
103
 
32
104
  module.exports = defineCases({
33
105
  type: 'suite',
34
106
  layer: 'build',
35
- description: 'validate-certs — provisioning profile parsing',
107
+ description: 'validate-certs: the shapes CSC_LINK takes, the three validity rungs, and the unset-key rung',
36
108
  tests: [
109
+ // BOTH mac credentials take three shapes
110
+ // ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)): a path, an https
111
+ // URL, or the file itself as inline base64. The build workflow ships two of them,
112
+ // because only the mac job decodes the base64 secrets to files, and treating that
113
+ // base64 as a path threw ENAMETOOLONG out of the precheck: first on CSC_LINK,
114
+ // then, once that was fixed, on APPLE_API_KEY.
115
+ // The cloud provider is read where sign-windows and the build read it,
116
+ // `platforms.windows.signing.cloud.provider` (#872): the checker used to look
117
+ // under `targets`, so a configured provider was always "not set" and its
118
+ // env vars were never checked.
119
+ {
120
+ name: 'the Windows cloud provider is read from platforms.windows, and its env vars are checked',
121
+ run: (ctx) => {
122
+ const config = { platforms: { windows: { signing: { strategy: 'cloud', cloud: { provider: 'azure' } } } } };
123
+ const azure = { AZURE_TENANT_ID: 't', AZURE_CLIENT_ID: 'c', AZURE_CLIENT_SECRET: 's', AZURE_TRUSTED_SIGNING_ENDPOINT: 'e' };
124
+ const none = Object.fromEntries(Object.keys(azure).map((key) => [key, undefined]));
125
+
126
+ const configured = [];
127
+ withEnv(azure, () => validateCerts.checkWindows(configured, 'cloud', config));
128
+ ctx.expect(configured).toEqual([]);
129
+
130
+ const bare = [];
131
+ withEnv(none, () => validateCerts.checkWindows(bare, 'cloud', config));
132
+ ctx.expect(bare.length).toBe(1);
133
+ ctx.expect(bare[0].message).toContain('Missing cloud-signing env vars for azure');
134
+ },
135
+ },
136
+ {
137
+ name: 'CSC_LINK carrying an inline base64 cert is reported, not statted',
138
+ run: (ctx) => {
139
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-csc-'));
140
+ const inline = 'MIIKvwIBAzCCCn8GCSqGSIb3DQEHAaCCCnAEggpsMIIKaDCCBR8'.repeat(60);
141
+
142
+ try {
143
+ ctx.expect(validateCerts.describeCertRef(inline, tmpDir).kind).toBe('inline');
144
+
145
+ const issues = [];
146
+ withEnv({ CSC_KEY_PASSWORD: 'hunter2' }, () => validateCerts.checkSigningCert(issues, inline, tmpDir));
147
+ ctx.expect(issues).toEqual([]);
148
+ } finally {
149
+ fs.rmSync(tmpDir, { recursive: true, force: true });
150
+ }
151
+ },
152
+ },
153
+ {
154
+ name: 'CSC_LINK carrying a URL is reported, not statted',
155
+ run: (ctx) => {
156
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-csc-'));
157
+ const url = 'https://certs.example.com/dev-id.p12';
158
+
159
+ try {
160
+ ctx.expect(validateCerts.describeCertRef(url, tmpDir).kind).toBe('url');
161
+
162
+ const issues = [];
163
+ withEnv({ CSC_KEY_PASSWORD: 'hunter2' }, () => validateCerts.checkSigningCert(issues, url, tmpDir));
164
+ ctx.expect(issues).toEqual([]);
165
+ } finally {
166
+ fs.rmSync(tmpDir, { recursive: true, force: true });
167
+ }
168
+ },
169
+ },
170
+ {
171
+ name: 'CSC_LINK naming a missing file is still the error it always was',
172
+ run: (ctx) => {
173
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-csc-'));
174
+
175
+ try {
176
+ const described = validateCerts.describeCertRef('config/certs/dev-id.p12', tmpDir);
177
+ ctx.expect(described.kind).toBe('file');
178
+ ctx.expect(described.exists).toBe(false);
179
+
180
+ const issues = [];
181
+ withEnv({ CSC_KEY_PASSWORD: 'hunter2' }, () => validateCerts.checkSigningCert(issues, 'config/certs/dev-id.p12', tmpDir));
182
+ ctx.expect(issues.length).toBe(1);
183
+ ctx.expect(issues[0].severity).toBe('error');
184
+ ctx.expect(issues[0].message).toContain('CSC_LINK points to a missing file');
185
+ } finally {
186
+ fs.rmSync(tmpDir, { recursive: true, force: true });
187
+ }
188
+ },
189
+ },
190
+ {
191
+ // Flipped by #891: a certificate nobody can open signs nothing, and the
192
+ // run that discovered this shipped an unsigned app on a warning.
193
+ name: 'a cert of any shape without CSC_KEY_PASSWORD is an ERROR',
194
+ run: (ctx) => {
195
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-csc-'));
196
+ const inline = 'MIIKvwIBAzCCCn8GCSqGSIb3DQEHAaCCCnAEggpsMIIKaDCCBR8'.repeat(60);
197
+
198
+ try {
199
+ const issues = [];
200
+ withEnv({ CSC_KEY_PASSWORD: undefined }, () => validateCerts.checkSigningCert(issues, inline, tmpDir));
201
+ ctx.expect(issues.length).toBe(1);
202
+ ctx.expect(issues[0].severity).toBe('error');
203
+ ctx.expect(issues[0].message).toContain('CSC_KEY_PASSWORD');
204
+ } finally {
205
+ fs.rmSync(tmpDir, { recursive: true, force: true });
206
+ }
207
+ },
208
+ },
209
+ // ── The three validity rungs, one rule (#892) ───────────────────────────
210
+ // The manager's certificates walk and this step read the SAME function and
211
+ // apply the same window, so a cert is never "fine" in one and stale in the
212
+ // other.
213
+ {
214
+ name: 'a real .p12 reports its expiry through the shared reader, and a healthy one raises nothing',
215
+ run: (ctx) => {
216
+ const { dir, p12Path, password } = stageSigningCert();
217
+
218
+ try {
219
+ const issues = [];
220
+ withEnv({ CSC_KEY_PASSWORD: password }, () => validateCerts.checkSigningCert(issues, p12Path, dir));
221
+ ctx.expect(issues).toEqual([]);
222
+ } finally {
223
+ fs.rmSync(dir, { recursive: true, force: true });
224
+ }
225
+ },
226
+ },
37
227
  {
38
- name: 'parseProvision extracts plist from CMS-wrapped file',
228
+ name: 'under 30 days WARNS, expired ERRORS, and a healthy date says nothing',
39
229
  run: (ctx) => {
40
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-test-'));
41
- const profPath = path.join(tmpDir, 'test.provisionprofile');
42
- fs.writeFileSync(profPath, makeMockProvision({
43
- appId: 'TEAMID.com.itwcw.testapp',
44
- expirationDate: new Date('2099-01-01T00:00:00Z'),
45
- }));
230
+ const days = (n) => ({ expiresAt: new Date(Date.now() + n * 86400000), daysLeft: n });
231
+
232
+ ctx.expect(validateCerts.expiryRung('config/certs/dev-id.p12', days(200))).toBeNull();
233
+
234
+ const soon = validateCerts.expiryRung('config/certs/dev-id.p12', days(10));
235
+ ctx.expect(soon.severity).toBe('warn');
236
+ ctx.expect(soon.message).toContain('expires in 10 day(s)');
237
+
238
+ const expired = validateCerts.expiryRung('config/certs/dev-id.p12', days(-5));
239
+ ctx.expect(expired.severity).toBe('error');
240
+ ctx.expect(expired.message).toContain('EXPIRED on');
241
+ ctx.expect(expired.message).toContain('omega manage --service certificates');
242
+
243
+ // A profile with no date at all is not a finding
244
+ ctx.expect(validateCerts.expiryRung('x.provisionprofile', null)).toBeNull();
245
+ },
246
+ },
247
+ {
248
+ name: 'a .p12 whose password does not open it is an error, not a silent pass',
249
+ run: (ctx) => {
250
+ const { dir, p12Path } = stageSigningCert();
46
251
 
47
252
  try {
48
- const result = validateCerts.parseProvision(profPath);
49
- ctx.expect(result.parsed).toBeTruthy();
50
- ctx.expect(result.parsed['application-identifier']).toBe('TEAMID.com.itwcw.testapp');
51
- ctx.expect(result.parsed.ExpirationDate).toBeInstanceOf(Date);
52
- ctx.expect(result.raw).toContain('<plist');
253
+ const issues = [];
254
+ withEnv({ CSC_KEY_PASSWORD: 'the-wrong-password' }, () => validateCerts.checkSigningCert(issues, p12Path, dir));
255
+ ctx.expect(issues.length).toBe(1);
256
+ ctx.expect(issues[0].severity).toBe('error');
257
+ } finally {
258
+ fs.rmSync(dir, { recursive: true, force: true });
259
+ }
260
+ },
261
+ },
262
+ {
263
+ name: 'APPLE_API_KEY carrying an inline base64 key is reported, not statted',
264
+ run: (ctx) => {
265
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-key-'));
266
+ const inline = 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tTUlHVEFnRUFNQk1HQnlxR1NNNDk'.repeat(50);
267
+
268
+ try {
269
+ ctx.expect(validateCerts.describeCertRef(inline, tmpDir).kind).toBe('inline');
270
+
271
+ // An inline key has no filename, so the AuthKey_<id>.p8 cross-check has
272
+ // nothing to read, while the id and issuer are still wanted.
273
+ const issues = [];
274
+ withEnv({ APPLE_API_KEY_ID: 'ABCDE12345', APPLE_API_ISSUER: '00000000-0000-0000-0000-000000000000' }, () => {
275
+ validateCerts.checkNotarizationKey(issues, inline, tmpDir);
276
+ });
277
+ ctx.expect(issues).toEqual([]);
53
278
  } finally {
54
279
  fs.rmSync(tmpDir, { recursive: true, force: true });
55
280
  }
56
281
  },
57
282
  },
58
283
  {
59
- name: 'parseProvision returns nulls for malformed file',
284
+ name: 'an inline APPLE_API_KEY still wants its key id and issuer',
60
285
  run: (ctx) => {
61
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-test-'));
62
- const profPath = path.join(tmpDir, 'bad.provisionprofile');
63
- fs.writeFileSync(profPath, 'this is not a plist at all');
286
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-key-'));
287
+ const inline = 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tTUlHVEFnRUFNQk1HQnlxR1NNNDk'.repeat(50);
64
288
 
65
289
  try {
66
- const result = validateCerts.parseProvision(profPath);
67
- ctx.expect(result.parsed).toBeNull();
68
- ctx.expect(result.raw).toBeNull();
290
+ const issues = [];
291
+ withEnv({ APPLE_API_KEY_ID: undefined, APPLE_API_ISSUER: undefined }, () => {
292
+ validateCerts.checkNotarizationKey(issues, inline, tmpDir);
293
+ });
294
+
295
+ ctx.expect(issues.length).toBe(2);
296
+ ctx.expect(issues.map((issue) => issue.message).join(' ')).toContain('APPLE_API_KEY_ID is missing');
297
+ ctx.expect(issues.map((issue) => issue.message).join(' ')).toContain('APPLE_API_ISSUER is missing');
69
298
  } finally {
70
299
  fs.rmSync(tmpDir, { recursive: true, force: true });
71
300
  }
72
301
  },
73
302
  },
74
303
  {
75
- name: 'parseProvision handles missing file',
304
+ name: 'APPLE_API_KEY naming a missing file is still the error it always was',
76
305
  run: (ctx) => {
77
- const result = validateCerts.parseProvision('/nonexistent/path.provisionprofile');
78
- ctx.expect(result.parsed).toBeNull();
306
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-key-'));
307
+
308
+ try {
309
+ const issues = [];
310
+ withEnv({ APPLE_API_KEY_ID: 'ABCDE12345', APPLE_API_ISSUER: 'x' }, () => {
311
+ validateCerts.checkNotarizationKey(issues, 'config/certs/AuthKey.p8', tmpDir);
312
+ });
313
+
314
+ ctx.expect(issues.length).toBe(1);
315
+ ctx.expect(issues[0].severity).toBe('error');
316
+ ctx.expect(issues[0].message).toContain('APPLE_API_KEY points to a missing file');
317
+ } finally {
318
+ fs.rmSync(tmpDir, { recursive: true, force: true });
319
+ }
79
320
  },
80
321
  },
81
322
  {
82
- name: 'parseProvision raw string contains the appId for substring match logic',
323
+ name: 'a real AuthKey file still gets the filename/key-id cross-check',
83
324
  run: (ctx) => {
84
- // The validator checks `parsed.raw.includes(expectedAppId)` — confirm the raw is
85
- // searchable for that.
86
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-test-'));
87
- const profPath = path.join(tmpDir, 'app.provisionprofile');
88
- fs.writeFileSync(profPath, makeMockProvision({
89
- appId: 'TEAMID.com.itwcw.somiibo',
90
- expirationDate: new Date('2099-01-01T00:00:00Z'),
91
- }));
325
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-key-'));
326
+ fs.mkdirSync(path.join(tmpDir, 'config', 'certs'), { recursive: true });
327
+ fs.writeFileSync(path.join(tmpDir, 'config', 'certs', 'AuthKey_ABCDE12345.p8'), 'key');
92
328
 
93
329
  try {
94
- const result = validateCerts.parseProvision(profPath);
95
- ctx.expect(result.raw.includes('com.itwcw.somiibo')).toBe(true);
96
- ctx.expect(result.raw.includes('com.itwcw.different-app')).toBe(false);
330
+ const issues = [];
331
+ withEnv({ APPLE_API_KEY_ID: 'ZZZZZ99999', APPLE_API_ISSUER: 'x' }, () => {
332
+ validateCerts.checkNotarizationKey(issues, 'config/certs/AuthKey_ABCDE12345.p8', tmpDir);
333
+ });
334
+
335
+ ctx.expect(issues.length).toBe(1);
336
+ ctx.expect(issues[0].message).toContain('filename suggests key ID "ABCDE12345"');
337
+ } finally {
338
+ fs.rmSync(tmpDir, { recursive: true, force: true });
339
+ }
340
+ },
341
+ },
342
+ {
343
+ // #891: the boot derives CSC_LINK from the signing tree, so an unset key
344
+ // here means the TREE has none. The line names where it looked and the
345
+ // walk that fills it, and there is no `config/certs/` scan left to
346
+ // contradict the build beside it.
347
+ name: 'an unset CSC_LINK names the signing tree it looked in, never a certs-dir scan (#891)',
348
+ run: async (ctx) => {
349
+ const brand = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-brand-'));
350
+ fs.mkdirSync(path.join(brand, 'config'), { recursive: true });
351
+ fs.writeFileSync(path.join(brand, 'config', 'omega.json5'), '{ brand: { id: "b" } }');
352
+ const target = path.join(brand, 'targets', 'desktop');
353
+ fs.mkdirSync(target, { recursive: true });
354
+ // A .p12 sitting in the OLD dispersed location: nothing may read it.
355
+ fs.mkdirSync(path.join(target, 'config', 'certs'), { recursive: true });
356
+ fs.writeFileSync(path.join(target, 'config', 'certs', 'developer-id-application.p12'), 'stale');
357
+
358
+ const cwd = process.cwd();
359
+ process.chdir(target);
360
+ try {
361
+ const issues = [];
362
+ await withPlatform('darwin', () => withEnv({ CSC_LINK: undefined, APPLE_API_KEY: undefined, APPLE_API_KEY_ID: undefined, CSC_KEY_PASSWORD: undefined }, () => (
363
+ validateCerts.checkMac(issues, { certificates: { providers: { apple: { teamId: 'TEAMTEST12' } } } }, { execFn: () => Promise.resolve('') })
364
+ )));
365
+
366
+ const csc = issues.find((issue) => issue.message.includes('CSC_LINK'));
367
+ ctx.expect(csc.message).toContain('.omega/certificates/apple/certificates/DEVELOPER_ID_APPLICATION_G2.p12');
368
+ ctx.expect(csc.message).toContain('omega manage --service certificates');
369
+ ctx.expect(issues.map((issue) => issue.message).join(' ')).not.toContain('Found .p12 in config/certs/');
370
+ } finally {
371
+ process.chdir(cwd);
372
+ fs.rmSync(brand, { recursive: true, force: true });
373
+ }
374
+ },
375
+ },
376
+ {
377
+ // #891: electron-builder imports a CSC_LINK .p12 into its OWN temporary
378
+ // keychain, so the login keychain answers nothing about that build. The
379
+ // rung that read it anyway failed a runner whose file rungs had both just
380
+ // passed, and it now belongs to the no-CSC_LINK path alone.
381
+ name: 'a CSC_LINK cert never consults the Keychain, and an unset one still does (#891)',
382
+ run: async (ctx) => {
383
+ const { dir, p12Path, password } = stageSigningCert();
384
+ const apple = { certificates: { providers: { apple: { teamId: 'TEAMTEST12' } } } };
385
+ // A machine with no Developer ID identity at all.
386
+ const queried = [];
387
+ const execFn = (command) => { queried.push(command); return Promise.resolve(''); };
388
+ const cwd = process.cwd();
389
+ process.chdir(dir);
390
+
391
+ try {
392
+ const withCert = [];
393
+ await withPlatform('darwin', () => withEnv({ CSC_LINK: p12Path, CSC_KEY_PASSWORD: password, APPLE_API_KEY: p12Path, APPLE_API_KEY_ID: 'ABCDE12345', APPLE_API_ISSUER: 'x' }, () => (
394
+ validateCerts.checkMac(withCert, apple, { execFn })
395
+ )));
396
+
397
+ ctx.expect(queried).toEqual([]);
398
+ ctx.expect(withCert.filter((issue) => issue.severity === 'error')).toEqual([]);
399
+
400
+ const without = [];
401
+ await withPlatform('darwin', () => withEnv({ CSC_LINK: undefined, CSC_KEY_PASSWORD: undefined, APPLE_API_KEY: undefined, APPLE_API_KEY_ID: undefined }, () => (
402
+ validateCerts.checkMac(without, apple, { execFn })
403
+ )));
404
+
405
+ const keychain = without.find((issue) => issue.message.includes('macOS Keychain'));
406
+ ctx.expect(queried.length).toBe(1);
407
+ ctx.expect(keychain.severity).toBe('error');
408
+ } finally {
409
+ process.chdir(cwd);
410
+ fs.rmSync(dir, { recursive: true, force: true });
411
+ }
412
+ },
413
+ },
414
+ {
415
+ // The gate the env schema already uses for the whole mac set: a brand
416
+ // that declares no Apple signing owes no credential, so the finding is a
417
+ // note rather than the rung that stops a deploy.
418
+ name: 'the unsigned findings are ERRORS only when the config declares Apple signing (#891)',
419
+ run: async (ctx) => {
420
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-gate-'));
421
+ const cwd = process.cwd();
422
+ process.chdir(tmpDir);
423
+
424
+ try {
425
+ const declared = [];
426
+ const undeclared = [];
427
+ const execFn = () => Promise.resolve('');
428
+ await withPlatform('darwin', () => withEnv({ CSC_LINK: undefined, APPLE_API_KEY: undefined, APPLE_API_KEY_ID: undefined }, async () => {
429
+ await validateCerts.checkMac(declared, { certificates: { providers: { apple: { teamId: 'TEAMTEST12' } } } }, { execFn });
430
+ await validateCerts.checkMac(undeclared, {}, { execFn });
431
+ }));
432
+
433
+ const severityOf = (issues, key) => issues.find((issue) => issue.message.includes(key)).severity;
434
+ ctx.expect(severityOf(declared, 'CSC_LINK')).toBe('error');
435
+ ctx.expect(severityOf(declared, 'APPLE_API_KEY')).toBe('error');
436
+ ctx.expect(severityOf(undeclared, 'CSC_LINK')).toBe('warn');
437
+ ctx.expect(severityOf(undeclared, 'APPLE_API_KEY')).toBe('warn');
438
+ } finally {
439
+ process.chdir(cwd);
440
+ fs.rmSync(tmpDir, { recursive: true, force: true });
441
+ }
442
+ },
443
+ },
444
+ {
445
+ // #891: the linux and windows legs of the build workflow blank the mac
446
+ // credentials on purpose (they cannot sign for mac), so running the mac
447
+ // rungs there only prints two warnings about material nobody put on that
448
+ // leg. The rungs belong to the leg that ships mac, and to no other.
449
+ name: 'off darwin the mac rungs do not run at all (#891)',
450
+ run: async (ctx) => {
451
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-leg-'));
452
+ const cwd = process.cwd();
453
+ process.chdir(tmpDir);
454
+
455
+ try {
456
+ const queried = [];
457
+ const execFn = (command) => { queried.push(command); return Promise.resolve(''); };
458
+
459
+ for (const platform of ['linux', 'win32']) {
460
+ const issues = [];
461
+ await withPlatform(platform, () => withEnv({ CSC_LINK: undefined, APPLE_API_KEY: undefined, APPLE_API_KEY_ID: undefined, CSC_KEY_PASSWORD: undefined }, () => (
462
+ validateCerts.checkMac(issues, { certificates: { providers: { apple: { teamId: 'TEAMTEST12' } } } }, { execFn })
463
+ )));
464
+
465
+ ctx.expect(issues).toEqual([]);
466
+ }
467
+
468
+ ctx.expect(queried).toEqual([]);
97
469
  } finally {
470
+ process.chdir(cwd);
98
471
  fs.rmSync(tmpDir, { recursive: true, force: true });
99
472
  }
100
473
  },