@reclaimprotocol/client 0.1.0-dev.2 → 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/README.md +37 -35
- package/lib/enckeys.d.ts +38 -31
- package/lib/enckeys.js +88 -13
- package/lib/encoding.d.ts +5 -0
- package/lib/encoding.js +19 -3
- package/lib/index.d.ts +3 -3
- package/lib/index.js +2 -2
- package/lib/keypair.d.ts +3 -2
- package/lib/keypair.js +15 -1
- package/lib/reclaim.d.ts +35 -32
- package/lib/reclaim.js +19 -15
- package/lib/result.d.ts +20 -16
- package/lib/result.js +68 -54
- package/lib/routes.gen.d.ts +48 -12
- package/lib/routes.gen.js +13 -4
- package/lib/secp.d.ts +1 -2
- package/lib/secp.js +1 -1
- package/lib/types/openapi.d.ts +2 -2
- package/lib/types/openapi.gen.d.ts +711 -122
- package/package.json +7 -2
- package/src/enckeys.ts +114 -35
- package/src/encoding.ts +21 -3
- package/src/index.ts +6 -3
- package/src/keypair.ts +20 -3
- package/src/reclaim.ts +54 -51
- package/src/result.ts +86 -65
- package/src/routes.gen.ts +13 -4
- package/src/secp.ts +1 -1
- package/src/types/openapi.gen.ts +711 -122
- package/src/types/openapi.ts +2 -2
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
|
|
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 {
|
|
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
|
-
*
|
|
51
|
-
* `{kty:'
|
|
52
|
-
* by the first-party Verification Client to
|
|
53
|
-
* Consumer before submitting it to the Builder
|
|
54
|
-
* ciphertext). Consumers never call this — they only
|
|
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
|
|
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
|
|
60
|
-
publicJwk:
|
|
65
|
+
export async function encryptForEthPublicKey(
|
|
66
|
+
publicJwk: Jwk,
|
|
61
67
|
plaintext: Uint8Array,
|
|
62
68
|
): Promise<string> {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
73
|
-
*
|
|
74
|
-
* no
|
|
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
|
|
95
|
-
*
|
|
96
|
-
*
|
|
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
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
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
|
|
151
|
+
// Otherwise it's an ECIES ciphertext (base64url).
|
|
147
152
|
if(!privateKey) {
|
|
148
153
|
throw new Error(
|
|
149
|
-
'received an encrypted (
|
|
150
|
-
+ 'configured — pass the
|
|
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
|
|
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-
|
|
365
|
-
|
|
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
|
|
370
|
-
const
|
|
371
|
-
typeof
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
378
|
-
|
|
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
|
-
|
|
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:
|
|
402
|
+
reason: np.reason,
|
|
401
403
|
attestorAddress: np.attestorAddress,
|
|
402
404
|
})
|
|
403
405
|
continue
|
|
404
406
|
}
|
|
405
|
-
}
|
|
406
407
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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 {
|
|
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. */
|