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 +8 -9
- package/dist/ClientSDK.d.ts +2 -0
- package/dist/ClientSDK.js +20 -16
- package/dist/frontend-services/BaseApiService.d.ts +1 -0
- package/dist/frontend-services/BaseApiService.js +23 -2
- package/dist/frontend-services/org-admin/OrgAdminService.d.ts +8 -4
- package/dist/frontend-services/org-admin/OrgAdminService.js +25 -9
- package/dist/interfaces/others.d.ts +4 -0
- package/dist/utils/entitlements.d.ts +10 -0
- package/dist/utils/entitlements.js +21 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +2 -2
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., `
|
|
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.
|
|
151
|
-
2. **`orgAdmin.admin.
|
|
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
|
-
//
|
|
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.
|
|
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.
|
|
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.
|
|
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 `
|
|
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
|
|
package/dist/ClientSDK.d.ts
CHANGED
|
@@ -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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
const rawDid = `${connectorDid}:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
18
|
-
console.log(`[OrgAdminService] Calling
|
|
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(
|
|
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
|
|
40
|
-
console.log(`[OrgAdminService] Calling
|
|
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(
|
|
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
|
+
};
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdc-sdk-client-ts",
|
|
3
|
-
"version": "1.0
|
|
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
|
|
8
|
+
"gdc-common-utils-ts": "^1.1.0"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "jest",
|