@omega.js/backend 0.1.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 (993) hide show
  1. package/LICENSE +98 -0
  2. package/README.md +1053 -0
  3. package/bin/omega +2 -0
  4. package/bin/omega-backend +2 -0
  5. package/bin/omg +2 -0
  6. package/cli.js +3 -0
  7. package/dist/cli/command-table.js +268 -0
  8. package/dist/cli/commands/auth.js +259 -0
  9. package/dist/cli/commands/base-command.js +451 -0
  10. package/dist/cli/commands/build.js +27 -0
  11. package/dist/cli/commands/clean.js +13 -0
  12. package/dist/cli/commands/clear.js +11 -0
  13. package/dist/cli/commands/cwd.js +9 -0
  14. package/dist/cli/commands/deploy.js +205 -0
  15. package/dist/cli/commands/emulator-orphans.js +440 -0
  16. package/dist/cli/commands/emulator.js +1829 -0
  17. package/dist/cli/commands/firebase-init.js +101 -0
  18. package/dist/cli/commands/firestore.js +261 -0
  19. package/dist/cli/commands/indexes.js +51 -0
  20. package/dist/cli/commands/install.js +151 -0
  21. package/dist/cli/commands/logs.js +358 -0
  22. package/dist/cli/commands/mcp.js +41 -0
  23. package/dist/cli/commands/migrate-markers.js +176 -0
  24. package/dist/cli/commands/migrate-rules.js +90 -0
  25. package/dist/cli/commands/migrate.js +59 -0
  26. package/dist/cli/commands/serve.js +208 -0
  27. package/dist/cli/commands/setup-tests/base-test.js +174 -0
  28. package/dist/cli/commands/setup-tests/emulator-config.js +109 -0
  29. package/dist/cli/commands/setup-tests/env-runtime-config-deprecated.js +47 -0
  30. package/dist/cli/commands/setup-tests/firebase-admin.js +49 -0
  31. package/dist/cli/commands/setup-tests/firebase-auth.js +26 -0
  32. package/dist/cli/commands/setup-tests/firebase-cli.js +26 -0
  33. package/dist/cli/commands/setup-tests/firebase-functions.js +49 -0
  34. package/dist/cli/commands/setup-tests/firestore-indexes-file.js +72 -0
  35. package/dist/cli/commands/setup-tests/firestore-indexes-in-json.js +20 -0
  36. package/dist/cli/commands/setup-tests/firestore-indexes-required.js +108 -0
  37. package/dist/cli/commands/setup-tests/firestore-indexes-synced.js +236 -0
  38. package/dist/cli/commands/setup-tests/firestore-rules-file.js +115 -0
  39. package/dist/cli/commands/setup-tests/firestore-rules-in-json.js +49 -0
  40. package/dist/cli/commands/setup-tests/functions-package.js +44 -0
  41. package/dist/cli/commands/setup-tests/gcloud-cli.js +26 -0
  42. package/dist/cli/commands/setup-tests/gitignore.js +122 -0
  43. package/dist/cli/commands/setup-tests/helpers/merge-line-files.js +28 -0
  44. package/dist/cli/commands/setup-tests/helpers/required-indexes.js +109 -0
  45. package/dist/cli/commands/setup-tests/helpers/seed-campaigns.js +246 -0
  46. package/dist/cli/commands/setup-tests/helpers.js +72 -0
  47. package/dist/cli/commands/setup-tests/hosting-folder.js +23 -0
  48. package/dist/cli/commands/setup-tests/hosting-rewrites.js +52 -0
  49. package/dist/cli/commands/setup-tests/index.js +111 -0
  50. package/dist/cli/commands/setup-tests/is-firebase-project.js +21 -0
  51. package/dist/cli/commands/setup-tests/java-installed.js +27 -0
  52. package/dist/cli/commands/setup-tests/legacy-tests-cleanup.js +43 -0
  53. package/dist/cli/commands/setup-tests/marketing-campaigns-seeded.js +230 -0
  54. package/dist/cli/commands/setup-tests/node-version.js +60 -0
  55. package/dist/cli/commands/setup-tests/npm-project-scripts.js +42 -0
  56. package/dist/cli/commands/setup-tests/nvmrc-version.js +36 -0
  57. package/dist/cli/commands/setup-tests/omega-backend.js +42 -0
  58. package/dist/cli/commands/setup-tests/omega-config.js +84 -0
  59. package/dist/cli/commands/setup-tests/project-directories.js +34 -0
  60. package/dist/cli/commands/setup-tests/project-id-consistency.js +201 -0
  61. package/dist/cli/commands/setup-tests/public-html-files.js +22 -0
  62. package/dist/cli/commands/setup-tests/realtime-rules-file.js +69 -0
  63. package/dist/cli/commands/setup-tests/realtime-rules-in-json.js +20 -0
  64. package/dist/cli/commands/setup-tests/remoteconfig-template-file.js +32 -0
  65. package/dist/cli/commands/setup-tests/remoteconfig-template-in-json.js +31 -0
  66. package/dist/cli/commands/setup-tests/service-account.js +86 -0
  67. package/dist/cli/commands/setup-tests/storage-lifecycle-policy.js +81 -0
  68. package/dist/cli/commands/setup-tests/storage-rules-file.js +32 -0
  69. package/dist/cli/commands/setup-tests/storage-rules-in-json.js +20 -0
  70. package/dist/cli/commands/stripe.js +14 -0
  71. package/dist/cli/commands/test-lanes/stripe-live.js +440 -0
  72. package/dist/cli/commands/test.js +976 -0
  73. package/dist/cli/commands/update.js +27 -0
  74. package/dist/cli/commands/version.js +10 -0
  75. package/dist/cli/commands/watch.js +196 -0
  76. package/dist/cli/flags.js +13 -0
  77. package/dist/cli/index.js +173 -0
  78. package/dist/cli/run.js +33 -0
  79. package/dist/cli/utils/attach-log-file.js +4 -0
  80. package/dist/cli/utils/compile-rules.js +1155 -0
  81. package/dist/cli/utils/ensure-target.js +314 -0
  82. package/dist/cli/utils/project-type.js +140 -0
  83. package/dist/cli/utils/public-files.js +49 -0
  84. package/dist/cli/utils/safe-install.js +4 -0
  85. package/dist/cli/utils/spawn-shell.js +94 -0
  86. package/dist/cli/utils/stage-functions.js +338 -0
  87. package/dist/cli/utils/stage-local-packages.js +210 -0
  88. package/dist/cli/utils/target-checks.js +161 -0
  89. package/dist/cli/utils/target.js +24 -0
  90. package/dist/cli/utils/ui.js +292 -0
  91. package/dist/defaults/AGENTS.md +125 -0
  92. package/dist/defaults/CHANGELOG.md +15 -0
  93. package/dist/defaults/CLAUDE.md +1 -0
  94. package/dist/defaults/_.gitignore +65 -0
  95. package/dist/defaults/docs/README.md +17 -0
  96. package/dist/defaults/test/README.md +65 -0
  97. package/dist/defaults/test/_init.js +14 -0
  98. package/dist/defaults/test/helpers/connect-trap.js +72 -0
  99. package/dist/defaults/test/unit/registration.test.js +143 -0
  100. package/dist/defaults/test/unit/rules-posture.test.js +101 -0
  101. package/dist/defaults/test/unit/socket-free.test.js +43 -0
  102. package/dist/manager/events/auth/before-create.js +89 -0
  103. package/dist/manager/events/auth/before-signin.js +76 -0
  104. package/dist/manager/events/auth/on-create.js +112 -0
  105. package/dist/manager/events/auth/on-delete.js +141 -0
  106. package/dist/manager/events/auth/utils.js +93 -0
  107. package/dist/manager/events/cron/daily/blog-auto-publisher.js +287 -0
  108. package/dist/manager/events/cron/daily/data-requests.js +57 -0
  109. package/dist/manager/events/cron/daily/expire-paypal-cancellations.js +191 -0
  110. package/dist/manager/events/cron/daily/marketing-prune.js +486 -0
  111. package/dist/manager/events/cron/daily/reset-usage.js +188 -0
  112. package/dist/manager/events/cron/daily/trial-lapse-sweep.js +355 -0
  113. package/dist/manager/events/cron/daily.js +3 -0
  114. package/dist/manager/events/cron/frequent/abandoned-carts.js +204 -0
  115. package/dist/manager/events/cron/frequent/email-queue.js +71 -0
  116. package/dist/manager/events/cron/frequent/marketing-campaigns.js +463 -0
  117. package/dist/manager/events/cron/frequent/retry-failed-webhooks.js +70 -0
  118. package/dist/manager/events/cron/frequent.js +3 -0
  119. package/dist/manager/events/cron/runner.js +64 -0
  120. package/dist/manager/events/firestore/notifications/on-write.js +123 -0
  121. package/dist/manager/events/firestore/payments-disputes/on-write.js +256 -0
  122. package/dist/manager/events/firestore/payments-disputes/providers/stripe.js +218 -0
  123. package/dist/manager/events/firestore/payments-disputes/providers/test.js +231 -0
  124. package/dist/manager/events/firestore/payments-webhooks/analytics.js +687 -0
  125. package/dist/manager/events/firestore/payments-webhooks/on-write.js +1224 -0
  126. package/dist/manager/events/firestore/payments-webhooks/transitions/index.js +395 -0
  127. package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +58 -0
  128. package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +28 -0
  129. package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-refunded.js +30 -0
  130. package/dist/manager/events/firestore/payments-webhooks/transitions/send-email.js +74 -0
  131. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-removed.js +14 -0
  132. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +28 -0
  133. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/checkout-declined.js +18 -0
  134. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +67 -0
  135. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +25 -0
  136. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +26 -0
  137. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-refunded.js +34 -0
  138. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +42 -0
  139. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +33 -0
  140. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-winback.js +17 -0
  141. package/dist/manager/functions/_legacy/actions/create-post-handler.js +188 -0
  142. package/dist/manager/functions/_legacy/actions/generate-uuid.js +63 -0
  143. package/dist/manager/functions/_legacy/actions/sign-up-handler.js +205 -0
  144. package/dist/manager/functions/_legacy/admin/create-post.js +207 -0
  145. package/dist/manager/functions/_legacy/admin/firestore-write.js +72 -0
  146. package/dist/manager/functions/_legacy/admin/get-stats.js +218 -0
  147. package/dist/manager/functions/_legacy/admin/query.js +198 -0
  148. package/dist/manager/functions/_legacy/admin/send-notification.js +206 -0
  149. package/dist/manager/functions/_legacy/template.js +33 -0
  150. package/dist/manager/functions/_legacy/test/authenticate.js +40 -0
  151. package/dist/manager/functions/_legacy/test/webhook.js +37 -0
  152. package/dist/manager/functions/wrappers/mailchimp/addToList.js +25 -0
  153. package/dist/manager/helpers/analytics.js +498 -0
  154. package/dist/manager/helpers/api-manager.js +313 -0
  155. package/dist/manager/helpers/backend-router.js +37 -0
  156. package/dist/manager/helpers/context/authenticate.js +215 -0
  157. package/dist/manager/helpers/context/client-info.js +91 -0
  158. package/dist/manager/helpers/context/index.js +228 -0
  159. package/dist/manager/helpers/context/logging.js +150 -0
  160. package/dist/manager/helpers/context/parse.js +160 -0
  161. package/dist/manager/helpers/context/respond.js +247 -0
  162. package/dist/manager/helpers/event-middleware.js +109 -0
  163. package/dist/manager/helpers/metadata.js +32 -0
  164. package/dist/manager/helpers/middleware.js +541 -0
  165. package/dist/manager/helpers/redact-secret.js +25 -0
  166. package/dist/manager/helpers/resolved-config.js +34 -0
  167. package/dist/manager/helpers/roles.js +69 -0
  168. package/dist/manager/helpers/safe-compare.js +31 -0
  169. package/dist/manager/helpers/schema-engine.js +314 -0
  170. package/dist/manager/helpers/schema-zod.js +203 -0
  171. package/dist/manager/helpers/settings.js +215 -0
  172. package/dist/manager/helpers/usage.js +541 -0
  173. package/dist/manager/helpers/user.js +57 -0
  174. package/dist/manager/helpers/utilities.js +565 -0
  175. package/dist/manager/index.js +1404 -0
  176. package/dist/manager/libraries/abandoned-cart-config.js +12 -0
  177. package/dist/manager/libraries/ai/index.js +321 -0
  178. package/dist/manager/libraries/ai/prompt.js +85 -0
  179. package/dist/manager/libraries/ai/providers/anthropic-format.js +297 -0
  180. package/dist/manager/libraries/ai/providers/anthropic.js +169 -0
  181. package/dist/manager/libraries/ai/providers/claude-code.js +183 -0
  182. package/dist/manager/libraries/ai/providers/openai.js +1272 -0
  183. package/dist/manager/libraries/ai/providers/test.js +257 -0
  184. package/dist/manager/libraries/ai/tokens.js +54 -0
  185. package/dist/manager/libraries/analytics/conversions.js +478 -0
  186. package/dist/manager/libraries/analytics/match-data.js +573 -0
  187. package/dist/manager/libraries/analytics/signup.js +120 -0
  188. package/dist/manager/libraries/auth-user.js +46 -0
  189. package/dist/manager/libraries/content/feed-parser.js +198 -0
  190. package/dist/manager/libraries/content/ghostii.js +191 -0
  191. package/dist/manager/libraries/content/source-resolver.js +629 -0
  192. package/dist/manager/libraries/email/constants.js +532 -0
  193. package/dist/manager/libraries/email/data/blocked-local-parts.json +55 -0
  194. package/dist/manager/libraries/email/data/blocked-local-patterns.js +14 -0
  195. package/dist/manager/libraries/email/data/corporate-domains.json +23 -0
  196. package/dist/manager/libraries/email/data/custom-disposable-domains.json +58 -0
  197. package/dist/manager/libraries/email/data/disposable-domains.json +8184 -0
  198. package/dist/manager/libraries/email/data/typo-domains.js +83 -0
  199. package/dist/manager/libraries/email/disposable-domains.js +92 -0
  200. package/dist/manager/libraries/email/generators/lib/filter.js +179 -0
  201. package/dist/manager/libraries/email/generators/lib/image-host.js +344 -0
  202. package/dist/manager/libraries/email/generators/lib/image-illustrator.js +154 -0
  203. package/dist/manager/libraries/email/generators/lib/markdown-renderer.js +294 -0
  204. package/dist/manager/libraries/email/generators/lib/mjml-template.js +120 -0
  205. package/dist/manager/libraries/email/generators/lib/structure.js +299 -0
  206. package/dist/manager/libraries/email/generators/lib/svg-illustrator.js +197 -0
  207. package/dist/manager/libraries/email/generators/lib/templates/base.js +229 -0
  208. package/dist/manager/libraries/email/generators/lib/templates/card.js +33 -0
  209. package/dist/manager/libraries/email/generators/lib/templates/classic-schema.js +59 -0
  210. package/dist/manager/libraries/email/generators/lib/templates/clean.js +82 -0
  211. package/dist/manager/libraries/email/generators/lib/templates/editorial/helpers.js +100 -0
  212. package/dist/manager/libraries/email/generators/lib/templates/editorial/index.js +318 -0
  213. package/dist/manager/libraries/email/generators/lib/templates/feedback.js +101 -0
  214. package/dist/manager/libraries/email/generators/lib/templates/field-report/helpers.js +138 -0
  215. package/dist/manager/libraries/email/generators/lib/templates/field-report/index.js +494 -0
  216. package/dist/manager/libraries/email/generators/lib/templates/index.js +47 -0
  217. package/dist/manager/libraries/email/generators/lib/templates/newsletter-shared.js +610 -0
  218. package/dist/manager/libraries/email/generators/lib/templates/order.js +461 -0
  219. package/dist/manager/libraries/email/generators/lib/templates/plain.js +83 -0
  220. package/dist/manager/libraries/email/generators/lib/templates/shared-campaign.js +64 -0
  221. package/dist/manager/libraries/email/generators/newsletter.js +899 -0
  222. package/dist/manager/libraries/email/index.js +146 -0
  223. package/dist/manager/libraries/email/marketing/index.js +754 -0
  224. package/dist/manager/libraries/email/prepare.js +365 -0
  225. package/dist/manager/libraries/email/providers/beehiiv.js +589 -0
  226. package/dist/manager/libraries/email/providers/sendgrid.js +845 -0
  227. package/dist/manager/libraries/email/transactional/index.js +510 -0
  228. package/dist/manager/libraries/email/utm.js +86 -0
  229. package/dist/manager/libraries/email/validation-provider-neverbounce.js +70 -0
  230. package/dist/manager/libraries/email/validation-provider-zerobounce.js +42 -0
  231. package/dist/manager/libraries/email/validation.js +255 -0
  232. package/dist/manager/libraries/env.js +198 -0
  233. package/dist/manager/libraries/infer-contact.js +101 -0
  234. package/dist/manager/libraries/load-provider.js +65 -0
  235. package/dist/manager/libraries/notification.js +256 -0
  236. package/dist/manager/libraries/openai.js +13 -0
  237. package/dist/manager/libraries/payment/discount-codes.js +204 -0
  238. package/dist/manager/libraries/payment/fetch-failure.js +56 -0
  239. package/dist/manager/libraries/payment/license.js +44 -0
  240. package/dist/manager/libraries/payment/order-id.js +57 -0
  241. package/dist/manager/libraries/payment/provider-errors.js +71 -0
  242. package/dist/manager/libraries/payment/providers/chargebee.js +817 -0
  243. package/dist/manager/libraries/payment/providers/coinbase.js +330 -0
  244. package/dist/manager/libraries/payment/providers/paypal.js +987 -0
  245. package/dist/manager/libraries/payment/providers/stripe.js +755 -0
  246. package/dist/manager/libraries/payment/providers/test.js +250 -0
  247. package/dist/manager/libraries/payment/refund-linkage.js +78 -0
  248. package/dist/manager/libraries/payment/refund-policy.js +113 -0
  249. package/dist/manager/libraries/payment/winback.js +55 -0
  250. package/dist/manager/libraries/prompts/infer-contact.md +78 -0
  251. package/dist/manager/libraries/rate-limits.js +17 -0
  252. package/dist/manager/libraries/recaptcha.js +58 -0
  253. package/dist/manager/libraries/user-doc.js +234 -0
  254. package/dist/manager/routes/admin/backup/post.js +106 -0
  255. package/dist/manager/routes/admin/cron/post.js +37 -0
  256. package/dist/manager/routes/admin/database/get.js +35 -0
  257. package/dist/manager/routes/admin/database/post.js +34 -0
  258. package/dist/manager/routes/admin/email/post.js +74 -0
  259. package/dist/manager/routes/admin/firestore/get.js +35 -0
  260. package/dist/manager/routes/admin/firestore/post.js +56 -0
  261. package/dist/manager/routes/admin/firestore/query/post.js +128 -0
  262. package/dist/manager/routes/admin/hook/post.js +104 -0
  263. package/dist/manager/routes/admin/infer-contact/post.js +35 -0
  264. package/dist/manager/routes/admin/notification/post.js +37 -0
  265. package/dist/manager/routes/admin/payment/post.js +56 -0
  266. package/dist/manager/routes/admin/post/deduplicate-image-alts.js +52 -0
  267. package/dist/manager/routes/admin/post/dispatch-deploy.js +36 -0
  268. package/dist/manager/routes/admin/post/post.js +487 -0
  269. package/dist/manager/routes/admin/post/put.js +146 -0
  270. package/dist/manager/routes/admin/post/templates/post.html +14 -0
  271. package/dist/manager/routes/admin/repo/content/post.js +106 -0
  272. package/dist/manager/routes/admin/stats/get.js +222 -0
  273. package/dist/manager/routes/admin/users/disable/post.js +50 -0
  274. package/dist/manager/routes/admin/users/list/get.js +110 -0
  275. package/dist/manager/routes/admin/users/sync/post.js +119 -0
  276. package/dist/manager/routes/brand/get.js +26 -0
  277. package/dist/manager/routes/content/post/get.js +110 -0
  278. package/dist/manager/routes/general/email/post.js +89 -0
  279. package/dist/manager/routes/general/email/templates/general/download-app-link.js +42 -0
  280. package/dist/manager/routes/general/uuid/post.js +31 -0
  281. package/dist/manager/routes/handler/post/post.js +142 -0
  282. package/dist/manager/routes/health/get.js +43 -0
  283. package/dist/manager/routes/index.js +11 -0
  284. package/dist/manager/routes/marketing/campaign/delete.js +45 -0
  285. package/dist/manager/routes/marketing/campaign/get.js +69 -0
  286. package/dist/manager/routes/marketing/campaign/post.js +124 -0
  287. package/dist/manager/routes/marketing/campaign/put.js +86 -0
  288. package/dist/manager/routes/marketing/campaign/utils.js +59 -0
  289. package/dist/manager/routes/marketing/contact/delete.js +100 -0
  290. package/dist/manager/routes/marketing/contact/post.js +149 -0
  291. package/dist/manager/routes/marketing/contact/put.js +37 -0
  292. package/dist/manager/routes/marketing/email-preferences/post.js +275 -0
  293. package/dist/manager/routes/marketing/webhook/forward/post.js +170 -0
  294. package/dist/manager/routes/marketing/webhook/post.js +123 -0
  295. package/dist/manager/routes/marketing/webhook/providers/beehiiv.js +197 -0
  296. package/dist/manager/routes/marketing/webhook/providers/sendgrid.js +194 -0
  297. package/dist/manager/routes/payments/cancel/_is-trialing.js +40 -0
  298. package/dist/manager/routes/payments/cancel/post.js +149 -0
  299. package/dist/manager/routes/payments/cancel/providers/chargebee.js +41 -0
  300. package/dist/manager/routes/payments/cancel/providers/paypal.js +56 -0
  301. package/dist/manager/routes/payments/cancel/providers/stripe.js +33 -0
  302. package/dist/manager/routes/payments/cancel/providers/test.js +118 -0
  303. package/dist/manager/routes/payments/discount/get.js +25 -0
  304. package/dist/manager/routes/payments/dispute-alert/post.js +117 -0
  305. package/dist/manager/routes/payments/dispute-alert/providers/chargeblast.js +55 -0
  306. package/dist/manager/routes/payments/intent/post.js +341 -0
  307. package/dist/manager/routes/payments/intent/providers/chargebee.js +197 -0
  308. package/dist/manager/routes/payments/intent/providers/coinbase.js +94 -0
  309. package/dist/manager/routes/payments/intent/providers/paypal.js +244 -0
  310. package/dist/manager/routes/payments/intent/providers/stripe.js +141 -0
  311. package/dist/manager/routes/payments/intent/providers/test.js +341 -0
  312. package/dist/manager/routes/payments/plan/post.js +147 -0
  313. package/dist/manager/routes/payments/plan/providers/chargebee.js +62 -0
  314. package/dist/manager/routes/payments/plan/providers/paypal.js +62 -0
  315. package/dist/manager/routes/payments/plan/providers/stripe.js +59 -0
  316. package/dist/manager/routes/payments/plan/providers/test.js +113 -0
  317. package/dist/manager/routes/payments/portal/post.js +113 -0
  318. package/dist/manager/routes/payments/portal/providers/chargebee.js +62 -0
  319. package/dist/manager/routes/payments/portal/providers/paypal.js +25 -0
  320. package/dist/manager/routes/payments/portal/providers/stripe.js +62 -0
  321. package/dist/manager/routes/payments/portal/providers/test.js +18 -0
  322. package/dist/manager/routes/payments/refund/post.js +207 -0
  323. package/dist/manager/routes/payments/refund/providers/chargebee.js +147 -0
  324. package/dist/manager/routes/payments/refund/providers/coinbase.js +40 -0
  325. package/dist/manager/routes/payments/refund/providers/paypal.js +239 -0
  326. package/dist/manager/routes/payments/refund/providers/stripe.js +160 -0
  327. package/dist/manager/routes/payments/refund/providers/test.js +204 -0
  328. package/dist/manager/routes/payments/trial-eligibility/get.js +29 -0
  329. package/dist/manager/routes/payments/uncancel/post.js +106 -0
  330. package/dist/manager/routes/payments/uncancel/providers/chargebee.js +30 -0
  331. package/dist/manager/routes/payments/uncancel/providers/paypal.js +25 -0
  332. package/dist/manager/routes/payments/uncancel/providers/stripe.js +27 -0
  333. package/dist/manager/routes/payments/uncancel/providers/test.js +114 -0
  334. package/dist/manager/routes/payments/webhook/post.js +157 -0
  335. package/dist/manager/routes/payments/webhook/providers/chargebee.js +218 -0
  336. package/dist/manager/routes/payments/webhook/providers/coinbase.js +91 -0
  337. package/dist/manager/routes/payments/webhook/providers/paypal.js +214 -0
  338. package/dist/manager/routes/payments/webhook/providers/stripe.js +182 -0
  339. package/dist/manager/routes/payments/webhook/providers/test.js +18 -0
  340. package/dist/manager/routes/payments/winback/post.js +276 -0
  341. package/dist/manager/routes/payments/winback/providers/chargebee.js +24 -0
  342. package/dist/manager/routes/payments/winback/providers/paypal.js +22 -0
  343. package/dist/manager/routes/payments/winback/providers/stripe.js +41 -0
  344. package/dist/manager/routes/payments/winback/providers/test.js +42 -0
  345. package/dist/manager/routes/restart/index.js +37 -0
  346. package/dist/manager/routes/special/electron-client/post.js +59 -0
  347. package/dist/manager/routes/test/authenticate/get.js +11 -0
  348. package/dist/manager/routes/test/health/get.js +8 -0
  349. package/dist/manager/routes/test/index.js +16 -0
  350. package/dist/manager/routes/test/lab/post.js +17 -0
  351. package/dist/manager/routes/test/redirect/get.js +27 -0
  352. package/dist/manager/routes/test/reset-account/post.js +79 -0
  353. package/dist/manager/routes/test/roster/get.js +40 -0
  354. package/dist/manager/routes/test/schema/post.js +20 -0
  355. package/dist/manager/routes/test/usage/post.js +44 -0
  356. package/dist/manager/routes/test/webhook/post.js +21 -0
  357. package/dist/manager/routes/user/api-keys/post.js +51 -0
  358. package/dist/manager/routes/user/connections/_context.js +194 -0
  359. package/dist/manager/routes/user/connections/_grant.js +247 -0
  360. package/dist/manager/routes/user/connections/_lease.js +184 -0
  361. package/dist/manager/routes/user/connections/_providers.js +191 -0
  362. package/dist/manager/routes/user/connections/_state.js +117 -0
  363. package/dist/manager/routes/user/connections/delete.js +55 -0
  364. package/dist/manager/routes/user/connections/get.js +167 -0
  365. package/dist/manager/routes/user/connections/post.js +370 -0
  366. package/dist/manager/routes/user/connections/providers/discord.js +41 -0
  367. package/dist/manager/routes/user/connections/providers/google.js +55 -0
  368. package/dist/manager/routes/user/connections/providers/kick.js +68 -0
  369. package/dist/manager/routes/user/connections/providers/spotify.js +40 -0
  370. package/dist/manager/routes/user/connections/providers/twitch.js +52 -0
  371. package/dist/manager/routes/user/data-request/delete.js +84 -0
  372. package/dist/manager/routes/user/data-request/get.js +229 -0
  373. package/dist/manager/routes/user/data-request/post.js +136 -0
  374. package/dist/manager/routes/user/delete.js +149 -0
  375. package/dist/manager/routes/user/feedback/post.js +81 -0
  376. package/dist/manager/routes/user/get.js +14 -0
  377. package/dist/manager/routes/user/orders/get.js +95 -0
  378. package/dist/manager/routes/user/sessions/delete.js +95 -0
  379. package/dist/manager/routes/user/sessions/get.js +40 -0
  380. package/dist/manager/routes/user/settings/validate/post.js +86 -0
  381. package/dist/manager/routes/user/signup/post.js +662 -0
  382. package/dist/manager/routes/user/subscription/get.js +77 -0
  383. package/dist/manager/routes/user/token/post.js +30 -0
  384. package/dist/manager/routes/verts/delete.js +43 -0
  385. package/dist/manager/routes/verts/get.js +49 -0
  386. package/dist/manager/routes/verts/post.js +61 -0
  387. package/dist/manager/routes/verts/put.js +75 -0
  388. package/dist/manager/routes/verts/redirect/get.js +36 -0
  389. package/dist/manager/routes/verts/serve/get.js +67 -0
  390. package/dist/manager/routes/verts/utils.js +436 -0
  391. package/dist/manager/schemas/admin/backup/post.js +8 -0
  392. package/dist/manager/schemas/admin/cron/post.js +8 -0
  393. package/dist/manager/schemas/admin/database/get.js +5 -0
  394. package/dist/manager/schemas/admin/database/post.js +6 -0
  395. package/dist/manager/schemas/admin/email/post.js +31 -0
  396. package/dist/manager/schemas/admin/firestore/get.js +5 -0
  397. package/dist/manager/schemas/admin/firestore/post.js +8 -0
  398. package/dist/manager/schemas/admin/firestore/query/post.js +5 -0
  399. package/dist/manager/schemas/admin/hook/post.js +8 -0
  400. package/dist/manager/schemas/admin/infer-contact/post.js +6 -0
  401. package/dist/manager/schemas/admin/notification/post.js +23 -0
  402. package/dist/manager/schemas/admin/payment/post.js +8 -0
  403. package/dist/manager/schemas/admin/post/post.js +23 -0
  404. package/dist/manager/schemas/admin/post/put.js +13 -0
  405. package/dist/manager/schemas/admin/repo/content/post.js +10 -0
  406. package/dist/manager/schemas/admin/stats/get.js +5 -0
  407. package/dist/manager/schemas/admin/users/disable/post.js +9 -0
  408. package/dist/manager/schemas/admin/users/list/get.js +10 -0
  409. package/dist/manager/schemas/admin/users/sync/post.js +7 -0
  410. package/dist/manager/schemas/brand/get.js +3 -0
  411. package/dist/manager/schemas/content/post/get.js +8 -0
  412. package/dist/manager/schemas/general/email/post.js +10 -0
  413. package/dist/manager/schemas/general/uuid/post.js +9 -0
  414. package/dist/manager/schemas/handler/post/post.js +13 -0
  415. package/dist/manager/schemas/health/get.js +3 -0
  416. package/dist/manager/schemas/marketing/campaign/delete.js +8 -0
  417. package/dist/manager/schemas/marketing/campaign/get.js +13 -0
  418. package/dist/manager/schemas/marketing/campaign/post.js +43 -0
  419. package/dist/manager/schemas/marketing/campaign/put.js +37 -0
  420. package/dist/manager/schemas/marketing/contact/delete.js +8 -0
  421. package/dist/manager/schemas/marketing/contact/post.js +14 -0
  422. package/dist/manager/schemas/marketing/contact/put.js +8 -0
  423. package/dist/manager/schemas/marketing/email-preferences/post.js +15 -0
  424. package/dist/manager/schemas/marketing/webhook/forward/post.js +8 -0
  425. package/dist/manager/schemas/marketing/webhook/post.js +7 -0
  426. package/dist/manager/schemas/payments/cancel/post.js +15 -0
  427. package/dist/manager/schemas/payments/discount/get.js +8 -0
  428. package/dist/manager/schemas/payments/dispute-alert/post.js +6 -0
  429. package/dist/manager/schemas/payments/intent/post.js +28 -0
  430. package/dist/manager/schemas/payments/plan/post.js +11 -0
  431. package/dist/manager/schemas/payments/portal/post.js +12 -0
  432. package/dist/manager/schemas/payments/refund/post.js +14 -0
  433. package/dist/manager/schemas/payments/trial-eligibility/get.js +7 -0
  434. package/dist/manager/schemas/payments/uncancel/post.js +9 -0
  435. package/dist/manager/schemas/payments/webhook/post.js +6 -0
  436. package/dist/manager/schemas/payments/winback/post.js +12 -0
  437. package/dist/manager/schemas/restart/index.js +5 -0
  438. package/dist/manager/schemas/special/electron-client/post.js +11 -0
  439. package/dist/manager/schemas/test/authenticate/get.js +3 -0
  440. package/dist/manager/schemas/test/health/get.js +3 -0
  441. package/dist/manager/schemas/test/index.js +5 -0
  442. package/dist/manager/schemas/test/lab/post.js +3 -0
  443. package/dist/manager/schemas/test/redirect/get.js +7 -0
  444. package/dist/manager/schemas/test/reset-account/post.js +3 -0
  445. package/dist/manager/schemas/test/roster/get.js +3 -0
  446. package/dist/manager/schemas/test/schema/post.js +70 -0
  447. package/dist/manager/schemas/test/usage/post.js +8 -0
  448. package/dist/manager/schemas/test/webhook/post.js +7 -0
  449. package/dist/manager/schemas/user/api-keys/post.js +6 -0
  450. package/dist/manager/schemas/user/connections/delete.js +8 -0
  451. package/dist/manager/schemas/user/connections/get.js +21 -0
  452. package/dist/manager/schemas/user/connections/post.js +12 -0
  453. package/dist/manager/schemas/user/data-request/delete.js +3 -0
  454. package/dist/manager/schemas/user/data-request/get.js +5 -0
  455. package/dist/manager/schemas/user/data-request/post.js +6 -0
  456. package/dist/manager/schemas/user/delete.js +6 -0
  457. package/dist/manager/schemas/user/feedback/post.js +8 -0
  458. package/dist/manager/schemas/user/get.js +3 -0
  459. package/dist/manager/schemas/user/orders/get.js +6 -0
  460. package/dist/manager/schemas/user/sessions/delete.js +6 -0
  461. package/dist/manager/schemas/user/sessions/get.js +6 -0
  462. package/dist/manager/schemas/user/settings/validate/post.js +8 -0
  463. package/dist/manager/schemas/user/signup/post.js +27 -0
  464. package/dist/manager/schemas/user/subscription/get.js +5 -0
  465. package/dist/manager/schemas/user/token/post.js +5 -0
  466. package/dist/manager/schemas/verts/delete.js +8 -0
  467. package/dist/manager/schemas/verts/get.js +9 -0
  468. package/dist/manager/schemas/verts/post.js +23 -0
  469. package/dist/manager/schemas/verts/put.js +20 -0
  470. package/dist/manager/schemas/verts/redirect/get.js +10 -0
  471. package/dist/manager/schemas/verts/serve/get.js +14 -0
  472. package/dist/manager/server-manager.js +69 -0
  473. package/dist/mcp/client.js +98 -0
  474. package/dist/mcp/handler.js +521 -0
  475. package/dist/mcp/index.js +157 -0
  476. package/dist/mcp/tools.js +555 -0
  477. package/dist/mcp/utils.js +108 -0
  478. package/dist/omega-bin.js +6 -0
  479. package/dist/require.js +3 -0
  480. package/dist/test/fixtures/firebase-project/.firebaserc +5 -0
  481. package/dist/test/fixtures/firebase-project/config/omega.json5 +131 -0
  482. package/dist/test/fixtures/firebase-project/database.rules.json +6 -0
  483. package/dist/test/fixtures/firebase-project/firebase.json +49 -0
  484. package/dist/test/fixtures/firebase-project/firestore.indexes.json +4 -0
  485. package/dist/test/fixtures/firebase-project/package.json +14 -0
  486. package/dist/test/fixtures/firebase-project/src/index.js +11 -0
  487. package/dist/test/fixtures/firebase-project/storage.rules +8 -0
  488. package/dist/test/parse-audit.js +25 -0
  489. package/dist/test/run-tests.js +90 -0
  490. package/dist/test/runner.js +982 -0
  491. package/dist/test/seed.js +242 -0
  492. package/dist/test/test-accounts.js +2660 -0
  493. package/dist/test/utils/assertions.js +189 -0
  494. package/dist/test/utils/email-capture.js +252 -0
  495. package/dist/test/utils/extended-mode-warning.js +11 -0
  496. package/dist/test/utils/firestore-rules-client.js +277 -0
  497. package/dist/test/utils/http-client.js +228 -0
  498. package/dist/test/utils/test-mode-file.js +192 -0
  499. package/dist/utils/merge-line-files.js +6 -0
  500. package/dist/utils/scaffold-defaults.js +71 -0
  501. package/dist/utils/test-lanes.js +62 -0
  502. package/dist/vendor/account/engine.js +182 -0
  503. package/dist/vendor/account/features.js +220 -0
  504. package/dist/vendor/account/index.js +53 -0
  505. package/dist/vendor/account/schema.js +272 -0
  506. package/dist/vendor/account/subscription.js +38 -0
  507. package/dist/vendor/analytics/adapters/ga4.js +26 -0
  508. package/dist/vendor/analytics/adapters/meta.js +26 -0
  509. package/dist/vendor/analytics/adapters/resolve.js +130 -0
  510. package/dist/vendor/analytics/adapters/tiktok.js +27 -0
  511. package/dist/vendor/analytics/catalog.js +908 -0
  512. package/dist/vendor/analytics/identity.js +136 -0
  513. package/dist/vendor/config/company.js +31 -0
  514. package/dist/vendor/config/defaults.js +173 -0
  515. package/dist/vendor/config/demo.js +18 -0
  516. package/dist/vendor/config/desktop-artifacts.js +110 -0
  517. package/dist/vendor/config/edit.js +769 -0
  518. package/dist/vendor/config/env-delivery.js +145 -0
  519. package/dist/vendor/config/env-rules.js +93 -0
  520. package/dist/vendor/config/env-schema.js +1078 -0
  521. package/dist/vendor/config/env.js +445 -0
  522. package/dist/vendor/config/hooks.js +97 -0
  523. package/dist/vendor/config/index.js +237 -0
  524. package/dist/vendor/config/instances.js +208 -0
  525. package/dist/vendor/config/load.js +490 -0
  526. package/dist/vendor/config/merge.js +68 -0
  527. package/dist/vendor/config/order.js +139 -0
  528. package/dist/vendor/config/ports.js +374 -0
  529. package/dist/vendor/config/providers.js +32 -0
  530. package/dist/vendor/config/repo.js +142 -0
  531. package/dist/vendor/config/retired-keys.js +430 -0
  532. package/dist/vendor/config/schema.js +1610 -0
  533. package/dist/vendor/config/secrets.js +50 -0
  534. package/dist/vendor/config/seed.js +34 -0
  535. package/dist/vendor/config/site-global.js +205 -0
  536. package/dist/vendor/config/validate.js +554 -0
  537. package/dist/vendor/config/winback.js +61 -0
  538. package/dist/vendor/devkit/attach-log-file.js +262 -0
  539. package/dist/vendor/devkit/bare-requires.js +153 -0
  540. package/dist/vendor/devkit/command-path.js +46 -0
  541. package/dist/vendor/devkit/defaults-engine.js +372 -0
  542. package/dist/vendor/devkit/deploy-record.js +180 -0
  543. package/dist/vendor/devkit/deploy.js +286 -0
  544. package/dist/vendor/devkit/license.js +155 -0
  545. package/dist/vendor/devkit/local-https.js +360 -0
  546. package/dist/vendor/devkit/local.js +1905 -0
  547. package/dist/vendor/devkit/logger.js +128 -0
  548. package/dist/vendor/devkit/merge-line-files.js +296 -0
  549. package/dist/vendor/devkit/npm-registry.js +52 -0
  550. package/dist/vendor/devkit/omega-bin.js +345 -0
  551. package/dist/vendor/devkit/parse-audit.js +74 -0
  552. package/dist/vendor/devkit/safe-install.js +18 -0
  553. package/dist/vendor/devkit/scaffold-guard.js +96 -0
  554. package/dist/vendor/devkit/stop-signals.js +28 -0
  555. package/dist/vendor/devkit/test/assert.js +120 -0
  556. package/dist/vendor/devkit/test/define-cases.js +104 -0
  557. package/dist/vendor/devkit/test/runner-core.js +554 -0
  558. package/dist/vendor/devkit/test/scope.js +162 -0
  559. package/dist/vendor/devkit/update.js +569 -0
  560. package/dist/vendor/monitoring/core.js +180 -0
  561. package/dist/vendor/monitoring/env.js +49 -0
  562. package/dist/vendor/monitoring/logger.js +39 -0
  563. package/dist/vendor/monitoring/node.js +72 -0
  564. package/docs/admin-post-route.md +57 -0
  565. package/docs/ai-library.md +171 -0
  566. package/docs/architecture.md +62 -0
  567. package/docs/audit.md +70 -0
  568. package/docs/auth-hooks.md +74 -0
  569. package/docs/build-system.md +65 -0
  570. package/docs/cdp-debugging.md +29 -0
  571. package/docs/cli-firestore-auth.md +85 -0
  572. package/docs/cli-logs.md +67 -0
  573. package/docs/cli-output.md +146 -0
  574. package/docs/code-patterns.md +77 -0
  575. package/docs/common-mistakes.md +12 -0
  576. package/docs/common-operations.md +60 -0
  577. package/docs/connections.md +212 -0
  578. package/docs/consent.md +362 -0
  579. package/docs/directory-structure.md +150 -0
  580. package/docs/email-system.md +459 -0
  581. package/docs/environment-detection.md +93 -0
  582. package/docs/file-naming.md +10 -0
  583. package/docs/firestore.md +134 -0
  584. package/docs/ghostii.md +240 -0
  585. package/docs/index.md +299 -0
  586. package/docs/key-files.md +37 -0
  587. package/docs/logging.md +60 -0
  588. package/docs/marketing-campaigns.md +407 -0
  589. package/docs/marketing-fields.md +25 -0
  590. package/docs/mcp.md +222 -0
  591. package/docs/migration.md +129 -0
  592. package/docs/payment-system.md +856 -0
  593. package/docs/paypal-sandbox-qa.md +105 -0
  594. package/docs/response-headers.md +9 -0
  595. package/docs/routes.md +211 -0
  596. package/docs/sanitization.md +71 -0
  597. package/docs/schemas.md +163 -0
  598. package/docs/shared/agent-docs.md +89 -0
  599. package/docs/shared/analytics.md +612 -0
  600. package/docs/shared/brands.md +51 -0
  601. package/docs/shared/breaking-changes.md +497 -0
  602. package/docs/shared/config.md +1387 -0
  603. package/docs/shared/deploys.md +215 -0
  604. package/docs/shared/icons.md +201 -0
  605. package/docs/shared/local-dev.md +147 -0
  606. package/docs/shared/logging.md +202 -0
  607. package/docs/shared/monitoring.md +153 -0
  608. package/docs/shared/publishing.md +183 -0
  609. package/docs/shared/rulings.md +34 -0
  610. package/docs/shared/testing.md +147 -0
  611. package/docs/shared/theming.md +604 -0
  612. package/docs/shared/translation.md +291 -0
  613. package/docs/shared/updates.md +61 -0
  614. package/docs/stripe-webhook-forwarding.md +20 -0
  615. package/docs/test-boot-layer.md +67 -0
  616. package/docs/test-framework.md +583 -0
  617. package/docs/usage-rate-limiting.md +121 -0
  618. package/docs/verts.md +29 -0
  619. package/package.json +143 -0
  620. package/templates/config/omega.json5 +322 -0
  621. package/templates/database.rules.json +82 -0
  622. package/templates/firebase.json +67 -0
  623. package/templates/firestore.framework.rules +191 -0
  624. package/templates/firestore.indexes.json +4 -0
  625. package/templates/firestore.rules +59 -0
  626. package/templates/index.js +12 -0
  627. package/templates/public/404.html +26 -0
  628. package/templates/public/index.html +24 -0
  629. package/templates/remoteconfig.template.json +1 -0
  630. package/templates/storage-lifecycle-config-1-day.json +9 -0
  631. package/templates/storage-lifecycle-config-30-days.json +9 -0
  632. package/templates/storage.rules +11 -0
  633. package/test/_init/accounts-validation.js +58 -0
  634. package/test/ai/tools-live.test.js +171 -0
  635. package/test/analytics/conversion-delivery.test.js +1013 -0
  636. package/test/analytics/match-normalization.test.js +238 -0
  637. package/test/analytics/signup-conversion.test.js +135 -0
  638. package/test/boot/cli-dispatch.test.js +142 -0
  639. package/test/boot/defaults-scaffold.test.js +248 -0
  640. package/test/boot/deploy-staging.test.js +446 -0
  641. package/test/boot/disposable-domains.test.js +154 -0
  642. package/test/boot/emulator-boots.test.js +39 -0
  643. package/test/boot/emulator-port-preflight.test.js +126 -0
  644. package/test/boot/emulator-ready-timeout.test.js +46 -0
  645. package/test/boot/project-id-refresh.test.js +73 -0
  646. package/test/boot/suite-portability.test.js +278 -0
  647. package/test/boot/update-command.test.js +63 -0
  648. package/test/cli/cross-platform.test.js +168 -0
  649. package/test/cli/custom-project-type.test.js +426 -0
  650. package/test/cli/deploy-license-env.test.js +100 -0
  651. package/test/cli/emulator-adoption.test.js +254 -0
  652. package/test/cli/emulator-orphans.test.js +658 -0
  653. package/test/cli/emulator-ownership.test.js +177 -0
  654. package/test/cli/emulator-port-retry.test.js +395 -0
  655. package/test/cli/emulator-shutdown.test.js +788 -0
  656. package/test/cli/emulator-stale-reap.test.js +574 -0
  657. package/test/cli/ensure-target.test.js +325 -0
  658. package/test/cli/flags.test.js +64 -0
  659. package/test/cli/https-trust.test.js +38 -0
  660. package/test/cli/install-update-dispatch.test.js +47 -0
  661. package/test/cli/lane-environments.test.js +159 -0
  662. package/test/cli/marketing-campaigns-seeded.test.js +194 -0
  663. package/test/cli/migrate-bare-requires.test.js +128 -0
  664. package/test/cli/migrate-markers.test.js +530 -0
  665. package/test/cli/required-indexes.test.js +64 -0
  666. package/test/cli/rules-compile.test.js +622 -0
  667. package/test/cli/rules-migration-deferral.test.js +259 -0
  668. package/test/cli/rules-version.test.js +104 -0
  669. package/test/cli/setup-load-files.test.js +66 -0
  670. package/test/cli/setup-offline-mode.test.js +276 -0
  671. package/test/cli/setup-retired.test.js +64 -0
  672. package/test/cli/setup-shared-project-indexes.test.js +262 -0
  673. package/test/cli/setup-tests-requires.test.js +45 -0
  674. package/test/cli/stage-env-compose.test.js +316 -0
  675. package/test/cli/stripe-live-lane.test.js +303 -0
  676. package/test/cli/target.test.js +182 -0
  677. package/test/cli/templates.test.js +66 -0
  678. package/test/cli/test-runner-env.test.js +115 -0
  679. package/test/cli/test-stack-shutdown.test.js +275 -0
  680. package/test/cli/test-target-no-match.test.js +128 -0
  681. package/test/cli/verb-logs.test.js +118 -0
  682. package/test/cli/version-dispatch.test.js +39 -0
  683. package/test/content/blog-generate.test.js +164 -0
  684. package/test/email/campaign-config-fault.test.js +376 -0
  685. package/test/email/campaign-cron-pipeline.test.js +536 -0
  686. package/test/email/campaign-send.test.js +47 -0
  687. package/test/email/consent-lifecycle.test.js +258 -0
  688. package/test/email/content-html-policy.test.js +270 -0
  689. package/test/email/feedback-and-plain-send.test.js +54 -0
  690. package/test/email/fixtures/clean.json +30 -0
  691. package/test/email/fixtures/editorial.json +30 -0
  692. package/test/email/fixtures/field-report.json +53 -0
  693. package/test/email/identity.test.js +321 -0
  694. package/test/email/marketing/consent-gate.test.js +265 -0
  695. package/test/email/marketing/custom-fields-catalog.test.js +259 -0
  696. package/test/email/marketing/prune-per-provider.test.js +762 -0
  697. package/test/email/marketing/remove-log-privacy.test.js +225 -0
  698. package/test/email/marketing-lifecycle.test.js +139 -0
  699. package/test/email/newsletter-generate.test.js +853 -0
  700. package/test/email/newsletter-svg-tokens.test.js +51 -0
  701. package/test/email/newsletter-templates.test.js +492 -0
  702. package/test/email/order-one-time-cta.test.js +97 -0
  703. package/test/email/render-content.test.js +248 -0
  704. package/test/email/safe-url.test.js +0 -0
  705. package/test/email/sanitize-images.test.js +66 -0
  706. package/test/email/send-log-privacy.test.js +107 -0
  707. package/test/email/templates.test.js +288 -0
  708. package/test/email/testing-capture.test.js +260 -0
  709. package/test/email/transactional-send.test.js +36 -0
  710. package/test/email/transactional.test.js +562 -0
  711. package/test/email/unsubscribe-groups.test.js +155 -0
  712. package/test/email/unsubscribe-key.test.js +91 -0
  713. package/test/email/validation-cases.test.js +152 -0
  714. package/test/email/validation.test.js +746 -0
  715. package/test/events/auth-delete-conversion.test.js +138 -0
  716. package/test/events/auth-delete-race.test.js +211 -0
  717. package/test/events/auth-on-create-log.test.js +164 -0
  718. package/test/events/auth-signup-conversion.test.js +141 -0
  719. package/test/events/auth-signup-limit.test.js +142 -0
  720. package/test/events/auth-trigger-log-privacy.test.js +256 -0
  721. package/test/events/cron-job-doc-shape.test.js +101 -0
  722. package/test/events/cron-reset-usage.test.js +116 -0
  723. package/test/events/notification-conversion.test.js +182 -0
  724. package/test/events/payments/_webhook-harness.js +307 -0
  725. package/test/events/payments/abandoned-cart-activity.test.js +135 -0
  726. package/test/events/payments/analytics-payment-events.test.js +945 -0
  727. package/test/events/payments/dispute-email-status.test.js +55 -0
  728. package/test/events/payments/journey-payments-abandoned.test.js +126 -0
  729. package/test/events/payments/journey-payments-cancel-endpoint.test.js +102 -0
  730. package/test/events/payments/journey-payments-cancel-no-order.test.js +76 -0
  731. package/test/events/payments/journey-payments-cancel.test.js +184 -0
  732. package/test/events/payments/journey-payments-decline.test.js +225 -0
  733. package/test/events/payments/journey-payments-discount.test.js +91 -0
  734. package/test/events/payments/journey-payments-dispute.test.js +190 -0
  735. package/test/events/payments/journey-payments-failure.test.js +148 -0
  736. package/test/events/payments/journey-payments-legacy-product.test.js +151 -0
  737. package/test/events/payments/journey-payments-one-time-decline.test.js +127 -0
  738. package/test/events/payments/journey-payments-one-time-failure.test.js +113 -0
  739. package/test/events/payments/journey-payments-one-time-refund.test.js +186 -0
  740. package/test/events/payments/journey-payments-one-time.test.js +175 -0
  741. package/test/events/payments/journey-payments-plan-change.test.js +146 -0
  742. package/test/events/payments/journey-payments-plan-switch-trial.test.js +161 -0
  743. package/test/events/payments/journey-payments-plan-switch.test.js +182 -0
  744. package/test/events/payments/journey-payments-refund-no-order.test.js +76 -0
  745. package/test/events/payments/journey-payments-refund-webhook.test.js +225 -0
  746. package/test/events/payments/journey-payments-suspend.test.js +183 -0
  747. package/test/events/payments/journey-payments-trial-cancel.test.js +132 -0
  748. package/test/events/payments/journey-payments-trial.test.js +173 -0
  749. package/test/events/payments/journey-payments-uid-resolution.test.js +134 -0
  750. package/test/events/payments/journey-payments-uncancel.test.js +157 -0
  751. package/test/events/payments/journey-payments-upgrade.test.js +137 -0
  752. package/test/events/payments/journey-payments-winback-decline.test.js +171 -0
  753. package/test/events/payments/journey-payments-winback.test.js +195 -0
  754. package/test/events/payments/paypal-expiry-cron.test.js +166 -0
  755. package/test/events/payments/purchase-failed-handler.test.js +80 -0
  756. package/test/events/payments/purchase-refunded-handler.test.js +108 -0
  757. package/test/events/payments/test-processor-doc-shape.test.js +162 -0
  758. package/test/events/payments/transition-order-emails.test.js +185 -0
  759. package/test/events/payments/transition-promo-lines.test.js +192 -0
  760. package/test/events/payments/transitions-detect.test.js +767 -0
  761. package/test/events/payments/trial-lapse-sweep-staleness.test.js +173 -0
  762. package/test/events/payments/trial-lapse-sweep.test.js +359 -0
  763. package/test/events/payments/webhook-atomic-writes.test.js +69 -0
  764. package/test/events/payments/webhook-chargebee-unreachable.test.js +106 -0
  765. package/test/events/payments/webhook-discount-clear.test.js +189 -0
  766. package/test/events/payments/webhook-failure-intent.test.js +93 -0
  767. package/test/events/payments/webhook-hosted-page-uid-trust.test.js +212 -0
  768. package/test/events/payments/webhook-ordering.test.js +326 -0
  769. package/test/events/payments/webhook-provider-lookup-trust.test.js +206 -0
  770. package/test/events/payments/webhook-refund-amount-trust.test.js +223 -0
  771. package/test/events/payments/webhook-refund-envelope-shape.test.js +148 -0
  772. package/test/events/payments/webhook-refund-linkage-trust.test.js +404 -0
  773. package/test/events/payments/webhook-refund-transaction-fallback.test.js +184 -0
  774. package/test/events/payments/webhook-refund-without-order.test.js +170 -0
  775. package/test/events/payments/webhook-refusal-reporting.test.js +194 -0
  776. package/test/events/payments/webhook-refused-intent.test.js +142 -0
  777. package/test/events/payments/webhook-retry-sweep.test.js +252 -0
  778. package/test/events/payments/webhook-transition-claim.test.js +389 -0
  779. package/test/events/payments/webhook-uid-trust.test.js +203 -0
  780. package/test/events/payments/webhook-user-without-auth.test.js +161 -0
  781. package/test/fixtures/chargebee/invoice-one-time.json +27 -0
  782. package/test/fixtures/chargebee/subscription-active.json +44 -0
  783. package/test/fixtures/chargebee/subscription-cancelled.json +42 -0
  784. package/test/fixtures/chargebee/subscription-in-trial.json +41 -0
  785. package/test/fixtures/chargebee/subscription-legacy-plan.json +41 -0
  786. package/test/fixtures/chargebee/subscription-non-renewing.json +41 -0
  787. package/test/fixtures/chargebee/subscription-paused.json +42 -0
  788. package/test/fixtures/chargebee/webhook-payment-failed.json +51 -0
  789. package/test/fixtures/chargebee/webhook-subscription-created.json +47 -0
  790. package/test/fixtures/coinbase/charge-confirmed.json +50 -0
  791. package/test/fixtures/coinbase/charge-failed.json +34 -0
  792. package/test/fixtures/coinbase/charge-pending.json +34 -0
  793. package/test/fixtures/migrate/ported-route.js.txt +11 -0
  794. package/test/fixtures/paypal/capture-completed.json +51 -0
  795. package/test/fixtures/paypal/capture-refunded.json +43 -0
  796. package/test/fixtures/paypal/order-approved.json +62 -0
  797. package/test/fixtures/paypal/order-completed.json +110 -0
  798. package/test/fixtures/paypal/sale-refunded.json +38 -0
  799. package/test/fixtures/paypal/subscription-active.json +76 -0
  800. package/test/fixtures/paypal/subscription-cancelled.json +50 -0
  801. package/test/fixtures/paypal/subscription-suspended.json +65 -0
  802. package/test/fixtures/stripe/checkout-session-completed.json +130 -0
  803. package/test/fixtures/stripe/invoice-payment-failed.json +148 -0
  804. package/test/fixtures/stripe/invoice-subscription-payment-failed.json +28 -0
  805. package/test/fixtures/stripe/invoice-subscription-payment-succeeded.json +28 -0
  806. package/test/fixtures/stripe/subscription-active.json +161 -0
  807. package/test/fixtures/stripe/subscription-canceled.json +161 -0
  808. package/test/fixtures/stripe/subscription-trialing.json +161 -0
  809. package/test/helpers/_shared-config.js +15 -0
  810. package/test/helpers/ai-request-payload.test.js +619 -0
  811. package/test/helpers/ai-schema-resolve.test.js +125 -0
  812. package/test/helpers/ai-test-provider.test.js +227 -0
  813. package/test/helpers/ai-token-accounting.test.js +150 -0
  814. package/test/helpers/ai-tools-format.test.js +384 -0
  815. package/test/helpers/analytics-no-id-notice.test.js +146 -0
  816. package/test/helpers/analytics-user-data.test.js +194 -0
  817. package/test/helpers/api-manager.test.js +311 -0
  818. package/test/helpers/backend-router.test.js +136 -0
  819. package/test/helpers/content/blog-auto-publisher.test.js +482 -0
  820. package/test/helpers/content/feed-parser.test.js +529 -0
  821. package/test/helpers/content/ghostii-blocks.test.js +135 -0
  822. package/test/helpers/content/ghostii-feed-integration.test.js +405 -0
  823. package/test/helpers/content/ghostii-write-article.test.js +244 -0
  824. package/test/helpers/dev-only-routes.test.js +225 -0
  825. package/test/helpers/env-reader.test.js +362 -0
  826. package/test/helpers/environment.test.js +263 -0
  827. package/test/helpers/event-middleware.test.js +407 -0
  828. package/test/helpers/infer-contact.test.js +157 -0
  829. package/test/helpers/lane-url.test.js +90 -0
  830. package/test/helpers/merge-line-files.test.js +272 -0
  831. package/test/helpers/metadata.test.js +122 -0
  832. package/test/helpers/middleware-request-log.test.js +134 -0
  833. package/test/helpers/middleware-user-log.test.js +124 -0
  834. package/test/helpers/payment/chargebee/parse-webhook.test.js +490 -0
  835. package/test/helpers/payment/chargebee/refund-details.test.js +198 -0
  836. package/test/helpers/payment/chargebee/to-unified-one-time.test.js +148 -0
  837. package/test/helpers/payment/chargebee/to-unified-subscription.test.js +649 -0
  838. package/test/helpers/payment/coinbase/create-intent.test.js +247 -0
  839. package/test/helpers/payment/coinbase/parse-webhook.test.js +195 -0
  840. package/test/helpers/payment/coinbase/refund-unsupported.test.js +141 -0
  841. package/test/helpers/payment/coinbase/to-unified-one-time.test.js +201 -0
  842. package/test/helpers/payment/discount-codes.test.js +141 -0
  843. package/test/helpers/payment/extract-resource.test.js +113 -0
  844. package/test/helpers/payment/fetch-failure.test.js +215 -0
  845. package/test/helpers/payment/license-gate.test.js +108 -0
  846. package/test/helpers/payment/order-id.test.js +99 -0
  847. package/test/helpers/payment/paypal/create-intent.test.js +382 -0
  848. package/test/helpers/payment/paypal/fetch-capture.test.js +123 -0
  849. package/test/helpers/payment/paypal/fetch-sale.test.js +183 -0
  850. package/test/helpers/payment/paypal/parse-webhook.test.js +678 -0
  851. package/test/helpers/payment/paypal/refund-details.test.js +202 -0
  852. package/test/helpers/payment/paypal/resolve-plan-id.test.js +108 -0
  853. package/test/helpers/payment/paypal/switch-plan.test.js +92 -0
  854. package/test/helpers/payment/paypal/to-unified-one-time.test.js +383 -0
  855. package/test/helpers/payment/paypal/to-unified-subscription.test.js +884 -0
  856. package/test/helpers/payment/stripe/fetch-charge.test.js +147 -0
  857. package/test/helpers/payment/stripe/parse-webhook.test.js +448 -0
  858. package/test/helpers/payment/stripe/refund-details.test.js +147 -0
  859. package/test/helpers/payment/stripe/to-unified-one-time.test.js +307 -0
  860. package/test/helpers/payment/stripe/to-unified-subscription.test.js +709 -0
  861. package/test/helpers/persona-domain.test.js +87 -0
  862. package/test/helpers/recaptcha.test.js +166 -0
  863. package/test/helpers/resolved-config.test.js +106 -0
  864. package/test/helpers/response-log-redaction.test.js +262 -0
  865. package/test/helpers/roles.test.js +155 -0
  866. package/test/helpers/route-context-debug-gate.test.js +118 -0
  867. package/test/helpers/route-context-logging.test.js +214 -0
  868. package/test/helpers/route-context.test.js +129 -0
  869. package/test/helpers/safe-compare.test.js +114 -0
  870. package/test/helpers/sanitize.test.js +223 -0
  871. package/test/helpers/schema-engine.test.js +537 -0
  872. package/test/helpers/schema-zod.test.js +554 -0
  873. package/test/helpers/seed-accounts-load.test.js +163 -0
  874. package/test/helpers/seed-google-personas.test.js +114 -0
  875. package/test/helpers/seeded-personas.test.js +823 -0
  876. package/test/helpers/settings.test.js +292 -0
  877. package/test/helpers/setup-engines-pin.test.js +68 -0
  878. package/test/helpers/setup-manifest-sync.test.js +116 -0
  879. package/test/helpers/slugify.test.js +395 -0
  880. package/test/helpers/storage.test.js +195 -0
  881. package/test/helpers/test-banner-latch.test.js +139 -0
  882. package/test/helpers/usage-consume.test.js +460 -0
  883. package/test/helpers/usage-log-privacy.test.js +121 -0
  884. package/test/helpers/user-doc-heal.test.js +540 -0
  885. package/test/helpers/user.test.js +771 -0
  886. package/test/helpers/webhook-forward.test.js +419 -0
  887. package/test/helpers/wipe-auth-project.test.js +143 -0
  888. package/test/mcp/discovery.test.js +52 -0
  889. package/test/mcp/oauth.test.js +160 -0
  890. package/test/mcp/protocol.test.js +278 -0
  891. package/test/mcp/roles.test.js +203 -0
  892. package/test/mcp/utils.test.js +245 -0
  893. package/test/notification/identity.test.js +155 -0
  894. package/test/routes/admin/create-post.test.js +362 -0
  895. package/test/routes/admin/database.test.js +133 -0
  896. package/test/routes/admin/deduplicate-image-alts.test.js +191 -0
  897. package/test/routes/admin/email-content-html.test.js +58 -0
  898. package/test/routes/admin/email-request-log.test.js +86 -0
  899. package/test/routes/admin/email.test.js +116 -0
  900. package/test/routes/admin/firestore-query.test.js +206 -0
  901. package/test/routes/admin/firestore.test.js +129 -0
  902. package/test/routes/admin/infer-contact.test.js +220 -0
  903. package/test/routes/admin/notification.test.js +199 -0
  904. package/test/routes/admin/post-convert-image.test.js +159 -0
  905. package/test/routes/admin/post-deploy-flag.test.js +81 -0
  906. package/test/routes/admin/post-download-error.test.js +90 -0
  907. package/test/routes/admin/post-resize-image.test.js +185 -0
  908. package/test/routes/admin/post.test.js +369 -0
  909. package/test/routes/admin/repo-content.test.js +223 -0
  910. package/test/routes/admin/stats.test.js +114 -0
  911. package/test/routes/admin/users-disable.test.js +63 -0
  912. package/test/routes/admin/users-list.test.js +71 -0
  913. package/test/routes/content/post.test.js +60 -0
  914. package/test/routes/general/uuid.test.js +133 -0
  915. package/test/routes/health.test.js +110 -0
  916. package/test/routes/marketing/campaign.test.js +184 -0
  917. package/test/routes/marketing/contact.test.js +416 -0
  918. package/test/routes/marketing/email-preferences.test.js +293 -0
  919. package/test/routes/marketing/push-send.test.js +34 -0
  920. package/test/routes/marketing/webhook-forward.test.js +63 -0
  921. package/test/routes/marketing/webhook.test.js +641 -0
  922. package/test/routes/payments/_route-harness.js +145 -0
  923. package/test/routes/payments/cancel-provider-errors.test.js +227 -0
  924. package/test/routes/payments/cancel-skip-guards.test.js +126 -0
  925. package/test/routes/payments/cancel-trialing.test.js +428 -0
  926. package/test/routes/payments/cancel.test.js +165 -0
  927. package/test/routes/payments/dedup-race.test.js +186 -0
  928. package/test/routes/payments/discount.test.js +82 -0
  929. package/test/routes/payments/dispute-alert.test.js +324 -0
  930. package/test/routes/payments/intent-discount-amounts.test.js +550 -0
  931. package/test/routes/payments/intent-discount-coupons.test.js +293 -0
  932. package/test/routes/payments/intent-one-time-metadata.test.js +122 -0
  933. package/test/routes/payments/intent-purchaser-guard.test.js +139 -0
  934. package/test/routes/payments/intent-zero-total.test.js +260 -0
  935. package/test/routes/payments/intent.test.js +413 -0
  936. package/test/routes/payments/plan.test.js +367 -0
  937. package/test/routes/payments/portal-return-url.test.js +107 -0
  938. package/test/routes/payments/portal.test.js +94 -0
  939. package/test/routes/payments/refund-one-time.test.js +254 -0
  940. package/test/routes/payments/refund-paypal-proration.test.js +190 -0
  941. package/test/routes/payments/refund.test.js +181 -0
  942. package/test/routes/payments/trial-eligibility.test.js +73 -0
  943. package/test/routes/payments/uncancel.test.js +270 -0
  944. package/test/routes/payments/webhook-stripe-invoice.test.js +129 -0
  945. package/test/routes/payments/webhook-stripe-refund-one-time.test.js +130 -0
  946. package/test/routes/payments/webhook-test-provider.test.js +156 -0
  947. package/test/routes/payments/webhook.test.js +115 -0
  948. package/test/routes/payments/winback-stripe-coupon.test.js +126 -0
  949. package/test/routes/payments/winback.test.js +597 -0
  950. package/test/routes/test/authenticate.test.js +79 -0
  951. package/test/routes/test/redirect.test.js +66 -0
  952. package/test/routes/test/reset-account.test.js +126 -0
  953. package/test/routes/test/roster.test.js +101 -0
  954. package/test/routes/test/schema.test.js +556 -0
  955. package/test/routes/test/usage.test.js +374 -0
  956. package/test/routes/user/api-keys.test.js +158 -0
  957. package/test/routes/user/connections-grant.test.js +759 -0
  958. package/test/routes/user/connections-identity.test.js +260 -0
  959. package/test/routes/user/connections-log-privacy.test.js +206 -0
  960. package/test/routes/user/connections-refresh-lease.test.js +696 -0
  961. package/test/routes/user/connections-return.test.js +261 -0
  962. package/test/routes/user/connections-uid.test.js +516 -0
  963. package/test/routes/user/delete.test.js +138 -0
  964. package/test/routes/user/feedback.test.js +106 -0
  965. package/test/routes/user/orders.test.js +233 -0
  966. package/test/routes/user/sessions.test.js +231 -0
  967. package/test/routes/user/settings-validate.test.js +84 -0
  968. package/test/routes/user/signup-emails.test.js +113 -0
  969. package/test/routes/user/signup-location.test.js +84 -0
  970. package/test/routes/user/signup-log-privacy.test.js +112 -0
  971. package/test/routes/user/signup.test.js +579 -0
  972. package/test/routes/user/subscription.test.js +101 -0
  973. package/test/routes/user/token.test.js +111 -0
  974. package/test/routes/user/user.test.js +158 -0
  975. package/test/routes/verts/cache.test.js +78 -0
  976. package/test/routes/verts/click-destination.test.js +76 -0
  977. package/test/routes/verts/crud.test.js +194 -0
  978. package/test/routes/verts/redirect.test.js +147 -0
  979. package/test/routes/verts/selection.test.js +237 -0
  980. package/test/routes/verts/serve.test.js +185 -0
  981. package/test/routes/verts/unit-document.test.js +117 -0
  982. package/test/rules/_environment.js +66 -0
  983. package/test/rules/brand-merge.test.js +195 -0
  984. package/test/rules/field-helpers.test.js +309 -0
  985. package/test/rules/notifications.test.js +577 -0
  986. package/test/rules/payments-carts.test.js +406 -0
  987. package/test/rules/sessions.test.js +170 -0
  988. package/test/rules/user.test.js +517 -0
  989. package/test/rules/verts.test.js +101 -0
  990. package/test/security/fetch-log-secrets.test.js +124 -0
  991. package/test/security/primitives.test.js +105 -0
  992. package/test/security/repo-pinning.test.js +77 -0
  993. package/test/stripe-live/subscription-lifecycle.test.js +141 -0
@@ -0,0 +1,1387 @@
1
+ # omega.json5 — the single OMEGA config
2
+
3
+ One config file, identical shape, for every OMEGA project type. Owned by `@omega.js/config`
4
+ (`packages/config`); frameworks vendor it at prepare time and read **only** this format —
5
+ there is no dual-read of legacy files. Legacy brands migrate by converting their old config
6
+ once (mapping tables below) and deleting the old file.
7
+
8
+ Consistency between this file and what pages actually say — brand facts read from config instead of typed,
9
+ one brand hex, the merge chain, secrets out of config — is the plugin's `omega:brandcheck` skill,
10
+ [agent-plugins/claude/skills/brandcheck/SKILL.md](../../agent-plugins/claude/skills/brandcheck/SKILL.md);
11
+ its quality hook fires on every `omega.json5` edit.
12
+
13
+ ## Location
14
+
15
+ | Project | File |
16
+ |---|---|
17
+ | Every project type (default) | `config/omega.json5` |
18
+ | Standalone backend repo | `functions/config/omega.json5` |
19
+ | Brand monorepo — brand level | `{brand}/config/omega.json5` |
20
+ | Brand monorepo — local level | `{brand}/targets/{target}/config/omega.json5` |
21
+
22
+ JSON5: comments, trailing commas, unquoted keys, single quotes all allowed.
23
+
24
+ ## Shape
25
+
26
+ ```json5
27
+ {
28
+ // SHARED sections — identical spelling in every project type.
29
+ // (`SHARED_SECTIONS` in @omega.js/config is the authoritative list.)
30
+ brand: { id, name, url, description, tagline, company, type, font, color, contact: { email, phone, person: {…}, carbonCopy: […] }, address: {…}, images: {…} }, // #524: type = the schema.org type the JSON-LD stamps ('Organization' unset), font = the display face the assets service renders the wordmark from. contact.person = the human who signs "personal" email (name, firstName, image, url, urlText), ASKED for by `omega onboard` ([#770](https://github.com/Omega-JS-Stack/omega/issues/770)) since nothing can derive a human name — the wizard prompt writes name + the optional image/url, `--contactName`/`--contactImage`/`--contactUrl` answer it non-interactively, and an unanswered brand gets no key at all; contact.carbonCopy = audit BCCs; images.companyWordmark = parent wordmark in email footers
31
+ cloud: { provider: 'firebase', config: { apiKey, authDomain, databaseURL, projectId, storageBucket, messagingSenderId, appId, measurementId }, messaging: { vapidKey }, shared, supportEmail, consentAudience, apiSubdomain, organizationId, billingAccount, oauthRedirectsConfigured }, // ONE cloud home (#23): the app config PLUS the provisioning fields; projectId lives only at cloud.config.projectId. vapidKey: web-push public key (console → Cloud Messaging), public by design
32
+ repo: { providers: { github: { enabled, org, repo, shared, private } } },
33
+ edge: { providers: { cloudflare: { enabled, zone, dns, settings, rules, cacheRules, speedTest, workers } } }, // rules.redirect: the ORDERED dynamic-redirect ruleset — [{ name, expression, statusCode, preserveQueryString, targetUrl, enabled }], `expression`/`targetUrl` in Cloudflare's own filter language. The ONE home for a TEMPLATED redirect, whose destination is computed from the request path (#466); the manager's edge service reconciles them by `name` (docs/manager/edge.md)
34
+ captcha: { providers: { recaptcha: { project, siteKey, domainsConfirmed: [] } } }, // domainsConfirmed: machine-written — the classic key's domain list has no API, so the captcha service records the owner's confirmation here and stops asking
35
+ search: { providers: { searchConsole: { enabled, submitSitemap, sitemapPaths, gaLinked } } }, // #546: each `enabled` is the service's own switch, default ON — false skips that whole service. gaLinked: machine-written — the Search Console ↔ GA association has no API, so the confirmation is the record
36
+ forms: { providers: { slapform: { enabled, formId, templateFormId, updateFormInfo, plan } } },
37
+ inbound: { chat: { providers: { chatsy: { enabled, agentId, accountId, templateAgentId, updateAgentInfo, plan, sponsorshipsUrl, settings } } },
38
+ email: { providers: { replyify: { enabled, agentId, templateAgentId, updateAgentInfo, plan, discount } } } },
39
+ analytics: { providers: { google: { id, propertyId, accountId }, meta: { id, accountId }, tiktok: { id, accountId, appId } } }, // #524: the pixel/measurement id is the RUNTIME value; propertyId/accountId are the platform ids the manager reconciles against (never secrets — tokens stay in .env). tiktok.appId: the DEVELOPER APP the token mint authorizes through (#448/#635) — public config; the app secret is pasted once and never saved
40
+ advertising: { providers: { adsense: { client, displaySlot, inArticleSlot, inFeedSlot, multiplexSlot }, inhouse: { source } }, fallback, tags: [] }, // C4 cp105; inhouse source: 'self' | 'company' | full URL (ads spec). #527: `client` is the ONE adsense switch — its presence drives the managed account, the ad units and the ads.txt record together (no `units`, no `enabled`). Role-level: `fallback: 'inhouse'` is the lane a provider miss falls through to (false/absent ends at the built-in promo), `tags` are the brand's contextual targeting tags
41
+ payment: { providers: { stripe: { publishableKey }, paypal: { clientId }, chargebee: { site }, coinbase: { enabled } }, products: […], winback: { enabled, percent, amount, duration } }, // #642: coinbase (Coinbase Commerce, crypto, one-time purchases only) is the one provider switched by an explicit `enabled`, default OFF — its whole credential is the secret COINBASE_COMMERCE_API_KEY, so there is no public datum to gate on. winback = the cancel-flow save offer (#268), on by default at 50% off the next cycle — see below
42
+ monitoring: { enabled, providers: { sentry: { org, dsn, environment, sampleRate, tracesSampleRate, replaysSessionSampleRate, replaysOnErrorSampleRate, scrubEmail, attachScreenshot, bundlePatterns: [] } } }, // #425: the monitor is a KEY under `providers`. dsn presence IS the runtime enable signal; environment unset = the host's gate names it; scrubEmail defaults true (email OFF), attachScreenshot is desktop-only, bundlePatterns + the two replay rates browser-only (replay defaults to 0 — opt-in, #485). docs/shared/monitoring.md
43
+ connections: { <provider>: { enabled, scope: [], name, logo, description } }, // #771/#788: per-provider USER-CONNECTION settings, keyed by provider name; public values only (the credentials are the CONNECTIONS_<PROVIDER>_CLIENT_ID/_SECRET env pair). The provider set is OPEN — a brand ships its own as `targets/backend/src/connections/<name>.js` — so the section stays free-form. See below
44
+ theme: { id, appearance }, // project-owned; seeded at onboarding
45
+ translation: { enabled, default, languages: [], providers: { claude: {} } | { chatgpt: {} }, model, exclude: [] }, // presence picks the engine; absent = claude. docs/shared/translation.md
46
+ socials: { twitter: 'somiibo', spotify: { handle, redirect } }, // platform → handle. The handle derives the profile URL every surface reads (JSON-LD sameAs, the footer row, omega_social) and @omega.js/web emits a shortlink redirect page at /<platform> per entry (#429); the object form adds a redirect target that WINS for the shortlink when it is not the profile URL. Blank handle = no entry, no page. Not a disperse-owned SHARED_SECTION — brand-level content, read by the web target
47
+
48
+ // MANAGER-read brand-level sections (#277). Schema-known at the TOP level: the
49
+ // manager loads the brand config unfolded, and a website-only brand has no
50
+ // `targets.backend` to hold them (presence there would enable the target). A
51
+ // `targets.backend.<same key>` block still overrides any of them.
52
+ parent: 'self' | 'https://parent.example.com' | false, // webhook parent topology; false = shared webhook account owned elsewhere
53
+ domain: { providers: { namecheap: {} }, email: { providers: { cloudflare: {} }, forwarding: [] } }, // TWO roles (#425): the REGISTRAR is the one key under `providers` (namecheap is the one the service drives by API; every other registrar gets manual instructions), the mailbox provider the one key under `email.providers`. Presence picks; no entry = nothing chosen, and the service skips
54
+ certificates: { enabled, providers: { apple: { bundleIdPrefix, capabilities: [], profiles: [], certificates: [] } } }, // Apple signing for desktop/mobile targets. bundleIdPrefix is the brand's own answer ('com.mycompany' + brand.id composes the bundle id); the credentials live in .env (APPLE_API_ISSUER, APPLE_API_KEY_ID, APPLE_TEAM_ID)
55
+ github: { user, website }, // GitHub identity for the brand (content identity; repo.providers.github is the source-hosting home)
56
+ reviews: { enabled, sites: [] },
57
+ marketing: { campaigns: { enabled, providers: { sendgrid: { listId, groups: { orders, hello, account, marketing, security, newsletter, internal } } } }, newsletter: { enabled, providers: { beehiiv: { publicationId } }, content: […] }, prune: { enabled } }, // #425: each role names its vendor as a KEY under `providers`; `enabled` and the newsletter `content` PIPELINE blob stay role-level. `prune` is ON by default (Ian 2026-08-22, #478) and per-brand disableable: packages/backend/docs/marketing-campaigns.md § Contact Pruning. `groups` holds the SendGrid unsubscribe (ASM) group ids — per ACCOUNT, so the campaigns service provisions them by name and writes the ids here (#649)
58
+ blog: { /* AI blog-content settings (Ghostii pipeline) */ },
59
+ devlog: { enabled, providers: { ghostii: { orgs, lookbackDays, … } } }, // commit-digest devlog (#553): `enabled: true` PUBLISHES AI-written posts to the live site, so it is case 3 — the literal true is the only ON, absence is off, and no default is materialized
60
+ seo: { github: { content: [] } }, // the manager's parasite-SEO content repos; big blocks may live in the `config/seo.json5` sidecar. The site-wide SEARCH POSTURE is NOT here: it is `targets.web.meta.index`, the same name a page writes (#564)
61
+ dataRequest: { /* GDPR/CCPA data-request query definitions */ },
62
+ directory: { enabled }, // opt in to the manager's directory PUSH — this brand's entry into `parent`'s brands collection (#246); default off, public facts only. docs/manager/directory.md
63
+ sponsorships: { acceptable: [], unacceptable: [], prices: { 'guest-post': 70, 'link-insertion': 50 } }, // sponsorship terms — the first directory BLOCK; `prices` is an open placement→USD map, not an enum
64
+
65
+ // TARGET-scoped config. KEY PRESENCE = "this brand enables this target"
66
+ // (replaces the legacy brand-config targets ARRAY). `extension: {}` means
67
+ // enabled-with-defaults. Unknown keys are validation errors UNLESS they
68
+ // declare `type: 'custom'` (see Custom targets below). A value may
69
+ // also be an ARRAY of id'd instances (see Multi-instance targets below).
70
+ targets: {
71
+ web: { meta: { index }, imagemin, collections, client: { consent, … }, dev: { limitCollections } }, // `meta` holds SITE-WIDE defaults for page-meta values, spelled exactly as a page spells them (#564; `index` is the only key today). No `redirects` key: #466 retired it — a TEMPLATED redirect is a Cloudflare redirect rule (edge.providers.cloudflare.rules.redirect), an enumerable one is a redirect PAGE (docs/web/index.md). client: the @omega.js/client runtime blob (auth, sentry, exitPopup, …) — a settings bag the client normalizes; only the keys a BRAND authors are schema-known: `consent` (see "Consent" below), plus `auth.config.policy` ('authenticated' | 'unauthenticated' | 'disabled'; absent = no policy, and the auth/admin layouts set theirs in page frontmatter), `exitPopup.enabled` and `serviceWorker.enabled` (both default true, materialized) ([#650](https://github.com/Omega-JS-Stack/omega/issues/650)). collections: the brand's OWN content collections — name → { field, size, title, description, permalink }; documents live in `_<name>/` and the engine generates the listing + one page per category of `field` (#207). dev.limitCollections: dev-only collection sampling — collection name → max documents ({ posts: 50 }) plus `randomize: true`; development builds only, production always ships the whole site (#190)
72
+ backend: { projectType, auth: { signup: { maxPerIpPerDay } } }, // projectType: 'firebase' (default — Cloud Functions) | 'custom' (the same backend as its own server on PORT, for a container host — no Functions deploy, no emulator lane; see "Backend project type" below). auth.signup.maxPerIpPerDay: signups allowed per client IP per day, positive integer, default 2. Raise it for audiences behind shared egress (NAT/CGNAT, VPNs, offices)
73
+ desktop: { app, platforms: { mac, win, linux }, autoUpdate, startup,
74
+ releases, remoteConfig, remoteScripts, restartManager },
75
+ extension: { /* near-empty at launch */ },
76
+ mobile: { /* RESERVED — MAM parked */ },
77
+ api: { type: 'custom' }, // any OTHER key = a custom target (#603) — the manager drives it entirely through its own package.json scripts; see "Custom targets" below
78
+ },
79
+ }
80
+ ```
81
+
82
+ ## Resolution
83
+
84
+ `loadConfig(projectDir, target, { defaults })` produces ONE resolved object per target:
85
+
86
+ ```
87
+ schema defaults ← framework defaults ← company ← brand shared ← brand targets[target] ← local shared ← local targets[target]
88
+ ```
89
+
90
+ - **The bottom layer is the SCHEMA's own defaults** ([#478](https://github.com/Omega-JS-Stack/omega/issues/478)) —
91
+ see [Defaults & self-healing](#defaults--self-healing) below. `options.defaults` sits directly
92
+ above it and carries only what a framework does differently.
93
+ - "shared" = the file minus its `targets` key. In a standalone repo only the local layers exist.
94
+ - **The company layer** is the company workspace's own `config/omega.json5`, found through the
95
+ brand's `.omega/company.json` stamp (the same marker the `.env` cascade and owner hooks read —
96
+ see below). It layers exactly like the brand file (company shared ← company `targets[target]`)
97
+ minus its `brands` key, which is company plumbing and never inherits. An unstamped brand has no
98
+ company layer; the resolved result reports the file it used as `files.company`.
99
+ - **`projectDir` may be one of a target's SUBDIRS** — `functions/` (@omega.js/backend's runtime cwd)
100
+ or `dist/` (its staged build output, the view `omega test` loads): every walk (brand root,
101
+ company marker, local-layer fallback, instance id, compose, `resolveBrandRoot`) treats the target root
102
+ as one level up, so `loadConfig(functionsDir, 'backend')`, `loadConfig(distDir, 'backend')` and
103
+ `loadConfig(targetRoot, 'backend')` resolve identically. A staged `config/omega.json5` inside either
104
+ subdir is the deployed runtime's own view, never an authored local layer.
105
+ - **Target sections overlay the TOP LEVEL**: `targets.desktop.platforms` resolves to
106
+ `config.platforms`; frameworks never read through `config.targets.<type>.…`.
107
+ - **Any shared key inside a target entry overrides it for that surface** — a desktop-only
108
+ Sentry DSN is just `targets.desktop.monitoring.providers.sentry.dsn`; disabling any integration per-surface is
109
+ uniformly `<key>: { enabled: false }`. One agnostic deep merge everywhere (objects merge,
110
+ arrays/scalars replace, `null` replaces, `undefined` is skipped).
111
+ - **A global value and its specific override share ONE name**: the standing rule is recorded in
112
+ [docs/shared/rulings.md](rulings.md) (Ian 2026-09-09).
113
+ - **@omega.js/web adds one MORE layer, per page** ([#607](https://github.com/Omega-JS-Stack/omega/issues/607)):
114
+ a page's (or layout's) `config:` frontmatter block merges over the resolved config for that page
115
+ alone, and templates read the result as `resolved.config.*` — the WHOLE merged config, never a
116
+ subset. It is the same deep merge, one layer higher — `… ← local targets[target] ← page config:`.
117
+ Nothing else in the config chain knows about it. The membership rule runs both ways: a page
118
+ restating a config section BARE is a build error, and a key under `config:` that no omega.json5
119
+ section answers to is a build error too. Page machinery — `meta`, `schema`, `layout`,
120
+ `permalink` — is not config and has no home in this file at all
121
+ ([docs/web/frontmatter.md](../web/frontmatter.md)).
122
+ - The merged `targets` map rides along on the resolved config so enabled-target enumeration
123
+ survives (`getEnabledTargets()`); the `enabled` flag on the result says whether the
124
+ requested target is listed.
125
+ - No target argument → whole-file merge (the shape omega-manager's disperse works with).
126
+
127
+ ## Multi-instance targets
128
+
129
+ One brand can run N instances of the SAME target type (the legacy `brand.subdomains` need:
130
+ admin/cdn/app sites of one brand) — `targets.<type>` takes an **object OR an array of id'd
131
+ instances** ([_attic/plans/multi-instance-targets.md](../../_attic/plans/multi-instance-targets.md), ratified
132
+ 2026-07-20):
133
+
134
+ ```json5
135
+ targets: {
136
+ backend: { /* single instance — today's shape, unchanged */ },
137
+ web: [
138
+ { id: 'main' }, // the primary — targets/website, acme.com
139
+ { id: 'admin', // targets/website-admin, admin.acme.com
140
+ brand: { name: 'Acme Admin' } }, // overrides brand shared for admin ONLY
141
+ { id: 'store', url: 'https://shop.acme.com' }, // targets/website-store, a custom host
142
+ ],
143
+ }
144
+ ```
145
+
146
+ - **Normalization is the whole mechanism** (`normalizeTargetInstances`): a single object is
147
+ `[{ id: 'main', ...entry }]` internally — every consumer iterates instances and the
148
+ single-instance world is just length 1. Zero breaking change for existing brands.
149
+ - **Target-dir mapping**: `main` → `targets/<canonical dir>` (unchanged); any other id →
150
+ `targets/<canonical dir>-<id>`. The inverse walk names the instance from the dir
151
+ (`website-admin` → web/admin), and `loadConfig`/`composeTargetConfig` slot THAT instance's
152
+ entry into the merge chain: `defaults ← brand shared ← instance entry ← local shared ← local
153
+ targets.<type>`. The instance `id` key is bookkeeping — stripped, never config. The result
154
+ carries `instance` (the resolved id).
155
+ - **Validator rules**: array entries MUST carry a dir-safe `id`, unique per type; an empty
156
+ array is an error; **>1 backend instance is a WARNING** (`warnings` on the result) — backend
157
+ stays single-instance in practice (one Cloud Functions surface per brand).
158
+ - **Scoping rules**: the single-object form applies to EVERY target of the type (today's
159
+ behavior, suffixed dirs included); the array form is exact-id — a target dir with no matching
160
+ id rides shared config alone. The workspace structure op expects every instance's exact dir
161
+ (missing = the same create-this-dir error as today).
162
+ - **The instance id IS the subdomain** ([#588](https://github.com/Omega-JS-Stack/omega/issues/588),
163
+ Ian 2026-09-01). An entry with no `url` of its own resolves to `https://<id>.<host of
164
+ brand.url>` for every id but `main`, and `main` keeps `brand.url`, so `web: [{ id: 'main' },
165
+ { id: 'admin' }]` is a COMPLETE declaration. An explicit `url` overrides it for a custom host
166
+ (`{ id: 'store', url: 'https://shop.acme.com' }`), and an instance-scoped `brand.url` overrides
167
+ it too. The host is taken EXACTLY as `brand.url` states it: a `www.` brand derives
168
+ `admin.www.acme.com`, and no usable `brand.url` derives nothing at all (null, never a
169
+ half-built `https://admin.`).
170
+ - **One shared `api.<domain>`**: every instance talks to the same backend, so the manager's cloud
171
+ hosting op ensures exactly one API domain no matter how many instances a brand runs
172
+ ([docs/manager/cloud.md](../manager/cloud.md)).
173
+ - **Brand-level facts stay brand-level.** `cloud.config.authDomain` compares against
174
+ `brand.url` for every instance (one Firebase project, one backend, one authDomain), and so
175
+ does the persona domain the test lanes seed. Only the instance's PUBLIC surface is per
176
+ instance: `site.url`, the gh-pages CNAME `omega deploy`/`omega build` write, and the deploy
177
+ path prefix.
178
+ - **An override AT the instance IS the instance url**, never a base to stack the id on: an
179
+ instance entry's own `brand.url`, a `targets/website-<id>/config/omega.json5` naming
180
+ `https://shop.acme.test`, or a dev layer naming `http://localhost:4000` are each the answer
181
+ as written (no `shop.shop.acme.test`, no `https://admin.localhost:4000`). The derivation only
182
+ runs while the resolved `brand.url` is still the brand layer's.
183
+ A CUSTOM host belongs on the entry's `url` (`{ id: 'store', url: 'https://shop.acme.test' }`),
184
+ never on an instance `brand.url`: the authDomain check reads `brand.url`, so overriding it
185
+ at the instance makes that check compare against the custom host and fail.
186
+ - **The resolved `url` is a declared key** (`packages/config/src/schema.js`), so an instance load
187
+ raises no undeclared-key warning for the url it just derived.
188
+ - **Per-instance surfaces**: dev ports offset by array position (docs/shared/local-dev.md), deploy
189
+ records key per target (docs/shared/deploys.md), and every reader of an instance's public URL
190
+ (the manager's live-URL checks, the resolved config's top-level `url`, `site.url` in templates)
191
+ goes through the one resolver (`resolveInstanceUrl`: instance `url` → instance `brand.url` →
192
+ the derived `<id>.<host>` → brand shared `brand.url`).
193
+ - **Legacy `brand.subdomains` conversion rule**: each subdomain becomes a web instance,
194
+ `["admin", "cdn"]` → `web: [{ id: 'main' }, { id: 'admin' }, { id: 'cdn' }]`; the ids carry the
195
+ subdomains, so nothing else is written. The key itself is a retired path (below), so a config
196
+ still carrying it fails validation with that recipe.
197
+ - Non-goals (v1): no cross-instance shared builds, no per-instance Firebase projects.
198
+
199
+ ## Custom targets (#603)
200
+
201
+ A brand also runs targets no framework owns — a Render API, a worker, a script. They are
202
+ declared under **any key that is not a framework name**, and the entry must say what it is:
203
+
204
+ ```json5
205
+ targets: {
206
+ web: {},
207
+ api: { type: 'custom' }, // → targets/api
208
+ jobs: [{ id: 'main', type: 'custom' }, { id: 'nightly', type: 'custom' }], // → targets/jobs, targets/jobs-nightly
209
+ }
210
+ ```
211
+
212
+ - **The type is the declaration.** An unknown key WITHOUT `type: 'custom'` is still a
213
+ validation error (it is a typo'd framework name), and a framework key WITH it is an error
214
+ too — a framework's verbs come from its framework, never from package scripts.
215
+ - **The array form works the same way**, and every instance must carry the type; the
216
+ target-dir mapping is the shared one (`main` → the bare dir, any other id → `<name>-<id>`).
217
+ - **Its verbs are its own package.json scripts**: `start`, `build`, `test`, `deploy`, `clean`.
218
+ The manager runs each through `npm run <verb>` when the script is present and skips it
219
+ loudly when it is absent — nothing is inferred or defaulted.
220
+ - **No framework service reconciles it.** The only manage op that sees a custom target is the
221
+ workspace service (structure, agent docs, settings). Nothing is composed into a `.env` of its
222
+ own; it INHERITS the brand keys — the manager loads the env chain into `process.env` before it
223
+ spawns anything, so a custom target started by `omega dev`/`omega deploy` has them. A standalone
224
+ run inside the target dir does not (there is no `@omega.js/config` in there to walk the cascade).
225
+ Full contract: [docs/manager/index.md](../manager/index.md) § Custom targets.
226
+
227
+ ## Backend project type (#584)
228
+
229
+ `targets.backend.projectType` says how the backend RUNS, and it is the only switch:
230
+
231
+ ```json5
232
+ targets: {
233
+ backend: { projectType: 'custom' }, // default is 'firebase'
234
+ }
235
+ ```
236
+
237
+ - **`'firebase'` (default)** — the backend exports Cloud Functions, deploys with `firebase deploy`,
238
+ and runs locally on the emulator suite. Everything OMEGA does today.
239
+ - **`'custom'`** — the SAME backend (same routes, same schemas, same auth middleware, same
240
+ helpers, same `.env`) served by its Express app on `process.env.PORT`, for a container host
241
+ (Render & co). `Manager.init()` reads the mode off this key, so a brand's `src/index.js` is
242
+ unchanged; an explicit `init` option still wins.
243
+ - **What custom mode removes is the Firebase LANE, not Firebase**: no Functions deploy, no
244
+ emulator, no emulator test run — those four verbs refuse loudly and name their replacement
245
+ ([docs/backend/index.md](../backend/index.md)). `firebase-admin` still loads, so a custom
246
+ server that reads Firestore or verifies an ID token works exactly as before.
247
+ - **Not to be confused with a custom TARGET** (above): that is a target no framework owns.
248
+ This one IS the `@omega.js/backend` target, with a different artifact. Same-type duplicates
249
+ are still the array (multi-instance) form.
250
+ - The brand-root behavior — deploy through the target's own `deploy` script, `omega dev` booting
251
+ the server instead of the emulator: [docs/manager/index.md](../manager/index.md).
252
+
253
+ ## Hard rules
254
+
255
+ - **Secrets NEVER live in omega.json5** — they live in `.env`. `loadConfig` throws on any
256
+ key matching `/(secret|privateKey|apiSecret)$/i` in any section of a raw file, before any
257
+ merge. Public credentials (`publishableKey`, `clientId`, `cloud.config.apiKey`) pass by
258
+ design.
259
+ - **The legacy `targets` ARRAY form throws** — `targets` is an object keyed by target name.
260
+ - Schema findings (required/type/min/match/enum) come back as `errors`, not throws — build-time
261
+ audit throws on them, boot warns/fails per framework policy.
262
+
263
+ ## The .env cascade (secrets) — D15
264
+
265
+ Secrets mirror the config hierarchy (`src/env.js`), weakest → strongest:
266
+
267
+ ```
268
+ company .env ← brand .env ← local .env ← shell env
269
+ ```
270
+
271
+ Every layer is TWO files: its `.env`, and the `.env.<environment>` overlay that wins
272
+ over it.
273
+
274
+ ```
275
+ .env ← .env.development | .env.testing | .env.production
276
+ ```
277
+
278
+ - **Same walk as the config cascade**: `{brand}/targets/{target}` layers the brand root's `.env`
279
+ under the target's; a brand stamped with `.omega/company.json` (written idempotently by
280
+ company manage runs) layers its company root's `.env` underneath that. `findBrandRoot`
281
+ in `load.js` is the ONE definition of the walk — both cascades use it.
282
+ - **`.env.<environment>` overlays the `.env` beside it**
283
+ ([#586](https://github.com/Omega-JS-Stack/omega/issues/586)) — the widespread standard
284
+ (Next.js, Vite, Rails dotenv, dotenv-flow). The three names are exactly what
285
+ `envEnvironment()` returns (`development` | `testing` | `production`), so the file name
286
+ and the runtime's own answer are ONE vocabulary; only the RUNNING environment's overlay
287
+ is read, and every key in it is equal — whatever it holds wins, values are TRUSTED, and
288
+ no key gets special treatment. The composed artifact is flat and single-environment: a
289
+ deploy composes base + production, the emulator base + development, a test lane base +
290
+ testing, and no other environment's file ever rides along. Onboard scaffolds all three
291
+ beside the brand `.env`, empty but for a header comment (`.env.*` is gitignored).
292
+ - **Precedence via dotenv's no-override semantics**: files load strongest-first and never
293
+ overwrite keys already set, so the shell always wins and local beats brand beats company.
294
+ - **A RELOAD honors edits, because ownership is remembered**
295
+ ([#724](https://github.com/Omega-JS-Stack/omega/issues/724)). After a boot load every key
296
+ is "already set", so presence can no longer tell a shell value from a file value — which
297
+ is why `loadEnv` alone can never deliver an edit. The first chain load in a process
298
+ snapshots what `process.env` carried before any file was read (shell-owned, forever) and
299
+ records what each file layer delivers (file-owned). `reloadEnv(startDir, options?)` drops
300
+ the file-owned keys, then loads again: a NEW key and an EDITED value both land, a key
301
+ dropped from the file is dropped from the process, and a shell-set value is never touched.
302
+ The dev lanes' `.env` watchers ([#681](https://github.com/Omega-JS-Stack/omega/issues/681))
303
+ are its one caller.
304
+ - **Empty file values never claim a key (cp95a, friction #20)**: `KEY=` / `KEY=""` in any
305
+ `.env` FILE means "documented here, value supplied by another layer" — a scaffolded local
306
+ file full of placeholders can't shadow the brand root's real values. Only the shell can
307
+ deliberately set a key to empty. The brand root's `.env` stub ships `# KEY=` commented
308
+ placeholders, rendered from the env schema (the merge protocol keeps set values on their
309
+ line, converges empties to the placeholder); no framework scaffolds a target `.env` at all.
310
+ - **Defined at the source, resolved at runtime/build**: a brand-wide `GH_TOKEN` lives once
311
+ in the brand `.env`; every framework CLI/build resolves the chain at boot
312
+ (`loadEnv(process.cwd())` in the web/desktop/extension CLIs + gulp pipelines,
313
+ `loadEnv(functionsDir)` in the @omega.js/backend CLI and runtime). Nothing is copied
314
+ between `.env` files just to be visible.
315
+ - **The brand root's `.env` is the ONE file humans and the manager edit**
316
+ ([#678](https://github.com/Omega-JS-Stack/omega/issues/678)). A target's own `.env` is
317
+ optional and overrides PER KEY, by hand; no machine ever writes one.
318
+ - **Backend's local layer is the target-root `.env`** (`targets/backend/.env`) — the layer a
319
+ human uses to override one key for that surface. What physically ships is the STAGED
320
+ `dist/.env`: it rides the Firebase deploy artifact (the cloud can't walk up), so every verb
321
+ that produces one (`omega build`, `dev`, `test`, `deploy`) composes it from the file layers,
322
+ filtered by the env schema. In the cloud the walk finds no brand/company and behavior
323
+ is identical to plain dotenv.
324
+ - **The brand-generated keys are the manager's to mint** — `OMEGA_ADMIN_KEY`,
325
+ `OMEGA_WEBHOOK_KEY`, `OMEGA_NAMESPACE` and `UNSUBSCRIBE_HMAC_KEY` have no dashboard
326
+ behind them, so the onboard stub writes them for a fresh brand and the workspace
327
+ service's `env-keys` step mints any the cascade doesn't serve on every manage
328
+ ([#569](https://github.com/Omega-JS-Stack/omega/issues/569)). The env schema below is
329
+ the one list feeding both, a company-served value is never shadowed by a brand-level
330
+ one, and nothing prints a minted value.
331
+ - Missing files and unreadable/stale markers skip silently — `loadEnv` never throws for
332
+ an absent layer.
333
+
334
+ ## The env schema (`src/env-schema.js`) — #581
335
+
336
+ The omega.json5 schema's sibling: ONE inventory of the env keys OMEGA needs — who owns
337
+ each, which targets read it, whether OMEGA mints it or a human pastes it from a third
338
+ party, whether it is required, and what it does. Everything that used to hand-keep its
339
+ own list derives from it, so a new key is **one entry**, never four edits.
340
+
341
+ ```js
342
+ {
343
+ name: 'OMEGA_ADMIN_KEY', // the env var (SCREAMING_SNAKE)
344
+ match: /^CONNECTIONS_.+$/, // …or a pattern, for dynamic families
345
+ owner: 'workspace', // the manager service that owns it
346
+ // ('backend' = the framework itself)
347
+ targets: ['backend'], // the targets whose runtime READS it
348
+ group: 'omega', // its ENV_GROUPS bucket (.env file order)
349
+ generated: () => randomBytes(32)…, // the function that MINTS a value
350
+ default: 'value', // …or a static default, where one applies
351
+ secret: true, // never printed, never in omega.json5
352
+ required: true, // absent = the backend refuses to boot
353
+ delivery: { backend: 'env' }, // per target, HOW the value gets there
354
+ requiredWhen: 'captcha.providers…', // non-empty when this config path is truthy
355
+ publicAtRest: true, // sanctions a 'bake' (readable in the artifact)
356
+ machineLocal: true, // this machine's fact — never published to CI
357
+ description: 'What the key drives.',
358
+ }
359
+ ```
360
+
361
+ - **`generated:` is the mint switch.** Those keys have no dashboard behind them, so the
362
+ manager writes them into a brand `.env` — at onboard, and on every manage that finds
363
+ one missing. Everything else is a credential a human provides.
364
+ - **Only a key OMEGA can produce may be `required`.** Refusing every boot over a secret
365
+ nobody can mint would be a hostage note, not a guard — the config test pins it.
366
+ - **`targets:` is the composition domain.** Every verb composes its target's RUNTIME env
367
+ (the backend's staged `dist/.env` — the only artifact that ships and so cannot walk up to
368
+ the brand layer) from the file layers, taking the entries whose `targets` name that
369
+ target. The schema is the only filter: no hand list, and PATTERN entries
370
+ (`match:`, e.g. the `CONNECTIONS_*` family) compose exactly like named ones. A key the schema
371
+ does not name for a target never reaches it — a desktop signing key stays out of the
372
+ functions upload. Other targets read brand values through the cascade above at runtime,
373
+ so nothing is written for them.
374
+ - **`deliverAs:` renames on delivery**: the entry's brand-level name is what the cascade
375
+ carries (`GOOGLE_ANALYTICS_SECRET_BACKEND`), and the target receives it under the name
376
+ its own code reads (`GOOGLE_ANALYTICS_SECRET`). One entry, both names.
377
+ - **`delivery:` says HOW a value reaches each target**
378
+ ([#627](https://github.com/Omega-JS-Stack/omega/issues/627)): `'env'` (read from the
379
+ composed `.env` at runtime — the backend), `'ci'` (the generated workflow injects it
380
+ into the runner env for the build step), or `'bake'` (the build writes it into the
381
+ shipped artifact, because the installed app runs with no `.env`). A bake implies the
382
+ CI injection — the workflow delivers the value the build then bakes. One renderer in
383
+ `@omega.js/config/env-delivery` derives everything from these declarations: each
384
+ target's workflow secrets block, its bake list, and its publish-step secret set. No
385
+ hand-kept `${{ secrets.KEY }}` list survives anywhere.
386
+ - **A baked key is public at rest.** Anyone who unpacks the app can read it, so a
387
+ `secret: true` entry may only bake when it also declares `publicAtRest: true` — the
388
+ renderer THROWS otherwise, on every lane, so a real credential can never reach an
389
+ artifact by accident. The GA Measurement Protocol secrets are the sanctioned baked keys.
390
+ - **`machineLocal: true` marks this machine's own facts** (`OMEGA_FONTAWESOME_ROOT`):
391
+ composed locally like any key, but filtered out of every rendered block and every
392
+ published secret set — a laptop path has no business in CI.
393
+ - **`requiredWhen: '<config path>'` is the conditional presence rule**
394
+ ([#626](https://github.com/Omega-JS-Stack/omega/issues/626)): when the resolved config
395
+ path is truthy, the key must be non-empty. Presence only, never a value-shape check,
396
+ and one-directional. One checker, `checkEnvRules()` in `@omega.js/config/env-rules`,
397
+ answers it for every consumer: the backend boot (production refuses, development warns
398
+ once), the desktop and extension build bakes (build mode throws, development warns),
399
+ and the manager's manage walk (warns per enabled target, never fails). A TARGET-LESS
400
+ entry (`targets: []` — a SERVICE's own key, like `SENTRY_AUTH_TOKEN`) is owed when its
401
+ path is truthy in the brand root **or in ANY enabled target's resolved config**
402
+ ([#683](https://github.com/Omega-JS-Stack/omega/issues/683)): the service writes its
403
+ values where the target lives — monitoring lands one Sentry DSN per surface and leaves
404
+ the shared slot null — so a root-only read never fired on the shape brands carry.
405
+ Violations name the BRAND-level key — the one a human sets in the brand `.env`.
406
+ - **Nothing is CLEARED, because nothing is written into a hand file**
407
+ ([#636](https://github.com/Omega-JS-Stack/omega/issues/636),
408
+ [#678](https://github.com/Omega-JS-Stack/omega/issues/678)): the composed artifact is
409
+ rebuilt from the cascade every verb, so a credential the brand root retires stops being
410
+ served the moment it is dropped.
411
+ - **`group:` picks the .env section**, and `ENV_GROUPS` owns the file order plus each
412
+ section's comment. A group marked `file: false` (the `runtime` group) never reaches a
413
+ brand `.env` at all — those keys resolve some other way (from config at boot, from the
414
+ developer's own shell), so they are neither rendered as placeholders nor composed.
415
+ - **Runtime/platform vars are NOT in the schema**: `FIREBASE_CONFIG`,
416
+ `FUNCTIONS_EMULATOR`, `GCLOUD_PROJECT`, the `OMEGA_*_PORT` map, the test-mode
417
+ switches. They are the runtime's facts about itself, not a brand's credentials, and
418
+ the frameworks read them directly.
419
+
420
+ ### One key, one entry — the environment supplies the value — #586
421
+
422
+ A key whose value must differ between a local run and a deployed one is **not a second
423
+ schema entry**. Public payment keys already split per machine through the config merge
424
+ chain's local layer; secrets now split the same way through the `.env` cascade's
425
+ environment overlay above — the brand puts the test credential in `.env.development`
426
+ under the SAME name. The backend uses whatever key the env chain resolves, nothing more:
427
+ the rule, and the `.env.development` advice that follows from it, live in
428
+ [docs/backend/index.md](../backend/index.md) (the payment-keys paragraph).
429
+
430
+ The schema declares WHAT a brand supplies, never which environment supplies it: `env.js`
431
+ owns that. So `STRIPE_SECRET_KEY`, `PAYPAL_CLIENT_SECRET`, `CHARGEBEE_API_KEY` and
432
+ `COINBASE_COMMERCE_API_KEY` (asked for through the setup contract, #608, only when
433
+ `payment.providers.coinbase.enabled` is on) are one entry each, every provider library
434
+ reads the one name, and there is no `<KEY>_DEV` twin, no live-shape guard, and no
435
+ payment-specific rule anywhere (ruled 2026-08-26; the twin system was replaced before it shipped, so
436
+ nothing migrates).
437
+
438
+ ### One key per AI provider — #639
439
+
440
+ `OPENAI_API_KEY` and `ANTHROPIC_API_KEY`. There is no second name for either.
441
+
442
+ | Key | Owner | Read by | Asked at |
443
+ |---|---|---|---|
444
+ | `OPENAI_API_KEY` | `backend` | Contact inference, content + newsletter generation, the `chatgpt` translation provider | `manage` — the `ai` service's setup gate |
445
+ | `ANTHROPIC_API_KEY` | `backend` | The backend's Anthropic provider (SVG generation, tool loops) | `manage` — the `ai` service's setup gate |
446
+
447
+ - **The company-wide fallback is the COMPANY LAYER, never a second key** (Ian
448
+ 2026-08-27). The legacy pair (`BACKEND_MANAGER_OPENAI_API_KEY`, then
449
+ `OMEGA_OPENAI_API_KEY`) existed so one company key could serve every brand; the
450
+ `.env` cascade already does that — put the value in the company `.env` and every
451
+ brand under it resolves it, with a brand `.env` overriding. The prefixed names are
452
+ gone from the schema and every reader; migration row in
453
+ [breaking-changes.md](breaking-changes.md).
454
+ - **Both are optional and neither gates a run.** The `ai` service declares them
455
+ `gates: false`, so preflight never blocks on them and a brand that calls one
456
+ provider is never nagged about the other. `ai.enabled: false` (what the gate's
457
+ Disable lands) stops the ask for good.
458
+
459
+ Who derives from it:
460
+
461
+ | Lane | What it takes |
462
+ |---|---|
463
+ | `@omega.js/manager` workspace `env-keys` + the onboard `.env` stub | `generatedEnvKeys()` — name → the function that mints a value |
464
+ | `@omega.js/manager` `lib/env-order.js` (canonical .env order) | `envFileGroups()` + `envKeysByGroup()` — the sections, their comments, their keys |
465
+ | `@omega.js/config` `composeTargetEnv()` (the delivery composition every verb runs) | `ENV_SCHEMA` + `envFileGroups()` — a brand key rides down when some entry claims it (by `name` or by `match`), its `targets` include the target, and its group renders into a file; `deliverAs` is applied on arrival, and each layer's `.env.<environment>` overlay composes above its own base (#586) |
466
+ | `@omega.js/config` `envKeysForTarget(target)` (the rendering lane's list) | `ENV_SCHEMA` + `envFileGroups()` — the NAMED keys a target reads, which placeholders a brand `.env` carries |
467
+ | `@omega.js/backend` `libraries/env.js` (the one reader) | `envSchemaEntry()` for every read, `requiredEnvKeys('backend')` for the boot guard, and `envEnvironment()` re-exported as `env.environment()` ([docs/backend/index.md](../backend/index.md)) |
468
+ | `@omega.js/manager` `lib/scaffold.js` (the onboard stub) + `lib/gitignore.js` (the heal) | `ENV_ENVIRONMENTS` — one empty `.env.<environment>` per name, and the `.env.*` ignore |
469
+ | `@omega.js/config` `env-delivery.js` (the one delivery renderer) | `delivery` + `deliverAs` + `machineLocal` + `publicAtRest` — each target's workflow secrets block, bake list, and publish-step secret set; web and extension render their workflow token from it, desktop's ensure-target template pass does the same, and all secret publishers send exactly its set |
470
+ | `@omega.js/config` `env-rules.js` (the one presence checker) | `required` + `requiredWhen` — the violations the backend boot, the desktop/extension bakes, and the manager's manage walk act on, each at its own severity |
471
+
472
+ ## Owner hooks (`config/hooks/`) — cp91
473
+
474
+ `src/hooks.js` — owner-supplied code the frameworks call at named hook points, so
475
+ company-specific logic lives in the OWNER'S tree, never in framework source. Layout is
476
+ **nested, mirroring the call site** (Ian's directive): the account service's password
477
+ step loads `config/hooks/account/password.js`; a future onboarding hook would live under
478
+ `config/hooks/onboard/…` — one file per hook point, path = the invoking structure.
479
+
480
+ - **Home = `config/`, versioned by default** (Ian 2026-07-11): hooks are AUTHORED code
481
+ and sit with the other owner-authored omega inputs (omega.json5, seo.json5, chatsy.md,
482
+ …) — never under machine-owned, gitignored `.omega/`, where a hook lost on a fresh
483
+ clone would silently change behavior (passwords falling back to the seed channel and
484
+ rotating). Secrets still belong in `.env` — a hook that needs one reads `process.env`;
485
+ to keep a hook out of git anyway, add your own `config/hooks/` ignore line.
486
+ - **Resolution order**: the brand root's own `config/hooks/<point>.js`, else the company
487
+ root's (via the `.omega/company.json` stamp) — a company-wide hook covers every brand,
488
+ a single brand can still override it.
489
+ - **Contract**: plain CJS, `module.exports = ({ … }) => …` (async fine). Each call site
490
+ documents its hook's signature/return. Absent hook → `loadHook` returns null and the
491
+ caller uses its default behavior; a hook that EXISTS but is broken (unloadable,
492
+ non-function export, bad return) throws — an owner who wrote a hook never gets silent
493
+ fallback.
494
+ - **First (and so far only) hook point**: `account/password` —
495
+ `({ email, domain, apex, brand }) => password` (string ≥ 6 chars), letting a company
496
+ formula generate per-brand passwords without ever living in a repo the framework ships.
497
+
498
+ ## Port auto-allocation (N7)
499
+
500
+ `src/ports.js` — classic defaults, probe at boot, per-port +1 bump only when taken, so
501
+ multiple brands run dev stacks concurrently. Single-brand dev with free defaults is
502
+ byte-identical to the pre-N7 behavior (no bumping, no artifacts).
503
+
504
+ - **`CLASSIC_PORTS`** — the historical defaults (functions 5001, hosting 5002, firestore
505
+ 8080, auth 9099, database 9000, storage 9199, pubsub 8085, ui 4050, website 4000,
506
+ livereload 35729, cdp 9222).
507
+ - **`resolvePorts({ wanted, pins, claimed })`** — each wanted port keeps its value when
508
+ free, bumps +1 until free when taken (shared `claimed` set prevents two names landing
509
+ on one port). `pins` (the config `ports` section) never bump — a busy pin throws.
510
+ The free-check is a QUADRUPLE bind-probe (127.0.0.1, ::1 when the host has IPv6,
511
+ the IPv4 wildcard 0.0.0.0, and the `::` wildcard) — on macOS/BSD, wildcard and
512
+ specific-address listeners COEXIST on one port, and the two wildcard FAMILIES
513
+ coexist with each other, so any single-surface probe false-positives against a
514
+ sibling brand's stack and the real bind crashes later (found live at cp186: the
515
+ playground's https proxy holds IPv6 `*:5002`; a 127.0.0.1-only probe handed 5002
516
+ to the second brand's functions emulator — and again at #345: a foreign `0.0.0.0`
517
+ squatter read free to the `::` probe and the auth emulator died with no bump).
518
+ - **Ports file** — `writePortsFile(projectDir, ports, facts)` /
519
+ `readPortsFile/clearPortsFile(projectDir)`: `<projectDir>/.temp/ports.json`
520
+ (pid-stamped; readers ignore dead-pid leftovers). The allocator (the backend emulator
521
+ boot) writes it; siblings of the same brand (`omega test` against a running emulator,
522
+ the e2e harness) read it; cleared on clean shutdown. `facts` publishes the resolved
523
+ NON-port facts beside the map — today `origin`, the website's dev origin
524
+ ([#262](https://github.com/Omega-JS-Stack/omega/issues/262)).
525
+ A consumer process targets `https`, the PUBLIC origin under the local certificate,
526
+ and never `hosting`, the internal plain-http port the mkcert proxy forwards to; every
527
+ `omega dev` leg trusts that certificate through `NODE_EXTRA_CA_CERTS` ([#795](https://github.com/Omega-JS-Stack/omega/issues/795)).
528
+ - **Sibling map** — `readSiblingPorts(targetDir)` merges every OTHER target's live ports file
529
+ in the same brand (a running backend's emulator map) for the target that asks;
530
+ `readSiblingOrigin(targetDir)` reads the published dev website origin the same way. Read
531
+ at USE time, never cached: the file appears when the backend boots and changes when it
532
+ restarts.
533
+ - **Env channel** — `portsToEnv(ports)` → `OMEGA_<NAME>_PORT` vars injected into spawned
534
+ children; `envPort(name)` reads one, `envPorts(env)` reads the whole map back out.
535
+ URL getters resolve env → classic default.
536
+ - **Browser channel (cp89, [#300](https://github.com/Omega-JS-Stack/omega/issues/300))** —
537
+ browser code can read neither env nor files, so a surface BAKES the map into its
538
+ client config: `omega dev` writes `dev: { ports }` into the Configuration chrome
539
+ PER RENDER (its resolved website port with the sibling backend's map merged over it),
540
+ and its auth-emulator proxy resolves the target port per REQUEST. The render-time
541
+ bake is ADVISORY ([#346](https://github.com/Omega-JS-Stack/omega/issues/346)): the
542
+ dev server REWRITES that chrome in every HTML response as it serves it, resolving the
543
+ sibling maps per request, because the normal boot order builds the whole page fleet in
544
+ under a second while the emulator suite seeds for minutes — nothing under `src/`
545
+ changes when it lands, so no page would ever re-render onto it. Mid-session emulator
546
+ restarts onto bumped numbers ride the same lane. Built `dist/` output is untouched on
547
+ disk. Desktop (`OMEGA_BUILD_JSON.config.dev`) and
548
+ extension (`OMEGA_BUILD_JSON.config.dev`, baked into every bundle) bake the same map at build time, from
549
+ the sibling file plus the env channel; production builds bake none. Drivers serving a
550
+ STATIC build set `window.__OMEGA_DEV_PORTS__` (the devkit e2e harness — the site
551
+ builds before the emulator boots), which is a FALLBACK: it fills only what a page's
552
+ chrome omits, so a side channel no real browser has can never hide a broken real one.
553
+ The dev WEBSITE ORIGIN rides the same map as one more resolved fact
554
+ ([#262](https://github.com/Omega-JS-Stack/omega/issues/262)): `omega dev` publishes
555
+ `dev.origin` (protocol AND port — the mkcert proxy fronts the public port by default,
556
+ so a port number alone cannot say the scheme) into the chrome and into its ports file,
557
+ and desktop/extension bake it from that file on their existing lanes. The extension
558
+ manifest's `externally_connectable` dev entry resolves from it at package time —
559
+ nothing hardcodes a dev origin any more. `@omega.js/client`'s `getDevWebsiteOrigin()`
560
+ is the one getter that answers it, falling back to the classic `https://localhost:4000`
561
+ with the same out-loud warning the ports take.
562
+ `@omega.js/client` resolves chrome `dev.ports` → runtime global → classic defaults,
563
+ and warns loudly (dev only) naming every port it had to assume; its dev `getApiUrl`
564
+ speaks plain http to a mapped `hosting` (the emulator serves http), https to a mapped
565
+ `https` (`mgr serve`'s mkcert proxy), and keeps the classic
566
+ `https://localhost:5002` serve assumption when no map was provided. Dev mode
567
+ resolves the LOCAL stack for every source, including `source: 'company'` —
568
+ `company.url` is a production concept, and dev deliberately makes no live server
569
+ hits (ratified, Ian 2026-08-03, [#34](https://github.com/Omega-JS-Stack/omega/issues/34)).
570
+ - **Website port (cp89)** — `omega dev` allocates through the same model: classic
571
+ **4000** (pre-N7 it defaulted to 8080, colliding with the SAME brand's firestore
572
+ emulator), bump when taken, `--port` flag or config `ports.website` pins; publishes
573
+ its own ports file in the website target dir.
574
+ - **Serve + per-target ports (cp90)** — `mgr serve` allocates through the same model
575
+ (`--port` pins, taken bumps — the old kill-the-incumbent check is gone) and PUBLISHES
576
+ its map: `https` (the mkcert proxy) + `hosting` (the internal plain-http
577
+ firebase-serve port), so `omega dev` bakes even a bumped serve into the chrome and
578
+ Stripe webhook forwarding targets the port that actually speaks http (it used to aim
579
+ plain http at the TLS proxy). Desktop + extension `serve` allocate `livereload` — two
580
+ targets of one brand land on distinct ports — and desktop allocates `cdp` when
581
+ requested (`OMEGA_CDP_PORT` set); desktop URL getters mirror the backend's
582
+ env-channel reads (`https` → mkcert, `hosting` → plain http, classic otherwise).
583
+ The manager's Google-OAuth loopback binds an EPHEMERAL port (`listen(0)`, RFC 8252)
584
+ instead of pinning 9876. The backend's `getWebsiteUrl` returns
585
+ `http://localhost:4000` unless its own mkcert proxy is up (plain http on the public
586
+ port 307s to https, so the link lands either way); desktop's reads the whole dev
587
+ ORIGIN instead (baked `dev.origin`, then `OMEGA_WEBSITE_PORT` composed over https,
588
+ then the classic `https://localhost:4000`), matching the browser-side answer scheme
589
+ and all ([#747](https://github.com/Omega-JS-Stack/omega/issues/747)). The BROWSER-side answer
590
+ is `getDevWebsiteOrigin()`, which needs the exact origin and takes it from the
591
+ resolved map (#262). Packaged
592
+ extension/desktop artifacts keep BUILD-TIME-BAKED ports by design (a shipped
593
+ extension can't probe); the extension manifest's dev-website origin documents that
594
+ inline.
595
+ - **Config `ports` section** (schema, optional object) — explicit pins for any port name;
596
+ unset = auto-allocate.
597
+ - When a boot bumps emulator ports, the backend CLI materializes
598
+ `firebase.resolved.json` next to firebase.json (same dir, so relative paths keep
599
+ resolving) and boots firebase-tools with `--config`; the committed firebase.json never
600
+ changes. Gitignored; removed on shutdown.
601
+
602
+ Design + slice plan: [_attic/plans/archive/n7-port-allocation.md](../../_attic/plans/archive/n7-port-allocation.md).
603
+
604
+ ## Validation
605
+
606
+ `validateConfig(config, { target })` = shared schema + that target's refinements
607
+ (`TARGET_SCHEMAS[target]`), run against the RESOLVED config. `brand.id` (URL-scheme-safe
608
+ slug) and `brand.name` are the only universally required fields.
609
+
610
+ **Undeclared keys WARN** ([#636](https://github.com/Omega-JS-Stack/omega/issues/636)):
611
+ every leaf path of the resolved config no rule declares comes back as ONE warning naming
612
+ them — never an error, because a brand config that outlives a framework version must still
613
+ build. A rule of type `object`/`array` declares its whole subtree (a brand's postal
614
+ address, an open provider map), and the `targets` namespace is exempt: those keys belong to
615
+ a framework, or to a custom target. A finding is a hole to fill — either the key is dead,
616
+ or the schema owes it a rule.
617
+
618
+ **authDomain is the brand's own host** (cp268): when `cloud.config.authDomain` is set it
619
+ must equal the BRAND host, `brand.url`, for every instance a brand runs
620
+ ([#588](https://github.com/Omega-JS-Stack/omega/issues/588)): one Firebase project, one
621
+ backend, one authDomain. The instance's own `url` is not read here (a
622
+ `targets/website-admin` load would otherwise fail its own brand's authDomain), and the
623
+ top-level `url` is only the fallback for a config carrying no `brand.url` at all. A
624
+ `*.firebaseapp.com` value hard-fails (self-hosted `/__/auth/*` on the brand
625
+ host is what keeps redirect sign-in working under browser storage partitioning; the web
626
+ build emits those helper files), and any other mismatch fails naming both values. Absent
627
+ passes, and `demo-*` (emulator-only) projects are exempt.
628
+
629
+ **A product price is a bare NUMBER** ([#674](https://github.com/Omega-JS-Stack/omega/issues/674)):
630
+ every entry of `payment.products[].prices` must be a number — `once: 49.99`, never
631
+ `once: { amount: 49.99 }` — and the object shape is a config ERROR naming the product and
632
+ the key. The two sides did not read it the same: the checkout page's resolver unwrapped
633
+ `{ amount: N }` while the backend's confirmation URL and all three provider libraries took
634
+ the bare number, so an object-shaped price rendered a correct order summary and reached
635
+ the confirmation URL as `[object Object]`. There is no shared resolver to settle it in —
636
+ the browser bundle cannot reach a build-time package, and the deployed backend runtime
637
+ carries none either — so the SHAPE is settled here, in the one place both sides' catalog
638
+ comes from. (`prices.amount` as a KEY is a different thing, a legacy one-time spelling the
639
+ checkout still reads; its value is a number like every other.)
640
+
641
+ ## The features catalog (`features`) and a product's values — #647
642
+
643
+ A feature is **defined once**, at the top level, and a product names only its **value**.
644
+ The two halves cannot disagree, because there is only one place a name, an icon or a
645
+ definition can be written:
646
+
647
+ ```json5
648
+ {
649
+ features: {
650
+ saves: {
651
+ name: 'Saves',
652
+ icon: 'feather',
653
+ definition: 'Notes, clips, and pages you can save per month.',
654
+ usage: { pace: 'daily', mirror: ['teams'] },
655
+ },
656
+ templates: { name: 'Page templates', icon: 'palette', usage: { pace: false } },
657
+ support: { name: 'Priority support', icon: 'headset', definition: 'Your tickets jump the queue.' },
658
+ },
659
+
660
+ payment: {
661
+ products: [
662
+ { id: 'basic', name: 'Basic', features: { saves: 100 } },
663
+ { id: 'premium', name: 'Premium', features: { saves: 10000, templates: 40, support: true } },
664
+ { id: 'pro', name: 'Pro', features: { saves: -1, templates: 120, support: true } },
665
+ ],
666
+ },
667
+ }
668
+ ```
669
+
670
+ ### The catalog
671
+
672
+ | Key | Type | What it does |
673
+ |-----|------|--------------|
674
+ | `features.<id>.name` | string, **required** | The label every surface prints: pricing rows, the comparison matrix, the account's usage bars |
675
+ | `features.<id>.icon` | string | Font Awesome icon name ([icons.md](icons.md)) |
676
+ | `features.<id>.definition` | string | The dotted-underline tooltip. Authored ONCE — every card, row and bar renders this one |
677
+ | `features.<id>.usage` | object | Its presence makes the feature **counted** (metered per user). Absent = a **perk**, never counted |
678
+ | `features.<id>.usage.pace` | `'daily'` \| `false` | Day pacing is the DEFAULT. `false` opts out to a plain monthly counter |
679
+ | `features.<id>.usage.mirror` | string[] | Document KINDS this feature's counters also land on, resolved from `user.owns.<kind>` — declared here, never at a call site |
680
+
681
+ **Key order is row order.** The pricing page's rows, the comparison matrix and the
682
+ account's usage bars all render the catalog in the order it is written.
683
+
684
+ ### A product's values
685
+
686
+ `payment.products[].features` is a MAP of `<catalog id>: value`:
687
+
688
+ | The feature is | Its value is | Renders as |
689
+ |---|---|---|
690
+ | Counted | a number — the **monthly limit** | `100 Saves` |
691
+ | Counted | `-1` — unlimited | `Unlimited Saves` |
692
+ | Perk | `true` | a check, name only |
693
+ | Perk | a string | `24/7 Support` |
694
+ | Either | `false` (or absent) | nothing — the tier does not include it |
695
+
696
+ The validator fails **a number on a perk** (it would draw a usage bar against a limit no
697
+ gate enforces) and **a perk value on a counted feature** (the gate would read it as zero,
698
+ so the plan advertises the feature and every call refuses it). It also fails a value on an
699
+ id the catalog does not define, because nothing reads it — the same silence a retired key
700
+ used to buy.
701
+
702
+ ### What this replaces
703
+
704
+ `payment.products[].limits`, the per-product `features` ARRAY, and the product-wide
705
+ `rateLimit` are **retired** — all three are validation errors naming their replacement.
706
+ The cross-product definition BACKFILL retires with them: nothing repeats, so nothing needs
707
+ unifying. The mapping is in [breaking-changes.md](breaking-changes.md).
708
+
709
+ ### Top-level `usage` is reserved
710
+
711
+ `usage` at the top level is reserved for **counting settings** (an anonymous-store mode, a
712
+ reset hour) and carries **no key today**. It is not a second home for the catalog. The
713
+ backend's gate reads `features`; how a user's counters behave is
714
+ [packages/backend/docs/usage-rate-limiting.md](../../packages/backend/docs/usage-rate-limiting.md).
715
+
716
+ ## User connections (`connections`) — #771, #788, #792, #793
717
+
718
+ Every key under `connections` is a PROVIDER a brand's users may link from the account page
719
+ — `google`, `discord`, `spotify`, `twitch`, `kick` ship with `@omega.js/backend`, and
720
+ any other one is a file the brand writes at `targets/backend/src/connections/<name>.js`
721
+ (the lane loads that directory before its own). So the section is free-form by design;
722
+ nothing here is required, and these are the keys an entry may carry:
723
+
724
+ | Key | What it does |
725
+ |---|---|
726
+ | `enabled` | Whether the connection is offered on the account page. The five PACKAGED providers are `false` in the framework defaults — turning one on is the brand's act, since a card with no `CONNECTIONS_<PROVIDER>_CLIENT_ID` behind it could connect nothing. A brand's OWN provider is on unless this is `false` |
727
+ | `scope` | An array that WINS over the provider module's default scope |
728
+ | `name` | The card's title on the account page |
729
+ | `logo` | What the card draws: the NAME of a mark `@omega.js/web` ships (`core/logos/brandmarks/original/<name>.svg`, drawn inline), or a full URL (drawn as an `<img>`). A value carrying a `/` or a `:` is a URL; anything else is a mark name |
730
+ | `description` | The line under the title |
731
+
732
+ Every key is a PROVIDER NAME, so it is strictly `[a-z0-9-]` — the same rule the
733
+ backend's confined loader enforces. A key outside it can never resolve to a
734
+ provider, and the account page says so on the card instead of offering a
735
+ connection that could only fail.
736
+
737
+ **This section is the ONLY card list** ([#792](https://github.com/Omega-JS-Stack/omega/issues/792)):
738
+ every entry carrying a `name` and a `logo` renders a card, and the account layout's old
739
+ `connections:` frontmatter rows — which shadowed a brand's own entry and pointed at CDN
740
+ files that 404 — are gone. The framework's defaults carry `name`, `logo` and `description`
741
+ for the five packaged providers, so `google: { enabled: true }` is a complete card and any
742
+ key a brand writes wins through the ordinary merge chain. Those defaults RESOLVE only
743
+ (`materialize: false`, above): they are never copied into a brand's file. An enabled
744
+ provider with no `name` + `logo` after the merge gets the "unsupported connection" card,
745
+ which says exactly that.
746
+
747
+ Secrets never live here: the credentials are the `CONNECTIONS_<PROVIDER>_CLIENT_ID` /
748
+ `CONNECTIONS_<PROVIDER>_CLIENT_SECRET` pair in the `.env` (the provider name uppercased,
749
+ dashes as underscores). The full provider contract — the module shape, the ONE context
750
+ every step takes, `pkce: 'S256'`, the route-owned identity uniqueness, and the `type` every
751
+ stored record carries — is `packages/backend/docs/connections.md`.
752
+
753
+ ## The cancel-flow save offer (`payment.winback`) — #268
754
+
755
+ When a customer starts cancelling a PAID subscription, the billing card pitches a
756
+ discount on the next cycle before it asks them why they are leaving. Accepting applies
757
+ the discount through the provider's own coupon plumbing, calls the cancel off, and
758
+ leaves the saving on the card (what comes off, and which bills it comes off — #325);
759
+ declining opens the cancellation questionnaire unchanged.
760
+
761
+ The pitch is made per cancel ATTEMPT, never once per session (#324): a customer who
762
+ declines, closes the questionnaire and comes back to cancel meets the offer again,
763
+ because nothing about their subscription changed. Only a CLAIM (the discount is applied,
764
+ and the backend refuses a second one) or a refusal no retry fixes ends it.
765
+
766
+ An accepted offer is recorded in TWO places, on purpose (#325). `payments-orders/{orderId}`
767
+ `.requests.winback` is the offer's MEMORY — what a second accept is refused against — and
768
+ the account carries the discount ITSELF at `subscription.discount`, shaped like every other
769
+ discount in the payment stack (`{ valid, code, percent | amount, duration }`) plus a
770
+ `source`. That is what the billing card renders on a later visit; without it the saving
771
+ disappeared on the next page load. `source` is the whole reason it is safe to read as a
772
+ claim: today only the winback claim writes this node, and `source` is what keeps the
773
+ read safe when checkout discounts start writing it too, because only
774
+ `source: 'winback'` says this customer already took the save offer.
775
+
776
+ **The claim ends with the subscription it was made on (#333).** The account's node also
777
+ carries `resourceId`, the subscription the discount was applied to, stamped at claim
778
+ time. The unified webhook write carries no discount key, so a merge would otherwise keep
779
+ the node forever: a customer who churned and resubscribed carried a spent claim into the
780
+ NEW subscription, where `source: 'winback'` reads as "already claimed" and the save offer
781
+ is silently never pitched again. Every subscription-resource webhook compares the stamp against
782
+ the subscription the event is about and CLEARS the node on a mismatch (a new subscription
783
+ is a clean slate), while same-subscription traffic (renewals, cancellations, plan
784
+ changes) leaves the saving exactly as claimed. A node with no stamp predates it and
785
+ nothing can prove it belongs to an older subscription, so it is read as riding the one it
786
+ is found on and stamped there: no live discount is taken away on a guess, and it clears
787
+ on the next resubscribe like any other. CONSUMPTION is not cleared: a spent `once` coupon
788
+ still reads as applied until the subscription changes, because no provider's unified
789
+ shape reports whether the coupon is still attached (#333).
790
+
791
+ The offer is the brand's, and **a brand that writes nothing gets one anyway**: 50% off
792
+ the next cycle, that cycle only.
793
+
794
+ | Key | Type | Default | What it does |
795
+ |-----|------|---------|--------------|
796
+ | `payment.winback.enabled` | boolean | `true` | The whole off switch. `false` skips the pitch, and the cancel meets the data-retention warning instead (#341) |
797
+ | `payment.winback.percent` | integer 1-100 | `50` | Whole percentage off. Mutually exclusive with `amount` |
798
+ | `payment.winback.amount` | number > 0 | — | Flat amount off in `payment.currency`'s major unit (`10` = $10). Mutually exclusive with `percent` |
799
+ | `payment.winback.duration` | `'once'` \| `'forever'` | `'once'` | `once` discounts the next cycle only; `forever` is a permanent price cut |
800
+
801
+ Setting **both** `percent` and `amount` is a validation error: a coupon is one shape or
802
+ the other everywhere in the payment stack, and two shapes on one offer has no honest
803
+ reading.
804
+
805
+ `resolveWinbackOffer(payment)` is the ONE home of these defaults. The backend's
806
+ `POST /payments/winback` route resolves the brand's section through it, and the web
807
+ build bakes the same call into the client blob (`site.client.payment.winback`), so the
808
+ dialog the customer reads and the coupon the provider creates can never name different
809
+ numbers — the browser never applies a default of its own.
810
+
811
+ Not every provider can discount a subscription that is already running. Stripe can
812
+ (the coupon plumbing the checkout already uses); PayPal has no discount object at all
813
+ and Chargebee has no way to reach a live subscription with one through existing
814
+ plumbing. Those two refuse with `not-supported-by-provider`, and the billing card
815
+ retires the offer for the session and opens the questionnaire — a subscriber can always
816
+ still cancel.
817
+
818
+ ## Consent (`client.consent`) — #383
819
+
820
+ The consent banner is a real GATE, so its config is schema-known even though the rest of
821
+ the `client` blob is not: a typo that silently disabled it would ship a site with no
822
+ consent surface and no error.
823
+
824
+ ```json5
825
+ targets: {
826
+ web: {
827
+ client: {
828
+ consent: {
829
+ enabled: true, // default true; false ships NO banner
830
+ config: {
831
+ position: 'bottom-left', // bottom-left | bottom-right | bottom
832
+ content: {
833
+ message: 'We use cookies … See our { terms }.', // the banner face
834
+ panelIntro: 'We and our partners … See our { cookies } and { terms }.',
835
+ accept: 'Accept', // the big grant
836
+ customize: 'Customize', // opens the panel
837
+ acceptAll: 'Accept all', // the panel's pair
838
+ acceptNone: 'Accept none',
839
+ },
840
+ // `{terms}` and `{cookies}` link the terms and cookie-policy pages.
841
+ // `save` retired with the Save button (#391) — a config still setting
842
+ // it is ignored, not an error.
843
+ },
844
+ },
845
+ },
846
+ },
847
+ }
848
+ ```
849
+
850
+ Two things are deliberately NOT config:
851
+
852
+ - **The regime.** The visitor's browser timezone picks it — the strict opt-in roster (plus an
853
+ unplaceable timezone) gets opt-in, where no provider script loads until they accept; everywhere
854
+ else gets opt-out, where the scripts load and a first visit sees only the Cookies Settings
855
+ tab ([#391](https://github.com/Omega-JS-Stack/omega/issues/391)). There is no key that
856
+ forces one, because the answer is legal, not stylistic.
857
+ - **The colors.** The panel paints itself from the `--omega-*` token sheet, which is the
858
+ only way it is correct in both color modes. The old `palette`/`theme` keys are gone.
859
+
860
+ `enabled: false` is legal only for a site that loads no analytics or marketing provider
861
+ at all — the gate and the banner are the same switch.
862
+
863
+ ## Feature gating polarity (#527)
864
+
865
+ Ratified 2026-08-24 (Ian). A feature has ONE switch with ONE polarity, and which
866
+ polarity it is follows from what the feature needs — never from taste at the read
867
+ site. Three cases:
868
+
869
+ | Case | The switch | The read | Examples |
870
+ |------|-----------|----------|----------|
871
+ | **1. Data-bearing** — the feature cannot run without a value only the brand can supply | that DATA's presence | `if (value)` | `advertising.providers.adsense.client`, `monitoring.providers.sentry.dsn`, `analytics.providers.*.id`, `edge.providers.cloudflare.zone`, `advertising.providers.inhouse.source` |
872
+ | **2. Zero-data** — the framework can run it for every brand with no input | `enabled`, default ON | `value !== false` | `forms.providers.slapform.enabled`, `inbound.chat.providers.chatsy.enabled`, `repo.providers.github.enabled`, `search.providers.searchConsole.enabled`, `edge.providers.cloudflare.enabled`, `targets.web.meta.index` |
873
+ | **3. Consequential** — it costs money, publishes to the world, or is irreversible | `enabled`, default OFF | `value === true` | `directory.enabled`, `devlog.enabled`, `targets.desktop.platforms.mac.mas.enabled` |
874
+
875
+ - **A block by itself NEVER enables.** Authoring `providers: { adsense: {} }` is an opt-IN
876
+ to being asked, not an ON — the case-1 data or the case-2/3 `enabled` still decides.
877
+ - **Never a second switch on one feature.** Two switches let a config say ON to one half
878
+ of the stack and OFF to the other: adsense carried exactly that (the manager gated on
879
+ `enabled`, the site on `client` + `units`), so `{ client, enabled: false }` stopped the
880
+ account being managed while the site kept serving ads off it. #527 collapsed adsense to
881
+ case 1 and deleted both extra gates. A state that needs a second switch to express
882
+ (managed account, ad-free site) is deliberately inexpressible.
883
+ - **Every code-read switch has a schema rule** ([#546](https://github.com/Omega-JS-Stack/omega/issues/546)):
884
+ an undeclared key validates clean, so a typo (`enbaled: false`) silently reads as ON and
885
+ the validator cannot document what the key does. The rule carries the `default:` that
886
+ states the polarity — except where the SECTION is presence-gated (`advertising`), because
887
+ a materialized default would write the section into every brand and switch the feature on
888
+ for brands that configured none; there the ON answer lives at the read site.
889
+
890
+ ## Tri-state provisioning values (#33)
891
+
892
+ Provisioning-flow keys (org, billing account, service/agent ids — anything a manage
893
+ flow can set up interactively) follow ONE contract, enforced by the manager's
894
+ config-flow engine (`packages/manager/src/lib/config-flow.js`):
895
+
896
+ | Value | Meaning |
897
+ |-------|---------|
898
+ | missing / `null` | ASK in an interactive run — the answer lands in omega.json5; without a TTY: warn + skip, aggregated in the run summary |
899
+ | `false` | The user opted OUT — silent skip, never prompt or warn again. `false` on an ancestor section (`inbound.chat.providers.chatsy: false`) opts out every key under it |
900
+ | anything else | Use it |
901
+
902
+ Every ask offers the opt-out (the gate's "Disable" and, in selection flows, an inline
903
+ "No …" choice), so `false` is always reachable; delete the line to be asked again.
904
+ First consumers: `cloud.organizationId` (asked at project creation — pick an org or
905
+ create standalone) and `cloud.billingAccount` (pick/create a billing account or stay
906
+ on Spark). ONE cloud home (#23, reversing the old `gcp`-vs-`firebase` split): the
907
+ platform-level org and billing account, the provisioning switches (`cloud.shared`,
908
+ `cloud.supportEmail`, `cloud.apiSubdomain`) and the app config (`cloud.provider`,
909
+ `cloud.config`) are all `cloud.*`, and the project id has exactly one address —
910
+ `cloud.config.projectId`. `cloud.supportEmail`'s null auto-derives the authorizing
911
+ user's email instead of asking.
912
+
913
+ ## The site global: the curated targets view (#85, #610)
914
+
915
+ `toSiteGlobal()` (the web build's `site.*`) strips the raw `targets` machinery and
916
+ replaces it with a CURATED `site.targets` — an allow-list of display-safe facts per
917
+ declared target, never a spread of the raw config: every entry carries `enabled: true`,
918
+ desktop adds a derived `releasesUrl` plus the per-artifact `downloads` map once releases
919
+ are opted in, extension adds its store `listings`.
920
+
921
+ It is the ONE home of those facts
922
+ ([#610](https://github.com/Omega-JS-Stack/omega/issues/610)). The legacy UJM-shaped
923
+ `targets.web.download` / `targets.web.extension` page maps — and the `site.download` /
924
+ `site.extension` data they filled — are GONE: a brand still carrying either key fails
925
+ validation naming the block it derives from, and `omega migrate` drops it with a note.
926
+
927
+ - **The desktop derivation is OPT-IN (#124)**: `releasesUrl` appears only when
928
+ `targets.desktop.releases` is present (its
929
+ `enabled` defaults true when the block exists); `releases.enabled: false` always
930
+ suppresses, and a bare desktop target with no `releases` block derives nothing —
931
+ declaring the target does not mean a release exists yet. `releases.enabled` is ONE
932
+ switch for the whole release surface: the desktop build also reads it
933
+ (electron-builder publish config), so `false` turns off desktop publishing too — and
934
+ there it defaults true even with no `releases` block.
935
+ - **Desktop releases URL**: `https://github.com/<owner>/<name>/releases/latest`, where
936
+ owner and name come from `releasesRepo(config)` in
937
+ [repo.js](../../packages/config/src/repo.js): the brand's ONE public releases repo
938
+ ([#799](https://github.com/Omega-JS-Stack/omega/issues/799)). `targets.desktop.releases.repo`
939
+ names it, else it is `<brand.id>-releases`; `releases.owner` owns it, else the brand
940
+ repo's own owner (`brandRepoOwner`). Nothing addressable, no URL. That helper is the ONE
941
+ home of the address: @omega.js/desktop's electron-builder publish block, its releases-repo
942
+ provisioning and its `finalize-release` uploads read the same call, so the feed a shipped
943
+ app polls and the link a download button carries cannot disagree.
944
+ - **Desktop direct downloads ([#620](https://github.com/Omega-JS-Stack/omega/issues/620))**:
945
+ `downloads.<platform>.<artifact>` = `<releasesUrl>/download/<asset>`, one per published
946
+ artifact (`mac.universal`, `windows.universal`, `linux.debian`, `linux.appimage`, in
947
+ offer order). The asset names are `desktop-artifacts.js`'s — the SAME rule
948
+ @omega.js/desktop's `build-config` writes into `electron-builder.yml`, so a button
949
+ hands over the file and never lands on a GitHub page. They carry no version, which is
950
+ what keeps `/releases/latest/download/<asset>` pointing at the newest build forever:
951
+ releasing a desktop version never touches the website. Derived from
952
+ `targets.desktop.app.productName` → `brand.name`; no product name, no `downloads` (a
953
+ guessed filename is a dead button).
954
+ - **Extension listings**: `targets.extension.listings.<store>.{url,state}` for the six
955
+ stores the theme renders (chrome, firefox, edge, opera, safari, brave) —
956
+ schema-declared; url must be http(s). Entries with neither url nor state stay absent.
957
+ - **Idempotent by contract**: the web build applies `toSiteGlobal` twice (loadSiteData,
958
+ then configureOmega) — a curated `releasesUrl` and its `downloads` survive the second
959
+ pass unchanged.
960
+ - **Array-form (multi-instance) targets derive nothing** — presence only: which
961
+ instance's facts belong on the site is ambiguous, so instance-form brands get an
962
+ `enabled: true` entry and nothing else. Those pages stay on their empty state.
963
+
964
+ Three consumers read the curated view and nothing else: the `/download` page (every
965
+ desktop button → `site.targets.desktop.downloads[platform][artifact]`), the `/extension` page
966
+ (`site.targets.extension.listings[browser].url`), and the shortlink generator
967
+ (`src/target-shortlinks.js`, #561), which publishes `/download/<platform>[/<artifact>]`
968
+ and `/extension/<store>` off the same facts. Mobile derives nothing while MAM is
969
+ parked, so the mobile band stays on its notify form.
970
+
971
+ ## Consumer access
972
+
973
+ Each framework exposes the vendored loader — desktop: `require('@omega.js/desktop/config')`,
974
+ extension: `require('@omega.js/extension/config')` → `{ loadConfig, validateConfig, … }`.
975
+ Consumer workflows use this instead of raw JSON5 reads so brand-monorepo resolution
976
+ always applies.
977
+
978
+ **Derived values reach brands as VALUES, never as a recipe to re-run**
979
+ ([#290](https://github.com/Omega-JS-Stack/omega/issues/290)). A brand target cannot require
980
+ this private package at runtime, so a framework that owns a derivation publishes its
981
+ ANSWER on the runtime config object the target already holds, under `resolved.*`: the
982
+ backend's `Manager.config.resolved.github` carries `{ owner, name, repo }` — the brand
983
+ repo derivation (`repo.providers.github` overlaid by `targets.backend.github`, slug or
984
+ bare name) as one finished value, `repo` being the `owner/name` slug. The derivations
985
+ themselves stay here (`brandRepo()` in `src/repo.js`): one implementation, called by the
986
+ framework, so no brand re-implements the merge rules and drifts from them. New derived
987
+ values join a framework's `resolved` group as real brand needs surface.
988
+
989
+ **The repo NAME itself derives from the `<brand.id>-<role>` rule** (Ian 2026-09-07,
990
+ [#809](https://github.com/Omega-JS-Stack/omega/issues/809)). Every repo a brand owns is
991
+ its id plus the role that repo plays, so nobody types a repo name to get the right one:
992
+ `brandRepoName()` answers a typed `repo.providers.github.repo` first (a bare name, or an
993
+ `owner/name` slug whose owner slot also wins the owner half), else the default
994
+ `<brand.id>-omega`, the SOURCE monorepo's role, beside `releases` for the one public
995
+ desktop releases repo (`releasesRepo()`, `<brand.id>-releases`). The typed slug stays the
996
+ override for a brand whose repo is named something else, which is the only way a brand
997
+ keeps a pre-rule name.
998
+
999
+ ## Defaults & self-healing
1000
+
1001
+ Ian's ruling (2026-08-22, [#478](https://github.com/Omega-JS-Stack/omega/issues/478)): the config
1002
+ stays complete and current. A subsystem that exists has its config structure IN the file —
1003
+ visible and editable — instead of an invisible framework fallback, and every default has ONE
1004
+ home.
1005
+
1006
+ **The schema is that home.** A `schema.js` entry carries its own `default:` beside its type and
1007
+ description; `schemaDefaults(target)` builds them into the merge chain's lowest layer, so nothing
1008
+ else re-states a default. A key with no sane framework answer carries none: owner decisions,
1009
+ tri-states that mean "ask" (`cloud.billingAccount`), ids the services provision
1010
+ (`forms.providers.slapform.formId`), anything secret-shaped. The other standing exclusion is the
1011
+ **presence gate** ([#425](https://github.com/Omega-JS-Stack/omega/issues/425)), which in practice
1012
+ means monitoring: that service reads `monitoring.providers.sentry` presence as the pick of
1013
+ Sentry, so its SDK knobs (`sampleRate`, `scrubEmail`, …) carry no default and keep their home in
1014
+ the package that reads them — and `advertising`, whose adsense `client` id is the whole switch
1015
+ ([#527](https://github.com/Omega-JS-Stack/omega/issues/527)), so a materialized block would turn
1016
+ the web build's automatic ad placements on for a brand that configured none. Blocks whose services
1017
+ gate on `enabled` or provisioned ids rather than presence (github, cloudflare, searchConsole,
1018
+ slapform, chatsy, replyify) DO carry defaults — see the polarity doctrine above.
1019
+ Role-level switches beside any providers block
1020
+ (`monitoring.enabled`, `marketing.campaigns.enabled`) are nobody's pick and always may.
1021
+
1022
+ **`omega manage` materializes what a brand lacks.** The workspace service's `defaults` operation
1023
+ (right after the `config` health check) diffs the brand's own `config/omega.json5` against the
1024
+ schema defaults and writes the missing blocks through the comment-preserving editor, each key
1025
+ documented with the schema's own description. The rules:
1026
+
1027
+ - **Never an overwrite.** Only keys the brand has NOT authored are written — a `false` a brand
1028
+ set (or a section it deliberately switched off) is a decision, and the heal never dives into it.
1029
+ - **Highest missing path, once.** A brand with no `marketing` at all gets one `marketing` block,
1030
+ not one edit per key inside it.
1031
+ - **Idempotent.** A converged config leaves the file byte-identical; a dry run reports the blocks
1032
+ and writes nothing.
1033
+ - **New subsystems arrive on the next run.** Adding a `default:` to the schema is all it takes for
1034
+ every consumer's config to grow the block the next time manage runs.
1035
+
1036
+ `marketing.prune.enabled` is the first ruling this carries: pruning is ON by default and lands in
1037
+ every brand config as an editable switch (Ian 2026-08-22, closing the
1038
+ [#422](https://github.com/Omega-JS-Stack/omega/issues/422) follow-up).
1039
+
1040
+ **`materialize: false` — a default that resolves but is never written**
1041
+ ([#793](https://github.com/Omega-JS-Stack/omega/issues/793)). A rule may carry the flag beside its
1042
+ `default:`, and then `schemaDefaults()` still puts the value at the merge chain's lowest layer —
1043
+ every reader resolves it — while `missingDefaults()` and `defaultComments()` skip it, so the manage
1044
+ walk never writes it into `config/omega.json5`. The line for when to use it: **is this value an
1045
+ OWNER's decision, or the framework's own fact?** A decision belongs in the brand's file, where it is
1046
+ visible and editable (that is every ordinary default). A framework fact a brand may override and
1047
+ rarely does — presentation the framework owns — belongs at the layer that owns it, because a copy in
1048
+ every brand config is a copy that drifts from the thing it came from. The `connections` section is
1049
+ the first: the framework ships the five packaged providers' `name`, `logo` and `description`, a
1050
+ brand overrides any key through the ordinary merge chain, and nothing is copied into a brand file to
1051
+ go stale ([packages/backend/docs/connections.md](../../packages/backend/docs/connections.md)).
1052
+
1053
+ ## Writeback (comment-preserving edits)
1054
+
1055
+ omega.json5 is hand-edited — comments, key order, and quote style carry meaning — so
1056
+ programmatic writes are surgical text edits, not a re-stringify (omega-manager's
1057
+ serializer rewrote the whole file in canonical order and lost comments; this replaces
1058
+ it). The manager's services use it to land resolved IDs in config: the SendGrid list,
1059
+ the Beehiiv publication, Stripe/PayPal product IDs, the Firebase SDK config.
1060
+
1061
+ `applyConfigEdits(source, edits, { comments }?)` applies `{ 'dot.path': value }` edits to JSON5 text:
1062
+ existing leaves get their value span replaced; missing branches insert as one property
1063
+ before the containing object's closing brace (matching indent; house style: unquoted
1064
+ keys, JSON.stringify strings, trailing commas). Every byte outside the edited spans
1065
+ survives. Paths take dots, numeric array indexes, and `[key=value]` matchers that select
1066
+ an array element by its own key — `payment.products[id=plus].stripe.productId` — so
1067
+ writes self-locate in the file being edited instead of trusting an index computed from a
1068
+ merged config. Array elements are never created.
1069
+
1070
+ `comments` (dot-path → text) documents INSERTED keys only — the comment lands above the key its
1071
+ path names, wherever inside the inserted block that is, wrapped at that key's indent. A path that
1072
+ already exists keeps whatever the brand wrote above it. That is how the manage-run heal
1073
+ ([Defaults & self-healing](#defaults--self-healing)) lands each materialized block with the
1074
+ schema's own guidance beside it.
1075
+
1076
+ Guarantees: edits whose value already matches are skipped entirely (reruns are
1077
+ byte-identical); after every edit the result must JSON5-parse and hold the requested
1078
+ value at the requested path, or the call throws and nothing is returned — a corrupted
1079
+ config can't land on disk.
1080
+
1081
+ `writeConfigValues(projectDir, edits, { dryRun }?)` is the file-level form: resolves the
1082
+ standard locations, skips the write when nothing changes, and returns
1083
+ `{ path, changed, applied }` (`applied` = the paths that actually differed). The manager
1084
+ wraps it in `lib/config-write.js` (`writeBrandConfig(context, edits)`) for the uniform
1085
+ dry-run gate + logging.
1086
+
1087
+ **Deleting** is the same surgery in reverse ([#612](https://github.com/Omega-JS-Stack/omega/issues/612)):
1088
+ `applyConfigRemovals(source, paths)` cuts the property a dot-path names — its key, its whole
1089
+ subtree, the comma that separated it, and the `//` comment block documenting it, because that
1090
+ comment describes the key being deleted and would otherwise dangle over the next one. A property
1091
+ sharing its line (`{ a: 1, b: 2 }`) takes only itself and its separator. Absent paths are skipped,
1092
+ so reruns are byte-identical, and each removal is verified (parses, path gone) or the call throws.
1093
+ Array ELEMENTS are never removed, the mirror of never creating them. `removeConfigValues(projectDir,
1094
+ paths, { dryRun }?)` is the file-level form → `{ path, changed, removed }`. Unlike a write it does
1095
+ NOT normalize top-level key order: a deletion is surgical, and re-sorting the file around it would
1096
+ bury the one line the caller means to report. The consumer is `omega migrate` at a brand root
1097
+ ([manager/index.md](../manager/index.md)).
1098
+
1099
+ ## Migration — legacy configs → omega.json5
1100
+
1101
+ No framework reads the legacy files anymore. Convert once, delete the old file. General
1102
+ recipe: shared-looking sections move to the TOP LEVEL (brand, analytics, payment, theme;
1103
+ `oauth2` lands as `connections` (#788); `firebaseConfig` becomes `cloud: { provider: 'firebase', config: {…} }` and
1104
+ `sentry` becomes `monitoring: { providers: { sentry: {…} } }`); everything framework-specific
1105
+ moves under `targets.<type>`.
1106
+
1107
+ **Retired keys fail loudly** ([#142](https://github.com/Omega-JS-Stack/omega/issues/142)):
1108
+ a name that was renamed OUTRIGHT is a validation error wherever it sits — shared level,
1109
+ inside a `targets.<type>` entry, inside an instance array — naming its replacement and
1110
+ pointing back here. Today that is `web_manager` → `client`, `firebaseConfig` → `cloud`,
1111
+ `cookieConsent` → `client.consent`, `subdomains` → `targets.web` and `oauth2` → `connections` (`src/retired-keys.js` is the list). Without the guard the old key validated clean and
1112
+ everything under it vanished, since nothing dual-reads it. Names that live on as legitimate
1113
+ keys elsewhere stay out of the list — the rows below are their only guide. `sentry` is the one
1114
+ that reads like a contradiction and is not: its NEW home is itself a `sentry` key
1115
+ (`monitoring.providers.sentry`), so a name test would fire on the very shape it is steering
1116
+ people toward. Same for `google`/`meta` under `analytics.providers`.
1117
+
1118
+ **`omega migrate` at the brand root deletes them** ([#612](https://github.com/Omega-JS-Stack/omega/issues/612)): both halves of the
1119
+ list, from the file as AUTHORED, through the comment-preserving editor (`removeConfigValues`) —
1120
+ the key, its subtree, and the comment documenting it, with every other byte untouched. One line
1121
+ per key naming its replacement, `--dry-run` for the plan, idempotent (a converged brand's rerun
1122
+ is byte-identical). It removes the dead key; moving the setting into the home named in the tables
1123
+ below is still by hand.
1124
+
1125
+ `subdomains` is the newest name ([#588](https://github.com/Omega-JS-Stack/omega/issues/588),
1126
+ Ian 2026-09-01). The key was READ by exactly one thing, the cloud hosting op, which ensured an
1127
+ `api.{sub}.{domain}` per entry, and DECLARED by nothing: no schema rule, no default, never
1128
+ materialized. The fact it was reaching for is a web instance, so the instance is its home now:
1129
+ the id is the subdomain, and every subdomain shares one `api.<domain>`. It is a NAME test by the
1130
+ rule above (`subdomains` exists nowhere else in the schema), so it fires wherever a brand wrote
1131
+ it, including down inside an instance entry's own `brand` block.
1132
+
1133
+ | Retired key | New home |
1134
+ |---|---|
1135
+ | `subdomains` | **`targets.web`** as an array of instances: `["admin", "cdn"]` becomes `web: [{ id: 'main' }, { id: 'admin' }, { id: 'cdn' }]` (§ Multi-instance targets). The id IS the subdomain (`https://admin.<brand host>`), an entry's own `url` overrides it for a custom host, and the instances share ONE `api.<domain>` |
1136
+
1137
+ `oauth2` is the newest name ([#788](https://github.com/Omega-JS-Stack/omega/issues/788),
1138
+ Ian 2026-09-03). The product concept is a CONNECTION, and a connection will not always be an
1139
+ OAuth grant — an API key or a bot token is one too — so the whole feature carries the product
1140
+ word (the section, the route, the user-record field, the env prefix, the brand provider folder,
1141
+ the callback URL) and each stored record names its own kind with `type: 'oauth2'`. A NAME test
1142
+ by the rule above: `oauth2` exists nowhere else in the schema. The by-hand steps a carrying
1143
+ brand still owes — the env rename, the provider-console redirect URI, the provider folder move —
1144
+ are in [breaking-changes.md](breaking-changes.md#the-user-connection-feature-is-connections-788).
1145
+
1146
+ | Retired key | New home |
1147
+ |---|---|
1148
+ | `oauth2` | **`connections`** — the per-provider block is unchanged. The credentials are the `CONNECTIONS_<PROVIDER>_CLIENT_ID`/`_SECRET` pair now, a brand's own provider module lives at `targets/backend/src/connections/<name>.js`, the route is `/omega/user/connections`, and the redirect URI to register is `<websiteUrl>/connections/callback` |
1149
+
1150
+ The de-branding rekey ([#23](https://github.com/Omega-JS-Stack/omega/issues/23)) adds a
1151
+ second, PATH-based half in the same file (`RETIRED_PATHS`): keys whose provider keeps its
1152
+ own name one level down inside the new home, so a name test would false-positive. Each
1153
+ entry matches ONE exact path from the root, array positions ignored — a `targets.<type>` row
1154
+ fires inside an instance array too ([#732](https://github.com/Omega-JS-Stack/omega/issues/732)),
1155
+ and the error names the real path, index and all:
1156
+
1157
+ | Retired path | New home |
1158
+ |---|---|
1159
+ | `slapform` | **`forms.providers.slapform`** |
1160
+ | `chatsy` | **`inbound.chat.providers.chatsy`** (the widget `settings` moved here too — one home) |
1161
+ | `replyify` | **`inbound.email.providers.replyify`** |
1162
+ | `cloudflare` | **`edge.providers.cloudflare`** |
1163
+ | `recaptcha` | **`captcha.providers.recaptcha`** (`site-key` → `siteKey`) |
1164
+ | `searchConsole` | **`search.providers.searchConsole`** (`seo` already means the parasite-SEO content feature) |
1165
+ | `gcp` | **`cloud`** (`cloud.organizationId`, `cloud.billingAccount`) |
1166
+ | `firebase` | **`cloud`** (`cloud.shared`, `cloud.supportEmail`, `cloud.apiSubdomain`; projectId only at `cloud.config.projectId`) |
1167
+ | `advertising.providers.google-adsense` | **`advertising.providers.adsense`** + camelCase slots |
1168
+ | `github` | **`repo.providers.github`** — the ONE row with no guard: the brand's own `github` (content identity, unchanged; a shared key since [#277](https://github.com/Omega-JS-Stack/omega/issues/277)) lives at the top level, so a name test would false-positive. This table is its only guide |
1169
+
1170
+ The one-provider-shape normalization ([#425](https://github.com/Omega-JS-Stack/omega/issues/425))
1171
+ adds its own rows to the same `RETIRED_PATHS` half — every role names its vendors
1172
+ `role.providers.<provider>` now, so the flat picks, the bare vendor key and payment's
1173
+ fourth word are all retired. Key PRESENCE is the pick; `false` is the deliberate off
1174
+ switch; no entry at all is "none chosen" (what a null provider meant). `cloud` stays the
1175
+ ratified exception. Full rationale + the by-hand step per row:
1176
+ [breaking-changes.md](breaking-changes.md#one-provider-shape--roleprovidersprovider-425).
1177
+
1178
+ | Retired path | New home |
1179
+ |---|---|
1180
+ | `payment.providers` | **`payment.providers`** — contents identical. The SINGULAR `provider` (Firestore document fields, the intent schema, the payments route params, the email merge field, `libraries/payment/providers/`) is a live data contract and is unchanged |
1181
+ | `certificates.apple` | **`certificates.providers.apple`** — no bare vendor keys; Windows signing sits beside it later |
1182
+ | `domain.provider` | **`domain.providers.<registrar>`** — `{ namecheap: {} }` / `{ squarespace: {} }` |
1183
+ | `domain.email.provider` | **`domain.email.providers.<provider>`** — `domain.email.forwarding` stays role-level (provider-agnostic) |
1184
+ | `translation.provider` | **`translation.providers.<name>`** — `{ claude: {} }` / `{ chatgpt: {} }`; an absent block still means claude. `translation.model` stays role-level |
1185
+ | `devlog.provider` + `devlog.{lookbackDays,orgs,excludeRepos,excludeCommits,excludeTopics,includePrivate,postPath,destinations,overrides}` | **`devlog.providers.ghostii.<same key>`** — the writer is the KEY, its settings live inside it. `devlog.enabled` stays role-level |
1186
+ | `monitoring.provider` + `monitoring.{org,dsn,environment,sampleRate,tracesSampleRate,scrubEmail,attachScreenshot,bundlePatterns}` | **`monitoring.providers.sentry.<same key>`** — the monitor is the KEY, every SDK-facing knob lives inside it. `monitoring.enabled` stays role-level, and per-surface DSNs are `targets.<type>.monitoring.providers.sentry.dsn` |
1187
+ | `marketing.campaigns.provider` + `marketing.campaigns.listId` | **`marketing.campaigns.providers.sendgrid.listId`** — `marketing.campaigns.enabled` stays role-level |
1188
+ | `marketing.newsletter.provider` + `marketing.newsletter.publicationId` | **`marketing.newsletter.providers.beehiiv.publicationId`** — `marketing.newsletter.enabled` AND `marketing.newsletter.content` stay role-level: content configures @omega.js/backend's newsletter generator, not Beehiiv |
1189
+
1190
+ The retired-key sweep only ever runs over an omega.json5: the `omega migrate` converter
1191
+ READS legacy files as input and emits the new names, and only its output is validated.
1192
+
1193
+ AdSense's one-switch collapse ([#527](https://github.com/Omega-JS-Stack/omega/issues/527))
1194
+ adds the last two rows ([#628](https://github.com/Omega-JS-Stack/omega/issues/628)). Both
1195
+ were unregistered until then, so a brand still carrying the deleted gate validated CLEAN
1196
+ while the account it meant to leave alone started being managed — the key reading exactly
1197
+ like it still worked. `omega migrate` drops both with the note.
1198
+
1199
+ | Retired path | New home |
1200
+ |---|---|
1201
+ | `advertising.providers.adsense.enabled` | **`advertising.providers.adsense`** — `client` presence is the ONE switch (managed account + rendered units + the ads.txt record). `advertising.providers.adsense: false` opts the provider out; there is no second gate |
1202
+ | `advertising.providers.adsense.units` | **`advertising.providers.adsense`** — the render-only gate #527 refused. A managed-but-ad-free brand omits the block and manages the account by hand |
1203
+
1204
+ The `meta` section is the other registered path ([#607](https://github.com/Omega-JS-Stack/omega/issues/607),
1205
+ Ian 2026-08-26 — meta never exists in two places). It shipped for one wave beside the
1206
+ bare `meta:` a page and a layout already wrote, which is two homes for one fact; deleting
1207
+ the config half leaves page frontmatter as the only meta, with `brand.name` /
1208
+ `brand.description` as the site-wide default the head falls back to. Registered at its
1209
+ authored path AND at the `targets.web` overlay — never by key NAME, because
1210
+ `analytics.providers.meta` is a legitimate key one level down.
1211
+
1212
+ | Retired path | New home |
1213
+ |---|---|
1214
+ | `meta.title`, `meta.description` (and the same two under `targets.web.meta`) | **`brand.name` / `brand.description`** for the site-wide default, and that page's own `meta:` frontmatter for anything per-page ([docs/web/frontmatter.md](../web/frontmatter.md)) |
1215
+ | `seo.index` | **`targets.web.meta.index`** ([#564](https://github.com/Omega-JS-Stack/omega/issues/564), Ian's same-name ruling 2026-09-09): the site-wide default and the page override are ONE name at both levels, so `meta.index` is what a page writes and `targets.web.meta.index` is what the site writes. `targets.web.meta` itself is LIVE for that key; only `title` and `description` are retired under it |
1216
+
1217
+ `targets.web.redirects` is the newest registered path ([#466](https://github.com/Omega-JS-Stack/omega/issues/466)).
1218
+ It shipped in 0.45.0 and was withdrawn: static hosting has no server, so the map could only
1219
+ ever be answered CLIENT-side off the built 404 page, and a search engine saw a 404 that
1220
+ redirects rather than a move. Redirects are not web config at all now — a TEMPLATED
1221
+ redirect needs edge computing, an enumerable one is a page. Registered at its authored
1222
+ path, the one place a carrying brand has it.
1223
+
1224
+ | Retired path | New home |
1225
+ |---|---|
1226
+ | `targets.web.redirects` | **`edge.providers.cloudflare.rules.redirect`** for a templated redirect (`/c/:id` → `/code?id=:id`, the DashQR pattern) — the manager's edge service reconciles the ruleset ([docs/manager/edge.md](../manager/edge.md)). A redirect whose URLs can be ENUMERATED is a redirect PAGE instead: `redirect.url` in frontmatter on the `modules/utilities/redirect` layout ([docs/web/index.md](../web/index.md)) |
1227
+
1228
+ `targets.desktop.downloads.*` are the newest registered paths ([#799](https://github.com/Omega-JS-Stack/omega/issues/799)).
1229
+ The `download-server` mirror gave marketing a fixed filename, which the versionless artifact
1230
+ names ([#620](https://github.com/Omega-JS-Stack/omega/issues/620)) made free: the site links
1231
+ the ONE public releases repo directly and reads nothing from the mirror, so #799 deleted the
1232
+ lane. Without these rows a brand carrying the block validates clean (it sits inside the
1233
+ exempt `targets` namespace), gets a second repo provisioned and nothing published to it.
1234
+ Registered per KEY at its authored path, never by name: the curated
1235
+ `site.targets.desktop.downloads` map is a legitimate `downloads` one level down.
1236
+
1237
+ | Retired path | New home |
1238
+ |---|---|
1239
+ | `targets.desktop.downloads.enabled` | **`targets.desktop.releases`**: one public releases repo per brand, and its versionless assets ARE the permanent download links |
1240
+ | `targets.desktop.downloads.owner` | **`targets.desktop.releases.owner`**: there is no second repo to own, and it defaults to the brand repo's owner |
1241
+ | `targets.desktop.downloads.repo` | **`targets.desktop.releases.repo`**, defaulting to `<brand.id>-releases` |
1242
+ | `targets.desktop.downloads.tag` | nothing: `/releases/latest/download/<asset>` is what the stable mirror tag was for |
1243
+
1244
+ ### electron-manager (`config/electron-manager.json` → `config/omega.json5`) — DONE (checkpoint 18)
1245
+
1246
+ | Legacy | New |
1247
+ |---|---|
1248
+ | `brand`, `analytics`, `payment`, `theme` | top level, unchanged |
1249
+ | `firebaseConfig` | **`cloud: { provider: 'firebase', config: {…} }`** (D12) |
1250
+ | `sentry` | **`monitoring: { providers: { sentry: { dsn } } }`** ([#425](https://github.com/Omega-JS-Stack/omega/issues/425)) |
1251
+ | `app` | `targets.desktop.app` |
1252
+ | `targets.mac` / `targets.win` / `targets.linux` (per-OS) | `targets.desktop.platforms.mac` / `.win` / `.linux` |
1253
+ | `autoUpdate`, `startup`, `releases`, `remoteConfig`, `restartManager` | `targets.desktop.<same key>` |
1254
+ | `electronBuilder` overrides | `targets.desktop.electronBuilder` |
1255
+ | `cdp` | `targets.desktop.cdp` |
1256
+ | `windows` (optional) | `targets.desktop.windows` |
1257
+ | `fileAssociations`, `protocols` | `targets.desktop.<same key>` |
1258
+
1259
+ ### backend-manager (`functions/backend-manager-config.json` → `functions/config/omega.json5`) — DONE (checkpoint 19)
1260
+
1261
+ | Legacy | New |
1262
+ |---|---|
1263
+ | `brand`, `analytics`, `payment` | top level, unchanged |
1264
+ | `oauth2` | **`connections`** — the per-provider block is unchanged ([#788](https://github.com/Omega-JS-Stack/omega/issues/788)); the credentials become the `CONNECTIONS_<PROVIDER>_CLIENT_ID`/`_SECRET` pair |
1265
+ | `firebaseConfig` | **`cloud: { provider: 'firebase', config: {…} }`** (D12) |
1266
+ | `sentry` | **`monitoring: { providers: { sentry: { dsn } } }`** ([#425](https://github.com/Omega-JS-Stack/omega/issues/425)) |
1267
+ | custom keys (`omega`, `mcp`, …) | top level, unchanged |
1268
+ | `parent`, `github`, `reviews`, `marketing`, `blog`, `dataRequest` | top level, unchanged ([#277](https://github.com/Omega-JS-Stack/omega/issues/277)): shared keys the manager reads brand-level, so a website-only brand has a home for them; `targets.backend.<same key>` still overrides |
1269
+
1270
+ Notes: @omega.js/backend's framework-defaults layer is `templates/config/omega.json5` resolved through
1271
+ the same loader and passed as `options.defaults`; `Manager.init()`'s
1272
+ `backendManagerConfigPath` option is gone (the loader discovers the file); boot warns on
1273
+ schema findings, `npx omega test`'s target checks are the hard audit. The sandbox brand dogfoods the full
1274
+ hierarchy: shared sections live in `brands/sandbox-brand/config/omega.json5` (brand level),
1275
+ the backend target's local file carries only `targets.backend`.
1276
+
1277
+ ### browser-extension-manager (`config/browser-extension-manager.json` → `config/omega.json5`) — DONE (checkpoint 20)
1278
+
1279
+ | Legacy | New |
1280
+ |---|---|
1281
+ | `brand`, `analytics`, `theme` | top level, unchanged |
1282
+ | `firebaseConfig` | **`cloud: { provider: 'firebase', config: {…} }`** (D12) |
1283
+ | `sentry` | **`monitoring: { providers: { sentry: { dsn } } }`** ([#425](https://github.com/Omega-JS-Stack/omega/issues/425)) |
1284
+ | custom keys (`liveReloadPort`, …) | top level, unchanged |
1285
+ | `analytics.providers.google.secret` | **`.env` → `GOOGLE_ANALYTICS_SECRET`** (secrets never in omega.json5; loader hard-fails) |
1286
+ | *(no extension-specific keys yet)* | `targets.extension: {}` — presence = enabled; extension-specific settings land here |
1287
+
1288
+ Notes: `Manager.getConfig()` returns the RESOLVED config (missing file → `{}`; schema
1289
+ findings warn once per process — BXM has no separate audit surface). The build snapshot
1290
+ (`OMEGA_BUILD_JSON`, baked into every bundle) bakes `GOOGLE_ANALYTICS_SECRET` from the environment at
1291
+ build time, same value flow as before. `bxm setup` scaffolds + merges `config/omega.json5`
1292
+ (the defaults merge now preserves consumer-only keys at every level — it previously
1293
+ dropped them).
1294
+
1295
+ ### ultimate-jekyll-manager (`src/_config.yml` + `config/ultimate-jekyll-manager.json`) — `omega migrate` (B4, checkpoint 32)
1296
+
1297
+ One command converts the consumer in place (and `--check` previews without
1298
+ writing). The converted file is validated through `loadConfig(root, 'web')`
1299
+ before the report prints.
1300
+
1301
+ | Legacy | New |
1302
+ |---|---|
1303
+ | `url` | `brand.url` (site.url derives; empty `baseurl` dropped) |
1304
+ | `brand`, `theme` | top level, verbatim |
1305
+ | `oauth2` | **`connections`** — same block, new name ([#788](https://github.com/Omega-JS-Stack/omega/issues/788)) |
1306
+ | `analytics.{google,meta,tiktok}` (flat scalars) | `analytics.providers.<p>.id` — the unified spelling; the web chrome emits the client's flat shape from it |
1307
+ | `web_manager.firebase.app.config` | **`cloud: { provider: 'firebase', config: {…} }`** (top level); the engine composes `cloud.config` back into `client.firebase.app.config` at build |
1308
+ | `web_manager.payment` | **`payment`** (top level); composed back into `client.payment` (pricing layouts + the client read it there); credential keys set to `false` (legacy "disabled") are dropped |
1309
+ | `web_manager` (rest: auth, exitPopup, …) | **`targets.web.client`** — the client-runtime settings blob, whole, under its new name (#1: `web_manager` → `client`, since it configures `@omega.js/client`; WebManager is not an OMEGA concept). No dual-read: the old key name is not honored anywhere |
1310
+ | `web_manager.sentry` (`{ enabled, config: {…} }`) | **`monitoring: { enabled, providers: { sentry: {…} } }`** (top level) — the SDK knobs inside `config` ARE the provider block ([#485](https://github.com/Omega-JS-Stack/omega/issues/485)). Leaving them in the client blob meant the converted brand had no `monitoring` key at all, so the manager's monitoring service skipped every run and no Sentry project was ever reconciled |
1311
+ | `web_manager.cookieConsent` (incl. `palette`, `theme`, `type`, `content.dismiss`) | **`targets.web.client.consent`** — the block that became a real gate (#383). `palette`/`theme` are gone (the panel paints from the `--omega-*` tokens); `type` is gone (the visitor's region picks opt-in vs opt-out); `content.dismiss` is now `content.accept`, beside `content.customize`, `content.panelIntro`, `content.acceptAll` and `content.acceptNone` (#391 retired `content.save` with the Save button) |
1312
+ | `web_manager.chatsy` (agentId + widget settings) | **`inbound.chat.providers.chatsy`** — the chat widget left the client blob for the one chat home the manager also provisions (#23) |
1313
+ | `socials` | **`socials`** (top level) — a SHARED_SCHEMA key, so the root is its home and the scaffold emits it there ([#483](https://github.com/Omega-JS-Stack/omega/issues/483)). A web load resolves `targets.web.socials` identically, which is why the converter used to leave it in the target; the handles are brand identity, read past the website too |
1314
+ | `translation` | **`translation`** (top level) — a SHARED_SECTIONS key, so the root is its home and the scaffold emits it there ([#526](https://github.com/Omega-JS-Stack/omega/issues/526)). A web load resolves `targets.web.translation` identically, which is why the converter used to leave it in the target; but disperse copies the SHARED sections, so a target-scoped engine config is invisible to every other target that translates (the extension's `_locales`). `translation.exclude` stays a web-only key at that shared home |
1315
+ | `meta` | **`targets.web.meta.index`, and nothing else**: the title/description half is DROPPED ([#607](https://github.com/Omega-JS-Stack/omega/issues/607), Ian 2026-08-26: meta never exists in two places): the site-wide defaults are `brand.name` / `brand.description` (what @omega.js/web's head falls back to) and everything per-page is that page's own `meta:` frontmatter ([docs/web/frontmatter.md](../web/frontmatter.md)). The `index` half lives on under the SAME name a page writes ([#564](https://github.com/Omega-JS-Stack/omega/issues/564), Ian 2026-09-09). `omega migrate` drops the legacy title/description with a note, and a config still carrying `meta.title` / `meta.description` is a retired-key error |
1316
+ | `download`, `extension`, `favicon`, `manifest`, `icons` | `targets.web.<same key>` (target overlay puts them back at the top level for web loads) |
1317
+ | `recaptcha` (incl. `site-key`) | **`captcha.providers.recaptcha`** (`siteKey` — every key is camelCase, #23) |
1318
+ | `cloudflare` (the purge `zone`) | **`edge.providers.cloudflare`** — one cloudflare home, shared with the manager's zone reconciliation (#23) |
1319
+ | `advertising.google-adsense` (flat or under `providers`) | **`advertising.providers.adsense`** with camelCase slots (`displaySlot`, `inArticleSlot`, `inFeedSlot`, `multiplexSlot`) — provider ids drop the vendor prefix (#23). An `enabled` or `units` gate beside them is DROPPED with a note ([#628](https://github.com/Omega-JS-Stack/omega/issues/628)): `client` presence is the one switch, so carrying the gate forward would validate as retired while the id turned the account and the units back on |
1320
+ | `permalink`, `pagination`, `collections`, `defaults`, `generators` | `targets.web.<same key>` (codemod rule 8's home — engine consumption of custom collections rides the consumer-theme waves) |
1321
+ | UJM-json `distribute`, `sass.purgecss`, `imagemin`, `github.workflows` | `targets.web.{distribute,purgecss,imagemin,workflows}` — `imagemin` is LIVE (schema-known; `enabled: false` ships images verbatim, otherwise the build-time 320/640/1024 + webp matrix runs), and `purgecss.safelist` is LIVE too ([#250](https://github.com/Omega-JS-Stack/omega/issues/250)): schema-known (`{ standard, deep, greedy, keyframes }` arrays, or a bare array for `standard`), it merges over the framework's built-in safelist in the purge pass |
1322
+ | UJM-json `webpack`, `gems`; `_config.yml` Jekyll machinery (`plugins`, `exclude`, …) | dropped, noted in the report |
1323
+ | secret-shaped keys anywhere | dropped + warned — move to `.env` |
1324
+
1325
+ Beyond config: the codemod rule table runs over `src/**` templates, the seed
1326
+ `src/assets/js/main.js` is deleted (core main + boot runtime replace it;
1327
+ customized ones are flagged with the port recipe), `main.scss`'s
1328
+ `@use 'ultimate-jekyll-manager' with (…)` becomes `@use 'omega:main' with (…)`,
1329
+ page-css self-`@use` lines are dropped, and Gemfile/Gemfile.lock/the legacy
1330
+ configs are removed.
1331
+
1332
+ ### omega-manager brand configs (`.brands/{id}/config.json`)
1333
+
1334
+ The brand-config `targets` ARRAY's role is absorbed by key presence in the omega.json5
1335
+ `targets` object. omega-manager's disperse writes omega.json5 from brand config + state at
1336
+ its Phase-3 cutover (enumerating `SHARED_SECTIONS`, per-surface values into
1337
+ `targets.<type>` overrides).
1338
+
1339
+ ## Package API (quick reference)
1340
+
1341
+ ```js
1342
+ const {
1343
+ loadConfig, // (projectDir, target?, { defaults }?) → { config, errors, warnings, enabled, instance, files }
1344
+ composeTargetConfig, // (projectDir, target) → { config, files } — company+brand+local frozen into ONE self-contained file (deploy upload boundary, #31)
1345
+ hasOmegaConfig, // (projectDir) → boolean — "is this project migrated?"
1346
+ resolveConfigPath, // (projectDir) → abs path | null
1347
+ getEnabledTargets, // (config) → ['web', 'backend', …]
1348
+ findBrandRoot, // (projectDir) → brand root | null — CLASSIFIES one target dir (THE hierarchy rule)
1349
+ findBrandConfigPath, // (projectDir) → the BRAND layer's omega.json5 | null — the file a target with no local-layer file rides
1350
+ resolveBrandRoot, // (startDir) → brand root | null — SEARCHES upward from anywhere (standalone → itself), bounded at the nearest .git
1351
+ loadEnv, // (startDir) → { chain, loaded } — resolve + load the .env cascade
1352
+ reloadEnv, // (startDir, options?) → same — drops the FILE-owned keys, then loads again, so an EDITED value lands and the shell still wins (#724)
1353
+ resolveEnvChain, // (startDir) → { local, brand, company } .env paths (no loading)
1354
+ loadEnvChain, // (paths) → loaded[] — dotenv strongest-first, nulls/missing skip
1355
+ readCompanyRoot, // (brandRoot) → company root | null (.omega/company.json)
1356
+ COMPANY_MARKER, // '.omega/company.json'
1357
+ resolveHook, // (startRoot, 'account/password') → hook file | null (brand → company)
1358
+ loadHook, // (startRoot, hookPath) → { fn, file } | null — broken hooks THROW
1359
+ validateConfig, // (config, { target }?) → { errors, warnings }
1360
+ runSchema, // low-level rule walker (EM's proven engine)
1361
+ formatErrors, // errors → numbered block
1362
+ resolvedBrandHost, // (config) → the BRAND's own host (`brand.url`, top-level `url` only as fallback), lowercased | '' — authDomain validation AND every persona address (#708)
1363
+ findSecretKeys, // (object) → dot-paths of secret-shaped keys
1364
+ findRetiredKeys, // (object) → [{ path, key, replacement, why }] — renamed-outright keys (#142)
1365
+ chosenProvider, // (role.providers) → the picked provider name | null — presence is the pick, `false` the off switch (#425)
1366
+ backendProjectType, // (targets.backend | resolved backend config) → 'firebase' | 'custom' — how the backend runs (#584)
1367
+ BACKEND_PROJECT_TYPES,
1368
+ applyConfigEdits, // (source, edits, { comments }?) → edited source — comment-preserving (see Writeback)
1369
+ writeConfigValues, // (projectDir, edits, { dryRun, comments }?) → { path, changed, applied }
1370
+ applyConfigRemovals, // (source, paths) → edited source — deletes a key, its subtree and its own comment (#612)
1371
+ removeConfigValues, // (projectDir, paths, { dryRun }?) → { path, changed, removed } — absent paths skip, so reruns are byte-identical
1372
+ schemaDefaults, // (target?) → the schema's own defaults, the merge chain's lowest layer (#478)
1373
+ missingDefaults, // (rawConfig, target?) → [{ path, value }] — the blocks a brand file lacks (the manage heal's list)
1374
+ defaultComments, // (target?) → { 'dot.path': description } — the guiding comments a materialized block carries
1375
+ deepMerge, // agnostic layer merge
1376
+ // Multi-instance targets (instances.js — the ONE iteration mechanism)
1377
+ normalizeTargetInstances, // (targets.<type> value) → [{ id, … }] (object form = [{ id: 'main', …entry }])
1378
+ instanceIdFromDirName, // ('website-admin', 'web') → 'admin'; canonical/unconventional dirs → 'main'
1379
+ instanceTargetDir, // ('web', 'admin') → 'website-admin'; main → the canonical dir
1380
+ targetInstance, // (projectDir, target) → this target dir's instance id (brand targets only; standalone → 'main')
1381
+ resolveInstanceEntry, // (entry, id) → the instance's merge layer (id stripped) | null
1382
+ instancePortOffset, // (entry, id) → position in the instances array (dev-port offsets)
1383
+ resolveInstanceUrl, // (entry, id, config) → instance url → instance brand.url → https://<id>.<brand host> (non-main) → brand.url | null
1384
+ DIR_TARGETS, TARGET_DIRS, MAIN_INSTANCE, // the target-dir mapping SSOT (manager re-exports)
1385
+ TARGETS, SHARED_SECTIONS, SHARED_SCHEMA, TARGET_SCHEMAS,
1386
+ } = require('@omega.js/config');
1387
+ ```