@reclaimprotocol/client 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +634 -42
  2. package/lib/api-client.d.ts +31 -0
  3. package/lib/api-client.js +112 -0
  4. package/lib/api-consts.d.ts +4 -0
  5. package/lib/api-consts.js +4 -0
  6. package/lib/api-errors.d.ts +13 -0
  7. package/lib/api-errors.js +30 -0
  8. package/lib/api.d.ts +7 -0
  9. package/lib/api.js +3 -0
  10. package/lib/attestor-proof.d.ts +6 -0
  11. package/lib/attestor-proof.js +45 -18
  12. package/lib/client.d.ts +1 -6
  13. package/lib/client.js +12 -33
  14. package/lib/consts.d.ts +1 -1
  15. package/lib/consts.js +1 -1
  16. package/lib/eip191.d.ts +4 -2
  17. package/lib/eip191.js +17 -1
  18. package/lib/errors.d.ts +38 -4
  19. package/lib/errors.js +99 -16
  20. package/lib/index.d.ts +9 -7
  21. package/lib/index.js +8 -4
  22. package/lib/jwks.d.ts +25 -1
  23. package/lib/jwks.js +69 -4
  24. package/lib/legacy-proof.d.ts +57 -0
  25. package/lib/legacy-proof.js +211 -0
  26. package/lib/proof-hash.d.ts +35 -0
  27. package/lib/proof-hash.js +216 -0
  28. package/lib/proof-recipe.d.ts +18 -0
  29. package/lib/proof-recipe.js +37 -0
  30. package/lib/proof-trust-constants.d.ts +3 -0
  31. package/lib/proof-trust-constants.js +43 -0
  32. package/lib/proof-types.d.ts +62 -0
  33. package/lib/proof-types.js +1 -0
  34. package/lib/proof-verification-error.d.ts +13 -0
  35. package/lib/proof-verification-error.js +11 -0
  36. package/lib/reclaim.d.ts +27 -5
  37. package/lib/reclaim.js +140 -14
  38. package/lib/result.d.ts +58 -6
  39. package/lib/result.js +252 -61
  40. package/lib/routes.gen.d.ts +54 -6
  41. package/lib/routes.gen.js +15 -3
  42. package/lib/tee-binding.d.ts +82 -0
  43. package/lib/tee-binding.js +112 -0
  44. package/lib/types/openapi.gen.d.ts +1343 -206
  45. package/lib/verification-clients.d.ts +17 -0
  46. package/lib/verification-clients.js +14 -0
  47. package/lib/verify-attestor-tee.d.ts +76 -0
  48. package/lib/verify-attestor-tee.js +243 -0
  49. package/lib/verify-tee.d.ts +32 -0
  50. package/lib/verify-tee.js +347 -0
  51. package/package.json +5 -1
  52. package/src/api.ts +15 -0
  53. package/src/attestor-proof.ts +57 -20
  54. package/src/client.ts +15 -38
  55. package/src/consts.ts +1 -2
  56. package/src/eip191.ts +31 -3
  57. package/src/errors.ts +132 -18
  58. package/src/index.ts +50 -5
  59. package/src/jwks.ts +128 -5
  60. package/src/legacy-proof.ts +367 -0
  61. package/src/proof-hash.ts +316 -0
  62. package/src/proof-recipe.ts +75 -0
  63. package/src/proof-trust-constants.ts +45 -0
  64. package/src/proof-types.ts +57 -0
  65. package/src/proof-verification-error.ts +13 -0
  66. package/src/reclaim.ts +206 -19
  67. package/src/result.ts +403 -68
  68. package/src/routes.gen.ts +15 -3
  69. package/src/tee-binding.ts +192 -0
  70. package/src/types/openapi.gen.ts +1343 -206
  71. package/src/verification-clients.ts +22 -0
  72. package/src/verify-attestor-tee.ts +365 -0
  73. package/src/verify-tee.ts +507 -0
package/README.md CHANGED
@@ -1,18 +1,104 @@
1
1
  # @reclaimprotocol/client
2
2
 
3
- The typed HTTP client and SDK for Reclaim Protocol — the programmatic way to verify **data points** about your users, such as a follower count, an account balance, or an order. Integrate it directly into your app to request a verification and to verify the result you get back.
3
+ Use this typed HTTP client and SDK to request verifications and verify **data
4
+ points** about your users, such as a follower count, an account balance, or an
5
+ order. Integrate it directly into your app.
4
6
 
5
7
  One package gives you:
6
8
 
7
- - **`createReclaim(...)`** — a small, declarative client for the consumer happy path: create verification sessions, register callbacks, and receive + verify results.
8
- - **`ReclaimClient`** — a low-level typed HTTP client (`call('OperationId', args)`) for any operation in the spec.
9
- - **Crypto helpers** — ECIES decryption with the org's eth key, result-JWS signing/verification, JWKS discovery, and nested attestor-proof verification.
9
+ - **`createReclaim(...)`** — create verification sessions, register callbacks,
10
+ and receive and verify results with a small declarative client.
11
+ - **`ReclaimClient`** — call any operation in the spec with the low-level typed
12
+ HTTP client (`call('OperationId', args)`).
13
+ - **Crypto helpers** — decrypt ECIES results with the organization's ETH key,
14
+ sign and verify result JWSs, discover JWKS keys, and verify exact legacy
15
+ proofs.
10
16
 
11
- Works in Node, Deno, browsers, and edge runtimes (it uses WebCrypto + `@noble`, no native deps).
17
+ Use the API-only entry point in browsers and edge runtimes. Use the package
18
+ root in trusted runtimes that need proof verification, signing, or encryption.
19
+
20
+ ## Choose an entry point
21
+
22
+ Use the narrowest entry point that fits the code:
23
+
24
+ | Import | Use it for | Includes |
25
+ | --- | --- | --- |
26
+ | `@reclaimprotocol/client/api` | Browser Verification Clients, frontend dashboards, agents, and backend API calls | Browser-safe `fetch` transport plus OpenAPI-generated operation and schema types |
27
+ | `@reclaimprotocol/client` | Consumer backends and other trusted runtimes | API client, session workflows, result verification, proof verification, signing, and encryption |
28
+ | `@reclaimprotocol/client/openapi` | Type-only integrations | OpenAPI-generated types only |
29
+
30
+ Both runtime entry points export the exact same `ReclaimClient`,
31
+ `ReclaimClientOptions`, `ProblemError`, and OpenAPI-generated types. The package
32
+ root re-exports `/api` and adds the trusted-runtime SDK features. It doesn't
33
+ define another API client or another problem-error type.
34
+
35
+ The `/api` dependency graph doesn't include proof, attestor, signing,
36
+ encryption, or Node-only modules. The shared client doesn't set `User-Agent`,
37
+ which browsers don't allow JavaScript to control. Pass an identifying header
38
+ explicitly from a runtime that permits it.
39
+
40
+ ```ts
41
+ import {
42
+ isProblemError,
43
+ ReclaimClient,
44
+ } from '@reclaimprotocol/client/api'
45
+
46
+ const builder = new ReclaimClient({
47
+ baseUrl: 'https://build.reclaimprotocol.org',
48
+ // Use a bearer token for consumer or administrative API calls.
49
+ token,
50
+ })
51
+
52
+ try {
53
+ const { data: session } = await builder.call('GetVerificationSession', {
54
+ params: { sessionId },
55
+ })
56
+ console.log(session.status)
57
+ } catch (error) {
58
+ if (isProblemError(error)) {
59
+ console.error(error.output.statusCode, error.data.title)
60
+ }
61
+ }
62
+ ```
63
+
64
+ `ProblemError` stores the RFC 9457 document in `error.data`. It is
65
+ Boom-compatible: `@hapi/boom`'s `isBoom(error)` returns `true`, and
66
+ `error.output.statusCode` contains the HTTP status. The API entry point creates
67
+ this shape without importing the Boom runtime into browser bundles.
68
+
69
+ A registered Verification Client uses its session-bound UUID header instead
70
+ of a consumer bearer token:
71
+
72
+ ```ts
73
+ import { ReclaimClient } from '@reclaimprotocol/client/api'
74
+
75
+ const builder = new ReclaimClient({
76
+ baseUrl: builderApiUrl,
77
+ headers: { 'x-reclaim-vc-id': verificationClientId },
78
+ })
79
+
80
+ const { data: recipe } = await builder.call('GetVerificationRecipe', {
81
+ params: { sessionId },
82
+ })
83
+ ```
84
+
85
+ Treat the Verification Client ID as deployment configuration, not as proof of
86
+ identity. Builder authorizes it only for the session assigned to that
87
+ registered client. Never put an organization secret, result-signing private
88
+ key, or proof-owner private key in browser code. Keep result signing in the
89
+ Verification Client backend and call `verifyResultFull` from the consumer
90
+ backend after receiving a result.
12
91
 
13
92
  ## The model in one paragraph
14
93
 
15
- 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.
94
+ Authentication uses one **per-organization secret** (`rorg_…`, the
95
+ `orgSecret`) as a bearer credential; it uses no signatures or per-call
96
+ secrets. An **Ethereum (secp256k1) public key** identifies the organization.
97
+ When `canEncryptResult` is enabled, verification **results are encrypted with
98
+ ECIES** (secp256k1 + AES-256-GCM) to the organization's ETH public key. Keep the
99
+ matching **ETH private key** (`0x`-hex, the form any wallet or ethers emits) to
100
+ decrypt. The Verification Client signs the result JWS with `ES256K`; verify it
101
+ against that client's registered trusted JWKS.
16
102
 
17
103
  ```
18
104
  ┌─ one-time setup ─────────────────────────┐
@@ -35,20 +121,23 @@ npm install @reclaimprotocol/client
35
121
 
36
122
  ## Quick start (consumer)
37
123
 
38
- Configure once with your org secret. `orgId` below is your organization's id (the org the secret belongs to).
124
+ Configure the client once with your organization secret. In the example,
125
+ `orgId` identifies the organization that owns the secret.
39
126
 
40
127
  ```ts
41
128
  import { createReclaim } from '@reclaimprotocol/client'
42
129
 
43
130
  const reclaim = createReclaim({
44
- orgSecret: process.env.RECLAIM_ORG_SECRET, // rorg_… (issued by an org owner)
131
+ orgSecret: process.env.RECLAIM_ORG_SECRET!, // rorg_… (issued by an org owner)
45
132
  })
46
- const orgId = process.env.RECLAIM_ORG_ID // your org's id
133
+ const orgId = process.env.RECLAIM_ORG_ID! // your org's id
47
134
  ```
48
135
 
49
136
  ### 1. Register a callback (one-time)
50
137
 
51
- 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](#three-ways-to-set-up).
138
+ Register the endpoint where Builder posts verification results. Complete this
139
+ one-time setup with the **dashboard** or the **agent** too; see [Three ways to
140
+ set up](#three-ways-to-set-up).
52
141
 
53
142
  ```ts
54
143
  await reclaim.callbacks.register({ callbackUrl: 'https://my.app/reclaim/callback' })
@@ -56,7 +145,10 @@ await reclaim.callbacks.register({ callbackUrl: 'https://my.app/reclaim/callback
56
145
 
57
146
  ### 2. Enable encryption (optional, one-time)
58
147
 
59
- 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:
148
+ By default, the Builder delivers plaintext signed JWSs. To have it **encrypt**
149
+ results so only you can read them, register your organization's ETH **public**
150
+ key with `canEncryptResult: true`. Use any ETH key (the `0x`-hex form wallets
151
+ and ethers emit), or mint one with the SDK:
60
152
 
61
153
  ```ts
62
154
  import { generateEthKeypair } from '@reclaimprotocol/client'
@@ -78,11 +170,364 @@ await reclaim.keypair.set(orgId, {
78
170
  const session = await reclaim.sessions.create({
79
171
  providers: [{ providerId: 'prov_…' }], // one or more, proved in order → one combined result
80
172
  context: { userId: 'abc' },
173
+ // Optional: snapshot this active organization theme onto the session.
174
+ themeId: '550e8400-e29b-41d4-a716-446655440000',
175
+ // Optional: derive and bind the legacy TEE nonce locally after creation.
176
+ teeAttestation: { appSecret: organizationVerificationPrivateKey },
81
177
  })
82
178
  console.log('open this:', session.verificationUrl) // hand to the end-user
83
179
  ```
84
180
 
85
- It's a plain authenticated POST, so you don't need the SDK — curl works too (the org secret is the only auth):
181
+ #### Choose a verification client
182
+
183
+ `sessions.create` always creates a Builder v2 session. Select the claimant-facing
184
+ Verification Client with its registered base URL:
185
+
186
+ ```ts
187
+ import {
188
+ KNOWN_VERIFICATION_CLIENTS,
189
+ type KnownVerificationClientUrlMap,
190
+ } from '@reclaimprotocol/client'
191
+
192
+ const verificationClientUrls = {
193
+ [KNOWN_VERIFICATION_CLIENTS.portals]:
194
+ process.env.RECLAIM_PORTAL_URL,
195
+ [KNOWN_VERIFICATION_CLIENTS.verifierApp]:
196
+ process.env.RECLAIM_VERIFIER_APP_URL,
197
+ [KNOWN_VERIFICATION_CLIENTS.inAppSdk]:
198
+ process.env.RECLAIM_INAPP_URL,
199
+ [KNOWN_VERIFICATION_CLIENTS.reclaimBrowserExtension]:
200
+ process.env.RECLAIM_BROWSER_EXTENSION_URL,
201
+ } satisfies KnownVerificationClientUrlMap
202
+
203
+ const clientName = KNOWN_VERIFICATION_CLIENTS.portals
204
+ const verificationClientUrl = verificationClientUrls[clientName]
205
+ if (!verificationClientUrl) throw new Error(`${clientName} URL isn't configured`)
206
+
207
+ const session = await reclaim.sessions.create({
208
+ providers: [
209
+ // Blank/omitted version → latest; exact and npm semver ranges also work.
210
+ { providerId: 'prov_…', version: '^1.2.0' },
211
+ ],
212
+ context: { userId: 'abc', orderId: 'order-123' },
213
+ verificationClientUrl,
214
+ })
215
+
216
+ // Builder resolves the URL to a registered client, pins its UUID on the
217
+ // session, and adds the session id plus api=2. Share this returned URL.
218
+ console.log(session.verificationUrl)
219
+ ```
220
+
221
+ Builder snapshots the selected theme at session creation. Omit `themeId` to use
222
+ the organization's most recently updated active theme. If the organization has
223
+ no active theme, the Verification Client uses its default presentation. A
224
+ theme's optional localized `consent` block controls whether the claimant sees a
225
+ consent screen before the first provider starts.
226
+
227
+ For a deliberate troubleshooting session, append `diag=1` to the returned URL
228
+ before opening it:
229
+
230
+ ```ts
231
+ const verificationUrl = new URL(session.verificationUrl)
232
+ verificationUrl.searchParams.set('diag', '1')
233
+ window.location.assign(verificationUrl)
234
+ ```
235
+
236
+ `diag=1` is honored only with Builder mode's existing `api=2` selector. It can
237
+ enable logs containing personal data, so don't add it to normal production
238
+ links. Redirect parameters are also client-owned: append them to the returned
239
+ URL instead of putting them in the session request.
240
+
241
+ Use `{{context.key}}` in new Builder recipes for values supplied in
242
+ `context`; Builder validates those through the provider's `requiredContext`.
243
+ For an old DevTools recipe that still uses bare `{{key}}`, Builder-mode
244
+ Verification Clients seed a missing `parameters.key` from scalar
245
+ `context.key`. An explicit parameter wins. This compatibility alias applies
246
+ only to `api=2`; it doesn't change legacy sessions. Builder-owned
247
+ `reclaimSessionId` and `attestationNonce` remain context-only.
248
+
249
+ The value must exactly equal that deployment's registered
250
+ `verification_clients.uri`, including its scheme, host, path, trailing slash,
251
+ query, and optional `{sessionId}` placeholder. Do not substitute the placeholder
252
+ yourself. Builder either substitutes it or adds a `sessionId` query parameter,
253
+ then adds `api=2`. An unregistered or slightly different URL is rejected with
254
+ HTTP 400.
255
+
256
+ The SDK exposes stable registry names for these intended uses. A standard
257
+ deployment seeds all six rows, but an administrator must configure each row's
258
+ URL, issuer, JWKS URL, and signing key before consumers select it:
259
+
260
+ | Registry name | How the consumer selects it | Builder v2 use |
261
+ | --- | --- | --- |
262
+ | `builder` | Omit `verificationClientUrl`. | Built-in `/v/{sessionId}` client; available whenever Builder is running. |
263
+ | `portals` | Pass the exact registered portal URL. | Interactive web portal. Its deployment must configure the bridge URL and registered Client UUID. |
264
+ | `verifier-app` | Pass the exact registered HTTPS app/universal-link URL. | Reclaim Verifier mobile app. Its build must configure the bridge URL and registered Client UUID. |
265
+ | `inapp-sdk` | Pass the exact registered URL owned by the host app. | In-app SDK or add-to-app module. The host app must configure Builder transport and the registered Client UUID. |
266
+ | `reclaim-browser-extension` | Pass the exact registered extension launch URL. | Browser extension. Use a build that supports Builder mode (`api=2`). Its existing legacy `oprf-raw` support is independent of Builder-mode hardening. |
267
+ | `zkfetch` | Don't use as an interactive claimant client unless the deployment explicitly supports it. | Separate programmatic `ZKFETCH` quota pool, not one of the current portal/mobile Builder flows. |
268
+
269
+ `KNOWN_VERIFICATION_CLIENTS` is the public string map for these stable registry
270
+ names:
271
+
272
+ ```ts
273
+ {
274
+ builder: 'builder',
275
+ portals: 'portals',
276
+ verifierApp: 'verifier-app',
277
+ inAppSdk: 'inapp-sdk',
278
+ reclaimBrowserExtension: 'reclaim-browser-extension',
279
+ zkFetch: 'zkfetch',
280
+ }
281
+ ```
282
+
283
+ Use `KnownVerificationClientName` when accepting a client choice in your own
284
+ API and `KnownVerificationClientUrlMap` for deployment configuration. Validate
285
+ untrusted input against the map's values. The names are stable; the URLs are
286
+ not.
287
+
288
+ Client URLs are deployment configuration, not package constants. A seeded
289
+ registry row can be only an identifier until an administrator replaces its URI
290
+ with the real deployed client URL and configures its issuer, JWKS, bridge, and
291
+ signing key. Get the exact value from the Builder administrator or the
292
+ admin-only `/admin/verification-clients` registry; never accept it from a
293
+ claimant or derive it from a signed result. An organization token cannot list
294
+ the admin registry. Administrators can also register additional clients with
295
+ their own URL, issuer, JWKS URL, and signing key.
296
+
297
+ Examples for the currently supported interactive clients:
298
+
299
+ ```ts
300
+ // Replace these examples with exact values from your Builder registry.
301
+ const registeredClients = {
302
+ portal: 'https://portal.your-deployment.example/',
303
+ verifierApp: 'https://verify.your-deployment.example/app',
304
+ inApp: 'https://app.your-deployment.example/reclaim',
305
+ browserExtension: 'https://extension.your-deployment.example/launch',
306
+ }
307
+
308
+ // 1. Built-in Builder client — omit verificationClientUrl.
309
+ const builtIn = await reclaim.sessions.create({
310
+ providers: [{ providerId }],
311
+ context,
312
+ })
313
+
314
+ // 2. Portal — the URL must be the exact registered portals.uri.
315
+ const portal = await reclaim.sessions.create({
316
+ providers: [{ providerId }],
317
+ context,
318
+ verificationClientUrl: registeredClients.portal,
319
+ })
320
+
321
+ // 3. Reclaim Verifier app — use its registered HTTPS app link.
322
+ const verifierApp = await reclaim.sessions.create({
323
+ providers: [{ providerId }],
324
+ context,
325
+ verificationClientUrl: registeredClients.verifierApp,
326
+ })
327
+
328
+ // 4. In-app SDK/add-to-app host — use the host app's registered link.
329
+ const inApp = await reclaim.sessions.create({
330
+ providers: [{ providerId }],
331
+ context,
332
+ verificationClientUrl: registeredClients.inApp,
333
+ })
334
+
335
+ // 5. Browser extension — use a deployed build with Builder-mode support.
336
+ const browserExtension = await reclaim.sessions.create({
337
+ providers: [{ providerId }],
338
+ context,
339
+ verificationClientUrl: registeredClients.browserExtension,
340
+ })
341
+ ```
342
+
343
+ The Consumer supplies a URL, not `verificationClientId` or
344
+ `x-reclaim-vc-id`. Builder resolves and stores the UUID. The selected
345
+ Verification Client reads its own configured UUID and sends it only on its
346
+ session-bound Builder requests.
347
+
348
+ To add client-owned redirect data, modify only the returned URL after session
349
+ creation:
350
+
351
+ ```ts
352
+ const claimantUrl = new URL(portal.verificationUrl)
353
+ claimantUrl.searchParams.set(
354
+ 'redirectUrl',
355
+ 'https://merchant.example/verification-complete',
356
+ )
357
+ // `redirectUrl` is a portal convention. Another client can use another name.
358
+ openForClaimant(claimantUrl.toString())
359
+ ```
360
+
361
+ Do not replace `api` or `sessionId`. Builder doesn't store or execute these
362
+ redirect values, and callbacks remain organization-scoped subscriptions rather
363
+ than per-session request fields.
364
+
365
+ #### Complete web application flow
366
+
367
+ Keep session creation and result verification on your backend. Give the
368
+ frontend only the returned launch URL. Treat a client redirect as navigation,
369
+ not as evidence that verification succeeded.
370
+
371
+ ```mermaid
372
+ sequenceDiagram
373
+ participant Backend as Consumer backend
374
+ participant Frontend as Consumer frontend
375
+ participant Client as Verification Client
376
+ participant Builder
377
+ Backend->>Builder: Create verification session
378
+ Builder-->>Backend: Return session ID and launch URL
379
+ Backend-->>Frontend: Return session ID and launch URL
380
+ Frontend->>Client: Open launch URL
381
+ Client->>Builder: Complete Builder-mode session
382
+ Builder->>Backend: Deliver signed result callback
383
+ Backend->>Backend: Verify every proof and save trusted claims
384
+ Frontend->>Backend: Read saved verification status
385
+ Backend-->>Frontend: Return verified status
386
+ ```
387
+
388
+ **1. Create the request on your backend.** Keep `RECLAIM_ORG_SECRET` private,
389
+ validate the requested client name against `KNOWN_VERIFICATION_CLIENTS`, map it
390
+ to your deployment's registered URL, and save the returned session ID with your
391
+ user or order.
392
+
393
+ ```ts
394
+ import {
395
+ type CallbackBody,
396
+ createReclaim,
397
+ KNOWN_VERIFICATION_CLIENTS,
398
+ type KnownVerificationClientName,
399
+ type KnownVerificationClientUrlMap,
400
+ } from '@reclaimprotocol/client'
401
+
402
+ const reclaim = createReclaim({
403
+ orgSecret: process.env.RECLAIM_ORG_SECRET!,
404
+ })
405
+
406
+ const clientUrls: KnownVerificationClientUrlMap = {
407
+ [KNOWN_VERIFICATION_CLIENTS.portals]: process.env.RECLAIM_PORTAL_URL!,
408
+ [KNOWN_VERIFICATION_CLIENTS.verifierApp]:
409
+ process.env.RECLAIM_VERIFIER_APP_URL!,
410
+ [KNOWN_VERIFICATION_CLIENTS.inAppSdk]: process.env.RECLAIM_INAPP_URL!,
411
+ [KNOWN_VERIFICATION_CLIENTS.reclaimBrowserExtension]:
412
+ process.env.RECLAIM_BROWSER_EXTENSION_URL!,
413
+ }
414
+
415
+ async function createVerificationRequest(
416
+ userId: string,
417
+ clientName: KnownVerificationClientName,
418
+ ) {
419
+ const verificationClientUrl = clientName === KNOWN_VERIFICATION_CLIENTS.builder
420
+ ? undefined
421
+ : clientUrls[clientName]
422
+ if (clientName !== KNOWN_VERIFICATION_CLIENTS.builder && !verificationClientUrl) {
423
+ throw new Error(`${clientName} URL isn't configured`)
424
+ }
425
+
426
+ const session = await reclaim.sessions.create({
427
+ providers: [{ providerId: 'prov_…', version: '^1.2.0' }],
428
+ context: { userId },
429
+ ...(verificationClientUrl ? { verificationClientUrl } : {}),
430
+ })
431
+ await savePendingVerification({
432
+ sessionId: session.id,
433
+ userId,
434
+ orgId: session.orgId,
435
+ })
436
+ return { sessionId: session.id, verificationUrl: session.verificationUrl }
437
+ }
438
+ ```
439
+
440
+ **2. Launch it from your frontend.** Open exactly the returned URL. Builder has
441
+ already added `sessionId` and `api=2`. You may append a redirect parameter that
442
+ the selected Verification Client supports, but don't replace either Builder
443
+ parameter.
444
+
445
+ ```ts
446
+ const response = await fetch('/api/verifications', {
447
+ method: 'POST',
448
+ headers: { 'content-type': 'application/json' },
449
+ body: JSON.stringify({ client: 'portals' }),
450
+ })
451
+ const { sessionId, verificationUrl } = await response.json()
452
+
453
+ const claimantUrl = new URL(verificationUrl)
454
+ claimantUrl.searchParams.set(
455
+ 'redirectUrl',
456
+ `${location.origin}/verification-returned?sessionId=${encodeURIComponent(sessionId)}`,
457
+ )
458
+ location.assign(claimantUrl.toString())
459
+ ```
460
+
461
+ On the return page, ask your backend for the saved session status. Don't read a
462
+ proof or trusted claim from the redirect URL.
463
+
464
+ **3. Receive and verify on your backend.** Register a terminal callback once.
465
+ The `{{sessionId}}` placeholder is filled from Builder's session record and URL
466
+ encoded. Persist the delivery before returning a successful HTTP status so a
467
+ process crash can't lose it.
468
+
469
+ ```ts
470
+ await reclaim.callbacks.register({
471
+ callbackUrl: 'https://my.app/reclaim/callback/{{sessionId}}',
472
+ })
473
+
474
+ // HTTP callback handler. `sessionId` comes from the route path.
475
+ async function acceptCallback(sessionId: string, requestBody: CallbackBody) {
476
+ const deliveryId = await saveCallbackDelivery({ sessionId, requestBody })
477
+ await enqueueCallbackVerification(deliveryId)
478
+ return new Response(null, { status: 202 })
479
+ }
480
+ ```
481
+
482
+ Verify the durable delivery in a backend worker. Load the expected session and
483
+ organization from your database; don't take either expected value from the
484
+ callback payload. `results.receive` decrypts when needed, validates the
485
+ registered Verification Client issuer and JWKS, verifies the outer result JWS,
486
+ checks the session/audience and any legacy result expiry, and runs the package's legacy-compatible
487
+ `verifyProof` checks for every proof.
488
+
489
+ ```ts
490
+ async function verifyCallbackDelivery(deliveryId: string) {
491
+ const delivery = await loadCallbackDelivery(deliveryId)
492
+ const expected = await loadPendingVerification(delivery.sessionId)
493
+ const outcome = await reclaim.results.receive(delivery.requestBody, {
494
+ credential: process.env.RECLAIM_ETH_PRIVATE_KEY,
495
+ expectedReclaimSessionId: expected.sessionId,
496
+ expectedAud: expected.orgId,
497
+ })
498
+
499
+ if (outcome.kind !== 'result') return // optional progress subscription
500
+
501
+ const trustedClaims = outcome.result.proofs?.flatMap((proof) => (
502
+ proof.data ? [proof.data] : []
503
+ )) ?? []
504
+ await markVerificationComplete({
505
+ sessionId: expected.sessionId,
506
+ trustedClaims,
507
+ })
508
+ }
509
+ ```
510
+
511
+ `results.receive` throws `ResultVerificationError` for an invalid signature,
512
+ issuer, audience, session binding, legacy result expiry, proof signature, proof hash, TEE, or
513
+ attestor trust check. Mark a verification complete only after this call
514
+ succeeds. Make callback storage idempotent because Builder can retry delivery.
515
+
516
+ Removing `api=2` does not turn this request into a valid legacy request. To
517
+ create a legacy verification, continue using the legacy JS SDK/backend request
518
+ format. URLs without `api=2` stay on each client's unchanged legacy parser.
519
+
520
+ With `teeAttestation`, the SDK creates the session, derives the selected exact
521
+ legacy nonce, signs the Builder binding with
522
+ EIP-191, and registers it at
523
+ `PUT /verifications/sessions/{sessionId}/tee-nonce`. The private key is never
524
+ sent to Builder. The binding is idempotent for identical values and immutable
525
+ afterward; bind before proof generation starts. The default `nonceMode: 'hash'`
526
+ uses `RECLAIM_TEE_NONCE_V1`. Set `nonceMode: 'signature'` for the older
527
+ 65-byte EIP-191 nonce that legacy `verifyProof` also accepts.
528
+
529
+ Because this endpoint is an authenticated POST, you can use `curl` without the
530
+ SDK. The organization secret is the only credential:
86
531
 
87
532
  ```bash
88
533
  curl -X POST "$BUILDER_URL/verifications/sessions" \
@@ -91,23 +536,41 @@ curl -X POST "$BUILDER_URL/verifications/sessions" \
91
536
  -d '{
92
537
  "providers": [{ "providerId": "prov_…" }],
93
538
  "context": { "userId": "abc" },
94
- "verificationClientUrl": "https://your.app/v/{sessionId}"
539
+ "verificationClientUrl": "https://your.app/"
95
540
  }'
96
- # → 201 { "id": "…", "verificationUrl": "…", "status": "pending", "mode": "…", … }
541
+ # → 201 { "id": "…", "verificationUrl": "…&api=2", "status": "pending", "mode": "…", … }
97
542
  ```
98
543
 
544
+ For a TEE-bound session, a raw HTTP client must derive the legacy hash nonce
545
+ with its organization verification private key, sign the exact
546
+ `RECLAIM_BUILDER_TEE_BINDING_V1:{organizationId}:{sessionId}:{applicationId}:{timestamp}:{attestationVersion}:{attestationNonce}`
547
+ payload with EIP-191, then call
548
+ `PUT /verifications/sessions/{sessionId}/tee-nonce` with the same `OrgToken`.
549
+ Never send the private key in either request.
550
+
99
551
  ### 4. Receive and verify the result
100
552
 
101
- 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.
553
+ When the user finishes, Builder sends a POST request to your `callbackUrl`.
554
+ Pass the request body to `results.receive`. It decrypts encrypted results,
555
+ verifies the outer signature, and verifies every exact legacy proof. It
556
+ **throws if the result is invalid**, so reaching the `result` branch means the
557
+ result is verified.
102
558
 
103
559
  ```ts
104
- // in your callback HTTP handler:
560
+ // Load these expected values from your database, not from the callback body.
561
+ const expected = await loadPendingVerification(callbackRouteSessionId)
105
562
  const outcome = await reclaim.results.receive(requestBody, {
106
- credential: process.env.RECLAIM_ETH_PRIVATE_KEY, // 0x-hex eth key; omit for plaintext
107
- expectedReclaimSessionId: session.id,
563
+ credential: process.env.RECLAIM_ETH_PRIVATE_KEY, // 0x-hex ETH key; omit for plaintext
564
+ expectedReclaimSessionId: expected.sessionId,
565
+ expectedAud: expected.orgId,
108
566
  })
109
567
  if (outcome.kind === 'result') {
110
- console.log('verified!', outcome.result.payload)
568
+ // These values came from verifyProof. Don't trust diagnostic copies from
569
+ // the outer payload for application decisions.
570
+ const trustedClaims = outcome.result.proofs?.flatMap((proof) => (
571
+ proof.data ? [proof.data] : []
572
+ )) ?? []
573
+ useVerifiedClaims(trustedClaims)
111
574
  } else {
112
575
  console.log('progress event:', outcome.event) // non-terminal notification
113
576
  }
@@ -115,13 +578,15 @@ if (outcome.kind === 'result') {
115
578
 
116
579
  ### Three ways to set up
117
580
 
118
- The one-time setup (registering the callback subscription + the encryption keypair) can be done however suits you — they all hit the same API:
581
+ Choose any of these one-time setup methods; each uses the same API:
119
582
 
120
583
  - **This SDK** — `reclaim.callbacks.register(...)` + `reclaim.keypair.set(...)`, as above (good for scripting/CI).
121
- - **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.
584
+ - **The dashboard** — the organization'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.
122
585
  - **The agent** (`@reclaimprotocol/agent` — the `reclaim` CLI / MCP server) — handy while building and testing a provider locally.
123
586
 
124
- 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.
587
+ At runtime, your backend needs only the organization secret for
588
+ authentication and the ETH private key for decryption. The
589
+ `results.receive(...)` step is unchanged.
125
590
 
126
591
  ---
127
592
 
@@ -129,7 +594,8 @@ Whichever you use, at runtime your backend needs only the org secret (to authent
129
594
 
130
595
  ### `createReclaim(config)`
131
596
 
132
- Returns a `Reclaim` with four namespaces — `sessions`, `callbacks`, `keypair`, and `results` — plus an `api` escape hatch.
597
+ Call `createReclaim(config)` to return a `Reclaim` with four namespaces —
598
+ `sessions`, `callbacks`, `keypair`, and `results` — plus an `api` escape hatch.
133
599
 
134
600
  | `ReclaimConfig` field | type | required | notes |
135
601
  |---|---|---|---|
@@ -138,38 +604,86 @@ Returns a `Reclaim` with four namespaces — `sessions`, `callbacks`, `keypair`,
138
604
  | `fetch` | `typeof fetch` | no | Custom fetch (tests, edge runtimes). |
139
605
  | `headers` | `Record<string,string>` | no | Extra headers merged into every request. |
140
606
 
141
- `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`.
607
+ `orgSecret` is the only required configuration value. Pass each other value to
608
+ the method that uses it: **`orgId`** to `keypair.set`/`get`, the optional **TEE
609
+ private key** (`0x`-hex) to `sessions.create({ teeAttestation: { appSecret } })`,
610
+ and the **ETH private key** to `results.receive`/`decrypt`.
611
+
612
+ ### Known verification client names
613
+
614
+ ```ts
615
+ import {
616
+ KNOWN_VERIFICATION_CLIENTS,
617
+ type KnownVerificationClientName,
618
+ type KnownVerificationClientUrlMap,
619
+ } from '@reclaimprotocol/client'
620
+ ```
621
+
622
+ - `KNOWN_VERIFICATION_CLIENTS` maps TypeScript-friendly keys to the stable
623
+ registry names `builder`, `portals`, `verifier-app`, `inapp-sdk`,
624
+ `reclaim-browser-extension`, and `zkfetch`.
625
+ - `KnownVerificationClientName` is the union of those string values.
626
+ - `KnownVerificationClientUrlMap` is a partial map from those names to the
627
+ exact URLs registered in one Builder deployment.
628
+
629
+ The map doesn't make a URL trusted and doesn't select a client by name in the
630
+ Builder API. Your backend chooses a configured URL, and
631
+ `sessions.create({ verificationClientUrl })` asks Builder to resolve that URL
632
+ to its registered client UUID. Omit the URL to select `builder`.
142
633
 
143
634
  ### `reclaim.sessions`
144
635
 
145
636
  ```ts
146
- reclaim.sessions.create({ providers, context, verificationClientUrl? })
637
+ reclaim.sessions.create({ providers, context, verificationClientUrl?, teeAttestation? })
147
638
  // providers: { providerId, version? }[] — proved in order, one combined result; all required.
148
639
  // → VerificationSession (has `id`, `verificationUrl`, `status`, `mode`, …)
149
- // verificationClientUrl defaults to the Builder's own /v/{sessionId} page.
640
+ // verificationClientUrl selects a registered Verification Client by URL.
641
+ // teeAttestation: { appSecret } — optional org verification private key;
642
+ // kept local while the SDK derives and registers the post-create TEE binding.
643
+ // The returned verificationUrl contains sessionId and api=2.
644
+ reclaim.sessions.bindTeeNonce(sessionId, { appSecret, timestamp? })
645
+ // Explicit two-step/retry form. Reads the session, derives locally, and binds.
150
646
  reclaim.sessions.get(sessionId) // → VerificationSession
151
647
  reclaim.sessions.listEvents(sessionId) // → VerificationEventRecord[]
152
648
  ```
153
649
 
650
+ The Builder resolves `verificationClientUrl` to the session's registered
651
+ `verificationClientId`. It does not accept a per-session callback or redirect
652
+ URL. After creation, the consumer may append redirect query parameters that
653
+ the selected Verification Client understands; the Builder does not interpret
654
+ or execute them. A URL without `api=2` is handled by the selected client's
655
+ legacy path.
656
+
154
657
  ### `reclaim.callbacks`
155
658
 
156
659
  ```ts
157
- reclaim.callbacks.register({ callbackUrl, events? }) // events defaults to the 3 terminal events
660
+ reclaim.callbacks.register({ callbackUrl, events? }) // the dashboard preselects 5 terminal events
158
661
  // → CallbackSubscription. Org-scoped; no address/publicKey/secret.
159
662
  reclaim.callbacks.list() // → CallbackSubscription[]
160
663
  reclaim.callbacks.remove(subscriptionId) // → void
161
664
  ```
162
665
 
163
- `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.
666
+ `events` is any subset of `VerificationEvent`. Builder terminal events are
667
+ `verification_success`, `verification_rejected`, `verification_error`,
668
+ `verification_cancelled`, and `session_expired`. The dashboard preselects all
669
+ five. Terminal result deliveries contain the signed result when one is
670
+ available. Treat cancellation and expiry as terminal outcomes.
671
+ Non-terminal events deliver plaintext progress notifications.
164
672
 
165
- ### `reclaim.keypair` (the org's single eth encryption key)
673
+ ### `reclaim.keypair` (the organization's single ETH encryption key)
166
674
 
167
675
  ```ts
168
676
  await reclaim.keypair.set(orgId, { publicKey, canEncryptResult?: true, label?: 'prod' }) // → OrgKeypair
169
677
  await reclaim.keypair.get(orgId) // → OrgKeypair | undefined (undefined ⇒ keyless / plaintext delivery)
170
678
  ```
171
679
 
172
- `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`.
680
+ `publicKey` is the organization's ETH **uncompressed public key** (`0x04` +
681
+ 128 hex — the form `generateEthKeypair()` and ethers produce). Alternatively,
682
+ pass `publicKeyJwk` (a secp256k1 JWK). `set` stores the JWK and derived ETH
683
+ address. There is exactly **one** keypair per organization; `set` replaces it
684
+ (rotation). `canEncryptResult` toggles whether results are encrypted to it
685
+ (default `false` ⇒ plaintext). Keep the matching **ETH private key** (`0x`-hex)
686
+ and pass it as the decryption key to `results.receive`/`decrypt`.
173
687
 
174
688
  ### `reclaim.results`
175
689
 
@@ -183,13 +697,77 @@ reclaim.results.verify(jws, opts?) // → VerifyResultFullOutcome
183
697
 
184
698
  ```ts
185
699
  type ReceivedDelivery =
186
- | { kind: 'result'; result: VerifyResultFullOutcome } // terminal: decrypted + verified
187
- | { kind: 'event'; event: VerificationEvent; eventData: Record<string, unknown> } // non-terminal: plaintext
700
+ | { kind: 'result'; result: VerifyResultFullOutcome } // signed, decrypted, verified result
701
+ | { kind: 'event'; event: VerificationEvent; eventData: Record<string, unknown> } // plaintext lifecycle event
188
702
  ```
189
703
 
190
- `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.
704
+ `opts` (`ReceiveOptions`) is `VerifyResultFullOptions` (`expectedReclaimSessionId`
705
+ nonce check, `expectedAud`, trusted issuer configuration, attestor allowlists,
706
+ `fetchImpl`, `now`, `ignoreExpiration`) **plus** an optional `credential` — the ETH decryption key
707
+ as a **`0x`-hex private key string** (the key content, not a path or environment
708
+ variable name). All three methods are async; pass `credential` only when the
709
+ delivery is encrypted.
710
+
711
+ `verifyResultFull` is the recommended server-side API. Configure either an
712
+ explicit `issuer`/`trustedIssuers` list, or resolve the registered issuer for
713
+ the signed session's `verificationClientId`:
714
+
715
+ ```ts
716
+ await reclaim.results.verify(jws, {
717
+ expectedReclaimSessionId: session.id,
718
+ expectedAud: orgId,
719
+ resolveIssuers: (verificationClientId) => registry.issuersFor(verificationClientId),
720
+ resolveJwksUrl: (verificationClientId) => registry.jwksUrlFor(verificationClientId),
721
+ })
722
+ ```
191
723
 
192
- **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:
724
+ The SDK compares the signed header `iss` with that trusted set before it
725
+ fetches JWKS. It never uses an untrusted `iss` as a network destination. Keep
726
+ issuer resolution server-side and treat a missing or changed issuer as a
727
+ verification failure.
728
+
729
+ Issuer trust and key discovery are separate inputs: resolve the registered
730
+ `verificationClientIssuer` and `verificationClientJwksUrl` independently (or
731
+ pass `trustedIssuers` and `jwksUrl`). Do not construct the JWKS URL from the
732
+ JWS payload.
733
+
734
+ `verifyResultFull` calls this package's legacy-compatible `verifyProof`
735
+ implementation for every exact `Proof` object in the signed result. It does
736
+ not base64-decode, wrap, reconstruct, or otherwise transform those proofs.
737
+ By default, it uses each result's
738
+ `provider_id` and exact `resolved_version` to resolve the legacy provider hash
739
+ requirements. You can instead pass `hashes`, `proofValidation`, or
740
+ `proofValidationByProviderId`. These options mirror the legacy verifier,
741
+ including `hasNoPii`, verifier TEE (`teeAttestation.appSecret`), and
742
+ attestor-TEE checks. The same registered Builder JWKS URL supplies the
743
+ purpose-bound result keys and its `x-reclaim-attestors` trust list. TEE settings preserve the legacy configuration objects;
744
+ `verifyResultFull` does not accept injected verification hooks. The surrounding
745
+ issuer, session, and context checks are stateless; persist accepted session IDs
746
+ in your application to prevent replay.
747
+
748
+ For proof-hash comparison only, the verifier canonicalizes the equivalent
749
+ redaction transport spellings `oprf`, `oprf-mpc`, and `oprf-raw` to `_oprf`.
750
+ This keeps extension-produced proofs compatible without changing the proof,
751
+ its signed claim parameters, or the redaction mode used to create it.
752
+
753
+ New Builder result JWS payloads don't contain `exp`; a verified callback is a
754
+ durable result. Older Builder payloads can still contain `exp`. Those payloads
755
+ use the producer-selected Unix timestamp and fail after that time by default.
756
+ Set `ignoreExpiration: true` only when intentionally accepting a stored legacy
757
+ result after its signed deadline. This option doesn't bypass Builder Session
758
+ expiry, issuer trust, session binding, proof verification, or application-level
759
+ replay prevention.
760
+
761
+ The result's `extracted_parameters` and `content_hash` fields are signed
762
+ diagnostics. Use the verified data returned for each proof in
763
+ `result.proofs[].data`; do not trust the diagnostic copies for an authorization
764
+ decision.
765
+
766
+ **Invalid results throw.** `receive` and `verify` (and the underlying
767
+ `verifyResult` / `verifyResultFull`) never *return* a failed verdict. They throw
768
+ a **`ResultVerificationError`** carrying the failure `reason` and the full
769
+ `outcome` (payload, `reclaimSessionId`, per-proof detail). The `result` branch
770
+ is therefore always verified; wrap the call to inspect failures:
193
771
 
194
772
  ```ts
195
773
  import { ResultVerificationError } from '@reclaimprotocol/client'
@@ -205,7 +783,8 @@ try {
205
783
  }
206
784
  ```
207
785
 
208
- A non-terminal `event` delivery is not a result and never throws — it returns `{ kind: 'event', … }`.
786
+ A non-terminal `event` delivery is not a result and never throws. It returns
787
+ `{ kind: 'event', … }`.
209
788
 
210
789
  ### `reclaim.api` — escape hatch
211
790
 
@@ -230,19 +809,24 @@ decryptCallback(decryptionKey, body): Promise<Jws> // ECIES/plaintext
230
809
  // Result JWS (eth ES256K)
231
810
  signResultJws(ethPrivHex, payload, issuer): Jws // VC-side
232
811
  verifyResult(jws, opts): VerifyResultOutcome // offline signature/nonce check (throws if invalid)
233
- verifyResultFull(jws, opts): Promise<VerifyResultFullOutcome> // + JWKS discovery + attestor proofs (throws if invalid)
812
+ verifyResultFull(jws, opts): Promise<VerifyResultFullOutcome> // + JWKS discovery + exact legacy-proof verification (throws if invalid)
234
813
  verifyJwsSignature(jws, signerPublicKey), decodeJwsPayload(jws)
235
814
 
236
815
  // JWKS (signature-key discovery / publishing)
237
816
  fetchJwks(url), jwksFromPrivateKey(ethPrivHex), selectKey(...), jwkToUncompressedPublicKey(jwk)
238
817
 
239
- // Nested attestor-proof verification
818
+ // Legacy compatibility helper. verifyResultFull does not use an inner proof JWS.
240
819
  verifyNestedProof(...): NestedProofOutcome
241
820
  ```
242
821
 
243
- ### Eth keypair utils (low-level)
822
+ ### ETH keypair utilities (low-level)
244
823
 
245
- `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).
824
+ `generateKeypair()`, `deriveFromPrivateKey(hex)`, `addressFromPublicKey(...)`,
825
+ plus the `secp` instance and `addressFromUncompressedPubkey(...)`, are exported
826
+ for the Verification Client and the agent (proof-owner signing). A consumer
827
+ that enables encryption holds an **ETH private key** (the same key family) to
828
+ decrypt; see the encryption-key helpers above. `canonicalJson` (RFC 8785) is
829
+ also exported for interoperable diagnostic hashes.
246
830
 
247
831
  ## `ReclaimClient` (low-level)
248
832
 
@@ -252,11 +836,19 @@ const client = new ReclaimClient({ baseUrl, token, fetch, headers })
252
836
  const { data, response } = await client.call('GetVerificationSession', { params: { sessionId } })
253
837
  ```
254
838
 
255
- Errors are thrown as `ProblemError` (RFC 9457). `throwProblemError`, `DEFAULT_BASE_URL`, `USER_AGENT`, and encoding helpers (`b64url`, `base64urlToBytes`, `bytesToHex`, `hexToBytes`) are also exported. The `call` method is fully typed against the generated OpenAPI operations.
839
+ Errors are thrown as the same Boom-compatible `ProblemError` exported by
840
+ `@reclaimprotocol/client/api`. `throwProblemError`, `DEFAULT_BASE_URL`,
841
+ `USER_AGENT`, and encoding helpers (`b64url`, `base64urlToBytes`, `bytesToHex`,
842
+ `hexToBytes`) are also exported. The `call` method is fully typed against the
843
+ generated OpenAPI operations.
256
844
 
257
845
  ## Security notes
258
846
 
259
- - 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.
260
- - **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.
847
+ - The **organization secret is a bearer credential**. Treat it like a password
848
+ and rotate it from the dashboard (`POST /orgs/{orgId}/token`, which
849
+ invalidates the previous secret). The server stores only an HMAC of it.
850
+ - **Encryption uses ECIES over secp256k1 + AES-256-GCM** (via `eciesjs` /
851
+ `@noble`). It runs in browsers, Node.js, Deno, and edge runtimes and uses the
852
+ same ETH key family as signing.
261
853
  - **Signature verification stays secp256k1** (not routed through `jose`) so it works on WebCrypto-only runtimes that lack `ES256K`.
262
854
  - The callback delivery is **unauthenticated transport** — never trust the raw POST body; trust only the decrypted, signature-verified result that `results.receive` returns.