import { createHash, createHmac } from 'node:crypto'; import { BadRequestException, HttpException } from '@nestjs/common'; import type { LookupFunction } from 'node:net'; /** Keep the validated address pinned while honoring Node's all-address lookup contract. */ export function pinnedWebhookLookup(address: string, family: number): LookupFunction { return (_hostname, options, callback) => { if (options.all) callback(null, [{ address, family }]); else callback(null, address, family); }; } /** Validate calendar components before Date can normalize an impossible day. */ export function parseOpenApiDate(value: string): Date { const parts = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?(Z|[+-]\d{2}:\d{2}))?$/.exec( value, ); if (parts) { const year = Number(parts[1]); const month = Number(parts[2]); const day = Number(parts[3]); const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; const zone = parts[8]; const validZone = !zone || zone === 'Z' || (Number(zone.slice(1, 3)) < 24 && Number(zone.slice(4)) < 60); const date = new Date(value); if ( month >= 1 && month <= 12 && day >= 1 && day <= days[month - 1] && Number(parts[4] ?? 0) < 24 && Number(parts[5] ?? 0) < 60 && Number(parts[6] ?? 0) < 60 && validZone && Number.isFinite(date.getTime()) ) return date; } throw new BadRequestException({ code: 'TIME_RANGE_INVALID', message: '时间必须为有效的ISO8601日期或带时区时间' }); } export type OpenApiProblemResponse = { setHeader(name: string, value: string): void; status(code: number): { type(value: string): { send(body: unknown): void } }; }; export function sendOpenApiProblem( response: OpenApiProblemResponse, requestId: string, failure: { status: number; code: string; message: string }, ) { response.setHeader('X-Request-Id', requestId); response .status(failure.status) .type('application/problem+json') .send({ type: `https://cmpp-platform.local/problems/${failure.code.toLowerCase()}`, title: failure.status >= 500 ? 'Internal Server Error' : 'Request failed', status: failure.status, code: failure.code, detail: failure.message, requestId, }); } /** v1 compatibility: an absent parsed body hashes as {}, never try alternate hashes. */ export function openApiBodyHash(rawBody: Buffer | undefined, body: unknown) { return createHash('sha256') .update(rawBody ?? Buffer.from(JSON.stringify(body ?? {}))) .digest('hex'); } export function openApiSignature( secret: string, method: string, path: string, timestamp: string, nonce: string, bodyHash: string, ) { return createHmac('sha256', secret) .update([method.toUpperCase(), path.split('?')[0], timestamp, nonce, bodyHash].join('\n')) .digest('hex'); } export function publicOpenApiFailure(error: unknown) { if (!(error instanceof HttpException) || error.getStatus() >= 500) { return { status: 500, code: 'INTERNAL_ERROR', message: 'Internal server error' }; } const value = error.getResponse(); const object = typeof value === 'object' && value ? (value as Record) : {}; const message = object.message ?? value; return { status: error.getStatus(), code: String(object.code ?? 'REQUEST_FAILED'), message: Array.isArray(message) ? message.join(';') : String(message), }; } export function webhookJobId(deliveryId: string, attemptNo: number) { return `webhook-${createHash('sha256').update(deliveryId).digest('hex')}-${attemptNo}`; }