run402-mcp
Version:
MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.
69 lines • 2.84 kB
JavaScript
/**
* RFC 8785 (JCS) canonical JSON utility — kept as a UX / diagnostic helper.
*
* **As of v1.34, the gateway is authoritative for the deploy manifest
* digest.** `POST /apply/v1/plans` returns the gateway-computed digest in
* the response, and operation idempotency keys on it (combined with
* `project_id`, `base_release_id`, and an optional client idempotency key).
* A drift between this implementation and the gateway's canonicalize
* therefore SHALL NOT silently break idempotency — retries still find the
* existing plan because the lookup uses the gateway-computed value.
*
* This module remains exported so callers can still produce a stable local
* digest for caching or debugging output. New code should treat the local
* digest as advisory.
*
* Spec for our value domain:
* - object keys sorted ASCII-ascending
* - arrays comma-separated, no whitespace
* - strings/numbers via JSON.stringify (matches RFC 8785 for ASCII paths,
* hex sha256, integer sizes, ASCII content_types)
*/
export function canonicalizeJson(value) {
if (value === null)
return "null";
if (typeof value === "boolean")
return value ? "true" : "false";
if (typeof value === "number")
return JSON.stringify(value);
if (typeof value === "string")
return JSON.stringify(value);
if (Array.isArray(value))
return "[" + value.map(canonicalizeJson).join(",") + "]";
if (typeof value === "object") {
const obj = value;
const keys = Object.keys(obj).sort();
const pairs = keys.map((k) => JSON.stringify(k) + ":" + canonicalizeJson(obj[k]));
return "{" + pairs.join(",") + "}";
}
throw new Error("canonicalizeJson: unsupported value type");
}
/**
* Build the canonical manifest object the gateway hashes: `{ files: [...] }`
* with entries sorted by path and only the four expected keys per entry.
* `content_type` defaults to `"application/octet-stream"` when absent.
*/
export function buildCanonicalManifest(entries) {
const files = entries
.map((e) => ({
path: e.path,
sha256: e.sha256,
size: e.size,
content_type: e.content_type ?? "application/octet-stream",
}))
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
return { files };
}
/**
* Compute the hex SHA-256 of the canonical JSON encoding of a manifest.
* Matches the gateway's `computeManifestDigest`.
*/
export async function computeManifestDigest(manifest) {
const canonical = canonicalizeJson(manifest);
const bytes = new TextEncoder().encode(canonical);
const hash = await crypto.subtle.digest("SHA-256", bytes);
return Array.from(new Uint8Array(hash))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
//# sourceMappingURL=canonicalize.js.map