@reclaimprotocol/client 0.1.0-dev.1 → 0.1.0-dev.2
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 +256 -0
- package/lib/attestor-proof.d.ts +16 -0
- package/lib/attestor-proof.js +121 -0
- package/lib/canonical.d.ts +1 -0
- package/lib/canonical.js +18 -0
- package/lib/eip712.d.ts +20 -0
- package/lib/eip712.js +108 -0
- package/lib/enckeys.d.ts +33 -0
- package/lib/enckeys.js +14 -0
- package/lib/encoding.d.ts +11 -0
- package/lib/encoding.js +34 -0
- package/lib/index.d.ts +11 -3
- package/lib/index.js +10 -2
- package/lib/jwks.d.ts +26 -0
- package/lib/jwks.js +63 -0
- package/lib/keypair.d.ts +12 -0
- package/lib/keypair.js +18 -17
- package/lib/reclaim.d.ts +128 -0
- package/lib/reclaim.js +155 -0
- package/lib/result.d.ts +132 -0
- package/lib/result.js +298 -0
- package/lib/routes.gen.d.ts +121 -21
- package/lib/routes.gen.js +32 -7
- package/lib/secp.d.ts +6 -0
- package/lib/secp.js +21 -0
- package/lib/types/index.d.ts +1 -1
- package/lib/types/index.js +1 -0
- package/lib/types/openapi.d.ts +18 -1
- package/lib/types/openapi.gen.d.ts +2657 -1148
- package/package.json +14 -5
- package/src/attestor-proof.ts +148 -0
- package/src/canonical.ts +20 -0
- package/src/enckeys.ts +46 -0
- package/src/encoding.ts +39 -0
- package/src/index.ts +60 -2
- package/src/jwks.ts +86 -0
- package/src/keypair.ts +22 -20
- package/src/reclaim.ts +332 -0
- package/src/result.ts +448 -0
- package/src/routes.gen.ts +32 -7
- package/src/secp.ts +24 -0
- package/src/types/index.ts +1 -1
- package/src/types/openapi.gen.ts +2657 -1148
- package/src/types/openapi.ts +48 -18
package/lib/index.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export * from "./client.js";
|
|
2
|
+
export { throwProblemError } from "./errors.js";
|
|
2
3
|
export { DEFAULT_BASE_URL, PROBLEM_CONTENT_TYPE, USER_AGENT } from "./consts.js";
|
|
3
|
-
export { deriveFromPrivateKey, generateKeypair, } from "./keypair.js";
|
|
4
|
-
export
|
|
4
|
+
export { addressFromPublicKey, deriveFromPrivateKey, generateKeypair, } from "./keypair.js";
|
|
5
|
+
export { canonicalJson } from "./canonical.js";
|
|
6
|
+
export { b64url, base64urlToBytes, bytesToHex, hexToBytes, } from "./encoding.js";
|
|
7
|
+
export { addressFromUncompressedPubkey, secp, } from "./secp.js";
|
|
8
|
+
export { decodeJwsPayload, decryptCallback, encryptForPublicJwk, ResultVerificationError, signResultJws, verifyJwsSignature, verifyResult, verifyResultFull, } from "./result.js";
|
|
9
|
+
export { loadDecryptionKey, publicPemToJwk, } from "./enckeys.js";
|
|
10
|
+
export { createReclaim, TERMINAL_EVENTS, } from "./reclaim.js";
|
|
11
|
+
export { fetchJwks, jwksFromPrivateKey, jwkToUncompressedPublicKey, selectKey, } from "./jwks.js";
|
|
12
|
+
export { verifyNestedProof, } from "./attestor-proof.js";
|
package/lib/jwks.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface Jwk {
|
|
2
|
+
kty: string;
|
|
3
|
+
crv: string;
|
|
4
|
+
x: string;
|
|
5
|
+
y: string;
|
|
6
|
+
kid?: string;
|
|
7
|
+
alg?: string;
|
|
8
|
+
use?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Build the JWK set for an ES256K signing key (the Verification Client's
|
|
12
|
+
* result-signing key). `kid` is the key's ETH address; `x`/`y` are the
|
|
13
|
+
* uncompressed public-key coordinates. Empty set when no key is configured.
|
|
14
|
+
*/
|
|
15
|
+
export declare function jwksFromPrivateKey(privateKeyHex?: string): {
|
|
16
|
+
keys: Jwk[];
|
|
17
|
+
};
|
|
18
|
+
/** Fetch the JWK set published at `<iss>/.well-known/jwks.json`. */
|
|
19
|
+
export declare function fetchJwks(iss: string, fetchImpl?: typeof fetch): Promise<Jwk[]>;
|
|
20
|
+
/** Find the key matching `kid` (case-insensitive — kids are ETH addresses). */
|
|
21
|
+
export declare function selectKey(keys: Jwk[], kid: string): Jwk | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Reconstruct the uncompressed secp256k1 public key (`0x04` + X + Y) from an
|
|
24
|
+
* EC JWK's base64url `x`/`y` coordinates, for signature verification.
|
|
25
|
+
*/
|
|
26
|
+
export declare function jwkToUncompressedPublicKey(jwk: Jwk): string;
|
package/lib/jwks.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JWKS (RFC 7517) discovery for verifying result/proof signatures. Signers
|
|
3
|
+
* (the Verification Client, the Attestor) publish their secp256k1 public keys
|
|
4
|
+
* at `<iss>/.well-known/jwks.json` with `kid` = the signer's ETH address.
|
|
5
|
+
*/
|
|
6
|
+
import { b64url, base64urlToBytes, bytesToHex } from "./encoding.js";
|
|
7
|
+
import { addressFromUncompressedPubkey, secp } from "./secp.js";
|
|
8
|
+
/**
|
|
9
|
+
* Build the JWK set for an ES256K signing key (the Verification Client's
|
|
10
|
+
* result-signing key). `kid` is the key's ETH address; `x`/`y` are the
|
|
11
|
+
* uncompressed public-key coordinates. Empty set when no key is configured.
|
|
12
|
+
*/
|
|
13
|
+
export function jwksFromPrivateKey(privateKeyHex) {
|
|
14
|
+
if (!privateKeyHex) {
|
|
15
|
+
return { keys: [] };
|
|
16
|
+
}
|
|
17
|
+
const priv = privateKeyHex.replace(/^0x/, '');
|
|
18
|
+
const pub = secp.getPublicKey(Uint8Array.from(Buffer.from(priv, 'hex')), false);
|
|
19
|
+
return {
|
|
20
|
+
keys: [
|
|
21
|
+
{
|
|
22
|
+
kty: 'EC',
|
|
23
|
+
crv: 'secp256k1',
|
|
24
|
+
alg: 'ES256K',
|
|
25
|
+
use: 'sig',
|
|
26
|
+
kid: addressFromUncompressedPubkey(pub),
|
|
27
|
+
x: b64url(pub.subarray(1, 33)),
|
|
28
|
+
y: b64url(pub.subarray(33, 65)),
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Fetch the JWK set published at `<iss>/.well-known/jwks.json`. */
|
|
34
|
+
export async function fetchJwks(iss, fetchImpl = fetch) {
|
|
35
|
+
const base = iss.replace(/\/+$/, '');
|
|
36
|
+
const res = await fetchImpl(`${base}/.well-known/jwks.json`);
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
throw new Error(`JWKS fetch failed for ${base}: HTTP ${res.status}`);
|
|
39
|
+
}
|
|
40
|
+
const body = (await res.json());
|
|
41
|
+
return body.keys ?? [];
|
|
42
|
+
}
|
|
43
|
+
/** Find the key matching `kid` (case-insensitive — kids are ETH addresses). */
|
|
44
|
+
export function selectKey(keys, kid) {
|
|
45
|
+
const want = kid.toLowerCase();
|
|
46
|
+
return keys.find((k) => (k.kid ?? '').toLowerCase() === want);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Reconstruct the uncompressed secp256k1 public key (`0x04` + X + Y) from an
|
|
50
|
+
* EC JWK's base64url `x`/`y` coordinates, for signature verification.
|
|
51
|
+
*/
|
|
52
|
+
export function jwkToUncompressedPublicKey(jwk) {
|
|
53
|
+
const x = base64urlToBytes(jwk.x);
|
|
54
|
+
const y = base64urlToBytes(jwk.y);
|
|
55
|
+
if (x.length !== 32 || y.length !== 32) {
|
|
56
|
+
throw new Error('invalid EC JWK: x/y must be 32 bytes each');
|
|
57
|
+
}
|
|
58
|
+
const out = new Uint8Array(65);
|
|
59
|
+
out[0] = 0x04;
|
|
60
|
+
out.set(x, 1);
|
|
61
|
+
out.set(y, 33);
|
|
62
|
+
return '0x' + bytesToHex(out);
|
|
63
|
+
}
|
package/lib/keypair.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export type EthKeypair = {
|
|
2
2
|
address: string;
|
|
3
3
|
privateKey: string;
|
|
4
|
+
/** Uncompressed secp256k1 public key (`0x04` + 128 hex). Used to derive the
|
|
5
|
+
* ETH address and to verify ES256K JWS signatures (published via JWKS as
|
|
6
|
+
* `x`/`y`). Result *encryption* now uses X25519 JWE, not this key. */
|
|
7
|
+
publicKey: string;
|
|
4
8
|
};
|
|
5
9
|
/**
|
|
6
10
|
* Generate a fresh secp256k1 key-pair and derive its lowercase
|
|
@@ -19,3 +23,11 @@ export declare function generateKeypair(): EthKeypair;
|
|
|
19
23
|
* importing a key the user already holds.
|
|
20
24
|
*/
|
|
21
25
|
export declare function deriveFromPrivateKey(privateKey: string): EthKeypair;
|
|
26
|
+
/**
|
|
27
|
+
* Derive the lowercase Ethereum address that owns an uncompressed secp256k1
|
|
28
|
+
* public key (`0x04` + 128 hex) — keccak-256 of the key minus the `0x04`
|
|
29
|
+
* prefix byte, last 20 bytes. The holder of the matching private key controls
|
|
30
|
+
* this address; used to scope a callback subscription to its key's owner
|
|
31
|
+
* without ever needing the private key.
|
|
32
|
+
*/
|
|
33
|
+
export declare function addressFromPublicKey(publicKey: string): string;
|
package/lib/keypair.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { keccak_256 } from '@noble/hashes/sha3.js';
|
|
2
|
+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js';
|
|
2
3
|
import * as secp from '@noble/secp256k1';
|
|
3
4
|
/**
|
|
4
5
|
* Generate a fresh secp256k1 key-pair and derive its lowercase
|
|
@@ -15,8 +16,9 @@ export function generateKeypair() {
|
|
|
15
16
|
const priv = secp.utils.randomSecretKey();
|
|
16
17
|
const pub = secp.getPublicKey(priv, false);
|
|
17
18
|
return {
|
|
18
|
-
address:
|
|
19
|
+
address: addressFromPubBytes(pub),
|
|
19
20
|
privateKey: '0x' + bytesToHex(priv),
|
|
21
|
+
publicKey: '0x' + bytesToHex(pub),
|
|
20
22
|
};
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
@@ -28,24 +30,23 @@ export function deriveFromPrivateKey(privateKey) {
|
|
|
28
30
|
const priv = hexToBytes(hex);
|
|
29
31
|
const pub = secp.getPublicKey(priv, false);
|
|
30
32
|
return {
|
|
31
|
-
address:
|
|
33
|
+
address: addressFromPubBytes(pub),
|
|
32
34
|
privateKey: '0x' + hex,
|
|
35
|
+
publicKey: '0x' + bytesToHex(pub),
|
|
33
36
|
};
|
|
34
37
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Derive the lowercase Ethereum address that owns an uncompressed secp256k1
|
|
40
|
+
* public key (`0x04` + 128 hex) — keccak-256 of the key minus the `0x04`
|
|
41
|
+
* prefix byte, last 20 bytes. The holder of the matching private key controls
|
|
42
|
+
* this address; used to scope a callback subscription to its key's owner
|
|
43
|
+
* without ever needing the private key.
|
|
44
|
+
*/
|
|
45
|
+
export function addressFromPublicKey(publicKey) {
|
|
46
|
+
return addressFromPubBytes(hexToBytes(publicKey.replace(/^0x/, '')));
|
|
41
47
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const out = new Uint8Array(hex.length / 2);
|
|
47
|
-
for (let i = 0; i < out.length; i++) {
|
|
48
|
-
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
49
|
-
}
|
|
50
|
-
return out;
|
|
48
|
+
/** keccak-256 of the uncompressed pubkey minus its `0x04` prefix byte,
|
|
49
|
+
* last 20 bytes — the Ethereum address derivation, lowercase (no checksum). */
|
|
50
|
+
function addressFromPubBytes(pub) {
|
|
51
|
+
return '0x' + bytesToHex(keccak_256(pub.slice(1))).slice(-40);
|
|
51
52
|
}
|
package/lib/reclaim.d.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative entry point for the consumer SDK. The ONLY config is the org
|
|
3
|
+
* secret (`rorg_…`) — it authenticates every call and identifies the org
|
|
4
|
+
* server-side. Everything else is passed where it's actually used: the `orgId`
|
|
5
|
+
* to the org-scoped `credential.set`/`get`, and the X25519 decryption key to
|
|
6
|
+
* `results.decrypt`/`receive` (only when results arrive encrypted):
|
|
7
|
+
*
|
|
8
|
+
* const reclaim = createReclaim({ orgSecret })
|
|
9
|
+
* // one-time setup:
|
|
10
|
+
* await reclaim.callbacks.register({ callbackUrl })
|
|
11
|
+
* // enable encryption:
|
|
12
|
+
* await reclaim.credential.set(orgId, { publicKeyJwk })
|
|
13
|
+
* const session = await reclaim.sessions.create({ providerId, context })
|
|
14
|
+
* // …end-user completes session.verificationUrl…
|
|
15
|
+
* const outcome = await reclaim.results.receive(callbackBody, {
|
|
16
|
+
* credential: privateJwk, // omit for plaintext
|
|
17
|
+
* expectedReclaimSessionId: session.id,
|
|
18
|
+
* })
|
|
19
|
+
*
|
|
20
|
+
* Built by composition over the package's pure helpers (`decryptCallback`,
|
|
21
|
+
* `verifyResultFull`, …) — drop down to those, or to `reclaim.api`, for full
|
|
22
|
+
* control.
|
|
23
|
+
*/
|
|
24
|
+
import type { components } from './types/openapi.gen.ts';
|
|
25
|
+
import { ReclaimClient } from './client.ts';
|
|
26
|
+
import { type DecryptionKeyInput } from './enckeys.ts';
|
|
27
|
+
import { type CallbackBody, type Jws, type VerifyResultFullOptions, type VerifyResultFullOutcome } from './result.ts';
|
|
28
|
+
type CallbackSubscription = components['schemas']['CallbackSubscription'];
|
|
29
|
+
type VerificationSession = components['schemas']['VerificationSession'];
|
|
30
|
+
type VerificationEvent = components['schemas']['VerificationEvent'];
|
|
31
|
+
type VerificationEventRecord = components['schemas']['VerificationEventRecord'];
|
|
32
|
+
type OrgCredential = components['schemas']['OrgCredential'];
|
|
33
|
+
/** The three terminal events a subscription can receive. */
|
|
34
|
+
export declare const TERMINAL_EVENTS: readonly ["verification_success", "verification_rejected", "verification_error"];
|
|
35
|
+
export interface RegisterCallbackInput {
|
|
36
|
+
callbackUrl: string;
|
|
37
|
+
events: VerificationEvent[];
|
|
38
|
+
}
|
|
39
|
+
export interface CreateSessionInput {
|
|
40
|
+
providerId: string;
|
|
41
|
+
version?: string;
|
|
42
|
+
context: Record<string, unknown>;
|
|
43
|
+
/** URL with a `{sessionId}` placeholder the end-user opens. */
|
|
44
|
+
verificationClientUrl: string;
|
|
45
|
+
}
|
|
46
|
+
export interface SetCredentialInput {
|
|
47
|
+
/** The org's X25519 encryption public key as an SPKI PEM — the content of
|
|
48
|
+
* `openssl pkey -in key.pem -pubout` (the `-----BEGIN PUBLIC KEY-----`
|
|
49
|
+
* block). It's converted to a JWK and stored; results are JWE-encrypted to
|
|
50
|
+
* it. Keep the matching private PEM to decrypt (pass it to
|
|
51
|
+
* `results.receive`/`decrypt`). */
|
|
52
|
+
publicKeyPem: string;
|
|
53
|
+
label?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface ReclaimConfig {
|
|
56
|
+
/** The org secret (`rorg_…`). Authenticates ALL API calls and identifies
|
|
57
|
+
* the org server-side. The only required config. */
|
|
58
|
+
orgSecret: string;
|
|
59
|
+
/** Backend base URL. Defaults to the production Builder. */
|
|
60
|
+
baseUrl?: string;
|
|
61
|
+
/** Custom fetch implementation. */
|
|
62
|
+
fetch?: typeof fetch;
|
|
63
|
+
/** Extra headers merged into every request. */
|
|
64
|
+
headers?: Record<string, string>;
|
|
65
|
+
}
|
|
66
|
+
/** Options for `results.receive` — the verification options plus an optional
|
|
67
|
+
* X25519 decryption key (a JWK, a PKCS#8 PEM string, or `{ env }` / env-var
|
|
68
|
+
* name). The key is needed only when the delivery is an encrypted (JWE). */
|
|
69
|
+
export type ReceiveOptions = VerifyResultFullOptions & {
|
|
70
|
+
credential?: DecryptionKeyInput;
|
|
71
|
+
};
|
|
72
|
+
export interface Reclaim {
|
|
73
|
+
/** Typed escape hatch: `reclaim.api('OperationId', args)` for any spec
|
|
74
|
+
* operation. */
|
|
75
|
+
api: ReclaimClient['call'];
|
|
76
|
+
sessions: {
|
|
77
|
+
/** Create a verification session. `verificationClientUrl` defaults to
|
|
78
|
+
* the Builder's own `/v/{sessionId}` page. */
|
|
79
|
+
create(input: Omit<CreateSessionInput, 'verificationClientUrl'> & Partial<Pick<CreateSessionInput, 'verificationClientUrl'>>): Promise<VerificationSession>;
|
|
80
|
+
get(sessionId: string): Promise<VerificationSession>;
|
|
81
|
+
listEvents(sessionId: string): Promise<VerificationEventRecord[]>;
|
|
82
|
+
};
|
|
83
|
+
callbacks: {
|
|
84
|
+
/** Register a callback subscription (one-time setup). Delivery is scoped
|
|
85
|
+
* to the token's org; results are JWE-encrypted to the org's credential
|
|
86
|
+
* when one is set (see `credential.set`), else plaintext. `events`
|
|
87
|
+
* defaults to all three terminal events. */
|
|
88
|
+
register(input: Partial<RegisterCallbackInput> & Pick<RegisterCallbackInput, 'callbackUrl'>): Promise<CallbackSubscription>;
|
|
89
|
+
list(): Promise<CallbackSubscription[]>;
|
|
90
|
+
remove(subscriptionId: string): Promise<void>;
|
|
91
|
+
};
|
|
92
|
+
credential: {
|
|
93
|
+
/** Set/replace org `orgId`'s single X25519 encryption credential. Once
|
|
94
|
+
* set, terminal results are delivered as a compact JWE encrypted to it. */
|
|
95
|
+
set(orgId: string, input: SetCredentialInput): Promise<OrgCredential>;
|
|
96
|
+
/** Get org `orgId`'s encryption credential, or `undefined` when none is
|
|
97
|
+
* set (delivery is plaintext). */
|
|
98
|
+
get(orgId: string): Promise<OrgCredential | undefined>;
|
|
99
|
+
};
|
|
100
|
+
results: {
|
|
101
|
+
/** Decrypt a callback body (JWE or plaintext JWS) to the signed JWS.
|
|
102
|
+
* Pass the X25519 `credential` (JWK/PEM/env) only when the delivery is
|
|
103
|
+
* a JWE. */
|
|
104
|
+
decrypt(body: CallbackBody, credential?: DecryptionKeyInput): Promise<Jws>;
|
|
105
|
+
/** Verify a decrypted JWS: JWKS discovery, ES256K signature,
|
|
106
|
+
* `reclaimSessionId`/`aud`/`exp`, nested attestor proofs. */
|
|
107
|
+
verify(jws: Jws, opts?: VerifyResultFullOptions): Promise<VerifyResultFullOutcome>;
|
|
108
|
+
/** Handle any callback delivery in one call — the 90% path. Branches on
|
|
109
|
+
* the envelope's `event`: a terminal event is decrypted (with
|
|
110
|
+
* `opts.credential` when encrypted) + verified into a `result`; a
|
|
111
|
+
* non-terminal event is returned as a plaintext (unsigned) `event`
|
|
112
|
+
* notification without decryption. (A raw JWE/JWS body with no envelope
|
|
113
|
+
* `event` is treated as a terminal result.) */
|
|
114
|
+
receive(body: CallbackBody, opts?: ReceiveOptions): Promise<ReceivedDelivery>;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Outcome of `results.receive` — a verified terminal result or a plaintext
|
|
118
|
+
* non-terminal progress notification. Branch on `kind`. */
|
|
119
|
+
export type ReceivedDelivery = {
|
|
120
|
+
kind: 'result';
|
|
121
|
+
result: VerifyResultFullOutcome;
|
|
122
|
+
} | {
|
|
123
|
+
kind: 'event';
|
|
124
|
+
event: VerificationEvent;
|
|
125
|
+
eventData: Record<string, unknown>;
|
|
126
|
+
};
|
|
127
|
+
export declare function createReclaim(config: ReclaimConfig): Reclaim;
|
|
128
|
+
export {};
|
package/lib/reclaim.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { ReclaimClient } from "./client.js";
|
|
2
|
+
import { loadDecryptionKey, publicPemToJwk, } from "./enckeys.js";
|
|
3
|
+
import { decryptCallback, verifyResultFull, } from "./result.js";
|
|
4
|
+
/** The three terminal events a subscription can receive. */
|
|
5
|
+
export const TERMINAL_EVENTS = [
|
|
6
|
+
'verification_success',
|
|
7
|
+
'verification_rejected',
|
|
8
|
+
'verification_error',
|
|
9
|
+
];
|
|
10
|
+
export function createReclaim(config) {
|
|
11
|
+
if (!config?.orgSecret) {
|
|
12
|
+
throw new Error('createReclaim requires an org secret — pass '
|
|
13
|
+
+ 'createReclaim({ orgSecret: <rorg_… org secret> })');
|
|
14
|
+
}
|
|
15
|
+
const client = new ReclaimClient({
|
|
16
|
+
baseUrl: config.baseUrl,
|
|
17
|
+
token: config.orgSecret,
|
|
18
|
+
fetch: config.fetch,
|
|
19
|
+
headers: config.headers,
|
|
20
|
+
});
|
|
21
|
+
// Load an X25519 decryption key on demand — `undefined` input ⇒ no key, so
|
|
22
|
+
// plaintext-only consumers never touch the loader.
|
|
23
|
+
const resolveKey = (input) => (input === undefined
|
|
24
|
+
? Promise.resolve(undefined)
|
|
25
|
+
: loadDecryptionKey(input));
|
|
26
|
+
return {
|
|
27
|
+
api: client.call.bind(client),
|
|
28
|
+
sessions: {
|
|
29
|
+
async create(input) {
|
|
30
|
+
const body = {
|
|
31
|
+
...input,
|
|
32
|
+
verificationClientUrl: input.verificationClientUrl ?? `${client.baseUrl}/v/{sessionId}`,
|
|
33
|
+
};
|
|
34
|
+
const { data } = await client.call('CreateVerificationSession', {
|
|
35
|
+
body,
|
|
36
|
+
});
|
|
37
|
+
return data;
|
|
38
|
+
},
|
|
39
|
+
async get(sessionId) {
|
|
40
|
+
const { data } = await client.call('GetVerificationSession', {
|
|
41
|
+
params: { sessionId },
|
|
42
|
+
});
|
|
43
|
+
return data;
|
|
44
|
+
},
|
|
45
|
+
async listEvents(sessionId) {
|
|
46
|
+
const { data } = await client.call('ListVerificationEvents', {
|
|
47
|
+
params: { sessionId },
|
|
48
|
+
});
|
|
49
|
+
return data.items;
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
callbacks: {
|
|
53
|
+
async register(input) {
|
|
54
|
+
const events = input.events ?? [...TERMINAL_EVENTS];
|
|
55
|
+
const { data } = await client.call('CreateCallbackSubscription', {
|
|
56
|
+
body: { callbackUrl: input.callbackUrl, events },
|
|
57
|
+
});
|
|
58
|
+
return data;
|
|
59
|
+
},
|
|
60
|
+
async list() {
|
|
61
|
+
const { data } = await client.call('ListCallbackSubscriptions');
|
|
62
|
+
return data.items;
|
|
63
|
+
},
|
|
64
|
+
async remove(subscriptionId) {
|
|
65
|
+
await client.call('DeleteCallbackSubscription', {
|
|
66
|
+
params: { subscriptionId },
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
credential: {
|
|
71
|
+
async set(orgId, input) {
|
|
72
|
+
// Convert the consumer's SPKI public PEM to the JWK the API stores
|
|
73
|
+
// (PEM at the edge; JWK on the wire/DB/VC stays unchanged).
|
|
74
|
+
const publicKeyJwk = (await publicPemToJwk(input.publicKeyPem));
|
|
75
|
+
const { data } = await client.call('SetOrgCredential', {
|
|
76
|
+
params: { orgId },
|
|
77
|
+
body: { publicKeyJwk, label: input.label },
|
|
78
|
+
});
|
|
79
|
+
return data;
|
|
80
|
+
},
|
|
81
|
+
async get(orgId) {
|
|
82
|
+
try {
|
|
83
|
+
const { data } = await client.call('GetOrgCredential', {
|
|
84
|
+
params: { orgId },
|
|
85
|
+
});
|
|
86
|
+
return data;
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
// 404 ⇒ no credential set (plaintext delivery).
|
|
90
|
+
if (isNotFound(err)) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
results: {
|
|
98
|
+
async decrypt(body, credential) {
|
|
99
|
+
return decryptCallback(await resolveKey(credential), body);
|
|
100
|
+
},
|
|
101
|
+
verify(jws, opts) {
|
|
102
|
+
return verifyResultFull(jws, { fetchImpl: config.fetch, ...opts });
|
|
103
|
+
},
|
|
104
|
+
async receive(body, opts) {
|
|
105
|
+
// A delivery envelope tagged with a non-terminal event carries a
|
|
106
|
+
// plaintext, unsigned `{ event, eventData }` payload — return it
|
|
107
|
+
// directly, no decryption/verification.
|
|
108
|
+
if (body && typeof body === 'object'
|
|
109
|
+
&& !(body instanceof Uint8Array)
|
|
110
|
+
&& !(body instanceof ArrayBuffer)) {
|
|
111
|
+
const env = body;
|
|
112
|
+
if (env.event
|
|
113
|
+
&& !TERMINAL_EVENTS.includes(env.event)) {
|
|
114
|
+
const decoded = parseEventData(env.data);
|
|
115
|
+
return {
|
|
116
|
+
kind: 'event',
|
|
117
|
+
event: env.event,
|
|
118
|
+
eventData: decoded,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const { credential, ...verifyOpts } = opts ?? {};
|
|
123
|
+
const key = await resolveKey(credential);
|
|
124
|
+
const jws = await decryptCallback(key, body);
|
|
125
|
+
const result = await verifyResultFull(jws, {
|
|
126
|
+
fetchImpl: config.fetch,
|
|
127
|
+
...verifyOpts,
|
|
128
|
+
});
|
|
129
|
+
return { kind: 'result', result };
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** Non-terminal `data` is a base64-encoded JSON `{ event, eventData }`. */
|
|
135
|
+
function parseEventData(data) {
|
|
136
|
+
if (!data) {
|
|
137
|
+
return {};
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const decoded = JSON.parse(Buffer.from(data, 'base64').toString('utf8'));
|
|
141
|
+
return decoded.eventData ?? {};
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** True for a `404` thrown by {@link ReclaimClient.call} — a Boom error whose
|
|
148
|
+
* `output.statusCode` is 404 (see ./errors.ts). */
|
|
149
|
+
function isNotFound(err) {
|
|
150
|
+
if (typeof err !== 'object' || err === null) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
const boom = err;
|
|
154
|
+
return boom.output?.statusCode === 404 || boom.statusCode === 404;
|
|
155
|
+
}
|
package/lib/result.d.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { JWK } from 'jose';
|
|
2
|
+
import type { components } from './types/openapi.gen.ts';
|
|
3
|
+
import type { DecryptionKey } from './enckeys.ts';
|
|
4
|
+
export interface Jws {
|
|
5
|
+
payload: string;
|
|
6
|
+
signatures: {
|
|
7
|
+
protected: string;
|
|
8
|
+
header?: Record<string, unknown>;
|
|
9
|
+
signature: string;
|
|
10
|
+
}[];
|
|
11
|
+
}
|
|
12
|
+
export interface VerifyResultOptions {
|
|
13
|
+
/** Uncompressed ETH public key (`0x04`+128 hex) of the signer; when given,
|
|
14
|
+
* the JWS signature is verified. Omit to only decode + check the nonce. */
|
|
15
|
+
signerPublicKey?: string;
|
|
16
|
+
/** The `reclaimSessionId` the consumer expects (the session id they got
|
|
17
|
+
* back at creation). When given, mismatch fails verification. */
|
|
18
|
+
expectedReclaimSessionId?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface VerifyResultOutcome {
|
|
21
|
+
valid: boolean;
|
|
22
|
+
reason?: string;
|
|
23
|
+
payload: Record<string, unknown>;
|
|
24
|
+
reclaimSessionId?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* JWE-encrypt `plaintext` for the holder of `publicJwk` (an X25519 public JWK,
|
|
28
|
+
* `{kty:'OKP', crv:'X25519', x}`) as a compact JWE (`ECDH-ES + A256GCM`). Used
|
|
29
|
+
* by the first-party Verification Client to encrypt the signed JWS for each
|
|
30
|
+
* Consumer before submitting it to the Builder (which only relays the
|
|
31
|
+
* ciphertext). Consumers never call this — they only {@link decryptCallback}.
|
|
32
|
+
*
|
|
33
|
+
* `plaintext` is bytes (e.g. `new TextEncoder().encode(JSON.stringify(jws))`);
|
|
34
|
+
* returns the compact JWE string.
|
|
35
|
+
*/
|
|
36
|
+
export declare function encryptForPublicJwk(publicJwk: JWK, plaintext: Uint8Array): Promise<string>;
|
|
37
|
+
/**
|
|
38
|
+
* What a consumer's callback endpoint receives — the Builder POSTs the JSON
|
|
39
|
+
* `VerificationResultDelivery` envelope: plaintext `sessionId`/`event`/
|
|
40
|
+
* `timestamp` routing hints. For terminal events `data` is a compact JWE
|
|
41
|
+
* string wrapping the signed JWS (or the plaintext JWS JSON when the org has
|
|
42
|
+
* no encryption credential). The envelope is unauthenticated; trust only the
|
|
43
|
+
* decrypted+verified JWS.
|
|
44
|
+
*
|
|
45
|
+
* Derived from the generated spec type so it can't drift; the hint fields
|
|
46
|
+
* are relaxed to optional — decryption only needs `data`.
|
|
47
|
+
*/
|
|
48
|
+
export type DeliveryEnvelope = Partial<components['schemas']['VerificationResultDelivery']> & Pick<components['schemas']['VerificationResultDelivery'], 'data'>;
|
|
49
|
+
/** Anything a callback handler may have on hand: the parsed envelope, its
|
|
50
|
+
* JSON text, its raw body bytes — or a bare compact JWE / JWS string. */
|
|
51
|
+
export type CallbackBody = DeliveryEnvelope | string | Uint8Array | ArrayBuffer;
|
|
52
|
+
/**
|
|
53
|
+
* Decrypt a delivered terminal callback to the signed JWS. `data` is either a
|
|
54
|
+
* compact JWE (decrypted with the X25519 `privateKey`) or — when the org has no
|
|
55
|
+
* encryption credential — the plaintext JWS JSON. A compact JWE has exactly
|
|
56
|
+
* five dot-separated base64url segments; a JSON JWS starts with `{`.
|
|
57
|
+
*
|
|
58
|
+
* `privateKey` may be absent (plaintext delivery); an encrypted delivery with
|
|
59
|
+
* no key throws a clear error.
|
|
60
|
+
*/
|
|
61
|
+
export declare function decryptCallback(privateKey: DecryptionKey | undefined, body: CallbackBody): Promise<Jws>;
|
|
62
|
+
/**
|
|
63
|
+
* Sign a verification result as a flattened JWS (JOSE ES256K = ECDSA/secp256k1
|
|
64
|
+
* over SHA-256 of the `<protected>.<payload>` signing input — a single hash).
|
|
65
|
+
* `kid` is the signer's ETH address (published via JWKS); `iss` is the issuer
|
|
66
|
+
* URL. Used by the Verification Client to sign results before encrypting them.
|
|
67
|
+
*/
|
|
68
|
+
export declare function signResultJws(privateKeyHex: string, payload: Record<string, unknown>, iss: string): Jws;
|
|
69
|
+
/** Decode the JWS payload (base64url JSON). */
|
|
70
|
+
export declare function decodeJwsPayload(jws: Jws): Record<string, unknown>;
|
|
71
|
+
/**
|
|
72
|
+
* Verify a JWS signature (JOSE ES256K = ECDSA/secp256k1 over SHA-256 of the
|
|
73
|
+
* `<protected>.<payload>` signing input) with an uncompressed public key.
|
|
74
|
+
*/
|
|
75
|
+
export declare function verifyJwsSignature(jws: Jws, signerPublicKey: string): boolean;
|
|
76
|
+
export interface VerifyResultFullOptions {
|
|
77
|
+
/** The `reclaimSessionId` the consumer expects (mismatch fails). */
|
|
78
|
+
expectedReclaimSessionId?: string;
|
|
79
|
+
/** The consumer's `orgId`; the result's `aud` must match it. */
|
|
80
|
+
expectedAud?: string;
|
|
81
|
+
/** Allowlist of trusted attestor ETH addresses (lowercased compared). */
|
|
82
|
+
attestorAddresses?: string[];
|
|
83
|
+
/** Override the VC signer key instead of JWKS discovery via header `iss`. */
|
|
84
|
+
signerPublicKey?: string;
|
|
85
|
+
/** Injectable fetch (for JWKS); defaults to global `fetch`. */
|
|
86
|
+
fetchImpl?: typeof fetch;
|
|
87
|
+
/** Override "now" (unix seconds) for `exp` checks. */
|
|
88
|
+
now?: number;
|
|
89
|
+
}
|
|
90
|
+
export interface ResultProofOutcome {
|
|
91
|
+
requestId?: string;
|
|
92
|
+
proofValid: boolean;
|
|
93
|
+
reason?: string;
|
|
94
|
+
attestorAddress?: string;
|
|
95
|
+
}
|
|
96
|
+
export interface VerifyResultFullOutcome extends VerifyResultOutcome {
|
|
97
|
+
proofs?: ResultProofOutcome[];
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Thrown by {@link verifyResult} / {@link verifyResultFull} (and therefore
|
|
101
|
+
* `reclaim.results.verify`/`receive`) when a result fails verification. Carries
|
|
102
|
+
* the full invalid outcome so callers can inspect it in a `catch`:
|
|
103
|
+
*
|
|
104
|
+
* try {
|
|
105
|
+
* const { result } = await reclaim.results.receive(body, { credential })
|
|
106
|
+
* // result is verified — use result.payload
|
|
107
|
+
* } catch (err) {
|
|
108
|
+
* if (err instanceof ResultVerificationError) {
|
|
109
|
+
* // e.g. 'expired', { payload, … }
|
|
110
|
+
* console.error(err.reason, err.outcome)
|
|
111
|
+
* }
|
|
112
|
+
* }
|
|
113
|
+
*/
|
|
114
|
+
export declare class ResultVerificationError extends Error {
|
|
115
|
+
/** Machine-readable failure reason — `'bad-signature'`, `'expired'`,
|
|
116
|
+
* `'reclaimSessionId-mismatch'`, `'aud-mismatch'`, `'invalid-proof'`, … */
|
|
117
|
+
readonly reason: string;
|
|
118
|
+
/** The full (invalid) outcome: `payload`, `reclaimSessionId`, and per-proof
|
|
119
|
+
* detail (`proofs`) when verification got that far. */
|
|
120
|
+
readonly outcome: VerifyResultFullOutcome;
|
|
121
|
+
constructor(outcome: VerifyResultFullOutcome);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Full consumer verification: discovers the Verification Client's signing key
|
|
125
|
+
* via JWKS (header `iss`/`kid`), verifies the top-level JWS, checks
|
|
126
|
+
* `reclaimSessionId`/`aud`/`exp`, then verifies each result's nested attestor
|
|
127
|
+
* proof and its `content_hash`. Network access (JWKS) makes this async; use
|
|
128
|
+
* {@link verifyResult} for the offline primitive.
|
|
129
|
+
*/
|
|
130
|
+
export declare function verifyResultFull(jws: Jws, opts?: VerifyResultFullOptions): Promise<VerifyResultFullOutcome>;
|
|
131
|
+
/** Decrypt-and-verify in one step. */
|
|
132
|
+
export declare function verifyResult(jws: Jws, opts?: VerifyResultOptions): VerifyResultOutcome;
|