import { createHash } from "node:crypto"; import { writeFile } from "node:fs/promises"; import type { SerializedRoute } from "@suaveplan/api-types/sandbox-bridge"; import { WASM_COMPILER_CODES, WasmCompilerError, } from "../error-codes/error-codes.js"; /** Inputs for {@link writeManifest}. */ export interface WriteManifestOptions { /** Absolute path the manifest will be written to. */ readonly manifestPath: string; /** Routes captured by the route-extract probe. */ readonly routes: readonly SerializedRoute[]; /** Pre-Javy bundled JS source — hashed into `sourceDigest`. */ readonly bundledJs: string; /** Upstream Javy version pinned by the wrapper that compiled. */ readonly javyVersion: string; /** This compiler's package version. */ readonly compilerVersion: string; /** RFC 3339 UTC timestamp; defaults to `new Date().toISOString()`. */ readonly compiledAt?: string; /** * Caller-supplied filesystem writer — defaults to * `node:fs/promises#writeFile`. Tests inject a recorder. */ readonly fsWrite?: (path: string, data: string) => Promise; } /** The full manifest record shape. */ export interface CompiledPluginManifest { readonly routes: readonly SerializedRoute[]; readonly abiVersion: string; readonly abiVariant: "wasi-stdio"; readonly javyVersion: string; readonly compilerVersion: string; readonly compiledAt: string; readonly sourceDigest: string; } /** The current compiler ABI version — semver string. */ export const COMPILER_ABI_VERSION = "1.0.0" as const; /** * Emit the `.wasm.json` sidecar. Refuses empty `routes` * with `WASM_COMPILER.MANIFEST_MISSING_ROUTES` — a routeless plugin * cannot route a single request through the bridge, so shipping the * artifact would be pointless. * * Returns the manifest record so the orchestrator can include it in * the compile result without re-reading from disk. */ export async function writeManifest( options: WriteManifestOptions ): Promise { if (options.routes.length === 0) { throw new WasmCompilerError( WASM_COMPILER_CODES.MANIFEST_MISSING_ROUTES, "error.wasm_compiler.manifest_missing_routes", { context: { manifestPath: options.manifestPath }, } ); } const sourceDigest = createHash("sha256") .update(options.bundledJs) .digest("hex"); const compiledAt = options.compiledAt ?? new Date().toISOString(); const manifest: CompiledPluginManifest = { routes: options.routes, abiVersion: COMPILER_ABI_VERSION, abiVariant: "wasi-stdio", javyVersion: options.javyVersion, compilerVersion: options.compilerVersion, compiledAt, sourceDigest, }; const writer = options.fsWrite ?? defaultWriter; await writer(options.manifestPath, JSON.stringify(manifest, null, 2)); return manifest; } async function defaultWriter(path: string, data: string): Promise { await writeFile(path, data, "utf8"); }