From 1f1a7d5a59a8698c772ddb7c5c6b47460c389fc6 Mon Sep 17 00:00:00 2001 From: jelisaveta-talbot <268838642+jelisaveta-talbot@users.noreply.github.com> Date: Mon, 11 May 2026 08:49:07 +0200 Subject: [PATCH] buyer: response archive (SQLite + filesystem) for off-chain audit trail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The on-chain Submit tx anchors sha256(canonicalize(receipt)) so the receipt's signature is provably tied to a specific Cardano block — but without persisting the receipt content + the actual response bytes nowhere can buyer or supplier produce the artefacts a dispute would need to argue about. This commit closes that gap on the buyer side. What's new - buyer/src/db/schema.ts (new): `responses` table keyed by escrow_ref, indexes on completed_at / capability_id / supplier_pkh. Stores metadata + receipt JSON + Ed25519 signature; large blobs (request envelope, response bytes) live on disk per-escrow. - buyer/src/db/archive.ts (new): ResponseArchive class — persistChat / persistTts / list / get / readRequest / readResponse. Filesystem layout under ARCHIVE_DIR: _/request.json (always JSON) _/response.{ext} (txt/json for chat, mp3/wav/… for TTS) Synchronous (better-sqlite3 + fs.writeFileSync) — volume is low and the simplicity is worth more than async overhead. - buyer/src/server.ts: - AppDeps.archive plumbed through createApp. - /v1/submit-prompt + /v1/submit-tts persist BEFORE returning to the SPA, using canonicalize() for chat (same bytes the supplier hashed; JSON.stringify produced different bytes → wrong sha256). Persist failures are logged but never abort the response — the on-chain receipt is still authoritative. - New read-only endpoints: GET /v1/responses?limit=… list metadata + parsed receipts GET /v1/responses/:ref single-row detail GET /v1/responses/:ref/request request artefact (application/json) GET /v1/responses/:ref/response response artefact (verbatim bytes) - URL path uses "_" (the dir-safe form) since "#" can't appear unencoded in URLs (fragment delimiter); server normalises back to canonical "#" for SQLite lookup. - 503 service_unavailable on /v1/responses* when boot has no archive (ARCHIVE_DIR unset or DB unopenable) — the rest of the buyer-app still works. - buyer/src/ui/pages/TaskHistory.tsx: Joins indexer escrows (chain state) + /v1/responses (archived artefacts) by escrow_ref. Each completed lifecycle gets an archive panel showing: - Request panel (above, chronological order): human-readable summary — first user message for chat, `text + voice + format + speed` for TTS, raw JSON fallback otherwise. - Response panel: assistant text inline for chat,