gdc-sdk-client-ts 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -141,14 +141,14 @@ The `SmartTokenManager` within the SDK handles this flow automatically. API serv
141
141
 
142
142
  1. **The `API_INTEGRATORS_GUIDE.md` is the Source of Truth:** All API method implementations, service selectors, and payload structures **must** be based on the official documentation available at `https://raw.githubusercontent.com/Global-DataCare/docs/refs/heads/main/API_INTEGRATORS_GUIDE.md`. The SDK should not invent or assume logic.
143
143
  2. **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-app `DEMO_MODE`.
144
- 3. **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). The `BaseApiService` engine (`resolveAndExecute`) is responsible for the "how" (DID resolution, message construction, job submission).
144
+ 3. **Specific Methods are Simple, The Engine is Smart:** A specific API method (e.g., `startOrganizationRegistration`) is responsible for knowing the "what" (the endpoint selector, the payload). The `BaseApiService` engine (`resolveAndExecute`) is responsible for the "how" (DID resolution, message construction, job submission).
145
145
 
146
146
  ## End-to-End Onboarding Flow
147
147
 
148
148
  The SDK is designed to follow a specific, multi-step business process for onboarding a new organization.
149
149
 
150
- 1. **`orgAdmin.admin.createOrganization()`**: A legal representative, authenticated with an external OIDC `id_token`, submits the organization's details to the **Host Provider's DID**. The async response will contain an `Offer`.
151
- 2. **`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.
150
+ 1. **`orgAdmin.admin.startOrganizationRegistration()`**: A legal representative, authenticated with an external OIDC `id_token`, submits the organization's details. The SDK automatically resolves and targets the operator host DID for this onboarding call. The async response will contain an `Offer`.
151
+ 2. **`orgAdmin.admin.confirmOrganizationRegistration()`**: The representative accepts the offer by submitting an order. The SDK again resolves the operator host DID automatically. The async response will contain a set of license codes.
152
152
  3. **Payment (External)**: If required, the user completes the payment flow.
153
153
  4. **`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 the `client_id` (`did:web`) for the device and marks it as active.
154
154
  5. **`smartToken` Acquisition**: From this point forward, all API calls are authorized (Post-DCR) and must acquire a `smartToken`. The SDK handles this automatically using the device's identity (via `private_key_jwt`) and the user's `id_token`.
@@ -316,16 +316,15 @@ const session = await sdk.initializeSession({
316
316
  const orgAdminService = session.orgAdmin.admin;
317
317
 
318
318
  // --- Step 2: Create the Organization ---
319
- // This call is directed at the HOST's DID.
320
- const hostDid = 'did:web:provider.example.com';
319
+ // The SDK derives the operator host DID from `providerDid`.
321
320
  const orgClaims = { /* ... organization registration data ... */ };
322
- const { thid: createOrgThid } = await orgAdminService.createOrganization(orgClaims, hostDid, legalRepIdToken);
321
+ const { thid: createOrgThid } = await orgAdminService.startOrganizationRegistration(orgClaims, legalRepIdToken);
323
322
  // Poll for the result, which will contain an Offer...
324
323
 
325
324
  // --- Step 3: Confirm the Order ---
326
- // The user accepts the offer. This call is also to the HOST's DID.
325
+ // The user accepts the offer. The SDK keeps targeting the operator host DID internally.
327
326
  const offerId = '...'; // From the result of the previous step
328
- const { thid: confirmOrderThid } = await orgAdminService.confirmOrder(offerId, hostDid, legalRepIdToken);
327
+ const { thid: confirmOrderThid } = await orgAdminService.confirmOrganizationRegistration(offerId, legalRepIdToken);
329
328
  // Poll for the result, which will contain a license code...
330
329
 
331
330
  // --- Step 4: Activate the First Device ---
@@ -337,7 +336,7 @@ const { thid: activateThid } = await commonAuthService.activateDevice(licenseCod
337
336
  ```
338
337
 
339
338
  ### Note on Production vs. Test Onboarding
340
- 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.
339
+ The self-service `startOrganizationRegistration` 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.
341
340
 
342
341
  ## Testing
343
342
 
@@ -15,6 +15,7 @@ export interface InitializeSessionParams {
15
15
  role: string;
16
16
  providerDid: string;
17
17
  appType?: AppInfo['appType'];
18
+ familyId?: string;
18
19
  }
19
20
  /**
20
21
  * @class ClientSDK
@@ -49,6 +50,7 @@ export declare class ClientSDK {
49
50
  */
50
51
  initializeProfileRegistry(createVault: (registryId: string) => IVaultRepository, registryId?: string): Promise<ProfileRegistry>;
51
52
  shutdownSession(): void;
53
+ private normalizeRoleCodeForDid;
52
54
  private constructEmployeeDid;
53
55
  private constructIndividualDid;
54
56
  }
package/dist/ClientSDK.js CHANGED
@@ -130,7 +130,7 @@ export class ClientSDK {
130
130
  const finalProfileId = params.profileId || this.sdkConfig.crypto.randomUUID();
131
131
  const userDid = appType === 'Organization'
132
132
  ? this.constructEmployeeDid(providerDid, email, role)
133
- : this.constructIndividualDid(providerDid, finalProfileId);
133
+ : await this.constructIndividualDid(providerDid, finalProfileId, email, role, params.familyId);
134
134
  const publicKeys = await this.wallet.provisionKeys(finalProfileId);
135
135
  const newProfile = {
136
136
  id: finalProfileId,
@@ -174,25 +174,29 @@ export class ClientSDK {
174
174
  this.currentSession = null;
175
175
  }
176
176
  }
177
- constructEmployeeDid(connectorDid, email, role) {
178
- // The user's role is expected in the FHIR-like format "system|code", e.g., "ISCO-08|1120"
179
- const roleParts = role.split('|');
180
- if (roleParts.length !== 2) {
181
- console.warn(`[ClientSDK] Invalid role format for Employee DID construction: ${role}. Expected "system|code".`);
182
- return `${connectorDid}:employee:invalid-role-format`;
177
+ normalizeRoleCodeForDid(role, fallback = 'unknown-role') {
178
+ const raw = (role || '').trim();
179
+ if (!raw)
180
+ return fallback;
181
+ const parts = raw.split('|');
182
+ if (parts.length === 2) {
183
+ return (parts[1] || '').trim() || fallback;
183
184
  }
184
- // Construct the DID according to the official spec in docs/did_generation.md
185
- const rawDid = `${connectorDid}:employee:${email}:${role}`;
185
+ return raw;
186
+ }
187
+ constructEmployeeDid(connectorDid, email, role) {
188
+ const roleCode = this.normalizeRoleCodeForDid(role, 'invalid-role');
189
+ const rawDid = `${connectorDid}:employee:${email}:${roleCode}`;
186
190
  // Normalize it to ensure canonical representation
187
191
  return normalizeDidWeb(rawDid);
188
192
  }
189
- // TODO: Per conversation, evolve this to align with the new path structure,
190
- // potentially incorporating a familyId. e.g., did:web:<provider>:family:<id>:individual:<uuid_base58>
191
- constructIndividualDid(connectorDid, profileId) {
192
- // For now, using a simplified version of the multibase approach.
193
- // A real implementation would use a proper base58btc encoder.
194
- const multibaseId = `z${profileId.replace(/-/g, '')}`;
195
- const rawDid = `${connectorDid}:individual:multibase:${multibaseId}`;
193
+ async constructIndividualDid(connectorDid, profileId, email, role, familyId) {
194
+ const normalizedEmail = (email || '').trim().toLowerCase();
195
+ const emailHash = await this.sdkConfig.crypto.digestString(normalizedEmail, 'SHA-256');
196
+ const multibaseEmailHash = `z${emailHash}`;
197
+ const normalizedFamilyId = (familyId || `profile-${profileId}`).trim();
198
+ const normalizedRole = this.normalizeRoleCodeForDid(role, 'ONESELF').toUpperCase();
199
+ const rawDid = `${connectorDid}:family:${normalizedFamilyId}:${multibaseEmailHash}:${normalizedRole}`;
196
200
  return normalizeDidWeb(rawDid);
197
201
  }
198
202
  }
@@ -34,6 +34,7 @@ export declare abstract class BaseApiService {
34
34
  protected readonly appInfo: AppInfo;
35
35
  protected readonly cryptoHelper: ICryptoHelper;
36
36
  constructor(context: ServiceContext);
37
+ private withRetryPolicy;
37
38
  /**
38
39
  * The generic "engine" for all API calls. It takes the endpoint ingredients,
39
40
  * builds the selector, finds the service, constructs the message, and executes the job.
@@ -25,6 +25,26 @@ export class BaseApiService {
25
25
  this.appInfo = context.appInfo; // Initialize AppInfo
26
26
  this.cryptoHelper = context.cryptoHelper;
27
27
  }
28
+ async withRetryPolicy(operationKey, fn) {
29
+ var _a, _b, _c, _d;
30
+ const policy = (_b = (_a = this.sdkConfig.api).getRetryPolicy) === null || _b === void 0 ? void 0 : _b.call(_a, operationKey);
31
+ const retries = Math.max(0, (_c = policy === null || policy === void 0 ? void 0 : policy.retries) !== null && _c !== void 0 ? _c : 0);
32
+ const delayMs = Math.max(0, (_d = policy === null || policy === void 0 ? void 0 : policy.delayMs) !== null && _d !== void 0 ? _d : 0);
33
+ const attempts = retries + 1;
34
+ let lastError;
35
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
36
+ try {
37
+ return await fn();
38
+ }
39
+ catch (error) {
40
+ lastError = error;
41
+ if (attempt < attempts && delayMs > 0) {
42
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
43
+ }
44
+ }
45
+ }
46
+ throw lastError instanceof Error ? lastError : new Error(`Operation failed: ${operationKey}`);
47
+ }
28
48
  /**
29
49
  * The generic "engine" for all API calls. It takes the endpoint ingredients,
30
50
  * builds the selector, finds the service, constructs the message, and executes the job.
@@ -69,16 +89,17 @@ export class BaseApiService {
69
89
  jti: this.jobManager.generateId(),
70
90
  thid: thid,
71
91
  };
92
+ const operationKey = `${serviceSelector.section || 'default'}/${serviceSelector.format}/${serviceSelector.resourceType}/${serviceSelector.action}`;
72
93
  // 6. Call the appropriate execution method.
73
94
  if (isPublic) {
74
95
  if (!idToken)
75
96
  throw new Error("idToken is required for public jobs.");
76
- await this.executePublicJob(endpointUrl, idToken, didcommMessage, serviceSelector);
97
+ await this.withRetryPolicy(operationKey, () => this.executePublicJob(endpointUrl, idToken, didcommMessage, serviceSelector));
77
98
  }
78
99
  else {
79
100
  if (!requiredScope || !idToken)
80
101
  throw new Error("requiredScope and idToken are required for authorized jobs.");
81
- await this.executeAuthorizedJob(targetDid, endpointUrl, requiredScope, idToken, didcommMessage, serviceSelector);
102
+ await this.withRetryPolicy(operationKey, () => this.executeAuthorizedJob(targetDid, endpointUrl, requiredScope, idToken, didcommMessage, serviceSelector));
82
103
  }
83
104
  return { thid };
84
105
  }
@@ -3,15 +3,20 @@ import { BaseApiService } from "../BaseApiService";
3
3
  * Service class for handling organization-related administrative actions.
4
4
  */
5
5
  export declare class OrgAdminService extends BaseApiService {
6
+ /**
7
+ * Resolves the operator host DID from the current session provider DID.
8
+ * For hosted organizations (`did:web:host:tenant:...`) this returns `did:web:host`.
9
+ * For self-hosted organizations (`did:web:org.example.com`) this returns the same DID.
10
+ */
11
+ private resolveOperatorDidFromSession;
6
12
  /**
7
13
  * Registers a new organization with the gateway using an external OIDC token.
8
14
  *
9
15
  * @param dataClaims The organization's registration form data.
10
- * @param gatewayDid The DID of the target gateway/host.
11
16
  * @param idToken The OIDC token of the legal representative.
12
17
  * @returns The thread ID (thid) of the submitted job for polling.
13
18
  */
14
- createOrganization(dataClaims: object, gatewayDid: string, idToken: string, targetNetwork: string): Promise<{
19
+ startOrganizationRegistration(dataClaims: object, idToken: string, targetNetwork?: string): Promise<{
15
20
  thid: string;
16
21
  }>;
17
22
  /**
@@ -19,11 +24,10 @@ export declare class OrgAdminService extends BaseApiService {
19
24
  * This is the second step in the organization onboarding flow.
20
25
  *
21
26
  * @param acceptedOfferId The URN identifier of the offer being accepted.
22
- * @param gatewayDid The DID of the target gateway/host.
23
27
  * @param idToken The OIDC token of the legal representative.
24
28
  * @returns The thread ID (thid) of the submitted job for polling.
25
29
  */
26
- confirmOrder(acceptedOfferId: string, gatewayDid: string, idToken: string, targetNetwork: string): Promise<{
30
+ confirmOrganizationRegistration(acceptedOfferId: string, idToken: string, targetNetwork?: string): Promise<{
27
31
  thid: string;
28
32
  }>;
29
33
  /**
@@ -1,21 +1,37 @@
1
1
  // Copyright 2025 Antifraud Services Inc. under the Apache License, Version 2.0.
2
2
  // File: client-sdk-ts/src/services/org-admin/OrgAdminService.ts
3
3
  import { BaseApiService } from "../BaseApiService.js";
4
- import { EndpointKey, ENDPOINT_REGISTRY } from "../../serviceSelectorRegistry.js";
4
+ import { EndpointKey, ENDPOINT_REGISTRY, defaultNetworkIdentifier } from "../../serviceSelectorRegistry.js";
5
5
  /**
6
6
  * Service class for handling organization-related administrative actions.
7
7
  */
8
8
  export class OrgAdminService extends BaseApiService {
9
+ /**
10
+ * Resolves the operator host DID from the current session provider DID.
11
+ * For hosted organizations (`did:web:host:tenant:...`) this returns `did:web:host`.
12
+ * For self-hosted organizations (`did:web:org.example.com`) this returns the same DID.
13
+ */
14
+ resolveOperatorDidFromSession() {
15
+ const providerDid = this.profile.providerDid;
16
+ if (!providerDid || !providerDid.startsWith('did:web:')) {
17
+ throw new Error(`[OrgAdminService] Invalid provider DID in session: '${providerDid}'.`);
18
+ }
19
+ const parts = providerDid.split(':');
20
+ if (parts.length < 3) {
21
+ throw new Error(`[OrgAdminService] Invalid provider DID format: '${providerDid}'.`);
22
+ }
23
+ return `did:web:${parts[2]}`;
24
+ }
9
25
  /**
10
26
  * Registers a new organization with the gateway using an external OIDC token.
11
27
  *
12
28
  * @param dataClaims The organization's registration form data.
13
- * @param gatewayDid The DID of the target gateway/host.
14
29
  * @param idToken The OIDC token of the legal representative.
15
30
  * @returns The thread ID (thid) of the submitted job for polling.
16
31
  */
17
- async createOrganization(dataClaims, gatewayDid, idToken, targetNetwork) {
18
- console.log(`[OrgAdminService] Calling createOrganization on network: ${targetNetwork}.`);
32
+ async startOrganizationRegistration(dataClaims, idToken, targetNetwork = defaultNetworkIdentifier) {
33
+ console.log(`[OrgAdminService] Calling startOrganizationRegistration on network: ${targetNetwork}.`);
34
+ const operatorDid = this.resolveOperatorDidFromSession();
19
35
  const serviceSelector = Object.assign({}, ENDPOINT_REGISTRY[EndpointKey.REGISTER_TENANT]);
20
36
  serviceSelector.sector = targetNetwork;
21
37
  const payloadBody = {
@@ -24,7 +40,7 @@ export class OrgAdminService extends BaseApiService {
24
40
  meta: { claims: dataClaims },
25
41
  }],
26
42
  };
27
- return this.resolveAndExecute(gatewayDid, serviceSelector, payloadBody, true, // isPublic
43
+ return this.resolveAndExecute(operatorDid, serviceSelector, payloadBody, true, // isPublic
28
44
  undefined, idToken);
29
45
  }
30
46
  /**
@@ -32,12 +48,12 @@ export class OrgAdminService extends BaseApiService {
32
48
  * This is the second step in the organization onboarding flow.
33
49
  *
34
50
  * @param acceptedOfferId The URN identifier of the offer being accepted.
35
- * @param gatewayDid The DID of the target gateway/host.
36
51
  * @param idToken The OIDC token of the legal representative.
37
52
  * @returns The thread ID (thid) of the submitted job for polling.
38
53
  */
39
- async confirmOrder(acceptedOfferId, gatewayDid, idToken, targetNetwork) {
40
- console.log(`[OrgAdminService] Calling confirmOrder for offer: ${acceptedOfferId} on network: ${targetNetwork}.`);
54
+ async confirmOrganizationRegistration(acceptedOfferId, idToken, targetNetwork = defaultNetworkIdentifier) {
55
+ console.log(`[OrgAdminService] Calling confirmOrganizationRegistration for offer: ${acceptedOfferId} on network: ${targetNetwork}.`);
56
+ const operatorDid = this.resolveOperatorDidFromSession();
41
57
  const serviceSelector = Object.assign({}, ENDPOINT_REGISTRY[EndpointKey.CONFIRM_ORDER]);
42
58
  serviceSelector.sector = targetNetwork;
43
59
  const payloadBody = {
@@ -51,7 +67,7 @@ export class OrgAdminService extends BaseApiService {
51
67
  }
52
68
  }],
53
69
  };
54
- return this.resolveAndExecute(gatewayDid, serviceSelector, payloadBody, true, // isPublic
70
+ return this.resolveAndExecute(operatorDid, serviceSelector, payloadBody, true, // isPublic
55
71
  undefined, idToken);
56
72
  }
57
73
  /**
@@ -39,6 +39,10 @@ export interface INetwork {
39
39
  export interface IApiConfig {
40
40
  operationMode: 'DEMO' | 'FAPI';
41
41
  legacyFhirEnabled: boolean;
42
+ getRetryPolicy?: (operationKey: string) => {
43
+ retries: number;
44
+ delayMs: number;
45
+ } | undefined;
42
46
  }
43
47
  /**
44
48
  * Defines the set of optional parameters for enabling the SDK's mock mode.
@@ -0,0 +1,10 @@
1
+ export type LicenseClass = 'member' | 'employee';
2
+ export type OfferEntitlement = {
3
+ offerId?: string;
4
+ checkoutUrl?: string;
5
+ quantity?: number;
6
+ unitPrice?: string;
7
+ totalPrice?: string;
8
+ taxes?: string;
9
+ };
10
+ export declare const parseOfferEntitlementFromClaims: (claims: Record<string, any> | undefined) => OfferEntitlement;
@@ -0,0 +1,21 @@
1
+ const findClaimBySuffix = (claims, suffix) => {
2
+ if (!claims)
3
+ return undefined;
4
+ for (const [key, value] of Object.entries(claims)) {
5
+ if (typeof value === 'string' && key.endsWith(suffix))
6
+ return value;
7
+ }
8
+ return undefined;
9
+ };
10
+ export const parseOfferEntitlementFromClaims = (claims) => {
11
+ const quantityRaw = findClaimBySuffix(claims, 'Offer.eligibleQuantity.value');
12
+ const quantity = typeof quantityRaw === 'string' && quantityRaw.trim().length > 0 ? Number(quantityRaw) : undefined;
13
+ return {
14
+ offerId: findClaimBySuffix(claims, 'Offer.identifier'),
15
+ checkoutUrl: findClaimBySuffix(claims, 'Offer.checkoutPageURLTemplate'),
16
+ quantity: Number.isFinite(quantity) ? quantity : undefined,
17
+ unitPrice: findClaimBySuffix(claims, 'Offer.unitPrice.value'),
18
+ totalPrice: findClaimBySuffix(claims, 'Offer.price.value'),
19
+ taxes: findClaimBySuffix(claims, 'Offer.tax.value'),
20
+ };
21
+ };
@@ -1,3 +1,4 @@
1
1
  export * from './mockData';
2
2
  export * from './scopeBuilder';
3
3
  export * from './rule';
4
+ export * from './entitlements';
@@ -4,3 +4,4 @@
4
4
  export * from './mockData.js';
5
5
  export * from './scopeBuilder.js';
6
6
  export * from './rule.js';
7
+ export * from './entitlements.js';
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "gdc-sdk-client-ts",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "dependencies": {
7
7
  "@noble/hashes": "^1.8.0",
8
- "gdc-common-utils-ts": "^1.0.7"
8
+ "gdc-common-utils-ts": "^1.1.0"
9
9
  },
10
10
  "scripts": {
11
11
  "test": "jest",