@reclaimprotocol/client 0.1.27 → 0.1.28
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 +634 -42
- package/lib/api-client.d.ts +31 -0
- package/lib/api-client.js +112 -0
- package/lib/api-consts.d.ts +4 -0
- package/lib/api-consts.js +4 -0
- package/lib/api-errors.d.ts +13 -0
- package/lib/api-errors.js +30 -0
- package/lib/api.d.ts +7 -0
- package/lib/api.js +3 -0
- package/lib/attestor-proof.d.ts +6 -0
- package/lib/attestor-proof.js +45 -18
- package/lib/client.d.ts +1 -6
- package/lib/client.js +12 -33
- package/lib/consts.d.ts +1 -1
- package/lib/consts.js +1 -1
- package/lib/eip191.d.ts +4 -2
- package/lib/eip191.js +17 -1
- package/lib/errors.d.ts +38 -4
- package/lib/errors.js +99 -16
- package/lib/index.d.ts +9 -7
- package/lib/index.js +8 -4
- package/lib/jwks.d.ts +25 -1
- package/lib/jwks.js +69 -4
- package/lib/legacy-proof.d.ts +57 -0
- package/lib/legacy-proof.js +211 -0
- package/lib/proof-hash.d.ts +35 -0
- package/lib/proof-hash.js +216 -0
- package/lib/proof-recipe.d.ts +18 -0
- package/lib/proof-recipe.js +37 -0
- package/lib/proof-trust-constants.d.ts +3 -0
- package/lib/proof-trust-constants.js +43 -0
- package/lib/proof-types.d.ts +62 -0
- package/lib/proof-types.js +1 -0
- package/lib/proof-verification-error.d.ts +13 -0
- package/lib/proof-verification-error.js +11 -0
- package/lib/reclaim.d.ts +27 -5
- package/lib/reclaim.js +140 -14
- package/lib/result.d.ts +58 -6
- package/lib/result.js +252 -61
- package/lib/routes.gen.d.ts +54 -6
- package/lib/routes.gen.js +15 -3
- package/lib/tee-binding.d.ts +82 -0
- package/lib/tee-binding.js +112 -0
- package/lib/types/openapi.gen.d.ts +1343 -206
- package/lib/verification-clients.d.ts +17 -0
- package/lib/verification-clients.js +14 -0
- package/lib/verify-attestor-tee.d.ts +76 -0
- package/lib/verify-attestor-tee.js +243 -0
- package/lib/verify-tee.d.ts +32 -0
- package/lib/verify-tee.js +347 -0
- package/package.json +5 -1
- package/src/api.ts +15 -0
- package/src/attestor-proof.ts +57 -20
- package/src/client.ts +15 -38
- package/src/consts.ts +1 -2
- package/src/eip191.ts +31 -3
- package/src/errors.ts +132 -18
- package/src/index.ts +50 -5
- package/src/jwks.ts +128 -5
- package/src/legacy-proof.ts +367 -0
- package/src/proof-hash.ts +316 -0
- package/src/proof-recipe.ts +75 -0
- package/src/proof-trust-constants.ts +45 -0
- package/src/proof-types.ts +57 -0
- package/src/proof-verification-error.ts +13 -0
- package/src/reclaim.ts +206 -19
- package/src/result.ts +403 -68
- package/src/routes.gen.ts +15 -3
- package/src/tee-binding.ts +192 -0
- package/src/types/openapi.gen.ts +1343 -206
- package/src/verification-clients.ts +22 -0
- package/src/verify-attestor-tee.ts +365 -0
- package/src/verify-tee.ts +507 -0
package/src/result.ts
CHANGED
|
@@ -2,27 +2,36 @@
|
|
|
2
2
|
* Consumer-side handling of a delivered callback. Two entry points:
|
|
3
3
|
* - {@link verifyResult}: the offline primitive — decrypt + decode + optional
|
|
4
4
|
* ES256K signature check (caller-supplied key) + `reclaimSessionId` nonce.
|
|
5
|
-
* - {@link verifyResultFull}: the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* - {@link verifyResultFull}: the recommended server-side verification —
|
|
6
|
+
* resolves a registered issuer, discovers the Verification Client's key
|
|
7
|
+
* via JWKS, checks
|
|
8
|
+
* `reclaimSessionId`/`aud` and any legacy `exp`, and passes every exact
|
|
9
|
+
* legacy proof to the in-package, legacy-compatible `verifyProof` verifier.
|
|
9
10
|
*
|
|
10
11
|
* See docs/verification-session.md → "Verification by Consumer".
|
|
11
12
|
*/
|
|
12
13
|
import { sha256 } from '@noble/hashes/sha2.js'
|
|
13
|
-
import { keccak_256 } from '@noble/hashes/sha3.js'
|
|
14
14
|
import { decrypt as eciesDecrypt, encrypt as eciesEncrypt } from 'eciesjs'
|
|
15
15
|
import type { components } from './types/openapi.gen.ts'
|
|
16
|
-
import { type NestedProofOutcome, verifyNestedProof } from './attestor-proof.ts'
|
|
17
|
-
import { canonicalJson } from './canonical.ts'
|
|
18
16
|
import type { DecryptionKey } from './enckeys.ts'
|
|
19
|
-
import { b64url, base64urlToBytes,
|
|
17
|
+
import { b64url, base64urlToBytes, hexToBytes } from './encoding.ts'
|
|
20
18
|
import {
|
|
21
|
-
|
|
19
|
+
fetchReclaimJwks,
|
|
22
20
|
type Jwk,
|
|
23
21
|
jwkToUncompressedPublicKey,
|
|
24
|
-
|
|
22
|
+
selectVerificationResultKey,
|
|
25
23
|
} from './jwks.ts'
|
|
24
|
+
import {
|
|
25
|
+
type LegacyProof,
|
|
26
|
+
type LegacyVerifyProofOptions,
|
|
27
|
+
type ProofHashRequirement,
|
|
28
|
+
verifyProof,
|
|
29
|
+
} from './legacy-proof.ts'
|
|
30
|
+
import {
|
|
31
|
+
type BuilderProofRecipe,
|
|
32
|
+
proofHashesFromBuilderRecipe,
|
|
33
|
+
} from './proof-recipe.ts'
|
|
34
|
+
import type { TrustedData } from './proof-types.ts'
|
|
26
35
|
import { addressFromUncompressedPubkey, secp } from './secp.ts'
|
|
27
36
|
|
|
28
37
|
export interface Jws {
|
|
@@ -224,27 +233,65 @@ export function verifyJwsSignature(jws: Jws, signerPublicKey: string): boolean {
|
|
|
224
233
|
})
|
|
225
234
|
}
|
|
226
235
|
|
|
227
|
-
/** Content hash binding extracted parameters — MUST mirror the Verification
|
|
228
|
-
* Client's `contentHash` exactly (keccak256 of the RFC 8785 canonical JSON,
|
|
229
|
-
* via the shared `canonicalJson`). */
|
|
230
|
-
function contentHash(params: Record<string, string>): string {
|
|
231
|
-
const canonical = canonicalJson(params)
|
|
232
|
-
return '0x' + bytesToHex(keccak_256(new TextEncoder().encode(canonical)))
|
|
233
|
-
}
|
|
234
|
-
|
|
235
236
|
export interface VerifyResultFullOptions {
|
|
237
|
+
/** Builder session id. Alias for `expectedReclaimSessionId`. */
|
|
238
|
+
sessionId?: string
|
|
236
239
|
/** The `reclaimSessionId` the consumer expects (mismatch fails). */
|
|
237
240
|
expectedReclaimSessionId?: string
|
|
238
241
|
/** The consumer's `orgId`; the result's `aud` must match it. */
|
|
239
242
|
expectedAud?: string
|
|
240
243
|
/** Allowlist of trusted attestor ETH addresses (lowercased compared). */
|
|
241
244
|
attestorAddresses?: string[]
|
|
245
|
+
/** One explicitly trusted Verification Client issuer. */
|
|
246
|
+
issuer?: string
|
|
247
|
+
/** Alias for `issuer`. */
|
|
248
|
+
trustedIssuer?: string
|
|
249
|
+
/** Explicitly trusted Verification Client issuers. */
|
|
250
|
+
issuers?: string[]
|
|
251
|
+
/** Alias for `issuers`, retained for security-focused callers. */
|
|
252
|
+
trustedIssuers?: string[]
|
|
253
|
+
/** Resolve registered issuer URLs for the signed session's client id. */
|
|
254
|
+
resolveIssuers?: (verificationClientId: string) =>
|
|
255
|
+
Promise<readonly string[]> | readonly string[]
|
|
256
|
+
/** Alias for `resolveIssuers`. */
|
|
257
|
+
issuerResolver?: (verificationClientId: string) =>
|
|
258
|
+
Promise<readonly string[]> | readonly string[]
|
|
259
|
+
/** Registered issuer URLs keyed by `verificationClientId`. */
|
|
260
|
+
issuersByVerificationClientId?: Record<string, string[]>
|
|
261
|
+
/** Explicit trusted JWKS URL. This value must come from your registry. */
|
|
262
|
+
jwksUrl?: string
|
|
263
|
+
/** Trusted JWKS URLs keyed by `verificationClientId`. */
|
|
264
|
+
jwksUrlsByVerificationClientId?: Record<string, string>
|
|
265
|
+
/** Resolve a registered JWKS URL for the signed session's client id. */
|
|
266
|
+
resolveJwksUrl?: (verificationClientId: string) =>
|
|
267
|
+
Promise<string | undefined> | string | undefined
|
|
268
|
+
/** Legacy-compatible proof validation options applied to all proofs. */
|
|
269
|
+
proofValidation?: LegacyVerifyProofOptions
|
|
270
|
+
/** Alias for `proofValidation`. */
|
|
271
|
+
proofVerification?: LegacyVerifyProofOptions
|
|
272
|
+
/** Per-provider overrides for sessions containing different providers. */
|
|
273
|
+
proofValidationByProviderId?: Record<string, LegacyVerifyProofOptions>
|
|
274
|
+
/** Legacy-compatible hash requirements, for the common simple case. */
|
|
275
|
+
hashes?: ProofHashRequirement[]
|
|
276
|
+
/** Resolved Builder recipes, used to derive strict proof hashes offline. */
|
|
277
|
+
proofRecipes?: BuilderProofRecipe[]
|
|
278
|
+
/** Resolve a Builder session and its immutable, resolved recipes. */
|
|
279
|
+
resolveSession?: (
|
|
280
|
+
sessionId: string,
|
|
281
|
+
) => Promise<{ recipes: BuilderProofRecipe[] }>
|
|
282
|
+
| { recipes: BuilderProofRecipe[] }
|
|
242
283
|
/** Override the VC signer key instead of JWKS discovery via header `iss`. */
|
|
243
284
|
signerPublicKey?: string
|
|
244
285
|
/** Injectable fetch (for JWKS); defaults to global `fetch`. */
|
|
245
286
|
fetchImpl?: typeof fetch
|
|
246
|
-
/** Override "now" (
|
|
287
|
+
/** Override "now" (Unix seconds) when checking a legacy result's `exp`. */
|
|
247
288
|
now?: number
|
|
289
|
+
/**
|
|
290
|
+
* Accept a legacy Builder result that contains an expired `exp` claim.
|
|
291
|
+
* New Builder results don't include `exp`; use this only while consuming
|
|
292
|
+
* older stored results whose signatures and proofs must remain verifiable.
|
|
293
|
+
*/
|
|
294
|
+
ignoreExpiration?: boolean
|
|
248
295
|
}
|
|
249
296
|
|
|
250
297
|
export interface ResultProofOutcome {
|
|
@@ -254,6 +301,10 @@ export interface ResultProofOutcome {
|
|
|
254
301
|
proofValid: boolean
|
|
255
302
|
reason?: string
|
|
256
303
|
attestorAddress?: string
|
|
304
|
+
/** Trusted only when `proofValid` is true. */
|
|
305
|
+
data?: TrustedData
|
|
306
|
+
/** Provider-supplied data. It isn't cryptographically validated. */
|
|
307
|
+
publicData?: unknown
|
|
257
308
|
}
|
|
258
309
|
|
|
259
310
|
export interface VerifyResultFullOutcome extends VerifyResultOutcome {
|
|
@@ -292,11 +343,12 @@ export class ResultVerificationError extends Error {
|
|
|
292
343
|
}
|
|
293
344
|
|
|
294
345
|
/**
|
|
295
|
-
* Full consumer verification:
|
|
296
|
-
*
|
|
297
|
-
* `reclaimSessionId`/`aud
|
|
298
|
-
* proof
|
|
299
|
-
*
|
|
346
|
+
* Full consumer verification: resolves a trusted, registered issuer before
|
|
347
|
+
* using JWKS (header `iss`/`kid`), verifies the top-level JWS, checks
|
|
348
|
+
* `reclaimSessionId`/`aud` and any legacy `exp`, then verifies each exact
|
|
349
|
+
* legacy proof.
|
|
350
|
+
* Network access (JWKS and optional provider resolution) makes this async.
|
|
351
|
+
* Use {@link verifyResult} for the offline primitive.
|
|
300
352
|
*/
|
|
301
353
|
export async function verifyResultFull(
|
|
302
354
|
jws: Jws,
|
|
@@ -304,6 +356,7 @@ export async function verifyResultFull(
|
|
|
304
356
|
): Promise<VerifyResultFullOutcome> {
|
|
305
357
|
const payload = decodeJwsPayload(jws)
|
|
306
358
|
const reclaimSessionId = payload.reclaimSessionId as string | undefined
|
|
359
|
+
let jwksAttestorAddresses: string[] = []
|
|
307
360
|
/** Throw with the full invalid outcome — verification never *returns* an
|
|
308
361
|
* invalid result. */
|
|
309
362
|
const fail = (
|
|
@@ -319,7 +372,8 @@ export async function verifyResultFull(
|
|
|
319
372
|
})
|
|
320
373
|
}
|
|
321
374
|
|
|
322
|
-
// 1. Resolve the VC signer key
|
|
375
|
+
// 1. Resolve the VC signer key. Never use the signed header's `iss` as a
|
|
376
|
+
// network destination until it has matched an explicit or registered issuer.
|
|
323
377
|
let signerPublicKey = opts.signerPublicKey
|
|
324
378
|
let header: Record<string, unknown> = {}
|
|
325
379
|
const sig0 = jws.signatures[0]
|
|
@@ -334,13 +388,55 @@ export async function verifyResultFull(
|
|
|
334
388
|
}
|
|
335
389
|
|
|
336
390
|
if(!signerPublicKey) {
|
|
391
|
+
const verificationClientId = getVerificationClientId(payload) ?? ''
|
|
392
|
+
let configuredIssuers: string[]
|
|
393
|
+
try {
|
|
394
|
+
configuredIssuers = await resolveTrustedIssuers(
|
|
395
|
+
opts,
|
|
396
|
+
verificationClientId,
|
|
397
|
+
)
|
|
398
|
+
} catch{
|
|
399
|
+
return fail('issuer-resolution-failed')
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if(!configuredIssuers.length) {
|
|
403
|
+
return fail('issuer-not-configured')
|
|
404
|
+
}
|
|
405
|
+
|
|
337
406
|
const iss = String(header.iss ?? '')
|
|
338
407
|
if(!iss) {
|
|
339
408
|
return fail('no-issuer')
|
|
340
409
|
}
|
|
341
410
|
|
|
342
|
-
const
|
|
343
|
-
|
|
411
|
+
const trustedIssuer = configuredIssuers.find((candidate) => (
|
|
412
|
+
normalizeIssuer(candidate) === normalizeIssuer(iss)
|
|
413
|
+
))
|
|
414
|
+
if(!trustedIssuer) {
|
|
415
|
+
return fail('issuer-not-trusted')
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
let keys: Jwk[]
|
|
419
|
+
try {
|
|
420
|
+
const jwksUrl = await resolveTrustedJwksUrl(
|
|
421
|
+
opts,
|
|
422
|
+
verificationClientId,
|
|
423
|
+
)
|
|
424
|
+
const registry = await fetchReclaimJwks(
|
|
425
|
+
jwksUrl
|
|
426
|
+
?? `${trustedIssuer.replace(/\/+$/, '')}/.well-known/jwks.json`,
|
|
427
|
+
opts.fetchImpl,
|
|
428
|
+
)
|
|
429
|
+
keys = registry.keys
|
|
430
|
+
jwksAttestorAddresses = registry['x-reclaim-attestors'] ?? []
|
|
431
|
+
} catch{
|
|
432
|
+
return fail('jwks-fetch-failed')
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const jwk = selectVerificationResultKey(
|
|
436
|
+
keys,
|
|
437
|
+
String(header.kid ?? ''),
|
|
438
|
+
verificationClientId,
|
|
439
|
+
)
|
|
344
440
|
if(!jwk) {
|
|
345
441
|
return fail('signer-key-not-found')
|
|
346
442
|
}
|
|
@@ -352,11 +448,18 @@ export async function verifyResultFull(
|
|
|
352
448
|
return fail('bad-signature')
|
|
353
449
|
}
|
|
354
450
|
|
|
355
|
-
// 2. Freshness / audience / expiry.
|
|
356
451
|
if(
|
|
357
|
-
|
|
358
|
-
|
|
452
|
+
typeof payload.iss === 'string'
|
|
453
|
+
&& typeof header.iss === 'string'
|
|
454
|
+
&& normalizeIssuer(payload.iss) !== normalizeIssuer(header.iss)
|
|
359
455
|
) {
|
|
456
|
+
return fail('issuer-mismatch')
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// 2. Session binding, audience, and optional legacy result expiry.
|
|
460
|
+
const expectedResultSessionId = opts.sessionId
|
|
461
|
+
?? opts.expectedReclaimSessionId
|
|
462
|
+
if(expectedResultSessionId && reclaimSessionId !== expectedResultSessionId) {
|
|
360
463
|
return fail('reclaimSessionId-mismatch')
|
|
361
464
|
}
|
|
362
465
|
|
|
@@ -365,79 +468,311 @@ export async function verifyResultFull(
|
|
|
365
468
|
}
|
|
366
469
|
|
|
367
470
|
const now = opts.now ?? Math.floor(Date.now() / 1000)
|
|
368
|
-
if(
|
|
471
|
+
if(
|
|
472
|
+
!opts.ignoreExpiration
|
|
473
|
+
&& typeof payload.exp === 'number'
|
|
474
|
+
&& payload.exp < now
|
|
475
|
+
) {
|
|
369
476
|
return fail('expired')
|
|
370
477
|
}
|
|
371
478
|
|
|
372
|
-
// 3.
|
|
373
|
-
//
|
|
374
|
-
// requests: [...] }]. Every request across every provider must verify.
|
|
479
|
+
// 3. Verify every exact legacy proof carried by the signed result. The outer
|
|
480
|
+
// envelope groups proofs by provider, but it never changes the proof shape.
|
|
375
481
|
const providerResults = Array.isArray(payload.results)
|
|
376
482
|
? (payload.results as Record<string, unknown>[])
|
|
377
483
|
: []
|
|
378
484
|
const proofs: ResultProofOutcome[] = []
|
|
485
|
+
const proofGroups: {
|
|
486
|
+
providerId?: string
|
|
487
|
+
providerVersion?: string
|
|
488
|
+
proof: LegacyProof
|
|
489
|
+
outcomeIndex: number
|
|
490
|
+
}[] = []
|
|
379
491
|
for(const prov of providerResults) {
|
|
380
492
|
const providerId =
|
|
381
493
|
typeof prov.provider_id === 'string' ? prov.provider_id : undefined
|
|
494
|
+
const providerVersion = typeof prov.resolved_version === 'string'
|
|
495
|
+
? prov.resolved_version
|
|
496
|
+
: undefined
|
|
382
497
|
const requests = Array.isArray(prov.requests)
|
|
383
498
|
? (prov.requests as Record<string, unknown>[])
|
|
384
499
|
: []
|
|
385
500
|
for(const r of requests) {
|
|
386
501
|
const requestId =
|
|
387
502
|
typeof r.request_id === 'string' ? r.request_id : undefined
|
|
388
|
-
if(
|
|
503
|
+
if(!isLegacyProof(r.proof)) {
|
|
389
504
|
proofs.push({
|
|
390
505
|
providerId, requestId, proofValid: false, reason: 'proof-missing',
|
|
391
506
|
})
|
|
392
507
|
continue
|
|
393
508
|
}
|
|
394
509
|
|
|
395
|
-
const np: NestedProofOutcome = verifyNestedProof(r.proof, {
|
|
396
|
-
attestorAddresses: opts.attestorAddresses,
|
|
397
|
-
})
|
|
398
|
-
if(!np.valid) {
|
|
399
|
-
proofs.push({
|
|
400
|
-
providerId,
|
|
401
|
-
requestId,
|
|
402
|
-
proofValid: false,
|
|
403
|
-
reason: np.reason,
|
|
404
|
-
attestorAddress: np.attestorAddress,
|
|
405
|
-
})
|
|
406
|
-
continue
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
if(
|
|
410
|
-
typeof r.content_hash === 'string' &&
|
|
411
|
-
r.extracted_parameters &&
|
|
412
|
-
typeof r.extracted_parameters === 'object'
|
|
413
|
-
) {
|
|
414
|
-
const params = r.extracted_parameters as Record<string, string>
|
|
415
|
-
if(contentHash(params).toLowerCase() !== r.content_hash.toLowerCase()) {
|
|
416
|
-
proofs.push({
|
|
417
|
-
providerId,
|
|
418
|
-
requestId,
|
|
419
|
-
proofValid: false,
|
|
420
|
-
reason: 'content-hash-mismatch',
|
|
421
|
-
attestorAddress: np.attestorAddress,
|
|
422
|
-
})
|
|
423
|
-
continue
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
510
|
proofs.push({
|
|
428
511
|
providerId,
|
|
429
512
|
requestId,
|
|
430
513
|
proofValid: true,
|
|
431
|
-
attestorAddress:
|
|
514
|
+
attestorAddress: r.proof.witnesses[0]?.id,
|
|
515
|
+
})
|
|
516
|
+
proofGroups.push({
|
|
517
|
+
providerId,
|
|
518
|
+
providerVersion,
|
|
519
|
+
proof: r.proof,
|
|
520
|
+
outcomeIndex: proofs.length - 1,
|
|
432
521
|
})
|
|
433
522
|
}
|
|
434
523
|
}
|
|
435
524
|
|
|
525
|
+
const verificationStatus = getVerificationStatus(payload)
|
|
526
|
+
if(verificationStatus === 'success' && !proofGroups.length) {
|
|
527
|
+
return fail('proof-missing', { proofs })
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if(proofGroups.length) {
|
|
531
|
+
const expectedSessionId = expectedResultSessionId
|
|
532
|
+
const configuredProofValidation = opts.proofValidation
|
|
533
|
+
?? opts.proofVerification
|
|
534
|
+
const attestorAddresses = [...new Set([
|
|
535
|
+
...(configuredProofValidation?.attestorAddresses ?? []),
|
|
536
|
+
...(opts.attestorAddresses ?? []),
|
|
537
|
+
...jwksAttestorAddresses,
|
|
538
|
+
])]
|
|
539
|
+
const commonValidation = {
|
|
540
|
+
...configuredProofValidation,
|
|
541
|
+
...(opts.hashes ? { hashes: opts.hashes } : {}),
|
|
542
|
+
...(attestorAddresses.length
|
|
543
|
+
? { attestorAddresses }
|
|
544
|
+
: {}),
|
|
545
|
+
...(expectedSessionId
|
|
546
|
+
? { expectedSessionId }
|
|
547
|
+
: {}),
|
|
548
|
+
}
|
|
549
|
+
if(hasContentValidation(commonValidation)) {
|
|
550
|
+
await verifyLegacyProofGroup(proofGroups, commonValidation)
|
|
551
|
+
} else {
|
|
552
|
+
await verifyByResolvedRecipes(commonValidation, expectedSessionId)
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
436
556
|
if(!proofs.every((p) => p.proofValid)) {
|
|
437
557
|
return fail('invalid-proof', { proofs })
|
|
438
558
|
}
|
|
439
559
|
|
|
440
560
|
return { valid: true, payload, reclaimSessionId, proofs }
|
|
561
|
+
|
|
562
|
+
async function verifyLegacyProofGroup(
|
|
563
|
+
group: typeof proofGroups,
|
|
564
|
+
validation: LegacyVerifyProofOptions,
|
|
565
|
+
) {
|
|
566
|
+
const legacyOutcome = await verifyProof(
|
|
567
|
+
group.map(({ proof }) => proof),
|
|
568
|
+
validation,
|
|
569
|
+
)
|
|
570
|
+
if(legacyOutcome.valid) {
|
|
571
|
+
for(const [index, item] of group.entries()) {
|
|
572
|
+
proofs[item.outcomeIndex].data = legacyOutcome.data?.[index]
|
|
573
|
+
proofs[item.outcomeIndex].publicData =
|
|
574
|
+
legacyOutcome.publicData?.[index]
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
for(const { outcomeIndex } of group) {
|
|
581
|
+
proofs[outcomeIndex].proofValid = false
|
|
582
|
+
proofs[outcomeIndex].reason = legacyOutcome.reason
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async function verifyByResolvedRecipes(
|
|
587
|
+
common: LegacyVerifyProofOptions,
|
|
588
|
+
expectedSessionId: string | undefined,
|
|
589
|
+
) {
|
|
590
|
+
let recipes = opts.proofRecipes
|
|
591
|
+
if(!recipes && expectedSessionId && opts.resolveSession) {
|
|
592
|
+
try {
|
|
593
|
+
const resolved = await opts.resolveSession(expectedSessionId)
|
|
594
|
+
recipes = resolved.recipes
|
|
595
|
+
} catch{
|
|
596
|
+
return fail('session-resolution-failed', { proofs })
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const groups = Map.groupBy(proofGroups, proofGroupKey)
|
|
601
|
+
if(recipes && !recipeGroupsMatch(recipes, groups)) {
|
|
602
|
+
markInvalid(proofGroups, 'recipe-mismatch')
|
|
603
|
+
return fail('invalid-proof', { proofs })
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
for(const group of groups.values()) {
|
|
607
|
+
await verifyResolvedGroup(group, recipes, common)
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async function verifyResolvedGroup(
|
|
612
|
+
group: typeof proofGroups,
|
|
613
|
+
recipes: BuilderProofRecipe[] | undefined,
|
|
614
|
+
common: LegacyVerifyProofOptions,
|
|
615
|
+
) {
|
|
616
|
+
const providerId = group[0]?.providerId
|
|
617
|
+
const providerVersion = group[0]?.providerVersion
|
|
618
|
+
const providerValidation = providerId
|
|
619
|
+
? opts.proofValidationByProviderId?.[providerId]
|
|
620
|
+
: undefined
|
|
621
|
+
const recipe = recipes?.find((candidate) => (
|
|
622
|
+
candidate.providerId === providerId
|
|
623
|
+
&& candidate.resolvedVersion === providerVersion
|
|
624
|
+
))
|
|
625
|
+
let hashes: ProofHashRequirement[] | undefined
|
|
626
|
+
if(recipe) {
|
|
627
|
+
try {
|
|
628
|
+
hashes = proofHashesFromBuilderRecipe(
|
|
629
|
+
recipe,
|
|
630
|
+
group.map(({ proof }) => proof),
|
|
631
|
+
)
|
|
632
|
+
} catch{
|
|
633
|
+
markInvalid(group, 'recipe-hash-failed')
|
|
634
|
+
return
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
await verifyLegacyProofGroup(group, {
|
|
639
|
+
...common,
|
|
640
|
+
...(recipe
|
|
641
|
+
? { hashes }
|
|
642
|
+
: providerId
|
|
643
|
+
? { providerId, providerVersion }
|
|
644
|
+
: {}),
|
|
645
|
+
...providerValidation,
|
|
646
|
+
})
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function markInvalid(group: typeof proofGroups, reason: string) {
|
|
650
|
+
for(const { outcomeIndex } of group) {
|
|
651
|
+
proofs[outcomeIndex].proofValid = false
|
|
652
|
+
proofs[outcomeIndex].reason = reason
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function hasContentValidation(config: LegacyVerifyProofOptions): boolean {
|
|
658
|
+
return Boolean(
|
|
659
|
+
config.hashes
|
|
660
|
+
|| config.providerId
|
|
661
|
+
|| config.dangerouslyDisableContentValidation,
|
|
662
|
+
)
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function proofGroupKey(group: {
|
|
666
|
+
providerId?: string
|
|
667
|
+
providerVersion?: string
|
|
668
|
+
}) {
|
|
669
|
+
return `${group.providerId ?? ''}\u0000${group.providerVersion ?? ''}`
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function recipeGroupsMatch(
|
|
673
|
+
recipes: BuilderProofRecipe[],
|
|
674
|
+
groups: ReadonlyMap<string, unknown>,
|
|
675
|
+
) {
|
|
676
|
+
const expected = new Set(recipes.map((recipe) => (
|
|
677
|
+
`${recipe.providerId}\u0000${recipe.resolvedVersion}`
|
|
678
|
+
)))
|
|
679
|
+
return expected.size === groups.size
|
|
680
|
+
&& [...groups.keys()].every((key) => expected.has(key))
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function isLegacyProof(value: unknown): value is LegacyProof {
|
|
684
|
+
if(!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
685
|
+
return false
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const proof = value as Record<string, unknown>
|
|
689
|
+
return typeof proof.identifier === 'string'
|
|
690
|
+
&& !!proof.claimData
|
|
691
|
+
&& typeof proof.claimData === 'object'
|
|
692
|
+
&& Array.isArray(proof.signatures)
|
|
693
|
+
&& Array.isArray(proof.witnesses)
|
|
694
|
+
&& 'extractedParameterValues' in proof
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function getVerificationStatus(payload: Record<string, unknown>) {
|
|
698
|
+
const session = payload.verification_session
|
|
699
|
+
if(!session || typeof session !== 'object' || Array.isArray(session)) {
|
|
700
|
+
return undefined
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const status = (session as Record<string, unknown>).status
|
|
704
|
+
return typeof status === 'string' ? status : undefined
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function normalizeIssuer(issuer: string): string {
|
|
708
|
+
return issuer.trim().replace(/\/+$/, '').toLowerCase()
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function getVerificationClientId(
|
|
712
|
+
payload: Record<string, unknown>,
|
|
713
|
+
): string | undefined {
|
|
714
|
+
if(typeof payload.verificationClientId === 'string') {
|
|
715
|
+
return payload.verificationClientId
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if(typeof payload.verification_client_id === 'string') {
|
|
719
|
+
return payload.verification_client_id
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const session = payload.verification_session
|
|
723
|
+
if(!session || typeof session !== 'object' || Array.isArray(session)) {
|
|
724
|
+
return undefined
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const sessionRecord = session as Record<string, unknown>
|
|
728
|
+
const id = sessionRecord.verificationClientId
|
|
729
|
+
?? sessionRecord.verification_client_id
|
|
730
|
+
return typeof id === 'string' ? id : undefined
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
async function resolveTrustedIssuers(
|
|
734
|
+
opts: VerifyResultFullOptions,
|
|
735
|
+
verificationClientId: string | undefined,
|
|
736
|
+
): Promise<string[]> {
|
|
737
|
+
const explicit = [
|
|
738
|
+
opts.issuer,
|
|
739
|
+
opts.trustedIssuer,
|
|
740
|
+
...(opts.issuers ?? []),
|
|
741
|
+
...(opts.trustedIssuers ?? []),
|
|
742
|
+
].filter((issuer): issuer is string => typeof issuer === 'string')
|
|
743
|
+
if(explicit.length) {
|
|
744
|
+
return [...new Set(explicit.map(normalizeIssuer))]
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if(!verificationClientId) {
|
|
748
|
+
return []
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const registered =
|
|
752
|
+
opts.issuersByVerificationClientId?.[verificationClientId] ?? []
|
|
753
|
+
const resolve = opts.resolveIssuers ?? opts.issuerResolver
|
|
754
|
+
const resolved = resolve
|
|
755
|
+
? await resolve(verificationClientId)
|
|
756
|
+
: []
|
|
757
|
+
return [...new Set([...registered, ...resolved].map(normalizeIssuer))]
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async function resolveTrustedJwksUrl(
|
|
761
|
+
opts: VerifyResultFullOptions,
|
|
762
|
+
verificationClientId: string | undefined,
|
|
763
|
+
): Promise<string | undefined> {
|
|
764
|
+
if(opts.jwksUrl) {
|
|
765
|
+
return opts.jwksUrl
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if(!verificationClientId) {
|
|
769
|
+
return undefined
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const registered = opts.jwksUrlsByVerificationClientId?.[
|
|
773
|
+
verificationClientId
|
|
774
|
+
]
|
|
775
|
+
return registered ?? await opts.resolveJwksUrl?.(verificationClientId)
|
|
441
776
|
}
|
|
442
777
|
|
|
443
778
|
/** Decrypt-and-verify in one step. */
|
package/src/routes.gen.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// AUTO-GENERATED by packages/app/scripts/mcp-tools-gen.ts
|
|
2
2
|
// from packages/app/openapi.yaml.
|
|
3
|
-
// Run `npm run openapi:
|
|
3
|
+
// Run `npm run openapi:gen --workspace=packages/app`
|
|
4
4
|
// to regenerate. Do not edit by hand.
|
|
5
5
|
|
|
6
6
|
export const ROUTES = {
|
|
7
7
|
"AcceptOrgInvite": {"method":"post","path":"/orgs/{orgId}/invite"},
|
|
8
8
|
"ActivateFixWithAiRepair": {"method":"post","path":"/v/{sessionId}/fix-with-ai/activate"},
|
|
9
9
|
"AddOrgMember": {"method":"post","path":"/orgs/{orgId}/members"},
|
|
10
|
+
"AddProviderContributionComment": {"method":"post","path":"/providers/{providerId}/contributions/{campaignId}/comments"},
|
|
10
11
|
"AddStaffMember": {"method":"post","path":"/admin/staff"},
|
|
11
12
|
"AddVerificationClientKey": {"method":"post","path":"/admin/verification-clients/{clientId}/keys"},
|
|
12
13
|
"AdminBillingPlans": {"method":"get","path":"/admin/billing/plans"},
|
|
@@ -25,6 +26,7 @@ export const ROUTES = {
|
|
|
25
26
|
"AdminUsers": {"method":"get","path":"/admin/users"},
|
|
26
27
|
"AdminVerificationClients": {"method":"get","path":"/admin/verification-clients"},
|
|
27
28
|
"AdminVoidOrgCredit": {"method":"post","path":"/admin/orgs/{orgId}/credits/{grantId}/void"},
|
|
29
|
+
"BindVerificationSessionTeeNonce": {"method":"put","path":"/verifications/sessions/{sessionId}/tee-nonce"},
|
|
28
30
|
"CancelBillingSubscription": {"method":"delete","path":"/orgs/{orgId}/billing/subscription"},
|
|
29
31
|
"CancelPublicListingRequest": {"method":"post","path":"/providers/{providerId}/versions/{version}/cancel-review"},
|
|
30
32
|
"ChangeOrgPlan": {"method":"post","path":"/orgs/{orgId}/billing/change-plan"},
|
|
@@ -40,7 +42,10 @@ export const ROUTES = {
|
|
|
40
42
|
"CreateOrg": {"method":"post","path":"/orgs"},
|
|
41
43
|
"CreateOrgCallbackSubscription": {"method":"post","path":"/orgs/{orgId}/callback-subscriptions"},
|
|
42
44
|
"CreateProvider": {"method":"post","path":"/providers"},
|
|
45
|
+
"CreateProviderContributionCampaign": {"method":"post","path":"/providers/{providerId}/contributions"},
|
|
46
|
+
"CreateProviderContributionRequirementRevision": {"method":"post","path":"/providers/{providerId}/contributions/{campaignId}/requirements"},
|
|
43
47
|
"CreateProviderVersion": {"method":"post","path":"/providers/{providerId}/versions"},
|
|
48
|
+
"CreatePublicContributionAttempt": {"method":"post","path":"/contribute/{publicId}"},
|
|
44
49
|
"CreateTheme": {"method":"post","path":"/orgs/{orgId}/themes"},
|
|
45
50
|
"CreateVerificationClient": {"method":"post","path":"/admin/verification-clients"},
|
|
46
51
|
"CreateVerificationSession": {"method":"post","path":"/verifications/sessions"},
|
|
@@ -74,8 +79,10 @@ export const ROUTES = {
|
|
|
74
79
|
"GetProvider": {"method":"get","path":"/providers/{providerId}"},
|
|
75
80
|
"GetProviderBySlug": {"method":"get","path":"/providers/{providerId}/{slug}"},
|
|
76
81
|
"GetProviderVersion": {"method":"get","path":"/providers/{providerId}/versions/{version}"},
|
|
82
|
+
"GetPublicContributionCampaign": {"method":"get","path":"/contribute/{publicId}"},
|
|
77
83
|
"GetTheme": {"method":"get","path":"/orgs/{orgId}/themes/{themeId}"},
|
|
78
84
|
"GetUser": {"method":"get","path":"/users/{userId}"},
|
|
85
|
+
"GetVerificationFunnel": {"method":"get","path":"/orgs/{orgId}/verification-funnel"},
|
|
79
86
|
"GetVerificationRecipe": {"method":"get","path":"/verifications/sessions/{sessionId}/recipe"},
|
|
80
87
|
"GetVerificationResult": {"method":"get","path":"/verifications/sessions/{sessionId}/result"},
|
|
81
88
|
"GetVerificationSession": {"method":"get","path":"/verifications/sessions/{sessionId}"},
|
|
@@ -92,6 +99,7 @@ export const ROUTES = {
|
|
|
92
99
|
"ListOrgTokens": {"method":"get","path":"/orgs/{orgId}/token"},
|
|
93
100
|
"ListOrgTransactions": {"method":"get","path":"/orgs/{orgId}/billing/transactions"},
|
|
94
101
|
"ListOrgVerificationSessions": {"method":"get","path":"/orgs/{orgId}/sessions"},
|
|
102
|
+
"ListProviderContributionCampaigns": {"method":"get","path":"/providers/{providerId}/contributions"},
|
|
95
103
|
"ListProviderVersions": {"method":"get","path":"/providers/{providerId}/versions"},
|
|
96
104
|
"ListSessionCallbackKeys": {"method":"get","path":"/verifications/sessions/{sessionId}/callback-keys"},
|
|
97
105
|
"ListStaffMembers": {"method":"get","path":"/admin/staff"},
|
|
@@ -103,6 +111,7 @@ export const ROUTES = {
|
|
|
103
111
|
"MakePrivate": {"method":"post","path":"/providers/{providerId}/make-private"},
|
|
104
112
|
"OpenApiSpec": {"method":"get","path":"/openapi.yaml"},
|
|
105
113
|
"OrgInvitePage": {"method":"get","path":"/orgs/{orgId}/invite"},
|
|
114
|
+
"PatchVerificationClaimant": {"method":"patch","path":"/verifications/sessions/{sessionId}/claimant"},
|
|
106
115
|
"ProviderPreviewImage": {"method":"get","path":"/providers/{providerId}/preview.png"},
|
|
107
116
|
"Providers": {"method":"get","path":"/providers"},
|
|
108
117
|
"PublishProviderVersion": {"method":"post","path":"/providers/{providerId}/versions/{version}/publish"},
|
|
@@ -122,9 +131,10 @@ export const ROUTES = {
|
|
|
122
131
|
"Robots": {"method":"get","path":"/robots.txt"},
|
|
123
132
|
"SearchOrgMembers": {"method":"get","path":"/orgs/{orgId}/members"},
|
|
124
133
|
"SetDefaultBillingPlan": {"method":"post","path":"/admin/billing/plans/{planId}/default"},
|
|
125
|
-
"
|
|
126
|
-
"SetOrgGrace": {"method":"post","path":"/admin/orgs/{orgId}/billing/grace"},
|
|
134
|
+
"SetOrgCreditAllowance": {"method":"post","path":"/admin/orgs/{orgId}/credits/allowance"},
|
|
127
135
|
"SetOrgKeypair": {"method":"put","path":"/orgs/{orgId}/keypair"},
|
|
136
|
+
"SetOrgPaymentAllowance": {"method":"post","path":"/admin/orgs/{orgId}/billing/allowance"},
|
|
137
|
+
"SetProviderContributionCampaignStatus": {"method":"post","path":"/providers/{providerId}/contributions/{campaignId}"},
|
|
128
138
|
"SetupBillingPlan": {"method":"post","path":"/admin/billing/plans/setup"},
|
|
129
139
|
"SetupProviderCreate": {"method":"post","path":"/providers/setup"},
|
|
130
140
|
"SiteAiProvider": {"method":"post","path":"/site/{domain}/ai-provider"},
|
|
@@ -140,6 +150,7 @@ export const ROUTES = {
|
|
|
140
150
|
"StripeWebhook": {"method":"post","path":"/billing/stripe/webhook"},
|
|
141
151
|
"SubmitConsent": {"method":"post","path":"/oauth/consent"},
|
|
142
152
|
"SubmitProviderVersionForReview": {"method":"post","path":"/providers/{providerId}/versions/{version}/review"},
|
|
153
|
+
"SubmitPublicContributionFeedback": {"method":"post","path":"/contribute/{publicId}/feedback"},
|
|
143
154
|
"SubmitVerificationResult": {"method":"post","path":"/verifications/sessions/{sessionId}/result"},
|
|
144
155
|
"TransferProvider": {"method":"post","path":"/providers/{providerId}/transfer"},
|
|
145
156
|
"UpdateBillingPlan": {"method":"put","path":"/admin/billing/plans/{planId}"},
|
|
@@ -147,6 +158,7 @@ export const ROUTES = {
|
|
|
147
158
|
"UpdateOrgIcon": {"method":"put","path":"/orgs/{orgId}/icon"},
|
|
148
159
|
"UpdateOrgMember": {"method":"patch","path":"/orgs/{orgId}/members/{memberId}"},
|
|
149
160
|
"UpdateProvider": {"method":"patch","path":"/providers/{providerId}"},
|
|
161
|
+
"UpdateProviderContributionCampaign": {"method":"patch","path":"/providers/{providerId}/contributions/{campaignId}"},
|
|
150
162
|
"UpdateProviderVersion": {"method":"patch","path":"/providers/{providerId}/versions/{version}"},
|
|
151
163
|
"UpdateTheme": {"method":"patch","path":"/orgs/{orgId}/themes/{themeId}"},
|
|
152
164
|
"UpdateVerificationClient": {"method":"put","path":"/admin/verification-clients/{clientId}"},
|