@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
@@ -11,6 +11,7 @@
11
11
  * max: Number, // only checked when value present + number
12
12
  * match: RegExp, // only checked when value present + string
13
13
  * enum: [...], // only checked when value present
14
+ * itemEnum: [...], // an ARRAY value's allowed members
14
15
  * default: <value>, // the framework answer — see below
15
16
  * description: 'What the field drives.',
16
17
  * }
@@ -47,7 +48,7 @@
47
48
  * (#527 — the adsense `client` id is the ONE switch, and a materialized
48
49
  * advertising block would switch the web build's automatic vert placements on
49
50
  * for a brand that configured none). Blocks whose services gate on `enabled`
50
- * instead — github, cloudflare, searchConsole, slapform, chatsy, replyify —
51
+ * instead (cloudflare, searchConsole, slapform, chatsy, replyify)
51
52
  * DO carry the default that states the polarity (`enabled: true`, read
52
53
  * `!== false`), because presence there picks nothing. Role-level switches
53
54
  * beside any providers block (`monitoring.enabled`,
@@ -73,31 +74,44 @@
73
74
  // winback.js re-exports it, so every existing import keeps working.
74
75
  const WINBACK_DURATIONS = ['once', 'forever'];
75
76
 
76
- // Canonical target names — the FRAMEWORK keys allowed under `targets`.
77
- // Key presence in a config's `targets` object = "this brand enables this
78
- // target" (absorbs the legacy brand-config targets ARRAY).
77
+ // Where a brand hosts its SOURCE, and where a web target is SERVED from
78
+ // ([#883](https://github.com/Omega-JS-Stack/omega/issues/883)). One value each
79
+ // today: a second provider is a new entry here plus that provider's own helper,
80
+ // never a reshape of the config. They live HERE, with the rules that enumerate
81
+ // them, so repo.js can derive without a require cycle (the WINBACK_DURATIONS
82
+ // pattern above); repo.js re-exports both, and every reader takes them there.
83
+ const REPO_PROVIDERS = ['github'];
84
+ const HOSTING_PROVIDERS = ['github'];
85
+
86
+ // The category slugs addons.mozilla.org accepts on a listing (its live set on
87
+ // 2026-09-11). AMO has no utilities or productivity slug, so the framework
88
+ // default is `alerts-updates`, the most generic one that is not `other`
89
+ // ([#884](https://github.com/Omega-JS-Stack/omega/issues/884)). It lives HERE,
90
+ // with the rule that enumerates it (the WINBACK_DURATIONS pattern above), and
91
+ // @omega.js/extension's Firefox lane takes the SAME list from this package when
92
+ // it writes a first publish's `--amo-metadata` file: a slug AMO does not know
93
+ // fails in the build instead of at the store.
94
+ const AMO_CATEGORIES = ['alerts-updates', 'appearance', 'bookmarks', 'download-management', 'feeds-news-blogging', 'games-entertainment', 'language-support', 'photos-music-videos', 'privacy-security', 'search-tools', 'shopping', 'social-communication', 'tabs', 'web-development', 'other'];
95
+
96
+ // Canonical target TYPES: the frameworks a `targets.<name>.type` may name
97
+ // (#886). Key presence in a config's `targets` object = "this brand enables a
98
+ // target by that name" (absorbs the legacy brand-config targets ARRAY).
79
99
  const TARGETS = ['web', 'backend', 'desktop', 'extension', 'mobile'];
80
100
 
81
101
  // A brand also runs targets no framework owns — a Render API, a worker, a
82
- // script (#603). Those are declared under any OTHER key, and the entry must
83
- // say so: `targets.api: { type: 'custom' }`. The type is what separates a
84
- // deliberate custom target from a typo'd framework name, which stays an error.
85
- // Their verbs come entirely from the target's own package.json scripts; the
86
- // manager is the one that runs them (docs/manager/index.md).
102
+ // script (#603). Those declare `type: 'custom'` like any other target
103
+ // (`targets.api: { type: 'custom' }`), and their verbs come entirely from the
104
+ // target's own package.json scripts; the manager is the one that runs them
105
+ // (docs/manager/index.md).
87
106
  const CUSTOM_TARGET_TYPE = 'custom';
88
107
 
89
108
  /**
90
- * Whether a `targets.<key>` entry declares itself custom. The array
91
- * (multi-instance) form is custom only when EVERY instance says so — a mixed
92
- * array has no honest reading, so the validator fails it.
109
+ * Whether a `targets.<name>` entry declares itself custom.
93
110
  *
94
- * @param {object|Array|null|undefined} entry - The raw targets.<key> value.
111
+ * @param {object|null|undefined} entry - The raw targets.<name> value.
95
112
  * @returns {boolean} True when the entry is a custom target.
96
113
  */
97
114
  function isCustomTargetEntry(entry) {
98
- if (Array.isArray(entry)) {
99
- return entry.length > 0 && entry.every((instance) => instance?.type === CUSTOM_TARGET_TYPE);
100
- }
101
115
  return entry?.type === CUSTOM_TARGET_TYPE;
102
116
  }
103
117
 
@@ -125,25 +139,90 @@ function backendProjectType(entry) {
125
139
  return entry?.projectType === 'custom' ? 'custom' : 'firebase';
126
140
  }
127
141
 
142
+ // The page routes a brand translates when it says nothing
143
+ // ([#858](https://github.com/Omega-JS-Stack/omega/issues/858), Ian 2026-09-13):
144
+ // the whole site except the blog. Named here, beside the rule that defaults to
145
+ // it, because the web build's matcher reads the same constant rather than
146
+ // keeping a second copy of the framework's answer.
147
+ const TRANSLATION_INCLUDE_DEFAULT = ['**', '!blog/**'];
148
+
128
149
  // The machine-owned top-level sections identical in every omega.json5 —
129
150
  // omega-manager's disperse enumerates THIS list instead of hardcoding
130
151
  // per-target mapping blocks. (`targets` itself is the scoping key, not a
131
152
  // shared section; `theme` is project-owned but shared-shaped.)
132
153
  const SHARED_SECTIONS = ['brand', 'cloud', 'repo', 'edge', 'captcha', 'search', 'forms', 'inbound', 'analytics', 'advertising', 'features', 'payment', 'monitoring', 'connections', 'theme', 'translation'];
133
154
 
155
+ // Which config sections a BROWSER may see, and how much of each: the ONE
156
+ // declaration behind clientConfig() ([#894](https://github.com/Omega-JS-Stack/omega/issues/894)).
157
+ // Every browser surface bakes its artifact through that function (web's page
158
+ // chrome, desktop's renderer bundle, every extension bundle), so a value goes
159
+ // public by gaining a row HERE, never by a per-framework list. The flag is
160
+ // per SECTION, at the granularity the section's public half needs:
161
+ //
162
+ // `client: true` the whole section rides (its keys are public by design:
163
+ // secrets never live in omega.json5, and the validator
164
+ // hard-fails a secret-shaped key anywhere in the file)
165
+ // `client: [paths]` only these sub-paths ride, for a section whose other
166
+ // keys are provisioning-only (nothing secret, nothing a
167
+ // runtime reads)
168
+ // no row the browser never sees it: cloud/GCP provisioning,
169
+ // the account admins, the repo and edge plumbing, every
170
+ // build-only section (platforms, purgecss, collections…)
171
+ //
172
+ // A new section defaults to INVISIBLE, which fails as a missing feature rather
173
+ // than a leak. The build FACTS a surface composes beside these sections
174
+ // (runtime, version, dev, …) are client-config.js's own list.
175
+ const CLIENT_SECTIONS = {
176
+ advertising: true,
177
+ analytics: true,
178
+ app: true,
179
+ brand: true,
180
+ captcha: true,
181
+ client: true,
182
+ // The Firebase WEB config is public by design (the api key included) and is
183
+ // what boots @omega.js/client on all three surfaces; the GCP account facts
184
+ // beside it (billingAccount, organizationId, apiSubdomain) are provisioning.
185
+ cloud: ['config', 'messaging.vapidKey'],
186
+ company: true,
187
+ connections: true,
188
+ features: true,
189
+ // The contact page's endpoint, and nothing else the slapform service keeps
190
+ // beside it (template ids, plan, update flags).
191
+ forms: ['providers.slapform.formId'],
192
+ // The chat widget's agent and its presentation, same rule.
193
+ inbound: ['chat.providers.chatsy.enabled', 'chat.providers.chatsy.agentId', 'chat.providers.chatsy.settings'],
194
+ listings: true,
195
+ monitoring: true,
196
+ payment: true,
197
+ theme: true,
198
+ };
199
+
134
200
  // brand.id's slug rule — the ONE home for the pattern. Anything that gates a
135
201
  // caller-supplied brand id (the backend verts routes' normalizeBrandId) imports
136
202
  // this instead of copying the regex.
137
203
  const BRAND_ID_PATTERN = /^[a-z][a-z0-9+\-.]*$/;
138
204
 
139
205
  const SHARED_SCHEMA = [
140
- // ── the instance's own url ───────────────────────────────────────────────
206
+ // ── the target's own url ─────────────────────────────────────────────────
141
207
  {
142
208
  path: 'url',
143
209
  type: 'string',
144
210
  required: false,
145
211
  match: /^https?:\/\//,
146
- description: "This instance's own public URL, and the one key a target entry may set at the top level. A multi-instance target derives it from the instance id (`{ id: 'admin' }` → https://admin.<host of brand.url>, #588; docs/shared/config.md → Multi-instance targets); an entry's own `url` overrides it for a custom host. `brand.url` stays the BRAND's url, which is what brand-level facts (authDomain, the persona domain) read.",
212
+ description: "This TARGET's own public URL, and the one key a target entry may set at the top level. A target whose name is not its type derives it from the name (`admin: { type: 'web' }` → https://admin.<host of brand.url>, #588/#886; docs/shared/config.md → Targets); an entry's own `url` overrides it for a custom host. `brand.url` stays the BRAND's url, which is what brand-level facts (authDomain, the persona domain) read.",
213
+ },
214
+
215
+ // ── the target's own type (#886) ─────────────────────────────────────────
216
+ // Declared ONCE, here: every target entry carries it, whatever its
217
+ // framework, and a copy per TARGET_SCHEMAS would be four copies of one
218
+ // contract ([#911](https://github.com/Omega-JS-Stack/omega/issues/911)). It
219
+ // is target-entry machinery like `url`, so it names no config SECTION.
220
+ {
221
+ path: 'type',
222
+ type: 'string',
223
+ required: false,
224
+ enum: [...TARGETS, CUSTOM_TARGET_TYPE],
225
+ description: "The FRAMEWORK that runs this target (#886), or `custom` for a target no framework owns. Key presence in `targets` enables a target by NAME, and this is what says which code runs there, so a target named `admin` with `type: 'web'` is a web build. A resolved target config carries it at the top level like every other key in the entry.",
147
226
  },
148
227
 
149
228
  // ── brand ────────────────────────────────────────────────────────────────
@@ -179,12 +258,6 @@ const SHARED_SCHEMA = [
179
258
  required: false,
180
259
  description: 'Short marketing tagline.',
181
260
  },
182
- {
183
- path: 'brand.company',
184
- type: 'string',
185
- required: false,
186
- description: 'Parent/legal entity name ("Acme Inc"). Legal documents, receipts; falls back to brand.name.',
187
- },
188
261
  {
189
262
  path: 'brand.type',
190
263
  type: 'string',
@@ -266,12 +339,6 @@ const SHARED_SCHEMA = [
266
339
  required: false,
267
340
  description: 'Brand image URLs/paths (wordmark, brandmark, combomark, icon).',
268
341
  },
269
- {
270
- path: 'brand.images.companyWordmark',
271
- type: 'string',
272
- required: false,
273
- description: 'Parent/legal-entity wordmark (brand.company) rendered in the transactional email footer. Omitted from the footer when unset.',
274
- },
275
342
 
276
343
  // ── socials ──────────────────────────────────────────────────────────────
277
344
  {
@@ -439,7 +506,7 @@ const SHARED_SCHEMA = [
439
506
  path: 'advertising.providers.inhouse.source',
440
507
  type: 'string',
441
508
  required: false,
442
- description: "In-house ads source: 'self' (this brand's backend serves its own `ads` inventory), 'company' (the parent company's api — resolved via company.url), or a full base URL used verbatim. Presence-driven — unset disables house units.",
509
+ description: "In-house ads source: 'self' (this brand's backend serves its own `ads` inventory), 'company' (the parent company's api, derived from the RESOLVED company.url), or a full base URL used verbatim. Presence-driven, and unset disables house units.",
443
510
  },
444
511
  {
445
512
  path: 'advertising.fallback',
@@ -454,13 +521,31 @@ const SHARED_SCHEMA = [
454
521
  description: "This brand's contextual tags (['music', 'audio-tools']) — the targeting match input a house unit sends with its serve request unless the include passes its own. No user tracking.",
455
522
  },
456
523
 
457
- // ── company ──────────────────────────────────────────────────────────────
524
+ // ── company (ONE typed key, the rest RESOLVED) ───────────────────────────
525
+ // The brand's relationship to its company lives OUTSIDE `brand` (Ian
526
+ // 2026-09-12: "brand key is for things about this brand, and the
527
+ // parent/company/organization key is OUTSIDE of that"), and the loader fills
528
+ // the SAME key with the company's public facts (#677): `company.name`,
529
+ // `company.url` and `company.images.wordmark` are RESOLVED, never typed, and
530
+ // the loader refuses each of them in an authored file. A brand with no
531
+ // company resolves to its own name and url under a null id, so no reader
532
+ // carries a fallback.
458
533
  {
459
- path: 'company.url',
534
+ path: 'company.id',
460
535
  type: 'string',
461
536
  required: false,
462
- match: /^https?:\/\//,
463
- description: "The parent company's canonical URL. Sub-brand surfaces derive the company api from it (e.g. advertising inhouse source 'company' → api.<company host>).",
537
+ description: "The company this brand belongs to, named by the parent's `brand.id` ('itw-creative-works'), or the literal 'self' on the company brand itself. The ONE key that joins a brand to a company: the parent's public facts, its config layer, its `.env`, its hooks and its signing tree all resolve from it (docs/manager/company.md).",
538
+ },
539
+ {
540
+ path: 'company.webhooks',
541
+ type: 'boolean',
542
+ required: false,
543
+ default: true,
544
+ // Resolution only: an opt-OUT written into every brand file as `true`
545
+ // would be noise, and the fact belongs to the brands that actually share
546
+ // somebody else's provider account.
547
+ materialize: false,
548
+ description: "Whether the manage walk may repoint this company's provider ACCOUNT webhooks (#677). `false` says the SendGrid Event Webhook and the Beehiiv webhook belong to someone else (a shared account whose one account-level webhook points at their production), so the campaigns and newsletter services leave them alone. The successor to the retired top-level `parent: false`.",
464
549
  },
465
550
 
466
551
  // ── features (the catalog every product prices) ──────────────────────────
@@ -468,10 +553,17 @@ const SHARED_SCHEMA = [
468
553
  path: 'features',
469
554
  type: 'object',
470
555
  required: false,
471
- description: "The feature catalog (#647): every feature DEFINED once, keyed by id, in the order every surface renders it. An entry carries name, icon, definition — and, when the feature is METERED, a `usage` block ({ pace: 'daily' | false, mirror: ['<doc kind>'] }); an entry without one is a perk. Products name only the VALUE (payment.products[].features). Key order is row order on the pricing page and the account's usage bars.",
556
+ description: "The feature catalog (#647): every feature DEFINED once, keyed by id, in the order every surface renders it. An entry carries name, icon (the full Font Awesome class string, #929), definition — and, when the feature is METERED, a `usage` block ({ pace: 'daily' | false, mirror: ['<doc kind>'] }); an entry without one is a perk. Products name only the VALUE (payment.products[].features). Key order is row order on the pricing page and the account's usage bars.",
472
557
  },
473
558
 
474
559
  // ── payment (role: billing; provider-discriminated) ──────────────────────
560
+ {
561
+ path: 'payment.currency',
562
+ type: 'string',
563
+ required: false,
564
+ default: 'USD',
565
+ description: "The ISO 4217 code every price in payment.products is quoted in (#850). One currency per brand, named wherever a price is shown or recorded: the pricing page's JSON-LD, the checkout, and the backend's order history (GET /user/orders). Defaults to 'USD', the fallback its readers carried before the fact had a home. The successor to the retired targets.web.currency.",
566
+ },
475
567
  {
476
568
  path: 'payment.providers.stripe.publishableKey',
477
569
  type: 'string',
@@ -555,7 +647,7 @@ const SHARED_SCHEMA = [
555
647
  type: 'string',
556
648
  required: false,
557
649
  match: /^https?:\/\//,
558
- description: 'Sentry DSN (public by design). Per-surface DSNs go in targets.<type>.monitoring.providers.sentry.dsn overrides.',
650
+ description: 'Sentry DSN (public by design). Per-surface DSNs go in targets.<name>.monitoring.providers.sentry.dsn overrides.',
559
651
  },
560
652
  {
561
653
  path: 'monitoring.providers.sentry.environment',
@@ -668,39 +760,35 @@ const SHARED_SCHEMA = [
668
760
  description: "Per-provider user-connection settings, keyed by provider name (`connections: { twitch: {…} }`) — public values only, never client secrets (those are the CONNECTIONS_<PROVIDER>_CLIENT_ID/_SECRET env pair). The set of providers is OPEN (a brand ships its own as `src/connections/<name>.js`), so the section stays free-form; the keys every entry may carry are `enabled` (the packaged providers default to false — set it true to offer one; a brand's own provider is on unless this is false), `scope` (an array that wins over the provider module's default), `name` and `logo` (what the account page's card draws — the CONFIG is the only card list, #793: `logo` is the name of a mark @omega.js/web ships in core/logos/brandmarks/original, rendered inline, or a full URL rendered as an img), and `description` (the line under the card's title). The five packaged providers carry all three by default, so enabling one is one line. packages/backend/docs/connections.md",
669
761
  },
670
762
 
671
- // ── repo (role: source hosting; provider-discriminated) ─────────────────
763
+ // ── repo (where the brand hosts its source; #883) ───────────────────────
764
+ // Two keys, and block PRESENCE is the switch, exactly as a target's key
765
+ // presence enables that target. Every repo NAME derives from
766
+ // `<brand.id>-<role>` (#809), and visibility is the brand root package.json's
767
+ // `private` field, so neither is configurable here.
672
768
  {
673
- path: 'repo.providers.github.enabled',
674
- type: 'boolean',
769
+ path: 'repo',
770
+ type: 'object',
675
771
  required: false,
676
- default: true,
677
- description: 'false = the repo service skips entirely — no org profile, no repo settings, no GitHub Pages reconciliation; the brand hosts its source somewhere the manager does not touch.',
772
+ description: 'Where the brand hosts its source. Presence enables the repo service; the block is provider + org and nothing else, since every repo name derives from `<brand.id>-<role>` (#883).',
678
773
  },
679
774
  {
680
- path: 'repo.providers.github.org',
775
+ path: 'repo.provider',
776
+ // NO `default:`, by the presence-gate exclusion (docs/shared/config.md,
777
+ // Defaults & self-healing): the BLOCK's presence is what enables the repo
778
+ // service, so a resolved `repo: { provider: 'github' }` would say a brand
779
+ // hosts its source somewhere when it declared nothing. The value a missing
780
+ // provider reads is repo.js's DEFAULT_REPO_PROVIDER, beside the derivation
781
+ // that reads it.
681
782
  type: 'string',
682
783
  required: false,
683
- description: "GitHub org/user the brand monorepo lives in. No org → the repo service skips and the site's desktop release URLs stay underived.",
784
+ enum: REPO_PROVIDERS,
785
+ description: 'Which host serves the brand source (default `github`, the only one built). A second provider is a new row here plus its own helper, never a reshape of this block.',
684
786
  },
685
787
  {
686
- path: 'repo.providers.github.repo',
788
+ path: 'repo.org',
687
789
  type: 'string',
688
790
  required: false,
689
- description: 'Brand repo name, or an "owner/name" slug when the repo lives under a different org than repo.providers.github.org. Defaults to <brand.id>-omega (the <brand.id>-<role> repo rule).',
690
- },
691
- {
692
- path: 'repo.providers.github.shared',
693
- type: 'boolean',
694
- required: false,
695
- default: false,
696
- description: 'true = the org is shared with other brands; org-level reconciliation is skipped.',
697
- },
698
- {
699
- path: 'repo.providers.github.private',
700
- type: 'boolean',
701
- required: false,
702
- default: true,
703
- description: 'Brand repo visibility (default true).',
791
+ description: "GitHub org/user every repo the brand owns lives in: `<brand.id>-omega` (source), `<brand.id>-releases` (public artifacts), `<brand.id>-<name>` (one per GitHub-hosted web target). No org → the repo service skips and the site's desktop release URLs stay underived.",
704
792
  },
705
793
 
706
794
  // ── domain (two roles: registrar + mailbox; provider-keyed, #425) ────────
@@ -964,6 +1052,14 @@ const SHARED_SCHEMA = [
964
1052
  description: "Certificate types managed for the Apple Developer account ([{ type, manual }]) — `manual: true` marks the ones Apple's API cannot create, which the Account Holder downloads from the portal. Unset uses the framework set.",
965
1053
  },
966
1054
 
1055
+ // ── publishing (role: ship credentials; no provider) ─────────────────
1056
+ {
1057
+ path: 'publishing.enabled',
1058
+ type: 'boolean',
1059
+ required: false,
1060
+ description: "The manage walk's ship-credential gate ([#867](https://github.com/Omega-JS-Stack/omega/issues/867)): the `publishing` service asks for every developer key and listing id the brand's declared formats need (`targets.<name>.platforms.<platform>.formats`). `false` is the permanent opt-out the gate's Disable lands; absence means ask, and no default is materialized. Dropping ONE store is the declaration's job (`platforms.<store>.formats.store: false`), never this switch.",
1061
+ },
1062
+
967
1063
  // ── devlog (role: commit-digest publishing; provider-discriminated) ──────
968
1064
  {
969
1065
  path: 'devlog',
@@ -994,28 +1090,12 @@ const SHARED_SCHEMA = [
994
1090
  description: "Managed Firebase Auth accounts (@omega.js/manager account service): enabled + admins ([{ email, account, marketing }]; '{domain}' templates to the brand domain). Owner-defined — typically set once in the COMPANY omega.json5 (arrays replace, so the company list wins whole). Passwords NEVER live here: per-account OMEGA_ACCOUNT_PASSWORD__* env vars, the config/hooks/account/password.js hook, or the ACCOUNT_PASSWORD_SEED derivation.",
995
1091
  },
996
1092
 
997
- // ── parent ───────────────────────────────────────────────────────────────
998
- // parent … dataRequest are brand-level sections the manager reads UNFOLDED
1093
+ // ── reviews ──────────────────────────────────────────────────────────────
1094
+ // reviews … dataRequest are brand-level sections the manager reads UNFOLDED
999
1095
  // (#277): a website-only brand has no targets.backend to hold them, and
1000
1096
  // adding one purely as a config home would falsely enable the target. A
1001
1097
  // targets.backend block still overrides any of them: that comes free from
1002
1098
  // the merge chain.
1003
- {
1004
- path: 'parent',
1005
- type: 'string|boolean',
1006
- required: false,
1007
- description: "Webhook parent topology: 'self' when this brand IS the parent, a parent URL otherwise, or false to deliberately opt out (shared webhook account owned elsewhere).",
1008
- },
1009
-
1010
- // ── github ───────────────────────────────────────────────────────────────
1011
- {
1012
- path: 'github',
1013
- type: 'object',
1014
- required: false,
1015
- description: 'GitHub identity for the brand (user, website repo URL).',
1016
- },
1017
-
1018
- // ── reviews ──────────────────────────────────────────────────────────────
1019
1099
  {
1020
1100
  path: 'reviews',
1021
1101
  type: 'object',
@@ -1141,7 +1221,7 @@ const SHARED_SCHEMA = [
1141
1221
  path: 'directory',
1142
1222
  type: 'object',
1143
1223
  required: false,
1144
- description: "Directory participation (@omega.js/manager directory service): { enabled } — opt in to push this brand's entry into the parent project's `brands` collection. Absent or false never pushes. Needs `parent` to name the relationship and DIRECTORY_SERVICE_ACCOUNT in the brand .env.",
1224
+ description: "Directory participation (@omega.js/manager directory service): { enabled } opts in to pushing this brand's entry into the parent project's `brands` collection. Absent or false never pushes. Needs `company.id` to name a parent (#677) and DIRECTORY_SERVICE_ACCOUNT in the brand .env.",
1145
1225
  },
1146
1226
  {
1147
1227
  path: 'directory.enabled',
@@ -1242,10 +1322,15 @@ const SHARED_SCHEMA = [
1242
1322
  description: "Model override for the chosen provider (defaults: claude → 'sonnet' alias, chatgpt → 'gpt-5.4-nano').",
1243
1323
  },
1244
1324
  {
1245
- path: 'translation.exclude',
1325
+ path: 'translation.include',
1246
1326
  type: 'array',
1247
1327
  required: false,
1248
- description: 'Web only: extra page paths/folders to skip (system pages like checkout/legal/auth are always skipped).',
1328
+ default: TRANSLATION_INCLUDE_DEFAULT,
1329
+ // Resolution only: the list is the FRAMEWORK's answer to "what is worth
1330
+ // paying a provider for", and a copy in every brand config is a copy that
1331
+ // drifts from it (#793). A brand that wants a different answer writes one.
1332
+ materialize: false,
1333
+ description: "Web only: the page routes to translate, as globs with `!` negation, read in order like a .gitignore: the LAST pattern that matches a route decides it, and a route no pattern matches is not translated. A folder pattern covers the folder itself (`!blog/**` excludes `/blog` and everything under it). The default ['**', '!blog/**'] translates the whole site except the blog, which is where the words (and the cost) pile up. A brand list REPLACES the default outright. A page overrides it for itself with `translation.include: true`/`false` in its own frontmatter, the same key name one level down (docs/web/frontmatter.md). The framework's own default pages carry their own exclusion and are never in this list's hands.",
1249
1334
  },
1250
1335
 
1251
1336
  // ── client (the @omega.js/client runtime blob) ───────────────────────────
@@ -1306,7 +1391,7 @@ const SHARED_SCHEMA = [
1306
1391
  path: 'targets',
1307
1392
  type: 'object',
1308
1393
  required: false,
1309
- description: "Key presence = target enabled; values = target-scoped config (any shared key inside overrides it) — an object, or an array of id'd instances (multi-instance targets). Framework keys are web/backend/desktop/extension/mobile; any other key must declare `type: 'custom'` (#603), a target the manager drives entirely through its own package.json scripts. Anything else is an error.",
1394
+ description: "Every key is a target NAME, which is the folder `targets/<name>`, the `--target=<name>` word and the derived-repo suffix (#886). Key presence = target enabled; the value is an object of target-scoped config (any shared key inside overrides it) and MUST declare `type`: web/backend/desktop/extension/mobile, or `custom` (#603) for a target the manager drives entirely through its own package.json scripts. Anything else is an error.",
1310
1395
  },
1311
1396
  ];
1312
1397
 
@@ -1316,6 +1401,22 @@ const TARGET_SCHEMAS = {
1316
1401
  // Grows with @omega.js/web's design: distribute, purgecss safelist,
1317
1402
  // workflows land as their features do.
1318
1403
  web: [
1404
+ {
1405
+ path: 'hosting',
1406
+ type: 'object',
1407
+ required: false,
1408
+ description: 'Where this web target is SERVED from (#883). Web targets only: a non-web target carrying it is a validation error.',
1409
+ },
1410
+ {
1411
+ path: 'hosting.provider',
1412
+ // No `default:` either, for the same reason as `repo.provider`: the value
1413
+ // is read off the TARGET's own entry (repo.js `hostingProvider`), where a
1414
+ // resolved top-level default never reaches.
1415
+ type: 'string',
1416
+ required: false,
1417
+ enum: HOSTING_PROVIDERS,
1418
+ description: "Who serves the built site. `github` publishes it to the target's own `<brand.id>-<name>` repo and serves it from GitHub Pages at the target's url; a second provider is a new row here plus its own deploy helper.",
1419
+ },
1319
1420
  {
1320
1421
  path: 'meta',
1321
1422
  type: 'object',
@@ -1387,9 +1488,8 @@ const TARGET_SCHEMAS = {
1387
1488
 
1388
1489
  // Seeded from the sandbox brand's real @omega.js/backend config (backend-manager-config.json).
1389
1490
  backend: [
1390
- // parent, github, reviews, marketing, blog, dataRequest moved to
1391
- // SHARED_SCHEMA (#277); a targets.backend block still overrides them
1392
- // through the merge chain.
1491
+ // reviews, marketing, blog, dataRequest moved to SHARED_SCHEMA (#277); a
1492
+ // targets.backend block still overrides them through the merge chain.
1393
1493
  {
1394
1494
  path: 'projectType',
1395
1495
  type: 'string',
@@ -1416,12 +1516,157 @@ const TARGET_SCHEMAS = {
1416
1516
  enum: ['productivity', 'developer-tools', 'utilities', 'media', 'social', 'network'],
1417
1517
  description: 'Generic high-level category. Maps to per-platform UTI + freedesktop strings.',
1418
1518
  },
1519
+ // ── the rest of app.* (#911) ─────────────────────────────────────────
1520
+ // The three DERIVED fields carry no schema `default:`: their answer comes
1521
+ // from the brand (the bundle-id policy, brand.name, the year), so a
1522
+ // materialized null would be a brand file restating "derive it".
1523
+ {
1524
+ path: 'app.appId',
1525
+ type: 'string',
1526
+ required: false,
1527
+ description: "The bundle identifier the app is signed and registered under. Unset, it derives as `certificates.providers.apple.bundleIdPrefix` plus `brand.id` with the dashes as dots ([#909](https://github.com/Omega-JS-Stack/omega/issues/909)), the very id the certificates service registers; a brand with no prefix falls back to the reverse-domain of `brand.url`, then `app.<brand.id>`. Set it only to stay on an id already-shipped builds carry.",
1528
+ },
1529
+ {
1530
+ path: 'app.productName',
1531
+ type: 'string',
1532
+ required: false,
1533
+ description: 'The product name the installer, the app menu and the artifact names carry. Unset, it is `brand.name`.',
1534
+ },
1535
+ {
1536
+ path: 'app.copyright',
1537
+ type: 'string',
1538
+ required: false,
1539
+ description: 'The copyright string baked into the mac plist and the Windows version resource. A `{YEAR}` token expands at build time. Unset, it is `© {YEAR}, <brand.name>`.',
1540
+ },
1541
+ {
1542
+ path: 'app.languages',
1543
+ type: 'array',
1544
+ required: false,
1545
+ description: "The languages the mac build ships (electron-builder's `mac.electronLanguages`). Unset reads as ['en'].",
1546
+ },
1547
+ {
1548
+ path: 'app.darkModeSupport',
1549
+ type: 'boolean',
1550
+ required: false,
1551
+ description: 'Whether the mac build declares dark-mode support (NSRequiresAquaSystemAppearance). Unset reads as true; windows and linux ignore it.',
1552
+ },
1553
+ {
1554
+ path: 'omega.authPersistence',
1555
+ type: 'string',
1556
+ required: false,
1557
+ description: "Where @omega.js/client's auth session is kept in the main process: `safeStorage` (the default, the OS keychain through Electron safeStorage), `none` (in-memory, the explicit opt-out), or the name of a strategy the consumer registered before `initialize()`. A test run is always `none`, whatever this says.",
1558
+ },
1559
+ // ── the shipping declaration (#867) ──────────────────────────────────
1560
+ // The SAME shape the extension target declares below: presence is the
1561
+ // switch, every platform and every format defaults ON, and the only thing
1562
+ // that drops one is a literal `false` (the `certificates: false` idiom).
1563
+ // Per-format settings live INSIDE the format. The vocabulary is the
1564
+ // client's (mac, windows, linux), and @omega.js/config's platforms.js is
1565
+ // the format table every reader derives from.
1566
+ {
1567
+ path: 'platforms',
1568
+ type: 'object',
1569
+ required: false,
1570
+ description: "What this desktop target SHIPS, plus each platform's install knobs (#867): `platforms.<mac|windows|linux>.formats.<dmg|nsis|deb|appimage|snap>`. Presence = enabled; drop a default with `false`. The install knobs beside `formats` (arch, the NSIS flags, mac entitlements) map onto the same electron-builder concepts, which @omega.js/desktop owns the target list of.",
1571
+ },
1572
+ {
1573
+ path: 'platforms.mac',
1574
+ type: 'object|boolean',
1575
+ required: false,
1576
+ description: 'The mac leg: `false` ships no mac build at all (no dmg, no auto-update zip).',
1577
+ },
1578
+ {
1579
+ path: 'platforms.mac.formats',
1580
+ type: 'object',
1581
+ required: false,
1582
+ description: 'Which mac formats ship. `dmg` is the only one a brand declares; the auto-update zip is not a format (electron-updater fetches it from the feed and nobody links it).',
1583
+ },
1584
+ {
1585
+ path: 'platforms.mac.formats.dmg',
1586
+ type: 'object|boolean',
1587
+ required: false,
1588
+ description: 'The macOS disk image, and the download `/download/mac/dmg` hands over. `false` drops it.',
1589
+ },
1590
+ {
1591
+ path: 'platforms.windows',
1592
+ type: 'object|boolean',
1593
+ required: false,
1594
+ description: 'The windows leg: `false` ships no windows build and skips the signing jobs with it.',
1595
+ },
1596
+ {
1597
+ path: 'platforms.windows.formats',
1598
+ type: 'object',
1599
+ required: false,
1600
+ description: 'Which windows formats ship. `nsis` is the one installer, and it merges every declared arch into one file.',
1601
+ },
1419
1602
  {
1420
- path: 'platforms.win.signing.strategy',
1603
+ path: 'platforms.windows.formats.nsis',
1604
+ type: 'object|boolean',
1605
+ required: false,
1606
+ description: 'The NSIS installer, and the download `/download/windows/nsis` hands over. `false` drops it. Its installer UX knobs (oneClick, shortcuts, perMachine) sit on `platforms.windows` itself.',
1607
+ },
1608
+ {
1609
+ path: 'platforms.windows.signing.strategy',
1421
1610
  type: 'string',
1422
1611
  required: false,
1423
1612
  enum: ['self-hosted', 'cloud', 'local'],
1424
- description: 'Windows code-signing path. self-hosted = EV USB token on a runner; cloud = provider CLI; local = developer signs manually.',
1613
+ description: 'Windows code-signing path. self-hosted = EV USB token on a runner; cloud = provider CLI; local = developer signs manually. The env schema gates each signing credential on this value, so the walk only ever asks for the set this strategy uses.',
1614
+ },
1615
+ {
1616
+ path: 'platforms.linux',
1617
+ type: 'object|boolean',
1618
+ required: false,
1619
+ description: 'The linux leg: `false` ships no linux build at all.',
1620
+ },
1621
+ {
1622
+ path: 'platforms.linux.formats',
1623
+ type: 'object',
1624
+ required: false,
1625
+ description: 'Which linux formats ship: `deb` and `appimage` are release assets, `snap` publishes to the Snap Store instead.',
1626
+ },
1627
+ {
1628
+ path: 'platforms.linux.formats.deb',
1629
+ type: 'object|boolean',
1630
+ required: false,
1631
+ description: 'The Debian package, and the download `/download/linux/deb` hands over. It needs `brand.url` and `brand.contact.email` (electron-builder refuses a deb with no homepage or maintainer).',
1632
+ },
1633
+ {
1634
+ path: 'platforms.linux.formats.appimage',
1635
+ type: 'object|boolean',
1636
+ required: false,
1637
+ description: 'The AppImage, and the download `/download/linux/appimage` hands over. `false` drops it.',
1638
+ },
1639
+ {
1640
+ path: 'platforms.linux.formats.snap',
1641
+ type: 'object|boolean',
1642
+ required: false,
1643
+ description: "The Snap Store publish (never a release asset: the store holds the file, and `/download/linux/snap` is the listing). Needs SNAPCRAFT_STORE_CREDENTIALS, which the env schema gates on this very key, so declaring it is what makes the walk ask.",
1644
+ },
1645
+ {
1646
+ path: 'platforms.linux.formats.snap.channels',
1647
+ type: 'array',
1648
+ required: false,
1649
+ description: "Snap Store channels the publish pushes to. Unset, the publish uses ['stable']; ['edge'] is the pre-release lane. No schema `default:` on any of these four: materializing one would declare the snap format on every brand that never mentioned it, and the declaration is what makes its credentials mandatory.",
1650
+ },
1651
+ {
1652
+ path: 'platforms.linux.formats.snap.confinement',
1653
+ type: 'string',
1654
+ required: false,
1655
+ enum: ['strict', 'classic', 'devmode'],
1656
+ description: 'Snap confinement. Unset reads as strict, which is what the store reviews fastest; classic needs manual store approval.',
1657
+ },
1658
+ {
1659
+ path: 'platforms.linux.formats.snap.grade',
1660
+ type: 'string',
1661
+ required: false,
1662
+ enum: ['stable', 'devel'],
1663
+ description: 'Snap grade. Unset reads as stable; a devel snap cannot be released to a stable channel.',
1664
+ },
1665
+ {
1666
+ path: 'platforms.linux.formats.snap.autoStart',
1667
+ type: 'boolean',
1668
+ required: false,
1669
+ description: 'Whether the snap registers the app to start on login. Unset reads as true.',
1425
1670
  },
1426
1671
  {
1427
1672
  path: 'startup.mode',
@@ -1430,29 +1675,81 @@ const TARGET_SCHEMAS = {
1430
1675
  enum: ['normal', 'hidden'],
1431
1676
  description: 'normal = main window appears at launch; hidden = bakes LSUIElement=true on macOS (no dock, no Cmd+Tab).',
1432
1677
  },
1678
+ // ── the OTHER startup knob (#911): what an OS LOGIN launch does ──────
1433
1679
  {
1434
- path: 'cdp.readySignal',
1680
+ path: 'startup.openAtLogin.enabled',
1681
+ type: 'boolean',
1682
+ required: false,
1683
+ description: 'Whether the app registers with the OS to auto-launch at login. Unset reads as true.',
1684
+ },
1685
+ {
1686
+ path: 'startup.openAtLogin.mode',
1435
1687
  type: 'string',
1436
1688
  required: false,
1437
- description: 'Boot-complete signal for `mgr cdp relaunch` — a URL substring matched against CDP page targets.',
1689
+ enum: ['normal', 'hidden'],
1690
+ description: 'How an AT-LOGIN launch behaves, independently of `startup.mode` (which owns user-direct launches). Unset reads as hidden, so a normal app still starts quietly at login and surfaces when the user opens it.',
1438
1691
  },
1692
+ // ── the auto-updater block (#911) ────────────────────────────────────
1693
+ // Its runtime answers live in the lib's own DEFAULTS, which every
1694
+ // unset key falls through to; the cadences are two separate timers on
1695
+ // purpose (a feed poll is network, an idle evaluation is arithmetic).
1439
1696
  {
1440
- path: 'releases.enabled',
1697
+ path: 'autoUpdate.enabled',
1441
1698
  type: 'boolean',
1442
1699
  required: false,
1443
- description: 'One switch for the release surface: false suppresses the site\'s derived download links AND desktop publishing (electron-builder publish). Site derivation defaults true only when the releases block exists; desktop publishing defaults true regardless.',
1700
+ description: 'One switch for the auto-updater. Unset reads as true; `false` leaves the whole lane inert (no feed poll, no idle evaluator, no install).',
1444
1701
  },
1445
1702
  {
1446
- path: 'releases.owner',
1447
- type: 'string',
1703
+ path: 'autoUpdate.autoDownload',
1704
+ type: 'boolean',
1705
+ required: false,
1706
+ description: 'Whether a found update downloads on its own. Unset reads as true; false leaves the download to `manager.autoUpdater` being asked for it.',
1707
+ },
1708
+ {
1709
+ path: 'autoUpdate.startupDelayMs',
1710
+ type: 'integer',
1711
+ required: false,
1712
+ min: 0,
1713
+ description: 'How long after the app is ready the FIRST update check fires. Unset reads as 10000 (10s), which keeps the check off the boot path.',
1714
+ },
1715
+ {
1716
+ path: 'autoUpdate.feedCheckIntervalMs',
1717
+ type: 'integer',
1718
+ required: false,
1719
+ min: 0,
1720
+ description: 'How often the update feed is polled. Network-bound, so keep it slow: unset reads as 3600000 (1h), the cadence Discord, Slack and VS Code use.',
1721
+ },
1722
+ {
1723
+ path: 'autoUpdate.idleEvalIntervalMs',
1724
+ type: 'integer',
1725
+ required: false,
1726
+ min: 0,
1727
+ description: 'How often a DOWNLOADED update re-asks "is the user idle enough to install". In-process arithmetic, so it is cheap: unset reads as 60000 (1m).',
1728
+ },
1729
+ {
1730
+ path: 'autoUpdate.maxAgeMs',
1731
+ type: 'integer',
1448
1732
  required: false,
1449
- description: 'GitHub owner of the releases repo; defaults to the brand repo owner.',
1733
+ min: 0,
1734
+ description: 'How long a downloaded update may sit pending before the install stops waiting for an idle moment. Unset reads as 2592000000 (30d).',
1450
1735
  },
1451
1736
  {
1452
- path: 'releases.repo',
1737
+ path: 'cdp.readySignal',
1453
1738
  type: 'string',
1454
1739
  required: false,
1455
- description: 'GitHub repo where built artifacts + the auto-update feed live. Defaults to `<brand.id>-releases`.',
1740
+ description: 'Boot-complete signal for `mgr cdp relaunch`: a URL substring matched against CDP page targets.',
1741
+ },
1742
+ {
1743
+ path: 'releases.enabled',
1744
+ type: 'boolean',
1745
+ required: false,
1746
+ description: 'One switch for the release surface: false suppresses the site\'s derived download links AND desktop publishing (electron-builder publish). Site derivation defaults true only when the releases block exists; desktop publishing defaults true regardless.',
1747
+ },
1748
+ {
1749
+ path: 'releases',
1750
+ type: 'object',
1751
+ required: false,
1752
+ description: "Presence opts the brand into the release surface (the site's download links, desktop publishing). WHICH repo is not configurable: it is the brand's one public `<brand.id>-releases` (#883).",
1456
1753
  },
1457
1754
  {
1458
1755
  path: 'restartManager.enabled',
@@ -1474,6 +1771,20 @@ const TARGET_SCHEMAS = {
1474
1771
  match: /^[\w.-]+$/,
1475
1772
  description: 'GitHub repo of the RM release feed.',
1476
1773
  },
1774
+ // ── hot config (#911): the JSON the app re-reads without a release ───
1775
+ {
1776
+ path: 'remoteConfig.enabled',
1777
+ type: 'boolean',
1778
+ required: false,
1779
+ description: 'The remote-config document, polled at the auto-updater feed cadence so app behavior (a force-update gate, a kill switch) flips without shipping a build. Unset reads as true.',
1780
+ },
1781
+ {
1782
+ path: 'remoteConfig.url',
1783
+ type: 'string',
1784
+ required: false,
1785
+ match: /^https?:\/\//,
1786
+ description: 'Where that document is fetched from. Unset, it derives as `${brand.url}/data/resources/main.json`.',
1787
+ },
1477
1788
  {
1478
1789
  path: 'remoteScripts.enabled',
1479
1790
  type: 'boolean',
@@ -1499,6 +1810,109 @@ const TARGET_SCHEMAS = {
1499
1810
  // Store listings ARE the extension page's declaration and the curated
1500
1811
  // site.targets.extension view (#85, #610 — no second copy in web config).
1501
1812
  extension: [
1813
+ // ── the shipping declaration (#867) ──────────────────────────────────
1814
+ // The SAME shape the desktop target declares above, in the browser half of
1815
+ // the client's vocabulary (chrome, firefox, edge). `edge` ships the CHROME
1816
+ // build: two stores, one chromium artifact.
1817
+ {
1818
+ path: 'platforms',
1819
+ type: 'object',
1820
+ required: false,
1821
+ description: 'What this extension target SHIPS (#867): `platforms.<chrome|firefox|edge>.formats.<zip|store>`. Presence = enabled; drop a default with `false`. The zip is attached to the release, the store is published to, and @omega.js/config\'s platforms.js says which credentials each store cannot ship without.',
1822
+ },
1823
+ {
1824
+ path: 'platforms.chrome',
1825
+ type: 'object|boolean',
1826
+ required: false,
1827
+ description: 'The Chrome leg (the chromium build every Chromium browser installs): `false` ships nothing for it.',
1828
+ },
1829
+ {
1830
+ path: 'platforms.chrome.formats',
1831
+ type: 'object',
1832
+ required: false,
1833
+ description: 'Which chrome formats ship: `zip` (attached to the release) and `store` (published to the Chrome Web Store).',
1834
+ },
1835
+ {
1836
+ path: 'platforms.chrome.formats.zip',
1837
+ type: 'object|boolean',
1838
+ required: false,
1839
+ description: 'The packaged zip, attached to the brand releases repo so anyone can install it unpacked.',
1840
+ },
1841
+ {
1842
+ path: 'platforms.chrome.formats.store',
1843
+ type: 'object|boolean',
1844
+ required: false,
1845
+ description: 'The Chrome Web Store publish. Needs the CHROME_* API credentials plus `listings.chrome.id` (the walk asks for both, #893).',
1846
+ },
1847
+ {
1848
+ path: 'platforms.firefox',
1849
+ type: 'object|boolean',
1850
+ required: false,
1851
+ description: 'The Firefox leg (its own build: scripts instead of a service worker, sidebar_action instead of side_panel).',
1852
+ },
1853
+ {
1854
+ path: 'platforms.firefox.formats',
1855
+ type: 'object',
1856
+ required: false,
1857
+ description: 'Which firefox formats ship: `zip` (attached to the release) and `store` (published to addons.mozilla.org).',
1858
+ },
1859
+ {
1860
+ path: 'platforms.firefox.formats.zip',
1861
+ type: 'object|boolean',
1862
+ required: false,
1863
+ description: 'The packaged zip, attached to the brand releases repo.',
1864
+ },
1865
+ {
1866
+ path: 'platforms.firefox.formats.store',
1867
+ type: 'object|boolean',
1868
+ required: false,
1869
+ description: 'The addons.mozilla.org publish. Needs FIREFOX_API_KEY + FIREFOX_API_SECRET; the add-on id is the manifest gecko id (`listings.firefox.id`), which the local scaffold pins into config when the brand declares none.',
1870
+ },
1871
+ {
1872
+ path: 'platforms.firefox.formats.store.channel',
1873
+ type: 'string',
1874
+ required: false,
1875
+ enum: ['listed', 'unlisted'],
1876
+ description: 'Which AMO channel the publish signs into. Unset reads as listed; `unlisted` self-distributes (signed, never listed on the store). No schema `default:`, for the same reason as the snap options: a materialized default would declare the format on a brand that never asked for it.',
1877
+ },
1878
+ {
1879
+ path: 'platforms.edge',
1880
+ type: 'object|boolean',
1881
+ required: false,
1882
+ description: 'The Edge leg. It ships the CHROME build, so dropping chrome while keeping edge still packages the chromium artifact.',
1883
+ },
1884
+ {
1885
+ path: 'platforms.edge.formats',
1886
+ type: 'object',
1887
+ required: false,
1888
+ description: 'Which edge formats ship: `zip` (attached to the release) and `store` (published to Edge Add-ons).',
1889
+ },
1890
+ {
1891
+ path: 'platforms.edge.formats.zip',
1892
+ type: 'object|boolean',
1893
+ required: false,
1894
+ description: 'The packaged zip, attached to the brand releases repo.',
1895
+ },
1896
+ {
1897
+ path: 'platforms.edge.formats.store',
1898
+ type: 'object|boolean',
1899
+ required: false,
1900
+ description: 'The Edge Add-ons publish. Needs EDGE_CLIENT_ID + EDGE_API_KEY plus `listings.edge.id` (the Partner Center product id, #893).',
1901
+ },
1902
+ {
1903
+ path: 'categories',
1904
+ type: 'array',
1905
+ required: false,
1906
+ itemEnum: AMO_CATEGORIES,
1907
+ default: ['alerts-updates'],
1908
+ description: "The AMO categories a FIRST Firefox publish lists the add-on under (#884): addons.mozilla.org requires them on a new listing, and the Firefox lane writes them into the `--amo-metadata` file it hands `web-ext sign`. Slugs come from AMO's own set; the default is `alerts-updates`, the most generic one that is not `other`. Chrome and Edge pick their category in their own dashboards.",
1909
+ },
1910
+ {
1911
+ path: 'listings.chrome.id',
1912
+ type: 'string',
1913
+ required: false,
1914
+ description: "The Chrome Web Store ITEM ID the publish uploads to, the 32-letter id in the listing URL (#893). Public by design, so it lives here and not in .env; the manage walk asks for it.",
1915
+ },
1502
1916
  {
1503
1917
  path: 'listings.chrome.url',
1504
1918
  type: 'string',
@@ -1512,6 +1926,12 @@ const TARGET_SCHEMAS = {
1512
1926
  required: false,
1513
1927
  description: 'Listing state note (e.g. live, pending review). Display-safe, informational only.',
1514
1928
  },
1929
+ {
1930
+ path: 'listings.firefox.id',
1931
+ type: 'string',
1932
+ required: false,
1933
+ description: "The addons.mozilla.org ADD-ON ID, which IS the manifest's browser_specific_settings.gecko.id (AMO uses it as the add-on guid, #893). Declared here it is authoritative: the firefox package writes it into the manifest. A brand that declares none gets the derived id pinned here by the extension's local scaffold, never by a publish.",
1934
+ },
1515
1935
  {
1516
1936
  path: 'listings.firefox.url',
1517
1937
  type: 'string',
@@ -1525,6 +1945,12 @@ const TARGET_SCHEMAS = {
1525
1945
  required: false,
1526
1946
  description: 'Listing state note (e.g. live, pending review). Display-safe, informational only.',
1527
1947
  },
1948
+ {
1949
+ path: 'listings.edge.id',
1950
+ type: 'string',
1951
+ required: false,
1952
+ description: "The Microsoft Edge Partner Center PRODUCT ID (a GUID) the publish uploads to (#893). Public by design, so it lives here and not in .env; the manage walk asks for it.",
1953
+ },
1528
1954
  {
1529
1955
  path: 'listings.edge.url',
1530
1956
  type: 'string',
@@ -1584,10 +2010,10 @@ const TARGET_SCHEMAS = {
1584
2010
  };
1585
2011
 
1586
2012
  // Declared top-level keys that are NOT config sections: `targets` is scoping
1587
- // machinery, and `url` is this instance's RESOLVED public url (#588): a value
2013
+ // machinery, and `url` is this target's RESOLVED public url (#588): a value
1588
2014
  // the loader derives, carried to templates as the `site.url` build fact, never
1589
2015
  // a namespace a page's `config:` block overrides.
1590
- const NON_SECTION_KEYS = ['targets', 'url'];
2016
+ const NON_SECTION_KEYS = ['targets', 'url', 'type'];
1591
2017
 
1592
2018
  /** The top-level sections a rule array declares (machinery keys excluded). */
1593
2019
  const sectionsOf = (rules) => [...new Set(rules.map((rule) => rule.path.split('.')[0]))].filter((section) => !NON_SECTION_KEYS.includes(section));
@@ -1607,4 +2033,4 @@ function configSections(target) {
1607
2033
  return [...new Set([...sectionsOf(SHARED_SCHEMA), ...sectionsOf(TARGET_SCHEMAS[target] || [])])].sort();
1608
2034
  }
1609
2035
 
1610
- module.exports = { TARGETS, CUSTOM_TARGET_TYPE, isCustomTargetEntry, BACKEND_PROJECT_TYPES, backendProjectType, SHARED_SECTIONS, SHARED_SCHEMA, TARGET_SCHEMAS, BRAND_ID_PATTERN, WINBACK_DURATIONS, configSections };
2036
+ module.exports = { TRANSLATION_INCLUDE_DEFAULT, TARGETS, CUSTOM_TARGET_TYPE, isCustomTargetEntry, BACKEND_PROJECT_TYPES, backendProjectType, SHARED_SECTIONS, CLIENT_SECTIONS, SHARED_SCHEMA, TARGET_SCHEMAS, BRAND_ID_PATTERN, WINBACK_DURATIONS, REPO_PROVIDERS, HOSTING_PROVIDERS, AMO_CATEGORIES, configSections };