backup: jules session 5556705001895338576 phase 3 wip

This commit is contained in:
Tyler Gillispie 2026-08-27 20:24:45 -07:00
parent 8b30b17dbe
commit 3e253f928d
15 changed files with 2212 additions and 0 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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);

8
scratch/clean.ts Normal file
View File

@ -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'));

15
scratch/fix_imports.ts Normal file
View File

@ -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);

4
scratch/fix_test.ts Normal file
View File

@ -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);

23
scratch/replace.ts Normal file
View File

@ -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 <div>Workshop Pass Drawer Placeholder</div>\n}',
'export const WorkshopPassDrawerFragment = ({ apps }: { apps: any[] }) => {' + workshopDrawer + '}'
);
content = content.replace(
'export const GuestDrawerAttendeesFragment = () => {\n return <div>Guest Drawer Attendees Placeholder</div>\n}',
'export const GuestDrawerAttendeesFragment = () => {' + eventGuestsDrawer + '}'
);
writeFileSync("src/features/events/fragments.tsx", content);

View File

@ -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 <div>DirectPassDrawerFragment Placeholder</div>\n}',
'export const DirectPassDrawerFragment = ({ apps = [], isAdmin = false }: { apps?: any[]; isAdmin?: boolean }) => {' + directPassDrawer + '}'
);
content = content.replace(
'export const ScopeModalFragment = ({ apps = [] }: { apps?: any[] }) => {\n return <div>ScopeModalFragment Placeholder</div>\n}',
'export const ScopeModalFragment = ({ apps = [] }: { apps?: any[] }) => {' + scopeModal + '}'
);
writeFileSync("src/features/sessions/fragments.tsx", content);

View File

@ -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 <div>SessionDeckFragment Placeholder</div>\n}',
'export const SessionDeckFragment = ({ sessions, currentSessionId }: { sessions: any[], currentSessionId: string }) => {' + sessionDeck + '}'
);
writeFileSync("src/features/sessions/fragments.tsx", content);

View File

@ -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(&#39;/api/join&#39;)\"");
});
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");
});

View File

@ -0,0 +1,910 @@
export const EventCockpitDeckFragment = ({ eventPasses }: { eventPasses: any[] }) => {
if (!eventPasses || eventPasses.length === 0) return null;
return (
<div style="margin-bottom: 2rem;">
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-subtle); padding-bottom: 0.5rem; margin-bottom: 1rem;">
<h2 style="font-size: 1.25rem; font-weight: 700; margin: 0; color: var(--text-primary);">
Events
</h2>
<div style="display: flex; background: var(--surface-muted); padding: 2px; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle); gap: 2px;">
<button
type="button"
id="viewModeGrid"
class="view-mode-btn active"
onclick="setEventViewMode('grid')"
aria-label="Grid View"
aria-pressed="true"
>
🗂 Grid
</button>
<button
type="button"
id="viewModeCompact"
class="view-mode-btn"
onclick="setEventViewMode('compact')"
aria-label="Compact View"
aria-pressed="false"
>
📋 Compact
</button>
</div>
</div>
<div id="eventDeckContainer" class="grid-view">
{eventPasses.map((event) => {
const expDate = new Date(event.expires_at);
const timeString = expDate.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
});
return (
<div
key={event.id}
class="card event-card"
style="border-left: 4px solid var(--primary); margin: 0;"
>
<div class="event-card-header">
<div>
<h3 style="margin: 0 0 0.25rem 0; font-size: 1.1rem; color: var(--text-primary);">
{event.name}
</h3>
<div
class="countdown-pill"
data-expires-at={event.expires_at}
>
0h 0m left · (Expires {timeString})
</div>
</div>
<span class="badge badge-success status-badge">Active</span>
</div>
{/* Progress Bar for Seats */}
<div style="margin-bottom: 1rem;">
<div style="display: flex; justify-content: space-between; font-size: 0.8rem; margin-bottom: 0.25rem; color: var(--text-secondary);">
<span>Seats Claimed</span>
<strong style="color: var(--text-primary);">
{event.seats_claimed} /{" "}
{event.max_seats === 0 ? "∞" : event.max_seats}
</strong>
</div>
<div style="width: 100%; height: 8px; background: var(--surface-muted); border-radius: 4px; overflow: hidden;">
<div
style={`height: 100%; background: var(--primary); width: ${
event.max_seats > 0
? Math.min(
(event.seats_claimed / event.max_seats) * 100,
100,
)
: 100
}%;`}
>
</div>
</div>
</div>
{/* Quick Copy Snippets */}
<div
class="event-card-snippets"
style="display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem;"
>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span style="font-size: 0.8rem; font-weight: 600; width: 45px; color: var(--text-muted);">
PIN:
</span>
<code
style="flex: 1; padding: 0.35rem 0.5rem; background: var(--surface-muted); border-radius: var(--radius-sm); font-family: monospace; font-size: 0.9rem; text-align: center; letter-spacing: 2px;"
id={`pin-${event.id}`}
>
{event.pin_code}
</code>
<button
type="button"
class="btn-outline"
aria-label={`Copy PIN code for ${event.name}`}
style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 28px;"
data-ignore onclick={`copyText(document.getElementById('pin-${event.id}').textContent.trim())`}
>
Copy
</button>
</div>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span style="font-size: 0.8rem; font-weight: 600; width: 45px; color: var(--text-muted);">
Link:
</span>
<code
id={`link-code-${event.id}`}
style="flex: 1; padding: 0.35rem 0.5rem; background: var(--surface-muted); border-radius: var(--radius-sm); font-family: monospace; font-size: 0.75rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
>
/e/{event.slug}
</code>
<button
type="button"
id={`link-btn-${event.id}`}
class="btn-outline"
aria-label={`Copy direct link for ${event.name}`}
style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 28px;"
data-ignore onclick={`copyText(window.location.origin + '/e/${event.slug}')`}
>
Copy
</button>
</div>
<details
class="cli-details"
style="margin-top: 0.1rem; width: 100%;"
>
<summary style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer; user-select: none;">
<span style="font-size: 0.8rem; font-weight: 600; width: 45px; color: var(--text-muted);">
CLI:
</span>
<code
id={`cli-code-${event.id}`}
style="flex: 1; padding: 0.35rem 0.5rem; background: var(--surface-muted); border-radius: var(--radius-sm); font-family: monospace; font-size: 0.75rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer;"
>
curl -sSL {Deno.env.get("RP_ID")
? `https://${Deno.env.get("RP_ID")}`
: ""}/join/{event.slug}?format=env | source /dev/stdin
</code>
<button
type="button"
id={`cli-btn-${event.id}`}
class="btn-outline"
aria-label={`Copy CLI command for ${event.name}`}
style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 28px;"
data-ignore onclick={`event.stopPropagation(); copyText("curl -sSL " + window.location.origin + "/join/${event.slug}?format=env | source /dev/stdin")`}
>
Copy
</button>
<span style="font-size: 0.75rem; padding-right: 0.25rem;">
[ ]
</span>
</summary>
<textarea
id={`cli-textarea-${event.id}`}
readonly
rows={2}
style="width: 100%; margin-top: 0.4rem; padding: 0.4rem 0.5rem; font-family: monospace; font-size: 0.75rem; background: var(--surface-muted); color: var(--text-primary); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); resize: vertical; box-sizing: border-box;"
onclick="this.select()"
>
{`curl -sSL ${
Deno.env.get("RP_ID")
? `https://${Deno.env.get("RP_ID")}`
: ""
}/join/${event.slug}?format=env | source /dev/stdin`}
</textarea>
</details>
</div>
{/* Cockpit Actions */}
<div class="event-card-actions">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-bottom: 0.5rem;">
<button
type="button"
class="btn-outline"
aria-label={`Manage attendees for ${event.name}`}
style="justify-content: center; min-height: 38px; font-size: 0.85rem;"
data-on-click={`@get("/api/events/${event.id}/attendees")`} onclick={`openAttendeesDrawer('${event.id}', '${
event.name.replace(/'/g, "\\'")
}')`}
>
👥 Manage Guests ({event.seats_claimed})
</button>
<button
type="button"
class="btn-outline"
aria-label={`Add 5 seats to ${event.name}`}
style="justify-content: center; min-height: 38px; font-size: 0.85rem;"
data-on-click={`@post("/api/events/${event.id}/expand")`} onclick={`expandSeats('${event.id}', 5)`}
>
+5 Seats
</button>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0.5rem;">
<button
type="button"
class="btn-outline"
aria-label={`Rotate Credentials for ${event.name}`}
style="justify-content: center; min-height: 38px; font-size: 0.82rem; padding: 0 0.25rem;"
data-on-click={`@post("/api/events/${event.id}/rotate-pin")`} onclick={`rotatePin('${event.id}')`}
>
🔄 Rotate Credentials
</button>
<button
type="button"
class="btn-outline extend-event-btn" data-on-click={`@post("/api/events/${event.id}/extend")`}
data-event-id={event.id}
aria-label={`Extend lifespan by 1 hour for ${event.name}`}
style="justify-content: center; min-height: 38px; font-size: 0.82rem; padding: 0 0.25rem;"
>
+1h Extend
</button>
<button
type="button"
class="btn-danger end-event-btn" data-on-click={`@post("/api/events/${event.id}/end")`}
data-event-id={event.id}
aria-label={`End event ${event.name} and revoke all attendees`}
style="justify-content: center; min-height: 38px; font-size: 0.82rem; padding: 0 0.25rem;"
>
End Event
</button>
</div>
</div>
{/* Compact Actions (only visible in compact mode) */}
<div class="event-card-compact-row-2">
<div class="compact-pills">
<span
class="compact-pill"
data-ignore onclick={`copyText(document.getElementById('pin-${event.id}').textContent.trim())`}
>
PIN:{" "}
<span id={`compact-pin-${event.id}`}>{event.pin_code}</span>
{" "}
(Copy)
</span>
<span
id={`compact-link-${event.id}`}
class="compact-pill"
data-ignore onclick={`copyText(window.location.origin + '/e/${event.slug}')`}
>
Link (Copy)
</span>
<span
id={`compact-cli-${event.id}`}
class="compact-pill"
data-ignore onclick={`copyText("curl -sSL " + window.location.origin + "/join/${event.slug}?format=env | source /dev/stdin")`}
>
CLI (Copy)
</span>
</div>
<div class="compact-actions-right">
<button
type="button"
class="btn-outline"
style="padding: 0.25rem 0.5rem; font-size: 0.8rem; min-height: 28px;"
data-on-click={`@get("/api/events/${event.id}/attendees")`} onclick={`openAttendeesDrawer('${event.id}', '${
event.name.replace(/'/g, "\\'")
}')`}
>
👥 Guests ({event.seats_claimed})
</button>
<button
type="button"
class="btn-outline extend-event-btn" data-on-click={`@post("/api/events/${event.id}/extend")`}
data-event-id={event.id}
style="padding: 0.25rem 0.5rem; font-size: 0.8rem; min-height: 28px;"
>
🔄 +1h
</button>
</div>
</div>
</div>
);
})}
</div>
<style>
{`
.cli-details summary {
list-style: none;
}
.cli-details summary::-webkit-details-marker {
display: none;
}
.view-mode-btn {
padding: 0.25rem 0.75rem;
border-radius: var(--radius-sm);
border: none;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
background: transparent;
color: var(--text-secondary);
opacity: 0.75;
}
.view-mode-btn.active {
background: var(--primary);
color: #ffffff;
font-weight: 700;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
border-radius: var(--radius-sm);
opacity: 1;
}
/* Grid View Layout */
.grid-view {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1rem;
}
.grid-view .event-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.75rem;
}
.grid-view .event-card-compact-row-2 {
display: none;
}
/* Compact View Layout */
.compact-view {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.compact-view .event-card {
display: flex;
flex-direction: column;
justify-content: center;
padding: 0.75rem 1rem;
box-sizing: border-box;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
min-height: 70px;
}
.compact-view .event-card > div {
margin-bottom: 0 !important; /* Reset component margins */
}
.compact-view .event-card-header {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
margin-bottom: 0.5rem !important;
}
.compact-view .event-card-header > div {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 1;
min-width: 0;
}
.compact-view .event-card-header h3 {
margin: 0 !important;
font-size: 1rem !important;
font-weight: 700;
color: var(--text-primary);
max-width: 280px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
}
.compact-view .event-card-header > .status-badge {
margin-left: auto;
}
.compact-view .event-card-compact-row-2 {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.compact-view .compact-pills {
display: flex;
gap: 0.5rem;
align-items: center;
}
.compact-view .compact-pill {
padding: 2px 6px;
font-size: 0.75rem;
background: var(--surface-muted);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
cursor: pointer;
color: var(--text-secondary);
font-family: monospace;
}
.compact-view .compact-pill:hover {
border-color: var(--primary);
color: var(--primary);
}
.compact-view .compact-actions-right {
display: flex;
gap: 0.5rem;
align-items: center;
}
.compact-view .event-card-snippets,
.compact-view .event-card-actions {
display: none !important; /* Hide heavy elements in compact */
}
/* Dynamic Countdown Colors */
.countdown-pill {
font-size: 0.8rem;
color: var(--text-secondary);
font-weight: 500;
}
.countdown-pill.status-green { color: var(--success); }
.countdown-pill.status-amber { color: var(--warning); }
.countdown-pill.status-red { color: var(--danger-text); }
`}
</style>
</div>
);
};
export const WorkshopPassDrawerFragment = ({ apps }: { apps: any[] }) => {
return (
<div id="tabWorkshopPass" style="display: none;">
<div id="eventCreateState" style="display: block;">
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0 0 1rem 0;">
Generate a shared event pass with a universal 6-digit PIN, vanity link
(<code style="font-size: 0.8rem;">/e/slug</code>), and CLI 1-liner for
workshops, hackathons, and multi-user kiosks.
</p>
<form
id="eventForm"
onsubmit="handleCreateEvent(event)"
style="margin-top: 1rem;"
>
{/* Event Name */}
<div style="margin-bottom: 1.25rem;">
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
Workshop / Event Name *
</label>
<input
type="text"
id="eventName"
placeholder="e.g. Deno & SPIRE Zero-Trust Workshop, Elite Fleet Live Demo"
required
style="width: 100%;"
/>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-bottom: 1.25rem;">
{/* Target App */}
<div>
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
Target App Destination
</label>
<select
id="eventAppId"
style="width: 100%; padding: 0.5rem 0.75rem; background: var(--surface-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); color: var(--text-primary); font-size: 0.9rem;"
>
<option value="">Universal Sandbox (Default)</option>
{apps.map((app) => (
<option value={app.id} key={app.id}>
{app.name} ({app.domain || "Internal"})
</option>
))}
</select>
</div>
{/* Guest Role */}
<div>
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
Attendee Access Role
</label>
<select
id="eventRole"
style="width: 100%; padding: 0.5rem 0.75rem; background: var(--surface-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); color: var(--text-primary); font-size: 0.9rem;"
>
<option value="viewer">Viewer (Read-Only Demo)</option>
<option value="operator">Operator (Workshop Actions)</option>
<option value="user">Standard User</option>
</select>
</div>
</div>
{/* Seat Capacity Presets */}
<div style="margin-bottom: 1.25rem;">
<label style="display: block; font-weight: 600; margin-bottom: 0.45rem; font-size: 0.875rem; color: var(--text-secondary);">
Seat Capacity Limit
</label>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;">
<button
type="button"
class="pill-btn seat-pill"
onclick="selectSeats(this, 25)"
>
25 Seats
</button>
<button
type="button"
class="pill-btn seat-pill active"
onclick="selectSeats(this, 50)"
>
50 Seats
</button>
<button
type="button"
class="pill-btn seat-pill"
onclick="selectSeats(this, 100)"
>
100 Seats
</button>
<button
type="button"
class="pill-btn seat-pill"
onclick="selectSeats(this, 0)"
>
Unlimited
</button>
<input
type="number"
id="eventMaxSeats"
value="50"
min="0"
max="10000"
style="width: 100px; padding: 0.35rem 0.5rem; text-align: center;"
title="Max seats (0 = unlimited)"
/>
</div>
</div>
{/* Duration Presets */}
<div style="margin-bottom: 1.25rem;">
<label style="display: block; font-weight: 600; margin-bottom: 0.45rem; font-size: 0.875rem; color: var(--text-secondary);">
Event Duration & Expiration
</label>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<button
type="button"
class="pill-btn event-lifespan-pill"
onclick="selectEventLifespan(this, 1)"
>
1 Hour
</button>
<button
type="button"
class="pill-btn event-lifespan-pill active"
onclick="selectEventLifespan(this, 3)"
>
🛠 3 Hours (Standard Workshop)
</button>
<button
type="button"
class="pill-btn event-lifespan-pill"
onclick="selectEventLifespan(this, 12)"
>
📅 12 Hours (All-Day Hackathon)
</button>
<button
type="button"
class="pill-btn event-lifespan-pill"
onclick="selectEventLifespan(this, 24)"
>
🗓 24 Hours
</button>
</div>
<input type="hidden" id="eventLifespanHours" value="3" />
</div>
{/* Custom Codes Accordion (Optional) */}
<details style="margin-bottom: 1.5rem; background: var(--surface-muted); padding: 0.75rem 1rem; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle);">
<summary style="font-size: 0.875rem; font-weight: 600; color: var(--text-primary); cursor: pointer;">
Custom Vanity Slug & Custom PIN Code (Optional)
</summary>
<div style="margin-top: 0.75rem; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
<div>
<label style="display: block; font-size: 0.8rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 0.25rem;">
Vanity Slug (e.g. /e/deno-lab)
</label>
<input
type="text"
id="eventSlug"
placeholder="auto-generated if blank"
style="width: 100%; font-size: 0.85rem;"
/>
</div>
<div>
<label style="display: block; font-size: 0.8rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 0.25rem;">
Custom 6-Digit PIN (e.g. 749-123)
</label>
<input
type="text"
id="eventPinCode"
placeholder="auto-generated if blank"
style="width: 100%; font-size: 0.85rem;"
/>
</div>
</div>
</details>
<div style="display: flex; gap: 0.75rem;">
<button
type="submit"
id="submitEventBtn" data-on-click="@post('/api/events')"
class="btn-primary"
style="min-height: 42px; background: var(--success); border-color: var(--success);"
>
🎟 Launch Workshop Pass
</button>
<button
type="button"
class="btn-outline"
onclick="closeDelegateDrawer()"
style="min-height: 42px;"
>
Cancel
</button>
</div>
</form>
</div>
{/* Event Launch Success State */}
<div
id="eventHandoffState"
style="display: none; margin-top: 1.5rem; padding: 1.25rem; background: var(--surface-card); border: 2px solid var(--success); border-radius: var(--radius-md); box-shadow: var(--shadow-md);"
>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
<div style="display: flex; align-items: center; gap: 0.5rem; flex: 1; min-width: 0;">
<span style="font-size: 1.3rem; flex-shrink: 0;">🎟</span>
<strong
id="createdEventTitle"
style="color: var(--text-primary); font-size: 1.1rem; overflow-wrap: break-word; word-break: break-word; max-width: 100%; display: block;"
>
Workshop Pass Live!
</strong>
</div>
<span
class="badge badge-success"
style="flex-shrink: 0; margin-left: 0.5rem;"
>
Active Now
</span>
</div>
<p style="font-size: 0.85rem; color: var(--text-secondary); margin: 0 0 1rem 0;">
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.
</p>
<div style="display: flex; flex-direction: column; gap: 0.75rem; margin-bottom: 1.25rem;">
{/* PIN Code */}
<div>
<label style="display: block; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.25rem;">
6-Digit Universal PIN (for /join or Kiosk Entry)
</label>
<div style="display: flex; gap: 0.5rem;">
<code
id="eventHandoffPin"
style="flex: 1; padding: 0.5rem 0.75rem; background: var(--surface-muted); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); font-family: monospace; font-size: 1.1rem; text-align: center; letter-spacing: 3px; font-weight: 700; color: var(--primary);"
>
</code>
<button
type="button"
class="btn-outline"
aria-label="Copy Universal PIN"
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
data-ignore onclick="copyEventHandoff('pin')"
>
Copy PIN
</button>
</div>
</div>
{/* 1-Click Vanity Link */}
<div>
<label style="display: block; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.25rem;">
Direct 1-Click Workshop Entrance URL
</label>
<div style="display: flex; gap: 0.5rem;">
<code
id="eventHandoffLink"
style="flex: 1; padding: 0.5rem 0.75rem; background: var(--surface-muted); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); font-family: monospace; font-size: 0.85rem; overflow-x: auto; white-space: nowrap; color: var(--success);"
>
</code>
<button
type="button"
class="btn-outline"
aria-label="Copy Direct Link"
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
data-ignore onclick="copyEventHandoff('link')"
>
Copy URL
</button>
</div>
</div>
{/* CLI 1-Liner */}
<div>
<label style="display: block; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.25rem;">
Terminal 1-Liner (Environment Injector)
</label>
<div style="display: flex; gap: 0.5rem;">
<code
id="eventHandoffCli"
style="flex: 1; padding: 0.5rem 0.75rem; background: var(--surface-muted); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); font-family: monospace; font-size: 0.8rem; overflow-x: auto; white-space: nowrap; color: var(--text-primary);"
>
</code>
<button
type="button"
class="btn-outline"
aria-label="Copy Terminal curl command"
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
data-ignore onclick="copyEventHandoff('cli')"
>
Copy 1-Liner
</button>
</div>
</div>
</div>
<button
type="button"
class="btn-outline"
style="width: 100%; min-height: 38px; justify-content: center; font-size: 0.85rem;"
onclick="closeEventHandoffModal()"
>
OK
</button>
</div>
</div>
);
};
export const GuestDrawerAttendeesFragment = () => {
return (
<div
id="attendeesDrawer"
class="drawer-overlay"
style="display: none; position: fixed; inset: 0; z-index: 1040; background: rgba(0,0,0,0.5); opacity: 0; transition: opacity 0.2s ease;"
>
<div
class="drawer-panel"
style="position: fixed; background: var(--surface-card); box-shadow: var(--shadow-lg); z-index: 1050; display: flex; flex-direction: column; transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);"
>
<div style="padding: 1.5rem; border-bottom: 1px solid var(--border-subtle); display: flex; flex-direction: column; gap: 0.25rem;">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<h2
id="guestDrawerTitle"
style="margin: 0; font-size: 1.25rem; color: var(--text-primary);"
>
[Event Name] Guests
</h2>
<button
type="button"
onclick="closeAttendeesDrawer()"
style="background: none; border: none; font-size: 1.5rem; color: var(--text-muted); cursor: pointer; line-height: 1;"
aria-label="Close Drawer"
>
&times;
</button>
</div>
<div
id="guestDrawerContext"
style="font-size: 0.8rem; color: var(--text-secondary);"
>
Event Pass · <span id="guestDrawerClaimed">0</span> /{" "}
<span id="guestDrawerMax">0</span> Claimed Seats ·{" "}
<span id="guestDrawerCountdown"> 0h 0m left</span>{" "}
· (<span id="guestDrawerExpiresAt">--:--</span>)
</div>
</div>
<div
id="attendeesDrawerContent"
style="flex: 1; overflow-y: auto; padding: 1.5rem;"
>
{/* Populated dynamically via JS */}
<div style="display: flex; justify-content: center; align-items: center; height: 100%; color: var(--text-muted); font-size: 0.9rem;">
Loading attendees...
</div>
</div>
<style>
{`
/* Desktop Slide-Over Panel */
@media (min-width: 768px) {
.drawer-panel {
top: 0;
right: 0;
width: 420px;
height: 100vh;
transform: translateX(100%);
}
.drawer-panel.open {
transform: translateX(0);
}
}
/* Mobile Bottom Sheet */
@media (max-width: 767px) {
.drawer-panel {
bottom: 0;
left: 0;
width: 100vw;
height: 80vh;
border-radius: 16px 16px 0 0;
transform: translateY(100%);
}
.drawer-panel.open {
transform: translateY(0);
}
}
.drawer-overlay.open {
display: block !important;
opacity: 1 !important;
}
`}
</style>
</div>
</div>
);
};
export const EventJoinPageFragment = () => {
return (
<div>
<div class="brand-header">
<div
class="brand-logo"
style="background: var(--primary-light); color: var(--primary);"
>
<svg
width="26"
height="26"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z">
</path>
<path d="M13 5v2"></path>
<path d="M13 17v2"></path>
<path d="M13 11v2"></path>
</svg>
</div>
<h1>Join Event or Workshop</h1>
<p class="subtitle">
Enter your event PIN code or slug to claim an instant sandbox seat.
</p>
</div>
<form id="joinForm" data-on-submit="@post('/api/join')">
<div style="margin-bottom: 1.25rem;">
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
Event PIN or Slug Code
</label>
<input
type="text"
name="code"
id="eventCode"
placeholder="e.g. 749-123 or deno-lab"
required
autofocus
style="width: 100%; font-size: 1.1rem; text-align: center; letter-spacing: 0.05em; font-weight: 600; min-height: 48px;"
/>
</div>
<div
id="joinNotice"
style="display: none; margin-bottom: 1rem; padding: 0.75rem 1rem; border-radius: var(--radius-md); font-size: 0.9rem;"
/>
<button
type="submit"
id="joinBtn"
class="btn-primary"
style="width: 100%; min-height: 48px; font-size: 1rem;"
>
Enter Workshop
</button>
</form>
<div style="margin-top: 1.5rem; text-align: center; font-size: 0.85rem; color: var(--text-muted);">
Looking for standard sign in?{" "}
<a
href="/login"
style="color: var(--primary); text-decoration: none; font-weight: 600;"
>
Sign in with Passkey
</a>
</div>
</div>
);
};

View File

@ -0,0 +1,149 @@
import { sqlWrapper } from "../../core/db.ts";
export async function rotateEventPin(eventId: string, userId: string, isGlobalAdmin: boolean): Promise<string | null> {
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];
}

View File

@ -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(<EventCockpitDeckFragment eventPasses={events} />);
});
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(<EventCockpitDeckFragment eventPasses={events} />);
});
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(<EventCockpitDeckFragment eventPasses={events} />);
});
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: `<span id="event-seats-${eventId}">${event.seats_claimed}</span>`
});
}
// 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: `<span id="event-seats-${eventId}">${parsed.seats_claimed}</span>`
});
} 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(<EventJoinPageFragment />);
});

View File

@ -0,0 +1,675 @@
import { Hono } from "jsr:@hono/hono@4";
export const SessionTableFragment = ({ sessions, currentSessionId }: { sessions: any[], currentSessionId: string }) => {
return (
<div class="card desktop-only" style="display: none;">
<div class="table-container">
<table>
<thead>
<tr>
<th>Type & Label</th>
<th>Permissions</th>
<th>Activity</th>
<th>Expires</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{sessions.length === 0
? (
<tr>
<td
colSpan={5}
style="text-align: center; padding: 2rem; color: var(--text-muted);"
>
No active sessions found.
</td>
</tr>
)
: (
sessions.map((session) => {
const isCurrent = session.id === currentSessionId;
const isAgent = !!session.is_agent;
const scopes = Array.isArray(session.custom_scopes)
? session.custom_scopes
: [];
return (
<tr key={session.id}>
<td>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span style="font-size: 1.1rem;">
{isAgent ? "🔑" : isCurrent ? "📱" : "💻"}
</span>
<div>
<strong style="color: var(--text-primary);">
{session.label ||
(isCurrent ? "This Device" : "Remote Device")}
</strong>
<div style="font-size: 0.75rem; color: var(--text-muted); font-family: monospace;">
{session.id.substring(0, 14)}...
</div>
</div>
</div>
</td>
<td>
{isAgent
? (
<span class="badge badge-info">
{scopes.length > 0
? `${scopes.length} Scopes`
: "Delegated"}
</span>
)
: (
<span class="badge badge-success">
Interactive
</span>
)}
</td>
<td style="color: var(--text-secondary); font-size: 0.85rem;">
{session.last_activity_action
? (
<div>
<span style="color: var(--primary); font-family: monospace; font-size: 0.8rem;">
{session.last_activity_action}
</span>
<div style="font-size: 0.75rem; color: var(--text-muted);">
{new Date(
session.last_activity_at ||
session.created_at,
).toLocaleTimeString()}
</div>
</div>
)
: (
<span>
{new Date(session.created_at)
.toLocaleDateString()}
</span>
)}
</td>
<td style="color: var(--text-secondary); font-size: 0.85rem;">
<span
class="countdown-pill"
data-expires-at={session.expires_at}
>
0h 0m left · (Expires{" "}
{new Date(session.expires_at).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})})
</span>
</td>
<td>
<div style="display: flex; gap: 0.35rem; align-items: center;">
{isAgent && (
<>
<button
type="button"
class="btn-outline"
style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 30px;"
data-on-click={`@post("/api/sessions/${session.id}/extend")`}
title="Extend session by 1 hour"
>
+1h
</button>
<button
type="button"
class="btn-outline"
style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 30px;"
onclick={`openEditScopesModal('${session.id}', '${
session.label || "Delegated"
}', ${JSON.stringify(JSON.stringify(scopes))})`}
title="Edit permissions"
>
Scopes
</button>
</>
)}
{!isCurrent && (
<button
type="button"
class="btn-danger revoke-btn" data-on-click={`@delete("/api/sessions/${session.id}")`}
data-on-click={`@delete("/api/sessions/${session.id}")`}
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 30px;"
>
Revoke
</button>
)}
</div>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
);
};;
export const DirectPassDrawerFragment = ({ apps = [], isAdmin = false }: { apps?: any[]; isAdmin?: boolean }) => {
<div id="tabDirectPass">
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0 0 1rem 0;">
Mint an isolated, scoped child session for 1-click magic links, CLI
tools, team members, or AI assistants without exposing your primary
passkeys.
</p>
<form
id="delegateForm"
onsubmit="handleDelegateSession(event)"
style="margin-top: 1rem;"
>
{/* Label Input */}
<div style="margin-bottom: 1.25rem;">
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
Session Label / Purpose *
</label>
<input
type="text"
id="delegateLabel"
placeholder="e.g. Friend Demo Pass, Antigravity Assistant, CI/CD Runner, Terminal CLI"
required
style="width: 100%;"
/>
</div>
{/* Direct 1-Click Destination (App Selection) */}
<div style="margin-bottom: 1.25rem;">
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
1-Click Destination App (Optional)
</label>
<select
id="delegateTargetApp"
onchange="handleTargetAppChange(this.value)"
style="width: 100%; padding: 0.5rem 0.75rem; background: var(--surface-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); color: var(--text-primary); font-size: 0.9rem;"
>
<option value="">Auth-Yes Central Dashboard (Default)</option>
{apps.map((app) => (
<option value={app.name} key={app.id}>
{app.name} ({app.domain || "Internal App"})
</option>
))}
</select>
<p style="font-size: 0.75rem; color: var(--text-muted); margin: 0.35rem 0 0 0;">
Selecting an app scopes the pass and automatically redirects the
recipient to that app upon 1-click redemption.
</p>
</div>
{/* Lifespan Presets */}
<div style="margin-bottom: 1.25rem;">
<label style="display: block; font-weight: 600; margin-bottom: 0.45rem; font-size: 0.875rem; color: var(--text-secondary);">
Lifespan & Auto-Expiration
</label>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<button
type="button"
class="pill-btn lifespan-pill active"
data-hours="1"
onclick="selectLifespan(this, 1)"
>
1 Hour (Quick Task)
</button>
<button
type="button"
class="pill-btn lifespan-pill"
data-hours="12"
onclick="selectLifespan(this, 12)"
>
🛠 12 Hours (Work Session)
</button>
<button
type="button"
class="pill-btn lifespan-pill"
data-hours="24"
onclick="selectLifespan(this, 24)"
>
📅 24 Hours
</button>
<button
type="button"
class="pill-btn lifespan-pill"
data-hours="168"
onclick="selectLifespan(this, 168)"
>
🗓 7 Days
</button>
</div>
<input type="hidden" id="delegateHours" value="1" />
</div>
{/* Access Mode Presets */}
<div style="margin-bottom: 1.25rem;">
<label style="display: block; font-weight: 600; margin-bottom: 0.45rem; font-size: 0.875rem; color: var(--text-secondary);">
Access Mode & Permissions
</label>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<button
type="button"
class="pill-btn mode-pill active"
data-mode="read_only"
onclick="selectMode(this, 'read_only')"
>
👁 Read-Only (Audit & Inspection)
</button>
<button
type="button"
class="pill-btn mode-pill"
data-mode="operator"
onclick="selectMode(this, 'operator')"
>
Operator (App Actions)
</button>
{isAdmin && (
<button
type="button"
class="pill-btn mode-pill"
data-mode="admin"
onclick="selectMode(this, 'admin')"
>
👑 Full Admin
</button>
)}
<button
type="button"
class="pill-btn mode-pill"
data-mode="custom"
onclick="selectMode(this, 'custom')"
>
🛠 Custom Scopes
</button>
</div>
<input type="hidden" id="delegateMode" value="read_only" />
</div>
{/* Progressive Disclosure: Specific App & Capability Scopes */}
<details
id="customScopesSection"
style="margin-bottom: 1.25rem; border: 1px solid var(--border-subtle); border-radius: var(--radius-md); padding: 0.75rem 1rem; background: var(--surface-muted);"
>
<summary style="cursor: pointer; font-weight: 600; font-size: 0.875rem; color: var(--text-primary);">
Customize Specific App Grants & Capabilities (Optional)
</summary>
<div style="margin-top: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem;">
<p style="margin: 0 0 0.5rem 0; font-size: 0.8rem; color: var(--text-secondary);">
Select which subsidiary microservices and IAM capabilities this
delegated token is permitted to access:
</p>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.5rem;">
<label style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; color: var(--text-primary); cursor: pointer;">
<input
type="checkbox"
class="scope-checkbox"
value="read:audit"
checked
/>
Read Audit Ledger
</label>
<label style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; color: var(--text-primary); cursor: pointer;">
<input
type="checkbox"
class="scope-checkbox"
value="read:users"
checked
/>
Read Users List
</label>
<label style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; color: var(--text-primary); cursor: pointer;">
<input
type="checkbox"
class="scope-checkbox"
value="read:apps"
checked
/>
Read Apps Registry
</label>
{apps.map((app) => (
<label
key={app.id}
style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; color: var(--text-primary); cursor: pointer;"
>
<input
type="checkbox"
class="scope-checkbox"
value={`app:${app.name}`}
/>
{app.name} ({app.domain || "Internal"})
</label>
))}
</div>
</div>
</details>
<div style="display: flex; gap: 0.75rem;">
<button
type="submit"
id="submitDelegateBtn"
class="btn-primary"
style="min-height: 42px;"
>
Mint & Delegate Session
</button>
<button
type="button"
class="btn-outline"
onclick="closeDelegateDrawer()"
style="min-height: 42px;"
>
Cancel
</button>
</div>
</form>
{/* Hand-Off Card (Revealed upon generation) */}
<div
id="handoffModal"
style="display: none; margin-top: 1.5rem; padding: 1.25rem; background: var(--surface-card); border: 1px solid var(--primary); border-radius: var(--radius-md); box-shadow: var(--shadow-md);"
>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span style="font-size: 1.2rem;">🔑</span>
<strong style="color: var(--text-primary); font-size: 1rem;">
Session Delegated Successfully!
</strong>
</div>
<span class="badge badge-success">Active Now</span>
</div>
<p style="font-size: 0.85rem; color: var(--text-secondary); margin: 0 0 1rem 0;">
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.
</p>
<div style="display: flex; flex-direction: column; gap: 0.75rem; margin-bottom: 1rem;">
{/* 1-Click Magic Link */}
<div>
<label style="display: block; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.25rem;">
1-Click Magic Link (Web / Friend / Interview)
</label>
<div style="display: flex; gap: 0.5rem;">
<code
id="handoffMagicLinkText"
style="flex: 1; padding: 0.5rem 0.75rem; background: var(--surface-muted); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); font-family: monospace; font-size: 0.85rem; overflow-x: auto; white-space: nowrap; color: var(--success);"
>
</code>
<button
type="button"
class="btn-primary"
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
data-ignore onclick="copyHandoff('magic')"
>
Copy Link
</button>
</div>
</div>
{/* 1-Tap Copy CLI */}
<div>
<label style="display: block; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.25rem;">
Terminal Environment Export (CLI)
</label>
<div style="display: flex; gap: 0.5rem;">
<code
id="handoffCliText"
style="flex: 1; padding: 0.5rem 0.75rem; background: var(--surface-muted); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); font-family: monospace; font-size: 0.85rem; overflow-x: auto; white-space: nowrap; color: var(--text-primary);"
>
</code>
<button
type="button"
class="btn-outline"
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
data-ignore onclick="copyHandoff('cli')"
>
Copy CLI
</button>
</div>
</div>
{/* 1-Tap Copy cURL Header */}
<div>
<label style="display: block; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.25rem;">
HTTP Authorization Header
</label>
<div style="display: flex; gap: 0.5rem;">
<code
id="handoffCurlText"
style="flex: 1; padding: 0.5rem 0.75rem; background: var(--surface-muted); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); font-family: monospace; font-size: 0.85rem; overflow-x: auto; white-space: nowrap; color: var(--text-primary);"
>
</code>
<button
type="button"
class="btn-outline"
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
data-ignore onclick="copyHandoff('curl')"
>
Copy Header
</button>
</div>
</div>
</div>
<button
type="button"
class="btn-outline"
style="width: 100%; min-height: 38px; justify-content: center; font-size: 0.85rem;"
onclick="closeHandoffModal()"
>
Done (Session is Active)
</button>
</div>
</div>
);
};
export const ScopeModalFragment = ({ apps = [] }: { apps?: any[] }) => {
return (
<div
id="editScopesModal"
style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.6); z-index: 9999; justify-content: center; align-items: center;"
>
<div style="background: var(--surface-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); width: 90%; max-width: 500px; padding: 1.5rem; box-shadow: var(--shadow-lg);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h3 style="margin: 0; font-size: 1.1rem; color: var(--text-primary);">
Update Scopes for:{" "}
<span id="editScopesLabel" style="color: var(--primary);"></span>
</h3>
<button
type="button"
onclick="closeEditScopesModal()"
style="background: none; border: none; font-size: 1.2rem; cursor: pointer; color: var(--text-muted);"
>
&times;
</button>
</div>
<input type="hidden" id="editScopesSessionId" value="" />
<div style="display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1.25rem;">
<label style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; color: var(--text-primary); cursor: pointer;">
<input
type="checkbox"
id="scope_read_audit"
class="edit-scope-chk"
value="read:audit"
/>
Read Audit Ledger
</label>
<label style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; color: var(--text-primary); cursor: pointer;">
<input
type="checkbox"
id="scope_read_users"
class="edit-scope-chk"
value="read:users"
/>
Read Users List
</label>
<label style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; color: var(--text-primary); cursor: pointer;">
<input
type="checkbox"
id="scope_read_apps"
class="edit-scope-chk"
value="read:apps"
/>
Read Apps Registry
</label>
{apps.map((app) => (
<label
key={app.id}
style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; color: var(--text-primary); cursor: pointer;"
>
<input
type="checkbox"
class="edit-scope-chk"
value={`app:${app.name}`}
/>
Access {app.name}
</label>
))}
</div>
<div style="display: flex; justify-content: flex-end; gap: 0.5rem;">
<button
type="button"
class="btn-outline"
onclick="closeEditScopesModal()"
>
Cancel
</button>
<button
type="button"
class="btn-primary"
onclick="saveUpdatedScopes()"
>
Save Scopes
</button>
</div>
</div>
</div>
);
};
}
export const SessionDeckFragment = ({ sessions, currentSessionId }: { sessions: any[], currentSessionId: string }) => {
return (
<div
class="mobile-only"
style="display: flex; flex-direction: column; gap: 1rem;"
>
{sessions.length === 0
? (
<div class="card" style="text-align: center; padding: 2rem;">
<p style="color: var(--text-muted); margin: 0;">
No active sessions found.
</p>
</div>
)
: (
sessions.map((session) => {
const isCurrent = session.id === currentSessionId;
const isAgent = !!session.is_agent;
const scopes = Array.isArray(session.custom_scopes)
? session.custom_scopes
: [];
return (
<div class="card" key={session.id} style="margin-bottom: 0;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
<div style="display: flex; align-items: center; gap: 0.65rem;">
<div style="display: flex; align-items: center; justify-content: center; width: 38px; height: 38px; background: var(--surface-muted); border-radius: var(--radius-md); font-size: 1.2rem;">
{isAgent ? "🔑" : isCurrent ? "📱" : "💻"}
</div>
<div>
<div style="font-weight: 700; font-size: 0.95rem; color: var(--text-primary);">
{session.label ||
(isCurrent ? "This Device" : "Remote Device")}
</div>
<div style="font-size: 0.75rem; color: var(--text-muted); font-family: monospace;">
{session.id.substring(0, 14)}...
</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 0.5rem;">
{isAgent
? <span class="badge badge-info">Delegated</span>
: isCurrent
? <span class="badge badge-success">Active Now</span>
: <span class="badge badge-secondary">Active</span>}
{!isCurrent && (
<button
type="button"
class="btn-danger revoke-btn" data-on-click={`@delete("/api/sessions/${session.id}")`}
data-session-id={session.id}
style="padding: 0.15rem 0.5rem; font-size: 0.75rem; min-height: 24px; border-radius: var(--radius-sm);"
>
🗑 Revoke
</button>
)}
</div>
</div>
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 1rem; line-height: 1.6;">
{isAgent && (
<div style="margin-bottom: 0.25rem;">
<strong>Scopes:</strong>{" "}
{scopes.length > 0 ? scopes.join(", ") : "Delegated"}
</div>
)}
<div>
<span
class="countdown-pill"
data-expires-at={session.expires_at}
>
0h 0m left · (Expires{" "}
{new Date(session.expires_at).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})})
</span>
</div>
{session.last_activity_action && (
<div>
<strong>Last Active:</strong>{" "}
{session.last_activity_action} ({new Date(
session.last_activity_at || session.created_at,
).toLocaleTimeString()})
</div>
)}
</div>
{isAgent && (
<div style="display: flex; gap: 0.5rem;">
<button
type="button"
class="btn-outline"
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.8rem;"
data-on-click={`@post("/api/sessions/${session.id}/extend")`}
>
+1h Extend
</button>
<button
type="button"
class="btn-outline"
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.8rem;"
onclick={`openEditScopesModal('${session.id}', '${
session.label || "Delegated"
}', ${JSON.stringify(JSON.stringify(scopes))})`}
>
Scopes
</button>
</div>
)}
</div>
);
})
)}
</div>
);
};
}

View File

@ -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];
}