sc-dev-deploy/lib/transport.js
2026-06-01 16:43:43 -05:00

109 lines
3.4 KiB
JavaScript

// 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
};