diff --git a/.agents/architecture.md b/.agents/architecture.md index be8b291..8104d98 100644 --- a/.agents/architecture.md +++ b/.agents/architecture.md @@ -71,6 +71,15 @@ editing templates or values. `server.configuration` file mount). Never mirror authup's config schema in templates (Authelia's 714-line configMap treadmill is the cautionary tale). `server.config` keys colliding with first-class env names fail the render. + The ONE mirrored schema is the theme manifest (`server.theme.title` / + `logo` / `tokens` / ... compose `theme.json`), and it earns the exception + on three counts: the file is a fixed 8-key document rather than a growing + config surface, authup fails the BOOT on an unknown key or a malformed + token so a typo has no cheaper detector, and the alternative is a JSON + blob inside a YAML string with no schema at all. It stays worth it only + while the manifest stays small: `files` remains the escape hatch, and a + hand-written `theme.json` there is still supported (the two are mutually + exclusive by validation). 15. **URL derivation is the chart's core UX.** `PUBLIC_URL`, `NUXT_PUBLIC_API_URL`, `NUXT_PUBLIC_PUBLIC_URL` derive from the two ingress blocks; the UI origin is auto-appended to `TRUSTED_ORIGINS` diff --git a/.agents/references/authup.md b/.agents/references/authup.md index f9d9708..6e9639d 100644 --- a/.agents/references/authup.md +++ b/.agents/references/authup.md @@ -3,7 +3,7 @@ Repo: https://github.com/authup/authup (local checkout commonly at `/opt/projects/authup/authup`). The chart encodes facts about the app; verify against these sources when authup releases change behavior. Pinned against the -v1.0.0-beta.58 line (chart `appVersion`). +v1.0.0-beta.62 line (chart `appVersion`). ## Image / entrypoint contract @@ -14,6 +14,7 @@ v1.0.0-beta.58 line (chart `appVersion`). | Image runs as root; writable paths `/usr/src/app/writable` + npm cache | `Dockerfile` (`WRITABLE_DIRECTORY_PATH`, no `USER`) | emptyDir mounts + `npm_config_cache=/tmp/.npm-cache`; root securityContext default | | `latest`/``/`beta`/`next` tags | `.github/workflows/release.yml`, `docker-nightly.yml` | `image.tag` defaults to `Chart.AppVersion` | | `authup` CLI supervisor NOT routable through the entrypoint | `entrypoint.sh` case statement | chart never offers a combined pod | +| An unknown service arg EXITS 1 since beta.59 (it used to exit 0 and start nothing); `client/web` was renamed `client/admin-console` with no alias | `entrypoint.sh` `*)` branch | chart already passes `client/admin-console` | ## server-core env surface @@ -29,9 +30,13 @@ Docs mirror: `docs/src/guide/deployment/configuration-server-core*.md`. | `SMTP` (URL form; per-field SMTP is config-file-only) | `secret-smtp.yaml` | | `PUBLIC_URL`, `TRUSTED_ORIGINS`, `TRUST_PROXY` (app default trusts every hop; chart pins "1") | `_urls.tpl` + `authup.server.configEnv` | | `REGISTRATION_ENABLED`, `PASSWORD_RECOVERY_ENABLED`, `EMAIL_VERIFICATION_ENABLED`, `MFA_ENABLED`, `MFA_REQUIRED` (strict booleans: unparsable value crashes boot) | `server.features.*` / `server.mfa.*`, always quoted | +| `ACCOUNT_CONSOLE_ENABLED` (beta.62, default true): serves the `/account` self-service SPA off the IdP origin | `server.features.accountConsole` | +| `THEME_DIRECTORY_PATH` / `THEME_FRAGMENTS_ENABLED` (beta.59, EXPERIMENTAL): operator theme for the two served consoles; manifest at `/theme.json`, HTTP mount root is `/assets` only | `server.theme.*` (the chart composes theme.json) | +| `AUTH_CONSOLE_PATH` / `ACCOUNT_CONSOLE_PATH`: substitute a whole console package, boot-asserted `CONTRACT_VERSION` | deliberately NOT first-class; `server.config` + `extraVolumes` escape hatch | | `USER_ADMIN_PASSWORD(_RESET)`, `CLIENT_SYSTEM_ENABLED/SECRET(_RESET)` | `auth.*` values + chart-managed secret | | `SECRETS_ENCRYPTION_KEY` (base64 32 bytes; write-once, removal with wrapped rows fails loud) | `auth.secretsEncryptionKey(+Enabled)`, never generated, never optional | | Cross-field boot validations (throttle needs event log, mfaRequired needs mfaEnabled, KEK length) | mirrored as render-time guards in `templates/validations.yaml` | +| `TRUSTED_ORIGINS` rejects `**` in a host at boot since beta.59 (`config/origins.ts`, `patternHasGlobstarInAuthority`); a single `*` is a supported host wildcard | `authup.assertTrustedOrigin` in `_urls.tpl`, asserted after tpl rendering | Config file: `authup.server.core.conf` in the process cwd (`app/modules/config/read/fs.ts`; env always wins) -> `server.configuration` / @@ -47,8 +52,10 @@ and dead): `apps/client-admin-console/nuxt.config.ts`, `NUXT_PUBLIC_API_URL` (browser-reachable server URL), `NUXT_PUBLIC_PUBLIC_URL`, `NUXT_API_URL` (SSR-side override), `NUXT_PUBLIC_COOKIE_DOMAIN` (deliberately never set by the chart: sharing a cookie domain with the server origin is -unsupported per `.agents/architecture.md` in the monorepo). Chart counterpart: -`_admin-console-env.tpl`. +unsupported per `.agents/architecture.md` in the monorepo), +`NUXT_PUBLIC_CLIENT_ID` (beta.59+, defaults to the per-realm built-in +`admin-console` client; fork-only override, reachable via +`adminConsole.config`). Chart counterpart: `_admin-console-env.tpl`. ## Operational contract @@ -65,3 +72,14 @@ unsupported per `.agents/architecture.md` in the monorepo). Chart counterpart: ServiceMonitor targets the Service; ingress warning in values/NOTES. - In-process cron sweepers (oauth2-cleaner, event-cleaner) are idempotent deletes; no leader election needed. +- Reserved client names: `admin-console` and `account-console` are provisioned + as built-in system clients in EVERY realm and take over a pre-existing client + of that name (beta.59). The shared per-realm `web` client was removed in the + same release; `TRUSTED_ORIGINS` now feeds the system clients' redirect + allowlists. NOTES warns against declaring either name in + `server.provisioning`. +- beta.60 ships a heavy migration (140 indexes, MySQL `varchar(36)` -> + `varchar(255)` table rewrites, three dropped tables) and beta.62 adds a + unique constraint on `auth_identity_provider_accounts` that ABORTS the boot + on pre-existing duplicates. Both are arguments for + `server.migration.enabled` on an upgrade, not just for multi-replica. diff --git a/.agents/testing.md b/.agents/testing.md index cbe742b..5533e4d 100644 --- a/.agents/testing.md +++ b/.agents/testing.md @@ -46,8 +46,23 @@ helm template t charts/authup --set server.publicUrl=auth.example.com # s helm template t charts/authup --set postgresql.enabled=false --set externalDatabase.host=db # extdb w/o password helm template t charts/authup --set server.ingress.enabled=true # ingress w/o hostname helm template t charts/authup --set server.config.PUBLIC_URL=http://x # first-class collision +helm template t charts/authup --set server.theme.enabled=true # theme with no carrier +helm template t charts/authup --set server.theme.enabled=true --set server.theme.title=X --set server.theme.existingConfigMap=cm # manifest + existing CM +helm template t charts/authup --set server.theme.enabled=true --set server.theme.logo=logo.svg # asset outside assets/ +helm template t charts/authup --set server.theme.enabled=true --set server.theme.logo=assets/logo.svg # asset missing from files +helm template t charts/authup --set server.theme.enabled=true --set 'server.theme.tokens.--authup-bg=url(x)' # token value authup rejects +helm template t charts/authup --set 'server.trustedOrigins[0]=https://**.x' # globstar host ``` +A single `*` host wildcard (`https://*.example.com`) must still RENDER: authup +supports it, only `**` is the allow-any-origin trap. + +The `server.theme` manifest guards assert the value AFTER `tpl` rendering, so +both directions need a case: a templated token or asset path must RENDER, and +one whose rendered result is illegal must FAIL. Validating the raw value gets +this backwards in a way that looks correct (every `{{ ... }}` contains `}`, so +the forbidden-character check rejects it for the wrong reason). + The generated `values.schema.json` must keep catching typos (`--set server.replicaCountt=3` fails) while free-form maps stay open (`--set server.config.X=y`, `--set server.resources.limits.cpu=1` succeed). diff --git a/charts/authup/Chart.yaml b/charts/authup/Chart.yaml index bcc5504..3800f0c 100644 --- a/charts/authup/Chart.yaml +++ b/charts/authup/Chart.yaml @@ -5,7 +5,7 @@ description: Authup is an authentication & authorization system. This chart with optional built-in PostgreSQL, MySQL and Valkey instances. type: application version: 0.2.0 -appVersion: "1.0.0-beta.58" +appVersion: "1.0.0-beta.62" kubeVersion: ">=1.25.0-0" home: https://authup.org icon: https://raw.githubusercontent.com/authup/helm/master/assets/icon.svg @@ -31,7 +31,11 @@ annotations: - name: Source url: https://github.com/authup/helm artifacthub.io/changes: | - - kind: fixed - description: Chart license corrected to Apache-2.0 + - kind: changed + description: appVersion tracks authup 1.0.0-beta.62 - kind: added - description: Chart icon + description: server.theme manifest values compose theme.json (title, logo, tokens) + - kind: added + description: server.features.accountConsole toggles the /account self-service console + - kind: added + description: Trusted origins carrying "**" now fail the render, as authup fails the boot diff --git a/charts/authup/README.md b/charts/authup/README.md index 0280f29..83fd55e 100644 --- a/charts/authup/README.md +++ b/charts/authup/README.md @@ -7,13 +7,14 @@ # authup ![Version](https://img.shields.io/badge/Version-0.2.0?style=flat-square&color=informational) -![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0-beta.58](https://img.shields.io/badge/AppVersion-1.0.0--beta.58-informational?style=flat-square) +![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0-beta.62](https://img.shields.io/badge/AppVersion-1.0.0--beta.62-informational?style=flat-square) Authup is an authentication & authorization system. This chart deploys the server-core IdP/API service and the client-admin-console admin UI, with optional built-in PostgreSQL, MySQL and Valkey instances. It deploys: - **server-core** — the Authup IdP/API service: the OAuth2/OIDC protocol - surface plus the server-rendered auth pages (login, consent, registration, - password recovery). This is the identity origin. + surface, the server-rendered auth pages (login, consent, registration, + password recovery) and the `/account` self-service console + (`server.features.accountConsole`). This is the identity origin. - **client-admin-console** — the Nuxt-based admin UI, an ordinary OAuth2 relying party (optional; disable with `adminConsole.enabled=false` for a headless IdP). - optionally, single-instance **PostgreSQL**, **MySQL** or **Valkey** built-in @@ -88,6 +89,53 @@ Notable operational facts (enforced or warned about by the chart): `server.configuration` file. See the [Authup configuration reference](https://authup.org). +## Theming the served consoles + +Both consoles server-core serves (the auth pages and `/account`) are rebranded +from a directory the chart mounts read-only. Set the manifest as values and the +chart composes `theme.json` for you; `files` carries the assets it references: + +```yaml +server: + theme: + enabled: true + title: Sign in to ACME + logo: assets/logo.svg + stylesheet: assets/theme.css + tokens: + # the accent the whole primary palette is mixed from + --authup-periwinkle: "#c0392b" + --authup-surface-card: "#ffffff" + tokensDark: + --authup-surface-card: "#201e1d" + files: + assets/logo.svg: | + + assets/theme.css: | + .a-auth-shell-card { border: 1px solid var(--authup-surface-border); } +``` + +A colour in `tokens` wins in dark mode too, so surface colours belong in both +maps. The chart rejects at render time what Authup rejects at boot or answers +with a 404: an asset outside `assets/`, an asset no file provides, a token name +that is not a lowercase custom property, and a token value carrying `url(` or +`;`. Only `assets/` is served over HTTP, so `theme.json` is unreachable by +construction. + +`existingConfigMap` replaces the whole mechanism when you need binary assets +(`binaryData`); it is mounted whole, so it must carry `theme.json` itself and +cannot be combined with the manifest values. `fragmentsEnabled` splices +`fragments/head.html` into the console `` verbatim: raw operator markup +on the origin that holds your users' session cookies, hence opt-in. + +> The theme directory is as sensitive as the config file: CSS there can restyle +> or cover the OAuth2 consent buttons. Never source it from somewhere a tenant +> or a lower-privileged CI job can write. + +Theming is experimental upstream: the directory layout and the `theme*` options +may change in an Authup minor release. See the +[Authup theming guide](https://authup.org/guide/deployment/theming.html). + ## GitOps / ArgoCD The generate-once-keep-forever behavior of empty passwords relies on helm's @@ -297,7 +345,7 @@ Kubernetes: `>=1.25.0-0` | server.autoscaling.hpa.targetCPU | int | `75` | Target CPU utilization percentage | | server.autoscaling.hpa.targetMemory | string | `""` | Target memory utilization percentage | | server.command | list | `[]` | Override the container command | -| server.config | object | `{}` | Extra environment variables rendered literally into the env ConfigMap (map of NAME: value) for options without first-class values | +| server.config | object | `{}` | Extra environment variables rendered literally into the env ConfigMap (map of NAME: value) for options without first-class values, e.g. AUTH_CONSOLE_PATH / ACCOUNT_CONSOLE_PATH, which replace a served console with your own build (pair them with extraVolumes; the substituted package owns the login flow, so use server.theme for branding instead) | | server.configuration | string | `""` | Content of an authup.server.core.conf mounted into the working directory for file-only options (middleware objects, per-field SMTP, CORS allowlist). Environment variables always win over file values. | | server.containerSecurityContext | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"enabled":true,"readOnlyRootFilesystem":false,"runAsNonRoot":false,"runAsUser":0,"seccompProfile":{"type":"RuntimeDefault"}}` | Container security context. The upstream image runs as root and needs a writable npm cache; the chart mounts emptyDirs at /usr/src/app/writable and /tmp to keep readOnlyRootFilesystem viable. | | server.customLivenessProbe | object | `{}` | Custom liveness probe | @@ -311,6 +359,7 @@ Kubernetes: `>=1.25.0-0` | server.extraEnvVarsSecret | string | `""` | Extra Secret with environment variables (tpl-rendered name) | | server.extraVolumeMounts | list | `[]` | Extra volume mounts (tpl-rendered) | | server.extraVolumes | list | `[]` | Extra volumes (tpl-rendered) | +| server.features.accountConsole | bool | `true` | Serve the account self-service console at /account (profile, password, authenticators, sessions, applications). ACCOUNT_CONSOLE_ENABLED; disable it when you run your own portal | | server.features.emailVerification | bool | `false` | Enable email verification (EMAIL_VERIFICATION_ENABLED; requires SMTP) | | server.features.passwordRecovery | bool | `false` | Enable password recovery (PASSWORD_RECOVERY_ENABLED; requires SMTP) | | server.features.registration | bool | `false` | Enable self-service user registration (REGISTRATION_ENABLED) | @@ -406,15 +455,22 @@ Kubernetes: `>=1.25.0-0` | server.startupProbe.successThreshold | int | `1` | | | server.startupProbe.timeoutSeconds | int | `5` | | | server.terminationGracePeriodSeconds | int | `30` | Pod termination grace period (server-core tears down within ~10s after signal) | -| server.theme.enabled | bool | `false` | Mount an operator theme for the served consoles (the auth console and the account console). Requires an authup image that supports THEME_DIRECTORY_PATH; older images ignore it | -| server.theme.existingConfigMap | string | `""` | Existing ConfigMap holding the theme (tpl-rendered name). Use for binary assets, which cannot be expressed in files | +| server.theme.enabled | bool | `false` | Mount an operator theme for the served consoles (the auth console and the account console). Requires an authup image that supports THEME_DIRECTORY_PATH; older images ignore it. Experimental upstream: the directory layout and the theme* options may change in a minor release | +| server.theme.existingConfigMap | string | `""` | Existing ConfigMap holding the theme (tpl-rendered name). Use for binary assets, which cannot be expressed in files. Mounted whole, so it must carry theme.json itself and excludes the manifest values above | | server.theme.existingConfigMapItems | list | `[]` | Key -> path projection for existingConfigMap, so its keys can land in subdirectories (e.g. [{key: theme-css, path: assets/theme.css}]). Empty mounts every key flat at the theme root | -| server.theme.files | object | `{}` | Map of path -> file content, relative to the theme root (tpl-rendered). Keys may carry a "/" ("assets/theme.css") and are projected into subdirectories. Only assets/ is served over HTTP. Text only — use existingConfigMap with binaryData for images | +| server.theme.favicon | string | `""` | Favicon path, relative to the theme root and under assets/ (e.g. assets/favicon.svg). Must be a key of files | +| server.theme.files | object | `{}` | Map of path -> file content, relative to the theme root (tpl-rendered). Keys may carry a "/" ("assets/theme.css") and are projected into subdirectories. Only assets/ is served over HTTP. Text only — use existingConfigMap with binaryData for images. Set theme.json here only when writing the manifest by hand instead of using the values above | | server.theme.fragmentsEnabled | bool | `false` | Read fragments/head.html and splice it into the console . Raw, unsanitized markup on the identity provider origin, so it is opt-in | +| server.theme.logo | string | `""` | Logo replacing the built-in mark on both consoles, under assets/. Painted into the existing mark's box, so it needs no sizing | +| server.theme.logoDark | string | `""` | Dark-mode logo variant, under assets/. Without it dark mode reuses logo, which disappears when the mark is drawn dark-on-light | +| server.theme.stylesheet | string | `""` | Stylesheet path, under assets/ and ending in .css. Linked last, so it beats the token block; it is unlayered, so set dark colors explicitly | +| server.theme.title | string | `""` | Document title of both served consoles ("" = authup's own) | +| server.theme.tokens | object | `{}` | authup-periwinkle alone recolors buttons, focus rings and links. A color set here also wins in dark mode: put surface colors in both tokens and tokensDark | +| server.theme.tokensDark | object | `{}` | CSS custom properties applied in dark mode only (tpl-rendered) | | server.tolerations | list | `[]` | Tolerations | | server.topologySpreadConstraints | list | `[]` | Topology spread constraints (a missing labelSelector is filled with the pod's selector labels) | | server.trustProxy | string | `"1"` | TRUST_PROXY setting. The chart defaults to one trusted hop (the ingress), not authup's spoofable trust-everything default | -| server.trustedOrigins | list | `[]` | Additional trusted first-party app origins (TRUSTED_ORIGINS). Each listed origin can obtain full-permission tokens via the per-realm web client. List or comma-separated string; tpl-rendered. | +| server.trustedOrigins | list | `[]` | Additional trusted first-party app origins (TRUSTED_ORIGINS). Each entry is added to the redirect allowlist of the per-realm built-in system clients (admin-console, account-console), so any listed origin can complete a login and obtain a full-permission token. A host may carry a single "*" (https://*.example.com); "**" in a host is rejected by authup at boot. List or comma-separated string; tpl-rendered. | | server.trustedOriginsAppendAdminConsole | bool | `true` | Automatically append the client-admin-console UI origin to TRUSTED_ORIGINS (removes the most common dead-login misconfiguration) | | server.updateStrategy | object | `{"type":"RollingUpdate"}` | Deployment update strategy | | serviceAccount.annotations | object | `{}` | ServiceAccount annotations (tpl-rendered) | diff --git a/charts/authup/README.md.gotmpl b/charts/authup/README.md.gotmpl index fdc2027..12931ad 100644 --- a/charts/authup/README.md.gotmpl +++ b/charts/authup/README.md.gotmpl @@ -25,8 +25,9 @@ {{ template "chart.description" . }} It deploys: - **server-core** — the Authup IdP/API service: the OAuth2/OIDC protocol - surface plus the server-rendered auth pages (login, consent, registration, - password recovery). This is the identity origin. + surface, the server-rendered auth pages (login, consent, registration, + password recovery) and the `/account` self-service console + (`server.features.accountConsole`). This is the identity origin. - **client-admin-console** — the Nuxt-based admin UI, an ordinary OAuth2 relying party (optional; disable with `adminConsole.enabled=false` for a headless IdP). - optionally, single-instance **PostgreSQL**, **MySQL** or **Valkey** built-in @@ -101,6 +102,53 @@ Notable operational facts (enforced or warned about by the chart): `server.configuration` file. See the [Authup configuration reference](https://authup.org). +## Theming the served consoles + +Both consoles server-core serves (the auth pages and `/account`) are rebranded +from a directory the chart mounts read-only. Set the manifest as values and the +chart composes `theme.json` for you; `files` carries the assets it references: + +```yaml +server: + theme: + enabled: true + title: Sign in to ACME + logo: assets/logo.svg + stylesheet: assets/theme.css + tokens: + # the accent the whole primary palette is mixed from + --authup-periwinkle: "#c0392b" + --authup-surface-card: "#ffffff" + tokensDark: + --authup-surface-card: "#201e1d" + files: + assets/logo.svg: | + + assets/theme.css: | + .a-auth-shell-card { border: 1px solid var(--authup-surface-border); } +``` + +A colour in `tokens` wins in dark mode too, so surface colours belong in both +maps. The chart rejects at render time what Authup rejects at boot or answers +with a 404: an asset outside `assets/`, an asset no file provides, a token name +that is not a lowercase custom property, and a token value carrying `url(` or +`;`. Only `assets/` is served over HTTP, so `theme.json` is unreachable by +construction. + +`existingConfigMap` replaces the whole mechanism when you need binary assets +(`binaryData`); it is mounted whole, so it must carry `theme.json` itself and +cannot be combined with the manifest values. `fragmentsEnabled` splices +`fragments/head.html` into the console `` verbatim: raw operator markup +on the origin that holds your users' session cookies, hence opt-in. + +> The theme directory is as sensitive as the config file: CSS there can restyle +> or cover the OAuth2 consent buttons. Never source it from somewhere a tenant +> or a lower-privileged CI job can write. + +Theming is experimental upstream: the directory layout and the `theme*` options +may change in an Authup minor release. See the +[Authup theming guide](https://authup.org/guide/deployment/theming.html). + ## GitOps / ArgoCD The generate-once-keep-forever behavior of empty passwords relies on helm's diff --git a/charts/authup/ci/theme-values.yaml b/charts/authup/ci/theme-values.yaml index e44d962..e28fb13 100644 --- a/charts/authup/ci/theme-values.yaml +++ b/charts/authup/ci/theme-values.yaml @@ -1,24 +1,26 @@ -# Operator theme mounted from an inline files map, covering the path-keyed -# ConfigMap projection (a "/" key must land in a subdirectory) and the env -# wiring. THEME_DIRECTORY_PATH is ignored by an authup image that predates -# console theming, so the rollout assertion holds either way. +# Operator theme covering both carriers in one install: the structured +# manifest values (the chart composes theme.json) and the inline files map +# for the assets they reference, which exercises the path-keyed ConfigMap +# projection (a "/" key must land in a subdirectory) and the env wiring. +# THEME_DIRECTORY_PATH is ignored by an authup image that predates console +# theming, so the rollout assertion holds either way. server: theme: enabled: true fragmentsEnabled: true + # tpl-rendered, so an umbrella chart can inject it (invariant 16). The + # manifest guards assert the RENDERED value, so this also covers the + # assert-after-render path. + title: "Sign in to {{ .Release.Name }}" + logo: assets/logo.svg + stylesheet: assets/theme.css + tokens: + --authup-periwinkle: "#c0392b" + tokensDark: + --authup-auth-accent: "#e06c5a" files: - theme.json: | - { - "version": 1, - "title": "Sign in to ACME", - "stylesheet": "assets/theme.css", - "tokens": { - "--authup-periwinkle": "#c0392b" - }, - "tokensDark": { - "--authup-auth-accent": "#e06c5a" - } - } + assets/logo.svg: | + assets/theme.css: | .a-auth-shell-card { border-radius: 2px; } fragments/head.html: | diff --git a/charts/authup/templates/NOTES.txt b/charts/authup/templates/NOTES.txt index 6e4a8e3..b6c6d42 100644 --- a/charts/authup/templates/NOTES.txt +++ b/charts/authup/templates/NOTES.txt @@ -9,6 +9,9 @@ server-core (IdP / API): {{- if $serverUrl }} Public URL / OIDC issuer: {{ $serverUrl }} OIDC discovery: {{ $serverUrl }}/.well-known/openid-configuration +{{- if .Values.server.features.accountConsole }} + Account self-service: {{ $serverUrl }}/account +{{- end }} {{- else }} In-cluster URL: http://{{ include "authup.server.fullname" . }}.{{ include "authup.namespace" . }}.svc:{{ .Values.server.service.ports.http }} (port-forward: kubectl -n {{ include "authup.namespace" . }} port-forward svc/{{ include "authup.server.fullname" . }} 3001:{{ .Values.server.service.ports.http }}) @@ -81,6 +84,13 @@ schema migrations are serialized in a pre-upgrade Job instead of racing at pod boot. {{- end }} +{{- if .Values.server.provisioning.enabled }} + +NOTE: "admin-console" and "account-console" are reserved client names: authup +provisions them as built-in system clients in every realm and overwrites a +client of either name. Do not declare them in server.provisioning. +{{- end }} + {{- if or .Values.postgresql.enabled .Values.mysql.enabled }} The built-in {{ ternary "PostgreSQL" "MySQL" .Values.postgresql.enabled }} instance is a single-node convenience for dev and diff --git a/charts/authup/templates/_server-env.tpl b/charts/authup/templates/_server-env.tpl index d5e71db..38ce47c 100644 --- a/charts/authup/templates/_server-env.tpl +++ b/charts/authup/templates/_server-env.tpl @@ -26,6 +26,7 @@ TRUST_PROXY: {{ .Values.server.trustProxy | toString | quote }} REGISTRATION_ENABLED: {{ .Values.server.features.registration | toString | quote }} PASSWORD_RECOVERY_ENABLED: {{ .Values.server.features.passwordRecovery | toString | quote }} EMAIL_VERIFICATION_ENABLED: {{ .Values.server.features.emailVerification | toString | quote }} +ACCOUNT_CONSOLE_ENABLED: {{ .Values.server.features.accountConsole | toString | quote }} MFA_ENABLED: {{ .Values.server.mfa.enabled | toString | quote }} MFA_REQUIRED: {{ .Values.server.mfa.required | toString | quote }} {{- if .Values.auth.adminPasswordReset }} @@ -37,7 +38,7 @@ CLIENT_SYSTEM_ENABLED: "true" CLIENT_SYSTEM_SECRET_RESET: "true" {{- end }} {{- end }} -{{- $reserved := list "DB_TYPE" "DB_HOST" "DB_PORT" "DB_USERNAME" "DB_DATABASE" "DB_PASSWORD" "PUBLIC_URL" "TRUSTED_ORIGINS" "TRUST_PROXY" "REGISTRATION_ENABLED" "PASSWORD_RECOVERY_ENABLED" "EMAIL_VERIFICATION_ENABLED" "MFA_ENABLED" "MFA_REQUIRED" "THEME_DIRECTORY_PATH" "THEME_FRAGMENTS_ENABLED" "USER_ADMIN_PASSWORD" "USER_ADMIN_PASSWORD_RESET" "CLIENT_SYSTEM_ENABLED" "CLIENT_SYSTEM_SECRET" "CLIENT_SYSTEM_SECRET_RESET" "REDIS" "SMTP" "SECRETS_ENCRYPTION_KEY" }} +{{- $reserved := list "DB_TYPE" "DB_HOST" "DB_PORT" "DB_USERNAME" "DB_DATABASE" "DB_PASSWORD" "PUBLIC_URL" "TRUSTED_ORIGINS" "TRUST_PROXY" "REGISTRATION_ENABLED" "PASSWORD_RECOVERY_ENABLED" "EMAIL_VERIFICATION_ENABLED" "ACCOUNT_CONSOLE_ENABLED" "MFA_ENABLED" "MFA_REQUIRED" "THEME_DIRECTORY_PATH" "THEME_FRAGMENTS_ENABLED" "USER_ADMIN_PASSWORD" "USER_ADMIN_PASSWORD_RESET" "CLIENT_SYSTEM_ENABLED" "CLIENT_SYSTEM_SECRET" "CLIENT_SYSTEM_SECRET_RESET" "REDIS" "SMTP" "SECRETS_ENCRYPTION_KEY" }} {{- range $key, $value := .Values.server.config }} {{- if has $key $reserved }} {{- fail (printf "authup: server.config.%s collides with a first-class chart value — set it through the dedicated value instead." $key) }} @@ -186,7 +187,7 @@ THEME_FRAGMENTS_ENABLED: {{ .Values.server.theme.fragmentsEnabled | toString | q {{- end }} {{- else }} items: - {{- range $path, $content := .Values.server.theme.files }} + {{- range $path := splitList "\n" (include "authup.server.themePaths" $) }} - key: {{ include "authup.server.themeConfigMapKey" $path }} path: {{ $path }} {{- end }} @@ -223,24 +224,152 @@ operator. True when a theme should be mounted at all. */}} {{- define "authup.server.themeMounted" -}} -{{- if and .Values.server.theme.enabled (or .Values.server.theme.files .Values.server.theme.existingConfigMap) -}} +{{- if and .Values.server.theme.enabled (or .Values.server.theme.files .Values.server.theme.existingConfigMap (include "authup.server.themeManifestConfigured" .)) -}} true {{- end -}} {{- end -}} +{{/* +True when any structured manifest value is set, i.e. when the chart owns +theme.json instead of the operator writing it into `files` by hand. +*/}} +{{- define "authup.server.themeManifestConfigured" -}} +{{- $t := .Values.server.theme -}} +{{- if or $t.title $t.favicon $t.logo $t.logoDark $t.stylesheet $t.tokens $t.tokensDark -}} +true +{{- end -}} +{{- end -}} + +{{/* +theme.json composed from the structured manifest values. Only keys the +operator actually set are emitted: authup fails the boot on an UNKNOWN +manifest key, and would also read an empty string as a real (broken) asset +reference, so a "" must never reach the file. + +`version` is authup's manifest version, not the chart's: it is how a future +per-realm layout change becomes detectable instead of silent. +*/}} +{{- define "authup.server.themeManifest" -}} +{{- $ctx := . -}} +{{- $t := .Values.server.theme -}} +{{- $manifest := dict "version" 1 -}} +{{- range $key := list "title" "favicon" "logo" "logoDark" "stylesheet" -}} +{{- $value := get $t $key -}} +{{- if $value -}} +{{- $_ := set $manifest $key (include "authup.tplvalues.render" (dict "value" $value "context" $ctx)) -}} +{{- end -}} +{{- end -}} +{{- range $key := list "tokens" "tokensDark" -}} +{{- $source := get $t $key -}} +{{- if $source -}} +{{- $rendered := dict -}} +{{- range $name, $value := $source -}} +{{- $_ := set $rendered $name (include "authup.tplvalues.render" (dict "value" ($value | toString) "context" $ctx)) -}} +{{- end -}} +{{- $_ := set $manifest $key $rendered -}} +{{- end -}} +{{- end -}} +{{- toPrettyJson $manifest -}} +{{- end -}} + +{{/* +Every path the chart-managed theme ConfigMap carries, newline separated. +Single source for the ConfigMap keys AND the volume's items projection, so +the two cannot diverge into a key that is stored but never mounted. +*/}} +{{- define "authup.server.themePaths" -}} +{{- $paths := list -}} +{{- if include "authup.server.themeManifestConfigured" . -}} +{{- $paths = append $paths "theme.json" -}} +{{- end -}} +{{- range $path, $content := .Values.server.theme.files -}} +{{- $paths = append $paths $path -}} +{{- end -}} +{{- $paths | join "\n" -}} +{{- end -}} + {{/* Render-time validation. The chart fails loud rather than shipping a silently-inert theme: the dominant failure mode of theming is a page that looks exactly like an un-themed page. */}} {{- define "authup.server.validateTheme" -}} +{{- $ctx := . }} +{{- $theme := .Values.server.theme }} +{{- $manifest := include "authup.server.themeManifestConfigured" . }} {{- if .Values.server.theme.enabled }} -{{- if not (or .Values.server.theme.files .Values.server.theme.existingConfigMap) }} -{{- fail "authup: server.theme.enabled requires server.theme.files or server.theme.existingConfigMap — an empty theme directory would render an un-themed page with no error." }} +{{- if not (or $theme.files $theme.existingConfigMap $manifest) }} +{{- fail "authup: server.theme.enabled requires server.theme.files, server.theme.existingConfigMap or the structured manifest values (title / logo / tokens / ...). An empty theme directory would render an un-themed page with no error." }} {{- end }} -{{- if and .Values.server.theme.files .Values.server.theme.existingConfigMap }} +{{- if and $theme.files $theme.existingConfigMap }} {{- fail "authup: set either server.theme.files or server.theme.existingConfigMap, not both — the existing ConfigMap would win and the inline files would be silently ignored." }} {{- end }} +{{- if and $manifest $theme.existingConfigMap }} +{{- fail "authup: the structured server.theme manifest values cannot be combined with server.theme.existingConfigMap: the ConfigMap is mounted whole, so the chart-composed theme.json would never reach the pod. Put theme.json into that ConfigMap instead." }} +{{- end }} +{{- if and $manifest (hasKey $theme.files "theme.json") }} +{{- fail "authup: server.theme.files has a \"theme.json\" key while the structured manifest values are also set. The chart composes theme.json from those values, so one of the two would be silently dropped. Use one or the other." }} +{{- end }} +{{- /* An asset reference the theme does not carry is the exact failure the + chart exists to catch: authup answers 404 and the page renders + un-themed, which looks identical to theming being off. Only assets/ + is served over HTTP (the theme root is deliberately unreachable, so + theme.json cannot be fetched), hence the prefix requirement. */}} +{{- $imageExtensions := list ".svg" ".png" ".jpg" ".jpeg" ".gif" ".webp" ".avif" ".ico" }} +{{- range $key := list "favicon" "logo" "logoDark" }} +{{- $raw := get $theme $key }} +{{- if $raw }} +{{- /* Assert the RENDERED value: themeManifest tpl-renders it, so a + template-valued path only materializes here (invariant 7). Validating + the raw string would reject every umbrella-injected expression, since + none of them start with "assets/". */}} +{{- $value := include "authup.tplvalues.render" (dict "value" $raw "context" $ctx) }} +{{- if not (hasPrefix "assets/" $value) }} +{{- fail (printf "authup: server.theme.%s must reference a file under \"assets/\" (got %q). assets/ is the only directory authup serves over HTTP." $key $value) }} +{{- end }} +{{- $matched := false }} +{{- range $extension := $imageExtensions }} +{{- if hasSuffix $extension (lower $value) }}{{- $matched = true }}{{- end }} +{{- end }} +{{- if not $matched }} +{{- fail (printf "authup: server.theme.%s must be an image (%s), got %q. authup returns 404 for any other type." $key (join ", " $imageExtensions) $value) }} +{{- end }} +{{- if not (hasKey ($theme.files | default dict) $value) }} +{{- fail (printf "authup: server.theme.%s references %q, which is not a key of server.theme.files. The asset would 404 and the console would render un-themed." $key $value) }} +{{- end }} +{{- end }} +{{- end }} +{{- if $theme.stylesheet }} +{{- $stylesheet := include "authup.tplvalues.render" (dict "value" $theme.stylesheet "context" $ctx) }} +{{- if not (hasPrefix "assets/" $stylesheet) }} +{{- fail (printf "authup: server.theme.stylesheet must reference a file under \"assets/\" (got %q). assets/ is the only directory authup serves over HTTP." $stylesheet) }} +{{- end }} +{{- if not (hasSuffix ".css" (lower $stylesheet)) }} +{{- fail (printf "authup: server.theme.stylesheet must be a .css file (got %q)." $stylesheet) }} +{{- end }} +{{- if not (hasKey ($theme.files | default dict) $stylesheet) }} +{{- fail (printf "authup: server.theme.stylesheet references %q, which is not a key of server.theme.files. The stylesheet would 404 and the console would render un-themed." $stylesheet) }} +{{- end }} +{{- end }} +{{- /* Mirrors authup's own manifest validation. It fails the BOOT on a bad + token, so catching it at render time turns a crash-looping IdP into a + failed `helm upgrade`. */}} +{{- range $key := list "tokens" "tokensDark" }} +{{- range $name, $value := (get $theme $key) }} +{{- if not (regexMatch "^--[a-z0-9-]+$" $name) }} +{{- fail (printf "authup: server.theme.%s key %q must be a lowercase CSS custom property (--foo-bar)." $key $name) }} +{{- end }} +{{- $rendered := include "authup.tplvalues.render" (dict "value" ($value | toString) "context" $ctx) }} +{{- if gt (len $rendered) 256 }} +{{- fail (printf "authup: server.theme.%s.%s exceeds the 256 character limit authup enforces on a token value." $key $name) }} +{{- end }} +{{- range $forbidden := list "}" "<" ">" ";" "@" "\\" "/*" "url(" "expression(" }} +{{- if contains $forbidden $rendered }} +{{- fail (printf "authup: server.theme.%s.%s contains %q, which authup rejects in a token value. Use server.theme.stylesheet for anything needing url() or multiple declarations." $key $name $forbidden) }} +{{- end }} +{{- end }} +{{- end }} +{{- end }} {{- range $path, $content := .Values.server.theme.files }} {{- if hasPrefix "/" $path }} {{- fail (printf "authup: server.theme.files key %q must be relative to the theme root." $path) }} diff --git a/charts/authup/templates/_urls.tpl b/charts/authup/templates/_urls.tpl index 633ad92..15f0c8e 100644 --- a/charts/authup/templates/_urls.tpl +++ b/charts/authup/templates/_urls.tpl @@ -89,5 +89,23 @@ disabled or already covered by the server public URL's origin. {{- $origins = append $origins $uiOrigin -}} {{- end -}} {{- end -}} -{{- $origins | uniq | join "," -}} +{{- $origins = $origins | uniq -}} +{{- range $origins -}} +{{- include "authup.assertTrustedOrigin" . -}} +{{- end -}} +{{- $origins | join "," -}} +{{- end -}} + +{{/* +Post-render origin assertion. A trusted origin becomes an `/**` +redirect pattern on every realm's built-in system clients, and `**` in the +host matches the rest of the value outright, so one typo turns that +allowlist into allow-any-origin. authup rejects it at boot (beta.59+); +asserting here turns a crash-looping IdP into a failed render. A single `*` +is a supported host wildcard and stays allowed. +*/}} +{{- define "authup.assertTrustedOrigin" -}} +{{- if contains "**" . -}} +{{- fail (printf "authup: a trusted origin must not use \"**\" in the host, it would match every origin. Use a single \"*\" for a host wildcard (got %q)." .) -}} +{{- end -}} {{- end -}} diff --git a/charts/authup/templates/server/configmap-theme.yaml b/charts/authup/templates/server/configmap-theme.yaml index 59d7169..5d5e79c 100644 --- a/charts/authup/templates/server/configmap-theme.yaml +++ b/charts/authup/templates/server/configmap-theme.yaml @@ -1,4 +1,4 @@ -{{- if and .Values.server.enabled .Values.server.theme.enabled .Values.server.theme.files (not .Values.server.theme.existingConfigMap) }} +{{- if and .Values.server.enabled .Values.server.theme.enabled (not .Values.server.theme.existingConfigMap) (or .Values.server.theme.files (include "authup.server.themeManifestConfigured" .)) }} apiVersion: v1 kind: ConfigMap metadata: @@ -11,6 +11,10 @@ data: subdirectories. Keys are flattened here and projected back to their real path by the volume's items list, so the operator writes "assets/theme.css" and never sees the encoding. */}} + {{- if include "authup.server.themeManifestConfigured" . }} + theme.json: |- + {{- include "authup.server.themeManifest" . | nindent 4 }} + {{- end }} {{- range $path, $content := .Values.server.theme.files }} {{ include "authup.server.themeConfigMapKey" $path }}: |- {{- include "authup.tplvalues.render" (dict "value" $content "context" $) | nindent 4 }} diff --git a/charts/authup/templates/server/deployment.yaml b/charts/authup/templates/server/deployment.yaml index 684b648..ee53043 100644 --- a/charts/authup/templates/server/deployment.yaml +++ b/charts/authup/templates/server/deployment.yaml @@ -44,7 +44,7 @@ spec: {{- if and .Values.server.provisioning.enabled .Values.server.provisioning.files }} checksum/provisioning: {{ include (print $.Template.BasePath "/server/configmap-provisioning.yaml") . | sha256sum }} {{- end }} - {{- if and .Values.server.theme.enabled .Values.server.theme.files }} + {{- if and .Values.server.theme.enabled (or .Values.server.theme.files (include "authup.server.themeManifestConfigured" .)) }} checksum/theme: {{ include (print $.Template.BasePath "/server/configmap-theme.yaml") . | sha256sum }} {{- end }} {{- if .Values.server.configuration }} diff --git a/charts/authup/values.schema.json b/charts/authup/values.schema.json index 2143ba7..a6c28cc 100644 --- a/charts/authup/values.schema.json +++ b/charts/authup/values.schema.json @@ -2200,7 +2200,7 @@ }, "config": { "additionalProperties": true, - "description": "Extra environment variables rendered literally into the env ConfigMap\n(map of NAME: value) for options without first-class values", + "description": "Extra environment variables rendered literally into the env ConfigMap\n(map of NAME: value) for options without first-class values, e.g.\nAUTH_CONSOLE_PATH / ACCOUNT_CONSOLE_PATH, which replace a served console\nwith your own build (pair them with extraVolumes; the substituted package\nowns the login flow, so use server.theme for branding instead)", "required": [], "title": "config" }, @@ -2380,6 +2380,13 @@ "features": { "additionalProperties": false, "properties": { + "accountConsole": { + "default": true, + "description": "Serve the account self-service console at \u003cpublicUrl\u003e/account\n(profile, password, authenticators, sessions, applications).\nACCOUNT_CONSOLE_ENABLED; disable it when you run your own portal", + "required": [], + "title": "accountConsole", + "type": "boolean" + }, "emailVerification": { "default": false, "description": "Enable email verification (EMAIL_VERIFICATION_ENABLED; requires SMTP)", @@ -2405,7 +2412,8 @@ "required": [ "registration", "passwordRecovery", - "emailVerification" + "emailVerification", + "accountConsole" ], "title": "features", "type": "object" @@ -3311,14 +3319,14 @@ "properties": { "enabled": { "default": false, - "description": "Mount an operator theme for the served consoles (the auth console and\nthe account console). Requires an authup image that supports\nTHEME_DIRECTORY_PATH; older images ignore it", + "description": "Mount an operator theme for the served consoles (the auth console and\nthe account console). Requires an authup image that supports\nTHEME_DIRECTORY_PATH; older images ignore it. Experimental upstream: the\ndirectory layout and the theme* options may change in a minor release", "required": [], "title": "enabled", "type": "boolean" }, "existingConfigMap": { "default": "", - "description": "Existing ConfigMap holding the theme (tpl-rendered name). Use for\nbinary assets, which cannot be expressed in files", + "description": "Existing ConfigMap holding the theme (tpl-rendered name). Use for\nbinary assets, which cannot be expressed in files. Mounted whole, so it\nmust carry theme.json itself and excludes the manifest values above", "required": [], "title": "existingConfigMap", "type": "string" @@ -3332,9 +3340,16 @@ "title": "existingConfigMapItems", "type": "array" }, + "favicon": { + "default": "", + "description": "Favicon path, relative to the theme root and under assets/\n(e.g. assets/favicon.svg). Must be a key of files", + "required": [], + "title": "favicon", + "type": "string" + }, "files": { "additionalProperties": true, - "description": "Map of path -\u003e file content, relative to the theme root (tpl-rendered).\nKeys may carry a \"/\" (\"assets/theme.css\") and are projected into\nsubdirectories. Only assets/ is served over HTTP. Text only — use\nexistingConfigMap with binaryData for images", + "description": "Map of path -\u003e file content, relative to the theme root (tpl-rendered).\nKeys may carry a \"/\" (\"assets/theme.css\") and are projected into\nsubdirectories. Only assets/ is served over HTTP. Text only — use\nexistingConfigMap with binaryData for images. Set theme.json here only\nwhen writing the manifest by hand instead of using the values above", "required": [], "title": "files" }, @@ -3344,10 +3359,55 @@ "required": [], "title": "fragmentsEnabled", "type": "boolean" + }, + "logo": { + "default": "", + "description": "Logo replacing the built-in mark on both consoles, under assets/.\nPainted into the existing mark's box, so it needs no sizing", + "required": [], + "title": "logo", + "type": "string" + }, + "logoDark": { + "default": "", + "description": "Dark-mode logo variant, under assets/. Without it dark mode reuses\nlogo, which disappears when the mark is drawn dark-on-light", + "required": [], + "title": "logoDark", + "type": "string" + }, + "stylesheet": { + "default": "", + "description": "Stylesheet path, under assets/ and ending in .css. Linked last, so it\nbeats the token block; it is unlayered, so set dark colors explicitly", + "required": [], + "title": "stylesheet", + "type": "string" + }, + "title": { + "default": "", + "description": "Document title of both served consoles (\"\" = authup's own)", + "required": [], + "title": "title", + "type": "string" + }, + "tokens": { + "additionalProperties": true, + "description": "CSS custom properties applied in both color modes (tpl-rendered).\nauthup-periwinkle alone recolors buttons, focus rings and links.\nA color set here also wins in dark mode: put surface colors in both\ntokens and tokensDark", + "required": [], + "title": "tokens" + }, + "tokensDark": { + "additionalProperties": true, + "description": "CSS custom properties applied in dark mode only (tpl-rendered)", + "required": [], + "title": "tokensDark" } }, "required": [ "enabled", + "title", + "favicon", + "logo", + "logoDark", + "stylesheet", "existingConfigMap", "existingConfigMapItems", "fragmentsEnabled" @@ -3381,7 +3441,7 @@ "type": "string" }, "trustedOrigins": { - "description": "Additional trusted first-party app origins (TRUSTED_ORIGINS). Each listed\norigin can obtain full-permission tokens via the per-realm web client.\nList or comma-separated string; tpl-rendered.", + "description": "Additional trusted first-party app origins (TRUSTED_ORIGINS). Each entry\nis added to the redirect allowlist of the per-realm built-in system clients\n(admin-console, account-console), so any listed origin can complete a login\nand obtain a full-permission token. A host may carry a single \"*\"\n(https://*.example.com); \"**\" in a host is rejected by authup at boot.\nList or comma-separated string; tpl-rendered.", "items": { "required": [] }, diff --git a/charts/authup/values.yaml b/charts/authup/values.yaml index 5917134..677b5e4 100644 --- a/charts/authup/values.yaml +++ b/charts/authup/values.yaml @@ -402,8 +402,11 @@ server: # "" = derived from server.ingress when enabled. Changing it later breaks # enrolled WebAuthn credentials and the OIDC issuer. publicUrl: "" - # -- Additional trusted first-party app origins (TRUSTED_ORIGINS). Each listed - # origin can obtain full-permission tokens via the per-realm web client. + # -- Additional trusted first-party app origins (TRUSTED_ORIGINS). Each entry + # is added to the redirect allowlist of the per-realm built-in system clients + # (admin-console, account-console), so any listed origin can complete a login + # and obtain a full-permission token. A host may carry a single "*" + # (https://*.example.com); "**" in a host is rejected by authup at boot. # List or comma-separated string; tpl-rendered. trustedOrigins: [] # -- Automatically append the client-admin-console UI origin to TRUSTED_ORIGINS (removes the @@ -419,6 +422,10 @@ server: passwordRecovery: false # -- Enable email verification (EMAIL_VERIFICATION_ENABLED; requires SMTP) emailVerification: false + # -- Serve the account self-service console at /account + # (profile, password, authenticators, sessions, applications). + # ACCOUNT_CONSOLE_ENABLED; disable it when you run your own portal + accountConsole: true mfa: # -- Enable multi-factor authentication (MFA_ENABLED) enabled: false @@ -428,7 +435,10 @@ server: # additionalProperties: true # @schema # -- Extra environment variables rendered literally into the env ConfigMap - # (map of NAME: value) for options without first-class values + # (map of NAME: value) for options without first-class values, e.g. + # AUTH_CONSOLE_PATH / ACCOUNT_CONSOLE_PATH, which replace a served console + # with your own build (pair them with extraVolumes; the substituted package + # owns the login flow, so use server.theme for branding instead) config: {} # -- Content of an authup.server.core.conf mounted into the working directory # for file-only options (middleware objects, per-field SMTP, CORS allowlist). @@ -454,18 +464,48 @@ server: theme: # -- Mount an operator theme for the served consoles (the auth console and # the account console). Requires an authup image that supports - # THEME_DIRECTORY_PATH; older images ignore it + # THEME_DIRECTORY_PATH; older images ignore it. Experimental upstream: the + # directory layout and the theme* options may change in a minor release enabled: false + # -- Document title of both served consoles ("" = authup's own) + title: "" + # -- Favicon path, relative to the theme root and under assets/ + # (e.g. assets/favicon.svg). Must be a key of files + favicon: "" + # -- Logo replacing the built-in mark on both consoles, under assets/. + # Painted into the existing mark's box, so it needs no sizing + logo: "" + # -- Dark-mode logo variant, under assets/. Without it dark mode reuses + # logo, which disappears when the mark is drawn dark-on-light + logoDark: "" + # -- Stylesheet path, under assets/ and ending in .css. Linked last, so it + # beats the token block; it is unlayered, so set dark colors explicitly + stylesheet: "" + # @schema + # additionalProperties: true + # @schema + # -- CSS custom properties applied in both color modes (tpl-rendered). + # --authup-periwinkle alone recolors buttons, focus rings and links. + # A color set here also wins in dark mode: put surface colors in both + # tokens and tokensDark + tokens: {} + # @schema + # additionalProperties: true + # @schema + # -- CSS custom properties applied in dark mode only (tpl-rendered) + tokensDark: {} # @schema # additionalProperties: true # @schema # -- Map of path -> file content, relative to the theme root (tpl-rendered). # Keys may carry a "/" ("assets/theme.css") and are projected into # subdirectories. Only assets/ is served over HTTP. Text only — use - # existingConfigMap with binaryData for images + # existingConfigMap with binaryData for images. Set theme.json here only + # when writing the manifest by hand instead of using the values above files: {} # -- Existing ConfigMap holding the theme (tpl-rendered name). Use for - # binary assets, which cannot be expressed in files + # binary assets, which cannot be expressed in files. Mounted whole, so it + # must carry theme.json itself and excludes the manifest values above existingConfigMap: "" # -- Key -> path projection for existingConfigMap, so its keys can land in # subdirectories (e.g. [{key: theme-css, path: assets/theme.css}]).