import { createHash, createHmac } from "node:crypto"; export type VolcengineSignatureInput = { method: "POST" | "GET"; endpoint: string; query: Record; body: string; accessKeyId: string; secretAccessKey: string; region: string; service: string; date?: Date; }; export type SignedVolcengineRequest = { url: string; headers: Record; canonicalRequest: string; stringToSign: string; }; export function sha256Hex(value: string | Buffer): string { return createHash("sha256").update(value).digest("hex"); } export function signVolcengineRequest(input: VolcengineSignatureInput): SignedVolcengineRequest { const endpoint = new URL(input.endpoint); const date = input.date || new Date(); const xDate = formatAmzDate(date); const shortDate = xDate.slice(0, 8); const bodyHash = sha256Hex(input.body); const canonicalQuery = canonicalizeQuery(input.query); const canonicalHeaders = [ `content-type:application/json`, `host:${endpoint.host}`, `x-content-sha256:${bodyHash}`, `x-date:${xDate}` ].join("\n"); const signedHeaders = "content-type;host;x-content-sha256;x-date"; const canonicalRequest = [ input.method, endpoint.pathname || "/", canonicalQuery, `${canonicalHeaders}\n`, signedHeaders, bodyHash ].join("\n"); const credentialScope = `${shortDate}/${input.region}/${input.service}/request`; const stringToSign = [ "HMAC-SHA256", xDate, credentialScope, sha256Hex(canonicalRequest) ].join("\n"); const signingKey = getSigningKey(input.secretAccessKey, shortDate, input.region, input.service); const signature = hmacHex(signingKey, stringToSign); const authorization = [ `HMAC-SHA256 Credential=${input.accessKeyId}/${credentialScope}`, `SignedHeaders=${signedHeaders}`, `Signature=${signature}` ].join(", "); endpoint.search = canonicalQuery; return { url: endpoint.toString(), headers: { "Authorization": authorization, "Content-Type": "application/json", "Host": endpoint.host, "X-Content-Sha256": bodyHash, "X-Date": xDate }, canonicalRequest, stringToSign }; } function canonicalizeQuery(query: Record): string { return Object.entries(query) .sort(([a], [b]) => a.localeCompare(b)) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join("&"); } function formatAmzDate(date: Date): string { return date.toISOString().replace(/[:-]|\.\d{3}/g, ""); } function hmac(key: string | Buffer, value: string): Buffer { return createHmac("sha256", key).update(value).digest(); } function hmacHex(key: string | Buffer, value: string): string { return createHmac("sha256", key).update(value).digest("hex"); } function getSigningKey(secretAccessKey: string, date: string, region: string, service: string): Buffer { const kDate = hmac(secretAccessKey, date); const kRegion = hmac(kDate, region); const kService = hmac(kRegion, service); return hmac(kService, "request"); }