@omega.js/desktop 0.53.0 → 0.54.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -38
- package/dist/cli-run.js +4 -1
- package/dist/cli.js +2 -2
- package/dist/commands/cdp/client.js +1 -1
- package/dist/commands/cdp.js +1 -1
- package/dist/commands/clean.js +2 -3
- package/dist/commands/dev.js +25 -0
- package/dist/commands/lib/ensure-target.js +12 -17
- package/dist/commands/lib/migrate.js +17 -0
- package/dist/commands/logs.js +1 -1
- package/dist/commands/release.js +1 -1
- package/dist/commands/test.js +4 -4
- package/dist/commands/update.js +5 -4
- package/dist/defaults/.github/workflows/build.yml +18 -18
- package/dist/defaults/_.gitignore +0 -2
- package/dist/defaults/_mas/README.md +3 -3
- package/dist/defaults/config/certs/README.md +1 -1
- package/dist/defaults/config/omega.json5 +36 -36
- package/dist/defaults/docs/README.md +3 -3
- package/dist/defaults/gulpfile.js +1 -1
- package/dist/defaults/hooks/build/post.js +1 -1
- package/dist/defaults/hooks/build/pre.js +1 -1
- package/dist/defaults/hooks/notarize/post.js +2 -2
- package/dist/defaults/hooks/release/post.js +1 -1
- package/dist/defaults/hooks/release/pre.js +1 -1
- package/dist/defaults/src/assets/scss/pages/about.scss +1 -1
- package/dist/defaults/src/assets/scss/pages/main.scss +1 -1
- package/dist/defaults/src/assets/scss/pages/settings.scss +1 -1
- package/dist/defaults/src/integrations/context-menu/index.js +11 -11
- package/dist/defaults/src/integrations/menu/index.js +5 -5
- package/dist/defaults/src/integrations/tray/index.js +9 -9
- package/dist/defaults/src/main.js +2 -2
- package/dist/defaults/src/preload.js +1 -1
- package/dist/defaults/test/README.md +3 -3
- package/dist/defaults/test/_init.js +1 -1
- package/dist/gulp/tasks/audit.js +5 -8
- package/dist/lib/restart-manager/index.js +1 -1
- package/dist/lib/restart-manager/install.js +1 -1
- package/dist/lib/restart-manager/protocol.js +1 -1
- package/dist/main.js +4 -3
- package/dist/preload.js +1 -1
- package/dist/test/suites/build/audit.test.js +20 -7
- package/dist/test/suites/build/build-workflow-jobs.test.js +2 -2
- package/dist/test/suites/build/cli.test.js +28 -0
- package/dist/test/suites/build/defaults-em-dash.test.js +22 -0
- package/dist/test/suites/build/defaults-scaffold.test.js +19 -5
- package/dist/test/suites/build/deploy-direct.test.js +7 -5
- package/dist/test/suites/build/deploy-dispatch.test.js +2 -1
- package/dist/test/suites/build/deploy-hook.test.js +4 -2
- package/dist/test/suites/build/dev-verb.test.js +67 -0
- package/dist/test/suites/build/ensure-target.test.js +11 -3
- package/dist/test/suites/build/merge-line-files.test.js +6 -6
- package/dist/test/suites/build/migrate.test.js +29 -0
- package/dist/test/suites/build/project-scripts-deps.test.js +6 -10
- package/dist/test/suites/build/runner-env-write.test.js +73 -0
- package/dist/test/suites/build/runner.test.js +9 -8
- package/dist/test/suites/build/setup-scripts.test.js +27 -0
- package/dist/test/suites/build/validate-config.test.js +13 -2
- package/dist/test/suites/build/verb-logs.test.js +20 -0
- package/dist/test/suites/renderer/window-desktop-surface.test.js +1 -1
- package/dist/utils/build-pipeline.js +4 -4
- package/dist/utils/runner-env.js +13 -28
- package/dist/vendor/config/company.js +46 -14
- package/dist/vendor/config/defaults.js +30 -7
- package/dist/vendor/config/edit.js +25 -3
- package/dist/vendor/config/env-delivery.js +1 -1
- package/dist/vendor/config/env-schema.js +3 -6
- package/dist/vendor/config/env.js +34 -22
- package/dist/vendor/config/index.js +13 -17
- package/dist/vendor/config/load.js +15 -7
- package/dist/vendor/config/repo.js +10 -27
- package/dist/vendor/config/schema-client.js +64 -0
- package/dist/vendor/config/schema-cloud.js +38 -0
- package/dist/vendor/config/schema-manager.js +118 -0
- package/dist/vendor/config/schema-overrides.js +68 -0
- package/dist/vendor/config/schema.js +99 -152
- package/dist/vendor/config/validate.js +97 -77
- package/dist/vendor/devkit/agents-md.js +233 -0
- package/dist/vendor/devkit/attach-log-file.js +15 -1
- package/dist/vendor/devkit/ci-workflows.js +30 -30
- package/dist/vendor/devkit/cli-router.js +13 -7
- package/dist/vendor/devkit/defaults-engine.js +9 -43
- package/dist/vendor/devkit/deploy-snapshot.js +44 -9
- package/dist/vendor/devkit/env-lines.js +183 -0
- package/dist/vendor/devkit/local.js +62 -10
- package/dist/vendor/devkit/lockfile.js +32 -13
- package/dist/vendor/devkit/logger.js +7 -2
- package/dist/vendor/devkit/merge-line-files.js +219 -176
- package/dist/vendor/devkit/omega-bin.js +208 -111
- package/dist/vendor/devkit/preludes/docs-sync.js +52 -0
- package/dist/vendor/devkit/preludes/index.js +1 -0
- package/dist/vendor/devkit/target-picker.js +45 -0
- package/dist/vendor/devkit/test/dashed-files.js +37 -0
- package/dist/vendor/devkit/test/run-verb-under-tee.js +71 -0
- package/dist/vendor/devkit/update.js +15 -15
- package/dist/vendor/devkit/verb-scripts.js +40 -0
- package/dist/vendor/devkit/verbs.js +170 -0
- package/package.json +18 -24
- package/dist/commands/install.js +0 -37
- package/dist/defaults/AGENTS.md +0 -119
- package/dist/defaults/CLAUDE.md +0 -1
- package/dist/vendor/config/env-retired.js +0 -137
- package/dist/vendor/config/retired-keys.js +0 -635
- package/docs/analytics.md +0 -140
- package/docs/app-state.md +0 -92
- package/docs/audit.md +0 -69
- package/docs/auth.md +0 -284
- package/docs/auto-updater.md +0 -243
- package/docs/boot-sequence.md +0 -44
- package/docs/build-system.md +0 -169
- package/docs/cdp-debugging.md +0 -169
- package/docs/common-mistakes.md +0 -21
- package/docs/config-schema.md +0 -120
- package/docs/context-menu.md +0 -112
- package/docs/context.md +0 -81
- package/docs/css.md +0 -84
- package/docs/deep-link.md +0 -186
- package/docs/environment-detection.md +0 -112
- package/docs/fontawesome.md +0 -109
- package/docs/hooks.md +0 -89
- package/docs/icons.md +0 -79
- package/docs/index.md +0 -328
- package/docs/installer-options.md +0 -165
- package/docs/ipc.md +0 -61
- package/docs/lib-modules.md +0 -53
- package/docs/logging.md +0 -227
- package/docs/menu.md +0 -160
- package/docs/releasing.md +0 -239
- package/docs/remote-config.md +0 -118
- package/docs/remote-scripts.md +0 -144
- package/docs/restart-manager.md +0 -144
- package/docs/runner.md +0 -290
- package/docs/sentry.md +0 -97
- package/docs/shared/agent-docs.md +0 -89
- package/docs/shared/analytics.md +0 -612
- package/docs/shared/brands.md +0 -57
- package/docs/shared/breaking-changes.md +0 -917
- package/docs/shared/config.md +0 -1948
- package/docs/shared/deploys.md +0 -341
- package/docs/shared/icons.md +0 -219
- package/docs/shared/local-dev.md +0 -167
- package/docs/shared/logging.md +0 -205
- package/docs/shared/monitoring.md +0 -167
- package/docs/shared/publishing.md +0 -187
- package/docs/shared/rulings.md +0 -34
- package/docs/shared/testing.md +0 -147
- package/docs/shared/theming.md +0 -629
- package/docs/shared/translation.md +0 -342
- package/docs/shared/updates.md +0 -61
- package/docs/signing.md +0 -293
- package/docs/startup.md +0 -142
- package/docs/storage.md +0 -59
- package/docs/templating.md +0 -101
- package/docs/test-boot-layer.md +0 -157
- package/docs/test-framework.md +0 -362
- package/docs/themes.md +0 -149
- package/docs/tooltips.md +0 -99
- package/docs/tray.md +0 -164
- package/docs/usage.md +0 -58
- package/docs/verts.md +0 -62
- package/docs/windows.md +0 -149
package/docs/shared/config.md
DELETED
|
@@ -1,1948 +0,0 @@
|
|
|
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, 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
|
|
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. shared: true limits the manage cycle to the per-brand operations AND skips the backend deploy whole (#882, deploys.md)
|
|
32
|
-
repo: { provider: 'github', org }, // ONE block (#883): where the brand hosts its SOURCE. Presence enables the repo service, `provider` defaults to `github`, and `org` is the only typed value: every repo name derives from `<brand.id>-<role>` and visibility is the brand root package.json's `private` field, so neither is configurable here
|
|
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: { currency, providers: { stripe: { publishableKey }, paypal: { clientId }, chargebee: { site }, coinbase: { enabled } }, products: […], winback: { enabled, percent, amount, duration } }, // currency: the ISO 4217 code every price in `products` is quoted in, default `USD` (#850). One currency per brand, named by the pricing page's JSON-LD, the checkout and the backend's order history alike; the successor to the retired `targets.web.currency`. #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, include: [] }, // presence picks the engine; absent = claude. `include` is the web route list (#858): globs with `!` negation, read in .gitignore order, default ['**', '!blog/**'], a brand list REPLACES it, and a page overrides it for itself with `translation.include: true`/`false` in frontmatter. 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
|
-
company: { id: '<parent brand.id>' | 'self', webhooks: true }, // #677: the ONE key joining a brand to its company, OUTSIDE `brand`. `id` and `webhooks` are the only typed keys; the loader FILLS the same key with { id, name, url, images: { wordmark }, webhooks } from the parent's own config, and a brand with no company resolves to its own name/url under a null id, so no reader carries a fallback. `webhooks: false` says the provider ACCOUNT (SendGrid Event Webhook, Beehiiv webhook) is somebody else's and the walk never repoints its one webhook. See "The company" below
|
|
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
|
-
reviews: { enabled, sites: [] },
|
|
56
|
-
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)
|
|
57
|
-
blog: { /* AI blog-content settings (Ghostii pipeline) */ },
|
|
58
|
-
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
|
|
59
|
-
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)
|
|
60
|
-
dataRequest: { /* GDPR/CCPA data-request query definitions */ },
|
|
61
|
-
directory: { enabled }, // opt in to the manager's directory PUSH: this brand's entry into the COMPANY's brands collection (#246/#677); default off, public facts only. docs/manager/directory.md
|
|
62
|
-
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
|
|
63
|
-
|
|
64
|
-
// TARGET-scoped config. EVERY KEY IS A TARGET NAME and key presence =
|
|
65
|
-
// "this brand enables a target by that name" (#886); the name is the folder
|
|
66
|
-
// `targets/<name>`. Every entry MUST declare its `type`: the framework that
|
|
67
|
-
// runs there, or `custom` (see Custom targets below). The keys below are
|
|
68
|
-
// named for their type, which is the common case, not a rule: a second web
|
|
69
|
-
// target is a sibling key with `type: 'web'` (see Targets below).
|
|
70
|
-
targets: {
|
|
71
|
-
web: { type: 'web', hosting: { provider: 'github' }, meta: { index }, imagemin, collections, client: { consent, … }, dev: { limitCollections } }, // `hosting` says who SERVES this target's built site (#883): web targets only, `github` by default, which publishes it to the target's own `<brand.id>-<name>` repo and serves it from Pages at the target's url. `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: { type: '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: { type: 'desktop', omega: { authPersistence }, app: { appId, productName, copyright, category, languages, darkModeSupport },
|
|
74
|
-
platforms: { mac, windows, linux },
|
|
75
|
-
autoUpdate: { enabled, autoDownload, startupDelayMs, feedCheckIntervalMs, idleEvalIntervalMs, maxAgeMs },
|
|
76
|
-
startup: { mode, openAtLogin: { enabled, mode } },
|
|
77
|
-
releases, remoteConfig: { enabled, url }, remoteScripts, restartManager }, // platforms: what this app SHIPS plus each platform's install knobs (#867): `platforms.<mac|windows|linux>.formats.<dmg|nsis|deb|appimage|snap>`, presence = enabled, `false` drops a default, per-format settings inside the format (see "The shipping declaration" below)
|
|
78
|
-
extension: { type: 'extension', platforms: { chrome, firefox, edge }, categories, listings }, // platforms: the SAME shipping declaration the desktop target carries (#867): `platforms.<chrome|firefox|edge>.formats.<zip|store>`, where the zip rides the release and the store is published to. categories: the AMO slugs a FIRST Firefox publish lists the add-on under (#884), default ['alerts-updates'] and validated against AMO's own set; the Firefox lane sends them with the summary (brand.description, cut to 250) and the target package.json `license` (UNLICENSED lists as all-rights-reserved). listings: per store, the `id` the store knows this extension by (#893: chrome/firefox/edge; the firefox one IS the manifest's gecko id) plus the `url` and `state` the site's /extension page reads (see "The site global" below)
|
|
79
|
-
mobile: { type: 'mobile' /* RESERVED: MAM parked */ },
|
|
80
|
-
community: { type: 'web', url: 'https://community.acme.com' }, // a SECOND web target → targets/community
|
|
81
|
-
api: { type: 'custom' }, // a target no framework owns (#603): the manager drives it entirely through its own package.json scripts; see "Custom targets" below
|
|
82
|
-
},
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
## Resolution
|
|
87
|
-
|
|
88
|
-
`loadConfig(projectDir, target, { defaults })` produces ONE resolved object per target:
|
|
89
|
-
|
|
90
|
-
```
|
|
91
|
-
schema defaults ← framework defaults ← company ← company.<environment> ← brand shared ← brand targets[name] ← brand.<environment> ← local shared ← local targets[name] ← local.<environment>
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
- **The bottom layer is the SCHEMA's own defaults** ([#478](https://github.com/Omega-JS-Stack/omega/issues/478)) —
|
|
95
|
-
see [Defaults & self-healing](#defaults--self-healing) below. `options.defaults` sits directly
|
|
96
|
-
above it and carries only what a framework does differently.
|
|
97
|
-
- "shared" = the file minus its `targets` key. In a standalone repo only the local layers exist.
|
|
98
|
-
- **Every layer is TWO files**: its `config/omega.json5` and an optional
|
|
99
|
-
`config/omega.<environment>.json5` overlay beside it
|
|
100
|
-
([#856](https://github.com/Omega-JS-Stack/omega/issues/856)), the config mirror of the
|
|
101
|
-
`.env` + `.env.<environment>` pair below. The overlay wins over its OWN base and still
|
|
102
|
-
loses to the layer above, each file keeping the shared-then-`targets[name]` split. The
|
|
103
|
-
three names are exactly what `envEnvironment()` returns, so the config overlay, the env
|
|
104
|
-
overlay and the runtime's own answer are ONE vocabulary: exactly ONE environment's
|
|
105
|
-
overlay composes, a missing overlay is nothing, and a file named anything else
|
|
106
|
-
(`omega.staging.json5`) is not a layer at all, so nothing reads it and nothing warns
|
|
107
|
-
about it. An overlay is a PARTIAL, holding only the overrides: the validator judges the
|
|
108
|
-
merged result, never an overlay on its own. Nothing scaffolds one; a brand writes the
|
|
109
|
-
file the day it has a public value that differs by environment (the case that named it:
|
|
110
|
-
a `pk_test_` Stripe publishable key for the local checkout, the live key in the base).
|
|
111
|
-
- **WHICH environment's overlay composes is the LANE's word, never the machine's**
|
|
112
|
-
([#856](https://github.com/Omega-JS-Stack/omega/issues/856)): `loadConfig` and
|
|
113
|
-
`composeTargetConfig` take `options.environment`, and every lane that produces a
|
|
114
|
-
production artifact names `production`, the way `stageFunctions({ environment })` already
|
|
115
|
-
names one for the `.env` overlay beside it. So a production build never carries a
|
|
116
|
-
development override, even though the machine that runs it answers `development` in a
|
|
117
|
-
terminal: the web build and both web deploy lanes name it, the desktop and extension
|
|
118
|
-
bakes name it under their own `OMEGA_BUILD_MODE` word, and the backend deploy stage names
|
|
119
|
-
it for both halves of the upload at once. The ambient answer (`envEnvironment()`) is what
|
|
120
|
-
a DEV boot and a deployed runtime take, which is the whole point of it; an environment
|
|
121
|
-
outside the three names throws. A compose asks outright: told nothing,
|
|
122
|
-
`composeTargetConfig` freezes the authored base layers and no overlay at all.
|
|
123
|
-
- **The company layer** is the company's own `company/config/omega.json5`, found through the
|
|
124
|
-
brand's `company: { id }` key and the machine registry (the same ONE resolver the `.env`
|
|
125
|
-
cascade, owner hooks and the signing tree read, [#677](https://github.com/Omega-JS-Stack/omega/issues/677)
|
|
126
|
-
see "The company" below). It layers exactly like the brand file (company shared <-
|
|
127
|
-
company `targets[name]`). A brand naming no company has no company layer; the resolved
|
|
128
|
-
result reports the file it used as `files.company`.
|
|
129
|
-
- **`projectDir` may be one of a target's SUBDIRS** — `functions/` (@omega.js/backend's runtime cwd)
|
|
130
|
-
or `dist/` (its staged build output, the view `omega test` loads): every walk (brand root,
|
|
131
|
-
company marker, local-layer fallback, target name, compose, `resolveBrandRoot`) treats the target root
|
|
132
|
-
as one level up, so `loadConfig(functionsDir, 'backend')`, `loadConfig(distDir, 'backend')` and
|
|
133
|
-
`loadConfig(targetRoot, 'backend')` resolve identically. A staged `config/omega.json5` inside either
|
|
134
|
-
subdir is the deployed runtime's own view, never an authored local layer.
|
|
135
|
-
- **WHICH entry is the target layer comes from the DIR**: `targets/community` resolves
|
|
136
|
-
`targets.community` (§ Targets). A standalone project has no such dir, so it is named by its
|
|
137
|
-
own file: the SINGLE declared target of the framework's type (a deployed backend staged from
|
|
138
|
-
`api: { type: 'backend' }` is still the `api` target, so its entry is the layer, `enabled` is
|
|
139
|
-
true and its url derives), and the type word when the file declares none or declares two.
|
|
140
|
-
- **Target sections overlay the TOP LEVEL**: `targets.desktop.platforms` resolves to
|
|
141
|
-
`config.platforms`; frameworks never read through `config.targets.<name>.…`.
|
|
142
|
-
- **Any shared key inside a target entry overrides it for that surface** — a desktop-only
|
|
143
|
-
Sentry DSN is just `targets.desktop.monitoring.providers.sentry.dsn`; disabling any integration per-surface is
|
|
144
|
-
uniformly `<key>: { enabled: false }`. One agnostic deep merge everywhere (objects merge,
|
|
145
|
-
arrays/scalars replace, `null` replaces, `undefined` is skipped).
|
|
146
|
-
- **A global value and its specific override share ONE name**: the standing rule is recorded in
|
|
147
|
-
[docs/shared/rulings.md](rulings.md) (Ian 2026-09-09).
|
|
148
|
-
- **@omega.js/web adds one MORE layer, per page** ([#607](https://github.com/Omega-JS-Stack/omega/issues/607)):
|
|
149
|
-
a page's (or layout's) `config:` frontmatter block merges over the resolved config for that page
|
|
150
|
-
alone, and templates read the result as `resolved.config.*` — the WHOLE merged config, never a
|
|
151
|
-
subset. It is the same deep merge, one layer higher: `… ← local targets[name] ← page config:`.
|
|
152
|
-
Nothing else in the config chain knows about it. The membership rule runs both ways: a page
|
|
153
|
-
restating a config section BARE is a build error, and a key under `config:` that no omega.json5
|
|
154
|
-
section answers to is a build error too. Page machinery — `meta`, `schema`, `layout`,
|
|
155
|
-
`permalink` — is not config and has no home in this file at all
|
|
156
|
-
([docs/web/frontmatter.md](../web/frontmatter.md)).
|
|
157
|
-
- The merged `targets` map rides along on the resolved config so enabled-target enumeration
|
|
158
|
-
survives (`getEnabledTargets()`); the `enabled` flag on the result says whether the resolved
|
|
159
|
-
NAME is listed, and the result carries that `name`.
|
|
160
|
-
- **A dir whose entry runs another framework is a loud error**: `loadConfig(targets/backend,
|
|
161
|
-
'desktop')` throws `targets.backend is type backend; this project runs the desktop framework`
|
|
162
|
-
rather than silently merging somebody else's layer.
|
|
163
|
-
- No target argument → whole-file merge (the shape omega-manager's disperse works with).
|
|
164
|
-
|
|
165
|
-
## Targets: every key is a NAME (#886)
|
|
166
|
-
|
|
167
|
-
`targets` is an object whose EVERY key is a target NAME. The name is the folder
|
|
168
|
-
(`targets/<name>`), the `--target=<name>` word, and the derived-repo suffix. Every entry
|
|
169
|
-
declares a `type`: the framework that runs there (`web`, `backend`, `desktop`, `extension`,
|
|
170
|
-
`mobile`) or `custom`. A brand runs two web sites by declaring two names:
|
|
171
|
-
|
|
172
|
-
```json5
|
|
173
|
-
targets: {
|
|
174
|
-
web: { type: 'web', url: 'https://somiibo.com' },
|
|
175
|
-
community: { type: 'web', url: 'https://community.somiibo.com' },
|
|
176
|
-
backend: { type: 'backend' },
|
|
177
|
-
desktop: { type: 'desktop' },
|
|
178
|
-
docs: { type: 'custom' },
|
|
179
|
-
}
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
- **No id, no folder key, no name table.** The key IS the answer, so the dir walk and the path
|
|
183
|
-
derivation are each one line: `targetPath(config, 'community')` → `targets/community`, and
|
|
184
|
-
`targetNameFromDir(projectDir)` → the target root's basename inside a brand (null for a
|
|
185
|
-
standalone project, whose dir name is arbitrary). `targetEntries(config)` is the ONE
|
|
186
|
-
enumeration, `{ name, type, ...entry }` in config order; `targetsOfType(config, 'web')`
|
|
187
|
-
filters it. Nothing derives a folder any other way.
|
|
188
|
-
- **The merge chain is by NAME**: `defaults ← company ← brand shared ← brand targets.<name> ←
|
|
189
|
-
local shared ← local targets.<name>`. A shared key inside an entry still overrides the shared
|
|
190
|
-
value for that surface, exactly as before.
|
|
191
|
-
- **The NAME is the subdomain** ([#588](https://github.com/Omega-JS-Stack/omega/issues/588),
|
|
192
|
-
Ian 2026-09-01). `targetUrl(config, name)` is the one resolver: the entry's own `url`, else a
|
|
193
|
-
target-scoped `brand.url`, else `brand.url` when the name IS its type (`web: { type: 'web' }`
|
|
194
|
-
is the brand itself), else `https://<name>.<host of brand.url>`, so `community: { type:
|
|
195
|
-
'web' }` is a COMPLETE declaration. An explicit `url` overrides it for a custom host. The host
|
|
196
|
-
is taken EXACTLY as `brand.url` states it (a `www.` brand derives `admin.www.acme.com`), and
|
|
197
|
-
no usable `brand.url` derives nothing at all (null, never a half-built `https://admin.`).
|
|
198
|
-
- **Validator rules**: a key must be a dir-safe slug (`/^[a-z][a-z0-9-]*$/`, because it is a
|
|
199
|
-
folder name), an entry must be a plain object, and its `type` must be one of the six. An ARRAY
|
|
200
|
-
is the retired multi-instance form and names its replacement, a sibling key
|
|
201
|
-
([breaking-changes.md](breaking-changes.md)). **More than one `backend` target is a WARNING**
|
|
202
|
-
(`warnings` on the result): backend stays single in practice, one Cloud Functions surface per
|
|
203
|
-
brand.
|
|
204
|
-
- **Dev ports offset by the target's position among its OWN type**
|
|
205
|
-
(`targetPortOffset(config, name)`): the first web target takes the classic base, the second
|
|
206
|
-
takes base + 1, and a backend declared between them shifts nothing
|
|
207
|
-
([docs/shared/local-dev.md](local-dev.md)). The N7 bump-if-taken allocator still guarantees a
|
|
208
|
-
free port.
|
|
209
|
-
- **An override AT the target IS its url**, never a base to stack the name on: the entry's own
|
|
210
|
-
`brand.url`, a `targets/<name>/config/omega.json5` naming `https://shop.acme.test`, or a dev
|
|
211
|
-
layer naming `http://localhost:4000` are each the answer as written (no `shop.shop.acme.test`,
|
|
212
|
-
no `https://admin.localhost:4000`). The derivation only runs while the resolved `brand.url` is
|
|
213
|
-
still the brand layer's. A CUSTOM host belongs on the entry's `url`, never on a target-scoped
|
|
214
|
-
`brand.url`: the authDomain check reads `brand.url`, so overriding it there makes that check
|
|
215
|
-
compare against the custom host and fail.
|
|
216
|
-
- **Brand-level facts stay brand-level.** `cloud.config.authDomain` compares against `brand.url`
|
|
217
|
-
for every target (one Firebase project, one backend, one authDomain), and so does the persona
|
|
218
|
-
domain the test lanes seed. Only a target's PUBLIC surface is per target: `site.url`, the
|
|
219
|
-
gh-pages CNAME `omega deploy`/`omega build` write, and the deploy path prefix.
|
|
220
|
-
- **One shared `api.<domain>`**: every web target talks to the same backend, so the manager's
|
|
221
|
-
cloud hosting op ensures exactly one API domain no matter how many targets a brand runs
|
|
222
|
-
([docs/manager/cloud.md](../manager/cloud.md)).
|
|
223
|
-
- **The resolved `url` is a declared key** (`packages/config/src/schema.js`), so a target load
|
|
224
|
-
raises no undeclared-key warning for the url it just derived.
|
|
225
|
-
- **Legacy `brand.subdomains` conversion rule**: each subdomain becomes its own web target,
|
|
226
|
-
`["admin", "cdn"]` → `admin: { type: 'web' }, cdn: { type: 'web' }` beside the main site; the
|
|
227
|
-
names carry the subdomains, so nothing else is written. The key itself is a retired path
|
|
228
|
-
(below), so a config still carrying it fails validation with that recipe.
|
|
229
|
-
- Non-goals: no cross-target shared builds, no per-target Firebase projects.
|
|
230
|
-
|
|
231
|
-
## The shipping declaration (`platforms`) and the format table (#867)
|
|
232
|
-
|
|
233
|
-
Desktop and extension declare what they SHIP in the SAME shape, in the ONE vocabulary
|
|
234
|
-
(@omega.js/client's `getPlatform()` / `getBrowser()`): platforms `mac`, `windows`,
|
|
235
|
-
`linux`, `chrome`, `firefox`, `edge`; formats `dmg`, `nsis`, `deb`, `appimage`, `snap`,
|
|
236
|
-
`zip`, `store`. It is `mac`, never `macos`, and `windows`, never `win`.
|
|
237
|
-
|
|
238
|
-
```json5
|
|
239
|
-
targets: {
|
|
240
|
-
desktop: {
|
|
241
|
-
type: 'desktop',
|
|
242
|
-
platforms: {
|
|
243
|
-
mac: { formats: { dmg: {} }, arch: ['universal'] },
|
|
244
|
-
windows: { formats: { nsis: {} }, oneClick: true, signing: { strategy: 'self-hosted' } },
|
|
245
|
-
linux: { formats: { deb: {}, appimage: {}, snap: { channels: ['stable'] } } },
|
|
246
|
-
},
|
|
247
|
-
},
|
|
248
|
-
extension: {
|
|
249
|
-
type: 'extension',
|
|
250
|
-
platforms: {
|
|
251
|
-
chrome: { formats: { zip: {}, store: {} } },
|
|
252
|
-
firefox: { formats: { zip: {}, store: { channel: 'listed' } } },
|
|
253
|
-
edge: false, // this brand does not ship to Edge
|
|
254
|
-
},
|
|
255
|
-
},
|
|
256
|
-
}
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
- **Presence is the switch, and every platform and format defaults ON.** A brand drops one
|
|
260
|
-
with the literal `false` (the `certificates: false` idiom), never by omitting it: a
|
|
261
|
-
config that says nothing ships everything the framework can build.
|
|
262
|
-
- **Per-format settings live INSIDE the format** (`linux.formats.snap.channels`,
|
|
263
|
-
`firefox.formats.store.channel`), which is why the declaration is an object and not a
|
|
264
|
-
list. The keys BESIDE `formats` are that platform's install knobs (arch, the NSIS
|
|
265
|
-
installer flags, mac entitlements, the Windows signing strategy).
|
|
266
|
-
- **The format table is [`platforms.js`](../../packages/config/src/platforms.js)**, the
|
|
267
|
-
successor of `desktop-artifacts.js`: per target, platform and format it says what the
|
|
268
|
-
format IS (`kind: 'asset'`, a file on the brand's releases repo with an `ext`; or
|
|
269
|
-
`kind: 'store'`, a publish), the env keys it cannot ship without (`requires`) and the
|
|
270
|
-
per-listing config paths a store needs before it can accept an upload (`listing`, the
|
|
271
|
-
`listings.<browser>.id` of [#893](https://github.com/Omega-JS-Stack/omega/issues/893)),
|
|
272
|
-
and, for a store, the `label` and `console` page a human creates that listing on.
|
|
273
|
-
Everything derives from it: the electron-builder target lists, the site's
|
|
274
|
-
`/download/<platform>/<format>` links and their versionless asset names, the manage
|
|
275
|
-
walk's per-brand list of ship credentials and its Enter-to-open ask, and the manual step
|
|
276
|
-
a publish prints for a listing that does not exist yet.
|
|
277
|
-
- **A `listing` says what a store needs BEFORE it can accept an upload**, which is why the
|
|
278
|
-
firefox store declares none: AMO takes the manifest's gecko id, so that publish always
|
|
279
|
-
goes through, and the id is the brand's own derived value, pinned into the brand config as
|
|
280
|
-
`listings.firefox.id` by the extension's local scaffold (never by a publish, which runs on
|
|
281
|
-
a throwaway checkout). Chrome and Edge assign theirs in a dashboard, so those two are
|
|
282
|
-
the ids the walk asks for, with a "not yet" skip.
|
|
283
|
-
- **`requires` names ENV keys, and the env schema owns them.** A key with a `requiredWhen`
|
|
284
|
-
applies only when that rule holds, so the Windows nsis format asks for the credentials of
|
|
285
|
-
the strategy the brand actually picked and no other's. The schema is also where each key's
|
|
286
|
-
`label`, `url` (the page that mints it) and `hint` live: one home for what a key is and
|
|
287
|
-
where it comes from (see "The env schema" below).
|
|
288
|
-
- **The one translation points.** Node's `darwin`/`win32` becomes OMEGA's word in
|
|
289
|
-
@omega.js/desktop's `utils/platform.js`; electron-builder's `mac`/`win`/`AppImage`/`nsis`
|
|
290
|
-
is spelled only in its `gulp/tasks/build-config.js`; GitHub's runner labels only in the
|
|
291
|
-
generated workflow. `APPLE_*` and `SNAPCRAFT_*` env names stay as they are: they name
|
|
292
|
-
vendors, not platforms.
|
|
293
|
-
- **Retired, with a migration**: `platforms.win` and `platforms.linux.snap` are registered
|
|
294
|
-
retired paths, so a brand still carrying either fails validation naming its replacement.
|
|
295
|
-
`npx omega manage --migration=platform-names --execute` performs the rewrite at the brand
|
|
296
|
-
root (every omega.json5 it owns, brand file and target files alike) and renames
|
|
297
|
-
`config/icons/macos/` to `config/icons/mac/` with it. Run it BEFORE `omega migrate`, which
|
|
298
|
-
DELETES a retired key rather than moving it. `snap.enabled: false` becomes
|
|
299
|
-
`formats.snap: false`, because presence is the switch now.
|
|
300
|
-
|
|
301
|
-
## Custom targets (#603)
|
|
302
|
-
|
|
303
|
-
A brand also runs targets no framework owns — a Render API, a worker, a script. They are
|
|
304
|
-
declared like any other target, with `type: 'custom'`:
|
|
305
|
-
|
|
306
|
-
```json5
|
|
307
|
-
targets: {
|
|
308
|
-
web: { type: 'web' },
|
|
309
|
-
api: { type: 'custom' }, // → targets/api
|
|
310
|
-
jobs: { type: 'custom' }, // → targets/jobs
|
|
311
|
-
nightly: { type: 'custom' }, // → targets/nightly
|
|
312
|
-
}
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
- **The type is the declaration**, and it is the ONLY thing checked: the key is a name, so a
|
|
316
|
-
name no framework owns is a deliberate brand choice rather than a typo to guess at.
|
|
317
|
-
- **Its verbs are its own package.json scripts**: `start`, `build`, `test`, `deploy`, `clean`.
|
|
318
|
-
The manager runs each through `npm run <verb>` when the script is present and skips it
|
|
319
|
-
loudly when it is absent — nothing is inferred or defaulted.
|
|
320
|
-
- **No framework service reconciles it.** The only manage op that sees a custom target is the
|
|
321
|
-
workspace service (structure, agent docs, settings). Nothing is composed into a `.env` of its
|
|
322
|
-
own; it INHERITS the brand keys — the manager loads the env chain into `process.env` before it
|
|
323
|
-
spawns anything, so a custom target started by `omega dev`/`omega deploy` has them. A standalone
|
|
324
|
-
run inside the target dir does not (there is no `@omega.js/config` in there to walk the cascade).
|
|
325
|
-
Full contract: [docs/manager/index.md](../manager/index.md) § Custom targets.
|
|
326
|
-
|
|
327
|
-
## Backend project type (#584)
|
|
328
|
-
|
|
329
|
-
`targets.backend.projectType` says how the backend RUNS, and it is the only switch:
|
|
330
|
-
|
|
331
|
-
```json5
|
|
332
|
-
targets: {
|
|
333
|
-
backend: { projectType: 'custom' }, // default is 'firebase'
|
|
334
|
-
}
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
- **`'firebase'` (default)** — the backend exports Cloud Functions, deploys with `firebase deploy`,
|
|
338
|
-
and runs locally on the emulator suite. Everything OMEGA does today.
|
|
339
|
-
- **`'custom'`** — the SAME backend (same routes, same schemas, same auth middleware, same
|
|
340
|
-
helpers, same `.env`) served by its Express app on `process.env.PORT`, for a container host
|
|
341
|
-
(Render & co). `initialize()` reads the mode off this key, so a brand's `src/index.js` is
|
|
342
|
-
unchanged; an explicit `projectType` option still wins.
|
|
343
|
-
- **What custom mode removes is the Firebase LANE, not Firebase**: no Functions deploy, no
|
|
344
|
-
emulator, no emulator test run — those four verbs refuse loudly and name their replacement
|
|
345
|
-
([docs/backend/index.md](../backend/index.md)). `firebase-admin` still loads, so a custom
|
|
346
|
-
server that reads Firestore or verifies an ID token works exactly as before.
|
|
347
|
-
- **Not to be confused with a custom TARGET** (above): that is a target no framework owns.
|
|
348
|
-
This one IS the `@omega.js/backend` target, with a different artifact. Same-type duplicates
|
|
349
|
-
are sibling keys (§ Targets).
|
|
350
|
-
- The brand-root behavior — deploy through the target's own `deploy` script, `omega dev` booting
|
|
351
|
-
the server instead of the emulator: [docs/manager/index.md](../manager/index.md).
|
|
352
|
-
|
|
353
|
-
## Hard rules
|
|
354
|
-
|
|
355
|
-
- **Secrets NEVER live in omega.json5** — they live in `.env`. `loadConfig` throws on any
|
|
356
|
-
key matching `/(secret|privateKey|apiSecret)$/i` in any section of a raw file, before any
|
|
357
|
-
merge. Public credentials (`publishableKey`, `clientId`, `cloud.config.apiKey`) pass by
|
|
358
|
-
design.
|
|
359
|
-
- **The legacy `targets` ARRAY form throws**: `targets` is an object keyed by target name,
|
|
360
|
-
and a per-target ARRAY (the retired multi-instance form) is a validation error naming its
|
|
361
|
-
replacement, a sibling key.
|
|
362
|
-
- Schema findings (required/type/min/match/enum) come back as `errors`, not throws — build-time
|
|
363
|
-
audit throws on them, boot warns/fails per framework policy.
|
|
364
|
-
|
|
365
|
-
## The company (`company: { id }`), #677
|
|
366
|
-
|
|
367
|
-
ONE key joins a brand to its company, OUTSIDE `brand` (Ian 2026-09-12: "brand key is for
|
|
368
|
-
things about this brand, and the parent/company/organization key is OUTSIDE of that"):
|
|
369
|
-
|
|
370
|
-
```json5
|
|
371
|
-
company: { id: 'itw-creative-works' }, // a sub-brand: the parent's brand.id
|
|
372
|
-
company: { id: 'self' }, // the company brand itself, kept visible on purpose
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
- **The `id` is the only key a consumer types.** The loader FILLS the same object at load,
|
|
376
|
-
from the parent's own config: `company: { id, name, url, images: { wordmark } }`. One name,
|
|
377
|
-
one shape, at every level.
|
|
378
|
-
- **A brand with no `company` key resolves to `{ id: null, name: brand.name, url: brand.url,
|
|
379
|
-
images: {} }`**, so no reader anywhere needs a fallback: the footer credit, the email
|
|
380
|
-
wordmark and the in-house ads api all read one shape. `company.name` / `company.url` /
|
|
381
|
-
`company.images` are RESOLVED, never typed: an authored one fails the load naming the key.
|
|
382
|
-
- **The company's shared files live in `company/` inside the company brand's own repo**
|
|
383
|
-
(`company/config/omega.json5`, `company/.env`, `company/.omega/certificates/apple/`), and
|
|
384
|
-
inheritance is ONE rule: a brand-level file the child lacks resolves from there at the same
|
|
385
|
-
relative path (`resolveCompany(brandRoot).file(relPath)`). A new kind of inherited file
|
|
386
|
-
costs zero code. The manage walk's preflight SENDS an operator there
|
|
387
|
-
([#910](https://github.com/Omega-JS-Stack/omega/issues/910)): once a company resolves,
|
|
388
|
-
every fix line it prints for a missing key names that `company/.env` by path, with the
|
|
389
|
-
brand `.env` as the override, so a credential every sibling brand needs is pasted once.
|
|
390
|
-
- **The doctrine line: a brand with a company reuses the company tree first, always.**
|
|
391
|
-
- **WHERE that repo is on this machine is a cache, never configuration**: `~/.omega/brands.json`
|
|
392
|
-
(`OMEGA_HOME` moves it), keyed by `brand.id`, which every `loadConfig()` refreshes for its
|
|
393
|
-
own brand, and nobody maintains it. A company that has never been loaded here prints ONE line
|
|
394
|
-
per run (`Company <id> is not on this machine: inheritance off…`) and the run continues with
|
|
395
|
-
no company layer.
|
|
396
|
-
- **Off-laptop, nothing ever reads `company/`**: the machine that dispatches a deploy resolves
|
|
397
|
-
it and writes `config/company-resolved.json5` beside the brand config (`{ config, company }`),
|
|
398
|
-
which the mirror push carries and then removes; the loader reads that generated layer when the
|
|
399
|
-
registry has no line. The env half needs nothing: the deploy precheck's secrets push sends the COMPOSED target
|
|
400
|
-
env (company ← brand ← target) as that repo's own secrets.
|
|
401
|
-
|
|
402
|
-
Full contract, and the `omega company init` half: [../manager/company.md](../manager/company.md).
|
|
403
|
-
|
|
404
|
-
## Config or env? (#893)
|
|
405
|
-
|
|
406
|
-
One value, one home, and the line between the two files is what it IS, not who
|
|
407
|
-
reads it:
|
|
408
|
-
|
|
409
|
-
- **config/omega.json5 holds what is PUBLIC by design**: a value a store URL
|
|
410
|
-
carries, a browser bundle ships, or a binary prints. A store item id, a
|
|
411
|
-
reCAPTCHA site key, a PayPal client id, a Chargebee site name, `cloud.config.apiKey`,
|
|
412
|
-
a Stripe `publishableKey`: anyone can read them off the shipped product, so
|
|
413
|
-
hiding them buys nothing and splitting them across two files costs a drift.
|
|
414
|
-
- **`.env` holds secrets, the login coordinates that only travel WITH a secret,
|
|
415
|
-
and machine paths**: an API key or token; the username, OAuth client id,
|
|
416
|
-
Apple issuer / key id / team id that are useless without the secret beside
|
|
417
|
-
them and are asked for in the same breath (Ian 2026-09-12: the Apple triple
|
|
418
|
-
and the Namecheap username stay in env); and a path that is true of one
|
|
419
|
-
laptop (`OMEGA_FONTAWESOME_ROOT`, `machineLocal`).
|
|
420
|
-
- The validator enforces the first half mechanically: a secret-shaped key in
|
|
421
|
-
omega.json5 hard-fails the load (Hard rules above). The second half is this
|
|
422
|
-
rule plus the retired-key register below.
|
|
423
|
-
- **A public value in config needs no CI delivery.** It rides the snapshot a
|
|
424
|
-
deploy pushes like every other config value, so it is never a repo secret and
|
|
425
|
-
never a `${{ secrets.KEY }}` line.
|
|
426
|
-
|
|
427
|
-
### Retired env keys (`src/env-retired.js`)
|
|
428
|
-
|
|
429
|
-
Six public identifiers moved out of `.env` with #893. There is no dual-read, so
|
|
430
|
-
a stale line is a value nothing consults: every `.env` layer read
|
|
431
|
-
(`parseEnvFile`, which both the process cascade and the artifact composer go
|
|
432
|
-
through) FAILS on one, naming the move.
|
|
433
|
-
|
|
434
|
-
| Retired `.env` key | Its config home |
|
|
435
|
-
|---|---|
|
|
436
|
-
| `CHROME_EXTENSION_ID` | `targets.<name>.listings.chrome.id` |
|
|
437
|
-
| `FIREFOX_EXTENSION_ID` | `targets.<name>.listings.firefox.id` |
|
|
438
|
-
| `EDGE_PRODUCT_ID` | `targets.<name>.listings.edge.id` |
|
|
439
|
-
| `RECAPTCHA_SITE_KEY` | `captcha.providers.recaptcha.siteKey` |
|
|
440
|
-
| `PAYPAL_CLIENT_ID` | `payment.providers.paypal.clientId` |
|
|
441
|
-
| `CHARGEBEE_SITE` | `payment.providers.chargebee.site` |
|
|
442
|
-
|
|
443
|
-
The by-hand move for a brand that still carries one:
|
|
444
|
-
[breaking-changes.md](breaking-changes.md). The SECRET halves are untouched:
|
|
445
|
-
`RECAPTCHA_SECRET_KEY`, `PAYPAL_CLIENT_SECRET`, `CHARGEBEE_API_KEY` and every
|
|
446
|
-
store API credential stay in `.env`.
|
|
447
|
-
|
|
448
|
-
The same register carries the keys that stayed secrets and only changed NAME
|
|
449
|
-
([#845](https://github.com/Omega-JS-Stack/omega/issues/845)). The refusal is
|
|
450
|
-
identical, and so is the silence it prevents: the env schema does not know the
|
|
451
|
-
old name, so the value reaches nothing. The fix is renaming the line, not
|
|
452
|
-
deleting it. Rows are exact names, one per provider.
|
|
453
|
-
|
|
454
|
-
| Renamed `.env` key | Its new `.env` name |
|
|
455
|
-
|---|---|
|
|
456
|
-
| `OAUTH2_GOOGLE_CLIENT_ID` | `CONNECTIONS_GOOGLE_CLIENT_ID` |
|
|
457
|
-
| `OAUTH2_GOOGLE_CLIENT_SECRET` | `CONNECTIONS_GOOGLE_CLIENT_SECRET` |
|
|
458
|
-
|
|
459
|
-
And it carries the keys retired OUTRIGHT
|
|
460
|
-
([#819](https://github.com/Omega-JS-Stack/omega/issues/819), Ian 2026-09-13): no config
|
|
461
|
-
path and no new env name, because a MECHANISM took the key's place rather than another
|
|
462
|
-
value. Such a row declares `replacement: null` and its refusal reads as a deletion with
|
|
463
|
-
nowhere to move the value to. The test-lane pair is the whole set today: web, desktop and
|
|
464
|
-
extension each test their own sign-in against a persona the backend emulator seeds
|
|
465
|
-
([#904](https://github.com/Omega-JS-Stack/omega/issues/904)), so no suite holds a
|
|
466
|
-
credential of its own and the `testing` schema group is gone with them.
|
|
467
|
-
|
|
468
|
-
| Retired `.env` key | What replaced it |
|
|
469
|
-
|---|---|
|
|
470
|
-
| `OMEGA_TEST_FIREBASE_ADMIN_KEY` | Nothing to move: delete the line. The seeded persona (#904) replaces the minted custom token |
|
|
471
|
-
| `OMEGA_TEST_USER_UID` | Nothing to move: delete the line. The emulator's roster names the persona (#904) |
|
|
472
|
-
|
|
473
|
-
## The .env cascade (secrets) — D15
|
|
474
|
-
|
|
475
|
-
Secrets mirror the config hierarchy (`src/env.js`), weakest → strongest:
|
|
476
|
-
|
|
477
|
-
```
|
|
478
|
-
company .env ← brand .env ← local .env ← shell env
|
|
479
|
-
```
|
|
480
|
-
|
|
481
|
-
Every layer is TWO files: its `.env`, and the `.env.<environment>` overlay that wins
|
|
482
|
-
over it.
|
|
483
|
-
|
|
484
|
-
```
|
|
485
|
-
.env ← .env.development | .env.testing | .env.production
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
- **Same walk as the config cascade**: `{brand}/targets/{target}` layers the brand root's `.env`
|
|
489
|
-
under the target's; a brand naming a company (`company: { id }`) layers that company's
|
|
490
|
-
`company/.env` underneath that (#677). `findBrandRoot` in `load.js` is the ONE definition
|
|
491
|
-
of the walk; both cascades use it.
|
|
492
|
-
- **`.env.<environment>` overlays the `.env` beside it**
|
|
493
|
-
([#586](https://github.com/Omega-JS-Stack/omega/issues/586)) — the widespread standard
|
|
494
|
-
(Next.js, Vite, Rails dotenv, dotenv-flow). The three names are exactly what
|
|
495
|
-
`envEnvironment()` returns (`development` | `testing` | `production`), so the file name
|
|
496
|
-
and the runtime's own answer are ONE vocabulary; only the RUNNING environment's overlay
|
|
497
|
-
is read, and every key in it is equal — whatever it holds wins, values are TRUSTED, and
|
|
498
|
-
no key gets special treatment. The composed artifact is flat and single-environment: a
|
|
499
|
-
deploy composes base + production, the emulator base + development, a test lane base +
|
|
500
|
-
testing, and no other environment's file ever rides along. Onboard scaffolds all three
|
|
501
|
-
beside the brand `.env`, empty but for a header comment (`.env.*` is gitignored).
|
|
502
|
-
- **A key of your own in the brand root `.env` reaches every target**
|
|
503
|
-
([#835](https://github.com/Omega-JS-Stack/omega/issues/835)). The schema filters the
|
|
504
|
-
company and brand layers by TARGET, and a key it never declared names no target, so it
|
|
505
|
-
composes everywhere (base `.env` and `.env.production` alike) and rides the same pipeline
|
|
506
|
-
to GitHub Actions as every declared one. Want it on one target only? Put it in that
|
|
507
|
-
target's own `.env`, the per-key override that was always unfiltered.
|
|
508
|
-
- **Precedence via dotenv's no-override semantics**: files load strongest-first and never
|
|
509
|
-
overwrite keys already set, so the shell always wins and local beats brand beats company.
|
|
510
|
-
- **A RELOAD honors edits, because ownership is remembered**
|
|
511
|
-
([#724](https://github.com/Omega-JS-Stack/omega/issues/724)). After a boot load every key
|
|
512
|
-
is "already set", so presence can no longer tell a shell value from a file value — which
|
|
513
|
-
is why `loadEnv` alone can never deliver an edit. The first chain load in a process
|
|
514
|
-
snapshots what `process.env` carried before any file was read (shell-owned, forever) and
|
|
515
|
-
records what each file layer delivers (file-owned). `reloadEnv(startDir, options?)` drops
|
|
516
|
-
the file-owned keys, then loads again: a NEW key and an EDITED value both land, a key
|
|
517
|
-
dropped from the file is dropped from the process, and a shell-set value is never touched.
|
|
518
|
-
The dev lanes' `.env` watchers ([#681](https://github.com/Omega-JS-Stack/omega/issues/681))
|
|
519
|
-
are its one caller.
|
|
520
|
-
- **Empty file values never claim a key (cp95a, friction #20)**: `KEY=` / `KEY=""` in any
|
|
521
|
-
`.env` FILE means "documented here, value supplied by another layer" — a scaffolded local
|
|
522
|
-
file full of placeholders can't shadow the brand root's real values. Only the shell can
|
|
523
|
-
deliberately set a key to empty. The brand root's `.env` stub ships `# KEY=` commented
|
|
524
|
-
placeholders, rendered from the env schema (the merge protocol keeps set values on their
|
|
525
|
-
line, converges empties to the placeholder); no framework scaffolds a target `.env` at all.
|
|
526
|
-
- **Defined at the source, resolved at runtime/build**: a brand-wide `GH_TOKEN` lives once
|
|
527
|
-
in the brand `.env`; every framework CLI/build resolves the chain at boot
|
|
528
|
-
(`loadEnv(process.cwd())` in the web/desktop/extension CLIs + gulp pipelines,
|
|
529
|
-
`loadEnv(functionsDir)` in the @omega.js/backend CLI and runtime). Nothing is copied
|
|
530
|
-
between `.env` files just to be visible.
|
|
531
|
-
- **The brand root's `.env` is the ONE file humans and the manager edit**
|
|
532
|
-
([#678](https://github.com/Omega-JS-Stack/omega/issues/678)). A target's own `.env` is
|
|
533
|
-
optional and overrides PER KEY, by hand; no machine ever writes one.
|
|
534
|
-
- **Backend's local layer is the target-root `.env`** (`targets/backend/.env`) — the layer a
|
|
535
|
-
human uses to override one key for that surface. What physically ships is the STAGED
|
|
536
|
-
`dist/.env`: it rides the Firebase deploy artifact (the cloud can't walk up), so every verb
|
|
537
|
-
that produces one (`omega build`, `dev`, `test`, `deploy`) composes it from the file layers,
|
|
538
|
-
filtered by the env schema. In the cloud the walk finds no brand/company and behavior
|
|
539
|
-
is identical to plain dotenv.
|
|
540
|
-
- **The brand-generated keys are the manager's to mint** — `OMEGA_ADMIN_KEY`,
|
|
541
|
-
`OMEGA_WEBHOOK_KEY`, `OMEGA_NAMESPACE` and `UNSUBSCRIBE_HMAC_KEY` have no dashboard
|
|
542
|
-
behind them, so the onboard stub writes them for a fresh brand and the workspace
|
|
543
|
-
service's `env-keys` step mints any the cascade doesn't serve on every manage
|
|
544
|
-
([#569](https://github.com/Omega-JS-Stack/omega/issues/569)). The env schema below is
|
|
545
|
-
the one list feeding both, a company-served value is never shadowed by a brand-level
|
|
546
|
-
one, and nothing prints a minted value.
|
|
547
|
-
- Missing files and unreadable/stale markers skip silently — `loadEnv` never throws for
|
|
548
|
-
an absent layer.
|
|
549
|
-
|
|
550
|
-
## The env schema (`src/env-schema.js`) — #581
|
|
551
|
-
|
|
552
|
-
The omega.json5 schema's sibling: ONE inventory of the env keys OMEGA needs — who owns
|
|
553
|
-
each, which targets read it, whether OMEGA mints it or a human pastes it from a third
|
|
554
|
-
party, whether it is required, and what it does. Everything that used to hand-keep its
|
|
555
|
-
own list derives from it, so a new key is **one entry**, never four edits.
|
|
556
|
-
|
|
557
|
-
```js
|
|
558
|
-
{
|
|
559
|
-
name: 'OMEGA_ADMIN_KEY', // the env var (SCREAMING_SNAKE)
|
|
560
|
-
match: /^CONNECTIONS_.+$/, // …or a pattern, for dynamic families
|
|
561
|
-
owner: 'workspace', // the manager service that owns it
|
|
562
|
-
// ('backend' = the framework itself)
|
|
563
|
-
targets: ['backend'], // the targets whose runtime READS it
|
|
564
|
-
group: 'omega', // its ENV_GROUPS bucket (.env file order)
|
|
565
|
-
generated: () => randomBytes(32)…, // the function that MINTS a value
|
|
566
|
-
default: 'value', // …or a static default, where one applies
|
|
567
|
-
secret: true, // never printed, never in omega.json5
|
|
568
|
-
required: true, // absent = the backend refuses to boot
|
|
569
|
-
delivery: { backend: 'env' }, // per target, HOW the value gets there
|
|
570
|
-
requiredWhen: 'captcha.providers…', // non-empty when this config path is truthy
|
|
571
|
-
publicAtRest: true, // sanctions a 'bake' (readable in the artifact)
|
|
572
|
-
machineLocal: true, // this machine's fact — never published to CI
|
|
573
|
-
label: 'Snap Store credentials', // the human name an ask opens with
|
|
574
|
-
url: 'https://snapcraft.io/account', // the page that MINTS it
|
|
575
|
-
hint: 'Run `snapcraft export-login -`', // how to get it there
|
|
576
|
-
description: 'What the key drives.',
|
|
577
|
-
}
|
|
578
|
-
```
|
|
579
|
-
|
|
580
|
-
- **`label`, `url` and `hint` are the human half, and this schema is their one home**
|
|
581
|
-
([#867](https://github.com/Omega-JS-Stack/omega/issues/867)). The manager's REQUIRES
|
|
582
|
-
registry used to carry a second copy per service, which is how every desktop signing key
|
|
583
|
-
and every extension store key ended up with a mint page in NEITHER place: no service
|
|
584
|
-
declared them, so nobody could be asked for them. `serviceInputSpec` fills each ask from
|
|
585
|
-
here now, and the registry keeps only its own fields (which service asks, whether the ask
|
|
586
|
-
is interactive, where Disable writes `false`). `url: null` is a deliberate statement that
|
|
587
|
-
no page mints the value, and such an entry owes a `hint` saying where it does come from.
|
|
588
|
-
The sweep test (`packages/manager/test/service-input.test.js`) holds the line: no
|
|
589
|
-
label/url/hint in the registry, a label on every pasted key, and a url (or an explicit
|
|
590
|
-
null plus a hint) on every one that is not machine-local.
|
|
591
|
-
|
|
592
|
-
- **`generated:` is the mint switch.** Those keys have no dashboard behind them, so the
|
|
593
|
-
manager writes them into a brand `.env` — at onboard, and on every manage that finds
|
|
594
|
-
one missing. Everything else is a credential a human provides.
|
|
595
|
-
- **Only a key OMEGA can produce may be `required`.** Refusing every boot over a secret
|
|
596
|
-
nobody can mint would be a hostage note, not a guard — the config test pins it.
|
|
597
|
-
- **`targets:` is the composition domain.** Every verb composes its target's RUNTIME env
|
|
598
|
-
(the backend's staged `dist/.env` — the only artifact that ships and so cannot walk up to
|
|
599
|
-
the brand layer) from the file layers, taking the entries whose `targets` name that
|
|
600
|
-
target. The schema is the only filter on DECLARED keys: no hand list, and PATTERN entries
|
|
601
|
-
(`match:`, e.g. the `CONNECTIONS_*` family) compose exactly like named ones. A key the schema
|
|
602
|
-
names for other targets never reaches this one, so a desktop signing key stays out of the
|
|
603
|
-
functions upload. A key the schema does not know AT ALL is the consumer's own
|
|
604
|
-
([#835](https://github.com/Omega-JS-Stack/omega/issues/835)): it names no target to be
|
|
605
|
-
filtered by, so a custom line in the brand root `.env` (or its `.env.production` overlay)
|
|
606
|
-
composes for EVERY target, and a consumer who wants one on a single target puts it in
|
|
607
|
-
that target's own `.env`, which passes unfiltered anyway. Other targets read brand values through the cascade above at runtime,
|
|
608
|
-
so nothing is written for them.
|
|
609
|
-
- **`deliverAs:` renames on delivery**: the entry's brand-level name is what the cascade
|
|
610
|
-
carries (`GOOGLE_ANALYTICS_SECRET_BACKEND`), and the target receives it under the name
|
|
611
|
-
its own code reads (`GOOGLE_ANALYTICS_SECRET`). One entry, both names.
|
|
612
|
-
- **`delivery:` says HOW a value reaches each target**
|
|
613
|
-
([#627](https://github.com/Omega-JS-Stack/omega/issues/627)): `'env'` (read from the
|
|
614
|
-
composed `.env` at runtime — the backend), `'ci'` (the generated workflow injects it
|
|
615
|
-
into the runner env for the build step), or `'bake'` (the build writes it into the
|
|
616
|
-
shipped artifact, because the installed app runs with no `.env`). A bake implies the
|
|
617
|
-
CI injection — the workflow delivers the value the build then bakes. One renderer in
|
|
618
|
-
`@omega.js/config/env-delivery` derives everything from these declarations: each
|
|
619
|
-
target's workflow secrets block, its bake list, and its publish-step secret set. No
|
|
620
|
-
hand-kept `${{ secrets.KEY }}` list survives anywhere.
|
|
621
|
-
- **The delivered set is the schema PLUS what only the composed env can name**
|
|
622
|
-
([#835](https://github.com/Omega-JS-Stack/omega/issues/835),
|
|
623
|
-
[#876](https://github.com/Omega-JS-Stack/omega/issues/876)). `deliveredKeys(target, modes,
|
|
624
|
-
{ values })` is the ONE primitive every list derives from, and the composed production
|
|
625
|
-
values it takes add two kinds of key the schema cannot name: a `match` FAMILY member (the
|
|
626
|
-
schema knows `CONNECTIONS_<PROVIDER>_CLIENT_ID` as a shape, so only a brand's own `.env`
|
|
627
|
-
says which providers exist), and a CUSTOM key the schema never declared. A custom key
|
|
628
|
-
travels in its target's FILE mode: on the backend it is written into the `.env` the runner
|
|
629
|
-
builds, like an `env` delivery; on web, desktop and the extension it reaches the runner env
|
|
630
|
-
only, like a `ci` one. `machineLocal` keys and declared keys this target does not deliver
|
|
631
|
-
stay home exactly as before, and neither a `WORKFLOW_OWNED_KEYS` name (the template
|
|
632
|
-
declares those itself) nor a `GITHUB_`-prefixed one (GitHub refuses that prefix as a
|
|
633
|
-
secret name) is ever taken from a composed set. Both halves of the backend workflow (the
|
|
634
|
-
injected block and the `.env` writer's key list) render from that one set, so they cannot
|
|
635
|
-
disagree inside a run.
|
|
636
|
-
- **`env` on a NON-backend target declares the laptop's composed `.env` as the channel**
|
|
637
|
-
([#819](https://github.com/Omega-JS-Stack/omega/issues/819)). The workflow renderer walks
|
|
638
|
-
`ci` and `bake` only, so such a key renders no workflow line, reaches no runner and is
|
|
639
|
-
published as no repo secret: it is read where the developer runs the build.
|
|
640
|
-
`OPENAI_API_KEY` is the case that made it explicit (`{ web: 'env', backend: 'env',
|
|
641
|
-
extension: 'env' }`), because translation runs locally and CI reads the committed cache
|
|
642
|
-
([#905](https://github.com/Omega-JS-Stack/omega/issues/905)).
|
|
643
|
-
- **A workflow block carries `ci` + `bake`, except the backend's, which carries `env` too**
|
|
644
|
-
([#872](https://github.com/Omega-JS-Stack/omega/issues/872)). Everywhere else the artifact holds its values inside itself and reads
|
|
645
|
-
no env file, so the CI half is the whole block. The backend's deployed artifact ships a
|
|
646
|
-
COMPOSED `.env`, and its deploy runs on a runner with no brand checkout to compose one
|
|
647
|
-
from, so its workflow writes that file out of the runner env: every `env` delivery has to
|
|
648
|
-
be up there to be written. `WORKFLOW_MODES` in `env-delivery.js` is the one home of that
|
|
649
|
-
exception; `DEFAULT_WORKFLOW_MODES` is every other target. Riding with it is
|
|
650
|
-
**`OMEGA_SERVICE_ACCOUNT_JSON`** (`delivery: { backend: 'ci' }`), the deploy credential as
|
|
651
|
-
the key file's own CONTENTS: it is a FILE on every other lane (minted into the brand's
|
|
652
|
-
`.omega/secrets/`, staged into `dist/`), so it renders no brand `.env` line, and the
|
|
653
|
-
backend's precheck values it from that authored chain instead of from a composed env.
|
|
654
|
-
Riding beside it: **`OMEGA_LICENSE_KEY`** (`ci` on all four targets now), because the
|
|
655
|
-
license check runs inside the deploy and the backend's deploy runs on a runner too.
|
|
656
|
-
- **`ci` is what keeps a key out of an artifact's `.env`.** The two lanes that write a
|
|
657
|
-
backend `.env` read that one declaration: `envFileKeys('backend')` (the generated key
|
|
658
|
-
list the workflow's writer reads out of the runner env) takes the `env` deliveries only,
|
|
659
|
-
and `artifactEnvValues('backend', values)` strips every `ci` key out of the COMPOSED
|
|
660
|
-
values before the stage serializes `dist/.env`. The composer itself resolves a value for
|
|
661
|
-
everything the target claims, `ci` included, because the secrets publisher has to value
|
|
662
|
-
what it publishes from the same cascade as everything else: the artifact is the narrower
|
|
663
|
-
half, and those two functions are where that is said.
|
|
664
|
-
- **A baked key is public at rest.** Anyone who unpacks the app can read it, so a
|
|
665
|
-
`secret: true` entry may only bake when it also declares `publicAtRest: true` — the
|
|
666
|
-
renderer THROWS otherwise, on every lane, so a real credential can never reach an
|
|
667
|
-
artifact by accident. The GA Measurement Protocol secrets are the sanctioned baked keys.
|
|
668
|
-
- **`machineLocal: true` marks this machine's own facts** (`OMEGA_FONTAWESOME_ROOT`):
|
|
669
|
-
composed locally like any key, but filtered out of every rendered block and every
|
|
670
|
-
published secret set — a laptop path has no business in CI.
|
|
671
|
-
- **`requiredWhen: '<config path>'` is the conditional presence rule**
|
|
672
|
-
([#626](https://github.com/Omega-JS-Stack/omega/issues/626)): when the resolved config
|
|
673
|
-
path is truthy, the key must be non-empty. Presence only, never a value-shape check,
|
|
674
|
-
and one-directional. One checker, `checkEnvRules()` in `@omega.js/config/env-rules`,
|
|
675
|
-
answers it for every consumer: the backend boot (production refuses, development warns
|
|
676
|
-
once), the desktop and extension build bakes (build mode throws, development warns),
|
|
677
|
-
and the manager's manage walk (warns per enabled target, never fails). A TARGET-LESS
|
|
678
|
-
entry (`targets: []` — a SERVICE's own key, like `SENTRY_AUTH_TOKEN`) is owed when its
|
|
679
|
-
path is truthy in the brand root **or in ANY enabled target's resolved config**
|
|
680
|
-
([#683](https://github.com/Omega-JS-Stack/omega/issues/683)): the service writes its
|
|
681
|
-
values where the target lives — monitoring lands one Sentry DSN per surface and leaves
|
|
682
|
-
the shared slot null — so a root-only read never fired on the shape brands carry.
|
|
683
|
-
Violations name the BRAND-level key — the one a human sets in the brand `.env`.
|
|
684
|
-
- **Nothing is CLEARED, because nothing is written into a hand file**
|
|
685
|
-
([#636](https://github.com/Omega-JS-Stack/omega/issues/636),
|
|
686
|
-
[#678](https://github.com/Omega-JS-Stack/omega/issues/678)): the composed artifact is
|
|
687
|
-
rebuilt from the cascade every verb, so a credential the brand root retires stops being
|
|
688
|
-
served the moment it is dropped.
|
|
689
|
-
- **`group:` picks the .env section**, and `ENV_GROUPS` owns the file order plus each
|
|
690
|
-
section's comment. A group marked `file: false` (the `runtime` group) never reaches a
|
|
691
|
-
brand `.env` at all — those keys resolve some other way (from config at boot, from the
|
|
692
|
-
developer's own shell), so they are neither rendered as placeholders nor composed.
|
|
693
|
-
- **Runtime/platform vars are NOT in the schema**: `FIREBASE_CONFIG`,
|
|
694
|
-
`FUNCTIONS_EMULATOR`, `GCLOUD_PROJECT`, the `OMEGA_*_PORT` map, the test-mode
|
|
695
|
-
switches. They are the runtime's facts about itself, not a brand's credentials, and
|
|
696
|
-
the frameworks read them directly.
|
|
697
|
-
|
|
698
|
-
### One key, one entry — the environment supplies the value — #586
|
|
699
|
-
|
|
700
|
-
A key whose value must differ between a local run and a deployed one is **not a second
|
|
701
|
-
schema entry**. Public payment keys already split per machine through the config merge
|
|
702
|
-
chain's local layer; secrets now split the same way through the `.env` cascade's
|
|
703
|
-
environment overlay above — the brand puts the test credential in `.env.development`
|
|
704
|
-
under the SAME name. The backend uses whatever key the env chain resolves, nothing more:
|
|
705
|
-
the rule, and the `.env.development` advice that follows from it, live in
|
|
706
|
-
[docs/backend/index.md](../backend/index.md) (the payment-keys paragraph).
|
|
707
|
-
|
|
708
|
-
The schema declares WHAT a brand supplies, never which environment supplies it: `env.js`
|
|
709
|
-
owns that. So `STRIPE_SECRET_KEY`, `PAYPAL_CLIENT_SECRET`, `CHARGEBEE_API_KEY` and
|
|
710
|
-
`COINBASE_COMMERCE_API_KEY` (asked for through the setup contract, #608, only when
|
|
711
|
-
`payment.providers.coinbase.enabled` is on) are one entry each, every provider library
|
|
712
|
-
reads the one name, and there is no `<KEY>_DEV` twin, no live-shape guard, and no
|
|
713
|
-
payment-specific rule anywhere (ruled 2026-08-26; the twin system was replaced before it shipped, so
|
|
714
|
-
nothing migrates).
|
|
715
|
-
|
|
716
|
-
### One key per AI provider — #639
|
|
717
|
-
|
|
718
|
-
`OPENAI_API_KEY` and `ANTHROPIC_API_KEY`. There is no second name for either.
|
|
719
|
-
|
|
720
|
-
| Key | Owner | Read by | Asked at |
|
|
721
|
-
|---|---|---|---|
|
|
722
|
-
| `OPENAI_API_KEY` | `backend` | Contact inference, content + newsletter generation, the `chatgpt` translation provider | `manage` — the `ai` service's setup gate |
|
|
723
|
-
| `ANTHROPIC_API_KEY` | `backend` | The backend's Anthropic provider (SVG generation, tool loops) | `manage` — the `ai` service's setup gate |
|
|
724
|
-
|
|
725
|
-
- **The company-wide fallback is the COMPANY LAYER, never a second key** (Ian
|
|
726
|
-
2026-08-27). The legacy pair (`BACKEND_MANAGER_OPENAI_API_KEY`, then
|
|
727
|
-
`OMEGA_OPENAI_API_KEY`) existed so one company key could serve every brand; the
|
|
728
|
-
`.env` cascade already does that — put the value in the company `.env` and every
|
|
729
|
-
brand under it resolves it, with a brand `.env` overriding. The prefixed names are
|
|
730
|
-
gone from the schema and every reader; migration row in
|
|
731
|
-
[breaking-changes.md](breaking-changes.md).
|
|
732
|
-
- **Both are optional and neither gates a run.** The `ai` service declares them
|
|
733
|
-
`gates: false`, so preflight never blocks on them and a brand that calls one
|
|
734
|
-
provider is never nagged about the other. `ai.enabled: false` (what the gate's
|
|
735
|
-
Disable lands) stops the ask for good.
|
|
736
|
-
|
|
737
|
-
Who derives from it:
|
|
738
|
-
|
|
739
|
-
| Lane | What it takes |
|
|
740
|
-
|---|---|
|
|
741
|
-
| `@omega.js/manager` workspace `env-keys` + the onboard `.env` stub | `generatedEnvKeys()` — name → the function that mints a value |
|
|
742
|
-
| `@omega.js/manager` `lib/env-order.js` (canonical .env order) | `envFileGroups()` + `envKeysByGroup()` — the sections, their comments, their keys |
|
|
743
|
-
| `@omega.js/config` `composeTargetEnv()` (the delivery composition every verb runs) | `ENV_SCHEMA` + `envFileGroups()`: a DECLARED brand key rides down when its entry claims it (by `name` or by `match`), its `targets` include the target, and its group renders into a file; a key no entry knows at all is the consumer's own and rides down to every target ([#835](https://github.com/Omega-JS-Stack/omega/issues/835)); `deliverAs` is applied on arrival, and each layer's `.env.<environment>` overlay composes above its own base (#586) |
|
|
744
|
-
| `@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 |
|
|
745
|
-
| `@omega.js/backend` `libraries/env.js` (the one reader) | `envSchemaEntry()` for every read, `requiredEnvKeys('backend')` for the boot guard, and `getEnvironment()` re-exported under its own name ([docs/backend/index.md](../backend/index.md)) |
|
|
746
|
-
| `@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 |
|
|
747
|
-
| `@omega.js/config` `env-delivery.js` (the one delivery renderer) | `delivery` + `deliverAs` + `machineLocal` + `publicAtRest`, plus the target's COMPOSED production values for the keys the schema cannot name (`match` families and custom keys, #835/#876): 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, backend's composed `deploy.yml` renders both its secrets block and the KEY LIST its node `.env` writer reads out of the runner env ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)), and all secret publishers send exactly its set |
|
|
748
|
-
| `@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 |
|
|
749
|
-
|
|
750
|
-
## The environment (`src/environment.js`), #817
|
|
751
|
-
|
|
752
|
-
`src/environment.js` is the ONE environment module every OMEGA target answers
|
|
753
|
-
from ([#817](https://github.com/Omega-JS-Stack/omega/issues/817)). Four calls,
|
|
754
|
-
one implementation, one call form everywhere: `getEnvironment()` plus
|
|
755
|
-
`isDevelopment()` / `isProduction()` / `isTesting()`, called directly by every
|
|
756
|
-
framework's `omega` instance and build module. It requires NOTHING, so it is bundled into browser
|
|
757
|
-
artifacts (the desktop renderer, every extension bundle) and vendored into
|
|
758
|
-
`@omega.js/client`, which reaches the same four through its own methods.
|
|
759
|
-
|
|
760
|
-
**ONE INPUT.** On Node it is `process.env.OMEGA_ENVIRONMENT`. In a browser
|
|
761
|
-
context, which can read neither env nor files, it is `config.environment`, the
|
|
762
|
-
build fact every surface already bakes into `OMEGA_BUILD_JSON.config`
|
|
763
|
-
([#896](https://github.com/Omega-JS-Stack/omega/issues/896)), reached as
|
|
764
|
-
`this.config.environment` off the instance the call is made on. Nothing else is
|
|
765
|
-
consulted: no `app.isPackaged`, no `manifest.update_url`, no `NODE_ENV`, no
|
|
766
|
-
terminal sniffing.
|
|
767
|
-
|
|
768
|
-
**NO DEFAULT.** A context with no input throws and names the variable. A guessed
|
|
769
|
-
environment is how a dev build ships as production and how a production build talks
|
|
770
|
-
to an emulator, and both failures stay silent until a user finds them.
|
|
771
|
-
|
|
772
|
-
**`setEnvironment(value)` is the only writer**, so a fourth word can never reach
|
|
773
|
-
the variable. Who calls it, and with what:
|
|
774
|
-
|
|
775
|
-
| Lane | Names |
|
|
776
|
-
|---|---|
|
|
777
|
-
| `@omega.js/backend`'s boot (`initialize()`, right after the `.env` cascade loads) | `envEnvironment()`, the AMBIENT answer (below) |
|
|
778
|
-
| `@omega.js/desktop` / `@omega.js/extension` `src/build.js`, at load | `buildLaneEnvironment(isBuildMode())`: `production` under `OMEGA_BUILD_MODE` (which WINS over an inherited value, so a production build spawned from a test run still bakes production), else an already-named value, else `development`. That rule is ONE exported helper beside `setEnvironment`, not a copy of the expression per framework, and those two build lanes are its only callers |
|
|
779
|
-
| `@omega.js/desktop`'s main process, at `initialize()` | the `environment` its baked config carries, for a packaged app that has no parent lane |
|
|
780
|
-
| `@omega.js/web`'s verbs | `production` for `omega build`, `development` for `omega dev`, `testing` for `omega test` |
|
|
781
|
-
| the desktop test runners, the extension `test` verb | `testing` |
|
|
782
|
-
|
|
783
|
-
`envEnvironment()` (in `src/env.js`) is the PRODUCER of that input, never a
|
|
784
|
-
second reader of it: it is the sniff a lane runs ONCE to decide what to set, and
|
|
785
|
-
the default for the two overlay selections (`.env.<environment>` and
|
|
786
|
-
`config/omega.<environment>.json5`). An already-set `OMEGA_ENVIRONMENT` wins
|
|
787
|
-
inside it too, so the producer and the reader can never disagree in one process.
|
|
788
|
-
|
|
789
|
-
## Owner hooks (`config/hooks/`) — cp91
|
|
790
|
-
|
|
791
|
-
`src/hooks.js` — owner-supplied code the frameworks call at named hook points, so
|
|
792
|
-
company-specific logic lives in the OWNER'S tree, never in framework source. Layout is
|
|
793
|
-
**nested, mirroring the call site** (Ian's directive): the account service's password
|
|
794
|
-
step loads `config/hooks/account/password.js`; a future onboarding hook would live under
|
|
795
|
-
`config/hooks/onboard/…` — one file per hook point, path = the invoking structure.
|
|
796
|
-
|
|
797
|
-
- **Home = `config/`, versioned by default** (Ian 2026-07-11): hooks are AUTHORED code
|
|
798
|
-
and sit with the other owner-authored omega inputs (omega.json5, seo.json5, chatsy.md,
|
|
799
|
-
…) — never under machine-owned, gitignored `.omega/`, where a hook lost on a fresh
|
|
800
|
-
clone would silently change behavior (passwords falling back to the seed channel and
|
|
801
|
-
rotating). Secrets still belong in `.env` — a hook that needs one reads `process.env`;
|
|
802
|
-
to keep a hook out of git anyway, add your own `config/hooks/` ignore line.
|
|
803
|
-
- **Resolution order**: the brand root's own `config/hooks/<point>.js`, else the company
|
|
804
|
-
tree's `company/config/hooks/<point>.js` (via `company: { id }`, #677): a company-wide
|
|
805
|
-
hook covers every brand, a single brand can still override it. It is the ONE inheritance
|
|
806
|
-
rule applied to one more relative path, and costs the hook loader no code of its own.
|
|
807
|
-
- **Contract**: plain CJS, `module.exports = ({ … }) => …` (async fine). Each call site
|
|
808
|
-
documents its hook's signature/return. Absent hook → `loadHook` returns null and the
|
|
809
|
-
caller uses its default behavior; a hook that EXISTS but is broken (unloadable,
|
|
810
|
-
non-function export, bad return) throws — an owner who wrote a hook never gets silent
|
|
811
|
-
fallback.
|
|
812
|
-
- **First (and so far only) hook point**: `account/password` —
|
|
813
|
-
`({ email, domain, apex, brand }) => password` (string ≥ 6 chars), letting a company
|
|
814
|
-
formula generate per-brand passwords without ever living in a repo the framework ships.
|
|
815
|
-
|
|
816
|
-
## Port auto-allocation (N7)
|
|
817
|
-
|
|
818
|
-
`src/ports.js` — classic defaults, probe at boot, per-port +1 bump only when taken, so
|
|
819
|
-
multiple brands run dev stacks concurrently. Single-brand dev with free defaults is
|
|
820
|
-
byte-identical to the pre-N7 behavior (no bumping, no artifacts).
|
|
821
|
-
|
|
822
|
-
- **`CLASSIC_PORTS`** — the historical defaults (functions 5001, hosting 5002, firestore
|
|
823
|
-
8080, auth 9099, database 9000, storage 9199, pubsub 8085, ui 4050, website 4000,
|
|
824
|
-
livereload 35729, cdp 9222). **This map is the ONLY source of those numbers**
|
|
825
|
-
([#834](https://github.com/Omega-JS-Stack/omega/issues/834)): @omega.js/client
|
|
826
|
-
carried four of them plus a classic dev origin, @omega.js/desktop's url-helpers
|
|
827
|
-
and the desktop auth lib three more, and @omega.js/extension's url-helpers and
|
|
828
|
-
background worker two, each as a last-resort fallback "for a build made with no
|
|
829
|
-
stack up". Every one of those is gone. The numbers reach a browser exactly one
|
|
830
|
-
way: each surface bakes the map into its build json with the classics as the
|
|
831
|
-
FLOOR and the live stack's resolved (possibly bumped) numbers over them, so a
|
|
832
|
-
dev artifact always carries a COMPLETE map and the classic dev ORIGIN rides it
|
|
833
|
-
the same way. A read that finds no map throws `devFactMissing()`
|
|
834
|
-
(`src/dev-facts.js`, the one home of that error), naming the fact it wanted and
|
|
835
|
-
the build step that writes it: a missing map is a broken build, and nothing
|
|
836
|
-
anywhere assumes a port again. Nothing identity-checks what answers on a
|
|
837
|
-
classic port, which is why an assumed one failed as an auth mystery or a silent
|
|
838
|
-
connection refusal rather than as a port problem.
|
|
839
|
-
- **`resolvePorts({ wanted, pins, claimed })`** — each wanted port keeps its value when
|
|
840
|
-
free, bumps +1 until free when taken (shared `claimed` set prevents two names landing
|
|
841
|
-
on one port). `pins` (the config `ports` section) never bump — a busy pin throws.
|
|
842
|
-
The free-check is a QUADRUPLE bind-probe (127.0.0.1, ::1 when the host has IPv6,
|
|
843
|
-
the IPv4 wildcard 0.0.0.0, and the `::` wildcard) — on macOS/BSD, wildcard and
|
|
844
|
-
specific-address listeners COEXIST on one port, and the two wildcard FAMILIES
|
|
845
|
-
coexist with each other, so any single-surface probe false-positives against a
|
|
846
|
-
sibling brand's stack and the real bind crashes later (found live at cp186: the
|
|
847
|
-
playground's https proxy holds IPv6 `*:5002`; a 127.0.0.1-only probe handed 5002
|
|
848
|
-
to the second brand's functions emulator — and again at #345: a foreign `0.0.0.0`
|
|
849
|
-
squatter read free to the `::` probe and the auth emulator died with no bump).
|
|
850
|
-
- **Ports file** — `writePortsFile(projectDir, ports, facts)` /
|
|
851
|
-
`readPortsFile/clearPortsFile(projectDir)`: `<projectDir>/.temp/ports.json`
|
|
852
|
-
(pid-stamped; readers ignore dead-pid leftovers). The allocator (the backend emulator
|
|
853
|
-
boot) writes it; siblings of the same brand (`omega test` against a running emulator,
|
|
854
|
-
the e2e harness) read it; cleared on clean shutdown. `facts` publishes the resolved
|
|
855
|
-
NON-port facts beside the map — today `origin`, the website's dev origin
|
|
856
|
-
([#262](https://github.com/Omega-JS-Stack/omega/issues/262)).
|
|
857
|
-
A consumer process targets `https`, the PUBLIC origin under the local certificate,
|
|
858
|
-
and never `hosting`, the internal plain-http port the mkcert proxy forwards to; every
|
|
859
|
-
`omega dev` leg trusts that certificate through `NODE_EXTRA_CA_CERTS` ([#795](https://github.com/Omega-JS-Stack/omega/issues/795)).
|
|
860
|
-
- **Sibling map** — `readSiblingPorts(targetDir)` merges every OTHER target's live ports file
|
|
861
|
-
in the same brand (a running backend's emulator map) for the target that asks;
|
|
862
|
-
`readSiblingOrigin(targetDir)` reads the published dev website origin the same way. Read
|
|
863
|
-
at USE time, never cached: the file appears when the backend boots and changes when it
|
|
864
|
-
restarts.
|
|
865
|
-
- **Env channel** — `portsToEnv(ports)` → `OMEGA_<NAME>_PORT` vars injected into spawned
|
|
866
|
-
children; `envPort(name)` reads one, `envPorts(env)` reads the whole map back out.
|
|
867
|
-
URL getters resolve env → classic default.
|
|
868
|
-
- **Browser channel (cp89, [#300](https://github.com/Omega-JS-Stack/omega/issues/300))** —
|
|
869
|
-
browser code can read neither env nor files, so a surface BAKES the map into its
|
|
870
|
-
client config, under the same key on all three (`OMEGA_BUILD_JSON.config.dev`, #894):
|
|
871
|
-
`omega dev` writes that line into the page chrome
|
|
872
|
-
PER RENDER (its resolved website port with the sibling backend's map merged over it),
|
|
873
|
-
and its auth-emulator proxy resolves the target port per REQUEST. The render-time
|
|
874
|
-
bake is ADVISORY ([#346](https://github.com/Omega-JS-Stack/omega/issues/346)): the
|
|
875
|
-
dev server REWRITES that chrome in every HTML response as it serves it, resolving the
|
|
876
|
-
sibling maps per request, because the normal boot order builds the whole page fleet in
|
|
877
|
-
under a second while the emulator suite seeds for minutes — nothing under `src/`
|
|
878
|
-
changes when it lands, so no page would ever re-render onto it. Mid-session emulator
|
|
879
|
-
restarts onto bumped numbers ride the same lane. Built `dist/` output is untouched on
|
|
880
|
-
disk. Desktop (its renderer bundle) and extension (every bundle) bake the same map at
|
|
881
|
-
the same key at build time, from the sibling file plus the env channel; production
|
|
882
|
-
builds bake none. Drivers serving a
|
|
883
|
-
STATIC build set `window.__OMEGA_DEV_PORTS__` (the devkit e2e harness — the site
|
|
884
|
-
builds before the emulator boots), which is a FALLBACK: it fills only what a page's
|
|
885
|
-
chrome omits, so a side channel no real browser has can never hide a broken real one.
|
|
886
|
-
The dev WEBSITE ORIGIN rides the same map as one more resolved fact
|
|
887
|
-
([#262](https://github.com/Omega-JS-Stack/omega/issues/262)): `omega dev` publishes
|
|
888
|
-
`dev.origin` (protocol AND port — the mkcert proxy fronts the public port by default,
|
|
889
|
-
so a port number alone cannot say the scheme) into the chrome and into its ports file,
|
|
890
|
-
and desktop/extension bake it from that file on their existing lanes, with
|
|
891
|
-
`CLASSIC_DEV_ORIGIN` as the floor under it (#834) so the key is always present.
|
|
892
|
-
The extension manifest's `externally_connectable` dev entry resolves from it at
|
|
893
|
-
package time; nothing hardcodes a dev origin any more.
|
|
894
|
-
`@omega.js/client`'s `getDevWebsiteOrigin()` is the one getter that answers it,
|
|
895
|
-
and it THROWS when the artifact carries none rather than assuming
|
|
896
|
-
`https://localhost:4000`, because a wrong dev origin fails as a silent
|
|
897
|
-
connection refusal.
|
|
898
|
-
`@omega.js/client` resolves chrome `dev.ports` → runtime global, and THROWS when
|
|
899
|
-
neither answers ([#834](https://github.com/Omega-JS-Stack/omega/issues/834)):
|
|
900
|
-
there is no classic-defaults step under them any more, and no
|
|
901
|
-
"assuming the classic …" warning, because nothing is assumed. Its dev
|
|
902
|
-
`getApiUrl` speaks plain http to a mapped `hosting` (the emulator serves http)
|
|
903
|
-
and https to a mapped `https` (`mgr serve`'s mkcert proxy); neither mapped is
|
|
904
|
-
the same loud error. Dev mode
|
|
905
|
-
resolves the LOCAL stack for every source, including `source: 'company'` —
|
|
906
|
-
`company.url` is a production concept, and dev deliberately makes no live server
|
|
907
|
-
hits (ratified, Ian 2026-08-03, [#34](https://github.com/Omega-JS-Stack/omega/issues/34)).
|
|
908
|
-
- **Website port (cp89)** — `omega dev` allocates through the same model: classic
|
|
909
|
-
**4000** (pre-N7 it defaulted to 8080, colliding with the SAME brand's firestore
|
|
910
|
-
emulator), bump when taken, `--port` flag or config `ports.website` pins; publishes
|
|
911
|
-
its own ports file in the website target dir.
|
|
912
|
-
- **Serve + per-target ports (cp90)** — `mgr serve` allocates through the same model
|
|
913
|
-
(`--port` pins, taken bumps — the old kill-the-incumbent check is gone) and PUBLISHES
|
|
914
|
-
its map: `https` (the mkcert proxy) + `hosting` (the internal plain-http
|
|
915
|
-
firebase-serve port), so `omega dev` bakes even a bumped serve into the chrome and
|
|
916
|
-
Stripe webhook forwarding targets the port that actually speaks http (it used to aim
|
|
917
|
-
plain http at the TLS proxy). Desktop + extension `serve` allocate `livereload` — two
|
|
918
|
-
targets of one brand land on distinct ports — and desktop allocates `cdp` when
|
|
919
|
-
requested (`OMEGA_CDP_PORT` set); desktop URL getters mirror the backend's
|
|
920
|
-
env-channel reads (`https` → mkcert, `hosting` → plain http, classic otherwise).
|
|
921
|
-
The manager's Google-OAuth loopback binds an EPHEMERAL port (`listen(0)`, RFC 8252)
|
|
922
|
-
instead of pinning 9876. The backend's `getWebsiteUrl` returns
|
|
923
|
-
`http://localhost:4000` unless its own mkcert proxy is up (plain http on the public
|
|
924
|
-
port 307s to https, so the link lands either way); desktop's reads the whole dev
|
|
925
|
-
ORIGIN instead (baked `dev.origin`, then `OMEGA_WEBSITE_PORT` composed over https,
|
|
926
|
-
then the classic `https://localhost:4000`), matching the browser-side answer scheme
|
|
927
|
-
and all ([#747](https://github.com/Omega-JS-Stack/omega/issues/747)). The BROWSER-side answer
|
|
928
|
-
is `getDevWebsiteOrigin()`, which needs the exact origin and takes it from the
|
|
929
|
-
resolved map (#262). Packaged
|
|
930
|
-
extension/desktop artifacts keep BUILD-TIME-BAKED ports by design (a shipped
|
|
931
|
-
extension can't probe); the extension manifest's dev-website origin documents that
|
|
932
|
-
inline.
|
|
933
|
-
- **Config `ports` section** (schema, optional object) — explicit pins for any port name;
|
|
934
|
-
unset = auto-allocate.
|
|
935
|
-
- When a boot bumps emulator ports, the backend CLI materializes
|
|
936
|
-
`firebase.resolved.json` next to firebase.json (same dir, so relative paths keep
|
|
937
|
-
resolving) and boots firebase-tools with `--config`; the committed firebase.json never
|
|
938
|
-
changes. Gitignored; removed on shutdown.
|
|
939
|
-
|
|
940
|
-
Design + slice plan: [_attic/plans/archive/n7-port-allocation.md](../../_attic/plans/archive/n7-port-allocation.md).
|
|
941
|
-
|
|
942
|
-
## Validation
|
|
943
|
-
|
|
944
|
-
`validateConfig(config, { target })` = shared schema + that target's refinements
|
|
945
|
-
(`TARGET_SCHEMAS[target]`), run against the RESOLVED config. `brand.id` (URL-scheme-safe
|
|
946
|
-
slug) and `brand.name` are the only universally required fields.
|
|
947
|
-
|
|
948
|
-
**Undeclared keys WARN** ([#636](https://github.com/Omega-JS-Stack/omega/issues/636)):
|
|
949
|
-
every leaf path of the resolved config no rule declares comes back as ONE warning naming
|
|
950
|
-
them — never an error, because a brand config that outlives a framework version must still
|
|
951
|
-
build. A rule of type `object`/`array` declares its whole subtree (a brand's postal
|
|
952
|
-
address, an open provider map), and the `targets` namespace is exempt: those keys belong to
|
|
953
|
-
a framework, or to a custom target. A finding is a hole to fill — either the key is dead,
|
|
954
|
-
or the schema owes it a rule.
|
|
955
|
-
|
|
956
|
-
**authDomain is the brand's own host** (cp268): when `cloud.config.authDomain` is set it
|
|
957
|
-
must equal the BRAND host, `brand.url`, for every target a brand runs
|
|
958
|
-
([#588](https://github.com/Omega-JS-Stack/omega/issues/588)): one Firebase project, one
|
|
959
|
-
backend, one authDomain. A target's own `url` is not read here (a
|
|
960
|
-
`targets/admin` load would otherwise fail its own brand's authDomain), and the
|
|
961
|
-
top-level `url` is only the fallback for a config carrying no `brand.url` at all. A
|
|
962
|
-
`*.firebaseapp.com` value hard-fails (self-hosted `/__/auth/*` on the brand
|
|
963
|
-
host is what keeps redirect sign-in working under browser storage partitioning; the web
|
|
964
|
-
build emits those helper files), and any other mismatch fails naming both values. Absent
|
|
965
|
-
passes, and `demo-*` (emulator-only) projects are exempt.
|
|
966
|
-
|
|
967
|
-
**A product price is a bare NUMBER** ([#674](https://github.com/Omega-JS-Stack/omega/issues/674)):
|
|
968
|
-
every entry of `payment.products[].prices` must be a number — `once: 49.99`, never
|
|
969
|
-
`once: { amount: 49.99 }` — and the object shape is a config ERROR naming the product and
|
|
970
|
-
the key. The two sides did not read it the same: the checkout page's resolver unwrapped
|
|
971
|
-
`{ amount: N }` while the backend's confirmation URL and all three provider libraries took
|
|
972
|
-
the bare number, so an object-shaped price rendered a correct order summary and reached
|
|
973
|
-
the confirmation URL as `[object Object]`. There is no shared resolver to settle it in —
|
|
974
|
-
the browser bundle cannot reach a build-time package, and the deployed backend runtime
|
|
975
|
-
carries none either — so the SHAPE is settled here, in the one place both sides' catalog
|
|
976
|
-
comes from. (`prices.amount` as a KEY is a different thing, a legacy one-time spelling the
|
|
977
|
-
checkout still reads; its value is a number like every other.)
|
|
978
|
-
|
|
979
|
-
## The features catalog (`features`) and a product's values — #647
|
|
980
|
-
|
|
981
|
-
A feature is **defined once**, at the top level, and a product names only its **value**.
|
|
982
|
-
The two halves cannot disagree, because there is only one place a name, an icon or a
|
|
983
|
-
definition can be written:
|
|
984
|
-
|
|
985
|
-
```json5
|
|
986
|
-
{
|
|
987
|
-
features: {
|
|
988
|
-
saves: {
|
|
989
|
-
name: 'Saves',
|
|
990
|
-
icon: 'fa-solid fa-feather',
|
|
991
|
-
definition: 'Notes, clips, and pages you can save per month.',
|
|
992
|
-
usage: { pace: 'daily', mirror: ['teams'] },
|
|
993
|
-
},
|
|
994
|
-
templates: { name: 'Page templates', icon: 'fa-solid fa-palette', usage: { pace: false } },
|
|
995
|
-
support: { name: 'Priority support', icon: 'fa-solid fa-headset', definition: 'Your tickets jump the queue.' },
|
|
996
|
-
},
|
|
997
|
-
|
|
998
|
-
payment: {
|
|
999
|
-
products: [
|
|
1000
|
-
{ id: 'basic', name: 'Basic', features: { saves: 100 } },
|
|
1001
|
-
{ id: 'premium', name: 'Premium', features: { saves: 10000, templates: 40, support: true } },
|
|
1002
|
-
{ id: 'pro', name: 'Pro', features: { saves: -1, templates: 120, support: true } },
|
|
1003
|
-
],
|
|
1004
|
-
},
|
|
1005
|
-
}
|
|
1006
|
-
```
|
|
1007
|
-
|
|
1008
|
-
### The catalog
|
|
1009
|
-
|
|
1010
|
-
| Key | Type | What it does |
|
|
1011
|
-
|-----|------|--------------|
|
|
1012
|
-
| `features.<id>.name` | string, **required** | The label every surface prints: pricing rows, the comparison matrix, the account's usage bars |
|
|
1013
|
-
| `features.<id>.icon` | string | Font Awesome icon name ([icons.md](icons.md)) |
|
|
1014
|
-
| `features.<id>.definition` | string | The dotted-underline tooltip. Authored ONCE — every card, row and bar renders this one |
|
|
1015
|
-
| `features.<id>.usage` | object | Its presence makes the feature **counted** (metered per user). Absent = a **perk**, never counted |
|
|
1016
|
-
| `features.<id>.usage.pace` | `'daily'` \| `false` | Day pacing is the DEFAULT. `false` opts out to a plain monthly counter |
|
|
1017
|
-
| `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 |
|
|
1018
|
-
|
|
1019
|
-
**Key order is row order.** The pricing page's rows, the comparison matrix and the
|
|
1020
|
-
account's usage bars all render the catalog in the order it is written.
|
|
1021
|
-
|
|
1022
|
-
### A product's values
|
|
1023
|
-
|
|
1024
|
-
`payment.products[].features` is a MAP of `<catalog id>: value`:
|
|
1025
|
-
|
|
1026
|
-
| The feature is | Its value is | Renders as |
|
|
1027
|
-
|---|---|---|
|
|
1028
|
-
| Counted | a number — the **monthly limit** | `100 Saves` |
|
|
1029
|
-
| Counted | `-1` — unlimited | `Unlimited Saves` |
|
|
1030
|
-
| Perk | `true` | a check, name only |
|
|
1031
|
-
| Perk | a string | `24/7 Support` |
|
|
1032
|
-
| Either | `false` (or absent) | nothing — the tier does not include it |
|
|
1033
|
-
|
|
1034
|
-
The validator fails **a number on a perk** (it would draw a usage bar against a limit no
|
|
1035
|
-
gate enforces) and **a perk value on a counted feature** (the gate would read it as zero,
|
|
1036
|
-
so the plan advertises the feature and every call refuses it). It also fails a value on an
|
|
1037
|
-
id the catalog does not define, because nothing reads it — the same silence a retired key
|
|
1038
|
-
used to buy.
|
|
1039
|
-
|
|
1040
|
-
### What this replaces
|
|
1041
|
-
|
|
1042
|
-
`payment.products[].limits`, the per-product `features` ARRAY, and the product-wide
|
|
1043
|
-
`rateLimit` are **retired** — all three are validation errors naming their replacement.
|
|
1044
|
-
The cross-product definition BACKFILL retires with them: nothing repeats, so nothing needs
|
|
1045
|
-
unifying. The mapping is in [breaking-changes.md](breaking-changes.md).
|
|
1046
|
-
|
|
1047
|
-
### Top-level `usage` is reserved
|
|
1048
|
-
|
|
1049
|
-
`usage` at the top level is reserved for **counting settings** (an anonymous-store mode, a
|
|
1050
|
-
reset hour) and carries **no key today**. It is not a second home for the catalog. The
|
|
1051
|
-
backend's gate reads `features`; how a user's counters behave is
|
|
1052
|
-
[packages/backend/docs/usage-rate-limiting.md](../../packages/backend/docs/usage-rate-limiting.md).
|
|
1053
|
-
|
|
1054
|
-
## User connections (`connections`) — #771, #788, #792, #793
|
|
1055
|
-
|
|
1056
|
-
Every key under `connections` is a PROVIDER a brand's users may link from the account page
|
|
1057
|
-
— `google`, `discord`, `spotify`, `twitch`, `kick` ship with `@omega.js/backend`, and
|
|
1058
|
-
any other one is a file the brand writes at `targets/backend/src/connections/<name>.js`
|
|
1059
|
-
(the lane loads that directory before its own). So the section is free-form by design;
|
|
1060
|
-
nothing here is required, and these are the keys an entry may carry:
|
|
1061
|
-
|
|
1062
|
-
| Key | What it does |
|
|
1063
|
-
|---|---|
|
|
1064
|
-
| `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` |
|
|
1065
|
-
| `scope` | An array that WINS over the provider module's default scope |
|
|
1066
|
-
| `name` | The card's title on the account page |
|
|
1067
|
-
| `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 |
|
|
1068
|
-
| `description` | The line under the title |
|
|
1069
|
-
|
|
1070
|
-
Every key is a PROVIDER NAME, so it is strictly `[a-z0-9-]` — the same rule the
|
|
1071
|
-
backend's confined loader enforces. A key outside it can never resolve to a
|
|
1072
|
-
provider, and the account page says so on the card instead of offering a
|
|
1073
|
-
connection that could only fail.
|
|
1074
|
-
|
|
1075
|
-
**This section is the ONLY card list** ([#792](https://github.com/Omega-JS-Stack/omega/issues/792)):
|
|
1076
|
-
every entry carrying a `name` and a `logo` renders a card, and the account layout's old
|
|
1077
|
-
`connections:` frontmatter rows — which shadowed a brand's own entry and pointed at CDN
|
|
1078
|
-
files that 404 — are gone. The framework's defaults carry `name`, `logo` and `description`
|
|
1079
|
-
for the five packaged providers, so `google: { enabled: true }` is a complete card and any
|
|
1080
|
-
key a brand writes wins through the ordinary merge chain. Those defaults RESOLVE only
|
|
1081
|
-
(`materialize: false`, above): they are never copied into a brand's file. An enabled
|
|
1082
|
-
provider with no `name` + `logo` after the merge gets the "unsupported connection" card,
|
|
1083
|
-
which says exactly that.
|
|
1084
|
-
|
|
1085
|
-
Secrets never live here: the credentials are the `CONNECTIONS_<PROVIDER>_CLIENT_ID` /
|
|
1086
|
-
`CONNECTIONS_<PROVIDER>_CLIENT_SECRET` pair in the `.env` (the provider name uppercased,
|
|
1087
|
-
dashes as underscores). The full provider contract — the module shape, the ONE context
|
|
1088
|
-
every step takes, `pkce: 'S256'`, the route-owned identity uniqueness, and the `type` every
|
|
1089
|
-
stored record carries — is `packages/backend/docs/connections.md`.
|
|
1090
|
-
|
|
1091
|
-
## The cancel-flow save offer (`payment.winback`) — #268
|
|
1092
|
-
|
|
1093
|
-
When a customer starts cancelling a PAID subscription, the billing card pitches a
|
|
1094
|
-
discount on the next cycle before it asks them why they are leaving. Accepting applies
|
|
1095
|
-
the discount through the provider's own coupon plumbing, calls the cancel off, and
|
|
1096
|
-
leaves the saving on the card (what comes off, and which bills it comes off — #325);
|
|
1097
|
-
declining opens the cancellation questionnaire unchanged.
|
|
1098
|
-
|
|
1099
|
-
The pitch is made per cancel ATTEMPT, never once per session (#324): a customer who
|
|
1100
|
-
declines, closes the questionnaire and comes back to cancel meets the offer again,
|
|
1101
|
-
because nothing about their subscription changed. Only a CLAIM (the discount is applied,
|
|
1102
|
-
and the backend refuses a second one) or a refusal no retry fixes ends it.
|
|
1103
|
-
|
|
1104
|
-
An accepted offer is recorded in TWO places, on purpose (#325). `payments-orders/{orderId}`
|
|
1105
|
-
`.requests.winback` is the offer's MEMORY — what a second accept is refused against — and
|
|
1106
|
-
the account carries the discount ITSELF at `subscription.discount`, shaped like every other
|
|
1107
|
-
discount in the payment stack (`{ valid, code, percent | amount, duration }`) plus a
|
|
1108
|
-
`source`. That is what the billing card renders on a later visit; without it the saving
|
|
1109
|
-
disappeared on the next page load. `source` is the whole reason it is safe to read as a
|
|
1110
|
-
claim: today only the winback claim writes this node, and `source` is what keeps the
|
|
1111
|
-
read safe when checkout discounts start writing it too, because only
|
|
1112
|
-
`source: 'winback'` says this customer already took the save offer.
|
|
1113
|
-
|
|
1114
|
-
**The claim ends with the subscription it was made on (#333).** The account's node also
|
|
1115
|
-
carries `resourceId`, the subscription the discount was applied to, stamped at claim
|
|
1116
|
-
time. The unified webhook write carries no discount key, so a merge would otherwise keep
|
|
1117
|
-
the node forever: a customer who churned and resubscribed carried a spent claim into the
|
|
1118
|
-
NEW subscription, where `source: 'winback'` reads as "already claimed" and the save offer
|
|
1119
|
-
is silently never pitched again. Every subscription-resource webhook compares the stamp against
|
|
1120
|
-
the subscription the event is about and CLEARS the node on a mismatch (a new subscription
|
|
1121
|
-
is a clean slate), while same-subscription traffic (renewals, cancellations, plan
|
|
1122
|
-
changes) leaves the saving exactly as claimed. A node with no stamp predates it and
|
|
1123
|
-
nothing can prove it belongs to an older subscription, so it is read as riding the one it
|
|
1124
|
-
is found on and stamped there: no live discount is taken away on a guess, and it clears
|
|
1125
|
-
on the next resubscribe like any other. CONSUMPTION is not cleared: a spent `once` coupon
|
|
1126
|
-
still reads as applied until the subscription changes, because no provider's unified
|
|
1127
|
-
shape reports whether the coupon is still attached (#333).
|
|
1128
|
-
|
|
1129
|
-
The offer is the brand's, and **a brand that writes nothing gets one anyway**: 50% off
|
|
1130
|
-
the next cycle, that cycle only.
|
|
1131
|
-
|
|
1132
|
-
| Key | Type | Default | What it does |
|
|
1133
|
-
|-----|------|---------|--------------|
|
|
1134
|
-
| `payment.winback.enabled` | boolean | `true` | The whole off switch. `false` skips the pitch, and the cancel meets the data-retention warning instead (#341) |
|
|
1135
|
-
| `payment.winback.percent` | integer 1-100 | `50` | Whole percentage off. Mutually exclusive with `amount` |
|
|
1136
|
-
| `payment.winback.amount` | number > 0 | — | Flat amount off in `payment.currency`'s major unit (`10` = $10). Mutually exclusive with `percent` |
|
|
1137
|
-
| `payment.winback.duration` | `'once'` \| `'forever'` | `'once'` | `once` discounts the next cycle only; `forever` is a permanent price cut |
|
|
1138
|
-
|
|
1139
|
-
Setting **both** `percent` and `amount` is a validation error: a coupon is one shape or
|
|
1140
|
-
the other everywhere in the payment stack, and two shapes on one offer has no honest
|
|
1141
|
-
reading.
|
|
1142
|
-
|
|
1143
|
-
`resolveWinbackOffer(payment)` is the ONE home of these defaults. The backend's
|
|
1144
|
-
`POST /payments/winback` route resolves the brand's section through it, and the web
|
|
1145
|
-
build bakes the same call into the client blob (`site.client.payment.winback`), so the
|
|
1146
|
-
dialog the customer reads and the coupon the provider creates can never name different
|
|
1147
|
-
numbers — the browser never applies a default of its own.
|
|
1148
|
-
|
|
1149
|
-
Not every provider can discount a subscription that is already running. Stripe can
|
|
1150
|
-
(the coupon plumbing the checkout already uses); PayPal has no discount object at all
|
|
1151
|
-
and Chargebee has no way to reach a live subscription with one through existing
|
|
1152
|
-
plumbing. Those two refuse with `not-supported-by-provider`, and the billing card
|
|
1153
|
-
retires the offer for the session and opens the questionnaire — a subscriber can always
|
|
1154
|
-
still cancel.
|
|
1155
|
-
|
|
1156
|
-
## Consent (`client.consent`) — #383
|
|
1157
|
-
|
|
1158
|
-
The consent banner is a real GATE, so its config is schema-known even though the rest of
|
|
1159
|
-
the `client` blob is not: a typo that silently disabled it would ship a site with no
|
|
1160
|
-
consent surface and no error.
|
|
1161
|
-
|
|
1162
|
-
```json5
|
|
1163
|
-
targets: {
|
|
1164
|
-
web: {
|
|
1165
|
-
client: {
|
|
1166
|
-
consent: {
|
|
1167
|
-
enabled: true, // default true; false ships NO banner
|
|
1168
|
-
config: {
|
|
1169
|
-
position: 'bottom-left', // bottom-left | bottom-right | bottom
|
|
1170
|
-
content: {
|
|
1171
|
-
message: 'We use cookies … See our { terms }.', // the banner face
|
|
1172
|
-
panelIntro: 'We and our partners … See our { cookies } and { terms }.',
|
|
1173
|
-
accept: 'Accept', // the big grant
|
|
1174
|
-
customize: 'Customize', // opens the panel
|
|
1175
|
-
acceptAll: 'Accept all', // the panel's pair
|
|
1176
|
-
acceptNone: 'Accept none',
|
|
1177
|
-
},
|
|
1178
|
-
// `{terms}` and `{cookies}` link the terms and cookie-policy pages.
|
|
1179
|
-
// `save` retired with the Save button (#391) — a config still setting
|
|
1180
|
-
// it is ignored, not an error.
|
|
1181
|
-
},
|
|
1182
|
-
},
|
|
1183
|
-
},
|
|
1184
|
-
},
|
|
1185
|
-
}
|
|
1186
|
-
```
|
|
1187
|
-
|
|
1188
|
-
Two things are deliberately NOT config:
|
|
1189
|
-
|
|
1190
|
-
- **The regime.** The visitor's browser timezone picks it — the strict opt-in roster (plus an
|
|
1191
|
-
unplaceable timezone) gets opt-in, where no provider script loads until they accept; everywhere
|
|
1192
|
-
else gets opt-out, where the scripts load and a first visit sees only the Cookies Settings
|
|
1193
|
-
tab ([#391](https://github.com/Omega-JS-Stack/omega/issues/391)). There is no key that
|
|
1194
|
-
forces one, because the answer is legal, not stylistic.
|
|
1195
|
-
- **The colors.** The panel paints itself from the `--omega-*` token sheet, which is the
|
|
1196
|
-
only way it is correct in both color modes. The old `palette`/`theme` keys are gone.
|
|
1197
|
-
|
|
1198
|
-
`enabled: false` is legal only for a site that loads no analytics or marketing provider
|
|
1199
|
-
at all — the gate and the banner are the same switch.
|
|
1200
|
-
|
|
1201
|
-
## Feature gating polarity (#527)
|
|
1202
|
-
|
|
1203
|
-
Ratified 2026-08-24 (Ian). A feature has ONE switch with ONE polarity, and which
|
|
1204
|
-
polarity it is follows from what the feature needs — never from taste at the read
|
|
1205
|
-
site. Three cases:
|
|
1206
|
-
|
|
1207
|
-
| Case | The switch | The read | Examples |
|
|
1208
|
-
|------|-----------|----------|----------|
|
|
1209
|
-
| **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` |
|
|
1210
|
-
| **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`, `search.providers.searchConsole.enabled`, `edge.providers.cloudflare.enabled`, `targets.web.meta.index` |
|
|
1211
|
-
| **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` |
|
|
1212
|
-
|
|
1213
|
-
- **A block by itself NEVER enables.** Authoring `providers: { adsense: {} }` is an opt-IN
|
|
1214
|
-
to being asked, not an ON — the case-1 data or the case-2/3 `enabled` still decides.
|
|
1215
|
-
- **Never a second switch on one feature.** Two switches let a config say ON to one half
|
|
1216
|
-
of the stack and OFF to the other: adsense carried exactly that (the manager gated on
|
|
1217
|
-
`enabled`, the site on `client` + `units`), so `{ client, enabled: false }` stopped the
|
|
1218
|
-
account being managed while the site kept serving ads off it. #527 collapsed adsense to
|
|
1219
|
-
case 1 and deleted both extra gates. A state that needs a second switch to express
|
|
1220
|
-
(managed account, ad-free site) is deliberately inexpressible.
|
|
1221
|
-
- **Every code-read switch has a schema rule** ([#546](https://github.com/Omega-JS-Stack/omega/issues/546)):
|
|
1222
|
-
an undeclared key validates clean, so a typo (`enbaled: false`) silently reads as ON and
|
|
1223
|
-
the validator cannot document what the key does. The rule carries the `default:` that
|
|
1224
|
-
states the polarity — except where the SECTION is presence-gated (`advertising`), because
|
|
1225
|
-
a materialized default would write the section into every brand and switch the feature on
|
|
1226
|
-
for brands that configured none; there the ON answer lives at the read site.
|
|
1227
|
-
|
|
1228
|
-
## Tri-state provisioning values (#33)
|
|
1229
|
-
|
|
1230
|
-
Provisioning-flow keys (org, billing account, service/agent ids — anything a manage
|
|
1231
|
-
flow can set up interactively) follow ONE contract, enforced by the manager's
|
|
1232
|
-
config-flow engine (`packages/manager/src/lib/config-flow.js`):
|
|
1233
|
-
|
|
1234
|
-
| Value | Meaning |
|
|
1235
|
-
|-------|---------|
|
|
1236
|
-
| missing / `null` | ASK in an interactive run — the answer lands in omega.json5; without a TTY: warn + skip, aggregated in the run summary |
|
|
1237
|
-
| `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 |
|
|
1238
|
-
| anything else | Use it |
|
|
1239
|
-
|
|
1240
|
-
Every ask offers the opt-out (the gate's "Disable" and, in selection flows, an inline
|
|
1241
|
-
"No …" choice), so `false` is always reachable; delete the line to be asked again.
|
|
1242
|
-
First consumers: `cloud.organizationId` (asked at project creation — pick an org or
|
|
1243
|
-
create standalone) and `cloud.billingAccount` (pick/create a billing account or stay
|
|
1244
|
-
on Spark). ONE cloud home (#23, reversing the old `gcp`-vs-`firebase` split): the
|
|
1245
|
-
platform-level org and billing account, the provisioning switches (`cloud.shared`,
|
|
1246
|
-
`cloud.supportEmail`, `cloud.apiSubdomain`) and the app config (`cloud.provider`,
|
|
1247
|
-
`cloud.config`) are all `cloud.*`, and the project id has exactly one address —
|
|
1248
|
-
`cloud.config.projectId`. `cloud.supportEmail`'s null auto-derives the authorizing
|
|
1249
|
-
user's email instead of asking.
|
|
1250
|
-
|
|
1251
|
-
## The site global: the curated targets view (#85, #610)
|
|
1252
|
-
|
|
1253
|
-
`toSiteGlobal()` (the web build's `site.*`) strips the raw `targets` machinery and
|
|
1254
|
-
replaces it with a CURATED `site.targets` — an allow-list of display-safe facts per
|
|
1255
|
-
declared target, never a spread of the raw config: every entry carries `enabled: true`,
|
|
1256
|
-
desktop adds a derived `releasesUrl` plus the per-artifact `downloads` map once releases
|
|
1257
|
-
are opted in, extension adds its store `listings`.
|
|
1258
|
-
|
|
1259
|
-
It is the ONE home of those facts
|
|
1260
|
-
([#610](https://github.com/Omega-JS-Stack/omega/issues/610)). The legacy UJM-shaped
|
|
1261
|
-
`targets.web.download` / `targets.web.extension` page maps — and the `site.download` /
|
|
1262
|
-
`site.extension` data they filled — are GONE: a brand still carrying either key fails
|
|
1263
|
-
validation naming the block it derives from, and `omega migrate` drops it with a note.
|
|
1264
|
-
|
|
1265
|
-
- **The desktop derivation is OPT-IN (#124)**: `releasesUrl` appears only when
|
|
1266
|
-
`targets.desktop.releases` is present (its
|
|
1267
|
-
`enabled` defaults true when the block exists); `releases.enabled: false` always
|
|
1268
|
-
suppresses, and a bare desktop target with no `releases` block derives nothing —
|
|
1269
|
-
declaring the target does not mean a release exists yet. `releases.enabled` is ONE
|
|
1270
|
-
switch for the whole release surface: the desktop build also reads it
|
|
1271
|
-
(electron-builder publish config), so `false` turns off desktop publishing too — and
|
|
1272
|
-
there it defaults true even with no `releases` block.
|
|
1273
|
-
- **Desktop releases URL**: `https://github.com/<owner>/<name>/releases/latest`, where
|
|
1274
|
-
owner and name come from `releasesRepo(config)` in
|
|
1275
|
-
[repo.js](../../packages/config/src/repo.js): the brand's ONE public releases repo
|
|
1276
|
-
([#799](https://github.com/Omega-JS-Stack/omega/issues/799),
|
|
1277
|
-
[#883](https://github.com/Omega-JS-Stack/omega/issues/883)). It is always
|
|
1278
|
-
`<brand.id>-releases` under `repo.org`, with no override key left anywhere;
|
|
1279
|
-
`targets.desktop.releases` presence is only the opt-in. Nothing addressable, no URL
|
|
1280
|
-
(no org, no brand id). That helper is the ONE
|
|
1281
|
-
home of the address: @omega.js/desktop's electron-builder publish block, its releases-repo
|
|
1282
|
-
provisioning and its `finalize-release` uploads read the same call, so the feed a shipped
|
|
1283
|
-
app polls and the link a download button carries cannot disagree.
|
|
1284
|
-
- **Desktop direct downloads ([#620](https://github.com/Omega-JS-Stack/omega/issues/620),
|
|
1285
|
-
[#867](https://github.com/Omega-JS-Stack/omega/issues/867))**:
|
|
1286
|
-
`downloads.<platform>.<format>` = `<releasesUrl>/download/<asset>`, one per format the
|
|
1287
|
-
brand SHIPS (`mac.dmg`, `windows.nsis`, `linux.deb`, `linux.appimage`, in offer order),
|
|
1288
|
-
plus `linux.snap`, which is the Snap Store listing because the store holds that file.
|
|
1289
|
-
The format keys and the asset names are `platforms.js`'s: the SAME table
|
|
1290
|
-
@omega.js/desktop's `build-config` writes into `electron-builder.yml`, so a button
|
|
1291
|
-
hands over the file and never lands on a GitHub page. They carry no version, which is
|
|
1292
|
-
what keeps `/releases/latest/download/<asset>` pointing at the newest build forever:
|
|
1293
|
-
releasing a desktop version never touches the website. Derived from
|
|
1294
|
-
`targets.desktop.app.productName` → `brand.name`; no product name, no `downloads` (a
|
|
1295
|
-
guessed filename is a dead button). A platform or format the brand dropped is absent
|
|
1296
|
-
here too, so the site never renders a button for something nobody builds.
|
|
1297
|
-
- **Extension listings**: `targets.extension.listings.<store>.{url,state}` for the six
|
|
1298
|
-
stores the theme renders (chrome, firefox, edge, opera, safari, brave) —
|
|
1299
|
-
schema-declared; url must be http(s). Entries with neither url nor state stay absent.
|
|
1300
|
-
The `id` beside them (#893) is the publish lane's, not the page's: it never
|
|
1301
|
-
reaches the curated view, because a listing with no url has nothing to link.
|
|
1302
|
-
- **Idempotent by contract**: the web build applies `toSiteGlobal` twice (loadSiteData,
|
|
1303
|
-
then configureOmega) — a curated `releasesUrl` and its `downloads` survive the second
|
|
1304
|
-
pass unchanged.
|
|
1305
|
-
- **The curated view is keyed by target NAME, and every fact derives from the entry's TYPE**
|
|
1306
|
-
([#886](https://github.com/Omega-JS-Stack/omega/issues/886)): a brand that names its desktop
|
|
1307
|
-
target `app` gets `site.targets.app.releasesUrl` and `site.targets.app.downloads`. The name is
|
|
1308
|
-
never the test. A target whose type derives nothing is a presence-only entry.
|
|
1309
|
-
|
|
1310
|
-
Three consumers read the curated view and nothing else: the `/download` page (every
|
|
1311
|
-
desktop button → `site.targets.desktop.downloads[platform][artifact]`), the `/extension` page
|
|
1312
|
-
(`site.targets.extension.listings[browser].url`), and the shortlink generator
|
|
1313
|
-
(`src/target-shortlinks.js`, #561), which publishes `/download/<platform>[/<artifact>]`
|
|
1314
|
-
and `/extension/<store>` off the same facts. Mobile derives nothing while MAM is
|
|
1315
|
-
parked, so the mobile band stays on its notify form.
|
|
1316
|
-
|
|
1317
|
-
## The browser subset: one `client` flag, one `OMEGA_BUILD_JSON`, one `build.js` (#894, #743)
|
|
1318
|
-
|
|
1319
|
-
Three surfaces hand a config to a browser: web's pages and its service worker, desktop's
|
|
1320
|
-
renderer windows, and every extension page plus its service worker. What may go in is ONE
|
|
1321
|
-
decision, declared once and made once, and it is DELIVERED one way:
|
|
1322
|
-
|
|
1323
|
-
- **The declaration** is `CLIENT_SECTIONS` in [src/schema.js](../../packages/config/src/schema.js):
|
|
1324
|
-
a per-SECTION `client` flag. `true` ships the whole section (its keys are public by
|
|
1325
|
-
design, and a secret-shaped key can never be in the file at all); an ARRAY of dot-paths
|
|
1326
|
-
ships only that section's public half, for a section whose other keys are
|
|
1327
|
-
provisioning-only. No row means the browser never sees it. Today: `advertising`,
|
|
1328
|
-
`analytics`, `app`, `brand`, `captcha`, `client`, `company`, `connections`, `features`,
|
|
1329
|
-
`listings`, `monitoring`, `payment`, `theme` ride whole; `cloud` rides
|
|
1330
|
-
`['config', 'messaging.vapidKey']` (the Firebase WEB config is public, the GCP account
|
|
1331
|
-
facts beside it are not); `forms` rides `['providers.slapform.formId']` and `inbound`
|
|
1332
|
-
the three chatsy leaves the widget reads. A NEW section defaults to invisible, which
|
|
1333
|
-
fails as a missing feature rather than as a leak.
|
|
1334
|
-
- **The function** is `clientConfig(resolved)`
|
|
1335
|
-
([src/client-config.js](../../packages/config/src/client-config.js)): the flagged
|
|
1336
|
-
sections, plus the build FACTS a surface composes onto the config before baking it
|
|
1337
|
-
(`runtime`, `environment`, `version`, `buildTime`, `target`, `url`, `dev`). `runtime` is
|
|
1338
|
-
baked on every surface, including desktop (#896): a packaged Electron renderer is a
|
|
1339
|
-
browser with no Electron globals of its own, so nothing can sniff it from inside.
|
|
1340
|
-
`payment` comes out with its winback offer RESOLVED, so the dialog a customer reads and
|
|
1341
|
-
the coupon the backend creates name the same numbers. A secret-shaped key inside a
|
|
1342
|
-
section about to ship THROWS: that config should never have loaded, and this is the last
|
|
1343
|
-
place before it is published. A value a build is sanctioned to bake (`publicAtRest`,
|
|
1344
|
-
"Config or env?" above) is added by that build AFTER this gate, never carried through it.
|
|
1345
|
-
- **The wrapper** is the same name and the same five keys on every surface:
|
|
1346
|
-
`OMEGA_BUILD_JSON = { config, package, mode, license, builtAt }`, reachable as
|
|
1347
|
-
`self.OMEGA_BUILD_JSON` in a window and in a worker alike. `config` is the subset; the
|
|
1348
|
-
rest are facts about the BUILD and never part of the client contract. `mode` is the same
|
|
1349
|
-
THREE keys everywhere, `{ environment, build, publish }`: the build's verdict, whether it
|
|
1350
|
-
was a build rather than a dev/watch run, and whether it publishes. A surface's own extra
|
|
1351
|
-
verdicts (desktop's `server`) stay inside that surface's build module.
|
|
1352
|
-
- **The delivery** is ONE FILE, `build.js` at the artifact's web root
|
|
1353
|
-
([#743](https://github.com/Omega-JS-Stack/omega/issues/743), Ian 2026-09-12: "make it
|
|
1354
|
-
same shape and consumption everywhere as much as possible"). Two plain statements, written
|
|
1355
|
-
by `composeBuildJson()` + `writeBuildJs()` in
|
|
1356
|
-
[@omega.js/devkit/build-json](../../packages/devkit/src/build-json.js):
|
|
1357
|
-
|
|
1358
|
-
```js
|
|
1359
|
-
self.OMEGA_BUILD_JSON = { config, package, mode, license, builtAt };
|
|
1360
|
-
self.OMEGA_BUILD_JSON.config.dev = { … } | null;
|
|
1361
|
-
```
|
|
1362
|
-
|
|
1363
|
-
Every HTML shell loads it with one script tag, FIRST (web's `core/_includes/core/head.html`
|
|
1364
|
-
at `/build.js`, desktop's page template at `../../build.js`, the extension's at
|
|
1365
|
-
`/build.js`), and every worker with one `importScripts('/build.js')` line (web's
|
|
1366
|
-
`sw/omega.js`, the extension's `background.js`). No bundle carries a copy: the esbuild
|
|
1367
|
-
banners and defines the browser bundles grew are retired, and so is web's inline foot
|
|
1368
|
-
script. The `dev` map is its own statement because `omega dev` REWRITES that one line per
|
|
1369
|
-
request (#346), so a site built before the emulator came up still serves the map of the
|
|
1370
|
-
stack running right now.
|
|
1371
|
-
|
|
1372
|
-
Desktop's MAIN and PRELOAD bundles are the one exception, and not to the file: they are
|
|
1373
|
-
Node, they boot from the WHOLE resolved config inside the asar, and they keep carrying it
|
|
1374
|
-
as a define plus a banner.
|
|
1375
|
-
|
|
1376
|
-
A web page whose own `config:` block (#607) changes what a runtime reader sees emits ONE
|
|
1377
|
-
more line after the loader tag, `Object.assign(self.OMEGA_BUILD_JSON.config, <delta>)`,
|
|
1378
|
-
naming only the sections that actually differ.
|
|
1379
|
-
- **`@omega.js/client` is handed `OMEGA_BUILD_JSON.config`** by the same call on all three
|
|
1380
|
-
surfaces, and it owns the mapping onto its own contract: the `client` blob IS its top
|
|
1381
|
-
level, `cloud.config` is the Firebase home it boots from, and
|
|
1382
|
-
`monitoring.providers.sentry` is the one error-reporting switch. No surface composes a
|
|
1383
|
-
second home for any of those.
|
|
1384
|
-
|
|
1385
|
-
The backend is deliberately outside this: it stages the WHOLE resolved config to
|
|
1386
|
-
`dist/config/omega.json5` and reads it with the loader, which is right for a server.
|
|
1387
|
-
|
|
1388
|
-
## Consumer access
|
|
1389
|
-
|
|
1390
|
-
Each framework exposes the vendored loader — desktop: `require('@omega.js/desktop/config')`,
|
|
1391
|
-
extension: `require('@omega.js/extension/config')` → `{ loadConfig, validateConfig, … }`.
|
|
1392
|
-
Consumer workflows use this instead of raw JSON5 reads so brand-monorepo resolution
|
|
1393
|
-
always applies.
|
|
1394
|
-
|
|
1395
|
-
**Derived values reach brands as VALUES, never as a recipe to re-run**
|
|
1396
|
-
([#290](https://github.com/Omega-JS-Stack/omega/issues/290)). A brand target cannot require
|
|
1397
|
-
this private package at runtime, so a framework that owns a derivation publishes its
|
|
1398
|
-
ANSWER on the runtime config object the target already holds, under `resolved.*`: the
|
|
1399
|
-
backend's `omega.config.resolved.github` carries `{ owner, name, slug }`, the brand's
|
|
1400
|
-
SOURCE repo as one finished value. The derivations themselves stay here (`src/repo.js`):
|
|
1401
|
-
one implementation, called by the framework, so no brand re-implements the rules and
|
|
1402
|
-
drifts from them. New derived values join a framework's `resolved` group as real brand
|
|
1403
|
-
needs surface.
|
|
1404
|
-
|
|
1405
|
-
## The repo block and the repos it derives (#883)
|
|
1406
|
-
|
|
1407
|
-
ONE block says where a brand hosts its code, and it is two keys:
|
|
1408
|
-
|
|
1409
|
-
```json5
|
|
1410
|
-
repo: { provider: 'github', org: 'Acme-Org' }
|
|
1411
|
-
```
|
|
1412
|
-
|
|
1413
|
-
- **Presence is the switch.** A brand with no `repo` block hosts its source somewhere the
|
|
1414
|
-
manager does not touch, and the repo service skips, exactly as a missing target key
|
|
1415
|
-
means that target is not enabled. There is no `enabled` key.
|
|
1416
|
-
- **`provider`** defaults to `github` and must be one of `REPO_PROVIDERS` (`['github']`).
|
|
1417
|
-
A second provider is one table row plus its own helper, never a reshape of the block.
|
|
1418
|
-
- **`org`** is the only typed value, and it is required whenever the block is present: it
|
|
1419
|
-
owns every repo the brand's names derive under.
|
|
1420
|
-
|
|
1421
|
-
**Every repo NAME derives from the `<brand.id>-<role>` rule** (Ian 2026-09-07,
|
|
1422
|
-
[#809](https://github.com/Omega-JS-Stack/omega/issues/809)), one function per role in
|
|
1423
|
-
[repo.js](../../packages/config/src/repo.js), which every package reads. No override key
|
|
1424
|
-
of any kind exists: a repo name that must differ is a brand id that must differ.
|
|
1425
|
-
|
|
1426
|
-
| Role | Derivation | Visibility | Holds |
|
|
1427
|
-
|---|---|---|---|
|
|
1428
|
-
| Source monorepo | `sourceRepo(config)` → `<repo.org>/<brand.id>-omega` | the brand root package.json `private` field | the brand's code, its scaffolded workflows and their Actions secrets, and the CMS's content commits. Every dispatch addresses this one |
|
|
1429
|
-
| Releases | `releasesRepo(config)` → `<repo.org>/<brand.id>-releases` | always public (the desktop updater polls it with no token) | every target's built artifacts: desktop installers, the extension's zips, tagged per target |
|
|
1430
|
-
| Website, one per GitHub-hosted web target | `websiteRepo(config, name)` → `<repo.org>/<brand.id>-<target name>` | private only when the brand is private AND the org's plan allows Pages from a private repo, else public, and the manage walk says which | the BUILT site only, one force-orphan commit on `gh-pages`, served by Pages at the target's url |
|
|
1431
|
-
|
|
1432
|
-
**`repoDrift(originSlug, config)`** is the one comparison of a checkout's `origin` with
|
|
1433
|
-
the source repo ([#934](https://github.com/Omega-JS-Stack/omega/issues/934)): the whole
|
|
1434
|
-
slug, case-insensitive, null when they agree or the config derives no source repo, else
|
|
1435
|
-
the line `origin is <slug> but config derives <derived>: fix repo.org in
|
|
1436
|
-
config/omega.json5 or move the repo`. The boot prelude prints it; `omega manage` and
|
|
1437
|
-
`omega deploy` refuse with it ([deploys.md](deploys.md#the-origin-gate-934)).
|
|
1438
|
-
|
|
1439
|
-
**Visibility lives in the brand root's `package.json`**, never in omega.json5
|
|
1440
|
-
(`brandVisibility(brandRoot)`): `private: true` or the field ABSENT is a private brand
|
|
1441
|
-
(every brand monorepo is private by default, Ian 2026-09-11), and only a literal `false`
|
|
1442
|
-
is a public one. The manage walk reconciles the repo to it in both directions.
|
|
1443
|
-
|
|
1444
|
-
**`targets.<name>.hosting.provider`** says who SERVES a web target's built site, on web
|
|
1445
|
-
targets only (a non-web target carrying `hosting` is a validation error). It defaults to
|
|
1446
|
-
`github`, must be one of `HOSTING_PROVIDERS` (`['github']`), and is what `websiteRepo`
|
|
1447
|
-
reads: another provider means there is no GitHub repo to address at all.
|
|
1448
|
-
|
|
1449
|
-
**The Pages custom domain is ONE derivation**, `pagesHost(config, name)`: the bare host of
|
|
1450
|
-
that target's `targetUrl`, and an empty string when the url names a default Pages address
|
|
1451
|
-
(`*.github.io` is an ADDRESS, never a domain, [#366](https://github.com/Omega-JS-Stack/omega/issues/366)).
|
|
1452
|
-
The manage walk sets the domain on the repo from it and the web deploy writes its `CNAME`
|
|
1453
|
-
file from it, so the two can never claim different domains for one site.
|
|
1454
|
-
|
|
1455
|
-
The by-hand steps for a brand still on the old shape are the register's
|
|
1456
|
-
[2026-09-11 section](breaking-changes.md#2026-09-11-one-repo-block-and-the-website-repo-883).
|
|
1457
|
-
|
|
1458
|
-
## Defaults & self-healing
|
|
1459
|
-
|
|
1460
|
-
Ian's ruling (2026-08-22, [#478](https://github.com/Omega-JS-Stack/omega/issues/478)): the config
|
|
1461
|
-
stays complete and current. A subsystem that exists has its config structure IN the file —
|
|
1462
|
-
visible and editable — instead of an invisible framework fallback, and every default has ONE
|
|
1463
|
-
home.
|
|
1464
|
-
|
|
1465
|
-
**The schema is that home.** A `schema.js` entry carries its own `default:` beside its type and
|
|
1466
|
-
description; `schemaDefaults(target)` builds them into the merge chain's lowest layer, so nothing
|
|
1467
|
-
else re-states a default. A key with no sane framework answer carries none: owner decisions,
|
|
1468
|
-
tri-states that mean "ask" (`cloud.billingAccount`), ids the services provision
|
|
1469
|
-
(`forms.providers.slapform.formId`), anything secret-shaped. The other standing exclusion is the
|
|
1470
|
-
**presence gate** ([#425](https://github.com/Omega-JS-Stack/omega/issues/425)), which in practice
|
|
1471
|
-
means monitoring: that service reads `monitoring.providers.sentry` presence as the pick of
|
|
1472
|
-
Sentry, so its SDK knobs (`sampleRate`, `scrubEmail`, …) carry no default and keep their home in
|
|
1473
|
-
the package that reads them — and `advertising`, whose adsense `client` id is the whole switch
|
|
1474
|
-
([#527](https://github.com/Omega-JS-Stack/omega/issues/527)), so a materialized block would turn
|
|
1475
|
-
the web build's automatic ad placements on for a brand that configured none. `repo` is the third
|
|
1476
|
-
([#883](https://github.com/Omega-JS-Stack/omega/issues/883)): the BLOCK's presence enables the
|
|
1477
|
-
repo service, so `repo.provider` carries no default either and the value a missing provider reads
|
|
1478
|
-
lives beside the derivation, in `src/repo.js`. Blocks whose services
|
|
1479
|
-
gate on `enabled` or provisioned ids rather than presence (cloudflare, searchConsole,
|
|
1480
|
-
slapform, chatsy, replyify) DO carry defaults — see the polarity doctrine above.
|
|
1481
|
-
Role-level switches beside any providers block
|
|
1482
|
-
(`monitoring.enabled`, `marketing.campaigns.enabled`) are nobody's pick and always may.
|
|
1483
|
-
|
|
1484
|
-
**`omega manage` materializes what a brand lacks.** The workspace service's `defaults` operation
|
|
1485
|
-
(right after the `config` health check) diffs the brand's own `config/omega.json5` against the
|
|
1486
|
-
schema defaults and writes the missing blocks through the comment-preserving editor, each key
|
|
1487
|
-
documented with the schema's own description. The rules:
|
|
1488
|
-
|
|
1489
|
-
- **Never an overwrite.** Only keys the brand has NOT authored are written — a `false` a brand
|
|
1490
|
-
set (or a section it deliberately switched off) is a decision, and the heal never dives into it.
|
|
1491
|
-
- **Highest missing path, once.** A brand with no `marketing` at all gets one `marketing` block,
|
|
1492
|
-
not one edit per key inside it.
|
|
1493
|
-
- **Idempotent.** A converged config leaves the file byte-identical; a dry run reports the blocks
|
|
1494
|
-
and writes nothing.
|
|
1495
|
-
- **New subsystems arrive on the next run.** Adding a `default:` to the schema is all it takes for
|
|
1496
|
-
every consumer's config to grow the block the next time manage runs.
|
|
1497
|
-
|
|
1498
|
-
`marketing.prune.enabled` is the first ruling this carries: pruning is ON by default and lands in
|
|
1499
|
-
every brand config as an editable switch (Ian 2026-08-22, closing the
|
|
1500
|
-
[#422](https://github.com/Omega-JS-Stack/omega/issues/422) follow-up).
|
|
1501
|
-
|
|
1502
|
-
**`materialize: false` — a default that resolves but is never written**
|
|
1503
|
-
([#793](https://github.com/Omega-JS-Stack/omega/issues/793)). A rule may carry the flag beside its
|
|
1504
|
-
`default:`, and then `schemaDefaults()` still puts the value at the merge chain's lowest layer —
|
|
1505
|
-
every reader resolves it — while `missingDefaults()` and `defaultComments()` skip it, so the manage
|
|
1506
|
-
walk never writes it into `config/omega.json5`. The line for when to use it: **is this value an
|
|
1507
|
-
OWNER's decision, or the framework's own fact?** A decision belongs in the brand's file, where it is
|
|
1508
|
-
visible and editable (that is every ordinary default). A framework fact a brand may override and
|
|
1509
|
-
rarely does — presentation the framework owns — belongs at the layer that owns it, because a copy in
|
|
1510
|
-
every brand config is a copy that drifts from the thing it came from. The `connections` section is
|
|
1511
|
-
the first: the framework ships the five packaged providers' `name`, `logo` and `description`, a
|
|
1512
|
-
brand overrides any key through the ordinary merge chain, and nothing is copied into a brand file to
|
|
1513
|
-
go stale ([packages/backend/docs/connections.md](../../packages/backend/docs/connections.md)).
|
|
1514
|
-
|
|
1515
|
-
## Writeback (comment-preserving edits)
|
|
1516
|
-
|
|
1517
|
-
omega.json5 is hand-edited — comments, key order, and quote style carry meaning — so
|
|
1518
|
-
programmatic writes are surgical text edits, not a re-stringify (omega-manager's
|
|
1519
|
-
serializer rewrote the whole file in canonical order and lost comments; this replaces
|
|
1520
|
-
it). The manager's services use it to land resolved IDs in config: the SendGrid list,
|
|
1521
|
-
the Beehiiv publication, Stripe/PayPal product IDs, the Firebase SDK config.
|
|
1522
|
-
|
|
1523
|
-
`applyConfigEdits(source, edits, { comments }?)` applies `{ 'dot.path': value }` edits to JSON5 text:
|
|
1524
|
-
existing leaves get their value span replaced; missing branches insert as one property
|
|
1525
|
-
before the containing object's closing brace (matching indent; house style: unquoted
|
|
1526
|
-
keys, JSON.stringify strings, trailing commas). Every byte outside the edited spans
|
|
1527
|
-
survives. Paths take dots, numeric array indexes, and `[key=value]` matchers that select
|
|
1528
|
-
an array element by its own key — `payment.products[id=plus].stripe.productId` — so
|
|
1529
|
-
writes self-locate in the file being edited instead of trusting an index computed from a
|
|
1530
|
-
merged config. Array elements are never created.
|
|
1531
|
-
|
|
1532
|
-
`comments` (dot-path → text) documents INSERTED keys only — the comment lands above the key its
|
|
1533
|
-
path names, wherever inside the inserted block that is, wrapped at that key's indent. A path that
|
|
1534
|
-
already exists keeps whatever the brand wrote above it. That is how the manage-run heal
|
|
1535
|
-
([Defaults & self-healing](#defaults--self-healing)) lands each materialized block with the
|
|
1536
|
-
schema's own guidance beside it.
|
|
1537
|
-
|
|
1538
|
-
Guarantees: edits whose value already matches are skipped entirely (reruns are
|
|
1539
|
-
byte-identical); after every edit the result must JSON5-parse and hold the requested
|
|
1540
|
-
value at the requested path, or the call throws and nothing is returned — a corrupted
|
|
1541
|
-
config can't land on disk.
|
|
1542
|
-
|
|
1543
|
-
`writeConfigValues(projectDir, edits, { dryRun }?)` is the file-level form: resolves the
|
|
1544
|
-
standard locations, skips the write when nothing changes, and returns
|
|
1545
|
-
`{ path, changed, applied }` (`applied` = the paths that actually differed). The manager
|
|
1546
|
-
wraps it in `lib/config-write.js` (`writeBrandConfig(context, edits)`) for the uniform
|
|
1547
|
-
dry-run gate + logging.
|
|
1548
|
-
|
|
1549
|
-
**Deleting** is the same surgery in reverse ([#612](https://github.com/Omega-JS-Stack/omega/issues/612)):
|
|
1550
|
-
`applyConfigRemovals(source, paths)` cuts the property a dot-path names — its key, its whole
|
|
1551
|
-
subtree, the comma that separated it, and the `//` comment block documenting it, because that
|
|
1552
|
-
comment describes the key being deleted and would otherwise dangle over the next one. A property
|
|
1553
|
-
sharing its line (`{ a: 1, b: 2 }`) takes only itself and its separator. Absent paths are skipped,
|
|
1554
|
-
so reruns are byte-identical, and each removal is verified (parses, path gone) or the call throws.
|
|
1555
|
-
Array ELEMENTS are never removed, the mirror of never creating them. `removeConfigValues(projectDir,
|
|
1556
|
-
paths, { dryRun }?)` is the file-level form → `{ path, changed, removed }`. Unlike a write it does
|
|
1557
|
-
NOT normalize top-level key order: a deletion is surgical, and re-sorting the file around it would
|
|
1558
|
-
bury the one line the caller means to report. The consumer is `omega migrate` at a brand root
|
|
1559
|
-
([manager/index.md](../manager/index.md)).
|
|
1560
|
-
|
|
1561
|
-
## Migration — legacy configs → omega.json5
|
|
1562
|
-
|
|
1563
|
-
No framework reads the legacy files anymore. Convert once, delete the old file. General
|
|
1564
|
-
recipe: shared-looking sections move to the TOP LEVEL (brand, analytics, payment, theme;
|
|
1565
|
-
`oauth2` lands as `connections` (#788); `firebaseConfig` becomes `cloud: { provider: 'firebase', config: {…} }` and
|
|
1566
|
-
`sentry` becomes `monitoring: { providers: { sentry: {…} } }`); everything framework-specific
|
|
1567
|
-
moves under `targets.<name>`.
|
|
1568
|
-
|
|
1569
|
-
### The targets shape rename (#886)
|
|
1570
|
-
|
|
1571
|
-
An OMEGA-era brand converts once: every entry declares its `type`, and the KEY is the folder.
|
|
1572
|
-
The by-hand steps (and the rest of the register) are in
|
|
1573
|
-
[breaking-changes.md](breaking-changes.md#2026-09-11-targets-keyed-by-name-886).
|
|
1574
|
-
|
|
1575
|
-
| Legacy | New |
|
|
1576
|
-
|---|---|
|
|
1577
|
-
| `targets/website` (the canonical web folder) | **`targets/web`**: the folder is the key, and the key is `web` |
|
|
1578
|
-
| `targets/website-admin` (an instance's suffixed folder) | **`targets/admin`**: the sibling key names its own folder |
|
|
1579
|
-
| `web: [{ id: 'main' }, { id: 'admin' }]` (the instance ARRAY) | **sibling keys**: `web: { type: 'web' }, admin: { type: 'web' }` |
|
|
1580
|
-
| `--target=website` | **`--target=web`**: the flag takes the NAME |
|
|
1581
|
-
|
|
1582
|
-
### The one repo block (#883)
|
|
1583
|
-
|
|
1584
|
-
The brand's repo hosting used to be spelled in four places: `repo.providers.github`, a
|
|
1585
|
-
separate top-level `github` identity, a `targets.<name>.github.repo` override, and the
|
|
1586
|
-
desktop releases owner/repo. One block says it now (`repo: { provider, org }`), every repo
|
|
1587
|
-
name derives from `<brand.id>-<role>`, and visibility is the brand root package.json's
|
|
1588
|
-
`private` field. Each retired key is a registered PATH, so a brand still carrying one
|
|
1589
|
-
fails validation instead of silently addressing a repo nothing publishes to. The by-hand
|
|
1590
|
-
steps are the register's
|
|
1591
|
-
[2026-09-11 section](breaking-changes.md#2026-09-11-one-repo-block-and-the-website-repo-883).
|
|
1592
|
-
|
|
1593
|
-
| Retired path | New home |
|
|
1594
|
-
|---|---|
|
|
1595
|
-
| `repo.providers.github.org` | **`repo.org`** |
|
|
1596
|
-
| `repo.providers.github.repo` | nothing: the source repo IS `<brand.id>-omega`. A name that must differ is a brand id that must differ |
|
|
1597
|
-
| `repo.providers.github.private` | the brand root `package.json` `private` field (absent = private) |
|
|
1598
|
-
| `repo.providers.github.shared` | nothing: an org may host many brands, and no brand rewrites an org profile |
|
|
1599
|
-
| `repo.providers.github.enabled` | the PRESENCE of the `repo` block |
|
|
1600
|
-
| `github.user` | nothing: the org is `repo.org` |
|
|
1601
|
-
| `github.website` | nothing: a web target's site repo is `<brand.id>-<target name>` |
|
|
1602
|
-
| `targets.<name>.github.repo` (every type) | nothing: the CMS commits to the SOURCE repo `<brand.id>-omega` |
|
|
1603
|
-
| `targets.desktop.releases.owner` | nothing: `<brand.id>-releases` under `repo.org` |
|
|
1604
|
-
| `targets.desktop.releases.repo` | nothing: `<brand.id>-releases` under `repo.org` (`releases: {}` stays the presence switch) |
|
|
1605
|
-
|
|
1606
|
-
**Retired keys fail loudly** ([#142](https://github.com/Omega-JS-Stack/omega/issues/142)):
|
|
1607
|
-
a name that was renamed OUTRIGHT is a validation error wherever it sits — shared level,
|
|
1608
|
-
inside a `targets.<name>` entry, naming its replacement and
|
|
1609
|
-
pointing back here. Today that is `web_manager` → `client`, `firebaseConfig` → `cloud`,
|
|
1610
|
-
`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
|
|
1611
|
-
everything under it vanished, since nothing dual-reads it. Names that live on as legitimate
|
|
1612
|
-
keys elsewhere stay out of the list — the rows below are their only guide. `sentry` is the one
|
|
1613
|
-
that reads like a contradiction and is not: its NEW home is itself a `sentry` key
|
|
1614
|
-
(`monitoring.providers.sentry`), so a name test would fire on the very shape it is steering
|
|
1615
|
-
people toward. Same for `google`/`meta` under `analytics.providers`.
|
|
1616
|
-
|
|
1617
|
-
**`omega migrate` at the brand root deletes them** ([#612](https://github.com/Omega-JS-Stack/omega/issues/612)): both halves of the
|
|
1618
|
-
list, from the file as AUTHORED, through the comment-preserving editor (`removeConfigValues`) —
|
|
1619
|
-
the key, its subtree, and the comment documenting it, with every other byte untouched. One line
|
|
1620
|
-
per key naming its replacement, `--dry-run` for the plan, idempotent (a converged brand's rerun
|
|
1621
|
-
is byte-identical). It removes the dead key; moving the setting into the home named in the tables
|
|
1622
|
-
below is still by hand, EXCEPT where a row carries a converter
|
|
1623
|
-
([#858](https://github.com/Omega-JS-Stack/omega/issues/858)): then the new value is written
|
|
1624
|
-
first, through the same comment-preserving editor, and the old key is deleted in the same
|
|
1625
|
-
run, so a brand never sits between the two shapes. A dry run prints both halves.
|
|
1626
|
-
|
|
1627
|
-
`subdomains` is the newest name ([#588](https://github.com/Omega-JS-Stack/omega/issues/588),
|
|
1628
|
-
Ian 2026-09-01). The key was READ by exactly one thing, the cloud hosting op, which ensured an
|
|
1629
|
-
`api.{sub}.{domain}` per entry, and DECLARED by nothing: no schema rule, no default, never
|
|
1630
|
-
materialized. The fact it was reaching for is a web target, so a target is its home now:
|
|
1631
|
-
the NAME is the subdomain, and every subdomain shares one `api.<domain>`. It is a NAME test by the
|
|
1632
|
-
rule above (`subdomains` exists nowhere else in the schema), so it fires wherever a brand wrote
|
|
1633
|
-
it, including down inside a target entry's own `brand` block.
|
|
1634
|
-
|
|
1635
|
-
| Retired key | New home |
|
|
1636
|
-
|---|---|
|
|
1637
|
-
| `subdomains` | **`targets`** as sibling web targets: `["admin", "cdn"]` becomes `admin: { type: 'web' }, cdn: { type: 'web' }` beside the main site (§ Targets). The NAME is the subdomain (`https://admin.<brand host>`), an entry's own `url` overrides it for a custom host, and the targets share ONE `api.<domain>` |
|
|
1638
|
-
|
|
1639
|
-
`oauth2` is the newest name ([#788](https://github.com/Omega-JS-Stack/omega/issues/788),
|
|
1640
|
-
Ian 2026-09-03). The product concept is a CONNECTION, and a connection will not always be an
|
|
1641
|
-
OAuth grant — an API key or a bot token is one too — so the whole feature carries the product
|
|
1642
|
-
word (the section, the route, the user-record field, the env prefix, the brand provider folder,
|
|
1643
|
-
the callback URL) and each stored record names its own kind with `type: 'oauth2'`. A NAME test
|
|
1644
|
-
by the rule above: `oauth2` exists nowhere else in the schema. The by-hand steps a carrying
|
|
1645
|
-
brand still owes — the env rename, the provider-console redirect URI, the provider folder move —
|
|
1646
|
-
are in [breaking-changes.md](breaking-changes.md#the-user-connection-feature-is-connections-788).
|
|
1647
|
-
|
|
1648
|
-
| Retired key | New home |
|
|
1649
|
-
|---|---|
|
|
1650
|
-
| `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` |
|
|
1651
|
-
|
|
1652
|
-
The de-branding rekey ([#23](https://github.com/Omega-JS-Stack/omega/issues/23)) adds a
|
|
1653
|
-
second, PATH-based half in the same file (`RETIRED_PATHS`): keys whose provider keeps its
|
|
1654
|
-
own name one level down inside the new home, so a name test would false-positive. Each
|
|
1655
|
-
entry matches ONE exact path from the root, array positions ignored, and the error names the
|
|
1656
|
-
real path, index and all
|
|
1657
|
-
([#732](https://github.com/Omega-JS-Stack/omega/issues/732)). A `targets.<name>` row names the
|
|
1658
|
-
TYPE, and the key is a NAME, so the row fires on EVERY target of that type: with `community:
|
|
1659
|
-
{ type: 'web' }` declared, `targets.community.meta.title` bounces against the
|
|
1660
|
-
`targets.web.meta.title` row and the error names the real path
|
|
1661
|
-
([#886](https://github.com/Omega-JS-Stack/omega/issues/886)):
|
|
1662
|
-
|
|
1663
|
-
| Retired path | New home |
|
|
1664
|
-
|---|---|
|
|
1665
|
-
| `slapform` | **`forms.providers.slapform`** |
|
|
1666
|
-
| `chatsy` | **`inbound.chat.providers.chatsy`** (the widget `settings` moved here too — one home) |
|
|
1667
|
-
| `replyify` | **`inbound.email.providers.replyify`** |
|
|
1668
|
-
| `cloudflare` | **`edge.providers.cloudflare`** |
|
|
1669
|
-
| `recaptcha` | **`captcha.providers.recaptcha`** (`site-key` → `siteKey`) |
|
|
1670
|
-
| `searchConsole` | **`search.providers.searchConsole`** (`seo` already means the parasite-SEO content feature) |
|
|
1671
|
-
| `gcp` | **`cloud`** (`cloud.organizationId`, `cloud.billingAccount`) |
|
|
1672
|
-
| `firebase` | **`cloud`** (`cloud.shared`, `cloud.supportEmail`, `cloud.apiSubdomain`; projectId only at `cloud.config.projectId`) |
|
|
1673
|
-
| `advertising.providers.google-adsense` | **`advertising.providers.adsense`** + camelCase slots |
|
|
1674
|
-
| `github` | **`repo`** ([#883](https://github.com/Omega-JS-Stack/omega/issues/883)): the separate identity block is gone, and `github.user` / `github.website` are registered retired PATHS now, so a carrying brand hears it from the validator instead of this table |
|
|
1675
|
-
|
|
1676
|
-
The one-provider-shape normalization ([#425](https://github.com/Omega-JS-Stack/omega/issues/425))
|
|
1677
|
-
adds its own rows to the same `RETIRED_PATHS` half — every role names its vendors
|
|
1678
|
-
`role.providers.<provider>` now, so the flat picks, the bare vendor key and payment's
|
|
1679
|
-
fourth word are all retired. Key PRESENCE is the pick; `false` is the deliberate off
|
|
1680
|
-
switch; no entry at all is "none chosen" (what a null provider meant). `cloud` stays the
|
|
1681
|
-
ratified exception. Full rationale + the by-hand step per row:
|
|
1682
|
-
[breaking-changes.md](breaking-changes.md#one-provider-shape--roleprovidersprovider-425).
|
|
1683
|
-
|
|
1684
|
-
| Retired path | New home |
|
|
1685
|
-
|---|---|
|
|
1686
|
-
| `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 |
|
|
1687
|
-
| `certificates.apple` | **`certificates.providers.apple`** — no bare vendor keys; Windows signing sits beside it later |
|
|
1688
|
-
| `domain.provider` | **`domain.providers.<registrar>`** — `{ namecheap: {} }` / `{ squarespace: {} }` |
|
|
1689
|
-
| `domain.email.provider` | **`domain.email.providers.<provider>`** — `domain.email.forwarding` stays role-level (provider-agnostic) |
|
|
1690
|
-
| `translation.provider` | **`translation.providers.<name>`** — `{ claude: {} }` / `{ chatgpt: {} }`; an absent block still means claude. `translation.model` stays role-level |
|
|
1691
|
-
| `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 |
|
|
1692
|
-
| `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.<name>.monitoring.providers.sentry.dsn` |
|
|
1693
|
-
| `marketing.campaigns.provider` + `marketing.campaigns.listId` | **`marketing.campaigns.providers.sendgrid.listId`** — `marketing.campaigns.enabled` stays role-level |
|
|
1694
|
-
| `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 |
|
|
1695
|
-
|
|
1696
|
-
The retired-key sweep only ever runs over an omega.json5: the `omega migrate` converter
|
|
1697
|
-
READS legacy files as input and emits the new names, and only its output is validated.
|
|
1698
|
-
|
|
1699
|
-
AdSense's one-switch collapse ([#527](https://github.com/Omega-JS-Stack/omega/issues/527))
|
|
1700
|
-
adds the last two rows ([#628](https://github.com/Omega-JS-Stack/omega/issues/628)). Both
|
|
1701
|
-
were unregistered until then, so a brand still carrying the deleted gate validated CLEAN
|
|
1702
|
-
while the account it meant to leave alone started being managed — the key reading exactly
|
|
1703
|
-
like it still worked. `omega migrate` drops both with the note.
|
|
1704
|
-
|
|
1705
|
-
| Retired path | New home |
|
|
1706
|
-
|---|---|
|
|
1707
|
-
| `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 |
|
|
1708
|
-
| `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 |
|
|
1709
|
-
|
|
1710
|
-
The `meta` section is the other registered path ([#607](https://github.com/Omega-JS-Stack/omega/issues/607),
|
|
1711
|
-
Ian 2026-08-26 — meta never exists in two places). It shipped for one wave beside the
|
|
1712
|
-
bare `meta:` a page and a layout already wrote, which is two homes for one fact; deleting
|
|
1713
|
-
the config half leaves page frontmatter as the only meta, with `brand.name` /
|
|
1714
|
-
`brand.description` as the site-wide default the head falls back to. Registered at its
|
|
1715
|
-
authored path AND at the `targets.web` overlay — never by key NAME, because
|
|
1716
|
-
`analytics.providers.meta` is a legitimate key one level down.
|
|
1717
|
-
|
|
1718
|
-
| Retired path | New home |
|
|
1719
|
-
|---|---|
|
|
1720
|
-
| `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)) |
|
|
1721
|
-
| `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 |
|
|
1722
|
-
|
|
1723
|
-
`translation.exclude` is the same ruling's second fold ([#858](https://github.com/Omega-JS-Stack/omega/issues/858),
|
|
1724
|
-
Ian 2026-09-13). The key named what NOT to translate and defaulted to nothing, so a brand
|
|
1725
|
-
that never thought about it paid a provider for every post it had, and a page had no way to
|
|
1726
|
-
say anything at all. The list says what to TRANSLATE now: globs on page routes with `!`
|
|
1727
|
-
negation, read in `.gitignore` order, defaulting to `['**', '!blog/**']` in the DEFAULTS
|
|
1728
|
-
layer, and a page overrides it for itself with the SAME key name one level down
|
|
1729
|
-
(`translation.include: true`/`false` in its own frontmatter). Registered at its authored
|
|
1730
|
-
path AND at the `targets.web` overlay, the two places a brand can write it, never by key
|
|
1731
|
-
NAME: `exclude` is a legitimate key elsewhere. This row carries a CONVERTER, so
|
|
1732
|
-
`omega migrate` MOVES the setting instead of only deleting it.
|
|
1733
|
-
|
|
1734
|
-
| Retired path | New home |
|
|
1735
|
-
|---|---|
|
|
1736
|
-
| `translation.exclude` (and the same key under `targets.web.translation`) | **`translation.include`**: the route list says what to translate, so `exclude: ['docs']` becomes `include: ['**', '!docs']` and `omega migrate` writes exactly that before deleting the old key ([translation.md](translation.md), [docs/web/frontmatter.md](../web/frontmatter.md)) |
|
|
1737
|
-
|
|
1738
|
-
`targets.web.redirects` is the newest registered path ([#466](https://github.com/Omega-JS-Stack/omega/issues/466)).
|
|
1739
|
-
It shipped in 0.45.0 and was withdrawn: static hosting has no server, so the map could only
|
|
1740
|
-
ever be answered CLIENT-side off the built 404 page, and a search engine saw a 404 that
|
|
1741
|
-
redirects rather than a move. Redirects are not web config at all now — a TEMPLATED
|
|
1742
|
-
redirect needs edge computing, an enumerable one is a page. Registered at its authored
|
|
1743
|
-
path, the one place a carrying brand has it.
|
|
1744
|
-
|
|
1745
|
-
| Retired path | New home |
|
|
1746
|
-
|---|---|
|
|
1747
|
-
| `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)) |
|
|
1748
|
-
|
|
1749
|
-
The four schema-less web sections are registered paths too
|
|
1750
|
-
([#850](https://github.com/Omega-JS-Stack/omega/issues/850), Ian 2026-09-09:
|
|
1751
|
-
everything the build processes has a schema home). They were legacy UJM
|
|
1752
|
-
presentation blocks the converter wrote under `targets.web` with no schema rule
|
|
1753
|
-
anywhere, and @omega.js/web carried a PRIVATE list (`WEB_ONLY_SECTIONS`) purely
|
|
1754
|
-
so its own `config:` guard would pass them. That list is gone: each key is a
|
|
1755
|
-
registered path now, so a brand still carrying one hears it from the validator
|
|
1756
|
-
instead of authoring a value nothing reads. Registered at their authored path,
|
|
1757
|
-
the one place a carrying brand has them, and `omega migrate` drops the three
|
|
1758
|
-
with a note and moves the currency.
|
|
1759
|
-
|
|
1760
|
-
| Retired path | New home |
|
|
1761
|
-
|---|---|
|
|
1762
|
-
| `targets.web.favicon` | nothing for the path: the favicon set is MINTED from the brand images (manager assets service) and bridged to `/assets/images/favicon`, so an override pointed the whole site at an unminted folder. Change the source image, **`brand.images.favicon`**. The `theme-color` half is **`brand.color`**, the one place a brand states its hex |
|
|
1763
|
-
| `targets.web.manifest` | nothing: no reader anywhere in @omega.js/web. The web app manifest that ships is the minted set's own `site.webmanifest` |
|
|
1764
|
-
| `targets.web.icons` | **the `icon` on the link itself**, as Font Awesome classes (`icon: 'fa-brands fa-github'`): one icon mechanism ([#619](https://github.com/Omega-JS-Stack/omega/issues/619), [icons.md](icons.md)), so there is no name-to-markup map |
|
|
1765
|
-
| `targets.web.currency` | **`payment.currency`**: the price currency belongs to the payment section every other surface reads it from, and the pricing JSON-LD reads it there |
|
|
1766
|
-
|
|
1767
|
-
`targets.desktop.downloads.*` are the newest registered paths ([#799](https://github.com/Omega-JS-Stack/omega/issues/799)).
|
|
1768
|
-
The `download-server` mirror gave marketing a fixed filename, which the versionless artifact
|
|
1769
|
-
names ([#620](https://github.com/Omega-JS-Stack/omega/issues/620)) made free: the site links
|
|
1770
|
-
the ONE public releases repo directly and reads nothing from the mirror, so #799 deleted the
|
|
1771
|
-
lane. Without these rows a brand carrying the block validates clean (it sits inside the
|
|
1772
|
-
exempt `targets` namespace), gets a second repo provisioned and nothing published to it.
|
|
1773
|
-
Registered per KEY at its authored path, never by name: the curated
|
|
1774
|
-
`site.targets.desktop.downloads` map is a legitimate `downloads` one level down.
|
|
1775
|
-
|
|
1776
|
-
| Retired path | New home |
|
|
1777
|
-
|---|---|
|
|
1778
|
-
| `targets.desktop.downloads.enabled` | **`targets.desktop.releases`**: one public releases repo per brand, and its versionless assets ARE the permanent download links |
|
|
1779
|
-
| `targets.desktop.downloads.owner` | nothing: there is no second repo to own, and the releases repo derives as `<brand.id>-releases` under `repo.org` (#883) |
|
|
1780
|
-
| `targets.desktop.downloads.repo` | nothing: the releases repo derives as `<brand.id>-releases` (#883) |
|
|
1781
|
-
| `targets.desktop.downloads.tag` | nothing: `/releases/latest/download/<asset>` is what the stable mirror tag was for |
|
|
1782
|
-
|
|
1783
|
-
The company is ONE key now ([#677](https://github.com/Omega-JS-Stack/omega/issues/677)),
|
|
1784
|
-
outside `brand`, and everything else about the company is RESOLVED at load. The typed
|
|
1785
|
-
display name and the typed parent wordmark are registered retired PATHS; the typed
|
|
1786
|
-
`company.url` (and any `company.name` / `company.images`) fails the LOAD instead, naming
|
|
1787
|
-
the file, because the resolver fills those same keys and a retired-key sweep over a
|
|
1788
|
-
resolved config would fire on its own answer. The by-hand step, and the `parent` /
|
|
1789
|
-
`.omega/company.json` half, are in
|
|
1790
|
-
[breaking-changes.md](breaking-changes.md#2026-09-12-one-company-key-and-a-company-tree-inside-the-parent-677).
|
|
1791
|
-
|
|
1792
|
-
| Retired path | New home |
|
|
1793
|
-
|---|---|
|
|
1794
|
-
| `brand.company` | **`company.name`**, resolved from `company: { id: '<parent brand.id>' }`: the parent's name is the parent's to state |
|
|
1795
|
-
| `brand.images.companyWordmark` | **`company.images.wordmark`**, resolved from the parent's own `brand.images.wordmark` |
|
|
1796
|
-
| `company.url` (typed) | **`company.url`** (resolved): type `company: { id }` and the loader fills it; an authored one fails the load |
|
|
1797
|
-
| `parent` (every value) | The key is RETIRED outright ([#677](https://github.com/Omega-JS-Stack/omega/issues/677)): the topology is **`company: { id: 'self' }`** / **`company: { id: '<parent brand.id>' }`**, and the opt-out `parent: false` is **`company: { webhooks: false }`**. A config still carrying `parent` fails the load naming `company.webhooks` |
|
|
1798
|
-
|
|
1799
|
-
### electron-manager (`config/electron-manager.json` → `config/omega.json5`) — DONE (checkpoint 18)
|
|
1800
|
-
|
|
1801
|
-
| Legacy | New |
|
|
1802
|
-
|---|---|
|
|
1803
|
-
| `brand`, `analytics`, `payment`, `theme` | top level, unchanged |
|
|
1804
|
-
| `firebaseConfig` | **`cloud: { provider: 'firebase', config: {…} }`** (D12) |
|
|
1805
|
-
| `sentry` | **`monitoring: { providers: { sentry: { dsn } } }`** ([#425](https://github.com/Omega-JS-Stack/omega/issues/425)) |
|
|
1806
|
-
| `app` | `targets.desktop.app` |
|
|
1807
|
-
| `targets.mac` / `targets.win` / `targets.linux` (per-OS) | `targets.desktop.platforms.mac` / `.windows` / `.linux` ([#867](https://github.com/Omega-JS-Stack/omega/issues/867): the vocabulary is `mac`, `windows`, `linux`, and what each ships is `platforms.<platform>.formats.<format>`; `platforms.linux.snap` moved inside as `platforms.linux.formats.snap`, its `enabled` flag replaced by presence) |
|
|
1808
|
-
| `autoUpdate`, `startup`, `releases`, `remoteConfig`, `restartManager` | `targets.desktop.<same key>` |
|
|
1809
|
-
| `electronBuilder` overrides | `targets.desktop.electronBuilder` |
|
|
1810
|
-
| `cdp` | `targets.desktop.cdp` |
|
|
1811
|
-
| `windows` (optional) | `targets.desktop.windows` |
|
|
1812
|
-
| `fileAssociations`, `protocols` | `targets.desktop.<same key>` |
|
|
1813
|
-
|
|
1814
|
-
### backend-manager (`functions/backend-manager-config.json` → `functions/config/omega.json5`) — DONE (checkpoint 19)
|
|
1815
|
-
|
|
1816
|
-
| Legacy | New |
|
|
1817
|
-
|---|---|
|
|
1818
|
-
| `brand`, `analytics`, `payment` | top level, unchanged |
|
|
1819
|
-
| `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 |
|
|
1820
|
-
| `firebaseConfig` | **`cloud: { provider: 'firebase', config: {…} }`** (D12) |
|
|
1821
|
-
| `sentry` | **`monitoring: { providers: { sentry: { dsn } } }`** ([#425](https://github.com/Omega-JS-Stack/omega/issues/425)) |
|
|
1822
|
-
| custom keys (`omega`, `mcp`, …) | top level, unchanged |
|
|
1823
|
-
| `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 |
|
|
1824
|
-
| `parent` | RETIRED outright ([#677](https://github.com/Omega-JS-Stack/omega/issues/677)): a URL or `'self'` becomes **`company: { id }`**, and `parent: false` becomes **`company: { webhooks: false }`**. The key itself fails the load now, naming its new home |
|
|
1825
|
-
|
|
1826
|
-
Notes: @omega.js/backend's framework-defaults layer is `templates/config/omega.json5` resolved through
|
|
1827
|
-
the same loader and passed as `options.defaults`; `initialize()` takes no
|
|
1828
|
-
`backendManagerConfigPath` option (the loader discovers the file); boot warns on
|
|
1829
|
-
schema findings, `npx omega test`'s target checks are the hard audit. The sandbox brand dogfoods the full
|
|
1830
|
-
hierarchy: shared sections live in `brands/sandbox-brand/config/omega.json5` (brand level),
|
|
1831
|
-
the backend target's local file carries only `targets.backend`.
|
|
1832
|
-
|
|
1833
|
-
### browser-extension-manager (`config/browser-extension-manager.json` → `config/omega.json5`) — DONE (checkpoint 20)
|
|
1834
|
-
|
|
1835
|
-
| Legacy | New |
|
|
1836
|
-
|---|---|
|
|
1837
|
-
| `brand`, `analytics`, `theme` | top level, unchanged |
|
|
1838
|
-
| `firebaseConfig` | **`cloud: { provider: 'firebase', config: {…} }`** (D12) |
|
|
1839
|
-
| `sentry` | **`monitoring: { providers: { sentry: { dsn } } }`** ([#425](https://github.com/Omega-JS-Stack/omega/issues/425)) |
|
|
1840
|
-
| custom keys (`liveReloadPort`, …) | top level, unchanged |
|
|
1841
|
-
| `analytics.providers.google.secret` | **`.env` → `GOOGLE_ANALYTICS_SECRET`** (secrets never in omega.json5; loader hard-fails) |
|
|
1842
|
-
| *(no extension-specific keys yet)* | `targets.extension: {}` — presence = enabled; extension-specific settings land here |
|
|
1843
|
-
|
|
1844
|
-
Notes: `build.getConfig()` (`require('@omega.js/extension/build')`) returns the RESOLVED config (missing file → `{}`; schema
|
|
1845
|
-
findings warn once per process — BXM has no separate audit surface). The build snapshot
|
|
1846
|
-
(`OMEGA_BUILD_JSON`, the artifact's one `build.js`) bakes `GOOGLE_ANALYTICS_SECRET` from the environment at
|
|
1847
|
-
build time, same value flow as before. `bxm setup` scaffolds + merges `config/omega.json5`
|
|
1848
|
-
(the defaults merge now preserves consumer-only keys at every level — it previously
|
|
1849
|
-
dropped them).
|
|
1850
|
-
|
|
1851
|
-
### ultimate-jekyll-manager (`src/_config.yml` + `config/ultimate-jekyll-manager.json`) — `omega migrate` (B4, checkpoint 32)
|
|
1852
|
-
|
|
1853
|
-
One command converts the consumer in place (and `--check` previews without
|
|
1854
|
-
writing). The converted file is validated through `loadConfig(root, 'web')`
|
|
1855
|
-
before the report prints.
|
|
1856
|
-
|
|
1857
|
-
| Legacy | New |
|
|
1858
|
-
|---|---|
|
|
1859
|
-
| `url` | `brand.url` (site.url derives; empty `baseurl` dropped) |
|
|
1860
|
-
| `brand`, `theme` | top level, verbatim |
|
|
1861
|
-
| `oauth2` | **`connections`** — same block, new name ([#788](https://github.com/Omega-JS-Stack/omega/issues/788)) |
|
|
1862
|
-
| `analytics.{google,meta,tiktok}` (flat scalars) | `analytics.providers.<p>.id` — the unified spelling; the web chrome emits the client's flat shape from it |
|
|
1863
|
-
| `web_manager.firebase.app.config` | **`cloud: { provider: 'firebase', config: {…} }`** (top level); the engine composes `cloud.config` back into `client.firebase.app.config` at build |
|
|
1864
|
-
| `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 |
|
|
1865
|
-
| `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 |
|
|
1866
|
-
| `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 |
|
|
1867
|
-
| `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) |
|
|
1868
|
-
| `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) |
|
|
1869
|
-
| `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 |
|
|
1870
|
-
| `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.include` stays a web-only key at that shared home, and the legacy `exclude` list CONVERTS to it on the way through ([#858](https://github.com/Omega-JS-Stack/omega/issues/858)): `exclude: ['docs']` becomes `include: ['**', '!docs']`, one note, so the brand keeps translating exactly what it was |
|
|
1871
|
-
| `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 |
|
|
1872
|
-
| `download`, `extension`, `favicon`, `manifest`, `icons`, `currency` | **DROPPED with a note, every one** (nothing lands under `targets.web`): `download` / `extension` derive from the desktop and extension targets ([#610](https://github.com/Omega-JS-Stack/omega/issues/610)); `favicon`, `manifest` and `icons` retire with [#850](https://github.com/Omega-JS-Stack/omega/issues/850) (the favicon set is minted and `theme-color` comes from `brand.color`, nothing reads a manifest block, and a link carries its own `fa-*` classes). `currency` MOVES to **`payment.currency`**. Each one is a registered retired path, so carrying it forward would write a config the validator refuses |
|
|
1873
|
-
| `recaptcha` (incl. `site-key`) | **`captcha.providers.recaptcha`** (`siteKey` — every key is camelCase, #23) |
|
|
1874
|
-
| `cloudflare` (the purge `zone`) | **`edge.providers.cloudflare`** — one cloudflare home, shared with the manager's zone reconciliation (#23) |
|
|
1875
|
-
| `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 |
|
|
1876
|
-
| `permalink`, `pagination`, `collections`, `defaults`, `generators` | `targets.web.<same key>` (codemod rule 8's home — engine consumption of custom collections rides the consumer-theme waves) |
|
|
1877
|
-
| 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 |
|
|
1878
|
-
| UJM-json `webpack`, `gems`; `_config.yml` Jekyll machinery (`plugins`, `exclude`, …) | dropped, noted in the report |
|
|
1879
|
-
| secret-shaped keys anywhere | dropped + warned — move to `.env` |
|
|
1880
|
-
|
|
1881
|
-
Beyond config: the codemod rule table runs over `src/**` templates, the seed
|
|
1882
|
-
`src/assets/js/main.js` is deleted (core main + boot runtime replace it;
|
|
1883
|
-
customized ones are flagged with the port recipe), `main.scss`'s
|
|
1884
|
-
`@use 'ultimate-jekyll-manager' with (…)` becomes `@use 'omega:main' with (…)`,
|
|
1885
|
-
page-css self-`@use` lines are dropped, and Gemfile/Gemfile.lock/the legacy
|
|
1886
|
-
configs are removed.
|
|
1887
|
-
|
|
1888
|
-
### omega-manager brand configs (`.brands/{id}/config.json`)
|
|
1889
|
-
|
|
1890
|
-
The brand-config `targets` ARRAY's role is absorbed by key presence in the omega.json5
|
|
1891
|
-
`targets` object. omega-manager's disperse writes omega.json5 from brand config + state at
|
|
1892
|
-
its Phase-3 cutover (enumerating `SHARED_SECTIONS`, per-surface values into
|
|
1893
|
-
`targets.<name>` overrides).
|
|
1894
|
-
|
|
1895
|
-
## Package API (quick reference)
|
|
1896
|
-
|
|
1897
|
-
```js
|
|
1898
|
-
const {
|
|
1899
|
-
loadConfig, // (projectDir, target?, { defaults }?) → { config, errors, warnings, enabled, name, files }
|
|
1900
|
-
composeTargetConfig, // (projectDir, target) → { config, files } — company+brand+local frozen into ONE self-contained file (deploy upload boundary, #31)
|
|
1901
|
-
hasOmegaConfig, // (projectDir) → boolean — "is this project migrated?"
|
|
1902
|
-
resolveConfigPath, // (projectDir) → abs path | null
|
|
1903
|
-
getEnabledTargets, // (config) → ['web', 'backend', …]
|
|
1904
|
-
findBrandRoot, // (projectDir) → brand root | null — CLASSIFIES one target dir (THE hierarchy rule)
|
|
1905
|
-
findBrandConfigPath, // (projectDir) → the BRAND layer's omega.json5 | null — the file a target with no local-layer file rides
|
|
1906
|
-
resolveBrandRoot, // (startDir) → brand root | null — SEARCHES upward from anywhere (standalone → itself), bounded at the nearest .git
|
|
1907
|
-
loadEnv, // (startDir) → { chain, loaded } — resolve + load the .env cascade
|
|
1908
|
-
reloadEnv, // (startDir, options?) → same — drops the FILE-owned keys, then loads again, so an EDITED value lands and the shell still wins (#724)
|
|
1909
|
-
resolveEnvChain, // (startDir) → { local, brand, company } .env paths (no loading)
|
|
1910
|
-
loadEnvChain, // (paths) → loaded[] — dotenv strongest-first, nulls/missing skip
|
|
1911
|
-
resolveCompany, // (brandRoot) → { id, name, url, images, root, dir, file(relPath), config }: the ONE company resolver (#677)
|
|
1912
|
-
recordBrand, // ({ id, root, name, url }) → wrote?: the machine registry line every loadConfig refreshes
|
|
1913
|
-
readRegistry, // () → { <brand.id>: { root, name, url, updatedAt } }: ~/.omega/brands.json (OMEGA_HOME moves it)
|
|
1914
|
-
COMPANY_RESOLVED_FILE, // 'config/company-resolved.json5': the generated layer a deploy hands a runner
|
|
1915
|
-
resolveHook, // (startRoot, 'account/password') → hook file | null (brand → company)
|
|
1916
|
-
loadHook, // (startRoot, hookPath) → { fn, file } | null — broken hooks THROW
|
|
1917
|
-
validateConfig, // (config, { target }?) → { errors, warnings }
|
|
1918
|
-
runSchema, // low-level rule walker (EM's proven engine)
|
|
1919
|
-
formatErrors, // errors → numbered block
|
|
1920
|
-
resolvedBrandHost, // (config) → the BRAND's own host (`brand.url`, top-level `url` only as fallback), lowercased | '' — authDomain validation AND every persona address (#708)
|
|
1921
|
-
findSecretKeys, // (object) → dot-paths of secret-shaped keys
|
|
1922
|
-
findRetiredKeys, // (object) → [{ path, key, replacement, why }] — renamed-outright keys (#142)
|
|
1923
|
-
chosenProvider, // (role.providers) → the picked provider name | null — presence is the pick, `false` the off switch (#425)
|
|
1924
|
-
backendProjectType, // (targets.backend | resolved backend config) → 'firebase' | 'custom' — how the backend runs (#584)
|
|
1925
|
-
BACKEND_PROJECT_TYPES,
|
|
1926
|
-
applyConfigEdits, // (source, edits, { comments }?) → edited source — comment-preserving (see Writeback)
|
|
1927
|
-
writeConfigValues, // (projectDir, edits, { dryRun, comments }?) → { path, changed, applied }
|
|
1928
|
-
applyConfigRemovals, // (source, paths) → edited source — deletes a key, its subtree and its own comment (#612)
|
|
1929
|
-
removeConfigValues, // (projectDir, paths, { dryRun }?) → { path, changed, removed } — absent paths skip, so reruns are byte-identical
|
|
1930
|
-
schemaDefaults, // (target?) → the schema's own defaults, the merge chain's lowest layer (#478)
|
|
1931
|
-
missingDefaults, // (rawConfig, target?) → [{ path, value }] — the blocks a brand file lacks (the manage heal's list)
|
|
1932
|
-
defaultComments, // (target?) → { 'dot.path': description } — the guiding comments a materialized block carries
|
|
1933
|
-
deepMerge, // agnostic layer merge
|
|
1934
|
-
// The targets map (targets.js: every key is a NAME, #886)
|
|
1935
|
-
targetEntries, // (config) → [{ name, type, …entry }] in config order; throws on a missing/unknown type
|
|
1936
|
-
targetsOfType, // (config, 'web') → the same entries, that type only
|
|
1937
|
-
targetPath, // (config, 'community') → 'targets/community'; an undeclared name throws with the declared list
|
|
1938
|
-
targetNameFromDir, // (projectDir) → the target root's basename inside a brand (functions//dist/ normalize up); null standalone
|
|
1939
|
-
targetUrl, // (config, name) → entry url → entry brand.url → brand.url when name IS the type → https://<name>.<brand host> | null
|
|
1940
|
-
targetPortOffset, // (config, name) → position among the SAME-type targets (dev-port offsets)
|
|
1941
|
-
brandHost, // (url) → the host exactly as brand.url states it (www kept, path dropped, port kept)
|
|
1942
|
-
TARGET_NAME_PATTERN, TARGET_TYPES, // the name slug rule and [web, backend, desktop, extension, mobile, custom]
|
|
1943
|
-
// The browser subset (#894): what a browser surface may see, and the one wrapper it is baked in
|
|
1944
|
-
clientConfig, // (resolved + this build's facts) → the blob every browser surface bakes as OMEGA_BUILD_JSON.config
|
|
1945
|
-
CLIENT_FACT_KEYS, // the build facts that ride beside the client sections (runtime, environment, version, buildTime, target, url, dev)
|
|
1946
|
-
TARGETS, SHARED_SECTIONS, CLIENT_SECTIONS, SHARED_SCHEMA, TARGET_SCHEMAS,
|
|
1947
|
-
} = require('@omega.js/config');
|
|
1948
|
-
```
|