affinidi_tdk_vdip 1.0.0
affinidi_tdk_vdip: ^1.0.0 copied to clipboard
A Dart package for implementing the Verifiable Data Issuance Protocol (VDIP) using DIDComm v2.1 to facilitate secure credential issuance.
Affinidi VDIP for Dart #
A Dart package for implementing the Verifiable Data Issuance Protocol (VDIP), a protocol that facilitates secure, interoperable verifiable credential issuance between Issuers and Holders using the DIDComm v2.1 protocol, an open standard for decentralised communication.
The VDIP consist of two main classes:
-
VdipIssuerthat represents the credential-issuing authority (Issuer) that creates and signs verifiable credentials, such as diplomas, certificates, licenses, or identity documents, attesting to the claim about the subject (Holder). -
VdipHolderthat represents the user or entity requesting and claiming the credential offer issued by the issuer.
These classes simplify the credential issuance using the DIDComm v2.1 protocol, including support for different credential formats such as W3C Data Model V1 and V2, JWT VC, and SD-JWT VC. Additionally, this package provides holder-bound assertions to bind the credential to the holder cryptographically, ensuring only the intended recipient of the credential offer can claim it.
The VDIP package provides developers with tools to build robust and secure credential issuance features into their applications. VDIP enables use cases such as:
- Verified identity credential issuance for onboarding and background screening
- Employee or student credential issuance to grant access to systems
- Issuance of certificates, such as training or course completion
These are a few scenarios from a wide range of use cases unlocked by VDIP that require the issuance of attested and verified information from a trusted issuer to the holders while maintaining privacy and control over their data.
Table of Contents #
- Core Concepts
- Key Features
- Protocol Flow
- Requirements
- Installation
- Usage
- Message References
- Security Features
- Problem Report Message
- Complete Example
- Support & Feedback
- Contributing
Core Concepts #
The Verifiable Data Issuance Protocol (VDIP) uses existing open standards, such as DIDComm v2.1, and cryptographic techniques to provide a secure, private, and trusted credential issuance flow.
-
DIDComm v2.1 protocol - An open standard for decentralised communication. Built on the foundation of Decentralised Identifiers (DIDs), it enables parties to exchange verifiable data such as credentials and establishes secure communication channels between parties without relying on centralised servers.
-
Verifiable Data Issuance Protocol (VDIP) - A protocol built on top of DIDComm v2 that enables secure issuance of verifiable credentials between issuers and holders. VDIP defines message types, workflows, and security requirements for credential issuance. There are two main roles in the credential issuance flow:
-
Issuer - An entity that creates, signs, and issues verifiable credentials. The issuer validates requests, constructs credentials in a supported format, signs them, and delivers them to the holder via DIDComm message.
-
Holder - An entity requesting and claiming the verifiable credentials. The holder initiates the issuance flow by sending a request specifying the desired credential format and providing any required metadata.
-
-
Verifiable Credential - A cryptographically secure, tamper-evident credential that contains claims about a subject (Holder), issued by a trusted issuer.
-
Holder-Bound Assertion - A signed JWT token that proves the holder controls a specific DID. It allows issuing credentials to a DID different from the DIDComm message sender requesting for credential issuance, with cryptographic proof of ownership.
-
Feature Discovery - A mechanism that allows holders and issuers to discover each other's supported features, credential formats, and operations before initiating credential issuance. It ensures compatibility and prevents protocol mismatches.
-
Credential Metadata - An extensible data structure (
CredentialMeta) that allows holders to pass auxiliary information to issuers during the credential request process, such as personal details or context needed for constructing a credential.
Key Features #
- Implements the Verifiable Data Issuance Protocol (VDIP) for secure credential issuance.
- Implements DIDComm v2.1 protocol for secure, end-to-end encrypted communication.
- Support for feature discovery between issuers and holders.
- Support multiple credential format, such as W3C VC Data Model v1 & v2, JWT VC, and SD-JWT VC.
- Holder-bound assertions to prove control of specific DIDs via signed JWT tokens.
- Extensible credential metadata to pass auxiliary information during issuance.
- Automatic security verification, such as message wrapping validation and addressing consistency checks.
- Problem reporting mechanism for error handling.
Supported Credential Formats #
| Format | Description | Use Case |
|---|---|---|
w3cV1 |
W3C Verifiable Credentials Data Model 1.0 with JSON-LD and Data Integrity proofs | Standard JSON-LD credentials with linked data semantics |
w3cV2 |
W3C Verifiable Credentials Data Model 2.0 with enhanced proof options | Next-generation W3C credentials with improved proof mechanisms |
jwtVc |
JWT-encoded Verifiable Credential (JWS) | Compact JWT format for credentials, widely supported |
sdJwtVc |
Selective Disclosure JWT-based Verifiable Credential | Privacy-preserving credentials allowing selective claim disclosure |
VDIP Protocol Support #
The package implements the following VDIP message types:
| Message Type | Purpose | Direction |
|---|---|---|
discover-features/2.0/queries |
Query Supported Features | Holder → Issuer |
discover-features/2.0/disclose |
Disclose Supported Features | Issuer → Holder |
vdip/1.0/request-issuance |
Request Credential Issuance | Holder → Issuer |
vdip/1.0/issued-credential |
Deliver Issued Credential | Issuer → Holder |
report-problem/2.0/problem-report |
Report Errors or Warnings | Any → Any |
Detailed protocol specification can be found here.
Protocol Flow #
Basic Issuance Flow #
The basic VDIP credential issuance flow follows these steps:
-
Setup: Holder and Issuer initialise their DID managers and establish connections to a DIDComm mediator.
-
Feature Discovery:
- Holder sends a
querymessage to discover what credential formats and features the issuer supports. - Issuer responds with a
disclosemessage listing supported formats (e.g., W3C v1, JWT VC, SD-JWT VC).
- Holder sends a
-
Request Issuance:
- Holder sends a
request-issuancemessage specifying:proposal_id: References an external proposal or out-of-band offer.credential_format: Desired format (optional, Issuer may choose).credential_meta: Additional data needed to construct the credential (e.g., email, attributes).
- Holder sends a
-
Validation:
- Issuer validates the request.
- Issuer verifies holder-bound assertions if present.
- Issuer determines if it can fulfil the request.
-
Credential Construction:
- Issuer constructs the credential with verified claims.
- Issuer signs the credential using the appropriate proof mechanism for the format.
-
Credential Delivery:
- Issuer sends an
issued-credentialmessage containing the serialised credential.
- Issuer sends an
-
Claim Credential:
- Holder receives and processes the credential.
- Holder stores it locally and presents it to verifiers.
Holder-Bound Assertions #
In some scenarios, the credential should be issued to a DID different from the DIDComm message sender requesting credential issuance. For example:
- Holder users different DIDs for DIDComm and Verifiable Credentials
- A parent requesting a credential for their child.
- A service requesting a credential on behalf of a user.
- An agent acting for a principal.
VDIP supports this through holder-bound assertions:
-
The Holder specifies
holder_didin the request body – the DID that should receive the credential. -
The Holder includes a signed
assertionJWT proving control of that DID. -
The assertion JWT contains:
iss: Holder DID (issuer of the assertion).sub: Holder DID (subject of the assertion).aud: Issuer DID (audience).proposalId: Links to the credential request.exp: Expiration timestamp (prevents replay attacks).iat: Issuance timestamp.- Optional
challenge: Additional anti-replay binding.
-
The Issuer:
- Resolves the
holder_didto get its DID Document. - Verifies the assertion JWT signature.
- Validates that
submatchesholder_did. - Checks expiration and challenge (if used).
- Issues the credential to the verified
holder_did.
- Resolves the
The holder-bound assertions mechanism ensures cryptographic proof of DID ownership while securing credential issuance.
Context Switching #
Context switching is a feature in VDIP that allows the credential issuance flow to transition user from the DIDComm messaging channel to an alternative context, typically a web browser. This enables richer user interactions and integrations with third-party services during the credential issuance process.
What is Context Switching?
During a standard VDIP flow, all communication happens through DIDComm messages. However, some issuance scenarios require:
- User interactions: Collecting consent, displaying terms and conditions, or completing forms
- Third-party verification: Integrating with external services like Veriff, KYC providers, OAuth/OIDC providers, or biometric systems
- Enhanced authentication: Performing additional security checks before credential issuance
- Rich UI experiences: Presenting complex information or media that would be difficult in a pure messaging flow
Context switching allows the issuer to pause the DIDComm flow, redirect the holder to a browser-based interaction, and then resume the DIDComm flow once the interaction is complete.
When to Use Context Switching
Context switching is optional and decision-based. The issuer evaluates each credential request and decides whether to:
- Issue directly: If sufficient information is available and no additional verification is needed
- Trigger context switch: If additional user interaction or third-party verification is required
Use context switching when:
- Additional user verification or consent is required (e.g., email/phone verification, identity proofing)
- Integration with third-party verification services is needed (e.g., KYC/AML checks, government ID verification)
- Complex user interactions are necessary (e.g., reviewing legal terms, filling out detailed forms)
- Browser-based authentication is preferred (e.g., OAuth flows, OIDC integration)
- Real-time user decisions are needed (e.g., approval/denial of credential terms)
Skip context switching when:
- The credential can be issued immediately based on the request data
- All necessary verification was completed before the VDIP flow started
- The issuer has sufficient information and authorization to issue the credential directly
- Low-friction issuance is desired for simple credential types (e.g., membership cards, basic attestations)
How Context Switching Works
- Holder sends a credential request via DIDComm
- Issuer evaluates the request and decides if context switching is needed
- Issuer sends a
switch-contextmessage containing:- A browser URL for the holder to open
- A unique nonce to bind the browser session to the DIDComm conversation
- Optional context about what verification is needed
- Holder receives the switch context message and opens the URL in a browser (or embedded web view)
- User completes the browser-based interaction (verification, consent, form submission, etc.)
- Browser context sends the result back to the issuer (typically via HTTP callback)
- Issuer validates the browser interaction results
- Issuer resumes the DIDComm flow and issues the credential
- Holder receives the verifiable credential through the original DIDComm channel
Security Considerations
When implementing context switching:
- Nonce validation: The nonce from the switch context message must be included in subsequent credential requests to prove the holder completed the browser interaction
- Token expiration: Browser context URLs should use time-limited JWT tokens (default: 15 minutes)
- Thread binding: The browser session must be bound to the DIDComm thread ID to prevent session hijacking
- HTTPS: Browser interactions should always use HTTPS in production environments
- State validation: The issuer must validate that the browser interaction completed successfully before issuing the credential
For a complete working example with HTTP servers and browser verification, see example/BROWSER_CONTEXT_EXAMPLE.md.
Requirements #
- Dart SDK version ^3.6.0
Installation #
Run:
dart pub add affinidi_tdk_vdip
or manually, add the package into your pubspec.yaml file:
dependencies:
affinidi_tdk_vdip: ^<version_number>
and then run the command below to install the package:
dart pub get
Usage #
Below is a step-by-step example of secure credential issuance between an issuer and holder using the VDIP protocol. The example demonstrates feature discovery, credential request, and issuance.
1. Set up DID Managers and Mediator #
Both the issuer and holder need DID managers and a connection to a DIDComm mediator:
// Resolve the mediator's DID document
final mediatorDid = 'did:web:...'; // Your mediator's DID
final mediatorDidDocument = await UniversalDIDResolver.defaultResolver.resolveDid(
mediatorDid,
);
// Set up issuer's DID manager
final issuerKeyStore = InMemoryKeyStore();
final issuerWallet = PersistentWallet(issuerKeyStore);
final issuerDidManager = DidKeyManager(
wallet: issuerWallet,
store: InMemoryDidStore(),
);
final issuerKeyId = 'issuer-key-1';
await issuerWallet.generateKey(
keyId: issuerKeyId,
keyType: KeyType.p256,
);
await issuerDidManager.addVerificationMethod(issuerKeyId);
// Set up holder's DID manager
final holderKeyStore = InMemoryKeyStore();
final holderWallet = PersistentWallet(holderKeyStore);
final holderDidManager = DidKeyManager(
wallet: holderWallet,
store: InMemoryDidStore(),
);
final holderKeyId = 'holder-key-1';
await holderWallet.generateKey(
keyId: holderKeyId,
keyType: KeyType.p256,
);
await holderDidManager.addVerificationMethod(holderKeyId);
2. Initialise VDIP Issuer #
Create a VDIP issuer for issuing credentials:
final vdipIssuer = await VdipIssuer.init(
mediatorDidDocument: mediatorDidDocument,
didManager: issuerDidManager,
featureDisclosures: FeatureDiscoveryHelper.vdipIssuerDisclosures,
);
3. Initialise VDIP Holder #
Create a VDIP holder for requesting credentials:
final vdipHolder = await VdipHolder.init(
mediatorDidDocument: mediatorDidDocument,
didManager: holderDidManager,
featureDisclosures: FeatureDiscoveryHelper.vdipHolderDisclosures,
);
4. Feature Discovery #
The holder first queries the issuer to discover supported features:
await vdipHolder.queryIssuerFeatures(
issuerDid: (await issuerDidManager.getDidDocument()).id,
featureQueries: FeatureDiscoveryHelper.getFeatureQueriesByDisclosures(
FeatureDiscoveryHelper.vdipIssuerDisclosures,
),
);
// Holder listens for disclose messages
vdipHolder.listenForIncomingMessages(
onDiscloseMessage: (message) async {
print('Issuer supports: \${message.body}');
// Check if issuer supports required features
final body = DiscloseBody.fromJson(message.body!);
// Proceed with credential request if features are supported
},
onCredentialsIssuanceResponse: (/* ... */) {
// Handle credential receipt (see section 7)
},
);
// Issuer listens for feature queries and responds
vdipIssuer.listenForIncomingMessages(
onFeatureQuery: (message) async {
await vdipIssuer.disclose(queryMessage: message);
},
onRequestToIssueCredential: ({required message, holderDidFromAssertion, isAssertionValid}) async {
// Handle credential request (see section 6)
},
);
5. Request Credential (Basic) #
After feature discovery, the holder sends a credential request:
final requestMessage = await vdipHolder.requestCredential(
issuerDid: issuerDidManager.getDidDocument().id,
options: RequestCredentialsOptions(
proposalId: 'proposal-123',
credentialFormat: CredentialFormat.w3cV1,
credentialMeta: CredentialMeta(
data: {
'email': '[email protected]',
'name': 'Alice Holder',
},
),
comment: 'Requesting email verification credential',
),
);
6. Request Credential with Holder-Bound Assertion #
Request a credential for a specific DID with proof of control (optional):
// Get the signer for the holder DID
final holderSigner = await holderDidManager.getSigner(
holderDidManager.assertionMethod.first,
);
// Request credential with assertion
final requestWithAssertion = await vdipHolder.requestCredentialForHolder(
holderSigner.did,
issuerDid: issuerDidManager.getDidDocument().id,
assertionSigner: holderSigner,
options: RequestCredentialsOptions(
proposalId: 'proposal-456',
credentialFormat: CredentialFormat.jwtVc,
jsonWebSignatureAlgorithm: JsonWebSignatureAlgorithm.es256,
challenge: 'random-challenge-12345',
credentialMeta: CredentialMeta(
data: {'membershipLevel': 'premium'},
),
tokenExpiration: Duration(minutes: 5),
),
);
7. Issue Credential #
The issuer receives and validates requests, then issues a credential:
vdipIssuer.listenForIncomingMessages(
onFeatureQuery: (message) async {
// Respond to feature discovery
await vdipIssuer.disclose(queryMessage: message);
},
onRequestToIssueCredential: ({
required message,
holderDidFromAssertion,
isAssertionValid,
}) async {
// Parse the request
final body = VdipRequestIssuanceMessageBody.fromJson(message.body!);
// Validate assertion if present
if (holderDidFromAssertion != null) {
if (isAssertionValid != true) {
// Send problem report
return;
}
}
// Extract metadata
final email = body.credentialMeta?.data?['email'] as String?;
if (email == null) {
throw ArgumentError('Email is required');
}
// Determine credential subject DID
final subjectDid = holderDidFromAssertion ?? message.from!;
// Get issuer signer
final issuerSigner = await issuerDidManager.getSigner(
issuerDidManager.assertionMethod.first,
);
// Construct unsigned credential (W3C v1 example)
final unsignedVc = VcDataModelV1(
context: [
dmV1ContextUrl,
'https://example.com/contexts/EmailCredentialV1.jsonld',
],
credentialSchema: [
CredentialSchema(
id: Uri.parse('https://example.com/schemas/EmailCredentialV1.json'),
type: 'JsonSchemaValidator2018',
),
],
id: Uri.parse('urn:uuid:${const Uuid().v4()}'),
issuer: Issuer.uri(issuerSigner.did),
type: {'VerifiableCredential', 'EmailVerificationCredential'},
issuanceDate: DateTime.now().toUtc(),
credentialSubject: [
CredentialSubject.fromJson({
'id': subjectDid,
'email': email,
'verified': true,
}),
],
);
// Sign the credential
final suite = LdVcDm1Suite();
final issuedVc = await suite.issue(
unsignedData: unsignedVc,
proofGenerator: DataIntegrityEcdsaJcsGenerator(signer: issuerSigner),
);
// Send the issued credential
await vdipIssuer.sendIssuedCredentials(
holderDid: message.from!,
verifiableCredential: issuedVc,
comment: 'Email verification credential issued successfully',
);
},
onProblemReport: (message) {
print('Problem reported: ${message.body}');
},
);
8. Claim Credential #
The holder claims, receives, and processes issued credentials:
holder.listenForIncomingMessages(
onCredentialsIssuanceResponse: (message) {
// Parse the issued credential
final body = VdipIssuedCredentialBody.fromJson(message.body!);
print('Received credential in format: ${body.credentialFormat}');
print('Credential: ${body.credential}');
if (body.comment != null) {
print('Issuer comment: ${body.comment}');
}
// Store or process the credential
// For W3C v1 credentials:
if (body.credentialFormat == CredentialFormat.w3cV1) {
final credential = VcDataModelV1.fromJson(
jsonDecode(body.credential) as Map<String, dynamic>,
);
// Verify the credential
// Store in wallet
// Present to verifier
}
},
onProblemReport: (message) {
print('Problem reported: ${message.body}');
},
);
Message References #
Request Issuance Message #
DIDComm message to request credential issuance to the issuer.
Message Type: https://affinidi.com/didcomm/protocols/vdip/1.0/request-issuance
Body Fields (VdipRequestIssuanceMessageBody):
| Field | Type | Required | Description |
|---|---|---|---|
proposal_id |
String |
Yes | Identifier referencing a credential proposal or out-of-band offer. |
holder_did |
String? |
Conditional | DID to issue the credential to (requires assertion if specified). |
assertion |
String? |
Conditional | Signed JWT proving control of holder_did (required if holder_did is specified). |
challenge |
String? |
Optional | Anti-replay or session binding value |
credential_format |
String? |
Optional | Requested credential format (e.g., "w3cV1", "jwtVc", "sdJwtVc"). |
json_web_signature_algorithm |
String? |
Optional | JWS algorithm for JWT-based credentials (e.g., "ES256"). |
comment |
String? |
Optional | Human-readable note from the requester. |
credential_meta |
CredentialMeta? |
Optional | Arbitrary metadata to assist credential construction. |
Example:
{
"type": "https://affinidi.com/didcomm/protocols/vdip/1.0/request-issuance",
"id": "123e4567-e89b-12d3-a456-426614174000",
"from": "did:peer:holder123",
"to": ["did:peer:issuer456"],
"body": {
"proposal_id": "proposal-789",
"credential_format": "w3cV1",
"credential_meta": {
"data": {
"email": "[email protected]"
}
},
"comment": "Please issue my email credential"
}
}
Issued Credential Message #
DIDComm message containing the issued credential to the holder.
Message Type: https://affinidi.com/didcomm/protocols/vdip/1.0/issued-credential
Body Fields (VdipIssuedCredentialBody):
| Field | Type | Required | Description |
|---|---|---|---|
credential |
String |
Yes | Serialized credential payload (format depends on credential_format). |
credential_format |
CredentialFormat |
Yes | Format of the issued credential. |
comment |
String? |
Optional | Human-readable note from the issuer. |
Credential Formats:
w3cV1: JSON string of W3C VC Data Model 1.0 credential.w3cV2: JSON string of W3C VC Data Model 2.0 credential.jwtVc: JWT (JWS) string.sdJwtVc: SD-JWT string with selective disclosure.
Example:
{
"type": "https://affinidi.com/didcomm/protocols/vdip/1.0/issued-credential",
"id": "234e5678-e89b-12d3-a456-426614174111",
"from": "did:peer:issuer456",
"to": ["did:peer:holder123"],
"thid": "123e4567-e89b-12d3-a456-426614174000",
"body": {
"credential": "{\"@context\":[...],\"type\":[...],\"credentialSubject\":{...}}",
"credential_format": "w3cV1",
"comment": "Email verification credential issued"
}
}
Security Features #
Message Wrapping Verification #
Both VdipHolder and VdipIssuer enforce strict message wrapping requirements to prevent downgrade attacks:
Accepted Message Wrapping Types:
authcryptPlaintext– Authenticated encryption (recommended default).authcryptSignPlaintext– Authenticated encryption with signature (highest security).anoncryptSignPlaintext– Anonymous encryption with signature (sender anonymous to intermediaries).anoncryptAuthcryptPlaintext– Layered encryption (advanced scenarios).
These restrictions ensure that messages cannot be downgraded to plaintext or improperly wrapped formats. The library automatically validates the wrapping when unpacking messages using DidcommMessage.unpackToPlainTextMessage.
Why this matters: Without envelope validation, an attacker could strip encryption or signatures, exposing sensitive credential data or enabling message forgery.
Addressing Consistency #
The library enforces DIDComm v2 addressing consistency rules:
- The
fromfield in the plaintext message must match theskid(sender key ID) in the encryption layer - The
tofield must contain thekid(recipient key ID) in the encryption layer - For signed messages, the
fromfield must match the signer's key ID
These checks are performed automatically during message unpacking. If consistency checks fail, the message is rejected with an error.
Why this matters: Addressing consistency prevents message spoofing and ensures that the claimed sender actually encrypted/signed the message.
Assertion Verification #
When a Holder requests a credential with a holder-bound assertion, the Issuer performs comprehensive verification:
-
Signature Verification:
- Resolves the
holder_didto obtain its DID Document - Verifies the assertion JWT signature using the public key from the DID Document
- Resolves the
-
Claims Validation:
- Validates that
issmatchesholder_did - Validates that
submatchesholder_did - Validates that
audmatches the Issuer's DID - Checks that
proposalIdmatches the request
- Validates that
-
Expiration Check:
- Verifies that the current time is before the
exp(expiration) timestamp - Prevents replay of old assertions
- Verifies that the current time is before the
-
Challenge Validation (optional):
- If a challenge was provided, it validates its uniqueness and binding to the session
The VdipIssuer performs these checks automatically and provides the verification result via the isAssertionValid parameter in the onRequestToIssueCredential callback.
Why this matters: Assertion verification ensures that only the legitimate controller of a DID can request credentials for that DID, preventing credential issuance to unauthorized parties.
Problem Report Message #
VDIP uses DIDComm's standard problem-report mechanism to communicate errors and issues during the protocol flow.
When to send problem reports:
- Invalid or missing required fields in request
- Unsupported credential format requested
- Assertion verification failure
- Authorization failure
- Internal issuer errors
Problem Report Structure:
{
"type": "https://didcomm.org/report-problem/2.0/problem-report",
"id": "345e6789-e89b-12d3-a456-426614174222",
"pthid": "123e4567-e89b-12d3-a456-426614174000",
"body": {
"code": "e.p.vdip.invalid-request",
"comment": "The credential_meta.email field is required",
"args": ["email"]
}
}
Handling problem reports:
// In your message listener
onProblemReport: (message) {
final body = ProblemReportBody.fromJson(message.body!);
print('Error code: ${body.code}');
print('Description: ${body.comment}');
// Take appropriate action based on the error
switch (body.code.descriptors.first) {
case 'invalid-request':
// Fix request and retry
break;
case 'unsupported-format':
// Try different credential format
break;
default:
// Log and handle other errors
}
},
Complete Example #
See example/basic_example.dart for a complete runnable example that demonstrates:
- Feature discovery between holder and issuer
- Credential request workflows with and without holder-bound assertions
- Credential issuance in multiple formats (W3C v1, JWT VC, SD-JWT VC)
- Security verification and error handling
- Problem report messaging
Support & Feedback #
If you face any issues or have suggestions, please don't hesitate to contact us using this link.
Reporting Technical Issues #
If you have a technical issue with the DIDComm's codebase, you can also create an issue directly in GitHub.
- Ensure the bug was not already reported by searching on GitHub under Issues.
- If you're unable to find an open issue addressing the problem, open a new one. Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behaviour that is not occurring.
Contributing #
Want to contribute?
Head over to our CONTRIBUTING guidelines.