# Changelog ## [0.28.4] — 2026-03-14 ### Summary Security tier red team. 58 adversarial tests added to the ICD runner exposing 5 bugs (2× P0, 2× P1, 1× P2), all fixed in this release. Auth middleware rewritten with a user-status cache so deactivation and role changes take effect within 30 seconds. `surfaces/` directory added to the repo with a build script for packaging extension surfaces. ### Security Fixes - **[P0] Deactivated user JWT accepted.** Auth middleware validated signature + expiry only — never checked `is_active` in the database. A deactivated user's existing JWT continued working until natural expiry (15 min). Fix: `middleware/auth.go` now performs a cached DB lookup (`UserStatusCache`, 30 s TTL) on every request. Deactivated users receive 401 within seconds. - **[P0] Demoted admin JWT retained admin access.** The `role` context value was read from JWT claims, not the database. Demoting an admin via `PUT /admin/users/:id/role` had no effect until the user's token expired. Fix: role is now resolved from the DB on every request via the same cached lookup. `RequireAdmin()` reads the live role. - **[P1] Null byte in path parameter → 500.** Gin passed `%00` through to the database layer, causing a scan error. Fix: new `ValidatePathParams()` middleware rejects null bytes with 400. - **[P1] 4096-char path parameter → 500.** Same root cause — oversized params reached the DB. Fix: `ValidatePathParams()` rejects params longer than 255 characters with 400. - **[P2] CORS `Access-Control-Allow-Origin: *` in all environments.** Fix: production mode now defaults to same-origin only. Override with `CORS_ALLOWED_ORIGINS=https://app.example.com` (comma-separated). Development mode retains `*`. ### Added - **ICD Security Tier** — 58 adversarial tests across 6 domains: auth-boundary (8), cross-tenant (20), input-validation (18), session (6), escalation (7), transport (3). Red "Security" button in the runner UI. Requires fixtures. - **`UserStatusCache`** (`middleware/auth.go`) — per-user `{isActive, role, fetchedAt}` cache with 30 s TTL. Shared by `Auth()`, `AuthOrSession()`, and `AuthOrRedirect()`. Cache eviction on `UpdateUserRole` and `ToggleUserActive` via callback. - **`ValidatePathParams()` middleware** (`middleware/validate.go`) — rejects null bytes and >255-char path params before they reach handlers. - **`surfaces/` directory** — source tree for extension surfaces with `build.sh` that packages each subdirectory into `dist/.surface`. `hello-dashboard` and `icd-test-runner` moved from loose archives into version-controlled source. - **`surfaces/README.md`** — structure docs, build instructions, conventions. - ICD runner: `safeDelete()` now retries 2× with 500 ms backoff on 502/503, swallows cleanup errors with `console.warn` instead of throwing. - ICD runner: `assertDenied()` / `assertRawDenied()` helpers treat 502 as INCONCLUSIVE instead of CRITICAL. ### Changed - `middleware.Auth()` signature: `(cfg)` → `(cfg, users, cache)`. - `middleware.AuthOrSession()` signature: `(cfg, stores)` → `(cfg, stores, cache)`. - `middleware.AuthOrRedirect()` signature: `(cfg)` → `(cfg, users, cache)`. - `middleware.CORS()` signature: `()` → `(cfg)`. - `AdminHandler` gains `OnUserChanged(func(userID string))` — wired to `cache.Evict` in `main.go`. - 10 test files updated for new middleware signatures. - `.gitignore`: added `*.surface` under build artifacts. - ICD runner manifest bumped to `0.28.4.0`. ### Removed - `hello-dashboard.surface` at repo root (replaced by `surfaces/hello-dashboard/` source directory). ### Verified (not bugs) - Cross-tenant isolation: 20/20 — notes, channels, messages, tasks, workspaces, projects, BYOK configs, memories, team providers/ personas/members all properly isolated. - SQL injection: 7 payloads across 3 search endpoints — all parameterized. - XSS storage: 3 round-trip tests — no server crashes (frontend sanitizes via DOMPurify). - Escalation: self-promote, cross-role create, team-admin → platform-admin, user_id substitution — all blocked. - JWT tampering + alg=none: both rejected. - Refresh token rotation + revocation: both working. - Auth rate limiting: 429 after burst. - Unauthenticated access: all 6 endpoints return 401. ### Known - `[P2]` WebSocket `?token=` query parameter exposes JWT in server logs and browser history. Informational — consider ticket exchange pattern post-1.0. - Cross-visitor session isolation: enforced by `session_auth.go` channel binding, but no E2E test yet (requires public_link workflow setup — defer to visitor E2E milestone). ## [0.28.3.5] — 2026-03-14 ### Summary Frontend decomposition Phase 4: ES module conversion. IIFE wrappers removed from all 47 JS files, `