#!/usr/bin/env bash set -euo pipefail script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" workspace_dir="$(cd -- "$script_dir/../.." && pwd)" backend_dir="$workspace_dir/backend" snapshot_path="$workspace_dir/frontend/src/shared/api/spec/openapi.json" if [[ ! -f "$snapshot_path" ]]; then echo "Missing committed OpenAPI snapshot: $snapshot_path" >&2 exit 1 fi node_major="$(node -p "process.versions.node.split('.')[0]")" if [[ "$node_major" != "22" ]]; then echo "Node 22 is required; found $(node --version)." >&2 exit 1 fi temporary_dir="$(mktemp -d "${TMPDIR:-/tmp}/wealth-ledger-runtime-openapi.XXXXXX")" backend_pid="" cleanup() { if [[ -n "$backend_pid" ]] && kill -0 "$backend_pid" 2>/dev/null; then kill "$backend_pid" 2>/dev/null || true wait "$backend_pid" 2>/dev/null || true fi rm -rf "$temporary_dir" } trap cleanup EXIT INT TERM port="$(node -e "const server = require('node:net').createServer(); server.listen(0, '127.0.0.1', () => { console.log(server.address().port); server.close(); });")" runtime_path="$temporary_dir/runtime.json" canonical_runtime_path="$temporary_dir/runtime.canonical.json" canonical_snapshot_path="$temporary_dir/snapshot.canonical.json" backend_log="$temporary_dir/backend.log" compile_log="$temporary_dir/compile.log" if ! ( cd "$backend_dir" ./node_modules/.bin/tsc --project tsconfig.build.json --outDir "$temporary_dir/dist" ) >"$compile_log" 2>&1; then echo "Temporary backend compilation failed:" >&2 sed -n '1,160p' "$compile_log" >&2 exit 1 fi ln -s "$backend_dir/node_modules" "$temporary_dir/dist/node_modules" ( cd "$backend_dir" NODE_ENV=development \ PORT="$port" \ DATABASE_URL="file:$temporary_dir/runtime.db" \ FRONTEND_ORIGIN="http://localhost:5173" \ JWT_SECRET="runtime-openapi-check-secret-at-least-32" \ TOKEN_ENCRYPTION_KEY="MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=" \ TBANK_GRPC_ENDPOINT="127.0.0.1:1" \ TBANK_RATE_LIMIT_PER_SECOND=5 \ LOG_LEVEL=error \ node "$temporary_dir/dist/src/main.js" ) >"$backend_log" 2>&1 & backend_pid="$!" for _ in $(seq 1 60); do if node -e "fetch(process.argv[1]).then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))" "http://127.0.0.1:$port/api/docs-json"; then break fi if ! kill -0 "$backend_pid" 2>/dev/null; then echo "Backend stopped before exposing /api/docs-json:" >&2 sed -n '1,160p' "$backend_log" >&2 exit 1 fi sleep 0.25 done if ! node -e 'fetch(process.argv[1]).then(async (response) => { if (!response.ok) throw new Error(`${response.status} ${response.statusText}`); require("node:fs").writeFileSync(process.argv[2], await response.text()); }).catch((error) => { console.error(`OpenAPI fetch failed: ${error.message}`); process.exit(1); })' "http://127.0.0.1:$port/api/docs-json" "$runtime_path"; then echo "Backend did not expose /api/docs-json within 15 seconds:" >&2 sed -n '1,160p' "$backend_log" >&2 exit 1 fi if node - "$runtime_path" "$snapshot_path" "$canonical_runtime_path" "$canonical_snapshot_path" <<'NODE' const { createHash } = require('node:crypto'); const { readFileSync, writeFileSync } = require('node:fs'); function canonicalize(value) { if (Array.isArray(value)) return value.map(canonicalize); if (value && typeof value === 'object') { return Object.fromEntries( Object.keys(value) .sort() .map((key) => [key, canonicalize(value[key])]), ); } return value; } const [runtimePath, snapshotPath, canonicalRuntimePath, canonicalSnapshotPath] = process.argv.slice(2); const documents = [runtimePath, snapshotPath].map((path) => canonicalize(JSON.parse(readFileSync(path, 'utf8'))), ); const serialized = documents.map((document) => `${JSON.stringify(document, null, 2)}\n`); writeFileSync(canonicalRuntimePath, serialized[0]); writeFileSync(canonicalSnapshotPath, serialized[1]); const hashes = serialized.map((value) => createHash('sha256').update(value).digest('hex'), ); console.log(`runtime SHA-256: ${hashes[0]}`); console.log(`snapshot SHA-256: ${hashes[1]}`); process.exitCode = hashes[0] === hashes[1] ? 0 : 1; NODE then comparison_status=0 else comparison_status=$? fi if [[ "$comparison_status" -ne 0 ]]; then echo "Runtime OpenAPI differs from the committed frontend snapshot:" >&2 diff -u "$canonical_snapshot_path" "$canonical_runtime_path" || true exit 1 fi echo "Runtime OpenAPI matches the committed frontend snapshot."