npm.io
0.1.1 • Published 2d ago

@reclaimprotocol/client

Licence
MIT
Version
0.1.1
Deps
6
Size
672 kB
Vulns
0
Weekly
26

@reclaimprotocol/client

Typed SDK for the Reclaim Builder API. One package gives you:

  • createReclaim(...) — a small, declarative client for the consumer happy path: create verification sessions, register callbacks, and receive + verify results.
  • ReclaimClient — a low-level typed HTTP client (call('OperationId', args)) for any operation in the spec.
  • Crypto helpers — ECIES decryption with the org's eth key, result-JWS signing/verification, JWKS discovery, and nested attestor-proof verification.

Works in Node, Deno, browsers, and edge runtimes (it uses WebCrypto + @noble, no native deps).

The model in one paragraph

Authentication is a single per-org secret (rorg_…, the orgSecret) — a bearer credential; no signatures, no per-call secrets. The org is identified by an Ethereum (secp256k1) public key, set as its keypair. When that keypair's canEncryptResult is enabled, verification results are encrypted with ECIES (secp256k1 + AES-256-GCM) to the org's eth public key, and you keep the matching eth private key (0x-hex — the form any wallet/ethers emits) to decrypt. Ethereum also signs the result JWS with ES256K, which you verify against the Builder's JWKS.

                          ┌─ one-time setup ─────────────────────────┐
  reclaim.callbacks.register({ callbackUrl })      ← where results go
  reclaim.keypair.set(orgId, { publicKey,          ← identity + (optional)
                       canEncryptResult: true })      encrypt to your key
                          └──────────────────────────────────────────┘
                          ┌─ per verification ───────────────────────┐
  reclaim.sessions.create(...) → open session.verificationUrl
        … end-user completes it …
  POST → your callbackUrl → reclaim.results.receive(body) → verified payload
                          └──────────────────────────────────────────┘

Install

npm install @reclaimprotocol/client

Quick start (consumer)

Configure once with your org secret. orgId below is your organization's id (the org the secret belongs to).

import { createReclaim } from '@reclaimprotocol/client'

const reclaim = createReclaim({
  orgSecret: process.env.RECLAIM_ORG_SECRET,   // rorg_… (issued by an org owner)
})
const orgId = process.env.RECLAIM_ORG_ID       // your org's id
1. Register a callback (one-time)

Tell the Builder where to POST verification results. This is one-time setup and can also be done from the dashboard or the agent — see Three ways to set up.

await reclaim.callbacks.register({ callbackUrl: 'https://my.app/reclaim/callback' })
2. Enable encryption (optional, one-time)

By default results are delivered as plaintext signed JWS (still verified). To have the Builder encrypt results so only you can read them, register your org's eth public key with canEncryptResult: true. Use any eth key (the 0x-hex form wallets/ethers emit), or mint one with the SDK:

import { generateEthKeypair } from '@reclaimprotocol/client'
// keep privateKeyHex secret — it decrypts results
const { privateKeyHex, publicKeyHex } = generateEthKeypair()
// (also doable from the dashboard, which can mint the keypair in-browser)
await reclaim.keypair.set(orgId, {
  publicKey: publicKeyHex,   // 0x04+128 hex (or pass publicKeyJwk)
  canEncryptResult: true,
})
3. Create a session (per verification)
const session = await reclaim.sessions.create({
  providerId: 'prov_…',
  context: { userId: 'abc' },
})
console.log('open this:', session.verificationUrl)   // hand to the end-user

It's a plain authenticated POST, so you don't need the SDK — curl works too (the org secret is the only auth):

curl -X POST "$BUILDER_URL/verifications/sessions" \
  -H "Authorization: Bearer $RECLAIM_ORG_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
        "providerId": "prov_…",
        "context": { "userId": "abc" }
      }'
# → 201 { "id": "…", "verificationUrl": "…", "status": "pending", "mode": "…", … }
4. Receive & verify the result

When the end-user finishes, the Builder POSTs to your callbackUrl. Pass the request body to results.receive. It decrypts (if encrypted) and verifies the signature + nested proofs, and throws if the result is invalid — so reaching the result branch means it's verified.

// in your callback HTTP handler:
const outcome = await reclaim.results.receive(requestBody, {
  credential: process.env.RECLAIM_ETH_PRIVATE_KEY,  // 0x-hex eth key; omit for plaintext
  expectedReclaimSessionId: session.id,
})
if (outcome.kind === 'result') {
  console.log('verified!', outcome.result.payload)
} else {
  console.log('progress event:', outcome.event)          // non-terminal notification
}
Three ways to set up

The one-time setup (registering the callback subscription + the encryption keypair) can be done however suits you — they all hit the same API:

  • This SDKreclaim.callbacks.register(...) + reclaim.keypair.set(...), as above (good for scripting/CI).
  • The dashboard — the org's web console has a callback-subscription form and a "Set encryption keypair" dialog that can generate the eth keypair in your browser and download the private key for you.
  • The agent (@reclaimprotocol/agent — the reclaim CLI / MCP server) — handy while building and testing a provider locally.

Whichever you use, at runtime your backend needs only the org secret (to authenticate) and the eth private key (to decrypt) — the results.receive(...) step is unchanged.


API reference

createReclaim(config)

Returns a Reclaim with four namespaces (sessions, callbacks, keypair, results) plus an api escape hatch.

ReclaimConfig field type required notes
orgSecret string yes The org secret rorg_…. Sent as Authorization: Bearer; identifies the org server-side.
baseUrl string no Defaults to the production Builder.
fetch typeof fetch no Custom fetch (tests, edge runtimes).
headers Record<string,string> no Extra headers merged into every request.

orgSecret is the only config. Everything else is passed where it's used: the orgId to keypair.set/get, and the eth private key (0x-hex) to results.receive/decrypt.

reclaim.sessions
reclaim.sessions.create({ providerId, version?, context, verificationClientUrl? })
  // → VerificationSession (has `id`, `verificationUrl`, `status`, `mode`, …)
  // verificationClientUrl defaults to the Builder's own /v/{sessionId} page.
reclaim.sessions.get(sessionId)         // → VerificationSession
reclaim.sessions.listEvents(sessionId)  // → VerificationEventRecord[]
reclaim.callbacks
reclaim.callbacks.register({ callbackUrl, events? })  // events defaults to the 3 terminal events
  // → CallbackSubscription. Org-scoped; no address/publicKey/secret.
reclaim.callbacks.list()                  // → CallbackSubscription[]
reclaim.callbacks.remove(subscriptionId)  //void

events is any subset of VerificationEvent. The terminal events (verification_success / verification_rejected / verification_error, exported as TERMINAL_EVENTS) deliver the signed result; non-terminal events deliver a plaintext progress notification.

reclaim.keypair (the org's single eth encryption key)
await reclaim.keypair.set(orgId, { publicKey, canEncryptResult?: true, label?: 'prod' })  // → OrgKeypair
await reclaim.keypair.get(orgId)  // → OrgKeypair | undefined (undefined ⇒ keyless / plaintext delivery)

publicKey is the org's eth uncompressed public key (0x04 + 128 hex — the form generateEthKeypair()/ethers produce); alternatively pass publicKeyJwk (a secp256k1 JWK). set stores the JWK + derived eth address. There is exactly one keypair per org; set replaces it (rotation). canEncryptResult toggles whether results are encrypted to it (default false ⇒ plaintext). Keep the matching eth private key (0x-hex) and pass it as the decryption key to results.receive/decrypt.

reclaim.results
reclaim.results.receive(body, opts?)        // → ReceivedDelivery  ← the 90% path
reclaim.results.decrypt(body, credential?)  //Jws   (ECIES or plaintext → signed JWS)
reclaim.results.verify(jws, opts?)          // → VerifyResultFullOutcome

receive branches on the delivery:

type ReceivedDelivery =
  | { kind: 'result'; result: VerifyResultFullOutcome }  // terminal: decrypted + verified
  | { kind: 'event'; event: VerificationEvent; eventData: Record<string, unknown> } // non-terminal: plaintext

opts (ReceiveOptions) is VerifyResultFullOptions (expectedReclaimSessionId nonce check, expectedAud, attestor allowlists, fetchImpl, now) plus an optional credential — the eth decryption key as a 0x-hex private key string (the key content, not a path or env name). All three methods are async; pass credential only when the delivery is encrypted.

Invalid results throw. receive and verify (and the underlying verifyResult / verifyResultFull) never return a failed verdict — they throw a ResultVerificationError carrying the failure reason and the full outcome (payload, reclaimSessionId, per-proof detail). So the result branch is always verified; wrap the call to inspect failures:

import { ResultVerificationError } from '@reclaimprotocol/client'
try {
  const { result } = await reclaim.results.receive(body, { credential })
  // result.payload is verified
} catch (err) {
  if (err instanceof ResultVerificationError) {
    console.error(err.reason, err.outcome)  // e.g. 'expired' / 'bad-signature' / 'invalid-proof'
  } else {
    throw err
  }
}

A non-terminal event delivery is not a result and never throws — it returns { kind: 'event', … }.

reclaim.api — escape hatch
// fully typed against the OpenAPI spec — for any operation the namespaces don't wrap
const { data } = await reclaim.api('GetVerificationSession', { params: { sessionId } })

Crypto & key helpers

// Eth encryption keys (consumer side)
generateEthKeypair(): { privateKeyHex, publicKeyHex, publicJwk, ethAddress }  // mint an org keypair
loadDecryptionKey(hex): DecryptionKey            // 0x-hex eth private key → raw scalar
uncompressedPubkeyToJwk(hex): Jwk                // 0x04+128 hex → secp256k1 JWK
pubkeyJwkToEthAddress(jwk): string               // secp256k1 JWK → 0x eth address

// ECIES (encryptForEthPublicKey is used by the first-party Verification Client; consumers only decrypt)
encryptForEthPublicKey(publicJwk, bytes): Promise<string>  // → ECIES ciphertext (base64url)
decryptCallback(decryptionKey, body): Promise<Jws>          // ECIES/plaintext → JWS

// Result JWS (eth ES256K)
signResultJws(ethPrivHex, payload, issuer): Jws                // VC-side
verifyResult(jws, opts): VerifyResultOutcome                   // offline signature/nonce check (throws if invalid)
verifyResultFull(jws, opts): Promise<VerifyResultFullOutcome>  // + JWKS discovery + attestor proofs (throws if invalid)
verifyJwsSignature(jws, signerPublicKey), decodeJwsPayload(jws)

// JWKS (signature-key discovery / publishing)
fetchJwks(url), jwksFromPrivateKey(ethPrivHex), selectKey(...), jwkToUncompressedPublicKey(jwk)

// Nested attestor-proof verification
verifyNestedProof(...): NestedProofOutcome
Eth keypair utils (low-level)

generateKeypair(), deriveFromPrivateKey(hex), addressFromPublicKey(...), plus the secp instance and addressFromUncompressedPubkey(...), are exported for the Verification Client and the agent (proof-owner signing). A consumer that enables encryption holds an eth private key (the same key family) to decrypt — see the encryption-key helpers above. canonicalJson (RFC 8785) is also exported (used by content-hash binding).

ReclaimClient (low-level)

import { ReclaimClient } from '@reclaimprotocol/client'
const client = new ReclaimClient({ baseUrl, token, fetch, headers })
const { data, response } = await client.call('GetVerificationSession', { params: { sessionId } })

Errors are thrown as ProblemError (RFC 9457). throwProblemError, DEFAULT_BASE_URL, USER_AGENT, encoding helpers (b64url, hexToBytes, …), and the generated components / operations types are also exported.

Security notes

  • The org secret is a bearer credential — treat it like a password; rotate it from the dashboard (POST /orgs/{orgId}/token, which invalidates the prior one). The server stores only an HMAC of it.
  • Encryption is ECIES over secp256k1 + AES-256-GCM (via eciesjs / @noble) — runs in browsers, Node, Deno, and edge runtimes, and uses the same eth key family as signing.
  • Signature verification stays secp256k1 (not routed through jose) so it works on WebCrypto-only runtimes that lack ES256K.
  • The callback delivery is unauthenticated transport — never trust the raw POST body; trust only the decrypted, signature-verified result that results.receive returns.

Keywords