gdc-sdk-node-ts 2.2.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/backend-profile-runtime.d.ts +23 -0
  2. package/dist/backend-profile-runtime.js +23 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +2 -0
  5. package/dist/node-runtime-client.d.ts +39 -4
  6. package/dist/node-runtime-client.js +124 -4
  7. package/dist/orchestration/client-port.d.ts +4 -1
  8. package/dist/orchestration/individual-controller-sdk.d.ts +10 -1
  9. package/dist/orchestration/individual-controller-sdk.js +15 -0
  10. package/dist/orchestration/individual-member-sdk.d.ts +10 -1
  11. package/dist/orchestration/individual-member-sdk.js +15 -0
  12. package/dist/resource-operations.d.ts +111 -4
  13. package/dist/resource-operations.js +83 -3
  14. package/dist/runtime-client-paths.d.ts +6 -2
  15. package/dist/runtime-client-paths.js +4 -0
  16. package/dist/runtime-transport.d.ts +13 -1
  17. package/dist/runtime-transport.js +18 -3
  18. package/dist/server-profile-protection.d.ts +64 -0
  19. package/dist/server-profile-protection.js +110 -0
  20. package/dist/server-profile-session.d.ts +115 -0
  21. package/dist/server-profile-session.js +259 -0
  22. package/dist/session.d.ts +7 -0
  23. package/dist/session.js +10 -0
  24. package/package.json +3 -3
  25. package/dist/legal-organization-onboarding-facade.d.ts +0 -70
  26. package/dist/legal-organization-onboarding-facade.js +0 -169
  27. package/dist/lifecycle-examples.d.ts +0 -7
  28. package/dist/lifecycle-examples.js +0 -8
  29. package/dist/runtime-software-proof.d.ts +0 -51
  30. package/dist/runtime-software-proof.js +0 -34
  31. package/dist/simple-device-activation.d.ts +0 -44
  32. package/dist/simple-device-activation.js +0 -44
  33. package/dist/simple-host-onboarding.d.ts +0 -27
  34. package/dist/simple-host-onboarding.js +0 -39
  35. package/dist/simple-individual-onboarding.d.ts +0 -27
  36. package/dist/simple-individual-onboarding.js +0 -38
  37. package/dist/simple-individual-start.d.ts +0 -45
  38. package/dist/simple-individual-start.js +0 -59
  39. package/dist/simple-poll-options.d.ts +0 -5
  40. package/dist/simple-poll-options.js +0 -17
  41. package/dist/simple-smart-token.d.ts +0 -58
  42. package/dist/simple-smart-token.js +0 -85
@@ -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
- communicationPayload: CommunicationInput & Record<string, unknown>;
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: 'org.hl7.fhir.api' | 'org.hl7.fhir.r4') => string;
554
- individualCommunicationPollPath: (ctx: RouteContext, pathFormatSegment: 'org.hl7.fhir.api' | 'org.hl7.fhir.r4') => string;
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
- return 'org.hl7.fhir.api';
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: 'org.hl7.fhir.api' | 'org.hl7.fhir.r4'): string;
68
- individualCommunicationPollPath(ctx: RouteContext, format: 'org.hl7.fhir.api' | 'org.hl7.fhir.r4'): string;
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: DIDCOMM_DEFAULT_ACCEPT_HEADER,
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
+ }