@verifiables/verifier-sdk
v0.2.0
Published
Framework-agnostic SDK to embed a verifier with QR code creation and API integration
Maintainers
Readme
@verifiables/verifier-sdk
A framework-agnostic SDK for embedding credential verification flows in web applications. Supports both OID4VP (remote/QR-based) and ISO 18013-5 Proximity (BLE-based in-person) verification.
Features
- OID4VP Flow: Remote verification via QR codes and wallet deep links
- Proximity Flow: In-person BLE verification per ISO 18013-5 (mDoc)
- API Client: Low-level client for interacting with the verifier backend
- QR Code Generation: Highly customizable QR code rendering with 40+ styling options
- Web Component: Drop-in
<verifier-qr>custom element for quick integration - TypeScript First: Full type safety with comprehensive type definitions
- Framework Agnostic: Works with any JavaScript framework or vanilla JS
Installation
npm install @verifiables/verifier-sdk
# or
pnpm add @verifiables/verifier-sdkQuick Start
Option 1: Web Component (Simplest)
<verifier-qr
api-url="https://your-verifier-api.com"
namespace="eu.europa.ec.eudi.pid.1"
attributes="given_name,family_name,age_over_18"
show-link
></verifier-qr>
<script type="module">
import { registerVerifierElement } from '@verifiables/verifier-sdk/component'
registerVerifierElement()
document.addEventListener('verifier:success', (event) => {
console.log('Verification succeeded:', event.detail)
})
</script>Option 2: Programmatic API (OID4VP)
import { createVerifierClient } from '@verifiables/verifier-sdk'
import { generateQRCode, createQRConfig } from '@verifiables/verifier-sdk/qr'
// 1. Create a client
const client = createVerifierClient({
apiUrl: 'https://your-verifier-api.com'
})
// 2. Create an authorization request
const response = await client.createAuthorizationRequest({
namespace: 'eu.europa.ec.eudi.pid.1',
attributes: ['given_name', 'family_name', 'age_over_18'],
version: 'draft18'
})
// 3. Render QR code
const canvas = document.getElementById('qr-canvas') as HTMLCanvasElement
const config = createQRConfig(response.url, { scale: 8, margin: 2 })
await generateQRCode(canvas, config)
// 4. Poll for completion
const status = await client.pollAuthorizationStatus(response.id, {
timeout: 180000, // 3 minutes
interval: 2000 // Check every 2 seconds
})
if (status.status === 'valid') {
console.log('Verification successful:', status.presentation)
}Option 3: Proximity Flow (BLE/In-Person)
For in-person verification using Web Bluetooth (ISO 18013-5):
import {
createMdocProximityReader,
MdocProximityReader,
type ProximityRequestConfig,
type ParsedDeviceEngagement,
} from '@verifiables/verifier-sdk/proximity'
// 1. Create a proximity reader
const reader = createMdocProximityReader({
apiClient: { baseUrl: 'https://your-verifier-api.com' },
logger: console, // Optional: for debug output
})
// 2. Check browser support
if (!MdocProximityReader.isSupported()) {
console.error('Web Bluetooth not supported')
}
// 3. Subscribe to state changes
reader.onStateChange((state) => {
console.log('Status:', state.status)
if (state.error) console.error('Error:', state.error)
if (state.result) console.log('Result:', state.result)
})
// 4. Parse device engagement from wallet's QR code
// (Use a QR scanner library like jsQR to scan the wallet's QR)
const qrData = 'mdoc:owBjMS4...' // QR code data starting with 'mdoc:'
const deviceEngagement = reader.parseDeviceEngagement(qrData)
// 5. Connect via Bluetooth
await reader.connect(deviceEngagement)
// 6. Request credentials
const result = await reader.requestCredentials({
docType: 'eu.europa.ec.eudi.pid.1',
namespace: 'eu.europa.ec.eudi.pid.1',
attributes: ['family_name', 'given_name', 'birth_date', 'issuing_country'],
})
if (result.status === 'valid') {
console.log('Verification successful:', result.presentation)
// Access data: result.presentation['eu.europa.ec.eudi.pid.1']['eu.europa.ec.eudi.pid.1']
}
// 7. Disconnect when done
reader.disconnect()Important: When using Vue/React with reactive state, use shallowRef (Vue) or avoid wrapping deviceEngagement in reactive wrappers, as the mDoc library uses private class fields that don't work with Proxy objects.
API Reference
Main Module (@verifiables/verifier-sdk)
createVerifierClient(config)
Creates a new verifier client instance.
import { createVerifierClient } from '@verifiables/verifier-sdk'
const client = createVerifierClient({
apiUrl: 'https://your-verifier-api.com',
timeout: 60000, // Optional: request timeout in ms (default: 60000)
headers: {} // Optional: custom headers
})VerifierClient
The main API client class.
createAuthorizationRequest(params?)
Creates a new authorization request with the verifier backend.
const response = await client.createAuthorizationRequest({
namespace: 'eu.europa.ec.eudi.pid.1', // Credential doctype
attributes: ['given_name', 'family_name'], // Requested attributes
version: 'draft18', // OID4VP version: 'v1' | 'draft18'
dcql_query: { ... }, // Optional: custom DCQL query (overrides namespace/attributes)
fin: false // Optional: whether this is a final request
})
// Returns: { url: string, id: string, version: 'v1' | 'draft18' }getAuthorizationStatus(requestId)
Gets the current status of an authorization request.
const status = await client.getAuthorizationStatus(response.id)
// Returns: AuthorizationStatus
// status.status: 'created' | 'claimed' | 'pending' | 'valid' | 'failed'
// status.verification: Record<string, VerificationCheck[]>
// status.presentation: Record<string, unknown>
// status.dcqlValidation: DcqlValidationResultpollAuthorizationStatus(requestId, options?)
Polls for authorization status until it reaches a target status or times out.
const status = await client.pollAuthorizationStatus(response.id, {
timeout: 120000, // Max wait time in ms (default: 120000)
interval: 2000, // Poll interval in ms (default: 2000)
targetStatuses: ['valid', 'failed'], // Statuses to stop polling on
signal: abortController.signal // Optional: AbortSignal for cancellation
})QR Module (@verifiables/verifier-sdk/qr)
createQRConfig(url, overrides?)
Creates a QR code configuration with sensible defaults.
import { createQRConfig } from '@verifiables/verifier-sdk/qr'
const config = createQRConfig('https://example.com/verify?id=123', {
scale: 10,
margin: 2,
darkColor: '#000000',
lightColor: '#ffffff',
pixelStyle: 'rounded'
})generateQRCode(canvas, config)
Renders a QR code to an HTML canvas element.
import { generateQRCode, createQRConfig } from '@verifiables/verifier-sdk/qr'
const canvas = document.getElementById('qr') as HTMLCanvasElement
const config = createQRConfig(url)
await generateQRCode(canvas, config)QRCodeConfig
Full configuration options for QR code generation:
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| text | string | - | The URL/text to encode |
| ecc | 'L' \| 'M' \| 'Q' \| 'H' | 'M' | Error correction level |
| margin | number \| MarginObject | 2 | Margin around QR code |
| scale | number | 20 | Size of each module/pixel |
| lightColor | string | '#ffffff' | Background color |
| darkColor | string | '#000000' | Foreground color |
| pixelStyle | PixelStyle | 'rounded' | Style for data pixels |
| markerStyle | PixelStyle \| 'auto' | 'rounded' | Style for position markers |
| markerShape | MarkerShape | 'square' | Shape of position markers |
| markerInnerShape | MarkerInnerShape \| 'auto' | 'auto' | Inner marker shape |
| rotate | 0 \| 90 \| 180 \| 270 | 0 | Rotation angle |
| invert | boolean | false | Invert colors |
| marginNoise | boolean | false | Add noise in margins |
| marginNoiseRate | number | 0.5 | Noise density |
| marginNoiseOpacity | number \| [number, number] | 1 | Noise opacity |
| effect | 'none' \| 'crystalize' \| 'liquidify' | 'none' | Visual effect |
| seed | number | 608496 | Random seed for consistent rendering |
Pixel Styles: 'square', 'rounded', 'dot', 'squircle', 'row', 'column'
Marker Shapes: 'square', 'circle', 'plus', 'box', 'octagon', 'random', 'tiny-plus'
Marker Inner Shapes: 'square', 'circle', 'plus', 'diamond', 'eye'
Component Module (@verifiables/verifier-sdk/component)
registerVerifierElement(tagName?)
Registers the <verifier-qr> custom element.
import { registerVerifierElement } from '@verifiables/verifier-sdk/component'
// Use default tag name
registerVerifierElement()
// Or use custom tag name
registerVerifierElement('my-verifier')<verifier-qr> Element
A custom HTML element for embedding verification flows.
Attributes:
| Attribute | Type | Default | Description |
|-----------|------|---------|-------------|
| api-url | string | required | Base URL of the verifier API |
| namespace | string | - | Credential namespace (e.g., eu.europa.ec.eudi.pid.1) |
| attributes | string | - | Comma-separated list of requested attributes |
| version | 'v1' \| 'draft18' | - | OID4VP protocol version |
| poll-interval | number | 3000 | Polling interval in milliseconds |
| auto-start | 'true' \| 'false' | 'true' | Auto-start verification on mount |
| show-link | boolean attr | - | Show "Open in wallet" link |
Events:
| Event | Detail | Description |
|-------|--------|-------------|
| verifier:status | AuthorizationStatus | Emitted when status changes |
| verifier:success | AuthorizationStatus | Emitted when verification succeeds |
| verifier:error | Error | Emitted when an error occurs |
Methods:
const element = document.querySelector('verifier-qr')
element.start() // Start the verification flow
element.stop() // Stop polling and cleanup
element.refresh() // Create new request and QR codeCSS Variables for Theming:
verifier-qr {
--verifier-font: system-ui, -apple-system, sans-serif;
--verifier-text: inherit;
--verifier-text-muted: #6b7280;
--verifier-success: #22c55e;
--verifier-error: #ef4444;
--verifier-warning: #f59e0b;
}Proximity Module (@verifiables/verifier-sdk/proximity)
The proximity module implements ISO 18013-5 mDoc credential verification over BLE (Bluetooth Low Energy) for in-person verification scenarios.
createMdocProximityReader(options)
Creates a new proximity reader instance.
import { createMdocProximityReader } from '@verifiables/verifier-sdk/proximity'
const reader = createMdocProximityReader({
apiClient: {
baseUrl: 'https://your-verifier-api.com', // Required: API endpoint for verification
},
logger: { // Optional: custom logger
log: (msg) => console.log(msg),
warn: (msg) => console.warn(msg),
error: (msg) => console.error(msg),
},
defaultMtu: 512, // Optional: BLE MTU size (default: negotiated)
})MdocProximityReader
The main class for proximity verification.
Static Methods
// Check if Web Bluetooth is supported in the current browser
MdocProximityReader.isSupported() // Returns: booleanInstance Methods
// Parse device engagement from wallet's QR code
// Input: string starting with 'mdoc:' or raw bytes
const deviceEngagement = reader.parseDeviceEngagement(qrData)
// Connect to wallet via BLE
await reader.connect(deviceEngagement)
// Request credentials from connected wallet
const result = await reader.requestCredentials({
docType: 'eu.europa.ec.eudi.pid.1',
namespace: 'eu.europa.ec.eudi.pid.1',
attributes: ['family_name', 'given_name', 'birth_date'],
})
// Get current state
const state = reader.getState()
// Subscribe to state changes (returns unsubscribe function)
const unsubscribe = reader.onStateChange((state) => {
console.log(state.status, state.error, state.result)
})
// Check connection status
reader.isConnected() // Returns: boolean
// Disconnect from wallet
reader.disconnect()Supported Document Types
The module includes predefined document types and their attributes:
import { DOC_TYPES, NAMESPACES, DOCUMENT_DEFINITIONS } from '@verifiables/verifier-sdk/proximity'
// Document types
DOC_TYPES.EU_PID // 'eu.europa.ec.eudi.pid.1'
DOC_TYPES.MDL // 'org.iso.18013.5.1.mDL'
DOC_TYPES.AGE_VERIFICATION // 'eu.europa.ec.av.1'
DOC_TYPES.PHOTO_ID // 'org.iso.23220.photoID.1'
DOC_TYPES.MVC // 'org.iso.7367.1.mVC' (Vehicle Registration)
// Namespaces
NAMESPACES.EU_PID // 'eu.europa.ec.eudi.pid.1'
NAMESPACES.MDL // 'org.iso.18013.5.1'
// Full document definitions with attributes
DOCUMENT_DEFINITIONS // Array of DocumentDefinition with localized labelsBrowser Support
Web Bluetooth is required for proximity verification. Supported browsers:
- Chrome (desktop and Android)
- Edge (desktop)
- Opera (desktop)
Not supported: Safari, Firefox, iOS browsers (due to Web Bluetooth restrictions).
Types
Core Types
// OID4VP protocol version
type Oid4VpVersion = 'v1' | 'draft18'
// Authorization request parameters
interface AuthorizationRequestParams {
namespace?: string // Credential doctype
attributes?: string[] // Requested attributes
version?: Oid4VpVersion // Protocol version
dcql_query?: DcqlQuery // Custom DCQL query
fin?: boolean // Final request flag
}
// Authorization request response
interface AuthorizationRequestResponse {
url: string // URL to encode in QR code
id: string // Request ID for polling
version: Oid4VpVersion // Protocol version used
}
// Authorization status
interface AuthorizationStatus {
status: 'created' | 'claimed' | 'pending' | 'valid' | 'failed'
verification?: Record<string, VerificationCheck[]>
presentation?: Record<string, unknown>
dcqlValidation?: DcqlValidationResult
}
// Polling options
interface PollOptions {
timeout?: number // Max wait time (default: 120000)
interval?: number // Poll interval (default: 2000)
targetStatuses?: AuthorizationStatus['status'][] // Statuses to wait for
signal?: AbortSignal // For cancellation
}DCQL Types
// DCQL claim definition
interface DcqlClaim {
path: [string, string]
intent_to_retain?: boolean
}
// DCQL credential definition
interface DcqlCredential {
id: string
format: 'mso_mdoc'
meta?: { doctype_value?: string }
claims?: DcqlClaim[]
}
// DCQL query
interface DcqlQuery {
credentials: DcqlCredential[]
}Proximity Types
// Configuration for proximity credential requests
interface ProximityRequestConfig {
docType: string // e.g., 'eu.europa.ec.eudi.pid.1'
namespace: string // e.g., 'eu.europa.ec.eudi.pid.1'
attributes: string[] // e.g., ['family_name', 'given_name', 'birth_date']
}
// Parsed device engagement from wallet's QR code
interface ParsedDeviceEngagement {
version: string // Protocol version (e.g., '1.0')
bleServiceUuid?: string // BLE service UUID for connection
deviceEngagement: DeviceEngagement // Raw device engagement object
rawBytes: Uint8Array // Original CBOR-encoded bytes
}
// Reader state for tracking connection status
interface ReaderState {
status: 'disconnected' | 'connecting' | 'connected' | 'requesting' | 'processing' | 'complete'
deviceEngagement?: ParsedDeviceEngagement
error?: string
result?: ProximityVerificationResult
}
// Verification result from proximity flow
interface ProximityVerificationResult {
status: 'valid' | 'failed'
verification: VerificationChecks // Grouped verification checks
presentation: Record<string, Record<string, Record<string, unknown>>>
error?: string
}
// Verification check details
interface VerificationCheck {
status: string // 'passed', 'failed', etc.
check: string // Description of the check
}
// Logger interface for debug output
interface Logger {
log: (message: string) => void
warn: (message: string) => void
error: (message: string) => void
}
// Document definition with localized labels
interface DocumentDefinition {
docType: string
namespace: string
display: { lang: string; label: string }[]
attributes: DocumentAttribute[]
}
interface DocumentAttribute {
id: string
display: { lang: string; label: string }[]
}Advanced Usage
Cancelling Polling with AbortController
const abortController = new AbortController()
// Start polling
const statusPromise = client.pollAuthorizationStatus(requestId, {
signal: abortController.signal
})
// Cancel after 30 seconds
setTimeout(() => abortController.abort(), 30000)
try {
const status = await statusPromise
} catch (error) {
if (error.message === 'Polling aborted') {
console.log('User cancelled verification')
}
}Custom DCQL Query
const response = await client.createAuthorizationRequest({
dcql_query: {
credentials: [{
id: 'pid',
format: 'mso_mdoc',
meta: { doctype_value: 'eu.europa.ec.eudi.pid.1' },
claims: [
{ path: ['eu.europa.ec.eudi.pid.1', 'given_name'] },
{ path: ['eu.europa.ec.eudi.pid.1', 'family_name'] },
{ path: ['eu.europa.ec.eudi.pid.1', 'age_over_18'], intent_to_retain: true }
]
}]
}
})Extracting Presented Credentials
const status = await client.pollAuthorizationStatus(requestId)
if (status.status === 'valid' && status.presentation) {
// Access presented credential data
const pid = status.presentation['eu.europa.ec.eudi.pid.1'] as Record<string, unknown>
const givenName = pid?.given_name
const familyName = pid?.family_name
const isOver18 = pid?.age_over_18
}Styled QR Code with Custom Colors
import { generateQRCode, createQRConfig } from '@verifiables/verifier-sdk/qr'
const config = createQRConfig(url, {
scale: 12,
margin: 3,
darkColor: '#1e40af', // Blue
lightColor: '#f0f9ff', // Light blue background
pixelStyle: 'dot',
markerShape: 'circle',
markerInnerShape: 'circle',
marginNoise: true,
marginNoiseRate: 0.3,
marginNoiseOpacity: 0.5
})
await generateQRCode(canvas, config)Dependencies
Core:
- ky - Modern HTTP client
- seedrandom - Deterministic random number generation
- uqr - QR code encoding
Proximity module:
- @animo-id/mdoc - ISO 18013-5 mDoc/CBOR/COSE handling
License
MIT
