feat(cutover): Plan 8b — point void.hynesy.com at Void 2 (alpha.18)
CF Access multi-aud: CF_ACCESS_AUD now accepts a comma-separated allow-list so requests through either the void.hynesy.com or void2-app.hynesy.com CF Access app are honoured as owner. Fails closed; unlisted auds rejected. Adds multi-aud test. Void 1 (CT 301) becomes legacy but stays running untouched as an instant rollback. -alpha tag kept pending owner sign-off. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "void-server",
|
||||
"version": "2.0.0-alpha.17",
|
||||
"version": "2.0.0-alpha.18",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user