@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
package/docs/signing.md CHANGED
@@ -5,52 +5,67 @@ Cross-platform code signing reference. Covers macOS (sign + notarize), Windows (
5
5
  ## tl;dr
6
6
 
7
7
  - **macOS production**: Developer ID Application cert + notarization API key. Files in `config/certs/`. Env vars point at them.
8
- - **Windows production**: EV USB token (self-hosted runner now) or cloud signing (future, pluggable via `platforms.win.signing.strategy`).
8
+ - **Windows production**: EV USB token (self-hosted runner now) or cloud signing (future, pluggable via `platforms.windows.signing.strategy`).
9
9
  - **Linux**: No signing for AppImage/deb. Snap/Flatpak have their own pipelines.
10
10
 
11
11
  ## Where files live
12
12
 
13
+ Signing material lives in the SIGNING TREE and is read there, never copied into a target
14
+ ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)):
15
+
13
16
  ```
17
+ <company brand>/company/.omega/certificates/apple/ # read FIRST when the brand names a company
18
+ <your brand>/.omega/certificates/apple/ # read second
19
+ certificates/DEVELOPER_ID_APPLICATION_G2.p12 # what CSC_LINK resolves to
20
+ certificates/DEVELOPER_ID_INSTALLER_G2.p12 # only for .pkg builds (rare)
21
+ AuthKey_XXXXXXXXXX.p8 # what APPLE_API_KEY resolves to
22
+
14
23
  <your-app>/
15
24
  build/
16
- entitlements.mac.plist # universal — already in @omega.js/desktop defaults
25
+ entitlements.mac.plist # universal, already in @omega.js/desktop defaults
17
26
  icon.icns / icon.ico / icon.png # per-brand
18
- certs/ # gitignored
19
- developer-id-application.p12 # universal across team
20
- developer-id-installer.p12 # only for .pkg builds (rare)
21
- AuthKey_XXXXXXXXXX.p8 # universal across team
22
- <app-name>.provisionprofile # per-app (only if entitlements require)
23
- .env # gitignored — env vars referencing config/certs/*
27
+ config/certs/ # gitignored, and EMPTY on a developer machine:
28
+ # only a RUNNER writes here, decoding the pushed secrets
29
+ README.md # the ONE tracked file here (#913): it says the above
30
+ .env # gitignored, and it carries no signing PATH: only
31
+ # CSC_KEY_PASSWORD and the APPLE_* three
24
32
  ```
25
33
 
34
+ There is no provisioning profile: Developer ID is direct distribution, signed and notarized,
35
+ and needs none.
36
+
26
37
  ## Where the macOS certificate comes from (the lookup order)
27
38
 
28
- A signed build resolves its Developer ID Application certificate in ONE order
29
- ([src/utils/resolve-signing-cert.js](../src/utils/resolve-signing-cert.js), applied at the
30
- gulp boundary):
39
+ Every desktop boot (the CLI and gulp alike) derives the signing paths ONCE, at the env load
40
+ ([src/utils/load-env.js](../src/utils/load-env.js), over `@omega.js/devkit/signing-env`), so
41
+ the build, `omega validate-certs` and the deploy precheck's secret publish all read the same
42
+ answer. `CSC_LINK` resolves in ONE order:
31
43
 
32
44
  1. **`CSC_LINK`** in the environment — an explicit answer always wins.
33
- 2. **The brand's signing tree** —
34
- `<brandRoot>/.omega/certificates/apple/certificates/DEVELOPER_ID_APPLICATION_G2.p12`,
35
- where the brand root is the nearest directory at or above the target carrying a `.omega/`.
36
- This is the portable/CI path: `@omega.js/manager`'s certificates service produces the
37
- tree ([the manager guide](../../../docs/manager/index.md)).
38
- 3. **The company's tree**, when the brand is company-managed. A brand is NEVER physically
39
- inside its company folder — membership is the `.omega/company.json` stamp at the brand
40
- root, read through `@omega.js/config`'s `readCompanyRoot` (the ecosystem's one company
41
- rule). The stamp's target is used as given: it is explicit config, not discovery.
45
+ 2. **The COMPANY's tree**, when the brand names a company and that company is on this
46
+ machine: a brand with a company reuses the company tree first, always
47
+ ([#677](https://github.com/Omega-JS-Stack/omega/issues/677)). A brand is NEVER physically
48
+ inside its company folder: membership is its `company: { id }` key, resolved through
49
+ `@omega.js/config`'s `resolveCompany` (the ecosystem's one company rule), and the
50
+ certificate is one more relative path through that resolver's `file()`:
51
+ `<company>/company/.omega/certificates/apple/certificates/DEVELOPER_ID_APPLICATION_G2.p12`.
52
+ 3. **The brand's own signing tree**:
53
+ `<brandRoot>/.omega/certificates/apple/certificates/DEVELOPER_ID_APPLICATION_G2.p12`.
54
+ `@omega.js/manager`'s certificates service produces the tree
55
+ ([the manager guide](../../../docs/manager/index.md)).
42
56
  4. **The macOS Keychain** — electron-builder's own identity auto-discovery.
43
57
 
58
+ `APPLE_API_KEY` follows the same order for `AuthKey_<APPLE_API_KEY_ID>.p8` (an unset id means
59
+ there is no filename to look for). Both derived values are ABSOLUTE paths into the tree.
60
+
44
61
  **Keychain stays the default.** A project with no signing tree resolves to it exactly as
45
62
  before. Two further conditions keep a build on the Keychain rather than handing
46
63
  electron-builder a file it cannot use: no `CSC_KEY_PASSWORD` in the `.env` cascade, or a
47
64
  password that does not OPEN the `.p12` (verified with `openssl pkcs12 -legacy`, retried
48
65
  without the flag for LibreSSL — stock macOS openssl — which reads legacy containers but
49
66
  rejects the flag; a missing openssl is named as the reason rather than blamed on the
50
- password). The build log names the source it picked, and the reason, on every run.
51
-
52
- The brand-root walk stops **below the home directory**: `~/.omega` is a real personal
53
- overlay on developer machines, so a tree at or above `~` is never a signing tree.
67
+ password). Either case prints one line naming the file and the reason, and leaves the key
68
+ unset rather than failing the build silently.
54
69
 
55
70
  The password itself never lives in config: `CSC_KEY_PASSWORD` comes from the `.env`
56
71
  cascade (the manager's certificates service writes it to the signing root's `.env`),
@@ -110,11 +125,13 @@ npx omega validate-certs
110
125
  ```
111
126
 
112
127
  This checks:
113
- - macOS Keychain has a Developer ID Application identity
114
128
  - `CSC_LINK` exists and is readable
129
+ - macOS Keychain has a Developer ID Application identity, and ONLY when `CSC_LINK` names no certificate: electron-builder imports a `CSC_LINK` `.p12` into its own temporary keychain, so the login keychain says nothing about that build
115
130
  - API key file exists and `APPLE_API_KEY_ID` + `APPLE_API_ISSUER` are set
116
131
  - Team ID matches the cert
117
132
 
133
+ The electron-builder floor is 26.16.1 (the package's peer range, installed by ensure-target for any consumer below it): every earlier build hands the `.p12` password to `security set-key-partition-list`, which wants the temporary keychain's own password, and the macos-26 runner image rejects the mismatch (electron-userland/electron-builder#10066, [#891](https://github.com/Omega-JS-Stack/omega/issues/891)).
134
+
118
135
  ### 5. Build + notarize
119
136
 
120
137
  ```bash
@@ -141,8 +158,15 @@ Buy an EV code-signing cert from Sectigo, DigiCert, SSL.com, etc. Get a physical
141
158
 
142
159
  Setting the box up is [`docs/runner.md`](runner.md) — the runner home, the Startup-folder auto-start, the commands, and the Session 0 / Session 1 rule that makes the whole thing work.
143
160
 
161
+ **A workflow that targets this runner fires only on dispatch**, and the box enforces it ([#875](https://github.com/Omega-JS-Stack/omega/issues/875)). Whoever can start a job on the signing box can sign binaries with your token, so two guards stand between a stray trigger and `signtool`:
162
+
163
+ - The generated `windows-sign` job runs only on `workflow_dispatch`, the one trigger the file carries (#923). A `push` or `pull_request` trigger added to `build.yml` later SKIPS the sign job instead of signing what it built.
164
+ - The runner itself refuses the job before its first step. `npx omega runner install` writes `job-started.js` beside the runner and points every registration's `ACTIONS_RUNNER_HOOK_JOB_STARTED` at it; the hook exits nonzero, failing the job with one line, unless the event is a dispatch, `GITHUB_REPOSITORY` is on `allowed-repos.txt` and `GITHUB_ACTOR` is on `allowed-actors.txt`. Both lists live on the box and are yours to edit ([`docs/runner.md`](runner.md) § One-time setup).
165
+
144
166
  `npx omega sign-windows` is the strategy-aware command that drives `signtool` (or the cloud provider CLI).
145
167
 
168
+ Every signed artifact is verified before the job moves on, and the log says so in the mac lane's shape: `Signed and verified: signtool verify /pa accepts <file>.` mirrors macOS's `Stapled and verified: ...`, so a deploy log proves the Windows signature was checked rather than only claimed ([#918](https://github.com/Omega-JS-Stack/omega/issues/918)). A failing verify aborts the job.
169
+
146
170
  ### Cloud signing (future migration)
147
171
 
148
172
  Once the framework's cloud strategy is finalized, set the provider in `config/omega.json5`:
@@ -163,22 +187,25 @@ If no signing runner is available, CI uploads the unsigned `.exe` and a develope
163
187
 
164
188
  ## Pushing secrets to GitHub Actions
165
189
 
166
- @omega.js/desktop ships a command that pushes the target's **composed env** to the repo's GitHub Actions secrets over the same shared `gh` transport that the web and extension publish verbs use (`gh secret set`, which seals each value with the repo's public key locally): values travel on **stdin**, never in argv, and are never logged. For env vars whose value is a path to a local file (`.p12`, `.p8`, etc.), the secret value pushed is the **base64-encoded file contents** — the workflow decodes back to a temp file at job start.
190
+ `omega deploy`'s precheck pushes the target's **composed env** to the repo's GitHub Actions secrets over the same shared `gh` transport the other three frameworks use (`gh secret set`, which seals each value with the repo's public key locally): values travel on **stdin**, never in argv, and are never logged. For env vars whose value is a path to a local file (`.p12`, `.p8`, etc.), the secret value pushed is the **base64-encoded file contents**: the workflow decodes back to a temp file at job start.
191
+
192
+ There is no standalone `omega push-secrets` verb ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)): the push happens where it matters, and the manage walk's `repo` service runs the same function when you want a brand brought current without deploying.
167
193
 
168
194
  ```bash
169
- # Make sure the brand root's .env holds your signing creds, and that `gh auth login` has run
170
- npx omega push-secrets
195
+ # Make sure the brand root's .env holds CSC_KEY_PASSWORD + the APPLE_* three, and `gh auth login` has run
196
+ npx omega deploy
171
197
 
172
- # Push only specific keys
173
- npx omega push-secrets --only=CSC_LINK,CSC_KEY_PASSWORD
198
+ # See exactly what it would publish, and publish nothing
199
+ npx omega deploy --dry-run
174
200
  ```
175
201
 
176
202
  Behavior:
177
203
  - The source is `composeTargetEnv({ targetDir, target: 'desktop' })`: company `.env` ← brand `.env` ← target `.env`, the brand-side layers filtered by the env schema to the keys the desktop target reads. The brand root's `.env` is the one file you keep; a target `.env` is an optional per-key override.
178
204
  - Files only — the shell is never a source — and an empty value never claims a key, so unset keys simply don't publish.
179
205
  - Auto-detects "is this a path?" — relative or absolute paths ending in `.p12`/`.pem`/`.cer`/`.p8`/`.provisionprofile`/`.crt`/`.key`/`.json` that exist on disk (target root first, brand root second) get base64-encoded.
180
- - Publishes to the git remote's `owner/repo`, and REFUSES unless the brand's own config claims it (`repo.providers.github`) — a fork, a template clone or a vendored target never arms a stranger's Actions with your certificates.
206
+ - Publishes to the brand's SOURCE repo (`repo.org` + `brand.id` → `<brand.id>-omega`), and REFUSES unless the checkout's own remote IS that repo: a fork, a template clone or a vendored target never arms a stranger's Actions with your certificates.
181
207
  - No PAT: the credential is `gh`'s auth session. A missing or signed-out `gh` fails loudly with install/auth instructions; a CI run, an empty cascade, a remote-less checkout or a repo mismatch skips loudly.
208
+ - `CSC_LINK` and `APPLE_API_KEY` are DERIVED, never typed ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)): the env load resolved them from the signing tree, so the publish sends those files' bytes. A typed value still wins, and a key the config REQUIRES that neither the tree nor the `.env` answers STOPS the deploy (the step is `fatal`), naming both tier paths it looked in and `omega manage --service certificates`, publishing nothing.
182
209
  - Logs key NAMES only; never a value.
183
210
 
184
211
  The corresponding decode step in CI looks like:
@@ -211,7 +238,6 @@ GitHub Actions secrets you'll need (per repo):
211
238
  | Secret | Purpose |
212
239
  |---|---|
213
240
  | `GH_TOKEN` | Cross-repo publish, secret rotation |
214
- | `OMEGA_ADMIN_KEY` | Privileged backend API calls |
215
241
  | `CSC_LINK` | base64-encoded `.p12` (workflow decodes to file) |
216
242
  | `CSC_KEY_PASSWORD` | `.p12` password |
217
243
  | `APPLE_API_KEY` | base64-encoded `.p8` |
@@ -251,7 +277,7 @@ The workflow base64-decodes secrets into temp files inside `config/certs/` at jo
251
277
 
252
278
  ### Windows: "SignTool Error: No certificates were found"
253
279
  - Token unplugged or middleware not running.
254
- - For cloud: verify `platforms.win.signing.cloud.provider` in `config/omega.json5` matches the provider whose creds are in `.env`.
280
+ - For cloud: verify `platforms.windows.signing.cloud.provider` in `config/omega.json5` matches the provider whose creds are in `.env`.
255
281
 
256
282
  ## What lives in `build/`
257
283
 
@@ -45,6 +45,7 @@ npx omega test --filter="storage" # only suites/tests whose name contains
45
45
  npx omega test --extended # opt into extended mode — real external APIs (also: TEST_EXTENDED_MODE=true)
46
46
  npx omega test --reporter=json # pretty output + machine-readable {"event":"summary",...} line
47
47
  OMEGA_TEST_DEBUG=1 npx omega test # see Electron stderr (otherwise drained silently)
48
+ OMEGA_TEST_BOOT_TIMEOUT_MS=120000 npx omega test # the boot lane's budget (default 60s)
48
49
  ```
49
50
 
50
51
  In @omega.js/desktop itself, `npm test` runs the framework's own suite — the self-test context flips the no-target default.
@@ -104,13 +105,13 @@ A target that names a path and matches NO file is a hard error: the run prints `
104
105
 
105
106
  ### Extended vs normal mode
106
107
 
107
- Suites that hit a live backend (Firebase Auth admin SDK, real GitHub API, etc.) are gated behind **extended mode** — `npx omega test --extended` or `TEST_EXTENDED_MODE=true`. `TEST_EXTENDED_MODE` is the **shared, unprefixed env var across @omega.js/backend, @omega.js/extension, UJM, and @omega.js/desktop** (cross-framework parity) — once set on `process.env` it propagates to every spawned test environment (the Electron main/renderer/boot children, the gulp boot build) automatically via `{ ...process.env }`. These external calls are **skipped in-source, NOT mocked** — the suite short-circuits / `ctx.skip()`s when `TEST_EXTENDED_MODE` is unset; with it set, it calls the real service. Default is to skip them so `npx omega test` is fast + green offline, and a warning prints when extended mode is on. The CI workflow runs normal mode by default; add a separate workflow that sets `TEST_EXTENDED_MODE: 'true'` for extended coverage.
108
+ Suites that hit a live backend (a real GitHub API call, a store publish, etc.) are gated behind **extended mode**: `npx omega test --extended` or `TEST_EXTENDED_MODE=true`. `TEST_EXTENDED_MODE` is the **shared, unprefixed env var across @omega.js/backend, @omega.js/extension, UJM, and @omega.js/desktop** (cross-framework parity): once set on `process.env` it propagates to every spawned test environment (the Electron main/renderer/boot children, the gulp boot build) automatically via `{ ...process.env }`. These external calls are **skipped in-source, NOT mocked**: the suite short-circuits / `ctx.skip()`s when `TEST_EXTENDED_MODE` is unset; with it set, it calls the real service. Default is to skip them so `npx omega test` is fast + green offline, and a warning prints when extended mode is on. The CI workflow runs normal mode by default; add a separate workflow that sets `TEST_EXTENDED_MODE: 'true'` for extended coverage.
108
109
 
109
110
  Anything an extended suite creates externally must be torn down in its `cleanup(ctx)` — the harness only resets local Electron state between runs, never external systems.
110
111
 
111
- ### `OMEGA_TEST_MODE=true` — the canonical "we're in tests" signal
112
+ ### `OMEGA_ENVIRONMENT=testing`, the one input a test run names
112
113
 
113
- Both @omega.js/desktop test runners (`runners/electron.js`, `runners/boot.js`) set `OMEGA_TEST_MODE=true` in the spawned child env. That powers `manager.isTesting()` (and `Manager.isTesting()` static) — the cross-context helper everything in @omega.js/desktop checks when it needs to behave differently in tests:
114
+ Both @omega.js/desktop test runners (`runners/electron.js`, `runners/boot.js`) spawn their child with `OMEGA_ENVIRONMENT=testing`, the ONE environment input ([#817](https://github.com/Omega-JS-Stack/omega/issues/817)), and nothing writes over an explicit one: the word a build baked into the artifact is the fallback for a packaged app, never an override ([#925](https://github.com/Omega-JS-Stack/omega/issues/925)). That powers `manager.isTesting()` (and the `Manager.isTesting()` static), the cross-context helper everything in @omega.js/desktop checks when it needs to behave differently in tests:
114
115
 
115
116
  - `auto-updater` flips its idle threshold from 15min → 3s and its periodic tick from 60s → 500ms, AND short-circuits the native install-prompt dialog (so tests don't pop modal windows).
116
117
  - **Every BrowserWindow surfaces stealth** — named windows via `window-manager._surface()` AND raw `new BrowserWindow()` ones (e.g. a consumer's automation popup) via a global `browser-window-created` hook registered in `main.js` step 1a-ii. The shared recipe lives in `src/utils/stealth-window.js`: shown INACTIVE (keyboard focus never leaves your editor), opacity 0, click-through (`setIgnoreMouseEvents`), and raw windows get `show()` rerouted to `showInactive()` + `focus()` no-op'd — a test run never interrupts you, and a stray real click physically can't land in the app, while synthetic test input (`executeJavaScript`, `sendInputEvent`, CDP) is unaffected. **`webContents.focus()` is suppressed separately** (a global `web-contents-created` hook in the same main.js block): it bypasses the window-level patches — it's a different object whose `focus()` reaches the native window directly and makes the invisible window KEY, grabbing the keyboard mid-typing even under the accessory policy (which only prevents *launch* activation, not key-window steals). Consumers call it legitimately (e.g. address-bar focus on tab select), so it's no-op'd per-contents under the same predicate. Set **`OMEGA_TEST_SHOW=1`** to surface windows normally and watch a run live (the predicate is evaluated per window, so flipping it mid-run works). Deliberately NOT `hide()`/`minimize()`: occluded windows get throttled by Chromium (`requestAnimationFrame` pauses, `document.visibilityState` flips to `hidden`) — tests would exercise a DIFFERENT runtime, whereas an opacity-0 shown-inactive window renders and behaves identically to a visible one. See [windows.md](windows.md).
@@ -119,9 +120,9 @@ Both @omega.js/desktop test runners (`runners/electron.js`, `runners/boot.js`) s
119
120
  - **The main-layer harness exposes a CDP endpoint.** `test/harness/main-entry.js` appends `--remote-debugging-port=0` at require time (loopback-only, OS-assigned port) and publishes the resolved port as **`process.env.OMEGA_CDP_PORT`** before suites run (read from Chromium's `DevToolsActivePort` file; any value inherited from your shell is overwritten — that one points at your dev app, not the harness). Consumer suites can drive real browser automation against the harness Electron itself, e.g. `playwright-core`'s `connectOverCDP('http://127.0.0.1:' + process.env.OMEGA_CDP_PORT)`. Covered by `suites/main/harness-cdp.test.js`.
120
121
  - Other lib code can branch on `manager.isTesting()` to suppress dock bounce, login-item changes, OS protocol-handler registration, etc.
121
122
 
122
- Consumers writing their own tests should set `OMEGA_TEST_MODE=true` in their test runner so the same signal applies — for example, in `package.json`:
123
+ Consumers writing their own tests name the same input in their own runner, for example in `package.json`:
123
124
  ```json
124
- "test": "OMEGA_TEST_MODE=true vitest"
125
+ "test": "OMEGA_ENVIRONMENT=testing vitest"
125
126
  ```
126
127
  Then in your code, gate test-only behavior on `manager.isTesting()` instead of inventing yet another env var.
127
128
 
@@ -295,6 +296,10 @@ All test output is also teed (ANSI-stripped) to `<projectRoot>/logs/test.log`, t
295
296
  - `ELECTRON_RUN_AS_NODE` is stripped from the spawn env (would otherwise make Electron behave as Node and break the harness).
296
297
  - **Consumer-scheme containment.** The consumer's `main.js` — where the real `protocol.handle('<brand.id>', …)` lives — never runs in this harness, so `brand://` URLs loaded by main-layer suites would be UNHANDLED. Chromium treats a load on an unhandled scheme as an *external protocol* and hands it to the OS — and if an installed copy of the app owns the scheme (e.g. `/Applications/Brand.app` on macOS, registered via its `Info.plist`), Launch Services launches the installed production app mid-test-run. The harness contains this two ways after Manager boots: (1) it registers a stub handler for the consumer's brand scheme (read from `<cwd>/config/omega.json5`) on the default session, so `brand://` loads commit in-process as a blank page; (2) it denies the `openExternal` permission on the default session and every session created after it (partitions included), so no navigation can hand ANY scheme to the OS during a test run. Suites that need real page content for `brand://` URLs belong in the boot layer, where the real app (and its real protocol handler) runs. A main-layer suite that genuinely needs its OWN handler for the brand scheme must call `protocol.unhandle(brand.id)` first — the harness stub holds the scheme on the default session.
297
298
 
299
+ ## A test run never touches the OS keychain, and a blocked boot reports
300
+
301
+ The harness resolves auth persistence to the **`none`** strategy on every layer, main and boot alike, before any config is read: a brand that declares `omega.authPersistence: 'safeStorage'` still runs its tests with Firebase auth in memory, because the lanes never sign a real user in. That is a hard rule, not a nicety. The boot lane spawns the raw, unsigned `Electron` binary, which carries no keychain ACL, so one `safeStorage` read parks the entire run behind a macOS SecurityAgent prompt ([#907](https://github.com/Omega-JS-Stack/omega/issues/907)). ONE signal answers "is this a test run" on both layers, `manager.isTesting()`: the boot lane boots the consumer's PRODUCTION artifact, and the word baked into it no longer writes over the `OMEGA_ENVIRONMENT=testing` the runner spawned it with ([#925](https://github.com/Omega-JS-Stack/omega/issues/925)), so the booted app reports the lane it is running in. And since a boot can block long before `main.js` requires the harness (where the per-test timeout lives), the boot runner carries its own budget, measured as SILENCE: every harness line rearms it, so a suite that legitimately runs longer keeps going, and a boot that produces no harness output for 60s (`OMEGA_TEST_BOOT_TIMEOUT_MS` overrides) counts its tests failed, prints `✗ boot: no harness output after 60s; last runtime.log line: "<line>"` from the booted app's `logs/runtime.log`, and ends the child instead of hanging `npx omega test`.
302
+
298
303
  ## Writing consumer tests
299
304
 
300
305
  In a consumer project, drop files in `test/` (or `test/**`):
package/docs/themes.md CHANGED
@@ -19,18 +19,32 @@ The active theme is selected via `config.theme.id` (default `'classy'`). The `gu
19
19
  Your `src/assets/scss/main.scss` becomes:
20
20
 
21
21
  ```scss
22
+ // Generated from `brand.color` in config/omega.json5 by the sass task.
23
+ @use 'brand';
24
+
22
25
  @use 'omega-desktop' as * with (
23
- $primary: #2563EB,
26
+ $primary: brand.$primary,
24
27
  // $secondary: #6C757D,
25
28
  // $border-radius: 0.5rem,
26
29
  );
27
30
 
31
+ // The runtime --omega-accent ramp, AFTER the framework import so it wins the
32
+ // cascade over the token sheet's placeholders.
33
+ @include brand.ramp;
34
+
28
35
  // Custom global styles below...
29
36
  main {
30
37
  padding: 2rem;
31
38
  }
32
39
  ```
33
40
 
41
+ `$primary` is `brand.color`, not a literal: the sass task writes
42
+ `dist/assets/scss/_brand.scss` from the resolved config before every compile, so
43
+ recoloring the app is a config edit. Want an accent that DIVERGES from the
44
+ brand? Put the literal back in its place: `$primary: #2563EB,`. No `brand.color`
45
+ set at all, and the partial carries the framework default
46
+ ([shared/theming.md](shared/theming.md), [#912](https://github.com/Omega-JS-Stack/omega/issues/912)).
47
+
34
48
  That single import gives you:
35
49
  - Full Bootstrap 5 (utilities, components, grid, etc.)
36
50
  - Classy theme overlays (typography, animations, refined spacing)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omega.js/desktop",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
4
4
  "description": "OMEGA desktop framework — build, test, and package Electron apps for macOS, Windows, and Linux",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -17,6 +17,7 @@
17
17
  "./build": "./dist/build.js",
18
18
  "./test": "./dist/test/index.js",
19
19
  "./hooks/notarize": "./dist/hooks/notarize.js",
20
+ "./hooks/notarize-artifacts": "./dist/hooks/notarize-artifacts.js",
20
21
  "./lib/app-state": "./dist/lib/app-state.js",
21
22
  "./lib/auto-updater": "./dist/lib/auto-updater.js",
22
23
  "./lib/context-menu": "./dist/lib/context-menu.js",
@@ -35,13 +36,13 @@
35
36
  "./lib/client-bridge": "./dist/lib/client-bridge.js",
36
37
  "./lib/window-manager": "./dist/lib/window-manager.js",
37
38
  "./cli": "./dist/cli-run.js",
39
+ "./ensure-target": "./dist/commands/lib/ensure-target.js",
38
40
  "./scripts/sync-nvmrc": "./scripts/sync-nvmrc.js"
39
41
  },
40
42
  "bin": {
41
43
  "omega": "./bin/omega",
42
44
  "omg": "./bin/omg",
43
- "mgr": "./bin/omega",
44
- "omega-desktop": "./bin/omega-desktop"
45
+ "mgr": "./bin/omega"
45
46
  },
46
47
  "files": [
47
48
  "bin/",
@@ -56,8 +57,8 @@
56
57
  "replace": {},
57
58
  "hooks": {
58
59
  "after": [
59
- "node -e \"require('@omega.js/devkit/vendor')()\"",
60
- "node -e \"require('@omega.js/devkit/prepare-guard').rewire()\""
60
+ "node -e \"require('@omega.js/devkit/prepare-guard').rewire()\"",
61
+ "node -e \"require('@omega.js/devkit/vendor')()\""
61
62
  ],
62
63
  "afterBlocking": true
63
64
  }
@@ -89,14 +90,19 @@
89
90
  "package": "@omega.js/web",
90
91
  "from": "themes",
91
92
  "to": "assets/themes"
93
+ },
94
+ {
95
+ "package": "@omega.js/devkit",
96
+ "from": "src/test/fixtures/esm-only-package",
97
+ "to": "vendor/devkit/test/fixtures/esm-only-package"
92
98
  }
93
99
  ]
94
100
  },
95
101
  "scripts": {
96
102
  "start": "npm run prepare:watch",
97
- "prepare": "node -e \"require('@omega.js/devkit/prepare-guard')() && require('prepare-package')()\"",
103
+ "prepare": "node -e \"require('../devkit/tools/prepare-guard').run()\"",
98
104
  "prepare:watch": "node -e \"require('prepare-package/watch')()\"",
99
- "test": "node ./bin/omega-desktop test"
105
+ "test": "node ./bin/omega test"
100
106
  },
101
107
  "projectScripts": {
102
108
  "start": "omega clean && npm run gulp --",
@@ -130,7 +136,7 @@
130
136
  "license": "Elastic-2.0",
131
137
  "peerDependencies": {
132
138
  "electron": ">=42",
133
- "electron-builder": ">=26",
139
+ "electron-builder": ">=26.16.1",
134
140
  "gulp": "^5.0.1"
135
141
  },
136
142
  "devDependencies": {
@@ -141,7 +147,7 @@
141
147
  "@omega.js/monitoring": "*",
142
148
  "@omega.js/web": "*",
143
149
  "electron": "^42.0.1",
144
- "electron-builder": "^26.8.1",
150
+ "electron-builder": "^26.16.1",
145
151
  "firebase-admin": "^13.9.0",
146
152
  "prepare-package": "^2.2.0"
147
153
  },
@@ -149,7 +155,7 @@
149
155
  "@fortawesome/fontawesome-free": "^7.3.0",
150
156
  "@inquirer/prompts": "^8.5.2",
151
157
  "@octokit/rest": "^22.0.1",
152
- "@omega.js/client": "0.50.0",
158
+ "@omega.js/client": "0.51.0",
153
159
  "@popperjs/core": "^2.11.8",
154
160
  "@sentry/electron": "^7.13.0",
155
161
  "chalk": "^5.6.2",
@@ -172,11 +178,11 @@
172
178
  "node-powertools": "^3.0.0",
173
179
  "plist": "^4.0.0",
174
180
  "sass": "^1.99.0",
181
+ "sharp": "^0.35.3",
175
182
  "uuid": "^14.0.0",
176
183
  "wonderful-fetch": "^2.0.5",
177
184
  "wonderful-version": "^1.3.2",
178
- "ws": "^8.20.0",
179
- "yargs": "^18.0.0"
185
+ "ws": "^8.20.0"
180
186
  },
181
187
  "optionalDependencies": {
182
188
  "automately": "^1.0.5"
package/bin/omega-desktop DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- require('../dist/cli-run.js').run();
@@ -1,141 +0,0 @@
1
- /**
2
- * The `.env` → GitHub Actions secrets publisher for the desktop target.
3
- *
4
- * Since [#682](https://github.com/Omega-JS-Stack/omega/issues/682) this is a
5
- * BIND of `@omega.js/devkit/target-secrets`, exactly as web and the extension
6
- * are: the same derive (the schema's desktop delivery set, #627) → collect (the
7
- * COMPOSED target env, company ← brand ← target, files only, #678) → guard (the
8
- * brand's DECLARED repo, never an inferred remote) → publish, over the ONE `gh`
9
- * transport (values on stdin, never logged), with the same five loud skips. It
10
- * used to run @octokit/rest + libsodium-wrappers itself on a GH_TOKEN PAT the
11
- * consumer had to mint; `gh`'s own auth session is the credential now and the
12
- * sealed-box crypto is the CLI's.
13
- *
14
- * What is desktop's OWN is the value resolver, the seam the shared publisher
15
- * applies between collect and publish: a desktop secret can name a FILE
16
- * (`CSC_LINK=config/certs/dev-id.p12`, `APPLE_API_KEY=.omega/secrets/key.p8` —
17
- * target-root relative, falling back to the brand root), and CI needs the
18
- * BYTES, so an existing file publishes as its base64 contents. The workflow
19
- * decodes it back to a temp file at job start.
20
- *
21
- * Usage:
22
- * npx omega push-secrets # push every composed desktop key
23
- * npx omega push-secrets --only=GH_TOKEN,CSC_LINK
24
- */
25
-
26
- const path = require('path');
27
- const fs = require('fs');
28
-
29
- const Manager = new (require('../build.js'));
30
- const logger = Manager.logger('push-secrets');
31
- const { collectTargetSecrets, declaredBrandRepo: declaredRepoFor, publishTargetSecrets } = require('../vendor/devkit/target-secrets.js');
32
- const { findBrandRoot } = require('../vendor/config/index.js');
33
-
34
- const TARGET = 'desktop';
35
-
36
- module.exports = async function (options) {
37
- options = options || {};
38
-
39
- return publishEnvSecrets({
40
- targetDir: process.cwd(),
41
- logger,
42
- only: options.only,
43
- });
44
- };
45
-
46
- /**
47
- * Desktop's value seam: base64 for a value that names an existing file, the
48
- * value itself otherwise, and null for a key `--only` leaves out (the publisher
49
- * drops a falsy value, the same way it drops an empty composed one).
50
- *
51
- * @param {object} options
52
- * @param {string} options.targetDir - The target root.
53
- * @param {string} [options.only] - Comma-separated key names to narrow to.
54
- * @returns {function} `(value, key) => string|null`
55
- */
56
- function fileValueResolver(options) {
57
- const { targetDir } = options;
58
- const only = options.only ? String(options.only).split(',').map((s) => s.trim()).filter(Boolean) : null;
59
- const brandRoot = findBrandRoot(targetDir);
60
-
61
- return (value, key) => {
62
- if (only && !only.includes(key)) return null;
63
- return fileContentsBase64(value, { targetDir, brandRoot }) || value;
64
- };
65
- }
66
-
67
- // The base64 contents of the file `value` names, or null when it names none.
68
- // Heuristic: a path-shaped value (a separator, or a typical cert/key extension)
69
- // that resolves to a real file under the target root or the brand root. A
70
- // path-shaped value that exists nowhere is pushed as-is — it is a string.
71
- function fileContentsBase64(value, { targetDir, brandRoot }) {
72
- const looksLikePath = /[/\\]/.test(value) || /\.(p12|pem|cer|p8|provisionprofile|crt|key|json)$/i.test(value);
73
- if (!looksLikePath) return null;
74
-
75
- const roots = path.isAbsolute(value) ? [''] : [targetDir, brandRoot].filter(Boolean);
76
- for (const root of roots) {
77
- const absolute = path.isAbsolute(value) ? value : path.join(root, value);
78
- if (fs.existsSync(absolute) && fs.statSync(absolute).isFile()) {
79
- return fs.readFileSync(absolute).toString('base64');
80
- }
81
- }
82
-
83
- return null;
84
- }
85
-
86
- /**
87
- * The publishable secrets: the schema's desktop delivery set, valued from the
88
- * composed env with file-path values resolved to their base64 contents. An
89
- * empty value never claims a key.
90
- *
91
- * @param {string} targetDir - The target root (its .env is the local layer).
92
- * @param {object} [options]
93
- * @param {string} [options.only] - Comma-separated key names to narrow to.
94
- * @returns {Object<string, string>} key → value, ready to publish.
95
- */
96
- function collectEnvSecrets(targetDir, options) {
97
- return collectTargetSecrets({
98
- targetDir,
99
- target: TARGET,
100
- resolveValue: fileValueResolver({ targetDir, only: (options || {}).only }),
101
- });
102
- }
103
-
104
- /**
105
- * The brand's OWN repo as `owner/name`, from the composed config. Null when the
106
- * config declares nothing usable or doesn't load.
107
- *
108
- * @param {string} targetDir - The target root.
109
- * @returns {string|null}
110
- */
111
- function declaredBrandRepo(targetDir) {
112
- return declaredRepoFor({ targetDir, target: TARGET });
113
- }
114
-
115
- /**
116
- * The `omega deploy` precheck step: publish the target's composed values as
117
- * repo Actions secrets.
118
- *
119
- * @param {object} options
120
- * @param {string} options.targetDir - The target root.
121
- * @param {object} options.logger - `{ log, warn, error }`.
122
- * @param {object} [options.env] - Env map (default: process.env).
123
- * @param {string} [options.only] - Comma-separated key names to narrow to (`--only`).
124
- * @param {function} [options.execFn] - Injectable `gh` exec (tests).
125
- * @param {function} [options.gitExecFn] - Injectable `git` exec for remote discovery (tests).
126
- * @returns {{ skipped: string }|{ published: string[], failed: Array<object> }}
127
- */
128
- function publishEnvSecrets(options) {
129
- options = options || {};
130
-
131
- return publishTargetSecrets({
132
- ...options,
133
- target: TARGET,
134
- resolveValue: fileValueResolver({ targetDir: options.targetDir, only: options.only }),
135
- });
136
- }
137
-
138
- module.exports.collectEnvSecrets = collectEnvSecrets;
139
- module.exports.declaredBrandRepo = declaredBrandRepo;
140
- module.exports.fileValueResolver = fileValueResolver;
141
- module.exports.publishEnvSecrets = publishEnvSecrets;
@@ -1,95 +0,0 @@
1
- // deliver-certs — the Apple artifacts reach the target's certs dir on every
2
- // verb, not only on a manager `manage` run
3
- // ([#678](https://github.com/Omega-JS-Stack/omega/issues/678)).
4
- //
5
- // The copy rules are devkit's; what is pinned here is desktop's framing: which
6
- // root holds the signing tree (company stamp wins over the brand root) and the
7
- // no-op when there is no tree at all.
8
-
9
- const path = require('path');
10
- const fs = require('fs');
11
- const os = require('os');
12
- const jetpack = require('fs-jetpack');
13
-
14
- const { deliverTargetCerts, resolveSourceRoot } = require('../../../utils/deliver-certs.js');
15
- const defineCases = require('../../../vendor/devkit/test/define-cases.js');
16
-
17
- const P12 = path.join('.omega', 'certificates', 'apple', 'certificates', 'DEVELOPER_ID_APPLICATION_G2.p12');
18
- const quiet = { log() {}, warn() {}, error() {} };
19
-
20
- // A brand root with a desktop target under it; `tree` seeds the signing tree.
21
- function stageBrand({ tree = true, companyTree = false } = {}) {
22
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-certs-'));
23
- const brandRoot = path.join(root, 'brand');
24
- const targetDir = path.join(brandRoot, 'targets', 'desktop');
25
-
26
- jetpack.write(path.join(brandRoot, 'config', 'omega.json5'), "{ brand: { id: 'acme', name: 'Acme', url: 'https://acme.test' }, targets: { desktop: {} } }\n");
27
- jetpack.dir(targetDir);
28
- if (tree) jetpack.write(path.join(brandRoot, P12), 'brand-cert');
29
-
30
- if (companyTree) {
31
- const companyRoot = path.join(root, 'company');
32
- jetpack.write(path.join(companyRoot, P12), 'company-cert');
33
- jetpack.write(path.join(brandRoot, '.omega', 'company.json'), JSON.stringify({ root: companyRoot }));
34
- return { root, brandRoot, companyRoot, targetDir };
35
- }
36
-
37
- return { root, brandRoot, targetDir };
38
- }
39
-
40
- module.exports = defineCases({
41
- type: 'group',
42
- layer: 'build',
43
- description: 'deliver-certs — signing artifacts on every verb (#678)',
44
- tests: [
45
- {
46
- name: 'the brand tree delivers into the target certs dir, and the rerun is current',
47
- run: (ctx) => {
48
- const { root, targetDir } = stageBrand();
49
-
50
- try {
51
- const first = deliverTargetCerts({ projectDir: targetDir, logger: quiet });
52
- ctx.expect(first.copied).toContain('config/certs/developer-id-application.p12');
53
- ctx.expect(jetpack.read(path.join(targetDir, 'config', 'certs', 'developer-id-application.p12'))).toBe('brand-cert');
54
- // Signing material can never be committed
55
- ctx.expect(jetpack.read(path.join(targetDir, 'config', 'certs', '.gitignore'))).toContain('*');
56
-
57
- const second = deliverTargetCerts({ projectDir: targetDir, logger: quiet });
58
- ctx.expect(second.copied).toEqual([]);
59
- ctx.expect(second.current).toContain('config/certs/developer-id-application.p12');
60
- } finally {
61
- fs.rmSync(root, { recursive: true, force: true });
62
- }
63
- },
64
- },
65
- {
66
- name: 'the COMPANY stamp wins the source root — company-managed brands share the material',
67
- run: (ctx) => {
68
- const { root, brandRoot, companyRoot, targetDir } = stageBrand({ companyTree: true });
69
-
70
- try {
71
- ctx.expect(resolveSourceRoot(targetDir)).toBe(companyRoot);
72
- ctx.expect(resolveSourceRoot(targetDir)).not.toBe(brandRoot);
73
-
74
- deliverTargetCerts({ projectDir: targetDir, logger: quiet });
75
- ctx.expect(jetpack.read(path.join(targetDir, 'config', 'certs', 'developer-id-application.p12'))).toBe('company-cert');
76
- } finally {
77
- fs.rmSync(root, { recursive: true, force: true });
78
- }
79
- },
80
- },
81
- {
82
- name: 'no signing tree → a silent no-op, never a throw (the normal state of a tree-less brand)',
83
- run: (ctx) => {
84
- const { root, targetDir } = stageBrand({ tree: false });
85
-
86
- try {
87
- ctx.expect(deliverTargetCerts({ projectDir: targetDir, logger: quiet })).toBe(null);
88
- ctx.expect(jetpack.exists(path.join(targetDir, 'config', 'certs'))).toBe(false);
89
- } finally {
90
- fs.rmSync(root, { recursive: true, force: true });
91
- }
92
- },
93
- },
94
- ],
95
- });