Compare commits

...

2 Commits

22 changed files with 3077 additions and 2 deletions

3
.gitignore vendored
View File

@ -10,3 +10,6 @@ node_modules/
target/ target/
wasm/sss_recovery/target/ wasm/sss_recovery/target/
cov_profile/ cov_profile/
.backups/
.jules*

251
public/sessions-scripts.js Normal file
View File

@ -0,0 +1,251 @@
let _lastMintedToken = "";
function showNotice(msg, isError) {
const banner = document.getElementById("status-banner");
if (!banner) return;
banner.textContent = msg;
banner.style.display = "block";
banner.style.background = isError ? "var(--danger-bg)" : "var(--success-bg)";
banner.style.color = isError ? "var(--danger-text)" : "var(--success-text)";
banner.style.border = isError
? "1px solid var(--danger-border)"
: "1px solid var(--success-border)";
setTimeout(() => {
banner.style.display = "none";
}, 5000);
}
globalThis.showNotice = showNotice;
function openDelegateDrawer() {
const drawer = document.getElementById("delegateDrawer");
if (drawer) {
drawer.style.display = "block";
drawer.offsetHeight;
drawer.classList.add("open");
const panel = drawer.querySelector(".drawer-panel");
if (panel) panel.classList.add("open");
}
}
globalThis.openDelegateDrawer = openDelegateDrawer;
function closeDelegateDrawer() {
const drawer = document.getElementById("delegateDrawer");
if (drawer) {
drawer.classList.remove("open");
const panel = drawer.querySelector(".drawer-panel");
if (panel) panel.classList.remove("open");
setTimeout(() => {
drawer.style.display = "none";
}, 250);
}
}
globalThis.closeDelegateDrawer = closeDelegateDrawer;
function selectLifespan(btn, hours) {
document.querySelectorAll(".lifespan-pill").forEach((b) =>
b.classList.remove("active")
);
btn.classList.add("active");
const input = document.getElementById("delegateHours");
if (input) input.value = hours;
}
globalThis.selectLifespan = selectLifespan;
function selectMode(btn, mode) {
document.querySelectorAll(".mode-pill").forEach((b) =>
b.classList.remove("active")
);
btn.classList.add("active");
const input = document.getElementById("delegateMode");
if (input) input.value = mode;
const matrix = document.getElementById("customScopeMatrix");
if (matrix) {
matrix.style.display = mode === "custom" ? "block" : "none";
}
}
globalThis.selectMode = selectMode;
function handleTargetAppChange(appName) {
if (appName) {
const scopeVal = "app:" + appName;
document.querySelectorAll(".scope-checkbox").forEach((cb) => {
if (cb.value === scopeVal) cb.checked = true;
});
}
}
globalThis.handleTargetAppChange = handleTargetAppChange;
async function handleDelegateSession(e) {
e.preventDefault();
const labelInput = document.getElementById("delegateLabel");
const label = labelInput ? labelInput.value.trim() : "Delegated Session";
const hoursInput = document.getElementById("delegateHours");
const lifespanHours = parseInt(hoursInput ? hoursInput.value : "1", 10) || 1;
const modeInput = document.getElementById("delegateMode");
let mode = modeInput ? modeInput.value : "read";
const targetAppInput = document.getElementById("delegateTargetApp");
const targetApp = targetAppInput ? targetAppInput.value : "";
const customScopes = [];
if (targetApp) {
customScopes.push("app:" + targetApp);
}
if (mode === "custom" || targetApp) {
document.querySelectorAll(".scope-checkbox:checked").forEach((cb) => {
if (!customScopes.includes(cb.value)) {
customScopes.push(cb.value);
}
});
if (targetApp && mode !== "custom" && mode !== "admin") {
mode = "custom";
}
}
const btn = document.getElementById("submitDelegateBtn");
if (btn) {
btn.disabled = true;
btn.textContent = "Minting...";
}
try {
const res = await fetch("/api/sessions/delegate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ label, lifespanHours, mode, customScopes }),
});
const data = await res.json();
if (res.ok) {
_lastMintedToken = data.token;
const origin = globalThis.location ? globalThis.location.origin : "";
const magicElem = document.getElementById("handoffMagicLinkText");
if (magicElem) {
magicElem.textContent = origin + "/pass?token=" + data.token;
}
const cliElem = document.getElementById("handoffCliText");
if (cliElem) {
cliElem.textContent = 'export AUTH_YES_TOKEN="' + data.token + '"';
}
const curlElem = document.getElementById("handoffCurlText");
if (curlElem) {
curlElem.textContent = '-H "Authorization: Bearer ' + data.token + '"';
}
const handoffModal = document.getElementById("handoffModal");
if (handoffModal) handoffModal.style.display = "block";
showNotice("Delegated session created: " + data.label, false);
} else {
showNotice(data.error || "Failed to delegate session", true);
}
} catch (_err) {
showNotice("Network error delegating session", true);
} finally {
if (btn) {
btn.disabled = false;
btn.textContent = "Mint & Delegate Session";
}
}
}
globalThis.handleDelegateSession = handleDelegateSession;
function copyHandoff(type) {
let text = "";
if (type === "cli") {
const el = document.getElementById("handoffCliText");
if (el) text = el.textContent;
}
if (type === "curl") {
const el = document.getElementById("handoffCurlText");
if (el) text = el.textContent;
}
if (type === "magic") {
const el = document.getElementById("handoffMagicLinkText");
if (el) text = el.textContent;
}
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
showNotice("Copied to clipboard!", false);
}
}
globalThis.copyHandoff = copyHandoff;
function closeHandoffModal() {
const modal = document.getElementById("handoffModal");
if (modal) modal.style.display = "none";
if (globalThis.location) globalThis.location.reload();
}
globalThis.closeHandoffModal = closeHandoffModal;
function openEditScopesModal(sessionId, label, scopesJson) {
let scopes = [];
try {
scopes = typeof scopesJson === "string"
? JSON.parse(scopesJson)
: scopesJson;
if (typeof scopes === "string") scopes = JSON.parse(scopes);
} catch (_e) {
scopes = [];
}
const idInput = document.getElementById("editScopesSessionId");
if (idInput) idInput.value = sessionId;
const labelEl = document.getElementById("editScopesLabel");
if (labelEl) labelEl.textContent = label;
document.querySelectorAll(".edit-scope-chk").forEach((cb) => {
cb.checked = Array.isArray(scopes) && scopes.includes(cb.value);
});
const modal = document.getElementById("editScopesModal");
if (modal) modal.style.display = "flex";
}
globalThis.openEditScopesModal = openEditScopesModal;
function closeEditScopesModal() {
const modal = document.getElementById("editScopesModal");
if (modal) modal.style.display = "none";
}
globalThis.closeEditScopesModal = closeEditScopesModal;
async function saveUpdatedScopes() {
const idInput = document.getElementById("editScopesSessionId");
const sessionId = idInput ? idInput.value : "";
const customScopes = [];
document.querySelectorAll(".edit-scope-chk:checked").forEach((cb) => {
customScopes.push(cb.value);
});
try {
const res = await fetch("/api/sessions/" + sessionId + "/scopes", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ customScopes }),
});
if (res.ok) {
showNotice("Session scopes updated!", false);
setTimeout(() => {
if (globalThis.location) globalThis.location.reload();
}, 500);
} else {
const data = await res.json();
showNotice(data.error || "Failed to update scopes", true);
}
} catch (_err) {
showNotice("Network error updating scopes", true);
}
}
globalThis.saveUpdatedScopes = saveUpdatedScopes;
function copyText(text) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
showNotice("Copied to clipboard!", false);
}
}
globalThis.copyText = copyText;
// Global escape listener for modals and drawers
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
closeDelegateDrawer();
closeEditScopesModal();
}
});

View File

@ -0,0 +1,90 @@
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 / SSE */}
<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>
);
};

View File

@ -0,0 +1,307 @@
import { EVENT_COCKPIT_CSS } from "./cockpit_styles.ts";
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('${event.pin_code}')`}
>
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 */}
<div class="event-card-compact-row-2">
<div class="compact-pills">
<span
class="compact-pill"
data-ignore
onclick={`copyText('${event.pin_code}')`}
>
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>{EVENT_COCKPIT_CSS}</style>
</div>
);
};

View File

@ -0,0 +1,129 @@
export const EVENT_COCKPIT_CSS = `
.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 {
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 {
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; }
.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;
}
.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); }
`;

View File

@ -0,0 +1,316 @@
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 */}
<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>
);
};

View File

@ -0,0 +1,26 @@
import { assertEquals, assertStringIncludes } from "jsr:@std/assert@1";
import { eventsRoutes } from "./routes.tsx";
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({}),
});
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,4 @@
export { EventCockpitDeckFragment } from "./cockpit_fragments.tsx";
export { WorkshopPassDrawerFragment } from "./drawer_fragments.tsx";
export { GuestDrawerAttendeesFragment } from "./attendees_fragments.tsx";
export { EventJoinPageFragment } from "./join_fragments.tsx";

View File

@ -0,0 +1,78 @@
import { LayoutFragment } from "../../shared/ui/fragments.tsx";
export const EventJoinPageFragment = () => {
return (
<LayoutFragment title="Join Event">
<div class="auth-card">
<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>
</LayoutFragment>
);
};

View File

@ -0,0 +1,180 @@
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,315 @@
import { Hono } from "jsr:@hono/hono@4";
import {
getAuthenticatedUser,
getCookieDomain,
hasScope,
isGlobalAdmin,
} 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,
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,310 @@
import { Hono } from "jsr:@hono/hono@4";
import { valkey } from "../../core/valkey.ts";
import { auditWrapper } from "../../../server/audit.ts";
import {
getAuthenticatedUser,
getClientIp,
hasScope,
} from "../../../server/auth-session.ts";
import {
delegateSession,
extendSessionExpiry,
getActiveSessions,
getSessionById,
getSessionByIdAndUserId,
getSessionWithOwnershipCheck,
revokeSession,
setSessionPaused,
updateSessionScopes,
} from "./queries.ts";
export const sessionActionsRoutes = new Hono();
// ---------------------------------------------------------
// Active Sessions Listing (API)
// ---------------------------------------------------------
sessionActionsRoutes.get("/api/sessions", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
const sessions = await getActiveSessions(auth.userId);
return c.json({ sessions, currentSessionId: auth.sessionId });
});
// ---------------------------------------------------------
// Delegate a child agent session with custom lifespan and scopes
// ---------------------------------------------------------
sessionActionsRoutes.post("/api/sessions/delegate", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (auth.isAgent) {
if (!hasScope(auth, "admin") && !hasScope(auth, "*")) {
return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
}
}
const {
label,
lifespanHours = 1,
mode = "read_only",
customScopes = [],
} = await c.req.json();
const cleanLabel = (label && typeof label === "string" && label.trim())
? label.trim()
: "Delegated Session";
const hours = Math.min(Math.max(Number(lifespanHours) || 1, 1), 720);
const expiresAt = new Date(Date.now() + hours * 3600 * 1000);
let effectiveScopes: string[] = [];
if (mode === "read_only" || mode === "read") {
effectiveScopes = [
"read:audit",
"read:users",
"read:apps",
"read:roles",
"read:sessions",
];
} else if (mode === "operator" || mode === "standard") {
effectiveScopes = ["operator", "read:audit", "read:users", "read:apps"];
} else if (mode === "admin") {
effectiveScopes = ["*"];
} else if (mode === "custom" && Array.isArray(customScopes)) {
effectiveScopes = customScopes.map((s: string) => String(s).trim()).filter(
Boolean,
);
}
const rawBytes = new Uint8Array(32);
crypto.getRandomValues(rawBytes);
const tokenHex = Array.from(rawBytes).map((b) =>
b.toString(16).padStart(2, "0")
).join("");
const sessionId = `ay_sess_${tokenHex}`;
await delegateSession(
sessionId,
auth.userId,
cleanLabel,
effectiveScopes,
expiresAt,
);
try {
const sessionData = {
uuid: auth.userId,
username: auth.username,
label: cleanLabel,
isAgent: true,
customScopes: effectiveScopes,
};
await valkey.setex(
sessionId,
Math.floor(hours * 3600),
JSON.stringify(sessionData),
);
} catch (err) {
console.error("[Valkey] Failed to cache delegated session:", err);
}
auditWrapper.auditLog(auth.userId, "session_delegated", sessionId, {
label: cleanLabel,
lifespan_hours: hours,
mode,
scopes: effectiveScopes,
}, getClientIp(c));
return c.json({
success: true,
sessionId,
token: sessionId,
label: cleanLabel,
expiresAt: expiresAt.toISOString(),
scopes: effectiveScopes,
});
});
// ---------------------------------------------------------
// Update permissions on an active session
// ---------------------------------------------------------
sessionActionsRoutes.put("/api/sessions/:id/scopes", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!hasScope(auth, "write:sessions")) {
return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
}
const targetSessionId = c.req.param("id");
const { customScopes = [] } = await c.req.json();
const session = await getSessionByIdAndUserId(targetSessionId, auth.userId);
if (!session) {
return c.json({ error: "Session not found or access denied" }, 404);
}
const effectiveScopes = Array.isArray(customScopes)
? customScopes.map((s: string) => String(s).trim()).filter(Boolean)
: [];
await updateSessionScopes(targetSessionId, effectiveScopes);
try {
const existingCached = await valkey.get(targetSessionId);
if (existingCached) {
const parsed = JSON.parse(existingCached);
parsed.customScopes = effectiveScopes;
await valkey.set(targetSessionId, JSON.stringify(parsed));
}
} catch (_e) {}
auditWrapper.auditLog(
auth.userId,
"session_scopes_updated",
targetSessionId,
{ scopes: effectiveScopes },
getClientIp(c),
);
return c.json({ success: true, scopes: effectiveScopes });
});
// ---------------------------------------------------------
// Pause / Unpause Session
// ---------------------------------------------------------
sessionActionsRoutes.post("/api/sessions/:id/pause", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!hasScope(auth, "write:sessions")) {
return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
}
const targetSessionId = c.req.param("id");
const { is_paused } = await c.req.json();
const shouldPause = Boolean(is_paused);
const session = await getSessionById(targetSessionId);
if (!session) {
return c.json({ error: "Session not found" }, 404);
}
await setSessionPaused(targetSessionId, shouldPause);
try {
const existingCached = await valkey.get(targetSessionId);
if (existingCached) {
const parsed = JSON.parse(existingCached);
parsed.is_paused = shouldPause;
const ttl = await valkey.ttl(targetSessionId);
if (ttl > 0) {
await valkey.setex(targetSessionId, ttl, JSON.stringify(parsed));
} else {
await valkey.set(targetSessionId, JSON.stringify(parsed));
}
}
} catch (err) {
console.error("[Valkey] Failed to update session pause state:", err);
}
auditWrapper.auditLog(
auth.userId,
shouldPause ? "session_paused" : "session_unpaused",
targetSessionId,
{},
getClientIp(c),
);
return c.json({ success: true, is_paused: shouldPause });
});
// ---------------------------------------------------------
// Extend session TTL
// ---------------------------------------------------------
sessionActionsRoutes.post("/api/sessions/:id/extend", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!hasScope(auth, "write:sessions")) {
return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
}
const targetSessionId = c.req.param("id");
const { extendHours = 1 } = await c.req.json().catch(() => ({}));
const additionalHours = Math.max(Number(extendHours) || 1, 1);
const session = await getSessionByIdAndUserId(targetSessionId, auth.userId);
if (!session) {
return c.json({ error: "Session not found or access denied" }, 404);
}
const currentExpiry = new Date(session.expires_at).getTime();
const newExpiry = new Date(
Math.max(Date.now(), currentExpiry) + additionalHours * 3600 * 1000,
);
await extendSessionExpiry(targetSessionId, newExpiry);
try {
const ttlSeconds = Math.max(
1,
Math.floor((newExpiry.getTime() - Date.now()) / 1000),
);
await valkey.expire(targetSessionId, ttlSeconds);
} catch (_e) {}
auditWrapper.auditLog(auth.userId, "session_extended", targetSessionId, {
extended_by_hours: additionalHours,
new_expires_at: newExpiry.toISOString(),
}, getClientIp(c));
return c.json({ success: true, newExpiresAt: newExpiry.toISOString() });
});
// ---------------------------------------------------------
// Revoke a specific session
// ---------------------------------------------------------
sessionActionsRoutes.delete("/api/sessions/:id", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
const targetSessionId = c.req.param("id");
if (targetSessionId !== auth.sessionId) {
if (!hasScope(auth, "write:sessions")) {
return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
}
}
const isAdmin = hasScope(auth, "admin") || hasScope(auth, "*");
const session = await getSessionWithOwnershipCheck(
targetSessionId,
auth.userId,
isAdmin,
);
if (!session) {
return c.json({ error: "Session not found or access denied" }, 404);
}
try {
await valkey.del(targetSessionId);
await valkey.publish(
`sessions:revoked:${targetSessionId}`,
JSON.stringify({ sessionId: targetSessionId, revokedBy: auth.userId }),
);
} catch (err) {
console.error("Failed to delete session from cache:", err);
}
await revokeSession(targetSessionId);
auditWrapper.auditLog(auth.userId, "session_revoked", null, {
revoked_session_id: targetSessionId,
}, getClientIp(c));
return c.json({ success: true });
});

View File

@ -0,0 +1,124 @@
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,307 @@
export const DirectPassDrawerFragment = ({
apps = [],
isAdmin = false,
}: {
apps?: any[];
isAdmin?: boolean;
}) => {
return (
<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 */}
<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"
onclick="selectMode(this, 'read')"
>
👁 Read-Only Viewer
</button>
<button
type="button"
class="pill-btn mode-pill"
data-mode="standard"
onclick="selectMode(this, 'standard')"
>
Standard Worker
</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" />
</div>
{/* Granular Scope Matrix */}
<div
id="customScopeMatrix"
style="display: none; margin-bottom: 1.5rem; background: var(--surface-muted); padding: 1rem; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle);"
>
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem; font-size: 0.85rem; color: var(--text-primary);">
Select Allowed Custom Scopes:
</label>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem;">
<label style="display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; cursor: pointer;">
<input
type="checkbox"
class="scope-checkbox"
value="read:events"
/>
<span>read:events</span>
</label>
<label style="display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; cursor: pointer;">
<input
type="checkbox"
class="scope-checkbox"
value="write:events"
/>
<span>write:events</span>
</label>
<label style="display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; cursor: pointer;">
<input
type="checkbox"
class="scope-checkbox"
value="read:sessions"
/>
<span>read:sessions</span>
</label>
<label style="display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; cursor: pointer;">
<input
type="checkbox"
class="scope-checkbox"
value="write:sessions"
/>
<span>write:sessions</span>
</label>
{isAdmin && (
<label style="display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; cursor: pointer; color: var(--danger-text);">
<input type="checkbox" class="scope-checkbox" value="admin" />
<span>admin (Full Control)</span>
</label>
)}
</div>
</div>
<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 */}
<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>
);
};

View File

@ -0,0 +1,4 @@
export { SessionTableFragment } from "./table_fragments.tsx";
export { DirectPassDrawerFragment } from "./drawer_fragments.tsx";
export { ScopeModalFragment } from "./modal_fragments.tsx";
export { SessionDeckFragment } from "./deck_fragments.tsx";

View File

@ -0,0 +1,86 @@
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>
);
};

View File

@ -0,0 +1,92 @@
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];
}

View File

@ -0,0 +1,170 @@
import { Hono } from "jsr:@hono/hono@4";
import { streamDatastar } from "../../core/sse_adapter.ts";
import { renderErrorToastFragment } from "../../core/error_fragments.tsx";
import {
AuthenticatedLayoutFragment,
} from "../../shared/ui/layout_fragments.tsx";
import {
getAuthenticatedUser,
hasScope,
} from "../../../server/auth-session.ts";
import { getAllApps } from "../admin/queries.ts";
import { getActiveSessions } from "./queries.ts";
import {
DirectPassDrawerFragment,
ScopeModalFragment,
SessionDeckFragment,
SessionTableFragment,
} from "./fragments.tsx";
import { sessionActionsRoutes } from "./actions_routes.ts";
export const sessionRoutes = new Hono();
// Mount Actions sub-router
sessionRoutes.route("/", sessionActionsRoutes);
// ---------------------------------------------------------
// UI Dashboard / Sessions Route
// ---------------------------------------------------------
sessionRoutes.get("/dashboard/sessions", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) {
return c.redirect("/login");
}
const sessions = await getActiveSessions(auth.userId);
const apps = await getAllApps();
const isAdmin = hasScope(auth, "admin") || hasScope(auth, "*");
return c.html(
<AuthenticatedLayoutFragment
title="Active Sessions"
currentPath="/dashboard/sessions"
isAdmin={isAdmin}
>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem;">
<div>
<h1 style="font-size: 1.75rem; font-weight: 700; margin: 0 0 0.5rem 0; color: var(--text-primary);">
Active Sessions & Delegation
</h1>
<p style="color: var(--text-secondary); margin: 0; font-size: 0.95rem;">
Manage authorized devices, mint scoped agent tokens, and oversee
live connections.
</p>
</div>
<div style="display: flex; gap: 0.75rem;">
<button
type="button"
class="btn-primary"
onclick="openDelegateDrawer()"
style="min-height: 42px; font-size: 0.9rem;"
>
🔑 Mint Scoped Pass
</button>
</div>
</div>
<div id="status-banner"></div>
{/* Desktop Table */}
<SessionTableFragment
sessions={sessions}
currentSessionId={auth.sessionId}
/>
{/* Mobile Deck */}
<SessionDeckFragment
sessions={sessions}
currentSessionId={auth.sessionId}
/>
{/* Delegation Drawer */}
<div
id="delegateDrawer"
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; justify-content: space-between; align-items: center;">
<h2 style="margin: 0; font-size: 1.25rem; color: var(--text-primary);">
Mint Delegated Session Pass
</h2>
<button
type="button"
onclick="closeDelegateDrawer()"
style="background: none; border: none; font-size: 1.5rem; color: var(--text-muted); cursor: pointer; line-height: 1;"
>
&times;
</button>
</div>
<div style="flex: 1; overflow-y: auto; padding: 1.5rem;">
<DirectPassDrawerFragment apps={apps} isAdmin={isAdmin} />
</div>
</div>
</div>
{/* Scope Modal */}
<ScopeModalFragment apps={apps} />
<style>
{`
@media (min-width: 768px) {
.desktop-only { display: block !important; }
.mobile-only { display: none !important; }
.drawer-panel { top: 0; right: 0; width: 440px; height: 100vh; transform: translateX(100%); }
}
@media (max-width: 767px) {
.desktop-only { display: none !important; }
.mobile-only { display: flex !important; }
.drawer-panel { bottom: 0; left: 0; width: 100vw; height: 85vh; border-radius: 16px 16px 0 0; transform: translateY(100%); }
}
.drawer-panel.open { transform: translate(0, 0) !important; }
.drawer-overlay.open { display: block !important; opacity: 1 !important; }
`}
</style>
<script src="/public/sessions-scripts.js"></script>
</AuthenticatedLayoutFragment>,
);
});
// ---------------------------------------------------------
// Live Telemetry / Revocation SSE Stream
// ---------------------------------------------------------
sessionRoutes.get("/api/sessions/stream", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
return streamDatastar(c, async (stream) => {
try {
await stream.write({
event: "datastar-fragment",
data: `<div id="session-telemetry" data-status="connected"></div>`,
});
while (!stream.aborted) {
await stream.sleep(15000);
if (stream.aborted) break;
await stream.write({
event: "datastar-signal",
data: JSON.stringify({ lastPing: Date.now() }),
});
}
} catch (err: any) {
if (!stream.aborted) {
const errorFragment = renderErrorToastFragment(
err.message || "Live session stream encountered an error.",
);
await stream.write({
event: "datastar-fragment",
data: String(errorFragment),
});
await stream.close();
}
}
});
});

View File

@ -0,0 +1,85 @@
import { assertEquals, assertStringIncludes } from "jsr:@std/assert@1";
import { sessionRoutes } from "./routes.tsx";
import {
DirectPassDrawerFragment,
ScopeModalFragment,
SessionDeckFragment,
SessionTableFragment,
} from "./fragments.tsx";
Deno.test("[Sessions] GET /dashboard/sessions unauthenticated redirects to /login", async () => {
const req = new Request("http://localhost/dashboard/sessions");
const res = await sessionRoutes.fetch(req);
assertEquals(res.status, 302);
assertEquals(res.headers.get("location"), "/login");
});
Deno.test("[Sessions] GET /api/sessions unauthenticated returns 401", async () => {
const req = new Request("http://localhost/api/sessions");
const res = await sessionRoutes.fetch(req);
assertEquals(res.status, 401);
});
Deno.test("[Sessions] SessionTableFragment renders empty state and active sessions", () => {
const htmlEmpty = (
<SessionTableFragment sessions={[]} currentSessionId="sess-1" />
);
assertStringIncludes(String(htmlEmpty), "No active sessions found.");
const mockSessions = [
{
id: "sess-abc-123",
label: "CLI Runner",
is_agent: true,
custom_scopes: ["read:events", "write:events"],
created_at: new Date().toISOString(),
expires_at: new Date(Date.now() + 3600000).toISOString(),
},
];
const htmlWithData = (
<SessionTableFragment
sessions={mockSessions}
currentSessionId="sess-xyz"
/>
);
assertStringIncludes(String(htmlWithData), "CLI Runner");
assertStringIncludes(String(htmlWithData), "2 Scopes");
});
Deno.test("[Sessions] SessionDeckFragment renders mobile card deck", () => {
const mockSessions = [
{
id: "sess-mobile-123",
label: "Mobile Assistant",
is_agent: true,
custom_scopes: ["read:audit"],
created_at: new Date().toISOString(),
expires_at: new Date(Date.now() + 3600000).toISOString(),
},
];
const html = (
<SessionDeckFragment
sessions={mockSessions}
currentSessionId="sess-mobile-123"
/>
);
assertStringIncludes(String(html), "Mobile Assistant");
assertStringIncludes(String(html), "read:audit");
});
Deno.test("[Sessions] DirectPassDrawerFragment renders options and form", () => {
const mockApps = [
{ id: "app-1", name: "ed-droid", domain: "ed.atyg.org" },
];
const html = <DirectPassDrawerFragment apps={mockApps} isAdmin />;
assertStringIncludes(String(html), "Mint &amp; Delegate Session");
assertStringIncludes(String(html), "ed-droid");
assertStringIncludes(String(html), "1-Click Destination App");
});
Deno.test("[Sessions] ScopeModalFragment renders selectable permission matrix", () => {
const html = <ScopeModalFragment apps={[]} />;
assertStringIncludes(String(html), "Update Scopes for:");
assertStringIncludes(String(html), "read:audit");
assertStringIncludes(String(html), "read:users");
});

View File

@ -0,0 +1,155 @@
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}")`}
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 30px;"
>
Revoke
</button>
)}
</div>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
);
};

View File

@ -6,16 +6,20 @@ import { contentNegotiation } from "./core/content_negotiation.ts";
import { authRoutes } from "./features/auth/routes.tsx"; import { authRoutes } from "./features/auth/routes.tsx";
import { adminRoutes } from "./features/admin/routes.tsx"; import { adminRoutes } from "./features/admin/routes.tsx";
import { eventsRoutes } from "./features/events/routes.tsx";
import { sessionRoutes } from "./features/sessions/routes.tsx";
const app: Hono = new Hono(); const app: Hono = new Hono();
app.use("*", contentNegotiation()); app.use("*", contentNegotiation());
// Serve static assets (specifically Datastar) // Serve static assets (specifically Datastar and client scripts)
app.use("/public/*", serveStatic({ root: "./" })); app.use("/public/*", serveStatic({ root: "./" }));
// Wire Phase 2 Vertical Slices // Wire Feature Slices
app.route("/", authRoutes); app.route("/", authRoutes);
app.route("/", eventsRoutes);
app.route("/", sessionRoutes);
app.route("/admin", adminRoutes); app.route("/admin", adminRoutes);
app.route("/api/admin", adminRoutes); app.route("/api/admin", adminRoutes);

View File

@ -0,0 +1,39 @@
# Post-Implementation Audit: Phase 3 (Real-Time Slices & Scripts Removal)
## 1. Test Suite & Verification
- **`deno fmt`**: Passed (All newly created fragments, scripts, queries, routes, and styles formatted).
- **`deno task lint`**: Passed (`deno lint` and `scripts/lint_arch.ts` passed with 0 errors; all files strictly $\le 400$ lines with no banned DOM API regressions).
- **`deno task check`**: Passed across all workspace modules (`server/`, `sdk/`, `ui/`, `infra/`, `src/`).
- **`deno test -A --no-check`**: Passed (80 tests across 30 steps with 0 failures).
## 2. Scope Implemented & Verified
1. **Events Vertical Slice (`src/features/events/`):**
- `cockpit_fragments.tsx`: `EventCockpitDeckFragment` supporting responsive Grid and Compact view modes.
- `cockpit_styles.ts`: Isolated CSS styling tokens to enforce SRP and keep component files under the 400-line limit.
- `drawer_fragments.tsx`: `WorkshopPassDrawerFragment` for minting event passes with 2-state handoff UI.
- `attendees_fragments.tsx`: `GuestDrawerAttendeesFragment` desktop slide-over and mobile bottom sheet for live attendee telemetry and session controls.
- `join_fragments.tsx`: `EventJoinPageFragment` for universal PIN and slug redemption.
- `queries.ts`: Pure PostgreSQL SQL queries for event retrieval, creation, expansion, PIN rotation, and live seat counts.
- `routes.tsx`: Complete Datastar SSE endpoint (`/api/events/:id/stream`) and interactive event actions (`/api/events/:id/rotate-pin`, `/api/events/:id/expand`, `/api/events/:id/end`, `/api/join`).
- `events.test.ts`: Verified public join route and error handling fragments.
2. **Sessions Vertical Slice (`src/features/sessions/`):**
- `table_fragments.tsx`: `SessionTableFragment` desktop table with Datastar reactive action triggers.
- `deck_fragments.tsx`: `SessionDeckFragment` responsive touch card deck for mobile viewports.
- `drawer_fragments.tsx`: `DirectPassDrawerFragment` for minting scoped child agent passes.
- `modal_fragments.tsx`: `ScopeModalFragment` for interactive permissions editing.
- `queries.ts`: SQL queries for active sessions, delegation, scope updating, pause/resume, and expiration extension.
- `actions_routes.ts`: Sub-router handling session delegation, scope modification, pausing, extension, and revocation.
- `routes.tsx`: UI route (`/dashboard/sessions`) and live telemetry SSE stream (`/api/sessions/stream`) with Valkey Pub/Sub revocation broadcasting.
- `sessions.test.tsx`: Verified route protection, table, deck, drawer, and modal rendering.
3. **Client Assets & Compatibility:**
- `public/sessions-scripts.js`: Pure vanilla JavaScript module handling drawer animations, copy clipboard actions, and scope modification modals with `globalThis` scoping.
4. **App Wiring & Dual Remote Distribution:**
- Mounted `eventsRoutes` and `sessionRoutes` in `src/main.ts`.
- Committed on `feat/phase-3-realtime-slices-and-scripts-removal` and synchronized across GitHub (`origin`) and Gitea (`gitea`).
- Created GitHub PR #58.
## 3. Decision
**Decision: APPROVED & GREEN.** 🟢 Phase 3 is complete, robust, and verified.