@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/README.md
CHANGED
|
@@ -750,16 +750,18 @@ including `hasNoPii`, verifier TEE (`teeAttestation.appSecret`), and
|
|
|
750
750
|
attestor-TEE checks. The configured Builder JWKS URL supplies the
|
|
751
751
|
purpose-bound result keys, its `x-reclaim-attestors` signer list, and its
|
|
752
752
|
`x-reclaim-attestor-image-digests` Confidential Space image allowlist. When
|
|
753
|
-
the JWKS publishes image digests, `verifyResultFull`
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
`
|
|
761
|
-
|
|
762
|
-
|
|
753
|
+
the JWKS publishes image digests, `verifyResultFull` verifies any attestor TEE
|
|
754
|
+
`claimAttestation` a witness carries — its attestor address and claim
|
|
755
|
+
signature, and its `attestation_report` pinned to those digests when present.
|
|
756
|
+
Neither is required: like `@reclaimprotocol/js-sdk`, a witness is trusted by
|
|
757
|
+
its signer being in `x-reclaim-attestors`. A portal TEE proof without a bound
|
|
758
|
+
nonce has no `claimAttestation`, and production attestors sign without
|
|
759
|
+
returning a report. When nothing was attested,
|
|
760
|
+
`isAttestorTeeAttestationVerified` comes back `false`. Set
|
|
761
|
+
`proofValidation.attestorTeeAttestation` to require a `claimAttestation` on
|
|
762
|
+
every witness. If its `expectedImageDigests` is also set, the SDK uses the
|
|
763
|
+
intersection: caller policy can narrow the published trust list, but can't
|
|
764
|
+
broaden it. TEE settings preserve the same configuration objects;
|
|
763
765
|
`verifyResultFull` does not accept injected verification hooks. The surrounding
|
|
764
766
|
issuer, session, and context checks are stateless; persist accepted session IDs
|
|
765
767
|
in your application to prevent replay.
|
|
@@ -808,6 +810,51 @@ try {
|
|
|
808
810
|
A non-terminal `event` delivery is not a result and never throws. It returns
|
|
809
811
|
`{ kind: 'event', … }`.
|
|
810
812
|
|
|
813
|
+
#### Respond to a callback delivery
|
|
814
|
+
|
|
815
|
+
Builder retries a delivery until your callback route answers 2xx, and a
|
|
816
|
+
delivery that never succeeds holds back that session's later events. If
|
|
817
|
+
retrying the same body can't help, answer 2xx. If the failure is temporary,
|
|
818
|
+
answer 5xx.
|
|
819
|
+
|
|
820
|
+
`ResultVerificationError.retryable` tells you which case you're in. It's
|
|
821
|
+
`true` only when a temporary condition caused the failure:
|
|
822
|
+
Builder's JWKS or provider recipe couldn't be fetched because of a network
|
|
823
|
+
error, a timeout, an HTTP 5xx, or rate limiting (`jwks-fetch-failed`,
|
|
824
|
+
`session-resolution-failed`, or an `invalid-proof` whose proofs all failed
|
|
825
|
+
this way). It's `false` for everything about the result itself, including
|
|
826
|
+
`bad-signature`, `issuer-not-trusted`, `aud-mismatch`,
|
|
827
|
+
`reclaimSessionId-mismatch`, `expired`, and an `invalid-proof` with any
|
|
828
|
+
genuinely invalid proof.
|
|
829
|
+
|
|
830
|
+
```ts
|
|
831
|
+
app.post('/reclaim/callback', async (req, res) => {
|
|
832
|
+
try {
|
|
833
|
+
const { result } = await reclaim.results.receive(req.body, {
|
|
834
|
+
orgEthPrivateKey,
|
|
835
|
+
reclaimSessionId,
|
|
836
|
+
})
|
|
837
|
+
await markVerificationComplete(result)
|
|
838
|
+
res.sendStatus(200)
|
|
839
|
+
} catch (err) {
|
|
840
|
+
if (err instanceof ResultVerificationError) {
|
|
841
|
+
await storeFailure(err.reason, err.outcome)
|
|
842
|
+
// Retrying a bad result can't help, so only ask for a retry when the
|
|
843
|
+
// failure is temporary.
|
|
844
|
+
res.sendStatus(err.retryable ? 503 : 200)
|
|
845
|
+
return
|
|
846
|
+
}
|
|
847
|
+
throw err
|
|
848
|
+
}
|
|
849
|
+
})
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
`results.receive` can also throw an ordinary `Error` instead of
|
|
853
|
+
`ResultVerificationError`, for example when `orgEthPrivateKey` is missing or
|
|
854
|
+
doesn't match the key the result was encrypted to. Treat it as a
|
|
855
|
+
configuration problem: log it and answer 5xx, so the delivery succeeds once
|
|
856
|
+
you fix the key. Builder holds an undelivered result for 7 days.
|
|
857
|
+
|
|
811
858
|
### `reclaim.api` — escape hatch
|
|
812
859
|
|
|
813
860
|
```ts
|
package/lib/errors.d.ts
CHANGED
|
@@ -22,6 +22,21 @@ export type ProblemError = Error & {
|
|
|
22
22
|
};
|
|
23
23
|
/** Returns whether a thrown value is a Builder {@link ProblemError}. */
|
|
24
24
|
export declare function isProblemError(error: unknown): error is ProblemError;
|
|
25
|
+
/**
|
|
26
|
+
* Whether retrying the same request later can succeed: a network failure, a
|
|
27
|
+
* timeout, or an HTTP 5xx, 408, or 429 from Builder (a {@link ProblemError}
|
|
28
|
+
* or a plain error carrying the HTTP `status`). Everything else, such as a
|
|
29
|
+
* malformed response, won't change on retry.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isTransientError(error: unknown): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Runs one network step, such as a JWKS fetch or a session or provider
|
|
34
|
+
* lookup, and marks a transient failure so {@link isTransientFailure} can
|
|
35
|
+
* tell it apart from a failed content check.
|
|
36
|
+
*/
|
|
37
|
+
export declare function networkStep<T>(step: () => Promise<T> | T): Promise<T>;
|
|
38
|
+
/** Whether `error` came from a {@link networkStep} and a retry can succeed. */
|
|
39
|
+
export declare function isTransientFailure(error: unknown): boolean;
|
|
25
40
|
/** Returns whether a value follows the RFC 9457 problem-details schema. */
|
|
26
41
|
export declare function isProblemDetails(value: unknown): value is ProblemDetails;
|
|
27
42
|
/** Reads an RFC 9457 response, with a status-only fallback for other bodies. */
|
package/lib/errors.js
CHANGED
|
@@ -14,6 +14,50 @@ export function isProblemError(error) {
|
|
|
14
14
|
return isProblemDetails(error.data)
|
|
15
15
|
&& isProblemErrorOutput(error.output);
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Whether retrying the same request later can succeed: a network failure, a
|
|
19
|
+
* timeout, or an HTTP 5xx, 408, or 429 from Builder (a {@link ProblemError}
|
|
20
|
+
* or a plain error carrying the HTTP `status`). Everything else, such as a
|
|
21
|
+
* malformed response, won't change on retry.
|
|
22
|
+
*/
|
|
23
|
+
export function isTransientError(error) {
|
|
24
|
+
// `fetch` rejects with a TypeError when the network fails.
|
|
25
|
+
if (error instanceof TypeError) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
if (error instanceof Error
|
|
29
|
+
&& (error.name === 'TimeoutError' || error.name === 'AbortError')) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
const status = isProblemError(error)
|
|
33
|
+
? error.output.statusCode
|
|
34
|
+
: isRecord(error) ? error.status : undefined;
|
|
35
|
+
return typeof status === 'number'
|
|
36
|
+
&& (status >= 500 || status === 408 || status === 429);
|
|
37
|
+
}
|
|
38
|
+
// Errors thrown by a network step and judged transient. Tracked by identity
|
|
39
|
+
// so a content check that throws, say, a TypeError is never mistaken for one.
|
|
40
|
+
const TRANSIENT_ERRORS = new WeakSet();
|
|
41
|
+
/**
|
|
42
|
+
* Runs one network step, such as a JWKS fetch or a session or provider
|
|
43
|
+
* lookup, and marks a transient failure so {@link isTransientFailure} can
|
|
44
|
+
* tell it apart from a failed content check.
|
|
45
|
+
*/
|
|
46
|
+
export async function networkStep(step) {
|
|
47
|
+
try {
|
|
48
|
+
return await step();
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error instanceof Error && isTransientError(error)) {
|
|
52
|
+
TRANSIENT_ERRORS.add(error);
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Whether `error` came from a {@link networkStep} and a retry can succeed. */
|
|
58
|
+
export function isTransientFailure(error) {
|
|
59
|
+
return error instanceof Error && TRANSIENT_ERRORS.has(error);
|
|
60
|
+
}
|
|
17
61
|
/** Returns whether a value follows the RFC 9457 problem-details schema. */
|
|
18
62
|
export function isProblemDetails(value) {
|
|
19
63
|
if (!isRecord(value)) {
|
package/lib/jwks.js
CHANGED
|
@@ -47,7 +47,8 @@ export async function fetchReclaimJwks(url, fetchImpl = fetch) {
|
|
|
47
47
|
assertSecureJwksUrl(url);
|
|
48
48
|
const res = await fetchImpl(url);
|
|
49
49
|
if (!res.ok) {
|
|
50
|
-
|
|
50
|
+
// `status` lets callers tell a temporary Builder outage from a bad URL.
|
|
51
|
+
throw Object.assign(new Error(`JWKS fetch failed for ${url}: HTTP ${res.status}`), { status: res.status });
|
|
51
52
|
}
|
|
52
53
|
const body = (await res.json());
|
|
53
54
|
return {
|
package/lib/request-proof.d.ts
CHANGED
|
@@ -25,9 +25,15 @@ export interface VerifyRequestProofOptions {
|
|
|
25
25
|
/** Read `providerHash` from context, while still verifying signatures. */
|
|
26
26
|
hasNoPii?: true;
|
|
27
27
|
teeAttestation?: TeeAttestationConfig;
|
|
28
|
+
/** Require every witness to carry a `claimAttestation`, and verify any
|
|
29
|
+
* `attestation_report` against `expectedImageDigests`. */
|
|
28
30
|
attestorTeeAttestation?: AttestorTeeAttestationConfig;
|
|
29
31
|
/** Explicit trusted attestor ETH addresses. */
|
|
30
32
|
attestorAddresses?: string[];
|
|
33
|
+
/** Trusted attestor image digests, like `x-reclaim-attestor-image-digests`.
|
|
34
|
+
* Pin any `attestation_report` a witness carries without requiring one,
|
|
35
|
+
* matching `@reclaimprotocol/js-sdk`'s default. */
|
|
36
|
+
attestorImageDigests?: string[];
|
|
31
37
|
/** Trusted Builder JWKS registry containing attestor-purpose keys. */
|
|
32
38
|
attestorJwksUrl?: string;
|
|
33
39
|
fetchImpl?: typeof fetch;
|
|
@@ -43,6 +49,10 @@ export interface VerifyRequestProofOptions {
|
|
|
43
49
|
export interface ReclaimRequestProofOutcome {
|
|
44
50
|
valid: boolean;
|
|
45
51
|
reason?: string;
|
|
52
|
+
/** On failure: true when a temporary condition caused it, such as a
|
|
53
|
+
* network error while resolving the session or provider. A retry can
|
|
54
|
+
* succeed. */
|
|
55
|
+
transient?: boolean;
|
|
46
56
|
proofHashes?: string[];
|
|
47
57
|
isTeeAttestationVerified?: boolean;
|
|
48
58
|
/** True when at least one witness's `attestation_report` verified; false
|
package/lib/request-proof.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createSignDataForClaim } from '@reclaimprotocol/attestor-core';
|
|
2
2
|
import { recoverEip191Address } from "./eip191.js";
|
|
3
|
+
import { isTransientFailure, networkStep } from "./errors.js";
|
|
3
4
|
import { fetchReclaimJwks, trustedAttestorAddresses } from "./jwks.js";
|
|
4
5
|
import { assertValidProofHashes, getHashFromProof } from "./proof-hash.js";
|
|
5
6
|
import { runAttestorTeeVerification, } from "./verify-attestor-tee.js";
|
|
@@ -31,11 +32,13 @@ export async function verifyRequestProof(proofOrProofs, config) {
|
|
|
31
32
|
let isAttestorTeeAttestationVerified;
|
|
32
33
|
const attestorTeeAttestation = resolveAttestorTeeAttestation(effective.attestorTeeAttestation, trust.imageDigests);
|
|
33
34
|
if (attestorTeeAttestation) {
|
|
35
|
+
// Only a caller-configured check requires an envelope on every
|
|
36
|
+
// witness; registry digests alone pin the reports that exist.
|
|
34
37
|
// `verified` counts witnesses whose attestation report was checked.
|
|
35
38
|
// A proof whose witnesses carry no report still passes the address
|
|
36
39
|
// and signature bindings, but nothing was attested — say so rather
|
|
37
40
|
// than reporting a TEE attestation that never happened.
|
|
38
|
-
const summary = await runAttestorTeeVerification(proofs, attestorTeeAttestation);
|
|
41
|
+
const summary = await runAttestorTeeVerification(proofs, attestorTeeAttestation, { requireClaimAttestation: !!effective.attestorTeeAttestation });
|
|
39
42
|
isAttestorTeeAttestationVerified = summary.verified > 0;
|
|
40
43
|
}
|
|
41
44
|
return {
|
|
@@ -51,27 +54,30 @@ export async function verifyRequestProof(proofOrProofs, config) {
|
|
|
51
54
|
return {
|
|
52
55
|
valid: false,
|
|
53
56
|
reason: failureReason(error),
|
|
57
|
+
transient: isTransientFailure(error),
|
|
54
58
|
};
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
async function resolveValidation(config, proofs) {
|
|
58
62
|
let effective = config;
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
const { sessionId, resolveSession } = config;
|
|
64
|
+
if (sessionId && !hasContentValidation(config)) {
|
|
65
|
+
if (!resolveSession) {
|
|
61
66
|
throw new Error('session-resolver-required');
|
|
62
67
|
}
|
|
63
68
|
effective = {
|
|
64
69
|
...config,
|
|
65
|
-
...await
|
|
70
|
+
...await networkStep(() => resolveSession(sessionId, proofs)),
|
|
66
71
|
};
|
|
67
72
|
}
|
|
68
73
|
return effective;
|
|
69
74
|
}
|
|
70
75
|
async function resolveAttestorTrust(config) {
|
|
71
76
|
const addresses = config.attestorAddresses?.map(normalized) ?? [];
|
|
72
|
-
const imageDigests = [];
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
const imageDigests = config.attestorImageDigests?.map(normalized) ?? [];
|
|
78
|
+
const { attestorJwksUrl, fetchImpl } = config;
|
|
79
|
+
if (attestorJwksUrl) {
|
|
80
|
+
const registry = await networkStep(() => (fetchReclaimJwks(attestorJwksUrl, fetchImpl)));
|
|
75
81
|
addresses.push(...(registry['x-reclaim-attestors'] ?? []), ...trustedAttestorAddresses(registry.keys));
|
|
76
82
|
imageDigests.push(...(registry['x-reclaim-attestor-image-digests'] ?? []));
|
|
77
83
|
}
|
|
@@ -131,11 +137,12 @@ async function assertContent(proofs, config) {
|
|
|
131
137
|
assertValidProofHashes(proofs, config.hashes, config.hasNoPii);
|
|
132
138
|
return;
|
|
133
139
|
}
|
|
134
|
-
|
|
135
|
-
|
|
140
|
+
const { providerId, providerVersion, allowedTags, resolveProvider } = config;
|
|
141
|
+
if (providerId) {
|
|
142
|
+
if (!resolveProvider) {
|
|
136
143
|
throw new Error('provider-resolver-required');
|
|
137
144
|
}
|
|
138
|
-
const candidates = await
|
|
145
|
+
const candidates = await networkStep(() => (resolveProvider(providerId, providerVersion, allowedTags, proofs)));
|
|
139
146
|
if (!candidates.length) {
|
|
140
147
|
throw new Error('provider-not-found');
|
|
141
148
|
}
|
package/lib/result.d.ts
CHANGED
|
@@ -132,6 +132,9 @@ export interface ResultProofOutcome {
|
|
|
132
132
|
proof?: ReclaimRequestProof;
|
|
133
133
|
proofValid: boolean;
|
|
134
134
|
reason?: string;
|
|
135
|
+
/** On failure: true when a temporary condition caused it, such as a
|
|
136
|
+
* network error while resolving the provider. */
|
|
137
|
+
transient?: boolean;
|
|
135
138
|
/** @deprecated Use `attestorAddresses`; retained for compatibility. */
|
|
136
139
|
attestorAddress?: string;
|
|
137
140
|
/** Every attestor address declared by the proof. */
|
|
@@ -157,11 +160,17 @@ export interface VerifyResultFullOutcome extends VerifyResultOutcome {
|
|
|
157
160
|
}
|
|
158
161
|
export interface ResultVerificationFailure extends VerifyResultFullOutcome {
|
|
159
162
|
reason: string;
|
|
163
|
+
/** True when a temporary condition caused the failure. Set by the
|
|
164
|
+
* verifier; read it as {@link ResultVerificationError.retryable}. */
|
|
165
|
+
retryable?: boolean;
|
|
160
166
|
}
|
|
161
167
|
/**
|
|
162
168
|
* Thrown by {@link verifyResult} / {@link verifyResultFull} (and therefore
|
|
163
169
|
* `reclaim.results.verify`/`receive`) when a result fails verification. Carries
|
|
164
|
-
* the full invalid outcome so callers can inspect it in a `catch
|
|
170
|
+
* the full invalid outcome so callers can inspect it in a `catch`.
|
|
171
|
+
*
|
|
172
|
+
* In a callback handler, answer 2xx when `retryable` is `false` (Builder
|
|
173
|
+
* retrying the same delivery can't help) and 5xx when it's `true`:
|
|
165
174
|
*
|
|
166
175
|
* try {
|
|
167
176
|
* const { result } = await reclaim.results.receive(body, {
|
|
@@ -172,6 +181,7 @@ export interface ResultVerificationFailure extends VerifyResultFullOutcome {
|
|
|
172
181
|
* if (err instanceof ResultVerificationError) {
|
|
173
182
|
* // for example, 'expired', { payload, … }
|
|
174
183
|
* console.error(err.reason, err.outcome)
|
|
184
|
+
* res.sendStatus(err.retryable ? 503 : 200)
|
|
175
185
|
* }
|
|
176
186
|
* }
|
|
177
187
|
*/
|
|
@@ -182,6 +192,15 @@ export declare class ResultVerificationError extends Error {
|
|
|
182
192
|
/** The full (invalid) outcome: `payload`, `reclaimSessionId`, and per-proof
|
|
183
193
|
* detail (`proofs`) when verification got that far. */
|
|
184
194
|
readonly outcome: ResultVerificationFailure;
|
|
195
|
+
/**
|
|
196
|
+
* Whether a retry can succeed. `true` only for a temporary condition on
|
|
197
|
+
* the verifier's side: Builder's JWKS or recipe couldn't be fetched
|
|
198
|
+
* (network error, timeout, 5xx, 429). `false` when the result itself is
|
|
199
|
+
* wrong (signature, issuer, audience, session, expiry, invalid proof), so
|
|
200
|
+
* redelivering the same body fails the same way. Respond 5xx to a
|
|
201
|
+
* callback only when `true`.
|
|
202
|
+
*/
|
|
203
|
+
readonly retryable: boolean;
|
|
185
204
|
constructor(outcome: ResultVerificationFailure);
|
|
186
205
|
}
|
|
187
206
|
/**
|
package/lib/result.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { sha256 } from '@noble/hashes/sha2.js';
|
|
14
14
|
import { decrypt as eciesDecrypt, encrypt as eciesEncrypt } from 'eciesjs';
|
|
15
15
|
import { b64url, base64urlToBytes, hexToBytes, stripTrailingSlashes, } from "./encoding.js";
|
|
16
|
+
import { isTransientError } from "./errors.js";
|
|
16
17
|
import { fetchReclaimJwks, jwkToUncompressedPublicKey, selectVerificationResultKey, } from "./jwks.js";
|
|
17
18
|
import { getHashFromProof } from "./proof-hash.js";
|
|
18
19
|
import { proofHashesFromBuilderRecipe, } from "./proof-recipe.js";
|
|
@@ -145,7 +146,10 @@ export function verifyJwsSignature(jws, signerPublicKey) {
|
|
|
145
146
|
/**
|
|
146
147
|
* Thrown by {@link verifyResult} / {@link verifyResultFull} (and therefore
|
|
147
148
|
* `reclaim.results.verify`/`receive`) when a result fails verification. Carries
|
|
148
|
-
* the full invalid outcome so callers can inspect it in a `catch
|
|
149
|
+
* the full invalid outcome so callers can inspect it in a `catch`.
|
|
150
|
+
*
|
|
151
|
+
* In a callback handler, answer 2xx when `retryable` is `false` (Builder
|
|
152
|
+
* retrying the same delivery can't help) and 5xx when it's `true`:
|
|
149
153
|
*
|
|
150
154
|
* try {
|
|
151
155
|
* const { result } = await reclaim.results.receive(body, {
|
|
@@ -156,6 +160,7 @@ export function verifyJwsSignature(jws, signerPublicKey) {
|
|
|
156
160
|
* if (err instanceof ResultVerificationError) {
|
|
157
161
|
* // for example, 'expired', { payload, … }
|
|
158
162
|
* console.error(err.reason, err.outcome)
|
|
163
|
+
* res.sendStatus(err.retryable ? 503 : 200)
|
|
159
164
|
* }
|
|
160
165
|
* }
|
|
161
166
|
*/
|
|
@@ -165,6 +170,7 @@ export class ResultVerificationError extends Error {
|
|
|
165
170
|
this.name = 'ResultVerificationError';
|
|
166
171
|
this.reason = outcome.reason;
|
|
167
172
|
this.outcome = outcome;
|
|
173
|
+
this.retryable = outcome.retryable ?? false;
|
|
168
174
|
}
|
|
169
175
|
}
|
|
170
176
|
/**
|
|
@@ -230,8 +236,10 @@ export async function verifyResultFull(jws, opts = {}) {
|
|
|
230
236
|
jwksAttestorImageDigests =
|
|
231
237
|
registry['x-reclaim-attestor-image-digests'] ?? [];
|
|
232
238
|
}
|
|
233
|
-
catch {
|
|
234
|
-
return fail('jwks-fetch-failed'
|
|
239
|
+
catch (error) {
|
|
240
|
+
return fail('jwks-fetch-failed', {
|
|
241
|
+
retryable: isTransientError(error),
|
|
242
|
+
});
|
|
235
243
|
}
|
|
236
244
|
const jwk = selectVerificationResultKey(keys, String(header.kid ?? ''));
|
|
237
245
|
if (!jwk) {
|
|
@@ -348,7 +356,12 @@ export async function verifyResultFull(jws, opts = {}) {
|
|
|
348
356
|
else if (configuredImageDigests?.length) {
|
|
349
357
|
expectedImageDigests = configuredImageDigests;
|
|
350
358
|
}
|
|
351
|
-
|
|
359
|
+
// A caller-configured check requires a `claimAttestation` on every
|
|
360
|
+
// witness. Registry digests alone only pin the attestor reports that
|
|
361
|
+
// are present, as `@reclaimprotocol/js-sdk` does by default — a portal
|
|
362
|
+
// TEE proof without a bound nonce carries no envelope.
|
|
363
|
+
const attestorTeeAttestation = configuredAttestorTee
|
|
364
|
+
&& expectedImageDigests.length
|
|
352
365
|
? { ...configuredAttestorTee, expectedImageDigests }
|
|
353
366
|
: configuredAttestorTee;
|
|
354
367
|
const commonValidation = {
|
|
@@ -360,6 +373,9 @@ export async function verifyResultFull(jws, opts = {}) {
|
|
|
360
373
|
...(attestorTeeAttestation
|
|
361
374
|
? { attestorTeeAttestation }
|
|
362
375
|
: {}),
|
|
376
|
+
...(expectedImageDigests.length
|
|
377
|
+
? { attestorImageDigests: expectedImageDigests }
|
|
378
|
+
: {}),
|
|
363
379
|
...(expectedSessionId
|
|
364
380
|
? {
|
|
365
381
|
expectedSessionId,
|
|
@@ -375,7 +391,12 @@ export async function verifyResultFull(jws, opts = {}) {
|
|
|
375
391
|
}
|
|
376
392
|
}
|
|
377
393
|
if (!proofs.every((p) => p.proofValid)) {
|
|
378
|
-
|
|
394
|
+
// Retry only when every failure is transient; one genuinely invalid
|
|
395
|
+
// proof makes redelivery pointless.
|
|
396
|
+
return fail('invalid-proof', {
|
|
397
|
+
proofs,
|
|
398
|
+
retryable: proofs.every((p) => p.proofValid || p.transient),
|
|
399
|
+
});
|
|
379
400
|
}
|
|
380
401
|
return { payload, reclaimSessionId, proofs };
|
|
381
402
|
async function verifyRequestProofGroup(group, validation) {
|
|
@@ -403,6 +424,7 @@ export async function verifyResultFull(jws, opts = {}) {
|
|
|
403
424
|
for (const { outcomeIndex } of group) {
|
|
404
425
|
proofs[outcomeIndex].proofValid = false;
|
|
405
426
|
proofs[outcomeIndex].reason = requestProofOutcome.reason;
|
|
427
|
+
proofs[outcomeIndex].transient = requestProofOutcome.transient;
|
|
406
428
|
}
|
|
407
429
|
}
|
|
408
430
|
async function verifyByResolvedRecipes(common, expectedSessionId) {
|
|
@@ -412,8 +434,11 @@ export async function verifyResultFull(jws, opts = {}) {
|
|
|
412
434
|
const resolved = await opts.resolveSession(expectedSessionId);
|
|
413
435
|
recipes = resolved.recipes;
|
|
414
436
|
}
|
|
415
|
-
catch {
|
|
416
|
-
return fail('session-resolution-failed', {
|
|
437
|
+
catch (error) {
|
|
438
|
+
return fail('session-resolution-failed', {
|
|
439
|
+
proofs,
|
|
440
|
+
retryable: isTransientError(error),
|
|
441
|
+
});
|
|
417
442
|
}
|
|
418
443
|
}
|
|
419
444
|
const groups = Map.groupBy(proofGroups, proofGroupKey);
|
package/lib/routes.gen.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ export declare const ROUTES: {
|
|
|
3
3
|
readonly method: "post";
|
|
4
4
|
readonly path: "/orgs/{orgId}/invite";
|
|
5
5
|
};
|
|
6
|
+
readonly AcknowledgeCallbackEvents: {
|
|
7
|
+
readonly method: "post";
|
|
8
|
+
readonly path: "/verifications/callback-events/acknowledge";
|
|
9
|
+
};
|
|
6
10
|
readonly AddOrgMember: {
|
|
7
11
|
readonly method: "post";
|
|
8
12
|
readonly path: "/orgs/{orgId}/members";
|
|
@@ -91,6 +95,10 @@ export declare const ROUTES: {
|
|
|
91
95
|
readonly method: "post";
|
|
92
96
|
readonly path: "/admin/orgs/{orgId}/credits/{grantId}/void";
|
|
93
97
|
};
|
|
98
|
+
readonly AgentInstallationMarkdown: {
|
|
99
|
+
readonly method: "get";
|
|
100
|
+
readonly path: "/docs/agent-installation.md";
|
|
101
|
+
};
|
|
94
102
|
readonly AppleAppSiteAssociation: {
|
|
95
103
|
readonly method: "get";
|
|
96
104
|
readonly path: "/.well-known/apple-app-site-association";
|
|
@@ -391,6 +399,10 @@ export declare const ROUTES: {
|
|
|
391
399
|
readonly method: "get";
|
|
392
400
|
readonly path: "/verifications/callback-dlq";
|
|
393
401
|
};
|
|
402
|
+
readonly ListCallbackEvents: {
|
|
403
|
+
readonly method: "get";
|
|
404
|
+
readonly path: "/verifications/callback-events";
|
|
405
|
+
};
|
|
394
406
|
readonly ListCallbackSubscriptions: {
|
|
395
407
|
readonly method: "get";
|
|
396
408
|
readonly path: "/verifications/callback-subscriptions";
|
|
@@ -495,6 +507,10 @@ export declare const ROUTES: {
|
|
|
495
507
|
readonly method: "post";
|
|
496
508
|
readonly path: "/admin/billing/transactions/{txId}/refund";
|
|
497
509
|
};
|
|
510
|
+
readonly ReleaseCallbackListener: {
|
|
511
|
+
readonly method: "post";
|
|
512
|
+
readonly path: "/verifications/callback-listener/release";
|
|
513
|
+
};
|
|
498
514
|
readonly RemoveOrgMember: {
|
|
499
515
|
readonly method: "delete";
|
|
500
516
|
readonly path: "/orgs/{orgId}/members/{memberId}";
|
package/lib/routes.gen.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
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
|
export const ROUTES = {
|
|
7
7
|
"AcceptOrgInvite": { "method": "post", "path": "/orgs/{orgId}/invite" },
|
|
8
|
+
"AcknowledgeCallbackEvents": { "method": "post", "path": "/verifications/callback-events/acknowledge" },
|
|
8
9
|
"AddOrgMember": { "method": "post", "path": "/orgs/{orgId}/members" },
|
|
9
10
|
"AddProviderContributionComment": { "method": "post", "path": "/providers/{providerId}/contributions/{campaignId}/comments" },
|
|
10
11
|
"AddStaffMember": { "method": "post", "path": "/admin/staff" },
|
|
@@ -27,6 +28,7 @@ export const ROUTES = {
|
|
|
27
28
|
"AdminUsers": { "method": "get", "path": "/admin/users" },
|
|
28
29
|
"AdminVerificationClients": { "method": "get", "path": "/admin/verification-clients" },
|
|
29
30
|
"AdminVoidOrgCredit": { "method": "post", "path": "/admin/orgs/{orgId}/credits/{grantId}/void" },
|
|
31
|
+
"AgentInstallationMarkdown": { "method": "get", "path": "/docs/agent-installation.md" },
|
|
30
32
|
"AppleAppSiteAssociation": { "method": "get", "path": "/.well-known/apple-app-site-association" },
|
|
31
33
|
"AssetLinksJson": { "method": "get", "path": "/.well-known/assetlinks.json" },
|
|
32
34
|
"BindVerificationSessionTeeNonce": { "method": "put", "path": "/verifications/sessions/{sessionId}/tee-nonce" },
|
|
@@ -102,6 +104,7 @@ export const ROUTES = {
|
|
|
102
104
|
"JwksJson": { "method": "get", "path": "/.well-known/jwks.json" },
|
|
103
105
|
"ListBillingPlans": { "method": "get", "path": "/billing/plans" },
|
|
104
106
|
"ListCallbackDlq": { "method": "get", "path": "/verifications/callback-dlq" },
|
|
107
|
+
"ListCallbackEvents": { "method": "get", "path": "/verifications/callback-events" },
|
|
105
108
|
"ListCallbackSubscriptions": { "method": "get", "path": "/verifications/callback-subscriptions" },
|
|
106
109
|
"ListFeatureFlagRevisions": { "method": "get", "path": "/admin/feature-flags/{flagKey}/revisions" },
|
|
107
110
|
"ListOrgCallbackSubscriptions": { "method": "get", "path": "/orgs/{orgId}/callback-subscriptions" },
|
|
@@ -128,6 +131,7 @@ export const ROUTES = {
|
|
|
128
131
|
"PublishProviderVersion": { "method": "post", "path": "/providers/{providerId}/versions/{version}/publish" },
|
|
129
132
|
"RefreshBillingPlans": { "method": "post", "path": "/admin/billing/plans/refresh" },
|
|
130
133
|
"RefundTransaction": { "method": "post", "path": "/admin/billing/transactions/{txId}/refund" },
|
|
134
|
+
"ReleaseCallbackListener": { "method": "post", "path": "/verifications/callback-listener/release" },
|
|
131
135
|
"RemoveOrgMember": { "method": "delete", "path": "/orgs/{orgId}/members/{memberId}" },
|
|
132
136
|
"RemoveStaffMember": { "method": "delete", "path": "/admin/staff/{email}" },
|
|
133
137
|
"ReportClaimant": { "method": "post", "path": "/v/{sessionId}/claimant" },
|