A guest login is a full account with a clock on it

Mitchell, on the read-only version built earlier today: scratch it. He hands
these to people he trusts and revokes them otherwise, so a console with half
its tabs missing solved a problem he doesn't have.

So the allow-list, the middleware check and every "not for a guest" branch in
the UI are gone. What is left is the part that was the point: a throwaway
account that expires by itself, is checked on every request, swept once a
minute, and can be ended in one tap. A guest now gets the whole console on
every machine, terminal included.

Said once, since it is his call and he has made it: that means a guest has a
root-capable shell on every host, and can make an account or a token of its own
that outlives the guest login.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-27 17:53:55 -07:00
co-authored by Claude Opus 5
parent 8a7167bf03
commit 179f342a2c
11 changed files with 65 additions and 346 deletions
+7 -25
View File
@@ -1,27 +1,7 @@
// Whether this console is being looked at through a guest login, kept here so any page can
// ask without it being threaded through every prop. Set from /api/me when the app loads.
//
// This is politeness, not security: a guest is stopped by the server, which answers 403 to
// everything but the handful of reads it allows (internal/api/guests.go). What it buys is a
// console that doesn't offer a visitor buttons that would only fail.
// A guest login's clock. That is the whole difference between a guest and an account: it
// can do everything, until it can't, so what the console has to show is how long is left.
import { useEffect, useState } from 'react'
let guest = false
let until = ''
export function setGuest(on: boolean, expires?: string) {
guest = on
until = expires ?? ''
}
export const isGuest = () => guest
// msLeft is how long this guest login has, or null when it isn't one.
export function msLeft(): number | null {
if (!guest || !until) return null
return Math.max(0, new Date(until).getTime() - Date.now())
}
// fmtLeft says how long is left, in the roundest terms that are still honest.
export function fmtLeft(ms: number): string {
const min = Math.floor(ms / 60000)
@@ -30,13 +10,15 @@ export function fmtLeft(ms: number): string {
return 'under a minute'
}
// useCountdown re-renders the caller every 15 seconds, for a clock that has to keep up.
export function useCountdown(active: boolean): number {
// useTimeLeft is the milliseconds until `until`, re-rendering the caller every 15 seconds so
// the number keeps up. 0 when there is no expiry, which is every normal account.
export function useTimeLeft(until?: string): number {
const [, tick] = useState(0)
const active = !!until
useEffect(() => {
if (!active) return
const t = setInterval(() => tick((n) => n + 1), 15000)
return () => clearInterval(t)
}, [active])
return msLeft() ?? 0
return until ? Math.max(0, new Date(until).getTime() - Date.now()) : 0
}