diff --git a/scratch/add_data_ignore.ts b/scratch/add_data_ignore.ts
new file mode 100644
index 0000000..55c8c6a
--- /dev/null
+++ b/scratch/add_data_ignore.ts
@@ -0,0 +1,14 @@
+import { readFileSync, writeFileSync } from "fs";
+
+let content = readFileSync("src/features/events/fragments.tsx", "utf-8");
+
+// Add data-ignore to buttons with onclick="copyText..."
+content = content.replace(/onclick=\{`copyText/g, 'data-ignore onclick={`copyText');
+content = content.replace(/onclick=\{`event\.stopPropagation\(\); copyText/g, 'data-ignore onclick={`event.stopPropagation(); copyText');
+content = content.replace(/class="btn-outline extend-event-btn"/g, 'class="btn-outline extend-event-btn" data-on-click={`@post("/api/events/${event.id}/extend")`}');
+content = content.replace(/class="btn-danger end-event-btn"/g, 'class="btn-danger end-event-btn" data-on-click={`@post("/api/events/${event.id}/end")`}');
+content = content.replace(/onclick=\{`openAttendeesDrawer/g, 'data-on-click={`@get("/api/events/${event.id}/attendees")`} onclick={`openAttendeesDrawer');
+content = content.replace(/onclick=\{`expandSeats/g, 'data-on-click={`@post("/api/events/${event.id}/expand")`} onclick={`expandSeats');
+content = content.replace(/onclick=\{`rotatePin/g, 'data-on-click={`@post("/api/events/${event.id}/rotate-pin")`} onclick={`rotatePin');
+
+writeFileSync("src/features/events/fragments.tsx", content);
diff --git a/scratch/add_data_ignore_2.ts b/scratch/add_data_ignore_2.ts
new file mode 100644
index 0000000..9ee72b0
--- /dev/null
+++ b/scratch/add_data_ignore_2.ts
@@ -0,0 +1,8 @@
+import { readFileSync, writeFileSync } from "fs";
+
+let content = readFileSync("src/features/events/fragments.tsx", "utf-8");
+
+content = content.replace(/onclick="copyEventHandoff/g, 'data-ignore onclick="copyEventHandoff');
+content = content.replace(/id="submitEventBtn"/g, 'id="submitEventBtn" data-on-click="@post(\'/api/events\')"');
+
+writeFileSync("src/features/events/fragments.tsx", content);
diff --git a/scratch/add_data_ignore_sessions.ts b/scratch/add_data_ignore_sessions.ts
new file mode 100644
index 0000000..4684cf4
--- /dev/null
+++ b/scratch/add_data_ignore_sessions.ts
@@ -0,0 +1,8 @@
+import { readFileSync, writeFileSync } from "fs";
+let content = readFileSync("src/features/sessions/fragments.tsx", "utf-8");
+
+content = content.replace(/onclick="copyHandoff/g, 'data-ignore onclick="copyHandoff');
+content = content.replace(/class="btn-danger revoke-btn"/g, 'class="btn-danger revoke-btn" data-on-click={`@delete("/api/sessions/${session.id}")`}');
+content = content.replace(/onclick=\{`extendSession\('\$\{session.id\}', 1\)`\}/g, 'data-on-click={`@post("/api/sessions/${session.id}/extend")`}');
+
+writeFileSync("src/features/sessions/fragments.tsx", content);
diff --git a/scratch/clean.ts b/scratch/clean.ts
new file mode 100644
index 0000000..32b0f5f
--- /dev/null
+++ b/scratch/clean.ts
@@ -0,0 +1,8 @@
+import { readFileSync, writeFileSync } from "fs";
+let content = readFileSync("src/features/sessions/fragments.tsx", "utf-8");
+content = content.replace(" );\n};", " );\n};"); // no-op just to make sure
+let lines = content.split('\n');
+lines[466] = ' );';
+lines[467] = '};';
+lines[468] = '';
+writeFileSync("src/features/sessions/fragments.tsx", lines.join('\n'));
diff --git a/scratch/fix_imports.ts b/scratch/fix_imports.ts
new file mode 100644
index 0000000..b5ce8a5
--- /dev/null
+++ b/scratch/fix_imports.ts
@@ -0,0 +1,15 @@
+import { readFileSync, writeFileSync } from "fs";
+
+let content = readFileSync("src/features/events/routes.tsx", "utf-8");
+
+content = content.replace('../../core/auth_session.ts', '../../../server/auth-session.ts');
+content = content.replace('../../core/middleware.ts', '../../../server/middleware.ts');
+content = content.replace('../../core/audit.ts', '../../../server/audit.ts');
+content = content.replace('../../core/ratelimit.ts', '../../../server/ratelimit.ts');
+
+content = content.replace(/data: errorHtml/g, 'data: await errorHtml'); // Since it might be a promise of HtmlEscapedString
+
+// Fix implicitly any array
+content = content.replace(/const events = \[\];/g, 'const events: any[] = [];');
+
+writeFileSync("src/features/events/routes.tsx", content);
diff --git a/scratch/fix_test.ts b/scratch/fix_test.ts
new file mode 100644
index 0000000..31fa1a8
--- /dev/null
+++ b/scratch/fix_test.ts
@@ -0,0 +1,4 @@
+import { readFileSync, writeFileSync } from "fs";
+let content = readFileSync("src/features/events/events.test.ts", "utf-8");
+content = content.replace('assertStringIncludes(html, "data-on-submit=\\\"@post(\'/api/join\')\\\"");', 'assertStringIncludes(html, "data-on-submit=\\\"@post('/api/join')\\\"");');
+writeFileSync("src/features/events/events.test.ts", content);
diff --git a/scratch/replace.ts b/scratch/replace.ts
new file mode 100644
index 0000000..64d5893
--- /dev/null
+++ b/scratch/replace.ts
@@ -0,0 +1,23 @@
+import { readFileSync, writeFileSync } from "fs";
+
+let content = readFileSync("src/features/events/fragments.tsx", "utf-8");
+
+const workshopDrawer = readFileSync("ui/components/sessions/WorkshopDrawer.tsx", "utf-8")
+ .replace('export const WorkshopDrawer = ({ apps }: { apps: any[] }) => {', '')
+ .replace(/}$/, '');
+
+const eventGuestsDrawer = readFileSync("ui/components/sessions/EventGuestsDrawer.tsx", "utf-8")
+ .replace('export const EventGuestsDrawer = () => {', '')
+ .replace(/}$/, '');
+
+content = content.replace(
+ 'export const WorkshopPassDrawerFragment = ({ apps }: { apps: any[] }) => {\n return
Workshop Pass Drawer Placeholder
\n}',
+ 'export const WorkshopPassDrawerFragment = ({ apps }: { apps: any[] }) => {' + workshopDrawer + '}'
+);
+
+content = content.replace(
+ 'export const GuestDrawerAttendeesFragment = () => {\n return Guest Drawer Attendees Placeholder
\n}',
+ 'export const GuestDrawerAttendeesFragment = () => {' + eventGuestsDrawer + '}'
+);
+
+writeFileSync("src/features/events/fragments.tsx", content);
diff --git a/scratch/replace_sessions.ts b/scratch/replace_sessions.ts
new file mode 100644
index 0000000..7282cca
--- /dev/null
+++ b/scratch/replace_sessions.ts
@@ -0,0 +1,23 @@
+import { readFileSync, writeFileSync } from "fs";
+
+let content = readFileSync("src/features/sessions/fragments.tsx", "utf-8");
+
+const directPassDrawer = readFileSync("ui/components/sessions/DirectPassDrawer.tsx", "utf-8")
+ .replace('export const DirectPassDrawer = ({ apps = [], isAdmin = false }: { apps?: any[]; isAdmin?: boolean }) => {', '')
+ .replace(/}$/, '');
+
+const scopeModal = readFileSync("ui/components/sessions/ScopeModal.tsx", "utf-8")
+ .replace('export const ScopeModal = ({ apps }: { apps: any[] }) => {', '')
+ .replace(/}$/, '');
+
+content = content.replace(
+ 'export const DirectPassDrawerFragment = ({ apps = [], isAdmin = false }: { apps?: any[]; isAdmin?: boolean }) => {\n// We will insert the content of DirectPassDrawer and adapt it to Datastar\n return DirectPassDrawerFragment Placeholder
\n}',
+ 'export const DirectPassDrawerFragment = ({ apps = [], isAdmin = false }: { apps?: any[]; isAdmin?: boolean }) => {' + directPassDrawer + '}'
+);
+
+content = content.replace(
+ 'export const ScopeModalFragment = ({ apps = [] }: { apps?: any[] }) => {\n return ScopeModalFragment Placeholder
\n}',
+ 'export const ScopeModalFragment = ({ apps = [] }: { apps?: any[] }) => {' + scopeModal + '}'
+);
+
+writeFileSync("src/features/sessions/fragments.tsx", content);
diff --git a/scratch/replace_sessions2.ts b/scratch/replace_sessions2.ts
new file mode 100644
index 0000000..5b2bb3e
--- /dev/null
+++ b/scratch/replace_sessions2.ts
@@ -0,0 +1,14 @@
+import { readFileSync, writeFileSync } from "fs";
+let content = readFileSync("src/features/sessions/fragments.tsx", "utf-8");
+
+const sessionDeck = readFileSync("ui/components/sessions/SessionDeck.tsx", "utf-8")
+ .replace('export const SessionDeck = (\n { sessions, currentSessionId }: {\n sessions: any[];\n currentSessionId: string;\n },\n) => {', '')
+ .replace('export const SessionDeck = (\n { sessions, currentSessionId }: {\n sessions: any[];\n currentSessionId: string;\n }\n) => {', '')
+ .replace(/}$/, '');
+
+content = content.replace(
+ 'export const SessionDeckFragment = ({ sessions, currentSessionId }: { sessions: any[], currentSessionId: string }) => {\n return SessionDeckFragment Placeholder
\n}',
+ 'export const SessionDeckFragment = ({ sessions, currentSessionId }: { sessions: any[], currentSessionId: string }) => {' + sessionDeck + '}'
+);
+
+writeFileSync("src/features/sessions/fragments.tsx", content);
diff --git a/src/features/events/events.test.ts b/src/features/events/events.test.ts
new file mode 100644
index 0000000..d584819
--- /dev/null
+++ b/src/features/events/events.test.ts
@@ -0,0 +1,32 @@
+import { assertEquals, assertStringIncludes } from "jsr:@std/assert@1";
+import { eventsRoutes } from "./routes.tsx";
+import { Hono } from "jsr:@hono/hono@4";
+import * as Queries from "./queries.ts";
+
+// Mocking authenticated user logic is complex in a simple pure unit test without a mock DB,
+// so we'll test the public join page for HTML response.
+
+Deno.test("[Events] GET /join returns EventJoinPage HTML", async () => {
+ const req = new Request("http://localhost/join");
+ const res = await eventsRoutes.fetch(req);
+
+ assertEquals(res.status, 200);
+ const html = await res.text();
+ assertStringIncludes(html, "Join Event or Workshop");
+ assertStringIncludes(html, "data-on-submit=\"@post('/api/join')\"");
+});
+
+Deno.test("[Events] POST /api/join without code returns HTML error fragment", async () => {
+ const req = new Request("http://localhost/api/join", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({})
+ });
+
+ // We expect it to fail early in validation and return an HTML error fragment
+ const res = await eventsRoutes.fetch(req);
+ assertEquals(res.status, 400);
+ const html = await res.text();
+ assertStringIncludes(html, "id=\"status-banner\"");
+ assertStringIncludes(html, "Event code or PIN is required");
+});
diff --git a/src/features/events/fragments.tsx b/src/features/events/fragments.tsx
new file mode 100644
index 0000000..e870de0
--- /dev/null
+++ b/src/features/events/fragments.tsx
@@ -0,0 +1,910 @@
+export const EventCockpitDeckFragment = ({ eventPasses }: { eventPasses: any[] }) => {
+ if (!eventPasses || eventPasses.length === 0) return null;
+
+ return (
+
+
+
+ Events
+
+
+
+ ๐๏ธ Grid
+
+
+ ๐ Compact
+
+
+
+
+
+ {eventPasses.map((event) => {
+ const expDate = new Date(event.expires_at);
+ const timeString = expDate.toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+
+ return (
+
+
+
+ {/* Progress Bar for Seats */}
+
+
+ Seats Claimed
+
+ {event.seats_claimed} /{" "}
+ {event.max_seats === 0 ? "โ" : event.max_seats}
+
+
+
+
0
+ ? Math.min(
+ (event.seats_claimed / event.max_seats) * 100,
+ 100,
+ )
+ : 100
+ }%;`}
+ >
+
+
+
+
+ {/* Quick Copy Snippets */}
+
+
+
+ PIN:
+
+
+ {event.pin_code}
+
+
+ Copy
+
+
+
+
+ Link:
+
+
+ /e/{event.slug}
+
+
+ Copy
+
+
+
+
+
+
+ CLI:
+
+
+ curl -sSL {Deno.env.get("RP_ID")
+ ? `https://${Deno.env.get("RP_ID")}`
+ : ""}/join/{event.slug}?format=env | source /dev/stdin
+
+
+ Copy
+
+
+ [ โพ ]
+
+
+
+
+
+
+ {/* Cockpit Actions */}
+
+
+
+ ๐ฅ Manage Guests ({event.seats_claimed})
+
+
+ +5 Seats
+
+
+
+
+ ๐ Rotate Credentials
+
+
+ +1h Extend
+
+
+ End Event
+
+
+
+
+ {/* Compact Actions (only visible in compact mode) */}
+
+
+
+ PIN:{" "}
+ {event.pin_code}
+ {" "}
+ (Copy)
+
+
+ Link (Copy)
+
+
+ CLI (Copy)
+
+
+
+
+ ๐ฅ Guests ({event.seats_claimed})
+
+
+ ๐ +1h
+
+
+
+
+ );
+ })}
+
+
+
+ );
+};
+
+export const WorkshopPassDrawerFragment = ({ apps }: { apps: any[] }) => {
+ return (
+
+
+
+ Generate a shared event pass with a universal 6-digit PIN, vanity link
+ (/e/slug), and CLI 1-liner for
+ workshops, hackathons, and multi-user kiosks.
+
+
+
+
+
+ {/* Event Launch Success State */}
+
+
+
+ ๐๏ธ
+
+ Workshop Pass Live!
+
+
+
+ Active Now
+
+
+
+
+ Share the 6-digit PIN, 1-click link, or CLI 1-liner with attendees.
+ You can monitor live claimed seats and trigger the master kill switch
+ from the Event Cockpit below.
+
+
+
+ {/* PIN Code */}
+
+
+ 6-Digit Universal PIN (for /join or Kiosk Entry)
+
+
+
+
+
+ Copy PIN
+
+
+
+
+ {/* 1-Click Vanity Link */}
+
+
+ Direct 1-Click Workshop Entrance URL
+
+
+
+
+
+ Copy URL
+
+
+
+
+ {/* CLI 1-Liner */}
+
+
+ Terminal 1-Liner (Environment Injector)
+
+
+
+
+
+ Copy 1-Liner
+
+
+
+
+
+
+ OK
+
+
+
+ );
+};
+
+export const GuestDrawerAttendeesFragment = () => {
+ return (
+
+
+
+
+
+ [Event Name] Guests
+
+
+ ×
+
+
+
+ Event Pass ยท 0 /{" "}
+ 0 Claimed Seats ยท{" "}
+ โณ 0h 0m left {" "}
+ ยท (--:-- )
+
+
+
+
+ {/* Populated dynamically via JS */}
+
+ Loading attendees...
+
+
+
+
+
+
+ );
+};
+
+export const EventJoinPageFragment = () => {
+ return (
+
+
+
+
Join Event or Workshop
+
+ Enter your event PIN code or slug to claim an instant sandbox seat.
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/features/events/queries.ts b/src/features/events/queries.ts
new file mode 100644
index 0000000..37ba1a6
--- /dev/null
+++ b/src/features/events/queries.ts
@@ -0,0 +1,149 @@
+import { sqlWrapper } from "../../core/db.ts";
+
+export async function rotateEventPin(eventId: string, userId: string, isGlobalAdmin: boolean): Promise {
+ const randPin = Math.floor(100000 + Math.random() * 900000).toString();
+ const formattedPin = `${randPin.slice(0,3)}-${randPin.slice(3)}`;
+
+ const result = await sqlWrapper.sql`
+ UPDATE event_passes
+ SET pin_code = ${formattedPin}
+ WHERE id = ${eventId}
+ AND (created_by = ${userId} OR ${isGlobalAdmin})
+ AND is_active = TRUE
+ RETURNING pin_code
+ `;
+
+ if (!result || result.length === 0) return null;
+ return result[0].pin_code;
+}
+
+export async function getEventAttendees(eventId: string, userId: string, isGlobalAdmin: boolean) {
+ const event = await sqlWrapper.sql`
+ SELECT slug FROM event_passes
+ WHERE id = ${eventId}
+ AND (created_by = ${userId} OR ${isGlobalAdmin})
+ `;
+
+ if (!event || event.length === 0) return null;
+
+ const slug = event[0].slug;
+ const guestPattern = `guest_${slug}_%`;
+
+ const users = await sqlWrapper.sql`
+ SELECT id, username, display_name, created_at
+ FROM users
+ WHERE username LIKE ${guestPattern}
+ ORDER BY created_at DESC
+ `;
+
+ return users;
+}
+
+export async function getEventBySlugOrPin(code: string) {
+ const rawNormalized = code.trim().toLowerCase();
+ const pinNormalized = code.trim().replace(/[-\s]/g, "");
+
+ const eventLookup = await sqlWrapper.sql`
+ SELECT * FROM event_passes
+ WHERE (LOWER(slug) = ${rawNormalized} OR REPLACE(pin_code, '-', '') = ${pinNormalized})
+ AND is_active = TRUE
+ AND (expires_at IS NULL OR expires_at > NOW())
+ `;
+
+ if (!eventLookup || eventLookup.length === 0) return null;
+ return eventLookup[0];
+}
+
+export async function incrementEventSeats(eventId: string) {
+ const updateResult = await sqlWrapper.sql`
+ UPDATE event_passes
+ SET seats_claimed = seats_claimed + 1
+ WHERE id = ${eventId}
+ AND (max_seats = 0 OR seats_claimed < max_seats)
+ RETURNING *
+ `;
+ if (!updateResult || updateResult.length === 0) return null;
+ return updateResult[0];
+}
+
+export async function createGuestUser(guestUuid: string, username: string, displayName: string, eventId: string) {
+ await sqlWrapper.sql`
+ INSERT INTO users (id, username, display_name, account_status, event_pass_id)
+ VALUES (${guestUuid}, ${username}, ${displayName}, 'guest', ${eventId})
+ ON CONFLICT DO NOTHING
+ `;
+}
+
+export async function getAppById(appId: string) {
+ const apps = await sqlWrapper.sql`SELECT name, domain FROM apps WHERE id = ${appId}`;
+ if (apps.length > 0) return apps[0];
+ return null;
+}
+
+export async function endEvent(eventId: string, userId: string, isGlobalAdmin: boolean) {
+ const result = await sqlWrapper.sql`
+ UPDATE event_passes
+ SET is_active = FALSE
+ WHERE id = ${eventId}
+ AND (created_by = ${userId} OR ${isGlobalAdmin})
+ RETURNING slug
+ `;
+ if (!result || result.length === 0) return null;
+ return result[0].slug;
+}
+
+export async function createEventSession(sessionId: string, userId: string, label: string, customScopes: string[], expiresAt: Date) {
+ await sqlWrapper.sql`
+ INSERT INTO sessions (id, user_id, label, is_agent, custom_scopes, expires_at)
+ VALUES (${sessionId}, ${userId}, ${label}, false, ${customScopes}, ${expiresAt})
+ `;
+}
+
+export async function expandEventSeats(eventId: string, addSeats: number, userId: string, isGlobalAdmin: boolean) {
+ const eventResult = await sqlWrapper.sql`
+ UPDATE event_passes
+ SET max_seats = max_seats + ${addSeats}
+ WHERE id = ${eventId}
+ AND (created_by = ${userId} OR ${isGlobalAdmin})
+ AND is_active = TRUE
+ RETURNING max_seats
+ `;
+ if (!eventResult || eventResult.length === 0) return null;
+ return eventResult[0].max_seats;
+}
+
+export async function createEventPass(
+ name: string,
+ slug: string,
+ pinCode: string,
+ appId: string | null,
+ role: string,
+ maxSeats: number,
+ lifespanHours: number,
+ userId: string,
+ expiresAt: Date
+) {
+ const result = await sqlWrapper.sql`
+ INSERT INTO event_passes (name, slug, pin_code, app_id, role, max_seats, lifespan_hours, created_by, expires_at)
+ VALUES (${name}, ${slug}, ${pinCode}, ${appId || null}, ${role}, ${Number(maxSeats)}, ${Number(lifespanHours)}, ${userId}, ${expiresAt.toISOString()})
+ RETURNING *
+ `;
+ if (!result || result.length === 0) return null;
+ return result[0];
+}
+
+export async function getEventById(eventId: string) {
+ const result = await sqlWrapper.sql`
+ SELECT * FROM event_passes WHERE id = ${eventId}
+ `;
+ if (!result || result.length === 0) return null;
+ return result[0];
+}
+
+export async function getEventBySlug(slug: string) {
+ const result = await sqlWrapper.sql`
+ SELECT * FROM event_passes WHERE slug = ${slug} AND is_active = TRUE
+ `;
+ if (!result || result.length === 0) return null;
+ return result[0];
+}
diff --git a/src/features/events/routes.tsx b/src/features/events/routes.tsx
new file mode 100644
index 0000000..3263658
--- /dev/null
+++ b/src/features/events/routes.tsx
@@ -0,0 +1,253 @@
+import { Hono } from "jsr:@hono/hono@4";
+import { getAuthenticatedUser, hasScope, isGlobalAdmin, getCookieDomain } from "../../../server/auth-session.ts";
+import { deleteCookie, setCookie } from "jsr:@hono/hono@4/cookie";
+import { encodeHex } from "jsr:@std/encoding@1/hex";
+import { getClientIp } from "../../../server/middleware.ts";
+import { valkey } from "../../core/valkey.ts";
+import { auditWrapper } from "../../../server/audit.ts";
+import { rateLimitWrapper } from "../../../server/ratelimit.ts";
+import { streamDatastar } from "../../core/sse_adapter.ts";
+import { renderErrorToastFragment } from "../../core/error_fragments.tsx";
+import * as Queries from "./queries.ts";
+import { EventCockpitDeckFragment, WorkshopPassDrawerFragment, GuestDrawerAttendeesFragment, EventJoinPageFragment } from "./fragments.tsx";
+
+export const eventsRoutes = new Hono();
+
+// We will implement routes here
+eventsRoutes.post("/api/events/:id/rotate-pin", async (c) => {
+ const user = await getAuthenticatedUser(c);
+ if (!user) return c.html(renderErrorToastFragment("Unauthorized"), 401);
+
+ if (!hasScope(user, "write:events")) {
+ return c.html(renderErrorToastFragment("Forbidden: Insufficient scopes"), 403);
+ }
+
+ const eventId = c.req.param("id");
+ const newPin = await Queries.rotateEventPin(eventId, user.userId, await isGlobalAdmin(user.userId));
+
+ if (!newPin) {
+ return c.html(renderErrorToastFragment("Event not found, inactive, or unauthorized"), 404);
+ }
+
+ // Refresh the cockpit deck fragment
+ const events: any[] = []; // TODO: fetch updated events list
+ return c.html( );
+});
+
+eventsRoutes.post("/api/events/:id/expand", async (c) => {
+ const user = await getAuthenticatedUser(c);
+ if (!user) return c.html(renderErrorToastFragment("Unauthorized"), 401);
+
+ if (!hasScope(user, "write:events")) {
+ return c.html(renderErrorToastFragment("Forbidden: Insufficient scopes"), 403);
+ }
+
+ const eventId = c.req.param("id");
+ let body = { addSeats: 5 };
+ if (c.req.header("content-type")?.includes("application/json")) {
+ body = await c.req.json().catch(() => ({ addSeats: 5 }));
+ }
+
+ const addSeats = Math.max(Number(body.addSeats) || 5, 1);
+ const maxSeats = await Queries.expandEventSeats(eventId, addSeats, user.userId, await isGlobalAdmin(user.userId));
+
+ if (maxSeats === null) {
+ return c.html(renderErrorToastFragment("Event not found, inactive, or unauthorized"), 404);
+ }
+
+ // Refresh cockpit deck
+ const events: any[] = []; // TODO: fetch updated events list
+ return c.html( );
+});
+
+eventsRoutes.post("/api/events/:id/end", async (c) => {
+ const user = await getAuthenticatedUser(c);
+ if (!user) return c.html(renderErrorToastFragment("Unauthorized"), 401);
+
+ if (!hasScope(user, "write:events")) {
+ return c.html(renderErrorToastFragment("Forbidden: Insufficient scopes"), 403);
+ }
+
+ const eventId = c.req.param("id");
+ const slug = await Queries.endEvent(eventId, user.userId, await isGlobalAdmin(user.userId));
+
+ if (!slug) {
+ return c.html(renderErrorToastFragment("Event not found or unauthorized"), 404);
+ }
+
+ // Revoke all sessions for this event
+ const guestPattern = `guest_${slug}_`;
+ // Iterate sessions and delete via valkey, db...
+
+ // Refresh cockpit deck
+ const events: any[] = []; // TODO: fetch updated events list
+ return c.html( );
+});
+eventsRoutes.post("/api/join", async (c) => {
+ let code = "";
+ if (
+ c.req.header("content-type")?.includes("application/x-www-form-urlencoded")
+ ) {
+ const fd = await c.req.formData();
+ code = (fd.get("code") as string) || "";
+ } else {
+ const body = await c.req.json().catch(() => ({}));
+ code = body.code || "";
+ }
+
+ if (!code || typeof code !== "string") {
+ return c.html(renderErrorToastFragment("Event code or PIN is required"), 400);
+ }
+
+ const clientIp = getClientIp(c);
+ const rateLimitKey = `ratelimit:join:fail:${clientIp}`;
+
+ if (await rateLimitWrapper.isRateLimited(rateLimitKey, 5, 60000)) {
+ return c.html(renderErrorToastFragment("Too Many Requests"), 429);
+ }
+
+ try {
+ const event = await Queries.getEventBySlugOrPin(code);
+
+ if (!event) {
+ await rateLimitWrapper.checkRateLimit(rateLimitKey, 5, 60000);
+ return c.html(renderErrorToastFragment("Invalid event code or workshop capacity reached"), 404);
+ }
+
+ const updatedEvent = await Queries.incrementEventSeats(String(event.id));
+
+ if (!updatedEvent) {
+ await rateLimitWrapper.checkRateLimit(rateLimitKey, 5, 60000);
+ return c.html(renderErrorToastFragment("Workshop capacity reached"), 404);
+ }
+
+ const guestUuid = crypto.randomUUID();
+ const eventShortId = String(updatedEvent.id).split("-")[0];
+ const username = `guest_${eventShortId}_${updatedEvent.seats_claimed}`;
+
+ await Queries.createGuestUser(guestUuid, username, updatedEvent.name + " Attendee", String(updatedEvent.id));
+
+ const randomBytes = crypto.getRandomValues(new Uint8Array(32));
+ const sessionId = `ay_sess_${encodeHex(randomBytes)}`;
+ const label = `${updatedEvent.name} Seat #${updatedEvent.seats_claimed}`;
+ const ttl = (Number(updatedEvent.lifespan_hours) || 3) * 3600;
+
+ let customScopes = ["guest", "trial"];
+ let appDomain = "";
+
+ if (updatedEvent.app_id) {
+ const app = await Queries.getAppById(String(updatedEvent.app_id));
+ if (app) {
+ customScopes = [`app:${app.name}`, updatedEvent.role || "viewer"];
+ appDomain = app.domain || "";
+ }
+ }
+
+ const expiresAt = new Date(Date.now() + ttl * 1000);
+
+ await Queries.createEventSession(sessionId, guestUuid, label, customScopes, expiresAt);
+
+ await valkey.setex(
+ sessionId,
+ ttl,
+ JSON.stringify({
+ uuid: guestUuid,
+ username,
+ account_status: "guest",
+ customScopes,
+ }),
+ );
+
+ auditWrapper.auditLog(guestUuid, "event_seat_claimed", updatedEvent.id, {
+ slug: updatedEvent.slug,
+ name: updatedEvent.name,
+ seatNumber: updatedEvent.seats_claimed,
+ method: "web",
+ }, getClientIp(c));
+
+ deleteCookie(c, "session_id", { path: "/" });
+ const rpID = Deno.env.get("RP_ID");
+ const cookieDomain = getCookieDomain(rpID);
+
+ setCookie(c, "session_id", sessionId, {
+ domain: cookieDomain,
+ path: "/",
+ httpOnly: true,
+ secure: true,
+ sameSite: "Lax",
+ maxAge: ttl,
+ });
+
+ const redirectUrl = appDomain ? `https://${appDomain}` : "/dashboard";
+
+ c.header("HX-Redirect", redirectUrl);
+ return c.html("");
+ } catch (e: any) {
+ console.error("[Events] Failed to join event:", e);
+ return c.html(renderErrorToastFragment("Failed to join event"), 500);
+ }
+});
+eventsRoutes.get("/api/events/:id/stream", async (c) => {
+ const user = await getAuthenticatedUser(c);
+ if (!user) {
+ return c.text("Unauthorized", 401);
+ }
+
+ if (!hasScope(user, "read:events")) {
+ return c.text("Forbidden", 403);
+ }
+
+ const eventId = c.req.param("id");
+
+ return streamDatastar(c, async (stream) => {
+ let subscriber: any;
+ try {
+ // initial state
+ const event = await Queries.getEventById(eventId);
+ if (event) {
+ await stream.write({
+ event: "datastar-fragment",
+ data: `${event.seats_claimed} `
+ });
+ }
+
+ // setup valkey subscriber for live seats broadcast
+ subscriber = valkey.duplicate();
+ await subscriber.subscribe(`event:seats:${eventId}`);
+
+ subscriber.on("message", async (channel: string, message: string) => {
+ if (stream.aborted) return;
+ try {
+ const parsed = JSON.parse(message);
+ await stream.write({
+ event: "datastar-fragment",
+ data: `${parsed.seats_claimed} `
+ });
+ } catch (err) {
+ // Ignore malformed messages
+ }
+ });
+
+ while (!stream.aborted) {
+ await stream.sleep(15000); // keep alive
+ }
+ } catch (err) {
+ console.error("[Events SSE] Stream error:", err);
+ if (!stream.aborted) {
+ const errorHtml = renderErrorToastFragment("Live updates connection lost.");
+ await stream.write({
+ event: "datastar-fragment",
+ data: await errorHtml
+ });
+ }
+ } finally {
+ if (subscriber) {
+ await subscriber.unsubscribe();
+ await subscriber.quit();
+ }
+ }
+ });
+});
+eventsRoutes.get("/join", (c) => {
+ return c.html( );
+});
diff --git a/src/features/sessions/fragments.tsx b/src/features/sessions/fragments.tsx
new file mode 100644
index 0000000..ab8917f
--- /dev/null
+++ b/src/features/sessions/fragments.tsx
@@ -0,0 +1,675 @@
+import { Hono } from "jsr:@hono/hono@4";
+
+export const SessionTableFragment = ({ sessions, currentSessionId }: { sessions: any[], currentSessionId: string }) => {
+ return (
+
+
+
+
+
+ Type & Label
+ Permissions
+ Activity
+ Expires
+ Actions
+
+
+
+ {sessions.length === 0
+ ? (
+
+
+ No active sessions found.
+
+
+ )
+ : (
+ sessions.map((session) => {
+ const isCurrent = session.id === currentSessionId;
+ const isAgent = !!session.is_agent;
+ const scopes = Array.isArray(session.custom_scopes)
+ ? session.custom_scopes
+ : [];
+
+ return (
+
+
+
+
+ {isAgent ? "๐" : isCurrent ? "๐ฑ" : "๐ป"}
+
+
+
+ {session.label ||
+ (isCurrent ? "This Device" : "Remote Device")}
+
+
+ {session.id.substring(0, 14)}...
+
+
+
+
+
+ {isAgent
+ ? (
+
+ {scopes.length > 0
+ ? `${scopes.length} Scopes`
+ : "Delegated"}
+
+ )
+ : (
+
+ Interactive
+
+ )}
+
+
+ {session.last_activity_action
+ ? (
+
+
+ {session.last_activity_action}
+
+
+ {new Date(
+ session.last_activity_at ||
+ session.created_at,
+ ).toLocaleTimeString()}
+
+
+ )
+ : (
+
+ {new Date(session.created_at)
+ .toLocaleDateString()}
+
+ )}
+
+
+
+ โณ 0h 0m left ยท (Expires{" "}
+ {new Date(session.expires_at).toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ })})
+
+
+
+
+ {isAgent && (
+ <>
+
+ +1h
+
+
+ Scopes
+
+ >
+ )}
+ {!isCurrent && (
+
+ Revoke
+
+ )}
+
+
+
+ );
+ })
+ )}
+
+
+
+
+ );
+};;
+
+export const DirectPassDrawerFragment = ({ apps = [], isAdmin = false }: { apps?: any[]; isAdmin?: boolean }) => {
+
+
+ Mint an isolated, scoped child session for 1-click magic links, CLI
+ tools, team members, or AI assistants without exposing your primary
+ passkeys.
+
+
+
+
+ {/* Hand-Off Card (Revealed upon generation) */}
+
+
+
+ ๐
+
+ Session Delegated Successfully!
+
+
+
Active Now
+
+
+
+ Copy this token, 1-click magic link, or CLI export string. You can
+ revoke or extend this session anytime from the active session cards
+ below.
+
+
+
+ {/* 1-Click Magic Link */}
+
+
+ 1-Click Magic Link (Web / Friend / Interview)
+
+
+
+
+
+ Copy Link
+
+
+
+
+ {/* 1-Tap Copy CLI */}
+
+
+ Terminal Environment Export (CLI)
+
+
+
+
+
+ Copy CLI
+
+
+
+
+ {/* 1-Tap Copy cURL Header */}
+
+
+ HTTP Authorization Header
+
+
+
+
+
+ Copy Header
+
+
+
+
+
+
+ Done (Session is Active)
+
+
+
+ );
+};
+
+export const ScopeModalFragment = ({ apps = [] }: { apps?: any[] }) => {
+ return (
+
+ );
+};
+}
+
+export const SessionDeckFragment = ({ sessions, currentSessionId }: { sessions: any[], currentSessionId: string }) => {
+ return (
+
+ {sessions.length === 0
+ ? (
+
+
+ No active sessions found.
+
+
+ )
+ : (
+ sessions.map((session) => {
+ const isCurrent = session.id === currentSessionId;
+ const isAgent = !!session.is_agent;
+ const scopes = Array.isArray(session.custom_scopes)
+ ? session.custom_scopes
+ : [];
+
+ return (
+
+
+
+
+ {isAgent ? "๐" : isCurrent ? "๐ฑ" : "๐ป"}
+
+
+
+ {session.label ||
+ (isCurrent ? "This Device" : "Remote Device")}
+
+
+ {session.id.substring(0, 14)}...
+
+
+
+
+
+ {isAgent
+ ? Delegated
+ : isCurrent
+ ? Active Now
+ : Active }
+ {!isCurrent && (
+
+ ๐๏ธ Revoke
+
+ )}
+
+
+
+
+ {isAgent && (
+
+ Scopes: {" "}
+ {scopes.length > 0 ? scopes.join(", ") : "Delegated"}
+
+ )}
+
+
+ โณ 0h 0m left ยท (Expires{" "}
+ {new Date(session.expires_at).toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ })})
+
+
+ {session.last_activity_action && (
+
+ Last Active: {" "}
+ {session.last_activity_action} ({new Date(
+ session.last_activity_at || session.created_at,
+ ).toLocaleTimeString()})
+
+ )}
+
+
+ {isAgent && (
+
+
+ +1h Extend
+
+
+ Scopes
+
+
+ )}
+
+ );
+ })
+ )}
+
+ );
+};
+}
diff --git a/src/features/sessions/queries.ts b/src/features/sessions/queries.ts
new file mode 100644
index 0000000..723eec7
--- /dev/null
+++ b/src/features/sessions/queries.ts
@@ -0,0 +1,76 @@
+import { sqlWrapper } from "../../core/db.ts";
+
+export async function getActiveSessions(userId: string) {
+ const sessions = await sqlWrapper.sql`
+ SELECT id, label, is_agent, custom_scopes, last_activity_at, last_activity_action, created_at, expires_at
+ FROM sessions
+ WHERE user_id = ${userId} AND expires_at > NOW()
+ ORDER BY created_at DESC
+ `;
+ return sessions;
+}
+
+export async function delegateSession(sessionId: string, userId: string, label: string, customScopes: string[], expiresAt: Date) {
+ await sqlWrapper.sql`
+ INSERT INTO sessions (id, user_id, label, is_agent, custom_scopes, expires_at, created_at)
+ VALUES (${sessionId}, ${userId}, ${label}, true, ${customScopes}, ${expiresAt.toISOString()}, NOW())
+ `;
+}
+
+export async function getSessionByIdAndUserId(sessionId: string, userId: string) {
+ const session = await sqlWrapper.sql`
+ SELECT id, is_agent, expires_at FROM sessions WHERE id = ${sessionId} AND user_id = ${userId}
+ `;
+ if (!session || session.length === 0) return null;
+ return session[0];
+}
+
+export async function updateSessionScopes(sessionId: string, customScopes: string[]) {
+ await sqlWrapper.sql`
+ UPDATE sessions SET custom_scopes = ${customScopes} WHERE id = ${sessionId}
+ `;
+}
+
+export async function getSessionById(sessionId: string) {
+ const session = await sqlWrapper.sql`
+ SELECT id FROM sessions WHERE id = ${sessionId}
+ `;
+ if (!session || session.length === 0) return null;
+ return session[0];
+}
+
+export async function setSessionPaused(sessionId: string, isPaused: boolean) {
+ await sqlWrapper.sql`
+ UPDATE sessions SET is_paused = ${isPaused} WHERE id = ${sessionId}
+ `;
+}
+
+export async function extendSessionExpiry(sessionId: string, newExpiry: Date) {
+ await sqlWrapper.sql`
+ UPDATE sessions SET expires_at = ${newExpiry.toISOString()} WHERE id = ${sessionId}
+ `;
+}
+
+export async function revokeSession(sessionId: string) {
+ await sqlWrapper.sql`DELETE FROM sessions WHERE id = ${sessionId}`;
+}
+
+export async function getSessionWithOwnershipCheck(sessionId: string, userId: string, isAdmin: boolean) {
+ const session = await sqlWrapper.sql`
+ SELECT s.id
+ FROM sessions s
+ JOIN users u ON s.user_id = u.id
+ WHERE s.id = ${sessionId}
+ AND (
+ s.user_id = ${userId}
+ OR ${isAdmin}
+ OR EXISTS (
+ SELECT 1 FROM event_passes ep
+ WHERE ep.created_by = ${userId}
+ AND u.event_pass_id = ep.id
+ )
+ )
+ `;
+ if (!session || session.length === 0) return null;
+ return session[0];
+}