mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
314 lines
12 KiB
TypeScript
314 lines
12 KiB
TypeScript
/** Browser-session authentication for the Host Connection carrier. */
|
|
|
|
import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto'
|
|
import { credentialKey } from '@deepseek-ai/dsh-credentials'
|
|
import type { CredentialProvider, CredentialRecord } from '@deepseek-ai/dsh-credentials'
|
|
import type {
|
|
ConnectionIndexRequest,
|
|
ConnectionIndexResponse,
|
|
ConnectionTrustRequest,
|
|
} from './rpc.ts'
|
|
|
|
const AUTH_RECORD_KEY = credentialKey('client-connection', 'browser-session')
|
|
const DAY_MILLISECONDS = 24 * 60 * 60 * 1000
|
|
const SECRET_BYTES = 32
|
|
const TOKEN_QUERY = 'token'
|
|
const COOKIE_PREFIX = 'dsh-auth-'
|
|
const COOKIE_PAYLOAD_VERSION = 1
|
|
const STORED_SECRET_VERSION = 1
|
|
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]*$/
|
|
const PROCESS_LAUNCH_TOKENS = new WeakMap<object, string>()
|
|
|
|
interface StoredSecretPayload {
|
|
readonly version: typeof STORED_SECRET_VERSION
|
|
readonly secret: string
|
|
}
|
|
|
|
interface BrowserCookiePayload {
|
|
readonly version: typeof COOKIE_PAYLOAD_VERSION
|
|
readonly authority: string
|
|
readonly issuedAt: number
|
|
readonly expiresAt: number
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
}
|
|
|
|
function encodeBase64Url(value: Uint8Array): string {
|
|
return Buffer.from(value).toString('base64')
|
|
.replaceAll('+', '-')
|
|
.replaceAll('/', '_')
|
|
.replace(/=+$/u, '')
|
|
}
|
|
|
|
function decodeBase64Url(value: string): Buffer | undefined {
|
|
if (!BASE64URL_PATTERN.test(value) || value.length % 4 === 1) return undefined
|
|
const padding = '='.repeat((4 - value.length % 4) % 4)
|
|
const decoded = Buffer.from(value.replaceAll('-', '+').replaceAll('_', '/') + padding, 'base64')
|
|
return encodeBase64Url(decoded) === value ? decoded : undefined
|
|
}
|
|
|
|
function processLaunchToken(owner: object): string {
|
|
const existing = PROCESS_LAUNCH_TOKENS.get(owner)
|
|
if (existing !== undefined) return existing
|
|
const created = encodeBase64Url(randomBytes(SECRET_BYTES))
|
|
PROCESS_LAUNCH_TOKENS.set(owner, created)
|
|
return created
|
|
}
|
|
|
|
function header(
|
|
headers: ConnectionTrustRequest['headers'],
|
|
name: string,
|
|
): string | undefined {
|
|
if (headers instanceof Headers) return headers.get(name) ?? undefined
|
|
const value = headers[name]
|
|
return typeof value === 'string' ? value : undefined
|
|
}
|
|
|
|
/** Canonical request authority used as the cookie name and signed audience. */
|
|
function requestAuthority(headers: ConnectionTrustRequest['headers']): string | undefined {
|
|
const host = header(headers, 'host')
|
|
if (host === undefined) return undefined
|
|
try {
|
|
return new URL(`http://${host}`).host
|
|
} catch {
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
function canonicalSecret(value: unknown): Buffer | undefined {
|
|
if (typeof value !== 'string') return undefined
|
|
const decoded = decodeBase64Url(value)
|
|
if (decoded === undefined || decoded.byteLength !== SECRET_BYTES) return undefined
|
|
return decoded
|
|
}
|
|
|
|
function storedSecret(record: CredentialRecord | undefined): Buffer | undefined {
|
|
if (record === undefined) return undefined
|
|
if (record.kind !== 'grant' || !isRecord(record.payload)
|
|
|| record.payload.version !== STORED_SECRET_VERSION) {
|
|
throw new Error('client-connection: browser-session credential record has an unsupported format')
|
|
}
|
|
const secret = canonicalSecret(record.payload.secret)
|
|
if (secret === undefined) {
|
|
throw new Error('client-connection: browser-session credential record has an invalid secret')
|
|
}
|
|
return secret
|
|
}
|
|
|
|
function tokenMatches(actual: string, expected: string): boolean {
|
|
const actualBytes = Buffer.from(actual, 'utf8')
|
|
const expectedBytes = Buffer.from(expected, 'utf8')
|
|
return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual(actualBytes, expectedBytes)
|
|
}
|
|
|
|
function cookieName(authority: string): string {
|
|
return COOKIE_PREFIX + encodeBase64Url(createHash('sha256').update(authority).digest())
|
|
}
|
|
|
|
/** Read the exact generated cookie without implementing general Cookie decoding. */
|
|
function cookieValue(headerValue: string, name: string): string | undefined {
|
|
for (const segment of headerValue.split(';')) {
|
|
const at = segment.indexOf('=')
|
|
if (at === -1 || segment.slice(0, at).trim() !== name) continue
|
|
return segment.slice(at + 1).trim()
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
/** Serialize the fixed browser-session attributes; generated names and values are cookie-safe base64url. */
|
|
function sessionCookie(name: string, value: string, expiresAt: number, maxAgeSeconds: number): string {
|
|
return `${name}=${value}; Max-Age=${String(maxAgeSeconds)}; Path=/; Expires=${new Date(expiresAt).toUTCString()}; HttpOnly; SameSite=Strict`
|
|
}
|
|
|
|
function signature(secret: Buffer, body: string): Buffer {
|
|
return createHmac('sha256', secret).update(body).digest()
|
|
}
|
|
|
|
function encodeCookie(payload: BrowserCookiePayload, secret: Buffer): string {
|
|
const body = encodeBase64Url(Buffer.from(JSON.stringify(payload), 'utf8'))
|
|
return `v1.${body}.${encodeBase64Url(signature(secret, body))}`
|
|
}
|
|
|
|
function decodeCookie(value: string, secret: Buffer): BrowserCookiePayload | undefined {
|
|
const parts = value.split('.')
|
|
const [version, body, encodedSignature] = parts
|
|
if (parts.length !== 3 || version !== 'v1' || body === undefined || encodedSignature === undefined) {
|
|
return undefined
|
|
}
|
|
const actualSignature = decodeBase64Url(encodedSignature)
|
|
if (actualSignature === undefined) return undefined
|
|
const expectedSignature = signature(secret, body)
|
|
if (actualSignature.byteLength !== expectedSignature.byteLength
|
|
|| !timingSafeEqual(actualSignature, expectedSignature)) return undefined
|
|
let decoded: unknown
|
|
try {
|
|
const bodyBytes = decodeBase64Url(body)
|
|
if (bodyBytes === undefined) return undefined
|
|
decoded = JSON.parse(bodyBytes.toString('utf8'))
|
|
} catch {
|
|
return undefined
|
|
}
|
|
if (!isRecord(decoded)
|
|
|| decoded.version !== COOKIE_PAYLOAD_VERSION
|
|
|| typeof decoded.authority !== 'string'
|
|
|| !Number.isSafeInteger(decoded.issuedAt)
|
|
|| !Number.isSafeInteger(decoded.expiresAt)) return undefined
|
|
return decoded as unknown as BrowserCookiePayload
|
|
}
|
|
|
|
async function initializeSecret(credentials: CredentialProvider): Promise<Buffer> {
|
|
const generated: StoredSecretPayload = {
|
|
version: STORED_SECRET_VERSION,
|
|
secret: encodeBase64Url(randomBytes(SECRET_BYTES)),
|
|
}
|
|
const record = await credentials.modifyRecord(AUTH_RECORD_KEY, (current) => {
|
|
if (current !== undefined) {
|
|
storedSecret(current)
|
|
return Promise.resolve(undefined)
|
|
}
|
|
return Promise.resolve({ kind: 'grant', payload: generated })
|
|
})
|
|
const secret = storedSecret(record)
|
|
if (secret === undefined) {
|
|
throw new Error('client-connection: browser-session credential record was not created')
|
|
}
|
|
return secret
|
|
}
|
|
|
|
/**
|
|
* Process launch-token exchange and persistent signed-cookie verification.
|
|
* Connection loads the credential provider's signing secret during activation
|
|
* and retains it for synchronous request authentication.
|
|
*/
|
|
export class BrowserAuth {
|
|
private readonly launchToken: string
|
|
private readonly maxAgeMilliseconds: number
|
|
|
|
private constructor(
|
|
processOwner: object,
|
|
private readonly secret: Buffer,
|
|
maxAgeDays: number,
|
|
) {
|
|
this.launchToken = processLaunchToken(processOwner)
|
|
this.maxAgeMilliseconds = maxAgeDays * DAY_MILLISECONDS
|
|
if (!Number.isSafeInteger(this.maxAgeMilliseconds)
|
|
|| !Number.isSafeInteger(Date.now() + this.maxAgeMilliseconds)) {
|
|
throw new Error('client-connection: cookieMaxAgeDays exceeds the safe timestamp range')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize browser authentication and create its durable signing secret
|
|
* when this Harness home has none.
|
|
* @param processOwner - root application context retaining one token across Connection reloads.
|
|
* @param credentials - persistent credential provider for the Web profile.
|
|
* @param maxAgeDays - positive absolute browser-cookie lifetime in days.
|
|
* @returns initialized authentication owner with the process owner's launch token.
|
|
*/
|
|
static async create(
|
|
processOwner: object,
|
|
credentials: CredentialProvider,
|
|
maxAgeDays: number,
|
|
): Promise<BrowserAuth> {
|
|
return new BrowserAuth(processOwner, await initializeSecret(credentials), maxAgeDays)
|
|
}
|
|
|
|
/**
|
|
* Add this process's launch token to the ordinary application root URL.
|
|
* @param baseUrl - canonical browser origin without credentials.
|
|
* @returns root URL carrying the process token as its sole authentication input.
|
|
*/
|
|
authenticatedUrl(baseUrl: string): string {
|
|
const url = new URL(baseUrl)
|
|
url.pathname = '/'
|
|
url.search = ''
|
|
url.hash = ''
|
|
url.searchParams.set(TOKEN_QUERY, this.launchToken)
|
|
return url.href
|
|
}
|
|
|
|
/**
|
|
* Authenticate an index request. A valid root query token mints the cookie
|
|
* and redirects to clean `/`; a valid cookie lets the caller serve the
|
|
* index; every other request receives the same minimal 401 response.
|
|
* @param req - incoming root or configured-index request.
|
|
* @param res - response owned when this method returns false.
|
|
* @returns true only when the caller may serve index.html.
|
|
*/
|
|
authorizeIndex(req: ConnectionIndexRequest, res: ConnectionIndexResponse): boolean {
|
|
/* v8 ignore next -- node:http always supplies url on server requests. */
|
|
const url = new URL(req.url ?? '/', 'http://dsh.invalid')
|
|
const tokens = url.searchParams.getAll(TOKEN_QUERY)
|
|
if (tokens.length > 0) {
|
|
const authority = requestAuthority(req.headers)
|
|
if (req.method === 'GET' && url.pathname === '/' && tokens.length === 1
|
|
&& authority !== undefined && tokenMatches(tokens.join(''), this.launchToken)) {
|
|
const issuedAt = Date.now()
|
|
const expiresAt = issuedAt + this.maxAgeMilliseconds
|
|
const value = encodeCookie({
|
|
version: COOKIE_PAYLOAD_VERSION,
|
|
authority,
|
|
issuedAt,
|
|
expiresAt,
|
|
}, this.secret)
|
|
res.writeHead(303, {
|
|
'cache-control': 'no-store',
|
|
'location': '/',
|
|
'referrer-policy': 'no-referrer',
|
|
'set-cookie': sessionCookie(
|
|
cookieName(authority), value, expiresAt, Math.floor(this.maxAgeMilliseconds / 1000),
|
|
),
|
|
})
|
|
res.end()
|
|
return false
|
|
}
|
|
if (req.method === 'GET' && url.pathname === '/' && this.isAuthenticated(req)) {
|
|
res.writeHead(303, {
|
|
'cache-control': 'no-store',
|
|
'location': '/',
|
|
'referrer-policy': 'no-referrer',
|
|
})
|
|
res.end()
|
|
return false
|
|
}
|
|
this.writeUnauthorized(req, res)
|
|
return false
|
|
}
|
|
if (this.isAuthenticated(req)) return true
|
|
this.writeUnauthorized(req, res)
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Verify the authority-bound browser cookie on a Host request.
|
|
* @param request - request headers carrying Host and Cookie.
|
|
* @returns true only for an unexpired cookie signed by this activation's loaded secret.
|
|
*/
|
|
isAuthenticated(request: ConnectionTrustRequest): boolean {
|
|
const authority = requestAuthority(request.headers)
|
|
const rawCookie = header(request.headers, 'cookie')
|
|
if (authority === undefined || rawCookie === undefined) return false
|
|
const value = cookieValue(rawCookie, cookieName(authority))
|
|
if (value === undefined) return false
|
|
const payload = decodeCookie(value, this.secret)
|
|
if (payload === undefined || payload.authority !== authority) return false
|
|
const now = Date.now()
|
|
return payload.issuedAt <= now
|
|
&& payload.expiresAt > now
|
|
&& payload.expiresAt > payload.issuedAt
|
|
&& payload.expiresAt - payload.issuedAt <= this.maxAgeMilliseconds
|
|
}
|
|
|
|
private writeUnauthorized(req: ConnectionIndexRequest, res: ConnectionIndexResponse): void {
|
|
res.writeHead(401, {
|
|
'cache-control': 'no-store',
|
|
'content-type': 'text/plain; charset=utf-8',
|
|
})
|
|
res.end(req.method === 'HEAD'
|
|
? undefined
|
|
: 'dsh web authentication required; reopen the URL printed by dsh web.\n')
|
|
}
|
|
}
|