JavaScript/TypeScript universal (browser and node) library to generate QR codes containing medical records for patients to share with providers. Implements both the SMART Health Cards Framework and SMART Health Links Specification for handling FHIR-based medical records, enabling patients to "Kill the Clipboard" by sharing health data via secure, verifiable QR codes.
This aligns with the CMS Interoperability Framework call to action for Patient Facing Apps to "Kill the Clipboard":
We pledge to empower patients to retrieve their health records from CMS Aligned Networks or personal health record apps and share them with providers via QR codes or SMART Health Cards/Links using FHIR bundles. When possible, we will return visit records to patients in the same format. We commit to seamless, secure data exchange—eliminating the need for patients to repeatedly recall and write out their medical history. We are committed to "kill the clipboard," one encounter at a time.
This is a recently released library. While it's well tested, verify the functionality before using in production. Please report any issues or suggestions to the GitHub Issues page. For sensitive security reports, please contact us at contact at vinta.com.br
SMART Health Cards (SHC)
SMART Health Links (SHL)
SHLManifestBuilder
for producing manifests with embedded and location file entriesSHLViewer
to resolve SHLs, fetch manifests, and decrypt contentGreat Developer Experience
Demo for SMART Health Link generation and view. Supports sharing International Patient Summary (IPS) FHIR data: allergies, medications, vitals, etc. via a passcode-protected SMART Health Link: https://kill-the-clipboard.vercel.app/
Interactive browser demo that showcases QR code generation and camera-based scanning: https://vintasoftware.github.io/kill-the-clipboard/demo/shc/
npm install kill-the-clipboard
# or
pnpm add kill-the-clipboard
# or
yarn add kill-the-clipboard
Security warning: Issue/sign on a secure backend only; never expose the ES256 private key in browsers.
Use SHCIssuer
on the server. Use SHCReader
in the browser or server to verify and render QR.
Typical flow: client sends FHIR Bundle → server returns JWS/.smart-health-card or QR data.
import { SHCIssuer, SHCReader } from 'kill-the-clipboard';
// Configure issuer with your details and ES256 key pair
// SECURITY: Use issuer on a secure backend only; never include privateKey in client-side code.
const issuer = new SHCIssuer({
issuer: 'https://your-healthcare-org.com',
privateKey: privateKeyPKCS8String, // ES256 private key in PKCS#8 format
publicKey: publicKeySPKIString, // ES256 public key in SPKI format
});
// Configure reader for verification (only needs public key)
const reader = new SHCReader({
publicKey: publicKeySPKIString, // ES256 public key in SPKI format
});
// Create FHIR Bundle
const fhirBundle = {
resourceType: 'Bundle',
type: 'collection',
entry: [
{
fullUrl: 'https://example.org/fhir/Patient/123',
resource: {
resourceType: 'Patient',
id: '123',
name: [{ family: 'Doe', given: ['John'] }],
birthDate: '1990-01-01',
},
},
{
fullUrl: 'https://example.org/fhir/Immunization/456',
resource: {
resourceType: 'Immunization',
id: '456',
status: 'completed',
vaccineCode: {
coding: [{
system: 'http://hl7.org/fhir/sid/cvx',
code: '207',
display: 'COVID-19 vaccine',
}],
},
patient: { reference: 'Patient/123' },
occurrenceDateTime: '2023-01-15',
},
},
],
};
// Issue a new SMART Health Card
const healthCard = await issuer.issue(fhirBundle);
console.log('Health Card JWS:', healthCard.asJWS());
// Generate QR codes
const qrCodes = await healthCard.asQR();
console.log('QR code data URL:', qrCodes[0]);
// Generate numeric QR codes (shc:/ prefixed strings)
const qrNumericStrings = healthCard.asQRNumeric();
console.log('Numeric QR code:', qrNumericStrings[0]);
// Create downloadable .smart-health-card file
const blob = await healthCard.asFileBlob();
console.log('File blob created, type:', blob.type);
// Verify and read the health card
const verifiedHealthCard = await reader.fromJWS(healthCard.asJWS());
const verifiedBundle = await verifiedHealthCard.asBundle();
console.log('Verified FHIR Bundle:', verifiedBundle);
// Read from file content
const fileContent = await healthCard.asFileContent();
const healthCardFromFile = await reader.fromFileContent(fileContent);
console.log('Bundle from file:', await healthCardFromFile.asBundle());
// Read from QR numeric data (simulate scanning QR codes)
const healthCardFromQR = await reader.fromQRNumeric(qrNumericStrings);
console.log('Bundle from QR:', await healthCardFromQR.asBundle());
SHLs enable encrypted, link-based sharing of health information. The flow involves:
SHLViewer
, providing the recipient identifier and (if applicable) a passcodeServer responsibilities are required for a functional SHL implementation. Refer to the demo code in demo/shl/
for a full working example (manifest endpoint, storage handlers, and passcode handling). In summary, the SHL generation and resolution flow involves:
SHL
instance with SHL.generate({ baseManifestURL, manifestPath, expirationDate?, label?, flag? })
SHLManifestBuilder
with implementations for uploadFile
, getFileURL
, and loadFile
that persist encrypted files and return retrievable URLsaddFHIRResource({ content, enableCompression? })
, addHealthCard({ shc, enableCompression? })
toDBAttrs()
shl.toURI()
SHLViewer
instance with the SHL URIresolveSHL({ recipient, passcode?, embeddedLengthMax?, shcReaderConfig? })
baseManifestURL + manifestPath
fromDBAttrs()
, call buildManifest({ embeddedLengthMax? })
, and return the manifest JSONHere's a condensed single-file example demonstrating the complete SHL workflow from creation to resolution:
import {
SHL,
SHLManifestBuilder,
SHLViewer
} from 'kill-the-clipboard';
// Mock storage for this example
const uploadedFiles = new Map<string, string>();
const uploadFile = async (content: string) => {
const path = `file-${uploadedFiles.size + 1}`;
uploadedFiles.set(path, content);
return path;
};
const getFileURL = async (path: string) => `https://files.example.org/${path}`;
const loadFile = async (path: string) => uploadedFiles.get(path);
// 1. [Server side] Generate an SHL
const shl = SHL.generate({
id: 'some-uuid', // Optional: server-generated ID for database
baseManifestURL: 'https://shl.example.org/manifests/',
manifestPath: '/manifest.json',
label: 'Complete Test Card',
});
// 2. [Server side] Create manifest builder with mocked file handling
const builder = new SHLManifestBuilder({
shl,
uploadFile: uploadFile,
getFileURL: getFileURL,
loadFile: loadFile,
});
// 3. [Server side] Add a FHIR bundle as content
const fhirBundle = {
resourceType: 'Bundle',
type: 'collection',
entry: [
{
fullUrl: 'https://example.org/fhir/Patient/123',
resource: {
resourceType: 'Patient',
id: '123',
name: [{ family: 'Doe', given: ['John'] }],
birthDate: '1990-01-01',
},
},
],
};
await builder.addFHIRResource({ content: fhirBundle });
// 4. [Server side] Persist the manifest builder state in the database
const builderAttrs = builder.toDBAttrs();
await storeBuilderAttrs('some-uuid', builderAttrs); // Implement your own function
// 5. [Server side] Generate the SHL URI for sharing
const shlinkURI = shl.toURI();
console.log('Share this SHL:', `https://viewer.example/#${shlinkURI}`);
// 6. [Server side] Implement manifest and file serving
const fetchImpl = async (url: string, init?: RequestInit) => {
// Handle manifest requests
if (init?.method === 'POST' && url === shl.url) {
// Load the builder state from the database
const builderAttrs = await loadBuilderAttrs('some-uuid'); // Implement your own function
// Reconstruct the builder
const builder = SHLManifestBuilder.fromDBAttrs({
attrs: builderAttrs,
uploadFile: uploadFile,
getFileURL: getFileURL,
loadFile: loadFile,
});
// Build the manifest
const body = JSON.parse(init.body);
const manifest = await builder.buildManifest({
embeddedLengthMax: body.embeddedLengthMax,
});
return {
ok: true,
status: 200,
text: async () => JSON.stringify(manifest),
};
}
// Handle file requests (could be an S3-like storage instead)
const fileId = url.split('/').pop();
if (init?.method === 'GET' && fileId && uploadedFiles.has(fileId)) {
return {
ok: true,
status: 200,
text: async () => uploadedFiles.get(fileId),
};
}
return { ok: false, status: 404, text: async () => '' };
};
// 7. Use SHLViewer to resolve the SHL (client-side)
const viewer = new SHLViewer({
shlinkURI: `https://viewer.example/#${shlinkURI}`,
// Note: No need to pass fetch implementation if you have a real server-side implementation:
fetch: fetchImpl
});
const resolved = await viewer.resolveSHL({
recipient: 'alice@example.org',
embeddedLengthMax: 4096 // Files smaller than this will be embedded
});
// 8. Access the resolved content
console.log('Resolved FHIR resources:', resolved.fhirResources);
This example above demonstrates the complete lifecycle: SHL generation, content addition, manifest builder persistence in server-side database, manifest serving, and client-side resolution. In a real application, you would implement persistence for the manifest builder state and serve the manifest endpoint from your backend server.
The library provides granular error handling with specific error codes for different failure scenarios. Check each method documentation for the specific errors that can be thrown.
Available at https://vintasoftware.github.io/kill-the-clipboard/.
To generate and view the full API documentation locally:
# Generate documentation
pnpm docs:build
# The documentation will be generated in the ./docs directory
# Open docs/index.html in your browser to view the complete API reference
import {
SHCIssuer,
SHCReader,
FHIRBundleProcessor,
VerifiableCredentialProcessor,
JWSProcessor,
QRCodeGenerator
} from 'kill-the-clipboard';
// High-level API with SHC object
const issuer = new SHCIssuer(config);
const healthCard = await issuer.issue(fhirBundle, {
includeAdditionalTypes: ['https://smarthealth.cards#covid19'],
});
// SHC provides various output formats
const qrCodes = await healthCard.asQR({
enableChunking: false,
encodeOptions: {
errorCorrectionLevel: 'L',
scale: 4,
},
});
const bundle = await healthCard.asBundle({ optimizeForQR: true, strictReferences: true });
const fileContent = await healthCard.asFileContent();
// Use individual processors for more control
const bundleProcessor = new FHIRBundleProcessor();
const vcProcessor = new VerifiableCredentialProcessor();
const jwsProcessor = new JWSProcessor();
// Process FHIR Bundle (standard processing)
const processedBundle = bundleProcessor.process(fhirBundle);
bundleProcessor.validate(processedBundle);
// Or process with QR code optimizations (shorter resource references, removes unnecessary fields)
// Use 'strictReferences' option. When true, missing references throw an error.
// When false, original references are preserved if target resource is not found in bundle.
const optimizedBundle = bundleProcessor.processForQR(fhirBundle, { strictReferences: true });
bundleProcessor.validate(optimizedBundle);
// Create Verifiable Credential
const vc = vcProcessor.create(processedBundle, {
fhirVersion: '4.0.1',
includeAdditionalTypes: [
'https://smarthealth.cards#immunization',
'https://smarthealth.cards#covid19',
],
});
// Create JWT payload
const jwtPayload = {
iss: 'https://your-org.com',
nbf: Math.floor(Date.now() / 1000),
vc: vc.vc,
};
// Sign to create JWS
const jws = await jwsProcessor.sign(jwtPayload, privateKey, publicKey, {
enableCompression: true, // Enable compression per SMART Health Cards spec
});
// Verify JWS
const verified = await jwsProcessor.verify(jws, publicKey);
// Generate QR codes
const qrGenerator = new QRCodeGenerator({
// maxSingleQRSize auto-derived from errorCorrectionLevel if not specified:
// L: 1195, M: 927, Q: 670, H: 519 (V22 limits from SMART Health Cards QR FAQ)
// See: https://github.com/smart-on-fhir/health-cards/blob/main/FAQ/qr.md
enableChunking: false, // Use single QR mode (recommended)
encodeOptions: {
errorCorrectionLevel: 'L', // L per SMART Health Cards spec
scale: 4, // QR code scale factor
margin: 1, // Quiet zone size
color: { dark: '#000000ff', light: '#ffffffff' },
// Optional: version, maskPattern, width
}
});
const qrDataUrls = await qrGenerator.generateQR(jws);
console.log('Generated QR code data URL:', qrDataUrls[0]);
// Scan QR codes (simulate scanning with numeric data)
const scannedData = ['shc:/56762959532654603460292540772804336028702864716745...'];
const reconstructedJWS = await qrGenerator.decodeQR(scannedData);
// Manual JWS chunking and numeric conversion
const chunks = qrGenerator.chunkJWS(jws); // Returns array of shc:/ prefixed strings
const numericData = qrGenerator.encodeJWSToNumeric(jws);
const decodedJWS = qrGenerator.decodeNumericToJWS(numericData);
import { SHCIssuer, SHCReader } from 'kill-the-clipboard';
const issuer = new SHCIssuer(config);
const reader = new SHCReader({ publicKey: config.publicKey });
// Issue a health card
const healthCard = await issuer.issue(fhirBundle);
// Create SMART Health Card file content (JSON wrapper with verifiableCredential array)
const fileContent = await healthCard.asFileContent();
console.log('File content:', fileContent); // JSON string with { verifiableCredential: [jws] }
// Create downloadable Blob (browser-compatible)
const blob = await healthCard.asFileBlob();
console.log('Blob type:', blob.type); // 'application/smart-health-card'
// Trigger download in browser (example implementation)
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'vaccination-card.smart-health-card';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
// Verify health card from file content
const verifiedFromFile = await reader.fromFileContent(fileContent);
console.log('Valid health card bundle:', await verifiedFromFile.asBundle());
// Verify health card from Blob (e.g., from file input)
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', async (event) => {
const file = event.target.files[0];
if (file && file.name.endsWith('.smart-health-card')) {
try {
const verified = await reader.fromFileContent(file);
console.log('Valid health card:', await verified.asBundle());
// Or get QR codes from the verified health card
const qrCodes = await verified.asQR();
console.log('QR code data URL:', qrCodes[0]);
} catch (error) {
console.error('Invalid health card file:', error.message);
}
}
});
// Generate ES256 key pair for testing (Node.js 18+)
import crypto from 'crypto';
import { exportPKCS8, exportSPKI } from 'jose';
// Requires Node.js 18+ for crypto.webcrypto.subtle
const { publicKey, privateKey } = await crypto.webcrypto.subtle.generateKey(
{ name: 'ECDSA', namedCurve: 'P-256' },
true,
['sign', 'verify']
);
const privateKeyPKCS8 = await exportPKCS8(privateKey);
const publicKeySPKI = await exportSPKI(publicKey);
// Use these keys in SHCIssuer config
const config = {
issuer: 'https://your-org.com',
privateKey: privateKeyPKCS8,
publicKey: publicKeySPKI,
};
P
flag is server-side access control only. Passcodes are never part of encryption and are not present in payloads/manifests.application/smart-api-access
: SMART on FHIR API access tokens are not supported in content types.L
Flag: Long-term SHLs (L
flag) require manual implementation of polling or push notifications for updates.See CONTRIBUTING.md for development setup and guidelines.
MIT License - see LICENSE file for details.
This project is maintained by Vinta Software. We offer design and development services for healthcare companies. If you need any commercial support, feel free to get in touch: contact@vinta.com.br