@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.
Files changed (73) hide show
  1. package/README.md +634 -42
  2. package/lib/api-client.d.ts +31 -0
  3. package/lib/api-client.js +112 -0
  4. package/lib/api-consts.d.ts +4 -0
  5. package/lib/api-consts.js +4 -0
  6. package/lib/api-errors.d.ts +13 -0
  7. package/lib/api-errors.js +30 -0
  8. package/lib/api.d.ts +7 -0
  9. package/lib/api.js +3 -0
  10. package/lib/attestor-proof.d.ts +6 -0
  11. package/lib/attestor-proof.js +45 -18
  12. package/lib/client.d.ts +1 -6
  13. package/lib/client.js +12 -33
  14. package/lib/consts.d.ts +1 -1
  15. package/lib/consts.js +1 -1
  16. package/lib/eip191.d.ts +4 -2
  17. package/lib/eip191.js +17 -1
  18. package/lib/errors.d.ts +38 -4
  19. package/lib/errors.js +99 -16
  20. package/lib/index.d.ts +9 -7
  21. package/lib/index.js +8 -4
  22. package/lib/jwks.d.ts +25 -1
  23. package/lib/jwks.js +69 -4
  24. package/lib/legacy-proof.d.ts +57 -0
  25. package/lib/legacy-proof.js +211 -0
  26. package/lib/proof-hash.d.ts +35 -0
  27. package/lib/proof-hash.js +216 -0
  28. package/lib/proof-recipe.d.ts +18 -0
  29. package/lib/proof-recipe.js +37 -0
  30. package/lib/proof-trust-constants.d.ts +3 -0
  31. package/lib/proof-trust-constants.js +43 -0
  32. package/lib/proof-types.d.ts +62 -0
  33. package/lib/proof-types.js +1 -0
  34. package/lib/proof-verification-error.d.ts +13 -0
  35. package/lib/proof-verification-error.js +11 -0
  36. package/lib/reclaim.d.ts +27 -5
  37. package/lib/reclaim.js +140 -14
  38. package/lib/result.d.ts +58 -6
  39. package/lib/result.js +252 -61
  40. package/lib/routes.gen.d.ts +54 -6
  41. package/lib/routes.gen.js +15 -3
  42. package/lib/tee-binding.d.ts +82 -0
  43. package/lib/tee-binding.js +112 -0
  44. package/lib/types/openapi.gen.d.ts +1343 -206
  45. package/lib/verification-clients.d.ts +17 -0
  46. package/lib/verification-clients.js +14 -0
  47. package/lib/verify-attestor-tee.d.ts +76 -0
  48. package/lib/verify-attestor-tee.js +243 -0
  49. package/lib/verify-tee.d.ts +32 -0
  50. package/lib/verify-tee.js +347 -0
  51. package/package.json +5 -1
  52. package/src/api.ts +15 -0
  53. package/src/attestor-proof.ts +57 -20
  54. package/src/client.ts +15 -38
  55. package/src/consts.ts +1 -2
  56. package/src/eip191.ts +31 -3
  57. package/src/errors.ts +132 -18
  58. package/src/index.ts +50 -5
  59. package/src/jwks.ts +128 -5
  60. package/src/legacy-proof.ts +367 -0
  61. package/src/proof-hash.ts +316 -0
  62. package/src/proof-recipe.ts +75 -0
  63. package/src/proof-trust-constants.ts +45 -0
  64. package/src/proof-types.ts +57 -0
  65. package/src/proof-verification-error.ts +13 -0
  66. package/src/reclaim.ts +206 -19
  67. package/src/result.ts +403 -68
  68. package/src/routes.gen.ts +15 -3
  69. package/src/tee-binding.ts +192 -0
  70. package/src/types/openapi.gen.ts +1343 -206
  71. package/src/verification-clients.ts +22 -0
  72. package/src/verify-attestor-tee.ts +365 -0
  73. 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 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).
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, bytesToHex, hexToBytes } from './encoding.ts'
17
+ import { b64url, base64urlToBytes, hexToBytes } from './encoding.ts'
20
18
  import {
21
- fetchJwks,
19
+ fetchReclaimJwks,
22
20
  type Jwk,
23
21
  jwkToUncompressedPublicKey,
24
- selectKey,
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" (unix seconds) for `exp` checks. */
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: discovers the Verification Client's signing key
296
- * via JWKS (header `iss`/`kid`), verifies the top-level JWS, checks
297
- * `reclaimSessionId`/`aud`/`exp`, then verifies each result's nested attestor
298
- * proof and its `content_hash`. Network access (JWKS) makes this async; use
299
- * {@link verifyResult} for the offline primitive.
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: explicit override, else JWKS via header iss.
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 keys = await fetchJwks(iss, opts.fetchImpl)
343
- const jwk = selectKey(keys, String(header.kid ?? ''))
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
- opts.expectedReclaimSessionId &&
358
- reclaimSessionId !== opts.expectedReclaimSessionId
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(typeof payload.exp === 'number' && payload.exp < now) {
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. Per-provider, per-request nested attestor proof + content_hash binding.
373
- // `results` is grouped by provider: [{ provider_id, resolved_version,
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(typeof r.proof !== 'string') {
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: np.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:typegen --workspace=packages/client`
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
- "SetOrgCreditGrace": {"method":"post","path":"/admin/orgs/{orgId}/credits/grace"},
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}"},