diff --git a/docs/plans/next-gen/components/api.md b/docs/plans/next-gen/components/api.md index 12f95f9..85f6814 100644 --- a/docs/plans/next-gen/components/api.md +++ b/docs/plans/next-gen/components/api.md @@ -212,7 +212,7 @@ Browser ──EventSource('/api/v1/events/stream')──→ Web tier The web tier already proxies all `/api/v1/*` calls — SSE is just a long-lived, streaming variant of the same proxy pattern. The implementation requirement: **the proxy must pipe chunks as they arrive, not buffer the response.** In Fastify, this means writing to `reply.raw` (the underlying `ServerResponse`) and calling `reply.raw.flushHeaders()` before the first chunk, or using `@fastify/http-proxy` which handles streaming correctly out of the box. -**Single-process alternative:** if the deployment runs web + API as one Fastify process (the simpler model for small/community deployments), no proxy is needed. The `AuthMiddleware` resolves the Principal directly from the cookie (same `OIDC_SESSION_SECRET`, same JWS verification the web tier uses). This adds a fourth resolution path to the middleware (after JWT-header, API-key, anonymous): cookie → verify JWS → resolve Principal. The auth boundary is unchanged — the middleware is still the single resolution point; the Principal is still the single authz artifact. The JWT becomes optional in this mode (the cookie is the credential, verified inline rather than transmitted over a network). +**Single-process alternative:** if the deployment runs web + API as one Fastify process (the simpler model for small/community deployments), no proxy is needed. The `AuthMiddleware` resolves the Principal directly from the cookie (same `JWT_SESSION_SECRET`, same JWS verification the web tier uses). This adds a fourth resolution path to the middleware (after JWT-header, API-key, anonymous): cookie → verify JWS → resolve Principal. The auth boundary is unchanged — the middleware is still the single resolution point; the Principal is still the single authz artifact. The JWT becomes optional in this mode (the cookie is the credential, verified inline rather than transmitted over a network). ### SSE realtime endpoint (concrete) @@ -268,7 +268,7 @@ Not all config is the same. Split by *when it can change*: ``` DATABASE_URL, NATS_URL, MQTT_HOST/PORT/..., REDIS_HOST/..., OIDC_CLIENT_ID/SECRET/DISCOVERY, -OIDC_SESSION_SECRET, API_HOST/PORT, LOG_LEVEL, INSTANCE_NAME +JWT_SESSION_SECRET, API_HOST/PORT, LOG_LEVEL, INSTANCE_NAME ``` Rule of thumb: if changing it requires reconnecting to an external system or re-authenticating, it's Tier 1. Secrets stay here (or in a secret manager) — they shouldn't sit in a DB backup. diff --git a/docs/plans/next-gen/components/auth.md b/docs/plans/next-gen/components/auth.md index 8a5099a..0114900 100644 --- a/docs/plans/next-gen/components/auth.md +++ b/docs/plans/next-gen/components/auth.md @@ -1,6 +1,6 @@ # Auth -> **Related decisions:** D6 (auth boundary — JWT issued by the web tier, verified at API middleware; `X-User-*` header injection removed), D12 (multi-source auth — OIDC optional, built-in local password store always available, `AUTH_MODE=local|oidc|hybrid` default **hybrid**, all sources converge on one JWT issuance), D18 (CLI for ops, not config — one config surface per item). +> **Related decisions:** D6 (auth boundary — JWT issued by the web tier, verified at API middleware; `X-User-*` header injection removed), D12 (multi-source auth — OIDC optional, built-in local password store always available, `AUTH_MODE=local|oidc|hybrid` default **hybrid**, all sources converge on one JWT issuance; per-tenant `auth_mode`/IdP in multi-tenant mode — multi-tenancy.md §6), D18 (CLI for ops, not config — one config surface per item). > > **Note:** Code examples are illustrative pseudocode showing design patterns (shapes, flows, contracts). > The implementation uses the TypeScript stack (D22): Fastify middleware, jose (JWT/JWS), argon2, Zod. @@ -24,7 +24,7 @@ flowchart LR The defining property: **the API is credential-source-agnostic.** It verifies a JWT and resolves a `Principal`; it never knows *how* the web tier authenticated the user. That abstraction is what lets us offer multiple login methods without the API caring: -- **Web tier** mints a short-lived JWT (signed `HS256`/`RS256` with `OIDC_SESSION_SECRET`) carrying `sub`, `roles`, `instance_id`, `exp`, stored in the existing signed cookie. Every login path converges here. +- **Web tier** mints a short-lived JWT (signed `HS256`/`RS256` with `JWT_SESSION_SECRET`) carrying `sub`, `roles`, `instance_id`, `exp`, stored in the existing signed cookie. Every login path converges here. - **API** verifies the JWT at a single middleware; handlers receive a resolved `Principal` (with `role_tier`, `user_id`, `instance_id`) via `Depends`. No more `X-User-*` header injection — the JWT *is* the credential. Fixes S1, S2. - **Direct Bearer (API keys)** remain for m2m/CLI; they map to a `Principal` with a fixed role. - **Channel visibility** (redaction, below) is computed from the `Principal`, once, per request. @@ -39,7 +39,7 @@ Three credential sources, each optional, all producing the same JWT: | **OIDC/OAuth2** *(optional — configure if you have an IdP)* | Org/multi-user deployments wanting SSO | Existing redirect/callback flow; roles from the IdP claim | | **API keys** *(always — for automation)* | CLI, scripts, other services talking to the API directly | Bearer token verified at the API middleware (no web tier involved) | -`AUTH_MODE` (Tier-1 env var) selects which interactive sources the login page offers: +`AUTH_MODE` selects which interactive sources the login page offers. It is a Tier-1 env var in single-tenant mode; in multi-tenant mode it is per-tenant (`tenant_oidc_configs.auth_mode`) with the env var as the platform default (multi-tenancy.md §6): - `local` — username/password form only. Zero external dependencies. - `oidc` — "Sign in with SSO" button only. For orgs that mandate the IdP. @@ -91,13 +91,13 @@ local password verified ┘ → sets meshcore-session cookie - **Local users** are managed via a Users admin page (fits naturally in the Settings UI / D11). Admins create users, assign roles, reset passwords, disable accounts — all runtime, no env-var changes. - **OIDC users** are still provisioned just-in-time on first login (as today), with roles from the IdP claim; an admin can promote/demote via the same Users page. -- The `AUTH_MODE` setting itself stays Tier-1 (env var) because it affects which bootstrap credentials are required. +- The `AUTH_MODE` env var is the platform default (Tier-1, because it gates bootstrap). In multi-tenant mode each tenant overrides it via `tenant_oidc_configs.auth_mode` — a runtime DB setting requiring no restart (multi-tenancy.md §6). ## JWT token shape & session model Two artifacts: a **session cookie** (long-lived, the "refresh") and an **access JWT** (short-lived, per-request credential). The web tier mints a fresh access JWT from the session on each proxied request; the API only ever sees non-expired access JWTs. No separate refresh token reaches the API. -**Access JWT claims (HS256, signed with `OIDC_SESSION_SECRET`):** +**Access JWT claims (HS256, signed with `JWT_SESSION_SECRET`):** ```json { "iss": "meshcore-hub", @@ -113,7 +113,7 @@ Two artifacts: a **session cookie** (long-lived, the "refresh") and an **access ``` - **5-minute access lifetime** — short enough that a stolen token has a tiny window, long enough that the web tier's per-request re-mint doesn't fight itself under burst load. -- **7-day session cookie** (`meshcore-session`, HttpOnly, SameSite=Lax, signed with `OIDC_SESSION_SECRET` via `jose` JWS — same mechanism, updated library). Sliding renewal: each proxied request reissues if past half-life. +- **7-day session cookie** (`meshcore-session`, HttpOnly, SameSite=Lax, signed with `JWT_SESSION_SECRET` via `jose` JWS — same mechanism, updated library). Sliding renewal: each proxied request reissues if past half-life. - **RS256 option:** if an operator wants asymmetric signing (web tier holds private key, API verifies with public key), expose via `JWT_SIGNING_ALG=rs256` + `JWT_PRIVATE_KEY`/`JWT_PUBLIC_KEY` PEM paths. Default HS256 — single issuer, simpler ops. ## The Principal (resolved once per request) @@ -165,7 +165,7 @@ async function resolve(request: FastifyRequest): Promise { } // 2. Session cookie (browser access — single-process mode, or SSE via proxy) // Only active when this process also serves the web tier. The cookie is - // a JWS signed with OIDC_SESSION_SECRET; verify inline → resolve Principal. + // a JWS signed with JWT_SESSION_SECRET; verify inline → resolve Principal. const cookie = request.cookies?.["meshcore-session"]; if (cookie && this.cookieVerifier) { const session = this.cookieVerifier.verify(cookie); // throws on tamper/expiry → 401 @@ -218,7 +218,7 @@ fastify.addHook("preHandler", async (request: FastifyRequest, reply: FastifyRepl 1. **Welcome / network identity** — network name, city, country, contact (writes the Tier-2 branding settings that are otherwise empty on a fresh DB). 2. **Admin account** — username + password + confirm. Creates the first admin (see bootstrap insert sequence below). -3. **Auth mode** — local (default) / oidc (enter IdP config) / hybrid. Sets the Tier-1 `AUTH_MODE`-equivalent as a setting. +3. **Auth mode** — local (default) / oidc (enter IdP config) / hybrid. Written as a per-instance setting (`tenant_oidc_configs.auth_mode` in multi-tenant mode, seeded from the `AUTH_MODE` platform default). 4. **Feature flags** — which pages to enable (sensible defaults pre-checked). 5. **Done** — sets `fastify.state.needsSetup = false`, redirects to the dashboard, logged in as the new admin. diff --git a/docs/plans/next-gen/components/infrastructure.md b/docs/plans/next-gen/components/infrastructure.md index 04d6411..33fd575 100644 --- a/docs/plans/next-gen/components/infrastructure.md +++ b/docs/plans/next-gen/components/infrastructure.md @@ -295,7 +295,7 @@ NATS_URL=nats://localhost:4222 REDIS_URL=redis://localhost:6379 MQTT_HOST=localhost MQTT_PORT=1883 -OIDC_SESSION_SECRET=dev-secret-not-for-production +JWT_SESSION_SECRET=dev-secret-not-for-production LOG_LEVEL=debug ``` @@ -421,11 +421,11 @@ Needed to start the process; can't be read from the DB. ~20 vars. Everything els | `MQTT_PASSWORD` | No | — | ingester | | `MQTT_TLS` | No | `false` | ingester | | `REDIS_URL` | No | — (disabled) | api | -| `OIDC_SESSION_SECRET` | Yes | — | web, api (JWT signing) | -| `OIDC_CLIENT_ID` | No | — | web (OIDC, if configured) | -| `OIDC_CLIENT_SECRET` | No | — | web | -| `OIDC_DISCOVERY_URL` | No | — | web | -| `AUTH_MODE` | No | `hybrid` | web (`local`/`oidc`/`hybrid`) | +| `JWT_SESSION_SECRET` | Yes | — | web, api — **platform** JWT + session-cookie signing key, shared across all tenants (the trust anchor; cannot be per-tenant) | +| `OIDC_CLIENT_ID` | No | — | web — platform-default IdP (fallback when a tenant has no own config in `tenant_oidc_configs`) | +| `OIDC_CLIENT_SECRET` | No | — | web — platform-default IdP secret | +| `OIDC_DISCOVERY_URL` | No | — | web — platform-default IdP discovery URL | +| `AUTH_MODE` | No | `hybrid` | web — platform-default `local`/`oidc`/`hybrid`; per-tenant override in `tenant_oidc_configs.auth_mode` | | `API_HOST` | No | `0.0.0.0` | api | | `API_PORT` | No | `3000` | api | | `WEB_HOST` | No | `0.0.0.0` | web | @@ -442,6 +442,8 @@ Needed to start the process; can't be read from the DB. ~20 vars. Everything els | `BLOB_STORE_ENDPOINT` | No | — | worker (`BLOB_STORE_TYPE=s3`) | | `BLOB_STORE_BUCKET` | No | — | worker | +**Platform-scope vs tenant-scope auth config.** `JWT_SESSION_SECRET` is genuinely platform-wide: one key signs the JWT/session cookie for *every* tenant, and the API trusts the `instance_id` claim because the platform signed it (it cannot be per-tenant — the API learns the tenant *from* the JWT this key signs). The `OIDC_CLIENT_*` and `AUTH_MODE` vars are **platform defaults only**: in multi-tenant mode (D21, Phase 7) each tenant overrides them via `tenant_oidc_configs` (their own IdP + `auth_mode`), resolved per hostname; the env vars are the fallback when a tenant hasn't configured their own (multi-tenancy.md §6). + ### CLI commands Run via `docker compose run --rm ` in production, or `npx tsx src/cli.ts` in development. diff --git a/docs/plans/next-gen/components/multi-tenancy.md b/docs/plans/next-gen/components/multi-tenancy.md index 892a58e..d1d9fbc 100644 --- a/docs/plans/next-gen/components/multi-tenancy.md +++ b/docs/plans/next-gen/components/multi-tenancy.md @@ -22,7 +22,7 @@ flowchart TB ING[MqttIngester
decodes ALL traffic
routes per tenant] end MQTT --> ING - ING -->|observer → tenant lookup| ROUTE{ObserverAllowlistCache
dict[prefix → set[tenant_id]]} + ING -->|observer → tenant lookup| ROUTE{"ObserverAllowlistCache
dict[prefix → set[tenant_id]]"} ROUTE -->|"O1 → Tenant A"| NS_A[(NATS: meshcore.ingest.<A>.*)] ROUTE -->|"O2 → Tenant A+B"| NS_A & NS_B @@ -407,7 +407,7 @@ async function instanceResolution(request: FastifyRequest, reply: FastifyReply) - `GET /api/v1/config` returns the **tenant's** settings (branding, features, pages, auth_mode). - The login page renders the **tenant's** auth options (local form, OIDC button, or both per the tenant's `auth_mode`). -- JWT issuance uses the **tenant's** OIDC config (for the OIDC redirect) and the platform's `OIDC_SESSION_SECRET` (for signing — shared across tenants, since the API verifies with the same key). +- JWT issuance uses the **tenant's** OIDC config (for the OIDC redirect) and the platform's `JWT_SESSION_SECRET` (for signing — shared across tenants, since the API verifies with the same key). - The setup wizard (D12) runs per-tenant: a fresh tenant with no admin sees the wizard on their hostname. - The platform's root domain (`meshhub.example.com`) serves a **landing page** with a "Create your community" link → the registration flow (§8). diff --git a/docs/plans/next-gen/decisions/D06-jwt-auth-boundary.md b/docs/plans/next-gen/decisions/D06-jwt-auth-boundary.md index 1b3eea1..1de12d0 100644 --- a/docs/plans/next-gen/decisions/D06-jwt-auth-boundary.md +++ b/docs/plans/next-gen/decisions/D06-jwt-auth-boundary.md @@ -11,7 +11,7 @@ Today's auth is two overlapping planes with an implicit trust boundary (S1): dir **Short-lived JWT issued by the web tier, verified at a single API middleware.** -- **Web tier** mints an access JWT (default HS256 signed with `OIDC_SESSION_SECRET`; optional RS256 via `JWT_SIGNING_ALG=rs256` + `JWT_PRIVATE_KEY` / `JWT_PUBLIC_KEY` PEM paths) carrying `sub`, `roles`, `role_tier` (pre-resolved), `instance_id`, `type=access`, `iat`, `exp` (+5 minutes), `jti`. Stored in the existing signed `meshcore-session` cookie (7-day sliding renewal via `jose` JWS — same mechanism, updated library). Every login path (local password, OIDC callback — see D12) converges here. +- **Web tier** mints an access JWT (default HS256 signed with `JWT_SESSION_SECRET`; optional RS256 via `JWT_SIGNING_ALG=rs256` + `JWT_PRIVATE_KEY` / `JWT_PUBLIC_KEY` PEM paths) carrying `sub`, `roles`, `role_tier` (pre-resolved), `instance_id`, `type=access`, `iat`, `exp` (+5 minutes), `jti`. Stored in the existing signed `meshcore-session` cookie (7-day sliding renewal via `jose` JWS — same mechanism, updated library). Every login path (local password, OIDC callback — see D12) converges here. - **API** verifies the JWT at a single `AuthMiddleware`; handlers receive a frozen `Principal` (`user_id`, `roles`, `role_tier`, `instance_id`, `channel_indices` pre-resolved) via `Depends`. **No more `X-User-*` header injection** — the JWT *is* the credential. - **Direct Bearer (API keys)** remain for CLI/automation; they map to a `Principal` with a fixed role at the same middleware. diff --git a/docs/plans/next-gen/decisions/D11-three-tier-config.md b/docs/plans/next-gen/decisions/D11-three-tier-config.md index afbf0c8..0a6e6b2 100644 --- a/docs/plans/next-gen/decisions/D11-three-tier-config.md +++ b/docs/plans/next-gen/decisions/D11-three-tier-config.md @@ -11,7 +11,7 @@ Today ~200 env vars configure everything from branding to tuning to feature flag **Three-tier config model.** A setting is in exactly one tier, determined by *when it can change*: -- **Tier 1 — Bootstrap (env vars, immutable at runtime, ~15–20 vars).** Needed to start the process or reach the DB/IdP: `DATABASE_URL`, `NATS_URL`, `MQTT_HOST/...`, `REDIS_HOST/...`, `OIDC_CLIENT_ID/SECRET/DISCOVERY`, `OIDC_SESSION_SECRET`, `API_HOST/PORT`, `LOG_LEVEL`, `INSTANCE_NAME`. Rule of thumb: if changing it requires reconnecting to an external system or re-authenticating, it is Tier 1. Secrets stay here (or in a secret manager) — never in a DB backup. +- **Tier 1 — Bootstrap (env vars, immutable at runtime, ~15–20 vars).** Needed to start the process or reach the DB/IdP: `DATABASE_URL`, `NATS_URL`, `MQTT_HOST/...`, `REDIS_HOST/...`, `OIDC_CLIENT_ID/SECRET/DISCOVERY`, `JWT_SESSION_SECRET`, `API_HOST/PORT`, `LOG_LEVEL`, `INSTANCE_NAME`. Rule of thumb: if changing it requires reconnecting to an external system or re-authenticating, it is Tier 1. Secrets stay here (or in a secret manager) — never in a DB backup. In multi-tenant mode (D21) the OIDC/`AUTH_MODE` vars are **platform defaults only** — each tenant overrides them via `tenant_oidc_configs` (a DB entity, multi-tenancy.md §6); `JWT_SESSION_SECRET` stays platform-wide because it signs for all tenants. - **Tier 2 — Runtime settings (DB-backed `settings` table, UI-editable, cached + NATS-invalidated).** Branding/content, feature flags, tuning (retention, spam thresholds, evaluator intervals, cache TTLs), webhooks, radio display. Exposed via `GET/PUT /api/v1/settings/{category}`; per-category Zod validation on write; defaults ship as a seed migration; cross-service invalidation via NATS `settings.updated..{category}`. Env vars override the seed at first boot only, then the DB is authoritative. - **Tier 3 — First-class entities (already DB-backed, unchanged).** Channels, routes, tags, profiles. Custom pages move to DB in D20 (previously file-based). diff --git a/docs/plans/next-gen/decisions/D12-multi-source-auth.md b/docs/plans/next-gen/decisions/D12-multi-source-auth.md index 75ded06..8333b69 100644 --- a/docs/plans/next-gen/decisions/D12-multi-source-auth.md +++ b/docs/plans/next-gen/decisions/D12-multi-source-auth.md @@ -17,19 +17,21 @@ Today interactive login is OIDC-only (with an API-key plane for m2m). That requi | **OIDC/OAuth2** | Org/multi-user deployments wanting SSO | Optional — configure if you have an IdP | | **API keys** (direct Bearer) | CLI, scripts, m2m | Yes — for automation | -`AUTH_MODE` (Tier-1 env var, because it affects which bootstrap credentials are required) selects which interactive sources the login page offers: +`AUTH_MODE` selects which interactive sources the login page offers. It is a Tier-1 env var in single-tenant mode (because it affects which bootstrap credentials are required at first boot); in multi-tenant mode (D21, Phase 7) it becomes a **per-tenant** setting (`tenant_oidc_configs.auth_mode`) with the env var as the platform default: - `local` — username/password form only. Zero external dependencies. - `oidc` — "Sign in with SSO" button only. For orgs that mandate the IdP. - **`hybrid` (default)** — both: the login page shows the OIDC button *and* the username/password form. Operators pick per user. +**Multi-tenant evolution (D21).** A single global `AUTH_MODE`/IdP would force every self-provisioned tenant into the platform operator's auth choice — contradicting self-service. So in Phase 7 the IdP config *and* `auth_mode` move to the per-tenant `tenant_oidc_configs` table (multi-tenancy.md §6): a community tenant runs local passwords, an enterprise tenant points at their own Okta, both on one platform. The Tier-1 `OIDC_CLIENT_*`/`AUTH_MODE` vars survive only as **platform-level defaults** (the fallback when a tenant hasn't configured their own). The one thing that stays irreducibly platform-wide is the JWT/session signing key (`JWT_SESSION_SECRET`, renamed from `OIDC_SESSION_SECRET` — it was never OIDC config): one key signs for all tenants, and the API trusts the `instance_id` claim because the platform signed it. + A `local_users` row links to a `user_profiles` row (adoptions, tags, route ownership work unchanged — keyed on `user_profile.id`). The profile's `user_id` is namespaced `local:` to avoid colliding with OIDC `sub` claims. Password hashing: argon2id (`argon2` npm package, native bindings), parameterised at install time. Rate limiting: `failed_attempts` + `locked_until` give exponential lockout (5 → 15s, 10 → 5m, 20 → 1h) without Redis. Bootstrap admin via `ADMIN_USERNAME`/`ADMIN_PASSWORD` env, `admin create-user` CLI, or the first-run setup wizard (§20.8). Local + OIDC users share one Users management page. ## Consequences **Positive:** Zero-dependency default deployment — the smallest operator runs the stack and logs in without an IdP. The D6 JWT boundary means adding/removing credential sources later doesn't touch the API. Argon2id + exponential lockout is the modern password-store baseline. One Users admin page manages both sources. -**Negative:** Two code paths to maintain (local verify + OIDC callback) — though both converge on JWT issuance. Local password storage is a security responsibility (hashing, lockout, rotation) that OIDC-only avoided. The `AUTH_MODE` setting is Tier-1 (env var) because it gates bootstrap, so changing it requires a restart. +**Negative:** Two code paths to maintain (local verify + OIDC callback) — though both converge on JWT issuance. Local password storage is a security responsibility (hashing, lockout, rotation) that OIDC-only avoided. The `AUTH_MODE` env var is Tier-1 in single-tenant mode because it gates bootstrap, so changing the platform default requires a restart; the per-tenant `auth_mode` (Phase 7) is a runtime DB setting and needs no restart. ## Alternatives considered @@ -38,4 +40,4 @@ A `local_users` row links to a `user_profiles` row (adoptions, tags, route owner | **Multi-source, `hybrid` default** (chosen) | OIDC optional; zero-dependency default; one JWT boundary. | | OIDC-only (today's model) | Rejected — forces every deployment to run an IdP; blocks the community-operator use case. | | Local-only (drop OIDC) | Rejected — loses SSO for org/multi-user deployments; regresses an existing capability. | -| Per-instance auth mode in DB (not env) | Rejected — auth mode gates which bootstrap credentials are required, so it must be available before the DB is reachable. | +| Per-instance auth mode in DB (not env) | Rejected as the *single-tenant bootstrap* mechanism (auth mode gates which bootstrap credentials are required, so it must predate DB reachability) — but **adopted for multi-tenant runtime** in Phase 7 (D21): `tenant_oidc_configs.auth_mode`, with the env var as platform default. | diff --git a/docs/plans/next-gen/implementation-checklist.md b/docs/plans/next-gen/implementation-checklist.md index 764bac6..36b39bd 100644 --- a/docs/plans/next-gen/implementation-checklist.md +++ b/docs/plans/next-gen/implementation-checklist.md @@ -134,7 +134,7 @@ ### Auth - [ ] Implement `AuthMiddleware` preHandler (JWT → cookie → API key → anonymous) — [auth.md](components/auth.md#authmiddleware-single-resolution-point) - [ ] Implement `Principal` frozen object + resolution from JWT claims / session cookie / API key -- [ ] Implement JWT issuance in the web tier (5m access, HS256, `OIDC_SESSION_SECRET`) +- [ ] Implement JWT issuance in the web tier (5m access, HS256, `JWT_SESSION_SECRET`) - [ ] Implement session-cookie sliding renewal (7d, JWS via `jose`) - [ ] Implement local password store: `local_users` table, argon2id verify, exponential lockout - [ ] Implement the shared 3-table bootstrap insert (user_profiles + local_users + user_profile_roles) in one transaction diff --git a/docs/plans/next-gen/phasing.md b/docs/plans/next-gen/phasing.md index 5772f4d..eef5499 100644 --- a/docs/plans/next-gen/phasing.md +++ b/docs/plans/next-gen/phasing.md @@ -75,7 +75,7 @@ The cleanup phase after all functional phases land. Three tracks: ### 6.2 Security hardening pass - [ ] RLS audit: verify every tenant-scoped table enforces the `instance_id` policy; add a test that asserts cross-instance queries return 0 rows. - [ ] Rate-limit review: confirm local-login lockout thresholds; verify reverse-proxy `limit_req` / `fail2ban` rules for the auth endpoint. -- [ ] JWT rotation drill: rotate `OIDC_SESSION_SECRET`; verify all sessions invalidate gracefully. +- [ ] JWT rotation drill: rotate `JWT_SESSION_SECRET`; verify all sessions invalidate gracefully. - [ ] Dependency audit: `npm audit` clean (backend + frontend); no known CVEs in the lockfiles. ### 6.3 New-repo `AGENTS.md` / `CONTRIBUTING.md`