// Mint DSH's own browser-session cookie for a given authority. // // Format (dsh-client-connection BrowserAuth): cookie name `dsh-auth-` + // base64url(sha256(authority)), value `v1..` // with json {version:1, authority, issuedAt, expiresAt}. The 32-byte secret is the // credential record `client-connection/browser-session` (kind grant, payload.secret). // // The target DSH accepts proxied requests only when Host matches the cookie's authority, // so the proxy rewrites Host to the loopback authority it forwards to and attaches a // cookie minted for exactly that authority. The browser never sees this cookie. import crypto from 'node:crypto' const DAY = 86400000 const b64u = (b) => Buffer.from(b).toString('base64url') export function cookieName(authority) { return 'dsh-auth-' + b64u(crypto.createHash('sha256').update(authority).digest()) } export function mintCookie(authority, secretB64u, maxAgeDays = 1) { const secret = Buffer.from(String(secretB64u || ''), 'base64url') if (secret.length !== 32) return null const issuedAt = Date.now() const body = b64u(JSON.stringify({ version: 1, authority, issuedAt, expiresAt: issuedAt + maxAgeDays * DAY })) const sig = b64u(crypto.createHmac('sha256', secret).update(body).digest()) return `${cookieName(authority)}=v1.${body}.${sig}` } /** Read the browser-session signing secret through ctx.credentials (no file access). */ export async function readBrowserSecret(ctx) { let creds try { creds = typeof ctx.get === 'function' ? ctx.get('credentials') : ctx.credentials } catch { creds = undefined } if (!creds || typeof creds.readRecord !== 'function') return '' try { const rec = await creds.readRecord('client-connection/browser-session') return rec?.payload?.secret || '' } catch { return '' } } /** Small cache: re-mint a cookie per authority once an hour. */ export class CookieJar { constructor(ctx) { this.ctx = ctx this.cache = new Map() this.secret = '' } async get(authority) { const hit = this.cache.get(authority) if (hit && hit.at > Date.now() - 3600000) return hit.cookie if (!this.secret) this.secret = await readBrowserSecret(this.ctx) const cookie = mintCookie(authority, this.secret, 1) if (cookie) this.cache.set(authority, { cookie, at: Date.now() }) return cookie } }