workerdeck 0.23.0 → 1.0.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.
- package/build/cli.mjs +18 -5
- package/build/cli.mjs.map +1 -1
- package/build/{guard-DkovUj7o.mjs → guard-BJNUnO3H.mjs} +1 -26
- package/build/guard-BJNUnO3H.mjs.map +1 -0
- package/build/index.d.mts +36 -327
- package/build/index.mjs +2 -2
- package/build/{instance-iBl_fOW2.mjs → instance-5sdv2Rrw.mjs} +91 -392
- package/build/instance-5sdv2Rrw.mjs.map +1 -0
- package/package.json +4 -4
- package/build/guard-DkovUj7o.mjs.map +0 -1
- package/build/instance-iBl_fOW2.mjs.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"instance-iBl_fOW2.mjs","names":[],"sources":["../src/apns/client.ts","../src/apns/devices.ts","../src/apns/forwarder.ts","../src/auth/auth-key.ts","../src/auth/auth-sessions.ts","../src/auth/auth.ts","../src/config.ts","../src/auth/login-page.ts","../src/lib/static.ts","../src/lib/instance.ts"],"sourcesContent":["import { createPrivateKey, type KeyObject, sign } from 'node:crypto'\nimport { readFile } from 'node:fs/promises'\nimport {\n type ClientHttp2Session,\n connect,\n constants,\n type SecureClientSessionOptions,\n} from 'node:http2'\n\n/**\n * A minimal APNs provider client: an HTTP/2 POST carrying an ES256 JWT.\n *\n * Hand-rolled rather than a dependency, because that is the whole of the\n * protocol and the published CLI's zero-runtime-dep posture is worth more than\n * the eighty lines. Note `fetch`/undici will not do: it does not speak HTTP/2,\n * and APNs accepts nothing else — hence `node:http2` directly.\n *\n * Token authentication, not certificates: one `.p8` serves every app in the team\n * and both environments, and it does not expire. Certificates are per-app,\n * per-environment, and expire annually.\n */\n\nexport type ApnsEnvironment = 'development' | 'production'\n\nexport type ApnsConfig = {\n /** Path to the `.p8`. A path, never the contents — a secret pasted into a\n * config file is a secret in every backup of that file. */\n keyFile: string\n /** The 10-character Key ID shown beside the key in the developer portal. */\n keyId: string\n /** The 10-character Team ID from the top right of the portal. */\n teamId: string\n /** The APNs topic, which is the app's bundle id (`bi.atomic.workerdeck.ios`). */\n topic: string\n /** Environment for a device that registered without naming one. Devices that\n * do name one are always routed by their own answer — see the note on\n * `ApnsEnvironment` below. */\n production?: boolean\n}\n\n/**\n * The environment is per *device token*, not per deployment, and that is the\n * single most expensive thing to get wrong here: a build run from Xcode gets a\n * sandbox token, a TestFlight build gets a production one, and the two\n * namespaces do not overlap. Same key, same phone, different token — cross them\n * and Apple answers `BadDeviceToken`. So the app declares which environment it\n * registered in and the forwarder routes each token to its own host.\n */\nconst HOSTS: Record<ApnsEnvironment, string> = {\n development: 'https://api.sandbox.push.apple.com',\n production: 'https://api.push.apple.com',\n}\n\n/**\n * Apple rejects a provider token older than an hour, and rate-limits refreshing\n * one (`TooManyProviderTokenUpdates`) if you re-sign much more often than every\n * twenty minutes. Forty sits in the middle of that window with room for clock\n * skew at both ends — and re-signing per push, the obvious-looking thing, is\n * exactly what the rate limit exists to punish.\n */\nconst TOKEN_TTL_MS = 40 * 60 * 1000\nconst REQUEST_TIMEOUT_MS = 10_000\n/** Per-address budget for a dial. Node defaults this to 250ms, which is under\n * Apple's observed handshake on at least one real path; see the comment at the\n * `connect()` call. Generous rather than tuned — the cost of being slow to give\n * up on an address is a slower dial, and the cost of being quick is a lost\n * notification. */\nconst DIAL_ATTEMPT_TIMEOUT_MS = 2_000\n\nexport type ApnsRequest = {\n deviceToken: string\n environment: ApnsEnvironment\n payload: unknown\n /** 10 for something a person is waiting on, 5 for everything else. */\n priority?: 5 | 10\n /** Unix seconds after which Apple stops trying. 0 means \"one attempt\". */\n expiration?: number\n /** Later pushes with the same id replace an undelivered earlier one. Max 64\n * bytes, so never pass a raw identifier of unbounded length. */\n collapseId?: string\n}\n\nexport type ApnsResult =\n | { ok: true; apnsId?: string }\n | {\n ok: false\n status: number\n reason: string\n /** The token is dead: drop it from the registry rather than retrying it\n * forever. The app re-registers on its next launch anyway. */\n unregistered: boolean\n }\n\nexport type ApnsClient = {\n send(request: ApnsRequest): Promise<ApnsResult>\n close(): void\n}\n\nconst base64url = (input: Buffer | string): string => Buffer.from(input).toString('base64url')\n\n/**\n * Load and sanity-check the auth key. Done once at startup rather than at the\n * first push, so a mistyped path is a launch error with a clear message instead\n * of a notification that silently never arrives.\n */\nexport async function loadApnsKey(keyFile: string): Promise<KeyObject> {\n let pem: string\n try {\n pem = await readFile(keyFile, 'utf8')\n } catch (error) {\n throw new Error(\n `apns: cannot read the auth key at ${keyFile}: ` +\n `${error instanceof Error ? error.message : String(error)}`,\n )\n }\n let key: KeyObject\n try {\n key = createPrivateKey(pem)\n } catch (error) {\n throw new Error(\n `apns: ${keyFile} is not a private key (${\n error instanceof Error ? error.message : String(error)\n })`,\n )\n }\n if (key.asymmetricKeyType !== 'ec') {\n throw new Error(\n `apns: ${keyFile} is a ${key.asymmetricKeyType ?? 'unknown'} key, not EC — an APNs auth ` +\n 'key is the .p8 downloaded from Keys in the developer portal, not a certificate',\n )\n }\n return key\n}\n\n/**\n * A cached provider JWT. The signature is over `{alg:ES256,kid}` + `{iss,iat}`,\n * and JWS wants the raw `r||s` pair — `sign()` produces a DER SEQUENCE unless\n * told otherwise, which Apple rejects with a bare 403 and no explanation.\n */\nexport function createProviderToken(key: KeyObject, keyId: string, teamId: string): {\n get(now?: number): string\n invalidate(): void\n} {\n let cached: { token: string; issuedAt: number } | null = null\n return {\n get(now = Date.now()) {\n if (cached !== null && now - cached.issuedAt < TOKEN_TTL_MS) return cached.token\n const header = base64url(JSON.stringify({ alg: 'ES256', kid: keyId }))\n const claims = base64url(JSON.stringify({ iss: teamId, iat: Math.floor(now / 1000) }))\n const signingInput = `${header}.${claims}`\n const signature = sign('sha256', Buffer.from(signingInput), {\n key,\n dsaEncoding: 'ieee-p1363',\n })\n const token = `${signingInput}.${base64url(signature)}`\n cached = { token, issuedAt: now }\n return token\n },\n invalidate() {\n cached = null\n },\n }\n}\n\n/**\n * One long-lived HTTP/2 session per environment, reconnected on demand. APNs\n * sends GOAWAY routinely (it rebalances connections), so a dead session is a\n * normal event and not an error worth surfacing — the next push simply dials\n * again.\n */\nfunction createSessionPool(hosts: Record<ApnsEnvironment, string>): {\n get(environment: ApnsEnvironment): ClientHttp2Session\n /** Why the last connection died, for reporting a stream that never got sent.\n * Only failures that *precede* the stream's own death are visible here: when\n * one event kills both, Node cancels the stream synchronously inside the\n * session teardown and emits the session 'error' a tick later, so the stream\n * settles first. The stream's own cause is mined in `describeStreamError`. */\n lastFailure(environment: ApnsEnvironment): string | undefined\n /** Put down a session that a failed send has proven is not making progress.\n * Only ever called on a *connecting* session, where every stream is still\n * pending — each canceled sibling classifies as never-sent and retries\n * itself. Destroying a connected session here would cancel siblings\n * mid-flight, which is exactly the loss this file exists to prevent. Note\n * `destroy()` without an error emits no 'error' event, so this can never\n * take the process down. */\n discard(environment: ApnsEnvironment, session: ClientHttp2Session): void\n close(): void\n} {\n const sessions = new Map<ApnsEnvironment, ClientHttp2Session>()\n const failures = new Map<ApnsEnvironment, string>()\n const drop = (environment: ApnsEnvironment, session: ClientHttp2Session): void => {\n if (sessions.get(environment) === session) sessions.delete(environment)\n }\n return {\n get(environment) {\n const existing = sessions.get(environment)\n if (existing !== undefined && !existing.closed && !existing.destroyed) return existing\n const session = connect(hosts[environment], {\n // Node's Happy Eyeballs (`autoSelectFamily`, default-on since v20) gives\n // each candidate address `autoSelectFamilyAttemptTimeout` — **250ms** by\n // default — before abandoning it and trying the next. Apple's handshake\n // does not fit in that on every path: measured from one machine during a\n // failure burst, IPv4 connects to api.sandbox.push.apple.com took\n // ~600-700ms and every IPv6 candidate answered EHOSTUNREACH instantly, so\n // all six candidates burned in ~765ms (≈3 × 250ms) and the dial failed —\n // 0/5 on the default against 5/5 with this option, interleaved seconds\n // apart. Each of those was a silently lost notification.\n //\n // Raising the window rather than setting `autoSelectFamily: false`\n // deliberately: disabling it entirely would strand a genuinely IPv6-only\n // host, and it measured no better (5/5 either way).\n // `http2.connect` forwards its options to `net`/`tls`, but @types/node\n // does not surface the family-selection ones on the http2 option type,\n // so the cast is the types lagging the runtime rather than a claim\n // about it — the measurement above was taken through this exact call.\n autoSelectFamilyAttemptTimeout: DIAL_ATTEMPT_TIMEOUT_MS,\n } as SecureClientSessionOptions)\n sessions.set(environment, session)\n failures.delete(environment)\n // Without an error listener a GOAWAY-then-error would take the whole\n // process down; the forwarder is a side channel and must never do that.\n session.on('error', (error) => {\n failures.set(environment, `${(error as NodeJS.ErrnoException).code ?? 'error'}: ${error.message}`)\n drop(environment, session)\n })\n // APNs sends GOAWAY routinely to rebalance connections, but it also sends\n // one to hang up on a client it is throttling — notably one that keeps\n // pushing to invalid tokens. The debug data carries the reason when there\n // is one.\n session.on('goaway', (code, _lastStreamId, data) => {\n const detail = data !== undefined && data.length > 0 ? `: ${data.toString('utf8').slice(0, 200)}` : ''\n failures.set(environment, `GOAWAY ${code}${detail}`)\n drop(environment, session)\n })\n session.on('close', () => drop(environment, session))\n // Nothing to say for a while is normal — an idle gateway pushes nothing.\n // Let the socket go rather than pinning a connection open all night.\n session.setTimeout(5 * 60_000, () => session.close())\n return session\n },\n lastFailure: (environment) => failures.get(environment),\n discard(environment, session) {\n drop(environment, session)\n if (!session.destroyed) session.destroy()\n },\n close() {\n for (const session of sessions.values()) session.close()\n sessions.clear()\n },\n }\n}\n\n/**\n * A stream that never left this machine is destroyed with\n * ERR_HTTP2_STREAM_CANCEL, which buries the real failure in `cause` — and when\n * every connect attempt fails (Happy Eyeballs walks both address families of\n * api.push.apple.com), that cause is an AggregateError whose own message is\n * EMPTY, so the text ends in \"(caused by: )\" and names nothing. Mine the\n * aggregate so the log says ECONNREFUSED/EHOSTUNREACH instead of nothing.\n */\nconst describeStreamError = (error: Error): string => {\n const cause = (error as Error & { cause?: unknown }).cause\n if (!(cause instanceof AggregateError)) return error.message\n const parts = cause.errors\n .map((inner) => (inner instanceof Error ? inner.message : String(inner)))\n .filter((message) => message !== '')\n if (parts.length === 0) return error.message\n return `${error.message.replace(' (caused by: )', '')} (caused by: ${parts.join('; ')})`\n}\n\n/**\n * Whether — and how — a failed attempt may be retried without risking a\n * duplicate notification on someone's lock screen:\n * - 'never': the request may have reached Apple. A push is not idempotent\n * (permission requests carry no collapse id on purpose), so give up.\n * - 'now': Apple provably refused the stream before processing anything\n * (REFUSED_STREAM — the routine GOAWAY-rebalance race, RFC 9113 §8.7), or\n * the cached session was found closed before a stream even existed. The\n * network is fine; retry immediately.\n * - 'redial': the stream never got an id, so its HEADERS frame was never\n * handed to the transport — the connect itself failed. Retry once on a\n * fresh connection after a beat, because whatever broke the dial (an\n * interface still coming up, say) may need a moment to pass.\n */\ntype Retry = 'never' | 'now' | 'redial'\n\ntype Attempt = { result: ApnsResult; retry: Retry }\n\nexport function createApnsClient(\n config: ApnsConfig,\n key: KeyObject,\n /** Test seam: point the two environments at a local HTTP/2 server, and\n * shorten the redial pause so a test does not sit out the real one. Nothing\n * in production should pass this — the real endpoints are not configurable,\n * and an operator who could redirect them could exfiltrate every push. */\n options: { hosts?: Record<ApnsEnvironment, string>; retryDelayMs?: number } = {},\n): ApnsClient {\n const providerToken = createProviderToken(key, config.keyId, config.teamId)\n const pool = createSessionPool(options.hosts ?? HOSTS)\n const retryDelayMs = options.retryDelayMs ?? 1000\n\n const post = (request: ApnsRequest, jwt: string): Promise<Attempt> =>\n new Promise((resolve) => {\n const body = Buffer.from(JSON.stringify(request.payload))\n const session = pool.get(request.environment)\n let stream: ReturnType<ClientHttp2Session['request']>\n try {\n stream = session.request({\n ':method': 'POST',\n ':path': `/3/device/${request.deviceToken}`,\n authorization: `bearer ${jwt}`,\n 'apns-topic': config.topic,\n 'apns-push-type': 'alert',\n 'apns-priority': String(request.priority ?? 10),\n 'apns-expiration': String(request.expiration ?? 0),\n 'content-length': String(body.byteLength),\n ...(request.collapseId === undefined ? {} : { 'apns-collapse-id': request.collapseId }),\n })\n } catch (error) {\n // request() refuses on a closed session before creating a stream, so\n // nothing reached the wire and an immediate retry redials safely.\n resolve({\n result: {\n ok: false,\n status: 0,\n reason: error instanceof Error ? error.message : String(error),\n unregistered: false,\n },\n retry: 'now',\n })\n return\n }\n\n let status = 0\n let apnsId: string | undefined\n const chunks: Buffer[] = []\n let settled = false\n const settle = (result: ApnsResult, retry: Retry = 'never'): void => {\n if (settled) return\n settled = true\n resolve({ result, retry })\n }\n\n /** Classify a transport-level death. `pending` is true only while the\n * stream has no id — its HEADERS frame was never handed to nghttp2, so a\n * retry cannot duplicate anything. REFUSED_STREAM is Apple's explicit\n * \"received but not processed\", defined by the RFC as safe to retry. */\n const transportRetry = (): Retry => {\n if (stream.pending) return 'redial'\n if (stream.rstCode === constants.NGHTTP2_REFUSED_STREAM) return 'now'\n return 'never'\n }\n /** A stream still pending when its attempt dies marks a connect that is\n * failing or hanging; without this, every later push (and the retry)\n * would queue behind the same doomed dial until the OS gave up on it. */\n const dropDoomedDial = (): void => {\n if (stream.pending && session.connecting) pool.discard(request.environment, session)\n }\n\n stream.setTimeout(REQUEST_TIMEOUT_MS, () => {\n const retry = transportRetry()\n dropDoomedDial()\n stream.close()\n settle({ ok: false, status: 0, reason: 'Timeout', unregistered: false }, retry)\n })\n stream.on('response', (headers) => {\n status = Number(headers[':status'] ?? 0)\n const id = headers['apns-id']\n apnsId = typeof id === 'string' ? id : undefined\n })\n stream.on('data', (chunk: Buffer) => chunks.push(chunk))\n stream.on('error', (error) => {\n const retry = transportRetry()\n dropDoomedDial()\n // A stream that never left the queue reports only that it was\n // canceled. When the failure *preceded* this stream, the session's\n // account of it is on record; when one event killed both, the session\n // 'error' has not fired yet (Node cancels the stream synchronously\n // inside the teardown and emits the session's own event a tick later),\n // so the cause is mined out of the stream error itself.\n const cause = pool.lastFailure(request.environment)\n const message = describeStreamError(error)\n settle(\n {\n ok: false,\n status,\n reason: cause !== undefined ? `${cause} (stream: ${message})` : message,\n unregistered: false,\n },\n retry,\n )\n })\n stream.on('end', () => {\n if (status === 200) {\n settle({ ok: true, apnsId })\n return\n }\n let reason = `HTTP ${status}`\n try {\n const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8')) as { reason?: string }\n if (typeof parsed.reason === 'string') reason = parsed.reason\n } catch {\n // A non-JSON body from APNs is exotic; the status still classifies it.\n }\n settle({\n ok: false,\n status,\n reason,\n // 410 Unregistered is Apple telling us the app was deleted.\n // BadDeviceToken means the token does not belong to this topic *or*\n // this environment; either way it will never work, and the app writes\n // a fresh one on its next launch.\n unregistered: reason === 'Unregistered' || reason === 'BadDeviceToken',\n })\n })\n // Last resort: a stream torn down with neither an error nor a response\n // ('end' never fires on a destroyed readable) must still settle, or the\n // forwarder's per-session delivery chain would hang on it forever.\n stream.on('close', () => {\n const retry = transportRetry()\n settle(\n {\n ok: false,\n status,\n reason:\n pool.lastFailure(request.environment) ??\n `connection closed (RST code ${stream.rstCode})`,\n unregistered: false,\n },\n retry,\n )\n })\n stream.end(body)\n })\n\n const wait = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))\n\n return {\n async send(request) {\n let attempt = await post(request, providerToken.get())\n // A push the transport provably never delivered is retried exactly once:\n // redialling is cheap, and a lost approval request is the whole cost of\n // this file. 'never' failures are never retried — the request may have\n // reached Apple, and a duplicated notification is a real harm here\n // (permission requests carry no collapse id on purpose).\n if (!attempt.result.ok && attempt.retry !== 'never') {\n if (attempt.retry === 'redial') await wait(retryDelayMs)\n attempt = await post(request, providerToken.get())\n }\n const first = attempt.result\n // The one *response* worth an automatic retry: our cached JWT aged out\n // against Apple's clock rather than ours. Re-sign once and try again;\n // anything else is the caller's problem.\n if (!first.ok && first.reason === 'ExpiredProviderToken') {\n providerToken.invalidate()\n return (await post(request, providerToken.get())).result\n }\n return first\n },\n close: pool.close,\n }\n}\n","import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { join } from 'node:path'\nimport type { ApnsEnvironment } from './client.ts'\n\n/**\n * The device-token registry, and the route that fills it.\n *\n * This has to live *somewhere*, and the point of the webhooks-first decision is\n * that it is not `packages/server`: the OSS gateway stays credential-free and\n * knows nothing about APNs. So the turnkey CLI mounts its own route through the\n * server's `fallback` hook — the same seam that serves the dashboard — and\n * registration lands on the gateway's own origin, behind the auth key that\n * already guards everything else.\n *\n * Storage is a JSON file under the state dir with the same 0600 posture as\n * `auth-key`. Device tokens are not secrets in the way an auth key is, but they\n * are a list of which phones belong to the operator, and there is no reason for\n * every user on the machine to read it.\n */\n\nexport type DeviceRecord = {\n /** Hex APNs device token. */\n token: string\n environment: ApnsEnvironment\n /**\n * Opaque to us: the client's own id for this gateway, echoed back in every\n * payload. It is what lets an app configured with two gateways tell which one\n * woke it — we store the string and never interpret it.\n */\n hostId?: string\n bundleId?: string\n platform?: string\n updatedAt: number\n}\n\nexport type DeviceRegistry = {\n list(): DeviceRecord[]\n register(record: Omit<DeviceRecord, 'updatedAt'>): Promise<void>\n remove(token: string): Promise<void>\n}\n\nconst FILENAME = 'apns-devices.json'\n/** Apple's tokens are 64 hex chars today. The upper bound is generous because\n * Apple has reserved the right to grow them; the lower one just rejects junk. */\nconst TOKEN_PATTERN = /^[0-9a-fA-F]{32,200}$/\nconst MAX_BODY_BYTES = 4096\n\nconst isEnvironment = (value: unknown): value is ApnsEnvironment =>\n value === 'development' || value === 'production'\n\n/**\n * `dir` null keeps the registry in memory: a restart then forgets every token,\n * which is survivable because the app re-registers on launch, but it does mean\n * an instance with no state dir goes quiet until each phone is next opened.\n */\nexport async function createDeviceRegistry(options: {\n dir: string | null\n onError?: (error: unknown, context: { op: string; path: string }) => void\n}): Promise<DeviceRegistry> {\n const path = options.dir === null ? null : join(options.dir, FILENAME)\n const devices = new Map<string, DeviceRecord>()\n\n if (path !== null) {\n try {\n const parsed = JSON.parse(await readFile(path, 'utf8')) as { devices?: DeviceRecord[] }\n for (const record of parsed.devices ?? []) {\n if (typeof record?.token === 'string' && isEnvironment(record.environment)) {\n devices.set(record.token, record)\n }\n }\n } catch {\n // Missing, unreadable, or corrupt all land here: start empty rather than\n // refusing to boot over a side channel. Every client re-registers anyway.\n }\n }\n\n const persist = async (): Promise<void> => {\n if (path === null) return\n try {\n await mkdir(options.dir!, { recursive: true, mode: 0o700 })\n await writeFile(path, `${JSON.stringify({ devices: [...devices.values()] }, null, 2)}\\n`, {\n mode: 0o600,\n })\n // writeFile's mode only applies on creation, so an existing file with\n // looser bits would keep them.\n await chmod(path, 0o600)\n } catch (error) {\n options.onError?.(error, { op: 'write', path })\n }\n }\n\n return {\n list: () => [...devices.values()],\n async register(record) {\n const existing = devices.get(record.token)\n devices.set(record.token, { ...record, updatedAt: Date.now() })\n // A repeat registration with nothing new to say is the common case — the\n // app re-registers on every launch — and rewriting the file for it is\n // pointless I/O.\n if (\n existing !== undefined &&\n existing.environment === record.environment &&\n existing.hostId === record.hostId\n ) {\n return\n }\n await persist()\n },\n async remove(token) {\n if (!devices.delete(token)) return\n await persist()\n },\n }\n}\n\nconst respond = (res: ServerResponse, status: number, body: Record<string, unknown>): void => {\n res\n .writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' })\n .end(JSON.stringify(body))\n}\n\nconst readBody = (req: IncomingMessage): 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 > MAX_BODY_BYTES) {\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/**\n * `POST /apns/devices` to register a token, `DELETE /apns/devices` to drop one.\n * Returns true when it consumed the request.\n *\n * Deliberately outside `/v1`: this is the forwarder's own surface, not part of\n * the protocol `packages/protocol` defines, and a client that finds a 404 here\n * has simply reached a gateway running without push configured.\n *\n * DELETE exists so removing a gateway from the app can stop its pushes. Without\n * it a forgotten server keeps buzzing a phone that no longer has any way to act\n * on what it says.\n */\nexport function createDeviceRoute(\n registry: DeviceRegistry,\n authenticate: (req: IncomingMessage) => unknown,\n): (req: IncomingMessage, res: ServerResponse) => Promise<boolean> {\n return async (req, res) => {\n let pathname: string\n try {\n pathname = new URL(req.url ?? '/', 'http://internal').pathname\n } catch {\n return false\n }\n if (pathname !== '/apns/devices') return false\n\n if (req.method !== 'POST' && req.method !== 'DELETE') {\n res.writeHead(405, { allow: 'POST, DELETE' }).end()\n return true\n }\n // The same operator secret that guards everything else. A device token is\n // an address this gateway can buzz, so handing one out unauthenticated\n // would let anyone who can reach the port aim it.\n if (authenticate(req) === null) {\n respond(res, 401, { error: 'unauthorized' })\n return true\n }\n\n const raw = await readBody(req)\n if (raw === null) {\n respond(res, 413, { error: 'body too large' })\n res.once('finish', () => req.destroy())\n return true\n }\n let body: Record<string, unknown>\n try {\n body = JSON.parse(raw) as Record<string, unknown>\n } catch {\n respond(res, 400, { error: 'invalid JSON body' })\n return true\n }\n\n const token = body.token\n if (typeof token !== 'string' || !TOKEN_PATTERN.test(token)) {\n respond(res, 400, { error: 'token must be a hex APNs device token' })\n return true\n }\n\n if (req.method === 'DELETE') {\n await registry.remove(token)\n res.writeHead(204).end()\n return true\n }\n\n if (!isEnvironment(body.environment)) {\n respond(res, 400, { error: \"environment must be 'development' or 'production'\" })\n return true\n }\n const optionalString = (value: unknown): string | undefined =>\n typeof value === 'string' && value.length > 0 && value.length <= 200 ? value : undefined\n\n await registry.register({\n token,\n environment: body.environment,\n hostId: optionalString(body.hostId),\n bundleId: optionalString(body.bundleId),\n platform: optionalString(body.platform),\n })\n // The environment is echoed so a client can see what it was recorded as —\n // the one fact that is expensive to get wrong and invisible otherwise.\n respond(res, 200, { registered: true, environment: body.environment })\n return true\n }\n}\n","import { createHash } from 'node:crypto'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { SessionInfo, SessionNotification } from '@workerdeck/protocol'\nimport {\n type ApnsClient,\n type ApnsConfig,\n createApnsClient,\n loadApnsKey,\n type ApnsRequest,\n} from './client.ts'\nimport { createDeviceRegistry, createDeviceRoute, type DeviceRegistry } from './devices.ts'\n\n/**\n * Turns the server's session notifications into APNs pushes.\n *\n * The architectural point, restated because it is easy to erode: session\n * webhooks are the primitive and this is one consumer of them. It hooks\n * `notifications.onNotification` in-process — same process as the gateway, so\n * there is no HTTP hop — but nothing about the server knows that, and the same\n * events can just as well drive Slack or a custom relay. Push credentials live\n * here and nowhere in `packages/server`.\n */\n\n/** APNs caps a payload at 4 KB. Staying well under leaves room for the alert\n * dictionary to grow without anyone rediscovering the limit the hard way. */\nconst MAX_PAYLOAD_BYTES = 3800\nconst BODY_LIMIT = 300\n\nexport type ApnsForwarder = {\n /** Hand to `createWorkerServer({ notifications: { onNotification } })`. */\n onNotification: (notification: SessionNotification) => void\n /** Mount ahead of the static host; true when it consumed the request. */\n handleRequest: (req: IncomingMessage, res: ServerResponse) => Promise<boolean>\n /** How many devices are registered, for the startup banner. */\n deviceCount: () => number\n close: () => void\n}\n\n/** Category identifiers are wire contract with the app: it registers the\n * Approve/Deny actions under this exact string, and a mismatch means a\n * notification that arrives with no buttons on it. */\nconst CATEGORY = {\n permission: 'PERMISSION_REQUEST',\n event: 'SESSION_EVENT',\n} as const\n\nconst label = (session: SessionInfo): string => {\n const title = session.title?.trim()\n if (title !== undefined && title !== '') return title\n const leaf = session.cwd.split('/').filter(Boolean).at(-1)\n return leaf !== undefined && leaf !== '' ? leaf : session.id\n}\n\nconst oneLine = (text: string | undefined, limit = BODY_LIMIT): string => {\n const flat = (text ?? '').replace(/\\s+/g, ' ').trim()\n return flat.length <= limit ? flat : `${flat.slice(0, limit - 1)}…`\n}\n\n/** A collapse id is capped at 64 bytes and a session id has no such bound, so\n * hash rather than truncate — two sessions sharing a prefix must not collapse\n * into each other. */\nconst collapseKey = (sessionId: string): string =>\n createHash('sha256').update(sessionId).digest('base64url').slice(0, 32)\n\nconst titleFor = (notification: SessionNotification, name: string): string => {\n switch (notification.type) {\n case 'permission_requested':\n return `Approval needed — ${name}`\n case 'turn_completed':\n return notification.result?.isError === true ? `Turn failed — ${name}` : name\n case 'session_error':\n return `Session error — ${name}`\n case 'session_closed':\n return `Session ended — ${name}`\n default:\n return name\n }\n}\n\nconst bodyFor = (notification: SessionNotification): string => {\n const preview = oneLine(notification.preview)\n if (preview !== '') return preview\n switch (notification.type) {\n case 'permission_requested':\n return 'The agent is waiting for your approval.'\n case 'turn_completed':\n return 'The turn finished.'\n case 'session_closed':\n return `Closed by the ${notification.reason ?? 'server'}.`\n default:\n return 'Something needs your attention.'\n }\n}\n\n/**\n * Build the push for one notification.\n *\n * The payload carries routing and nothing else — `sessionId` to deep-link,\n * `requestId` because a lock-screen Approve has nothing to POST to without it,\n * and `hostId` so a client with two gateways knows which one this came from.\n * Everything else the app fetches over REST the moment it opens; a transcript\n * has no business in a 4 KB envelope.\n */\nexport function buildPush(\n notification: SessionNotification,\n hostId: string | undefined,\n): Omit<ApnsRequest, 'deviceToken' | 'environment'> {\n const permission = notification.type === 'permission_requested'\n const name = label(notification.session)\n let body = bodyFor(notification)\n\n const build = (text: string): Record<string, unknown> => ({\n aps: {\n alert: { title: titleFor(notification, name), body: text },\n sound: 'default',\n category: permission ? CATEGORY.permission : CATEGORY.event,\n // Groups a session's notifications together in Notification Center.\n 'thread-id': notification.sessionId,\n },\n type: notification.type,\n sessionId: notification.sessionId,\n seq: notification.seq,\n ...(hostId === undefined ? {} : { hostId }),\n ...(notification.request === undefined ? {} : { requestId: notification.request.id }),\n })\n\n let payload = build(body)\n // Shrink the one field that can be arbitrarily long until it fits. Everything\n // else in here is bounded by construction.\n while (Buffer.byteLength(JSON.stringify(payload)) > MAX_PAYLOAD_BYTES && body.length > 16) {\n body = oneLine(body, Math.floor(body.length / 2))\n payload = build(body)\n }\n\n return {\n payload,\n // 10 for the two a person is actually waiting on; 5 lets the system batch\n // the rest against the device's power budget.\n priority: permission || notification.type === 'session_error' ? 10 : 5,\n // A permission request that has already timed out server-side is worse than\n // useless on a lock screen, so Apple is told to stop trying at the same\n // moment the server would have resolved it.\n expiration:\n permission && notification.request?.expiresAt !== undefined\n ? Math.floor(notification.request.expiresAt / 1000)\n : Math.floor(Date.now() / 1000) + 3600,\n // Ten turns finishing while the phone is in a pocket should be one badge,\n // not ten. Permission requests are never collapsed: each one is a distinct\n // question with its own `requestId`, and replacing one with the next would\n // silently drop a decision the operator still owes.\n ...(notification.type === 'turn_completed'\n ? { collapseId: `t:${collapseKey(notification.sessionId)}` }\n : {}),\n }\n}\n\nexport async function createApnsForwarder(options: {\n config: ApnsConfig\n /** Where the device registry is persisted; null keeps it in memory. */\n stateDir: string | null\n /** Guards `/apns/devices` — the instance's own `authenticate`. */\n authenticate: (req: IncomingMessage) => unknown\n warn?: (message: string) => void\n}): Promise<ApnsForwarder> {\n const warn =\n options.warn ?? ((message: string) => process.stderr.write(`[workerdeck] apns: ${message}\\n`))\n const key = await loadApnsKey(options.config.keyFile)\n const client: ApnsClient = createApnsClient(options.config, key)\n const registry: DeviceRegistry = await createDeviceRegistry({\n dir: options.stateDir,\n onError: (error, context) =>\n warn(\n `device registry ${context.op} failed for ${context.path}: ` +\n `${error instanceof Error ? error.message : String(error)}`,\n ),\n })\n const handleRequest = createDeviceRoute(registry, options.authenticate)\n const fallbackEnvironment = options.config.production === false ? 'development' : 'production'\n\n /** Per-session delivery chain, so a session's pushes arrive in the order the\n * events happened — which matters precisely because `turn_completed` collapses\n * and an out-of-order pair would leave the older text on screen. */\n const chains = new Map<string, Promise<void>>()\n\n const deliver = async (notification: SessionNotification): Promise<void> => {\n const devices = registry.list()\n if (devices.length === 0) return\n await Promise.all(\n devices.map(async (device) => {\n const push = buildPush(notification, device.hostId)\n const result = await client.send({\n ...push,\n deviceToken: device.token,\n environment: device.environment ?? fallbackEnvironment,\n })\n if (result.ok) return\n if (result.unregistered) {\n await registry.remove(device.token)\n return\n }\n warn(`push to ${device.token.slice(0, 8)}… failed: ${result.reason} (${result.status})`)\n }),\n )\n }\n\n return {\n onNotification(notification) {\n const previous = chains.get(notification.sessionId) ?? Promise.resolve()\n const next = previous.then(() =>\n deliver(notification).catch((error: unknown) => {\n warn(error instanceof Error ? error.message : String(error))\n }),\n )\n chains.set(notification.sessionId, next)\n // Drop the chain once it drains, so a long-lived gateway does not keep one\n // resolved promise per session it ever ran.\n void next.then(() => {\n if (chains.get(notification.sessionId) === next) chains.delete(notification.sessionId)\n })\n },\n handleRequest,\n deviceCount: () => registry.list().length,\n close: client.close,\n }\n}\n","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 { chmod, mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport type { CliSessionStore, StoredSession } from './auth.ts'\n\n/**\n * Durable browser-login sessions for the turnkey CLI — the other half of\n * `materializeAuthKey`, and here for the same reason: the key survives a\n * restart so clients stay paired, and the login cookie should too. Without\n * this the cookie outlives the table it points into, so a restart signs every\n * browser out while the browser still holds a perfectly valid-looking cookie.\n *\n * What lands on disk is **not credential material**: `createCliAuth` keys its\n * table by `HMAC-SHA256(secret, token)`, so a stolen file yields neither the\n * operator secret nor any cookie value (inverting either needs a preimage of a\n * 256-bit-entropy input). That keying is also what makes key rotation\n * invalidate every outstanding cookie for free — entries written under the old\n * secret simply never match a lookup again, and age out on their own expiry.\n *\n * Writes are whole-file and serialized behind one promise chain (the table is\n * capped at `MAX_SESSIONS`, so \"whole file\" is a few kilobytes), and go through\n * a temp file + rename so a crash mid-write cannot leave a truncated table.\n * Every failure is a warning, never a throw: losing durability signs the\n * operator out, losing the gateway does much worse.\n */\n\nconst FORMAT_VERSION = 1\nconst FILE_NAME = 'auth-sessions.json'\n\nexport type AuthSessionStoreOptions = {\n /** Where `<stateDir>/auth-sessions.json` lives. */\n stateDir: string\n /** Defaults to a `[workerdeck]` line on stderr. */\n warn?: (message: string) => void\n /** Test seam; defaults to `Date.now`. */\n now?: () => number\n}\n\ntype FileShape = { version: number; sessions: [string, number][] }\n\nconst parseSessions = (raw: string, now: number): [string, StoredSession][] => {\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n return []\n }\n const file = parsed as Partial<FileShape>\n if (file?.version !== FORMAT_VERSION || !Array.isArray(file.sessions)) return []\n const entries: [string, StoredSession][] = []\n for (const entry of file.sessions) {\n if (!Array.isArray(entry) || entry.length !== 2) continue\n const [key, expiresAt] = entry\n // Expired entries are dropped on load rather than carried and swept later:\n // the cap they would otherwise occupy is the operator's own login budget.\n if (typeof key !== 'string' || key === '') continue\n if (typeof expiresAt !== 'number' || !Number.isFinite(expiresAt) || expiresAt <= now) continue\n entries.push([key, { expiresAt }])\n }\n return entries\n}\n\nexport async function createAuthSessionStore(options: AuthSessionStoreOptions): Promise<CliSessionStore> {\n const now = options.now ?? Date.now\n const warn = options.warn ?? ((message: string) => process.stderr.write(`[workerdeck] ${message}\\n`))\n const path = join(options.stateDir, FILE_NAME)\n\n let initial: [string, StoredSession][] = []\n try {\n const raw = await readFile(path, 'utf8')\n initial = parseSessions(raw, now())\n try {\n const { mode } = await stat(path)\n if ((mode & 0o077) !== 0) {\n warn(\n `session 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 table still works.\n }\n } catch {\n initial = [] // Missing, unreadable and corrupt all mean \"start empty\".\n }\n\n // One chain, so two mutations in the same tick cannot interleave their\n // writes and land the older table last.\n let pending: Promise<void> = Promise.resolve()\n let warned = false\n\n const write = async (entries: [string, StoredSession][]): Promise<void> => {\n const body: FileShape = { version: FORMAT_VERSION, sessions: entries.map(([key, s]) => [key, s.expiresAt]) }\n const tmp = `${path}.${process.pid}.tmp`\n try {\n await mkdir(dirname(path), { recursive: true, mode: 0o700 })\n await writeFile(tmp, `${JSON.stringify(body)}\\n`, { mode: 0o600 })\n // `writeFile`'s mode applies only on creation — a reused temp path must\n // not inherit whatever bits an earlier run left on it.\n await chmod(tmp, 0o600)\n await rename(tmp, path)\n warned = false\n } catch (error) {\n // Warn once per outage, not once per login: a read-only state dir would\n // otherwise print on every request that touches the table.\n if (!warned) {\n warned = true\n warn(`could not persist browser logins to ${path}: ${error instanceof Error ? error.message : String(error)}`)\n }\n }\n }\n\n return {\n initial,\n save(entries) {\n pending = pending.then(() => write(entries))\n },\n flush: () => pending,\n }\n}\n","import { createHash, createHmac, 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, three transports.\n *\n * Services present the secret itself on every request (`x-workerdeck-key`,\n * or `Authorization: Bearer`). The dashboard *served by this gateway* cannot:\n * it calls `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 * The third transport exists for a dashboard served *elsewhere* attaching to\n * this gateway: it holds the key (the operator typed it in) and can put it on\n * REST as a header, but still cannot put it on the WS handshake — and the\n * cookie is another origin's, so it does not ride either. Such a client passes\n * the key as `?key=` on the upgrade URL, and `querySecret` below accepts it\n * **on upgrades only**.\n *\n * That is a deliberate, narrow concession, and its cost should be understood\n * rather than rediscovered: a key in a query string is a permanent, replayable\n * credential sitting in reverse-proxy access logs, where a header would not be.\n * It is confined to upgrades so a leaked URL buys an attach and nothing else,\n * and the seam it arrives through (`ClientOptions.buildWsUrl`) is the same one\n * a short-lived minted ticket would use — swapping to one later needs no client\n * change.\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 simply lands the\n * operator back on the login page — as does a restart, unless a `sessions`\n * store is supplied (the CLI supplies one whenever it has a state dir).\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 * Makes browser logins survive a restart. Absent, the session table is\n * in-memory and a restart signs every browser out — see the table's own note.\n * `createAuthSessionStore` is the CLI's file-backed implementation.\n */\n sessions?: CliSessionStore\n}\n\n/** One session-table row as it is handed to a store; the key is opaque here. */\nexport type StoredSession = { expiresAt: number }\n\n/**\n * The durability seam for browser logins. Deliberately narrow and\n * fire-and-forget: the auth paths are synchronous, so a store may not make them\n * wait, and a store that cannot write must degrade to \"logins do not survive a\n * restart\" rather than refuse a login.\n */\nexport type CliSessionStore = {\n /** Rows recovered at startup, already pruned of expired ones. */\n initial?: Iterable<[string, StoredSession]>\n /** The whole live table after a mutation. Must not throw. */\n save(entries: [string, StoredSession][]): void\n /** Resolves once queued writes have landed — for tests and shutdown. */\n flush?(): Promise<void>\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 — optionally mirrored to a\n * `CliSessionStore` so a restart does not sign every browser out while the\n * browser still holds a cookie the ttl says is good for a week.\n *\n * Keys are `HMAC-SHA256(secret, token)`, which buys three things at once:\n * recovering a token from a key (or from lookup timing) needs a preimage;\n * what a store writes to disk is not credential material; and rotating the\n * operator secret invalidates every outstanding cookie **for free**, since\n * rows written under the old secret can no longer be looked up and age out on\n * their own expiry.\n */\n const sessions = new Map<string, StoredSession>()\n const store = options.sessions\n const tokenKey = (token: string): string =>\n secret === undefined ? sha256(token).toString('hex') : createHmac('sha256', secret).update(token).digest('hex')\n\n if (store?.initial !== undefined) {\n for (const [key, entry] of store.initial) {\n if (sessions.size >= MAX_SESSIONS) break\n sessions.set(key, { expiresAt: entry.expiresAt })\n }\n }\n\n // Every mutation republishes the whole table: it is capped at MAX_SESSIONS,\n // and a diff protocol would be a second source of truth to keep honest.\n const persist = (): void => store?.save([...sessions])\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 persist()\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 persist()\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 isUpgradeRequest = (req: IncomingMessage): boolean => {\n const upgrade = req.headers.upgrade\n return typeof upgrade === 'string' && upgrade.toLowerCase().includes('websocket')\n }\n\n /**\n * The secret from `?key=` — **accepted on WebSocket upgrades only**.\n *\n * A browser cannot put a header on a WS handshake, so a tab attaching to a\n * gateway that is not its own origin (where the cookie would ride) has no\n * other way to present the key. That is the whole reason this exists.\n *\n * Restricting it to upgrades is what keeps the blast radius at \"one attach\":\n * a key in a query string is not a transport we want anywhere else, because\n * URLs land in proxy access logs and browser history in a way headers do not.\n * A REST call with `?key=` is therefore *not* authenticated by it.\n */\n const querySecret = (req: IncomingMessage): string | undefined => {\n if (!isUpgradeRequest(req)) return undefined\n const key = new URL(req.url ?? '/', 'http://internal').searchParams.get('key')\n return key !== null && key !== '' ? key : undefined\n }\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) ?? querySecret(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 isUpgrade = isUpgradeRequest(req)\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))) persist()\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 { ApnsConfig } from './apns/client.ts'\nimport type { CliAuthOptions } from './auth/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 * Serve the web dashboard at all. Default true — being turnkey is the point\n * of this package.\n *\n * `false` makes the instance a bare gateway: `/v1` and the auth routes answer\n * exactly as before, everything else 404s, and the dashboard build is never\n * even looked for (so a checkout with no `@workerdeck/web` build can still\n * run one). For an operator who reaches this gateway only from the VS Code\n * extension, the phone, or another machine's dashboard, the served copy is\n * surface they were not using.\n */\n web?: boolean\n /**\n * Browser origins allowed to call this gateway cross-origin — for a dashboard\n * served somewhere else. Exact origins (`https://deck.example`), never a\n * wildcard. Refused unless auth is on: CORS on an open gateway would let any\n * allowlisted page drive it with no credential at all.\n */\n corsOrigins?: string[]\n /**\n * Forward session notifications to Apple Push Notification service, for the\n * iOS app. Absent turns the forwarder off entirely — including its\n * `/apns/devices` route, so a gateway without this answers 404 there and the\n * app quietly stops asking.\n *\n * Lives here rather than in `packages/server` on purpose: this is the only\n * place in the project that holds a push credential, and the OSS gateway\n * stays credential-free. `keyFile` is a path, never key contents.\n */\n apns?: ApnsConfig\n}\n\nexport type CliFlags = {\n config?: string\n port?: number\n host?: string\n authKey?: string\n profiles: ProfileInfo[]\n cwdRoots: string[]\n fsRoots: string[]\n fsWrite?: boolean\n allowedOrigins: string[]\n allowedHosts: string[]\n insecureHosts: string[]\n trustProxy?: boolean\n stateDir?: string\n parking?: boolean\n insecure?: boolean\n web?: boolean\n corsOrigins: string[]\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 fsRoots: [],\n allowedOrigins: [],\n allowedHosts: [],\n insecureHosts: [],\n corsOrigins: [],\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 '--fs-root':\n flags.fsRoots.push(resolve(next(i, arg)))\n i++\n break\n case '--fs-write':\n flags.fsWrite = true\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 '--no-web':\n flags.web = false\n break\n case '--cors-origin':\n flags.corsOrigins.push(next(i, arg))\n i++\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 /** Whether to serve the dashboard at all. False makes this a bare gateway. */\n web: boolean\n /** Browser origins allowed to call this gateway cross-origin. Empty = off. */\n corsOrigins: string[]\n /** APNs forwarder settings with `keyFile` made absolute, or undefined for an\n * instance that does not push. */\n apns?: ApnsConfig\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 // --fs-root *narrows* file access; it does not enable it. Reading follows\n // --cwd-root, since a caller who may start a session in a tree can already read\n // that tree through the agent. Writing still needs --fs-write.\n const envFsRoots = env.WORKERDECK_FS_ROOTS?.split(':')\n .filter(Boolean)\n .map((p) => resolve(cwd, p))\n const fsRoots = flags.fsRoots.length ? flags.fsRoots : envFsRoots\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 // Flag wins over config file, which wins over the turnkey default.\n const web = flags.web ?? loaded.options.web ?? true\n\n const corsOrigins = flags.corsOrigins.length\n ? flags.corsOrigins\n : (loaded.options.corsOrigins ?? [])\n for (const origin of corsOrigins) {\n // Exactness is the guarantee. A wildcard would hand the API to every page\n // on the internet that can reach this port, and a bare hostname is not an\n // origin — the browser sends scheme + host + port, and compares literally.\n if (origin === '*') throw new ConfigError('--cors-origin does not accept \"*\"')\n let parsed: URL\n try {\n parsed = new URL(origin)\n } catch {\n throw new ConfigError(`--cors-origin expects a full origin, got: ${origin}`)\n }\n if (parsed.origin !== origin) {\n throw new ConfigError(\n `--cors-origin expects scheme://host[:port] with no path, got: ${origin}`,\n )\n }\n }\n // Naming both is a mistake, not a preference to silently resolve: one of the\n // two says what the operator meant and we cannot tell which.\n if (!web && flags.open) {\n throw new ConfigError('--open cannot be used with --no-web: there is no dashboard to open')\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 web: _web,\n corsOrigins: _cors,\n apns: _ap,\n ...serverOptions\n } = loaded.options\n const options: WorkerServerOptions = { ...serverOptions }\n if (cwdRoots?.length) options.allowedCwdRoots = cwdRoots\n if (fsRoots?.length || flags.fsWrite) {\n options.hostFiles = {\n ...loaded.options.hostFiles,\n ...(fsRoots?.length ? { roots: fsRoots } : {}),\n ...(flags.fsWrite ? { write: true } : {}),\n }\n }\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 web,\n corsOrigins,\n apns: resolveApns(loaded),\n open: flags.open ?? false,\n options,\n }\n}\n\n/**\n * A relative `keyFile` resolves against the config file's own directory, not the\n * cwd — the same convention `defaultStateDir` uses, and the one that makes a\n * deployment directory self-contained. Missing required fields throw here rather\n * than at the first push, since a half-configured forwarder is a notification\n * that silently never arrives.\n */\nfunction resolveApns(loaded: LoadedConfig): ApnsConfig | undefined {\n const apns = loaded.options.apns\n if (!apns) return undefined\n for (const field of ['keyFile', 'keyId', 'teamId', 'topic'] as const) {\n if (typeof apns[field] !== 'string' || apns[field] === '') {\n throw new ConfigError(`apns.${field} is required when apns is configured`)\n }\n }\n const base = loaded.path ? dirname(loaded.path) : process.cwd()\n return { ...apns, keyFile: resolve(base, apns.keyFile) }\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 ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' })[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 { createApnsForwarder } from '../apns/forwarder.ts'\nimport { materializeAuthKey, type MaterializedAuthKey } from '../auth/auth-key.ts'\nimport { createAuthSessionStore } from '../auth/auth-sessions.ts'\nimport { createCliAuth, type CliAuth } from '../auth/auth.ts'\nimport { hostnameOf, isLoopbackHostname, type ResolvedConfig } from '../config.ts'\nimport { renderLoginPage } from '../auth/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/** The request path, or null when the target is malformed enough that `URL`\n * refuses it — a caller matching a fixed route wants a miss, not a throw. */\nfunction pathnameOf(req: IncomingMessage): string | null {\n try {\n return new URL(req.url ?? '/', 'http://internal').pathname\n } catch {\n return null\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 *\n * The APNs device route sits between auth and assets: it does its own\n * authentication (header only — it is never called by a browser), and it must\n * not fall through to the SPA's catch-all, which would answer a failed\n * registration with a 200 and an HTML document.\n *\n * That last rule holds whether or not a forwarder is configured, which is why\n * the path is claimed unconditionally below. With no `apns` config the route\n * does not exist, and an unclaimed `/apns/devices` reaches the catch-all, where\n * a registration POST draws a 405 (`GET, HEAD`) rather than the documented 404.\n * A client cannot tell that from a broken gateway: the iOS app reads only 404\n * as `unsupported`, so it throws instead, never marks the host synced, and\n * retries on every foreground with a visible error — for what is the normal\n * state of every gateway that never wanted push.\n */\nfunction createFallback(\n auth: CliAuth,\n /** Undefined when the dashboard is switched off — everything below the auth\n * and APNs routes then 404s, and `/v1` is unaffected. */\n webRoot: string | undefined,\n hostAllowed: (req: IncomingMessage) => boolean,\n apnsRoute?: (req: IncomingMessage, res: ServerResponse) => Promise<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 if (apnsRoute !== undefined && (await apnsRoute(req, res))) return\n if (apnsRoute === undefined && pathnameOf(req) === '/apns/devices') {\n res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' })\n res.end('this gateway runs without push\\n')\n return\n }\n\n // Nothing below this point exists without a dashboard. Deliberately after\n // the auth and APNs routes: turning the *dashboard* off must not turn off\n // the gateway's own surfaces, and `/v1` never reaches this hook at all.\n if (webRoot === undefined) {\n res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' })\n res.end('the web dashboard is disabled on this gateway\\n')\n return\n }\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 // Resolved lazily: `resolveWebRoot` throws when there is no build, and an\n // instance told not to serve the dashboard should not need one to exist.\n const webRoot = config.web ? (config.webRoot ?? resolveWebRoot()) : undefined\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 authOptions = config.hostAuthenticates\n ? { ...config.auth, secret: undefined }\n : generated\n ? { ...config.auth, secret: generated.key }\n : config.auth\n // Browser logins persist beside the key they were granted against, for the\n // same reason the key does: a restart should not un-pair every client. Only\n // meaningful when there is a secret to log in with and somewhere to write.\n const sessions =\n authOptions.secret !== undefined && config.stateDir\n ? await createAuthSessionStore({ stateDir: config.stateDir })\n : undefined\n const auth = createCliAuth(sessions ? { ...authOptions, sessions } : authOptions)\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 // Sibling of the assert above, guarding the same promise from the other side.\n // CORS on an open gateway would let any allowlisted page read and drive it\n // with no credential at all — the sanctioned answer to \"I want a remote\n // dashboard on a keyless gateway\" is to set a key.\n if (config.corsOrigins.length > 0 && !config.hostAuthenticates && !auth.enabled) {\n throw new Error(\n 'refusing to serve: corsOrigins is set but this instance has no auth — ' +\n 'a cross-origin page could drive it with no credential. Set --auth-key.',\n )\n }\n const hostAllowed = createHostGuard(config.allowedHosts)\n\n // The only push credential in the project, and it is here rather than in\n // `packages/server` by design: the OSS gateway emits notifications and holds\n // nothing that could deliver one. A bad key path throws before we listen —\n // better a refusal at launch than a phone that never buzzes and never says why.\n const apns =\n config.apns === undefined\n ? undefined\n : await createApnsForwarder({\n config: config.apns,\n stateDir: config.stateDir,\n // The same principal check as everything else on this origin, so the\n // app registers with the key it already holds.\n authenticate: (req) => (hostAllowed(req) ? auth.authenticate(req) : null),\n })\n\n const fallback = createFallback(auth, webRoot, hostAllowed, apns?.handleRequest)\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 // Composed, not replaced: a config file may already have a webhook and its\n // own observer, and turning push on must not silently unhook either.\n notifications: apns === undefined ? config.options.notifications : {\n ...config.options.notifications,\n onNotification: (notification) => {\n config.options.notifications?.onNotification?.(notification)\n apns.onNotification(notification)\n },\n },\n fallback,\n ...(config.corsOrigins.length ? { cors: { origins: config.corsOrigins } } : {}),\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 // Said plainly, because the URL above is the first thing an operator will\n // paste into a browser and it now answers 404.\n if (!config.web) line(' dashboard: off — bare gateway, /v1 and /auth only')\n if (config.corsOrigins.length) {\n line(` cors: ${config.corsOrigins.join(', ')} may call /v1 (still key-gated)`)\n }\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 (apns) {\n const count = apns.deviceCount()\n line(\n ` push: APNs forwarder on ${config.apns?.topic} — ` +\n `${count === 0 ? 'no devices registered yet' : `${count} device(s)`}`,\n )\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 // The session table's writes are queued, not awaited by the request that\n // caused them — a login should not wait on a disk write. So shutdown is\n // where the queue has to be drained: without this, a login or logout in\n // the last moments of a process is simply lost, and `close()` resolves\n // while a `writeFile` is still in flight (which is how the CLI's own\n // teardown came to race its temp directory).\n await sessions?.flush?.()\n apns?.close()\n resolveClosed()\n },\n }\n}\n"],"mappings":"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"}
|