docs(specs): archive local verification remediation

This commit is contained in:
2026-08-23 21:21:22 +03:00
parent 51344d4988
commit 72444dbbb1
11 changed files with 441 additions and 4 deletions
+126
View File
@@ -0,0 +1,126 @@
#!/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."