import { createReclaim, decryptCallback, type DecryptionKey, } from '@reclaimprotocol/client' import assert from 'node:assert' import { apiUrl, orgSecret } from '../../../consts.ts' import { defineTool, type RegisteredTool } from '../../server.ts' import { resolveDecryptionKey } from '../authenticate/decrypt-key.ts' /** Statuses that can carry a submitted (terminal) result. `expired`/`pending` * never have one. */ const TERMINAL = new Set(['success', 'rejected', 'error']) /** * Resolve the org secret used to authenticate verification reads. Auth is a * single per-org bearer secret (`rorg_…`), not an eth signature — read it from * `RECLAIM_ORG_SECRET`. */ function resolveOrgSecret(): string { const secret = orgSecret()?.trim() assert( secret, new Error( 'RECLAIM_ORG_SECRET is not set. Verification reads authenticate with ' + 'the org secret (rorg_…) — issue one from the dashboard ' + '(POST /orgs/{orgId}/token) and export it as RECLAIM_ORG_SECRET.', ), ) return secret } /** * `get_verification_result` — check a session's status and, once a result has * been submitted, pull the stored payload(s) and verify locally. Auth is the * org token; delivery is org-scoped. Each stored payload is either the * plaintext signed JWS (when the org's encryption is off) or an ECIES * ciphertext that we decrypt here with the optional eth key * (`RECLAIM_DECRYPT_KEY` or the `reclaim-decrypt.key` hex file). The Builder * only ever relays the payload — decryption, when needed, happens here. */ export function verificationResultTool(): RegisteredTool { return defineTool<{ sessionId: string }>( { name: 'get_verification_result', description: 'Reads a verification session and locally checks each submitted ' + 'result. Requires RECLAIM_ORG_SECRET. Encrypted ECIES deliveries ' + 'also require the matching optional eth key ' + '(RECLAIM_DECRYPT_KEY or reclaim-decrypt.key); plaintext deliveries ' + 'are read directly. Returns `ready: false` until the session is ' + 'terminal and at least one payload is readable. When `ready: true`, ' + 'act only on result entries where `valid` is true; `ready` means a ' + 'payload was read, not that verification succeeded. Use ' + 'list_verification_events to diagnose an unfinished session.', inputSchema: { type: 'object', required: ['sessionId'], properties: { sessionId: { type: 'string', description: 'The verification session id from create_verification_session.', }, }, }, }, async(args) => { const secret = resolveOrgSecret() // Optional eth decryption key — absent ⇒ plaintext-only. const decryptionKey: DecryptionKey | undefined = await resolveDecryptionKey() const reclaim = createReclaim({ baseUrl: apiUrl(), orgSecret: secret, }) const sessionRes = await reclaim.api('GetVerificationSession', { params: { sessionId: args.sessionId }, }) const status = sessionRes.data.status if(!TERMINAL.has(status)) { return { sessionId: args.sessionId, status, ready: false } } const resultRes = await reclaim.api('GetVerificationResult', { params: { sessionId: args.sessionId }, }) const { items } = resultRes.data if(!items.length) { return { sessionId: args.sessionId, status, ready: false, note: 'Session is terminal but no result has been submitted yet.', } } // Each item's `payload` is an ECIES ciphertext (encrypted to the org's // credential) or the plaintext signed JWS, as raw text. `decryptCallback` // branches on the body shape; an encrypted delivery with no key throws // a clear error, which we surface per-subscription rather than skip. const results: unknown[] = [] const undecryptable: string[] = [] for(const item of items) { let jws try { jws = await decryptCallback(decryptionKey, item.payload) } catch(err) { undecryptable.push(item.subscriptionId) void err continue } const outcome = await reclaim.results.verify(jws, { expectedReclaimSessionId: args.sessionId, }) results.push({ subscriptionId: item.subscriptionId, valid: outcome.valid, reason: outcome.reason, payload: outcome.payload, proofs: outcome.proofs, }) } if(!results.length) { return { sessionId: args.sessionId, status, ready: false, note: 'A result was stored, but no payload could be read — it is ' + 'corrupt, or encrypted (ECIES) to a key this consumer does not ' + 'hold. Set RECLAIM_DECRYPT_KEY (or reclaim-decrypt.key) to the ' + 'eth private key matching the org keypair.', } } return { sessionId: args.sessionId, status, ready: true, results, ...(undecryptable.length ? { undecryptable } : {}), } }, ) }