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.
Files changed (32) hide show
  1. package/README.md +18 -7
  2. package/dist/constants/lifecycle.d.ts +8 -0
  3. package/dist/constants/lifecycle.js +8 -0
  4. package/dist/device-activation.d.ts +48 -1
  5. package/dist/device-activation.js +157 -1
  6. package/dist/digital-twin.d.ts +4 -0
  7. package/dist/digital-twin.js +16 -4
  8. package/dist/host-onboarding.d.ts +7 -0
  9. package/dist/node-managed-wallet.d.ts +41 -1
  10. package/dist/node-managed-wallet.js +52 -0
  11. package/dist/node-runtime-client.d.ts +39 -4
  12. package/dist/node-runtime-client.js +142 -51
  13. package/dist/orchestration/client-port.d.ts +11 -1
  14. package/dist/orchestration/digital-twin-sdk.js +3 -1
  15. package/dist/orchestration/host-onboarding-sdk.d.ts +9 -3
  16. package/dist/orchestration/host-onboarding-sdk.js +10 -1
  17. package/dist/orchestration/individual-controller-sdk.d.ts +29 -3
  18. package/dist/orchestration/individual-controller-sdk.js +48 -2
  19. package/dist/orchestration/organization-controller-sdk.d.ts +19 -6
  20. package/dist/orchestration/organization-controller-sdk.js +27 -6
  21. package/dist/organization-controller-recovery.d.ts +4 -2
  22. package/dist/organization-controller-recovery.js +14 -8
  23. package/dist/organization-employee-lifecycle.d.ts +4 -0
  24. package/dist/organization-employee-lifecycle.js +17 -1
  25. package/dist/organization-license-order.d.ts +8 -3
  26. package/dist/resource-operations.d.ts +67 -0
  27. package/dist/resource-operations.js +62 -1
  28. package/dist/runtime-client-paths.d.ts +10 -0
  29. package/dist/runtime-client-paths.js +10 -0
  30. package/dist/server-profile-session.d.ts +32 -3
  31. package/dist/server-profile-session.js +21 -17
  32. 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 current live baseline implemented by the Node runtime client
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`, `DigitalTwinGenerationInput`
642
- - functions: `createOrganizationEmployeeWithDeps(...)`, `importIpsOrFhirAndUpdateIndexWithDeps(...)`, `upsertRelatedPersonAndPollWithDeps(...)`, `ingestCommunicationAndUpdateIndexWithDeps(...)`, `searchClinicalBundleWithDeps(...)`, `searchLatestIpsWithDeps(...)`, `grantProfessionalAccessWithDeps(...)`, `generateDigitalTwinFromSubjectDataWithDeps(...)`
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 production-shaped enough
793
- - the current GW `_exchange` path expects at least:
794
- - syntactically valid JWT format
795
- - `tenant_id` claim matching the target tenant
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
- dcrPayload: Record<string, unknown>;
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.dcrPayload,
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
+ }
@@ -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";
@@ -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
- if (!twinSubjectId)
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
- if (!twinSubjectId)
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
  */