Kill the Clipboard - SMART Health Cards Library - v1.0.0
    Preparing search index...

    Kill the Clipboard - SMART Health Cards Library - v1.0.0

    Kill the Clipboard TypeScript Library

    Tests Badge

    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)

    • Follows SMART Health Cards Framework v1.4.0
    • JWS generation
    • File generation (.smart-health-card files)
    • QR code generation with optional chunking support
    • Decoding / Verification support

    SMART Health Links (SHL)

    • Implements SMART Health Links Specification v1.0.0 STU 1
    • SHL URI generation per SHL specification
    • SHLManifestBuilder for producing manifests with embedded and location file entries
    • SHLViewer to resolve SHLs, fetch manifests, and decrypt content
    • QR code generation
    • Passcode flow supported at the application level (see SHL demo)

    Great Developer Experience

    • TypeScript support with full type definitions
    • Comprehensive test suite
    • Proper error handling hierarchy
    • Built both for Node.js and browser environments

    Kill the Clipboard JavaScript / TypeScript library - SMART Health Links demo screenshot

    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/

    Kill the Clipboard JavaScript / TypeScript library - SMART Health Cards demo screenshot

    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:

    • Server issues an SHL (creates state, attributes an ID to the SHL, persists SHL builder data, hosts a manifest endpoint, and stores encrypted files)
    • Client resolves the SHL using SHLViewer, providing the recipient identifier and (if applicable) a passcode

    Server 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:

    • On the server side, during SHL creation:
      1. Create an SHL instance with SHL.generate({ baseManifestURL, manifestPath, expirationDate?, label?, flag? })
      2. Attribute a server-generated ID to the SHL (useful to identify the SHL and related models in the database)
      3. Use SHLManifestBuilder with implementations for uploadFile, getFileURL, and loadFile that persist encrypted files and return retrievable URLs
      4. Add content: addFHIRResource({ content, enableCompression? }), addHealthCard({ shc, enableCompression? })
      5. Persist the builder state via toDBAttrs()
      6. Return the SHL URI to clients via shl.toURI()
    • On the client side, during SHL resolution:
      1. Create a SHLViewer instance with the SHL URI
      2. Resolve the SHL using resolveSHL({ recipient, passcode?, embeddedLengthMax?, shcReaderConfig? })
    • On the server side, after client resolves the SHL:
      1. Implement a POST manifest endpoint at baseManifestURL + manifestPath
      2. On each manifest request, load the builder state from the database, reconstruct the builder with fromDBAttrs(), call buildManifest({ embeddedLengthMax? }), and return the manifest JSON

    Here'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,
    };
    • Secure backend only: Issue/sign SHCs on a secure backend; never expose ES256 private keys in browsers. SHLs are not signed; encrypt SHL files (JWE) and serve manifests/files from a secure backend.
    • No X.509 Certificate Validation: SHC verification uses a provided public key or the issuer's JWKS. X.509 certificate chain validation (x5c/PKI-based trust) is not implemented. If you require PKI-based trust, implement certificate validation out of band or extend the verification flow.
    • Untrusted Content: Treat SHC content as untrusted until signature verification succeeds, and treat SHL payloads/manifests as untrusted until decryption succeeds.
    • Passcode Security: The SHL P flag is server-side access control only. Passcodes are never part of encryption and are not present in payloads/manifests.
    • HTTPS Only: All SHL manifest and file URLs must be served over HTTPS to prevent interception.
    • Short-Lived file URLs: File location URLs should be short-lived and single-use to minimize exposure window.
    • No Logging: Never log encryption keys, passcodes, or decrypted content in server logs.
    • No Built-in Rate Limiting: Manifest endpoint rate limiting must be implemented separately.
    • Proper Database: Use a production database (PostgreSQL, MySQL, etc.) for manifest builder persistence.
    • Secure Passcode Storage: Implement proper password hashing with salt and pepper (Argon2 recommended).
    • Access Control: Implement proper authentication and authorization for manifest endpoints.
    • Rate Limiting: Add rate limiting and abuse protection for manifest and file endpoints.
    • Monitoring: Implement logging and monitoring while avoiding sensitive data exposure.
    • Backup Strategy: Ensure encrypted files and DB records are properly backed up.
    • Key Rotation: Plan for encryption key rotation and migration strategies.
    • No application/smart-api-access: SMART on FHIR API access tokens are not supported in content types.
    • No Built-in Automatic Refresh for SHLs with 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.

    alt text

    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