@welshare/sdk
v0.4.0
Published
Primitives, schemas, utilities for interacting with Welshare's sovereign health data sharing platform
Downloads
184
Maintainers
Readme
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/
Overview
Welshare API Client
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 givenapplicationIdfetchDelegation()- 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()- Convertswelshare://URLs to storage keys.
Environment configuration:
WELSHARE_API_ENVIRONMENT- Pre-configured environments (production,staging,preview,development)resolveEnvironment()- Resolve environment configuration with optional overridesWelshareApiEnvironment- Type definition for environment config
Authentication Tools
- JWT handling:
createJWTForStorageKeys(),verifyJWT() - Key derivation:
deriveKey()(with typesContext,KeyId,UserSecret) - Storage keys:
deriveApplicationKeypair(),deriveStorageKeypair(),verifyAuthProof()
FHIR Resources
- FHIR namespace (
Fhir):validateQuestionnaire()- Validate FHIR Questionnaire resourcesvalidateQuestionnaireResponse()- Validate FHIR QuestionnaireResponse resourceswrapFhirResourceForNillion()- Wrap FHIR resources for Nillion insertionfhirDateRegex,fhirDateTimeRegex,fhirBase64BinaryRegex- Validation regex patterns
- FHIR types:
FhirCoding,FhirCodeableConcept,FhirQuantity,FhirSimpleQuantity,FhirRange,FhirRatio,FhirPeriod,FhirAttachment,FhirSampledData - Standard codes (
StandardCodesnamespace) - 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 Integration
- Nillion namespace (
Nillion):Keypair- Keypair class from@nillion/nuc(this will be replaced with a signer interface soon)makeUserClient()- Create a SecretVault UserClientsummarizeUploadResults()- 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
General Utilities
- Base64 encoding/decoding:
base64urlEncode(),base64urlDecode(),base64urlDecodeUtf() - Hex utilities:
bytesToHex(),hexToBytes()(re-exported fromviem) - UUID generation:
makeUid()
Encryption Utilities
Browser-compatible AES-256-GCM encryption utilities for file handling:
generateRandomAESKey()- Generate a random 256-bit AES-GCM keyencryptFile(file, key)- Encrypt a file with an AES key, returns encrypted data and IVencodeEncryptionKey(key, iv)- Export key and IV to a storableEncryptionKeyformatdecrypt(encryptedData, encryptionKey)- Decrypt data using an encoded encryption keydecodeEncryptionKey(encryptionKey)- Decode anEncryptionKeyback 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";Validation Schemas
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
Types
- Session types:
SessionKeyAuthMessage,AuthorizedSessionProof,SessionKeyData - Validation types:
InsertableSubmission,InterpolateSocials,ValidatableSchema
Submitting Questionnaire Responses
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.
Quick Start
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.
Deriving User Keypairs with a Signer Function
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, Keypair } 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:key : ", Keypair.keypairKeyDid(keypair).didString);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)
Submitting a QuestionnaireResponse
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);Under the hood
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.
Binary File Uploads
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.
Quick Start
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.
Uploading binary files
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,
};Low-level upload (for more control)
If you need more control over the upload process, you can use the lower-level functions:
import { WelshareApi, resolveEnvironment, Keypair } 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.keypairKeyDid(keypair).didString, //Nillion 2.0 requires did:key as a method
url: `welshare://${uploadKey}`,
},
environment,
applicationId
);API Side Effects
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(): CallsfetchDelegation()internally, then writes to Nillion storage. Grants builder read/execute access to the uploaded file metadata.
Creating React Hooks for Binary Uploads
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);Schemas
Fhir Resources
as an example, we're storing a subset of Fhir HL7 R5 QuestionnaireResponses. See questionnaire-response.schema.json.
- A good json schema validation tester: https://www.jsonschemavalidator.net/
- more tools https://forgetoolbox.com/
- even more tools https://www.jsonforge.com/
