diff --git a/CHANGELOG.md b/CHANGELOG.md index 919ecdc..6b0de3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ All notable changes to Void 2.0 are documented here. Format: [Keep a Changelog](https://keepachangelog.com). +## 2.0.0-alpha.18 — Plan 8b cutover: `void.hynesy.com` now serves Void 2 +- **Go-live.** `void.hynesy.com` (CT 301 → Void 1) is repointed at **Void 2** (CT 311, `.216:3000`) at the Traefik edge. Void 1 is now **legacy** — CT 301 stays running untouched as an instant-rollback fallback; nothing is retired or renamed yet. The `-alpha` tag is intentionally **kept** pending owner sign-off. +- **CF Access multi-aud** (`lib/auth/cf_access.js`): `CF_ACCESS_AUD` now accepts a **comma-separated allow-list** so a request through *either* CF Access app — `void.hynesy.com` (aud `0e7190f4…`) or `void2-app.hynesy.com` (aud `a381f270…`) — is honoured as owner. Still fails closed; an unlisted aud is rejected. Prod env updated to carry both auds. +- Cutover is fully reversible: revert the Traefik `void` service URL to `http://192.168.1.11:2424` and `docker restart traefik`. + ## 2.0.0-alpha.17 — Settings, project management, terminal, AI Usage, "The Void" space + UI polish - **Settings** (`#/settings`): API tokens (mint/list/revoke), Agents list with an expandable **profile viewer** (persona/"soul" + capabilities/scopes via `GET /api/agents/:id/profile`), Orthos Mode placeholder. - **Per-space project management**: Void-1-style expandable cards with inline **status**, **Details**, **Tasks**, **Linked references**, **↻ Research** (Eithan stub → `POST /api/projects/:id/research`), Edit/New modal, Delete-with-confirm. Migration 019 adds research fields; `GET /api/projects/:id/links` resolves linked pages/refs. diff --git a/lib/auth/cf_access.js b/lib/auth/cf_access.js index 54dbf5d..57e249d 100644 --- a/lib/auth/cf_access.js +++ b/lib/auth/cf_access.js @@ -49,7 +49,12 @@ export async function verifyAccessJwt(jwt, { teamDomain, aud, fetchImpl = fetch, const claims = b64urlJson(p); const auds = Array.isArray(claims.aud) ? claims.aud : [claims.aud]; - if (!auds.includes(aud)) throw new Error('aud mismatch'); + // `aud` may be a single value or a comma-separated list (multiple CF Access + // apps front the same origin — e.g. void.hynesy.com and void2-app.hynesy.com + // during the 8b cutover). Accept the token if it carries ANY allowed aud. + const wanted = (Array.isArray(aud) ? aud : String(aud).split(',')) + .map((a) => a.trim()).filter(Boolean); + if (!auds.some((a) => wanted.includes(a))) throw new Error('aud mismatch'); const t = Math.floor(now() / 1000); if (claims.exp && t > claims.exp) throw new Error('expired'); if (claims.nbf && t < claims.nbf - 60) throw new Error('not yet valid'); diff --git a/package.json b/package.json index dc8e8cf..3a9afb0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "void-server", - "version": "2.0.0-alpha.17", + "version": "2.0.0-alpha.18", "type": "module", "private": true, "scripts": { diff --git a/server.js b/server.js index bfe4613..fc2c93d 100644 --- a/server.js +++ b/server.js @@ -13,7 +13,7 @@ import { mcpAuth } from './lib/api/middleware/mcp_auth.js'; import { handleMcp } from './lib/mcp/http.js'; import httpProxy from 'http-proxy'; -const VERSION = '2.0.0-alpha.17'; +const VERSION = '2.0.0-alpha.18'; // Proxy /terminal (+ its WebSocket) to ttyd on CT 300, so the embedded terminal // works whether the Void is reached via Traefik (void2-app.hynesy.com) OR the diff --git a/tests/auth/cf_access.test.js b/tests/auth/cf_access.test.js index 9877553..419308e 100644 --- a/tests/auth/cf_access.test.js +++ b/tests/auth/cf_access.test.js @@ -39,6 +39,12 @@ describe('verifyAccessJwt', () => { it('rejects a wrong audience (another app cannot grant access)', async () => { await expect(verifyAccessJwt(mintJwt({ ...valid, aud: 'someone-else' }), opts)).rejects.toThrow(/aud/); }); + it('accepts any aud from a comma-separated allow-list (8b cutover: two CF apps front one origin)', async () => { + const multi = { ...opts, aud: `void-app-aud, ${AUD}` }; + const c = await verifyAccessJwt(mintJwt(valid), multi); // token carries AUD (the 2nd allowed) + expect(c.email).toBe(EMAIL); + await expect(verifyAccessJwt(mintJwt({ ...valid, aud: 'unlisted' }), multi)).rejects.toThrow(/aud/); + }); it('rejects an expired token', async () => { await expect(verifyAccessJwt(mintJwt({ ...valid, exp: sec - 10 }), opts)).rejects.toThrow(/expired/); });