@openeudi/openid4vp 0.11.1 → 0.13.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/dist/index.d.cts CHANGED
@@ -79,10 +79,49 @@ interface HaipQueryInput {
79
79
  claims: string[];
80
80
  trustedAuthorities?: TrustedAuthoritiesQuery[];
81
81
  }
82
+ /**
83
+ * Input to `buildCredentialSetQuery` — a DCQL `credential_sets` disjunction
84
+ * offering the wallet several alternative credentials for the same question.
85
+ */
86
+ interface CredentialSetQueryInput {
87
+ /**
88
+ * The alternatives, **most-preferred first**. Option order is a privacy
89
+ * signal, not cosmetic: list the credential that discloses least first.
90
+ * Wallets are not obliged to honour the order.
91
+ */
92
+ options: HaipQueryInput[];
93
+ /** Whether the wallet must satisfy one option. Default: `true`. */
94
+ required?: boolean;
95
+ }
82
96
 
83
97
  declare const HAIP_DOCTYPE_NAMESPACES: Record<string, string>;
84
98
  declare function buildHaipQuery(input: HaipQueryInput): DcqlQuery;
99
+ /**
100
+ * Build a DCQL query offering the wallet a choice between several credentials —
101
+ * a `credential_sets` disjunction.
102
+ *
103
+ * The motivating case is a proof-of-age check: ask for a Proof-of-Age
104
+ * attestation (`mso_mdoc`, `age_over_18` in namespace `eu.europa.ec.av.1`) OR a
105
+ * PID (`dc+sd-jwt`, `birth_date`), because the PID carries no age attribute and
106
+ * proof-of-age attestations have almost no issuer coverage yet. The wallet
107
+ * satisfies ONE option and returns ONE presentation.
108
+ *
109
+ * This is deliberately NOT HAIP-minimal: `validateHaipQuery` rejects
110
+ * `credential_sets`, and `isHaipQuery` returns `false` for what this produces.
111
+ * That boundary is intentional — use `validateCredentialSetQuery` instead.
112
+ *
113
+ * Each option is built through {@link buildHaipQuery}, so per-credential
114
+ * validation and its `HaipValidationError` codes are identical.
115
+ */
116
+ declare function buildCredentialSetQuery(input: CredentialSetQueryInput): DcqlQuery;
85
117
  declare function validateHaipQuery(query: DcqlQuery): void;
118
+ /**
119
+ * Validate a DCQL query that offers the wallet a choice — the profile
120
+ * `buildCredentialSetQuery` produces. Per-credential rules are identical to
121
+ * HAIP-minimal; the difference is that `credential_sets` is REQUIRED here
122
+ * rather than forbidden.
123
+ */
124
+ declare function validateCredentialSetQuery(query: DcqlQuery): void;
86
125
  declare function isHaipQuery(query: DcqlQuery): boolean;
87
126
 
88
127
  interface IssuerInfo {
@@ -228,8 +267,9 @@ declare class StaticTrustStore implements TrustStore {
228
267
  }
229
268
  /**
230
269
  * Combines multiple `TrustStore` instances. Children are queried in parallel;
231
- * results concatenate in child-order; duplicate anchors are dropped by
232
- * Subject Key Identifier.
270
+ * results concatenate in child-order; duplicate anchors (byte-identical DER)
271
+ * are dropped, first occurrence wins. Distinct certificates that share a key
272
+ * (e.g. a re-issued root) are both kept, so neither validity window is lost.
233
273
  */
234
274
  declare class CompositeTrustStore implements TrustStore {
235
275
  private readonly stores;
@@ -291,7 +331,9 @@ interface PresentationResult {
291
331
  interface ParseOptions {
292
332
  /**
293
333
  * DER-encoded issuer leaf certificates used for byte-equality trust check.
294
- * Kept for 0.4.0 compatibility.
334
+ * Kept for 0.4.0 compatibility. No chain building: a credential signed by a
335
+ * document signer issued under a listed root (e.g. an IACA) is rejected on
336
+ * this path — use `trustStore` for that.
295
337
  * @deprecated since 0.5.0. Use `trustStore: new StaticTrustStore([...])`
296
338
  * with root/intermediate CAs for RFC 5280 chain validation. Scheduled
297
339
  * for removal in 1.0.0.
@@ -332,6 +374,15 @@ interface ParseOptions {
332
374
  * Trust anchor resolver. When provided, the library performs RFC 5280
333
375
  * chain validation and ignores `trustedCertificates`. When unset, the
334
376
  * library falls back to 0.4.0 byte-equality against `trustedCertificates`.
377
+ *
378
+ * The credential's signer certificate (`x5c[0]` / `x5chain[0]`) is trusted
379
+ * when it either IS an anchor (byte-identical DER) or chains to one through
380
+ * verified signatures, using the remaining `x5c` / `x5chain` entries as
381
+ * untrusted path candidates. Anchors may be roots such as an ISO 18013-5
382
+ * IACA, or the document-signer certificate itself. For `mso_mdoc`, built
383
+ * chains must also satisfy the ISO 18013-5 Annex B certificate profile
384
+ * (DS EKU `1.0.18013.5.1.2`, mandatory keyUsage). Every certificate must be
385
+ * valid now and at the credential's issuance time (MSO `signed` / `iat`).
335
386
  */
336
387
  trustStore?: TrustStore;
337
388
  /**
@@ -339,7 +390,13 @@ interface ParseOptions {
339
390
  * ship in 0.5.0 workstream A.2 — passing them today throws.
340
391
  */
341
392
  revocationPolicy?: 'skip' | 'prefer' | 'require';
342
- /** HTTP transport for CRL/OCSP/LOTL fetches. Defaults to `globalThis.fetch`. */
393
+ /**
394
+ * HTTP transport for CRL/OCSP/LOTL fetches. Defaults to a guarded fetcher
395
+ * (`createGuardedFetcher({ allowHttp: true })`): no private/loopback/metadata
396
+ * targets, manual re-validated redirects (max 3), 15 s timeout, 16 MiB cap.
397
+ * A caller-supplied fetcher replaces those guards entirely — wrap it with
398
+ * `createGuardedFetcher({ fetch: yourFetcher })` to keep them.
399
+ */
343
400
  fetcher?: Fetcher;
344
401
  /** Cache for CRL/OCSP/LOTL artefacts. Defaults to `new InMemoryCache()`. */
345
402
  cache?: Cache;
@@ -366,6 +423,15 @@ interface ParseOptions {
366
423
  * secure path.
367
424
  */
368
425
  trustedIssuerJwks?: JsonWebKey[];
426
+ /**
427
+ * Accept the legacy `vc+sd-jwt` media type in the SD-JWT VC issuer JWT's
428
+ * `typ` header, in addition to `dc+sd-jwt`. Default `false`.
429
+ *
430
+ * @deprecated draft-ietf-oauth-sd-jwt-vc-19 removed `vc+sd-jwt`; only
431
+ * `dc+sd-jwt` is conformant. This exists solely to keep interoperating
432
+ * with wallets that have not migrated yet and will be removed in 1.0.0.
433
+ */
434
+ allowLegacyVcSdJwtTyp?: boolean;
369
435
  }
370
436
  interface ICredentialParser {
371
437
  readonly format: CredentialFormat;
@@ -574,7 +640,7 @@ declare class MalformedCredentialError extends Error {
574
640
  declare class NonceValidationError extends Error {
575
641
  constructor(message?: string);
576
642
  }
577
- type HaipValidationCode = 'EMPTY_QUERY' | 'UNSUPPORTED_FORMAT' | 'MISSING_SDJWT_META' | 'MISSING_MDOC_META' | 'NO_CLAIMS' | 'CLAIM_SETS_DISALLOWED' | 'CREDENTIAL_SETS_DISALLOWED';
643
+ type HaipValidationCode = 'EMPTY_QUERY' | 'UNSUPPORTED_FORMAT' | 'MISSING_SDJWT_META' | 'MISSING_MDOC_META' | 'NO_CLAIMS' | 'CLAIM_SETS_DISALLOWED' | 'CREDENTIAL_SETS_DISALLOWED' | 'EMPTY_OPTIONS' | 'DUPLICATE_CREDENTIAL_ID' | 'MISSING_CREDENTIAL_SETS' | 'UNKNOWN_OPTION_REFERENCE';
578
644
  declare class HaipValidationError extends Error {
579
645
  readonly code: HaipValidationCode;
580
646
  readonly credentialId?: string;
@@ -589,7 +655,7 @@ declare abstract class OpenID4VPError extends Error {
589
655
  declare class TrustAnchorNotFoundError extends OpenID4VPError {
590
656
  readonly code: "trust_anchor_not_found";
591
657
  }
592
- type ChainErrorReason = 'signature' | 'validity' | 'name_constraints' | 'key_usage' | 'basic_constraints' | 'path_length' | 'algorithm_disallowed' | 'aki_ski_mismatch';
658
+ type ChainErrorReason = 'signature' | 'validity' | 'name_constraints' | 'key_usage' | 'extended_key_usage' | 'basic_constraints' | 'path_length' | 'algorithm_disallowed' | 'aki_ski_mismatch';
593
659
  declare class CertificateChainError extends OpenID4VPError {
594
660
  readonly code: "chain_invalid";
595
661
  readonly reason: ChainErrorReason;
@@ -624,6 +690,19 @@ declare class LotlFetchError extends OpenID4VPError {
624
690
  declare class LotlSignatureError extends OpenID4VPError {
625
691
  readonly code: "lotl_signature_invalid";
626
692
  }
693
+ /**
694
+ * The environment is not configured to verify trusted-list signatures at all —
695
+ * distinct from a list whose signature did not verify.
696
+ *
697
+ * `xmldsigjs` needs a WebCrypto engine registered once per process, and this
698
+ * library deliberately leaves that to the consumer rather than picking a
699
+ * provider on their behalf. Without it, no trusted list can ever verify, so
700
+ * this is raised eagerly instead of degrading per-list: a configuration fault
701
+ * reported as a trust failure sends people bisecting valid XML.
702
+ */
703
+ declare class LotlConfigurationError extends OpenID4VPError {
704
+ readonly code: "lotl_configuration_invalid";
705
+ }
627
706
  type SignedRequestBuildErrorCode = 'empty_cert_chain' | 'hostname_cert_mismatch' | 'signing_key_cert_mismatch' | 'missing_encryption_jwk' | 'missing_encryption_alg' | 'missing_vp_formats' | 'unsupported_signing_alg' | 'empty_supported_enc_values' | 'missing_hostname' | 'self_signed_leaf';
628
707
  declare class SignedRequestBuildError extends OpenID4VPError {
629
708
  readonly code: SignedRequestBuildErrorCode;
@@ -655,6 +734,84 @@ declare class MultipleCredentialsNotSupportedError extends OpenID4VPError {
655
734
  readonly presentationCount: number;
656
735
  constructor(entryCount: number, presentationCount: number);
657
736
  }
737
+ type GuardedFetchRejectReason = 'invalid_url' | 'insecure_scheme' | 'private_address' | 'dns_resolution_failed' | 'too_many_redirects' | 'invalid_redirect' | 'response_too_large' | 'timeout';
738
+ /**
739
+ * A request was refused (or cut off) by the guarded fetcher before an
740
+ * untrusted endpoint could be reached or could exhaust resources.
741
+ */
742
+ declare class GuardedFetchError extends OpenID4VPError {
743
+ readonly code: "guarded_fetch_rejected";
744
+ readonly reason: GuardedFetchRejectReason;
745
+ readonly url: string;
746
+ constructor(reason: GuardedFetchRejectReason, message: string, options: {
747
+ url: string;
748
+ cause?: Error;
749
+ });
750
+ }
751
+
752
+ /**
753
+ * Resolves a hostname to every IP address it currently maps to. Must return
754
+ * all records (A and AAAA): the guard rejects the host if ANY of them is
755
+ * non-public, and treats an empty result as a resolution failure.
756
+ */
757
+ type HostLookup = (hostname: string) => Promise<readonly string[]>;
758
+ interface GuardedFetcherOptions {
759
+ /**
760
+ * Underlying transport. Defaults to `globalThis.fetch`, resolved at call
761
+ * time. Must honour `redirect: 'manual'` and expose the `Location` header
762
+ * of 3xx responses (Node's fetch/undici does; browsers return an opaque
763
+ * redirect, which the guard rejects).
764
+ */
765
+ fetch?: Fetcher;
766
+ /**
767
+ * DNS resolver used for the private-address check. Defaults to
768
+ * `node:dns` `lookup(host, { all: true })` when available. In runtimes
769
+ * without `node:dns` (browsers, some edge workers) only IP-literal and
770
+ * `localhost` targets are checked — inject a resolver there if needed.
771
+ */
772
+ lookup?: HostLookup;
773
+ /** Permit `http:` targets. Default `false` (SD-JWT VC draft-19: HTTPS only). */
774
+ allowHttp?: boolean;
775
+ /** Permit loopback / private / link-local / metadata targets. Default `false`. */
776
+ allowPrivateNetworks?: boolean;
777
+ /** Maximum redirects followed; every hop is re-validated. Default 3. */
778
+ maxRedirects?: number;
779
+ /** Wall-clock budget for the whole exchange (DNS, redirects, body). Default 15 000 ms. */
780
+ timeoutMs?: number;
781
+ /** Maximum response body size, enforced while streaming. Default 16 MiB. */
782
+ maxResponseBytes?: number;
783
+ }
784
+ declare const GUARDED_FETCH_DEFAULTS: {
785
+ readonly maxRedirects: 3;
786
+ readonly timeoutMs: 15000;
787
+ readonly maxResponseBytes: number;
788
+ };
789
+ /**
790
+ * Builds a {@link Fetcher} that applies the HTTP-retrieval rules of
791
+ * draft-ietf-oauth-sd-jwt-vc-19 to every request:
792
+ *
793
+ * - HTTPS only (unless `allowHttp`), and never an https → http downgrade
794
+ * - no loopback / private / link-local / cloud-metadata targets, checked
795
+ * on the URL host AND every address it resolves to (IPv4, IPv6,
796
+ * IPv4-mapped/compatible, NAT64, 6to4)
797
+ * - redirects handled manually, bounded, and re-validated hop by hop
798
+ * - a single timeout covering DNS, every hop and the body
799
+ * - a response-size cap enforced while streaming
800
+ *
801
+ * **DNS rebinding.** The guard resolves the host, validates the addresses,
802
+ * then hands the *hostname* to the transport, which resolves it again. An
803
+ * attacker-controlled DNS server can answer differently the second time
804
+ * (TOCTOU). Closing that gap requires pinning the validated address at the
805
+ * socket layer, which `fetch` does not expose portably. Deployments that
806
+ * dereference attacker-influenced URLs should additionally route egress
807
+ * through a filtering proxy or inject a transport whose connector validates
808
+ * the connected address (e.g. an undici `Agent` with a checking
809
+ * `connect.lookup`).
810
+ *
811
+ * Caching is out of scope here: the trust module caches CRL/OCSP/LOTL
812
+ * artefacts through its own {@link Cache} plug.
813
+ */
814
+ declare function createGuardedFetcher(options?: GuardedFetcherOptions): Fetcher;
658
815
 
659
816
  interface LotlTrustStoreOptions {
660
817
  /** Override the bundled signing anchors. Empty is rejected. */
@@ -693,6 +850,6 @@ declare class LotlTrustStore implements TrustStore {
693
850
  private refresh;
694
851
  }
695
852
 
696
- declare const VERSION = "0.11.1";
853
+ declare const VERSION = "0.13.0";
697
854
 
698
- export { type AuthorizationRequest, type AuthorizationRequestInput, type AuthorizationResponse, type Cache, CertificateChainError, type ChainErrorReason, CompositeTrustStore, type CredentialClaims, type CredentialFormat, DecryptionFailedError, type EncryptedResponse, ExpiredCredentialError, type Fetcher, HAIP_DOCTYPE_NAMESPACES, type HaipQueryInput, type HaipValidationCode, HaipValidationError, type ICredentialParser, InMemoryCache, InvalidSignatureError, type IssuerInfo, type LotlAnchorMetadata, LotlFetchError, LotlSignatureError, LotlTrustStore, type LotlTrustStoreOptions, MalformedCredentialError, MdocParser, MissingDecryptionKeyError, MissingVerifierEncryptionKeyError, MultipleCredentialsNotSupportedError, NonceValidationError, OpenID4VPError, type ParseOptions, type PresentationResult, RevocationCheckFailedError, RevokedCertificateError, SdJwtParser, type SignedAuthorizationRequest, type SignedAuthorizationRequestInput, SignedRequestBuildError, type SignedRequestBuildErrorCode, StaticTrustStore, type TrustAnchor, TrustAnchorNotFoundError, type TrustStore, type TrustStoreHint, type TrustStoreInput, UnsupportedFormatError, UnsupportedJweError, VERSION, type VerifyAuthorizationResponseOptions, type VerifyOptions, type VerifyResult, buildHaipQuery, buildOid4vpSessionTranscript, buildOpenID4VPHandoverSessionTranscript, createAuthorizationRequest, createSignedAuthorizationRequest, decryptAuthorizationResponse, isHaipQuery, parsePresentation, validateHaipQuery, verifyAuthorizationResponse, verifyPresentation };
855
+ export { type AuthorizationRequest, type AuthorizationRequestInput, type AuthorizationResponse, type Cache, CertificateChainError, type ChainErrorReason, CompositeTrustStore, type CredentialClaims, type CredentialFormat, type CredentialSetQueryInput, DecryptionFailedError, type EncryptedResponse, ExpiredCredentialError, type Fetcher, GUARDED_FETCH_DEFAULTS, GuardedFetchError, type GuardedFetchRejectReason, type GuardedFetcherOptions, HAIP_DOCTYPE_NAMESPACES, type HaipQueryInput, type HaipValidationCode, HaipValidationError, type HostLookup, type ICredentialParser, InMemoryCache, InvalidSignatureError, type IssuerInfo, type LotlAnchorMetadata, LotlConfigurationError, LotlFetchError, LotlSignatureError, LotlTrustStore, type LotlTrustStoreOptions, MalformedCredentialError, MdocParser, MissingDecryptionKeyError, MissingVerifierEncryptionKeyError, MultipleCredentialsNotSupportedError, NonceValidationError, OpenID4VPError, type ParseOptions, type PresentationResult, RevocationCheckFailedError, RevokedCertificateError, SdJwtParser, type SignedAuthorizationRequest, type SignedAuthorizationRequestInput, SignedRequestBuildError, type SignedRequestBuildErrorCode, StaticTrustStore, type TrustAnchor, TrustAnchorNotFoundError, type TrustStore, type TrustStoreHint, type TrustStoreInput, UnsupportedFormatError, UnsupportedJweError, VERSION, type VerifyAuthorizationResponseOptions, type VerifyOptions, type VerifyResult, buildCredentialSetQuery, buildHaipQuery, buildOid4vpSessionTranscript, buildOpenID4VPHandoverSessionTranscript, createAuthorizationRequest, createGuardedFetcher, createSignedAuthorizationRequest, decryptAuthorizationResponse, isHaipQuery, parsePresentation, validateCredentialSetQuery, validateHaipQuery, verifyAuthorizationResponse, verifyPresentation };
package/dist/index.d.ts CHANGED
@@ -79,10 +79,49 @@ interface HaipQueryInput {
79
79
  claims: string[];
80
80
  trustedAuthorities?: TrustedAuthoritiesQuery[];
81
81
  }
82
+ /**
83
+ * Input to `buildCredentialSetQuery` — a DCQL `credential_sets` disjunction
84
+ * offering the wallet several alternative credentials for the same question.
85
+ */
86
+ interface CredentialSetQueryInput {
87
+ /**
88
+ * The alternatives, **most-preferred first**. Option order is a privacy
89
+ * signal, not cosmetic: list the credential that discloses least first.
90
+ * Wallets are not obliged to honour the order.
91
+ */
92
+ options: HaipQueryInput[];
93
+ /** Whether the wallet must satisfy one option. Default: `true`. */
94
+ required?: boolean;
95
+ }
82
96
 
83
97
  declare const HAIP_DOCTYPE_NAMESPACES: Record<string, string>;
84
98
  declare function buildHaipQuery(input: HaipQueryInput): DcqlQuery;
99
+ /**
100
+ * Build a DCQL query offering the wallet a choice between several credentials —
101
+ * a `credential_sets` disjunction.
102
+ *
103
+ * The motivating case is a proof-of-age check: ask for a Proof-of-Age
104
+ * attestation (`mso_mdoc`, `age_over_18` in namespace `eu.europa.ec.av.1`) OR a
105
+ * PID (`dc+sd-jwt`, `birth_date`), because the PID carries no age attribute and
106
+ * proof-of-age attestations have almost no issuer coverage yet. The wallet
107
+ * satisfies ONE option and returns ONE presentation.
108
+ *
109
+ * This is deliberately NOT HAIP-minimal: `validateHaipQuery` rejects
110
+ * `credential_sets`, and `isHaipQuery` returns `false` for what this produces.
111
+ * That boundary is intentional — use `validateCredentialSetQuery` instead.
112
+ *
113
+ * Each option is built through {@link buildHaipQuery}, so per-credential
114
+ * validation and its `HaipValidationError` codes are identical.
115
+ */
116
+ declare function buildCredentialSetQuery(input: CredentialSetQueryInput): DcqlQuery;
85
117
  declare function validateHaipQuery(query: DcqlQuery): void;
118
+ /**
119
+ * Validate a DCQL query that offers the wallet a choice — the profile
120
+ * `buildCredentialSetQuery` produces. Per-credential rules are identical to
121
+ * HAIP-minimal; the difference is that `credential_sets` is REQUIRED here
122
+ * rather than forbidden.
123
+ */
124
+ declare function validateCredentialSetQuery(query: DcqlQuery): void;
86
125
  declare function isHaipQuery(query: DcqlQuery): boolean;
87
126
 
88
127
  interface IssuerInfo {
@@ -228,8 +267,9 @@ declare class StaticTrustStore implements TrustStore {
228
267
  }
229
268
  /**
230
269
  * Combines multiple `TrustStore` instances. Children are queried in parallel;
231
- * results concatenate in child-order; duplicate anchors are dropped by
232
- * Subject Key Identifier.
270
+ * results concatenate in child-order; duplicate anchors (byte-identical DER)
271
+ * are dropped, first occurrence wins. Distinct certificates that share a key
272
+ * (e.g. a re-issued root) are both kept, so neither validity window is lost.
233
273
  */
234
274
  declare class CompositeTrustStore implements TrustStore {
235
275
  private readonly stores;
@@ -291,7 +331,9 @@ interface PresentationResult {
291
331
  interface ParseOptions {
292
332
  /**
293
333
  * DER-encoded issuer leaf certificates used for byte-equality trust check.
294
- * Kept for 0.4.0 compatibility.
334
+ * Kept for 0.4.0 compatibility. No chain building: a credential signed by a
335
+ * document signer issued under a listed root (e.g. an IACA) is rejected on
336
+ * this path — use `trustStore` for that.
295
337
  * @deprecated since 0.5.0. Use `trustStore: new StaticTrustStore([...])`
296
338
  * with root/intermediate CAs for RFC 5280 chain validation. Scheduled
297
339
  * for removal in 1.0.0.
@@ -332,6 +374,15 @@ interface ParseOptions {
332
374
  * Trust anchor resolver. When provided, the library performs RFC 5280
333
375
  * chain validation and ignores `trustedCertificates`. When unset, the
334
376
  * library falls back to 0.4.0 byte-equality against `trustedCertificates`.
377
+ *
378
+ * The credential's signer certificate (`x5c[0]` / `x5chain[0]`) is trusted
379
+ * when it either IS an anchor (byte-identical DER) or chains to one through
380
+ * verified signatures, using the remaining `x5c` / `x5chain` entries as
381
+ * untrusted path candidates. Anchors may be roots such as an ISO 18013-5
382
+ * IACA, or the document-signer certificate itself. For `mso_mdoc`, built
383
+ * chains must also satisfy the ISO 18013-5 Annex B certificate profile
384
+ * (DS EKU `1.0.18013.5.1.2`, mandatory keyUsage). Every certificate must be
385
+ * valid now and at the credential's issuance time (MSO `signed` / `iat`).
335
386
  */
336
387
  trustStore?: TrustStore;
337
388
  /**
@@ -339,7 +390,13 @@ interface ParseOptions {
339
390
  * ship in 0.5.0 workstream A.2 — passing them today throws.
340
391
  */
341
392
  revocationPolicy?: 'skip' | 'prefer' | 'require';
342
- /** HTTP transport for CRL/OCSP/LOTL fetches. Defaults to `globalThis.fetch`. */
393
+ /**
394
+ * HTTP transport for CRL/OCSP/LOTL fetches. Defaults to a guarded fetcher
395
+ * (`createGuardedFetcher({ allowHttp: true })`): no private/loopback/metadata
396
+ * targets, manual re-validated redirects (max 3), 15 s timeout, 16 MiB cap.
397
+ * A caller-supplied fetcher replaces those guards entirely — wrap it with
398
+ * `createGuardedFetcher({ fetch: yourFetcher })` to keep them.
399
+ */
343
400
  fetcher?: Fetcher;
344
401
  /** Cache for CRL/OCSP/LOTL artefacts. Defaults to `new InMemoryCache()`. */
345
402
  cache?: Cache;
@@ -366,6 +423,15 @@ interface ParseOptions {
366
423
  * secure path.
367
424
  */
368
425
  trustedIssuerJwks?: JsonWebKey[];
426
+ /**
427
+ * Accept the legacy `vc+sd-jwt` media type in the SD-JWT VC issuer JWT's
428
+ * `typ` header, in addition to `dc+sd-jwt`. Default `false`.
429
+ *
430
+ * @deprecated draft-ietf-oauth-sd-jwt-vc-19 removed `vc+sd-jwt`; only
431
+ * `dc+sd-jwt` is conformant. This exists solely to keep interoperating
432
+ * with wallets that have not migrated yet and will be removed in 1.0.0.
433
+ */
434
+ allowLegacyVcSdJwtTyp?: boolean;
369
435
  }
370
436
  interface ICredentialParser {
371
437
  readonly format: CredentialFormat;
@@ -574,7 +640,7 @@ declare class MalformedCredentialError extends Error {
574
640
  declare class NonceValidationError extends Error {
575
641
  constructor(message?: string);
576
642
  }
577
- type HaipValidationCode = 'EMPTY_QUERY' | 'UNSUPPORTED_FORMAT' | 'MISSING_SDJWT_META' | 'MISSING_MDOC_META' | 'NO_CLAIMS' | 'CLAIM_SETS_DISALLOWED' | 'CREDENTIAL_SETS_DISALLOWED';
643
+ type HaipValidationCode = 'EMPTY_QUERY' | 'UNSUPPORTED_FORMAT' | 'MISSING_SDJWT_META' | 'MISSING_MDOC_META' | 'NO_CLAIMS' | 'CLAIM_SETS_DISALLOWED' | 'CREDENTIAL_SETS_DISALLOWED' | 'EMPTY_OPTIONS' | 'DUPLICATE_CREDENTIAL_ID' | 'MISSING_CREDENTIAL_SETS' | 'UNKNOWN_OPTION_REFERENCE';
578
644
  declare class HaipValidationError extends Error {
579
645
  readonly code: HaipValidationCode;
580
646
  readonly credentialId?: string;
@@ -589,7 +655,7 @@ declare abstract class OpenID4VPError extends Error {
589
655
  declare class TrustAnchorNotFoundError extends OpenID4VPError {
590
656
  readonly code: "trust_anchor_not_found";
591
657
  }
592
- type ChainErrorReason = 'signature' | 'validity' | 'name_constraints' | 'key_usage' | 'basic_constraints' | 'path_length' | 'algorithm_disallowed' | 'aki_ski_mismatch';
658
+ type ChainErrorReason = 'signature' | 'validity' | 'name_constraints' | 'key_usage' | 'extended_key_usage' | 'basic_constraints' | 'path_length' | 'algorithm_disallowed' | 'aki_ski_mismatch';
593
659
  declare class CertificateChainError extends OpenID4VPError {
594
660
  readonly code: "chain_invalid";
595
661
  readonly reason: ChainErrorReason;
@@ -624,6 +690,19 @@ declare class LotlFetchError extends OpenID4VPError {
624
690
  declare class LotlSignatureError extends OpenID4VPError {
625
691
  readonly code: "lotl_signature_invalid";
626
692
  }
693
+ /**
694
+ * The environment is not configured to verify trusted-list signatures at all —
695
+ * distinct from a list whose signature did not verify.
696
+ *
697
+ * `xmldsigjs` needs a WebCrypto engine registered once per process, and this
698
+ * library deliberately leaves that to the consumer rather than picking a
699
+ * provider on their behalf. Without it, no trusted list can ever verify, so
700
+ * this is raised eagerly instead of degrading per-list: a configuration fault
701
+ * reported as a trust failure sends people bisecting valid XML.
702
+ */
703
+ declare class LotlConfigurationError extends OpenID4VPError {
704
+ readonly code: "lotl_configuration_invalid";
705
+ }
627
706
  type SignedRequestBuildErrorCode = 'empty_cert_chain' | 'hostname_cert_mismatch' | 'signing_key_cert_mismatch' | 'missing_encryption_jwk' | 'missing_encryption_alg' | 'missing_vp_formats' | 'unsupported_signing_alg' | 'empty_supported_enc_values' | 'missing_hostname' | 'self_signed_leaf';
628
707
  declare class SignedRequestBuildError extends OpenID4VPError {
629
708
  readonly code: SignedRequestBuildErrorCode;
@@ -655,6 +734,84 @@ declare class MultipleCredentialsNotSupportedError extends OpenID4VPError {
655
734
  readonly presentationCount: number;
656
735
  constructor(entryCount: number, presentationCount: number);
657
736
  }
737
+ type GuardedFetchRejectReason = 'invalid_url' | 'insecure_scheme' | 'private_address' | 'dns_resolution_failed' | 'too_many_redirects' | 'invalid_redirect' | 'response_too_large' | 'timeout';
738
+ /**
739
+ * A request was refused (or cut off) by the guarded fetcher before an
740
+ * untrusted endpoint could be reached or could exhaust resources.
741
+ */
742
+ declare class GuardedFetchError extends OpenID4VPError {
743
+ readonly code: "guarded_fetch_rejected";
744
+ readonly reason: GuardedFetchRejectReason;
745
+ readonly url: string;
746
+ constructor(reason: GuardedFetchRejectReason, message: string, options: {
747
+ url: string;
748
+ cause?: Error;
749
+ });
750
+ }
751
+
752
+ /**
753
+ * Resolves a hostname to every IP address it currently maps to. Must return
754
+ * all records (A and AAAA): the guard rejects the host if ANY of them is
755
+ * non-public, and treats an empty result as a resolution failure.
756
+ */
757
+ type HostLookup = (hostname: string) => Promise<readonly string[]>;
758
+ interface GuardedFetcherOptions {
759
+ /**
760
+ * Underlying transport. Defaults to `globalThis.fetch`, resolved at call
761
+ * time. Must honour `redirect: 'manual'` and expose the `Location` header
762
+ * of 3xx responses (Node's fetch/undici does; browsers return an opaque
763
+ * redirect, which the guard rejects).
764
+ */
765
+ fetch?: Fetcher;
766
+ /**
767
+ * DNS resolver used for the private-address check. Defaults to
768
+ * `node:dns` `lookup(host, { all: true })` when available. In runtimes
769
+ * without `node:dns` (browsers, some edge workers) only IP-literal and
770
+ * `localhost` targets are checked — inject a resolver there if needed.
771
+ */
772
+ lookup?: HostLookup;
773
+ /** Permit `http:` targets. Default `false` (SD-JWT VC draft-19: HTTPS only). */
774
+ allowHttp?: boolean;
775
+ /** Permit loopback / private / link-local / metadata targets. Default `false`. */
776
+ allowPrivateNetworks?: boolean;
777
+ /** Maximum redirects followed; every hop is re-validated. Default 3. */
778
+ maxRedirects?: number;
779
+ /** Wall-clock budget for the whole exchange (DNS, redirects, body). Default 15 000 ms. */
780
+ timeoutMs?: number;
781
+ /** Maximum response body size, enforced while streaming. Default 16 MiB. */
782
+ maxResponseBytes?: number;
783
+ }
784
+ declare const GUARDED_FETCH_DEFAULTS: {
785
+ readonly maxRedirects: 3;
786
+ readonly timeoutMs: 15000;
787
+ readonly maxResponseBytes: number;
788
+ };
789
+ /**
790
+ * Builds a {@link Fetcher} that applies the HTTP-retrieval rules of
791
+ * draft-ietf-oauth-sd-jwt-vc-19 to every request:
792
+ *
793
+ * - HTTPS only (unless `allowHttp`), and never an https → http downgrade
794
+ * - no loopback / private / link-local / cloud-metadata targets, checked
795
+ * on the URL host AND every address it resolves to (IPv4, IPv6,
796
+ * IPv4-mapped/compatible, NAT64, 6to4)
797
+ * - redirects handled manually, bounded, and re-validated hop by hop
798
+ * - a single timeout covering DNS, every hop and the body
799
+ * - a response-size cap enforced while streaming
800
+ *
801
+ * **DNS rebinding.** The guard resolves the host, validates the addresses,
802
+ * then hands the *hostname* to the transport, which resolves it again. An
803
+ * attacker-controlled DNS server can answer differently the second time
804
+ * (TOCTOU). Closing that gap requires pinning the validated address at the
805
+ * socket layer, which `fetch` does not expose portably. Deployments that
806
+ * dereference attacker-influenced URLs should additionally route egress
807
+ * through a filtering proxy or inject a transport whose connector validates
808
+ * the connected address (e.g. an undici `Agent` with a checking
809
+ * `connect.lookup`).
810
+ *
811
+ * Caching is out of scope here: the trust module caches CRL/OCSP/LOTL
812
+ * artefacts through its own {@link Cache} plug.
813
+ */
814
+ declare function createGuardedFetcher(options?: GuardedFetcherOptions): Fetcher;
658
815
 
659
816
  interface LotlTrustStoreOptions {
660
817
  /** Override the bundled signing anchors. Empty is rejected. */
@@ -693,6 +850,6 @@ declare class LotlTrustStore implements TrustStore {
693
850
  private refresh;
694
851
  }
695
852
 
696
- declare const VERSION = "0.11.1";
853
+ declare const VERSION = "0.13.0";
697
854
 
698
- export { type AuthorizationRequest, type AuthorizationRequestInput, type AuthorizationResponse, type Cache, CertificateChainError, type ChainErrorReason, CompositeTrustStore, type CredentialClaims, type CredentialFormat, DecryptionFailedError, type EncryptedResponse, ExpiredCredentialError, type Fetcher, HAIP_DOCTYPE_NAMESPACES, type HaipQueryInput, type HaipValidationCode, HaipValidationError, type ICredentialParser, InMemoryCache, InvalidSignatureError, type IssuerInfo, type LotlAnchorMetadata, LotlFetchError, LotlSignatureError, LotlTrustStore, type LotlTrustStoreOptions, MalformedCredentialError, MdocParser, MissingDecryptionKeyError, MissingVerifierEncryptionKeyError, MultipleCredentialsNotSupportedError, NonceValidationError, OpenID4VPError, type ParseOptions, type PresentationResult, RevocationCheckFailedError, RevokedCertificateError, SdJwtParser, type SignedAuthorizationRequest, type SignedAuthorizationRequestInput, SignedRequestBuildError, type SignedRequestBuildErrorCode, StaticTrustStore, type TrustAnchor, TrustAnchorNotFoundError, type TrustStore, type TrustStoreHint, type TrustStoreInput, UnsupportedFormatError, UnsupportedJweError, VERSION, type VerifyAuthorizationResponseOptions, type VerifyOptions, type VerifyResult, buildHaipQuery, buildOid4vpSessionTranscript, buildOpenID4VPHandoverSessionTranscript, createAuthorizationRequest, createSignedAuthorizationRequest, decryptAuthorizationResponse, isHaipQuery, parsePresentation, validateHaipQuery, verifyAuthorizationResponse, verifyPresentation };
855
+ export { type AuthorizationRequest, type AuthorizationRequestInput, type AuthorizationResponse, type Cache, CertificateChainError, type ChainErrorReason, CompositeTrustStore, type CredentialClaims, type CredentialFormat, type CredentialSetQueryInput, DecryptionFailedError, type EncryptedResponse, ExpiredCredentialError, type Fetcher, GUARDED_FETCH_DEFAULTS, GuardedFetchError, type GuardedFetchRejectReason, type GuardedFetcherOptions, HAIP_DOCTYPE_NAMESPACES, type HaipQueryInput, type HaipValidationCode, HaipValidationError, type HostLookup, type ICredentialParser, InMemoryCache, InvalidSignatureError, type IssuerInfo, type LotlAnchorMetadata, LotlConfigurationError, LotlFetchError, LotlSignatureError, LotlTrustStore, type LotlTrustStoreOptions, MalformedCredentialError, MdocParser, MissingDecryptionKeyError, MissingVerifierEncryptionKeyError, MultipleCredentialsNotSupportedError, NonceValidationError, OpenID4VPError, type ParseOptions, type PresentationResult, RevocationCheckFailedError, RevokedCertificateError, SdJwtParser, type SignedAuthorizationRequest, type SignedAuthorizationRequestInput, SignedRequestBuildError, type SignedRequestBuildErrorCode, StaticTrustStore, type TrustAnchor, TrustAnchorNotFoundError, type TrustStore, type TrustStoreHint, type TrustStoreInput, UnsupportedFormatError, UnsupportedJweError, VERSION, type VerifyAuthorizationResponseOptions, type VerifyOptions, type VerifyResult, buildCredentialSetQuery, buildHaipQuery, buildOid4vpSessionTranscript, buildOpenID4VPHandoverSessionTranscript, createAuthorizationRequest, createGuardedFetcher, createSignedAuthorizationRequest, decryptAuthorizationResponse, isHaipQuery, parsePresentation, validateCredentialSetQuery, validateHaipQuery, verifyAuthorizationResponse, verifyPresentation };