@omega.js/desktop 0.50.0 → 0.52.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 (268) hide show
  1. package/README.md +2 -2
  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 +176 -42
  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 +146 -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 +635 -5
  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 +125 -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 +249 -103
  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 +425 -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 +505 -75
  197. package/dist/vendor/devkit/git-auth.js +73 -0
  198. package/dist/vendor/devkit/git-remote.js +172 -0
  199. package/dist/vendor/devkit/github-repo.js +290 -0
  200. package/dist/vendor/devkit/local.js +98 -3
  201. package/dist/vendor/devkit/lockfile.js +127 -0
  202. package/dist/vendor/devkit/merge-line-files.js +23 -16
  203. package/dist/vendor/devkit/omega-bin.js +18 -3
  204. package/dist/vendor/devkit/pack-local.js +388 -0
  205. package/dist/vendor/devkit/preludes/index.js +120 -0
  206. package/dist/vendor/devkit/preludes/origin-heal.js +142 -0
  207. package/dist/vendor/devkit/service-account.js +43 -0
  208. package/dist/vendor/devkit/ship-plan.js +112 -0
  209. package/dist/vendor/devkit/signing-env.js +180 -0
  210. package/dist/vendor/devkit/signing-tree.js +92 -0
  211. package/dist/vendor/devkit/target-seams.js +142 -0
  212. package/dist/vendor/devkit/target-secrets.js +231 -57
  213. package/dist/vendor/devkit/test/esm-only-fixture.js +48 -0
  214. package/dist/vendor/devkit/test/fixtures/esm-only-package/browser.js +19 -0
  215. package/dist/vendor/devkit/test/fixtures/esm-only-package/index.js +20 -0
  216. package/dist/vendor/devkit/test/fixtures/esm-only-package/package.json +13 -0
  217. package/dist/vendor/monitoring/env.js +20 -10
  218. package/dist/vendor/monitoring/main.js +1 -1
  219. package/dist/vendor/monitoring/preload.js +1 -1
  220. package/dist/vendor/monitoring/renderer.js +1 -1
  221. package/docs/analytics.md +1 -1
  222. package/docs/auto-updater.md +5 -5
  223. package/docs/boot-sequence.md +1 -1
  224. package/docs/build-system.md +15 -7
  225. package/docs/client-bridge.md +9 -7
  226. package/docs/config-schema.md +4 -4
  227. package/docs/css.md +8 -2
  228. package/docs/deep-link.md +12 -4
  229. package/docs/environment-detection.md +32 -24
  230. package/docs/hooks.md +3 -1
  231. package/docs/icons.md +7 -7
  232. package/docs/index.md +61 -23
  233. package/docs/installer-options.md +24 -21
  234. package/docs/logging.md +5 -5
  235. package/docs/releasing.md +29 -19
  236. package/docs/runner.md +51 -14
  237. package/docs/shared/brands.md +12 -6
  238. package/docs/shared/breaking-changes.md +375 -21
  239. package/docs/shared/config.md +764 -199
  240. package/docs/shared/deploys.md +217 -91
  241. package/docs/shared/icons.md +18 -0
  242. package/docs/shared/local-dev.md +28 -8
  243. package/docs/shared/logging.md +9 -6
  244. package/docs/shared/monitoring.md +27 -13
  245. package/docs/shared/publishing.md +3 -3
  246. package/docs/shared/rulings.md +2 -2
  247. package/docs/shared/testing.md +1 -1
  248. package/docs/shared/theming.md +26 -1
  249. package/docs/shared/translation.md +49 -7
  250. package/docs/shared/updates.md +1 -1
  251. package/docs/signing.md +60 -34
  252. package/docs/test-framework.md +10 -5
  253. package/docs/themes.md +15 -1
  254. package/package.json +18 -12
  255. package/bin/omega-desktop +0 -2
  256. package/dist/commands/push-secrets.js +0 -141
  257. package/dist/test/suites/build/deliver-certs.test.js +0 -95
  258. package/dist/test/suites/build/derive-signing-env.test.js +0 -122
  259. package/dist/test/suites/build/push-secrets.test.js +0 -226
  260. package/dist/test/suites/build/resolve-signing-cert.test.js +0 -342
  261. package/dist/utils/deliver-certs.js +0 -69
  262. package/dist/utils/derive-signing-env.js +0 -56
  263. package/dist/utils/resolve-signing-cert.js +0 -175
  264. package/dist/vendor/config/desktop-artifacts.js +0 -110
  265. package/dist/vendor/config/instances.js +0 -208
  266. /package/dist/defaults/config/icons/{macos → mac}/dmg.png +0 -0
  267. /package/dist/defaults/config/icons/{macos → mac}/icon.png +0 -0
  268. /package/dist/defaults/config/icons/{macos → mac}/tray.png +0 -0
@@ -11,6 +11,10 @@
11
11
  // callers normally pass NO argument. An explicit `environment` arg is an override
12
12
  // (used mainly by tests to pin a specific environment's mapping).
13
13
 
14
+ // The missing-fact error is the ONE shared one (#834). Its module requires
15
+ // nothing, so it rides the renderer bundle exactly as this file does.
16
+ const { devFactMissing } = require('../vendor/config/dev-facts.js');
17
+
14
18
  // Guarded the way @omega.js/extension's twin is: the renderer bundle is a
15
19
  // browser context, and only the Node-side contexts (main, preload, build, the
16
20
  // test harness) carry the resolved-port env channel.
@@ -20,14 +24,9 @@ function envPort(name) {
20
24
  : undefined;
21
25
  }
22
26
 
23
- // The classic dev WEBSITE ORIGIN: a lockstep copy of @omega.js/config's
24
- // CLASSIC_DEV_ORIGIN, which is the SSOT. This module is attached in the RENDERER
25
- // too (src/renderer.js), so it rides the browser bundle and cannot require that
26
- // Node-only package (fs, net, json5); @omega.js/client mirrors the same constant
27
- // for the same reason. The build suite pins this copy to the config package's
28
- // value, so a drift fails a test instead of a user
29
- // ([#747](https://github.com/Omega-JS-Stack/omega/issues/747)).
30
- const CLASSIC_DEV_ORIGIN = 'https://localhost:4000';
27
+ // The step that writes the resolved dev map into this surface's artifact, named
28
+ // in every missing-fact error below (#834).
29
+ const DEV_FACT_WRITER = "@omega.js/desktop's bundle task, from the live stack's ports file plus the OMEGA_*_PORT env channel";
31
30
 
32
31
  // One local port, from whichever channel this context has: the env var first
33
32
  // (the CLI that booted the stack publishes it, N7), then the `dev.ports` map
@@ -35,10 +34,26 @@ const CLASSIC_DEV_ORIGIN = 'https://localhost:4000';
35
34
  // parent env, so a bumped emulator port reaches it only that way
36
35
  // ([#745](https://github.com/Omega-JS-Stack/omega/issues/745)). Same helper,
37
36
  // same order as @omega.js/extension's src/utils/url-helpers.js.
37
+ //
38
+ // There is no third step any more (#834): the classic numbers used to be
39
+ // hand-typed here as a last resort for "a build made with no stack up", which
40
+ // is a guess about a port nothing identity-checks. A miss is loud instead.
38
41
  function localPort(context, envName, name) {
39
42
  return envPort(envName) || context?.config?.dev?.ports?.[name];
40
43
  }
41
44
 
45
+ // The same read, REQUIRED: the one every URL getter below uses, because every
46
+ // one of them is answering with a real local address or not at all.
47
+ function requiredPort(context, envName, name) {
48
+ const port = localPort(context, envName, name);
49
+
50
+ if (!port) {
51
+ throw devFactMissing(`dev port for \`${name}\``, DEV_FACT_WRITER);
52
+ }
53
+
54
+ return port;
55
+ }
56
+
42
57
  function getFunctionsUrl(environment) {
43
58
  const env = environment || this.getEnvironment();
44
59
  const projectId = this?.config?.cloud?.config?.projectId;
@@ -51,7 +66,7 @@ function getFunctionsUrl(environment) {
51
66
  // same three-step chain getApiUrl() walks: the OMEGA_*_PORT env channel (N7),
52
67
  // then the baked map, then the classic default.
53
68
  if (env === 'development' || env === 'testing') {
54
- const port = localPort(this, 'OMEGA_FUNCTIONS_PORT', 'functions') || 5001;
69
+ const port = requiredPort(this, 'OMEGA_FUNCTIONS_PORT', 'functions');
55
70
  return `http://localhost:${port}/${projectId}/us-central1`;
56
71
  }
57
72
 
@@ -64,12 +79,13 @@ function getApiUrl(environment) {
64
79
  // Local for development OR testing; production otherwise. Mirrors
65
80
  // @omega.js/backend's getApiUrl (N7): a resolved OMEGA_HTTPS_PORT means
66
81
  // `mgr serve`'s mkcert proxy is up (https); otherwise plain http to the
67
- // hosting emulator (env port, baked port, or classic 5002).
82
+ // hosting emulator (env port, then baked port). Neither resolved is a broken
83
+ // build, not a case to assume the classic 5002 through (#834).
68
84
  if (env === 'development' || env === 'testing') {
69
85
  const httpsPort = localPort(this, 'OMEGA_HTTPS_PORT', 'https');
70
86
  return httpsPort
71
87
  ? `https://localhost:${httpsPort}`
72
- : `http://localhost:${localPort(this, 'OMEGA_HOSTING_PORT', 'hosting') || 5002}`;
88
+ : `http://localhost:${requiredPort(this, 'OMEGA_HOSTING_PORT', 'hosting')}`;
73
89
  }
74
90
 
75
91
  // Prod: api.<brand host>. Mirrors @omega.js/client.getApiUrl. Never derive
@@ -89,9 +105,10 @@ function getApiUrl(environment) {
89
105
  // ([#262](https://github.com/Omega-JS-Stack/omega/issues/262)): the baked
90
106
  // `dev.origin` the live website published (scheme, host AND port, the complete
91
107
  // fact) wins, else a port from the usual chain (OMEGA_WEBSITE_PORT, then the baked
92
- // `dev.ports.website`) composed over https, else the classic dev origin. The scheme
93
- // is https because `omega dev` fronts its public port with the mkcert proxy by
94
- // default, so a port alone can never say it
108
+ // `dev.ports.website`) composed over https. There is no classic-origin fallback
109
+ // any more (#834): a wrong dev origin fails as a silent connection refusal. The
110
+ // scheme is https because `omega dev` fronts its public port with the mkcert
111
+ // proxy by default, so a port alone can never say it
95
112
  // ([#747](https://github.com/Omega-JS-Stack/omega/issues/747)).
96
113
  // Prod → `config.brand.url`. Use this whenever app code wants to link out to "the
97
114
  // website" (Help → Website tray/menu items, "Open in browser," billing portal
@@ -106,8 +123,7 @@ function getWebsiteUrl(environment) {
106
123
  return origin;
107
124
  }
108
125
 
109
- const port = localPort(this, 'OMEGA_WEBSITE_PORT', 'website');
110
- return port ? `https://localhost:${port}` : CLASSIC_DEV_ORIGIN;
126
+ return `https://localhost:${requiredPort(this, 'OMEGA_WEBSITE_PORT', 'website')}`;
111
127
  }
112
128
 
113
129
  const url = this?.config?.brand?.url;
@@ -160,8 +176,8 @@ function attachTo(Manager) {
160
176
 
161
177
  module.exports = {
162
178
  attachTo,
163
- CLASSIC_DEV_ORIGIN,
164
179
  localPort,
180
+ requiredPort,
165
181
  getFunctionsUrl,
166
182
  getApiUrl,
167
183
  getWebsiteUrl,
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Bundle-identifier policy — reverse-DNS ids derived from the brand's web
3
+ * identity (Ian 2026-07-14: the prefix is the reverse domain, autoset at
4
+ * onboarding from the parent company when one exists, else the brand's own
5
+ * url).
6
+ *
7
+ * deriveBundleIdPrefix('https://itwcreativeworks.com') → 'com.itwcreativeworks'
8
+ * deriveBundleIdPrefix('https://playground.omegajs.dev') → 'dev.omegajs.playground'
9
+ * composeBundleId('com.itwcreativeworks', 'daily-build')
10
+ * → 'com.itwcreativeworks.daily.build'
11
+ *
12
+ * Sanitization keeps ids legal on BOTH stores: Apple allows hyphens but
13
+ * Android package segments don't, so domain labels drop non-alphanumerics
14
+ * and brand-id hyphens become dots (each id word is its own segment).
15
+ */
16
+
17
+ /**
18
+ * Reverse-DNS prefix from a company/brand url, or null when the url does
19
+ * not parse. `www.` is dropped; every other hostname label survives (a
20
+ * subdomain brand derives a subdomain-scoped prefix).
21
+ *
22
+ * @param {string} url - http(s) url (brand.url shape)
23
+ * @returns {string|null}
24
+ */
25
+ function deriveBundleIdPrefix(url) {
26
+ let hostname;
27
+ try {
28
+ hostname = new URL(url).hostname;
29
+ } catch {
30
+ return null;
31
+ }
32
+
33
+ const labels = hostname
34
+ .replace(/^www\./, '')
35
+ .split('.')
36
+ .map((label) => label.toLowerCase().replace(/[^a-z0-9]/g, ''))
37
+ .filter(Boolean);
38
+
39
+ return labels.length > 0 ? labels.reverse().join('.') : null;
40
+ }
41
+
42
+ /**
43
+ * Full bundle identifier: prefix + the brand id with hyphens as dots.
44
+ *
45
+ * @param {string} prefix - reverse-DNS prefix (config or derived)
46
+ * @param {string} brandId - brand.id ('daily-build')
47
+ * @returns {string}
48
+ */
49
+ function composeBundleId(prefix, brandId) {
50
+ return `${prefix}.${String(brandId).toLowerCase().replace(/-+/g, '.')}`;
51
+ }
52
+
53
+ module.exports = { deriveBundleIdPrefix, composeBundleId };
@@ -0,0 +1,141 @@
1
+ /**
2
+ * client-config.js: the ONE browser-safe subset of a resolved config
3
+ * ([#894](https://github.com/Omega-JS-Stack/omega/issues/894)).
4
+ *
5
+ * Three surfaces bake a config into an artifact a browser can read: web's page
6
+ * chrome, desktop's renderer bundle, and every extension bundle. Each one used
7
+ * to decide what may go public on its own, three different ways (desktop
8
+ * shipped the WHOLE resolved config, the extension kept a hand-written allow
9
+ * list, web wired a subset together in its engine and its foot include), so a
10
+ * new public value had to be added in three places and every one of them could
11
+ * be the place it leaked.
12
+ *
13
+ * This is that decision, once. The schema's `CLIENT_SECTIONS` says which
14
+ * sections a browser may see (and how much of each), the list below says which
15
+ * BUILD FACTS ride beside them, and nothing else leaves the build. All three
16
+ * bakes call this and wrap the answer identically:
17
+ *
18
+ * OMEGA_BUILD_JSON = { config: clientConfig(resolved), package, mode, license, builtAt }
19
+ *
20
+ * Secrets are a loud failure, not a filter: a secret-shaped key inside a
21
+ * section that is about to ship means the resolved config carried one at all,
22
+ * which loadConfig() and validateConfig() already refuse. A value a build is
23
+ * SANCTIONED to bake (`publicAtRest`, docs/shared/config.md) comes from .env
24
+ * and is added by that build after this gate, never carried through it.
25
+ */
26
+
27
+ const { CLIENT_SECTIONS } = require('./schema.js');
28
+ const { findSecretKeys } = require('./secrets.js');
29
+ const { resolveWinbackOffer } = require('./winback.js');
30
+
31
+ // The per-build facts a surface composes ON TOP of the resolved config before
32
+ // baking it. They are not config sections (nothing authors them in
33
+ // omega.json5): each is a fact about the artifact, spelled the same on every
34
+ // surface so one read answers everywhere.
35
+ //
36
+ // runtime 'web' | 'browser-extension' | 'electron'
37
+ // environment the build's verdict ('development' | 'testing' | 'production')
38
+ // version the target's own package version, the release tag every error
39
+ // report carries as `<brand.id>@<version>` (#380)
40
+ // buildTime the build stamp (ms)
41
+ // target WHICH target this artifact IS, by name (#887)
42
+ // url this target's own resolved public url (#588)
43
+ // dev the local stack's resolved map: ports + the sibling website's
44
+ // origin, development builds only (#300/#262). The extension's
45
+ // live-reload port rides here too (#896), the one home of a
46
+ // local-stack number.
47
+ const CLIENT_FACT_KEYS = ['runtime', 'environment', 'version', 'buildTime', 'target', 'url', 'dev'];
48
+
49
+ /**
50
+ * Copy exactly `paths` out of a section (dot-paths, deep).
51
+ * @param {*} section - the resolved section value.
52
+ * @param {string[]} paths - dot-paths inside it.
53
+ * @returns {object|undefined} the picked sub-tree, or undefined when it is empty.
54
+ */
55
+ function pickPaths(section, paths) {
56
+ if (!section || typeof section !== 'object') {
57
+ return undefined;
58
+ }
59
+
60
+ const picked = {};
61
+ let found = false;
62
+
63
+ for (const path of paths) {
64
+ const keys = path.split('.');
65
+ let source = section;
66
+ let missing = false;
67
+
68
+ for (const key of keys) {
69
+ if (!source || typeof source !== 'object' || !(key in source)) {
70
+ missing = true;
71
+ break;
72
+ }
73
+ source = source[key];
74
+ }
75
+ if (missing) continue;
76
+
77
+ let cursor = picked;
78
+ for (const key of keys.slice(0, -1)) {
79
+ cursor[key] = cursor[key] || {};
80
+ cursor = cursor[key];
81
+ }
82
+ cursor[keys[keys.length - 1]] = source;
83
+ found = true;
84
+ }
85
+
86
+ return found ? picked : undefined;
87
+ }
88
+
89
+ /**
90
+ * The subset of a resolved config a browser may see.
91
+ * @param {object} [resolved] - a resolved config (Manager.getConfig() output),
92
+ * with this build's facts composed onto it.
93
+ * @returns {object} the blob every browser surface bakes as OMEGA_BUILD_JSON.config.
94
+ * @throws {Error} when a section that is about to ship carries a secret-shaped key.
95
+ */
96
+ function clientConfig(resolved) {
97
+ const source = resolved || {};
98
+ const client = {};
99
+
100
+ for (const key of CLIENT_FACT_KEYS) {
101
+ if (key in source) client[key] = source[key];
102
+ }
103
+
104
+ for (const [section, rule] of Object.entries(CLIENT_SECTIONS)) {
105
+ if (!(section in source)) continue;
106
+
107
+ const value = rule === true ? source[section] : pickPaths(source[section], rule);
108
+ if (value === undefined) continue;
109
+
110
+ client[section] = value;
111
+ }
112
+
113
+ // A secret in here is not a value to drop: it is a config that should never
114
+ // have loaded, so it fails the build at the one place that would have
115
+ // published it.
116
+ const secrets = findSecretKeys(client);
117
+ if (secrets.length) {
118
+ throw new Error(
119
+ `[@omega.js/config] refusing to bake a browser config carrying secret-shaped keys: ${secrets.join(', ')}. `
120
+ + 'Secrets live in .env, never in config/omega.json5 (docs/shared/config.md).',
121
+ );
122
+ }
123
+
124
+ // The one home of the cancel-flow save offer's default (#268) is
125
+ // @omega.js/config, so the browser is handed the RESOLVED offer: the dialog a
126
+ // customer reads and the coupon the backend creates can never name different
127
+ // numbers.
128
+ if (client.payment) {
129
+ client.payment = { ...client.payment, winback: resolveWinbackOffer(client.payment) };
130
+ }
131
+
132
+ // Never an alias of the resolved config: what a bake stamps into an artifact
133
+ // cannot be something a later build step can still mutate. A JSON round trip
134
+ // and not structuredClone, because JSON is what a bake CAN carry: a page's
135
+ // resolved config arrives through a template data cascade that hangs
136
+ // functions and accessors off it, and those drop out here rather than
137
+ // crashing the build.
138
+ return JSON.parse(JSON.stringify(client));
139
+ }
140
+
141
+ module.exports = { clientConfig, CLIENT_FACT_KEYS };
@@ -1,31 +1,350 @@
1
1
  /**
2
- * The company layer's ONE discovery rule.
2
+ * The company layer's ONE resolver
3
+ * ([#677](https://github.com/Omega-JS-Stack/omega/issues/677)).
3
4
  *
4
- * A brand managed by a company workspace carries `.omega/company.json` at its
5
- * root (stamped idempotently by company manage runs) pointing at the company
6
- * root. Everything that cascades — the config chain (load.js), the .env chain
7
- * (env.js), owner hooks (hooks.js) — resolves the company root through here,
8
- * so the stamp is read exactly one way.
5
+ * A brand joins its company with ONE typed key, outside `brand` (Ian
6
+ * 2026-09-12: "brand key is for things about this brand, and the
7
+ * parent/company/organization key is OUTSIDE of that"):
8
+ *
9
+ * company: { id: 'itw-creative-works' } // a sub-brand: the parent's brand.id
10
+ * company: { id: 'self' } // the company brand itself
11
+ *
12
+ * The resolver fills that SAME object at load (`{ id, name, url, images,
13
+ * webhooks }`), so one name carries one shape at every level and no reader
14
+ * needs a fallback: a brand with no `company` key resolves to `{ id: null,
15
+ * name: brand.name, url: brand.url, images: {}, webhooks: true }`.
16
+ *
17
+ * `webhooks` is the one key beside `id` a brand TYPES (#677): `false` says the
18
+ * provider ACCOUNT is somebody else's and the manage walk never repoints its
19
+ * one account-level webhook. It is the brand's own statement, so it is read
20
+ * from the brand config at every branch, never from the company.
21
+ *
22
+ * The COMPANY TREE is a `company/` folder INSIDE the parent brand's repo,
23
+ * shaped like a brand (`company/config/omega.json5`, `company/.env`,
24
+ * `company/.omega/certificates/apple/`). Inheritance is ONE rule: a
25
+ * brand-level file the child lacks resolves from the parent's `company/` at the
26
+ * same relative path, through `file(relPath)`. Everything that cascades (the
27
+ * config chain in load.js, the .env chain in env.js, owner hooks in hooks.js,
28
+ * the desktop signing tree) asks that one function, so a NEW kind of inherited
29
+ * file costs zero code.
30
+ *
31
+ * WHERE the parent lives on this machine is a cache, never configuration: the
32
+ * machine registry `~/.omega/brands.json` (`OMEGA_HOME` overrides the home),
33
+ * keyed by `brand.id`, which every loadConfig() refreshes for its own brand.
34
+ * Nobody maintains it, and a parent that has never been loaded here is simply
35
+ * inheritance-off with one loud line.
36
+ *
37
+ * OFF-LAPTOP the tree never travels: the machine that dispatches a deploy
38
+ * resolves the company and writes `config/company-resolved.json5` beside the
39
+ * brand config (@omega.js/devkit's deploy snapshot), and a runner reads that
40
+ * generated layer instead of a registry it has no lines in.
9
41
  */
10
42
 
11
43
  const fs = require('node:fs');
44
+ const os = require('node:os');
12
45
  const path = require('node:path');
46
+ const JSON5 = require('json5');
13
47
 
14
- // A brand's pointer at its company root.
15
- const COMPANY_MARKER = path.join('.omega', 'company.json');
48
+ // The company TREE inside the parent brand's repo, and the file an off-laptop
49
+ // run reads the resolved layer from: GENERATED beside the brand config by the
50
+ // deploy snapshot, which is the only place a brand-wide generated file can sit
51
+ // and still ride a mirror (git cannot re-include a path under an excluded
52
+ // directory, and every brand ignores `.omega/` whole).
53
+ const COMPANY_DIR = 'company';
54
+ const COMPANY_RESOLVED_FILE = path.join('config', 'company-resolved.json5');
55
+
56
+ // The machine registry: brand id to { root, name, url, updatedAt }. One file,
57
+ // no other company-shaped state.
58
+ const REGISTRY_FILE = 'brands.json';
59
+
60
+ // `company: { id: 'self' }` is the company brand itself, kept visible on
61
+ // purpose (Ian 2026-09-12: "dont retire parent field for parent brands, i like
62
+ // it visible").
63
+ const SELF = 'self';
64
+
65
+ // The legacy stamp #677 retired, warned about where an owner still carries one.
66
+ const RETIRED_MARKER = path.join('.omega', 'company.json');
67
+
68
+ // One warning per company id per process: a missing parent is a state a whole
69
+ // build runs in, never a per-call event.
70
+ const warned = new Set();
16
71
 
17
72
  /**
18
- * The company root a brand points at via its .omega/company.json marker.
19
- * @param {string} brandRoot
20
- * @returns {string|null} Absolute company root, or null when unstamped/unreadable.
73
+ * The machine home OMEGA keeps its per-machine state in. `OMEGA_HOME` moves it
74
+ * (the test lanes point it at a temp dir so a fixture never writes a real line).
75
+ * @returns {string} Absolute path.
21
76
  */
22
- function readCompanyRoot(brandRoot) {
77
+ function omegaHome() {
78
+ return process.env.OMEGA_HOME || path.join(os.homedir(), '.omega');
79
+ }
80
+
81
+ /**
82
+ * The machine registry file.
83
+ * @returns {string} Absolute path to `<home>/brands.json`.
84
+ */
85
+ function registryFile() {
86
+ return path.join(omegaHome(), REGISTRY_FILE);
87
+ }
88
+
89
+ /**
90
+ * Parse a JSON/JSON5 file, tolerantly: this whole module is a CACHE plus a
91
+ * parent's own public facts, and neither may ever fail a brand's load.
92
+ * @param {string} file - Absolute path.
93
+ * @returns {object|null} The parsed object, or null.
94
+ */
95
+ function readJson(file) {
23
96
  try {
24
- const marker = JSON.parse(fs.readFileSync(path.join(brandRoot, COMPANY_MARKER), 'utf8'));
25
- return typeof marker.root === 'string' && marker.root ? marker.root : null;
97
+ const parsed = JSON5.parse(fs.readFileSync(file, 'utf8'));
98
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
26
99
  } catch {
27
100
  return null;
28
101
  }
29
102
  }
30
103
 
31
- module.exports = { readCompanyRoot, COMPANY_MARKER };
104
+ /**
105
+ * A brand root's authored omega.json5.
106
+ * @param {string|null} root - Brand root.
107
+ * @returns {object|null}
108
+ */
109
+ function readBrandConfig(root) {
110
+ return root ? readJson(path.join(root, 'config', 'omega.json5')) : null;
111
+ }
112
+
113
+ /**
114
+ * The whole machine registry.
115
+ * @returns {Object<string, { root: string, name: string, url: string, updatedAt: string }>}
116
+ */
117
+ function readRegistry() {
118
+ return readJson(registryFile()) || {};
119
+ }
120
+
121
+ /**
122
+ * Record (or refresh) ONE brand's line in the machine registry, which is what
123
+ * every `loadConfig()` does for the brand it just loaded: the map of where the
124
+ * brands are on this machine maintains itself.
125
+ *
126
+ * IDEMPOTENT: a line whose root, name and url already match is left untouched
127
+ * (`updatedAt` is not churn). It NEVER throws: a read-only home, a racing
128
+ * writer, or a machine with no home directory at all is a registry that misses,
129
+ * which is inheritance-off, never a failed load.
130
+ *
131
+ * A write also PRUNES the lines whose root is gone: the file is a map of what
132
+ * is on this machine, so a brand that was deleted or moved (and re-recorded
133
+ * under its new root) leaves nothing behind to resolve into.
134
+ *
135
+ * @param {object} entry - The brand's own facts.
136
+ * @param {string} entry.id - `brand.id` (the key).
137
+ * @param {string} entry.root - The brand root on this machine.
138
+ * @param {string} [entry.name] - `brand.name`.
139
+ * @param {string} [entry.url] - `brand.url`.
140
+ * @returns {boolean} True when a line was written.
141
+ */
142
+ function recordBrand({ id, root, name, url }) {
143
+ if (!id || !root) return false;
144
+
145
+ try {
146
+ const registry = readRegistry();
147
+ const current = registry[id];
148
+ const line = { root, name: name || null, url: url || null };
149
+
150
+ if (current && current.root === line.root && (current.name || null) === line.name && (current.url || null) === line.url) {
151
+ return false;
152
+ }
153
+
154
+ registry[id] = { ...line, updatedAt: new Date().toISOString() };
155
+
156
+ const live = Object.fromEntries(Object.entries(registry).filter(([, entry]) => entry && isDir(entry.root)));
157
+
158
+ fs.mkdirSync(omegaHome(), { recursive: true });
159
+ fs.writeFileSync(registryFile(), `${JSON.stringify(live, null, 2)}\n`);
160
+ return true;
161
+ } catch {
162
+ return false;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * The generated layer an off-laptop run reads: what the dispatching machine
168
+ * resolved, written beside the brand config and carried by the deploy snapshot.
169
+ * @param {string|null} brandRoot
170
+ * @returns {{ config: object, company: object }|null}
171
+ */
172
+ function readResolvedLayer(brandRoot) {
173
+ const generated = brandRoot ? readJson(path.join(brandRoot, COMPANY_RESOLVED_FILE)) : null;
174
+ return generated && generated.company ? generated : null;
175
+ }
176
+
177
+ /** A directory that is really there. */
178
+ function isDir(dir) {
179
+ try {
180
+ return fs.statSync(dir).isDirectory();
181
+ } catch {
182
+ return false;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * The one line a missing parent prints, once per company id per process.
188
+ * @param {string} id
189
+ */
190
+ function warnMissing(id) {
191
+ if (warned.has(id)) return;
192
+ warned.add(id);
193
+ console.warn(`Company ${id} is not on this machine: inheritance off. Clone it and run any omega verb inside it once.`);
194
+ }
195
+
196
+ /**
197
+ * The company tree (`<root>/company`), when the root has one.
198
+ * @param {string|null} root
199
+ * @returns {string|null}
200
+ */
201
+ function companyDir(root) {
202
+ const dir = root ? path.join(root, COMPANY_DIR) : null;
203
+ return dir && isDir(dir) ? dir : null;
204
+ }
205
+
206
+ /**
207
+ * The wordmark a company hands its brands: the company brand's OWN wordmark.
208
+ * @param {object} brand - A brand block.
209
+ * @returns {{ wordmark?: string }}
210
+ */
211
+ function wordmarkOf(brand) {
212
+ const wordmark = brand && brand.images ? brand.images.wordmark : null;
213
+ return wordmark ? { wordmark } : {};
214
+ }
215
+
216
+ /**
217
+ * The resolver's answer, built around ONE inheritance rule.
218
+ * @param {object} facts - The company's id/name/url/images/root, the brand's
219
+ * own `webhooks` statement, plus the generated layer's config when there is
220
+ * no tree to read one from.
221
+ * @returns {object} The resolveCompany() shape.
222
+ */
223
+ function answer({ id = null, name = null, url = null, images = {}, root = null, config = null, configFile = null, webhooks = true }) {
224
+ const dir = companyDir(root);
225
+
226
+ // The WHOLE inheritance rule: a brand-level file the child lacks resolves
227
+ // from the parent's company/ at the same relative path. Nothing per-kind.
228
+ const file = (relPath) => {
229
+ const candidate = dir && relPath ? path.join(dir, relPath) : null;
230
+ return candidate && fs.existsSync(candidate) ? candidate : null;
231
+ };
232
+
233
+ const layerFile = configFile || file(path.join('config', 'omega.json5'));
234
+
235
+ return {
236
+ id,
237
+ name: name || null,
238
+ url: url || null,
239
+ images: images || {},
240
+ // Typed boolean, default true: only an explicit `false` opts out.
241
+ webhooks: webhooks !== false,
242
+ root: root || null,
243
+ dir,
244
+ file,
245
+ // The company CONFIG layer itself, already parsed: read from the parent's
246
+ // company/config/omega.json5 here, handed over by the generated layer on a
247
+ // runner.
248
+ config: config || (layerFile ? readJson(layerFile) : null),
249
+ configFile: layerFile || null,
250
+ };
251
+ }
252
+
253
+ /**
254
+ * The company a brand belongs to, filled from its typed `company.id`.
255
+ *
256
+ * @param {string} brandRoot - The brand (or standalone-project) root.
257
+ * @param {object} [brandConfig] - The brand's authored config, when the caller
258
+ * already has it (loadConfig does); read from the brand root otherwise.
259
+ * @returns {{ id: string|null, name: string|null, url: string|null, images: object, webhooks: boolean, root: string|null, dir: string|null, file: Function, config: object|null, configFile: string|null }}
260
+ * `root` is the parent brand's root ON THIS MACHINE (null = inheritance off),
261
+ * `file(relPath)` that path inside the parent's `company/` when it exists.
262
+ */
263
+ function resolveCompany(brandRoot, brandConfig) {
264
+ const root = brandRoot ? path.resolve(brandRoot) : null;
265
+ const config = brandConfig || readBrandConfig(root) || {};
266
+ const brand = config.brand || {};
267
+ const id = config.company && typeof config.company.id === 'string' ? config.company.id.trim() : '';
268
+
269
+ // The brand's OWN statement, on every branch: whose provider account the
270
+ // manage walk may repoint (#677). Never the company's to answer.
271
+ const webhooks = !(config.company && config.company.webhooks === false);
272
+ const reply = (facts) => answer({ ...facts, webhooks });
273
+
274
+ // A brand still carrying the retired stamp hears about it once: the folder it
275
+ // points at is not read any more, by anything.
276
+ if (root && fs.existsSync(path.join(root, RETIRED_MARKER)) && !warned.has(RETIRED_MARKER)) {
277
+ warned.add(RETIRED_MARKER);
278
+ console.warn(`${path.join(root, RETIRED_MARKER)} is retired (#677): delete it, and name the company with company: { id: '<parent brand.id>' } in omega.json5.`);
279
+ }
280
+
281
+ // No company: the brand IS the whole entity, so the company facts are its own
282
+ // and every reader works without a fallback.
283
+ if (!id) {
284
+ return reply({ id: null, name: brand.name, url: brand.url });
285
+ }
286
+
287
+ // The company brand itself: its own company/ tree, its own public facts.
288
+ if (id === SELF) {
289
+ return reply({ id: SELF, name: brand.name, url: brand.url, images: wordmarkOf(brand), root });
290
+ }
291
+
292
+ const line = readRegistry()[id];
293
+ const parentRoot = line && typeof line.root === 'string' && isDir(line.root) ? line.root : null;
294
+
295
+ if (parentRoot) {
296
+ const parentBrand = (readBrandConfig(parentRoot) || {}).brand || {};
297
+
298
+ return reply({
299
+ id,
300
+ name: parentBrand.name || line.name,
301
+ url: parentBrand.url || line.url,
302
+ images: wordmarkOf(parentBrand),
303
+ root: parentRoot,
304
+ });
305
+ }
306
+
307
+ // Off this laptop: what the dispatching machine resolved rides beside the
308
+ // brand config. Never a tree, so `file()` answers null for everything.
309
+ const generated = readResolvedLayer(root);
310
+ if (generated) {
311
+ const company = generated.company || {};
312
+ return reply({
313
+ id: company.id || id,
314
+ name: company.name,
315
+ url: company.url,
316
+ images: company.images || {},
317
+ config: generated.config || {},
318
+ configFile: path.join(root, COMPANY_RESOLVED_FILE),
319
+ });
320
+ }
321
+
322
+ // A config that already CARRIES the facts: composeTargetConfig freezes the
323
+ // resolved company into a target's upload (the deployed backend has no
324
+ // registry, no company tree and no brand walk-up above it), so what the
325
+ // dispatching machine resolved is the answer here. A human never authors
326
+ // these keys: the loader refuses them in a brand file.
327
+ const frozen = config.company || {};
328
+ if (frozen.name || frozen.url || frozen.images) {
329
+ return reply({ id, name: frozen.name, url: frozen.url, images: frozen.images || {} });
330
+ }
331
+
332
+ // Known in the registry but the folder is gone: the cached public facts still
333
+ // stand, the tree does not.
334
+ if (line) {
335
+ return reply({ id, name: line.name, url: line.url });
336
+ }
337
+
338
+ warnMissing(id);
339
+ return reply({ id });
340
+ }
341
+
342
+ module.exports = {
343
+ resolveCompany,
344
+ recordBrand,
345
+ readRegistry,
346
+ registryFile,
347
+ COMPANY_DIR,
348
+ COMPANY_RESOLVED_FILE,
349
+ COMPANY_SELF: SELF,
350
+ };