@reclaimprotocol/client 0.1.34 → 0.1.36
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 +57 -10
- package/lib/errors.d.ts +15 -0
- package/lib/errors.js +44 -0
- package/lib/jwks.js +2 -1
- package/lib/request-proof.d.ts +10 -0
- package/lib/request-proof.js +17 -10
- package/lib/result.d.ts +20 -1
- package/lib/result.js +32 -7
- package/lib/routes.gen.d.ts +16 -0
- package/lib/routes.gen.js +5 -1
- package/lib/types/openapi.gen.d.ts +318 -23
- package/lib/types/openapi.gen.js +1 -1
- package/lib/verify-attestor-tee.d.ts +19 -4
- package/lib/verify-attestor-tee.js +10 -4
- package/package.json +1 -1
- package/src/errors.ts +52 -0
- package/src/jwks.ts +5 -1
- package/src/request-proof.ts +31 -17
- package/src/result.ts +47 -7
- package/src/routes.gen.ts +5 -1
- package/src/types/openapi.gen.ts +319 -24
- package/src/verify-attestor-tee.ts +33 -5
package/src/request-proof.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createSignDataForClaim } from '@reclaimprotocol/attestor-core'
|
|
2
2
|
import { recoverEip191Address } from './eip191.ts'
|
|
3
|
+
import { isTransientFailure, networkStep } from './errors.ts'
|
|
3
4
|
import { fetchReclaimJwks, trustedAttestorAddresses } from './jwks.ts'
|
|
4
5
|
import { assertValidProofHashes, getHashFromProof } from './proof-hash.ts'
|
|
5
6
|
import type {
|
|
@@ -41,9 +42,15 @@ export interface VerifyRequestProofOptions {
|
|
|
41
42
|
/** Read `providerHash` from context, while still verifying signatures. */
|
|
42
43
|
hasNoPii?: true
|
|
43
44
|
teeAttestation?: TeeAttestationConfig
|
|
45
|
+
/** Require every witness to carry a `claimAttestation`, and verify any
|
|
46
|
+
* `attestation_report` against `expectedImageDigests`. */
|
|
44
47
|
attestorTeeAttestation?: AttestorTeeAttestationConfig
|
|
45
48
|
/** Explicit trusted attestor ETH addresses. */
|
|
46
49
|
attestorAddresses?: string[]
|
|
50
|
+
/** Trusted attestor image digests, like `x-reclaim-attestor-image-digests`.
|
|
51
|
+
* Pin any `attestation_report` a witness carries without requiring one,
|
|
52
|
+
* matching `@reclaimprotocol/js-sdk`'s default. */
|
|
53
|
+
attestorImageDigests?: string[]
|
|
47
54
|
/** Trusted Builder JWKS registry containing attestor-purpose keys. */
|
|
48
55
|
attestorJwksUrl?: string
|
|
49
56
|
fetchImpl?: typeof fetch
|
|
@@ -70,6 +77,10 @@ export interface VerifyRequestProofOptions {
|
|
|
70
77
|
export interface ReclaimRequestProofOutcome {
|
|
71
78
|
valid: boolean
|
|
72
79
|
reason?: string
|
|
80
|
+
/** On failure: true when a temporary condition caused it, such as a
|
|
81
|
+
* network error while resolving the session or provider. A retry can
|
|
82
|
+
* succeed. */
|
|
83
|
+
transient?: boolean
|
|
73
84
|
proofHashes?: string[]
|
|
74
85
|
isTeeAttestationVerified?: boolean
|
|
75
86
|
/** True when at least one witness's `attestation_report` verified; false
|
|
@@ -116,6 +127,8 @@ export async function verifyRequestProof(
|
|
|
116
127
|
trust.imageDigests,
|
|
117
128
|
)
|
|
118
129
|
if(attestorTeeAttestation) {
|
|
130
|
+
// Only a caller-configured check requires an envelope on every
|
|
131
|
+
// witness; registry digests alone pin the reports that exist.
|
|
119
132
|
// `verified` counts witnesses whose attestation report was checked.
|
|
120
133
|
// A proof whose witnesses carry no report still passes the address
|
|
121
134
|
// and signature bindings, but nothing was attested — say so rather
|
|
@@ -123,6 +136,7 @@ export async function verifyRequestProof(
|
|
|
123
136
|
const summary = await runAttestorTeeVerification(
|
|
124
137
|
proofs,
|
|
125
138
|
attestorTeeAttestation,
|
|
139
|
+
{ requireClaimAttestation: !!effective.attestorTeeAttestation },
|
|
126
140
|
)
|
|
127
141
|
isAttestorTeeAttestationVerified = summary.verified > 0
|
|
128
142
|
}
|
|
@@ -139,6 +153,7 @@ export async function verifyRequestProof(
|
|
|
139
153
|
return {
|
|
140
154
|
valid: false,
|
|
141
155
|
reason: failureReason(error),
|
|
156
|
+
transient: isTransientFailure(error),
|
|
142
157
|
}
|
|
143
158
|
}
|
|
144
159
|
}
|
|
@@ -148,14 +163,15 @@ async function resolveValidation(
|
|
|
148
163
|
proofs: ReclaimRequestProof[],
|
|
149
164
|
) {
|
|
150
165
|
let effective = config
|
|
151
|
-
|
|
152
|
-
|
|
166
|
+
const { sessionId, resolveSession } = config
|
|
167
|
+
if(sessionId && !hasContentValidation(config)) {
|
|
168
|
+
if(!resolveSession) {
|
|
153
169
|
throw new Error('session-resolver-required')
|
|
154
170
|
}
|
|
155
171
|
|
|
156
172
|
effective = {
|
|
157
173
|
...config,
|
|
158
|
-
...await
|
|
174
|
+
...await networkStep(() => resolveSession(sessionId, proofs)),
|
|
159
175
|
}
|
|
160
176
|
}
|
|
161
177
|
|
|
@@ -164,12 +180,12 @@ async function resolveValidation(
|
|
|
164
180
|
|
|
165
181
|
async function resolveAttestorTrust(config: VerifyRequestProofOptions) {
|
|
166
182
|
const addresses = config.attestorAddresses?.map(normalized) ?? []
|
|
167
|
-
const imageDigests
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
)
|
|
183
|
+
const imageDigests = config.attestorImageDigests?.map(normalized) ?? []
|
|
184
|
+
const { attestorJwksUrl, fetchImpl } = config
|
|
185
|
+
if(attestorJwksUrl) {
|
|
186
|
+
const registry = await networkStep(() => (
|
|
187
|
+
fetchReclaimJwks(attestorJwksUrl, fetchImpl)
|
|
188
|
+
))
|
|
173
189
|
addresses.push(
|
|
174
190
|
...(registry['x-reclaim-attestors'] ?? []),
|
|
175
191
|
...trustedAttestorAddresses(registry.keys),
|
|
@@ -258,17 +274,15 @@ async function assertContent(
|
|
|
258
274
|
return
|
|
259
275
|
}
|
|
260
276
|
|
|
261
|
-
|
|
262
|
-
|
|
277
|
+
const { providerId, providerVersion, allowedTags, resolveProvider } = config
|
|
278
|
+
if(providerId) {
|
|
279
|
+
if(!resolveProvider) {
|
|
263
280
|
throw new Error('provider-resolver-required')
|
|
264
281
|
}
|
|
265
282
|
|
|
266
|
-
const candidates = await
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
config.allowedTags,
|
|
270
|
-
proofs,
|
|
271
|
-
)
|
|
283
|
+
const candidates = await networkStep(() => (
|
|
284
|
+
resolveProvider(providerId, providerVersion, allowedTags, proofs)
|
|
285
|
+
))
|
|
272
286
|
if(!candidates.length) {
|
|
273
287
|
throw new Error('provider-not-found')
|
|
274
288
|
}
|
package/src/result.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
hexToBytes,
|
|
21
21
|
stripTrailingSlashes,
|
|
22
22
|
} from './encoding.ts'
|
|
23
|
+
import { isTransientError } from './errors.ts'
|
|
23
24
|
import {
|
|
24
25
|
fetchReclaimJwks,
|
|
25
26
|
type Jwk,
|
|
@@ -303,6 +304,9 @@ export interface ResultProofOutcome {
|
|
|
303
304
|
proof?: ReclaimRequestProof
|
|
304
305
|
proofValid: boolean
|
|
305
306
|
reason?: string
|
|
307
|
+
/** On failure: true when a temporary condition caused it, such as a
|
|
308
|
+
* network error while resolving the provider. */
|
|
309
|
+
transient?: boolean
|
|
306
310
|
/** @deprecated Use `attestorAddresses`; retained for compatibility. */
|
|
307
311
|
attestorAddress?: string
|
|
308
312
|
/** Every attestor address declared by the proof. */
|
|
@@ -330,12 +334,18 @@ export interface VerifyResultFullOutcome extends VerifyResultOutcome {
|
|
|
330
334
|
|
|
331
335
|
export interface ResultVerificationFailure extends VerifyResultFullOutcome {
|
|
332
336
|
reason: string
|
|
337
|
+
/** True when a temporary condition caused the failure. Set by the
|
|
338
|
+
* verifier; read it as {@link ResultVerificationError.retryable}. */
|
|
339
|
+
retryable?: boolean
|
|
333
340
|
}
|
|
334
341
|
|
|
335
342
|
/**
|
|
336
343
|
* Thrown by {@link verifyResult} / {@link verifyResultFull} (and therefore
|
|
337
344
|
* `reclaim.results.verify`/`receive`) when a result fails verification. Carries
|
|
338
|
-
* the full invalid outcome so callers can inspect it in a `catch
|
|
345
|
+
* the full invalid outcome so callers can inspect it in a `catch`.
|
|
346
|
+
*
|
|
347
|
+
* In a callback handler, answer 2xx when `retryable` is `false` (Builder
|
|
348
|
+
* retrying the same delivery can't help) and 5xx when it's `true`:
|
|
339
349
|
*
|
|
340
350
|
* try {
|
|
341
351
|
* const { result } = await reclaim.results.receive(body, {
|
|
@@ -346,6 +356,7 @@ export interface ResultVerificationFailure extends VerifyResultFullOutcome {
|
|
|
346
356
|
* if (err instanceof ResultVerificationError) {
|
|
347
357
|
* // for example, 'expired', { payload, … }
|
|
348
358
|
* console.error(err.reason, err.outcome)
|
|
359
|
+
* res.sendStatus(err.retryable ? 503 : 200)
|
|
349
360
|
* }
|
|
350
361
|
* }
|
|
351
362
|
*/
|
|
@@ -356,12 +367,22 @@ export class ResultVerificationError extends Error {
|
|
|
356
367
|
/** The full (invalid) outcome: `payload`, `reclaimSessionId`, and per-proof
|
|
357
368
|
* detail (`proofs`) when verification got that far. */
|
|
358
369
|
readonly outcome: ResultVerificationFailure
|
|
370
|
+
/**
|
|
371
|
+
* Whether a retry can succeed. `true` only for a temporary condition on
|
|
372
|
+
* the verifier's side: Builder's JWKS or recipe couldn't be fetched
|
|
373
|
+
* (network error, timeout, 5xx, 429). `false` when the result itself is
|
|
374
|
+
* wrong (signature, issuer, audience, session, expiry, invalid proof), so
|
|
375
|
+
* redelivering the same body fails the same way. Respond 5xx to a
|
|
376
|
+
* callback only when `true`.
|
|
377
|
+
*/
|
|
378
|
+
readonly retryable: boolean
|
|
359
379
|
|
|
360
380
|
constructor(outcome: ResultVerificationFailure) {
|
|
361
381
|
super(`result verification failed: ${outcome.reason}`)
|
|
362
382
|
this.name = 'ResultVerificationError'
|
|
363
383
|
this.reason = outcome.reason
|
|
364
384
|
this.outcome = outcome
|
|
385
|
+
this.retryable = outcome.retryable ?? false
|
|
365
386
|
}
|
|
366
387
|
}
|
|
367
388
|
|
|
@@ -446,8 +467,10 @@ export async function verifyResultFull(
|
|
|
446
467
|
jwksAttestorAddresses = registry['x-reclaim-attestors'] ?? []
|
|
447
468
|
jwksAttestorImageDigests =
|
|
448
469
|
registry['x-reclaim-attestor-image-digests'] ?? []
|
|
449
|
-
} catch{
|
|
450
|
-
return fail('jwks-fetch-failed'
|
|
470
|
+
} catch(error) {
|
|
471
|
+
return fail('jwks-fetch-failed', {
|
|
472
|
+
retryable: isTransientError(error),
|
|
473
|
+
})
|
|
451
474
|
}
|
|
452
475
|
|
|
453
476
|
const jwk = selectVerificationResultKey(
|
|
@@ -594,7 +617,12 @@ export async function verifyResultFull(
|
|
|
594
617
|
expectedImageDigests = configuredImageDigests
|
|
595
618
|
}
|
|
596
619
|
|
|
597
|
-
|
|
620
|
+
// A caller-configured check requires a `claimAttestation` on every
|
|
621
|
+
// witness. Registry digests alone only pin the attestor reports that
|
|
622
|
+
// are present, as `@reclaimprotocol/js-sdk` does by default — a portal
|
|
623
|
+
// TEE proof without a bound nonce carries no envelope.
|
|
624
|
+
const attestorTeeAttestation = configuredAttestorTee
|
|
625
|
+
&& expectedImageDigests.length
|
|
598
626
|
? { ...configuredAttestorTee, expectedImageDigests }
|
|
599
627
|
: configuredAttestorTee
|
|
600
628
|
const commonValidation = {
|
|
@@ -606,6 +634,9 @@ export async function verifyResultFull(
|
|
|
606
634
|
...(attestorTeeAttestation
|
|
607
635
|
? { attestorTeeAttestation }
|
|
608
636
|
: {}),
|
|
637
|
+
...(expectedImageDigests.length
|
|
638
|
+
? { attestorImageDigests: expectedImageDigests }
|
|
639
|
+
: {}),
|
|
609
640
|
...(expectedSessionId
|
|
610
641
|
? {
|
|
611
642
|
expectedSessionId,
|
|
@@ -622,7 +653,12 @@ export async function verifyResultFull(
|
|
|
622
653
|
}
|
|
623
654
|
|
|
624
655
|
if(!proofs.every((p) => p.proofValid)) {
|
|
625
|
-
|
|
656
|
+
// Retry only when every failure is transient; one genuinely invalid
|
|
657
|
+
// proof makes redelivery pointless.
|
|
658
|
+
return fail('invalid-proof', {
|
|
659
|
+
proofs,
|
|
660
|
+
retryable: proofs.every((p) => p.proofValid || p.transient),
|
|
661
|
+
})
|
|
626
662
|
}
|
|
627
663
|
|
|
628
664
|
return { payload, reclaimSessionId, proofs }
|
|
@@ -666,6 +702,7 @@ export async function verifyResultFull(
|
|
|
666
702
|
for(const { outcomeIndex } of group) {
|
|
667
703
|
proofs[outcomeIndex].proofValid = false
|
|
668
704
|
proofs[outcomeIndex].reason = requestProofOutcome.reason
|
|
705
|
+
proofs[outcomeIndex].transient = requestProofOutcome.transient
|
|
669
706
|
}
|
|
670
707
|
}
|
|
671
708
|
|
|
@@ -678,8 +715,11 @@ export async function verifyResultFull(
|
|
|
678
715
|
try {
|
|
679
716
|
const resolved = await opts.resolveSession(expectedSessionId)
|
|
680
717
|
recipes = resolved.recipes
|
|
681
|
-
} catch{
|
|
682
|
-
return fail('session-resolution-failed', {
|
|
718
|
+
} catch(error) {
|
|
719
|
+
return fail('session-resolution-failed', {
|
|
720
|
+
proofs,
|
|
721
|
+
retryable: isTransientError(error),
|
|
722
|
+
})
|
|
683
723
|
}
|
|
684
724
|
}
|
|
685
725
|
|
package/src/routes.gen.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// AUTO-GENERATED by packages/app/scripts/mcp-tools-gen.ts
|
|
2
|
-
// from packages/app/openapi.yaml (spec v0.1.0, source sha256:
|
|
2
|
+
// from packages/app/openapi.yaml (spec v0.1.0, source sha256:96339e8dff91).
|
|
3
3
|
// The hash catches drift the version misses (a spec can change without a
|
|
4
4
|
// version bump) — do not remove it as "redundant" with the version.
|
|
5
5
|
// Run `npm run openapi:gen --workspace=packages/app` to regenerate. Do not edit by hand.
|
|
6
6
|
|
|
7
7
|
export const ROUTES = {
|
|
8
8
|
"AcceptOrgInvite": {"method":"post","path":"/orgs/{orgId}/invite"},
|
|
9
|
+
"AcknowledgeCallbackEvents": {"method":"post","path":"/verifications/callback-events/acknowledge"},
|
|
9
10
|
"AddOrgMember": {"method":"post","path":"/orgs/{orgId}/members"},
|
|
10
11
|
"AddProviderContributionComment": {"method":"post","path":"/providers/{providerId}/contributions/{campaignId}/comments"},
|
|
11
12
|
"AddStaffMember": {"method":"post","path":"/admin/staff"},
|
|
@@ -28,6 +29,7 @@ export const ROUTES = {
|
|
|
28
29
|
"AdminUsers": {"method":"get","path":"/admin/users"},
|
|
29
30
|
"AdminVerificationClients": {"method":"get","path":"/admin/verification-clients"},
|
|
30
31
|
"AdminVoidOrgCredit": {"method":"post","path":"/admin/orgs/{orgId}/credits/{grantId}/void"},
|
|
32
|
+
"AgentInstallationMarkdown": {"method":"get","path":"/docs/agent-installation.md"},
|
|
31
33
|
"AppleAppSiteAssociation": {"method":"get","path":"/.well-known/apple-app-site-association"},
|
|
32
34
|
"AssetLinksJson": {"method":"get","path":"/.well-known/assetlinks.json"},
|
|
33
35
|
"BindVerificationSessionTeeNonce": {"method":"put","path":"/verifications/sessions/{sessionId}/tee-nonce"},
|
|
@@ -103,6 +105,7 @@ export const ROUTES = {
|
|
|
103
105
|
"JwksJson": {"method":"get","path":"/.well-known/jwks.json"},
|
|
104
106
|
"ListBillingPlans": {"method":"get","path":"/billing/plans"},
|
|
105
107
|
"ListCallbackDlq": {"method":"get","path":"/verifications/callback-dlq"},
|
|
108
|
+
"ListCallbackEvents": {"method":"get","path":"/verifications/callback-events"},
|
|
106
109
|
"ListCallbackSubscriptions": {"method":"get","path":"/verifications/callback-subscriptions"},
|
|
107
110
|
"ListFeatureFlagRevisions": {"method":"get","path":"/admin/feature-flags/{flagKey}/revisions"},
|
|
108
111
|
"ListOrgCallbackSubscriptions": {"method":"get","path":"/orgs/{orgId}/callback-subscriptions"},
|
|
@@ -129,6 +132,7 @@ export const ROUTES = {
|
|
|
129
132
|
"PublishProviderVersion": {"method":"post","path":"/providers/{providerId}/versions/{version}/publish"},
|
|
130
133
|
"RefreshBillingPlans": {"method":"post","path":"/admin/billing/plans/refresh"},
|
|
131
134
|
"RefundTransaction": {"method":"post","path":"/admin/billing/transactions/{txId}/refund"},
|
|
135
|
+
"ReleaseCallbackListener": {"method":"post","path":"/verifications/callback-listener/release"},
|
|
132
136
|
"RemoveOrgMember": {"method":"delete","path":"/orgs/{orgId}/members/{memberId}"},
|
|
133
137
|
"RemoveStaffMember": {"method":"delete","path":"/admin/staff/{email}"},
|
|
134
138
|
"ReportClaimant": {"method":"post","path":"/v/{sessionId}/claimant"},
|