@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
@@ -1,199 +1,21 @@
1
1
  /**
2
- * Signing-artifact delivery — the copy layer of the ONE delivery step (#678).
2
+ * The signing-tree READERS, in one place.
3
3
  *
4
- * Sources are the certificates service's outputs under a signing tree,
5
- * `<sourceRoot>/.omega/certificates/apple/` (the company workspace's root for
6
- * a company-managed brand, the brand root otherwise); destinations are the
7
- * target's own certs dir (desktop: config/certs/ per EM v1.4.1+, mobile:
8
- * build/certs/). Copies are byte-compared first, so a converged run rewrites
9
- * nothing, and every certs dir gets a self-protecting .gitignore (`*`) — signing
10
- * material can never be committed, even in a target the framework's setup
11
- * hasn't touched yet.
4
+ * There is no delivery here any more
5
+ * ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)): nothing COPIES
6
+ * signing material into a target. The tree is read IN PLACE (company tier
7
+ * first, `signing-tree.js`, #892), and the paths into it are derived once, at
8
+ * the desktop env load, by `signing-env.js`. The copy step and its rule table
9
+ * had two homes for one fact (which file a key points at) and produced the
10
+ * dispersed duplicates a build could silently sign with; the manager's disperse
11
+ * `certs` operation went with it.
12
12
  *
13
- * Delivery is WARN-ONLY on a miss: a rule with no source reports `missing` and
14
- * leaves the dest alone. The dests are documented HUMAN drop points (desktop's
15
- * signing docs tell an operator to copy their .p12/.p8 straight into
16
- * config/certs/), and `omega company init` scaffolds the signing tree EMPTY —
17
- * so a delete-on-missing pass would eat hand-placed signing material on the
18
- * next build.
19
- *
20
- * This module DELIVERS and reports; it logs nothing and classifies nothing.
21
- * Which misses are tolerable (`optional`) is on the rules, which callers read
22
- * through resolveCertRules — the manager's disperse writer turns them into its
23
- * per-rule lines, a desktop build into whatever its own voice is.
24
- */
25
- const { join, dirname } = require('node:path');
26
- const jetpack = require('fs-jetpack');
27
-
28
- // Paths: source relative to .omega/certificates/apple/, dest relative to the
29
- // target dir. `{env.VAR}` and `{brand.id}` placeholders supported on both sides.
30
- const CERT_FILE_MAP = {
31
- desktop: [
32
- // macOS code signing — Developer ID Application (.p12 with private key)
33
- {
34
- source: 'certificates/DEVELOPER_ID_APPLICATION_G2.p12',
35
- dest: 'config/certs/developer-id-application.p12',
36
- },
37
- // macOS installer signing (only used if shipping a .pkg installer)
38
- {
39
- source: 'certificates/DEVELOPER_ID_INSTALLER_G2.p12',
40
- dest: 'config/certs/developer-id-installer.p12',
41
- optional: true,
42
- },
43
- // Notarization API key — App Store Connect .p8
44
- {
45
- source: 'AuthKey_{env.APPLE_API_KEY_ID}.p8',
46
- dest: 'config/certs/AuthKey_{env.APPLE_API_KEY_ID}.p8',
47
- },
48
- // Per-brand provisioning profile (only present if cert generation produced one)
49
- {
50
- source: 'profiles/{brand.id}/DEVELOPER_ID_APPLICATION_G2/MACOS.mobileprovision',
51
- dest: 'config/certs/{brand.id}.provisionprofile',
52
- optional: true,
53
- },
54
- ],
55
- mobile: [
56
- // iOS App Store distribution — used by xcodebuild
57
- {
58
- source: 'certificates/IOS_DISTRIBUTION.p12',
59
- dest: 'build/certs/ios-distribution.p12',
60
- },
61
- // Notarization / API access from CI
62
- {
63
- source: 'AuthKey_{env.APPLE_API_KEY_ID}.p8',
64
- dest: 'build/certs/AuthKey_{env.APPLE_API_KEY_ID}.p8',
65
- },
66
- // Per-brand iOS provisioning profile
67
- {
68
- source: 'profiles/{brand.id}/IOS_DISTRIBUTION/IOS.mobileprovision',
69
- dest: 'build/certs/{brand.id}.mobileprovision',
70
- optional: true,
71
- },
72
- ],
73
- };
74
-
75
- /**
76
- * Resolve `{env.VAR}` and `{brand.key}` placeholders. Returns null when an
77
- * env placeholder is unset — the rule can't produce a real path, and a
78
- * literal `AuthKey_.p8` miss would only confuse.
79
- */
80
- function resolveTemplate(template, brand) {
81
- let missing = false;
82
-
83
- const resolved = template.replace(/\{([^}]+)\}/g, (match, path) => {
84
- const [source, ...keyParts] = path.split('.');
85
- const key = keyParts.join('.');
86
- if (source === 'env') {
87
- const value = process.env[key] || '';
88
- if (!value) missing = true;
89
- return value;
90
- }
91
- if (source === 'brand') return brand?.[key] || '';
92
- return match;
93
- });
94
-
95
- return missing ? null : resolved;
96
- }
97
-
98
- /**
99
- * Ensure the certs dir ignores everything it holds — signing material must
100
- * never land in git, regardless of whether the framework's own .gitignore
101
- * exists yet.
102
- */
103
- function ensureCertsIgnore(destDir) {
104
- const ignorePath = join(destDir, '.gitignore');
105
- if (!jetpack.exists(ignorePath)) {
106
- jetpack.write(ignorePath, '*\n!.gitignore\n');
107
- }
108
- }
109
-
110
- /**
111
- * The signing tree a source root holds — the ONE home of that path.
112
- *
113
- * @param {string} sourceRoot - Company root (company-managed brands) or brand root.
114
- * @returns {string} Absolute path to .omega/certificates/apple/.
115
- */
116
- function certsSourceDir(sourceRoot) {
117
- return join(sourceRoot, '.omega', 'certificates', 'apple');
118
- }
119
-
120
- /**
121
- * A target's signing rules with their placeholders resolved.
122
- *
123
- * @param {object} input
124
- * @param {string} input.target - Target type ('desktop'/'mobile'); anything
125
- * else has no signing rules and returns [].
126
- * @param {object} [input.brand] - Brand config block (`{brand.id}` resolves from it).
127
- * @returns {Array<{ source, dest, optional, sourceRel, destRel }>} `sourceRel`/
128
- * `destRel` are null when an env placeholder is unset; `dest` is the template.
129
- */
130
- function resolveCertRules({ target, brand = null }) {
131
- return (CERT_FILE_MAP[target] || []).map((rule) => {
132
- const sourceRel = resolveTemplate(rule.source, brand);
133
- const destRel = sourceRel === null ? null : resolveTemplate(rule.dest, brand);
134
-
135
- return {
136
- source: rule.source,
137
- dest: rule.dest,
138
- optional: !!rule.optional,
139
- sourceRel,
140
- destRel,
141
- };
142
- });
143
- }
144
-
145
- /**
146
- * Copy a target's signing artifacts out of the signing tree.
147
- *
148
- * @param {object} input
149
- * @param {string} input.sourceRoot - Company root or brand root holding .omega/certificates/apple/.
150
- * @param {string} input.targetDir - The target's own directory (destinations resolve inside it).
151
- * @param {string} input.target - Target type ('desktop'/'mobile').
152
- * @param {object} [input.brand] - Brand config block, for `{brand.id}` rules.
153
- * @param {boolean} [input.dryRun] - Plan only: `copied` says what WOULD be
154
- * written, nothing is written.
155
- * @returns {{ copied: string[], current: string[], missing: string[] }}
156
- * Target-relative paths — copied (written this pass), current (already
157
- * byte-identical), missing (no source, or an unresolved placeholder: that rule
158
- * reports its dest TEMPLATE).
13
+ * What remains is what every surface asks the tree: WHERE it is (signingTree)
14
+ * and WHEN what it holds expires (certificateExpiry), so the certificates walk,
15
+ * `validate-certs` and a build all answer with the same date and the same three
16
+ * rungs.
159
17
  */
160
- function deliverCerts({ sourceRoot, targetDir, target, brand = null, dryRun = false }) {
161
- const appleDir = certsSourceDir(sourceRoot);
162
- const copied = [];
163
- const current = [];
164
- const missing = [];
165
-
166
- for (const rule of resolveCertRules({ target, brand })) {
167
- // An unset env placeholder (e.g. APPLE_API_KEY_ID) means the rule has no
168
- // real path to resolve to — it reports its template, which names the gap
169
- if (rule.destRel === null) {
170
- missing.push(rule.dest);
171
- continue;
172
- }
173
-
174
- const destPath = join(targetDir, rule.destRel);
175
- const sourcePath = join(appleDir, rule.sourceRel);
176
- if (!jetpack.exists(sourcePath)) {
177
- missing.push(rule.destRel);
178
- continue;
179
- }
180
-
181
- const sourceBytes = jetpack.read(sourcePath, 'buffer');
182
- const destBytes = jetpack.exists(destPath) ? jetpack.read(destPath, 'buffer') : null;
183
-
184
- if (destBytes && sourceBytes.equals(destBytes)) {
185
- current.push(rule.destRel);
186
- continue;
187
- }
188
-
189
- copied.push(rule.destRel);
190
- if (dryRun) continue;
191
-
192
- jetpack.copy(sourcePath, destPath, { overwrite: true });
193
- ensureCertsIgnore(dirname(destPath));
194
- }
195
-
196
- return { copied, current, missing };
197
- }
18
+ const { signingTree, certsSourceDir } = require('./signing-tree.js');
19
+ const { certificateExpiry, EXPIRY_WARN_DAYS } = require('./certificate-expiry.js');
198
20
 
199
- module.exports = { deliverCerts, resolveCertRules, certsSourceDir, CERT_FILE_MAP };
21
+ module.exports = { signingTree, certsSourceDir, certificateExpiry, EXPIRY_WARN_DAYS };
@@ -29,11 +29,43 @@
29
29
  const path = require('path');
30
30
  const jetpack = require('fs-jetpack');
31
31
 
32
+ // The ONE pinned Socket Firewall action. A workflow installs the firewall
33
+ // through Socket's own action rather than through npm ([#871](https://github.com/Omega-JS-Stack/omega/issues/871)):
34
+ // the action downloads the binary with the job's token, so a hosted runner's
35
+ // shared anonymous GitHub API quota never fails the step, and it caches it.
36
+ // Pinning a new version is this one edit, and every brand picks it up on its
37
+ // next verb ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)).
38
+ const FIREWALL_ACTION = 'SocketDev/action@v1.3.2';
39
+
40
+ // The token every framework template carries where that step belongs, on its
41
+ // own line, indented as the list item it becomes.
42
+ const FIREWALL_TOKEN = '{{ installFirewall }}';
43
+
44
+ // The action step's `id`, so the Windows shim below can read the binary path
45
+ // the action reports (`firewall-path-binary`) instead of guessing at it.
46
+ const FIREWALL_STEP_ID = 'omega-firewall';
47
+
48
+ // The token every framework template carries at the END of its install command,
49
+ // and what a COMPOSED job renders it into ([#898](https://github.com/Omega-JS-Stack/omega/issues/898)).
50
+ // At a brand root npm installs every target's workspace, so the desktop job on
51
+ // Electron's node installed the backend workspace whose engines.node is the
52
+ // Firebase runtime pin and warned EBADENGINE on every leg. Each job installs
53
+ // only the workspace it belongs to instead.
54
+ //
55
+ // `.`, not the target's path: npm resolves `--workspace` against the CWD, and
56
+ // every composed run step already carries `working-directory: targets/<name>`,
57
+ // so `--workspace targets/desktop` from inside targets/desktop looks for
58
+ // targets/desktop/targets/desktop and refuses with "No workspaces found". The
59
+ // dot is the target's real directory whatever the brand named it ([#886](https://github.com/Omega-JS-Stack/omega/issues/886)),
60
+ // and no template ever spells a path.
61
+ const INSTALL_WORKSPACE_TOKEN = '{{ installWorkspace }}';
62
+ const INSTALL_WORKSPACE_FLAG = '--workspace .';
63
+
32
64
  /**
33
65
  * Action inputs that name a path inside the checkout, per action. `uses:` steps
34
66
  * ignore `working-directory:` entirely (it is a `run:` key), so a composed
35
- * workflow has to scope these itself — the gh-pages publish otherwise pushes a
36
- * repo-root `dist/` no build ever wrote, and the cache key hashes nothing.
67
+ * workflow has to scope these itself: an unscoped cache key hashes nothing and
68
+ * an unscoped artifact upload collects an empty repo-root path.
37
69
  *
38
70
  * Named per action on purpose: a blanket "any input called path" rule would
39
71
  * rewrite inputs that mean something else — `actions/checkout`'s `path:` names
@@ -44,9 +76,72 @@ const SCOPED_ACTION_INPUTS = {
44
76
  'actions/cache': ['path'],
45
77
  'actions/upload-artifact': ['path'],
46
78
  'actions/download-artifact': ['path'],
47
- 'peaceiris/actions-gh-pages': ['publish_dir'],
48
79
  };
49
80
 
81
+ /**
82
+ * Render every `{{ installFirewall }}` token into the pinned Socket Firewall
83
+ * step, at the indentation the token itself stands at.
84
+ *
85
+ * The step is written ONCE, here, instead of four times across the framework
86
+ * templates: the action and its version live in `FIREWALL_ACTION` above, so a
87
+ * version bump is one edit rather than a sweep of every template that happens
88
+ * to install the firewall ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)).
89
+ * A template carrying no token is returned untouched.
90
+ *
91
+ * @param {string} contents - The workflow template.
92
+ * @returns {string} The same text with every token replaced.
93
+ */
94
+ function renderInstallFirewall(contents) {
95
+ return contents.replace(new RegExp(`^([ \\t]*)${escapeRegExp(FIREWALL_TOKEN)}[ \\t]*$`, 'gm'), (full, indent) => [
96
+ `${indent}# The firewall binary comes through Socket's own action (#871): it downloads`,
97
+ `${indent}# with the job's token, so the shared anonymous GitHub API quota of a hosted`,
98
+ `${indent}# runner never fails the step, and it caches the binary between runs.`,
99
+ `${indent}- name: Install Socket Firewall`,
100
+ `${indent} id: ${FIREWALL_STEP_ID}`,
101
+ `${indent} uses: ${FIREWALL_ACTION}`,
102
+ `${indent} with:`,
103
+ `${indent} mode: firewall-free`,
104
+ // The action caches the Windows binary under the extension-less name `sfw`
105
+ // and puts its directory on PATH. cmd.exe cannot execute a file with no
106
+ // extension, and the desktop build forces `shell: cmd` on Windows, so every
107
+ // windows leg died on `'sfw' is not recognized` (#872). One copy next to the
108
+ // original, named `sfw.exe`, and cmd finds it through the same PATH entry.
109
+ // Rendered on every template: the `if` makes it a no-op everywhere else.
110
+ // Under cmd, not bash: a self-hosted Windows box (the EV signer) has no bash
111
+ // on the runner's PATH, and cmd is the one shell every Windows runner has.
112
+ `${indent}- name: Expose Socket Firewall to cmd (Windows)`,
113
+ `${indent} if: runner.os == 'Windows'`,
114
+ `${indent} shell: cmd`,
115
+ `${indent} run: copy "\${{ steps.${FIREWALL_STEP_ID}.outputs.firewall-path-binary }}" "\${{ steps.${FIREWALL_STEP_ID}.outputs.firewall-path-binary }}.exe"`,
116
+ ].join('\n'));
117
+ }
118
+
119
+ /**
120
+ * Render every `{{ installWorkspace }}` token: into the workspace flag on a
121
+ * COMPOSED brand-root copy, into nothing at all on a standalone one.
122
+ *
123
+ * Both lanes go through this one function, like the firewall step above, so the
124
+ * flag lives in ONE place instead of four templates. A standalone target IS its
125
+ * own repo root and declares no workspaces, so the flag there would refuse the
126
+ * install by name ("No workspaces found: --workspace=."); the token and the
127
+ * space before it are dropped and the command is the plain install it always was.
128
+ *
129
+ * @param {string} contents - The workflow template.
130
+ * @param {object} [options]
131
+ * @param {boolean} [options.composed] - Composing into a brand root.
132
+ * @returns {string} The same text with every token replaced.
133
+ */
134
+ function renderInstallWorkspace(contents, options) {
135
+ const composed = !!(options && options.composed);
136
+
137
+ return contents.replace(new RegExp(`[ \\t]*${escapeRegExp(INSTALL_WORKSPACE_TOKEN)}`, 'g'), composed ? ` ${INSTALL_WORKSPACE_FLAG}` : '');
138
+ }
139
+
140
+ /** The literal, safe to drop into a RegExp. */
141
+ function escapeRegExp(value) {
142
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
143
+ }
144
+
50
145
  /**
51
146
  * Compose one framework workflow template for a target inside a brand monorepo.
52
147
  * Pure: text in, text out.
@@ -60,13 +155,20 @@ function composeWorkflow(contents, options) {
60
155
  const targetPath = options.targetPath;
61
156
  const targetName = options.targetName;
62
157
 
63
- let composed = contents;
158
+ // Before anything else, so the scoping pass below sees the rendered step for
159
+ // what it is: a `uses:` step, which is never scoped to the target dir.
160
+ let composed = renderInstallFirewall(contents);
161
+
162
+ // This job installs ONLY the target it belongs to (#898): the composed copy
163
+ // is the one that runs inside a brand, where every other target's workspace
164
+ // (and its own engines.node pin) sits beside it.
165
+ composed = renderInstallWorkspace(composed, { composed: true });
64
166
 
65
167
  // Display name carries the target — two targets' runs are told apart in the
66
168
  // Actions list, where only the workflow name shows.
67
169
  composed = composed.replace(/^name:[ \t]*(.*)$/m, (full, value) => `name: ${value.trim()} (${targetPath})`);
68
170
 
69
- // Per-target concurrency: `${{ github.ref }}` alone means the website's deploy
171
+ // Per-target concurrency: `${{ github.ref }}` alone means the web target's deploy
70
172
  // cancels the extension's.
71
173
  composed = composed.replace(/^(concurrency:\n(?:[ \t]+.*\n)*?[ \t]+group:[ \t]*)(.*)$/m, (full, prefix, value) => `${prefix}${targetName}-${value.trim()}`);
72
174
 
@@ -149,7 +251,7 @@ function composeTargetWorkflows(options) {
149
251
  * @param {object} options
150
252
  * @param {string} options.brandRoot - The brand (repo) root
151
253
  * @param {string[]} options.liveTargets - The target DIR names the brand's config
152
- * still enables (`website`, `extension`, …). A composed file owned by anything
254
+ * still enables (`web`, `extension`, …). A composed file owned by anything
153
255
  * else is a leftover.
154
256
  * @param {boolean} [options.dryRun] - Report what would go, delete nothing
155
257
  * @param {object} [options.logger] - Logger with `log`/`warn` (defaults to console)
@@ -201,7 +303,21 @@ function composedWorkflowName(options) {
201
303
  if (!options.brandRoot || path.resolve(options.brandRoot) === path.resolve(options.targetDir)) {
202
304
  return options.workflow;
203
305
  }
204
- return `${path.basename(options.targetDir)}-${options.workflow}`;
306
+ return composedWorkflowNameFor(path.basename(options.targetDir), options.workflow);
307
+ }
308
+
309
+ /**
310
+ * The same name from the target's NAME, for a caller that holds the name and no
311
+ * path at all (the backend's deploy dispatch reads the target out of a request,
312
+ * never off a disk it does not have). The path-taking form above is this one
313
+ * plus a basename, so the two can never spell the rule differently.
314
+ *
315
+ * @param {string} name - The target name (e.g. web)
316
+ * @param {string} workflow - The framework's workflow file name (e.g. build.yml)
317
+ * @returns {string} the composed workflow file name
318
+ */
319
+ function composedWorkflowNameFor(name, workflow) {
320
+ return `${name}-${workflow}`;
205
321
  }
206
322
 
207
323
  // Scope every `run:` step that executes AFTER its job's checkout to the target dir.
@@ -517,4 +633,4 @@ function header(targetPath) {
517
633
  ].join('\n');
518
634
  }
519
635
 
520
- module.exports = { composeWorkflow, composeTargetWorkflows, composedWorkflowName, reconcileComposedWorkflows };
636
+ module.exports = { composeWorkflow, composeTargetWorkflows, composedWorkflowName, composedWorkflowNameFor, reconcileComposedWorkflows, renderInstallFirewall, renderInstallWorkspace, FIREWALL_ACTION, FIREWALL_STEP_ID, FIREWALL_TOKEN, INSTALL_WORKSPACE_TOKEN, INSTALL_WORKSPACE_FLAG };
@@ -1,6 +1,6 @@
1
1
  // createCliRouter(config) — the shared OMEGA framework CLI dispatcher.
2
2
  //
3
- // Every framework bin parses argv (yargs) and hands the result to a Main class
3
+ // Every framework bin parses argv (src/argv.js) and hands the result to a Main class
4
4
  // whose process() resolves a command name and runs commands/<name>.js from the
5
5
  // framework's dist. That router was copy-pasted per framework (UJM/BXM/EM,
6
6
  // drift-identical); this is the single implementation. @omega.js/backend's CLI is a different
@@ -46,8 +46,8 @@ function createCliRouter(config) {
46
46
  // `--help`/`-h` route to the built-in help FIRST — before positionals, so
47
47
  // `omega deploy --help` prints help instead of RUNNING deploy, and never
48
48
  // the default command (the old fallthrough ran setup/manage on a help
49
- // request). Frameworks must disable yargs' built-in help so the flag
50
- // reaches the router.
49
+ // request). No parse may claim `--help` for itself: the flag has to reach
50
+ // the router.
51
51
  if (options.help || options.h) {
52
52
  return 'help';
53
53
  }
@@ -15,7 +15,9 @@
15
15
  // template object render `{{ key.path }}` tokens with this data
16
16
  // (tolerant: unknown keys survive verbatim, so GitHub
17
17
  // Actions' `${{ secrets.X }}` passes through)
18
- // merge bool JSON5 defaults-merge with the existing file
18
+ // merge bool JSON5 defaults-merge with the existing file, written
19
+ // through @omega.js/config's comment-preserving editor
20
+ // (only the differing keys change; comments stay)
19
21
  // mergeLines bool OMEGA marker-section line merge (.env/.gitignore/AGENTS.md)
20
22
  // retire bool brand-context per-target doc retirement: NEVER scaffold
21
23
  // the file; an existing framework-owned-only copy is
@@ -38,6 +40,7 @@
38
40
 
39
41
  const path = require('path');
40
42
  const fs = require('fs');
43
+ const { isDeepStrictEqual } = require('node:util');
41
44
  const jetpack = require('fs-jetpack');
42
45
  const { minimatch } = require('minimatch');
43
46
  const { mergeLineBasedFiles, hasSectionMarkers, getCustomSection } = require('./merge-line-files');
@@ -206,9 +209,7 @@ function applyDefaults(config) {
206
209
 
207
210
  if (options.merge && exists) {
208
211
  try {
209
- const JSON5 = require('json5');
210
- const merged = mergeJson5Defaults(JSON5.parse(jetpack.read(destination)), JSON5.parse(contents));
211
- contents = JSON5.stringify(merged, null, 2);
212
+ contents = mergeJson5Text(jetpack.read(destination), contents);
212
213
  didMerge = true;
213
214
  } catch (error) {
214
215
  logger.error(`Error merging config file ${finalRelative}: ${error.message}`);
@@ -335,7 +336,10 @@ function renderTemplate(content, context) {
335
336
 
336
337
  // JSON5 defaults merge (BXM's mergeConfigs): framework defaults provide shape and
337
338
  // order; the consumer's values win unless they're the literal string 'default';
338
- // consumer-only keys survive at every nesting level.
339
+ // consumer-only keys survive at every nesting level, EXCEPT one holding the
340
+ // 'default' sentinel, which is the framework's own unset marker and drops (#926).
341
+ // A JSON5 file carries no framework-block marker, so that sentinel is the only
342
+ // signal telling a key the framework retired from a key the consumer added.
339
343
  function mergeJson5Defaults(existingConfig, newConfig) {
340
344
  const merged = { ...newConfig };
341
345
 
@@ -356,9 +360,11 @@ function mergeJson5Defaults(existingConfig, newConfig) {
356
360
  }
357
361
  }
358
362
 
359
- // Consumer-only keys survive at every level.
363
+ // Consumer-only keys survive at every level, except a leftover 'default'
364
+ // sentinel: that is the framework's unset marker for a key it no longer
365
+ // declares, so it leaves with the key (#926).
360
366
  for (const key in source) {
361
- if (!Object.prototype.hasOwnProperty.call(newDefaults, key)) {
367
+ if (!Object.prototype.hasOwnProperty.call(newDefaults, key) && source[key] !== 'default') {
362
368
  target[key] = source[key];
363
369
  }
364
370
  }
@@ -369,4 +375,60 @@ function mergeJson5Defaults(existingConfig, newConfig) {
369
375
  return merged;
370
376
  }
371
377
 
378
+ // The merge WRITE: the consumer's JSON5 is hand-edited, so the merged object is
379
+ // never re-stringified over it (that stripped every comment and the trailing
380
+ // newline, #928). The merge result is diffed against the file on disk and the
381
+ // differences go through @omega.js/config's comment-preserving editor, so only
382
+ // the changed value spans move, every other byte survives, and an unchanged merge
383
+ // returns the source untouched so the engine's idempotency check skips the write.
384
+ // Output always ends with exactly one newline.
385
+ function mergeJson5Text(existingText, defaultsText) {
386
+ const JSON5 = require('json5');
387
+ const { applyConfigEdits, applyConfigRemovals } = require('../config/index.js');
388
+
389
+ const existing = JSON5.parse(existingText);
390
+ const merged = mergeJson5Defaults(existing, JSON5.parse(defaultsText));
391
+
392
+ const edits = {};
393
+ const removals = [];
394
+ diffConfigPaths(existing, merged, '', edits, removals);
395
+
396
+ // Nothing to change is not a write: the file goes back byte for byte, final
397
+ // newline included or not, so a rerun never touches it.
398
+ if (removals.length === 0 && Object.keys(edits).length === 0) {
399
+ return existingText;
400
+ }
401
+
402
+ const written = applyConfigEdits(applyConfigRemovals(existingText, removals), edits);
403
+
404
+ return written.replace(/\n*$/, '\n');
405
+ }
406
+
407
+ // Diff the merged object against the one on disk into editor operations: dot-paths
408
+ // to set (a leaf whose value differs, plus a key or whole branch the file lacks)
409
+ // and dot-paths to delete (a key the merge dropped, #926's `default` sentinel).
410
+ // Two objects at the same path recurse; anything else (primitive, array, or a
411
+ // branch landing where the file has no object) is one value the editor writes whole.
412
+ function diffConfigPaths(existing, merged, prefix, edits, removals) {
413
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
414
+
415
+ for (const key of Object.keys(merged)) {
416
+ const keyPath = prefix ? `${prefix}.${key}` : key;
417
+ const nextValue = merged[key];
418
+ const priorValue = existing[key];
419
+
420
+ if (isObject(nextValue) && isObject(priorValue)) {
421
+ diffConfigPaths(priorValue, nextValue, keyPath, edits, removals);
422
+ } else if (!isDeepStrictEqual(priorValue, nextValue)) {
423
+ edits[keyPath] = nextValue;
424
+ }
425
+ }
426
+
427
+ for (const key of Object.keys(existing)) {
428
+ if (!Object.prototype.hasOwnProperty.call(merged, key)) {
429
+ removals.push(prefix ? `${prefix}.${key}` : key);
430
+ }
431
+ }
432
+ }
433
+
372
434
  module.exports = { applyDefaults, mergeJson5Defaults, renderTemplate };