Centralize session scope, chat projections and typed task bridges. Keep historical interaction replies authoritative and publish terminal state to subscribers. Validate Server contracts before replacing generated artifacts.
58 lines
3.3 KiB
TypeScript
58 lines
3.3 KiB
TypeScript
import { createHash, randomUUID } from "node:crypto";
|
|
import { readFile, writeFile, rename, rm } from "node:fs/promises";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import openapiTS, { astToString, COMMENT_HEADER } from "openapi-typescript";
|
|
|
|
const local = new URL("../openapi/openapi.json", import.meta.url);
|
|
const manifest = new URL("../openapi/server-manifest.json", import.meta.url);
|
|
const generated = new URL("../src/generated/server-api.d.ts", import.meta.url);
|
|
const defaultUpstream = new URL("../../next-tjwater-server/contracts/server-v1.openapi.json", import.meta.url);
|
|
|
|
export function assertContractDigest(raw: string, expected: string) {
|
|
if (createHash("sha256").update(raw).digest("hex") !== expected) throw new Error("Server contract digest differs; run bun run sync:openapi");
|
|
}
|
|
|
|
async function main() {
|
|
const sync = process.argv.includes("--sync");
|
|
const upstreamIndex = process.argv.indexOf("--upstream");
|
|
if (upstreamIndex >= 0 && !process.argv[upstreamIndex + 1]) throw new Error("--upstream requires an OpenAPI file");
|
|
const upstream = upstreamIndex >= 0 ? resolve(process.argv[upstreamIndex + 1]) : defaultUpstream;
|
|
let upstreamRaw: string | undefined;
|
|
try { upstreamRaw = await readFile(upstream, "utf8"); }
|
|
catch (error) {
|
|
if (sync || upstreamIndex >= 0 || (error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
}
|
|
if (sync) {
|
|
const producerManifest = JSON.parse(await readFile(resolve(
|
|
dirname(upstream instanceof URL ? fileURLToPath(upstream) : upstream), "manifest.json"
|
|
), "utf8"));
|
|
assertContractDigest(upstreamRaw!, producerManifest.contracts.server.sha256);
|
|
// Complete validation and generation before replacing any checked-in artifact.
|
|
const types = COMMENT_HEADER + astToString(await openapiTS(JSON.parse(upstreamRaw!)));
|
|
const artifacts = [
|
|
{ target: local, content: upstreamRaw! },
|
|
{ target: manifest, content: JSON.stringify(producerManifest, null, 2) + "\n" },
|
|
{ target: generated, content: types },
|
|
].map((artifact) => ({ ...artifact, temporary: new URL(`${artifact.target.href}.${randomUUID()}.tmp`) }));
|
|
try {
|
|
for (const artifact of artifacts) await writeFile(artifact.temporary, artifact.content);
|
|
for (const artifact of artifacts) await rename(artifact.temporary, artifact.target);
|
|
} finally {
|
|
await Promise.all(artifacts.map((artifact) => rm(artifact.temporary, { force: true })));
|
|
}
|
|
console.log("Server OpenAPI snapshot, producer digest and types synchronized.");
|
|
return;
|
|
}
|
|
const raw = await readFile(local, "utf8");
|
|
const receipt = JSON.parse(await readFile(manifest, "utf8"));
|
|
assertContractDigest(raw, receipt.contracts.server.sha256);
|
|
if (upstreamRaw !== undefined && upstreamRaw !== raw) throw new Error("Upstream Server contract changed; run bun run sync:openapi");
|
|
if (await readFile(generated, "utf8") !== COMMENT_HEADER + astToString(await openapiTS(JSON.parse(raw)))) throw new Error("Generated Server types differ; run bun run generate:openapi");
|
|
console.log(upstreamRaw === undefined
|
|
? "Local Server contract digest and types checked; upstream unavailable in this checkout."
|
|
: "Upstream Server contract, digest and generated types checked.");
|
|
}
|
|
|
|
if (import.meta.main) await main();
|