@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/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/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reclaimprotocol/client",
|
|
3
3
|
"title": "Reclaim Client",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.1",
|
|
5
5
|
"description": "Typed SDK client for @reclaimprotocol/app. Used by @reclaimprotocol/agent and any other consumer that needs programmatic API access without the full agent.",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
7
8
|
"main": "./lib/index.js",
|
|
8
9
|
"types": "./lib/index.d.ts",
|
|
9
10
|
"exports": {
|
|
@@ -14,6 +15,10 @@
|
|
|
14
15
|
"./openapi": {
|
|
15
16
|
"types": "./lib/types/index.d.ts",
|
|
16
17
|
"import": "./lib/types/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./enckeys": {
|
|
20
|
+
"types": "./lib/enckeys.d.ts",
|
|
21
|
+
"import": "./lib/enckeys.js"
|
|
17
22
|
}
|
|
18
23
|
},
|
|
19
24
|
"files": [
|
|
@@ -54,7 +59,7 @@
|
|
|
54
59
|
"@noble/secp256k1": "^3.1.0",
|
|
55
60
|
"@reclaimprotocol/attestor-core": "^5.0.7",
|
|
56
61
|
"canonicalize": "^2.1.0",
|
|
57
|
-
"
|
|
62
|
+
"eciesjs": "^0.4.15"
|
|
58
63
|
},
|
|
59
64
|
"devDependencies": {
|
|
60
65
|
"@types/node": "^24.0.0",
|
package/src/enckeys.ts
CHANGED
|
@@ -1,46 +1,125 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* `ECDH-ES + A256GCM`), all via `jose`. Consumers mint the keypair themselves
|
|
4
|
-
* with openssl (we never generate keys for them):
|
|
2
|
+
* secp256k1 (Ethereum) encryption-key helpers for ECIES result delivery.
|
|
5
3
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* The org's encryption key is its **Ethereum public key** (secp256k1). The
|
|
5
|
+
* Verification Client ECIES-encrypts the signed JWS to it (see ./result.ts) and
|
|
6
|
+
* the Consumer decrypts with the matching eth **private key**. Eth-native `0x`
|
|
7
|
+
* hex is used everywhere at the edges (the format MetaMask/ethers/wallets
|
|
8
|
+
* emit); the wire/DB store the public key as a secp256k1 JWK
|
|
9
|
+
* (`{kty:'EC',crv:'secp256k1',x,y}`), matching the signing JWKS shape in
|
|
10
|
+
* ./jwks.ts.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* results (`results.receive(body, { credential: <private PEM> })`). Pass PEM
|
|
15
|
-
* *content* — never a file path or env var. Eth is not involved in encryption;
|
|
16
|
-
* it only signs/verifies the inner JWS (see ./result.ts, ./secp.ts).
|
|
12
|
+
* No PEM, no DER, no WebCrypto — `@noble/secp256k1` handles hex in every
|
|
13
|
+
* runtime.
|
|
17
14
|
*/
|
|
18
|
-
import
|
|
19
|
-
import {
|
|
15
|
+
import { b64url, hexToBytes } from './encoding.ts'
|
|
16
|
+
import { type Jwk, jwkToUncompressedPublicKey } from './jwks.ts'
|
|
17
|
+
import {
|
|
18
|
+
addressFromPublicKey,
|
|
19
|
+
deriveFromPrivateKey,
|
|
20
|
+
generateKeypair,
|
|
21
|
+
} from './keypair.ts'
|
|
20
22
|
|
|
21
|
-
/**
|
|
22
|
-
*
|
|
23
|
-
export type DecryptionKey = Awaited<ReturnType<typeof importPKCS8>>
|
|
24
|
-
|
|
25
|
-
/** The consumer's X25519 private key as a PKCS#8 PEM string — the *content* of
|
|
26
|
-
* `openssl genpkey -algorithm X25519` (the `-----BEGIN PRIVATE KEY-----`
|
|
27
|
-
* block), not a path or env-var name. */
|
|
23
|
+
/** The Consumer's eth private key as `0x`+64 hex (a 32-byte secp256k1
|
|
24
|
+
* scalar). */
|
|
28
25
|
export type DecryptionKeyInput = string
|
|
26
|
+
/** The raw 32-byte private scalar — what `eciesjs.decrypt` consumes. */
|
|
27
|
+
export type DecryptionKey = Uint8Array
|
|
28
|
+
|
|
29
|
+
const ETH_PRIVATE_HEX = /^0x[0-9a-fA-F]{64}$/
|
|
30
|
+
const UNCOMPRESSED_PUBKEY_HEX = /^0x04[0-9a-fA-F]{128}$/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validate and load an eth private key (`0x`+64 hex) into the raw scalar bytes
|
|
34
|
+
* `eciesjs.decrypt` needs. Pass the hex content — never a path or env-var name.
|
|
35
|
+
*/
|
|
36
|
+
export function loadDecryptionKey(input: DecryptionKeyInput): DecryptionKey {
|
|
37
|
+
const v = input.trim().replace(/^['"]|['"]$/g, '')
|
|
38
|
+
const hex = v.startsWith('0x') ? v : '0x' + v
|
|
39
|
+
if(!ETH_PRIVATE_HEX.test(hex)) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
'not a valid Ethereum private key (expected a 32-byte / 0x+64-hex '
|
|
42
|
+
+ 'scalar)',
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return hexToBytes(hex)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Convert an uncompressed secp256k1 public key (`0x04` + 128 hex) to the public
|
|
51
|
+
* JWK the API stores and the Verification Client encrypts to — so callers only
|
|
52
|
+
* ever handle eth-native hex, never a JWK. Mirrors the `x`/`y` coordinate split
|
|
53
|
+
* in `jwksFromPrivateKey` (./jwks.ts).
|
|
54
|
+
*/
|
|
55
|
+
export function uncompressedPubkeyToJwk(publicKeyHex: string): Jwk {
|
|
56
|
+
const v = publicKeyHex.trim()
|
|
57
|
+
const hex = v.startsWith('0x') ? v : '0x' + v
|
|
58
|
+
if(!UNCOMPRESSED_PUBKEY_HEX.test(hex)) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'not a valid uncompressed secp256k1 public key '
|
|
61
|
+
+ '(expected `0x04` + 128 hex)',
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const bytes = hexToBytes(hex) // 65 bytes: 0x04 ‖ X ‖ Y
|
|
66
|
+
return {
|
|
67
|
+
kty: 'EC',
|
|
68
|
+
crv: 'secp256k1',
|
|
69
|
+
x: b64url(bytes.subarray(1, 33)),
|
|
70
|
+
y: b64url(bytes.subarray(33, 65)),
|
|
71
|
+
}
|
|
72
|
+
}
|
|
29
73
|
|
|
30
|
-
|
|
74
|
+
/** Lowercase 0x ETH address that owns a secp256k1 public JWK (the org's
|
|
75
|
+
* identity address). */
|
|
76
|
+
export function pubkeyJwkToEthAddress(jwk: Jwk): string {
|
|
77
|
+
return addressFromPublicKey(jwkToUncompressedPublicKey(jwk))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A freshly generated org encryption keypair, in every form callers need:
|
|
81
|
+
* the eth `0x`-hex private key (kept by the Consumer to decrypt), the
|
|
82
|
+
* uncompressed public key hex, the public JWK (sent to the API / stored), and
|
|
83
|
+
* the derived eth address. */
|
|
84
|
+
export interface GeneratedEthKeypair {
|
|
85
|
+
privateKeyHex: string
|
|
86
|
+
publicKeyHex: string
|
|
87
|
+
publicJwk: Jwk
|
|
88
|
+
ethAddress: string
|
|
89
|
+
}
|
|
31
90
|
|
|
32
|
-
/**
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return
|
|
91
|
+
/** Mint a fresh secp256k1 keypair for org encryption — the in-browser dashboard
|
|
92
|
+
* generator, the SDK, and tests all use this so every keypair is shaped the
|
|
93
|
+
* same way. The private key MUST stay local; only the public JWK is sent. */
|
|
94
|
+
export function generateEthKeypair(): GeneratedEthKeypair {
|
|
95
|
+
const { privateKey, publicKey, address } = generateKeypair()
|
|
96
|
+
return {
|
|
97
|
+
privateKeyHex: privateKey,
|
|
98
|
+
publicKeyHex: publicKey,
|
|
99
|
+
publicJwk: uncompressedPubkeyToJwk(publicKey),
|
|
100
|
+
ethAddress: address,
|
|
101
|
+
}
|
|
38
102
|
}
|
|
39
103
|
|
|
40
|
-
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
104
|
+
/** Derive the same `GeneratedEthKeypair` shape from a private key the user
|
|
105
|
+
* already holds (`0x`+64 hex), so they can BYO a key instead of generating
|
|
106
|
+
* one. Validates the format and throws otherwise. The private key stays
|
|
107
|
+
* local — only the derived public key/address are ever surfaced. */
|
|
108
|
+
export function ethKeypairFromPrivateKey(input: string): GeneratedEthKeypair {
|
|
109
|
+
const v = input.trim().replace(/^['"]|['"]$/g, '')
|
|
110
|
+
const hex = v.startsWith('0x') ? v : '0x' + v
|
|
111
|
+
if(!ETH_PRIVATE_HEX.test(hex)) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
'not a valid Ethereum private key (expected a 32-byte / 0x+64-hex '
|
|
114
|
+
+ 'scalar)',
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { privateKey, publicKey, address } = deriveFromPrivateKey(hex)
|
|
119
|
+
return {
|
|
120
|
+
privateKeyHex: privateKey,
|
|
121
|
+
publicKeyHex: publicKey,
|
|
122
|
+
publicJwk: uncompressedPubkeyToJwk(publicKey),
|
|
123
|
+
ethAddress: address,
|
|
124
|
+
}
|
|
46
125
|
}
|
package/src/encoding.ts
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* Byte / hex / base64url encoders shared across the verification crypto paths
|
|
3
3
|
* (result, jwks, attestor-proof). `hexToBytes` tolerates an optional `0x`
|
|
4
4
|
* prefix, matching how ETH keys/addresses are passed around.
|
|
5
|
+
*
|
|
6
|
+
* base64url uses the `atob`/`btoa` globals + `TextEncoder` rather than Node's
|
|
7
|
+
* `Buffer` so the same code runs in the browser (the in-page keypair
|
|
8
|
+
* generator) — `Buffer` is undefined there. Both globals exist in Node ≥ 16
|
|
9
|
+
* and every browser.
|
|
5
10
|
*/
|
|
6
11
|
export function hexToBytes(hex: string): Uint8Array {
|
|
7
12
|
const clean = hex.startsWith('0x') ? hex.slice(2) : hex
|
|
@@ -26,13 +31,26 @@ export function bytesToHex(bytes: Uint8Array): string {
|
|
|
26
31
|
export function base64urlToBytes(s: string): Uint8Array {
|
|
27
32
|
const b64 = s.replace(/-/g, '+').replace(/_/g, '/')
|
|
28
33
|
const pad = b64.length % 4 ? '='.repeat(4 - (b64.length % 4)) : ''
|
|
29
|
-
|
|
34
|
+
const bin = atob(b64 + pad)
|
|
35
|
+
const out = new Uint8Array(bin.length)
|
|
36
|
+
for(let i = 0; i < bin.length; i++) {
|
|
37
|
+
out[i] = bin.charCodeAt(i)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return out
|
|
30
41
|
}
|
|
31
42
|
|
|
32
43
|
/** Encode bytes (or a UTF-8 string) as unpadded base64url. */
|
|
33
44
|
export function b64url(input: Uint8Array | string): string {
|
|
34
|
-
|
|
35
|
-
.
|
|
45
|
+
const bytes = typeof input === 'string'
|
|
46
|
+
? new TextEncoder().encode(input)
|
|
47
|
+
: input
|
|
48
|
+
let bin = ''
|
|
49
|
+
for(const b of bytes) {
|
|
50
|
+
bin += String.fromCharCode(b)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return btoa(bin)
|
|
36
54
|
.replace(/\+/g, '-')
|
|
37
55
|
.replace(/\//g, '_')
|
|
38
56
|
.replace(/=+$/, '')
|
package/src/index.ts
CHANGED
|
@@ -25,7 +25,7 @@ export {
|
|
|
25
25
|
decodeJwsPayload,
|
|
26
26
|
decryptCallback,
|
|
27
27
|
type DeliveryEnvelope,
|
|
28
|
-
|
|
28
|
+
encryptForEthPublicKey,
|
|
29
29
|
type Jws,
|
|
30
30
|
ResultVerificationError,
|
|
31
31
|
type ResultProofOutcome,
|
|
@@ -41,8 +41,11 @@ export {
|
|
|
41
41
|
export {
|
|
42
42
|
type DecryptionKey,
|
|
43
43
|
type DecryptionKeyInput,
|
|
44
|
+
generateEthKeypair,
|
|
45
|
+
type GeneratedEthKeypair,
|
|
44
46
|
loadDecryptionKey,
|
|
45
|
-
|
|
47
|
+
pubkeyJwkToEthAddress,
|
|
48
|
+
uncompressedPubkeyToJwk,
|
|
46
49
|
} from './enckeys.ts'
|
|
47
50
|
export {
|
|
48
51
|
createReclaim,
|
|
@@ -52,7 +55,7 @@ export {
|
|
|
52
55
|
type ReceiveOptions,
|
|
53
56
|
type ReceivedDelivery,
|
|
54
57
|
type RegisterCallbackInput,
|
|
55
|
-
type
|
|
58
|
+
type SetKeypairInput,
|
|
56
59
|
TERMINAL_EVENTS,
|
|
57
60
|
} from './reclaim.ts'
|
|
58
61
|
export {
|
package/src/keypair.ts
CHANGED
|
@@ -6,11 +6,28 @@ export type EthKeypair = {
|
|
|
6
6
|
address: string
|
|
7
7
|
privateKey: string
|
|
8
8
|
/** Uncompressed secp256k1 public key (`0x04` + 128 hex). Used to derive the
|
|
9
|
-
* ETH address
|
|
10
|
-
* `x`/`y`)
|
|
9
|
+
* ETH address, to verify ES256K JWS signatures (published via JWKS as
|
|
10
|
+
* `x`/`y`), and — for an org's encryption key — to ECIES-encrypt results
|
|
11
|
+
* (see ./enckeys.ts, ./result.ts). */
|
|
11
12
|
publicKey: string
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
/** Bytes of CSPRNG seed `@noble`'s `randomSecretKey` expects (it reduces a
|
|
16
|
+
* ≥48-byte seed into a valid secp256k1 scalar). */
|
|
17
|
+
const SECRET_KEY_SEED_LEN = 48
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Fresh CSPRNG bytes from the Web Crypto API. We read `crypto`
|
|
21
|
+
* (`getRandomValues`) — the WHATWG global present in browsers and Node ≥ 20
|
|
22
|
+
* alike — and SEED `@noble`'s key generation from it, rather than letting
|
|
23
|
+
* `@noble` dereference `globalThis.crypto` itself: its access is unguarded
|
|
24
|
+
* (`(globalThis?.crypto).getRandomValues(...)`) and throws an opaque
|
|
25
|
+
* TypeError wherever the global is absent.
|
|
26
|
+
*/
|
|
27
|
+
function randomSeed(): Uint8Array {
|
|
28
|
+
return crypto.getRandomValues(new Uint8Array(SECRET_KEY_SEED_LEN))
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
/**
|
|
15
32
|
* Generate a fresh secp256k1 key-pair and derive its lowercase
|
|
16
33
|
* Ethereum address (keccak-256 of the uncompressed pubkey minus the
|
|
@@ -23,7 +40,7 @@ export type EthKeypair = {
|
|
|
23
40
|
* intermediate value used to compute the address and is not retained.
|
|
24
41
|
*/
|
|
25
42
|
export function generateKeypair(): EthKeypair {
|
|
26
|
-
const priv = secp.utils.randomSecretKey()
|
|
43
|
+
const priv = secp.utils.randomSecretKey(randomSeed())
|
|
27
44
|
const pub = secp.getPublicKey(priv, false)
|
|
28
45
|
return {
|
|
29
46
|
address: addressFromPubBytes(pub),
|
package/src/reclaim.ts
CHANGED
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
* Declarative entry point for the consumer SDK. The ONLY config is the org
|
|
3
3
|
* secret (`rorg_…`) — it authenticates every call and identifies the org
|
|
4
4
|
* server-side. Everything else is passed where it's actually used: the `orgId`
|
|
5
|
-
* to the org-scoped `
|
|
5
|
+
* to the org-scoped `keypair.set`/`get`, and the eth decryption key to
|
|
6
6
|
* `results.decrypt`/`receive` (only when results arrive encrypted):
|
|
7
7
|
*
|
|
8
8
|
* const reclaim = createReclaim({ orgSecret })
|
|
9
9
|
* // one-time setup:
|
|
10
10
|
* await reclaim.callbacks.register({ callbackUrl })
|
|
11
|
-
* // enable encryption:
|
|
12
|
-
* await reclaim.
|
|
13
|
-
* const session = await reclaim.sessions.create({
|
|
11
|
+
* // enable encryption (eth public key, hex):
|
|
12
|
+
* await reclaim.keypair.set(orgId, { publicKey, canEncryptResult: true })
|
|
13
|
+
* const session = await reclaim.sessions.create({ providers, context })
|
|
14
14
|
* // …end-user completes session.verificationUrl…
|
|
15
15
|
* const outcome = await reclaim.results.receive(callbackBody, {
|
|
16
|
-
* credential:
|
|
16
|
+
* credential: ethPrivateKeyHex, // omit for plaintext
|
|
17
17
|
* expectedReclaimSessionId: session.id,
|
|
18
18
|
* })
|
|
19
19
|
*
|
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
type DecryptionKey,
|
|
28
28
|
type DecryptionKeyInput,
|
|
29
29
|
loadDecryptionKey,
|
|
30
|
-
publicPemToJwk,
|
|
31
30
|
} from './enckeys.ts'
|
|
32
31
|
import {
|
|
33
32
|
type CallbackBody,
|
|
@@ -42,8 +41,8 @@ type CallbackSubscription = components['schemas']['CallbackSubscription']
|
|
|
42
41
|
type VerificationSession = components['schemas']['VerificationSession']
|
|
43
42
|
type VerificationEvent = components['schemas']['VerificationEvent']
|
|
44
43
|
type VerificationEventRecord = components['schemas']['VerificationEventRecord']
|
|
45
|
-
type
|
|
46
|
-
type
|
|
44
|
+
type OrgKeypair = components['schemas']['OrgKeypair']
|
|
45
|
+
type OrgKeypairRequest = components['schemas']['OrgKeypairRequest']
|
|
47
46
|
|
|
48
47
|
/** The three terminal events a subscription can receive. */
|
|
49
48
|
export const TERMINAL_EVENTS = [
|
|
@@ -58,22 +57,23 @@ export interface RegisterCallbackInput {
|
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
export interface CreateSessionInput {
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
/** Providers to verify, in run order. The end-user proves each (one browser
|
|
61
|
+
* session) and one combined result is delivered; all are required. */
|
|
62
|
+
providers: { providerId: string, version?: string }[]
|
|
63
63
|
context: Record<string, unknown>
|
|
64
64
|
/** URL with a `{sessionId}` placeholder the end-user opens. */
|
|
65
65
|
verificationClientUrl: string
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
68
|
+
/** Input for `keypair.set`: the org's eth public key as uncompressed hex
|
|
69
|
+
* (`publicKey`, the form wallets/ethers emit) OR as a secp256k1 JWK
|
|
70
|
+
* (`publicKeyJwk`); keep the matching eth private key to decrypt results.
|
|
71
|
+
* `canEncryptResult` toggles ECIES encryption (default off). The two request
|
|
72
|
+
* shapes are the spec's `OrgKeypairRequest` union members, relaxed to optional
|
|
73
|
+
* so the SDK caller supplies just one key form. */
|
|
74
|
+
export type SetKeypairInput =
|
|
75
|
+
& Partial<components['schemas']['OrgKeypairHexRequest']>
|
|
76
|
+
& Partial<components['schemas']['OrgKeypairJwkRequest']>
|
|
77
77
|
|
|
78
78
|
export interface ReclaimConfig {
|
|
79
79
|
/** The org secret (`rorg_…`). Authenticates ALL API calls and identifies
|
|
@@ -88,8 +88,8 @@ export interface ReclaimConfig {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/** Options for `results.receive` — the verification options plus an optional
|
|
91
|
-
*
|
|
92
|
-
*
|
|
91
|
+
* eth decryption key (the org's eth private key as `0x`-hex). The key is
|
|
92
|
+
* needed only when the delivery is an ECIES ciphertext. */
|
|
93
93
|
export type ReceiveOptions = VerifyResultFullOptions & {
|
|
94
94
|
credential?: DecryptionKeyInput
|
|
95
95
|
}
|
|
@@ -110,9 +110,9 @@ export interface Reclaim {
|
|
|
110
110
|
}
|
|
111
111
|
callbacks: {
|
|
112
112
|
/** Register a callback subscription (one-time setup). Delivery is scoped
|
|
113
|
-
* to the token's org; results are
|
|
114
|
-
* when
|
|
115
|
-
* defaults to all three terminal events. */
|
|
113
|
+
* to the token's org; results are ECIES-encrypted to the org's eth key
|
|
114
|
+
* when `canEncryptResult` is enabled (see `keypair.set`), else plaintext.
|
|
115
|
+
* `events` defaults to all three terminal events. */
|
|
116
116
|
register(
|
|
117
117
|
input: Partial<RegisterCallbackInput> &
|
|
118
118
|
Pick<RegisterCallbackInput, 'callbackUrl'>,
|
|
@@ -120,18 +120,19 @@ export interface Reclaim {
|
|
|
120
120
|
list(): Promise<CallbackSubscription[]>
|
|
121
121
|
remove(subscriptionId: string): Promise<void>
|
|
122
122
|
}
|
|
123
|
-
|
|
124
|
-
/** Set/
|
|
125
|
-
*
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
123
|
+
keypair: {
|
|
124
|
+
/** Set/rotate org `orgId`'s single eth encryption keypair. When
|
|
125
|
+
* `canEncryptResult` is enabled, terminal results are delivered as an
|
|
126
|
+
* ECIES ciphertext encrypted to it. */
|
|
127
|
+
set(orgId: string, input: SetKeypairInput): Promise<OrgKeypair>
|
|
128
|
+
/** Get org `orgId`'s encryption keypair, or `undefined` when none is set
|
|
129
|
+
* (the org is keyless ⇒ plaintext delivery). */
|
|
130
|
+
get(orgId: string): Promise<OrgKeypair | undefined>
|
|
130
131
|
}
|
|
131
132
|
results: {
|
|
132
|
-
/** Decrypt a callback body (
|
|
133
|
-
* Pass the
|
|
134
|
-
*
|
|
133
|
+
/** Decrypt a callback body (ECIES ciphertext or plaintext JWS) to the
|
|
134
|
+
* signed JWS. Pass the eth `credential` (`0x`-hex private key) only when
|
|
135
|
+
* the delivery is encrypted. */
|
|
135
136
|
decrypt(body: CallbackBody, credential?: DecryptionKeyInput): Promise<Jws>
|
|
136
137
|
/** Verify a decrypted JWS: JWKS discovery, ES256K signature,
|
|
137
138
|
* `reclaimSessionId`/`aud`/`exp`, nested attestor proofs. */
|
|
@@ -177,14 +178,12 @@ export function createReclaim(config: ReclaimConfig): Reclaim {
|
|
|
177
178
|
headers: config.headers,
|
|
178
179
|
})
|
|
179
180
|
|
|
180
|
-
// Load
|
|
181
|
+
// Load the eth decryption key on demand — `undefined` input ⇒ no key, so
|
|
181
182
|
// plaintext-only consumers never touch the loader.
|
|
182
183
|
const resolveKey = (
|
|
183
184
|
input: DecryptionKeyInput | undefined,
|
|
184
|
-
):
|
|
185
|
-
input === undefined
|
|
186
|
-
? Promise.resolve(undefined)
|
|
187
|
-
: loadDecryptionKey(input)
|
|
185
|
+
): DecryptionKey | undefined => (
|
|
186
|
+
input === undefined ? undefined : loadDecryptionKey(input)
|
|
188
187
|
)
|
|
189
188
|
|
|
190
189
|
return {
|
|
@@ -232,27 +231,31 @@ export function createReclaim(config: ReclaimConfig): Reclaim {
|
|
|
232
231
|
})
|
|
233
232
|
},
|
|
234
233
|
},
|
|
235
|
-
|
|
234
|
+
keypair: {
|
|
236
235
|
async set(orgId, input) {
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
const
|
|
240
|
-
input.
|
|
241
|
-
|
|
242
|
-
|
|
236
|
+
// Forward the eth public key as given (hex or JWK); the API stores
|
|
237
|
+
// the secp256k1 JWK + derived address.
|
|
238
|
+
const body = {
|
|
239
|
+
...(input.publicKeyJwk
|
|
240
|
+
? { publicKeyJwk: input.publicKeyJwk }
|
|
241
|
+
: { publicKey: input.publicKey }),
|
|
242
|
+
canEncryptResult: input.canEncryptResult ?? false,
|
|
243
|
+
...(input.label ? { label: input.label } : {}),
|
|
244
|
+
} as OrgKeypairRequest
|
|
245
|
+
const { data } = await client.call('SetOrgKeypair', {
|
|
243
246
|
params: { orgId },
|
|
244
|
-
body
|
|
247
|
+
body,
|
|
245
248
|
})
|
|
246
249
|
return data
|
|
247
250
|
},
|
|
248
251
|
async get(orgId) {
|
|
249
252
|
try {
|
|
250
|
-
const { data } = await client.call('
|
|
253
|
+
const { data } = await client.call('GetOrgKeypair', {
|
|
251
254
|
params: { orgId },
|
|
252
255
|
})
|
|
253
256
|
return data
|
|
254
257
|
} catch(err) {
|
|
255
|
-
// 404 ⇒
|
|
258
|
+
// 404 ⇒ keyless org (plaintext delivery).
|
|
256
259
|
if(isNotFound(err)) {
|
|
257
260
|
return undefined
|
|
258
261
|
}
|
|
@@ -263,7 +266,7 @@ export function createReclaim(config: ReclaimConfig): Reclaim {
|
|
|
263
266
|
},
|
|
264
267
|
results: {
|
|
265
268
|
async decrypt(body, credential) {
|
|
266
|
-
return decryptCallback(
|
|
269
|
+
return decryptCallback(resolveKey(credential), body)
|
|
267
270
|
},
|
|
268
271
|
verify(jws, opts) {
|
|
269
272
|
return verifyResultFull(jws, { fetchImpl: config.fetch, ...opts })
|
|
@@ -292,7 +295,7 @@ export function createReclaim(config: ReclaimConfig): Reclaim {
|
|
|
292
295
|
}
|
|
293
296
|
|
|
294
297
|
const { credential, ...verifyOpts } = opts ?? {}
|
|
295
|
-
const key =
|
|
298
|
+
const key = resolveKey(credential)
|
|
296
299
|
const jws = await decryptCallback(key, body)
|
|
297
300
|
const result = await verifyResultFull(jws, {
|
|
298
301
|
fetchImpl: config.fetch,
|