gdc-sdk-node-ts 2.6.7 → 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.
@@ -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 {
@@ -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
+ }
@@ -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
@@ -392,3 +400,8 @@ export declare class ServerProfileSessionManager {
392
400
  private requireSubject;
393
401
  private now;
394
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;
@@ -769,6 +791,33 @@ function requireBase64UrlSeed32(seed) {
769
791
  throw new Error('Profile enrollment walletSeed must be a 32-byte base64url value.');
770
792
  }
771
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
+ }
772
821
  function requireEnrollment(input) {
773
822
  for (const [name, value] of Object.entries({
774
823
  ownerId: input.ownerId,
@@ -789,6 +838,10 @@ function requireEnrollment(input) {
789
838
  if (input.professionalProof && !String(input.professionalProof.role || '').trim()) {
790
839
  throw new Error('Profile enrollment professionalProof requires role.');
791
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
+ }
792
845
  if (!input.allowedSubjectDids.length)
793
846
  throw new Error('Profile enrollment requires an allowed subject.');
794
847
  }
@@ -137,16 +137,17 @@ async function resolveClientAssertion(input) {
137
137
  });
138
138
  }
139
139
  function resolveTokenExchangeResult(exchange, normalizedScopes, tokenCacheKey, setTokenCache) {
140
- const pollBody = exchange.poll.body ?? {};
140
+ const pollResult = exchange.poll;
141
+ const pollPayload = pollResult.body ?? {};
141
142
  // Encrypted DIDComm returns the token in the authenticated message body;
142
143
  // plain compatibility transports may still expose the token at the root.
143
- const exchangeBody = pollBody.body || pollBody;
144
+ const exchangeBody = pollPayload.body || pollPayload;
144
145
  const accessToken = String(exchangeBody.access_token || '').trim();
145
- if (exchange.poll.status >= 400 || !accessToken) {
146
+ if (pollResult.status >= 400 || !accessToken) {
146
147
  return {
147
148
  status: 'failed',
148
- statusCode: exchange.poll.status,
149
- response: exchange.poll.body,
149
+ statusCode: pollResult.status,
150
+ response: exchangeBody,
150
151
  };
151
152
  }
152
153
  const tokenType = String(exchangeBody.token_type || 'Bearer');
@@ -164,8 +165,8 @@ function resolveTokenExchangeResult(exchange, normalizedScopes, tokenCacheKey, s
164
165
  accessToken,
165
166
  tokenType,
166
167
  scopes: resolvedScopes,
167
- statusCode: exchange.poll.status,
168
- response: exchange.poll.body,
168
+ statusCode: pollResult.status,
169
+ response: exchangeBody,
169
170
  };
170
171
  }
171
172
  function createRuntimeUuid() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-sdk-node-ts",
3
- "version": "2.6.7",
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.8",
42
- "gdc-sdk-core-ts": "2.4.9"
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",