gdc-sdk-node-ts 2.2.2 → 2.3.1
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/dist/backend-profile-runtime.d.ts +23 -0
- package/dist/backend-profile-runtime.js +23 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/individual-start.d.ts +33 -0
- package/dist/individual-start.js +45 -2
- package/dist/node-runtime-client.d.ts +39 -4
- package/dist/node-runtime-client.js +124 -4
- package/dist/orchestration/client-port.d.ts +4 -1
- package/dist/orchestration/individual-controller-sdk.d.ts +10 -1
- package/dist/orchestration/individual-controller-sdk.js +15 -0
- package/dist/orchestration/individual-member-sdk.d.ts +10 -1
- package/dist/orchestration/individual-member-sdk.js +15 -0
- package/dist/resource-operations.d.ts +111 -4
- package/dist/resource-operations.js +83 -3
- package/dist/runtime-client-paths.d.ts +6 -2
- package/dist/runtime-client-paths.js +4 -0
- package/dist/runtime-transport.d.ts +13 -1
- package/dist/runtime-transport.js +18 -3
- package/dist/server-profile-protection.d.ts +64 -0
- package/dist/server-profile-protection.js +110 -0
- package/dist/server-profile-session.d.ts +115 -0
- package/dist/server-profile-session.js +259 -0
- package/dist/session.d.ts +7 -0
- package/dist/session.js +10 -0
- package/package.json +3 -3
- package/dist/legal-organization-onboarding-facade.d.ts +0 -70
- package/dist/legal-organization-onboarding-facade.js +0 -169
- package/dist/lifecycle-examples.d.ts +0 -7
- package/dist/lifecycle-examples.js +0 -8
- package/dist/runtime-software-proof.d.ts +0 -51
- package/dist/runtime-software-proof.js +0 -34
- package/dist/simple-device-activation.d.ts +0 -44
- package/dist/simple-device-activation.js +0 -44
- package/dist/simple-host-onboarding.d.ts +0 -27
- package/dist/simple-host-onboarding.js +0 -39
- package/dist/simple-individual-onboarding.d.ts +0 -27
- package/dist/simple-individual-onboarding.js +0 -38
- package/dist/simple-individual-start.d.ts +0 -45
- package/dist/simple-individual-start.js +0 -59
- package/dist/simple-poll-options.d.ts +0 -5
- package/dist/simple-poll-options.js +0 -17
- package/dist/simple-smart-token.d.ts +0 -58
- package/dist/simple-smart-token.js +0 -85
|
@@ -2,7 +2,7 @@ import { type IndividualMemberCredentialInput, type IndividualMemberVpPayloadInp
|
|
|
2
2
|
import { type NodeRuntimeClient } from './client-port.js';
|
|
3
3
|
import type { SmartTokenExchangeResult, SmartTokenRequestInput } from '../smart-token.js';
|
|
4
4
|
import type { RouteContext } from '../individual-onboarding.js';
|
|
5
|
-
import type { RelatedPersonUpsertInput } from '../resource-operations.js';
|
|
5
|
+
import type { ClinicalBundleSearchInput, CommunicationIngestionInput, RelatedPersonUpsertInput } from '../resource-operations.js';
|
|
6
6
|
export declare class IndividualMemberSdk {
|
|
7
7
|
private readonly client;
|
|
8
8
|
constructor(client: NodeRuntimeClient);
|
|
@@ -15,6 +15,15 @@ export declare class IndividualMemberSdk {
|
|
|
15
15
|
* caregiver, guardian, or family member.
|
|
16
16
|
*/
|
|
17
17
|
requestSmartToken(input: SmartTokenRequestInput): Promise<SmartTokenExchangeResult>;
|
|
18
|
+
/**
|
|
19
|
+
* Writes clinical content for a subject only after GW authorizes this member's
|
|
20
|
+
* SMART token and accepted RelatedPerson/Consent relationship.
|
|
21
|
+
*/
|
|
22
|
+
ingestCommunicationAndUpdateIndex(ctx: RouteContext, input: CommunicationIngestionInput): Promise<import("gdc-sdk-core-ts/polling-model.js").SubmitAndPollResult>;
|
|
23
|
+
/** Reads subject-scoped clinical documents permitted to this member. */
|
|
24
|
+
searchClinicalBundle(ctx: RouteContext, input: ClinicalBundleSearchInput): Promise<import("gdc-sdk-core-ts/polling-model.js").SubmitAndPollResult>;
|
|
25
|
+
/** Reads the latest permitted IPS document for the selected subject. */
|
|
26
|
+
getLatestIps(ctx: RouteContext, input: Omit<ClinicalBundleSearchInput, 'includedTypes'>): Promise<import("gdc-sdk-core-ts/polling-model.js").SubmitAndPollResult>;
|
|
18
27
|
/**
|
|
19
28
|
* Returns the normalized public continuity aliases that would be embedded in
|
|
20
29
|
* the individual-member identity VC for SMART/OpenID4VP flows.
|
|
@@ -18,6 +18,21 @@ export class IndividualMemberSdk {
|
|
|
18
18
|
requestSmartToken(input) {
|
|
19
19
|
return requireClientMethod(this.client, 'requestSmartToken')(input);
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Writes clinical content for a subject only after GW authorizes this member's
|
|
23
|
+
* SMART token and accepted RelatedPerson/Consent relationship.
|
|
24
|
+
*/
|
|
25
|
+
ingestCommunicationAndUpdateIndex(ctx, input) {
|
|
26
|
+
return requireClientMethod(this.client, 'ingestCommunicationAndUpdateIndex')(ctx, input);
|
|
27
|
+
}
|
|
28
|
+
/** Reads subject-scoped clinical documents permitted to this member. */
|
|
29
|
+
searchClinicalBundle(ctx, input) {
|
|
30
|
+
return requireClientMethod(this.client, 'searchClinicalBundle')(ctx, input);
|
|
31
|
+
}
|
|
32
|
+
/** Reads the latest permitted IPS document for the selected subject. */
|
|
33
|
+
getLatestIps(ctx, input) {
|
|
34
|
+
return requireClientMethod(this.client, 'getLatestIps')(ctx, input);
|
|
35
|
+
}
|
|
21
36
|
/**
|
|
22
37
|
* Returns the normalized public continuity aliases that would be embedded in
|
|
23
38
|
* the individual-member identity VC for SMART/OpenID4VP flows.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { DeviceAppType } from 'gdc-common-utils-ts/constants/device';
|
|
1
2
|
import type { LicenseOfferSearchState, LicenseOrderSearchState } from 'gdc-common-utils-ts/utils/license-commercial-search';
|
|
2
3
|
import type { LicenseListSearchState } from 'gdc-common-utils-ts/utils/license-list-search';
|
|
3
|
-
import type { BundleSearchQuery, CommunicationInput, DateRange, EmployeeSearchValue, IndividualOrganizationLifecycleInput } from 'gdc-sdk-core-ts';
|
|
4
|
+
import type { BundleSearchQuery, CommMsgExtendedCommunicationOutboxJob, CommunicationOutboxJob, CommunicationInput, DateRange, EmployeeSearchValue, IndividualOrganizationLifecycleInput, TransportProfile } from 'gdc-sdk-core-ts';
|
|
4
5
|
import type { BundleEntry, BundleJsonApi } from 'gdc-common-utils-ts/models/bundle';
|
|
5
6
|
import type { SubmitAndPollResult } from './orchestration/client-port.js';
|
|
6
7
|
import type { RouteContext } from './individual-onboarding.js';
|
|
@@ -85,6 +86,52 @@ export type LicenseListRuntimeSearchInput = {
|
|
|
85
86
|
intervalMs?: number;
|
|
86
87
|
};
|
|
87
88
|
};
|
|
89
|
+
/**
|
|
90
|
+
* Reserves one individual-organization seat for an existing RelatedPerson.
|
|
91
|
+
*
|
|
92
|
+
* Only FHIR v3 RoleCode members belong here. ISCO professionals receive
|
|
93
|
+
* Consent/Communication access but keep using the license paid by their
|
|
94
|
+
* professional organization.
|
|
95
|
+
*/
|
|
96
|
+
export type IndividualMemberLicenseInvitationInput = {
|
|
97
|
+
ownerOrganizationId: string;
|
|
98
|
+
/** Exact card/subject DID granted after the invitation is accepted. */
|
|
99
|
+
subjectDid: string;
|
|
100
|
+
relatedPersonId: string;
|
|
101
|
+
invitationId: string;
|
|
102
|
+
role: string;
|
|
103
|
+
email?: string;
|
|
104
|
+
telephone?: string;
|
|
105
|
+
type?: DeviceAppType;
|
|
106
|
+
requestThid?: string;
|
|
107
|
+
pollOptions?: {
|
|
108
|
+
timeoutMs?: number;
|
|
109
|
+
intervalMs?: number;
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
/** Adds zero-cost member seats to one individual organization. */
|
|
113
|
+
export type IndividualMemberLicenseAddInput = {
|
|
114
|
+
ownerOrganizationId: string;
|
|
115
|
+
quantity: number;
|
|
116
|
+
requestThid?: string;
|
|
117
|
+
pollOptions?: {
|
|
118
|
+
timeoutMs?: number;
|
|
119
|
+
intervalMs?: number;
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
/** Input shared by invitation acceptance, deactivation and release. */
|
|
123
|
+
export type IndividualMemberLicenseTransitionInput = {
|
|
124
|
+
/** Required for controller deactivation/release; acceptance resolves by code. */
|
|
125
|
+
ownerOrganizationId?: string;
|
|
126
|
+
activationCode: string;
|
|
127
|
+
subjectId?: string;
|
|
128
|
+
verifiedActorIdentifier?: string;
|
|
129
|
+
requestThid?: string;
|
|
130
|
+
pollOptions?: {
|
|
131
|
+
timeoutMs?: number;
|
|
132
|
+
intervalMs?: number;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
88
135
|
/**
|
|
89
136
|
* Runtime search/list input for commercial offer read-models exposed through
|
|
90
137
|
* actor facades.
|
|
@@ -144,6 +191,13 @@ export type IpsOrFhirImportInput = {
|
|
|
144
191
|
};
|
|
145
192
|
};
|
|
146
193
|
export type RelatedPersonUpsertInput = {
|
|
194
|
+
/**
|
|
195
|
+
* Canonical RelatedPerson bundle. Keep kinship/legal relationship in
|
|
196
|
+
* `RelatedPerson.relationship`; the optional GDC `RelatedPerson.role`
|
|
197
|
+
* extension may carry comma-separated functions such as
|
|
198
|
+
* `CAREGIVER,ECON,DEPEN,BILL`.
|
|
199
|
+
* Access decisions such as PERMITTED do not belong in either claim.
|
|
200
|
+
*/
|
|
147
201
|
relatedPersonPayload: {
|
|
148
202
|
thid?: string;
|
|
149
203
|
} & Record<string, unknown>;
|
|
@@ -153,7 +207,22 @@ export type RelatedPersonUpsertInput = {
|
|
|
153
207
|
};
|
|
154
208
|
};
|
|
155
209
|
export type CommunicationIngestionInput = {
|
|
156
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Preferred claims-first job created by
|
|
212
|
+
* `createCommunicationOutboxJobFromCommMsgExtendedDraft(...)`. Legacy
|
|
213
|
+
* `createOutboxJobFromDraft(...)` jobs remain accepted temporarily.
|
|
214
|
+
*/
|
|
215
|
+
communicationJob?: CommunicationOutboxJob | CommMsgExtendedCommunicationOutboxJob;
|
|
216
|
+
/**
|
|
217
|
+
* Compatibility input for callers that already own one GW envelope.
|
|
218
|
+
* New application code should pass `communicationJob` instead.
|
|
219
|
+
*/
|
|
220
|
+
communicationPayload?: CommunicationInput & Record<string, unknown>;
|
|
221
|
+
/** Overrides the client default only for this clinical submission. */
|
|
222
|
+
transportProfile?: TransportProfile;
|
|
223
|
+
/** Clinical representation rendered from canonical claims: built-in `api`/`r4` or an extension format. */
|
|
224
|
+
clinicalFormat?: string;
|
|
225
|
+
/** @deprecated Use `clinicalFormat`. This value selects representation and route, not transport. */
|
|
157
226
|
pathFormatSegment?: 'org.hl7.fhir.api' | 'org.hl7.fhir.r4' | 'api' | 'r4' | 'fhir.r4';
|
|
158
227
|
autoConvertClaimsToFhirR4?: boolean;
|
|
159
228
|
pollOptions?: {
|
|
@@ -250,6 +319,8 @@ export type ClinicalBundleSearchInput = Omit<BundleSearchQuery, 'section' | 'sea
|
|
|
250
319
|
timeoutMs?: number;
|
|
251
320
|
intervalMs?: number;
|
|
252
321
|
};
|
|
322
|
+
/** Overrides the client default for this subject-scoped clinical read. */
|
|
323
|
+
transportProfile?: TransportProfile;
|
|
253
324
|
};
|
|
254
325
|
export type ConsentActorTargetInput = string | string[] | {
|
|
255
326
|
identifier?: string;
|
|
@@ -481,6 +552,40 @@ export declare function searchIndividualLicensesWithDeps(routeCtx: RouteContext,
|
|
|
481
552
|
* search route without mandatory filters.
|
|
482
553
|
*/
|
|
483
554
|
export declare function listIndividualLicensesWithDeps(routeCtx: RouteContext, input: LicenseListRuntimeSearchInput | undefined, deps: Parameters<typeof searchIndividualLicensesWithDeps>[2]): Promise<SubmitAndPollResult>;
|
|
555
|
+
type IndividualLicenseMutationDeps = {
|
|
556
|
+
individualLicenseActionPath: (ctx: RouteContext, action: string) => string;
|
|
557
|
+
individualLicenseActionPollPath: (ctx: RouteContext, action: string) => string;
|
|
558
|
+
submitAndPoll: (submitPath: string, pollPath: string, payload: {
|
|
559
|
+
thid?: string;
|
|
560
|
+
} & Record<string, unknown>, pollOptions?: {
|
|
561
|
+
timeoutMs?: number;
|
|
562
|
+
intervalMs?: number;
|
|
563
|
+
}) => Promise<SubmitAndPollResult>;
|
|
564
|
+
};
|
|
565
|
+
/**
|
|
566
|
+
* Reserves one available household/member seat for a selected contact.
|
|
567
|
+
*
|
|
568
|
+
* Ordered application flow:
|
|
569
|
+
* 1. Create/select RelatedPerson.
|
|
570
|
+
* 2. Author Consent rules.
|
|
571
|
+
* 3. Call this operation only for a FHIR v3 RoleCode member.
|
|
572
|
+
* 4. Put the returned activation code in the invitation Communication.
|
|
573
|
+
*/
|
|
574
|
+
export declare function issueIndividualMemberLicenseWithDeps(routeCtx: RouteContext, input: IndividualMemberLicenseInvitationInput, deps: IndividualLicenseMutationDeps): Promise<SubmitAndPollResult>;
|
|
575
|
+
/**
|
|
576
|
+
* Adds free seats without creating contacts, permissions or invitations.
|
|
577
|
+
* Price zero is explicit so GW never routes this MVP operation through a
|
|
578
|
+
* payment-proof branch.
|
|
579
|
+
*/
|
|
580
|
+
export declare function addFreeIndividualMemberLicensesWithDeps(routeCtx: RouteContext, input: IndividualMemberLicenseAddInput, deps: IndividualLicenseMutationDeps): Promise<SubmitAndPollResult>;
|
|
581
|
+
/**
|
|
582
|
+
* Accepts, deactivates or releases one member seat.
|
|
583
|
+
*
|
|
584
|
+
* `_accept` needs the Firebase/BFF verified recipient plus the authenticated
|
|
585
|
+
* subject id. `_release` remains backend-guarded and cannot free an active
|
|
586
|
+
* member directly.
|
|
587
|
+
*/
|
|
588
|
+
export declare function transitionIndividualMemberLicenseWithDeps(routeCtx: RouteContext, action: '_accept' | '_deactivate' | '_release', input: IndividualMemberLicenseTransitionInput, deps: IndividualLicenseMutationDeps): Promise<SubmitAndPollResult>;
|
|
484
589
|
export declare function searchIndividualLicenseOffersWithDeps(routeCtx: RouteContext, input: LicenseOfferRuntimeSearchInput, deps: {
|
|
485
590
|
individualLicenseOfferSearchPath: (ctx: RouteContext) => string;
|
|
486
591
|
individualLicenseOfferSearchPollPath: (ctx: RouteContext) => string;
|
|
@@ -550,8 +655,8 @@ export declare function upsertRelatedPersonAndPollWithDeps(routeCtx: RouteContex
|
|
|
550
655
|
}) => Promise<SubmitAndPollResult>;
|
|
551
656
|
}): Promise<SubmitAndPollResult>;
|
|
552
657
|
export declare function ingestCommunicationAndUpdateIndexWithDeps(routeCtx: RouteContext, input: CommunicationIngestionInput, deps: {
|
|
553
|
-
individualCommunicationBatchPath: (ctx: RouteContext, pathFormatSegment:
|
|
554
|
-
individualCommunicationPollPath: (ctx: RouteContext, pathFormatSegment:
|
|
658
|
+
individualCommunicationBatchPath: (ctx: RouteContext, pathFormatSegment: string) => string;
|
|
659
|
+
individualCommunicationPollPath: (ctx: RouteContext, pathFormatSegment: string) => string;
|
|
555
660
|
transformPayloadForFhirR4?: (payload: Record<string, unknown>, enabled: boolean) => Record<string, unknown>;
|
|
556
661
|
submitAndPoll: (submitPath: string, pollPath: string, payload: {
|
|
557
662
|
thid?: string;
|
|
@@ -651,3 +756,5 @@ export declare function generateDigitalTwinFromSubjectDataWithDeps(routeCtx: Rou
|
|
|
651
756
|
intervalMs?: number;
|
|
652
757
|
}) => Promise<SubmitAndPollResult>;
|
|
653
758
|
}): Promise<SubmitAndPollResult>;
|
|
759
|
+
export declare function normalizeCommunicationPathFormatSegment(raw?: string): string;
|
|
760
|
+
export {};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { HealthcareBasicSections, ResourceTypesFhirR4 } from 'gdc-common-utils-ts/constants';
|
|
3
3
|
import { Format } from 'gdc-common-utils-ts/constants/Schemas';
|
|
4
4
|
import { RelatedPersonClaim } from 'gdc-common-utils-ts/models/interoperable-claims/related-person-claims';
|
|
5
|
-
import { buildCommunicationParticipantSearchBundle, buildBlockchainArtifactDocumentReference, BundleQuery, createInteroperableResourceOperationEditor, IndividualOrganizationLifecycleEditor, LicenseOfferSearchEditor, LicenseOrderSearchEditor, InteroperableLifecycleStatuses, LicenseListSearchEditor, } from 'gdc-common-utils-ts';
|
|
5
|
+
import { buildCommunicationParticipantSearchBundle, buildBlockchainArtifactDocumentReference, BundleQuery, createInteroperableResourceOperationEditor, IndividualOrganizationLifecycleEditor, LicenseOfferSearchEditor, LicenseOrderSearchEditor, InteroperableLifecycleStatuses, LicenseListSearchEditor, buildLicenseIssueEntry, buildLicensePurchaseEntry, } from 'gdc-common-utils-ts';
|
|
6
6
|
import { addFhirResourceToCommunication, createCommunicationResource, } from 'gdc-sdk-core-ts';
|
|
7
7
|
import { GwCoreLifecycleRequestMethod, GwCoreLifecycleRequestType, GwCoreLifecycleTodo, } from './constants/lifecycle.js';
|
|
8
8
|
import { buildEmployeeBatchEntry, buildEmployeeSearchBundle, ConsentClaims, } from 'gdc-sdk-core-ts';
|
|
@@ -169,6 +169,81 @@ export async function searchIndividualLicensesWithDeps(routeCtx, input, deps) {
|
|
|
169
169
|
export async function listIndividualLicensesWithDeps(routeCtx, input, deps) {
|
|
170
170
|
return searchIndividualLicensesWithDeps(routeCtx, input || {}, deps);
|
|
171
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Reserves one available household/member seat for a selected contact.
|
|
174
|
+
*
|
|
175
|
+
* Ordered application flow:
|
|
176
|
+
* 1. Create/select RelatedPerson.
|
|
177
|
+
* 2. Author Consent rules.
|
|
178
|
+
* 3. Call this operation only for a FHIR v3 RoleCode member.
|
|
179
|
+
* 4. Put the returned activation code in the invitation Communication.
|
|
180
|
+
*/
|
|
181
|
+
export async function issueIndividualMemberLicenseWithDeps(routeCtx, input, deps) {
|
|
182
|
+
const entry = buildLicenseIssueEntry({
|
|
183
|
+
email: input.email,
|
|
184
|
+
telephone: input.telephone,
|
|
185
|
+
role: input.role,
|
|
186
|
+
userClass: 'individual',
|
|
187
|
+
type: input.type || 'web',
|
|
188
|
+
ownerOrganizationId: input.ownerOrganizationId,
|
|
189
|
+
relatedPersonId: input.relatedPersonId,
|
|
190
|
+
invitationId: input.invitationId,
|
|
191
|
+
});
|
|
192
|
+
entry.meta.subjectDid = input.subjectDid;
|
|
193
|
+
return deps.submitAndPoll(deps.individualLicenseActionPath(routeCtx, '_issue'), deps.individualLicenseActionPollPath(routeCtx, '_issue'), {
|
|
194
|
+
thid: input.requestThid || `individual-license-issue-${createRuntimeUuid()}`,
|
|
195
|
+
body: { resourceType: 'Bundle', type: 'batch', data: [entry] },
|
|
196
|
+
}, input.pollOptions);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Adds free seats without creating contacts, permissions or invitations.
|
|
200
|
+
* Price zero is explicit so GW never routes this MVP operation through a
|
|
201
|
+
* payment-proof branch.
|
|
202
|
+
*/
|
|
203
|
+
export async function addFreeIndividualMemberLicensesWithDeps(routeCtx, input, deps) {
|
|
204
|
+
const entry = buildLicensePurchaseEntry({
|
|
205
|
+
quantity: input.quantity,
|
|
206
|
+
userClass: 'individual',
|
|
207
|
+
type: 'web',
|
|
208
|
+
price: 0,
|
|
209
|
+
priceCurrency: 'EUR',
|
|
210
|
+
ownerOrganizationId: input.ownerOrganizationId,
|
|
211
|
+
});
|
|
212
|
+
return deps.submitAndPoll(deps.individualLicenseActionPath(routeCtx, '_add'), deps.individualLicenseActionPollPath(routeCtx, '_add'), {
|
|
213
|
+
thid: input.requestThid || `individual-license-add-${createRuntimeUuid()}`,
|
|
214
|
+
body: { resourceType: 'Bundle', type: 'batch', data: [entry] },
|
|
215
|
+
}, input.pollOptions);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Accepts, deactivates or releases one member seat.
|
|
219
|
+
*
|
|
220
|
+
* `_accept` needs the Firebase/BFF verified recipient plus the authenticated
|
|
221
|
+
* subject id. `_release` remains backend-guarded and cannot free an active
|
|
222
|
+
* member directly.
|
|
223
|
+
*/
|
|
224
|
+
export async function transitionIndividualMemberLicenseWithDeps(routeCtx, action, input, deps) {
|
|
225
|
+
return deps.submitAndPoll(deps.individualLicenseActionPath(routeCtx, action), deps.individualLicenseActionPollPath(routeCtx, action), {
|
|
226
|
+
thid: input.requestThid || `individual-license-${action.slice(1)}-${createRuntimeUuid()}`,
|
|
227
|
+
body: {
|
|
228
|
+
resourceType: 'Bundle',
|
|
229
|
+
type: 'batch',
|
|
230
|
+
data: [{
|
|
231
|
+
id: input.activationCode,
|
|
232
|
+
type: `IndividualMemberLicense${action}`,
|
|
233
|
+
meta: {
|
|
234
|
+
...(input.ownerOrganizationId ? { ownerOrganizationId: input.ownerOrganizationId } : {}),
|
|
235
|
+
...(input.subjectId ? { subjectId: input.subjectId } : {}),
|
|
236
|
+
...(input.verifiedActorIdentifier ? { verifiedActorIdentifier: input.verifiedActorIdentifier } : {}),
|
|
237
|
+
claims: {
|
|
238
|
+
'@context': 'org.schema',
|
|
239
|
+
'org.schema.IndividualProduct.serialNumber': input.activationCode,
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
request: { method: 'POST' },
|
|
243
|
+
}],
|
|
244
|
+
},
|
|
245
|
+
}, input.pollOptions);
|
|
246
|
+
}
|
|
172
247
|
export async function searchIndividualLicenseOffersWithDeps(routeCtx, input, deps) {
|
|
173
248
|
return deps.submitAndPoll(deps.individualLicenseOfferSearchPath(routeCtx), deps.individualLicenseOfferSearchPollPath(routeCtx), {
|
|
174
249
|
thid: input.requestThid || `individual-license-offer-search-${createRuntimeUuid()}`,
|
|
@@ -281,6 +356,9 @@ export async function upsertRelatedPersonAndPollWithDeps(routeCtx, input, deps)
|
|
|
281
356
|
return deps.submitAndPoll(deps.individualRelatedPersonBatchPath(routeCtx), deps.individualRelatedPersonPollPath(routeCtx), payload, input.pollOptions);
|
|
282
357
|
}
|
|
283
358
|
export async function ingestCommunicationAndUpdateIndexWithDeps(routeCtx, input, deps) {
|
|
359
|
+
if (!input.communicationPayload) {
|
|
360
|
+
throw new Error('Legacy communication ingestion requires communicationPayload.');
|
|
361
|
+
}
|
|
284
362
|
const payload = {
|
|
285
363
|
thid: input.communicationPayload.thid || `communication-${createRuntimeUuid()}`,
|
|
286
364
|
...input.communicationPayload,
|
|
@@ -588,13 +666,15 @@ export async function generateDigitalTwinFromSubjectDataWithDeps(routeCtx, input
|
|
|
588
666
|
: deps.digitalTwinCompositionR4PollPath(routeCtx);
|
|
589
667
|
return deps.submitAndPoll(submitPath, pollPath, payload, input.pollOptions);
|
|
590
668
|
}
|
|
591
|
-
function normalizeCommunicationPathFormatSegment(raw) {
|
|
669
|
+
export function normalizeCommunicationPathFormatSegment(raw) {
|
|
592
670
|
const value = String(raw || '').trim().toLowerCase();
|
|
593
671
|
if (!value || value === 'api' || value === 'org.hl7.fhir.api')
|
|
594
672
|
return 'org.hl7.fhir.api';
|
|
595
673
|
if (value === 'r4' || value === 'fhir.r4' || value === 'org.hl7.fhir.r4')
|
|
596
674
|
return 'org.hl7.fhir.r4';
|
|
597
|
-
|
|
675
|
+
if (/^org\.hl7\.fhir\.[a-z0-9.-]+$/.test(value))
|
|
676
|
+
return value;
|
|
677
|
+
throw new Error(`Unsupported Communication clinical format '${String(raw || '')}'.`);
|
|
598
678
|
}
|
|
599
679
|
function createRuntimeUuid() {
|
|
600
680
|
const fromCrypto = globalThis.crypto?.randomUUID?.();
|
|
@@ -52,6 +52,10 @@ export declare class RuntimeClientPaths {
|
|
|
52
52
|
individualFamilyOrganizationPurgePollPath(ctx?: RouteContext): string;
|
|
53
53
|
individualLicenseSearchPath(ctx?: RouteContext): string;
|
|
54
54
|
individualLicenseSearchPollPath(ctx?: RouteContext): string;
|
|
55
|
+
/** Builds one individual-member license mutation path. */
|
|
56
|
+
individualLicenseActionPath(ctx: RouteContext | undefined, action: string): string;
|
|
57
|
+
/** Builds the poll path paired with one individual-member license mutation. */
|
|
58
|
+
individualLicenseActionPollPath(ctx: RouteContext | undefined, action: string): string;
|
|
55
59
|
individualLicenseOfferSearchPath(ctx?: RouteContext): string;
|
|
56
60
|
individualLicenseOfferSearchPollPath(ctx?: RouteContext): string;
|
|
57
61
|
individualLicenseOrderSearchPath(ctx?: RouteContext): string;
|
|
@@ -64,8 +68,8 @@ export declare class RuntimeClientPaths {
|
|
|
64
68
|
individualRelatedPersonPurgePollPath(ctx?: RouteContext): string;
|
|
65
69
|
individualConsentR4BatchPath(ctx: RouteContext): string;
|
|
66
70
|
individualConsentR4PollPath(ctx: RouteContext): string;
|
|
67
|
-
individualCommunicationBatchPath(ctx: RouteContext, format:
|
|
68
|
-
individualCommunicationPollPath(ctx: RouteContext, format:
|
|
71
|
+
individualCommunicationBatchPath(ctx: RouteContext, format: string): string;
|
|
72
|
+
individualCommunicationPollPath(ctx: RouteContext, format: string): string;
|
|
69
73
|
individualCommunicationSearchPath(ctx: RouteContext): string;
|
|
70
74
|
individualCommunicationSearchPollPath(ctx: RouteContext): string;
|
|
71
75
|
individualDocumentReferenceBatchPath(ctx: RouteContext): string;
|
|
@@ -66,6 +66,10 @@ export class RuntimeClientPaths {
|
|
|
66
66
|
individualFamilyOrganizationPurgePollPath(ctx) { return this.v1Path(ctx, 'individual', 'org.schema', 'Organization', `${GwCoreLifecycleAction.Purge}-response`); }
|
|
67
67
|
individualLicenseSearchPath(ctx) { return this.v1Path(ctx, 'individual', 'org.schema', 'License', '_search'); }
|
|
68
68
|
individualLicenseSearchPollPath(ctx) { return this.v1Path(ctx, 'individual', 'org.schema', 'License', '_search-response'); }
|
|
69
|
+
/** Builds one individual-member license mutation path. */
|
|
70
|
+
individualLicenseActionPath(ctx, action) { return this.v1Path(ctx, 'individual', 'org.schema', 'License', action); }
|
|
71
|
+
/** Builds the poll path paired with one individual-member license mutation. */
|
|
72
|
+
individualLicenseActionPollPath(ctx, action) { return this.v1Path(ctx, 'individual', 'org.schema', 'License', `${action}-response`); }
|
|
69
73
|
individualLicenseOfferSearchPath(ctx) { return this.v1Path(ctx, 'individual', 'org.schema', 'Offer', '_search'); }
|
|
70
74
|
individualLicenseOfferSearchPollPath(ctx) { return this.v1Path(ctx, 'individual', 'org.schema', 'Offer', '_search-response'); }
|
|
71
75
|
individualLicenseOrderSearchPath(ctx) { return this.v1Path(ctx, 'individual', 'org.schema', 'Order', '_search'); }
|
|
@@ -4,8 +4,9 @@ export type RuntimeTransportConfig = Readonly<{
|
|
|
4
4
|
defaultHeaders: Record<string, string>;
|
|
5
5
|
requestTimeoutMs: number;
|
|
6
6
|
httpTraceFile?: string;
|
|
7
|
+
fetchImpl?: typeof fetch;
|
|
7
8
|
}>;
|
|
8
|
-
export declare function buildRuntimeHeaders(config: RuntimeTransportConfig, contentType: string): Record<string, string>;
|
|
9
|
+
export declare function buildRuntimeHeaders(config: RuntimeTransportConfig, contentType: string, accept?: string): Record<string, string>;
|
|
9
10
|
export declare function pollBatchResponseWithRuntimeConfig(config: RuntimeTransportConfig, path: string, request: {
|
|
10
11
|
thid: string;
|
|
11
12
|
}): Promise<{
|
|
@@ -18,5 +19,16 @@ export declare function postJsonWithRuntimeConfig(config: RuntimeTransportConfig
|
|
|
18
19
|
location?: string;
|
|
19
20
|
body: unknown;
|
|
20
21
|
}>;
|
|
22
|
+
/** Submits an already-rendered JSON, FHIR, DIDComm or secure-form request. */
|
|
23
|
+
export declare function postRenderedWithRuntimeConfig(config: RuntimeTransportConfig, path: string, request: {
|
|
24
|
+
contentType: string;
|
|
25
|
+
accept: string;
|
|
26
|
+
body: Record<string, unknown> | string;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
status: number;
|
|
29
|
+
location?: string;
|
|
30
|
+
body: unknown;
|
|
31
|
+
retryAfterMs?: number;
|
|
32
|
+
}>;
|
|
21
33
|
export declare function fetchWithTimeout(config: RuntimeTransportConfig, path: string, init: RequestInit): Promise<Response>;
|
|
22
34
|
export declare function parseResponseBody(response: Response): Promise<unknown>;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
|
|
2
2
|
import { DIDCOMM_DEFAULT_ACCEPT_HEADER } from 'gdc-common-utils-ts/utils/didcomm-submit';
|
|
3
3
|
import { appendHttpTrace, parseTraceBody, parseTraceRawText, redactTraceValue, } from './runtime-http-trace.js';
|
|
4
|
-
export function buildRuntimeHeaders(config, contentType) {
|
|
4
|
+
export function buildRuntimeHeaders(config, contentType, accept = DIDCOMM_DEFAULT_ACCEPT_HEADER) {
|
|
5
5
|
const headers = {
|
|
6
6
|
...config.defaultHeaders,
|
|
7
7
|
'Content-Type': contentType,
|
|
8
|
-
Accept:
|
|
8
|
+
Accept: accept,
|
|
9
9
|
};
|
|
10
10
|
if (config.bearerToken)
|
|
11
11
|
headers.Authorization = `Bearer ${config.bearerToken}`;
|
|
@@ -36,6 +36,21 @@ export async function postJsonWithRuntimeConfig(config, path, payload, contentTy
|
|
|
36
36
|
body: await parseResponseBody(response),
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
+
/** Submits an already-rendered JSON, FHIR, DIDComm or secure-form request. */
|
|
40
|
+
export async function postRenderedWithRuntimeConfig(config, path, request) {
|
|
41
|
+
const response = await fetchWithTimeout(config, path, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: buildRuntimeHeaders(config, request.contentType, request.accept),
|
|
44
|
+
body: typeof request.body === 'string' ? request.body : JSON.stringify(request.body),
|
|
45
|
+
});
|
|
46
|
+
const retryAfter = Number(response.headers.get('retry-after'));
|
|
47
|
+
return {
|
|
48
|
+
status: response.status,
|
|
49
|
+
location: response.headers.get('location') || undefined,
|
|
50
|
+
body: await parseResponseBody(response),
|
|
51
|
+
retryAfterMs: Number.isFinite(retryAfter) ? retryAfter * 1000 : undefined,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
39
54
|
export async function fetchWithTimeout(config, path, init) {
|
|
40
55
|
const controller = new AbortController();
|
|
41
56
|
const timeout = setTimeout(() => controller.abort(), config.requestTimeoutMs);
|
|
@@ -51,7 +66,7 @@ export async function fetchWithTimeout(config, path, init) {
|
|
|
51
66
|
},
|
|
52
67
|
};
|
|
53
68
|
try {
|
|
54
|
-
const response = await fetch(url, { ...init, signal: controller.signal });
|
|
69
|
+
const response = await (config.fetchImpl || fetch)(url, { ...init, signal: controller.signal });
|
|
55
70
|
const responseClone = response.clone();
|
|
56
71
|
const responseRaw = await responseClone.text();
|
|
57
72
|
appendHttpTrace(config.httpTraceFile, {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-controlled KEK boundary.
|
|
3
|
+
*
|
|
4
|
+
* A BFF normally implements this with Cloud KMS. A native confidential app
|
|
5
|
+
* implements the same boundary with a non-exportable Keychain/Keystore key.
|
|
6
|
+
* The adapter never receives the user's PIN.
|
|
7
|
+
*/
|
|
8
|
+
export type ServerProfileSealer = Readonly<{
|
|
9
|
+
seal(cleartext: string, aad: string): Promise<string>;
|
|
10
|
+
unseal(ciphertext: string, aad: string): Promise<string>;
|
|
11
|
+
}>;
|
|
12
|
+
/** Persisted scrypt work factor. The salt and parameters are public metadata. */
|
|
13
|
+
export type ProfileScryptParameters = Readonly<{
|
|
14
|
+
name: 'scrypt';
|
|
15
|
+
saltBase64Url: string;
|
|
16
|
+
cost: number;
|
|
17
|
+
blockSize: number;
|
|
18
|
+
parallelization: number;
|
|
19
|
+
keyLength: 32;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* Portable v1 envelope for a profile secret.
|
|
23
|
+
*
|
|
24
|
+
* `ciphertext` contains the secret encrypted by a random DEK. The host first
|
|
25
|
+
* wraps that DEK; the PIN-derived key then encrypts the host-wrapped value.
|
|
26
|
+
* Opening therefore requires both factors without exposing either factor to
|
|
27
|
+
* the other adapter.
|
|
28
|
+
*/
|
|
29
|
+
export type PinProtectedProfileSecret = Readonly<{
|
|
30
|
+
version: 'gdc-pin-host-envelope-v1';
|
|
31
|
+
kdf: ProfileScryptParameters;
|
|
32
|
+
payload: AesGcmCiphertext;
|
|
33
|
+
pinWrappedHostDek: AesGcmCiphertext;
|
|
34
|
+
}>;
|
|
35
|
+
export type AesGcmCiphertext = Readonly<{
|
|
36
|
+
ivBase64Url: string;
|
|
37
|
+
ciphertextBase64Url: string;
|
|
38
|
+
tagBase64Url: string;
|
|
39
|
+
}>;
|
|
40
|
+
export type ProfileProtectionOptions = Readonly<{
|
|
41
|
+
cost?: number;
|
|
42
|
+
blockSize?: number;
|
|
43
|
+
parallelization?: number;
|
|
44
|
+
}>;
|
|
45
|
+
/** Distinguishes a wrong PIN from a host KMS outage or corrupted payload. */
|
|
46
|
+
export declare class ProfilePinRejectedError extends Error {
|
|
47
|
+
constructor();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Encrypt one seed, private-key export or credential using a fresh DEK.
|
|
51
|
+
*
|
|
52
|
+
* The returned salt is intentionally stored in clear. Security comes from the
|
|
53
|
+
* PIN work factor, the independent host KEK and authenticated encryption, not
|
|
54
|
+
* from hiding the salt or algorithm parameters.
|
|
55
|
+
*/
|
|
56
|
+
export declare function protectServerProfileSecret(cleartext: string, pin: string, aad: string, hostSealer: ServerProfileSealer, options?: ProfileProtectionOptions): Promise<PinProtectedProfileSecret>;
|
|
57
|
+
/**
|
|
58
|
+
* Open one protected profile secret using both PIN and host protection.
|
|
59
|
+
*
|
|
60
|
+
* The PIN layer is authenticated before the host adapter is called. A wrong
|
|
61
|
+
* PIN therefore neither reaches KMS nor becomes indistinguishable from a KMS
|
|
62
|
+
* availability failure in audit logs.
|
|
63
|
+
*/
|
|
64
|
+
export declare function openServerProfileSecret(envelope: PinProtectedProfileSecret, pin: string, aad: string, hostSealer: ServerProfileSealer): Promise<string>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
|
|
2
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';
|
|
3
|
+
/** Distinguishes a wrong PIN from a host KMS outage or corrupted payload. */
|
|
4
|
+
export class ProfilePinRejectedError extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super('Profile PIN rejected.');
|
|
7
|
+
this.name = 'ProfilePinRejectedError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Encrypt one seed, private-key export or credential using a fresh DEK.
|
|
12
|
+
*
|
|
13
|
+
* The returned salt is intentionally stored in clear. Security comes from the
|
|
14
|
+
* PIN work factor, the independent host KEK and authenticated encryption, not
|
|
15
|
+
* from hiding the salt or algorithm parameters.
|
|
16
|
+
*/
|
|
17
|
+
export async function protectServerProfileSecret(cleartext, pin, aad, hostSealer, options = {}) {
|
|
18
|
+
requirePin(pin);
|
|
19
|
+
const kdf = {
|
|
20
|
+
name: 'scrypt',
|
|
21
|
+
saltBase64Url: randomBytes(16).toString('base64url'),
|
|
22
|
+
cost: options.cost ?? 16384,
|
|
23
|
+
blockSize: options.blockSize ?? 8,
|
|
24
|
+
parallelization: options.parallelization ?? 1,
|
|
25
|
+
keyLength: 32,
|
|
26
|
+
};
|
|
27
|
+
const pinKey = derivePinKey(pin, kdf);
|
|
28
|
+
const dek = randomBytes(32);
|
|
29
|
+
const hostWrappedDek = await hostSealer.seal(dek.toString('base64url'), `${aad}:dek`);
|
|
30
|
+
try {
|
|
31
|
+
return {
|
|
32
|
+
version: 'gdc-pin-host-envelope-v1',
|
|
33
|
+
kdf,
|
|
34
|
+
payload: encryptAesGcm(Buffer.from(cleartext), dek, `${aad}:payload`),
|
|
35
|
+
pinWrappedHostDek: encryptAesGcm(Buffer.from(hostWrappedDek), pinKey, `${aad}:host-wrapped-dek`),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
pinKey.fill(0);
|
|
40
|
+
dek.fill(0);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Open one protected profile secret using both PIN and host protection.
|
|
45
|
+
*
|
|
46
|
+
* The PIN layer is authenticated before the host adapter is called. A wrong
|
|
47
|
+
* PIN therefore neither reaches KMS nor becomes indistinguishable from a KMS
|
|
48
|
+
* availability failure in audit logs.
|
|
49
|
+
*/
|
|
50
|
+
export async function openServerProfileSecret(envelope, pin, aad, hostSealer) {
|
|
51
|
+
validateEnvelope(envelope);
|
|
52
|
+
const pinKey = derivePinKey(pin, envelope.kdf);
|
|
53
|
+
let hostWrappedDek;
|
|
54
|
+
try {
|
|
55
|
+
hostWrappedDek = decryptAesGcm(envelope.pinWrappedHostDek, pinKey, `${aad}:host-wrapped-dek`).toString('utf8');
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw new ProfilePinRejectedError();
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
pinKey.fill(0);
|
|
62
|
+
}
|
|
63
|
+
const dek = Buffer.from(await hostSealer.unseal(hostWrappedDek, `${aad}:dek`), 'base64url');
|
|
64
|
+
try {
|
|
65
|
+
if (dek.length !== 32)
|
|
66
|
+
throw new Error('Profile host returned an invalid DEK.');
|
|
67
|
+
return decryptAesGcm(envelope.payload, dek, `${aad}:payload`).toString('utf8');
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
dek.fill(0);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function derivePinKey(pin, parameters) {
|
|
74
|
+
requirePin(pin);
|
|
75
|
+
return scryptSync(pin, Buffer.from(parameters.saltBase64Url, 'base64url'), parameters.keyLength, {
|
|
76
|
+
N: parameters.cost,
|
|
77
|
+
r: parameters.blockSize,
|
|
78
|
+
p: parameters.parallelization,
|
|
79
|
+
maxmem: Math.max(64 * 1024 * 1024, 256 * parameters.cost * parameters.blockSize),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function encryptAesGcm(cleartext, key, aad) {
|
|
83
|
+
const iv = randomBytes(12);
|
|
84
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
85
|
+
cipher.setAAD(Buffer.from(aad));
|
|
86
|
+
const ciphertext = Buffer.concat([cipher.update(cleartext), cipher.final()]);
|
|
87
|
+
return {
|
|
88
|
+
ivBase64Url: iv.toString('base64url'),
|
|
89
|
+
ciphertextBase64Url: ciphertext.toString('base64url'),
|
|
90
|
+
tagBase64Url: cipher.getAuthTag().toString('base64url'),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function decryptAesGcm(value, key, aad) {
|
|
94
|
+
const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(value.ivBase64Url, 'base64url'));
|
|
95
|
+
decipher.setAAD(Buffer.from(aad));
|
|
96
|
+
decipher.setAuthTag(Buffer.from(value.tagBase64Url, 'base64url'));
|
|
97
|
+
return Buffer.concat([
|
|
98
|
+
decipher.update(Buffer.from(value.ciphertextBase64Url, 'base64url')),
|
|
99
|
+
decipher.final(),
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
function requirePin(pin) {
|
|
103
|
+
if (String(pin).length < 6)
|
|
104
|
+
throw new Error('Production profile PIN must contain at least 6 characters.');
|
|
105
|
+
}
|
|
106
|
+
function validateEnvelope(value) {
|
|
107
|
+
if (value?.version !== 'gdc-pin-host-envelope-v1' || value.kdf?.name !== 'scrypt') {
|
|
108
|
+
throw new Error('Unsupported protected profile envelope.');
|
|
109
|
+
}
|
|
110
|
+
}
|