← Files OpsTruthARCHIVED FILE

src/utils.js

4.53 KB · Oct 2, 2026 · 00:31 UTC

↓ Download file

const encoder = new TextEncoder();
const OPSTRUTH_RECEIPT_DOMAIN = "opstruth.evidence-receipt.v2\0";

export function stableJson(value) {
  if (value === null || typeof value !== "object") return JSON.stringify(value);
  if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
  const entries = Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`);
  return `{${entries.join(",")}}`;
}

export async function sha256(value) {
  const bytes = typeof value === "string" ? encoder.encode(value) : value;
  const digest = await crypto.subtle.digest("SHA-256", bytes);
  return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
}

export function unique(values) {
  return [...new Set(values.filter(Boolean))];
}

export function bounded(values, limit = 100) {
  return values.slice(0, limit);
}

export function asErrorMessage(error) {
  return error instanceof Error ? error.message : String(error);
}

export function textResult(structuredContent, text) {
  return {
    structuredContent,
    content: [{ type: "text", text }],
  };
}

function decodeBase64(value) {
  const binary = atob(value);
  return Uint8Array.from(binary, (character) => character.charCodeAt(0));
}

function encodeBase64(value) {
  let binary = "";
  for (const byte of new Uint8Array(value)) binary += String.fromCharCode(byte);
  return btoa(binary);
}

function pemBytes(value, label) {
  const body = String(value || "")
    .replace(`-----BEGIN ${label}-----`, "")
    .replace(`-----END ${label}-----`, "")
    .replace(/\s+/g, "");
  if (!body || !/^[A-Za-z0-9+/]+={0,2}$/.test(body)) throw new Error("signing_key_invalid");
  return decodeBase64(body);
}

export async function signingMetadata(env = {}) {
  const privateKeyPem = env?.OPSTRUTH_RECEIPT_PRIVATE_KEY_PKCS8;
  const publicKeyPem = env?.OPSTRUTH_RECEIPT_PUBLIC_KEY_SPKI;
  if (!privateKeyPem || !publicKeyPem) {
    return { configured: false, status: "not_configured", algorithm: "Ed25519", signerFingerprint: null, publicKeyPem: null };
  }
  try {
    const publicDer = pemBytes(publicKeyPem, "PUBLIC KEY");
    await crypto.subtle.importKey("pkcs8", pemBytes(privateKeyPem, "PRIVATE KEY"), { name: "Ed25519" }, false, ["sign"]);
    await crypto.subtle.importKey("spki", publicDer, { name: "Ed25519" }, false, ["verify"]);
    return {
      configured: true,
      status: "configured",
      algorithm: "Ed25519",
      signerFingerprint: `sha256:${await sha256(publicDer)}`,
      publicKeyPem: String(publicKeyPem).trim(),
    };
  } catch {
    return { configured: false, status: "invalid_configuration", algorithm: "Ed25519", signerFingerprint: null, publicKeyPem: null };
  }
}

export async function evidenceReceipt(report, env = {}) {
  const payload = { ...report };
  delete payload.receipt;
  const canonical = `${OPSTRUTH_RECEIPT_DOMAIN}${stableJson(payload)}`;
  const digest = await sha256(canonical);
  const metadata = await signingMetadata(env);
  const base = {
    schema: "opstruth.evidence-receipt",
    schemaVersion: "2.0.0",
    digestAlgorithm: "SHA-256",
    payloadDigest: digest,
    changedState: false,
  };
  if (!metadata.configured) return { ...base, signed: false, signingStatus: metadata.status };
  try {
    const key = await crypto.subtle.importKey(
      "pkcs8",
      pemBytes(env.OPSTRUTH_RECEIPT_PRIVATE_KEY_PKCS8, "PRIVATE KEY"),
      { name: "Ed25519" },
      false,
      ["sign"],
    );
    const signature = await crypto.subtle.sign({ name: "Ed25519" }, key, encoder.encode(canonical));
    return {
      ...base,
      signed: true,
      signingStatus: "signed",
      algorithm: "Ed25519",
      signerFingerprint: metadata.signerFingerprint,
      publicKeyPem: metadata.publicKeyPem,
      signatureBase64: encodeBase64(signature),
    };
  } catch {
    return { ...base, signed: false, signingStatus: "signing_failed" };
  }
}

export { OPSTRUTH_RECEIPT_DOMAIN, pemBytes };

export function jsonResponse(value, status = 200, headers = {}) {
  return new Response(JSON.stringify(value), {
    status,
    headers: {
      "content-type": "application/json; charset=utf-8",
      "cache-control": "no-store",
      ...headers,
    },
  });
}

export function htmlResponse(value, status = 200) {
  return new Response(value, {
    status,
    headers: {
      "content-type": "text/html; charset=utf-8",
      "content-security-policy": "default-src 'self'; style-src 'unsafe-inline'; script-src 'none'; frame-ancestors 'none'; base-uri 'none'",
      "x-content-type-options": "nosniff",
      "referrer-policy": "no-referrer",
    },
  });
}

SHA-256: 8c5181f9bb9336d06664bf091d02a3c1bf00a30f30e1ba5bbacbff7d9c5f3b9e