import { randomBytes, randomUUID, timingSafeEqual } from 'node:crypto' import { chmod, mkdir, rm } from 'node:fs/promises' import { createServer } from 'node:http' import { dirname } from 'node:path' const MAX_BODY_BYTES = 4 * 1024 function exactObject(value, keys, name) { if (value === null || typeof value !== 'object' || Array.isArray(value)) { throw new Error(`${name} must be an object`) } const actual = Object.keys(value).sort() const expected = [...keys].sort() if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) { throw new Error(`${name} fields are invalid`) } return value } async function jsonBody(request) { const chunks = [] let size = 0 for await (const chunk of request) { size += chunk.byteLength if (size > MAX_BODY_BYTES) throw new Error('lifecycle request body is too large') chunks.push(chunk) } if (size === 0) throw new Error('lifecycle request body is required') return JSON.parse(Buffer.concat(chunks).toString('utf8')) } function send(response, status, value) { response.writeHead(status, { 'content-type': 'application/json; charset=utf-8' }) response.end(`${JSON.stringify(value)}\n`) } function matchesSecret(actual, expected) { if (typeof actual !== 'string' || typeof expected !== 'string') return false const left = Buffer.from(actual) const right = Buffer.from(expected) return left.byteLength === right.byteLength && timingSafeEqual(left, right) } export class DshLifecycleBroker { constructor({ shouldTerminate = async () => false, report = async () => {} } = {}) { this.shouldTerminate = shouldTerminate this.report = report this.launch = null this.session = null this.published = false this.shuttingDown = false } record(message, fields = {}) { return Promise.resolve().then(() => this.report(message, fields)).catch(() => {}) } prepareLaunch(componentId) { if (componentId !== 'dsh-runtime') return Object.freeze({ environment: Object.freeze({}), release: () => {} }) const launch = Object.freeze({ id: randomUUID(), token: randomBytes(32).toString('base64url') }) this.launch = launch this.session = null this.published = false void this.record('dsh.launch.authorized') return Object.freeze({ environment: Object.freeze({ DSH_PLATFORM_LAUNCH_TOKEN: launch.token }), release: () => this.release(launch.id), }) } release(launchId) { if (this.launch?.id !== launchId) return this.launch = null this.session = null void this.record('dsh.launch.released') } claim(launchToken) { if (this.launch === null || this.session !== null || !matchesSecret(launchToken, this.launch.token)) { throw Object.assign(new Error('DSH launch token is invalid or already consumed'), { statusCode: 409 }) } const sessionId = randomUUID() this.session = Object.freeze({ id: sessionId, launchId: this.launch.id, ready: false, verified: false }) this.launch = Object.freeze({ ...this.launch, token: null }) void this.record('dsh.launch.claimed') return Object.freeze({ sessionId }) } ready(sessionId, readyUrl = null) { if (this.session === null || !matchesSecret(sessionId, this.session.id)) { throw Object.assign(new Error('DSH lifecycle session is invalid'), { statusCode: 409 }) } if (readyUrl !== null) { let parsed try { parsed = new URL(readyUrl) } catch { throw Object.assign(new Error('DSH Web readiness URL is invalid'), { statusCode: 400 }) } if (parsed.protocol !== 'http:' || parsed.hostname !== '127.0.0.1' || parsed.pathname !== '/' || parsed.username !== '' || parsed.password !== '' || parsed.hash !== '' || parsed.searchParams.getAll('token').length !== 1) { throw Object.assign(new Error('DSH Web readiness URL is invalid'), { statusCode: 400 }) } } if (!this.session.ready) { this.session = Object.freeze({ ...this.session, ready: true, readyUrl }) void this.record('dsh.launch.ready') } return Object.freeze({ ready: true }) } verify() { if (this.session?.ready !== true) throw new Error('DSH lifecycle is not ready for verification') if (!this.session.verified) { this.session = Object.freeze({ ...this.session, verified: true }) return this.record('dsh.launch.verified') } } publish() { if (this.session?.verified !== true) throw new Error('DSH lifecycle is not verified for publication') if (!this.published) { this.published = true return this.record('platform.public-ready') } } readiness() { const ready = this.session?.ready === true const verified = this.session?.verified === true return Object.freeze({ generation: this.session?.id ?? null, publicReady: ready && verified && this.published, ready, readyUrl: ready ? this.session.readyUrl ?? null : null, verified, }) } async signal(sessionId, signal) { if (signal !== 'SIGTERM') throw Object.assign(new Error('lifecycle signal is invalid'), { statusCode: 400 }) if (this.session === null || !matchesSecret(sessionId, this.session.id)) { throw Object.assign(new Error('DSH lifecycle session is invalid'), { statusCode: 409 }) } const disposition = this.session.ready !== true || this.shuttingDown || await this.shouldTerminate() ? 'terminate' : 'request-restart' await this.record('dsh.signal.disposition', { disposition, signal }) return Object.freeze({ disposition }) } beginShutdown() { this.published = false this.shuttingDown = true } } export function createDshLifecycleServer(broker) { return createServer(async (request, response) => { let pathname = 'invalid-url' try { pathname = new URL(request.url ?? '/', 'http://dsh-lifecycle.internal').pathname if (request.method !== 'POST') { send(response, 404, { error: 'not found' }) return } if (pathname === '/v1/runtime/claim') { const body = exactObject(await jsonBody(request), ['launchToken'], 'claim request') send(response, 200, broker.claim(body.launchToken)) } else if (pathname === '/v1/runtime/ready') { const value = await jsonBody(request) const body = Object.hasOwn(value, 'readyUrl') ? exactObject(value, ['readyUrl', 'sessionId'], 'ready request') : exactObject(value, ['sessionId'], 'ready request') send(response, 200, broker.ready(body.sessionId, body.readyUrl ?? null)) } else if (pathname === '/v1/runtime/readiness') { exactObject(await jsonBody(request), [], 'readiness request') send(response, 200, broker.readiness()) } else if (pathname === '/v1/runtime/signal') { const body = exactObject(await jsonBody(request), ['sessionId', 'signal'], 'signal request') send(response, 200, await broker.signal(body.sessionId, body.signal)) } else send(response, 404, { error: 'not found' }) } catch (error) { await broker.record('dsh.lifecycle-request.failed', { error, method: request.method ?? null, pathname, level: error?.statusCode === 409 ? 'warning' : 'error', }) send(response, Number.isInteger(error?.statusCode) ? error.statusCode : 400, { error: error instanceof Error ? error.message : 'invalid lifecycle request', }) } }) } export async function listenDshLifecycle(server, socketPath) { await mkdir(dirname(socketPath), { recursive: true }) await rm(socketPath, { force: true }) await new Promise((resolve, reject) => { server.once('error', reject) server.listen(socketPath, resolve) }) await chmod(socketPath, 0o600) }