@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
@@ -21,14 +21,31 @@
21
21
  * ([#454](https://github.com/Omega-JS-Stack/omega/issues/454)), so they
22
22
  * drop out of every rendered and published set.
23
23
  *
24
- * Pattern families (`match`, no `name`) carry a delivery like anything else,
25
- * but have no fixed name to render, so nothing here can list them.
24
+ * The schema NAMES most of what travels, and two kinds of key it cannot name
25
+ * ride the same pipeline through the target's COMPOSED production values
26
+ * (`values`, from composeTargetEnv), which every list here takes as an option:
26
27
  *
27
- * Every function takes an optional `schema` so a caller — the tests — can
28
+ * - a PATTERN family member (`match`, no `name`): the schema knows
29
+ * `CONNECTIONS_<PROVIDER>_CLIENT_ID` as a shape, so only a composed set can
30
+ * say which providers this brand actually configured
31
+ * ([#876](https://github.com/Omega-JS-Stack/omega/issues/876)).
32
+ * - a CUSTOM key the schema does not know at all: a consumer's own line in
33
+ * the brand `.env`, which used to reach no runner and fail the consumer's
34
+ * own workflow step silently
35
+ * ([#835](https://github.com/Omega-JS-Stack/omega/issues/835)). It travels
36
+ * in the target's FILE mode: written into the `.env` the runner builds on
37
+ * the backend, the runner env alone everywhere else.
38
+ *
39
+ * Both halves come out of ONE primitive (`deliveredKeys`), so what a workflow
40
+ * injects, what push-secrets publishes, and what the backend's `.env` writer
41
+ * names can never disagree inside a run. No `values` = the schema half alone,
42
+ * which is what a lane with no brand to compose from gets.
43
+ *
44
+ * Every function takes an optional `schema` so a caller (the tests) can
28
45
  * exercise the rules against a fixture; the default is the real one.
29
46
  */
30
47
 
31
- const { ENV_SCHEMA } = require('./env-schema.js');
48
+ const { ENV_SCHEMA, envSchemaEntry } = require('./env-schema.js');
32
49
 
33
50
  // Keys a generated workflow's own `env:` block already declares. The generated
34
51
  // block never restates one (a repeated YAML mapping key is invalid) — they are
@@ -39,6 +56,39 @@ const WORKFLOW_OWNED_KEYS = ['GH_TOKEN', 'CLOUDFLARE_TOKEN', 'NODE_VERSION', 'NO
39
56
  // generated region is always a valid, self-explaining line of YAML.
40
57
  const EMPTY_BLOCK = '# (no CI-delivered keys for this target — the next omega verb regenerates this block)';
41
58
 
59
+ // What a target's generated workflow carries in the runner env, per target.
60
+ //
61
+ // The default is the CI half: `ci` plus `bake` (a bake is injected before it is
62
+ // baked), because those artifacts carry their values inside themselves and
63
+ // never read an env file.
64
+ //
65
+ // The BACKEND is the one target whose deployed artifact ships a composed `.env`
66
+ // and whose deploy now runs on a runner
67
+ // ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)): the runner has
68
+ // no brand checkout to compose that file from, so its workflow WRITES the .env
69
+ // from the runner env, which puts every `env` delivery on the runner too.
70
+ const WORKFLOW_MODES = {
71
+ backend: ['env', 'ci', 'bake'],
72
+ };
73
+
74
+ const DEFAULT_WORKFLOW_MODES = ['ci', 'bake'];
75
+
76
+ /**
77
+ * The mode a key the schema does not declare travels in, per target: the
78
+ * target's own FILE mode where it has one (backend, whose workflow writes the
79
+ * `.env` its upload ships with), the runner env everywhere else (#835).
80
+ *
81
+ * A custom key declares no delivery, so it takes the channel its target
82
+ * already uses for the keys it composes. A packaged app carries no `.env`, so
83
+ * there is nothing on web, desktop or the extension for a file mode to mean.
84
+ *
85
+ * @param {string} target - Target name.
86
+ * @returns {string} `'env'` or `'ci'`.
87
+ */
88
+ function customDeliveryMode(target) {
89
+ return (WORKFLOW_MODES[target] || DEFAULT_WORKFLOW_MODES).includes('env') ? 'env' : 'ci';
90
+ }
91
+
42
92
  /**
43
93
  * The delivered names a target receives in the given modes, sorted and
44
94
  * deduplicated.
@@ -74,6 +124,79 @@ function deliveredNames(target, modes, schema) {
74
124
  return [...names].sort();
75
125
  }
76
126
 
127
+ // GitHub's own rule for a secret name: letters, digits and underscores, never
128
+ // leading with a digit. A composed key that breaks it is refused by name.
129
+ const SECRET_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
130
+
131
+ /**
132
+ * The ONE delivered key set (#835, #876): the schema's named deliveries for
133
+ * this target in these modes, plus what only the target's COMPOSED production
134
+ * values can name.
135
+ *
136
+ * Every list in this file derives from here, so the workflow block, the
137
+ * published secrets and the backend's `.env` key list are three renderings of
138
+ * one set rather than three sets that have to agree.
139
+ *
140
+ * A composed key is added when it is a `match` family member this target
141
+ * delivers in these modes, or when the schema does not know it at all and the
142
+ * target's custom mode is one of them. It is NOT added when the schema names
143
+ * it: the walk above already ruled on every fixed name, so a `machineLocal`
144
+ * path and a key whose only delivery here is the local `.env` stay home
145
+ * exactly as they did before a composed set existed. A WORKFLOW_OWNED name is
146
+ * never taken from a composed set either: those belong to the template, and
147
+ * neither is a `GITHUB_`-prefixed name, which GitHub refuses as a secret. A
148
+ * composed name GitHub could not hold as a secret at all throws, naming the key
149
+ * and the rule.
150
+ *
151
+ * @param {string} target - Target name ('web', 'backend', …).
152
+ * @param {string[]} modes - The delivery modes to collect.
153
+ * @param {object} [options]
154
+ * @param {object[]} [options.schema] - Env schema entries (default: the real schema).
155
+ * @param {Object<string, string>} [options.values] - The target's composed
156
+ * production values (names are read, values never are).
157
+ * @returns {string[]} Delivered env var names, sorted.
158
+ */
159
+ function deliveredKeys(target, modes, { schema = ENV_SCHEMA, values = null } = {}) {
160
+ const names = new Set(deliveredNames(target, modes, schema));
161
+ if (!values) return [...names].sort();
162
+
163
+ const customMode = customDeliveryMode(target);
164
+
165
+ for (const key of Object.keys(values)) {
166
+ if (names.has(key)) continue;
167
+
168
+ const entry = envSchemaEntry(key, schema);
169
+
170
+ if (entry) {
171
+ // A fixed name is the walk above's business, whatever it decided.
172
+ if (entry.name) continue;
173
+ if (entry.machineLocal) continue;
174
+ if (!modes.includes(entry.delivery && entry.delivery[target])) continue;
175
+ names.add(key);
176
+ continue;
177
+ }
178
+
179
+ if (!modes.includes(customMode)) continue;
180
+ if (WORKFLOW_OWNED_KEYS.includes(key)) continue;
181
+ // GitHub REFUSES an Actions secret whose name starts with GITHUB_ (the
182
+ // prefix is the runner's own), so publishing one would fail the deploy
183
+ // precheck outright. A brand .env carrying GITHUB_TOKEN is a plausible
184
+ // typo for GH_TOKEN, and the workflow reads its own `secrets.GITHUB_TOKEN`
185
+ // either way.
186
+ if (key.startsWith('GITHUB_')) continue;
187
+ // dotenv reads `[\w.-]+` as a key, so a brand `.env` can carry a name
188
+ // GitHub's secret API refuses outright (`ACME.WEBHOOK`). Delivering it
189
+ // would render a workflow line nothing can resolve and fail the publish
190
+ // three layers from the typo, so it fails HERE, by name.
191
+ if (!SECRET_NAME.test(key)) {
192
+ throw new Error(`${key}: a delivered env key must be a GitHub secret name (${SECRET_NAME.source.replace(/^\^|\$$/g, '')}), so rename it in the target's .env`);
193
+ }
194
+ names.add(key);
195
+ }
196
+
197
+ return [...names].sort();
198
+ }
199
+
77
200
  /**
78
201
  * The keys a target's generated workflow needs in the runner env: every `ci`
79
202
  * delivery, plus every `bake` (a bake is injected before it is baked).
@@ -83,8 +206,60 @@ function deliveredNames(target, modes, schema) {
83
206
  * @param {object[]} [options.schema] - Env schema entries (default: the real schema).
84
207
  * @returns {string[]} Delivered env var names, sorted.
85
208
  */
86
- function workflowSecretKeys(target, { schema = ENV_SCHEMA } = {}) {
87
- return deliveredNames(target, ['ci', 'bake'], schema);
209
+ function workflowSecretKeys(target, { schema = ENV_SCHEMA, values = null } = {}) {
210
+ return deliveredKeys(target, WORKFLOW_MODES[target] || DEFAULT_WORKFLOW_MODES, { schema, values });
211
+ }
212
+
213
+ /**
214
+ * The keys a target's RUNTIME reads from the `.env` its artifact ships with:
215
+ * every `env` delivery, and ONLY those. Backend is the only target with one
216
+ * today, and the one caller is its generated workflow, which writes that file
217
+ * on the runner from the secrets the block above injects (#872).
218
+ *
219
+ * A `ci` key is deliberately not here: it reaches the deploy PROCESS through
220
+ * the runner env (the deploy credential, the license key the verdict is checked
221
+ * with) and the shipped artifact must never carry it. The mode list below is
222
+ * the whole of that exclusion, and `artifactEnvValues` applies the same rule to
223
+ * the other writer of a backend `.env`, the stage's composer.
224
+ *
225
+ * @param {string} target - Target name.
226
+ * @param {object} [options]
227
+ * @param {object[]} [options.schema] - Env schema entries.
228
+ * @returns {string[]} Delivered env var names, sorted.
229
+ */
230
+ function envFileKeys(target, { schema = ENV_SCHEMA, values = null } = {}) {
231
+ return deliveredKeys(target, ['env'], { schema, values });
232
+ }
233
+
234
+ /**
235
+ * Composed env values, narrowed to what a target's shipped artifact may carry:
236
+ * everything the composer resolved, minus every key the schema delivers to this
237
+ * target as `ci` ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)).
238
+ *
239
+ * The two sets differ on purpose. `composeTargetEnv` (env.js) resolves a value
240
+ * for EVERY key a target claims, because the secrets publisher needs the `ci`
241
+ * values too: a repo secret the workflow injects has to be valued from the same
242
+ * cascade as everything else. The `.env` written INTO an artifact is the
243
+ * narrower half, and this is the filter that says so, from the same declaration
244
+ * `envFileKeys` reads. A pattern family and a target-layer key have no `ci`
245
+ * delivery to match, so they pass through untouched.
246
+ *
247
+ * @param {string} target - Target name.
248
+ * @param {Object<string, string>} values - The composed values.
249
+ * @param {object} [options]
250
+ * @param {object[]} [options.schema] - Env schema entries.
251
+ * @returns {Object<string, string>} A new map, runner-only keys removed.
252
+ */
253
+ function artifactEnvValues(target, values, { schema = ENV_SCHEMA } = {}) {
254
+ const runnerOnly = new Set(deliveredKeys(target, ['ci'], { schema }));
255
+ const shipped = {};
256
+
257
+ for (const [key, value] of Object.entries(values)) {
258
+ if (runnerOnly.has(key)) continue;
259
+ shipped[key] = value;
260
+ }
261
+
262
+ return shipped;
88
263
  }
89
264
 
90
265
  /**
@@ -97,7 +272,27 @@ function workflowSecretKeys(target, { schema = ENV_SCHEMA } = {}) {
97
272
  * @returns {string[]} Delivered env var names, sorted.
98
273
  */
99
274
  function bakeKeys(target, { schema = ENV_SCHEMA } = {}) {
100
- return deliveredNames(target, ['bake'], schema);
275
+ return deliveredKeys(target, ['bake'], { schema });
276
+ }
277
+
278
+ /**
279
+ * The SOURCE names of a target's baked keys: the brand-level name a human sets
280
+ * (`GOOGLE_ANALYTICS_SECRET_DESKTOP`), not the `deliverAs` name the build reads
281
+ * it under. The bake GUARD names keys at the level a human can fix them, and it
282
+ * must not judge a target on keys it never bakes
283
+ * ([#891](https://github.com/Omega-JS-Stack/omega/issues/891): the desktop's
284
+ * other rules are CI-delivered signing credentials, which the deploy lane owns).
285
+ *
286
+ * @param {string} target - Target name.
287
+ * @param {object} [options]
288
+ * @param {object[]} [options.schema] - Env schema entries.
289
+ * @returns {string[]} Brand-level env var names, sorted.
290
+ */
291
+ function bakeSourceKeys(target, { schema = ENV_SCHEMA } = {}) {
292
+ return schema
293
+ .filter((entry) => entry.name && !entry.machineLocal && entry.delivery && entry.delivery[target] === 'bake')
294
+ .map((entry) => entry.name)
295
+ .sort();
101
296
  }
102
297
 
103
298
  /**
@@ -128,18 +323,45 @@ function publishSecretKeys(target, options) {
128
323
  * @param {string} [options.indent] - The token's indent (default two spaces).
129
324
  * @returns {string} The block body (no leading indent).
130
325
  */
131
- function renderSecretsBlock(target, { schema = ENV_SCHEMA, indent = ' ' } = {}) {
132
- const lines = workflowSecretKeys(target, { schema })
326
+ function renderSecretsBlock(target, { schema = ENV_SCHEMA, indent = ' ', values = null } = {}) {
327
+ const lines = workflowSecretKeys(target, { schema, values })
133
328
  .filter((key) => !WORKFLOW_OWNED_KEYS.includes(key))
134
329
  .map((key) => `${key}: \${{ secrets.${key} }}`);
135
330
 
136
331
  return lines.length ? lines.join(`\n${indent}`) : EMPTY_BLOCK;
137
332
  }
138
333
 
334
+ /**
335
+ * The KEY NAMES a workflow writes its target's `.env` from, as a JSON array
336
+ * ready to drop into the template's writer step (#872).
337
+ *
338
+ * NAMES only, never `KEY="$KEY"` lines: the writer is a node one-liner that
339
+ * reads each name out of the runner env and serializes the file through
340
+ * `serializeEnv` (env.js), the documented SSOT for every `.env` writeback. The
341
+ * step this replaced pasted values into a shell heredoc, where a value carrying
342
+ * a NEWLINE split its own line and everything after it parsed as a key of its
343
+ * own (verified: a `K="$K"` heredoc with a multi-line value yields the injected
344
+ * key), and a quote or a backslash left the file malformed. No shell ever sees
345
+ * a value now.
346
+ *
347
+ * @param {string} target - Target name.
348
+ * @param {object} [options]
349
+ * @param {object[]} [options.schema] - Env schema entries.
350
+ * @returns {string} A JSON array of names, sorted (`[]` when the target ships no .env).
351
+ */
352
+ function renderEnvFileKeys(target, { schema = ENV_SCHEMA, values = null } = {}) {
353
+ return JSON.stringify(envFileKeys(target, { schema, values }));
354
+ }
355
+
139
356
  module.exports = {
140
357
  WORKFLOW_OWNED_KEYS,
358
+ deliveredKeys,
141
359
  workflowSecretKeys,
360
+ envFileKeys,
361
+ artifactEnvValues,
142
362
  bakeKeys,
363
+ bakeSourceKeys,
143
364
  publishSecretKeys,
144
365
  renderSecretsBlock,
366
+ renderEnvFileKeys,
145
367
  };
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Retired ENV keys ([#893](https://github.com/Omega-JS-Stack/omega/issues/893))
3
+ * - the `.env` half of retired-keys.js.
4
+ *
5
+ * Six values were declared in both homes at once: the env schema carried them
6
+ * as non-secret keys while the config schema already declared (or now
7
+ * declares) the same fact. Two homes for one value drift, and the rule that
8
+ * settles which one wins is in docs/shared/config.md ("Config or env?"):
9
+ * config holds what is PUBLIC by design (an id a store URL carries, a key a
10
+ * browser bundle ships, a value printed in a binary), `.env` holds secrets,
11
+ * the login coordinates that only travel with a secret, and machine paths.
12
+ *
13
+ * There is no dual-read anywhere in OMEGA, so a `.env` line left behind is a
14
+ * value nothing reads: the id a brand carefully pasted would be ignored and
15
+ * the publish would address the wrong listing, or none. Every layer read
16
+ * therefore FAILS on one, naming the config path the value moved to
17
+ * (env.js's parseEnvFile, the one place a `.env` layer is parsed).
18
+ *
19
+ * The config-side register gets nothing from this move: these were never
20
+ * config keys, so nothing there was renamed.
21
+ *
22
+ * The register also carries the env-side RENAMES
23
+ * ([#845](https://github.com/Omega-JS-Stack/omega/issues/845)): a key that is
24
+ * still a secret and still lives in `.env`, under a new name. Same silence,
25
+ * same refusal; the fix it names is renaming the line, not deleting it.
26
+ *
27
+ * And it carries the keys that are retired OUTRIGHT
28
+ * ([#819](https://github.com/Omega-JS-Stack/omega/issues/819)): no config path,
29
+ * no new env name, because what replaced the key is a MECHANISM rather than a
30
+ * value. Such a row declares `replacement: null` and its refusal reads as a
31
+ * deletion with nowhere to move the value to.
32
+ */
33
+
34
+ // env var name -> { replacement, why, home } (docs/shared/config.md carries the
35
+ // rows). `home` is where the replacement LIVES: 'config' by default (the #893
36
+ // move), or 'env' for a key that stayed a secret and only changed NAME, which
37
+ // is a rename of the line rather than a deletion. `replacement: null` is a key
38
+ // with no successor of any kind (#819): the line is simply deleted, and the
39
+ // row's `why` names the mechanism that made it unnecessary.
40
+ const RETIRED_ENV_KEYS = {
41
+ CHROME_EXTENSION_ID: {
42
+ replacement: 'targets.<name>.listings.chrome.id',
43
+ why: 'a store item id is public by design (it is in the listing URL), so it lives beside that listing in config',
44
+ },
45
+ FIREFOX_EXTENSION_ID: {
46
+ replacement: 'targets.<name>.listings.firefox.id',
47
+ why: "the AMO add-on id IS the manifest's gecko id, and config is its ONE home: the local scaffold pins the derived id there and the package task writes it into the manifest",
48
+ },
49
+ EDGE_PRODUCT_ID: {
50
+ replacement: 'targets.<name>.listings.edge.id',
51
+ why: 'a store product id is public by design (it is in the listing URL), so it lives beside that listing in config',
52
+ },
53
+ RECAPTCHA_SITE_KEY: {
54
+ replacement: 'captcha.providers.recaptcha.siteKey',
55
+ why: 'the site key is rendered into every page that carries a form, so it is public by definition; RECAPTCHA_SECRET_KEY stays in .env',
56
+ },
57
+ PAYPAL_CLIENT_ID: {
58
+ replacement: 'payment.providers.paypal.clientId',
59
+ why: 'the public half of the PayPal pair, already declared in config; the backend read it through a boot bridge that copied config into env',
60
+ },
61
+ CHARGEBEE_SITE: {
62
+ replacement: 'payment.providers.chargebee.site',
63
+ why: 'the site name is in every Chargebee URL, already declared in config; the backend read it through a boot bridge that copied config into env',
64
+ },
65
+ // The connections pair ([#845](https://github.com/Omega-JS-Stack/omega/issues/845)):
66
+ // these are SECRETS and stayed in `.env`, so the row is a rename, not a move.
67
+ // Exact names only, one per provider: the register is a name lookup, so the
68
+ // `OAUTH2_*` family is spelled out a row at a time as a brand needs it, and
69
+ // Google is the pair every brand carries today.
70
+ OAUTH2_GOOGLE_CLIENT_ID: {
71
+ home: 'env',
72
+ replacement: 'CONNECTIONS_GOOGLE_CLIENT_ID',
73
+ why: '#788 renamed the oauth2 feature to connections, the env family included; the backend reads only CONNECTIONS_<PROVIDER>_CLIENT_ID now',
74
+ },
75
+ OAUTH2_GOOGLE_CLIENT_SECRET: {
76
+ home: 'env',
77
+ replacement: 'CONNECTIONS_GOOGLE_CLIENT_SECRET',
78
+ why: '#788 renamed the oauth2 feature to connections, the env family included; the backend reads only CONNECTIONS_<PROVIDER>_CLIENT_SECRET now',
79
+ },
80
+ // The test-lane pair ([#819](https://github.com/Omega-JS-Stack/omega/issues/819),
81
+ // Ian 2026-09-13): retired outright, so neither row names a replacement. A
82
+ // suite no longer holds a credential of its own, which is why there is
83
+ // nothing to move the value to.
84
+ OMEGA_TEST_FIREBASE_ADMIN_KEY: {
85
+ replacement: null,
86
+ why: 'retired: desktop, extension and web test their own sign-in through a seeded persona from the backend emulator (#904), so no suite mints a custom token from a service account any more',
87
+ },
88
+ OMEGA_TEST_USER_UID: {
89
+ replacement: null,
90
+ why: 'retired: desktop, extension and web test their own sign-in through a seeded persona from the backend emulator (#904), and the roster names the persona, so no uid is configured anywhere',
91
+ },
92
+ };
93
+
94
+ /**
95
+ * The retired keys a parsed `.env` layer carries, in the layer's own order.
96
+ *
97
+ * Presence is the whole test: an empty value (`KEY=`) documents a key nothing
98
+ * reads, so it is owed the same deletion as a valued one.
99
+ *
100
+ * @param {Object<string, string>} values - Parsed `.env` values.
101
+ * @returns {Array<{ key: string, replacement: string|null, why: string }>}
102
+ */
103
+ function findRetiredEnvKeys(values) {
104
+ return Object.keys(values || {})
105
+ .filter((key) => RETIRED_ENV_KEYS[key])
106
+ .map((key) => ({ key, ...RETIRED_ENV_KEYS[key] }));
107
+ }
108
+
109
+ /**
110
+ * Refuse a `.env` layer that still carries a retired key, spelling the move
111
+ * out. A row with no replacement says so plainly: the line is deleted and
112
+ * nothing takes the value anywhere (#819).
113
+ *
114
+ * @param {Object<string, string>} values - Parsed `.env` values.
115
+ * @param {string} envPath - The file the values came from (named in the error).
116
+ * @throws {Error} Naming every retired key in the layer, its new home, and why.
117
+ */
118
+ function assertNoRetiredEnvKeys(values, envPath) {
119
+ const found = findRetiredEnvKeys(values);
120
+ if (found.length === 0) return;
121
+
122
+ const moves = found.map(({ key, replacement, why, home }) => {
123
+ if (!replacement) return ` ${key} is retired outright: delete the .env line, nothing replaces the value (${why})`;
124
+
125
+ return home === 'env'
126
+ ? ` ${key} renamed to ${replacement}; rename the .env line (${why})`
127
+ : ` ${key} moved to ${replacement} in config/omega.json5; delete the .env line (${why})`;
128
+ });
129
+
130
+ throw new Error(
131
+ `${envPath} carries ${found.length} retired env key(s). Nothing reads them, so the value is silently lost:\n`
132
+ + `${moves.join('\n')}\n`
133
+ + 'The rule, and the whole register: docs/shared/config.md ("Config or env?").',
134
+ );
135
+ }
136
+
137
+ module.exports = { RETIRED_ENV_KEYS, findRetiredEnvKeys, assertNoRetiredEnvKeys };
@@ -26,7 +26,11 @@
26
26
  * PRESENCE ONLY, never a value shape (Ian 2026-08-26): a rule fires when the
27
27
  * key is empty, never because a value "looks wrong". `requiredWhen` is
28
28
  * one-directional — a truthy config path makes the key mandatory; an empty key
29
- * never says anything about the config.
29
+ * never says anything about the config. A rule may PIN the path to one value
30
+ * (`platforms.windows.signing.strategy=self-hosted`,
31
+ * [#891](https://github.com/Omega-JS-Stack/omega/issues/891)), because an
32
+ * enum's value is what decides which credentials exist: the EV token belongs to
33
+ * the self-hosted strategy, never to the cloud one.
30
34
  *
31
35
  * The checker NEVER throws and never logs. It returns the violations and the
32
36
  * caller decides the severity (build mode and production boot fail loudly,
@@ -53,6 +57,21 @@ function hasValue(env, entry) {
53
57
  return Boolean(entry.deliverAs && env[entry.deliverAs]);
54
58
  }
55
59
 
60
+ /**
61
+ * Whether a `requiredWhen` expression holds for a config: a truthy path, or an
62
+ * exact match when the expression pins one (`<path>=<value>`).
63
+ *
64
+ * @param {object} config - The resolved omega.json5 config.
65
+ * @param {string} expression - The schema entry's `requiredWhen`.
66
+ * @returns {boolean}
67
+ */
68
+ function requiredWhenHolds(config, expression) {
69
+ const [path, expected] = String(expression).split('=');
70
+ const value = getPath(config, path);
71
+
72
+ return expected === undefined ? Boolean(value) : String(value) === expected;
73
+ }
74
+
56
75
  /**
57
76
  * Check a resolved config + env pair against the schema's presence rules.
58
77
  *
@@ -82,7 +101,7 @@ function checkEnvRules(config, env, { target, schema = ENV_SCHEMA } = {}) {
82
101
 
83
102
  if (entry.required) {
84
103
  violations.push({ key: entry.name, rule: 'required', path: null });
85
- } else if (getPath(config, entry.requiredWhen)) {
104
+ } else if (requiredWhenHolds(config, entry.requiredWhen)) {
86
105
  violations.push({ key: entry.name, rule: 'requiredWhen', path: entry.requiredWhen });
87
106
  }
88
107
  }
@@ -90,4 +109,4 @@ function checkEnvRules(config, env, { target, schema = ENV_SCHEMA } = {}) {
90
109
  return violations;
91
110
  }
92
111
 
93
- module.exports = { checkEnvRules };
112
+ module.exports = { checkEnvRules, requiredWhenHolds };