gdc-sdk-node-ts 2.6.6 → 2.7.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.
@@ -34,11 +34,11 @@ export async function listAuthorizedIndividualSubjectsWithDeps(routeContext, inp
34
34
  data: [{
35
35
  type: 'Family-search-v1.0',
36
36
  request: { method: 'POST' },
37
- meta: { claims: {
38
- '@context': SCHEMA_CONTEXT,
39
- ...(email ? { [CLAIM_ORGANIZATION_OWNER_EMAIL]: email } : {}),
40
- ...(telephone ? { [CLAIM_ORGANIZATION_OWNER_TELEPHONE]: telephone } : {}),
41
- } },
37
+ resource: { meta: { claims: {
38
+ '@context': SCHEMA_CONTEXT,
39
+ ...(email ? { [CLAIM_ORGANIZATION_OWNER_EMAIL]: email } : {}),
40
+ ...(telephone ? { [CLAIM_ORGANIZATION_OWNER_TELEPHONE]: telephone } : {}),
41
+ } } },
42
42
  }],
43
43
  },
44
44
  }, input.pollOptions);
@@ -58,7 +58,7 @@ export async function listAuthorizedIndividualSubjectsWithDeps(routeContext, inp
58
58
  type: 'batch',
59
59
  data: [{
60
60
  request: { method: 'POST' },
61
- meta: { status: 'active', claims: licenseClaims },
61
+ resource: { meta: { status: 'active', claims: licenseClaims } },
62
62
  }],
63
63
  },
64
64
  }, input.pollOptions);
@@ -76,7 +76,7 @@ export async function listAuthorizedIndividualSubjectsWithDeps(routeContext, inp
76
76
  data: [{
77
77
  type: 'Family-search-v1.0',
78
78
  request: { method: 'POST' },
79
- meta: { claims: organizationClaims },
79
+ resource: { meta: { claims: organizationClaims } },
80
80
  }],
81
81
  },
82
82
  }, input.pollOptions);
@@ -155,7 +155,7 @@ function readAcceptedLicenses(result) {
155
155
  }
156
156
  function readFirstClaims(result) {
157
157
  const entry = readData(result)[0];
158
- const claims = entry?.meta?.claims || entry?.resource?.meta?.claims;
158
+ const claims = entry?.resource?.meta?.claims || entry?.meta?.claims;
159
159
  return claims && typeof claims === 'object' ? claims : {};
160
160
  }
161
161
  function readData(result) {
@@ -1,3 +1,4 @@
1
+ import type { ClinicalCreatorBinding } from 'gdc-common-utils-ts/utils/fhir-ips-creator-identity';
1
2
  import type { PollOptions, SubmitAndPollResult } from './orchestration/client-port.js';
2
3
  import type { RouteContext } from './individual-onboarding.js';
3
4
  export type EmployeeDeviceActivationInput = {
@@ -32,6 +33,7 @@ export type ProfileDeviceRegistrationInput = Readonly<{
32
33
  deviceName?: string;
33
34
  actorDid?: string;
34
35
  profileDid?: string;
36
+ clinicalCreatorBinding?: ClinicalCreatorBinding;
35
37
  }>;
36
38
  /** Advanced typed editor used by SDK runtimes that already own profile keys. */
37
39
  export interface ProfileDeviceActivationDraft {
@@ -43,6 +45,7 @@ export interface ProfileDeviceActivationDraft {
43
45
  setDeviceName(value: string): ProfileDeviceActivationDraft;
44
46
  setActorDid(value: string): ProfileDeviceActivationDraft;
45
47
  setProfileDid(value: string): ProfileDeviceActivationDraft;
48
+ setClinicalCreatorBinding(value: ClinicalCreatorBinding): ProfileDeviceActivationDraft;
46
49
  setTimeoutSeconds(value: number): ProfileDeviceActivationDraft;
47
50
  setIntervalSeconds(value: number): ProfileDeviceActivationDraft;
48
51
  build(): EmployeeDeviceActivationRequestInput & {
@@ -20,6 +20,7 @@ export function createProfileDeviceActivationRequest(input) {
20
20
  let deviceName = '';
21
21
  let actorDid = '';
22
22
  let profileDid = '';
23
+ let clinicalCreatorBinding;
23
24
  let timeoutSeconds;
24
25
  let intervalSeconds;
25
26
  const draft = {
@@ -31,6 +32,7 @@ export function createProfileDeviceActivationRequest(input) {
31
32
  setDeviceName(value) { deviceName = normalizedText(value); return draft; },
32
33
  setActorDid(value) { actorDid = normalizedText(value); return draft; },
33
34
  setProfileDid(value) { profileDid = normalizedText(value); return draft; },
35
+ setClinicalCreatorBinding(value) { clinicalCreatorBinding = stableClinicalCreatorBinding(value); return draft; },
34
36
  setTimeoutSeconds(value) { timeoutSeconds = positiveNumber(value, 'timeout seconds'); return draft; },
35
37
  setIntervalSeconds(value) { intervalSeconds = positiveNumber(value, 'interval seconds'); return draft; },
36
38
  build() {
@@ -43,6 +45,7 @@ export function createProfileDeviceActivationRequest(input) {
43
45
  ...(deviceName ? { deviceName } : {}),
44
46
  ...(actorDid ? { actorDid } : {}),
45
47
  ...(profileDid ? { profileDid } : {}),
48
+ ...(clinicalCreatorBinding ? { clinicalCreatorBinding } : {}),
46
49
  };
47
50
  return {
48
51
  ...input,
@@ -165,12 +168,24 @@ function resolveDcrPayload(input) {
165
168
  },
166
169
  ...(registration.actorDid ? { [IdentityDcrMetadataFields.ActorDid]: registration.actorDid } : {}),
167
170
  ...(registration.profileDid ? { [IdentityDcrMetadataFields.ProfileDid]: registration.profileDid } : {}),
171
+ ...(registration.clinicalCreatorBinding ? {
172
+ [IdentityDcrMetadataFields.ClinicalCreatorBinding]: stableClinicalCreatorBinding(registration.clinicalCreatorBinding),
173
+ } : {}),
168
174
  };
169
175
  }
170
176
  if (input.dcrPayload)
171
177
  return { ...input.dcrPayload };
172
178
  throw new Error('Device activation requires a device registration built by createProfileDeviceActivationRequest.');
173
179
  }
180
+ function stableClinicalCreatorBinding(binding) {
181
+ return {
182
+ kind: binding.kind,
183
+ actorIdentifier: binding.actorIdentifier,
184
+ authorIdentifier: binding.authorIdentifier,
185
+ ownerIdentifier: binding.ownerIdentifier,
186
+ role: binding.role,
187
+ };
188
+ }
174
189
  function responseBody(value) {
175
190
  if (typeof value === 'string') {
176
191
  try {
@@ -234,12 +234,13 @@ export async function searchDigitalTwinsWithDeps(ctx, input, deps) {
234
234
  ? {
235
235
  data: [{
236
236
  type: `${resourceType}-search-request-v1.0`,
237
- resource: { resourceType: 'Parameters', parameter: parameters },
238
- meta: {
239
- claims: Object.fromEntries([
240
- ['@context', format],
241
- ...parameters.map((parameter) => [parameter.name, parameter.valueString || parameter.valueDate || '']),
242
- ]),
237
+ resource: {
238
+ resourceType: 'Parameters',
239
+ parameter: parameters,
240
+ meta: { claims: Object.fromEntries([
241
+ ['@context', format],
242
+ ...parameters.map((parameter) => [parameter.name, parameter.valueString || parameter.valueDate || '']),
243
+ ]) },
243
244
  },
244
245
  }],
245
246
  }
@@ -35,7 +35,6 @@ export async function searchFamilyOrganizationWithDeps(deps) {
35
35
  body: {
36
36
  data: [{
37
37
  type: 'Family-search-v1.0',
38
- meta: { claims },
39
38
  resource: { meta: { claims } },
40
39
  }],
41
40
  },
@@ -27,7 +27,6 @@ export async function confirmIndividualOrganizationOrderWithDeps(deps) {
27
27
  body: {
28
28
  data: [{
29
29
  type: 'Family-order-request-v1.0',
30
- meta: { claims: orderClaims },
31
30
  resource: { meta: { claims: orderClaims } },
32
31
  }],
33
32
  },
@@ -54,7 +54,6 @@ export async function startIndividualOrganizationWithDeps(deps) {
54
54
  body: {
55
55
  data: [{
56
56
  type: GwCoreLifecycleRequestType.IndividualOrganizationRegistration,
57
- meta: { claims },
58
57
  resource: { meta: { claims } },
59
58
  }],
60
59
  },
@@ -68,7 +68,10 @@ export const recoverOrganizationControllerWithIssueWithDeps = recoverOrganizatio
68
68
  export function readLegalOrganizationCredentialReissuanceActivationCode(result) {
69
69
  const pollBody = (result?.poll?.body || {});
70
70
  for (const candidate of nestedObjects(pollBody)) {
71
- const claims = candidate.meta?.claims;
71
+ const resource = candidate.resource;
72
+ const resourceClaims = resource?.meta?.claims;
73
+ const legacyClaims = candidate.meta?.claims;
74
+ const claims = resourceClaims || legacyClaims;
72
75
  const activationCode = String(claims?.['org.schema.IndividualProduct.serialNumber'] || '').trim();
73
76
  if (activationCode)
74
77
  return activationCode;
@@ -84,8 +84,10 @@ export function readEmployeeLicenseOfferId(value) {
84
84
  for (const candidate of nestedRecords(value)) {
85
85
  if (candidate.type !== 'Employee-license-offer-v1.0')
86
86
  continue;
87
- const meta = record(candidate.meta);
88
- const claims = record(meta?.claims);
87
+ const resource = record(candidate.resource);
88
+ const resourceMeta = record(resource?.meta);
89
+ const legacyMeta = record(candidate.meta);
90
+ const claims = record(resourceMeta?.claims) || record(legacyMeta?.claims);
89
91
  const offerId = String(claims?.[ClaimsOfferSchemaorg.identifier] || '').trim();
90
92
  if (offerId)
91
93
  return offerId;
@@ -27,7 +27,6 @@ export async function confirmOrganizationLicenseOrderWithDeps(deps) {
27
27
  body: {
28
28
  data: [{
29
29
  type: deps.input.dataType || 'Organization-order-request-v1.0',
30
- meta: { claims },
31
30
  resource: { meta: { claims } },
32
31
  }],
33
32
  },
@@ -0,0 +1,16 @@
1
+ import type { VerifiableCredentialV2 } from 'gdc-common-utils-ts/models/verifiable-credential';
2
+ import type { IWallet, WalletExecutionContext, WalletKeySelection } from 'gdc-sdk-core-ts';
3
+ /** Input required to add one human-authorizer proof to the out-of-band VC. */
4
+ export type SignOrganizationRegistrationAuthorizationInput = Readonly<{
5
+ credential: VerifiableCredentialV2;
6
+ wallet: IWallet;
7
+ context: WalletExecutionContext;
8
+ key: WalletKeySelection;
9
+ verificationMethod: string;
10
+ createdAt?: string;
11
+ }>;
12
+ /**
13
+ * Adds one detached JWS proof made by the already-unlocked professional
14
+ * profile. The browser never receives the private key or clear wallet seed.
15
+ */
16
+ export declare function signOrganizationRegistrationAuthorizationCredential(input: SignOrganizationRegistrationAuthorizationInput): Promise<VerifiableCredentialV2>;
@@ -0,0 +1,34 @@
1
+ import { canonicalizeOrganizationRegistrationAuthorizationCredential, } from 'gdc-common-utils-ts/utils/organization-registration-authorization';
2
+ /**
3
+ * Adds one detached JWS proof made by the already-unlocked professional
4
+ * profile. The browser never receives the private key or clear wallet seed.
5
+ */
6
+ export async function signOrganizationRegistrationAuthorizationCredential(input) {
7
+ if (!input.wallet.signDetachedJws) {
8
+ throw new Error('Wallet does not support detached organization authorization proofs.');
9
+ }
10
+ const verificationMethod = String(input.verificationMethod || '').trim();
11
+ if (!verificationMethod)
12
+ throw new Error('Organization authorization proof requires verificationMethod.');
13
+ const payload = canonicalizeOrganizationRegistrationAuthorizationCredential(input.credential);
14
+ const jws = await input.wallet.signDetachedJws(input.context, {
15
+ payload,
16
+ header: { typ: 'application/vc+ld+json' },
17
+ key: input.key,
18
+ });
19
+ const existingProofs = Array.isArray(input.credential.proof)
20
+ ? input.credential.proof
21
+ : input.credential.proof
22
+ ? [input.credential.proof]
23
+ : [];
24
+ return {
25
+ ...input.credential,
26
+ proof: [...existingProofs, {
27
+ type: 'JsonWebSignature2020',
28
+ created: input.createdAt || new Date().toISOString(),
29
+ proofPurpose: 'contractAgreement',
30
+ verificationMethod,
31
+ jws,
32
+ }],
33
+ };
34
+ }
@@ -340,11 +340,11 @@ export async function transitionIndividualMemberLicenseWithDeps(routeCtx, action
340
340
  ...(input.ownerOrganizationId ? { ownerOrganizationId: input.ownerOrganizationId } : {}),
341
341
  ...(input.subjectId ? { subjectId: input.subjectId } : {}),
342
342
  ...(input.verifiedActorIdentifier ? { verifiedActorIdentifier: input.verifiedActorIdentifier } : {}),
343
- claims: {
344
- '@context': 'org.schema',
345
- 'org.schema.IndividualProduct.serialNumber': input.activationCode,
346
- },
347
343
  },
344
+ resource: { meta: { claims: {
345
+ '@context': 'org.schema',
346
+ 'org.schema.IndividualProduct.serialNumber': input.activationCode,
347
+ } } },
348
348
  request: { method: 'POST' },
349
349
  }],
350
350
  },
@@ -414,7 +414,6 @@ export async function disableIndividualMemberWithDeps(routeCtx, input, options,
414
414
  type: 'batch',
415
415
  entry: [{
416
416
  request: { method: GwCoreLifecycleRequestMethod.Post },
417
- meta: { claims },
418
417
  resource: {
419
418
  ...resource,
420
419
  ...(input.resourceId ? { id: input.resourceId } : {}),
@@ -444,7 +443,6 @@ export async function purgeIndividualMemberWithDeps(routeCtx, input, options, de
444
443
  entry: [{
445
444
  type: input.dataType || GwCoreLifecycleRequestType.IndividualMemberPurge,
446
445
  request: { method: GwCoreLifecycleRequestMethod.Post },
447
- meta: { claims },
448
446
  resource: {
449
447
  ...resource,
450
448
  ...(input.resourceId ? { id: input.resourceId } : {}),
@@ -550,7 +548,6 @@ export async function requestProfessionalAccessWithDeps(routeCtx, input, deps) {
550
548
  type: 'batch',
551
549
  data: [{
552
550
  type: 'CommMsgExtended',
553
- meta: { claims: communicationClaims },
554
551
  resource: communicationResource,
555
552
  }],
556
553
  },
@@ -701,7 +698,6 @@ export function buildBlockchainArtifactBundleFromSearchResponse(input) {
701
698
  });
702
699
  return {
703
700
  type: 'DocumentReference',
704
- meta: { claims: artifact.documentReference.meta?.claims || {} },
705
701
  resource: artifact.documentReference,
706
702
  request: {
707
703
  method: 'POST',
@@ -762,7 +758,6 @@ export async function registerBlockchainArtifactAndUpdateIndexWithDeps(routeCtx,
762
758
  type: 'batch',
763
759
  data: [{
764
760
  type: 'DocumentReference',
765
- meta: { claims: artifact.documentReference.meta?.claims || {} },
766
761
  resource: artifact.documentReference,
767
762
  request: {
768
763
  method: 'POST',
@@ -840,7 +835,6 @@ export async function grantProfessionalAccessWithDeps(routeCtx, input, deps) {
840
835
  body: {
841
836
  data: [{
842
837
  type: input.dataType || 'Consent-grant-request-v1.0',
843
- meta: { claims: built.consentClaims },
844
838
  resource: { resourceType: 'Consent', meta: { claims: built.consentClaims } },
845
839
  }],
846
840
  },
@@ -983,7 +977,6 @@ export async function revokeProfessionalAccessWithDeps(routeCtx, input, deps) {
983
977
  body: {
984
978
  data: [{
985
979
  type: input.dataType || 'Consent-grant-request-v1.0',
986
- meta: { claims: revokedClaims },
987
980
  resource: { resourceType: 'Consent', meta: { claims: revokedClaims } },
988
981
  }],
989
982
  },
@@ -65,13 +65,6 @@ export async function activateOrganizationInGatewayFromIcaProofWithDeps(input) {
65
65
  ...(input.activationInput.controller ? { controller: input.activationInput.controller } : {}),
66
66
  data: [{
67
67
  type: 'Organization-activation-request-v1.0',
68
- meta: {
69
- claims: {
70
- '@context': 'org.schema',
71
- ...serviceClaims,
72
- ...(input.activationInput.additionalClaims || {}),
73
- },
74
- },
75
68
  resource: {
76
69
  meta: {
77
70
  claims: {
@@ -1,6 +1,7 @@
1
1
  // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
2
  import { buildGwCoreTenantResourceActionPath } from 'gdc-common-utils-ts/utils/gw-core-path';
3
- import { IdentityAuthActions, IdentityAuthRouteSegments } from 'gdc-common-utils-ts/constants/identity-auth';
3
+ import { IdentityAuthActions, IdentityAuthResourceTypes, IdentityAuthRouteSegments, SmartPostDcrActions, } from 'gdc-common-utils-ts/constants/identity-auth';
4
+ import { GatewayRouteFormats, GatewayRouteSections, } from 'gdc-common-utils-ts/constants/gateway-response';
4
5
  export function buildV1Path(routeCtx, section, format, resourceType, action) {
5
6
  return buildGwCoreTenantResourceActionPath({
6
7
  tenantId: routeCtx.tenantId,
@@ -60,8 +61,8 @@ function buildIdentityAuthPath(ctx, action) {
60
61
  return `/${encodeURIComponent(IdentityAuthRouteSegments.Host)}/cds-${encodeURIComponent(ctx.jurisdiction)}/${IdentityAuthRouteSegments.Version}/${encodeURIComponent(ctx.sector)}/${encodeURIComponent(ctx.tenantId)}/${IdentityAuthRouteSegments.Section}/${IdentityAuthRouteSegments.Format}/${encodeURIComponent(action)}`;
61
62
  }
62
63
  export function buildIdentityOpenIdSmartTokenPath(ctx) {
63
- return `/${encodeURIComponent(ctx.tenantId)}/cds-${encodeURIComponent(ctx.jurisdiction)}/v1/${encodeURIComponent(ctx.sector)}/identity/openid/smart/token`;
64
+ return buildV1Path(ctx, GatewayRouteSections.Identity, GatewayRouteFormats.OpenId, IdentityAuthResourceTypes.Smart, SmartPostDcrActions.Token);
64
65
  }
65
66
  export function buildIdentityOpenIdSmartTokenPollPath(ctx) {
66
- return `/${encodeURIComponent(ctx.tenantId)}/cds-${encodeURIComponent(ctx.jurisdiction)}/v1/${encodeURIComponent(ctx.sector)}/identity/openid/smart/_batch-response`;
67
+ return buildV1Path(ctx, GatewayRouteSections.Identity, GatewayRouteFormats.OpenId, IdentityAuthResourceTypes.Smart, SmartPostDcrActions.TokenResponse);
67
68
  }
@@ -1,7 +1,7 @@
1
1
  import type { JWK } from 'gdc-common-utils-ts/models/jwk';
2
2
  import type { ActorKind } from 'gdc-common-utils-ts/models/actor-session';
3
3
  import type { LegalOrganizationVerificationTransactionInput } from 'gdc-common-utils-ts/utils/legal-organization-verification-transaction';
4
- import { type AppInfo, type ConfidentialStorageProfile, type PollOptions, type SubmitAndPollResult } from 'gdc-sdk-core-ts';
4
+ import { type AppInfo, type ConfidentialStorageProfile, type PollOptions, type SubmitAndPollResult, type ClinicalCreatorBinding, type ClinicalCreatorIpsExport } from 'gdc-sdk-core-ts';
5
5
  import { OrganizationControllerSdk } from './orchestration/organization-controller-sdk.js';
6
6
  import { ProfessionalSdk } from './orchestration/professional-sdk.js';
7
7
  import { DigitalTwinSdk } from './orchestration/digital-twin-sdk.js';
@@ -29,6 +29,8 @@ export type ServerProfileRecord = Readonly<{
29
29
  clientId: string;
30
30
  /** Stable non-secret id of the browser/app installation registered by DCR. */
31
31
  clientInstanceId?: string;
32
+ /** Stable FHIR creator/permission identity; channel and device values are aliases only. */
33
+ clinicalCreatorBinding?: ClinicalCreatorBinding;
32
34
  deviceDid: string;
33
35
  publicJwks: Record<string, unknown>[];
34
36
  /** Public recipient key for local confidential storage; not a DCR communication key. */
@@ -122,6 +124,12 @@ export type ServerProfileEnrollmentInput = Readonly<{
122
124
  * derives the identity from the wallet key it creates for this profile.
123
125
  */
124
126
  clientInstanceId?: string;
127
+ /**
128
+ * Server-authorized stable creator identity imported from FHIR or generated
129
+ * locally. Enrollment adds the operational actor DID and registered DCR
130
+ * client/key aliases; it never derives UUIDs from those aliases.
131
+ */
132
+ clinicalCreatorBinding?: ClinicalCreatorBinding;
125
133
  /**
126
134
  * Signed actor/controller VP protected for later SMART operations. Required
127
135
  * for organization/professional/member actors. Individual-controller
@@ -379,6 +387,7 @@ export declare class ServerProfileSessionManager {
379
387
  openProfessional(input: ServerProfessionalOpenInput): Promise<OpenedServerProfessional>;
380
388
  lock(ownerId: string, sessionId: string): Promise<void>;
381
389
  private createClient;
390
+ private createRegisteredProfileClient;
382
391
  private createWallet;
383
392
  /**
384
393
  * Upgrade legacy profiles deterministically after successful PIN unlock.
@@ -391,3 +400,8 @@ export declare class ServerProfileSessionManager {
391
400
  private requireSubject;
392
401
  private now;
393
402
  }
403
+ /**
404
+ * Exports the stable FHIR IPS creator attached to a server profile. Direct
405
+ * writes continue to use `profile.actorDid`; this function is export-only.
406
+ */
407
+ export declare function exportServerProfileClinicalCreatorIps(profile: ServerProfileRecord): ClinicalCreatorIpsExport;
@@ -3,7 +3,7 @@ import { randomBytes } from 'node:crypto';
3
3
  import { ActorKinds } from 'gdc-common-utils-ts/constants/actor-session';
4
4
  import { readLegalOrganizationVerificationCredentialPairFromResponseBody, readServiceControllerCredentialFromResponseBody, } from 'gdc-common-utils-ts/utils/legal-organization-verification-result';
5
5
  import { addLegalRepresentativeCredential, addOrganizationCredential, addServiceControllerCredential, createVP, } from 'gdc-common-utils-ts/utils/vp-token';
6
- import { TransportProfiles, } from 'gdc-sdk-core-ts';
6
+ import { TransportProfiles, resolveClinicalCreatorIpsExport, } from 'gdc-sdk-core-ts';
7
7
  import { NodeManagedWallet } from './node-managed-wallet.js';
8
8
  import { NodeHttpClient } from './node-runtime-client.js';
9
9
  import { OrganizationControllerSdk } from './orchestration/organization-controller-sdk.js';
@@ -58,7 +58,7 @@ export class ServerProfileSessionManager {
58
58
  });
59
59
  const redirectUris = input.redirectUris || input.dcrRedirectUris || [];
60
60
  const clientName = String(input.clientName || input.dcrClientName || '').trim();
61
- const activationRequest = createProfileDeviceActivationRequest({
61
+ const activationDraft = createProfileDeviceActivationRequest({
62
62
  activationCode: input.activationCode,
63
63
  idToken,
64
64
  ...input.routeContext,
@@ -69,8 +69,11 @@ export class ServerProfileSessionManager {
69
69
  .setRedirectUris(redirectUris)
70
70
  .setPublicJwks(publicKeys.filter((entry) => entry.purpose !== 'document-at-rest').map((entry) => entry.publicJwk))
71
71
  .setActorDid(input.actorDid)
72
- .setProfileDid(input.profileDid)
73
- .build();
72
+ .setProfileDid(input.profileDid);
73
+ if (input.clinicalCreatorBinding) {
74
+ activationDraft.setClinicalCreatorBinding(input.clinicalCreatorBinding);
75
+ }
76
+ const activationRequest = activationDraft.build();
74
77
  const activation = await client.activateProfileDeviceWithActivationRequest(activationRequest);
75
78
  const dcrBody = terminalBody(activation.dcr.poll.body);
76
79
  const clientId = findText(dcrBody, ['client_id', 'clientId']);
@@ -86,6 +89,16 @@ export class ServerProfileSessionManager {
86
89
  ...input.professionalProof,
87
90
  })
88
91
  : undefined);
92
+ const clinicalCreatorBinding = input.clinicalCreatorBinding
93
+ ? bindClinicalCreatorChannels(input.clinicalCreatorBinding, {
94
+ actorDid: input.actorDid,
95
+ clientId,
96
+ clientInstanceId,
97
+ keyIds: publicKeys
98
+ .map((entry) => String(entry.publicJwk?.kid || entry.kid || '').trim())
99
+ .filter(Boolean),
100
+ })
101
+ : undefined;
89
102
  const record = {
90
103
  profileId: input.profileId,
91
104
  walletKeyDerivationId,
@@ -99,6 +112,7 @@ export class ServerProfileSessionManager {
99
112
  allowedSubjectDids: unique(input.allowedSubjectDids),
100
113
  clientId,
101
114
  clientInstanceId,
115
+ ...(clinicalCreatorBinding ? { clinicalCreatorBinding } : {}),
102
116
  deviceDid,
103
117
  publicJwks: publicKeys.filter((entry) => entry.purpose !== 'document-at-rest').map((entry) => entry.publicJwk),
104
118
  storagePublicJwk: storagePublicJwk,
@@ -157,6 +171,14 @@ export class ServerProfileSessionManager {
157
171
  role: replacement.employeeRole,
158
172
  sameAs: replacement.employeeActorIdentifier,
159
173
  },
174
+ ...(previous.clinicalCreatorBinding ? {
175
+ clinicalCreatorBinding: {
176
+ ...previous.clinicalCreatorBinding,
177
+ actorDids: [previous.actorDid],
178
+ dcrClientIds: [],
179
+ keyIds: [],
180
+ },
181
+ } : {}),
160
182
  });
161
183
  await this.options.store.deleteSessionsForProfile(profile.profileId);
162
184
  return profile;
@@ -328,12 +350,13 @@ export class ServerProfileSessionManager {
328
350
  profile = await this.ensureRequiredStorageProfile(profile, seed);
329
351
  const walletKeyDerivationId = profile.walletKeyDerivationId || profile.profileId;
330
352
  const wallet = await this.createWallet(walletKeyDerivationId, seed);
353
+ const context = walletContext(walletKeyDerivationId);
331
354
  const smartTokenEndpoint = [
332
355
  this.options.gatewayBaseUrl.replace(/\/+$/, ''),
333
356
  buildIdentityOpenIdSmartTokenPath(profile.routeContext),
334
357
  ].join('');
335
358
  const assertion = await buildWalletClientAssertion(wallet, profile, smartTokenEndpoint, now);
336
- const token = await this.createClient(profile.routeContext, input.idToken).requestSmartToken({
359
+ const token = await this.createRegisteredProfileClient(profile, wallet, context, input.idToken).requestSmartToken({
337
360
  ...profile.routeContext,
338
361
  actorDid: profile.actorDid,
339
362
  subjectDid: input.subjectDid,
@@ -394,13 +417,14 @@ export class ServerProfileSessionManager {
394
417
  : undefined;
395
418
  const walletKeyDerivationId = profile.walletKeyDerivationId || profile.profileId;
396
419
  const wallet = await this.createWallet(walletKeyDerivationId, seed);
420
+ const context = walletContext(walletKeyDerivationId);
397
421
  const now = this.now();
398
422
  const smartTokenEndpoint = [
399
423
  this.options.gatewayBaseUrl.replace(/\/+$/, ''),
400
424
  buildIdentityOpenIdSmartTokenPath(profile.routeContext),
401
425
  ].join('');
402
426
  const assertion = await buildWalletClientAssertion(wallet, profile, smartTokenEndpoint, now);
403
- const token = await this.createClient(profile.routeContext, normalizedIdToken).requestSmartToken({
427
+ const token = await this.createRegisteredProfileClient(profile, wallet, context, normalizedIdToken).requestSmartToken({
404
428
  ...profile.routeContext,
405
429
  actorDid: profile.actorDid,
406
430
  subjectDid: session.subjectDid,
@@ -546,13 +570,11 @@ export class ServerProfileSessionManager {
546
570
  transportProfile: TransportProfiles.DidcommEncryptedForm,
547
571
  secureTransportAdapter,
548
572
  });
549
- // SMART/OpenID uses its own JSON/form protocol and must not be wrapped in
550
- // the encrypted resource transport selected for later business actions.
551
- // Professional business operations are authored by the registered employee
552
- // wallet and therefore use the encrypted DIDComm client. SMART/OpenID below
553
- // remains an independent JSON/form exchange through its dedicated method.
573
+ // Both SMART and later business operations are post-DCR profile actions.
574
+ // Strict GW therefore resolves the same registered employee wallet for the
575
+ // encrypted request and poll transport used by both facades.
554
576
  const sdk = new ProfessionalSdk(operationalClient);
555
- const smartSdk = new ProfessionalSdk(this.createClient(profile.routeContext, idToken));
577
+ const smartSdk = sdk;
556
578
  const digitalTwin = new DigitalTwinSdk(operationalClient, profile.actorDid);
557
579
  let digitalTwinAccessToken;
558
580
  const request = async (smart) => {
@@ -625,6 +647,20 @@ export class ServerProfileSessionManager {
625
647
  appInfo: this.options.appInfo,
626
648
  });
627
649
  }
650
+ createRegisteredProfileClient(profile, wallet, context, bearerToken) {
651
+ return new NodeHttpClient({
652
+ baseUrl: this.options.gatewayBaseUrl,
653
+ ctx: profile.routeContext,
654
+ bearerToken,
655
+ fetchImpl: this.options.fetchImpl,
656
+ appInfo: this.options.appInfo,
657
+ transportProfile: TransportProfiles.DidcommEncryptedForm,
658
+ secureTransportAdapter: {
659
+ pack: (message) => wallet.packForRecipientWithContext(bindOrganizationControllerTransport(message, profile), profile.providerDid, { context }),
660
+ unpack: async (jwe) => (await wallet.unpackWithContext(jwe, { context })).content,
661
+ },
662
+ });
663
+ }
628
664
  async createWallet(walletKeyDerivationId, seed) {
629
665
  const wallet = new NodeManagedWallet({ resolveRecipientJwk: this.options.resolveRecipientJwk });
630
666
  const context = walletContext(walletKeyDerivationId);
@@ -755,6 +791,33 @@ function requireBase64UrlSeed32(seed) {
755
791
  throw new Error('Profile enrollment walletSeed must be a 32-byte base64url value.');
756
792
  }
757
793
  }
794
+ /**
795
+ * Exports the stable FHIR IPS creator attached to a server profile. Direct
796
+ * writes continue to use `profile.actorDid`; this function is export-only.
797
+ */
798
+ export function exportServerProfileClinicalCreatorIps(profile) {
799
+ if (!profile.clinicalCreatorBinding) {
800
+ throw new Error('Server profile has no clinical creator binding.');
801
+ }
802
+ return resolveClinicalCreatorIpsExport({
803
+ bindings: [profile.clinicalCreatorBinding],
804
+ evidence: { actorDid: profile.actorDid },
805
+ });
806
+ }
807
+ function bindClinicalCreatorChannels(binding, channel) {
808
+ const stored = {
809
+ ...binding,
810
+ actorDids: unique([...(binding.actorDids || []), channel.actorDid]),
811
+ dcrClientIds: unique([
812
+ ...(binding.dcrClientIds || []),
813
+ channel.clientId,
814
+ channel.clientInstanceId,
815
+ ]),
816
+ keyIds: unique([...(binding.keyIds || []), ...channel.keyIds]),
817
+ };
818
+ resolveClinicalCreatorIpsExport({ bindings: [stored], evidence: { actorDid: channel.actorDid } });
819
+ return stored;
820
+ }
758
821
  function requireEnrollment(input) {
759
822
  for (const [name, value] of Object.entries({
760
823
  ownerId: input.ownerId,
@@ -775,6 +838,10 @@ function requireEnrollment(input) {
775
838
  if (input.professionalProof && !String(input.professionalProof.role || '').trim()) {
776
839
  throw new Error('Profile enrollment professionalProof requires role.');
777
840
  }
841
+ if (input.professionalProof && input.clinicalCreatorBinding
842
+ && String(input.professionalProof.role).trim() !== String(input.clinicalCreatorBinding.role).trim()) {
843
+ throw new Error('Clinical creator binding role must equal professionalProof.role.');
844
+ }
778
845
  if (!input.allowedSubjectDids.length)
779
846
  throw new Error('Profile enrollment requires an allowed subject.');
780
847
  }
@@ -1,5 +1,7 @@
1
1
  // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
2
  import { HealthcareConsentPurposes } from 'gdc-common-utils-ts/constants';
3
+ import { IdentityAuthResourceTypes, SmartPostDcrActions, } from 'gdc-common-utils-ts/constants/identity-auth';
4
+ import { GatewayRouteFormats, GatewayRouteSections, } from 'gdc-common-utils-ts/constants/gateway-response';
3
5
  import { buildClientAssertionJwt } from 'gdc-common-utils-ts/utils/client-assertion';
4
6
  import { resolvePollOptionsFromSeconds } from './poll-options.js';
5
7
  /**
@@ -109,16 +111,19 @@ function normalizeResolvedSmartTokenEndpoint(value) {
109
111
  if (!['http:', 'https:'].includes(endpoint.protocol)) {
110
112
  throw new Error('Resolved SMART token endpoint must use HTTP(S).');
111
113
  }
112
- if (!endpoint.pathname.endsWith('/identity/openid/smart/token')) {
113
- throw new Error('Resolved SMART token endpoint must target /identity/openid/smart/token.');
114
+ if (!endpoint.pathname.endsWith(smartTokenRouteSuffix(SmartPostDcrActions.Token))) {
115
+ throw new Error(`Resolved SMART token endpoint must target ${smartTokenRouteSuffix(SmartPostDcrActions.Token)}.`);
114
116
  }
115
117
  return endpoint.toString();
116
118
  }
117
119
  function smartTokenPollEndpointFromTokenEndpoint(tokenEndpoint) {
118
120
  const endpoint = new URL(tokenEndpoint);
119
- endpoint.pathname = endpoint.pathname.replace(/\/identity\/openid\/smart\/token$/, '/identity/openid/smart/_batch-response');
121
+ endpoint.pathname = `${endpoint.pathname.slice(0, -smartTokenRouteSuffix(SmartPostDcrActions.Token).length)}${smartTokenRouteSuffix(SmartPostDcrActions.TokenResponse)}`;
120
122
  return endpoint.toString();
121
123
  }
124
+ function smartTokenRouteSuffix(action) {
125
+ return `/${GatewayRouteSections.Identity}/${GatewayRouteFormats.OpenId}/${IdentityAuthResourceTypes.Smart}/${action}`;
126
+ }
122
127
  async function resolveClientAssertion(input) {
123
128
  const explicit = String(input.clientAssertion || '').trim();
124
129
  if (explicit)
@@ -132,13 +137,17 @@ async function resolveClientAssertion(input) {
132
137
  });
133
138
  }
134
139
  function resolveTokenExchangeResult(exchange, normalizedScopes, tokenCacheKey, setTokenCache) {
135
- const exchangeBody = exchange.poll.body ?? {};
140
+ const pollResult = exchange.poll;
141
+ const pollPayload = pollResult.body ?? {};
142
+ // Encrypted DIDComm returns the token in the authenticated message body;
143
+ // plain compatibility transports may still expose the token at the root.
144
+ const exchangeBody = pollPayload.body || pollPayload;
136
145
  const accessToken = String(exchangeBody.access_token || '').trim();
137
- if (exchange.poll.status >= 400 || !accessToken) {
146
+ if (pollResult.status >= 400 || !accessToken) {
138
147
  return {
139
148
  status: 'failed',
140
- statusCode: exchange.poll.status,
141
- response: exchange.poll.body,
149
+ statusCode: pollResult.status,
150
+ response: exchangeBody,
142
151
  };
143
152
  }
144
153
  const tokenType = String(exchangeBody.token_type || 'Bearer');
@@ -156,8 +165,8 @@ function resolveTokenExchangeResult(exchange, normalizedScopes, tokenCacheKey, s
156
165
  accessToken,
157
166
  tokenType,
158
167
  scopes: resolvedScopes,
159
- statusCode: exchange.poll.status,
160
- response: exchange.poll.body,
168
+ statusCode: pollResult.status,
169
+ response: exchangeBody,
161
170
  };
162
171
  }
163
172
  function createRuntimeUuid() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-sdk-node-ts",
3
- "version": "2.6.6",
3
+ "version": "2.7.0",
4
4
  "description": "Next-generation Node runtime package for the GDC SDK family",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Antifraud Services Inc.",
@@ -38,8 +38,8 @@
38
38
  "test:e2e:live-gw:clean": "bash ./scripts/run-live-gw-clean.sh"
39
39
  },
40
40
  "dependencies": {
41
- "gdc-common-utils-ts": "2.7.4",
42
- "gdc-sdk-core-ts": "2.4.6"
41
+ "gdc-common-utils-ts": "2.8.1",
42
+ "gdc-sdk-core-ts": "2.5.1"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^20.14.10",