@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.
@@ -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
- if(config.sessionId && !hasContentValidation(config)) {
152
- if(!config.resolveSession) {
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 config.resolveSession(config.sessionId, proofs),
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: string[] = []
168
- if(config.attestorJwksUrl) {
169
- const registry = await fetchReclaimJwks(
170
- config.attestorJwksUrl,
171
- config.fetchImpl,
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
- if(config.providerId) {
262
- if(!config.resolveProvider) {
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 config.resolveProvider(
267
- config.providerId,
268
- config.providerVersion,
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
- const attestorTeeAttestation = expectedImageDigests.length
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
- return fail('invalid-proof', { proofs })
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', { proofs })
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:e2db795ab15c).
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"},