# Vault Iota

> Implements OpenID for Verifiable Presentations (OID4VP) data sharing flow using the Affinidi Iota Framework.

Affinidi TDK - Vault Iota for Dart implements the holder (wallet) side of the OpenID for Verifiable Presentations (OID4VP) sharing flow used by the Affinidi Iota Framework. It takes a verifier’s OID4VP request URI, matches it against the credentials stored in your Vault, and builds and submits the resulting Verifiable Presentation (VP). This covers the full presentation exchange, from request ingestion to VP submission.

The package exposes four main services that together implement the share flow:

    1
    ShareFlowService

Parses and validates the verifier’s OID4VP request URI.

    2
    CredentialMatcherService

Matches vault credentials against the request, for both Presentation Exchange (PEX) and Digital Credentials Query Language (DCQL) queries.

    3
    IotaShareResponseService

Builds and submits the signed VP, or sends a rejection, to the verifier’s callback endpoint.

    4
    IotaConsentRecordService

Persists consent records and re-submits automatically for previously approved verifiers.

## Key Features

- Parse and validate Iota OID4VP request URIs, including JWT signature verification and nonce replay protection.

- Match vault credentials against a verifier’s request through one unified API, regardless of whether it uses PEX or DCQL.

- Build and submit a signed Verifiable Presentation, or send a rejection, directly to the verifier’s callback endpoint.

- Restrict VP submission to a caller-supplied list of trusted verifier hosts.

- Persist consent records and support automatic, silent re-consent for previously approved verifiers.

- Storage-agnostic: bring your own backend for consent records and nonce replay tracking.

## Installation

Package: affinidi_tdk_vault_iota

```bash
dart pub add affinidi_tdk_vault_iota
```

Check the latest version on [pub.dev](https://pub.dev/packages/affinidi_tdk_vault_iota) or view the source on [GitHub](https://github.com/affinidi/affinidi-tdk-dart/tree/main/packages/vault_iota).

## Parse and Validate the OID4VP Request

The OID4VP request URI typically arrives via a QR code scan, a deep link, or an API response. ShareFlowService.validateOid4vpRequest decodes the JWT from its request query parameter, verifies the signature and expiry, checks nonce uniqueness to prevent replay, and returns a structured Oid4vpShareRequest.

Treat the returned Oid4vpShareRequest as an opaque handle. Pass the same instance to CredentialMatcherService and IotaShareResponseService regardless of whether the verifier used a Presentation Definition or a DCQL query. The package routes between the two protocols internally.

Import

```dart
import 'package:affinidi_tdk_cryptography/affinidi_tdk_cryptography.dart';
import 'package:affinidi_tdk_vault_iota/affinidi_tdk_vault_iota.dart';
```

Parameters

    uri
    Uri
    Required
  The OID4VP request URI containing a request JWT query parameter.

    walletDid
    String
    Optional
  DID of the current wallet. When provided, the JWT aud claim is validated against it.

Example

```dart
final cryptography = CryptographyService();
final service = ShareFlowService(cryptography: cryptography);

// The request URI typically arrives via a QR code scan, deep link, or API response.
final uri = Uri.parse('openid4vp://authorize?request=');

try {
  // Pass walletDid to validate the JWT's aud claim against your wallet's DID.
  final shareRequest = await service.validateOid4vpRequest(
    uri,
    walletDid: 'did:key:z6Mk...',
  );

  print('Verifier DID: ${shareRequest.request.clientId}');
  print('Accept response URI: ${shareRequest.request.acceptResponseUri}');
} on TdkException catch (e) {
  print('OID4VP validation failed [${e.code}]: ${e.message}');
}
```

## Match Credentials Against the Vault

Once the request is validated, match it against the credentials stored in the user’s Vault. CredentialMatcherService.match accepts the Oid4vpShareRequest from the previous step and the full list of vault credentials, and returns a MatchedCredentialsResult with the same shape whether the verifier used PEX or DCQL.

Import

```dart
import 'package:affinidi_tdk_vault_iota/affinidi_tdk_vault_iota.dart';
import 'package:ssi/ssi.dart';
```

Parameters

    shareRequest
    Oid4vpShareRequest
    Required
  The parsed OID4VP request returned by ShareFlowService.validateOid4vpRequest.

    allVCs
    List<VerifiableCredential>
    Required
  All credentials from the user’s Vault.

Example

```dart
// allVaultCredentials comes from your own persistence layer.
final allVaultCredentials = [];

final matcher = CredentialMatcherService();
final result = await matcher.match(shareRequest, allVaultCredentials);

if (!result.hasEnoughVCsAvailableToShare) {
  print('Not enough credentials available to satisfy the request.');
  return;
}

// A sensible default selection to present to the user, or submit directly.
final selectedVcs = result.recommendedMaximumVCs;
```

The MatchedCredentialsResult exposes:

- hasEnoughVCsAvailableToShare - whether every requested credential group has at least its minimum required number of matches.

- recommendedMaximumVCs - the recommended credentials to share across all groups, capped at each group’s maximum.

- availableCredentials - every vault credential that satisfies any group.

- groups - the requested credential groups (MatchedCredentialGroup), each with minimumVCsCountToShare, maximumVCsCountToShare, availableCredentials, recommendedCredentials, and allowsMultiple. Use this to build a selection UI that enforces per-group minimum and maximum counts.

- credentialSetOptions - the DCQL credential_set alternatives, or null when the request does not use credential sets (PEX, or DCQL without credential_sets).

## Submit or Reject the Verifiable Presentation

The IotaShareResponseService builds the VP from the selected credentials and posts it to the verifier’s callback endpoint, or sends a rejection instead. It requires a DidSigner that controls the holder’s signing key, and a trustedVerifiersList of plain host names the VP is allowed to be posted to. See [Security Considerations](#security-considerations) for the validation rules enforced on this list.

### Submit the Verifiable Presentation

Import

```dart
import 'package:affinidi_tdk_vault_iota/affinidi_tdk_vault_iota.dart';
import 'package:ssi/ssi.dart';
```

Parameters

    shareRequest
    Oid4vpShareRequest
    Required
  The parsed OID4VP request returned by ShareFlowService.validateOid4vpRequest.

    selectedCredentials
    List<VerifiableCredential>
    Required
  The credentials to include in the VP.

    acceptResponseUri
    String
    Required
  The URI from the OID4VP request to POST the VP to, normally shareRequest.request.acceptResponseUri.

Example

```dart
// Create a DidSigner that controls the holder's signing key.
// In a real application this comes from your wallet integration.
final wallet = PersistentWallet(InMemoryKeyStore());
final keyPair = await wallet.generateKey(keyType: KeyType.ed25519);
final didManager = DidKeyManager(wallet: wallet, store: InMemoryDidStore());
await didManager.addVerificationMethod(keyPair.id);
final signer = await didManager.getSigner(
  didManager.assertionMethod.first,
  signatureScheme: SignatureScheme.ed25519,
);

final responseService = IotaShareResponseService(
  signer: signer,
  // Only these hosts may be used as VP submission targets.
  trustedVerifiersList: ['verifier.example.com'],
);

try {
  final redirectUri = await responseService.submitShareResponse(
    shareRequest: shareRequest,
    selectedCredentials:
        selectedVcs.cast

>(),
    acceptResponseUri: shareRequest.request.acceptResponseUri,
  );

  // When non-null, navigate the user to this URL to complete the flow on
  // the verifier's side, for example back to the verifier's web app.
  if (redirectUri != null) {
    print('Redirect to: $redirectUri');
  }
} on TdkException catch (e) {
  print('VP submission failed [${e.code}]: ${e.message}');
}
```

### Reject the Verifiable Presentation

Call rejectShareResponse instead of submitShareResponse when the user declines the request. It notifies the verifier without building or sending a VP.

Import

```dart
import 'package:affinidi_tdk_vault_iota/affinidi_tdk_vault_iota.dart';
```

Parameters

    shareRequest
    Oid4vpShareRequest
    Required
  The parsed OID4VP request to reject.

    rejectResponseUri
    String
    Required
  The URI from the OID4VP request to POST the rejection to, normally shareRequest.request.rejectResponseUri.

Example

```dart
final redirectUri = await responseService.rejectShareResponse(
  shareRequest: shareRequest,
  rejectResponseUri: shareRequest.request.rejectResponseUri,
);
```

## Persist and Reuse Consent

After a successful VP submission, persist a consent record so a future share to the same verifier can be recognised and, if the user opted in, submitted automatically without prompting again. IotaConsentRecordService is storage-agnostic: implement ConsentStorage with any persistence technology, such as SQLite, Drift, or a remote API, to back it. If your app already depends on affinidi_tdk_vault_flutter_utils, use its FlutterSecureConsentStorage implementation instead of writing your own.

### Implement ConsentStorage

The three methods have distinct contracts: saveOrUpdate must upsert by IotaConsentRecord.hash, findByRequestHash returns the most recently saved record matching a request fingerprint, and findAllByRequestHash returns every matching record for tryAutomaticConsent to evaluate.

Import

```dart
import 'package:affinidi_tdk_vault_iota/affinidi_tdk_vault_iota.dart';
```

Example

```dart
// Replace the in-memory map with your real database, for example
// sqflite, Drift, Isar, or a remote API.
class MyConsentStorage implements ConsentStorage {
  final Map _records = {};

  @override
  Future saveOrUpdate(IotaConsentRecord record) async {
    _records[record.hash] = record;
  }

  @override
  Future findByRequestHash(String requestHash) async {
    final matches = _records.values
        .where((r) => r.requestHash == requestHash)
        .toList()
      ..sort((a, b) => b.sharedAt.compareTo(a.sharedAt));
    return matches.firstOrNull;
  }

  @override
  Future
- > findAllByRequestHash(
    String requestHash,
  ) async {
    return _records.values.where((r) => r.requestHash == requestHash).toList();
  }
}

final myConsentStorage = MyConsentStorage();
```

### Resolve Verifier Metadata

Both consent methods take a VerifierClientMetadata describing the verifier’s display name, logo, and origin, typically shown on the consent screen alongside the credential selection. Resolve it with VerifierMetadataService, which reads inline client_metadata from the request when present, or fetches it from the Affinidi login configuration API otherwise.

Import

```dart
import 'package:affinidi_tdk_vault_iota/affinidi_tdk_vault_iota.dart';
```

Parameters

    clientId
    String
    Required
  The verifier’s client_id, normally shareRequest.request.clientId.

    clientMetadataUri
    String
    Optional
  URI to fetch client metadata from, when the verifier provided it by reference.

    clientMetadata
    Map<String, dynamic>
    Optional
  Inline client metadata, when the verifier provided it directly in the request.

Example

```dart
final metadataService = VerifierMetadataService(
  baseUrl: 'https://apse1.api.affinidi.io',
);

final verifierMetadata = await metadataService.fetchVerifierMetadata(
  clientId: shareRequest.request.clientId,
  clientMetadataUri: shareRequest.request.clientMetadataUri,
  clientMetadata: shareRequest.request.clientMetadata,
);

// Release the underlying HTTP client once you're done with the service.
metadataService.dispose();
```

### Save a Consent Record

Persist a consent record once the VP has been submitted, keyed by an internal fingerprint the SDK derives from the request and the vault ID. Set isAutoShareEnabled: true when the user opts in to skip the consent screen on their next share to this verifier.

Import

```dart
import 'package:affinidi_tdk_vault_iota/affinidi_tdk_vault_iota.dart';
```

Parameters

    shareRequest
    Oid4vpShareRequest
    Required
  The validated OID4VP share request. The SDK derives the request fingerprint and the verifier client_id from it.

    verifierMetadata
    VerifierClientMetadata
    Required
  Resolved branding of the verifier, from VerifierMetadataService.fetchVerifierMetadata.

    profileId
    String
    Required
  ID of the profile used for the share.

    profileName
    String
    Required
  Display name of the profile used for the share.

    vaultId
    String
    Required
  Opaque identifier of the Vault or wallet that signed the VP, for example a DID or account ID.

    sharedVcs
    List<VerifiableCredential>
    Required
  The VCs included in the VP, in presentation order.

    claimedVcTypesCsv
    String
    Required
  Comma-separated VC types included in the VP.

    isAutoShareEnabled
    bool
    Required
  Whether the user enabled automatic sharing for this verifier.

    historySharedData
    Map<String, String>
    Optional
  Labelled data points shared in the VP. Defaults to an empty map.

    isConsentManagementEnabled
    bool
    Optional
  Whether the verifier has consent management enabled. Defaults to false.

Example

```dart
final consentService = IotaConsentRecordService(
  store: myConsentStorage,
  cryptography: cryptography,
  shareResponseService: responseService,
);

// These normally come from the wallet/profile currently in use, not literals.
const holderVaultId = 'did:key:z6MkHolder456';
const profileId = 'profile-abc';
const profileName = 'Personal';

await consentService.saveConsentRecord(
  shareRequest: shareRequest,
  verifierMetadata: verifierMetadata,
  profileId: profileId,
  profileName: profileName,
  vaultId: holderVaultId,
  sharedVcs: selectedVcs,
  claimedVcTypesCsv: 'EmailV1VC,PhoneNumberV1VC',
  isAutoShareEnabled: true, // user opted in to auto-share for this verifier
  historySharedData: {
    'Email address': 'user@example.com',
  },
);
```

### Attempt Automatic Consent

Before showing the consent screen, call tryAutomaticConsent to check whether a prior approved record covers the current request, and if so, submit the VP silently. It re-validates every security-sensitive field against the live request, including the verifier’s clientId, the credential count, and the full share fingerprint, before submitting, so a stale or tampered record can never be replayed as consent.
Caution

If reading the stored records fails, tryAutomaticConsent does not throw. It logs a warning and returns AutoConsentDeclined, so a storage fault falls back to the interactive consent screen instead of blocking the user. Do not wrap this call in a try/catch expecting a storage-error code; check the returned AutoConsentResult instead.

Import

```dart
import 'package:affinidi_tdk_vault_iota/affinidi_tdk_vault_iota.dart';
```

Parameters

    shareRequest
    Oid4vpShareRequest
    Required
  The parsed OID4VP share request, providing the presentation definition, state, nonce, and clientId.

    matchedCredentials
    MatchedCredentialsResult
    Required
  The already-matched credentials from CredentialMatcherService.match.

    verifierMetadata
    VerifierClientMetadata
    Required
  Current verifier branding, compared against the stored fingerprint to detect changes.

    vaultId
    String
    Required
  Opaque identifier of the vault or wallet that will sign the VP, for example a DID.

Example

```dart
final autoResult = await consentService.tryAutomaticConsent(
  shareRequest: shareRequest,
  matchedCredentials: result, // from CredentialMatcherService.match
  verifierMetadata: verifierMetadata,
  vaultId: holderVaultId,
);

switch (autoResult) {
  case AutoConsentApproved(:final redirectUri):
    print('Auto-consent approved. Redirect: ${redirectUri ?? "(none)"}');
  case AutoConsentDeclined():
    print('Auto-consent declined. Show the share approval UI.');
}
```

## Security Considerations

### Replay Attack Protection

The ShareFlowService prevents replay attacks by rejecting a request whose nonce has already been seen. Calling validateOid4vpRequest a second time with the same JWT nonce, while the JWT is still within its exp window, throws a TdkException with code replay_detected.

By default, nonces are tracked in an in-memory cache scoped to the ShareFlowService instance, so replay protection does not survive a process restart. Implement NonceReplayStore with persistent storage and inject it through the replayCache constructor parameter for replay protection that survives app restarts.

Example

```dart
class MyPersistentNonceStore implements NonceReplayStore {
  @override
  Future record(String nonce, int expEpochSeconds) async {
    // Atomically check and store; return false if the nonce was already seen.
    return myDb.recordNonce(nonce, expEpochSeconds);
  }
}

final service = ShareFlowService(
  cryptography: cryptography,
  replayCache: MyPersistentNonceStore(),
);
```

### Trusted Verifier Hosts

The IotaShareResponseService requires a non-empty trustedVerifiersList of plain host names, and enforces the following rules:

- Entries must be plain host names, with no scheme, port, path, query string, or userinfo. An empty list, or an entry that fails this check, throws immediately when the service is constructed.

- Matching against the list is case-insensitive.

- Both acceptResponseUri and rejectResponseUri must be well-formed HTTPS URIs with no IP-address hostname, or the call throws invalid_response_uri. If the URI is well-formed but its host is not in the list, the call throws untrusted_response_uri instead. Both checks run before any network request is made.

Caution

The rules above apply to acceptResponseUri and rejectResponseUri. The verifier’s own redirect_uri is checked the same way, but on failure the SDK logs a warning and drops it instead of throwing, so a null return means no redirect was followed, not that none was attempted.

Example

```dart
final responseService = IotaShareResponseService(
  signer: mySigner,
  trustedVerifiersList: ['verifier.example.com', 'other-verifier.example.com'],
);
```

## Error Handling

All errors are thrown as TdkException, exposing the code that identifies the failure.

| Code | Thrown by | Description |
| parse_failure | validateOid4vpRequest | The request query parameter is missing, the JWT could not be decoded, or a required payload field is missing |
| invalid_or_expired_jwt | validateOid4vpRequest | The JWT signature is invalid, the token has expired, or its iat claim is in the future |
| invalid_client_id_scheme | validateOid4vpRequest | client_id_scheme is not did |
| invalid_audience | validateOid4vpRequest | The JWT aud claim is present but does not match walletDid, or walletDid was omitted |
| missing_client_id | validateOid4vpRequest | The client_id field is missing from the request |
| invalid_response_mode | validateOid4vpRequest | response_mode is not direct_post |
| invalid_response_type | validateOid4vpRequest | response_type is not vp_token |
| replay_detected | validateOid4vpRequest | The request nonce has already been consumed within its expiry window |
| invalid_presentation_definition | validateOid4vpRequest, match | The Presentation Definition is structurally invalid |
| invalid_dcql_query | validateOid4vpRequest | The DCQL query is structurally invalid |
| unsupported_multiple_idv_types | match | A single input descriptor requests more than two verified-identity-document credential types |
| unsupported_credential_format | submitShareResponse | A selected credential is not JSON-LD (for example JWT-VC or SD-JWT-VC), which the current VP builder cannot embed |
| empty_trusted_verifiers_list | IotaShareResponseService(...) constructor | trustedVerifiersList was empty |
| invalid_response_uri | IotaShareResponseService(...) constructor, submitShareResponse, rejectShareResponse | A trustedVerifiersList entry was not a plain host name, or acceptResponseUri/rejectResponseUri was not a valid HTTPS URI |
| untrusted_response_uri | submitShareResponse, rejectShareResponse | The callback host is not in trustedVerifiersList |
| submission_failed | submitShareResponse, rejectShareResponse | Posting to the verifier’s callback endpoint failed, due to a network error or a non-2xx response |
| invalid_client_id | fetchVerifierMetadata | An empty clientId was passed to the verifier metadata service |
| failed_to_fetch_verifier_metadata | fetchVerifierMetadata | The verifier’s client metadata could not be fetched, or the response was not a JSON object |
| failed_to_persist_consent_record | saveConsentRecord | The underlying ConsentStorage.saveOrUpdate call failed |

For complete runnable examples covering each step in this guide, see the [examples on GitHub](https://github.com/affinidi/affinidi-tdk-dart/tree/main/packages/vault_iota/example):

- Parsing and validating an OID4VP request.

- Matching credentials and submitting a share response.

- Saving a consent record and using automatic consent.

## Related

      [

Vault →

Libraries to implement digital identity wallet into your Flutter/Dart applications.

      ](/dev-tools/affinidi-tdk/dart/libraries/vault.md)

      [

Affinidi Iota Framework →

Manage Affinidi Iota Framework configuration to request data from Affinidi Vault.

      ](/dev-tools/affinidi-tdk/dart/clients/iota-framework.md)
