@omega.js/desktop 0.53.0 → 0.54.1

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 (161) hide show
  1. package/README.md +38 -38
  2. package/dist/cli-run.js +4 -1
  3. package/dist/cli.js +2 -2
  4. package/dist/commands/cdp/client.js +1 -1
  5. package/dist/commands/cdp.js +1 -1
  6. package/dist/commands/clean.js +2 -3
  7. package/dist/commands/dev.js +25 -0
  8. package/dist/commands/lib/ensure-target.js +12 -17
  9. package/dist/commands/lib/migrate.js +17 -0
  10. package/dist/commands/logs.js +1 -1
  11. package/dist/commands/release.js +1 -1
  12. package/dist/commands/test.js +4 -4
  13. package/dist/commands/update.js +5 -4
  14. package/dist/defaults/.github/workflows/build.yml +18 -18
  15. package/dist/defaults/_.gitignore +0 -2
  16. package/dist/defaults/_mas/README.md +3 -3
  17. package/dist/defaults/config/certs/README.md +1 -1
  18. package/dist/defaults/config/omega.json5 +36 -36
  19. package/dist/defaults/docs/README.md +3 -3
  20. package/dist/defaults/gulpfile.js +1 -1
  21. package/dist/defaults/hooks/build/post.js +1 -1
  22. package/dist/defaults/hooks/build/pre.js +1 -1
  23. package/dist/defaults/hooks/notarize/post.js +2 -2
  24. package/dist/defaults/hooks/release/post.js +1 -1
  25. package/dist/defaults/hooks/release/pre.js +1 -1
  26. package/dist/defaults/src/assets/scss/pages/about.scss +1 -1
  27. package/dist/defaults/src/assets/scss/pages/main.scss +1 -1
  28. package/dist/defaults/src/assets/scss/pages/settings.scss +1 -1
  29. package/dist/defaults/src/integrations/context-menu/index.js +11 -11
  30. package/dist/defaults/src/integrations/menu/index.js +5 -5
  31. package/dist/defaults/src/integrations/tray/index.js +9 -9
  32. package/dist/defaults/src/main.js +2 -2
  33. package/dist/defaults/src/preload.js +1 -1
  34. package/dist/defaults/test/README.md +3 -3
  35. package/dist/defaults/test/_init.js +1 -1
  36. package/dist/gulp/tasks/audit.js +5 -8
  37. package/dist/lib/restart-manager/index.js +1 -1
  38. package/dist/lib/restart-manager/install.js +1 -1
  39. package/dist/lib/restart-manager/protocol.js +1 -1
  40. package/dist/main.js +4 -3
  41. package/dist/preload.js +1 -1
  42. package/dist/test/suites/build/audit.test.js +20 -7
  43. package/dist/test/suites/build/build-workflow-jobs.test.js +2 -2
  44. package/dist/test/suites/build/cli.test.js +28 -0
  45. package/dist/test/suites/build/defaults-em-dash.test.js +22 -0
  46. package/dist/test/suites/build/defaults-scaffold.test.js +19 -5
  47. package/dist/test/suites/build/deploy-direct.test.js +7 -5
  48. package/dist/test/suites/build/deploy-dispatch.test.js +2 -1
  49. package/dist/test/suites/build/deploy-hook.test.js +4 -2
  50. package/dist/test/suites/build/dev-verb.test.js +67 -0
  51. package/dist/test/suites/build/ensure-target.test.js +11 -3
  52. package/dist/test/suites/build/merge-line-files.test.js +6 -6
  53. package/dist/test/suites/build/migrate.test.js +29 -0
  54. package/dist/test/suites/build/project-scripts-deps.test.js +6 -10
  55. package/dist/test/suites/build/runner-env-write.test.js +73 -0
  56. package/dist/test/suites/build/runner.test.js +9 -8
  57. package/dist/test/suites/build/setup-scripts.test.js +27 -0
  58. package/dist/test/suites/build/validate-config.test.js +13 -2
  59. package/dist/test/suites/build/verb-logs.test.js +20 -0
  60. package/dist/test/suites/renderer/window-desktop-surface.test.js +1 -1
  61. package/dist/utils/build-pipeline.js +4 -4
  62. package/dist/utils/runner-env.js +13 -28
  63. package/dist/vendor/config/company.js +46 -14
  64. package/dist/vendor/config/defaults.js +30 -7
  65. package/dist/vendor/config/edit.js +25 -3
  66. package/dist/vendor/config/env-delivery.js +1 -1
  67. package/dist/vendor/config/env-schema.js +3 -6
  68. package/dist/vendor/config/env.js +34 -22
  69. package/dist/vendor/config/index.js +13 -17
  70. package/dist/vendor/config/load.js +15 -7
  71. package/dist/vendor/config/repo.js +10 -27
  72. package/dist/vendor/config/schema-client.js +64 -0
  73. package/dist/vendor/config/schema-cloud.js +38 -0
  74. package/dist/vendor/config/schema-manager.js +118 -0
  75. package/dist/vendor/config/schema-overrides.js +68 -0
  76. package/dist/vendor/config/schema.js +99 -152
  77. package/dist/vendor/config/validate.js +97 -77
  78. package/dist/vendor/devkit/agents-md.js +233 -0
  79. package/dist/vendor/devkit/attach-log-file.js +15 -1
  80. package/dist/vendor/devkit/ci-workflows.js +30 -30
  81. package/dist/vendor/devkit/cli-router.js +13 -7
  82. package/dist/vendor/devkit/defaults-engine.js +9 -43
  83. package/dist/vendor/devkit/deploy-snapshot.js +44 -9
  84. package/dist/vendor/devkit/env-lines.js +183 -0
  85. package/dist/vendor/devkit/local.js +62 -10
  86. package/dist/vendor/devkit/lockfile.js +32 -13
  87. package/dist/vendor/devkit/logger.js +7 -2
  88. package/dist/vendor/devkit/merge-line-files.js +219 -176
  89. package/dist/vendor/devkit/omega-bin.js +208 -111
  90. package/dist/vendor/devkit/preludes/docs-sync.js +52 -0
  91. package/dist/vendor/devkit/preludes/index.js +1 -0
  92. package/dist/vendor/devkit/target-picker.js +45 -0
  93. package/dist/vendor/devkit/test/dashed-files.js +37 -0
  94. package/dist/vendor/devkit/test/run-verb-under-tee.js +71 -0
  95. package/dist/vendor/devkit/update.js +15 -15
  96. package/dist/vendor/devkit/verb-scripts.js +40 -0
  97. package/dist/vendor/devkit/verbs.js +170 -0
  98. package/package.json +18 -24
  99. package/dist/commands/install.js +0 -37
  100. package/dist/defaults/AGENTS.md +0 -119
  101. package/dist/defaults/CLAUDE.md +0 -1
  102. package/dist/vendor/config/env-retired.js +0 -137
  103. package/dist/vendor/config/retired-keys.js +0 -635
  104. package/docs/analytics.md +0 -140
  105. package/docs/app-state.md +0 -92
  106. package/docs/audit.md +0 -69
  107. package/docs/auth.md +0 -284
  108. package/docs/auto-updater.md +0 -243
  109. package/docs/boot-sequence.md +0 -44
  110. package/docs/build-system.md +0 -169
  111. package/docs/cdp-debugging.md +0 -169
  112. package/docs/common-mistakes.md +0 -21
  113. package/docs/config-schema.md +0 -120
  114. package/docs/context-menu.md +0 -112
  115. package/docs/context.md +0 -81
  116. package/docs/css.md +0 -84
  117. package/docs/deep-link.md +0 -186
  118. package/docs/environment-detection.md +0 -112
  119. package/docs/fontawesome.md +0 -109
  120. package/docs/hooks.md +0 -89
  121. package/docs/icons.md +0 -79
  122. package/docs/index.md +0 -328
  123. package/docs/installer-options.md +0 -165
  124. package/docs/ipc.md +0 -61
  125. package/docs/lib-modules.md +0 -53
  126. package/docs/logging.md +0 -227
  127. package/docs/menu.md +0 -160
  128. package/docs/releasing.md +0 -239
  129. package/docs/remote-config.md +0 -118
  130. package/docs/remote-scripts.md +0 -144
  131. package/docs/restart-manager.md +0 -144
  132. package/docs/runner.md +0 -290
  133. package/docs/sentry.md +0 -97
  134. package/docs/shared/agent-docs.md +0 -89
  135. package/docs/shared/analytics.md +0 -612
  136. package/docs/shared/brands.md +0 -57
  137. package/docs/shared/breaking-changes.md +0 -917
  138. package/docs/shared/config.md +0 -1948
  139. package/docs/shared/deploys.md +0 -341
  140. package/docs/shared/icons.md +0 -219
  141. package/docs/shared/local-dev.md +0 -167
  142. package/docs/shared/logging.md +0 -205
  143. package/docs/shared/monitoring.md +0 -167
  144. package/docs/shared/publishing.md +0 -187
  145. package/docs/shared/rulings.md +0 -34
  146. package/docs/shared/testing.md +0 -147
  147. package/docs/shared/theming.md +0 -629
  148. package/docs/shared/translation.md +0 -342
  149. package/docs/shared/updates.md +0 -61
  150. package/docs/signing.md +0 -293
  151. package/docs/startup.md +0 -142
  152. package/docs/storage.md +0 -59
  153. package/docs/templating.md +0 -101
  154. package/docs/test-boot-layer.md +0 -157
  155. package/docs/test-framework.md +0 -362
  156. package/docs/themes.md +0 -149
  157. package/docs/tooltips.md +0 -99
  158. package/docs/tray.md +0 -164
  159. package/docs/usage.md +0 -58
  160. package/docs/verts.md +0 -62
  161. package/docs/windows.md +0 -149
@@ -64,6 +64,12 @@
64
64
  * refinements, seeded from real brand configs, never from guesses.
65
65
  */
66
66
 
67
+ // Rule sets kept in their own files, one concern each.
68
+ const { MANAGER_RULES } = require('./schema-manager.js');
69
+ const { CLIENT_RULES } = require('./schema-client.js');
70
+ const { CLOUD_CONFIG_RULES } = require('./schema-cloud.js');
71
+ const { OVERRIDE_RULES } = require('./schema-overrides.js');
72
+
67
73
  // The durations a winback coupon can be built for on every provider that
68
74
  // supports the offer (#268). Stripe's third option ('repeating') needs a
69
75
  // duration_in_months beside it, which is provider surface nothing here asks
@@ -201,13 +207,14 @@ const CLIENT_SECTIONS = {
201
207
  const BRAND_ID_PATTERN = /^[a-z][a-z0-9+\-.]*$/;
202
208
 
203
209
  const SHARED_SCHEMA = [
210
+ ...MANAGER_RULES,
204
211
  // ── the target's own url ─────────────────────────────────────────────────
205
212
  {
206
213
  path: 'url',
207
214
  type: 'string',
208
215
  required: false,
209
216
  match: /^https?:\/\//,
210
- 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.",
217
+ 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>; 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.",
211
218
  },
212
219
 
213
220
  // ── the target's own type (#886) ─────────────────────────────────────────
@@ -220,7 +227,7 @@ const SHARED_SCHEMA = [
220
227
  type: 'string',
221
228
  required: false,
222
229
  enum: [...TARGETS, CUSTOM_TARGET_TYPE],
223
- 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.",
230
+ description: "The FRAMEWORK that runs this target, 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.",
224
231
  },
225
232
 
226
233
  // ── brand ────────────────────────────────────────────────────────────────
@@ -260,7 +267,7 @@ const SHARED_SCHEMA = [
260
267
  path: 'brand.type',
261
268
  type: 'string',
262
269
  required: false,
263
- description: "The brand's schema.org type ('Organization', 'Corporation', 'LocalBusiness'). @omega.js/web stamps it on the JSON-LD brand node and every @id that cross-references it; unset renders the framework default 'Organization' (#271).",
270
+ description: "The brand's schema.org type ('Organization', 'Corporation', 'LocalBusiness'). @omega.js/web stamps it on the JSON-LD brand node and every @id that cross-references it; unset renders the framework default 'Organization'.",
264
271
  },
265
272
  {
266
273
  path: 'brand.font',
@@ -285,7 +292,7 @@ const SHARED_SCHEMA = [
285
292
  path: 'brand.contact.person.name',
286
293
  type: 'string',
287
294
  required: false,
288
- description: 'The human who signs "personal" emails, as it should read ("Jane Doe, CEO"). Required only when something sends a personal-signoff email — that path fails loudly rather than substituting a framework identity.',
295
+ description: 'The human who signs "personal" emails, as it should read ("Jane Doe, CEO"). Required only when something sends a personal-signoff email; that path fails loudly rather than substituting a framework identity.',
289
296
  },
290
297
  {
291
298
  path: 'brand.contact.person.firstName',
@@ -329,7 +336,7 @@ const SHARED_SCHEMA = [
329
336
  type: 'string',
330
337
  required: false,
331
338
  match: /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/,
332
- description: 'Brand accent color as a hex string (#RGB or #RRGGBB). @omega.js/web composes the --omega-accent ramps (light + dark) from it; unset leaves the theme\'s neutral placeholder standing.',
339
+ description: "Brand accent color as a hex string, #RGB or #RRGGBB. @omega.js/web composes the --omega-accent ramps (light + dark) from it; unset leaves the theme's neutral placeholder standing.",
333
340
  },
334
341
  {
335
342
  path: 'brand.images',
@@ -343,7 +350,7 @@ const SHARED_SCHEMA = [
343
350
  path: 'socials',
344
351
  type: 'object',
345
352
  required: false,
346
- description: "Platform → handle ({ twitter: 'somiibo' }). The handle derives the profile URL every surface reads (JSON-LD sameAs, the footer row, omega_social), and @omega.js/web emits a redirect shortlink page at /<platform> for each entry (#429). An entry that redirects somewhere OTHER than its profile URL takes the object form { handle, redirect } — the redirect target wins for the shortlink, the handle still names the profile.",
353
+ description: "Platform → handle ({ twitter: 'somiibo' }). The handle derives the profile URL every surface reads (JSON-LD sameAs, the footer row, omega_social), and @omega.js/web emits a redirect shortlink page at /<platform> for each entry. An entry that redirects somewhere OTHER than its profile URL takes the object form { handle, redirect }: the redirect target wins for the shortlink, the handle still names the profile.",
347
354
  },
348
355
 
349
356
  // ── cloud (role: app/cloud platform; D12 provider-discriminated) ─────────
@@ -352,25 +359,14 @@ const SHARED_SCHEMA = [
352
359
  type: 'string',
353
360
  required: false,
354
361
  enum: ['firebase'],
355
- description: "App/cloud platform provider. Only 'firebase' today — the discriminator exists so a second provider slots in without a key rename.",
356
- },
357
- {
358
- path: 'cloud.config',
359
- type: 'object',
360
- required: false,
361
- description: 'Provider app config. For firebase: the web-app config verbatim from the console. Public by design — the web API key is not a secret.',
362
- },
363
- {
364
- path: 'cloud.config.projectId',
365
- type: 'string',
366
- required: false,
367
- description: 'Drives auth, emulator project selection, analytics uuidv5 namespace, remote-config URL fallbacks.',
362
+ description: "App/cloud platform provider. Only 'firebase' today; the discriminator exists so a second provider slots in without a key rename.",
368
363
  },
364
+ ...CLOUD_CONFIG_RULES,
369
365
  {
370
366
  path: 'cloud.messaging.vapidKey',
371
367
  type: 'string',
372
368
  required: false,
373
- description: 'Web-push VAPID public key (Firebase console → Cloud Messaging → Web Push certificates). Public by design — it ships to every browser; the private half stays in the console.',
369
+ description: 'Web-push VAPID public key (Firebase console → Cloud Messaging → Web Push certificates). Public by design: it ships to every browser, and the private half stays in the console.',
374
370
  },
375
371
  {
376
372
  path: 'cloud.shared',
@@ -384,19 +380,19 @@ const SHARED_SCHEMA = [
384
380
  type: 'string',
385
381
  required: false,
386
382
  match: /@/,
387
- description: "OAuth consent screen support email. Defaults to the AUTHORIZING user's email — the provider rejects any address the caller doesn't own; set only for an owned Google Group.",
383
+ description: "OAuth consent screen support email. Defaults to the AUTHORIZING user's email, because the provider rejects any address the caller doesn't own; set only for an owned Google Group.",
388
384
  },
389
385
  {
390
386
  path: 'cloud.oauthRedirectsConfigured',
391
387
  type: 'boolean',
392
388
  required: false,
393
- description: "true = the Google OAuth client's authorized origins + redirect URIs are configured by hand. The ONE reconcile flag config keeps (#434) — that surface has no API to re-check, so the cloud service asks once, writes the answer back here, and every later run trusts it. Never set it without doing the work: the confirm is the only proof there is.",
389
+ description: "true = the Google OAuth client's authorized origins + redirect URIs are configured by hand. The ONE reconcile flag config keeps: that surface has no API to re-check, so the cloud service asks once, writes the answer back here, and every later run trusts it. Never set it without doing the work: the confirm is the only proof there is.",
394
390
  },
395
391
  {
396
392
  path: 'cloud.consentAudience',
397
393
  type: 'boolean',
398
394
  required: false,
399
- description: "false = stop asking about the OAuth consent screen's audience. Tri-state opt-out (#667): Google gives NO API write for the audience, so an Internal screen (Error 403: org_internal for every non-org account) is a manage-time stopper that opens the console page and polls — `false` records the deliberate choice to live with an org-only sign-in, and only the cloud service's audience step goes quiet.",
395
+ description: "false = stop asking about the OAuth consent screen's audience. A tri-state opt-out: Google gives NO API write for the audience, so an Internal screen (Error 403: org_internal for every non-org account) is a manage-time stopper that opens the console page and polls; `false` records the deliberate choice to live with an org-only sign-in, and only the cloud service's audience step goes quiet.",
400
396
  },
401
397
  {
402
398
  path: 'cloud.apiSubdomain',
@@ -409,13 +405,13 @@ const SHARED_SCHEMA = [
409
405
  path: 'cloud.organizationId',
410
406
  type: 'string|boolean',
411
407
  required: false,
412
- description: 'Cloud organization ID the project is created inside — tri-state: unset = ask at project create, false = no org (standalone), value = create inside it.',
408
+ description: 'Cloud organization ID the project is created inside, a tri-state: unset = ask at project create, false = no org (standalone), value = create inside it.',
413
409
  },
414
410
  {
415
411
  path: 'cloud.billingAccount',
416
412
  type: 'string|boolean',
417
413
  required: false,
418
- description: "Billing account ('billingAccounts/XXXXXX-XXXXXX-XXXXXX') — tri-state: unset = ask, false = stay on the free tier, value = auto-upgrade.",
414
+ description: "Billing account ('billingAccounts/XXXXXX-XXXXXX-XXXXXX'), a tri-state: unset = ask, false = stay on the free tier, value = auto-upgrade.",
419
415
  },
420
416
 
421
417
  // ── analytics ────────────────────────────────────────────────────────────
@@ -430,13 +426,13 @@ const SHARED_SCHEMA = [
430
426
  path: 'analytics.providers.google.propertyId',
431
427
  type: 'string',
432
428
  required: false,
433
- description: 'GA4 property id (digits, as the Admin API returns it) — the manager\'s analytics service reconciles the brand\'s data streams and the Firebase link against it, and it gates the whole google half: no propertyId, no google operations. Interactive setup selects/creates the property and writes it back here.',
429
+ description: "GA4 property id (digits, as the Admin API returns it). The manager's analytics service reconciles the brand's data streams and the Firebase link against it, and it gates the whole google half: no propertyId, no google operations. Interactive setup selects/creates the property and writes it back here.",
434
430
  },
435
431
  {
436
432
  path: 'analytics.providers.google.accountId',
437
433
  type: 'string',
438
434
  required: false,
439
- description: "GA account id the property lives under (digits) — the analytics property picker and the console bookmarks read it. A company-managed brand inherits the COMPANY layer's value through the merge chain; a brand-level value always wins.",
435
+ description: "GA account id the property lives under (digits), read by the analytics property picker and the console bookmarks. A company-managed brand inherits the COMPANY layer's value through the merge chain; a brand-level value always wins.",
440
436
  },
441
437
  {
442
438
  path: 'analytics.providers.meta.id',
@@ -448,7 +444,7 @@ const SHARED_SCHEMA = [
448
444
  path: 'analytics.providers.meta.accountId',
449
445
  type: 'string',
450
446
  required: false,
451
- description: "Meta AD ACCOUNT id ('act_…' or the bare digits) the pixel is created on and found by name against. A non-secret platform id, so it lives here beside google.accountId — the token that reaches the account stays in .env (META_ACCESS_TOKEN).",
447
+ description: "Meta AD ACCOUNT id ('act_…' or the bare digits) the pixel is created on and found by name against. A non-secret platform id, so it lives here beside google.accountId; the token that reaches the account stays in .env (META_ACCESS_TOKEN).",
452
448
  },
453
449
  {
454
450
  path: 'analytics.providers.tiktok.id',
@@ -466,7 +462,7 @@ const SHARED_SCHEMA = [
466
462
  path: 'analytics.providers.tiktok.appId',
467
463
  type: 'string',
468
464
  required: false,
469
- description: "TikTok DEVELOPER APP id the access-token mint authorizes through (#448) — public config, so the manager can walk the portal without asking twice; setup writes it back here (#635). The app secret is pasted once and never saved, and the token lands in .env (TIKTOK_ACCESS_TOKEN).",
465
+ description: 'TikTok DEVELOPER APP id the access-token mint authorizes through. Public config, so the manager can walk the portal without asking twice; setup writes it back here. The app secret is pasted once and never saved, and the token lands in .env (TIKTOK_ACCESS_TOKEN).',
470
466
  },
471
467
 
472
468
  // ── advertising ──────────────────────────────────────────────────────────
@@ -474,7 +470,7 @@ const SHARED_SCHEMA = [
474
470
  path: 'advertising.providers.adsense.client',
475
471
  type: 'string',
476
472
  required: false,
477
- description: 'AdSense publisher client id (ca-pub-…). The ONE adsense switch (#527), presence-driven: set it and the manager service manages the account, the site renders units from it and ads.txt carries the record; leave it out and none of that happens. No second gate — a managed-but-ad-free brand is deliberately inexpressible.',
473
+ description: 'AdSense publisher client id (ca-pub-…). The ONE adsense switch, presence-driven: set it and the manager service manages the account, the site renders units from it and ads.txt carries the record; leave it out and none of that happens. No second gate exists, so a managed-but-ad-free brand is deliberately inexpressible.',
478
474
  },
479
475
  {
480
476
  path: 'advertising.providers.adsense.displaySlot',
@@ -510,13 +506,13 @@ const SHARED_SCHEMA = [
510
506
  path: 'advertising.fallback',
511
507
  type: 'string|boolean',
512
508
  required: false,
513
- description: "The role a provider miss falls through to — 'inhouse' renders the house unit, false (or absent) ends the ladder at the built-in promo. Role-level, beside `providers` (docs/web/ads-system.md).",
509
+ description: "The role a provider miss falls through to: 'inhouse' renders the house unit, false (or absent) ends the ladder at the built-in promo. Role-level, beside `providers` (docs/web/ads-system.md).",
514
510
  },
515
511
  {
516
512
  path: 'advertising.tags',
517
513
  type: 'array',
518
514
  required: false,
519
- 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.",
515
+ 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.",
520
516
  },
521
517
 
522
518
  // ── company (ONE typed key, the rest RESOLVED) ───────────────────────────
@@ -543,7 +539,7 @@ const SHARED_SCHEMA = [
543
539
  // would be noise, and the fact belongs to the brands that actually share
544
540
  // somebody else's provider account.
545
541
  materialize: false,
546
- 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`.",
542
+ description: "Whether the manage walk may repoint this company's provider ACCOUNT webhooks. `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.",
547
543
  },
548
544
 
549
545
  // ── features (the catalog every product prices) ──────────────────────────
@@ -551,7 +547,7 @@ const SHARED_SCHEMA = [
551
547
  path: 'features',
552
548
  type: 'object',
553
549
  required: false,
554
- 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.",
550
+ description: "The feature catalog: 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) and definition, plus, 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.",
555
551
  },
556
552
 
557
553
  // ── payment (role: billing; provider-discriminated) ──────────────────────
@@ -560,7 +556,7 @@ const SHARED_SCHEMA = [
560
556
  type: 'string',
561
557
  required: false,
562
558
  default: 'USD',
563
- 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.",
559
+ description: "The ISO 4217 code every price in payment.products is quoted in. 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'.",
564
560
  },
565
561
  {
566
562
  path: 'payment.providers.stripe.publishableKey',
@@ -586,20 +582,20 @@ const SHARED_SCHEMA = [
586
582
  type: 'boolean',
587
583
  required: false,
588
584
  default: false,
589
- description: "Coinbase Commerce (crypto) checkout, one-time purchases only. Its ONLY credential is a secret (COINBASE_COMMERCE_API_KEY), so there is no public datum to gate on — this is the switch, default OFF, because an accidental ON shows a Crypto button no key can complete.",
585
+ description: 'Coinbase Commerce (crypto) checkout, one-time purchases only. Its ONLY credential is a secret (COINBASE_COMMERCE_API_KEY), so there is no public datum to gate on: this is the switch, default OFF, because an accidental ON shows a Crypto button no key can complete.',
590
586
  },
591
587
  {
592
588
  path: 'payment.products',
593
589
  type: 'array',
594
590
  required: false,
595
- description: 'Product catalog (@omega.js/backend-shaped: id, name, type, prices, per-provider IDs) — referenceable from every target. The ONLY pricing-page source (C2); optional presentation fields: tagline, popular, enterprise (the talk-to-us tier: its own full-width row, never a card), hidden (still created on every provider and purchasable by id — QA tiers, grandfathered plans — but never rendered on the pricing page), url, features { <catalog id>: value } — a number on a counted feature (the MONTHLY limit, -1 unlimited), true/false/a string on a perk (#647).',
591
+ description: 'Product catalog (@omega.js/backend-shaped: id, name, type, prices, per-provider IDs), referenceable from every target and the ONLY pricing-page source. Optional presentation fields: tagline, popular, enterprise (the talk-to-us tier: its own full-width row, never a card), hidden (still created on every provider and purchasable by id, as QA tiers and grandfathered plans are, but never rendered on the pricing page), url, and features { <catalog id>: value }, a number on a counted feature (the MONTHLY limit, -1 unlimited) or true/false/a string on a perk.',
596
592
  },
597
593
  {
598
594
  path: 'payment.winback.enabled',
599
595
  type: 'boolean',
600
596
  required: false,
601
597
  default: true,
602
- description: 'The cancel-flow save offer (#268), shown before the cancellation questionnaire. Defaults ON — false is the whole off switch, and the cancel flow goes straight to the questionnaire.',
598
+ description: 'The cancel-flow save offer, shown before the cancellation questionnaire. Defaults ON; false is the whole off switch, and the cancel flow goes straight to the questionnaire.',
603
599
  },
604
600
  {
605
601
  path: 'payment.winback.percent',
@@ -638,7 +634,7 @@ const SHARED_SCHEMA = [
638
634
  path: 'monitoring.providers.sentry.org',
639
635
  type: 'string',
640
636
  required: false,
641
- description: 'Sentry organization slug (public). Auto-detected when the auth token sees exactly one org — the monitoring service writes it back here.',
637
+ description: 'Sentry organization slug (public). Auto-detected when the auth token sees exactly one org; the monitoring service writes it back here.',
642
638
  },
643
639
  {
644
640
  path: 'monitoring.providers.sentry.dsn',
@@ -675,7 +671,7 @@ const SHARED_SCHEMA = [
675
671
  required: false,
676
672
  min: 0,
677
673
  max: 1,
678
- description: 'Browser only: fraction of SESSIONS recorded as a replay, 0..1. Defaults to 0 — replay costs bandwidth and captures the DOM, so it is strictly opt-in; any value above 0 loads the replay integration.',
674
+ description: 'Browser only: fraction of SESSIONS recorded as a replay, 0..1. Defaults to 0, because replay costs bandwidth and captures the DOM, so it is strictly opt-in; any value above 0 loads the replay integration.',
679
675
  },
680
676
  {
681
677
  path: 'monitoring.providers.sentry.replaysOnErrorSampleRate',
@@ -701,7 +697,7 @@ const SHARED_SCHEMA = [
701
697
  path: 'monitoring.providers.sentry.bundlePatterns',
702
698
  type: 'array',
703
699
  required: false,
704
- description: "Browser only: the URL fragments that identify OUR bundles — a browser event reports only when a stack frame matches one. Defaults to ['/assets/js/'], where both @omega.js/web and @omega.js/extension serve every framework bundle.",
700
+ description: "Browser only: the URL fragments that identify OUR bundles; a browser event reports only when a stack frame matches one. Defaults to ['/assets/js/'], where both @omega.js/web and @omega.js/extension serve every framework bundle.",
705
701
  },
706
702
 
707
703
  // ── connections ──────────────────────────────────────────────────────────
@@ -755,7 +751,7 @@ const SHARED_SCHEMA = [
755
751
  description: 'Connect your Kick channel',
756
752
  },
757
753
  },
758
- 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",
754
+ 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, so 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, and `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. docs/backend/connections.md",
759
755
  },
760
756
 
761
757
  // ── repo (where the brand hosts its source; #883) ───────────────────────
@@ -767,7 +763,7 @@ const SHARED_SCHEMA = [
767
763
  path: 'repo',
768
764
  type: 'object',
769
765
  required: false,
770
- 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).',
766
+ 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>`.',
771
767
  },
772
768
  {
773
769
  path: 'repo.provider',
@@ -794,19 +790,19 @@ const SHARED_SCHEMA = [
794
790
  path: 'domain.providers',
795
791
  type: 'object',
796
792
  required: false,
797
- description: "The brand's REGISTRAR, named as the one KEY under it ({ namecheap: {} }) — presence picks it, and no entry means nothing chosen, so the domain service skips. The provider set is open; the entry may be empty.",
793
+ description: "The brand's REGISTRAR, named as the one KEY under it ({ namecheap: {} }). Presence picks it, and no entry means nothing chosen, so the domain service skips. The provider set is open; the entry may be empty.",
798
794
  },
799
795
  {
800
796
  path: 'domain.providers.namecheap',
801
797
  type: 'object',
802
798
  required: false,
803
- description: 'Namecheap as the registrar — the one provider whose nameservers the domain service points at the Cloudflare zone via API (NAMECHEAP_USERNAME + NAMECHEAP_API_KEY in .env). An empty object is the whole declaration; every other registrar gets manual instructions.',
799
+ description: 'Namecheap as the registrar, the one provider whose nameservers the domain service points at the Cloudflare zone via API (NAMECHEAP_USERNAME + NAMECHEAP_API_KEY in .env). An empty object is the whole declaration; every other registrar gets manual instructions.',
804
800
  },
805
801
  {
806
802
  path: 'domain.email.providers',
807
803
  type: 'object',
808
804
  required: false,
809
- description: "The brand's MAILBOX provider, named as the one KEY under it ({ cloudflare: {} } | { squarespace: {} } | { privateemail: {} }) — presence picks it; no entry leaves the edge service's email-routing operations off.",
805
+ description: "The brand's MAILBOX provider, named as the one KEY under it ({ cloudflare: {} } | { squarespace: {} } | { privateemail: {} }). Presence picks it; no entry leaves the edge service's email-routing operations off.",
810
806
  },
811
807
  {
812
808
  path: 'domain.email.forwarding',
@@ -821,11 +817,11 @@ const SHARED_SCHEMA = [
821
817
  type: 'boolean',
822
818
  required: false,
823
819
  default: true,
824
- description: 'false = the edge service skips entirely — no zone, DNS, settings, rules, speed-test or worker reconciliation for this brand.',
820
+ description: 'false = the edge service skips entirely: no zone, DNS, settings, rules, speed-test or worker reconciliation for this brand.',
825
821
  },
826
822
  {
827
823
  path: 'edge.providers.cloudflare',
828
- type: 'object',
824
+ type: 'object', open: true,
829
825
  required: false,
830
826
  description: 'Cloudflare zone reconciliation (@omega.js/manager cloudflare service): dns, settings, rules, cacheRules, speedTest, workers. Tokens live in .env.',
831
827
  },
@@ -839,7 +835,7 @@ const SHARED_SCHEMA = [
839
835
  path: 'edge.providers.cloudflare.rules.redirect',
840
836
  type: 'array',
841
837
  required: false,
842
- description: "The zone's dynamic redirect rules, ORDERED — the ONE home for a TEMPLATED redirect, i.e. one whose destination is computed from the request path (#466): [{ name, expression, statusCode, preserveQueryString, targetUrl, enabled }]. `expression` and `targetUrl` ({ value } for a fixed URL, { expression } for a computed one) are Cloudflare's own filter language, because only the edge can answer a URL the build cannot enumerate — DashQR's printed `/c/<id>` codes redirect to `/code?id=<id>` with `targetUrl.expression: concat(\"https://\", http.host, \"/code?id=\", substring(http.request.uri.path, 3))`. The @omega.js/manager edge service reconciles them by `name`. A redirect whose URLs CAN be enumerated is a redirect PAGE instead (docs/web/index.md), never config.",
838
+ description: "The zone's dynamic redirect rules, ORDERED, and the ONE home for a TEMPLATED redirect, i.e. one whose destination is computed from the request path: [{ name, expression, statusCode, preserveQueryString, targetUrl, enabled }]. `expression` and `targetUrl` ({ value } for a fixed URL, { expression } for a computed one) are Cloudflare's own filter language, because only the edge can answer a URL the build cannot enumerate: DashQR's printed `/c/<id>` codes redirect to `/code?id=<id>` with `targetUrl.expression: concat(\"https://\", http.host, \"/code?id=\", substring(http.request.uri.path, 3))`. The @omega.js/manager edge service reconciles them by `name`. A redirect whose URLs CAN be enumerated is a redirect PAGE instead (docs/web/index.md), never config.",
843
839
  },
844
840
 
845
841
  // ── captcha (role: bot defense; provider-discriminated) ──────────────────
@@ -847,13 +843,13 @@ const SHARED_SCHEMA = [
847
843
  path: 'captcha.providers.recaptcha.project',
848
844
  type: 'string',
849
845
  required: false,
850
- description: "The brand's OWN GCP project hosting the classic reCAPTCHA key — used only for the console deep-link in guidance. Site/secret keys stay in .env.",
846
+ description: "The brand's OWN GCP project hosting the classic reCAPTCHA key, used only for the console deep-link in guidance. Site/secret keys stay in .env.",
851
847
  },
852
848
  {
853
849
  path: 'captcha.providers.recaptcha.siteKey',
854
850
  type: 'string',
855
851
  required: false,
856
- description: 'Classic reCAPTCHA site key rendered client-side. Public by design — the secret half stays in .env (RECAPTCHA_SECRET_KEY).',
852
+ description: 'Classic reCAPTCHA site key rendered client-side. Public by design; the secret half stays in .env (RECAPTCHA_SECRET_KEY).',
857
853
  },
858
854
  {
859
855
  path: 'captcha.providers.recaptcha.domainsConfirmed',
@@ -874,7 +870,7 @@ const SHARED_SCHEMA = [
874
870
  path: 'forms.providers.slapform.formId',
875
871
  type: 'string',
876
872
  required: false,
877
- description: 'Slapform form id — the contact page posts to https://api.slapform.com/{formId}. Interactive manager runs land it here.',
873
+ description: 'Slapform form id; the contact page posts to https://api.slapform.com/{formId}. Interactive manager runs land it here.',
878
874
  },
879
875
  {
880
876
  path: 'forms.providers.slapform.templateFormId',
@@ -894,7 +890,7 @@ const SHARED_SCHEMA = [
894
890
  type: 'object',
895
891
  required: false,
896
892
  default: { id: 'grandmaster', name: 'Grandmaster' },
897
- description: 'Tier granted to the form-owner account ({ id, name }) — Slapform-operator only.',
893
+ description: 'Tier granted to the form-owner account ({ id, name }), for Slapform operators only.',
898
894
  },
899
895
 
900
896
  // ── inbound (role: inbound conversations; per-channel providers) ─────────
@@ -915,7 +911,7 @@ const SHARED_SCHEMA = [
915
911
  path: 'inbound.chat.providers.chatsy.accountId',
916
912
  type: 'string',
917
913
  required: false,
918
- description: "Chatsy owner-account uid the agent belongs to — the account whose subscription the chat service reconciles to `plan`. The service resolves it from the agent itself; set it only when the agent's owner is provisioned elsewhere.",
914
+ description: "Chatsy owner-account uid the agent belongs to, the account whose subscription the chat service reconciles to `plan`. The service resolves it from the agent itself; set it only when the agent's owner is provisioned elsewhere.",
919
915
  },
920
916
  {
921
917
  path: 'inbound.chat.providers.chatsy.templateAgentId',
@@ -935,7 +931,7 @@ const SHARED_SCHEMA = [
935
931
  type: 'object',
936
932
  required: false,
937
933
  default: { id: 'max', name: 'Max' },
938
- description: 'Tier granted to the agent-owner account ({ id, name }) — Chatsy-operator only.',
934
+ description: 'Tier granted to the agent-owner account ({ id, name }), for Chatsy operators only.',
939
935
  },
940
936
  {
941
937
  path: 'inbound.chat.providers.chatsy.sponsorshipsUrl',
@@ -980,7 +976,7 @@ const SHARED_SCHEMA = [
980
976
  type: 'object',
981
977
  required: false,
982
978
  default: { id: 'max', name: 'Max' },
983
- description: 'Tier granted to the agent-owner account ({ id, name }) — Replyify-operator only.',
979
+ description: 'Tier granted to the agent-owner account ({ id, name }), for Replyify operators only.',
984
980
  },
985
981
  {
986
982
  path: 'inbound.email.providers.replyify.discount',
@@ -995,7 +991,7 @@ const SHARED_SCHEMA = [
995
991
  type: 'boolean',
996
992
  required: false,
997
993
  default: true,
998
- description: 'false = the search service skips entirely — the property is never verified and no sitemap is submitted (submitSitemap only turns the sitemap half off).',
994
+ description: 'false = the search service skips entirely: the property is never verified and no sitemap is submitted (submitSitemap only turns the sitemap half off).',
999
995
  },
1000
996
  {
1001
997
  path: 'search.providers.searchConsole.submitSitemap',
@@ -1023,13 +1019,13 @@ const SHARED_SCHEMA = [
1023
1019
  path: 'certificates.enabled',
1024
1020
  type: 'boolean',
1025
1021
  required: false,
1026
- description: 'false = the certificates service skips entirely — no bundle ids, no signing certificates, no provisioning profiles for this brand. Absent reads as ON for brands with a desktop/mobile target (the service still skips one with neither).',
1022
+ description: 'false = the certificates service skips entirely: no bundle ids, no signing certificates, no provisioning profiles for this brand. Absent reads as ON for brands with a desktop/mobile target (the service still skips one with neither).',
1027
1023
  },
1028
1024
  {
1029
1025
  path: 'certificates.providers.apple.bundleIdPrefix',
1030
1026
  type: 'string',
1031
1027
  required: false,
1032
- description: "Reverse-DNS prefix the brand's bundle id is composed from ('com.mycompany' + brand.id → com.mycompany.my.brand). The brand's own answer — the onboard wizard seeds it and setup asks for it (#635); unset, the Apple operations have no id to reconcile.",
1028
+ description: "Reverse-DNS prefix the brand's bundle id is composed from ('com.mycompany' + brand.id → com.mycompany.my.brand). The brand's own answer: the onboard wizard seeds it and setup asks for it; unset, the Apple operations have no id to reconcile.",
1033
1029
  },
1034
1030
  {
1035
1031
  path: 'certificates.providers.apple.capabilities',
@@ -1047,7 +1043,7 @@ const SHARED_SCHEMA = [
1047
1043
  path: 'certificates.providers.apple.certificates',
1048
1044
  type: 'array',
1049
1045
  required: false,
1050
- 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.",
1046
+ 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.",
1051
1047
  },
1052
1048
 
1053
1049
  // ── publishing (role: ship credentials; no provider) ─────────────────
@@ -1055,7 +1051,7 @@ const SHARED_SCHEMA = [
1055
1051
  path: 'publishing.enabled',
1056
1052
  type: 'boolean',
1057
1053
  required: false,
1058
- 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.",
1054
+ description: "The manage walk's ship-credential gate: 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.",
1059
1055
  },
1060
1056
 
1061
1057
  // ── devlog (role: commit-digest publishing; provider-discriminated) ──────
@@ -1069,7 +1065,7 @@ const SHARED_SCHEMA = [
1069
1065
  path: 'devlog.enabled',
1070
1066
  type: 'boolean',
1071
1067
  required: false,
1072
- description: 'true PUBLISHES AI-written commit-digest posts to the brand\'s live website. Case 3 (docs/shared/config.md): the literal true is the only ON, absence is off, and no default is materialized — publishing is never implicit.',
1068
+ description: "true PUBLISHES AI-written commit-digest posts to the brand's live website. Case 3 (docs/shared/config.md): the literal true is the only ON, absence is off, and no default is materialized, so publishing is never implicit.",
1073
1069
  },
1074
1070
 
1075
1071
  // ── seo ──────────────────────────────────────────────────────────────────
@@ -1077,7 +1073,7 @@ const SHARED_SCHEMA = [
1077
1073
  path: 'seo',
1078
1074
  type: 'object',
1079
1075
  required: false,
1080
- description: 'Parasite-SEO content: seo.github.content is the @omega.js/manager seo service\'s repos, and big content blocks may live in the config/seo.json5 sidecar. The indexing switch is NOT here: it is `targets.web.meta.index`, the same name a page writes (#564).',
1076
+ description: "Parasite-SEO content: seo.github.content is the @omega.js/manager seo service's repos, and big content blocks may live in the config/seo.json5 sidecar. The indexing switch is NOT here: it is `targets.web.meta.index`, the same name a page writes.",
1081
1077
  },
1082
1078
 
1083
1079
  // ── account ──────────────────────────────────────────────────────────────
@@ -1085,7 +1081,7 @@ const SHARED_SCHEMA = [
1085
1081
  path: 'account',
1086
1082
  type: 'object',
1087
1083
  required: false,
1088
- 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.",
1084
+ 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.",
1089
1085
  },
1090
1086
 
1091
1087
  // ── reviews ──────────────────────────────────────────────────────────────
@@ -1141,7 +1137,7 @@ const SHARED_SCHEMA = [
1141
1137
  path: 'marketing.campaigns.providers.sendgrid.groups.account',
1142
1138
  type: 'integer',
1143
1139
  required: false,
1144
- description: "SendGrid unsubscribe (ASM) group id for account-action email (deletion, data requests) — also the fallback for any send naming no group. Written back by the campaigns service, which provisions the group by name; ASM ids are per SendGrid ACCOUNT, so this is config and never code. Unset makes @omega.js/backend fail the send loudly.",
1140
+ description: 'SendGrid unsubscribe (ASM) group id for account-action email (deletion, data requests), and the fallback for any send naming no group. Written back by the campaigns service, which provisions the group by name; ASM ids are per SendGrid ACCOUNT, so this is config and never code. Unset makes @omega.js/backend fail the send loudly.',
1145
1141
  },
1146
1142
  {
1147
1143
  path: 'marketing.campaigns.providers.sendgrid.groups.marketing',
@@ -1184,14 +1180,14 @@ const SHARED_SCHEMA = [
1184
1180
  path: 'marketing.newsletter.content',
1185
1181
  type: 'object|array',
1186
1182
  required: false,
1187
- description: 'Newsletter-PIPELINE config (sources, categories, tone, template, theme, sponsorships) — a single object or an array whose first entry the generator uses. Role-level on purpose: it configures @omega.js/backend\'s generator, not Beehiiv.',
1183
+ description: "Newsletter-PIPELINE config (sources, categories, tone, template, theme, sponsorships), a single object or an array whose first entry the generator uses. Role-level on purpose: it configures @omega.js/backend's generator, not Beehiiv.",
1188
1184
  },
1189
1185
  {
1190
1186
  path: 'marketing.prune.enabled',
1191
1187
  type: 'boolean',
1192
1188
  required: false,
1193
1189
  default: true,
1194
- description: 'Monthly cold-contact prune across both providers. ON by default (Ian 2026-08-22) and materialized into every brand config — false is the per-brand off switch.',
1190
+ description: 'Monthly cold-contact prune across both providers. ON by default and materialized into every brand config; false is the per-brand off switch.',
1195
1191
  },
1196
1192
 
1197
1193
  // ── blog ─────────────────────────────────────────────────────────────────
@@ -1219,14 +1215,14 @@ const SHARED_SCHEMA = [
1219
1215
  path: 'directory',
1220
1216
  type: 'object',
1221
1217
  required: false,
1222
- 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.",
1218
+ 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 and DIRECTORY_SERVICE_ACCOUNT in the brand .env.",
1223
1219
  },
1224
1220
  {
1225
1221
  path: 'directory.enabled',
1226
1222
  type: 'boolean',
1227
1223
  required: false,
1228
1224
  default: false,
1229
- description: 'true opts the brand into the directory push. Default false — participation is never implicit.',
1225
+ description: 'true opts the brand into the directory push. Default false, so participation is never implicit.',
1230
1226
  },
1231
1227
 
1232
1228
  // ── sponsorships ─────────────────────────────────────────────────────────
@@ -1234,7 +1230,7 @@ const SHARED_SCHEMA = [
1234
1230
  path: 'sponsorships',
1235
1231
  type: 'object',
1236
1232
  required: false,
1237
- description: "The brand's sponsorship terms — the first directory BLOCK (#246), and the section the server service also publishes. Public terms only: { acceptable, unacceptable, prices }.",
1233
+ description: "The brand's sponsorship terms, the first directory BLOCK and the section the server service also publishes. Public terms only: { acceptable, unacceptable, prices }.",
1238
1234
  },
1239
1235
  {
1240
1236
  path: 'sponsorships.acceptable',
@@ -1252,7 +1248,7 @@ const SHARED_SCHEMA = [
1252
1248
  path: 'sponsorships.prices',
1253
1249
  type: 'object',
1254
1250
  required: false,
1255
- description: "Price in USD per placement type — placement key to number ({ 'guest-post': 70, 'link-insertion': 50 }). A placement with no price is not for sale.",
1251
+ description: "Price in USD per placement type, placement key to number ({ 'guest-post': 70, 'link-insertion': 50 }). A placement with no price is not for sale.",
1256
1252
  },
1257
1253
 
1258
1254
  // ── ports (dev-only) ─────────────────────────────────────────────────────
@@ -1260,7 +1256,7 @@ const SHARED_SCHEMA = [
1260
1256
  path: 'ports',
1261
1257
  type: 'object',
1262
1258
  required: false,
1263
- description: 'Explicit dev-port pins (N7). Unset ports auto-allocate (classic defaults, bump-if-taken); a pinned port never bumps — busy pin is a hard error. Keys: auth, functions, firestore, database, hosting, storage, pubsub, ui, website, livereload, cdp. Dev/emulator only — production never reads this.',
1259
+ description: 'Explicit dev-port pins. Unset ports auto-allocate (classic defaults, bump-if-taken); a pinned port never bumps, and a busy pin is a hard error. Keys: auth, functions, firestore, database, hosting, storage, pubsub, ui, website, livereload, cdp. Dev/emulator only; production never reads this.',
1264
1260
  },
1265
1261
 
1266
1262
  // ── theme ────────────────────────────────────────────────────────────────
@@ -1268,7 +1264,7 @@ const SHARED_SCHEMA = [
1268
1264
  path: 'theme.id',
1269
1265
  type: 'string',
1270
1266
  required: false,
1271
- description: "Theme id (seeded 'classy' at onboarding). Project-owned — the manager never overwrites it.",
1267
+ description: "Theme id (seeded 'classy' at onboarding). Project-owned: the manager never overwrites it.",
1272
1268
  },
1273
1269
  {
1274
1270
  path: 'theme.appearance',
@@ -1283,7 +1279,7 @@ const SHARED_SCHEMA = [
1283
1279
  path: 'ai.enabled',
1284
1280
  type: 'boolean',
1285
1281
  required: false,
1286
- description: "The manager's AI-provider setup gate ([#639](https://github.com/Omega-JS-Stack/omega/issues/639)): the `ai` service asks once for OPENAI_API_KEY / ANTHROPIC_API_KEY. `false` is the permanent opt-out the gate's Disable lands; absence means ask. No default is materialized — a brand that never answers is never re-shaped.",
1282
+ description: "The manager's AI-provider setup gate: the `ai` service asks once for OPENAI_API_KEY / ANTHROPIC_API_KEY. `false` is the permanent opt-out the gate's Disable lands; absence means ask. No default is materialized, so a brand that never answers is never re-shaped.",
1287
1283
  },
1288
1284
 
1289
1285
  // ── translation ──────────────────────────────────────────────────────────
@@ -1311,7 +1307,7 @@ const SHARED_SCHEMA = [
1311
1307
  path: 'translation.providers',
1312
1308
  type: 'object',
1313
1309
  required: false,
1314
- description: "AI translation provider, keyed by name — presence picks the engine ({ claude: {} } or { chatgpt: {} }). 'claude' (the default when the block is absent) rides the local Claude Code install — no API key; 'chatgpt' uses the OpenAI API via OPENAI_API_KEY in env.",
1310
+ description: "AI translation provider, keyed by name; presence picks the engine ({ claude: {} } or { chatgpt: {} }). 'claude' (the default when the block is absent) rides the local Claude Code install with no API key; 'chatgpt' uses the OpenAI API via OPENAI_API_KEY in env.",
1315
1311
  },
1316
1312
  {
1317
1313
  path: 'translation.model',
@@ -1331,65 +1327,15 @@ const SHARED_SCHEMA = [
1331
1327
  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.",
1332
1328
  },
1333
1329
 
1334
- // ── client (the @omega.js/client runtime blob) ───────────────────────────
1335
- // The blob is a settings bag the client normalizes against its own defaults,
1336
- // so it is deliberately NOT enumerated key by key here. `consent` is the
1337
- // exception (#383): it decides whether a visitor is tracked at all, which is
1338
- // a legal surface, not a preference — a typo that silently disabled the
1339
- // banner would ship a site with no consent gate and no error.
1340
- //
1341
- // The three below are the second exception (#650 — found adopting omega in a
1342
- // consumer): keys a BRAND authors. They work (they reach the client payload
1343
- // and change what the site does), so every validate run told the brand that
1344
- // turned one off it might be a typo. The rest of the blob stays the client's.
1345
- {
1346
- path: 'client.consent.enabled',
1347
- type: 'boolean',
1348
- required: false,
1349
- default: true,
1350
- description: 'The consent banner + the provider-script gate (default true). false ships NO banner — legal only for a site that loads no analytics/marketing provider at all.',
1351
- },
1352
- {
1353
- path: 'client.consent.config.position',
1354
- type: 'string',
1355
- required: false,
1356
- enum: ['bottom-left', 'bottom-right', 'bottom'],
1357
- description: "Where the panel sits. 'bottom' is the centered full-width form.",
1358
- },
1359
- {
1360
- path: 'client.consent.config.content',
1361
- type: 'object',
1362
- required: false,
1363
- description: "Banner copy: message, panelIntro, accept, customize, acceptAll, acceptNone (a literal `{terms}`/`{cookies}` links the terms/cookie-policy page). Category labels are framework copy — a brand renames the buttons, not the categories.",
1364
- },
1365
- {
1366
- path: 'client.auth.config.policy',
1367
- type: 'string',
1368
- required: false,
1369
- enum: ['authenticated', 'unauthenticated', 'disabled'],
1370
- description: "Who a page is FOR: 'authenticated' redirects a signed-out visitor to the signin route, 'unauthenticated' redirects a signed-in one away, 'disabled' skips the auth module entirely (a vert iframe). Absent = no policy, which is the site-wide answer — the auth/admin layouts set theirs in page frontmatter, so a brand only sets this to blanket a whole site.",
1371
- },
1372
- {
1373
- path: 'client.exitPopup.enabled',
1374
- type: 'boolean',
1375
- required: false,
1376
- default: true,
1377
- description: 'The exit-intent offer popup (default true). false ships no popup at all; its copy, timeout and avatars live under client.exitPopup.config.',
1378
- },
1379
- {
1380
- path: 'client.serviceWorker.enabled',
1381
- type: 'boolean',
1382
- required: false,
1383
- default: true,
1384
- description: 'Registers the site service worker (default true) — the offline/refresh lane and the push-notification registration ride it. false unregisters any worker the visitor already has.',
1385
- },
1330
+ // ── client (the @omega.js/client runtime blob): its own file ────────────
1331
+ ...CLIENT_RULES,
1386
1332
 
1387
1333
  // ── targets ──────────────────────────────────────────────────────────────
1388
1334
  {
1389
1335
  path: 'targets',
1390
1336
  type: 'object',
1391
1337
  required: false,
1392
- 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.",
1338
+ description: 'Every key is a target NAME, which is the folder `targets/<name>`, the `--target=<name>` word and the derived-repo suffix. 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` for a target the manager drives entirely through its own package.json scripts. Anything else is an error.',
1393
1339
  },
1394
1340
  ];
1395
1341
 
@@ -1403,7 +1349,7 @@ const TARGET_SCHEMAS = {
1403
1349
  path: 'hosting',
1404
1350
  type: 'object',
1405
1351
  required: false,
1406
- description: 'Where this web target is SERVED from (#883). Web targets only: a non-web target carrying it is a validation error.',
1352
+ description: 'Where this web target is SERVED from. Web targets only: a non-web target carrying it is a validation error.',
1407
1353
  },
1408
1354
  {
1409
1355
  path: 'hosting.provider',
@@ -1426,7 +1372,7 @@ const TARGET_SCHEMAS = {
1426
1372
  type: 'boolean',
1427
1373
  required: false,
1428
1374
  default: true,
1429
- description: "false takes the WHOLE site out of search: every page emits noindex and sitemap.xml, llms.txt and pages.json list nothing. It is the site-wide DEFAULT of the one flag every signal reads (#564), so a page's own `meta.index: true` still exempts it. Case 2 (docs/shared/config.md): default ON, and the literal false is the only OFF.",
1375
+ description: "false takes the WHOLE site out of search: every page emits noindex and sitemap.xml, llms.txt and pages.json list nothing. It is the site-wide DEFAULT of the one flag every signal reads, so a page's own `meta.index: true` still exempts it. Case 2 (docs/shared/config.md): default ON, and the literal false is the only OFF.",
1430
1376
  },
1431
1377
  {
1432
1378
  path: 'imagemin',
@@ -1438,13 +1384,13 @@ const TARGET_SCHEMAS = {
1438
1384
  path: 'dev.limitCollections',
1439
1385
  type: 'object',
1440
1386
  required: false,
1441
- description: "Dev-only collection sampling (#190): collection name → max documents ({ posts: 50 }), plus `randomize: true` for a random sample instead of the first N. Collection names are @omega.js/web's (posts, alternatives, team, updates) plus the brand's own `collections`, and the engine hard-fails an unknown one. Development builds only — production never samples.",
1387
+ description: "Dev-only collection sampling: collection name → max documents ({ posts: 50 }), plus `randomize: true` for a random sample instead of the first N. Collection names are @omega.js/web's (posts, alternatives, team, updates) plus the brand's own `collections`, and the engine hard-fails an unknown one. Development builds only; production never samples.",
1442
1388
  },
1443
1389
  {
1444
1390
  path: 'collections',
1445
1391
  type: 'object',
1446
1392
  required: false,
1447
- description: "The brand's own content collections (#207): collection name → { field, size, title, description, permalink }. Documents live in `_<name>/`, and @omega.js/web generates the paginated listing page plus one page per category of `field` (the dotted frontmatter path the categories group on, e.g. 'doc.category'). A built-in collection name (posts, alternatives, team, updates) is an error.",
1393
+ description: "The brand's own content collections: collection name → { field, size, title, description, permalink }. Documents live in `_<name>/`, and @omega.js/web generates the paginated listing page plus one page per category of `field` (the dotted frontmatter path the categories group on, e.g. 'doc.category'). A built-in collection name (posts, alternatives, team, updates) is an error.",
1448
1394
  },
1449
1395
  {
1450
1396
  path: 'purgecss',
@@ -1456,7 +1402,7 @@ const TARGET_SCHEMAS = {
1456
1402
  path: 'purgecss.safelist',
1457
1403
  type: 'object|array',
1458
1404
  required: false,
1459
- description: "Selectors the content scan cannot see, merged OVER the framework's own safelist (#250). The object form takes PurgeCSS's lanes (standard, deep, greedy, keyframes); a bare array is PurgeCSS's shorthand for `standard`.",
1405
+ description: "Selectors the content scan cannot see, merged OVER the framework's own safelist. The object form takes PurgeCSS's lanes (standard, deep, greedy, keyframes); a bare array is PurgeCSS's shorthand for `standard`.",
1460
1406
  },
1461
1407
  {
1462
1408
  path: 'purgecss.safelist.standard',
@@ -1474,7 +1420,7 @@ const TARGET_SCHEMAS = {
1474
1420
  path: 'purgecss.safelist.greedy',
1475
1421
  type: 'array',
1476
1422
  required: false,
1477
- description: 'Patterns (strings, compiled to RegExp) keeping every selector that contains a match — the lane for a runtime-stamped namespace.',
1423
+ description: 'Patterns (strings, compiled to RegExp) keeping every selector that contains a match, the lane for a runtime-stamped namespace.',
1478
1424
  },
1479
1425
  {
1480
1426
  path: 'purgecss.safelist.keyframes',
@@ -1486,6 +1432,7 @@ const TARGET_SCHEMAS = {
1486
1432
 
1487
1433
  // Seeded from the sandbox brand's real @omega.js/backend config.
1488
1434
  backend: [
1435
+ ...OVERRIDE_RULES.backend,
1489
1436
  // reviews, marketing, blog, dataRequest moved to SHARED_SCHEMA (#277); a
1490
1437
  // targets.backend block still overrides them through the merge chain.
1491
1438
  {
@@ -1493,7 +1440,7 @@ const TARGET_SCHEMAS = {
1493
1440
  type: 'string',
1494
1441
  required: false,
1495
1442
  enum: BACKEND_PROJECT_TYPES,
1496
- description: "How this backend RUNS and deploys (#584). 'firebase' (default) exports Cloud Functions; 'custom' runs the same app as an Express server on `PORT` for a container host (Render & co) — no Functions deploy, no emulator lane, and the target's own `deploy` script is the publish lane. Everything else — the auth middleware, the routes, the schemas, every helper — is identical in both modes.",
1443
+ description: "How this backend RUNS and deploys. 'firebase' (default) exports Cloud Functions; 'custom' runs the same app as an Express server on `PORT` for a container host (Render & co), with no Functions deploy, no emulator lane, and the target's own `deploy` script as the publish lane. Everything else (the auth middleware, the routes, the schemas, every helper) is identical in both modes.",
1497
1444
  },
1498
1445
  {
1499
1446
  path: 'auth.signup.maxPerIpPerDay',
@@ -1506,6 +1453,7 @@ const TARGET_SCHEMAS = {
1506
1453
 
1507
1454
  // The per-OS key is `platforms` (avoids targets.desktop.targets).
1508
1455
  desktop: [
1456
+ ...OVERRIDE_RULES.desktop,
1509
1457
  {
1510
1458
  path: 'app.category',
1511
1459
  type: 'string',
@@ -1521,7 +1469,7 @@ const TARGET_SCHEMAS = {
1521
1469
  path: 'app.appId',
1522
1470
  type: 'string',
1523
1471
  required: false,
1524
- 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.",
1472
+ 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, 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.',
1525
1473
  },
1526
1474
  {
1527
1475
  path: 'app.productName',
@@ -1564,7 +1512,7 @@ const TARGET_SCHEMAS = {
1564
1512
  path: 'platforms',
1565
1513
  type: 'object',
1566
1514
  required: false,
1567
- 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.",
1515
+ description: "What this desktop target SHIPS, plus each platform's install knobs: `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.",
1568
1516
  },
1569
1517
  {
1570
1518
  path: 'platforms.mac',
@@ -1746,7 +1694,7 @@ const TARGET_SCHEMAS = {
1746
1694
  path: 'releases',
1747
1695
  type: 'object',
1748
1696
  required: false,
1749
- 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).",
1697
+ 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`.",
1750
1698
  },
1751
1699
  {
1752
1700
  path: 'restartManager.enabled',
@@ -1786,14 +1734,14 @@ const TARGET_SCHEMAS = {
1786
1734
  path: 'remoteScripts.enabled',
1787
1735
  type: 'boolean',
1788
1736
  required: false,
1789
- description: 'Emergency remote code execution in the main process — OPT-IN: only `true` arms the lane, anything else leaves it inert.',
1737
+ description: 'Emergency remote code execution in the main process, OPT-IN: only `true` arms the lane, anything else leaves it inert.',
1790
1738
  },
1791
1739
  {
1792
1740
  path: 'remoteScripts.url',
1793
1741
  type: 'string',
1794
1742
  required: false,
1795
1743
  match: /^https?:\/\//,
1796
- description: 'Remote-script source override (default `${brand.url}/data/scripts/main.js`). https-only at runtime — a cleartext URL is refused (localhost excepted for dev).',
1744
+ description: 'Remote-script source override (default `${brand.url}/data/scripts/main.js`). https-only at runtime: a cleartext URL is refused (localhost excepted for dev).',
1797
1745
  },
1798
1746
  {
1799
1747
  path: 'restartManager.feed.url',
@@ -1815,7 +1763,7 @@ const TARGET_SCHEMAS = {
1815
1763
  path: 'platforms',
1816
1764
  type: 'object',
1817
1765
  required: false,
1818
- 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.',
1766
+ description: "What this extension target SHIPS: `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.",
1819
1767
  },
1820
1768
  {
1821
1769
  path: 'platforms.chrome',
@@ -1839,7 +1787,7 @@ const TARGET_SCHEMAS = {
1839
1787
  path: 'platforms.chrome.formats.store',
1840
1788
  type: 'object|boolean',
1841
1789
  required: false,
1842
- description: 'The Chrome Web Store publish. Needs the CHROME_* API credentials plus `listings.chrome.id` (the walk asks for both, #893).',
1790
+ description: 'The Chrome Web Store publish. Needs the CHROME_* API credentials plus `listings.chrome.id` (the walk asks for both).',
1843
1791
  },
1844
1792
  {
1845
1793
  path: 'platforms.firefox',
@@ -1894,7 +1842,7 @@ const TARGET_SCHEMAS = {
1894
1842
  path: 'platforms.edge.formats.store',
1895
1843
  type: 'object|boolean',
1896
1844
  required: false,
1897
- description: 'The Edge Add-ons publish. Needs EDGE_CLIENT_ID + EDGE_API_KEY plus `listings.edge.id` (the Partner Center product id, #893).',
1845
+ description: 'The Edge Add-ons publish. Needs EDGE_CLIENT_ID + EDGE_API_KEY plus `listings.edge.id` (the Partner Center product id).',
1898
1846
  },
1899
1847
  {
1900
1848
  path: 'categories',
@@ -1902,20 +1850,20 @@ const TARGET_SCHEMAS = {
1902
1850
  required: false,
1903
1851
  itemEnum: AMO_CATEGORIES,
1904
1852
  default: ['alerts-updates'],
1905
- 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.",
1853
+ description: "The AMO categories a FIRST Firefox publish lists the add-on under: 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.",
1906
1854
  },
1907
1855
  {
1908
1856
  path: 'listings.chrome.id',
1909
1857
  type: 'string',
1910
1858
  required: false,
1911
- 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.",
1859
+ description: 'The Chrome Web Store ITEM ID the publish uploads to, the 32-letter id in the listing URL. Public by design, so it lives here and not in .env; the manage walk asks for it.',
1912
1860
  },
1913
1861
  {
1914
1862
  path: 'listings.chrome.url',
1915
1863
  type: 'string',
1916
1864
  required: false,
1917
1865
  match: /^https?:\/\//,
1918
- description: "Chrome Web Store listing URL. Feeds site.targets.extension.listings.chrome — the /extension page's button and its /extension/chrome shortlink.",
1866
+ description: "Chrome Web Store listing URL. Feeds site.targets.extension.listings.chrome, the /extension page's button and its /extension/chrome shortlink.",
1919
1867
  },
1920
1868
  {
1921
1869
  path: 'listings.chrome.state',
@@ -1927,7 +1875,7 @@ const TARGET_SCHEMAS = {
1927
1875
  path: 'listings.firefox.id',
1928
1876
  type: 'string',
1929
1877
  required: false,
1930
- 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.",
1878
+ 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). 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.",
1931
1879
  },
1932
1880
  {
1933
1881
  path: 'listings.firefox.url',
@@ -1946,7 +1894,7 @@ const TARGET_SCHEMAS = {
1946
1894
  path: 'listings.edge.id',
1947
1895
  type: 'string',
1948
1896
  required: false,
1949
- 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.",
1897
+ description: 'The Microsoft Edge Partner Center PRODUCT ID (a GUID) the publish uploads to. Public by design, so it lives here and not in .env; the manage walk asks for it.',
1950
1898
  },
1951
1899
  {
1952
1900
  path: 'listings.edge.url',
@@ -2007,10 +1955,9 @@ const TARGET_SCHEMAS = {
2007
1955
  };
2008
1956
 
2009
1957
  // Declared top-level keys that are NOT config sections: `targets` is scoping
2010
- // machinery, and `url` is this target's RESOLVED public url (#588): a value
2011
- // the loader derives, carried to templates as the `site.url` build fact, never
2012
- // a namespace a page's `config:` block overrides.
2013
- const NON_SECTION_KEYS = ['targets', 'url', 'type'];
1958
+ // machinery, `url` the target's RESOLVED public url (the `site.url` build fact),
1959
+ // `type` an entry's framework, and `enabled` the brand's on/off switch.
1960
+ const NON_SECTION_KEYS = ['targets', 'url', 'type', 'enabled'];
2014
1961
 
2015
1962
  /** The top-level sections a rule array declares (machinery keys excluded). */
2016
1963
  const sectionsOf = (rules) => [...new Set(rules.map((rule) => rule.path.split('.')[0]))].filter((section) => !NON_SECTION_KEYS.includes(section));