@reclaimprotocol/client 0.1.0-dev.1 → 0.1.0
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/src/result.ts
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consumer-side handling of a delivered callback. Two entry points:
|
|
3
|
+
* - {@link verifyResult}: the offline primitive — decrypt + decode + optional
|
|
4
|
+
* ES256K signature check (caller-supplied key) + `reclaimSessionId` nonce.
|
|
5
|
+
* - {@link verifyResultFull}: the full async verification — discovers the
|
|
6
|
+
* Verification Client's key via JWKS, verifies the top-level JWS, checks
|
|
7
|
+
* `reclaimSessionId`/`aud`/`exp`, and verifies each result's nested attestor
|
|
8
|
+
* proof + `content_hash` (see ./attestor-proof.ts and ./jwks.ts).
|
|
9
|
+
*
|
|
10
|
+
* See docs/verification-session.md → "Verification by Consumer".
|
|
11
|
+
*/
|
|
12
|
+
import { sha256 } from '@noble/hashes/sha2.js'
|
|
13
|
+
import { keccak_256 } from '@noble/hashes/sha3.js'
|
|
14
|
+
import type { JWK } from 'jose'
|
|
15
|
+
import { compactDecrypt, CompactEncrypt, importJWK } from 'jose'
|
|
16
|
+
import type { components } from './types/openapi.gen.ts'
|
|
17
|
+
import { type NestedProofOutcome, verifyNestedProof } from './attestor-proof.ts'
|
|
18
|
+
import { canonicalJson } from './canonical.ts'
|
|
19
|
+
import type { DecryptionKey } from './enckeys.ts'
|
|
20
|
+
import { b64url, base64urlToBytes, bytesToHex, hexToBytes } from './encoding.ts'
|
|
21
|
+
import { fetchJwks, jwkToUncompressedPublicKey, selectKey } from './jwks.ts'
|
|
22
|
+
import { addressFromUncompressedPubkey, secp } from './secp.ts'
|
|
23
|
+
|
|
24
|
+
export interface Jws {
|
|
25
|
+
payload: string
|
|
26
|
+
signatures: {
|
|
27
|
+
protected: string
|
|
28
|
+
header?: Record<string, unknown>
|
|
29
|
+
signature: string
|
|
30
|
+
}[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface VerifyResultOptions {
|
|
34
|
+
/** Uncompressed ETH public key (`0x04`+128 hex) of the signer; when given,
|
|
35
|
+
* the JWS signature is verified. Omit to only decode + check the nonce. */
|
|
36
|
+
signerPublicKey?: string
|
|
37
|
+
/** The `reclaimSessionId` the consumer expects (the session id they got
|
|
38
|
+
* back at creation). When given, mismatch fails verification. */
|
|
39
|
+
expectedReclaimSessionId?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface VerifyResultOutcome {
|
|
43
|
+
valid: boolean
|
|
44
|
+
reason?: string
|
|
45
|
+
payload: Record<string, unknown>
|
|
46
|
+
reclaimSessionId?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
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}.
|
|
55
|
+
*
|
|
56
|
+
* `plaintext` is bytes (e.g. `new TextEncoder().encode(JSON.stringify(jws))`);
|
|
57
|
+
* returns the compact JWE string.
|
|
58
|
+
*/
|
|
59
|
+
export async function encryptForPublicJwk(
|
|
60
|
+
publicJwk: JWK,
|
|
61
|
+
plaintext: Uint8Array,
|
|
62
|
+
): 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)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* What a consumer's callback endpoint receives — the Builder POSTs the JSON
|
|
71
|
+
* `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.
|
|
76
|
+
*
|
|
77
|
+
* Derived from the generated spec type so it can't drift; the hint fields
|
|
78
|
+
* are relaxed to optional — decryption only needs `data`.
|
|
79
|
+
*/
|
|
80
|
+
export type DeliveryEnvelope = Partial<
|
|
81
|
+
components['schemas']['VerificationResultDelivery']
|
|
82
|
+
> &
|
|
83
|
+
Pick<components['schemas']['VerificationResultDelivery'], 'data'>
|
|
84
|
+
|
|
85
|
+
/** Anything a callback handler may have on hand: the parsed envelope, its
|
|
86
|
+
* JSON text, its raw body bytes — or a bare compact JWE / JWS string. */
|
|
87
|
+
export type CallbackBody =
|
|
88
|
+
| DeliveryEnvelope
|
|
89
|
+
| string
|
|
90
|
+
| Uint8Array
|
|
91
|
+
| ArrayBuffer
|
|
92
|
+
|
|
93
|
+
/** 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. */
|
|
97
|
+
function payloadFrom(body: CallbackBody): string {
|
|
98
|
+
if(typeof body === 'string') {
|
|
99
|
+
const s = body.trim()
|
|
100
|
+
// A JSON envelope/JWS starts with '{'; anything else is the bare `data`
|
|
101
|
+
// (compact JWE). An envelope's `data` is extracted; a bare JWS is returned.
|
|
102
|
+
if(s.startsWith('{')) {
|
|
103
|
+
const parsed = JSON.parse(s) as DeliveryEnvelope | Jws
|
|
104
|
+
if(typeof (parsed as DeliveryEnvelope).data === 'string') {
|
|
105
|
+
return (parsed as DeliveryEnvelope).data
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return s
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return s
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if(body instanceof ArrayBuffer) {
|
|
115
|
+
return payloadFrom(new TextDecoder().decode(new Uint8Array(body)))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if(body instanceof Uint8Array) {
|
|
119
|
+
return payloadFrom(new TextDecoder().decode(body))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return body.data
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
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 `{`.
|
|
130
|
+
*
|
|
131
|
+
* `privateKey` may be absent (plaintext delivery); an encrypted delivery with
|
|
132
|
+
* no key throws a clear error.
|
|
133
|
+
*/
|
|
134
|
+
export async function decryptCallback(
|
|
135
|
+
privateKey: DecryptionKey | undefined,
|
|
136
|
+
body: CallbackBody,
|
|
137
|
+
): Promise<Jws> {
|
|
138
|
+
const data = payloadFrom(body)
|
|
139
|
+
const trimmed = data.trim()
|
|
140
|
+
|
|
141
|
+
// Plaintext delivery: `data` is the JWS JSON directly.
|
|
142
|
+
if(trimmed.startsWith('{')) {
|
|
143
|
+
return JSON.parse(trimmed) as Jws
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Otherwise it's a compact JWE — five base64url segments.
|
|
147
|
+
if(!privateKey) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
'received an encrypted (JWE) delivery but no decryption credential was '
|
|
150
|
+
+ 'configured — pass the X25519 private key (JWK/PEM): '
|
|
151
|
+
+ 'results.receive(body, { credential }) or decrypt(body, credential)',
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const { plaintext } = await compactDecrypt(trimmed, privateKey)
|
|
156
|
+
return JSON.parse(new TextDecoder().decode(plaintext)) as Jws
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Sign a verification result as a flattened JWS (JOSE ES256K = ECDSA/secp256k1
|
|
161
|
+
* over SHA-256 of the `<protected>.<payload>` signing input — a single hash).
|
|
162
|
+
* `kid` is the signer's ETH address (published via JWKS); `iss` is the issuer
|
|
163
|
+
* URL. Used by the Verification Client to sign results before encrypting them.
|
|
164
|
+
*/
|
|
165
|
+
export function signResultJws(
|
|
166
|
+
privateKeyHex: string,
|
|
167
|
+
payload: Record<string, unknown>,
|
|
168
|
+
iss: string,
|
|
169
|
+
): Jws {
|
|
170
|
+
const priv = hexToBytes(privateKeyHex)
|
|
171
|
+
const protectedHeader = b64url(
|
|
172
|
+
JSON.stringify({
|
|
173
|
+
alg: 'ES256K',
|
|
174
|
+
kid: addressFromUncompressedPubkey(secp.getPublicKey(priv, false)),
|
|
175
|
+
typ: 'JWS',
|
|
176
|
+
iss,
|
|
177
|
+
}),
|
|
178
|
+
)
|
|
179
|
+
const payloadB64 = b64url(JSON.stringify(payload))
|
|
180
|
+
const signingInput = new TextEncoder().encode(
|
|
181
|
+
`${protectedHeader}.${payloadB64}`,
|
|
182
|
+
)
|
|
183
|
+
// Standard ES256K: a single SHA-256 of the signing input. `prehash:false`
|
|
184
|
+
// stops @noble hashing the digest again, so any JOSE tool can verify it.
|
|
185
|
+
const sig = secp.sign(sha256(signingInput), priv, { prehash: false })
|
|
186
|
+
return {
|
|
187
|
+
payload: payloadB64,
|
|
188
|
+
signatures: [{ protected: protectedHeader, signature: b64url(sig) }],
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Decode the JWS payload (base64url JSON). */
|
|
193
|
+
export function decodeJwsPayload(jws: Jws): Record<string, unknown> {
|
|
194
|
+
return JSON.parse(
|
|
195
|
+
new TextDecoder().decode(base64urlToBytes(jws.payload)),
|
|
196
|
+
) as Record<string, unknown>
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Verify a JWS signature (JOSE ES256K = ECDSA/secp256k1 over SHA-256 of the
|
|
201
|
+
* `<protected>.<payload>` signing input) with an uncompressed public key.
|
|
202
|
+
*/
|
|
203
|
+
export function verifyJwsSignature(jws: Jws, signerPublicKey: string): boolean {
|
|
204
|
+
const pub = hexToBytes(signerPublicKey)
|
|
205
|
+
return jws.signatures.some((s) => {
|
|
206
|
+
try {
|
|
207
|
+
const signingInput = new TextEncoder().encode(
|
|
208
|
+
`${s.protected}.${jws.payload}`
|
|
209
|
+
)
|
|
210
|
+
const digest = sha256(signingInput)
|
|
211
|
+
const sig = base64urlToBytes(s.signature)
|
|
212
|
+
// digest is the single SHA-256 of the signing input — don't let
|
|
213
|
+
// @noble hash it again (standard ES256K).
|
|
214
|
+
return secp.verify(sig, digest, pub, { prehash: false })
|
|
215
|
+
} catch{
|
|
216
|
+
return false
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Content hash binding extracted parameters — MUST mirror the Verification
|
|
222
|
+
* Client's `contentHash` exactly (keccak256 of the RFC 8785 canonical JSON,
|
|
223
|
+
* via the shared `canonicalJson`). */
|
|
224
|
+
function contentHash(params: Record<string, string>): string {
|
|
225
|
+
const canonical = canonicalJson(params)
|
|
226
|
+
return '0x' + bytesToHex(keccak_256(new TextEncoder().encode(canonical)))
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface VerifyResultFullOptions {
|
|
230
|
+
/** The `reclaimSessionId` the consumer expects (mismatch fails). */
|
|
231
|
+
expectedReclaimSessionId?: string
|
|
232
|
+
/** The consumer's `orgId`; the result's `aud` must match it. */
|
|
233
|
+
expectedAud?: string
|
|
234
|
+
/** Allowlist of trusted attestor ETH addresses (lowercased compared). */
|
|
235
|
+
attestorAddresses?: string[]
|
|
236
|
+
/** Override the VC signer key instead of JWKS discovery via header `iss`. */
|
|
237
|
+
signerPublicKey?: string
|
|
238
|
+
/** Injectable fetch (for JWKS); defaults to global `fetch`. */
|
|
239
|
+
fetchImpl?: typeof fetch
|
|
240
|
+
/** Override "now" (unix seconds) for `exp` checks. */
|
|
241
|
+
now?: number
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface ResultProofOutcome {
|
|
245
|
+
requestId?: string
|
|
246
|
+
proofValid: boolean
|
|
247
|
+
reason?: string
|
|
248
|
+
attestorAddress?: string
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface VerifyResultFullOutcome extends VerifyResultOutcome {
|
|
252
|
+
proofs?: ResultProofOutcome[]
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Thrown by {@link verifyResult} / {@link verifyResultFull} (and therefore
|
|
257
|
+
* `reclaim.results.verify`/`receive`) when a result fails verification. Carries
|
|
258
|
+
* the full invalid outcome so callers can inspect it in a `catch`:
|
|
259
|
+
*
|
|
260
|
+
* try {
|
|
261
|
+
* const { result } = await reclaim.results.receive(body, { credential })
|
|
262
|
+
* // result is verified — use result.payload
|
|
263
|
+
* } catch (err) {
|
|
264
|
+
* if (err instanceof ResultVerificationError) {
|
|
265
|
+
* // e.g. 'expired', { payload, … }
|
|
266
|
+
* console.error(err.reason, err.outcome)
|
|
267
|
+
* }
|
|
268
|
+
* }
|
|
269
|
+
*/
|
|
270
|
+
export class ResultVerificationError extends Error {
|
|
271
|
+
/** Machine-readable failure reason — `'bad-signature'`, `'expired'`,
|
|
272
|
+
* `'reclaimSessionId-mismatch'`, `'aud-mismatch'`, `'invalid-proof'`, … */
|
|
273
|
+
readonly reason: string
|
|
274
|
+
/** The full (invalid) outcome: `payload`, `reclaimSessionId`, and per-proof
|
|
275
|
+
* detail (`proofs`) when verification got that far. */
|
|
276
|
+
readonly outcome: VerifyResultFullOutcome
|
|
277
|
+
|
|
278
|
+
constructor(outcome: VerifyResultFullOutcome) {
|
|
279
|
+
super(`result verification failed: ${outcome.reason ?? 'invalid'}`)
|
|
280
|
+
this.name = 'ResultVerificationError'
|
|
281
|
+
this.reason = outcome.reason ?? 'invalid'
|
|
282
|
+
this.outcome = outcome
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Full consumer verification: discovers the Verification Client's signing key
|
|
288
|
+
* via JWKS (header `iss`/`kid`), verifies the top-level JWS, checks
|
|
289
|
+
* `reclaimSessionId`/`aud`/`exp`, then verifies each result's nested attestor
|
|
290
|
+
* proof and its `content_hash`. Network access (JWKS) makes this async; use
|
|
291
|
+
* {@link verifyResult} for the offline primitive.
|
|
292
|
+
*/
|
|
293
|
+
export async function verifyResultFull(
|
|
294
|
+
jws: Jws,
|
|
295
|
+
opts: VerifyResultFullOptions = {},
|
|
296
|
+
): Promise<VerifyResultFullOutcome> {
|
|
297
|
+
const payload = decodeJwsPayload(jws)
|
|
298
|
+
const reclaimSessionId = payload.reclaimSessionId as string | undefined
|
|
299
|
+
/** Throw with the full invalid outcome — verification never *returns* an
|
|
300
|
+
* invalid result. */
|
|
301
|
+
const fail = (
|
|
302
|
+
reason: string,
|
|
303
|
+
extra: Partial<VerifyResultFullOutcome> = {},
|
|
304
|
+
): never => {
|
|
305
|
+
throw new ResultVerificationError({
|
|
306
|
+
valid: false,
|
|
307
|
+
reason,
|
|
308
|
+
payload,
|
|
309
|
+
reclaimSessionId,
|
|
310
|
+
...extra,
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 1. Resolve the VC signer key: explicit override, else JWKS via header iss.
|
|
315
|
+
let signerPublicKey = opts.signerPublicKey
|
|
316
|
+
let header: Record<string, unknown> = {}
|
|
317
|
+
const sig0 = jws.signatures[0]
|
|
318
|
+
if(sig0) {
|
|
319
|
+
try {
|
|
320
|
+
header = JSON.parse(
|
|
321
|
+
new TextDecoder().decode(base64urlToBytes(sig0.protected)),
|
|
322
|
+
)
|
|
323
|
+
} catch{
|
|
324
|
+
return fail('bad-header')
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if(!signerPublicKey) {
|
|
329
|
+
const iss = String(header.iss ?? '')
|
|
330
|
+
if(!iss) {
|
|
331
|
+
return fail('no-issuer')
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const keys = await fetchJwks(iss, opts.fetchImpl)
|
|
335
|
+
const jwk = selectKey(keys, String(header.kid ?? ''))
|
|
336
|
+
if(!jwk) {
|
|
337
|
+
return fail('signer-key-not-found')
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
signerPublicKey = jwkToUncompressedPublicKey(jwk)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if(!verifyJwsSignature(jws, signerPublicKey)) {
|
|
344
|
+
return fail('bad-signature')
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 2. Freshness / audience / expiry.
|
|
348
|
+
if(
|
|
349
|
+
opts.expectedReclaimSessionId &&
|
|
350
|
+
reclaimSessionId !== opts.expectedReclaimSessionId
|
|
351
|
+
) {
|
|
352
|
+
return fail('reclaimSessionId-mismatch')
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if(opts.expectedAud && payload.aud !== opts.expectedAud) {
|
|
356
|
+
return fail('aud-mismatch')
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const now = opts.now ?? Math.floor(Date.now() / 1000)
|
|
360
|
+
if(typeof payload.exp === 'number' && payload.exp < now) {
|
|
361
|
+
return fail('expired')
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 3. Per-result nested attestor proof + content_hash binding.
|
|
365
|
+
const results = Array.isArray(payload.results)
|
|
366
|
+
? (payload.results as Record<string, unknown>[])
|
|
367
|
+
: []
|
|
368
|
+
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
|
+
}
|
|
376
|
+
|
|
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,
|
|
386
|
+
})
|
|
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
|
+
proofs.push({
|
|
398
|
+
requestId,
|
|
399
|
+
proofValid: false,
|
|
400
|
+
reason: 'content-hash-mismatch',
|
|
401
|
+
attestorAddress: np.attestorAddress,
|
|
402
|
+
})
|
|
403
|
+
continue
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
proofs.push({
|
|
408
|
+
requestId,
|
|
409
|
+
proofValid: true,
|
|
410
|
+
attestorAddress: np.attestorAddress,
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if(!proofs.every((p) => p.proofValid)) {
|
|
415
|
+
return fail('invalid-proof', { proofs })
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return { valid: true, payload, reclaimSessionId, proofs }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Decrypt-and-verify in one step. */
|
|
422
|
+
export function verifyResult(
|
|
423
|
+
jws: Jws,
|
|
424
|
+
opts: VerifyResultOptions = {},
|
|
425
|
+
): VerifyResultOutcome {
|
|
426
|
+
const payload = decodeJwsPayload(jws)
|
|
427
|
+
const reclaimSessionId = payload.reclaimSessionId as string | undefined
|
|
428
|
+
|
|
429
|
+
if(opts.signerPublicKey && !verifyJwsSignature(jws, opts.signerPublicKey)) {
|
|
430
|
+
throw new ResultVerificationError({
|
|
431
|
+
valid: false, reason: 'bad-signature', payload, reclaimSessionId,
|
|
432
|
+
})
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if(
|
|
436
|
+
opts.expectedReclaimSessionId &&
|
|
437
|
+
reclaimSessionId !== opts.expectedReclaimSessionId
|
|
438
|
+
) {
|
|
439
|
+
throw new ResultVerificationError({
|
|
440
|
+
valid: false,
|
|
441
|
+
reason: 'reclaimSessionId-mismatch',
|
|
442
|
+
payload,
|
|
443
|
+
reclaimSessionId,
|
|
444
|
+
})
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return { valid: true, payload, reclaimSessionId }
|
|
448
|
+
}
|
package/src/routes.gen.ts
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
export const ROUTES = {
|
|
7
7
|
"AddOrgMember": {"method":"post","path":"/orgs/{orgId}/members"},
|
|
8
|
+
"AdminCallbackDlq": {"method":"get","path":"/admin/callback-dlq"},
|
|
8
9
|
"AdminHome": {"method":"get","path":"/admin"},
|
|
9
10
|
"AdminOrgs": {"method":"get","path":"/admin/orgs"},
|
|
10
11
|
"AdminProviders": {"method":"get","path":"/admin/providers"},
|
|
11
12
|
"AdminReviews": {"method":"get","path":"/admin/reviews"},
|
|
13
|
+
"AdminSessions": {"method":"get","path":"/admin/sessions"},
|
|
12
14
|
"AdminThemes": {"method":"get","path":"/admin/themes"},
|
|
13
15
|
"AdminUsers": {"method":"get","path":"/admin/users"},
|
|
14
16
|
"AttachPage": {"method":"get","path":"/attach/{code}"},
|
|
@@ -16,15 +18,19 @@ export const ROUTES = {
|
|
|
16
18
|
"ComponentsDemo": {"method":"get","path":"/components_demo"},
|
|
17
19
|
"ConfirmAttachSession": {"method":"post","path":"/credentials/attach/sessions/{code}/confirm"},
|
|
18
20
|
"CreateAttachSession": {"method":"post","path":"/credentials/attach/sessions"},
|
|
21
|
+
"CreateCallbackSubscription": {"method":"post","path":"/verifications/callback-subscriptions"},
|
|
19
22
|
"CreateOrg": {"method":"post","path":"/orgs"},
|
|
23
|
+
"CreateOrgCallbackSubscription": {"method":"post","path":"/orgs/{orgId}/callback-subscriptions"},
|
|
20
24
|
"CreateProvider": {"method":"post","path":"/providers"},
|
|
21
25
|
"CreateProviderVersion": {"method":"post","path":"/providers/{providerId}/versions"},
|
|
22
26
|
"CreateTheme": {"method":"post","path":"/orgs/{orgId}/themes"},
|
|
23
|
-
"
|
|
24
|
-
"CredentialAuthVerify": {"method":"post","path":"/credentials/auth/verify"},
|
|
27
|
+
"CreateVerificationSession": {"method":"post","path":"/verifications/sessions"},
|
|
25
28
|
"DecideProviderVersionReview": {"method":"post","path":"/providers/{providerId}/versions/{version}/review-decision"},
|
|
29
|
+
"DeleteCallbackSubscription": {"method":"delete","path":"/verifications/callback-subscriptions/{subscriptionId}"},
|
|
26
30
|
"DeleteOrg": {"method":"delete","path":"/orgs/{orgId}"},
|
|
27
31
|
"DeleteOrgBilling": {"method":"delete","path":"/orgs/{orgId}/billing"},
|
|
32
|
+
"DeleteOrgCallbackSubscription": {"method":"delete","path":"/orgs/{orgId}/callback-subscriptions/{subscriptionId}"},
|
|
33
|
+
"DeleteOrgCredential": {"method":"delete","path":"/orgs/{orgId}/credential"},
|
|
28
34
|
"DeleteOrgIcon": {"method":"delete","path":"/orgs/{orgId}/icon"},
|
|
29
35
|
"DeleteProvider": {"method":"delete","path":"/providers/{providerId}"},
|
|
30
36
|
"DeleteProviderVersion": {"method":"delete","path":"/providers/{providerId}/versions/{version}"},
|
|
@@ -32,23 +38,34 @@ export const ROUTES = {
|
|
|
32
38
|
"Docs": {"method":"get","path":"/docs"},
|
|
33
39
|
"ExplorePreviewImage": {"method":"get","path":"/og/explore.png"},
|
|
34
40
|
"GetAttachSession": {"method":"get","path":"/credentials/attach/sessions/{code}"},
|
|
35
|
-
"GetCredential": {"method":"get","path":"/credentials/{credentialId}"},
|
|
36
|
-
"GetCredentialByAddress": {"method":"get","path":"/credentials/by-address/{address}"},
|
|
37
41
|
"GetImage": {"method":"get","path":"/images/{imageId}"},
|
|
38
42
|
"GetMe": {"method":"get","path":"/me"},
|
|
39
43
|
"GetOrg": {"method":"get","path":"/orgs/{orgId}"},
|
|
40
44
|
"GetOrgBilling": {"method":"get","path":"/orgs/{orgId}/billing"},
|
|
45
|
+
"GetOrgCredential": {"method":"get","path":"/orgs/{orgId}/credential"},
|
|
46
|
+
"GetOrgSession": {"method":"get","path":"/orgs/{orgId}/sessions/{sessionId}"},
|
|
47
|
+
"GetOrgToken": {"method":"get","path":"/orgs/{orgId}/token"},
|
|
41
48
|
"GetProvider": {"method":"get","path":"/providers/{providerId}"},
|
|
42
49
|
"GetProviderBySlug": {"method":"get","path":"/providers/{providerId}/{slug}"},
|
|
43
50
|
"GetProviderVersion": {"method":"get","path":"/providers/{providerId}/versions/{version}"},
|
|
44
51
|
"GetTheme": {"method":"get","path":"/orgs/{orgId}/themes/{themeId}"},
|
|
45
52
|
"GetUser": {"method":"get","path":"/users/{userId}"},
|
|
53
|
+
"GetVerificationRecipe": {"method":"get","path":"/verifications/sessions/{sessionId}/recipe"},
|
|
54
|
+
"GetVerificationResult": {"method":"get","path":"/verifications/sessions/{sessionId}/result"},
|
|
55
|
+
"GetVerificationSession": {"method":"get","path":"/verifications/sessions/{sessionId}"},
|
|
46
56
|
"Index": {"method":"get","path":"/"},
|
|
47
|
-
"
|
|
48
|
-
"
|
|
57
|
+
"IssueOrgToken": {"method":"post","path":"/orgs/{orgId}/token"},
|
|
58
|
+
"JwksJson": {"method":"get","path":"/.well-known/jwks.json"},
|
|
59
|
+
"ListCallbackDlq": {"method":"get","path":"/verifications/callback-dlq"},
|
|
60
|
+
"ListCallbackSubscriptions": {"method":"get","path":"/verifications/callback-subscriptions"},
|
|
61
|
+
"ListOrgCallbackSubscriptions": {"method":"get","path":"/orgs/{orgId}/callback-subscriptions"},
|
|
49
62
|
"ListOrgs": {"method":"get","path":"/orgs"},
|
|
63
|
+
"ListOrgVerificationSessions": {"method":"get","path":"/orgs/{orgId}/sessions"},
|
|
50
64
|
"ListProviderVersions": {"method":"get","path":"/providers/{providerId}/versions"},
|
|
65
|
+
"ListSessionCallbackKeys": {"method":"get","path":"/verifications/sessions/{sessionId}/callback-keys"},
|
|
51
66
|
"ListThemes": {"method":"get","path":"/orgs/{orgId}/themes"},
|
|
67
|
+
"ListVerificationDeliveries": {"method":"get","path":"/verifications/sessions/{sessionId}/deliveries"},
|
|
68
|
+
"ListVerificationEvents": {"method":"get","path":"/verifications/sessions/{sessionId}/events"},
|
|
52
69
|
"Login": {"method":"post","path":"/auth/login"},
|
|
53
70
|
"LoginPage": {"method":"get","path":"/login"},
|
|
54
71
|
"Logout": {"method":"post","path":"/logout"},
|
|
@@ -58,12 +75,17 @@ export const ROUTES = {
|
|
|
58
75
|
"Providers": {"method":"get","path":"/providers"},
|
|
59
76
|
"PublishProviderVersion": {"method":"post","path":"/providers/{providerId}/versions/{version}/publish"},
|
|
60
77
|
"RemoveOrgMember": {"method":"delete","path":"/orgs/{orgId}/members/{memberId}"},
|
|
78
|
+
"ReportClaimant": {"method":"post","path":"/v/{sessionId}/claimant"},
|
|
79
|
+
"ReportVerificationEvent": {"method":"post","path":"/verifications/sessions/{sessionId}/events"},
|
|
61
80
|
"RequestPublicListing": {"method":"post","path":"/providers/{providerId}/request-public"},
|
|
62
|
-
"
|
|
81
|
+
"RetryCallbackDlq": {"method":"post","path":"/verifications/callback-dlq/{dlqId}/retry"},
|
|
82
|
+
"RetryVerificationCallback": {"method":"post","path":"/verifications/sessions/{sessionId}/retry-callback"},
|
|
63
83
|
"Robots": {"method":"get","path":"/robots.txt"},
|
|
84
|
+
"SetOrgCredential": {"method":"put","path":"/orgs/{orgId}/credential"},
|
|
64
85
|
"SetupProviderCreate": {"method":"post","path":"/providers/setup"},
|
|
65
86
|
"Sitemap": {"method":"get","path":"/sitemap.xml"},
|
|
66
87
|
"SubmitProviderVersionForReview": {"method":"post","path":"/providers/{providerId}/versions/{version}/review"},
|
|
88
|
+
"SubmitVerificationResult": {"method":"post","path":"/verifications/sessions/{sessionId}/result"},
|
|
67
89
|
"TransferProvider": {"method":"post","path":"/providers/{providerId}/transfer"},
|
|
68
90
|
"UpdateMe": {"method":"patch","path":"/me"},
|
|
69
91
|
"UpdateOrg": {"method":"patch","path":"/orgs/{orgId}"},
|
|
@@ -73,6 +95,9 @@ export const ROUTES = {
|
|
|
73
95
|
"UpdateProvider": {"method":"patch","path":"/providers/{providerId}"},
|
|
74
96
|
"UpdateProviderVersion": {"method":"patch","path":"/providers/{providerId}/versions/{version}"},
|
|
75
97
|
"UpdateTheme": {"method":"patch","path":"/orgs/{orgId}/themes/{themeId}"},
|
|
98
|
+
"VerificationPage": {"method":"get","path":"/v/{sessionId}"},
|
|
99
|
+
"VerificationSessionSse": {"method":"get","path":"/v/{sessionId}/sse"},
|
|
100
|
+
"VerificationStatus": {"method":"get","path":"/v/{sessionId}/status"},
|
|
76
101
|
} as const
|
|
77
102
|
|
|
78
103
|
export type RouteId = keyof typeof ROUTES
|
package/src/secp.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared `@noble/secp256k1` instance with the v3 hash hooks wired once
|
|
3
|
+
* (sign + recover need SHA-256 / HMAC-SHA256). Import `secp` from here — never
|
|
4
|
+
* set `secp.hashes` ad-hoc — so the hooks are guaranteed initialised before any
|
|
5
|
+
* sign/verify/recover call, regardless of import order.
|
|
6
|
+
*/
|
|
7
|
+
import { hmac } from '@noble/hashes/hmac.js'
|
|
8
|
+
import { sha256 } from '@noble/hashes/sha2.js'
|
|
9
|
+
import { keccak_256 } from '@noble/hashes/sha3.js'
|
|
10
|
+
import * as secp from '@noble/secp256k1'
|
|
11
|
+
import { bytesToHex } from './encoding.ts'
|
|
12
|
+
|
|
13
|
+
secp.hashes.sha256 = sha256
|
|
14
|
+
secp.hashes.hmacSha256 = (key: Uint8Array, msg: Uint8Array) => {
|
|
15
|
+
return hmac(sha256, key, msg)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { keccak_256, secp }
|
|
19
|
+
|
|
20
|
+
/** Lowercase 0x ETH address from an uncompressed (65-byte) public key:
|
|
21
|
+
* keccak256 of the 64-byte X‖Y (drop the 0x04 prefix), last 20 bytes. */
|
|
22
|
+
export function addressFromUncompressedPubkey(pub: Uint8Array): string {
|
|
23
|
+
return '0x' + bytesToHex(keccak_256(pub.slice(1))).slice(-40)
|
|
24
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * from './openapi.ts'
|
|
2
|
-
export
|
|
2
|
+
export * from './openapi.gen.ts'
|