import * as crypto from 'node:crypto'; import { URL } from 'node:url'; type Request = { url: string; method: string; headers: Record; }; type PrivateKey = { privateKeyPem: string; keyId: string; }; export function createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record }) { const u = new URL(args.url); const digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`; const request: Request = { url: u.href, method: 'POST', headers: objectAssignWithLcKey({ 'Date': new Date().toUTCString(), 'Host': u.hostname, 'Content-Type': 'application/activity+json', 'Digest': digestHeader, }, args.additionalHeaders), }; const result = signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']); return { request, signingString: result.signingString, signature: result.signature, signatureHeader: result.signatureHeader, }; } export function createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record }) { const u = new URL(args.url); const request: Request = { url: u.href, method: 'GET', headers: objectAssignWithLcKey({ 'Accept': 'application/activity+json, application/ld+json', 'Date': new Date().toUTCString(), 'Host': new URL(args.url).hostname, }, args.additionalHeaders), }; const result = signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']); return { request, signingString: result.signingString, signature: result.signature, signatureHeader: result.signatureHeader, }; } function signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]) { const signingString = genSigningString(request, includeHeaders); const signature = crypto.sign('sha256', Buffer.from(signingString), key.privateKeyPem).toString('base64'); const signatureHeader = `keyId="${key.keyId}",algorithm="rsa-sha256",headers="${includeHeaders.join(' ')}",signature="${signature}"`; request.headers = objectAssignWithLcKey(request.headers, { Signature: signatureHeader, }); return { request, signingString, signature, signatureHeader, }; } function genSigningString(request: Request, includeHeaders: string[]) { request.headers = lcObjectKey(request.headers); const results: string[] = []; for (const key of includeHeaders.map(x => x.toLowerCase())) { if (key === '(request-target)') { results.push(`(request-target): ${request.method.toLowerCase()} ${new URL(request.url).pathname}`); } else { results.push(`${key}: ${request.headers[key]}`); } } return results.join('\n'); } function lcObjectKey(src: Record) { const dst: Record = {}; for (const key of Object.keys(src).filter(x => x != '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key]; return dst; } function objectAssignWithLcKey(a: Record, b: Record) { return Object.assign(lcObjectKey(a), lcObjectKey(b)); }