workerdeck 0.0.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instance-kupxU5UD.mjs","names":[],"sources":["../src/auth-key.ts","../src/auth.ts","../src/config.ts","../src/login-page.ts","../src/static.ts","../src/instance.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto'\nimport { chmod, mkdir, readFile, stat, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\n\n/**\n * Materializes the key that `resolveInstanceConfig` promised via\n * `generateAuthKey`: resolution is pure and synchronous, reading a key file is\n * I/O, so the two halves meet here at startup. The contract is that this\n * function returns a usable secret or throws — it never returns \"no key\",\n * because the resolved config has already stood down the Host-header guard on\n * the strength of the promise, and a silent miss would serve an open gateway\n * that reports itself authenticated.\n *\n * The key persists under `stateDir` so a restart does not un-pair every client\n * that stored it (the iOS app keeps it in its keychain). No `stateDir` means\n * nothing durable to write, so the key is ephemeral per run — the banner says\n * so. This is the gateway's own operator secret and nothing else: Anthropic\n * credentials never pass through here (root CLAUDE.md, auth red lines).\n */\n\nexport type MaterializedAuthKey = {\n key: string\n /** 'stored' reused the file, 'created' wrote a new one, 'ephemeral' had nowhere to write. */\n source: 'stored' | 'created' | 'ephemeral'\n /** Where the key lives, or null when ephemeral. */\n path: string | null\n}\n\n/** 48 hex chars — far past `createCliAuth`'s 12-char floor, and header-safe. */\nconst generateKey = (): string => randomBytes(24).toString('hex')\n\n/** A stored key must be one printable-ASCII line long enough to be a secret:\n * both transports (HTTP header, login form) choke on anything else, and a\n * truncated or garbage file should regenerate, not crash or half-work. */\nconst usableStoredKey = (raw: string): string | null => {\n const line = raw.split('\\n', 1)[0]?.trim() ?? ''\n return line.length >= 12 && /^[\\x21-\\x7e]+$/.test(line) ? line : null\n}\n\nexport async function materializeAuthKey(\n stateDir: string | null,\n options: { warn?: (message: string) => void } = {},\n): Promise<MaterializedAuthKey> {\n const warn =\n options.warn ?? ((message: string) => process.stderr.write(`[workerdeck] ${message}\\n`))\n if (stateDir === null) return { key: generateKey(), source: 'ephemeral', path: null }\n\n const path = join(stateDir, 'auth-key')\n let raw: string | null = null\n try {\n raw = await readFile(path, 'utf8')\n } catch {\n raw = null // Missing and unreadable land in the same place: generate fresh.\n }\n if (raw !== null) {\n const key = usableStoredKey(raw)\n if (key !== null) {\n try {\n const { mode } = await stat(path)\n if ((mode & 0o077) !== 0) {\n warn(\n `auth key file ${path} is readable by other users ` +\n `(mode ${(mode & 0o777).toString(8)}) — run: chmod 600 ${path}`,\n )\n }\n } catch {\n // stat failing after a successful read is exotic; the key still works.\n }\n return { key, source: 'stored', path }\n }\n }\n\n const key = generateKey()\n await mkdir(stateDir, { recursive: true, mode: 0o700 })\n await writeFile(path, `${key}\\n`, { mode: 0o600 })\n // writeFile's mode applies only on creation — regenerating over a corrupt\n // file must not inherit whatever loose bits the corrupt one had.\n await chmod(path, 0o600)\n return { key, source: 'created', path }\n}\n","import { createHash, randomBytes, timingSafeEqual } from 'node:crypto'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { Authenticator } from '@workerdeck/server'\n\n/**\n * Gateway auth for the turnkey CLI: one shared operator secret, two transports.\n *\n * Services present the secret itself on every request (`x-workerdeck-key`,\n * or `Authorization: Bearer`). The dashboard cannot: the SPA is prebuilt to\n * call `location.origin + '/v1'` with no headers, and a browser WebSocket\n * handshake carries no custom headers at all. So browsers POST the secret once\n * to `/auth/login`, get an HttpOnly cookie naming a server-side session, and\n * the cookie rides same-origin REST and the WS upgrade automatically. That\n * automatic ride is also the threat: the cookie is ambient authority, and the\n * WS handshake is exempt from CORS, so cross-origin misuse is fenced off by an\n * explicit Origin check here — not by the browser.\n *\n * This file guards the operator's own gateway and nothing else. It never sees\n * an Anthropic credential — those are resolved by the SDK/CLI from the\n * operator's environment (root CLAUDE.md, auth red lines).\n */\n\nexport type CliAuthOptions = {\n /**\n * The shared operator secret. Unset disables auth entirely (the CLI then\n * refuses to bind anything but loopback — enforced by the caller, not here).\n * An empty or short value is a config accident, not a choice: anything under\n * 12 characters throws rather than standing up a guessable gateway.\n */\n secret?: string\n /** Default 'workerdeck_session'. No `__Host-` prefix — it requires\n * `Secure`, and plain-HTTP localhost is the primary deployment. */\n cookieName?: string\n /**\n * Browser session lifetime, default 7 days. Fixed, not sliding: the auth\n * hooks only see the request, so a renewed cookie has nowhere to ride — and\n * for a dashboard whose whole login is retyping one secret, a periodic\n * re-login is cheaper than a refresh endpoint. Expiry (or a process restart —\n * sessions are in-memory by design) simply lands the operator back on the\n * login page.\n */\n ttlMs?: number\n /**\n * Trust `x-forwarded-proto` / `x-forwarded-host` / `x-forwarded-for` from\n * exactly one reverse proxy in front of this process; the *last* value of\n * each is used (the one the proxy set, the only position a client cannot\n * forge). Off by default: these are attacker-writable headers on a directly\n * exposed port. Behind a TLS-terminating proxy this must be on, or the\n * `Secure` cookie flag is skipped and the Origin check computes `http://`\n * where the browser says `https://` and rejects the dashboard's own writes.\n */\n trustProxy?: boolean\n /**\n * Origins accepted in addition to the request's own (scheme + Host). Needed\n * when the proxy rewrites Host so the external origin no longer matches what\n * this process sees. Entries must be full origins ('https://ops.example.com');\n * invalid ones throw at startup rather than silently never matching.\n */\n allowedOrigins?: string[]\n /** Login throttle tuning; defaults: 15 min window, 10 failures per IP, 100\n * globally. Exposed mainly so tests need not wait out real windows. */\n throttle?: { windowMs?: number; maxFailuresPerIp?: number; maxFailuresGlobal?: number }\n}\n\n/** What `authenticate` hands the worker server as the request principal. */\nexport type CliPrincipal = {\n /** Which transport authenticated the request; 'open' when auth is disabled. */\n via: 'header' | 'cookie' | 'open'\n /** One secret, one trust level: whoever holds it is the operator, so the\n * dashboard may manage provider profiles (still bounded by the server's\n * `allowedConfigDirRoots`). */\n canManageProfiles: true\n}\n\nexport type CliAuth = {\n enabled: boolean\n /** Hand straight to `createWorkerServer({ authenticate })` — it guards both\n * REST and the WS upgrade, which is exactly why the Origin policy lives in it. */\n authenticate: Authenticator\n /** Claims `/auth` and everything under it (login/logout/status); returns true\n * when it consumed the request. The static host must call this first. */\n handleAuthRequest(req: IncomingMessage, res: ServerResponse): boolean | Promise<boolean>\n /** Cookie-only check for the static host: login page or SPA? Gating the SPA\n * shell is UX, not security — every byte of data sits behind `authenticate`. */\n hasValidSession(req: IncomingMessage): boolean\n /** What the login page needs to render for this request. The endpoint path,\n * field name, and the `?auth=` redirect params are all this module's wire\n * format, so the page learns them here instead of hardcoding them. */\n loginPage(req: IncomingMessage): { action: string; field: string; error?: string }\n}\n\nconst MIN_SECRET_LENGTH = 12\nconst DEFAULT_COOKIE_NAME = 'workerdeck_session'\nconst DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000\nconst DEFAULT_THROTTLE_WINDOW_MS = 15 * 60 * 1000\nconst DEFAULT_MAX_FAILURES_PER_IP = 10\nconst DEFAULT_MAX_FAILURES_GLOBAL = 100\n/** Only successful logins insert, so this cap only fences the secret-holder's\n * own memory use (every login within the ttl is a live entry). Oldest goes. */\nconst MAX_SESSIONS = 100\nconst MAX_LOGIN_BODY_BYTES = 4096\nconst SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS'])\n\nconst sha256 = (value: string): Buffer => createHash('sha256').update(value).digest()\n\nexport function createCliAuth(options: CliAuthOptions = {}): CliAuth {\n const { secret } = options\n const enabled = secret !== undefined\n if (secret !== undefined && secret.length < MIN_SECRET_LENGTH) {\n throw new Error(\n `createCliAuth: secret must be at least ${MIN_SECRET_LENGTH} characters — ` +\n 'use a long random value, or leave it unset to run without auth on loopback',\n )\n }\n const cookieName = options.cookieName ?? DEFAULT_COOKIE_NAME\n const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS\n if (!(ttlMs > 0)) throw new Error('createCliAuth: ttlMs must be positive')\n const trustProxy = options.trustProxy === true\n const windowMs = options.throttle?.windowMs ?? DEFAULT_THROTTLE_WINDOW_MS\n const maxFailuresPerIp = options.throttle?.maxFailuresPerIp ?? DEFAULT_MAX_FAILURES_PER_IP\n const maxFailuresGlobal = options.throttle?.maxFailuresGlobal ?? DEFAULT_MAX_FAILURES_GLOBAL\n const allowedOrigins = new Set(\n (options.allowedOrigins ?? []).map((entry) => {\n try {\n return new URL(entry).origin\n } catch {\n throw new Error(`createCliAuth: allowedOrigins entry is not a valid origin: ${JSON.stringify(entry)}`)\n }\n }),\n )\n\n /** Both the raw secret and session tokens are compared as fixed-length SHA-256\n * digests via timingSafeEqual / digest-keyed lookup, so no code path compares\n * secret material byte-by-byte with early exit — and unequal input lengths\n * leak nothing either. */\n const secretDigest = secret === undefined ? undefined : sha256(secret)\n const secretMatches = (candidate: string): boolean =>\n secretDigest !== undefined && timingSafeEqual(sha256(candidate), secretDigest)\n\n /**\n * Browser sessions are a server-side table, not signed tokens: logout must\n * actually invalidate, and a stateless HMAC token stays valid until expiry no\n * matter what the server thinks. This is one long-lived process (multi-node\n * is a non-goal), so \"table\" means one Map — the cost is that a restart signs\n * every browser out, which for an operator dashboard one secret away from\n * re-login is the right trade. Keys are token digests: recovering an entry's\n * token from lookup timing would need a SHA-256 preimage.\n */\n const sessions = new Map<string, { expiresAt: number }>()\n const tokenKey = (token: string): string => sha256(token).toString('hex')\n\n const createSession = (): string => {\n if (sessions.size >= MAX_SESSIONS) {\n const oldest = sessions.keys().next().value\n if (oldest !== undefined) sessions.delete(oldest)\n }\n const token = randomBytes(32).toString('base64url')\n sessions.set(tokenKey(token), { expiresAt: Date.now() + ttlMs })\n return token\n }\n\n const cookieToken = (req: IncomingMessage): string | undefined => {\n const header = req.headers.cookie\n if (typeof header !== 'string') return undefined\n for (const part of header.split(';')) {\n const eq = part.indexOf('=')\n if (eq === -1) continue\n if (part.slice(0, eq).trim() === cookieName) return part.slice(eq + 1).trim()\n }\n return undefined\n }\n\n const hasSession = (req: IncomingMessage): boolean => {\n const token = cookieToken(req)\n if (token === undefined || token === '') return false\n const key = tokenKey(token)\n const entry = sessions.get(key)\n if (entry === undefined) return false\n if (entry.expiresAt <= Date.now()) {\n sessions.delete(key)\n return false\n }\n return true\n }\n\n /** Last value of a possibly comma-joined forwarded header — the one appended\n * (or set) by the trusted proxy; every earlier position is client-writable. */\n const forwardedLast = (value: string | string[] | undefined): string | undefined => {\n if (value === undefined) return undefined\n const joined = Array.isArray(value) ? value.join(',') : value\n const last = joined.split(',').at(-1)?.trim()\n return last === '' ? undefined : last\n }\n\n const isSecure = (req: IncomingMessage): boolean => {\n if ((req.socket as { encrypted?: boolean }).encrypted === true) return true\n return trustProxy && forwardedLast(req.headers['x-forwarded-proto'])?.toLowerCase() === 'https'\n }\n\n /** The origin this server believes it is being served as, from the request's\n * own Host (or the proxy's forwarded host) — the only self-knowledge we have. */\n const expectedOrigin = (req: IncomingMessage): string | null => {\n const host = (trustProxy ? forwardedLast(req.headers['x-forwarded-host']) : undefined) ?? req.headers.host\n if (host === undefined || host === '') return null\n try {\n return new URL(`${isSecure(req) ? 'https' : 'http'}://${host}`).origin\n } catch {\n return null\n }\n }\n\n /**\n * The CSRF core. `SameSite=Lax` alone is not enough for two reasons: same\n * *site* is not same *origin* (another port on localhost — any other local\n * web app — is same-site, cookies attach), and the WS handshake is exempt\n * from CORS, so a foreign page that gets the cookie attached can read the\n * stream. So the Origin header is checked explicitly, against the request's\n * own origin (full scheme + authority: an http:// page on the same host must\n * not drive the https:// dashboard) or the operator's allowlist. `Origin:\n * null` and unparseable values are foreign. Verdicts are tri-state because\n * absence means different things per call site: every current browser sends\n * Origin on cross-site POSTs and every WS handshake, so absence means a\n * non-browser client — which carries no ambient cookie and gets to decide\n * per-endpoint below.\n */\n const originVerdict = (req: IncomingMessage): 'absent' | 'ok' | 'foreign' => {\n const raw = req.headers.origin\n if (raw === undefined) return 'absent'\n let origin: string\n try {\n origin = new URL(raw).origin\n } catch {\n return 'foreign'\n }\n if (allowedOrigins.has(origin)) return 'ok'\n const expected = expectedOrigin(req)\n return expected !== null && origin === expected ? 'ok' : 'foreign'\n }\n\n const headerSecret = (req: IncomingMessage): string | undefined => {\n const key = req.headers['x-workerdeck-key']\n if (typeof key === 'string' && key !== '') return key\n const authorization = req.headers.authorization\n if (typeof authorization === 'string') {\n const match = /^Bearer\\s+(.+)$/i.exec(authorization)\n if (match !== null) return match[1]\n }\n return undefined\n }\n\n const openPrincipal: CliPrincipal = { via: 'open', canManageProfiles: true }\n\n const authenticate: Authenticator = (req) => {\n if (!enabled) return openPrincipal\n // Header first: the secret itself is not ambient — the sender chose to\n // attach it — so no Origin check applies. A present-but-wrong header is a\n // rejection, never a fall-through to the cookie.\n const provided = headerSecret(req)\n if (provided !== undefined) {\n return secretMatches(provided) ? ({ via: 'header', canManageProfiles: true } satisfies CliPrincipal) : null\n }\n if (!hasSession(req)) return null\n const verdict = originVerdict(req)\n // A cookie-authenticated request must never carry a foreign Origin, no\n // matter the method — there is no legitimate cross-origin use of this API\n // from a browser (no CORS headers are ever served).\n if (verdict === 'foreign') return null\n // State-changing methods and WS upgrades additionally require Origin to be\n // present: browsers always send it there, so absence means a non-browser\n // client replaying the cookie — which should be using the header transport.\n const upgrade = req.headers.upgrade\n const isUpgrade = typeof upgrade === 'string' && upgrade.toLowerCase().includes('websocket')\n const unsafe = !SAFE_METHODS.has((req.method ?? 'GET').toUpperCase())\n if ((isUpgrade || unsafe) && verdict !== 'ok') return null\n return { via: 'cookie', canManageProfiles: true } satisfies CliPrincipal\n }\n\n /**\n * Login throttle: the secret is the only factor and the endpoint is reachable\n * by anyone who can reach the port, so guessing must be rate-limited. Failed\n * attempts count per client IP inside a fixed window, with a global cap\n * behind it so rotating IPs (trivial over IPv6) buys an attacker nothing.\n * Only wrong secrets count — malformed requests and foreign-Origin posts are\n * refused earlier precisely so a hostile page cannot burn a victim IP's\n * budget cross-site. The global cap also bounds this map's size: expired\n * entries are swept once it grows past a nominal size.\n */\n const failures = new Map<string, { count: number; windowStart: number }>()\n const globalFailures = { count: 0, windowStart: 0 }\n\n const clientIp = (req: IncomingMessage): string =>\n (trustProxy ? forwardedLast(req.headers['x-forwarded-for']) : undefined) ??\n req.socket.remoteAddress ??\n 'unknown'\n\n const blockedMs = (entry: { count: number; windowStart: number } | undefined, max: number, now: number): number =>\n entry !== undefined && entry.count >= max && now - entry.windowStart < windowMs\n ? entry.windowStart + windowMs - now\n : 0\n\n const loginBlockedMs = (ip: string, now: number): number =>\n Math.max(blockedMs(failures.get(ip), maxFailuresPerIp, now), blockedMs(globalFailures, maxFailuresGlobal, now))\n\n const recordFailure = (ip: string, now: number): void => {\n if (failures.size > 256) {\n for (const [key, entry] of failures) {\n if (now - entry.windowStart >= windowMs) failures.delete(key)\n }\n }\n const entry = failures.get(ip)\n if (entry === undefined || now - entry.windowStart >= windowMs) {\n failures.set(ip, { count: 1, windowStart: now })\n } else {\n entry.count += 1\n }\n if (now - globalFailures.windowStart >= windowMs) {\n globalFailures.count = 1\n globalFailures.windowStart = now\n } else {\n globalFailures.count += 1\n }\n }\n\n const cookieAttributes = (req: IncomingMessage): string[] => {\n // Lax over Strict deliberately: Strict drops the cookie on top-level\n // navigation from an external link, landing a logged-in operator on the\n // login page; the delta Strict would buy (cookie-bearing cross-site GET\n // navigations) changes no state and is unreadable cross-origin anyway. The\n // real CSRF surfaces — same-site-different-port and the WS handshake —\n // need the Origin check regardless of SameSite value.\n const attrs = ['Path=/', 'HttpOnly', 'SameSite=Lax']\n if (isSecure(req)) attrs.push('Secure')\n return attrs\n }\n\n const setCookieValue = (token: string, req: IncomingMessage): string =>\n [`${cookieName}=${token}`, `Max-Age=${Math.ceil(ttlMs / 1000)}`, ...cookieAttributes(req)].join('; ')\n\n const clearCookieValue = (req: IncomingMessage): string =>\n [`${cookieName}=`, 'Max-Age=0', ...cookieAttributes(req)].join('; ')\n\n const respondJson = (\n res: ServerResponse,\n status: number,\n body: Record<string, unknown>,\n headers?: Record<string, string>,\n ): void => {\n res\n .writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store', ...headers })\n .end(JSON.stringify(body))\n }\n\n const respondRedirect = (res: ServerResponse, location: string, headers?: Record<string, string>): void => {\n res.writeHead(303, { location, 'cache-control': 'no-store', ...headers }).end()\n }\n\n /** JSON responses when the client asks for them, 303 redirects otherwise —\n * so a dependency-free `<form method=\"post\">` login page works without JS,\n * and a fetch()-based one gets real status codes. */\n const wantsJson = (req: IncomingMessage): boolean => (req.headers.accept ?? '').includes('application/json')\n\n const readBody = (req: IncomingMessage, maxBytes: number): Promise<string | null> =>\n new Promise((resolve) => {\n const chunks: Buffer[] = []\n let size = 0\n let settled = false\n const finish = (value: string | null): void => {\n if (!settled) {\n settled = true\n resolve(value)\n }\n }\n req.on('data', (chunk: Buffer) => {\n size += chunk.length\n if (size > maxBytes) {\n finish(null)\n return\n }\n chunks.push(chunk)\n })\n req.on('end', () => finish(Buffer.concat(chunks).toString('utf8')))\n req.on('error', () => finish(null))\n })\n\n const handleLogin = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n const json = wantsJson(req)\n if (!enabled) {\n respondJson(res, 409, { error: 'auth is disabled: no secret is configured' })\n return\n }\n // Foreign-Origin posts are refused before touching the throttle — see the\n // throttle note. Absent Origin is allowed here (curl-style provisioning):\n // a browser-borne forgery always carries Origin, so absence proves this is\n // not a victim's browser being driven cross-site.\n if (originVerdict(req) === 'foreign') {\n respondJson(res, 403, { error: 'origin not allowed' })\n return\n }\n const ip = clientIp(req)\n const blocked = loginBlockedMs(ip, Date.now())\n if (blocked > 0) {\n const retryAfter = String(Math.ceil(blocked / 1000))\n if (json) respondJson(res, 429, { error: 'too many failed attempts' }, { 'retry-after': retryAfter })\n else respondRedirect(res, '/?auth=throttled', { 'retry-after': retryAfter })\n return\n }\n const body = await readBody(req, MAX_LOGIN_BODY_BYTES)\n if (body === null) {\n respondJson(res, 413, { error: 'body too large' })\n // The socket cannot be reused: the remainder of an oversized request\n // would desync the next keep-alive exchange. Destroy once the response\n // has flushed.\n res.once('finish', () => req.destroy())\n return\n }\n const contentType = (req.headers['content-type'] ?? '').split(';')[0].trim().toLowerCase()\n let candidate: unknown\n if (contentType === 'application/x-www-form-urlencoded') {\n candidate = new URLSearchParams(body).get('secret')\n } else if (contentType === 'application/json') {\n try {\n candidate = (JSON.parse(body) as { secret?: unknown }).secret\n } catch {\n respondJson(res, 400, { error: 'invalid body' })\n return\n }\n } else {\n respondJson(res, 415, { error: 'expected application/x-www-form-urlencoded or application/json' })\n return\n }\n if (typeof candidate !== 'string' || candidate === '') {\n respondJson(res, 400, { error: 'missing secret' })\n return\n }\n if (!secretMatches(candidate)) {\n recordFailure(ip, Date.now())\n if (json) respondJson(res, 401, { error: 'invalid secret' })\n else respondRedirect(res, '/?auth=failed')\n return\n }\n failures.delete(ip)\n const cookie = setCookieValue(createSession(), req)\n if (json) res.writeHead(204, { 'set-cookie': cookie, 'cache-control': 'no-store' }).end()\n else respondRedirect(res, '/', { 'set-cookie': cookie })\n }\n\n const handleLogout = (req: IncomingMessage, res: ServerResponse): void => {\n const json = wantsJson(req)\n if (enabled) {\n // A forged logout is a nuisance, not a breach, so absent Origin stays\n // allowed like on login — but present-and-foreign is still refused.\n if (originVerdict(req) === 'foreign') {\n respondJson(res, 403, { error: 'origin not allowed' })\n return\n }\n const token = cookieToken(req)\n // Deleting the table entry is the invalidation; clearing the cookie is\n // just tidiness. A stale copy of the token is dead either way.\n if (token !== undefined && token !== '') sessions.delete(tokenKey(token))\n }\n const cookie = clearCookieValue(req)\n if (json) res.writeHead(204, { 'set-cookie': cookie, 'cache-control': 'no-store' }).end()\n else respondRedirect(res, '/', { 'set-cookie': cookie })\n }\n\n const handleAuthRoute = async (pathname: string, req: IncomingMessage, res: ServerResponse): Promise<void> => {\n if (pathname === '/auth/status') {\n if (req.method !== 'GET') respondJson(res, 405, { error: 'method not allowed' }, { allow: 'GET' })\n else respondJson(res, 200, { enabled, authenticated: enabled ? hasSession(req) : true })\n return\n }\n if (pathname === '/auth/login') {\n if (req.method !== 'POST') respondJson(res, 405, { error: 'method not allowed' }, { allow: 'POST' })\n else await handleLogin(req, res)\n return\n }\n if (pathname === '/auth/logout') {\n if (req.method !== 'POST') respondJson(res, 405, { error: 'method not allowed' }, { allow: 'POST' })\n else handleLogout(req, res)\n return\n }\n respondJson(res, 404, { error: 'not found' })\n }\n\n const handleAuthRequest = (req: IncomingMessage, res: ServerResponse): boolean | Promise<boolean> => {\n let pathname: string\n try {\n pathname = new URL(req.url ?? '/', 'http://internal').pathname\n } catch {\n return false\n }\n // Claim the whole /auth prefix (unknown subpaths 404 here) so nothing under\n // it ever falls through to the SPA's catch-all.\n if (pathname !== '/auth' && !pathname.startsWith('/auth/')) return false\n return handleAuthRoute(pathname, req, res).then(() => true)\n }\n\n // Failure detail rides the redirect query (`/?auth=failed|throttled`), not a\n // server-side flash store: the login page is stateless and the message never\n // says more than the response status already did.\n const loginPage = (req: IncomingMessage): { action: string; field: string; error?: string } => {\n let reason: string | null = null\n try {\n reason = new URL(req.url ?? '/', 'http://internal').searchParams.get('auth')\n } catch {\n reason = null\n }\n const error =\n reason === 'failed'\n ? 'Invalid access key. Try again.'\n : reason === 'throttled'\n ? 'Too many failed attempts. Wait a few minutes, then try again.'\n : undefined\n return { action: '/auth/login', field: 'secret', error }\n }\n\n return {\n enabled,\n authenticate,\n handleAuthRequest,\n hasValidSession: (req) => (enabled ? hasSession(req) : true),\n loginPage,\n }\n}\n","import { existsSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, isAbsolute, join, resolve } from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport type { ProfileInfo } from '@workerdeck/protocol'\nimport type { WorkerServerOptions } from '@workerdeck/server'\nimport type { CliAuthOptions } from './auth.ts'\n\n/**\n * The config surface has to be JavaScript, not JSON: the two options a real\n * deployment always needs — `authenticate` and `buildRunnerConfig` — are\n * functions. So the file default-exports `WorkerServerOptions` (optionally as a\n * function, sync or async, for config that has to await something), and flags\n * and env cover the cases that fit on a command line.\n *\n * Precedence, narrowest wins: flags > env > config file > defaults. A config\n * file that sets `authenticate` itself opts out of the built-in shared-secret\n * auth entirely — see `resolveInstanceConfig`.\n */\n\nconst CONFIG_BASENAMES = [\n 'workerdeck.config.mjs',\n 'workerdeck.config.js',\n 'workerdeck.config.cjs',\n]\n\n/**\n * What a `workerdeck.config.mjs` default-exports: the server options, plus\n * the few instance-level settings that aren't the server's business. Keeping\n * them in one object means a deployment is one file, not a file plus a\n * memorised command line.\n */\nexport type WorkerDeckConfig = WorkerServerOptions & {\n port?: number\n host?: string\n /** Built-in shared-secret auth. Ignored entirely if you supply `authenticate`. */\n auth?: CliAuthOptions\n /** Where parked sessions are persisted; null disables durable parking. */\n stateDir?: string | null\n /**\n * Host header values accepted when running *without* auth. Defaults to the\n * loopback names; see `resolveInstanceConfig` for why this exists at all.\n */\n allowedHosts?: string[]\n /**\n * Bind hosts that may serve without auth. One declaration, two effects:\n * binding a listed host waives the auth requirement (no key demanded, none\n * generated), and while unauthenticated every entry is also accepted as a\n * Host header, so the operator states the intent once. Entries name a host,\n * never an endpoint — a port is rejected — and match the bind host literally\n * and case-insensitively: nothing is inferred from DNS or the network, and\n * `0.0.0.0` means the all-interfaces bind itself, not \"any host\". When auth\n * is on this widens nothing.\n */\n insecureHosts?: string[]\n /** Serve a dashboard build from here instead of the bundled one. */\n webRoot?: string\n}\n\nexport type CliFlags = {\n config?: string\n port?: number\n host?: string\n authKey?: string\n profiles: ProfileInfo[]\n cwdRoots: string[]\n allowedOrigins: string[]\n allowedHosts: string[]\n insecureHosts: string[]\n trustProxy?: boolean\n stateDir?: string\n parking?: boolean\n insecure?: boolean\n open?: boolean\n help?: boolean\n version?: boolean\n}\n\nexport class ConfigError extends Error {}\n\nfunction parsePort(raw: string, source: string): number {\n const port = Number(raw)\n if (!Number.isInteger(port) || port < 0 || port > 65535) {\n throw new ConfigError(`${source}: not a valid port: ${raw}`)\n }\n return port\n}\n\n/**\n * Hand-rolled rather than a dependency: the CLI's whole value is that `npx\n * workerdeck` pulls down a small tree, and an arg parser is a hundred lines\n * of it.\n */\nexport function parseArgs(argv: string[]): CliFlags {\n const flags: CliFlags = {\n profiles: [],\n cwdRoots: [],\n allowedOrigins: [],\n allowedHosts: [],\n insecureHosts: [],\n }\n const next = (i: number, name: string): string => {\n const value = argv[i + 1]\n if (value === undefined || value.startsWith('-')) {\n throw new ConfigError(`${name} requires a value`)\n }\n return value\n }\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i]!\n switch (arg) {\n case '-h':\n case '--help':\n flags.help = true\n break\n case '-v':\n case '--version':\n flags.version = true\n break\n case '-c':\n case '--config':\n flags.config = next(i, arg)\n i++\n break\n case '-p':\n case '--port':\n flags.port = parsePort(next(i, arg), arg)\n i++\n break\n case '--host':\n flags.host = next(i, arg)\n i++\n break\n case '--auth-key':\n flags.authKey = next(i, arg)\n i++\n break\n case '--profile': {\n // name=dir — the config dir is a credential store, so naming one is a\n // deliberate act and never inferred.\n const raw = next(i, arg)\n i++\n const eq = raw.indexOf('=')\n if (eq <= 0) throw new ConfigError(`--profile expects name=dir, got: ${raw}`)\n const name = raw.slice(0, eq)\n const dir = raw.slice(eq + 1)\n if (!dir) throw new ConfigError(`--profile ${name}= is missing a directory`)\n flags.profiles.push({ name, configDir: resolve(dir) })\n break\n }\n case '--cwd-root':\n flags.cwdRoots.push(resolve(next(i, arg)))\n i++\n break\n case '--allowed-origin':\n flags.allowedOrigins.push(next(i, arg))\n i++\n break\n case '--allowed-host':\n flags.allowedHosts.push(next(i, arg))\n i++\n break\n case '--insecure-host':\n flags.insecureHosts.push(next(i, arg))\n i++\n break\n case '--trust-proxy':\n flags.trustProxy = true\n break\n case '--state-dir':\n flags.stateDir = resolve(next(i, arg))\n i++\n break\n case '--no-parking-store':\n flags.parking = false\n break\n case '--insecure':\n flags.insecure = true\n break\n case '--open':\n flags.open = true\n break\n default:\n throw new ConfigError(`unknown option: ${arg}`)\n }\n }\n return flags\n}\n\nexport type LoadedConfig = {\n /** Absolute path of the file that was loaded, or null if there wasn't one. */\n path: string | null\n options: WorkerDeckConfig\n}\n\n/**\n * Explicit `--config` must exist — a typo that silently starts a default\n * instance is worse than a failure. An implicit one is looked up in cwd only:\n * walking parent directories would make what a given command does depend on\n * where it was run from.\n */\nexport async function loadConfigFile(explicit?: string, cwd = process.cwd()): Promise<LoadedConfig> {\n let path: string | null = null\n if (explicit) {\n path = isAbsolute(explicit) ? explicit : resolve(cwd, explicit)\n if (!existsSync(path)) throw new ConfigError(`no config file at ${path}`)\n } else {\n path = CONFIG_BASENAMES.map((name) => join(cwd, name)).find((p) => existsSync(p)) ?? null\n }\n if (!path) return { path: null, options: {} }\n\n let mod: { default?: unknown }\n try {\n // The specifier is a runtime value on purpose: the config file is the\n // operator's code, not part of our module graph, so no bundler should ever\n // try to resolve or inline it.\n mod = (await import(pathToFileURL(path).href)) as { default?: unknown }\n } catch (error) {\n throw new ConfigError(\n `failed to load ${path}: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n const exported = mod.default\n if (exported === undefined) {\n throw new ConfigError(`${path} has no default export (expected WorkerServerOptions)`)\n }\n const options = typeof exported === 'function' ? await (exported as () => unknown)() : exported\n if (typeof options !== 'object' || options === null) {\n throw new ConfigError(`${path} default export is not an options object`)\n }\n return { path, options: options as WorkerDeckConfig }\n}\n\nconst LOOPBACK = new Set(['127.0.0.1', '::1', 'localhost', '::ffff:127.0.0.1'])\n\nexport function isLoopback(host: string): boolean {\n return LOOPBACK.has(host)\n}\n\nexport type ResolvedConfig = {\n port: number\n host: string\n /** Shared secret, or undefined for an unauthenticated instance. */\n authKey?: string\n /** Everything else the built-in auth takes (proxy trust, extra origins). */\n auth: CliAuthOptions\n /** Where parked sessions and other instance state live; null disables durable parking. */\n stateDir: string | null\n configPath: string | null\n /** True when the config file supplied its own `authenticate` — built-in auth stands down. */\n hostAuthenticates: boolean\n /**\n * Auth is required here but no key was supplied: `startInstance` must\n * materialize one (stored under `stateDir`, ephemeral without one). This is a\n * *promise* rather than a key because resolution is pure and synchronous while\n * reading a key file is I/O — and the promise is load-bearing: `allowedHosts`\n * is already null on the strength of it, so `startInstance` refuses to serve\n * if materialization ever fails to arm the built-in auth.\n */\n generateAuthKey: boolean\n /**\n * Host header values to accept, or null to accept any. Non-null only for an\n * unauthenticated instance — see `resolveInstanceConfig`.\n */\n allowedHosts: Set<string> | null\n /** Dashboard build to serve; resolved from the package when unset. */\n webRoot?: string\n open: boolean\n options: WorkerServerOptions\n}\n\n/**\n * Durable parking is on by default because this is a long-lived instance: a\n * turnkey tool that silently drops parked work on every restart is the wrong\n * default. The store writes whole transcripts in plaintext, so it goes beside\n * the config file (or under the home directory) rather than anywhere temporary,\n * and one directory serves exactly one instance — the store is single-process\n * by design, which the single-port model already implies.\n */\nexport function defaultStateDir(configPath: string | null): string {\n return configPath ? join(dirname(configPath), '.workerdeck') : join(homedir(), '.workerdeck')\n}\n\n/** Hostname out of a Host header, minus the port and any IPv6 brackets. */\nexport function hostnameOf(hostHeader: string): string {\n try {\n return new URL(`http://${hostHeader}`).hostname.replace(/^\\[|\\]$/g, '').toLowerCase()\n } catch {\n return ''\n }\n}\n\n/** 127.0.0.0/8, ::1, and the names that mean them. */\nexport function isLoopbackHostname(hostname: string): boolean {\n if (LOOPBACK.has(hostname)) return true\n return /^127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/.test(hostname)\n}\n\n/**\n * An `insecureHosts` entry names a host, never an endpoint: it is compared\n * against the bind host and against Host headers, and both are portless by the\n * time they are compared. An entry carrying a port would therefore never match\n * anything — a gate that looks armed and is not — so it is rejected loudly, as\n * is anything that does not parse as a host name or address. Bare IPv6 is\n * bracketed before parsing (WHATWG URL requires that), and the result is\n * lowercased to match `hostnameOf`'s normal form.\n */\nfunction normalizeInsecureHost(raw: string): string {\n const entry = raw.trim()\n const looksLikeNamePort = /^[^:]+:\\d+$/.test(entry)\n const candidate =\n entry.includes(':') && !entry.startsWith('[') && !looksLikeNamePort ? `[${entry}]` : entry\n let url: URL\n try {\n url = new URL(`http://${candidate}`)\n } catch {\n throw new ConfigError(`not a host name or address in insecureHosts: ${JSON.stringify(raw)}`)\n }\n if (url.port !== '') {\n throw new ConfigError(\n `insecureHosts entry ${JSON.stringify(raw)} carries a port — name the host alone; ` +\n `the bind host and the Host header are both compared portless`,\n )\n }\n return url.hostname.replace(/^\\[|\\]$/g, '').toLowerCase()\n}\n\nexport function resolveInstanceConfig(\n flags: CliFlags,\n loaded: LoadedConfig,\n env: NodeJS.ProcessEnv = process.env,\n cwd = process.cwd(),\n): ResolvedConfig {\n const envPort = env.WORKERDECK_PORT\n ? parsePort(env.WORKERDECK_PORT, 'WORKERDECK_PORT')\n : undefined\n const port = flags.port ?? envPort ?? loaded.options.port ?? 8787\n const host = flags.host ?? env.WORKERDECK_HOST ?? loaded.options.host ?? '127.0.0.1'\n // `?? undefined` would keep an empty string, and an empty secret is not a\n // secret: WORKERDECK_AUTH_KEY= in an env file means \"unset\", not \"no auth\n // but pretend otherwise\".\n const authKey =\n flags.authKey || env.WORKERDECK_AUTH_KEY || loaded.options.auth?.secret || undefined\n const hostAuthenticates = typeof loaded.options.authenticate === 'function'\n\n const insecureHosts = new Set(\n [...flags.insecureHosts, ...(loaded.options.insecureHosts ?? [])].map(normalizeInsecureHost),\n )\n /** The bind host in the same normal form the entries were put in. It never\n * carries a port — that is a separate flag — so only brackets and case vary. */\n const bindHost = host.trim().replace(/^\\[|\\]$/g, '').toLowerCase()\n\n // An unauthenticated Claude Code gateway on a routable interface is a shell\n // for anyone who can reach the port, so serving one takes an explicit opt-out:\n // `--insecure`, `allowUnauthenticated`, or the bind host declared in\n // `insecureHosts`. Absent all three and absent a key, auth still goes ON — a\n // key gets generated at startup rather than the old refusal, because \"secure\n // by default\" beats \"off by default\". Materializing it is I/O, which this\n // function must stay free of, so the decision is recorded as `generateAuthKey`\n // and `startInstance` does the reading and writing.\n const generateAuthKey =\n !authKey &&\n !hostAuthenticates &&\n !loaded.options.allowUnauthenticated &&\n !isLoopback(host) &&\n !flags.insecure &&\n !insecureHosts.has(bindHost)\n\n const stateDir =\n flags.parking === false || loaded.options.stateDir === null\n ? null\n : (flags.stateDir ??\n env.WORKERDECK_STATE_DIR ??\n loaded.options.stateDir ??\n defaultStateDir(loaded.path))\n\n const envCwdRoots = env.WORKERDECK_CWD_ROOTS?.split(':')\n .filter(Boolean)\n .map((p) => resolve(cwd, p))\n const cwdRoots = flags.cwdRoots.length ? flags.cwdRoots : envCwdRoots\n\n const auth: CliAuthOptions = {\n ...loaded.options.auth,\n secret: authKey,\n trustProxy: flags.trustProxy ?? loaded.options.auth?.trustProxy,\n allowedOrigins: [...(loaded.options.auth?.allowedOrigins ?? []), ...flags.allowedOrigins],\n }\n\n /**\n * DNS rebinding is the one attack an unauthenticated loopback instance is\n * actually exposed to: a hostile page resolves its own name to 127.0.0.1 and\n * then talks to us same-origin, with no cookie needed because nothing is\n * checked. The defence is to require the Host header to name a loopback\n * address — an attacker controls their DNS, not the name the victim's browser\n * puts in Host. With auth on this is moot (their origin holds no cookie), and\n * for a deliberately exposed instance the operator has said what they want, so\n * only the unauthenticated case is fenced.\n *\n * `generateAuthKey` counts as auth here on the promise that `startInstance`\n * materializes the key — and `startInstance` asserts the promise was kept\n * before serving. Entries are lowercased because the guard compares them\n * against `hostnameOf`'s lowercased hostnames; `insecureHosts` folds in so\n * declaring a bind host once also names it as an acceptable Host header.\n */\n const authEnabled = Boolean(authKey) || hostAuthenticates || generateAuthKey\n const allowedHosts = authEnabled\n ? null\n : new Set([\n ...LOOPBACK,\n ...[...flags.allowedHosts, ...(loaded.options.allowedHosts ?? [])].map((name) =>\n name.toLowerCase(),\n ),\n ...insecureHosts,\n ])\n\n // Strip the instance-level keys: what's left is exactly WorkerServerOptions.\n const {\n port: _p,\n host: _h,\n auth: _a,\n stateDir: _s,\n allowedHosts: _ah,\n insecureHosts: _ih,\n webRoot: _w,\n ...serverOptions\n } = loaded.options\n const options: WorkerServerOptions = { ...serverOptions }\n if (cwdRoots?.length) options.allowedCwdRoots = cwdRoots\n // Flags win, but they *replace* rather than merge: a half-declared profile set\n // is a credential mix-up waiting to happen.\n if (flags.profiles.length) options.profiles = flags.profiles\n\n return {\n port,\n host,\n authKey,\n auth,\n stateDir,\n configPath: loaded.path,\n hostAuthenticates,\n generateAuthKey,\n allowedHosts,\n webRoot: loaded.options.webRoot,\n open: flags.open ?? false,\n options,\n }\n}\n","/**\n * The login page is the CLI's, not the dashboard's. That split is deliberate:\n * the SPA ships prebuilt and is also served straight from vite in dev, so making\n * it aware of an auth scheme that only exists in the turnkey instance would\n * couple two things that are otherwise independent. An unauthenticated document\n * request gets this instead of index.html; nothing in the SPA changes.\n *\n * Self-contained by necessity — it renders before any bundled asset is worth\n * fetching, and it must not depend on the app it is gating.\n */\n\nexport type LoginPageOptions = {\n /** Where the form POSTs. Comes from the auth module, not hardcoded here. */\n action: string\n /** Form field name carrying the secret. */\n field: string\n /** Shown when a previous attempt failed. */\n error?: string\n /** Where to send the browser after a successful login. */\n redirectTo?: string\n /** Field name carrying the post-login redirect. */\n redirectField?: string\n}\n\nconst escapeHtml = (value: string): string =>\n value.replace(\n /[&<>\"']/g,\n (c) =>\n ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' })[c] ?? c,\n )\n\nexport function renderLoginPage(options: LoginPageOptions): string {\n const { action, field, error, redirectTo, redirectField } = options\n const hidden =\n redirectField && redirectTo\n ? `<input type=\"hidden\" name=\"${escapeHtml(redirectField)}\" value=\"${escapeHtml(redirectTo)}\">`\n : ''\n const alert = error\n ? `<p class=\"error\" role=\"alert\">${escapeHtml(error)}</p>`\n : ''\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<meta name=\"robots\" content=\"noindex\">\n<title>workerdeck</title>\n<style>\n :root {\n color-scheme: light dark;\n --bg: #fbfaf9; --fg: #1c1917; --muted: #78716c;\n --card: #ffffff; --border: #e7e5e4; --accent: #c2410c; --error: #b91c1c;\n }\n @media (prefers-color-scheme: dark) {\n :root {\n --bg: #1c1917; --fg: #fafaf9; --muted: #a8a29e;\n --card: #292524; --border: #44403c; --accent: #fb923c; --error: #fca5a5;\n }\n }\n * { box-sizing: border-box; }\n body {\n margin: 0; min-height: 100dvh; display: grid; place-items: center; padding: 1.5rem;\n background: var(--bg); color: var(--fg);\n font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif;\n }\n form {\n width: 100%; max-width: 22rem; background: var(--card);\n border: 1px solid var(--border); border-radius: 12px; padding: 1.75rem;\n }\n h1 { margin: 0 0 .25rem; font-size: 1.05rem; letter-spacing: -0.01em; }\n p.sub { margin: 0 0 1.25rem; color: var(--muted); font-size: .875rem; }\n label { display: block; font-size: .8125rem; font-weight: 500; margin-bottom: .375rem; }\n input[type=password] {\n width: 100%; padding: .5rem .625rem; font: inherit; color: inherit;\n background: var(--bg); border: 1px solid var(--border); border-radius: 7px;\n }\n input[type=password]:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }\n button {\n width: 100%; margin-top: 1rem; padding: .5rem .75rem; font: inherit; font-weight: 500;\n color: var(--bg); background: var(--fg); border: 0; border-radius: 7px; cursor: pointer;\n }\n button:hover { opacity: .9; }\n .error { margin: 0 0 1rem; color: var(--error); font-size: .8125rem; }\n</style>\n</head>\n<body>\n <form method=\"post\" action=\"${escapeHtml(action)}\">\n <h1>workerdeck</h1>\n <p class=\"sub\">This instance is protected. Enter its access key to continue.</p>\n ${alert}\n <label for=\"key\">Access key</label>\n <input id=\"key\" name=\"${escapeHtml(field)}\" type=\"password\" autocomplete=\"current-password\"\n autofocus required spellcheck=\"false\">\n ${hidden}\n <button type=\"submit\">Sign in</button>\n </form>\n</body>\n</html>\n`\n}\n","import { createReadStream } from 'node:fs'\nimport { stat } from 'node:fs/promises'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { join, resolve, sep } from 'node:path'\n\n/**\n * Static file serving for the bundled dashboard. Deliberately policy-free: what\n * counts as a document, and whether an unauthenticated visitor gets the app or a\n * login page, is decided by the caller (see `instance.ts`). This module only\n * answers \"is there such a file, and what headers does it want\".\n */\n\nconst CONTENT_TYPES: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.mjs': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.map': 'application/json; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.webp': 'image/webp',\n '.avif': 'image/avif',\n '.ico': 'image/x-icon',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.ttf': 'font/ttf',\n '.wasm': 'application/wasm',\n '.txt': 'text/plain; charset=utf-8',\n}\n\nexport function contentTypeFor(pathname: string): string {\n const dot = pathname.lastIndexOf('.')\n if (dot < 0) return 'application/octet-stream'\n return CONTENT_TYPES[pathname.slice(dot).toLowerCase()] ?? 'application/octet-stream'\n}\n\n/** A request for a file rather than an app route: anything with a known extension. */\nexport function looksLikeAsset(pathname: string): boolean {\n const dot = pathname.lastIndexOf('.')\n if (dot < 0) return false\n return pathname.slice(dot).toLowerCase() in CONTENT_TYPES\n}\n\n/**\n * Resolve `pathname` inside `root`, or null if it escapes. Vite emits every\n * asset under a content-hashed name, so the only paths that ever reach here are\n * ones the app itself generated — but this server is reachable by anything that\n * can open a socket, and `..` in a URL is the oldest trick there is.\n */\nexport function resolveWithinRoot(root: string, pathname: string): string | null {\n let decoded: string\n try {\n decoded = decodeURIComponent(pathname)\n } catch {\n return null // malformed percent-encoding\n }\n if (decoded.includes('\\0')) return null\n // Joined *without* normalising `decoded` first: normalising an absolute\n // pathname collapses its leading `..` segments, which quietly rebases an\n // escape attempt inside the root and leaves the containment check below with\n // nothing to catch. Let `join` resolve the traversal for real, then reject it.\n const candidate = resolve(join(root, decoded))\n const base = resolve(root)\n if (candidate !== base && !candidate.startsWith(base + sep)) return null\n return candidate\n}\n\nexport function sendHtml(\n req: IncomingMessage,\n res: ServerResponse,\n status: number,\n html: string,\n cache: string,\n): void {\n const body = Buffer.from(html, 'utf8')\n res.writeHead(status, {\n 'content-type': 'text/html; charset=utf-8',\n 'content-length': body.byteLength,\n 'cache-control': cache,\n // The dashboard is same-origin with the API and holds an ambient session\n // cookie; framing it elsewhere is only ever clickjacking.\n 'x-frame-options': 'DENY',\n 'x-content-type-options': 'nosniff',\n 'referrer-policy': 'same-origin',\n })\n res.end(req.method === 'HEAD' ? undefined : body)\n}\n\nexport type ServeResult = 'served' | 'not-found' | 'method-not-allowed'\n\n/**\n * Stream a file out of `root`. `immutable` is the caller's call, because it is a\n * promise about the URL, not the file: Vite's hashed assets can be cached\n * forever, but index.html must be revalidated every time or a deployed update\n * never reaches a browser that already has the old one.\n */\nexport async function serveFile(\n req: IncomingMessage,\n res: ServerResponse,\n filePath: string,\n options: { immutable?: boolean } = {},\n): Promise<ServeResult> {\n if (req.method !== 'GET' && req.method !== 'HEAD') return 'method-not-allowed'\n\n let size: number\n try {\n const info = await stat(filePath)\n if (!info.isFile()) return 'not-found'\n size = info.size\n } catch {\n return 'not-found'\n }\n\n res.writeHead(200, {\n 'content-type': contentTypeFor(filePath),\n 'content-length': size,\n 'cache-control': options.immutable\n ? 'public, max-age=31536000, immutable'\n : 'no-cache, must-revalidate',\n 'x-content-type-options': 'nosniff',\n })\n if (req.method === 'HEAD') {\n res.end()\n return 'served'\n }\n await new Promise<void>((resolvePromise) => {\n const stream = createReadStream(filePath)\n stream.on('error', () => {\n res.destroy()\n resolvePromise()\n })\n stream.on('end', () => resolvePromise())\n stream.pipe(res)\n })\n return 'served'\n}\n","import { existsSync } from 'node:fs'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { join } from 'node:path'\nimport { createFileSessionStore, createWorkerServer, type WorkerServer } from '@workerdeck/server'\nimport { dashboardDir } from '@workerdeck/web'\nimport { materializeAuthKey, type MaterializedAuthKey } from './auth-key.ts'\nimport { createCliAuth, type CliAuth } from './auth.ts'\nimport { hostnameOf, isLoopbackHostname, type ResolvedConfig } from './config.ts'\nimport { renderLoginPage } from './login-page.ts'\nimport { looksLikeAsset, resolveWithinRoot, sendHtml, serveFile } from './static.ts'\n\nexport type Instance = {\n server: WorkerServer\n url: string\n port: number\n /** Resolves when the instance stops serving. */\n closed: Promise<void>\n close: () => Promise<void>\n}\n\n/**\n * The dashboard comes from `@workerdeck/web`, which ships it prebuilt and\n * exports the path to it. Depending on the package rather than vendoring a copy\n * means one dashboard, versioned in lockstep with everything else.\n *\n * In a checkout that directory only exists once the app has been built — dev\n * never builds — so the miss is worth a real message rather than a stack trace\n * from the static host.\n */\nexport function resolveWebRoot(): string {\n if (existsSync(join(dashboardDir, 'index.html'))) return dashboardDir\n throw new Error(\n `no dashboard build at ${dashboardDir}\\n` +\n ` in a checkout: pnpm --filter @workerdeck/web run build`,\n )\n}\n\n/**\n * The Host-header gate for an unauthenticated instance. `allowedHosts` is null\n * whenever auth is on, and then this is the identity function — with a\n * credential in play a rebound origin holds no cookie and fails `authenticate`\n * anyway. Loopback *names* are what's checked, not the socket: the attacker in\n * this scenario controls DNS, so the connection genuinely arrives on 127.0.0.1;\n * what they cannot control is the name the victim's browser writes into Host.\n */\nexport function createHostGuard(allowedHosts: Set<string> | null): (req: IncomingMessage) => boolean {\n if (allowedHosts === null) return () => true\n return (req) => {\n const header = req.headers.host\n // No Host at all is an HTTP/1.0 client or a raw script, never a browser\n // being driven cross-origin — and it cannot be a rebinding victim.\n if (header === undefined) return true\n const hostname = hostnameOf(header)\n if (hostname === '') return false\n return isLoopbackHostname(hostname) || allowedHosts.has(hostname)\n }\n}\n\n/**\n * Everything outside `/v1`. Order matters: the auth endpoints first (they are\n * how a browser gets a session in the first place), then assets, which stay\n * ungated — they are the app's own code, hold no secrets, and gating them would\n * only mean the login page could not be styled by the app it gates. Documents\n * come last, and that is the single place the auth decision is made.\n */\nfunction createFallback(\n auth: CliAuth,\n webRoot: string,\n hostAllowed: (req: IncomingMessage) => boolean,\n): (req: IncomingMessage, res: ServerResponse) => Promise<void> {\n return async (req, res) => {\n if (!hostAllowed(req)) {\n res.writeHead(403, { 'content-type': 'text/plain; charset=utf-8' })\n res.end(\n 'unrecognised Host header.\\n\\nThis instance runs without auth, so it only answers to ' +\n 'loopback host names and the ones it was told to expect. Declare this name with ' +\n '--insecure-host <name> (which also lets it be bound without a key) or --allowed-host ' +\n '<name>, or set --auth-key.\\n',\n )\n return\n }\n if (await auth.handleAuthRequest(req, res)) return\n\n const pathname = new URL(req.url ?? '/', 'http://internal').pathname\n\n if (looksLikeAsset(pathname)) {\n const filePath = resolveWithinRoot(webRoot, pathname)\n if (!filePath) {\n res.writeHead(400, { 'content-type': 'text/plain; charset=utf-8' })\n res.end('bad request')\n return\n }\n // Hashed filenames are immutable by construction; index.html never is, and\n // it is served below as a document, not here.\n const result = await serveFile(req, res, filePath, {\n immutable: pathname.startsWith('/assets/'),\n })\n if (result === 'served') return\n if (result === 'method-not-allowed') {\n res.writeHead(405, { allow: 'GET, HEAD' })\n res.end()\n return\n }\n res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' })\n res.end('not found')\n return\n }\n\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n res.writeHead(405, { allow: 'GET, HEAD' })\n res.end()\n return\n }\n\n if (auth.enabled && !auth.hasValidSession(req)) {\n sendHtml(req, res, 401, renderLoginPage(auth.loginPage(req)), 'no-store')\n return\n }\n\n // The SPA uses hash history, so every route is `#/…` and the server only\n // ever serves the entry document — no rewrite rules, and a deep link works\n // on a static host. `no-cache` on it is what lets an update actually land.\n const entry = join(webRoot, 'index.html')\n const result = await serveFile(req, res, entry, { immutable: false })\n if (result !== 'served') {\n res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' })\n res.end('dashboard build is missing its entry document')\n }\n }\n}\n\nexport async function startInstance(\n config: ResolvedConfig,\n options: { quiet?: boolean } = {},\n): Promise<Instance> {\n const webRoot = config.webRoot ?? resolveWebRoot()\n // The other half of `generateAuthKey`: resolution promised auth without doing\n // I/O, this is where the key actually comes to exist.\n const generated: MaterializedAuthKey | null =\n config.generateAuthKey && !config.hostAuthenticates\n ? await materializeAuthKey(config.stateDir)\n : null\n const auth = createCliAuth(\n config.hostAuthenticates\n ? { ...config.auth, secret: undefined }\n : generated\n ? { ...config.auth, secret: generated.key }\n : config.auth,\n )\n // The failure mode this seam must make impossible: a resolved config that\n // stood down the Host-header guard (allowedHosts null) believing auth would\n // be on, while `createCliAuth` ended up with no secret — that instance would\n // be wide open and reporting itself authenticated. Every start passes through\n // here, so a bug on either side of the seam dies loudly instead of serving.\n if (config.allowedHosts === null && !config.hostAuthenticates && !auth.enabled) {\n throw new Error(\n 'refusing to serve: the resolved config expects auth but no shared secret was ' +\n 'materialized — this instance would be open while believing itself authenticated',\n )\n }\n const hostAllowed = createHostGuard(config.allowedHosts)\n const fallback = createFallback(auth, webRoot, hostAllowed)\n\n const parking = { ...config.options.parking }\n if (config.stateDir && !parking.store) {\n parking.store = createFileSessionStore({\n dir: join(config.stateDir, 'parked'),\n onError: (error, context) => {\n process.stderr.write(\n `[workerdeck] parked-session store ${context.op} failed for ${context.path}: ` +\n `${error instanceof Error ? error.message : String(error)}\\n`,\n )\n },\n })\n }\n\n const server = createWorkerServer({\n ...config.options,\n // The turnkey instance checks profile credentials at startup by default —\n // a mispointed config dir should say so here, not as the first session's\n // \"Not logged in\" error. A config file can set `checkCredentials: false`.\n checkCredentials: config.options.checkCredentials ?? true,\n parking,\n fallback,\n // A config file's own `authenticate` wins outright — mixing two auth schemes\n // on one hook is how you end up with a bypass nobody meant to write. The\n // host guard still wraps it, but it is a no-op whenever auth is on.\n //\n // Note the unauthenticated case supplies an `authenticate` too rather than\n // `allowUnauthenticated`: the Host check has to cover `/v1`, which is the\n // half of the surface a rebinding attack actually wants.\n authenticate: config.hostAuthenticates\n ? (req) => (hostAllowed(req) ? config.options.authenticate!(req) : null)\n : (req) => (hostAllowed(req) ? auth.authenticate(req) : null),\n })\n\n const { port } = await server.listen(config.port, config.host)\n const displayHost = config.host === '0.0.0.0' || config.host === '::' ? 'localhost' : config.host\n const url = `http://${displayHost.includes(':') ? `[${displayHost}]` : displayHost}:${port}`\n\n let resolveClosed: () => void = () => {}\n const closed = new Promise<void>((r) => {\n resolveClosed = r\n })\n\n if (!options.quiet) {\n const line = (text: string): void => void process.stdout.write(`${text}\\n`)\n line('')\n line(` workerdeck ${url}`)\n if (config.hostAuthenticates) line(' auth: the config file supplies its own `authenticate`')\n else if (generated?.source === 'created') {\n // Printed exactly once, at creation — later starts reuse the file and\n // point at it instead of spraying the secret into every log.\n line(` auth: generated key ${generated.key}`)\n line(` stored in ${generated.path} — later starts reuse it without printing it`)\n } else if (generated?.source === 'ephemeral') {\n line(` auth: generated key ${generated.key}`)\n line(' ephemeral — no state dir to keep it, so the next start mints a new one')\n } else if (generated?.source === 'stored') {\n line(` auth: shared key from ${generated.path}`)\n } else if (auth.enabled) line(' auth: shared key — browsers sign in, services send a header')\n else line(' NO AUTH — anyone who can reach this port gets a session')\n line(\n config.stateDir\n ? ` parked sessions persist in ${join(config.stateDir, 'parked')}`\n : ' parked sessions are in memory only — a restart drops them',\n )\n if (config.configPath) line(` config ${config.configPath}`)\n line('')\n }\n\n return {\n server,\n url,\n port,\n closed,\n close: async () => {\n await server.close()\n resolveClosed()\n },\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,22 +1,65 @@
1
1
  {
2
2
  "name": "workerdeck",
3
- "version": "0.0.0",
4
- "description": "Name placeholder — WorkerDeck, a web-controlled runner for coding-agent sessions. Not yet released.",
3
+ "version": "0.6.0",
4
+ "type": "module",
5
+ "description": "Turnkey WorkerDeck instance: `npx workerdeck` runs the session gateway and the full dashboard on one port. The @workerdeck/* packages are the libraries you embed; this is the service you run.",
5
6
  "license": "MIT",
7
+ "main": "./build/index.mjs",
8
+ "types": "./build/index.d.mts",
9
+ "bin": {
10
+ "workerdeck": "./build/cli.mjs"
11
+ },
12
+ "files": [
13
+ "build"
14
+ ],
15
+ "exports": {
16
+ ".": {
17
+ "@workerdeck/source": "./src/index.ts",
18
+ "types": "./build/index.d.mts",
19
+ "default": "./build/index.mjs"
20
+ }
21
+ },
22
+ "dependencies": {
23
+ "@workerdeck/server": "0.6.0",
24
+ "@workerdeck/web": "0.6.0"
25
+ },
26
+ "devDependencies": {
27
+ "@anthropic-ai/claude-agent-sdk": "^0.3.217",
28
+ "@types/ws": "^8.5.13",
29
+ "@swc-node/register": "^1.11.1",
30
+ "@swc/core": "^1.15.41",
31
+ "@types/node": "^22.10.0",
32
+ "rimraf": "^6.1.3",
33
+ "tsdown": "^0.21.10",
34
+ "vitest": "^3.2.0",
35
+ "ws": "^8.21.1",
36
+ "@workerdeck/protocol": "0.6.0"
37
+ },
6
38
  "author": "Tobias Strebitzer",
7
- "homepage": "https://github.com/tobiasstrebitzer/claude-worker#readme",
8
39
  "repository": {
9
40
  "type": "git",
10
- "url": "git+https://github.com/tobiasstrebitzer/claude-worker.git"
41
+ "url": "git+https://github.com/workerdeck/workerdeck.git",
42
+ "directory": "packages/cli"
11
43
  },
44
+ "homepage": "https://workerdeck.github.io/workerdeck/",
45
+ "bugs": "https://github.com/workerdeck/workerdeck/issues",
12
46
  "keywords": [
13
- "workerdeck",
47
+ "claude",
48
+ "claude-code",
49
+ "anthropic",
14
50
  "agent",
15
- "session",
16
- "runner",
17
- "placeholder"
51
+ "cli",
52
+ "dashboard",
53
+ "server"
18
54
  ],
19
- "files": [
20
- "README.md"
21
- ]
22
- }
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "scripts": {
59
+ "clean": "rimraf build",
60
+ "build": "tsdown",
61
+ "typecheck": "tsgo -p tsconfig.json",
62
+ "test": "vitest run",
63
+ "dev": "node --conditions=@workerdeck/source --import @swc-node/register/esm-register src/cli.ts"
64
+ }
65
+ }