@openeudi/openid4vp 0.11.1-rc.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -119,6 +119,73 @@ Supported formats: `dc+sd-jwt` and `mso_mdoc`. Other formats (e.g., `jwt_vc_json
119
119
 
120
120
  Known EUDI doctypes auto-namespace their claim paths (e.g., `org.iso.18013.5.1.mDL` → claims under `org.iso.18013.5.1`). Unknown doctypes use the full doctype string as the namespace.
121
121
 
122
+ ### Offering the wallet a choice (`credential_sets`)
123
+
124
+ Some questions can be answered by more than one credential. The clearest case is
125
+ proof of age: a wallet may hold a **Proof-of-Age attestation** (`mso_mdoc`, with
126
+ `age_over_18` in namespace `eu.europa.ec.av.1`) or only a **PID** (`dc+sd-jwt`,
127
+ carrying `birth_date` — the PID has no age attribute, so the verifier computes the
128
+ threshold). Asking for both in one request avoids a second wallet interaction:
129
+
130
+ ```ts
131
+ import { buildCredentialSetQuery } from "@openeudi/openid4vp";
132
+
133
+ const query = buildCredentialSetQuery({
134
+ options: [
135
+ // Most-preferred first — discloses only a boolean.
136
+ {
137
+ credentialId: "age-attestation",
138
+ format: "mso_mdoc",
139
+ doctypeValue: "eu.europa.ec.av.1",
140
+ claims: ["age_over_18"],
141
+ },
142
+ // Fallback — the verifier computes the threshold from birth_date.
143
+ {
144
+ credentialId: "pid",
145
+ format: "dc+sd-jwt",
146
+ vctValues: ["urn:eu.europa.ec.eudi:pid:1"],
147
+ claims: ["birth_date"],
148
+ },
149
+ ],
150
+ });
151
+ ```
152
+
153
+ The wallet satisfies **one** option and returns **one** presentation;
154
+ `verifyAuthorizationResponse` resolves it by the id the wallet keyed `vp_token`
155
+ with. Option order is a privacy preference signal, not a constraint — wallets are
156
+ not obliged to honour it.
157
+
158
+ Validate a hand-built disjunction with `validateCredentialSetQuery`. Note this is
159
+ deliberately **not** HAIP-minimal: `validateHaipQuery` rejects `credential_sets`
160
+ and `isHaipQuery` returns `false` for these queries.
161
+
162
+ > **`match.unmatched` is non-empty on success here.** A disjunction is satisfied
163
+ > when the wallet takes **one** option, so the option it did *not* take is
164
+ > reported as unmatched — typically with `format_mismatch` or `vct_mismatch`
165
+ > against the credential the wallet never held. That is the expected result, not
166
+ > a failure:
167
+ >
168
+ > ```ts
169
+ > // Wallet presented the PID; the attestation option was not taken.
170
+ > result.valid // true
171
+ > result.match.satisfied // true
172
+ > result.match.matches // [{ queryId: "pid", ... }]
173
+ > result.match.unmatched // [{ queryId: "age-attestation", reason: "format_mismatch" }]
174
+ > ```
175
+ >
176
+ > Gate on `result.valid` (or `match.satisfied`), **never** on
177
+ > `match.unmatched.length === 0`. The latter is the correct test only for a query
178
+ > without `credential_sets`, and silently rejects every valid disjunction
179
+ > response. If you log `unmatched` as "mismatch reasons" (as the verification
180
+ > example below does), expect a benign entry per successful disjunction and
181
+ > filter accordingly so it does not trip alerting.
182
+
183
+ > **Not ZKP.** This is the plain `mso_mdoc` attestation path. The AV technical
184
+ > specification's preferred mechanism — a Zero-Knowledge Proof — is not supported
185
+ > over OpenID4VP (no standardised DCQL query for it exists); it rides only the W3C
186
+ > Digital Credentials API. Plain mDoc is the profile's designated fallback for this
187
+ > transport.
188
+
122
189
  ## Verifying presentations against a query
123
190
 
124
191
  Use `verifyPresentation` to combine crypto/structural verification with DCQL matching in a single call:
@@ -143,6 +210,11 @@ if (result.valid) {
143
210
 
144
211
  Mismatches return `valid: false` — they do not throw. Only crypto/structural failures (malformed VP tokens, invalid signatures, expired credentials) and malformed DCQL queries throw exceptions.
145
212
 
213
+ The `else` branch above is the right place to read `unmatched` for an ordinary
214
+ query. For a **`credential_sets` disjunction** it is not the whole story: the
215
+ option the wallet did not take is reported as unmatched even when `valid` is
216
+ `true`. See [Offering the wallet a choice](#offering-the-wallet-a-choice-credential_sets).
217
+
146
218
  > **Privacy — diagnostics are verifier-internal.** `match.unmatched[].reason` and `detail` (including `value_mismatch`) are intended for verifier-side logging, debugging, and admin UIs. OpenID4VP §11 warns that per-claim verification outcomes can reveal wallet contents to observers. Do NOT echo these diagnostics into the OpenID4VP wire response sent back to the wallet, into end-user-visible error messages that another party could correlate, or into public analytics/third-party logs. The protocol's own error codes are the public interface; these fields are your internal instrumentation.
147
219
 
148
220
  ## Signed authorization requests (x509_san_dns, x509_hash)
@@ -318,6 +390,21 @@ Both `parsePresentation` and `verifyPresentation` accept:
318
390
  - `verifierEncryptionJwk?` — *(`VerifyAuthorizationResponseOptions` only)* the verifier's response-encryption public JWK. Required on the encrypted path when `sessionTranscriptProfile: 'openid4vp-1.0'` is used, to derive the handover's JWK thumbprint; ignored for the `'iso-18013-7'` profile.
319
391
  - `trustedCertificates` (required when `trustStore` is unset) — DER-encoded issuer leaf certificates for the 0.4.x byte-equality trust check. Deprecated since 0.5.0 — pass an empty array and supply `trustStore` for production deployments.
320
392
  - `trustStore?` — `TrustStore` instance for full RFC 5280 chain validation (e.g. `LotlTrustStore`, `StaticTrustStore`, or `CompositeTrustStore`). When set, takes precedence over `trustedCertificates`.
393
+
394
+ > **`LotlTrustStore` requires an `xmldsigjs` crypto engine.** Trusted lists are
395
+ > verified as signed XML, and this library does not pick a WebCrypto provider on
396
+ > your behalf. Register one **once at startup**, before any trusted-list fetch:
397
+ >
398
+ > ```ts
399
+ > import * as xmldsig from "xmldsigjs";
400
+ > import { Crypto } from "@peculiar/webcrypto";
401
+ >
402
+ > xmldsig.Application.setEngine("NodeJS", new Crypto());
403
+ > ```
404
+ >
405
+ > Omit it and `LotlTrustStore` throws `LotlConfigurationError` on first use,
406
+ > naming this as the cause. `StaticTrustStore` is unaffected.
407
+
321
408
  - `revocationPolicy?` — `'skip'` (default) | `'prefer'` | `'require'`. Controls whether the chain validator consults OCSP / CRL.
322
409
  - `fetcher?` — HTTP transport for CRL/OCSP/LOTL fetches. Defaults to `globalThis.fetch`.
323
410
  - `cache?` — cache for CRL/OCSP/LOTL artefacts. Defaults to `new InMemoryCache()`.
@@ -405,6 +492,7 @@ class MyCustomParser implements ICredentialParser {
405
492
  | `MalformedCredentialError` | Credential structure is malformed | Token cannot be decoded or is structurally invalid |
406
493
  | `NonceValidationError` | Nonce does not match expected value | Key binding JWT nonce does not match |
407
494
  | `HaipValidationError` | HAIP query constraint violated | DCQL query fails `validateHaipQuery` |
495
+ | `LotlConfigurationError` | No xmldsigjs crypto engine registered | `LotlTrustStore` used without `Application.setEngine` |
408
496
 
409
497
  ```ts
410
498
  import { MalformedCredentialError, ExpiredCredentialError } from "@openeudi/openid4vp";
@@ -427,7 +515,7 @@ This library implements the **verifier side** of OpenID4VP for SD-JWT VC and mDO
427
515
  - **SD-JWT VC** — full cryptographic verification (issuer JWT signature via `x5c`, transitive disclosure-hash check, key binding JWT signature + `sd_hash`, nonce check). Holder binding (KB-JWT) is **mandatory and fails closed** whenever the issuer JWT carries a `cnf` claim; `requireKeyBinding` extends the requirement to credentials without `cnf`. Optional `trustedIssuerJwks` alternate trust path for VCs without `x5c`.
428
516
  - **mDOC / ISO 18013-5** `mso_mdoc` format — CBOR decoding, claim extraction, COSE_Sign1 signature verification, MobileSecurityObject validity enforcement, IssuerSignedItem digest verification. Device authentication (ISO 18013-5 §9.1.3) is **mandatory and fails closed**: the `DeviceSignature` (COSE_Sign1) over `DeviceAuthentication`/`SessionTranscript` is verified against the MSO-committed device key. `DeviceMac` (COSE_Mac0) is **not supported and is rejected**.
429
517
  - **DCQL** — authorization request builder with DCQL query, matching via [@openeudi/dcql](https://www.npmjs.com/package/@openeudi/dcql), `verifyPresentation` for combined crypto + match.
430
- - **HAIP** — `buildHaipQuery` / `validateHaipQuery` helpers for the High Assurance Interoperability Profile.
518
+ - **HAIP** — `buildHaipQuery` / `validateHaipQuery` helpers for the High Assurance Interoperability Profile, plus `buildCredentialSetQuery` / `validateCredentialSetQuery` for `credential_sets` disjunctions.
431
519
  - **Signed authorization requests (JAR)** — `createSignedAuthorizationRequest` per RFC 9101 / OpenID4VP 1.0 §5.10 with `x509_san_dns` or `x509_hash` client-id binding. Emits the OpenID4VP 1.0 Final `client_metadata` shape. Self-signed verifier leaf certificates are rejected by default per HAIP 1.0 Final.
432
520
  - **Encrypted responses** — `decryptAuthorizationResponse` for `direct_post.jwt` (ECDH-ES + A128GCM/A256GCM), `verifyAuthorizationResponse` for the 1.0 §8.1 object-keyed `vp_token` envelope.
433
521
  - **X.509 chain validation** — RFC 5280 chain building including `nameConstraints`, `StaticTrustStore`, `CompositeTrustStore`.
package/dist/index.cjs CHANGED
@@ -45,7 +45,7 @@ var init_reflect_polyfill = __esm({
45
45
  });
46
46
 
47
47
  // src/errors.ts
48
- exports.InvalidSignatureError = void 0; exports.ExpiredCredentialError = void 0; exports.UnsupportedFormatError = void 0; exports.MalformedCredentialError = void 0; exports.NonceValidationError = void 0; exports.HaipValidationError = void 0; exports.OpenID4VPError = void 0; exports.TrustAnchorNotFoundError = void 0; exports.CertificateChainError = void 0; exports.RevokedCertificateError = void 0; exports.RevocationCheckFailedError = void 0; exports.LotlFetchError = void 0; exports.LotlSignatureError = void 0; exports.SignedRequestBuildError = void 0; exports.UnsupportedJweError = void 0; exports.DecryptionFailedError = void 0; exports.MissingDecryptionKeyError = void 0; exports.MissingVerifierEncryptionKeyError = void 0; exports.MultipleCredentialsNotSupportedError = void 0;
48
+ exports.InvalidSignatureError = void 0; exports.ExpiredCredentialError = void 0; exports.UnsupportedFormatError = void 0; exports.MalformedCredentialError = void 0; exports.NonceValidationError = void 0; exports.HaipValidationError = void 0; exports.OpenID4VPError = void 0; exports.TrustAnchorNotFoundError = void 0; exports.CertificateChainError = void 0; exports.RevokedCertificateError = void 0; exports.RevocationCheckFailedError = void 0; exports.LotlFetchError = void 0; exports.LotlSignatureError = void 0; exports.LotlConfigurationError = void 0; exports.SignedRequestBuildError = void 0; exports.UnsupportedJweError = void 0; exports.DecryptionFailedError = void 0; exports.MissingDecryptionKeyError = void 0; exports.MissingVerifierEncryptionKeyError = void 0; exports.MultipleCredentialsNotSupportedError = void 0;
49
49
  var init_errors = __esm({
50
50
  "src/errors.ts"() {
51
51
  init_reflect_polyfill();
@@ -142,6 +142,9 @@ var init_errors = __esm({
142
142
  exports.LotlSignatureError = class extends exports.OpenID4VPError {
143
143
  code = "lotl_signature_invalid";
144
144
  };
145
+ exports.LotlConfigurationError = class extends exports.OpenID4VPError {
146
+ code = "lotl_configuration_invalid";
147
+ };
145
148
  exports.SignedRequestBuildError = class extends exports.OpenID4VPError {
146
149
  code;
147
150
  constructor(code, message) {
@@ -8443,6 +8446,33 @@ function buildHaipQuery(input) {
8443
8446
  }
8444
8447
  return { credentials: [credential] };
8445
8448
  }
8449
+ function buildCredentialSetQuery(input) {
8450
+ if (input.options.length === 0) {
8451
+ throw new exports.HaipValidationError(
8452
+ "EMPTY_OPTIONS",
8453
+ "a credential set must offer at least one option"
8454
+ );
8455
+ }
8456
+ const seen = /* @__PURE__ */ new Set();
8457
+ for (const option of input.options) {
8458
+ if (seen.has(option.credentialId)) {
8459
+ throw new exports.HaipValidationError(
8460
+ "DUPLICATE_CREDENTIAL_ID",
8461
+ `credential '${option.credentialId}' is declared by more than one option; credential ids must be unique for credential_sets to reference them`,
8462
+ option.credentialId
8463
+ );
8464
+ }
8465
+ seen.add(option.credentialId);
8466
+ }
8467
+ const credentials = input.options.map((option) => buildHaipQuery(option).credentials[0]);
8468
+ const credentialSet = {
8469
+ options: input.options.map((option) => [option.credentialId])
8470
+ };
8471
+ if (input.required === false) {
8472
+ credentialSet.required = false;
8473
+ }
8474
+ return { credentials, credential_sets: [credentialSet] };
8475
+ }
8446
8476
  var HAIP_FORMATS = /* @__PURE__ */ new Set(["dc+sd-jwt", "mso_mdoc"]);
8447
8477
  function validateHaipQuery(query) {
8448
8478
  if (!query.credentials || query.credentials.length === 0) {
@@ -8458,46 +8488,86 @@ function validateHaipQuery(query) {
8458
8488
  );
8459
8489
  }
8460
8490
  for (const credential of query.credentials) {
8461
- if (!HAIP_FORMATS.has(credential.format)) {
8491
+ validateCredential(credential);
8492
+ }
8493
+ }
8494
+ function validateCredential(credential) {
8495
+ if (!HAIP_FORMATS.has(credential.format)) {
8496
+ throw new exports.HaipValidationError(
8497
+ "UNSUPPORTED_FORMAT",
8498
+ `credential '${credential.id}' uses unsupported format '${credential.format}' (allowed: dc+sd-jwt, mso_mdoc)`,
8499
+ credential.id
8500
+ );
8501
+ }
8502
+ if (credential.format === "dc+sd-jwt") {
8503
+ const vctValues = credential.meta?.vct_values;
8504
+ if (!vctValues || vctValues.length === 0) {
8462
8505
  throw new exports.HaipValidationError(
8463
- "UNSUPPORTED_FORMAT",
8464
- `credential '${credential.id}' uses unsupported format '${credential.format}' (allowed: dc+sd-jwt, mso_mdoc)`,
8506
+ "MISSING_SDJWT_META",
8507
+ `credential '${credential.id}' with format 'dc+sd-jwt' must set meta.vct_values`,
8465
8508
  credential.id
8466
8509
  );
8467
8510
  }
8468
- if (credential.format === "dc+sd-jwt") {
8469
- const vctValues = credential.meta?.vct_values;
8470
- if (!vctValues || vctValues.length === 0) {
8471
- throw new exports.HaipValidationError(
8472
- "MISSING_SDJWT_META",
8473
- `credential '${credential.id}' with format 'dc+sd-jwt' must set meta.vct_values`,
8474
- credential.id
8475
- );
8476
- }
8477
- }
8478
- if (credential.format === "mso_mdoc") {
8479
- if (typeof credential.meta?.doctype_value !== "string") {
8480
- throw new exports.HaipValidationError(
8481
- "MISSING_MDOC_META",
8482
- `credential '${credential.id}' with format 'mso_mdoc' must set meta.doctype_value`,
8483
- credential.id
8484
- );
8485
- }
8486
- }
8487
- if (!credential.claims || credential.claims.length === 0) {
8511
+ }
8512
+ if (credential.format === "mso_mdoc") {
8513
+ if (typeof credential.meta?.doctype_value !== "string") {
8488
8514
  throw new exports.HaipValidationError(
8489
- "NO_CLAIMS",
8490
- `credential '${credential.id}' must declare at least one claim`,
8515
+ "MISSING_MDOC_META",
8516
+ `credential '${credential.id}' with format 'mso_mdoc' must set meta.doctype_value`,
8491
8517
  credential.id
8492
8518
  );
8493
8519
  }
8494
- if (credential.claim_sets !== void 0) {
8520
+ }
8521
+ if (!credential.claims || credential.claims.length === 0) {
8522
+ throw new exports.HaipValidationError(
8523
+ "NO_CLAIMS",
8524
+ `credential '${credential.id}' must declare at least one claim`,
8525
+ credential.id
8526
+ );
8527
+ }
8528
+ if (credential.claim_sets !== void 0) {
8529
+ throw new exports.HaipValidationError(
8530
+ "CLAIM_SETS_DISALLOWED",
8531
+ `credential '${credential.id}' uses claim_sets, not permitted in HAIP-minimal`,
8532
+ credential.id
8533
+ );
8534
+ }
8535
+ }
8536
+ function validateCredentialSetQuery(query) {
8537
+ if (!query.credentials || query.credentials.length === 0) {
8538
+ throw new exports.HaipValidationError(
8539
+ "EMPTY_QUERY",
8540
+ "query.credentials must have at least one entry"
8541
+ );
8542
+ }
8543
+ if (!query.credential_sets || query.credential_sets.length === 0) {
8544
+ throw new exports.HaipValidationError(
8545
+ "MISSING_CREDENTIAL_SETS",
8546
+ "a credential-set query must declare at least one credential_sets entry"
8547
+ );
8548
+ }
8549
+ for (const credential of query.credentials) {
8550
+ validateCredential(credential);
8551
+ }
8552
+ const declaredIds = new Set(query.credentials.map((c) => c.id));
8553
+ for (const credentialSet of query.credential_sets) {
8554
+ if (!credentialSet.options || credentialSet.options.length === 0) {
8495
8555
  throw new exports.HaipValidationError(
8496
- "CLAIM_SETS_DISALLOWED",
8497
- `credential '${credential.id}' uses claim_sets, not permitted in HAIP-minimal`,
8498
- credential.id
8556
+ "EMPTY_OPTIONS",
8557
+ "a credential set must offer at least one option"
8499
8558
  );
8500
8559
  }
8560
+ for (const option of credentialSet.options) {
8561
+ for (const id of option) {
8562
+ if (!declaredIds.has(id)) {
8563
+ throw new exports.HaipValidationError(
8564
+ "UNKNOWN_OPTION_REFERENCE",
8565
+ `credential_sets option references '${id}', which is not declared in query.credentials`,
8566
+ id
8567
+ );
8568
+ }
8569
+ }
8570
+ }
8501
8571
  }
8502
8572
  }
8503
8573
  function isHaipQuery(query) {
@@ -9967,6 +10037,7 @@ function bytesToHex3(bytes) {
9967
10037
 
9968
10038
  // src/trust/LotlTrustStore.ts
9969
10039
  init_reflect_polyfill();
10040
+ init_errors();
9970
10041
 
9971
10042
  // src/trust/lotl-signing-anchors.ts
9972
10043
  init_reflect_polyfill();
@@ -10035,6 +10106,7 @@ var LotlFetcher = class {
10035
10106
  this.fetcher = opts.fetcher ?? globalThis.fetch.bind(globalThis);
10036
10107
  }
10037
10108
  async fetchSigned(url, signingAnchors) {
10109
+ assertCryptoEngineRegistered();
10038
10110
  let xmlText;
10039
10111
  try {
10040
10112
  const response = await this.fetcher(url, { method: "GET" });
@@ -10087,6 +10159,22 @@ var LotlFetcher = class {
10087
10159
  );
10088
10160
  }
10089
10161
  };
10162
+ function assertCryptoEngineRegistered() {
10163
+ let engine;
10164
+ try {
10165
+ engine = xmldsig__namespace.Application.crypto;
10166
+ } catch (err) {
10167
+ throw new exports.LotlConfigurationError(
10168
+ `no xmldsigjs crypto engine registered \u2014 trusted-list signatures cannot be verified. Call xmldsig.Application.setEngine('NodeJS', new Crypto()) (e.g. from @peculiar/webcrypto) once at startup, before using LotlTrustStore. Underlying error: ${err instanceof Error ? err.message : String(err)}`,
10169
+ { cause: err instanceof Error ? err : void 0 }
10170
+ );
10171
+ }
10172
+ if (!engine) {
10173
+ throw new exports.LotlConfigurationError(
10174
+ "no xmldsigjs crypto engine registered \u2014 trusted-list signatures cannot be verified. Call xmldsig.Application.setEngine('NodeJS', new Crypto()) (e.g. from @peculiar/webcrypto) once at startup, before using LotlTrustStore."
10175
+ );
10176
+ }
10177
+ }
10090
10178
  async function verifyAgainst(doc, sigEl, anchor) {
10091
10179
  try {
10092
10180
  const signed = new xmldsig__namespace.SignedXml(doc);
@@ -10234,6 +10322,7 @@ function parsePointer(el) {
10234
10322
 
10235
10323
  // src/trust/NationalTlResolver.ts
10236
10324
  init_reflect_polyfill();
10325
+ init_errors();
10237
10326
  var TSL_NS2 = "http://uri.etsi.org/02231/v2#";
10238
10327
  var NationalTlResolver = class {
10239
10328
  constructor(opts) {
@@ -10249,6 +10338,7 @@ var NationalTlResolver = class {
10249
10338
  );
10250
10339
  return parseNationalTl(doc, pointer.country);
10251
10340
  } catch (err) {
10341
+ if (err instanceof exports.LotlConfigurationError) throw err;
10252
10342
  console.warn(
10253
10343
  `[openid4vp] national TL for ${pointer.country} failed: ${err.message}`
10254
10344
  );
@@ -10427,6 +10517,7 @@ var LotlTrustStore = class {
10427
10517
  };
10428
10518
  return this.snapshot;
10429
10519
  } catch (err) {
10520
+ if (err instanceof exports.LotlConfigurationError) throw err;
10430
10521
  if (this.snapshot) {
10431
10522
  console.warn(
10432
10523
  `[openid4vp] LOTL refresh failed; serving cached snapshot (${err.message})`
@@ -10474,7 +10565,7 @@ function equalBytes(a, b) {
10474
10565
 
10475
10566
  // src/index.ts
10476
10567
  init_errors();
10477
- var VERSION = "0.11.1-rc.0";
10568
+ var VERSION = "0.12.0";
10478
10569
  /*! Bundled license information:
10479
10570
 
10480
10571
  pvtsutils/build/index.es.js:
@@ -10560,6 +10651,7 @@ exports.MdocParser = MdocParser;
10560
10651
  exports.SdJwtParser = SdJwtParser;
10561
10652
  exports.StaticTrustStore = StaticTrustStore;
10562
10653
  exports.VERSION = VERSION;
10654
+ exports.buildCredentialSetQuery = buildCredentialSetQuery;
10563
10655
  exports.buildHaipQuery = buildHaipQuery;
10564
10656
  exports.buildOid4vpSessionTranscript = buildOid4vpSessionTranscript;
10565
10657
  exports.buildOpenID4VPHandoverSessionTranscript = buildOpenID4VPHandoverSessionTranscript;
@@ -10568,6 +10660,7 @@ exports.createSignedAuthorizationRequest = createSignedAuthorizationRequest;
10568
10660
  exports.decryptAuthorizationResponse = decryptAuthorizationResponse;
10569
10661
  exports.isHaipQuery = isHaipQuery;
10570
10662
  exports.parsePresentation = parsePresentation;
10663
+ exports.validateCredentialSetQuery = validateCredentialSetQuery;
10571
10664
  exports.validateHaipQuery = validateHaipQuery;
10572
10665
  exports.verifyAuthorizationResponse = verifyAuthorizationResponse;
10573
10666
  exports.verifyPresentation = verifyPresentation;