gdc-sdk-client-ts
v1.0.4
Published
This SDK provides a platform-agnostic, secure interface for interacting with the backend services. It encapsulates the complexity of cryptographic operations, secure storage, and the two-phase authentication model required by the API.
Readme
Client SDK (client-sdk-ts)
This SDK provides a platform-agnostic, secure interface for interacting with the backend services. It encapsulates the complexity of cryptographic operations, secure storage, and the two-phase authentication model required by the API.
Core Architectural Concepts
Data Integrity & Conflict Resolution Model
The SDK uses a sophisticated model to ensure data integrity and manage versioning, especially in scenarios where a draft might be edited on multiple devices before being submitted.
job.id/job.content.jti(Stable Job/Draft ID):- Purpose: This is the persistent, primary identifier for the entire job or "draft."
- Implementation: It's a
UUIDv4, created once when the draft is first initiated. - Behavior: It never changes, even when the content is edited. It serves as the primary key in the local
JobManagervault and is the public-facing identifier used for anti-replay protection, as required by FAPI.
job.versionId/job.content.id(Ephemeral Content Version ID):- Purpose: This is a verifiable "fingerprint" of the current version of the draft's content.
- Implementation: It is a
SHAhash of the canonicalizedcontentobject (withmetaandidfields excluded). - Behavior: It is recalculated and changes every time the draft is saved. Its sole purpose is internal version tracking and integrity verification. Crucially, this
idfield within the payload is removed before the message is sent, as the digital signature covers the integrity of the sent content.
job.sequence(Ordering Timestamp):- Purpose: To provide a definitive order for different versions of a draft.
- Implementation: An epoch timestamp (number) generated whenever a draft is saved.
- Behavior: This is the primary mechanism for conflict resolution. The version with the highest
sequencenumber is considered the most recent.
job.previousSequence(Version History Link):- Purpose: To create a linked list or "change history" of a draft's versions.
- Implementation: When a draft is updated, the
sequencenumber of the previous version is stored in this field. - Behavior: This allows the system to trace the history of edits. It is the key to detecting uncoordinated simultaneous edits. If a device tries to sync a version whose
previousSequencedoes not match thesequenceof the current latest version on the server, a conflict has occurred. The default strategy is last-write-wins (based on the highestsequence), but this history makes more complex merging strategies possible in the future.
User vs. Device Identity
1. Host vs. Gateway Providers
The SDK's data model and service resolution are built on a clear distinction between two entity types:
- Host Provider: The central infrastructure provider. It is responsible for onboarding new organizations. Its DID Document contains
registryservices, such ascreateOrganizationandconfirmOrder. All onboarding operations are directed at the Host's DID. - Gateway Provider (Tenant): An individual organization that has been onboarded. Its DID Document contains
entityandindividualservices for day-to-day operations (e.g.,createEmployee,activateDevice). Once onboarded, an organization's services are resolved against its own DID.
2. The roleRegistry as the Single Source of Truth
The SDK is designed to be highly scalable and maintainable by centralizing all business logic for role capabilities into one place: client-sdk-ts/src/roleRegistry.ts.
roleRegistry.tsdefines which services (e.g.,PhysicianService) and capabilities (e.g.,MedicationService) are available to a user based on their ISCO-08 role code.capabilityMapper.tsis a pure, agnostic engine that reads this registry at runtime. It dynamically assembles and injects the correct services when a user session is created.- To add or modify a role's capabilities, a developer only needs to edit the
roleRegistry.tsfile. The rest of the system adapts automatically.
The SDK's security model is built on two distinct layers of identity and authentication, which are managed automatically.
Service-Layer Architecture: Hierarchical & Composable Capability Services
Fundamental Principle
The API must reflect a user's real-world capabilities, which are a combination of their role, their context, and their authorizations. The architecture is built on two core principles: Inheritance for "is-a" relationships (a Physician is-a Paramedic in terms of skills) and Composition for "has-a" capabilities (a Physician has-a medication administration capability).
Core Concepts
- Context: The environment of the interaction: Organization or Family.
- Role: The user's specific professional function, aligned with an ISCO-08 code (e.g.,
Physician,Paramedic). - Shared Capability: A function that multiple roles can perform, managed by a dedicated Capability Service (e.g.,
MedicationService). - Explicit Authorization Principle: Significant actions must be preceded by a signed, verifiable authorization object (e.g., a
MedicationRequestsigned as a JWS). The API does not allow for optional or implicit authorizations for such actions.
Key Components & Design Patterns
Role Services (using Inheritance):
- Represent the user's job in a logical inheritance chain that models skill sets. A more specialized role extends a more foundational one. This relationship is for code reuse and does not dictate the directory structure.
- Example:
PhysicianService(inhealth-care) can extendParamedicService(inemergencies) to inherit emergency response capabilities.
Capability Services (using Composition):
- Specialized classes that manage a single, shared capability (e.g.,
MedicationServiceinsrc/services/capabilities). They are not part of the role inheritance chain. - Usage: Role services (
PhysicianService,NurseService) hold an instance of the relevant capability service and delegate tasks to it. - Explicit Authorization: Methods in a capability service require a signed authorization object.
MedicationService.administer(patientDid: string, signedMedicationRequest: Jws)
- Specialized classes that manage a single, shared capability (e.g.,
The Smart Hub (
ProfileManagerandcapabilityMapper):- The
capabilityMapperis the "brain". It receives the user's role and the entity'sDidDocument. - It instantiates the final, most specific role service (e.g.,
new PhysicianService(...)). - It also instantiates and injects the required capability services (e.g.,
new MedicationService(...)) into the role service's constructor. ProfileManagerthen exposes the final, fully-formed service object (e.g.,session.professional.physician).
- The
Example Directory & Class Structure
This architecture translates into the following physical directory structure. Note how the directory structure is organized by sector/domain, while class inheritance can cross these boundaries.
src/services/
├── base/
│ ├── BaseProfessionalService.ts
│ ├── BaseOrgAdminService.ts
│ └── BaseFamilyAdminService.ts
│
├── capabilities/
│ └── MedicationService.ts
│
├── org-admin/
│ └── OrgAdminService.ts
│
├── family-admin/
│ └── FamilyAdminService.ts
│
├── professional/
│ ├── health-care/
│ │ ├── PhysicianService.ts // Extends ParamedicService
│ │ ├── NurseService.ts
│ │ └── PhysiotherapistService.ts
│ │
│ ├── emergencies/
│ │ └── ParamedicService.ts // Extends BaseProfessionalService
│ │
│ ├── health-insurance/
│ │ └── InsuranceAgentService.ts
│ │
│ └── care/
│ └── CaregiverService.ts
│
└── individual/
└── IndividualAccountService.ts1. The User's Identity (via id_token)
- What it is: A proof of who the human is. It's a standard OIDC
id_tokenobtained from an external identity provider (e.g., Google, Apple, eIDAS). - When it's used: This token is used for initial user authentication and for "public" API calls that happen before a device profile is officially registered in the system (Pre-DCR).
- Examples:
registerOrganization,acceptLicenseOffer. - How the SDK uses it: The
id_tokenis passed to "public" service methods (e.g.,executePublicJob) to prove the legal representative's identity. It's also used during the acquisition of asmartToken.
2. The Device's Identity (via private_key_jwt Client Assertion)
- What it is: A proof of what the client application/device is. The device's identity is its
client_id, which is adid:webidentifier created by the backend connector upon activation. - When it's used: It is used every time the SDK needs to request a
smartTokenfrom our own Authorization Server (/tokenendpoint). This happens for all secure, data-access calls after the device is registered (Post-DCR). - How it works: The SDK uses the device's private JWK (stored securely by the
IWallet) to sign a JWT. This signed JWT is the "Client Assertion," and it proves to the token endpoint that the request is coming from a legitimate, registered client.
The Bridge: Acquiring a smartToken
For all Post-DCR operations, the SDK must acquire a short-lived smartToken (an OAuth 2.0 Access Token). To do this, our Authorization Server needs proof of both the device and the user.
- Proof of Device: The
private_key_jwtclient assertion. - Proof of User: The user's current, active
id_token.
The SmartTokenManager within the SDK handles this flow automatically. API service methods simply declare the scopes they need, and the manager orchestrates the acquisition and caching of the required smartToken.
Guiding Principles
- The
API_INTEGRATORS_GUIDE.mdis the Source of Truth: All API method implementations, service selectors, and payload structures must be based on the official documentation available athttps://raw.githubusercontent.com/Global-DataCare/docs/refs/heads/main/API_INTEGRATORS_GUIDE.md. The SDK should not invent or assume logic. - Test Data is the Second Source of Truth: The mock data files in
client-sdk-ts/__tests__/data/are sourced from the backend's own test suite and documentation. They represent the ground truth for API responses and must be used for all Jest tests and for the in-appDEMO_MODE. - Specific Methods are Simple, The Engine is Smart: A specific API method (e.g.,
createOrganization) is responsible for knowing the "what" (the endpoint selector, the payload). TheBaseApiServiceengine (resolveAndExecute) is responsible for the "how" (DID resolution, message construction, job submission).
End-to-End Onboarding Flow
The SDK is designed to follow a specific, multi-step business process for onboarding a new organization.
orgAdmin.admin.createOrganization(): A legal representative, authenticated with an external OIDCid_token, submits the organization's details to the Host Provider's DID. The async response will contain anOffer.orgAdmin.admin.confirmOrder(): The representative accepts the offer by submitting an order, again to the Host Provider's DID. The async response will contain a set of license codes.- Payment (External): If required, the user completes the payment flow.
common.auth.activateDevice(): The representative (or another employee) uses a valid license code to register their device. This is a DCR (Dynamic Client Registration) call made to the Gateway's (Tenant's) DID. This call creates theclient_id(did:web) for the device and marks it as active.smartTokenAcquisition: From this point forward, all API calls are authorized (Post-DCR) and must acquire asmartToken. The SDK handles this automatically using the device's identity (viaprivate_key_jwt) and the user'sid_token.
Quick Start & Core Usage
This guide demonstrates how to implement the primary business flows using the SDK.
The Core Principle: The SDK is a Platform-Agnostic Engine
Think of the SDK as a powerful engine. It is designed to run in any environment but it cannot directly perform platform-specific tasks like storing a cryptographic key on a device. Instead, the SDK defines interfaces (or "ports") for these tasks, such as IWallet for secure storage or ICryptoHelper for cryptographic operations.
Your application is responsible for providing the concrete implementations (the "adapters" or "platform services") that connect to these ports.
For a detailed explanation of the overall multi-package architecture and how the different pieces (like adapters-sdk-expo and platformServices) fit together, please see the main project README.
Implementing the Platform Adapters (IWallet & IVaultRepository)
The two most important interfaces you must implement when bringing the SDK to a new platform are:
IWallet: This interface defines the contract for secure cryptographic key storage. Your app must provide an object that implements this interface. For an Expo app, this is typically a class that usesexpo-secure-storeto interact with the device's Keychain or Keystore.IVaultRepository: This defines the contract for storing user data (like profiles and job requests). The SDK requires a factory function that can create a separate, isolated storage vault for each user profile.
Conceptual Example (platformServices.ts for a new platform):
// In a file like `platformServices.ts` in your new platform's source.
import { IWallet, IVaultRepository } from '@gdc/client-sdk'; // Assuming published package
import { MySecureStorage } from './my-platform-secure-storage';
import { MyDatabase } from './my-platform-database';
// 1. Your platform's implementation of IWallet
class MyPlatformWallet implements IWallet {
// ... implement all methods of IWallet using your platform's secure storage ...
async provisionKeys(entityId: string): Promise<JwkSet> {
const privateKey = await MySecureStorage.generateAndStoreKey(entityId);
return getPublicKey(privateKey);
}
// ... other methods like digest, protectConfidentialData, etc.
}
// 2. Create a single, app-wide instance of your wallet.
export const appWallet = new MyPlatformWallet();
// 3. Your platform's factory function for creating user vaults.
export function createVaultForProfile(profileId: string): IVaultRepository {
return new MyDatabase(profileId);
}Using the Pre-built Expo Implementation
This project already provides the necessary platform-specific implementations for Expo. You do not need to build them from scratch.
- Low-Level Adapters: The primitive adapters can be found in the
adapters-sdk-expo/directory. - High-Level Services: The concrete implementations are located in the
platformServices/directory.platformServices/ExpoWallet.tsis the implementation of theIWalletinterface.platformServices/index.tsis where the singletonappWalletis instantiated and thecreateVaultForProfilefunction is defined.
Therefore, to use the pre-built services for Expo, you simply import them:
// In your ProfileContext.tsx or equivalent setup file:
import { appWallet, createVaultForProfile } from '../platformServices';
import { ClientSDK } from '../client-sdk-ts';
// ... then, when you initialize the SDK, you pass these directly.
const sdk = new ClientSDK(sdkConfig, appInfo, appWallet, verifierService, icaDid);
// ... and when you create a session, you pass the factory function.
await sdk.initializeSession(params, createVaultForProfile);SDK Initialization (Platform-Specific)
The first step is to instantiate the ClientSDK. This requires providing the platform-specific "adapters" and implementations discussed above.
// In a central provider file, e.g., ProfileContext.tsx
import { ClientSDK, InitializeSessionParams, IscoCode } from '../client-sdk-ts';
import { appWallet, createVaultForProfile } from '../platformServices';
import { AdapterCryptoSdkExpo, AdapterNetworkSdkExpo, AdapterApiConfigSdkExpo } from '../adapters-sdk-expo';
import { VerifierService } from '../client-sdk-ts/src/VerifierService';
import { CryptographyService } from '../crypto-ts/CryptographyService';
// This initialization should happen once in your application's lifecycle.
const sdk = useMemo(() => {
// 1. Instantiate platform-specific adapters.
const cryptoAdapter = new AdapterCryptoSdkExpo();
const sdkConfig = {
crypto: cryptoAdapter,
network: new AdapterNetworkSdkExpo(),
api: new AdapterApiConfigSdkExpo(),
fetcher: fetch.bind(window),
// ... mockOptions for DEMO mode
};
// 2. Set up the Verifier Service with your trust anchors.
const cryptoService = new CryptographyService(cryptoAdapter);
const rootGoverningKeyPub = process.env.EXPO_PUBLIC_ROOT_GOVERNING_KEY_PUB; // From environment
const verifierService = new VerifierService(cryptoService, rootGoverningKeyPub, sdkConfig.fetcher);
// 3. Define your application's static info.
const appInfo = { /* ... device info, app type, etc. ... */ };
// 4. The trusted Intermediate CA DID.
const icaDid = process.env.EXPO_PUBLIC_ICA_DID; // From environment
// 5. Create the SDK instance, injecting your platform-specific wallet.
return new ClientSDK(sdkConfig, appInfo, appWallet, verifierService, icaDid);
}, []);Constructing the Provider DID
Before initializing a session, your application must determine the canonical DID of the provider (the organization or host) the user wants to connect to. The SDK supports two scenarios:
Case A: The organization has its own domain
If the user provides a dedicated domain for their organization (e.g., identity.acme.com), constructing the DID is straightforward.
Example:
const userInputDomain = 'identity.acme.com';
const providerDid = `did:web:${userInputDomain.toLowerCase()}`;Case B: The organization is hosted by a provider
If the user's organization is hosted on a shared platform, you must construct a more complex "hosted" DID. The SDK provides a utility function for this.
Example:
import { buildHostedDidDetails } from '../client-sdk-ts';
const hostDomain = 'provider.example.com';
const tenantName = 'acme-health'; // Provided by the user in the UI
const { did: providerDid } = buildHostedDidDetails({
host: hostDomain,
alternateName: tenantName,
jurisdiction: 'ES', // Or another value from the UI
});
// providerDid -> "did:web:provider.example.com:acme-health:cds-ES:v1:health-care"Core Flow: Onboarding a New Organization
This flow uses the providerDid constructed in the previous step. It involves a human legal representative authenticating themselves to bootstrap the organization's identity and its first device.
// Assume you have the `providerDid` from the previous step.
// Assume you have the legal rep's idToken from an OIDC login.
const legalRepIdToken = '...';
// --- Step 1: Initialize a session for the Legal Representative ---
const session = await sdk.initializeSession({
email: '[email protected]',
role: `ISCO-08|${IscoCode.ManagingDirector}`,
providerDid: providerDid, // The DID is the entry point
}, createVaultForProfile); // Pass the vault factory function here
const orgAdminService = session.orgAdmin.admin;
// --- Step 2: Create the Organization ---
// This call is directed at the HOST's DID.
const hostDid = 'did:web:provider.example.com';
const orgClaims = { /* ... organization registration data ... */ };
const { thid: createOrgThid } = await orgAdminService.createOrganization(orgClaims, hostDid, legalRepIdToken);
// Poll for the result, which will contain an Offer...
// --- Step 3: Confirm the Order ---
// The user accepts the offer. This call is also to the HOST's DID.
const offerId = '...'; // From the result of the previous step
const { thid: confirmOrderThid } = await orgAdminService.confirmOrder(offerId, hostDid, legalRepIdToken);
// Poll for the result, which will contain a license code...
// --- Step 4: Activate the First Device ---
// This call is directed at the new TENANT's DID (the one we used in initializeSession).
const licenseCode = '...'; // From the result of the previous step
const commonAuthService = session.common.auth;
const { thid: activateThid } = await commonAuthService.activateDevice(licenseCode, providerDid);
// Poll for the result. The device is now registered.Note on Production vs. Test Onboarding
The self-service createOrganization flow detailed above is intended for the test-network. Onboarding a new organization in the production environment involves a manual verification process by the host provider to ensure the legitimacy of the legal entity.
Testing
Unit tests:
npm testE2E (SDK against external gateway):
scripts/run-e2e-external.shOverride the backend env file:
scripts/run-e2e-external.sh --env-file /path/to/.envCore Flow: Day-to-Day Authenticated Operations
Once a device is registered, subsequent sessions are initialized using the same logic. The SDK handles token management automatically.
// Assume a registered employee logs in.
// The app first constructs the correct `providerDid` as shown above.
const providerDid = '...'; // Constructed using Case A or Case B.
// --- Step 1: Initialize Session ---
const session = await sdk.initializeSession({
email: '[email protected]',
role: `ISCO-08|${IscoCode.GeneralistMedicalPractitioner}`,
providerDid: providerDid,
}, createVaultForProfile); // Pass the vault factory function
// --- Step 2: Perform an Authenticated Action ---
const physicianService = session.professional.physician;
if (physicianService) {
// The SDK will automatically handle acquiring a smartToken for this call.
const { thid } = await physicianService.createEmployee(...);
}Other Business Flows
Flows for managing families, adding members, managing consent, and sending communications follow a similar pattern:
- Initialize a session for the authenticated user.
- Access the appropriate service from the
sessionobject (e.g.,session.familyAdmin.admin). - Call the method for the desired action.
For the specific service methods and the exact structure of the data payloads required, always consult the API_INTEGRATORS_GUIDE.md.
