// Outbound signed peer requests. // // Build a canonical string, sign it with the peer's shared secret, and send // it via fetch. Body is JSON-serialized once and used for both the HTTP body // and the signature payload to keep client/server hash agreement trivial. const { buildCanonical, normalizeHost, sign, randomNonce } = require("./crypto"); const signedFetch = async ({ baseUrl, method, path, body, sourceEnvId, secret, requireTls }) => { if (requireTls && new URL(baseUrl).protocol !== "https:") { throw new Error(`peer requires TLS but base_url is not https: ${baseUrl}`); } const timestamp = String(Date.now()); const nonce = randomNonce().toString("hex"); const bodyStr = body ? JSON.stringify(body) : ""; const targetHost = normalizeHost(new URL(baseUrl).host); const canonical = buildCanonical({ timestamp: timestamp, nonce: nonce, method: method, path: path, targetHost: targetHost, body: bodyStr }); const signature = sign(secret, canonical); const url = baseUrl.replace(/\/+$/, "") + path; const headers = { "X-DD-Env-Id": sourceEnvId, "X-DD-Timestamp": timestamp, "X-DD-Nonce": nonce, "X-DD-Signature": signature }; if (bodyStr) { // Custom Content-Type so Saltcorn's express.json() middleware leaves the // request stream untouched; the server reads exact bytes for HMAC. headers["Content-Type"] = "application/vnd.dev-deploy+json"; } const init = { method: method, headers: headers }; if (bodyStr) { init.body = bodyStr; } const res = await fetch(url, init); const text = await res.text(); let parsed = null; if (text) { try { parsed = JSON.parse(text); } catch (e) { parsed = { raw: text }; } } return { status: res.status, ok: res.ok, body: parsed }; }; // Like signedFetch but returns the response body as a Buffer (raw bytes). // For binary endpoints like GET /dev-deploy/api/file/:uuid. const signedFetchBinary = async ({ baseUrl, method, path, body, sourceEnvId, secret, requireTls }) => { if (requireTls && new URL(baseUrl).protocol !== "https:") { throw new Error(`peer requires TLS but base_url is not https: ${baseUrl}`); } const timestamp = String(Date.now()); const nonce = randomNonce().toString("hex"); const bodyStr = body ? JSON.stringify(body) : ""; const targetHost = normalizeHost(new URL(baseUrl).host); const canonical = buildCanonical({ timestamp: timestamp, nonce: nonce, method: method, path: path, targetHost: targetHost, body: bodyStr }); const signature = sign(secret, canonical); const url = baseUrl.replace(/\/+$/, "") + path; const headers = { "X-DD-Env-Id": sourceEnvId, "X-DD-Timestamp": timestamp, "X-DD-Nonce": nonce, "X-DD-Signature": signature }; if (bodyStr) { headers["Content-Type"] = "application/vnd.dev-deploy+json"; } const init = { method: method, headers: headers }; if (bodyStr) { init.body = bodyStr; } const res = await fetch(url, init); const bytes = Buffer.from(await res.arrayBuffer()); return { status: res.status, ok: res.ok, bytes: bytes }; }; module.exports = { signedFetch, signedFetchBinary };