/** * Web accounts, PER INSTANCE (per launch directory) — the same scope as * channels, crons and the instance lock. * * Per instance rather than global is the consistent choice: SHADOK_GUI_PASSWORD * is already per process, so accounts share the scope of the door they extend. * Profiles and the secret vault are the global exception, not the rule. */ export type Role = "admin" | "member"; export interface Account { name: string; role: Role; /** Absent until an invitation is redeemed. */ passwordHash?: string; createdAt: number; /** Present only while the invitation is outstanding. */ invite?: { token: string; expiresAt: number; }; } /** The account that lives in SHADOK_GUI_PASSWORD, never in the file. */ export declare const BOOTSTRAP_ADMIN = "admin"; export declare function loadAccounts(): Account[]; export declare function saveAccounts(list: Account[]): void; /** `scrypt$$` — salted, so two identical passwords do * not produce the same hash and cannot be spotted as identical. */ export declare function hashPassword(plain: string): string; export declare function verifyPassword(plain: string, hash: string): boolean; /** * Who may change which account. Pure, so the whole policy is one testable * place rather than a condition repeated at three endpoints. */ export declare function userWriteVerdict(o: { actorRole: Role | null; action: "create" | "delete" | "role"; target: string; exists: boolean; }): { ok: true; } | { ok: false; error: string; }; /** * The key that signs sessions — per instance, drawn once, persisted. * * NOT derived from SHADOK_GUI_PASSWORD, and never exported into an agent's * environment. The password reaches every agent's env today (measured on three * production agents, 2026-08-23); signing with it would let any agent mint a * cookie for any user. Untidy becomes impersonation the moment accounts exist. */ export declare function sessionSecret(): Buffer; /** `..` — the name is encoded so a dot in it * cannot shift the fields. The ROLE is deliberately absent: it is re-read from * the account file at use time, so a demotion takes effect immediately instead * of riding in a stale cookie. */ export declare function signSession(user: string, issuedAt: number, secret: Buffer): string; export declare function readSession(token: string, secret: Buffer, now: number, maxAgeMs: number): string | null; /** A week: long enough to hand the link over by another channel, short enough * that a forgotten one stops working. */ export declare const INVITE_TTL_MS: number; export declare function newInvite(now: number): { token: string; expiresAt: number; }; /** * Whether this link may still be redeemed. * * Each refusal names its own reason: "expired" tells the holder to ask for a * new link, "already redeemed" tells them the account is live, and "invalid" is * a real mismatch. One generic error would send all three to the wrong place. */ export declare function inviteVerdict(account: Account | undefined, token: string, now: number): { ok: true; } | { ok: false; error: string; }; /** * Who a prompt is attributed to. * * The security property of the accounts feature, in one place: for a WEB client * the session decides and the frame's claim is discarded, because a browser can * put anything in `from`. The Telegram bridge is a trusted bridge that knows its * sender, so it keeps naming them; other origins (cli, cron) are the server's * own callers and keep whatever they supplied. */ export declare function promptAuthor(origin: string | undefined, sessionName: string | undefined, claimed: string | undefined): string | undefined; /** * The per-session capability key handed to an agent as `SHADOK_SESSION_KEY`. * * DERIVED, never stored. It used to be a `randomUUID` kept in an in-memory Map, * which had a cliff nobody had noticed: the Map dies with the server, while a * tmux agent does not. So every auto-update left every surviving agent holding * a key the new process had never issued — `/reload` and `/profiles/prompt` * answered 403 from then on, and the agent could not repair itself either. * * An HMAC of the session id needs no state to survive a restart, and carrying * the id inside the key keeps the wire format one opaque string, so nothing * that presents a key had to learn a second field. * * It authenticates a LIVE session and nothing else: the id half is public * (`/live` lists every id), and only the MAC proves the holder was handed this * by the server at spawn. Same-user shell access still trumps it — soft * isolation, not a sandbox (invariant 26). */ export declare function signSessionKey(sessionId: string, secret: Buffer): string; /** Pure: the session id a key attests to, or null if it does not verify. */ export declare function readSessionKey(key: string, secret: Buffer): string | null;