@reclaimprotocol/client 0.1.0 → 0.1.1

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/src/result.ts CHANGED
@@ -11,14 +11,18 @@
11
11
  */
12
12
  import { sha256 } from '@noble/hashes/sha2.js'
13
13
  import { keccak_256 } from '@noble/hashes/sha3.js'
14
- import type { JWK } from 'jose'
15
- import { compactDecrypt, CompactEncrypt, importJWK } from 'jose'
14
+ import { decrypt as eciesDecrypt, encrypt as eciesEncrypt } from 'eciesjs'
16
15
  import type { components } from './types/openapi.gen.ts'
17
16
  import { type NestedProofOutcome, verifyNestedProof } from './attestor-proof.ts'
18
17
  import { canonicalJson } from './canonical.ts'
19
18
  import type { DecryptionKey } from './enckeys.ts'
20
19
  import { b64url, base64urlToBytes, bytesToHex, hexToBytes } from './encoding.ts'
21
- import { fetchJwks, jwkToUncompressedPublicKey, selectKey } from './jwks.ts'
20
+ import {
21
+ fetchJwks,
22
+ type Jwk,
23
+ jwkToUncompressedPublicKey,
24
+ selectKey,
25
+ } from './jwks.ts'
22
26
  import { addressFromUncompressedPubkey, secp } from './secp.ts'
23
27
 
24
28
  export interface Jws {
@@ -47,32 +51,33 @@ export interface VerifyResultOutcome {
47
51
  }
48
52
 
49
53
  /**
50
- * JWE-encrypt `plaintext` for the holder of `publicJwk` (an X25519 public JWK,
51
- * `{kty:'OKP', crv:'X25519', x}`) as a compact JWE (`ECDH-ES + A256GCM`). Used
52
- * by the first-party Verification Client to encrypt the signed JWS for each
53
- * Consumer before submitting it to the Builder (which only relays the
54
- * ciphertext). Consumers never call this — they only {@link decryptCallback}.
54
+ * ECIES-encrypt `plaintext` for the holder of `publicJwk` (the org's secp256k1
55
+ * **eth** public JWK, `{kty:'EC', crv:'secp256k1', x, y}`) — `eciesjs` defaults
56
+ * (secp256k1 + AES-256-GCM). Used by the first-party Verification Client to
57
+ * encrypt the signed JWS for the Consumer before submitting it to the Builder
58
+ * (which only relays the ciphertext). Consumers never call this — they only
59
+ * {@link decryptCallback}.
55
60
  *
56
61
  * `plaintext` is bytes (e.g. `new TextEncoder().encode(JSON.stringify(jws))`);
57
- * returns the compact JWE string.
62
+ * returns the ciphertext as a base64url string (single token, never starts with
63
+ * `{`, so {@link decryptCallback} can tell it apart from a plaintext JWS JSON).
58
64
  */
59
- export async function encryptForPublicJwk(
60
- publicJwk: JWK,
65
+ export async function encryptForEthPublicKey(
66
+ publicJwk: Jwk,
61
67
  plaintext: Uint8Array,
62
68
  ): Promise<string> {
63
- const key = await importJWK(publicJwk, 'ECDH-ES')
64
- return new CompactEncrypt(plaintext)
65
- .setProtectedHeader({ alg: 'ECDH-ES', enc: 'A256GCM' })
66
- .encrypt(key)
69
+ // Uncompressed `0x04`‖X‖Y bytes — what eciesjs accepts as the recipient key.
70
+ const pubBytes = hexToBytes(jwkToUncompressedPublicKey(publicJwk))
71
+ return b64url(eciesEncrypt(pubBytes, plaintext))
67
72
  }
68
73
 
69
74
  /**
70
75
  * What a consumer's callback endpoint receives — the Builder POSTs the JSON
71
76
  * `VerificationResultDelivery` envelope: plaintext `sessionId`/`event`/
72
- * `timestamp` routing hints. For terminal events `data` is a compact JWE
73
- * string wrapping the signed JWS (or the plaintext JWS JSON when the org has
74
- * no encryption credential). The envelope is unauthenticated; trust only the
75
- * decrypted+verified JWS.
77
+ * `timestamp` routing hints. For terminal events `data` is an ECIES ciphertext
78
+ * (base64url) wrapping the signed JWS (or the plaintext JWS JSON when the org
79
+ * has no key / `canEncryptResult` is off). The envelope is unauthenticated;
80
+ * trust only the decrypted+verified JWS.
76
81
  *
77
82
  * Derived from the generated spec type so it can't drift; the hint fields
78
83
  * are relaxed to optional — decryption only needs `data`.
@@ -91,9 +96,9 @@ export type CallbackBody =
91
96
  | ArrayBuffer
92
97
 
93
98
  /** Normalize any {@link CallbackBody} to the terminal-result `data` string —
94
- * either a compact JWE (`xxxx.yyyy.zzzz.aaaa.bbbb`) or the plaintext JWS JSON
95
- * (`{…}`). The envelope object/text wraps it in `.data`; raw bytes are decoded
96
- * to text first. */
99
+ * either an ECIES ciphertext (base64url) or the plaintext JWS JSON (`{…}`).
100
+ * The envelope object/text wraps it in `.data`; raw bytes are decoded to text
101
+ * first. */
97
102
  function payloadFrom(body: CallbackBody): string {
98
103
  if(typeof body === 'string') {
99
104
  const s = body.trim()
@@ -123,10 +128,10 @@ function payloadFrom(body: CallbackBody): string {
123
128
  }
124
129
 
125
130
  /**
126
- * Decrypt a delivered terminal callback to the signed JWS. `data` is either a
127
- * compact JWE (decrypted with the X25519 `privateKey`) or — when the org has no
128
- * encryption credential — the plaintext JWS JSON. A compact JWE has exactly
129
- * five dot-separated base64url segments; a JSON JWS starts with `{`.
131
+ * Decrypt a delivered terminal callback to the signed JWS. `data` is either an
132
+ * ECIES ciphertext (base64url, decrypted with the eth `privateKey` scalar) or —
133
+ * when the org has no key / `canEncryptResult` is off — the plaintext JWS JSON.
134
+ * A JSON JWS starts with `{`; anything else is the ECIES ciphertext.
130
135
  *
131
136
  * `privateKey` may be absent (plaintext delivery); an encrypted delivery with
132
137
  * no key throws a clear error.
@@ -143,16 +148,16 @@ export async function decryptCallback(
143
148
  return JSON.parse(trimmed) as Jws
144
149
  }
145
150
 
146
- // Otherwise it's a compact JWE — five base64url segments.
151
+ // Otherwise it's an ECIES ciphertext (base64url).
147
152
  if(!privateKey) {
148
153
  throw new Error(
149
- 'received an encrypted (JWE) delivery but no decryption credential was '
150
- + 'configured — pass the X25519 private key (JWK/PEM): '
154
+ 'received an encrypted (ECIES) delivery but no decryption credential was '
155
+ + 'configured — pass the eth private key (0x-hex): '
151
156
  + 'results.receive(body, { credential }) or decrypt(body, credential)',
152
157
  )
153
158
  }
154
159
 
155
- const { plaintext } = await compactDecrypt(trimmed, privateKey)
160
+ const plaintext = eciesDecrypt(privateKey, base64urlToBytes(trimmed))
156
161
  return JSON.parse(new TextDecoder().decode(plaintext)) as Jws
157
162
  }
158
163
 
@@ -242,6 +247,8 @@ export interface VerifyResultFullOptions {
242
247
  }
243
248
 
244
249
  export interface ResultProofOutcome {
250
+ /** Which provider this proof belongs to (results grouped per provider). */
251
+ providerId?: string
245
252
  requestId?: string
246
253
  proofValid: boolean
247
254
  reason?: string
@@ -361,54 +368,68 @@ export async function verifyResultFull(
361
368
  return fail('expired')
362
369
  }
363
370
 
364
- // 3. Per-result nested attestor proof + content_hash binding.
365
- const results = Array.isArray(payload.results)
371
+ // 3. Per-provider, per-request nested attestor proof + content_hash binding.
372
+ // `results` is grouped by provider: [{ provider_id, resolved_version,
373
+ // requests: [...] }]. Every request across every provider must verify.
374
+ const providerResults = Array.isArray(payload.results)
366
375
  ? (payload.results as Record<string, unknown>[])
367
376
  : []
368
377
  const proofs: ResultProofOutcome[] = []
369
- for(const r of results) {
370
- const requestId =
371
- typeof r.request_id === 'string' ? r.request_id : undefined
372
- if(typeof r.proof !== 'string') {
373
- proofs.push({ requestId, proofValid: false, reason: 'proof-missing' })
374
- continue
375
- }
378
+ for(const prov of providerResults) {
379
+ const providerId =
380
+ typeof prov.provider_id === 'string' ? prov.provider_id : undefined
381
+ const requests = Array.isArray(prov.requests)
382
+ ? (prov.requests as Record<string, unknown>[])
383
+ : []
384
+ for(const r of requests) {
385
+ const requestId =
386
+ typeof r.request_id === 'string' ? r.request_id : undefined
387
+ if(typeof r.proof !== 'string') {
388
+ proofs.push({
389
+ providerId, requestId, proofValid: false, reason: 'proof-missing',
390
+ })
391
+ continue
392
+ }
376
393
 
377
- const np: NestedProofOutcome = verifyNestedProof(r.proof, {
378
- attestorAddresses: opts.attestorAddresses,
379
- })
380
- if(!np.valid) {
381
- proofs.push({
382
- requestId,
383
- proofValid: false,
384
- reason: np.reason,
385
- attestorAddress: np.attestorAddress,
394
+ const np: NestedProofOutcome = verifyNestedProof(r.proof, {
395
+ attestorAddresses: opts.attestorAddresses,
386
396
  })
387
- continue
388
- }
389
-
390
- if(
391
- typeof r.content_hash === 'string' &&
392
- r.extracted_parameters &&
393
- typeof r.extracted_parameters === 'object'
394
- ) {
395
- const params = r.extracted_parameters as Record<string, string>
396
- if(contentHash(params).toLowerCase() !== r.content_hash.toLowerCase()) {
397
+ if(!np.valid) {
397
398
  proofs.push({
399
+ providerId,
398
400
  requestId,
399
401
  proofValid: false,
400
- reason: 'content-hash-mismatch',
402
+ reason: np.reason,
401
403
  attestorAddress: np.attestorAddress,
402
404
  })
403
405
  continue
404
406
  }
405
- }
406
407
 
407
- proofs.push({
408
- requestId,
409
- proofValid: true,
410
- attestorAddress: np.attestorAddress,
411
- })
408
+ if(
409
+ typeof r.content_hash === 'string' &&
410
+ r.extracted_parameters &&
411
+ typeof r.extracted_parameters === 'object'
412
+ ) {
413
+ const params = r.extracted_parameters as Record<string, string>
414
+ if(contentHash(params).toLowerCase() !== r.content_hash.toLowerCase()) {
415
+ proofs.push({
416
+ providerId,
417
+ requestId,
418
+ proofValid: false,
419
+ reason: 'content-hash-mismatch',
420
+ attestorAddress: np.attestorAddress,
421
+ })
422
+ continue
423
+ }
424
+ }
425
+
426
+ proofs.push({
427
+ providerId,
428
+ requestId,
429
+ proofValid: true,
430
+ attestorAddress: np.attestorAddress,
431
+ })
432
+ }
412
433
  }
413
434
 
414
435
  if(!proofs.every((p) => p.proofValid)) {
package/src/routes.gen.ts CHANGED
@@ -13,6 +13,7 @@ export const ROUTES = {
13
13
  "AdminSessions": {"method":"get","path":"/admin/sessions"},
14
14
  "AdminThemes": {"method":"get","path":"/admin/themes"},
15
15
  "AdminUsers": {"method":"get","path":"/admin/users"},
16
+ "AttachLanding": {"method":"get","path":"/attach"},
16
17
  "AttachPage": {"method":"get","path":"/attach/{code}"},
17
18
  "CancelPublicListingRequest": {"method":"post","path":"/providers/{providerId}/versions/{version}/cancel-review"},
18
19
  "ComponentsDemo": {"method":"get","path":"/components_demo"},
@@ -30,11 +31,15 @@ export const ROUTES = {
30
31
  "DeleteOrg": {"method":"delete","path":"/orgs/{orgId}"},
31
32
  "DeleteOrgBilling": {"method":"delete","path":"/orgs/{orgId}/billing"},
32
33
  "DeleteOrgCallbackSubscription": {"method":"delete","path":"/orgs/{orgId}/callback-subscriptions/{subscriptionId}"},
33
- "DeleteOrgCredential": {"method":"delete","path":"/orgs/{orgId}/credential"},
34
34
  "DeleteOrgIcon": {"method":"delete","path":"/orgs/{orgId}/icon"},
35
+ "DeleteOrgKeypair": {"method":"delete","path":"/orgs/{orgId}/keypair"},
35
36
  "DeleteProvider": {"method":"delete","path":"/providers/{providerId}"},
36
37
  "DeleteProviderVersion": {"method":"delete","path":"/providers/{providerId}/versions/{version}"},
37
38
  "DeleteTheme": {"method":"delete","path":"/orgs/{orgId}/themes/{themeId}"},
39
+ "DiscoverResolve": {"method":"get","path":"/discover/resolve"},
40
+ "DiscoverTeachAgent": {"method":"get","path":"/discover/teach"},
41
+ "DiscoverWebsites": {"method":"get","path":"/discover/websites"},
42
+ "DisposeVerificationSession": {"method":"post","path":"/v/{sessionId}/dispose"},
38
43
  "Docs": {"method":"get","path":"/docs"},
39
44
  "ExplorePreviewImage": {"method":"get","path":"/og/explore.png"},
40
45
  "GetAttachSession": {"method":"get","path":"/credentials/attach/sessions/{code}"},
@@ -42,7 +47,7 @@ export const ROUTES = {
42
47
  "GetMe": {"method":"get","path":"/me"},
43
48
  "GetOrg": {"method":"get","path":"/orgs/{orgId}"},
44
49
  "GetOrgBilling": {"method":"get","path":"/orgs/{orgId}/billing"},
45
- "GetOrgCredential": {"method":"get","path":"/orgs/{orgId}/credential"},
50
+ "GetOrgKeypair": {"method":"get","path":"/orgs/{orgId}/keypair"},
46
51
  "GetOrgSession": {"method":"get","path":"/orgs/{orgId}/sessions/{sessionId}"},
47
52
  "GetOrgToken": {"method":"get","path":"/orgs/{orgId}/token"},
48
53
  "GetProvider": {"method":"get","path":"/providers/{providerId}"},
@@ -81,13 +86,17 @@ export const ROUTES = {
81
86
  "RetryCallbackDlq": {"method":"post","path":"/verifications/callback-dlq/{dlqId}/retry"},
82
87
  "RetryVerificationCallback": {"method":"post","path":"/verifications/sessions/{sessionId}/retry-callback"},
83
88
  "Robots": {"method":"get","path":"/robots.txt"},
84
- "SetOrgCredential": {"method":"put","path":"/orgs/{orgId}/credential"},
89
+ "SearchOrgMembers": {"method":"get","path":"/orgs/{orgId}/members"},
90
+ "SetOrgKeypair": {"method":"put","path":"/orgs/{orgId}/keypair"},
85
91
  "SetupProviderCreate": {"method":"post","path":"/providers/setup"},
92
+ "SiteBuild": {"method":"get","path":"/site/{domain}/build"},
93
+ "SiteBuildTry": {"method":"post","path":"/site/{domain}/try"},
86
94
  "Sitemap": {"method":"get","path":"/sitemap.xml"},
95
+ "SitePage": {"method":"get","path":"/site/{domain}"},
96
+ "SitePanel": {"method":"get","path":"/site/{domain}/panel"},
87
97
  "SubmitProviderVersionForReview": {"method":"post","path":"/providers/{providerId}/versions/{version}/review"},
88
98
  "SubmitVerificationResult": {"method":"post","path":"/verifications/sessions/{sessionId}/result"},
89
99
  "TransferProvider": {"method":"post","path":"/providers/{providerId}/transfer"},
90
- "UpdateMe": {"method":"patch","path":"/me"},
91
100
  "UpdateOrg": {"method":"patch","path":"/orgs/{orgId}"},
92
101
  "UpdateOrgBilling": {"method":"put","path":"/orgs/{orgId}/billing"},
93
102
  "UpdateOrgIcon": {"method":"put","path":"/orgs/{orgId}/icon"},
package/src/secp.ts CHANGED
@@ -15,7 +15,7 @@ secp.hashes.hmacSha256 = (key: Uint8Array, msg: Uint8Array) => {
15
15
  return hmac(sha256, key, msg)
16
16
  }
17
17
 
18
- export { keccak_256, secp }
18
+ export { secp }
19
19
 
20
20
  /** Lowercase 0x ETH address from an uncompressed (65-byte) public key:
21
21
  * keccak256 of the 64-byte X‖Y (drop the 0x04 prefix), last 20 bytes. */