@welshare/sdk
    Preparing search index...

    @welshare/sdk

    Welshare shared library functions

    This SDK contains provides functions, types, schemas and utilities to interact with Welshare's sovereign health data sharing protocol. It includes FHIR resource handling, Nillion data storage abstractions, API clients and validation schemas.

    If you're just looking for an easy way to submit data from your own frontend, the @welshare/react package might be a better fit.

    Use this SDK if you want to build infrastructure to manage, authenticate and profile users yourself. The bare minimum requirement is to support a signer function that users use to deterministically derive storage keypairs from their control wallets.

    The package wraps Nillion's secretvaults / nuc / blindfold libraries. Some of which have server side dependencies (pino-pretty) that can create quirky side effects when running in browsers if not deliberately treeshaken. Make sure to not expose server facing features in this library (i.e. code that makes use of Nillion "standard" collections)

    We're using tshy to build ESM compatible outputs. CommonJS is not supported.

    You can find compiled API docs here: https://docs.welshare.app/sdk-docs/

    This Welshare barrel object export contains functions that talk to the Welshare backend infrastructure. While user data is stored in owned collections that conceptually aren't directly under our control, the application data (e.g. questionnaire definitions) is stored on Nillion collections that we have write control over.

    • WelshareApi:

      • fetchWelshareApp() - Fetches the AppRecord of a given applicationId
      • fetchDelegation() - users call this to request a delegation NUC that allows them to access to owned collections.
      • submitData() - A facade that helps submitting (questionnaire) data correctly. Fetches apps and delegations in the background, interpolates fields with client side knowledge, and pushes data to Nillion nodes.
      • submitBinaryData() - Stores encrypted binary file metadata in Nillion. See Binary File Uploads below.
      • fetchS3WriteDelegation() - Requests a presigned S3 URL for uploading encrypted files.
      • fetchS3ReadDelegation() - Requests a presigned S3 URL for downloading encrypted files.
      • resolveStorageKey() - Converts welshare:// URLs to storage keys.
    • Environment configuration:

      • WELSHARE_API_ENVIRONMENT - Pre-configured environments (production, staging, preview, development)
      • resolveEnvironment() - Resolve environment configuration with optional overrides
      • WelshareApiEnvironment - Type definition for environment config
    • JWT handling: createJWTForStorageKeys(), verifyJWT()
    • Key derivation: deriveKey() (with types Context, KeyId, UserSecret)
    • Storage keys: deriveApplicationKeypair(), deriveStorageKeypair(), verifyAuthProof()
    • FHIR namespace (Fhir):
      • validateQuestionnaire() - Validate FHIR Questionnaire resources
      • validateQuestionnaireResponse() - Validate FHIR QuestionnaireResponse resources
      • wrapFhirResourceForNillion() - Wrap FHIR resources for Nillion insertion
      • fhirDateRegex, fhirDateTimeRegex, fhirBase64BinaryRegex - Validation regex patterns
    • FHIR types: FhirCoding, FhirCodeableConcept, FhirQuantity, FhirSimpleQuantity, FhirRange, FhirRatio, FhirPeriod, FhirAttachment, FhirSampledData
    • Standard codes (StandardCodes namespace) - FHIR standard code definitions. This is VERY helpful to refer to commonly used health conditions and observations and they frequently are found in user facing questionnaires.
    • Nillion namespace (Nillion):
      • Keypair - Keypair class from @nillion/nuc (this will be replaced with a signer interface soon)
      • makeUserClient() - Create a SecretVault UserClient
      • summarizeUploadResults() - Processes an array of redundant "upload" result that's returned by cluster nodes to one single result
    • Nillion types: CollectionType, NillionClusterConfig, NillionByNodeName, NillionDid, NillionUuid, BlindfoldFactoryConfig
    • Base64 encoding/decoding: base64urlEncode(), base64urlDecode(), base64urlDecodeUtf()
    • Hex utilities: bytesToHex(), hexToBytes() (re-exported from viem)
    • UUID generation: makeUid()

    Browser-compatible AES-256-GCM encryption utilities for file handling:

    • generateRandomAESKey() - Generate a random 256-bit AES-GCM key
    • encryptFile(file, key) - Encrypt a file with an AES key, returns encrypted data and IV
    • encodeEncryptionKey(key, iv) - Export key and IV to a storable EncryptionKey format
    • decrypt(encryptedData, encryptionKey) - Decrypt data using an encoded encryption key
    • decodeEncryptionKey(encryptionKey) - Decode an EncryptionKey back to raw bytes
    • Types: EncryptionKey, Algorithm, ALGORITHM

    Note: Encryption utilities can be imported from the main package or from the lightweight /encryption entry point that has no Nillion dependencies:

    // Option 1: Import from main package (includes all SDK features)
    import { encryptFile, decrypt, type EncryptionKey } from "@welshare/sdk";

    // Option 2: Import from /encryption (lightweight, no Nillion deps)
    import {
    encryptFile,
    decrypt,
    type EncryptionKey,
    } from "@welshare/sdk/encryption";

    All schemas are exported as Zod schemas for validation:

    • AppSchema - Application configuration
    • QuestionnaireSchema - FHIR Questionnaire
    • QuestionnaireResponseSchema - FHIR QuestionnaireResponse
    • PatientSchema - root FHIR Patient data
    • ObservationSchema - FHIR Observation
    • ObservationHelpers - Helper functions for observations
    • ConditionSchema - FHIR Condition
    • ConditionHelpers - Helper functions for conditions
    • BinaryFilesSchema - Binary files
    • NotificationsSchema - Notifications
    • Session types: SessionKeyAuthMessage, AuthorizedSessionProof, SessionKeyData
    • Validation types: InsertableSubmission, InterpolateSocials, ValidatableSchema

    The submitData() function is the primary method for submitting questionnaire response data to Welshare. It handles fetching application configuration, requesting delegations, validating data, and storing it on Nillion.

    This assumes that your app helps your users to manage their keypairs, connects to wallets and signs messages. If you're looking for a more high level integration option, check out our dedicated React integration library: @welshare/react.

    Before users can submit data, they need to derive a dedicated storage keypair that's used to authenticate against the Nillion nodes without disclosing the controlling key pair. Welshare uses deterministic key derivation based on signed messages from the user's wallet.

    import { deriveStorageKeypair, Nillion } from "@welshare/sdk";
    import { createWalletClient, custom } from "viem";
    import { mainnet } from "viem/chains";

    // Connect to user's wallet (e.g., MetaMask)
    const walletClient = createWalletClient({
    chain: mainnet,
    transport: custom(window.ethereum),
    });

    const [userAddress] = await walletClient.getAddresses();

    // Define a signer function that requests signatures from the wallet
    const signMessageAsync = async (message: string): Promise<string> => {
    const signature = await walletClient.signMessage({
    account: userAddress,
    message,
    });
    return signature;
    };

    // Derive the storage keypair deterministically

    const keypair: Nillion.Keypair = await deriveStorageKeypair(
    signMessageAsync,
    userAddress
    );

    // The keypair can now be used for all Welshare operations
    console.log("User DID:", keypair.toDidString());

    Important Notes:

    • The keypair must not leave the client environment at any time!
    • The signer function must return a valid signature for the provided message
    • The same signer will always derive the same keypair
    • Users only need to sign once per session (you can cache the derived keypair)

    For this example to work, you must register an Application and a Questionnaire resource first. Use their ids in this example code.

    import { WelshareApi, resolveEnvironment } from "@welshare/sdk";

    // Using the keypair and applicationId from the previous example
    const environment = resolveEnvironment("production");

    // Prepare your FHIR QuestionnaireResponse
    const questionnaireResponse = {
    resourceType: "QuestionnaireResponse",
    status: "completed",
    questionnaire: "questionnaire-id",
    authored: new Date().toISOString(),
    item: [
    {
    linkId: "1",
    text: "What is your age?",
    answer: [
    {
    valueInteger: 35,
    },
    ],
    },
    {
    linkId: "2",
    text: "Do you have any allergies?",
    answer: [
    {
    valueBoolean: true,
    },
    ],
    },
    ],
    };

    // Submit the data
    const result = await WelshareApi.submitData(
    keypair,
    {
    collectionType: "QuestionnaireResponse",
    data: questionnaireResponse,
    },
    environment,
    applicationId
    );

    console.log("Submitted successfully:", result.insertedUid);

    submitData() orchestrates the entire submission process:

    • Fetches the application configuration via fetchWelshareApp()
    • Requests a delegation NUC via fetchDelegation()
    • Validates the data against the appropriate schema
    • Wraps the FHIR resource for Nillion storage
    • Writes to the user's owned collection on Nillion
    • Grants the builder application read access to the submitted data

    The function automatically handles client-side field interpolation and ensures data is correctly formatted for storage.

    The SDK provides functions to upload encrypted binary files (images, documents) to an S3 compatible storage bucket that's hosted by Welshare. Files are AES-256-GCM encrypted client-side before they're leaving the user's environment. Meta file information (mime types, sizes, file names) are stored on the BinaryFile collection on Nillion.

    The symmetric encryption keys are threshold encrypted an distributed across Nillion cluster nodes; a single node cannot recover a key. When anchoring a user owned binary file document on Nillion, we're also adding an ACL entry that allows us to decipher the data on behalf of the submitting application, but technically users could withhold that allowance or withdraw it at any time.

    This assumes that your app helps your users to manage their keypairs, connects to wallets and signs messages. If you're looking for a more high level integration option, check out our dedicated React integration library: @welshare/react.

    The simplest way to upload a file is using uploadAndEncryptFile(), which handles the entire flow (generates a new encryption key, encrypts, uploads, and stores metadata):

    import { WelshareApi } from "@welshare/sdk";

    // Upload a file with one function call
    const result = await WelshareApi.uploadAndEncryptFile(
    keypair,
    file, // File object from input or drag-drop
    {
    reference: `questionnaire/${questionnaireId}/photo`,
    applicationId: "my-app-id",
    },
    "production"
    );

    console.log("File uploaded:", result.insertedUid);
    console.log("File URL:", result.url);

    // Use in a FHIR QuestionnaireResponse attachment
    const attachment = {
    id: result.insertedUid,
    contentType: file.type,
    size: file.size,
    title: file.name,
    url: result.url,
    };

    If you need more control over the upload process, you can use the lower-level functions:

    import { WelshareApi, resolveEnvironment } from "@welshare/sdk";
    import {
    generateRandomAESKey,
    encryptFile,
    encodeEncryptionKey,
    } from "@welshare/sdk/encryption";

    const environment = resolveEnvironment("production");

    // 1. Get presigned URL for S3 upload
    const { presignedUrl, uploadKey } = await WelshareApi.fetchS3WriteDelegation(
    keypair,
    {
    reference: "questionnaire/abc/photo",
    fileName: file.name,
    fileType: file.type,
    },
    environment
    );

    // 2. Encrypt and upload file to S3
    const cryptoKey = await generateRandomAESKey();
    const { encryptedData, iv } = await encryptFile(file, cryptoKey);
    const encryptionKey = await encodeEncryptionKey(cryptoKey, iv);

    await fetch(presignedUrl, {
    method: "PUT",
    body: encryptedData,
    headers: { "Content-Type": file.type },
    });

    // 3. Store metadata on Nillion
    // This call doesn't disclose the key to a single server - it's threshold-encrypted
    // and distributed among cluster nodes.
    const { insertedUid } = await WelshareApi.submitBinaryData(
    keypair,
    {
    encryption_key: JSON.stringify(encryptionKey),
    reference: "questionnaire/abc/photo",
    file_name: file.name,
    file_size: file.size,
    file_type: file.type,
    controller_did: keypair.toDidString(),
    url: `welshare://${uploadKey}`,
    },
    environment,
    applicationId
    );
    • uploadAndEncryptFile(): Combines all steps below into one convenient function.
    • fetchS3WriteDelegation() / fetchS3ReadDelegation(): Makes HTTP requests to the Welshare API (/auth/delegate/storage). Requires a valid self-signed JWT. Returns short-lived presigned URLs (15 sec expiry).
    • submitBinaryData(): Calls fetchDelegation() internally, then writes to Nillion storage. Grants builder read/execute access to the uploaded file metadata.

    If you're building a React application that manages keypairs directly, you can create a custom hook using the SDK functions. Here's an example implementation:

    import { useCallback, useEffect, useMemo, useRef, useState } from "react";
    import {
    Nillion,
    WelshareApi,
    resolveEnvironment,
    type WelshareApiEnvironment,
    type WelshareEnvironmentName,
    } from "@welshare/sdk";
    import { decrypt, type EncryptionKey } from "@welshare/sdk/encryption";

    export interface UseBinaryUploadsOptions {
    keypair: Nillion.Keypair | null | undefined;
    environment: WelshareApiEnvironment | WelshareEnvironmentName;
    }

    export const useBinaryUploads = (options: UseBinaryUploadsOptions) => {
    const [isRunning, setIsRunning] = useState(false);
    const [error, setError] = useState<string | null>(null);
    const mountedRef = useRef(true);

    useEffect(() => {
    mountedRef.current = true;
    return () => {
    mountedRef.current = false;
    };
    }, []);

    const resolvedEnvironment = useMemo(
    () => resolveEnvironment(options.environment),
    [options.environment]
    );
    const { keypair } = options;

    const createUploadCredentials = useCallback(
    async (payload: {
    reference: string;
    fileName: string;
    fileType: string;
    }) => {
    if (!keypair) throw new Error("No keypair available");

    return WelshareApi.fetchS3WriteDelegation(
    keypair,
    payload,
    resolvedEnvironment
    );
    },
    [keypair, resolvedEnvironment]
    );

    const downloadAndDecryptFile = useCallback(
    async (documentId: string): Promise<File | undefined> => {
    if (!keypair) throw new Error("No keypair available");

    try {
    setIsRunning(true);
    setError(null);

    const { binaryFile, data } = await WelshareApi.fetchBinaryData(
    keypair,
    resolvedEnvironment,
    documentId
    );

    const encryptionKey: EncryptionKey = JSON.parse(
    binaryFile.encryption_key
    );
    const decryptedData = await decrypt(await data, encryptionKey);

    if (!decryptedData) throw new Error("Failed to decrypt file");

    return new File([decryptedData], binaryFile.file_name, {
    type: binaryFile.file_type,
    });
    } catch (err) {
    const errorMessage =
    err instanceof Error ? err.message : "Failed to download/decrypt";
    if (mountedRef.current) setError(errorMessage);
    return undefined;
    } finally {
    if (mountedRef.current) setIsRunning(false);
    }
    },
    [keypair, resolvedEnvironment]
    );

    return { createUploadCredentials, downloadAndDecryptFile, isRunning, error };
    };

    Usage:

    const { createUploadCredentials, downloadAndDecryptFile } = useBinaryUploads({
    keypair: storageKeyPair,
    environment: "production",
    });

    // Get presigned URL for upload
    const { presignedUrl, uploadKey } = await createUploadCredentials({
    reference: "questionnaire/abc/photo",
    fileName: file.name,
    fileType: file.type,
    });

    // Download and decrypt a file
    const decryptedFile = await downloadAndDecryptFile(documentId);

    as an example, we're storing a subset of Fhir HL7 R5 QuestionnaireResponses. See questionnaire-response.schema.json.