@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.
- package/README.md +634 -42
- package/lib/api-client.d.ts +31 -0
- package/lib/api-client.js +112 -0
- package/lib/api-consts.d.ts +4 -0
- package/lib/api-consts.js +4 -0
- package/lib/api-errors.d.ts +13 -0
- package/lib/api-errors.js +30 -0
- package/lib/api.d.ts +7 -0
- package/lib/api.js +3 -0
- package/lib/attestor-proof.d.ts +6 -0
- package/lib/attestor-proof.js +45 -18
- package/lib/client.d.ts +1 -6
- package/lib/client.js +12 -33
- package/lib/consts.d.ts +1 -1
- package/lib/consts.js +1 -1
- package/lib/eip191.d.ts +4 -2
- package/lib/eip191.js +17 -1
- package/lib/errors.d.ts +38 -4
- package/lib/errors.js +99 -16
- package/lib/index.d.ts +9 -7
- package/lib/index.js +8 -4
- package/lib/jwks.d.ts +25 -1
- package/lib/jwks.js +69 -4
- package/lib/legacy-proof.d.ts +57 -0
- package/lib/legacy-proof.js +211 -0
- package/lib/proof-hash.d.ts +35 -0
- package/lib/proof-hash.js +216 -0
- package/lib/proof-recipe.d.ts +18 -0
- package/lib/proof-recipe.js +37 -0
- package/lib/proof-trust-constants.d.ts +3 -0
- package/lib/proof-trust-constants.js +43 -0
- package/lib/proof-types.d.ts +62 -0
- package/lib/proof-types.js +1 -0
- package/lib/proof-verification-error.d.ts +13 -0
- package/lib/proof-verification-error.js +11 -0
- package/lib/reclaim.d.ts +27 -5
- package/lib/reclaim.js +140 -14
- package/lib/result.d.ts +58 -6
- package/lib/result.js +252 -61
- package/lib/routes.gen.d.ts +54 -6
- package/lib/routes.gen.js +15 -3
- package/lib/tee-binding.d.ts +82 -0
- package/lib/tee-binding.js +112 -0
- package/lib/types/openapi.gen.d.ts +1343 -206
- package/lib/verification-clients.d.ts +17 -0
- package/lib/verification-clients.js +14 -0
- package/lib/verify-attestor-tee.d.ts +76 -0
- package/lib/verify-attestor-tee.js +243 -0
- package/lib/verify-tee.d.ts +32 -0
- package/lib/verify-tee.js +347 -0
- package/package.json +5 -1
- package/src/api.ts +15 -0
- package/src/attestor-proof.ts +57 -20
- package/src/client.ts +15 -38
- package/src/consts.ts +1 -2
- package/src/eip191.ts +31 -3
- package/src/errors.ts +132 -18
- package/src/index.ts +50 -5
- package/src/jwks.ts +128 -5
- package/src/legacy-proof.ts +367 -0
- package/src/proof-hash.ts +316 -0
- package/src/proof-recipe.ts +75 -0
- package/src/proof-trust-constants.ts +45 -0
- package/src/proof-types.ts +57 -0
- package/src/proof-verification-error.ts +13 -0
- package/src/reclaim.ts +206 -19
- package/src/result.ts +403 -68
- package/src/routes.gen.ts +15 -3
- package/src/tee-binding.ts +192 -0
- package/src/types/openapi.gen.ts +1343 -206
- package/src/verification-clients.ts +22 -0
- package/src/verify-attestor-tee.ts +365 -0
- package/src/verify-tee.ts +507 -0
package/README.md
CHANGED
|
@@ -1,18 +1,104 @@
|
|
|
1
1
|
# @reclaimprotocol/client
|
|
2
2
|
|
|
3
|
-
|
|
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(...)`** —
|
|
8
|
-
|
|
9
|
-
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
131
|
+
orgSecret: process.env.RECLAIM_ORG_SECRET!, // rorg_… (issued by an org owner)
|
|
45
132
|
})
|
|
46
|
-
const orgId = process.env.RECLAIM_ORG_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
|
-
|
|
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
|
|
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
|
-
|
|
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/
|
|
539
|
+
"verificationClientUrl": "https://your.app/"
|
|
95
540
|
}'
|
|
96
|
-
# → 201 { "id": "…", "verificationUrl": "
|
|
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
|
|
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
|
-
//
|
|
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
|
|
107
|
-
expectedReclaimSessionId:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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? }) //
|
|
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`.
|
|
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
|
|
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
|
|
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 } //
|
|
187
|
-
| { kind: 'event'; event: VerificationEvent; eventData: Record<string, unknown> } //
|
|
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`
|
|
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
|
-
|
|
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
|
|
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 +
|
|
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
|
-
//
|
|
818
|
+
// Legacy compatibility helper. verifyResultFull does not use an inner proof JWS.
|
|
240
819
|
verifyNestedProof(...): NestedProofOutcome
|
|
241
820
|
```
|
|
242
821
|
|
|
243
|
-
###
|
|
822
|
+
### ETH keypair utilities (low-level)
|
|
244
823
|
|
|
245
|
-
`generateKeypair()`, `deriveFromPrivateKey(hex)`, `addressFromPublicKey(...)`,
|
|
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
|
|
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 **
|
|
260
|
-
|
|
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.
|