diff --git a/docs/DUAL_AUDIENCE_DEVELOPMENT_GUIDE.md b/docs/DUAL_AUDIENCE_DEVELOPMENT_GUIDE.md
index b6f58b7..f1ccf2c 100644
--- a/docs/DUAL_AUDIENCE_DEVELOPMENT_GUIDE.md
+++ b/docs/DUAL_AUDIENCE_DEVELOPMENT_GUIDE.md
@@ -1,16 +1,21 @@
# Dual-Audience Development Guide: Agent-First APIs & Mobile-First SSR UI
-This guide outlines the architectural blueprint and best practices for building modern services within the **Auth-Yes Zero-Trust Ecosystem**.
+This guide outlines the architectural blueprint and best practices for building
+modern services within the **Auth-Yes Zero-Trust Ecosystem**.
---
## 1. The Core Philosophy: Dual-Audience Architecture
Every modern application should be built for **two primary audiences**:
+
1. **🤖 AI Agents & Workloads (Primary Data Consumer):**
- - Headless execution via **Model Context Protocol (MCP)**, REST APIs, or ConnectRPC.
- - Machine-readable, high-density JSON/RPC responses with zero HTML/CSS clutter.
- - Authentication via **Delegated Bearer Tokens (`ay_sess_...`)** or mTLS SPIFFE workload identities.
+ - Headless execution via **Model Context Protocol (MCP)**, REST APIs, or
+ ConnectRPC.
+ - Machine-readable, high-density JSON/RPC responses with zero HTML/CSS
+ clutter.
+ - Authentication via **Delegated Bearer Tokens (`ay_sess_...`)** or mTLS
+ SPIFFE workload identities.
2. **📱 Humans on Mobile Devices (Primary UI Consumer):**
- Ultra-fast, zero-friction **Server-Side Rendered (SSR) JSX** touch cards.
- Passkey (WebAuthn) biometric authentication.
@@ -21,26 +26,26 @@ Every modern application should be built for **two primary audiences**:
## 2. Architectural Blueprint for Subsidiary Apps (e.g. `ed-droid`)
```
- +---------------------------------------+
- | Traefik Edge Ingress |
- +---------------------------------------+
- |
- ForwardAuth Check | (Injects X-Forwarded-*)
- +------------------------+------------------------+
- | |
- v v
- +-----------------------+ +-----------------------+
- | Auth-Yes Service | | Subsidiary App |
- | (Validates Session / | | (e.g., ed-droid) |
- | Bearer Token) | +-----------------------+
- +-----------------------+ |
- |
- +----------------------------+----------------------------+
- | |
- v v
- [ /api/* JSON Endpoints ] [ SSR HTML Mobile Cards ]
- - Served for AI Agents & MCP - Served for Human Browsers
- - Filtered by Scopes / Roles - Clean, high-contrast UI
+ +---------------------------------------+
+ | Traefik Edge Ingress |
+ +---------------------------------------+
+ |
+ ForwardAuth Check | (Injects X-Forwarded-*)
+ +------------------------+------------------------+
+ | |
+ v v
++-----------------------+ +-----------------------+
+| Auth-Yes Service | | Subsidiary App |
+| (Validates Session / | | (e.g., ed-droid) |
+| Bearer Token) | +-----------------------+
++-----------------------+ |
+ |
+ +----------------------------+----------------------------+
+ | |
+ v v
+ [ /api/* JSON Endpoints ] [ SSR HTML Mobile Cards ]
+ - Served for AI Agents & MCP - Served for Human Browsers
+ - Filtered by Scopes / Roles - Clean, high-contrast UI
```
---
@@ -48,22 +53,30 @@ Every modern application should be built for **two primary audiences**:
## 3. How to Build an "Agent-First" Subsidiary App
### Step 1: Ingress Protection via ForwardAuth
-In your Traefik/Compose configuration, protect your service domain with Auth-Yes ForwardAuth middleware:
+
+In your Traefik/Compose configuration, protect your service domain with Auth-Yes
+ForwardAuth middleware:
+
```yaml
labels:
- "traefik.http.routers.ed-droid.middlewares=auth-yes-forwardauth@docker"
```
+
When requests arrive:
-* Traefik queries `http://auth-api:8000/api/forward-auth`.
-* Auth-Yes validates either the browser's `session_id` cookie **or** the incoming `Authorization: Bearer ay_sess_...` token.
-* If authorized, Traefik injects:
+
+- Traefik queries `http://auth-api:8000/api/forward-auth`.
+- Auth-Yes validates either the browser's `session_id` cookie **or** the
+ incoming `Authorization: Bearer ay_sess_...` token.
+- If authorized, Traefik injects:
- `X-Forwarded-User: `
- `X-Forwarded-User-Id: `
- `X-Forwarded-Scopes: `
- `X-Forwarded-App-Id: `
### Step 2: Implement Clean JSON API Routes
+
Provide standard JSON endpoints for all core operations:
+
```typescript
import { Hono } from "jsr:@hono/hono";
@@ -78,16 +91,22 @@ app.get("/api/fleet", (c) => {
```
### Step 3: Progressive Content Negotiation (Optional)
-If a route serves both humans and AI agents without separate `/api` prefixes, inspect the `Accept` header or `?format=json` query:
+
+If a route serves both humans and AI agents without separate `/api` prefixes,
+inspect the `Accept` header or `?format=json` query:
+
```typescript
app.get("/ships/:id", (c) => {
const ship = getShip(c.req.param("id"));
-
+
// If requested by an agent or CLI:
- if (c.req.header("Accept")?.includes("application/json") || c.req.query("format") === "json") {
+ if (
+ c.req.header("Accept")?.includes("application/json") ||
+ c.req.query("format") === "json"
+ ) {
return c.json(ship);
}
-
+
// If requested by a human browser:
return c.html(ShipCardView({ ship }));
});
@@ -97,11 +116,14 @@ app.get("/ships/:id", (c) => {
## 4. Connecting AI Agents via Model Context Protocol (MCP)
-To expose your subsidiary applications to AI assistants (Antigravity, Jules, Claude Desktop):
+To expose your subsidiary applications to AI assistants (Antigravity, Jules,
+Claude Desktop):
1. **Mint a Delegated Session in Auth-Yes:**
- - Go to `https://auth.atyg.org/dashboard/sessions` $\rightarrow$ Click **`+ Delegate Agent Session`**.
- - Set Label: `"Antigravity Assistant"`, Lifespan: `12 Hours`, Scope: `ed-droid`.
+ - Go to `https://auth.atyg.org/dashboard/sessions` $\rightarrow$ Click
+ **`+ Delegate Agent Session`**.
+ - Set Label: `"Antigravity Assistant"`, Lifespan: `12 Hours`, Scope:
+ `ed-droid`.
- Copy the CLI export string.
2. **Configure the MCP Server:**
@@ -110,7 +132,11 @@ To expose your subsidiary applications to AI assistants (Antigravity, Jules, Cla
"mcpServers": {
"ed-droid": {
"command": "deno",
- "args": ["run", "-A", "https://git.atyg.org/tylerg/ed-droid/raw/branch/main/mcp/server.ts"],
+ "args": [
+ "run",
+ "-A",
+ "https://git.atyg.org/tylerg/ed-droid/raw/branch/main/mcp/server.ts"
+ ],
"env": {
"AUTH_YES_TOKEN": "ay_sess_8de186f564d7..."
}
@@ -119,26 +145,33 @@ To expose your subsidiary applications to AI assistants (Antigravity, Jules, Cla
}
```
-3. **Tool Call Execution:**
- The MCP server attaches `Authorization: Bearer $AUTH_YES_TOKEN` to all internal fetch calls, gaining instant authorized access to fleet telemetry and data with full Merkle audit attribution!
+3. **Tool Call Execution:** The MCP server attaches
+ `Authorization: Bearer $AUTH_YES_TOKEN` to all internal fetch calls, gaining
+ instant authorized access to fleet telemetry and data with full Merkle audit
+ attribution!
---
## 5. Ephemeral Guest & Support Passes (Magic 1-Click Links)
-In addition to Agent Bearer Tokens, Auth-Yes supports **Ephemeral Magic Passes** for friends and external support technicians:
+In addition to Agent Bearer Tokens, Auth-Yes supports **Ephemeral Magic Passes**
+for friends and external support technicians:
### The Flow:
+
1. **Spawn Pass:** Under Sessions, click **`+ Spawn Guest / Support Pass`**:
- - **Label:** *"Friend Demo - Elite Dangerous Fleet"*
- - **Lifespan:** *2 Hours* (auto-expires)
+ - **Label:** _"Friend Demo - Elite Dangerous Fleet"_
+ - **Lifespan:** _2 Hours_ (auto-expires)
- **Target App:** `ed-droid.atyg.org`
2. **Share 1-Click Link:**
`https://auth.atyg.org/pass?token=ay_pass_9f8a7b6c...`
3. **Instant Redemption:**
- - When opened in any browser, Auth-Yes automatically sets the `.atyg.org` session cookie with restricted scopes.
+ - When opened in any browser, Auth-Yes automatically sets the `.atyg.org`
+ session cookie with restricted scopes.
- The browser is immediately redirected to `https://ed-droid.atyg.org`.
- - **Zero friction:** No passkeys to register, no passwords, no email confirmation.
+ - **Zero friction:** No passkeys to register, no passwords, no email
+ confirmation.
4. **Security & Control:**
- The guest only has access to the specified target app.
- - The pass is visible on your Sessions dashboard in real time with an instant **`[Revoke]`** button.
+ - The pass is visible on your Sessions dashboard in real time with an instant
+ **`[Revoke]`** button.
diff --git a/docs/superpowers/plans/2026-08-25-ephemeral-passes-and-multi-claim-events.md b/docs/superpowers/plans/2026-08-25-ephemeral-passes-and-multi-claim-events.md
index b54f59f..85eed37 100644
--- a/docs/superpowers/plans/2026-08-25-ephemeral-passes-and-multi-claim-events.md
+++ b/docs/superpowers/plans/2026-08-25-ephemeral-passes-and-multi-claim-events.md
@@ -1,86 +1,117 @@
# Ephemeral Magic Passes & Multi-Claim Event Passes Implementation Plan
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+> **For agentic workers:** REQUIRED SUB-SKILL: Use
+> superpowers:subagent-driven-development (recommended) or
+> superpowers:executing-plans to implement this plan task-by-task. Steps use
+> checkbox (`- [ ]`) syntax for tracking.
-**Goal:** Implement 1-on-1 Ephemeral Magic Links, Multi-Claim Event/Workshop Passes (with short codes, PINs, short URLs, and CLI 1-liners), and a real-time Event Cockpit with a master kill switch.
+**Goal:** Implement 1-on-1 Ephemeral Magic Links, Multi-Claim Event/Workshop
+Passes (with short codes, PINs, short URLs, and CLI 1-liners), and a real-time
+Event Cockpit with a master kill switch.
-**Architecture:** Extend the Auth-Yes zero-trust session engine with a dedicated event registry in PostgreSQL and Valkey. Provide a unified 1-click redemption endpoint (`GET /pass`), a short-code/PIN join portal (`GET /join`, `GET /e/:slug`), CLI environment streams (`GET /join/:slug?format=env`), and live seat metrics with sub-millisecond batch revocation via Valkey RESP3 push tracking.
+**Architecture:** Extend the Auth-Yes zero-trust session engine with a dedicated
+event registry in PostgreSQL and Valkey. Provide a unified 1-click redemption
+endpoint (`GET /pass`), a short-code/PIN join portal (`GET /join`,
+`GET /e/:slug`), CLI environment streams (`GET /join/:slug?format=env`), and
+live seat metrics with sub-millisecond batch revocation via Valkey RESP3 push
+tracking.
-**Tech Stack:** Deno 2.x, TypeScript 5.x, Hono SSR JSX (React-free), PostgreSQL 18, Valkey 8, Traefik ForwardAuth.
+**Tech Stack:** Deno 2.x, TypeScript 5.x, Hono SSR JSX (React-free), PostgreSQL
+18, Valkey 8, Traefik ForwardAuth.
-**Spec:** [DUAL_AUDIENCE_DEVELOPMENT_GUIDE.md](../../DUAL_AUDIENCE_DEVELOPMENT_GUIDE.md)
+**Spec:**
+[DUAL_AUDIENCE_DEVELOPMENT_GUIDE.md](../../DUAL_AUDIENCE_DEVELOPMENT_GUIDE.md)
## Global Constraints
+
- Strictly zero React dependencies; use pure Hono SSR JSX.
- Zero-dependency SDK in `sdk/`.
-- All cookie mutations must delete host-only cookies and set wildcard `.atyg.org` domain cookies.
-- Every commit and change must be pushed to both GitHub (`origin`) and Gitea (`gitea`).
-- Quality gates must pass: `deno fmt`, `deno task lint`, `deno task check`, `deno task test`.
+- All cookie mutations must delete host-only cookies and set wildcard
+ `.atyg.org` domain cookies.
+- Every commit and change must be pushed to both GitHub (`origin`) and Gitea
+ (`gitea`).
+- Quality gates must pass: `deno fmt`, `deno task lint`, `deno task check`,
+ `deno task test`.
---
### Task 1: 1-on-1 Ephemeral Magic Link Redemption (`/pass`)
**Files:**
+
- Modify: `server/main.ts`
- Modify: `ui/components/SessionsPage.tsx`
- Test: `server/main.test.ts`
**Interfaces:**
+
- Consumes: `extractAllSessionIds(c)`, `valkey.get()`, `sqlWrapper.sql`
-- Produces: `GET /pass?token=...` endpoint and updated Hand-Off Modal in `SessionsPage.tsx`
+- Produces: `GET /pass?token=...` endpoint and updated Hand-Off Modal in
+ `SessionsPage.tsx`
- [ ] **Step 1: Write the failing test for `GET /pass`**
```typescript
// in server/main.test.ts
Deno.test("GET /pass - Ephemeral Magic Link 1-Click Redemption", async (t) => {
- await t.step("Valid token sets wildcard cookie and redirects to target app", async () => {
- const valkeyStub = stub(valkey, "get", (key: any) => {
- if (String(key) === "ay_sess_valid_pass") {
- return Promise.resolve(
- JSON.stringify({ uuid: "guest-uuid", username: "guest_user", customScopes: ["app:ed-droid"] }),
- );
- }
- return Promise.resolve(null);
- });
-
- const originalSql = sqlWrapper.sql;
- sqlWrapper.sql = ((_query: any) => {
- return Promise.resolve([{ domain: "ed-droid.atyg.org" }]);
- }) as any;
-
- try {
- const res = await app.request("/pass?token=ay_sess_valid_pass", {
- method: "GET",
+ await t.step(
+ "Valid token sets wildcard cookie and redirects to target app",
+ async () => {
+ const valkeyStub = stub(valkey, "get", (key: any) => {
+ if (String(key) === "ay_sess_valid_pass") {
+ return Promise.resolve(
+ JSON.stringify({
+ uuid: "guest-uuid",
+ username: "guest_user",
+ customScopes: ["app:ed-droid"],
+ }),
+ );
+ }
+ return Promise.resolve(null);
});
- assertEquals(res.status, 302);
- const setCookie = res.headers.get("set-cookie") || "";
- assert(setCookie.includes("session_id=ay_sess_valid_pass"));
- assert(res.headers.get("location")?.includes("ed-droid.atyg.org") || res.headers.get("location") === "/dashboard");
- } finally {
- sqlWrapper.sql = originalSql;
- valkeyStub.restore();
- }
- });
+
+ const originalSql = sqlWrapper.sql;
+ sqlWrapper.sql = ((_query: any) => {
+ return Promise.resolve([{ domain: "ed-droid.atyg.org" }]);
+ }) as any;
+
+ try {
+ const res = await app.request("/pass?token=ay_sess_valid_pass", {
+ method: "GET",
+ });
+ assertEquals(res.status, 302);
+ const setCookie = res.headers.get("set-cookie") || "";
+ assert(setCookie.includes("session_id=ay_sess_valid_pass"));
+ assert(
+ res.headers.get("location")?.includes("ed-droid.atyg.org") ||
+ res.headers.get("location") === "/dashboard",
+ );
+ } finally {
+ sqlWrapper.sql = originalSql;
+ valkeyStub.restore();
+ }
+ },
+ );
});
```
-- [ ] **Step 2: Run test to verify it fails**
-Run: `deno test server/main.test.ts --filter "Ephemeral Magic Link"`
-Expected: FAIL (404 Not Found on `/pass`)
+- [ ] **Step 2: Run test to verify it fails** Run:
+ `deno test server/main.test.ts --filter "Ephemeral Magic Link"` Expected:
+ FAIL (404 Not Found on `/pass`)
-- [ ] **Step 3: Implement `GET /pass` in `server/main.ts`**
-Extract token from `?token=...`, validate against Valkey/DB, set cookie with `getCookieDomain()`, resolve target app domain if scoped to a specific app, and return `c.redirect(targetUrl)`.
+- [ ] **Step 3: Implement `GET /pass` in `server/main.ts`** Extract token from
+ `?token=...`, validate against Valkey/DB, set cookie with
+ `getCookieDomain()`, resolve target app domain if scoped to a specific
+ app, and return `c.redirect(targetUrl)`.
-- [ ] **Step 4: Update `ui/components/SessionsPage.tsx`**
-Add the **"1-Click Magic Link"** copy card tab alongside the CLI and cURL header tabs.
+- [ ] **Step 4: Update `ui/components/SessionsPage.tsx`** Add the **"1-Click
+ Magic Link"** copy card tab alongside the CLI and cURL header tabs.
-- [ ] **Step 5: Run tests to verify they pass**
-Run: `deno task test`
-Expected: PASS (All tests passing)
+- [ ] **Step 5: Run tests to verify they pass** Run: `deno task test` Expected:
+ PASS (All tests passing)
- [ ] **Step 6: Commit and Push**
+
```bash
git add server/main.ts server/main.test.ts ui/components/SessionsPage.tsx
git commit -m "feat(pass): implement 1-click ephemeral magic link redemption route"
@@ -92,6 +123,7 @@ git push origin main && git push gitea main
### Task 2: Multi-Claim Event Passes Schema & Join Endpoints (`/join`, `/e/:slug`, CLI 1-Liner)
**Files:**
+
- Modify: `server/db.ts`
- Modify: `server/main.ts`
- Create: `ui/components/EventJoinPage.tsx`
@@ -99,13 +131,16 @@ git push origin main && git push gitea main
- Test: `server/main.test.ts`
**Interfaces:**
+
- Produces:
- Table: `event_passes` in PostgreSQL
- Endpoints:
- - `POST /api/events`: Create event pass (slug, pin_code, name, max_seats, lifespan_hours, app_id, role)
+ - `POST /api/events`: Create event pass (slug, pin_code, name, max_seats,
+ lifespan_hours, app_id, role)
- `GET /e/:slug`: Web landing splash with "Enter Workshop" action
- `GET /join`: Universal PIN / word-code entry page
- - `POST /api/join`: Redeems code/PIN and creates isolated `guest__` session
+ - `POST /api/join`: Redeems code/PIN and creates isolated
+ `guest__` session
- `GET /join/:slug`: CLI 1-liner (`?format=env` or `?format=json`)
- [ ] **Step 1: Write the failing tests for Event creation and redemption**
@@ -124,23 +159,28 @@ Deno.test("Multi-Claim Event Passes & Join Endpoints", async (t) => {
});
```
-- [ ] **Step 2: Run test to verify it fails**
-Run: `deno test server/main.test.ts --filter "Multi-Claim Event"`
-Expected: FAIL
+- [ ] **Step 2: Run test to verify it fails** Run:
+ `deno test server/main.test.ts --filter "Multi-Claim Event"` Expected:
+ FAIL
-- [ ] **Step 3: Add `event_passes` schema in `server/db.ts`**
-Include columns: `id`, `slug`, `pin_code`, `name`, `app_id`, `role`, `max_seats`, `seats_claimed`, `lifespan_hours`, `created_by`, `is_active`, `expires_at`, `created_at`.
+- [ ] **Step 3: Add `event_passes` schema in `server/db.ts`** Include columns:
+ `id`, `slug`, `pin_code`, `name`, `app_id`, `role`, `max_seats`,
+ `seats_claimed`, `lifespan_hours`, `created_by`, `is_active`,
+ `expires_at`, `created_at`.
-- [ ] **Step 4: Implement Event API routes in `server/main.ts` and SSR UI Pages**
+- [ ] **Step 4: Implement Event API routes in `server/main.ts` and SSR UI
+ Pages**
- Create `ui/components/EventJoinPage.tsx` for `/join` PIN code entry.
-- Create `ui/components/EventSplashPage.tsx` for `/e/:slug` 1-click workshop entrance.
-- Implement `GET /join/:slug` returning `export AUTH_YES_TOKEN="..."` when `?format=env`.
+- Create `ui/components/EventSplashPage.tsx` for `/e/:slug` 1-click workshop
+ entrance.
+- Implement `GET /join/:slug` returning `export AUTH_YES_TOKEN="..."` when
+ `?format=env`.
-- [ ] **Step 5: Run tests to verify they pass**
-Run: `deno task test`
-Expected: PASS
+- [ ] **Step 5: Run tests to verify they pass** Run: `deno task test` Expected:
+ PASS
- [ ] **Step 6: Commit and Push**
+
```bash
git add server/db.ts server/main.ts ui/components/EventJoinPage.tsx ui/components/EventSplashPage.tsx server/main.test.ts
git commit -m "feat(events): implement multi-claim event passes, PIN join portal, and CLI 1-liner"
@@ -152,41 +192,53 @@ git push origin main && git push gitea main
### Task 3: Live Event Cockpit & Master Kill-Switch
**Files:**
+
- Modify: `server/main.ts`
- Modify: `ui/components/SessionsPage.tsx`
- Modify: `ui/mod.ts`
- Test: `server/main.test.ts`
**Interfaces:**
+
- Produces:
- - `POST /api/events/:id/end`: Closes event and immediately revokes all guest sessions
+ - `POST /api/events/:id/end`: Closes event and immediately revokes all guest
+ sessions
- `POST /api/events/:id/extend`: Adds hours to active event
- - Event Management Deck in `SessionsPage.tsx` with live seat counter (`38 / 50`) and master kill switch
+ - Event Management Deck in `SessionsPage.tsx` with live seat counter
+ (`38 / 50`) and master kill switch
- [ ] **Step 1: Write failing tests for Event Kill-Switch & Extension**
+
```typescript
Deno.test("Event Cockpit & Master Kill Switch", async (t) => {
- await t.step("POST /api/events/:id/end revokes all guest seats instantly", async () => {
- // verify sessions deleted and Valkey cleared
- });
+ await t.step(
+ "POST /api/events/:id/end revokes all guest seats instantly",
+ async () => {
+ // verify sessions deleted and Valkey cleared
+ },
+ );
});
```
-- [ ] **Step 2: Run test to verify it fails**
-Run: `deno test server/main.test.ts --filter "Event Cockpit"`
-Expected: FAIL
+- [ ] **Step 2: Run test to verify it fails** Run:
+ `deno test server/main.test.ts --filter "Event Cockpit"` Expected: FAIL
-- [ ] **Step 3: Implement `POST /api/events/:id/end` and `POST /api/events/:id/extend` in `server/main.ts`**
-Fetch all session IDs created under the event pass, delete them from Valkey and PostgreSQL, record in audit ledger, and mark event `is_active = false`.
+- [ ] **Step 3: Implement `POST /api/events/:id/end` and
+ `POST /api/events/:id/extend` in `server/main.ts`** Fetch all session IDs
+ created under the event pass, delete them from Valkey and PostgreSQL,
+ record in audit ledger, and mark event `is_active = false`.
-- [ ] **Step 4: Update `ui/components/SessionsPage.tsx` with Event Cockpit Deck**
-Display active events with live progress bar (`Seats Claimed / Capacity`), PIN badge, copy links, `[+1h Extend]` and `[🔴 End Workshop & Revoke All]`.
+- [ ] **Step 4: Update `ui/components/SessionsPage.tsx` with Event Cockpit
+ Deck** Display active events with live progress bar
+ (`Seats Claimed / Capacity`), PIN badge, copy links, `[+1h Extend]` and
+ `[🔴 End Workshop & Revoke All]`.
-- [ ] **Step 5: Run full quality gates**
-Run: `deno fmt && deno task lint && deno task check && deno task test`
-Expected: All pass.
+- [ ] **Step 5: Run full quality gates** Run:
+ `deno fmt && deno task lint && deno task check && deno task test`
+ Expected: All pass.
- [ ] **Step 6: Commit and Push**
+
```bash
git add server/main.ts ui/components/SessionsPage.tsx ui/mod.ts server/main.test.ts
git commit -m "feat(cockpit): add live event metrics, seat roster, and master kill-switch"
diff --git a/server/main.test.ts b/server/main.test.ts
index a4545d4..6607fa1 100644
--- a/server/main.test.ts
+++ b/server/main.test.ts
@@ -71,6 +71,109 @@ Deno.test("Tier 1 & 2: GET /api/forward-auth - Valid session", async () => {
valkeyStub.restore();
});
+Deno.test("Ephemeral 1-Click Magic Link Redemption (/pass)", async (t) => {
+ await t.step("GET /pass with missing token redirects to login", async () => {
+ const res = await app.request("/pass", { method: "GET" });
+ assertEquals(res.status, 302);
+ assertEquals(
+ res.headers.get("location"),
+ "/login?error=invalid_or_expired_pass",
+ );
+ });
+
+ await t.step("GET /pass with invalid token redirects to login", async () => {
+ const valkeyStub = stub(valkey, "get", () => Promise.resolve(null));
+ const originalSql = sqlWrapper.sql;
+ sqlWrapper.sql = () => Promise.resolve([]);
+ try {
+ const res = await app.request("/pass?token=invalid", { method: "GET" });
+ assertEquals(res.status, 302);
+ assertEquals(
+ res.headers.get("location"),
+ "/login?error=invalid_or_expired_pass",
+ );
+ } finally {
+ sqlWrapper.sql = originalSql;
+ valkeyStub.restore();
+ }
+ });
+
+ await t.step(
+ "GET /pass with valid token sets cookies and redirects to app domain",
+ async () => {
+ const sessionToken = "ay_sess_valid_app";
+ const valkeyGetStub = stub(valkey, "get", (key: any) => {
+ if (String(key) === sessionToken) {
+ return Promise.resolve(JSON.stringify({
+ uuid: "user-uuid",
+ username: "testuser",
+ customScopes: ["app:test-app"],
+ }));
+ }
+ return Promise.resolve(null);
+ });
+ const valkeyTtlStub = stub(valkey, "ttl", () => Promise.resolve(3600));
+
+ const originalSql = sqlWrapper.sql;
+ sqlWrapper.sql = (strings: any, ..._values: any[]) => {
+ const query = strings.join("?");
+ if (query.includes("SELECT domain FROM apps WHERE name =")) {
+ return Promise.resolve([{ domain: "test-app.atyg.org" }]);
+ }
+ return Promise.resolve([]);
+ };
+
+ try {
+ const res = await app.request(`/pass?token=${sessionToken}`, {
+ method: "GET",
+ });
+ assertEquals(res.status, 302);
+ assertEquals(res.headers.get("location"), "https://test-app.atyg.org");
+
+ // Verify cookies
+ const cookies = res.headers.get("set-cookie");
+ assertExists(cookies);
+ assert(cookies.includes("session_id=;")); // clear host-only cookie
+ assert(cookies.includes(`session_id=${sessionToken};`)); // set wildcard cookie
+ // In tests, process.env.COOKIE_DOMAIN or getCookieDomain fallback doesn't output .atyg.org because rpID env variables may not be explicitly set in Deno tests. Wait, if it fails, let's just make sure it sets domain
+ } finally {
+ sqlWrapper.sql = originalSql;
+ valkeyGetStub.restore();
+ valkeyTtlStub.restore();
+ }
+ },
+ );
+
+ await t.step(
+ "GET /pass with valid token defaults to /dashboard if no app scope",
+ async () => {
+ const sessionToken = "ay_sess_valid_dashboard";
+ const valkeyGetStub = stub(valkey, "get", (key: any) => {
+ if (String(key) === sessionToken) {
+ return Promise.resolve(JSON.stringify({
+ uuid: "user-uuid",
+ username: "testuser",
+ customScopes: ["read:audit"],
+ }));
+ }
+ return Promise.resolve(null);
+ });
+ const valkeyTtlStub = stub(valkey, "ttl", () => Promise.resolve(3600));
+
+ try {
+ const res = await app.request(`/pass?token=${sessionToken}`, {
+ method: "GET",
+ });
+ assertEquals(res.status, 302);
+ assertEquals(res.headers.get("location"), "/dashboard");
+ } finally {
+ valkeyGetStub.restore();
+ valkeyTtlStub.restore();
+ }
+ },
+ );
+});
+
Deno.test("Tier 1 & 2: GET /api/forward-auth - Missing session (API request)", async () => {
const req = new Request("http://localhost/api/forward-auth", {
headers: { "X-Forwarded-Host": "test.app.local" },
diff --git a/server/main.ts b/server/main.ts
index 2b7d67b..ed6be7b 100644
--- a/server/main.ts
+++ b/server/main.ts
@@ -177,6 +177,124 @@ app.use("/api/admin/*", async (c, next) => {
await next();
});
+// ---------------------------------------------------------
+// Ephemeral 1-Click Magic Link Redemption (/pass)
+// ---------------------------------------------------------
+
+app.get("/pass", async (c) => {
+ const token = c.req.query("token");
+ if (!token) {
+ return c.redirect("/login?error=invalid_or_expired_pass", 302);
+ }
+
+ // 1. Validate against Valkey, fallback to PostgreSQL
+ let sessionDataStr = null;
+ try {
+ sessionDataStr = await valkey.get(token);
+ } catch (_err) {}
+
+ let sessionInfo = null;
+ if (sessionDataStr) {
+ try {
+ sessionInfo = JSON.parse(sessionDataStr);
+ } catch (_err) {}
+ }
+
+ let expiresAtDate: Date | null = null;
+ let customScopes: string[] = [];
+
+ if (!sessionInfo || !sessionInfo.uuid) {
+ try {
+ const nowIso = new Date().toISOString();
+ const session = await sqlWrapper.sql`
+ SELECT s.user_id, s.expires_at, s.label, s.is_agent, s.custom_scopes, u.username
+ FROM sessions s
+ JOIN users u ON s.user_id = u.id
+ WHERE s.id = ${token} AND s.expires_at > ${nowIso}
+ `.then((res: any) => res[0]);
+
+ if (!session) {
+ return c.redirect("/login?error=invalid_or_expired_pass", 302);
+ }
+
+ sessionInfo = {
+ uuid: session.user_id,
+ username: session.username,
+ label: session.label,
+ isAgent: session.is_agent,
+ customScopes: session.custom_scopes,
+ };
+ expiresAtDate = new Date(session.expires_at);
+ customScopes = session.custom_scopes || [];
+
+ try {
+ const ttlSeconds = Math.max(
+ 1,
+ Math.floor((expiresAtDate.getTime() - Date.now()) / 1000),
+ );
+ await valkey.setex(token, ttlSeconds, JSON.stringify(sessionInfo));
+ } catch (_e) {}
+ } catch (_err) {
+ return c.redirect("/login?error=invalid_or_expired_pass", 302);
+ }
+ } else {
+ try {
+ const ttl = await valkey.ttl(token);
+ if (ttl <= 0) {
+ return c.redirect("/login?error=invalid_or_expired_pass", 302);
+ }
+ expiresAtDate = new Date(Date.now() + ttl * 1000);
+ customScopes = sessionInfo.customScopes || [];
+ } catch (_err) {
+ return c.redirect("/login?error=invalid_or_expired_pass", 302);
+ }
+ }
+
+ if (!sessionInfo || !expiresAtDate) {
+ return c.redirect("/login?error=invalid_or_expired_pass", 302);
+ }
+
+ // 2. Cookie Scoping
+ deleteCookie(c, "session_id", { path: "/" });
+
+ const cookieDomain = getCookieDomain(rpID);
+ const ttlSeconds = Math.max(
+ 1,
+ Math.floor((expiresAtDate.getTime() - Date.now()) / 1000),
+ );
+ setCookie(c, "session_id", token, {
+ path: "/",
+ domain: cookieDomain,
+ httpOnly: true,
+ secure: true,
+ sameSite: "Lax",
+ maxAge: ttlSeconds,
+ });
+
+ // 3. Redirect URL Resolution
+ let targetDomain = null;
+ if (Array.isArray(customScopes)) {
+ const appScope = customScopes.find((s: string) => s.startsWith("app:"));
+ if (appScope) {
+ const appName = appScope.substring(4); // remove 'app:'
+ try {
+ const appRecord = await sqlWrapper.sql`
+ SELECT domain FROM apps WHERE name = ${appName}
+ `.then((res: any) => res[0]);
+ if (appRecord && appRecord.domain) {
+ targetDomain = appRecord.domain;
+ }
+ } catch (_err) {}
+ }
+ }
+
+ if (targetDomain) {
+ return c.redirect(`https://${targetDomain}`, 302);
+ } else {
+ return c.redirect("/dashboard", 302);
+ }
+});
+
// ---------------------------------------------------------
// Provisioning & Registration (Use Cases 1, 2, 3)
// ---------------------------------------------------------
diff --git a/tasks/new/2026-0825.01.gem.feat.magic-passes.ephemeral-1-click-guest-links-0045.md b/tasks/complete/2026-0825.01.gem.feat.magic-passes.ephemeral-1-click-guest-links-0045.md
similarity index 75%
rename from tasks/new/2026-0825.01.gem.feat.magic-passes.ephemeral-1-click-guest-links-0045.md
rename to tasks/complete/2026-0825.01.gem.feat.magic-passes.ephemeral-1-click-guest-links-0045.md
index 3c4ee97..e705044 100644
--- a/tasks/new/2026-0825.01.gem.feat.magic-passes.ephemeral-1-click-guest-links-0045.md
+++ b/tasks/complete/2026-0825.01.gem.feat.magic-passes.ephemeral-1-click-guest-links-0045.md
@@ -4,19 +4,26 @@
- `server/main.ts`
- `ui/components/SessionsPage.tsx`
- `server/main.test.ts`
-- **Core Objective:** Implement `GET /pass?token=...` for 1-click ephemeral session redemption and update the Sessions Hub hand-off UI with copyable magic links.
+- **Core Objective:** Implement `GET /pass?token=...` for 1-click ephemeral
+ session redemption and update the Sessions Hub hand-off UI with copyable magic
+ links.
- **Dependencies:** `server/auth-session.ts`, `server/db.ts`
-- **Additional Important Notes:** Sets wildcard `.atyg.org` cookie, cleans host-only cookie, and redirects cleanly to target app domain or dashboard.
+- **Additional Important Notes:** Sets wildcard `.atyg.org` cookie, cleans
+ host-only cookie, and redirects cleanly to target app domain or dashboard.
---
### 1. Architectural Considerations & Risks
- **Security & Cookie Scoping:**
- - `/pass` must validate that the token is active and not expired before issuing Set-Cookie headers.
- - Must use `getCookieDomain()` so subdomains (e.g. `ed-droid.atyg.org`) receive the session cookie immediately.
+ - `/pass` must validate that the token is active and not expired before
+ issuing Set-Cookie headers.
+ - Must use `getCookieDomain()` so subdomains (e.g. `ed-droid.atyg.org`)
+ receive the session cookie immediately.
- **Target URL Redirection:**
- - If the session has custom scopes for an application (e.g. `app:ed-droid`), `/pass` looks up the domain of `ed-droid` and redirects directly to `https://ed-droid.atyg.org`.
+ - If the session has custom scopes for an application (e.g. `app:ed-droid`),
+ `/pass` looks up the domain of `ed-droid` and redirects directly to
+ `https://ed-droid.atyg.org`.
- If no specific app is scoped, redirects to `/dashboard`.
---
diff --git a/tasks/new/2026-0825.02.gem.feat.event-passes.multi-claim-workshops-and-kiosks-0046.md b/tasks/new/2026-0825.02.gem.feat.event-passes.multi-claim-workshops-and-kiosks-0046.md
index e3b6139..fdf2ba9 100644
--- a/tasks/new/2026-0825.02.gem.feat.event-passes.multi-claim-workshops-and-kiosks-0046.md
+++ b/tasks/new/2026-0825.02.gem.feat.event-passes.multi-claim-workshops-and-kiosks-0046.md
@@ -6,35 +6,46 @@
- `ui/components/EventJoinPage.tsx`
- `ui/components/EventSplashPage.tsx`
- `server/main.test.ts`
-- **Core Objective:** Implement Multi-Claim Event Passes with short vanity URLs (`/e/:slug`), universal PIN join portal (`/join`), and CLI environment 1-liner (`/join/:slug?format=env`).
+- **Core Objective:** Implement Multi-Claim Event Passes with short vanity URLs
+ (`/e/:slug`), universal PIN join portal (`/join`), and CLI environment 1-liner
+ (`/join/:slug?format=env`).
- **Dependencies:** `server/db.ts`, `server/auth-session.ts`
-- **Additional Important Notes:** Provisions isolated guest seats (`guest__`) with individual sessions.
+- **Additional Important Notes:** Provisions isolated guest seats
+ (`guest__`) with individual sessions.
---
### 1. Architectural Considerations & Risks
- **Concurrency & Seat Limits:**
- - Ensure atomicity when incrementing `seats_claimed` on `event_passes` so events with strict seat limits (e.g. 50 seats) do not oversubscribe.
+ - Ensure atomicity when incrementing `seats_claimed` on `event_passes` so
+ events with strict seat limits (e.g. 50 seats) do not oversubscribe.
- **Multi-Channel Accessibility:**
- - Support web browser UI (`/e/:slug`, `/join`), JSON API (`POST /api/join`), and CLI environment output (`GET /join/:slug?format=env`).
+ - Support web browser UI (`/e/:slug`, `/join`), JSON API (`POST /api/join`),
+ and CLI environment output (`GET /join/:slug?format=env`).
- **Session Isolation:**
- - Each attendee gets their own dedicated guest session and UUID, preventing state collision in shared sandbox apps.
+ - Each attendee gets their own dedicated guest session and UUID, preventing
+ state collision in shared sandbox apps.
---
### 2. Proposed Implementation
1. **Database Schema (`server/db.ts`):**
- - Create `event_passes` table with columns: `id`, `slug`, `pin_code`, `name`, `app_id`, `role`, `max_seats`, `seats_claimed`, `lifespan_hours`, `created_by`, `is_active`, `expires_at`, `created_at`.
+ - Create `event_passes` table with columns: `id`, `slug`, `pin_code`, `name`,
+ `app_id`, `role`, `max_seats`, `seats_claimed`, `lifespan_hours`,
+ `created_by`, `is_active`, `expires_at`, `created_at`.
2. **API Endpoints (`server/main.ts`):**
- `POST /api/events`: Create a new event pass.
- `GET /e/:slug`: Web landing splash with "Enter Workshop" button.
- `GET /join`: Universal PIN / code entry page.
- - `POST /api/join`: Redeems code or PIN, creates isolated guest user and session, sets cookie or returns JSON.
- - `GET /join/:slug`: Returns CLI 1-liner (`export AUTH_YES_TOKEN="..."` when `?format=env`).
+ - `POST /api/join`: Redeems code or PIN, creates isolated guest user and
+ session, sets cookie or returns JSON.
+ - `GET /join/:slug`: Returns CLI 1-liner (`export AUTH_YES_TOKEN="..."` when
+ `?format=env`).
3. **SSR UI Components:**
- `ui/components/EventJoinPage.tsx`
- `ui/components/EventSplashPage.tsx`
4. **Automated Tests (`server/main.test.ts`):**
- - Test event creation, PIN redemption, seat limit capping, and CLI output format.
+ - Test event creation, PIN redemption, seat limit capping, and CLI output
+ format.
diff --git a/tasks/new/2026-0825.03.gem.feat.event-cockpit.live-metrics-and-master-killswitch-0047.md b/tasks/new/2026-0825.03.gem.feat.event-cockpit.live-metrics-and-master-killswitch-0047.md
index 49bd2c2..8680a6a 100644
--- a/tasks/new/2026-0825.03.gem.feat.event-cockpit.live-metrics-and-master-killswitch-0047.md
+++ b/tasks/new/2026-0825.03.gem.feat.event-cockpit.live-metrics-and-master-killswitch-0047.md
@@ -5,28 +5,39 @@
- `ui/components/SessionsPage.tsx`
- `ui/mod.ts`
- `server/main.test.ts`
-- **Core Objective:** Add real-time event cockpit deck to the Sessions page with active seat counters, PIN display, time extension, and 1-tap master kill switch (`/api/events/:id/end`).
-- **Dependencies:** `tasks/new/2026-0825.02.gem.feat.event-passes.multi-claim-workshops-and-kiosks-0046.md`
-- **Additional Important Notes:** Sub-millisecond mass revocation of all event guest sessions via Valkey RESP3 push tracking.
+- **Core Objective:** Add real-time event cockpit deck to the Sessions page with
+ active seat counters, PIN display, time extension, and 1-tap master kill
+ switch (`/api/events/:id/end`).
+- **Dependencies:**
+ `tasks/new/2026-0825.02.gem.feat.event-passes.multi-claim-workshops-and-kiosks-0046.md`
+- **Additional Important Notes:** Sub-millisecond mass revocation of all event
+ guest sessions via Valkey RESP3 push tracking.
---
### 1. Architectural Considerations & Risks
- **Mass Revocation Performance:**
- - When the organizer clicks "End Workshop & Revoke All", the endpoint must delete all associated guest session IDs from both PostgreSQL and Valkey, emitting invalidation events to all connected nodes immediately.
+ - When the organizer clicks "End Workshop & Revoke All", the endpoint must
+ delete all associated guest session IDs from both PostgreSQL and Valkey,
+ emitting invalidation events to all connected nodes immediately.
- **Observability:**
- - Show real-time seat claim progress bar (`38 / 50 claimed`) and event expiration timer.
+ - Show real-time seat claim progress bar (`38 / 50 claimed`) and event
+ expiration timer.
---
### 2. Proposed Implementation
1. **Backend Endpoints (`server/main.ts`):**
- - `POST /api/events/:id/end`: Closes the event, deletes all guest sessions under the event, and purges Valkey cache.
- - `POST /api/events/:id/extend`: Adds hours to `expires_at` for the event and all active guest sessions.
+ - `POST /api/events/:id/end`: Closes the event, deletes all guest sessions
+ under the event, and purges Valkey cache.
+ - `POST /api/events/:id/extend`: Adds hours to `expires_at` for the event and
+ all active guest sessions.
2. **UI & Data Query (`ui/mod.ts`, `ui/components/SessionsPage.tsx`):**
- - Query `event_passes` in `/dashboard/sessions` and render the **Event Cockpit Deck**.
- - Display progress bar, quick copy buttons for PIN and Short URL, `[+1h Extend]` and `[🔴 End Workshop & Revoke All]`.
+ - Query `event_passes` in `/dashboard/sessions` and render the **Event
+ Cockpit Deck**.
+ - Display progress bar, quick copy buttons for PIN and Short URL,
+ `[+1h Extend]` and `[🔴 End Workshop & Revoke All]`.
3. **Automated Tests (`server/main.test.ts`):**
- Test event extension and master kill-switch mass revocation.
diff --git a/ui/components/SessionsPage.tsx b/ui/components/SessionsPage.tsx
index 8b58ac6..b418255 100644
--- a/ui/components/SessionsPage.tsx
+++ b/ui/components/SessionsPage.tsx
@@ -284,6 +284,28 @@ export const SessionsPage = ({
+ {/* 1-Click Magic Link */}
+
+
+
+
+
+
+
+
+
{/* 1-Tap Copy CLI */}