gdc-sdk-node-ts 2.4.23 → 2.4.30
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 +18 -7
- package/dist/constants/lifecycle.d.ts +8 -0
- package/dist/constants/lifecycle.js +8 -0
- package/dist/device-activation.d.ts +48 -1
- package/dist/device-activation.js +157 -1
- package/dist/digital-twin.d.ts +4 -0
- package/dist/digital-twin.js +16 -4
- package/dist/host-onboarding.d.ts +7 -0
- package/dist/node-managed-wallet.d.ts +41 -1
- package/dist/node-managed-wallet.js +52 -0
- package/dist/node-runtime-client.d.ts +39 -4
- package/dist/node-runtime-client.js +142 -51
- package/dist/orchestration/client-port.d.ts +11 -1
- package/dist/orchestration/digital-twin-sdk.js +3 -1
- package/dist/orchestration/host-onboarding-sdk.d.ts +9 -3
- package/dist/orchestration/host-onboarding-sdk.js +10 -1
- package/dist/orchestration/individual-controller-sdk.d.ts +29 -3
- package/dist/orchestration/individual-controller-sdk.js +48 -2
- package/dist/orchestration/organization-controller-sdk.d.ts +19 -6
- package/dist/orchestration/organization-controller-sdk.js +27 -6
- package/dist/organization-controller-recovery.d.ts +4 -2
- package/dist/organization-controller-recovery.js +14 -8
- package/dist/organization-employee-lifecycle.d.ts +4 -0
- package/dist/organization-employee-lifecycle.js +17 -1
- package/dist/organization-license-order.d.ts +8 -3
- package/dist/resource-operations.d.ts +67 -0
- package/dist/resource-operations.js +62 -1
- package/dist/runtime-client-paths.d.ts +10 -0
- package/dist/runtime-client-paths.js +10 -0
- package/dist/server-profile-session.d.ts +32 -3
- package/dist/server-profile-session.js +21 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -238,6 +238,11 @@ Current runtime boundary:
|
|
|
238
238
|
- `OrganizationControllerSdk.confirmOrganizationLicenseOrder(...)` now uses the
|
|
239
239
|
public host `Order/_batch` route used by GW CORE for portal-managed
|
|
240
240
|
post-payment seat activation
|
|
241
|
+
- that route is commercial routing only: `iss` remains the exact controller DID
|
|
242
|
+
registered by tenant DCR, `aud` remains the tenant id, and GW resolves its
|
|
243
|
+
JWS/JWE key identifiers from the tenant rather than the host
|
|
244
|
+
- applications use the high-level SDK method and configured transport profile;
|
|
245
|
+
they do not populate `meta.jws`, move public keys, or construct DIDComm by hand
|
|
241
246
|
- the long root lifecycle is still not fully closed because the suite does not
|
|
242
247
|
yet orchestrate the whole `license list -> pay -> confirm -> relist -> two
|
|
243
248
|
employees -> selective purge -> cleanup` dialogue as one single test
|
|
@@ -288,9 +293,11 @@ npm run test:e2e:live-gw:all
|
|
|
288
293
|
|
|
289
294
|
Profile note:
|
|
290
295
|
|
|
291
|
-
- `didcomm-plain` is the
|
|
296
|
+
- `didcomm-plain` is the explicit demo-only live profile
|
|
292
297
|
- `legacy-fhir` exercises raw `application/fhir+json` async batch submission for `org.hl7.fhir.*`
|
|
293
298
|
- `all` runs every implemented profile from the same suite file
|
|
299
|
+
- each `NodeHttpClient` selects one profile at construction; every facade
|
|
300
|
+
operation and poll uses it and cannot override or downgrade it
|
|
294
301
|
|
|
295
302
|
Run the IPS ingestion/search branch as well:
|
|
296
303
|
|
|
@@ -638,8 +645,8 @@ modules below.
|
|
|
638
645
|
- types: `SmartTokenRequestInput`, `SmartTokenExchangeResult`
|
|
639
646
|
- function: `requestSmartTokenWithDeps(...)`
|
|
640
647
|
- [`src/resource-operations.ts`](src/resource-operations.ts)
|
|
641
|
-
- types: `OrganizationEmployeeCreationInput`, `IpsOrFhirImportInput`, `RelatedPersonUpsertInput`, `CommunicationIngestionInput`, `ClinicalDateRange`, `ClinicalBundleSearchInput`, `ConsentActorTargetInput`, `GrantProfessionalAccessInput`, `GrantProfessionalAccessResult`, `
|
|
642
|
-
- functions: `createOrganizationEmployeeWithDeps(...)`, `importIpsOrFhirAndUpdateIndexWithDeps(...)`, `upsertRelatedPersonAndPollWithDeps(...)`, `ingestCommunicationAndUpdateIndexWithDeps(...)`, `searchClinicalBundleWithDeps(...)`, `searchLatestIpsWithDeps(...)`, `grantProfessionalAccessWithDeps(...)`, `
|
|
648
|
+
- types: `OrganizationEmployeeCreationInput`, `IpsOrFhirImportInput`, `RelatedPersonUpsertInput`, `CommunicationIngestionInput`, `ClinicalDateRange`, `ClinicalBundleSearchInput`, `ConsentActorTargetInput`, `GrantProfessionalAccessInput`, `GrantProfessionalAccessResult`, `DigitalTwinSecondaryUseConsentInput`, `DigitalTwinSubjectLinkPurgeInput`
|
|
649
|
+
- functions: `createOrganizationEmployeeWithDeps(...)`, `importIpsOrFhirAndUpdateIndexWithDeps(...)`, `upsertRelatedPersonAndPollWithDeps(...)`, `ingestCommunicationAndUpdateIndexWithDeps(...)`, `searchClinicalBundleWithDeps(...)`, `searchLatestIpsWithDeps(...)`, `grantProfessionalAccessWithDeps(...)`, `setDigitalTwinSecondaryUseConsentWithDeps(...)`, `purgeDigitalTwinSubjectLinkWithDeps(...)`
|
|
643
650
|
- [`src/digital-twin.ts`](src/digital-twin.ts)
|
|
644
651
|
- types: `DigitalTwinSearchInput`, `DigitalTwinSelectionInput`, `DigitalTwinResearchTag`, `DigitalTwinMaterializationInput`
|
|
645
652
|
- functions: `searchDigitalTwinsWithDeps(...)`, `saveDigitalTwinSelectionWithDeps(...)`, `materializeDigitalTwinWithDeps(...)`
|
|
@@ -789,10 +796,14 @@ Live E2E legal PDF source:
|
|
|
789
796
|
Recovery-specific live rule:
|
|
790
797
|
|
|
791
798
|
- `Organization/_issue` can succeed and still be followed by `_exchange` failure
|
|
792
|
-
if the controller `id_token` is not
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
799
|
+
if the controller `id_token` is invalid, expired or does not prove the
|
|
800
|
+
expected actor/contact
|
|
801
|
+
- the current GW `_exchange` path expects a valid IdP token but takes tenant
|
|
802
|
+
authority from the already validated request route
|
|
803
|
+
- a custom `tenant_id` claim is optional; when present it must match the route
|
|
804
|
+
and cannot select another tenant
|
|
805
|
+
- a failed poll `OperationOutcome` is surfaced before the helper checks for
|
|
806
|
+
`initial_access_token`, preserving the real GW diagnostic
|
|
796
807
|
- the bundled recovery runner generates a syntactically valid demo JWT if
|
|
797
808
|
`CONTROLLER_ID_TOKEN` is not provided, but production/staging should use a
|
|
798
809
|
real IdP-issued token
|
|
@@ -16,7 +16,11 @@ export declare const GwCoreLifecycleAction: Readonly<{
|
|
|
16
16
|
readonly Transaction: "_transaction";
|
|
17
17
|
readonly TransactionResponse: "_transaction-response";
|
|
18
18
|
readonly Disable: "_disable";
|
|
19
|
+
readonly Enable: "_enable";
|
|
19
20
|
readonly Purge: "_purge";
|
|
21
|
+
readonly Status: "_status";
|
|
22
|
+
readonly DisableDescendants: "_disable-descendants";
|
|
23
|
+
readonly PurgeDescendants: "_purge-descendants";
|
|
20
24
|
}>;
|
|
21
25
|
/**
|
|
22
26
|
* Entry request methods currently used by GW CORE lifecycle handlers.
|
|
@@ -36,6 +40,10 @@ export declare const GwCoreLifecycleRequestType: Readonly<{
|
|
|
36
40
|
readonly IndividualOrganizationDisable: "Family-disable-request-v1.0";
|
|
37
41
|
readonly IndividualOrganizationPurge: "Family-purge-request-v1.0";
|
|
38
42
|
readonly IndividualMemberPurge: "RelatedPerson-purge-request-v1.0";
|
|
43
|
+
readonly TenantStatus: "Organization-lifecycle-status-request-v1.0";
|
|
44
|
+
readonly TenantEnable: "Organization-enable-request-v1.0";
|
|
45
|
+
readonly TenantDisableDescendants: "Organization-disable-descendants-request-v1.0";
|
|
46
|
+
readonly TenantPurgeDescendants: "Organization-purge-descendants-request-v1.0";
|
|
39
47
|
}>;
|
|
40
48
|
/**
|
|
41
49
|
* Named TODO ids kept close to the current lifecycle implementation so the
|
|
@@ -18,7 +18,11 @@ export const GwCoreLifecycleAction = Object.freeze({
|
|
|
18
18
|
Transaction: '_transaction',
|
|
19
19
|
TransactionResponse: '_transaction-response',
|
|
20
20
|
Disable: '_disable',
|
|
21
|
+
Enable: '_enable',
|
|
21
22
|
Purge: '_purge',
|
|
23
|
+
Status: '_status',
|
|
24
|
+
DisableDescendants: '_disable-descendants',
|
|
25
|
+
PurgeDescendants: '_purge-descendants',
|
|
22
26
|
});
|
|
23
27
|
/**
|
|
24
28
|
* Entry request methods currently used by GW CORE lifecycle handlers.
|
|
@@ -38,6 +42,10 @@ export const GwCoreLifecycleRequestType = Object.freeze({
|
|
|
38
42
|
IndividualOrganizationDisable: 'Family-disable-request-v1.0',
|
|
39
43
|
IndividualOrganizationPurge: 'Family-purge-request-v1.0',
|
|
40
44
|
IndividualMemberPurge: LifecycleRequestType.RelatedPersonPurge,
|
|
45
|
+
TenantStatus: 'Organization-lifecycle-status-request-v1.0',
|
|
46
|
+
TenantEnable: 'Organization-enable-request-v1.0',
|
|
47
|
+
TenantDisableDescendants: 'Organization-disable-descendants-request-v1.0',
|
|
48
|
+
TenantPurgeDescendants: 'Organization-purge-descendants-request-v1.0',
|
|
41
49
|
});
|
|
42
50
|
/**
|
|
43
51
|
* Named TODO ids kept close to the current lifecycle implementation so the
|
|
@@ -12,10 +12,57 @@ export type EmployeeDeviceActivationRequestInput = {
|
|
|
12
12
|
sector?: string;
|
|
13
13
|
activationCode: string;
|
|
14
14
|
idToken: string;
|
|
15
|
-
|
|
15
|
+
/** Canonical high-level description converted to OpenID DCR metadata by the SDK. */
|
|
16
|
+
deviceRegistration?: ProfileDeviceRegistrationInput;
|
|
17
|
+
/**
|
|
18
|
+
* @deprecated Low-level OpenID escape hatch. Use
|
|
19
|
+
* `createProfileDeviceActivationRequest(...).set...build()` instead.
|
|
20
|
+
*/
|
|
21
|
+
dcrPayload?: Record<string, unknown>;
|
|
16
22
|
timeoutSeconds?: number;
|
|
17
23
|
intervalSeconds?: number;
|
|
18
24
|
};
|
|
25
|
+
/** Application concepts required to bind one portal/app installation. */
|
|
26
|
+
export type ProfileDeviceRegistrationInput = Readonly<{
|
|
27
|
+
clientInstanceId: string;
|
|
28
|
+
clientName: string;
|
|
29
|
+
applicationType: 'native' | 'web';
|
|
30
|
+
redirectUris: readonly string[];
|
|
31
|
+
publicJwks: readonly Record<string, unknown>[];
|
|
32
|
+
deviceName?: string;
|
|
33
|
+
actorDid?: string;
|
|
34
|
+
profileDid?: string;
|
|
35
|
+
}>;
|
|
36
|
+
/** Advanced typed editor used by SDK runtimes that already own profile keys. */
|
|
37
|
+
export interface ProfileDeviceActivationDraft {
|
|
38
|
+
setClientInstanceId(value: string): ProfileDeviceActivationDraft;
|
|
39
|
+
setClientName(value: string): ProfileDeviceActivationDraft;
|
|
40
|
+
setApplicationType(value: 'native' | 'web'): ProfileDeviceActivationDraft;
|
|
41
|
+
setRedirectUris(values: readonly string[]): ProfileDeviceActivationDraft;
|
|
42
|
+
setPublicJwks(values: readonly Record<string, unknown>[]): ProfileDeviceActivationDraft;
|
|
43
|
+
setDeviceName(value: string): ProfileDeviceActivationDraft;
|
|
44
|
+
setActorDid(value: string): ProfileDeviceActivationDraft;
|
|
45
|
+
setProfileDid(value: string): ProfileDeviceActivationDraft;
|
|
46
|
+
setTimeoutSeconds(value: number): ProfileDeviceActivationDraft;
|
|
47
|
+
setIntervalSeconds(value: number): ProfileDeviceActivationDraft;
|
|
48
|
+
build(): EmployeeDeviceActivationRequestInput & {
|
|
49
|
+
deviceRegistration: ProfileDeviceRegistrationInput;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Starts an advanced profile-device activation request.
|
|
54
|
+
*
|
|
55
|
+
* Product portals should normally use `ServerProfileSessionManager.enroll`,
|
|
56
|
+
* which provisions the wallet and calls this editor internally. This surface
|
|
57
|
+
* exists for runtimes that already own and select the profile public keys.
|
|
58
|
+
*/
|
|
59
|
+
export declare function createProfileDeviceActivationRequest(input: Readonly<{
|
|
60
|
+
activationCode: string;
|
|
61
|
+
idToken: string;
|
|
62
|
+
tenantId?: string;
|
|
63
|
+
jurisdiction?: string;
|
|
64
|
+
sector?: string;
|
|
65
|
+
}>): ProfileDeviceActivationDraft;
|
|
19
66
|
export type EmployeeDeviceActivationResult = {
|
|
20
67
|
initialAccessToken: string;
|
|
21
68
|
exchange: SubmitAndPollResult;
|
|
@@ -2,6 +2,60 @@
|
|
|
2
2
|
import { resolvePollOptionsFromSeconds } from './poll-options.js';
|
|
3
3
|
import { IdentityAuthRequestFields, IdentityAuthResponseFields, IdentityDcrMetadataFields, IdentityDeviceInfoFields, } from 'gdc-common-utils-ts/constants/identity-auth';
|
|
4
4
|
import { buildEmployeeDeviceRevocationBody } from 'gdc-common-utils-ts/utils/organization-employee-lifecycle';
|
|
5
|
+
/**
|
|
6
|
+
* Starts an advanced profile-device activation request.
|
|
7
|
+
*
|
|
8
|
+
* Product portals should normally use `ServerProfileSessionManager.enroll`,
|
|
9
|
+
* which provisions the wallet and calls this editor internally. This surface
|
|
10
|
+
* exists for runtimes that already own and select the profile public keys.
|
|
11
|
+
*/
|
|
12
|
+
export function createProfileDeviceActivationRequest(input) {
|
|
13
|
+
const activationCode = requiredText(input.activationCode, 'activation code');
|
|
14
|
+
const idToken = requiredText(input.idToken, 'signed identity token');
|
|
15
|
+
let clientInstanceId = '';
|
|
16
|
+
let clientName = '';
|
|
17
|
+
let applicationType;
|
|
18
|
+
let redirectUris = [];
|
|
19
|
+
let publicJwks = [];
|
|
20
|
+
let deviceName = '';
|
|
21
|
+
let actorDid = '';
|
|
22
|
+
let profileDid = '';
|
|
23
|
+
let timeoutSeconds;
|
|
24
|
+
let intervalSeconds;
|
|
25
|
+
const draft = {
|
|
26
|
+
setClientInstanceId(value) { clientInstanceId = normalizedText(value); return draft; },
|
|
27
|
+
setClientName(value) { clientName = normalizedText(value); return draft; },
|
|
28
|
+
setApplicationType(value) { applicationType = value; return draft; },
|
|
29
|
+
setRedirectUris(values) { redirectUris = uniqueText(values); return draft; },
|
|
30
|
+
setPublicJwks(values) { publicJwks = values.map((value) => ({ ...value })); return draft; },
|
|
31
|
+
setDeviceName(value) { deviceName = normalizedText(value); return draft; },
|
|
32
|
+
setActorDid(value) { actorDid = normalizedText(value); return draft; },
|
|
33
|
+
setProfileDid(value) { profileDid = normalizedText(value); return draft; },
|
|
34
|
+
setTimeoutSeconds(value) { timeoutSeconds = positiveNumber(value, 'timeout seconds'); return draft; },
|
|
35
|
+
setIntervalSeconds(value) { intervalSeconds = positiveNumber(value, 'interval seconds'); return draft; },
|
|
36
|
+
build() {
|
|
37
|
+
const registration = {
|
|
38
|
+
clientInstanceId: requiredText(clientInstanceId, 'client instance id'),
|
|
39
|
+
clientName: requiredText(clientName, 'client name'),
|
|
40
|
+
applicationType: applicationType || failRequired('application type'),
|
|
41
|
+
redirectUris: requiredList(redirectUris, 'redirect URI'),
|
|
42
|
+
publicJwks: requiredList(publicJwks, 'public JWK'),
|
|
43
|
+
...(deviceName ? { deviceName } : {}),
|
|
44
|
+
...(actorDid ? { actorDid } : {}),
|
|
45
|
+
...(profileDid ? { profileDid } : {}),
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
...input,
|
|
49
|
+
activationCode,
|
|
50
|
+
idToken,
|
|
51
|
+
deviceRegistration: registration,
|
|
52
|
+
...(timeoutSeconds !== undefined ? { timeoutSeconds } : {}),
|
|
53
|
+
...(intervalSeconds !== undefined ? { intervalSeconds } : {}),
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
return draft;
|
|
58
|
+
}
|
|
5
59
|
export async function revokeEmployeeDeviceWithDeps(deps) {
|
|
6
60
|
const licenseId = String(deps.input.licenseId || '').trim();
|
|
7
61
|
const clientId = String(deps.input.clientId || '').trim();
|
|
@@ -28,6 +82,16 @@ export async function activateEmployeeDeviceWithActivationCodeWithDeps(deps) {
|
|
|
28
82
|
[IdentityAuthRequestFields.ClientInstanceId]: clientInstanceId,
|
|
29
83
|
};
|
|
30
84
|
const exchange = await deps.submitAndPollWithBearerToken(deps.input.idToken, deps.identityTokenExchangePath(deps.routeCtx), deps.identityTokenExchangePollPath(deps.routeCtx), exchangePayload, deps.input.pollOptions);
|
|
85
|
+
const exchangeDiagnostics = firstFailedOperationOutcomeDiagnostic(exchange.poll.body)
|
|
86
|
+
|| firstFailedOperationOutcomeDiagnostic(exchange.submit.body);
|
|
87
|
+
if (exchangeDiagnostics) {
|
|
88
|
+
throw new Error(`activateEmployeeDeviceWithActivationCode: exchange failed: ${exchangeDiagnostics}`);
|
|
89
|
+
}
|
|
90
|
+
const failedExchangeStatus = [exchange.submit.status, exchange.poll.status]
|
|
91
|
+
.find((status) => status < 200 || status >= 300);
|
|
92
|
+
if (failedExchangeStatus !== undefined) {
|
|
93
|
+
throw new Error(`activateEmployeeDeviceWithActivationCode: exchange failed (HTTP ${failedExchangeStatus}).`);
|
|
94
|
+
}
|
|
31
95
|
const pollBody = exchange.poll.body || {};
|
|
32
96
|
const exchangeBody = (pollBody.body || pollBody);
|
|
33
97
|
const initialAccessToken = String(exchangeBody[IdentityAuthResponseFields.InitialAccessToken]
|
|
@@ -56,10 +120,58 @@ export async function activateEmployeeDeviceWithActivationRequestWithDeps(deps)
|
|
|
56
120
|
return deps.activateEmployeeDeviceWithActivationCode(deps.routeCtx, {
|
|
57
121
|
activationCode: deps.input.activationCode,
|
|
58
122
|
idToken: deps.input.idToken,
|
|
59
|
-
dcrPayload: deps.input
|
|
123
|
+
dcrPayload: resolveDcrPayload(deps.input),
|
|
60
124
|
pollOptions,
|
|
61
125
|
});
|
|
62
126
|
}
|
|
127
|
+
function resolveDcrPayload(input) {
|
|
128
|
+
if (input.deviceRegistration && input.dcrPayload) {
|
|
129
|
+
throw new Error('Device activation accepts either deviceRegistration or the deprecated dcrPayload, not both.');
|
|
130
|
+
}
|
|
131
|
+
if (input.deviceRegistration) {
|
|
132
|
+
const registration = input.deviceRegistration;
|
|
133
|
+
return {
|
|
134
|
+
[IdentityDcrMetadataFields.ApplicationType]: registration.applicationType,
|
|
135
|
+
[IdentityDcrMetadataFields.ClientName]: registration.clientName,
|
|
136
|
+
[IdentityDcrMetadataFields.RedirectUris]: [...registration.redirectUris],
|
|
137
|
+
[IdentityDcrMetadataFields.Jwks]: { keys: registration.publicJwks.map((value) => ({ ...value })) },
|
|
138
|
+
[IdentityDcrMetadataFields.ExtendedDeviceInfo]: {
|
|
139
|
+
[IdentityDeviceInfoFields.DeviceId]: registration.clientInstanceId,
|
|
140
|
+
device_name: registration.deviceName || registration.clientName,
|
|
141
|
+
},
|
|
142
|
+
...(registration.actorDid ? { [IdentityDcrMetadataFields.ActorDid]: registration.actorDid } : {}),
|
|
143
|
+
...(registration.profileDid ? { [IdentityDcrMetadataFields.ProfileDid]: registration.profileDid } : {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (input.dcrPayload)
|
|
147
|
+
return { ...input.dcrPayload };
|
|
148
|
+
throw new Error('Device activation requires a device registration built by createProfileDeviceActivationRequest.');
|
|
149
|
+
}
|
|
150
|
+
function normalizedText(value) {
|
|
151
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
152
|
+
}
|
|
153
|
+
function requiredText(value, label) {
|
|
154
|
+
const normalized = normalizedText(value);
|
|
155
|
+
if (!normalized)
|
|
156
|
+
throw new Error(`Profile device activation requires ${label}.`);
|
|
157
|
+
return normalized;
|
|
158
|
+
}
|
|
159
|
+
function uniqueText(values) {
|
|
160
|
+
return [...new Set((values || []).map((value) => normalizedText(value)).filter(Boolean))];
|
|
161
|
+
}
|
|
162
|
+
function requiredList(values, label) {
|
|
163
|
+
if (!values.length)
|
|
164
|
+
throw new Error(`Profile device activation requires at least one ${label}.`);
|
|
165
|
+
return [...values];
|
|
166
|
+
}
|
|
167
|
+
function positiveNumber(value, label) {
|
|
168
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
169
|
+
throw new Error(`Profile device activation ${label} must be positive.`);
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
function failRequired(label) {
|
|
173
|
+
throw new Error(`Profile device activation requires ${label}.`);
|
|
174
|
+
}
|
|
63
175
|
function createRuntimeUuid() {
|
|
64
176
|
const fromCrypto = globalThis.crypto?.randomUUID?.();
|
|
65
177
|
if (fromCrypto) {
|
|
@@ -67,3 +179,47 @@ function createRuntimeUuid() {
|
|
|
67
179
|
}
|
|
68
180
|
return `fallback-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
69
181
|
}
|
|
182
|
+
/** Preserves a terminal async GW diagnostic before checking success fields. */
|
|
183
|
+
function firstFailedOperationOutcomeDiagnostic(value) {
|
|
184
|
+
if (typeof value === 'string') {
|
|
185
|
+
const candidate = value.trim();
|
|
186
|
+
if (!candidate || (candidate[0] !== '{' && candidate[0] !== '['))
|
|
187
|
+
return undefined;
|
|
188
|
+
try {
|
|
189
|
+
return firstFailedOperationOutcomeDiagnostic(JSON.parse(candidate));
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (!value || typeof value !== 'object')
|
|
196
|
+
return undefined;
|
|
197
|
+
if (Array.isArray(value)) {
|
|
198
|
+
for (const child of value) {
|
|
199
|
+
const diagnostic = firstFailedOperationOutcomeDiagnostic(child);
|
|
200
|
+
if (diagnostic)
|
|
201
|
+
return diagnostic;
|
|
202
|
+
}
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
const record = value;
|
|
206
|
+
if (record.resourceType === 'OperationOutcome' && Array.isArray(record.issue)) {
|
|
207
|
+
for (const rawIssue of record.issue) {
|
|
208
|
+
if (!rawIssue || typeof rawIssue !== 'object')
|
|
209
|
+
continue;
|
|
210
|
+
const issue = rawIssue;
|
|
211
|
+
const severity = String(issue.severity || '').trim().toLowerCase();
|
|
212
|
+
if (severity !== 'error' && severity !== 'fatal')
|
|
213
|
+
continue;
|
|
214
|
+
const diagnostic = String(issue.diagnostics || '').trim();
|
|
215
|
+
if (diagnostic)
|
|
216
|
+
return diagnostic;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
for (const child of Object.values(record)) {
|
|
220
|
+
const diagnostic = firstFailedOperationOutcomeDiagnostic(child);
|
|
221
|
+
if (diagnostic)
|
|
222
|
+
return diagnostic;
|
|
223
|
+
}
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
package/dist/digital-twin.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ import type { MetaTagCoding } from 'gdc-common-utils-ts/models/confidential-stor
|
|
|
3
3
|
import type { RouteContext } from './individual-onboarding.js';
|
|
4
4
|
import type { PollOptions, SubmitAndPollResult } from './orchestration/client-port.js';
|
|
5
5
|
export type DigitalTwinFhirFormat = 'org.hl7.fhir.r4' | 'org.hl7.fhir.api';
|
|
6
|
+
/** Returns whether a value can be used as a pseudonymous digital-twin subject. */
|
|
7
|
+
export declare function isDigitalTwinSubjectId(value: unknown): value is string;
|
|
8
|
+
/** Rejects operational DIDs and malformed or caller-invented subject shapes. */
|
|
9
|
+
export declare function assertDigitalTwinSubjectId(value: unknown): asserts value is string;
|
|
6
10
|
/** Search parameters added by the digital-twin Composition profile. */
|
|
7
11
|
export declare const DigitalTwinSearchParameter: Readonly<{
|
|
8
12
|
Section: "section";
|
package/dist/digital-twin.js
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { HealthcareDocumentTypes } from 'gdc-common-utils-ts/constants';
|
|
4
4
|
import { CompositionClaim } from 'gdc-common-utils-ts/models';
|
|
5
|
+
const DIGITAL_TWIN_SUBJECT_URN_UUID = /^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
6
|
+
/** Returns whether a value can be used as a pseudonymous digital-twin subject. */
|
|
7
|
+
export function isDigitalTwinSubjectId(value) {
|
|
8
|
+
return DIGITAL_TWIN_SUBJECT_URN_UUID.test(String(value || '').trim());
|
|
9
|
+
}
|
|
10
|
+
/** Rejects operational DIDs and malformed or caller-invented subject shapes. */
|
|
11
|
+
export function assertDigitalTwinSubjectId(value) {
|
|
12
|
+
if (!isDigitalTwinSubjectId(value)) {
|
|
13
|
+
throw new Error('Digital twin subject must be a valid urn:uuid identifier.');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
5
16
|
/** Search parameters added by the digital-twin Composition profile. */
|
|
6
17
|
export const DigitalTwinSearchParameter = Object.freeze({
|
|
7
18
|
Section: 'section',
|
|
@@ -61,6 +72,9 @@ export function readDigitalTwinSearchResult(operation) {
|
|
|
61
72
|
throw new Error('Digital twin search did not return a Composition result set.');
|
|
62
73
|
}
|
|
63
74
|
const matches = responseEntry.resource.data;
|
|
75
|
+
for (const match of matches) {
|
|
76
|
+
assertDigitalTwinSubjectId(match?.[CompositionClaim.Subject]);
|
|
77
|
+
}
|
|
64
78
|
const parsedTotal = Number(responseEntry.resource.total);
|
|
65
79
|
return {
|
|
66
80
|
total: Number.isFinite(parsedTotal) ? parsedTotal : matches.length,
|
|
@@ -73,8 +87,7 @@ export async function saveDigitalTwinSelectionWithDeps(ctx, input, deps) {
|
|
|
73
87
|
const twinSubjectId = String(input.twinSubjectId || '').trim();
|
|
74
88
|
const section = String(input.section || '').trim();
|
|
75
89
|
const authorDid = String(input.authorDid || '').trim();
|
|
76
|
-
|
|
77
|
-
throw new Error('twinSubjectId is required.');
|
|
90
|
+
assertDigitalTwinSubjectId(twinSubjectId);
|
|
78
91
|
if (!section)
|
|
79
92
|
throw new Error('Digital twin selection section is required.');
|
|
80
93
|
if (!authorDid)
|
|
@@ -146,8 +159,7 @@ export async function searchDigitalTwinsWithDeps(ctx, input, deps) {
|
|
|
146
159
|
/** Materializes one selected pseudonymous twin as a research summary Bundle. */
|
|
147
160
|
export async function materializeDigitalTwinWithDeps(ctx, input, deps) {
|
|
148
161
|
const twinSubjectId = String(input.twinSubjectId || '').trim();
|
|
149
|
-
|
|
150
|
-
throw new Error('twinSubjectId is required.');
|
|
162
|
+
assertDigitalTwinSubjectId(twinSubjectId);
|
|
151
163
|
const format = input.format || 'org.hl7.fhir.r4';
|
|
152
164
|
const thid = String(input.thid || randomUUID());
|
|
153
165
|
const parameters = [
|
|
@@ -7,3 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export { confirmLegalOrganizationOrderWithDeps, HostLifecycleRequestType, HostedTenantLifecycleRequestType, submitHostedTenantLifecycleWithDeps, } from 'gdc-sdk-core-ts';
|
|
9
9
|
export type { HostingControllerFacade, HostLifecycleInput, HostRouteContext, HostedTenantLifecycleInput, LegalOrganizationOrderInput, } from 'gdc-sdk-core-ts';
|
|
10
|
+
/** Descendant groups that a controller can clean explicitly before tenant shutdown. */
|
|
11
|
+
/**
|
|
12
|
+
* Tenant-owned subject group supported by the generic cleanup endpoint.
|
|
13
|
+
* Employees use their dedicated encrypted lifecycle so license release and
|
|
14
|
+
* audit metadata cannot be bypassed.
|
|
15
|
+
*/
|
|
16
|
+
export type HostedTenantDescendantKind = 'individuals';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CryptographyService } from 'gdc-common-utils-ts/CryptographyService';
|
|
2
2
|
import type { ICryptoHelper } from 'gdc-common-utils-ts/interfaces/ICryptoHelper';
|
|
3
|
-
import type { IWallet, WalletAlgorithm, WalletCompactJweRequest, WalletCompactJwsRequest, WalletDetachedJwsRequest, WalletExecutionContext, WalletKeyDescriptor, WalletKeyPurpose, WalletKeySelection, WalletPackOptions, WalletProvisionRequest, WalletUnpackOptions } from 'gdc-sdk-core-ts';
|
|
3
|
+
import type { IWallet, WalletAlgorithm, WalletCompactJweRequest, WalletCompactJwsRequest, WalletDetachedJwsRequest, WalletExecutionContext, WalletKeyDescriptor, WalletKeyPurpose, WalletKeySelection, WalletPackOptions, WalletProvisionMode, WalletProvisionRequest, WalletUnpackOptions } from 'gdc-sdk-core-ts';
|
|
4
4
|
import type { JWK, JwkSet } from 'gdc-common-utils-ts/models/jwk';
|
|
5
5
|
export type NodeManagedWalletPolicy = {
|
|
6
6
|
defaults: Partial<Record<WalletKeyPurpose, WalletAlgorithm>>;
|
|
@@ -11,6 +11,20 @@ export type NodeManagedWalletOptions = {
|
|
|
11
11
|
resolveRecipientJwk?: (recipientDid: string) => Promise<JWK>;
|
|
12
12
|
policy?: Partial<NodeManagedWalletPolicy>;
|
|
13
13
|
};
|
|
14
|
+
export type NodeCommunicationWalletInitialization = {
|
|
15
|
+
/**
|
|
16
|
+
* Stable secret used to reconstruct the same communication keys after a
|
|
17
|
+
* process restart. Persist it only through the portal's protected wallet
|
|
18
|
+
* store; never send it to ICA or GW.
|
|
19
|
+
*/
|
|
20
|
+
seedMaterial?: string | Uint8Array;
|
|
21
|
+
/**
|
|
22
|
+
* Defaults to deterministic when seedMaterial is present, otherwise random.
|
|
23
|
+
* Random keys are suitable only when the wallet implementation itself is
|
|
24
|
+
* durably persisted.
|
|
25
|
+
*/
|
|
26
|
+
mode?: WalletProvisionMode;
|
|
27
|
+
};
|
|
14
28
|
/**
|
|
15
29
|
* Node-focused managed wallet implementation for BFF, portal, and backend flows.
|
|
16
30
|
*
|
|
@@ -20,6 +34,11 @@ export type NodeManagedWalletOptions = {
|
|
|
20
34
|
* - OpenID/JWT signing
|
|
21
35
|
* - DIDComm-style transport wrapping
|
|
22
36
|
* - confidential document protection
|
|
37
|
+
*
|
|
38
|
+
* OpenID boundary: managing an `openid-id-token-signing` key and producing a
|
|
39
|
+
* compact JWT does not turn this wallet into an OpenID Provider. The hosting
|
|
40
|
+
* portal/BFF must authenticate the account, verify any asserted email, publish
|
|
41
|
+
* provider metadata and JWKS, and be explicitly trusted by the receiving GW.
|
|
23
42
|
*/
|
|
24
43
|
export declare class NodeManagedWallet implements IWallet {
|
|
25
44
|
private readonly cryptoHelper;
|
|
@@ -47,6 +66,27 @@ export declare class NodeManagedWallet implements IWallet {
|
|
|
47
66
|
* Returns the currently available public JWKs for the selected context and filter.
|
|
48
67
|
*/
|
|
49
68
|
getPublicJwks(context?: WalletExecutionContext, filter?: WalletKeySelection): Promise<WalletKeyDescriptor[]>;
|
|
69
|
+
/**
|
|
70
|
+
* Initializes the runtime communication wallet and returns exactly the
|
|
71
|
+
* public signing/encryption JWKS accepted as `controller.publicKeys` by the
|
|
72
|
+
* legacy organization activation builder.
|
|
73
|
+
*
|
|
74
|
+
* This does not replace the professional-role signing key supplied as
|
|
75
|
+
* `controller.publicSignKey`. The role key identifies the historical legal
|
|
76
|
+
* representative; these runtime keys protect DIDComm communications.
|
|
77
|
+
*
|
|
78
|
+
* The caller owns durable wallet custody. When deterministic provisioning is
|
|
79
|
+
* used, protect `seedMaterial` in the portal wallet store (for example behind
|
|
80
|
+
* the user's PIN and a server-held KEK) so the same private keys can be
|
|
81
|
+
* reconstructed after restart. ICA and GW receive only this returned public
|
|
82
|
+
* JWKS.
|
|
83
|
+
*/
|
|
84
|
+
initializeCommunicationJsonWebKeySet(context: WalletExecutionContext, options?: NodeCommunicationWalletInitialization): Promise<JwkSet>;
|
|
85
|
+
/**
|
|
86
|
+
* Returns only the public DIDComm signing/encryption keys for a previously
|
|
87
|
+
* initialized runtime wallet. Private material never leaves the wallet.
|
|
88
|
+
*/
|
|
89
|
+
getCommunicationJsonWebKeySet(context: WalletExecutionContext): Promise<JwkSet>;
|
|
50
90
|
/**
|
|
51
91
|
* Computes a digest of a string using the configured runtime helper.
|
|
52
92
|
*/
|
|
@@ -25,6 +25,11 @@ const DEFAULT_POLICY = {
|
|
|
25
25
|
* - OpenID/JWT signing
|
|
26
26
|
* - DIDComm-style transport wrapping
|
|
27
27
|
* - confidential document protection
|
|
28
|
+
*
|
|
29
|
+
* OpenID boundary: managing an `openid-id-token-signing` key and producing a
|
|
30
|
+
* compact JWT does not turn this wallet into an OpenID Provider. The hosting
|
|
31
|
+
* portal/BFF must authenticate the account, verify any asserted email, publish
|
|
32
|
+
* provider metadata and JWKS, and be explicitly trusted by the receiving GW.
|
|
28
33
|
*/
|
|
29
34
|
export class NodeManagedWallet {
|
|
30
35
|
/**
|
|
@@ -128,6 +133,53 @@ export class NodeManagedWallet {
|
|
|
128
133
|
}
|
|
129
134
|
return descriptors;
|
|
130
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Initializes the runtime communication wallet and returns exactly the
|
|
138
|
+
* public signing/encryption JWKS accepted as `controller.publicKeys` by the
|
|
139
|
+
* legacy organization activation builder.
|
|
140
|
+
*
|
|
141
|
+
* This does not replace the professional-role signing key supplied as
|
|
142
|
+
* `controller.publicSignKey`. The role key identifies the historical legal
|
|
143
|
+
* representative; these runtime keys protect DIDComm communications.
|
|
144
|
+
*
|
|
145
|
+
* The caller owns durable wallet custody. When deterministic provisioning is
|
|
146
|
+
* used, protect `seedMaterial` in the portal wallet store (for example behind
|
|
147
|
+
* the user's PIN and a server-held KEK) so the same private keys can be
|
|
148
|
+
* reconstructed after restart. ICA and GW receive only this returned public
|
|
149
|
+
* JWKS.
|
|
150
|
+
*/
|
|
151
|
+
async initializeCommunicationJsonWebKeySet(context, options = {}) {
|
|
152
|
+
if (!context.runtime?.runtimeId) {
|
|
153
|
+
throw new Error('Communication wallet initialization requires context.runtime.runtimeId.');
|
|
154
|
+
}
|
|
155
|
+
await this.provisionManagedKeys(context, {
|
|
156
|
+
ownerScope: 'runtime',
|
|
157
|
+
purposes: ['comm-signing', 'comm-encryption'],
|
|
158
|
+
...(options.seedMaterial !== undefined ? { seedMaterial: options.seedMaterial } : {}),
|
|
159
|
+
mode: options.mode ?? (options.seedMaterial !== undefined ? 'deterministic' : 'random'),
|
|
160
|
+
});
|
|
161
|
+
return this.getCommunicationJsonWebKeySet(context);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Returns only the public DIDComm signing/encryption keys for a previously
|
|
165
|
+
* initialized runtime wallet. Private material never leaves the wallet.
|
|
166
|
+
*/
|
|
167
|
+
async getCommunicationJsonWebKeySet(context) {
|
|
168
|
+
if (!context.runtime?.runtimeId) {
|
|
169
|
+
throw new Error('Communication JWKS lookup requires context.runtime.runtimeId.');
|
|
170
|
+
}
|
|
171
|
+
const descriptors = await this.getPublicJwks(context, { ownerScope: 'runtime' });
|
|
172
|
+
return {
|
|
173
|
+
keys: descriptors
|
|
174
|
+
.filter((entry) => entry.purpose === 'comm-signing' || entry.purpose === 'comm-encryption')
|
|
175
|
+
.map((entry) => ({
|
|
176
|
+
...entry.publicJwk,
|
|
177
|
+
kid: entry.publicJwk.kid ?? entry.kid,
|
|
178
|
+
use: entry.use,
|
|
179
|
+
alg: entry.publicJwk.alg ?? entry.alg,
|
|
180
|
+
})),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
131
183
|
/**
|
|
132
184
|
* Computes a digest of a string using the configured runtime helper.
|
|
133
185
|
*/
|