Files
deepseek-harness/packages/webhook/webhook-github/src/body.ts
T

70 lines
2.3 KiB
TypeScript

/** Bounded raw HTTP body intake for GitHub signature verification. */
import type { IncomingMessage } from 'node:http'
/** HTTP refusal whose message is safe to return without request data. */
export class WebhookHttpError extends Error {
override readonly name = 'WebhookHttpError'
constructor(
readonly status: 400 | 401 | 405 | 413 | 415 | 503,
message: string,
) {
super(message)
}
}
/** Parse a decimal Content-Length or reject an ambiguous header. */
function contentLength(request: IncomingMessage): number | undefined {
const value = request.headers['content-length']
if (value === undefined) return undefined
if (!/^(0|[1-9]\d*)$/.test(value)) {
throw new WebhookHttpError(400, 'invalid Content-Length')
}
const length = Number(value)
if (!Number.isSafeInteger(length)) throw new WebhookHttpError(413, 'request body is too large')
return length
}
/**
* Read one request body as exact, bounded UTF-8 text.
* @param request - incoming request before any parser consumes it.
* @param maxBodyBytes - positive byte ceiling.
* @returns the decoded body after EOF.
* @throws {WebhookHttpError} for invalid length, excessive bytes, invalid UTF-8, or an aborted stream.
*/
export async function readBoundedUtf8Body(
request: IncomingMessage,
maxBodyBytes: number,
): Promise<string> {
const declared = contentLength(request)
if (declared !== undefined && declared > maxBodyBytes) {
request.resume()
throw new WebhookHttpError(413, 'request body is too large')
}
const chunks: Buffer[] = []
let size = 0
try {
for await (const raw of request) {
const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw as string)
size += chunk.byteLength
if (size > maxBodyBytes) {
request.resume()
throw new WebhookHttpError(413, 'request body is too large')
}
chunks.push(chunk)
}
} catch (error: unknown) {
if (error instanceof WebhookHttpError) throw error
throw new WebhookHttpError(400, 'request body was aborted')
}
if (!request.complete) throw new WebhookHttpError(400, 'request body was aborted')
try {
return new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks, size))
} catch {
// TextDecoder is the only statement in the try; GitHub JSON must be valid UTF-8.
throw new WebhookHttpError(400, 'request body is not valid UTF-8')
}
}