@reclaimprotocol/client 0.1.29 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +255 -234
  2. package/lib/api.d.ts +2 -0
  3. package/lib/api.js +2 -0
  4. package/lib/client.js +2 -0
  5. package/lib/enckeys.d.ts +1 -1
  6. package/lib/enckeys.js +2 -2
  7. package/lib/encoding.d.ts +4 -0
  8. package/lib/encoding.js +10 -0
  9. package/lib/feature-flags.d.ts +9 -0
  10. package/lib/feature-flags.js +19 -0
  11. package/lib/index.d.ts +4 -4
  12. package/lib/index.js +4 -4
  13. package/lib/jwks.d.ts +8 -8
  14. package/lib/jwks.js +38 -17
  15. package/lib/launch/destination.d.ts +14 -0
  16. package/lib/launch/destination.js +89 -0
  17. package/lib/launch/device.d.ts +30 -0
  18. package/lib/launch/device.js +168 -0
  19. package/lib/launch/environment.d.ts +224 -0
  20. package/lib/launch/environment.js +98 -0
  21. package/lib/launch/errors.d.ts +20 -0
  22. package/lib/launch/errors.js +25 -0
  23. package/lib/launch/extension.d.ts +42 -0
  24. package/lib/launch/extension.js +72 -0
  25. package/lib/launch/flow.d.ts +82 -0
  26. package/lib/launch/flow.js +222 -0
  27. package/lib/launch/index.d.ts +16 -0
  28. package/lib/launch/index.js +12 -0
  29. package/lib/launch/mobile.d.ts +77 -0
  30. package/lib/launch/mobile.js +124 -0
  31. package/lib/launch/modal.d.ts +50 -0
  32. package/lib/launch/modal.js +258 -0
  33. package/lib/launch/options.d.ts +74 -0
  34. package/lib/launch/options.js +20 -0
  35. package/lib/launch/portal.d.ts +28 -0
  36. package/lib/launch/portal.js +68 -0
  37. package/lib/launch/qr.d.ts +5 -0
  38. package/lib/launch/qr.js +28 -0
  39. package/lib/launch/target.d.ts +46 -0
  40. package/lib/launch/target.js +128 -0
  41. package/lib/launch/vendor/qrcodegen.d.ts +134 -0
  42. package/lib/launch/vendor/qrcodegen.js +822 -0
  43. package/lib/legacy-proof.d.ts +2 -0
  44. package/lib/legacy-proof.js +37 -7
  45. package/lib/proof-hash.d.ts +5 -5
  46. package/lib/proof-hash.js +1 -1
  47. package/lib/proof-recipe.d.ts +3 -3
  48. package/lib/proof-recipe.js +1 -1
  49. package/lib/proof-types.d.ts +35 -8
  50. package/lib/reclaim.d.ts +81 -70
  51. package/lib/reclaim.js +79 -130
  52. package/lib/request-proof.d.ts +60 -0
  53. package/lib/request-proof.js +241 -0
  54. package/lib/result.d.ts +46 -38
  55. package/lib/result.js +160 -100
  56. package/lib/routes.gen.d.ts +76 -28
  57. package/lib/routes.gen.js +24 -11
  58. package/lib/tee-binding.d.ts +1 -1
  59. package/lib/types/openapi.d.ts +1 -0
  60. package/lib/types/openapi.gen.d.ts +1598 -485
  61. package/lib/types/openapi.gen.js +5 -0
  62. package/lib/verification-clients.d.ts +28 -14
  63. package/lib/verification-clients.js +31 -11
  64. package/lib/verify-attestor-tee.d.ts +19 -2
  65. package/lib/verify-attestor-tee.js +40 -4
  66. package/lib/verify-tee.d.ts +10 -1
  67. package/lib/verify-tee.js +34 -10
  68. package/package.json +5 -1
  69. package/src/api.ts +9 -0
  70. package/src/client.ts +2 -0
  71. package/src/enckeys.ts +2 -2
  72. package/src/encoding.ts +12 -0
  73. package/src/feature-flags.ts +39 -0
  74. package/src/index.ts +13 -14
  75. package/src/jwks.ts +52 -30
  76. package/src/launch/destination.ts +111 -0
  77. package/src/launch/device.ts +242 -0
  78. package/src/launch/environment.ts +337 -0
  79. package/src/launch/errors.ts +49 -0
  80. package/src/launch/extension.ts +124 -0
  81. package/src/launch/flow.ts +369 -0
  82. package/src/launch/index.ts +42 -0
  83. package/src/launch/mobile.ts +184 -0
  84. package/src/launch/modal.ts +378 -0
  85. package/src/launch/options.ts +99 -0
  86. package/src/launch/portal.ts +104 -0
  87. package/src/launch/qr.ts +37 -0
  88. package/src/launch/target.ts +150 -0
  89. package/src/launch/vendor/qrcodegen.ts +1034 -0
  90. package/src/proof-hash.ts +6 -6
  91. package/src/proof-recipe.ts +3 -3
  92. package/src/proof-types.ts +36 -8
  93. package/src/reclaim.ts +185 -218
  94. package/src/{legacy-proof.ts → request-proof.ts} +81 -36
  95. package/src/result.ts +238 -172
  96. package/src/routes.gen.ts +24 -11
  97. package/src/tee-binding.ts +1 -1
  98. package/src/types/openapi.gen.ts +1603 -485
  99. package/src/types/openapi.ts +3 -1
  100. package/src/verification-clients.ts +33 -19
  101. package/src/verify-attestor-tee.ts +66 -8
  102. package/src/verify-tee.ts +44 -13
  103. package/lib/api-client.d.ts +0 -31
  104. package/lib/api-client.js +0 -112
  105. package/lib/api-consts.d.ts +0 -4
  106. package/lib/api-consts.js +0 -4
  107. package/lib/api-errors.d.ts +0 -13
  108. package/lib/api-errors.js +0 -30
  109. package/lib/eip712.d.ts +0 -20
  110. package/lib/eip712.js +0 -108
package/lib/reclaim.js CHANGED
@@ -1,26 +1,25 @@
1
1
  import { ReclaimClient } from "./client.js";
2
2
  import { loadDecryptionKey, } from "./enckeys.js";
3
- import { isProblemError } from "./errors.js";
4
3
  import { proofHashesFromBuilderRecipe } from "./proof-recipe.js";
5
4
  import { decryptCallback, verifyResultFull, } from "./result.js";
6
5
  import { createBuilderTeeBinding, } from "./tee-binding.js";
7
- /** Terminal lifecycle events selected by default for callback subscriptions. */
8
- export const TERMINAL_EVENTS = [
9
- 'verification_success',
10
- 'verification_rejected',
11
- 'verification_error',
12
- 'verification_cancelled',
13
- 'session_expired',
14
- ];
15
6
  const SIGNED_RESULT_EVENTS = [
16
7
  'verification_success',
17
8
  'verification_rejected',
18
9
  'verification_error',
19
10
  ];
20
- export function createReclaim(config) {
11
+ export const ReclaimVerification = Object.freeze({
12
+ create: createReclaimVerification,
13
+ import: importReclaimVerification,
14
+ });
15
+ function importReclaimVerification(serialized, config) {
16
+ const state = parseReclaimVerificationState(serialized);
17
+ return createReclaimVerification({ ...config, baseUrl: state.baseUrl });
18
+ }
19
+ function createReclaimVerification(config) {
21
20
  if (!config?.orgSecret) {
22
- throw new Error('createReclaim requires an org secret — pass '
23
- + 'createReclaim({ orgSecret: <rorg_… org secret> })');
21
+ throw new Error('ReclaimVerification.create requires an org secret — pass '
22
+ + 'ReclaimVerification.create({ orgSecret: <rorg_… org secret> })');
24
23
  }
25
24
  const client = new ReclaimClient({
26
25
  baseUrl: config.baseUrl,
@@ -32,99 +31,42 @@ export function createReclaim(config) {
32
31
  // plaintext-only consumers never touch the loader.
33
32
  const resolveKey = (input) => (input === undefined ? undefined : loadDecryptionKey(input));
34
33
  return {
34
+ export() {
35
+ return JSON.stringify({
36
+ version: 1,
37
+ baseUrl: client.baseUrl,
38
+ });
39
+ },
35
40
  api: client.call.bind(client),
36
41
  sessions: {
37
- async create(input) {
38
- const { teeAttestation, ...request } = input;
39
- const body = {
40
- ...request,
41
- verificationClientUrl: request.verificationClientUrl ?? `${client.baseUrl}/v/{sessionId}`,
42
- };
43
- const { data } = await client.call('CreateVerificationSession', {
44
- body,
45
- });
46
- return teeAttestation
47
- ? bindTeeNonce(data, teeAttestation)
48
- : data;
49
- },
50
- async bindTeeNonce(sessionId, input) {
42
+ create: createSession,
43
+ async bindTeeNonce(reclaimSessionId, input) {
51
44
  const { data } = await client.call('GetVerificationSession', {
52
- params: { sessionId },
45
+ params: { sessionId: reclaimSessionId },
53
46
  });
54
47
  return bindTeeNonce(data, input);
55
48
  },
56
- async get(sessionId) {
49
+ async get(reclaimSessionId) {
57
50
  const { data } = await client.call('GetVerificationSession', {
58
- params: { sessionId },
51
+ params: { sessionId: reclaimSessionId },
59
52
  });
60
53
  return data;
61
54
  },
62
- async listEvents(sessionId) {
55
+ async listEvents(reclaimSessionId) {
63
56
  const { data } = await client.call('ListVerificationEvents', {
64
- params: { sessionId },
57
+ params: { sessionId: reclaimSessionId },
65
58
  });
66
59
  return data.items;
67
60
  },
68
61
  },
69
- callbacks: {
70
- async register(input) {
71
- const events = input.events ?? [...TERMINAL_EVENTS];
72
- const { data } = await client.call('CreateCallbackSubscription', {
73
- body: { callbackUrl: input.callbackUrl, events },
74
- });
75
- return data;
76
- },
77
- async list() {
78
- const { data } = await client.call('ListCallbackSubscriptions');
79
- return data.items;
80
- },
81
- async remove(subscriptionId) {
82
- await client.call('DeleteCallbackSubscription', {
83
- params: { subscriptionId },
84
- });
85
- },
86
- },
87
- keypair: {
88
- async set(orgId, input) {
89
- // Forward the eth public key as given (hex or JWK); the API stores
90
- // the secp256k1 JWK + derived address.
91
- const body = {
92
- ...(input.publicKeyJwk
93
- ? { publicKeyJwk: input.publicKeyJwk }
94
- : { publicKey: input.publicKey }),
95
- canEncryptResult: input.canEncryptResult ?? false,
96
- ...(input.label ? { label: input.label } : {}),
97
- };
98
- const { data } = await client.call('SetOrgKeypair', {
99
- params: { orgId },
100
- body,
101
- });
102
- return data;
103
- },
104
- async get(orgId) {
105
- try {
106
- const { data } = await client.call('GetOrgKeypair', {
107
- params: { orgId },
108
- });
109
- return data;
110
- }
111
- catch (err) {
112
- // 404 ⇒ keyless org (plaintext delivery).
113
- if (isNotFound(err)) {
114
- return undefined;
115
- }
116
- throw err;
117
- }
118
- },
119
- },
120
62
  results: {
121
- async decrypt(body, credential) {
122
- return decryptCallback(resolveKey(credential), body);
63
+ async decrypt(body, orgEthPrivateKey) {
64
+ return decryptCallback(resolveKey(orgEthPrivateKey), body);
123
65
  },
124
66
  verify(jws, opts) {
125
67
  return verifyResultFull(jws, {
126
68
  fetchImpl: config.fetch,
127
- ...withRegisteredIssuer(opts),
69
+ ...withBuilderTrust(opts),
128
70
  });
129
71
  },
130
72
  async receive(body, opts) {
@@ -145,24 +87,44 @@ export function createReclaim(config) {
145
87
  };
146
88
  }
147
89
  }
148
- const { credential, ...verifyOpts } = opts ?? {};
149
- const key = resolveKey(credential);
90
+ const { orgEthPrivateKey, ...verifyOpts } = opts ?? {};
91
+ const key = resolveKey(orgEthPrivateKey);
150
92
  const jws = await decryptCallback(key, body);
151
93
  const result = await verifyResultFull(jws, {
152
94
  fetchImpl: config.fetch,
153
- ...withRegisteredIssuer(verifyOpts),
95
+ ...withBuilderTrust({ ...verifyOpts, orgEthPrivateKey }),
154
96
  });
155
97
  return { kind: 'result', result };
156
98
  },
157
99
  },
100
+ flow: Object.assign((session) => makeSessionFlow(session), {
101
+ async create(input) {
102
+ return makeSessionFlow(await createSession(input));
103
+ },
104
+ }),
158
105
  };
106
+ async function createSession(input) {
107
+ const { orgEthPrivateKey, teeNonceMode, ...request } = input;
108
+ const body = {
109
+ ...request,
110
+ };
111
+ const { data } = await client.call('CreateVerificationSession', {
112
+ body,
113
+ });
114
+ return orgEthPrivateKey
115
+ ? bindTeeNonce(data, {
116
+ orgEthPrivateKey,
117
+ teeNonceMode,
118
+ })
119
+ : data;
120
+ }
159
121
  async function bindTeeNonce(session, input) {
160
122
  const binding = createBuilderTeeBinding({
161
123
  organizationId: session.orgId,
162
- organizationVerificationPrivateKey: input.appSecret,
124
+ organizationVerificationPrivateKey: input.orgEthPrivateKey,
163
125
  sessionId: session.id,
164
126
  timestamp: input.timestamp ?? Date.now().toString(),
165
- nonceMode: input.nonceMode,
127
+ nonceMode: input.teeNonceMode,
166
128
  });
167
129
  const { data } = await client.call('BindVerificationSessionTeeNonce', {
168
130
  params: { sessionId: session.id },
@@ -202,10 +164,9 @@ export function createReclaim(config) {
202
164
  }, proofs),
203
165
  }];
204
166
  }
205
- function withRegisteredIssuer(opts = {}) {
206
- const sessionId = opts.sessionId ?? opts.expectedReclaimSessionId;
207
- const requestedProofValidation = opts.proofValidation
208
- ?? opts.proofVerification;
167
+ function withBuilderTrust(opts = {}) {
168
+ const reclaimSessionId = opts.reclaimSessionId;
169
+ const requestedProofValidation = opts.proofValidation;
209
170
  const resolvedOpts = {
210
171
  ...opts,
211
172
  proofValidation: {
@@ -213,51 +174,30 @@ export function createReclaim(config) {
213
174
  resolveProvider: requestedProofValidation?.resolveProvider
214
175
  ?? resolvedProvider,
215
176
  },
216
- ...(sessionId && !opts.resolveSession && !opts.proofRecipes
177
+ ...(reclaimSessionId && !opts.resolveSession && !opts.proofRecipes
217
178
  ? { resolveSession: resolvedSession }
218
179
  : {}),
219
180
  };
220
181
  if (opts.signerPublicKey
221
- || opts.issuer
222
- || opts.trustedIssuer
223
- || opts.issuers?.length
224
182
  || opts.trustedIssuers?.length
225
- || opts.resolveIssuers
226
- || opts.issuerResolver
227
- || opts.issuersByVerificationClientId
228
- || opts.jwksUrl
229
- || opts.jwksUrlsByVerificationClientId
230
- || opts.resolveJwksUrl) {
183
+ || opts.jwksUrl) {
231
184
  return resolvedOpts;
232
185
  }
233
186
  return {
234
187
  ...resolvedOpts,
235
- resolveIssuers: async (verificationClientId) => {
236
- const session = await registeredClient(verificationClientId);
237
- if (!session) {
238
- return [];
239
- }
240
- return session.verificationClientIssuer
241
- ? [session.verificationClientIssuer]
242
- : [];
243
- },
244
- resolveJwksUrl: async (verificationClientId) => {
245
- const session = await registeredClient(verificationClientId);
246
- return session?.verificationClientJwksUrl;
247
- },
188
+ trustedIssuers: [client.baseUrl],
189
+ jwksUrl: `${client.baseUrl}/.well-known/jwks.json`,
248
190
  };
249
- async function registeredClient(verificationClientId) {
250
- if (!sessionId) {
251
- return undefined;
252
- }
253
- const { data: session } = await client.call('GetVerificationSession', { params: { sessionId } });
254
- if (session.verificationClientId !== verificationClientId) {
255
- return undefined;
256
- }
257
- return session;
258
- }
259
191
  }
260
192
  }
193
+ function makeSessionFlow(session) {
194
+ return {
195
+ session,
196
+ async getUrl() {
197
+ return session.verificationUrl;
198
+ },
199
+ };
200
+ }
261
201
  function canonicalVersion(version) {
262
202
  const base = `${version.major}.${version.minor}.${version.patch}`;
263
203
  return version.branch
@@ -279,7 +219,16 @@ function parseEventData(data) {
279
219
  return {};
280
220
  }
281
221
  }
282
- /** True for a `404` thrown by {@link ReclaimClient.call}. */
283
- function isNotFound(err) {
284
- return isProblemError(err) && err.output.statusCode === 404;
222
+ function parseReclaimVerificationState(serialized) {
223
+ const state = JSON.parse(serialized);
224
+ if (!state
225
+ || typeof state !== 'object'
226
+ || Array.isArray(state)
227
+ || !('version' in state)
228
+ || state.version !== 1
229
+ || !('baseUrl' in state)
230
+ || typeof state.baseUrl !== 'string') {
231
+ throw new Error('Invalid ReclaimVerification state');
232
+ }
233
+ return { version: 1, baseUrl: state.baseUrl };
285
234
  }
@@ -0,0 +1,60 @@
1
+ import type { HashRequirement, ReclaimRequestProof, TrustedData } from './proof-types.ts';
2
+ import { type AttestorTeeAttestationConfig } from './verify-attestor-tee.ts';
3
+ import { type TeeAttestationConfig } from './verify-tee.ts';
4
+ export type { ReclaimRequestProof } from './proof-types.ts';
5
+ /** A proof hash requirement accepted by `verifyRequestProof`. */
6
+ export type ProofHashRequirement = string | HashRequirement;
7
+ export interface ResolvedSessionProofValidation {
8
+ hashes?: ProofHashRequirement[];
9
+ providerId?: string;
10
+ providerVersion?: string;
11
+ allowedTags?: string[];
12
+ }
13
+ /** Exact provider requirements returned by a caller-controlled resolver. */
14
+ export interface ResolvedProviderProofValidation {
15
+ hashes: ProofHashRequirement[];
16
+ }
17
+ /** Field names match the request spec a provider recipe declares, so a
18
+ * caller configuring verification reads the same vocabulary it authored. */
19
+ export interface VerifyRequestProofOptions {
20
+ hashes?: ProofHashRequirement[];
21
+ providerId?: string;
22
+ providerVersion?: string;
23
+ allowedTags?: string[];
24
+ dangerouslyDisableContentValidation?: true;
25
+ /** Read `providerHash` from context, while still verifying signatures. */
26
+ hasNoPii?: true;
27
+ teeAttestation?: TeeAttestationConfig;
28
+ attestorTeeAttestation?: AttestorTeeAttestationConfig;
29
+ /** Explicit trusted attestor ETH addresses. */
30
+ attestorAddresses?: string[];
31
+ /** Trusted Builder JWKS registry containing attestor-purpose keys. */
32
+ attestorJwksUrl?: string;
33
+ fetchImpl?: typeof fetch;
34
+ /** Builder session id. Requires a resolver without hashes or a provider. */
35
+ sessionId?: string;
36
+ resolveSession?: (sessionId: string, proofs: ReclaimRequestProof[]) => Promise<ResolvedSessionProofValidation> | ResolvedSessionProofValidation;
37
+ /** Resolve an exact provider without coupling this verifier to HTTP. */
38
+ resolveProvider?: (providerId: string, providerVersion: string | undefined, allowedTags: string[] | undefined, proofs: ReclaimRequestProof[]) => Promise<ResolvedProviderProofValidation[]> | ResolvedProviderProofValidation[];
39
+ expectedSessionId?: string;
40
+ requireSessionBinding?: boolean;
41
+ expectedContext?: Record<string, unknown>;
42
+ }
43
+ export interface ReclaimRequestProofOutcome {
44
+ valid: boolean;
45
+ reason?: string;
46
+ proofHashes?: string[];
47
+ isTeeAttestationVerified?: boolean;
48
+ /** True when at least one witness's `attestation_report` verified; false
49
+ * when the check ran and no witness carried one. */
50
+ isAttestorTeeAttestationVerified?: boolean;
51
+ data?: TrustedData[];
52
+ publicData?: unknown[];
53
+ }
54
+ /** Return the provider hashes encoded in an exact request proof. */
55
+ export declare function proofHashes(proof: ReclaimRequestProof): string[];
56
+ /**
57
+ * Verify exact request proofs without depending on `@reclaimprotocol/js-sdk`.
58
+ * The signed claim is never renamed or reconstructed.
59
+ */
60
+ export declare function verifyRequestProof(proofOrProofs: ReclaimRequestProof | ReclaimRequestProof[], config: VerifyRequestProofOptions): Promise<ReclaimRequestProofOutcome>;
@@ -0,0 +1,241 @@
1
+ import { createSignDataForClaim } from '@reclaimprotocol/attestor-core';
2
+ import { recoverEip191Address } from "./eip191.js";
3
+ import { fetchReclaimJwks, trustedAttestorAddresses } from "./jwks.js";
4
+ import { assertValidProofHashes, getHashFromProof } from "./proof-hash.js";
5
+ import { runAttestorTeeVerification, } from "./verify-attestor-tee.js";
6
+ import { runTeeVerification } from "./verify-tee.js";
7
+ /** Return the provider hashes encoded in an exact request proof. */
8
+ export function proofHashes(proof) {
9
+ return getHashFromProof(proof);
10
+ }
11
+ /**
12
+ * Verify exact request proofs without depending on `@reclaimprotocol/js-sdk`.
13
+ * The signed claim is never renamed or reconstructed.
14
+ */
15
+ export async function verifyRequestProof(proofOrProofs, config) {
16
+ const proofs = Array.isArray(proofOrProofs) ? proofOrProofs : [proofOrProofs];
17
+ if (!proofs.length) {
18
+ return { valid: false, reason: 'no-proofs' };
19
+ }
20
+ try {
21
+ const effective = await resolveValidation(config, proofs);
22
+ assertSessionAndContext(proofs, effective);
23
+ const trust = await resolveAttestorTrust(effective);
24
+ assertVerifiedSignatures(proofs, trust.addresses);
25
+ await assertContent(proofs, effective);
26
+ let isTeeAttestationVerified;
27
+ if (effective.teeAttestation) {
28
+ await runTeeVerification(proofs, effective.teeAttestation);
29
+ isTeeAttestationVerified = true;
30
+ }
31
+ let isAttestorTeeAttestationVerified;
32
+ const attestorTeeAttestation = resolveAttestorTeeAttestation(effective.attestorTeeAttestation, trust.imageDigests);
33
+ if (attestorTeeAttestation) {
34
+ // `verified` counts witnesses whose attestation report was checked.
35
+ // A proof whose witnesses carry no report still passes the address
36
+ // and signature bindings, but nothing was attested — say so rather
37
+ // than reporting a TEE attestation that never happened.
38
+ const summary = await runAttestorTeeVerification(proofs, attestorTeeAttestation);
39
+ isAttestorTeeAttestationVerified = summary.verified > 0;
40
+ }
41
+ return {
42
+ valid: true,
43
+ proofHashes: collectProofHashes(proofs, effective.hasNoPii),
44
+ isTeeAttestationVerified,
45
+ isAttestorTeeAttestationVerified,
46
+ data: proofs.map(trustedData),
47
+ publicData: proofs.map((proof) => proof.publicData ?? {}),
48
+ };
49
+ }
50
+ catch (error) {
51
+ return {
52
+ valid: false,
53
+ reason: failureReason(error),
54
+ };
55
+ }
56
+ }
57
+ async function resolveValidation(config, proofs) {
58
+ let effective = config;
59
+ if (config.sessionId && !hasContentValidation(config)) {
60
+ if (!config.resolveSession) {
61
+ throw new Error('session-resolver-required');
62
+ }
63
+ effective = {
64
+ ...config,
65
+ ...await config.resolveSession(config.sessionId, proofs),
66
+ };
67
+ }
68
+ return effective;
69
+ }
70
+ async function resolveAttestorTrust(config) {
71
+ const addresses = config.attestorAddresses?.map(normalized) ?? [];
72
+ const imageDigests = [];
73
+ if (config.attestorJwksUrl) {
74
+ const registry = await fetchReclaimJwks(config.attestorJwksUrl, config.fetchImpl);
75
+ addresses.push(...(registry['x-reclaim-attestors'] ?? []), ...trustedAttestorAddresses(registry.keys));
76
+ imageDigests.push(...(registry['x-reclaim-attestor-image-digests'] ?? []));
77
+ }
78
+ const unique = [...new Set(addresses)];
79
+ if (!unique.length) {
80
+ throw new Error('attestor-trust-required');
81
+ }
82
+ return {
83
+ addresses: unique,
84
+ imageDigests: [...new Set(imageDigests)],
85
+ };
86
+ }
87
+ function resolveAttestorTeeAttestation(configured, registryImageDigests) {
88
+ if (!registryImageDigests.length) {
89
+ return configured;
90
+ }
91
+ const configuredDigests = configured?.expectedImageDigests;
92
+ if (configuredDigests?.length) {
93
+ const registry = new Set(registryImageDigests);
94
+ const expectedImageDigests = [...new Set(configuredDigests
95
+ .map((digest) => digest.trim().toLowerCase())
96
+ .filter((digest) => registry.has(digest)))];
97
+ if (!expectedImageDigests.length) {
98
+ throw new Error('attestor-image-trust-mismatch');
99
+ }
100
+ return { ...configured, expectedImageDigests };
101
+ }
102
+ return {
103
+ ...configured,
104
+ expectedImageDigests: registryImageDigests,
105
+ };
106
+ }
107
+ function assertVerifiedSignatures(proofs, trustedAttestors) {
108
+ for (const proof of proofs) {
109
+ if (!proof.signatures?.length || !proof.witnesses?.length) {
110
+ throw new Error('proof-signature-missing');
111
+ }
112
+ const signData = createSignDataForClaim(proof.claimData);
113
+ const signers = proof.signatures.map((signature) => (normalized(recoverEip191Address(signData, signature))));
114
+ const witnesses = proof.witnesses.map(({ id }) => normalized(id));
115
+ if (witnesses.some((witness) => !trustedAttestors.includes(witness))) {
116
+ throw new Error('attestor-not-allowed');
117
+ }
118
+ if (!signers.some((signer) => trustedAttestors.includes(signer))) {
119
+ throw new Error('attestor-not-allowed');
120
+ }
121
+ if (!signers.every((signer) => witnesses.includes(signer))) {
122
+ throw new Error('witness-signature-mismatch');
123
+ }
124
+ }
125
+ }
126
+ async function assertContent(proofs, config) {
127
+ if (config.dangerouslyDisableContentValidation) {
128
+ return;
129
+ }
130
+ if (config.hashes) {
131
+ assertValidProofHashes(proofs, config.hashes, config.hasNoPii);
132
+ return;
133
+ }
134
+ if (config.providerId) {
135
+ if (!config.resolveProvider) {
136
+ throw new Error('provider-resolver-required');
137
+ }
138
+ const candidates = await config.resolveProvider(config.providerId, config.providerVersion, config.allowedTags, proofs);
139
+ if (!candidates.length) {
140
+ throw new Error('provider-not-found');
141
+ }
142
+ let lastError;
143
+ for (const candidate of candidates) {
144
+ try {
145
+ assertValidProofHashes(proofs, candidate.hashes, config.hasNoPii);
146
+ return;
147
+ }
148
+ catch (error) {
149
+ lastError = error;
150
+ }
151
+ }
152
+ throw lastError;
153
+ }
154
+ throw new Error('proof-validation-required');
155
+ }
156
+ function assertSessionAndContext(proofs, config) {
157
+ const expectedSessionId = config.sessionId ?? config.expectedSessionId;
158
+ for (const proof of proofs) {
159
+ if (expectedSessionId) {
160
+ const ids = sessionIds(proof);
161
+ if (ids.some((id) => id !== expectedSessionId)) {
162
+ throw new Error('session-mismatch');
163
+ }
164
+ if ((config.sessionId || config.requireSessionBinding) && !ids.length) {
165
+ throw new Error('session-missing');
166
+ }
167
+ }
168
+ if (!contextMatches(proof, config.expectedContext)) {
169
+ throw new Error('context-mismatch');
170
+ }
171
+ }
172
+ }
173
+ function trustedData(proof) {
174
+ const context = contextFrom(proof);
175
+ const extractedParameters = isRecord(context.extractedParameters)
176
+ ? Object.fromEntries(Object.entries(context.extractedParameters).map(([key, value]) => [key, String(value)]))
177
+ : {};
178
+ delete context.extractedParameters;
179
+ return { context, extractedParameters };
180
+ }
181
+ function collectProofHashes(proofs, hasNoPii) {
182
+ try {
183
+ return proofs.flatMap((proof) => getHashFromProof(proof, hasNoPii));
184
+ }
185
+ catch {
186
+ return undefined;
187
+ }
188
+ }
189
+ function hasContentValidation(config) {
190
+ return Boolean(config.hashes
191
+ || config.providerId
192
+ || config.dangerouslyDisableContentValidation);
193
+ }
194
+ function failureReason(error) {
195
+ const message = error instanceof Error ? error.message : String(error);
196
+ const lower = message.toLowerCase();
197
+ if (lower.includes('multiple')
198
+ || lower.includes('duplicate')
199
+ || lower.includes('more than once')) {
200
+ return 'duplicate-proof-hash';
201
+ }
202
+ return message || 'proof-not-verified';
203
+ }
204
+ function normalized(value) {
205
+ return value.trim().toLowerCase();
206
+ }
207
+ function contextFrom(proof) {
208
+ const raw = proof.claimData.context;
209
+ if (!raw) {
210
+ return {};
211
+ }
212
+ try {
213
+ const context = JSON.parse(raw);
214
+ return isRecord(context) ? { ...context } : {};
215
+ }
216
+ catch {
217
+ return {};
218
+ }
219
+ }
220
+ function sessionIds(proof) {
221
+ const context = contextFrom(proof);
222
+ const ids = [context.reclaimSessionId];
223
+ try {
224
+ const parameters = JSON.parse(proof.claimData.parameters);
225
+ ids.push(parameters.proxySessionId, parameters.sessionId);
226
+ }
227
+ catch {
228
+ // A non-HTTP claim is valid when session binding wasn't requested.
229
+ }
230
+ return ids.filter((value) => (typeof value === 'string' && value.length > 0));
231
+ }
232
+ function contextMatches(proof, expected) {
233
+ if (!expected) {
234
+ return true;
235
+ }
236
+ const context = contextFrom(proof);
237
+ return Object.entries(expected).every(([key, value]) => (JSON.stringify(context[key]) === JSON.stringify(value)));
238
+ }
239
+ function isRecord(value) {
240
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
241
+ }