@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
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The Firebase service-account key's ONE lookup
3
+ * ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)), and the ONE
4
+ * spelling of every path under the brand's secrets folder
5
+ * ([#897](https://github.com/Omega-JS-Stack/omega/issues/897)).
6
+ *
7
+ * The key is a FILE the cloud manage service mints, never a `.env` value, so
8
+ * everything that needs it (the backend stage, the deploy-roles read, the
9
+ * secrets publish that sends it to a runner) resolves the same two candidates
10
+ * in the same order: the target root's own, then the brand's secrets folder.
11
+ * It lived in the backend's stage helper while the backend was its only reader;
12
+ * the shared secrets publisher reads it now, and devkit never requires a
13
+ * framework, so the lookup lives here and the backend imports it.
14
+ *
15
+ * The Google OAuth client the authentication service writes obeys the same
16
+ * folder rule, so it is named here too: a rename of the folder is one edit,
17
+ * not a hunt through every manager and backend reader.
18
+ */
19
+ const path = require('node:path');
20
+ const jetpack = require('fs-jetpack');
21
+ const { findBrandRoot } = require('../config/index.js');
22
+
23
+ // The brand-level home of every minted credential, and the two files in it.
24
+ const SECRETS_DIR = path.join('.omega', 'secrets');
25
+ const SERVICE_ACCOUNT_REL = path.join(SECRETS_DIR, 'service-account.json');
26
+ const GOOGLE_OAUTH_REL = path.join(SECRETS_DIR, 'google-oauth.json');
27
+
28
+ /**
29
+ * The service-account key a target deploys with, or null when none is minted.
30
+ *
31
+ * @param {string} projectDir - The target root.
32
+ * @returns {string|null} Absolute path to the key file.
33
+ */
34
+ function resolveServiceAccountPath(projectDir) {
35
+ const brandRoot = findBrandRoot(projectDir);
36
+
37
+ return [
38
+ path.join(projectDir, 'service-account.json'),
39
+ brandRoot ? path.join(brandRoot, SERVICE_ACCOUNT_REL) : null,
40
+ ].filter(Boolean).find((candidate) => jetpack.exists(candidate)) || null;
41
+ }
42
+
43
+ module.exports = { resolveServiceAccountPath, SECRETS_DIR, SERVICE_ACCOUNT_REL, GOOGLE_OAUTH_REL };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * The ship plan ([#867](https://github.com/Omega-JS-Stack/omega/issues/867)):
3
+ * what a brand's declaration says a target ships, what each shipped format is
4
+ * still missing, and the ONE wording both are reported in.
5
+ *
6
+ * @omega.js/config's platforms.js owns WHAT each format is and needs (the
7
+ * format table). This owns the two questions every lane asks of that table and
8
+ * the sentences the answers are printed in, so its three readers say the same
9
+ * thing in the same words:
10
+ *
11
+ * - the manage walk's `publishing` service, which collects what is missing;
12
+ * - the desktop publish, which refuses before it builds;
13
+ * - the extension publish, which refuses on a missing credential and prints
14
+ * the manual step for a listing only a human can create.
15
+ *
16
+ * Values are never read here, only NAMES: whether a key has a value is the
17
+ * whole question, and an empty string is absent (the .env cascade's own rule).
18
+ */
19
+
20
+ const { enabledFormats, formatKeys, FORMATS } = require('../config/index.js');
21
+
22
+ // The ONE walk that collects every ship credential. Named in each refusal, so
23
+ // the fix is a command away instead of a hunt through the env schema.
24
+ const PUBLISHING_WALK = 'omega manage --service publishing';
25
+
26
+ /**
27
+ * Everything a target ships, each entry carrying what it needs.
28
+ *
29
+ * @param {object} config - A resolved target config (the declaration is read
30
+ * at `config.platforms`, which is where `targets.<name>.platforms` resolves).
31
+ * @param {string} target - Target type ('desktop' | 'extension').
32
+ * @returns {Array<object>} In offer order: `{ platform, format, options, kind,
33
+ * ext, label, console, path, requires, listing }`. `path` is the declaration
34
+ * that enabled it, `requires` is narrowed to THIS brand, and `listing` names
35
+ * the config paths the format cannot ship without.
36
+ */
37
+ function shipPlan(config, target) {
38
+ return enabledFormats(config, target).map((entry) => {
39
+ const spec = FORMATS[target][entry.platform][entry.format];
40
+ const { requires, listing } = formatKeys(target, entry.platform, entry.format, config);
41
+
42
+ return {
43
+ ...entry,
44
+ kind: spec.kind,
45
+ ...(spec.ext ? { ext: spec.ext } : {}),
46
+ ...(spec.label ? { label: spec.label } : {}),
47
+ ...(spec.console ? { console: spec.console } : {}),
48
+ path: `platforms.${entry.platform}.formats.${entry.format}`,
49
+ requires,
50
+ listing,
51
+ };
52
+ });
53
+ }
54
+
55
+ /**
56
+ * The keys a plan needs that the environment does not answer.
57
+ *
58
+ * @param {Array<object>} plan - A shipPlan.
59
+ * @param {object} [env] - Env map (default: process.env).
60
+ * @returns {Array<{ key: string, path: string, label: string }>} One entry per
61
+ * empty key, in declaration order; `path` is what requires it.
62
+ */
63
+ function missingShipKeys(plan, env) {
64
+ const values = env || process.env;
65
+ const missing = [];
66
+
67
+ for (const entry of plan) {
68
+ for (const key of entry.requires) {
69
+ if (values[key]) continue;
70
+ missing.push({ key, path: entry.path, label: entry.label || entry.format });
71
+ }
72
+ }
73
+
74
+ return missing;
75
+ }
76
+
77
+ /**
78
+ * The refusal a missing ship credential earns: ONE line shape per key,
79
+ * `<KEY> (required by <path>): <the fix>`, the same shape push-secrets prints
80
+ * ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)), so the two
81
+ * rungs of the same story read alike.
82
+ *
83
+ * @param {Array<{ key, path, label }>} missing - missingShipKeys' return.
84
+ * @returns {string} The message (callers throw it, or return it as an error).
85
+ */
86
+ function shipKeyRefusal(missing) {
87
+ const lines = missing.map(({ key, path, label }) => ` ${key} (required by ${path}): `
88
+ + `${label} cannot publish without it. Run \`${PUBLISHING_WALK}\` (it asks for the key and writes the brand .env), then re-run.`);
89
+
90
+ return `${missing.length} ship credential(s) this brand's declaration requires are empty in the .env cascade (company/brand/target).\n${lines.join('\n')}`;
91
+ }
92
+
93
+ /**
94
+ * The step a missing listing id leaves a human. The store assigns the id when
95
+ * somebody creates the listing, so no lane can mint one: the publish attaches
96
+ * the artifact to the release and prints this, and the walk prints it when it
97
+ * had nobody to ask.
98
+ *
99
+ * @param {object} store
100
+ * @param {string} store.label - The store's name ('Chrome Web Store').
101
+ * @param {string} store.console - The page the listing is created on.
102
+ * @param {string} store.path - The brand-config path the id lands at.
103
+ * @param {string} [store.asset] - The release asset to upload, when a lane has one.
104
+ * @returns {string} One line.
105
+ */
106
+ function listingManualStep({ label, console: consoleUrl, path, asset }) {
107
+ const upload = asset ? `, upload ${asset} from the release` : '';
108
+
109
+ return `${label} has no listing id yet: create the listing at ${consoleUrl}${upload}, then set ${path} in config/omega.json5 and re-run.`;
110
+ }
111
+
112
+ module.exports = { shipPlan, missingShipKeys, shipKeyRefusal, listingManualStep, PUBLISHING_WALK };
@@ -0,0 +1,180 @@
1
+ /**
2
+ * The signing paths, derived ONCE
3
+ * ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)).
4
+ *
5
+ * CSC_LINK and APPLE_API_KEY name FILES, and the files live in the signing
6
+ * tree: `<company or brand root>/.omega/certificates/apple/`, company tier
7
+ * first (`@omega.js/devkit/signing-tree`, #892). Nobody types those paths into
8
+ * a `.env`, so every reader used to derive them for itself: the build did, the
9
+ * secrets publish did, and validate-certs did NOT, which is how a deploy stopped
10
+ * on `Found .p12 in config/certs/ but CSC_LINK env var is not set` while the
11
+ * build beside it signed fine. One rule, one home: the desktop env load calls
12
+ * this, and the build, validate-certs and the secrets publish all read the env
13
+ * it populated.
14
+ *
15
+ * The lookup order this function IS (the four rungs the desktop signing lookup
16
+ * always had):
17
+ * 1. an explicit env value wins, always: nothing here overwrites a set key;
18
+ * 2. the COMPANY tree, when the brand names one (`company: { id }`, #677);
19
+ * 3. the BRAND's own tree;
20
+ * 4. nothing set, which leaves electron-builder's Keychain identity
21
+ * discovery as the default a tree-less machine keeps getting.
22
+ *
23
+ * A file that exists and cannot be USED is a PROBLEM, never a silent skip: a
24
+ * `.p12` the password does not open would fail the build outright, so it is
25
+ * reported and the key stays unset. Nothing here logs or throws; the caller
26
+ * speaks.
27
+ *
28
+ * There is no provisioning profile: Developer ID direct distribution needs
29
+ * none (the certificates walk requests none for those types, #891), so nothing
30
+ * derives one.
31
+ */
32
+ const { execFileSync } = require('node:child_process');
33
+ const { join } = require('node:path');
34
+ const { findBrandRoot } = require('../config/index.js');
35
+
36
+ const { signingTree } = require('./signing-tree.js');
37
+
38
+ // Tree-relative paths, the ONE home of both: the certificates walk writes them
39
+ // here and every reader asks this module for them.
40
+ const CERT_REL = join('certificates', 'DEVELOPER_ID_APPLICATION_G2.p12');
41
+ const authKeyRel = (keyId) => `AuthKey_${keyId}.p8`;
42
+
43
+ /**
44
+ * Does this password actually open the container?
45
+ *
46
+ * Two openssl families answer differently: OpenSSL 3 needs `-legacy` to read
47
+ * the legacy-format containers the manager exports (macOS `security import`
48
+ * needs that format), while stock macOS /usr/bin/openssl is LibreSSL, which
49
+ * rejects the `-legacy` FLAG but reads legacy containers natively. A flag
50
+ * rejection is retried without it; only a real password/container failure
51
+ * counts as "does not open".
52
+ *
53
+ * @param {string} p12Path - Path to the .p12.
54
+ * @param {string} password - Candidate password.
55
+ * @returns {{ opens: boolean, tooling?: string }} - tooling set when openssl
56
+ * itself was unavailable/unusable, so the caller can say so instead of
57
+ * blaming the password.
58
+ */
59
+ function opensWith(p12Path, password) {
60
+ // Password rides an env var, never argv, never a shell string.
61
+ const run = (args) => execFileSync(
62
+ 'openssl',
63
+ ['pkcs12', ...args, '-in', p12Path, '-nokeys', '-noout', '-passin', 'env:OMEGA_P12_PASSWORD'],
64
+ { stdio: 'pipe', env: { ...process.env, OMEGA_P12_PASSWORD: password } },
65
+ );
66
+
67
+ try {
68
+ run(['-legacy']);
69
+ return { opens: true };
70
+ } catch (error) {
71
+ if (error.code === 'ENOENT') {
72
+ return { opens: false, tooling: 'openssl is not available' };
73
+ }
74
+
75
+ if (String(error.stderr || '').includes('unknown option')) {
76
+ try {
77
+ run([]);
78
+ return { opens: true };
79
+ } catch (retryError) {
80
+ if (retryError.code === 'ENOENT') {
81
+ return { opens: false, tooling: 'openssl is not available' };
82
+ }
83
+ return { opens: false };
84
+ }
85
+ }
86
+
87
+ return { opens: false };
88
+ }
89
+ }
90
+
91
+ /** An env value that is really set (an empty placeholder reads as unset). */
92
+ function isSet(value) {
93
+ return typeof value === 'string' && value.trim() !== '';
94
+ }
95
+
96
+ /**
97
+ * Derive the signing paths this target's tree implies. MUTATES `env`.
98
+ *
99
+ * @param {object} input
100
+ * @param {object} input.env - The env map to derive into (the process env).
101
+ * @param {string} input.targetDir - The target root; its brand names the tree.
102
+ * @returns {{ derived: Array<{ key: string, value: string }>, problems: Array<{ key: string, reason: string }> }}
103
+ * `derived` is what was written (absolute tree paths); `problems` are files
104
+ * that exist and cannot be used. Both empty outside a brand.
105
+ */
106
+ function deriveSigningEnv({ env, targetDir }) {
107
+ const derived = [];
108
+ const problems = [];
109
+
110
+ // Outside a brand there is no tree, so there is nothing to derive.
111
+ const brandRoot = findBrandRoot(targetDir);
112
+ if (!brandRoot) {
113
+ return { derived, problems };
114
+ }
115
+
116
+ const tree = signingTree({ brandRoot });
117
+
118
+ // CSC_LINK: the Developer ID Application certificate.
119
+ if (!isSet(env.CSC_LINK)) {
120
+ const found = tree.find(CERT_REL);
121
+
122
+ if (found) {
123
+ if (!isSet(env.CSC_KEY_PASSWORD)) {
124
+ problems.push({ key: 'CSC_LINK', reason: `${found.path} is there but CSC_KEY_PASSWORD is unset, so it cannot be opened` });
125
+ } else {
126
+ const check = opensWith(found.path, env.CSC_KEY_PASSWORD);
127
+
128
+ if (check.tooling) {
129
+ problems.push({ key: 'CSC_LINK', reason: `${check.tooling}, so ${found.path} cannot be verified` });
130
+ } else if (!check.opens) {
131
+ problems.push({ key: 'CSC_LINK', reason: `CSC_KEY_PASSWORD does not open ${found.path}` });
132
+ } else {
133
+ env.CSC_LINK = found.path;
134
+ derived.push({ key: 'CSC_LINK', value: found.path });
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ // APPLE_API_KEY: the App Store Connect key, whose FILENAME carries the id,
141
+ // so an unset APPLE_API_KEY_ID means there is no path to look for.
142
+ if (!isSet(env.APPLE_API_KEY) && isSet(env.APPLE_API_KEY_ID)) {
143
+ const found = tree.find(authKeyRel(env.APPLE_API_KEY_ID.trim()));
144
+
145
+ if (found) {
146
+ env.APPLE_API_KEY = found.path;
147
+ derived.push({ key: 'APPLE_API_KEY', value: found.path });
148
+ }
149
+ }
150
+
151
+ return { derived, problems };
152
+ }
153
+
154
+ /**
155
+ * Where each derivable key WOULD read from, whether or not the file is there:
156
+ * the line a refusal names instead of asking an operator to paste a path.
157
+ *
158
+ * @param {object} input
159
+ * @param {object} input.env - The env map (APPLE_API_KEY_ID names the AuthKey file).
160
+ * @param {string} input.targetDir - The target root.
161
+ * @returns {Array<{ key: string, paths: string[] }>} Absolute candidate paths,
162
+ * company tier first. Empty outside a brand.
163
+ */
164
+ function signingPathCandidates({ env, targetDir }) {
165
+ const brandRoot = findBrandRoot(targetDir);
166
+ if (!brandRoot) {
167
+ return [];
168
+ }
169
+
170
+ const { readDirs } = signingTree({ brandRoot });
171
+
172
+ return [
173
+ { key: 'CSC_LINK', rel: CERT_REL },
174
+ { key: 'APPLE_API_KEY', rel: isSet(env.APPLE_API_KEY_ID) ? authKeyRel(env.APPLE_API_KEY_ID.trim()) : null },
175
+ ]
176
+ .filter((candidate) => candidate.rel)
177
+ .map(({ key, rel }) => ({ key, paths: readDirs.map((dir) => join(dir, rel)) }));
178
+ }
179
+
180
+ module.exports = { deriveSigningEnv, signingPathCandidates, opensWith, CERT_REL };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The signing tree, in TWO tiers: the ONE resolution of where Apple signing
3
+ * material is read from and written to
4
+ * ([#892](https://github.com/Omega-JS-Stack/omega/issues/892)).
5
+ *
6
+ * ONE Apple account signs everything a company ships, so a brand that names a
7
+ * company ([#677](https://github.com/Omega-JS-Stack/omega/issues/677)) REUSES
8
+ * that company's material instead of minting its own:
9
+ *
10
+ * READ company tree first (`<companyRoot>/.omega/certificates/apple/`),
11
+ * then the brand's own. The tier a file came from is part of the
12
+ * answer, so a walk can say "reused from company".
13
+ * WRITE the company tree when there is one, the brand tree otherwise. New
14
+ * material a company-managed brand produces belongs to the company.
15
+ *
16
+ * Every consumer resolves through this one function: the manager's certificates
17
+ * walk, the manager's disperse writer, and the desktop verbs' own delivery (the
18
+ * `omega deploy --local` lane), which has no manager context and reads the
19
+ * company root through @omega.js/config's resolver instead.
20
+ */
21
+ const { join } = require('node:path');
22
+ const jetpack = require('fs-jetpack');
23
+ const { resolveCompany } = require('../config/index.js');
24
+
25
+ const APPLE_REL = join('.omega', 'certificates', 'apple');
26
+
27
+ /**
28
+ * The signing tree a root holds: the ONE home of that path.
29
+ *
30
+ * @param {string} root - Company tree root or brand root.
31
+ * @returns {string} Absolute path to .omega/certificates/apple/.
32
+ */
33
+ function certsSourceDir(root) {
34
+ return join(root, APPLE_REL);
35
+ }
36
+
37
+ /**
38
+ * The company TREE a brand inherits from (`<parent root>/company`), or null.
39
+ * The manager hands its own `context.companyRoot`; everything else asks here.
40
+ *
41
+ * @param {string|null} brandRoot - The brand root.
42
+ * @returns {string|null}
43
+ */
44
+ function companyTree(brandRoot) {
45
+ return brandRoot ? (resolveCompany(brandRoot).dir || null) : null;
46
+ }
47
+
48
+ /**
49
+ * Resolve a brand's signing tree.
50
+ *
51
+ * @param {object} input
52
+ * @param {string|null} input.brandRoot - The brand root (null outside a brand).
53
+ * @param {string|null} [input.companyRoot] - The company TREE, when the caller
54
+ * already resolved it (the manager's context does). Pass nothing and it is
55
+ * resolved from the brand root.
56
+ * @returns {{ writeDir: string|null, readDirs: string[], companyDir: string|null,
57
+ * brandDir: string|null, find: Function, path: Function }}
58
+ * `find(rel)` answers `{ path, source }` for the first tier holding that
59
+ * relative path (company first), null when neither does; `path(rel)` is where
60
+ * a WRITE of it goes.
61
+ */
62
+ function signingTree({ brandRoot = null, companyRoot } = {}) {
63
+ const company = companyRoot === undefined ? companyTree(brandRoot) : companyRoot;
64
+ const companyDir = company ? certsSourceDir(company) : null;
65
+ const brandDir = brandRoot ? certsSourceDir(brandRoot) : null;
66
+
67
+ // Company FIRST, always: a brand of a company reuses its material.
68
+ const tiers = [
69
+ ...(companyDir ? [{ dir: companyDir, source: 'company' }] : []),
70
+ ...(brandDir ? [{ dir: brandDir, source: 'brand' }] : []),
71
+ ];
72
+ const writeDir = companyDir || brandDir;
73
+
74
+ return {
75
+ writeDir,
76
+ readDirs: tiers.map((tier) => tier.dir),
77
+ companyDir,
78
+ brandDir,
79
+ find: (rel) => {
80
+ for (const tier of tiers) {
81
+ const candidate = join(tier.dir, rel);
82
+ if (jetpack.exists(candidate)) {
83
+ return { path: candidate, source: tier.source };
84
+ }
85
+ }
86
+ return null;
87
+ },
88
+ path: (rel) => (writeDir ? join(writeDir, rel) : null),
89
+ };
90
+ }
91
+
92
+ module.exports = { signingTree, certsSourceDir, companyTree, APPLE_REL };
@@ -0,0 +1,142 @@
1
+ /**
2
+ * The per-target shape differences on the ONE secrets transport
3
+ * ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)).
4
+ *
5
+ * `publishTargetSecrets` is the same call for every target; what differs is
6
+ * three small things, and they live here rather than in four framework bind
7
+ * files that each had to remember the contract:
8
+ *
9
+ * resolveValue applied to each collected value before it travels
10
+ * deriveValues applied to the COMPOSED map before anything reads it, so a
11
+ * key the brand never typed can still be valued (and a key it
12
+ * could NOT value names its own fix line)
13
+ * extraSecrets already-valued keys with no `.env` home at all
14
+ *
15
+ * devkit never requires a framework package, so nothing here imports desktop or
16
+ * backend: the desktop derivation is devkit's own signing-env module (the same
17
+ * one the desktop env load calls), and the backend credential is devkit's own
18
+ * service-account lookup.
19
+ */
20
+ const path = require('node:path');
21
+ const fs = require('node:fs');
22
+ const jetpack = require('fs-jetpack');
23
+ const { findBrandRoot } = require('../config/index.js');
24
+
25
+ const { deriveSigningEnv, signingPathCandidates } = require('./signing-env.js');
26
+ const { resolveServiceAccountPath } = require('./service-account.js');
27
+
28
+ // The env schema's name for the backend deploy credential
29
+ // (`delivery: { backend: 'ci' }`): the service-account JSON itself, which the
30
+ // workflow writes back to disk.
31
+ const SERVICE_ACCOUNT_KEY = 'OMEGA_SERVICE_ACCOUNT_JSON';
32
+
33
+ // The walk that PRODUCES the signing material a derivation could not find.
34
+ const CERTIFICATES_WALK = 'omega manage --service certificates';
35
+
36
+ /**
37
+ * Desktop's value seam: base64 for a value that names an existing file, the
38
+ * value itself otherwise.
39
+ *
40
+ * The derived signing paths are ABSOLUTE (they point into the signing tree,
41
+ * which lives outside the target), so an absolute value is resolved as given.
42
+ * A RELATIVE one still resolves under the target root then the brand root,
43
+ * because an operator may still type `CSC_LINK=config/certs/mine.p12` by hand.
44
+ *
45
+ * @param {object} options
46
+ * @param {string} options.targetDir - The target root.
47
+ * @returns {function} `(value, key) => string`
48
+ */
49
+ function fileValueResolver({ targetDir }) {
50
+ const brandRoot = findBrandRoot(targetDir);
51
+
52
+ return (value) => fileContentsBase64(value, { targetDir, brandRoot }) || value;
53
+ }
54
+
55
+ // The base64 contents of the file `value` names, or null when it names none.
56
+ // Heuristic: a path-shaped value (a separator, or a typical cert/key extension)
57
+ // that resolves to a real file. A path-shaped value that exists nowhere is
58
+ // pushed as-is: it is a string.
59
+ function fileContentsBase64(value, { targetDir, brandRoot }) {
60
+ const looksLikePath = /[/\\]/.test(value) || /\.(p12|pem|cer|p8|provisionprofile|crt|key|json)$/i.test(value);
61
+ if (!looksLikePath) return null;
62
+
63
+ const roots = path.isAbsolute(value) ? [''] : [targetDir, brandRoot].filter(Boolean);
64
+ for (const root of roots) {
65
+ const absolute = path.isAbsolute(value) ? value : path.join(root, value);
66
+ if (fs.existsSync(absolute) && fs.statSync(absolute).isFile()) {
67
+ return fs.readFileSync(absolute).toString('base64');
68
+ }
69
+ }
70
+
71
+ return null;
72
+ }
73
+
74
+ /**
75
+ * Desktop's COMPOSED-map seam: the signing paths the brand's signing tree
76
+ * holds, by the SAME derivation the desktop env load runs, plus the fix line
77
+ * each key the tree could not answer should carry.
78
+ *
79
+ * @param {object} options
80
+ * @param {string} options.targetDir - The target root.
81
+ * @returns {function} `(values) => { derived, fixes }`
82
+ */
83
+ function signingValueDeriver({ targetDir }) {
84
+ return (values) => {
85
+ const { derived, problems } = deriveSigningEnv({ env: values, targetDir });
86
+ const problemFor = new Map(problems.map(({ key, reason }) => [key, reason]));
87
+
88
+ const fixes = {};
89
+ for (const { key, paths } of signingPathCandidates({ env: values, targetDir })) {
90
+ if (values[key]) continue;
91
+
92
+ fixes[key] = problemFor.has(key)
93
+ ? `${problemFor.get(key)}. Fix it, then re-run.`
94
+ : `no signing file at ${paths.join(' or ')}. Run \`${CERTIFICATES_WALK}\` to produce it, then re-run.`;
95
+ }
96
+
97
+ return { derived, fixes };
98
+ };
99
+ }
100
+
101
+ /**
102
+ * The backend's service-account secret, valued from the authored key chain. An
103
+ * absent key publishes nothing: a brand that has not minted one yet deploys
104
+ * from a runner that cannot authenticate, and the missing secret is what says
105
+ * so.
106
+ *
107
+ * @param {string} targetDir - The target root.
108
+ * @returns {Object<string, string>} The one-key map, or an empty one.
109
+ */
110
+ function serviceAccountSecret(targetDir) {
111
+ const keyPath = resolveServiceAccountPath(targetDir);
112
+ const contents = keyPath ? jetpack.read(keyPath) : null;
113
+
114
+ return contents ? { [SERVICE_ACCOUNT_KEY]: contents } : {};
115
+ }
116
+
117
+ // Target type to its seams. A type with no entry has none, which is web and the
118
+ // extension: plain string secrets, nothing to reshape.
119
+ const SEAMS = {
120
+ desktop: ({ targetDir }) => ({
121
+ resolveValue: fileValueResolver({ targetDir }),
122
+ deriveValues: signingValueDeriver({ targetDir }),
123
+ }),
124
+ backend: ({ targetDir }) => ({
125
+ extraSecrets: serviceAccountSecret(targetDir),
126
+ }),
127
+ };
128
+
129
+ /**
130
+ * The seams this target type brings to the publish.
131
+ *
132
+ * @param {object} input
133
+ * @param {string} input.target - Target type ('desktop', 'backend', 'web', 'extension').
134
+ * @param {string} input.targetDir - The target root.
135
+ * @returns {{ resolveValue?: function, deriveValues?: function, extraSecrets?: object }}
136
+ */
137
+ function targetSeams({ target, targetDir }) {
138
+ const build = SEAMS[target];
139
+ return build ? build({ targetDir }) : {};
140
+ }
141
+
142
+ module.exports = { targetSeams, fileValueResolver, signingValueDeriver, serviceAccountSecret, SERVICE_ACCOUNT_KEY };