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
+1 -9
View File
@@ -1,12 +1,5 @@
import { isGuest } from './guest'
export class Unauthenticated extends Error { constructor(msg = 'unauthenticated') { super(msg) } }
// What a guest login may send. Everything else that would change something is refused here
// as well as by the server, so a button a page forgot to hide fails without a round trip.
const GUEST_WRITES = new Set(['POST /api/logout', 'PUT /api/prefs/ui'])
export const guestRefusal = 'a guest login can only look around'
// Which machine the console is looking at. '' is this host; otherwise every /api/… call
// is rewritten to /api/nodes/<name>/… and the hub proxies it. Hub-only calls (login,
// the machine list itself) pass local=true and are never rewritten.
@@ -26,7 +19,6 @@ function track(delta: number) {
}
async function req<T>(method: string, path: string, body?: unknown, local = false): Promise<T> {
if (isGuest() && method !== 'GET' && !GUEST_WRITES.has(`${method} ${path}`)) throw new Error(guestRefusal)
const target = local ? path : apiPath(path)
const mach = machineOf(target)
if (mach && downUntil[mach] > Date.now()) throw new Error(`${mach} unreachable (retrying shortly)`)
@@ -166,7 +158,7 @@ export interface UIPrefs { theme?: string; refreshMs?: number; accents?: Record<
export interface User { name: string; created: string; last_seen?: string; sessions: number; self: boolean }
export interface Session { id: string; created: string; last_seen: string; expires: string; ip?: string; agent?: string; current: boolean }
export interface TokenInfo { name: string; created: string; scope?: string; sensors?: string[]; issued_by?: string }
// A guest login: temporary, look-only, gone at `expires` or when revoked.
// A guest login: a full account with a clock on it, gone at `expires` or when revoked.
export interface Guest { name: string; created: string; expires: string; issued_by?: string; sessions: number; used: boolean }
export interface FreshGuest { name: string; password: string; expires: string; minutes: number }