@reclaimprotocol/client 0.1.26 → 0.1.28

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 (73) hide show
  1. package/README.md +634 -42
  2. package/lib/api-client.d.ts +31 -0
  3. package/lib/api-client.js +112 -0
  4. package/lib/api-consts.d.ts +4 -0
  5. package/lib/api-consts.js +4 -0
  6. package/lib/api-errors.d.ts +13 -0
  7. package/lib/api-errors.js +30 -0
  8. package/lib/api.d.ts +7 -0
  9. package/lib/api.js +3 -0
  10. package/lib/attestor-proof.d.ts +6 -0
  11. package/lib/attestor-proof.js +45 -18
  12. package/lib/client.d.ts +1 -6
  13. package/lib/client.js +12 -33
  14. package/lib/consts.d.ts +1 -1
  15. package/lib/consts.js +1 -1
  16. package/lib/eip191.d.ts +4 -2
  17. package/lib/eip191.js +17 -1
  18. package/lib/errors.d.ts +38 -4
  19. package/lib/errors.js +99 -16
  20. package/lib/index.d.ts +9 -7
  21. package/lib/index.js +8 -4
  22. package/lib/jwks.d.ts +25 -1
  23. package/lib/jwks.js +69 -4
  24. package/lib/legacy-proof.d.ts +57 -0
  25. package/lib/legacy-proof.js +211 -0
  26. package/lib/proof-hash.d.ts +35 -0
  27. package/lib/proof-hash.js +216 -0
  28. package/lib/proof-recipe.d.ts +18 -0
  29. package/lib/proof-recipe.js +37 -0
  30. package/lib/proof-trust-constants.d.ts +3 -0
  31. package/lib/proof-trust-constants.js +43 -0
  32. package/lib/proof-types.d.ts +62 -0
  33. package/lib/proof-types.js +1 -0
  34. package/lib/proof-verification-error.d.ts +13 -0
  35. package/lib/proof-verification-error.js +11 -0
  36. package/lib/reclaim.d.ts +27 -5
  37. package/lib/reclaim.js +140 -14
  38. package/lib/result.d.ts +58 -6
  39. package/lib/result.js +252 -61
  40. package/lib/routes.gen.d.ts +58 -6
  41. package/lib/routes.gen.js +16 -3
  42. package/lib/tee-binding.d.ts +82 -0
  43. package/lib/tee-binding.js +112 -0
  44. package/lib/types/openapi.gen.d.ts +1412 -193
  45. package/lib/verification-clients.d.ts +17 -0
  46. package/lib/verification-clients.js +14 -0
  47. package/lib/verify-attestor-tee.d.ts +76 -0
  48. package/lib/verify-attestor-tee.js +243 -0
  49. package/lib/verify-tee.d.ts +32 -0
  50. package/lib/verify-tee.js +347 -0
  51. package/package.json +5 -1
  52. package/src/api.ts +15 -0
  53. package/src/attestor-proof.ts +57 -20
  54. package/src/client.ts +15 -38
  55. package/src/consts.ts +1 -2
  56. package/src/eip191.ts +31 -3
  57. package/src/errors.ts +132 -18
  58. package/src/index.ts +50 -5
  59. package/src/jwks.ts +128 -5
  60. package/src/legacy-proof.ts +367 -0
  61. package/src/proof-hash.ts +316 -0
  62. package/src/proof-recipe.ts +75 -0
  63. package/src/proof-trust-constants.ts +45 -0
  64. package/src/proof-types.ts +57 -0
  65. package/src/proof-verification-error.ts +13 -0
  66. package/src/reclaim.ts +206 -19
  67. package/src/result.ts +403 -68
  68. package/src/routes.gen.ts +16 -3
  69. package/src/tee-binding.ts +192 -0
  70. package/src/types/openapi.gen.ts +1412 -193
  71. package/src/verification-clients.ts +22 -0
  72. package/src/verify-attestor-tee.ts +365 -0
  73. package/src/verify-tee.ts +507 -0
package/src/errors.ts CHANGED
@@ -1,30 +1,144 @@
1
- import { Boom } from '@hapi/boom'
2
1
  import type { ProblemDetails } from './types/openapi.ts'
3
2
  import { PROBLEM_CONTENT_TYPE } from './consts.ts'
4
- export type { ProblemDetails } from './types/openapi.ts'
5
3
 
6
- export type ProblemError = Boom<ProblemDetails>
4
+ type ProblemErrorOutput = {
5
+ statusCode: number
6
+ headers: Record<string, string>
7
+ payload: ProblemDetails & {
8
+ statusCode: number
9
+ error: string
10
+ message: string
11
+ }
12
+ }
13
+
14
+ /**
15
+ * RFC 9457 error with the fields expected by Boom consumers.
16
+ *
17
+ * `@hapi/boom`'s `isBoom()` recognizes this structural contract. The client
18
+ * doesn't import Boom at runtime, so this error remains browser-safe.
19
+ */
20
+ export type ProblemError = Error & {
21
+ readonly isBoom: true
22
+ readonly isServer: boolean
23
+ readonly data: ProblemDetails
24
+ readonly output: ProblemErrorOutput
25
+ }
26
+
27
+ /** Returns whether a thrown value is a Builder {@link ProblemError}. */
28
+ export function isProblemError(error: unknown): error is ProblemError {
29
+ if(!(error instanceof Error) || !('isBoom' in error)) {
30
+ return false
31
+ }
32
+
33
+ if(
34
+ error.isBoom !== true
35
+ || !('isServer' in error)
36
+ || typeof error.isServer !== 'boolean'
37
+ || !('data' in error)
38
+ || !('output' in error)
39
+ ) {
40
+ return false
41
+ }
42
+
43
+ return isProblemDetails(error.data)
44
+ && isProblemErrorOutput(error.output)
45
+ }
46
+
47
+ /** Returns whether a value follows the RFC 9457 problem-details schema. */
48
+ export function isProblemDetails(value: unknown): value is ProblemDetails {
49
+ if(!isRecord(value)) {
50
+ return false
51
+ }
52
+
53
+ return isOptionalString(value.type)
54
+ && isOptionalString(value.title)
55
+ && isOptionalStatus(value.status)
56
+ && isOptionalString(value.detail)
57
+ && isOptionalString(value.instance)
58
+ && isOptionalString(value.event)
59
+ && isOptionalString(value.sessionId)
60
+ }
7
61
 
8
- // Reads an `application/problem+json` response and throws a Boom-style
9
- // error. Falls back to status-only mapping when the body isn't a problem doc.
10
- export async function throwProblemError(res: Response) {
62
+ /** Reads an RFC 9457 response, with a status-only fallback for other bodies. */
63
+ export async function readProblemDetails(res: Response) {
11
64
  const contentType = res.headers.get('Content-Type') || ''
12
- let problem: ProblemDetails
13
65
  if(contentType.includes(PROBLEM_CONTENT_TYPE)) {
14
- problem = (await res.json()) as ProblemDetails
15
- } else {
16
- problem = {
17
- type: 'about:blank',
18
- status: res.status,
19
- title: res.statusText,
66
+ try {
67
+ const value: unknown = await res.json()
68
+ if(isProblemDetails(value)) {
69
+ return {
70
+ ...value,
71
+ type: value.type || 'about:blank',
72
+ title: value.title || res.statusText,
73
+ status: value.status || res.status,
74
+ } satisfies ProblemDetails
75
+ }
76
+ } catch{
77
+ // Fall back to the HTTP status when the problem body isn't valid JSON.
20
78
  }
21
79
  }
22
80
 
23
- throw new Boom<ProblemDetails>(
24
- problem.title,
25
- {
81
+ return {
82
+ type: 'about:blank',
83
+ status: res.status,
84
+ title: res.statusText,
85
+ } satisfies ProblemDetails
86
+ }
87
+
88
+ /** Throws a browser-safe, Boom-compatible RFC 9457 error. */
89
+ export async function throwProblemError(res: Response): Promise<never> {
90
+ const problem = await readProblemDetails(res)
91
+ const title = problem.title || res.statusText || 'Request failed'
92
+ const fields = {
93
+ isBoom: true,
94
+ isServer: res.status >= 500,
95
+ data: problem,
96
+ output: {
26
97
  statusCode: res.status,
27
- data: problem,
98
+ headers: { 'content-type': PROBLEM_CONTENT_TYPE },
99
+ payload: {
100
+ ...problem,
101
+ statusCode: res.status,
102
+ error: title,
103
+ message: problem.detail || title,
104
+ },
28
105
  },
29
- )
106
+ } satisfies Omit<ProblemError, keyof Error>
107
+ const error = Object.assign(new Error(title), fields)
108
+ error.name = 'ProblemError'
109
+ throw error
110
+ }
111
+
112
+ function isRecord(value: unknown): value is Record<string, unknown> {
113
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
114
+ }
115
+
116
+ function isProblemErrorOutput(value: unknown): value is ProblemErrorOutput {
117
+ if(!isRecord(value) || !isRecord(value.payload)) {
118
+ return false
119
+ }
120
+
121
+ return typeof value.statusCode === 'number'
122
+ && isStringRecord(value.headers)
123
+ && isProblemDetails(value.payload)
124
+ && typeof value.payload.statusCode === 'number'
125
+ && typeof value.payload.error === 'string'
126
+ && typeof value.payload.message === 'string'
127
+ }
128
+
129
+ function isStringRecord(value: unknown): value is Record<string, string> {
130
+ return isRecord(value)
131
+ && Object.values(value).every(item => typeof item === 'string')
132
+ }
133
+
134
+ function isOptionalString(value: unknown) {
135
+ return value === undefined || typeof value === 'string'
136
+ }
137
+
138
+ function isOptionalStatus(value: unknown) {
139
+ return value === undefined
140
+ || typeof value === 'number'
141
+ && Number.isInteger(value)
142
+ && value >= 100
143
+ && value <= 599
30
144
  }
package/src/index.ts CHANGED
@@ -1,8 +1,5 @@
1
- export * from './client.ts'
2
- export type * from './client.ts'
3
- export { throwProblemError } from './errors.ts'
4
- export type { ProblemDetails, ProblemError } from './errors.ts'
5
- export { DEFAULT_BASE_URL, PROBLEM_CONTENT_TYPE, USER_AGENT } from './consts.ts'
1
+ export * from './api.ts'
2
+ export { USER_AGENT } from './consts.ts'
6
3
  export {
7
4
  addressFromPublicKey,
8
5
  deriveFromPrivateKey,
@@ -51,6 +48,7 @@ export {
51
48
  export {
52
49
  createReclaim,
53
50
  type CreateSessionInput,
51
+ type CreateSessionTeeAttestation,
54
52
  type Reclaim,
55
53
  type ReclaimConfig,
56
54
  type ReceiveOptions,
@@ -61,12 +59,59 @@ export {
61
59
  } from './reclaim.ts'
62
60
  export {
63
61
  fetchJwks,
62
+ fetchJwksUrl,
63
+ fetchReclaimJwks,
64
64
  type Jwk,
65
65
  jwksFromPrivateKey,
66
66
  jwkToUncompressedPublicKey,
67
+ type ReclaimJwks,
67
68
  selectKey,
69
+ selectVerificationResultKey,
70
+ trustedAttestorAddresses,
71
+ validateAttestorAddresses,
68
72
  } from './jwks.ts'
69
73
  export {
70
74
  type NestedProofOutcome,
71
75
  verifyNestedProof,
72
76
  } from './attestor-proof.ts'
77
+ export {
78
+ type LegacyProof,
79
+ type LegacyProofOutcome,
80
+ type LegacyVerifyProofOptions,
81
+ type ProofHashRequirement,
82
+ proofHashes,
83
+ verifyProof,
84
+ } from './legacy-proof.ts'
85
+ export {
86
+ type BuilderProofRecipe,
87
+ proofHashesFromBuilderRecipe,
88
+ } from './proof-recipe.ts'
89
+ export {
90
+ getHashFromProof,
91
+ hashProofClaimParams,
92
+ hashRequestSpec,
93
+ type HttpProofParams,
94
+ type ProofRequestSpec,
95
+ } from './proof-hash.ts'
96
+ export {
97
+ builderTeeBindingPayload,
98
+ BUILDER_TEE_BINDING_DOMAIN,
99
+ createBuilderTeeBinding,
100
+ createLegacyTeeNonceContext,
101
+ deriveLegacySignatureTeeNonce,
102
+ deriveLegacyTeeNonce,
103
+ LEGACY_TEE_ATTESTATION_VERSION,
104
+ LEGACY_TEE_NONCE_DOMAIN,
105
+ legacyApplicationIdFromPrivateKey,
106
+ signBuilderTeeBinding,
107
+ type BuilderTeeBinding,
108
+ type BuilderTeeBindingPayload,
109
+ type LegacyTeeNonceContext,
110
+ type LegacyTeeNonceData,
111
+ type LegacyTeeNonceMode,
112
+ } from './tee-binding.ts'
113
+ export {
114
+ KNOWN_VERIFICATION_CLIENTS,
115
+ type KnownVerificationClientName,
116
+ type KnownVerificationClientUrlMap,
117
+ } from './verification-clients.ts'
package/src/jwks.ts CHANGED
@@ -14,6 +14,21 @@ export interface Jwk {
14
14
  kid?: string
15
15
  alg?: string
16
16
  use?: string
17
+ key_ops?: string[]
18
+ /** Restricts how Builder consumers may use this key. */
19
+ 'x-reclaim-purpose'?: 'verification-result' | 'attestor-proof'
20
+ /** Verification Client that owns a result-signing key. */
21
+ 'x-reclaim-verification-client-id'?: string
22
+ /** Verification Clients that share a built-in result-signing key. */
23
+ 'x-reclaim-verification-client-ids'?: string[]
24
+ /** Attestor proofs use EIP-191 rather than JOSE ES256K. */
25
+ 'x-reclaim-signature-scheme'?: 'EIP-191'
26
+ }
27
+
28
+ export interface ReclaimJwks {
29
+ keys: Jwk[]
30
+ /** Trusted EIP-191 attestor signer addresses (public-key hashes). */
31
+ 'x-reclaim-attestors'?: string[]
17
32
  }
18
33
 
19
34
  /**
@@ -21,7 +36,15 @@ export interface Jwk {
21
36
  * result-signing key). `kid` is the key's ETH address; `x`/`y` are the
22
37
  * uncompressed public-key coordinates. Empty set when no key is configured.
23
38
  */
24
- export function jwksFromPrivateKey(privateKeyHex?: string): { keys: Jwk[] } {
39
+ export function jwksFromPrivateKey(
40
+ privateKeyHex?: string,
41
+ metadata: Pick<
42
+ Jwk,
43
+ 'x-reclaim-purpose'
44
+ | 'x-reclaim-verification-client-id'
45
+ | 'x-reclaim-verification-client-ids'
46
+ > = {},
47
+ ): { keys: Jwk[] } {
25
48
  if(!privateKeyHex) {
26
49
  return { keys: [] }
27
50
  }
@@ -38,6 +61,8 @@ export function jwksFromPrivateKey(privateKeyHex?: string): { keys: Jwk[] } {
38
61
  crv: 'secp256k1',
39
62
  alg: 'ES256K',
40
63
  use: 'sig',
64
+ key_ops: ['verify'],
65
+ ...metadata,
41
66
  kid: addressFromUncompressedPubkey(pub),
42
67
  x: b64url(pub.subarray(1, 33)),
43
68
  y: b64url(pub.subarray(33, 65)),
@@ -52,13 +77,35 @@ export async function fetchJwks(
52
77
  fetchImpl: typeof fetch = fetch,
53
78
  ): Promise<Jwk[]> {
54
79
  const base = iss.replace(/\/+$/, '')
55
- const res = await fetchImpl(`${base}/.well-known/jwks.json`)
80
+ return fetchJwksUrl(`${base}/.well-known/jwks.json`, fetchImpl)
81
+ }
82
+
83
+ /** Fetch a JWK set from an explicitly trusted registry URL. */
84
+ export async function fetchJwksUrl(
85
+ url: string,
86
+ fetchImpl: typeof fetch = fetch,
87
+ ): Promise<Jwk[]> {
88
+ const jwks = await fetchReclaimJwks(url, fetchImpl)
89
+ return jwks.keys
90
+ }
91
+
92
+ /** Fetch Builder's JWKS plus its namespaced attestor trust registry. */
93
+ export async function fetchReclaimJwks(
94
+ url: string,
95
+ fetchImpl: typeof fetch = fetch,
96
+ ): Promise<ReclaimJwks> {
97
+ const res = await fetchImpl(url)
56
98
  if(!res.ok) {
57
- throw new Error(`JWKS fetch failed for ${base}: HTTP ${res.status}`)
99
+ throw new Error(`JWKS fetch failed for ${url}: HTTP ${res.status}`)
58
100
  }
59
101
 
60
- const body = (await res.json()) as { keys?: Jwk[] }
61
- return body.keys ?? []
102
+ const body = (await res.json()) as Partial<ReclaimJwks>
103
+ return {
104
+ keys: body.keys ?? [],
105
+ 'x-reclaim-attestors': validateAttestorAddresses(
106
+ body['x-reclaim-attestors'] ?? [],
107
+ ),
108
+ }
62
109
  }
63
110
 
64
111
  /** Find the key matching `kid` (case-insensitive — kids are ETH addresses). */
@@ -67,6 +114,59 @@ export function selectKey(keys: Jwk[], kid: string): Jwk | undefined {
67
114
  return keys.find((k) => (k.kid ?? '').toLowerCase() === want)
68
115
  }
69
116
 
117
+ /** Select a result key and enforce its Verification Client ownership. */
118
+ export function selectVerificationResultKey(
119
+ keys: Jwk[],
120
+ kid: string,
121
+ verificationClientId: string,
122
+ ): Jwk | undefined {
123
+ return keys.find((key) => (
124
+ (key.kid ?? '').toLowerCase() === kid.toLowerCase()
125
+ && key['x-reclaim-purpose'] !== 'attestor-proof'
126
+ && belongsToVerificationClient(key, verificationClientId)
127
+ ))
128
+ }
129
+
130
+ /** Return ETH addresses from keys explicitly registered for attestor proofs. */
131
+ export function trustedAttestorAddresses(keys: Jwk[]): string[] {
132
+ return keys
133
+ .filter((key) => key['x-reclaim-purpose'] === 'attestor-proof')
134
+ .map((key) => {
135
+ if(
136
+ key.kty !== 'EC'
137
+ || key.crv !== 'secp256k1'
138
+ || key.use !== 'sig'
139
+ || key['x-reclaim-signature-scheme'] !== 'EIP-191'
140
+ || (key.key_ops && !key.key_ops.includes('verify'))
141
+ ) {
142
+ throw new Error('invalid attestor JWK metadata')
143
+ }
144
+
145
+ const address = addressFromUncompressedPubkey(
146
+ hexPublicKeyBytes(jwkToUncompressedPublicKey(key)),
147
+ ).toLowerCase()
148
+ if((key.kid ?? '').toLowerCase() !== address) {
149
+ throw new Error(
150
+ 'attestor JWK kid does not match its ETH public key',
151
+ )
152
+ }
153
+
154
+ return address
155
+ })
156
+ }
157
+
158
+ /** Validate and normalize the JWKS address-only trust extension. */
159
+ export function validateAttestorAddresses(addresses: string[]): string[] {
160
+ return [...new Set(addresses.map((address) => {
161
+ const normalized = address.trim().toLowerCase()
162
+ if(!/^0x[0-9a-f]{40}$/.test(normalized)) {
163
+ throw new Error('invalid attestor address in JWKS trust registry')
164
+ }
165
+
166
+ return normalized
167
+ }))]
168
+ }
169
+
70
170
  /**
71
171
  * Reconstruct the uncompressed secp256k1 public key (`0x04` + X + Y) from an
72
172
  * EC JWK's base64url `x`/`y` coordinates, for signature verification.
@@ -84,3 +184,26 @@ export function jwkToUncompressedPublicKey(jwk: Jwk): string {
84
184
  out.set(y, 33)
85
185
  return '0x' + bytesToHex(out)
86
186
  }
187
+
188
+ function belongsToVerificationClient(
189
+ key: Jwk,
190
+ verificationClientId: string,
191
+ ): boolean {
192
+ const one = key['x-reclaim-verification-client-id']
193
+ const many = key['x-reclaim-verification-client-ids']
194
+ if(one) {
195
+ return one === verificationClientId
196
+ }
197
+
198
+ if(many) {
199
+ return many.includes(verificationClientId)
200
+ }
201
+
202
+ // Untagged self-hosted JWKS remain compatible because their URL is itself
203
+ // selected from the registered Verification Client record.
204
+ return key['x-reclaim-purpose'] !== 'verification-result'
205
+ }
206
+
207
+ function hexPublicKeyBytes(value: string) {
208
+ return Uint8Array.from(Buffer.from(value.slice(2), 'hex'))
209
+ }