gdc-sdk-node-ts 2.5.2 → 2.6.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.
@@ -98,8 +98,10 @@ export async function activateEmployeeDeviceWithActivationCodeWithDeps(deps) {
98
98
  throw new Error('Device activation requires a stable device, software, redirect, or key identifier.');
99
99
  const exchangePayload = {
100
100
  thid: `exchange-${createRuntimeUuid()}`,
101
- [IdentityAuthRequestFields.SubjectToken]: deps.input.activationCode,
102
- [IdentityAuthRequestFields.ClientInstanceId]: clientInstanceId,
101
+ body: {
102
+ [IdentityAuthRequestFields.SubjectToken]: deps.input.activationCode,
103
+ [IdentityAuthRequestFields.ClientInstanceId]: clientInstanceId,
104
+ },
103
105
  };
104
106
  const exchange = await deps.submitAndPollWithBearerToken(deps.input.idToken, deps.identityTokenExchangePath(deps.routeCtx), deps.identityTokenExchangePollPath(deps.routeCtx), exchangePayload, deps.input.pollOptions);
105
107
  const exchangeDiagnostics = firstFailedOperationOutcomeDiagnostic(exchange.poll.body)
@@ -122,8 +124,10 @@ export async function activateEmployeeDeviceWithActivationCodeWithDeps(deps) {
122
124
  }
123
125
  const dcrPayload = {
124
126
  thid: `dcr-${createRuntimeUuid()}`,
125
- [IdentityAuthRequestFields.Code]: deps.input.activationCode,
126
- ...deps.input.dcrPayload,
127
+ body: {
128
+ [IdentityAuthRequestFields.Code]: deps.input.activationCode,
129
+ ...deps.input.dcrPayload,
130
+ },
127
131
  };
128
132
  const dcr = await deps.submitAndPollWithBearerToken(initialAccessToken, deps.identityDeviceDcrPath(deps.routeCtx), deps.identityDeviceDcrPollPath(deps.routeCtx), dcrPayload, deps.input.pollOptions);
129
133
  return {
@@ -37,9 +37,11 @@ export type DigitalTwinSearchInput = {
37
37
  /**
38
38
  * One public ResearchSubject digital twin returned by search.
39
39
  *
40
- * `composition` is the canonical Composition GW uses as the aggregate's
41
- * internal index document. It joins the ResearchSubject to the projected
42
- * clinical resources later returned by `ResearchSubject/$summary`.
40
+ * `composition` is an SDK-only normalized view of the canonical Composition:
41
+ * it comes from mixed `meta.claims` in claims-first API responses or from
42
+ * `ResearchSubject.contained[]` in strict FHIR R4/R5. GW never emits an
43
+ * ad-hoc `ResearchSubject.composition` wire property. The canonical index joins
44
+ * the ResearchSubject to resources later returned by `$summary`.
43
45
  */
44
46
  export type DigitalTwinSearchMatch = Record<string, unknown> & {
45
47
  resourceType?: 'ResearchSubject';
@@ -2,6 +2,7 @@
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { HealthcareDocumentTypes } from 'gdc-common-utils-ts/constants';
4
4
  import { CompositionClaim } from 'gdc-common-utils-ts/models';
5
+ import { extractBundleSearchResources } from 'gdc-common-utils-ts/utils/organization-employee-lifecycle';
5
6
  const DIGITAL_TWIN_SUBJECT_URN_UUID = /^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
6
7
  /** Returns whether a value can be used as a pseudonymous digital-twin subject. */
7
8
  export function isDigitalTwinSubjectId(value) {
@@ -38,6 +39,62 @@ export function buildDigitalTwinSelectionIdentifier(input) {
38
39
  compositionId: selectionId,
39
40
  });
40
41
  }
42
+ function asRecord(value) {
43
+ return value && typeof value === 'object' && !Array.isArray(value)
44
+ ? value
45
+ : undefined;
46
+ }
47
+ /**
48
+ * Normalizes the public aggregate without leaking its wire representation.
49
+ * Claims-first responses mix ResearchSubject and Composition flat claims in
50
+ * `meta.claims`; versioned FHIR responses carry the translated Composition in
51
+ * `contained[]`. The ad-hoc `composition` property remains read-only legacy.
52
+ */
53
+ function normalizeDigitalTwinSearchMatch(resource) {
54
+ const metaClaims = asRecord(asRecord(resource.meta)?.claims);
55
+ const containedComposition = Array.isArray(resource.contained)
56
+ ? resource.contained.map(asRecord).find((candidate) => candidate?.resourceType === 'Composition')
57
+ : undefined;
58
+ const legacyComposition = asRecord(resource.composition);
59
+ const legacyCompositionClaims = asRecord(asRecord(legacyComposition?.meta)?.claims);
60
+ const containedSubject = asRecord(containedComposition?.subject)?.reference;
61
+ const subject = resource[CompositionClaim.Subject]
62
+ ?? metaClaims?.[CompositionClaim.Subject]
63
+ ?? containedSubject
64
+ ?? legacyComposition?.[CompositionClaim.Subject]
65
+ ?? legacyCompositionClaims?.[CompositionClaim.Subject];
66
+ assertDigitalTwinSubjectId(subject);
67
+ const composition = containedComposition
68
+ ?? legacyComposition
69
+ ?? (metaClaims ? { resourceType: 'Composition', meta: { claims: metaClaims } } : undefined);
70
+ return {
71
+ ...resource,
72
+ [CompositionClaim.Subject]: subject,
73
+ ...(composition ? { composition } : {}),
74
+ };
75
+ }
76
+ /** Finds the primary-document Bundle inside a direct or decoded DIDComm response. */
77
+ function findDigitalTwinSearchBundle(value) {
78
+ const pending = [value];
79
+ const visited = new Set();
80
+ while (pending.length > 0) {
81
+ const candidate = pending.shift();
82
+ if (!candidate || typeof candidate !== 'object' || visited.has(candidate))
83
+ continue;
84
+ visited.add(candidate);
85
+ if (Array.isArray(candidate)) {
86
+ pending.push(...candidate);
87
+ continue;
88
+ }
89
+ const record = candidate;
90
+ const hasEntries = Array.isArray(record.data) || Array.isArray(record.entry);
91
+ if (hasEntries && (record.resourceType === 'Bundle' || candidate === value)) {
92
+ return record;
93
+ }
94
+ pending.push(...Object.values(record));
95
+ }
96
+ return undefined;
97
+ }
41
98
  function normalizeResearchTags(tags) {
42
99
  if (!Array.isArray(tags) || tags.length === 0)
43
100
  throw new Error('At least one research tag is required.');
@@ -61,24 +118,21 @@ function normalizeResearchTags(tags) {
61
118
  }
62
119
  /** Opens the GW async response and exposes the matched ResearchSubjects directly. */
63
120
  export function readDigitalTwinSearchResult(operation) {
64
- const body = operation?.poll?.body;
65
- const responseEntries = Array.isArray(body?.data)
66
- ? body.data
67
- : Array.isArray(body?.entry)
68
- ? body.entry
69
- : [];
70
- const responseEntry = responseEntries.find((entry) => {
71
- const resource = entry?.resource;
72
- return Boolean(resource && typeof resource === 'object' && Array.isArray(resource.data));
73
- });
74
- if (!responseEntry?.resource || !Array.isArray(responseEntry.resource.data)) {
75
- throw new Error('Digital twin search did not return a Composition result set.');
76
- }
77
- const matches = responseEntry.resource.data;
78
- for (const match of matches) {
79
- assertDigitalTwinSubjectId(match?.[CompositionClaim.Subject]);
121
+ const bundle = findDigitalTwinSearchBundle(operation?.poll?.body);
122
+ const entries = Array.isArray(bundle?.data)
123
+ ? bundle.data
124
+ : Array.isArray(bundle?.entry)
125
+ ? bundle.entry
126
+ : undefined;
127
+ if (!bundle || !entries) {
128
+ throw new Error('Digital twin search did not return a ResearchSubject result set.');
80
129
  }
81
- const parsedTotal = Number(responseEntry.resource.total);
130
+ const matches = extractBundleSearchResources({ ...bundle, data: entries })
131
+ .map(normalizeDigitalTwinSearchMatch);
132
+ const legacyAggregate = entries
133
+ .map((entry) => entry?.resource)
134
+ .find((resource) => resource && typeof resource === 'object' && Array.isArray(resource.data));
135
+ const parsedTotal = Number(legacyAggregate?.total ?? bundle.total);
82
136
  return {
83
137
  total: Number.isFinite(parsedTotal) ? parsedTotal : matches.length,
84
138
  matches,
@@ -12,8 +12,9 @@ export declare class DigitalTwinSdk {
12
12
  requestSmartToken(input: SmartTokenRequestInput): Promise<SmartTokenExchangeResult>;
13
13
  /**
14
14
  * Searches the public `ResearchSubject/_search` aggregate with FHIR
15
- * Parameters. Each match includes the canonical Composition GW uses
16
- * internally to index that ResearchSubject and connect its resources.
15
+ * Parameters. The high-level match normalizes either mixed claims-first
16
+ * `meta.claims` or a versioned FHIR `contained[]` Composition. Callers never
17
+ * need to navigate either transport representation.
17
18
  *
18
19
  * MVP basic search supplies `sections`, `dateFrom`, optional `dateTo`, and
19
20
  * `text`. GW resolves an omitted `dateTo` to its current time, applies OR
@@ -37,8 +37,9 @@ export class DigitalTwinSdk {
37
37
  }
38
38
  /**
39
39
  * Searches the public `ResearchSubject/_search` aggregate with FHIR
40
- * Parameters. Each match includes the canonical Composition GW uses
41
- * internally to index that ResearchSubject and connect its resources.
40
+ * Parameters. The high-level match normalizes either mixed claims-first
41
+ * `meta.claims` or a versioned FHIR `contained[]` Composition. Callers never
42
+ * need to navigate either transport representation.
42
43
  *
43
44
  * MVP basic search supplies `sections`, `dateFrom`, optional `dateTo`, and
44
45
  * `text`. GW resolves an omitted `dateTo` to its current time, applies OR
@@ -72,7 +72,11 @@ export type EmployeeActivationGrantInput = Readonly<{
72
72
  export declare function createEmployeeActivationGrant(input: EmployeeActivationGrantInput): EmployeeActivationGrant;
73
73
  /** Verifies that a stored routing grant belongs to the authenticated email. */
74
74
  export declare function employeeActivationGrantMatchesEmail(grant: EmployeeActivationGrant, email: string): boolean;
75
- /** Stable profile id derived from account, employee and provider identities. */
75
+ /**
76
+ * Stable profile id derived from account, employee, provider and portal
77
+ * installation identities. The portal installation id only separates local
78
+ * profiles; it is not a cryptographic DCR key identifier.
79
+ */
76
80
  export declare function buildOrganizationEmployeeProfileId(input: Readonly<{
77
81
  ownerId: string;
78
82
  employeeDid: string;
@@ -187,7 +187,11 @@ export function createEmployeeActivationGrant(input) {
187
187
  export function employeeActivationGrantMatchesEmail(grant, email) {
188
188
  return grant.employeeActorIdentifier === buildStableActorIdentifier({ contactKind: 'email', contact: email });
189
189
  }
190
- /** Stable profile id derived from account, employee and provider identities. */
190
+ /**
191
+ * Stable profile id derived from account, employee, provider and portal
192
+ * installation identities. The portal installation id only separates local
193
+ * profiles; it is not a cryptographic DCR key identifier.
194
+ */
191
195
  export function buildOrganizationEmployeeProfileId(input) {
192
196
  const digest = createHash('sha256')
193
197
  .update([input.ownerId, input.employeeDid, input.providerDid, input.clientInstanceId].join('\u0000'))
@@ -215,7 +219,6 @@ export async function enrollInvitedOrganizationEmployeeWithDeps(input, deps) {
215
219
  pin: input.pin,
216
220
  idToken: input.idToken,
217
221
  activationCode: input.activationCode,
218
- clientInstanceId: input.clientInstanceId,
219
222
  dcrRedirectUris: input.dcrRedirectUris,
220
223
  dcrClientName: input.dcrClientName,
221
224
  // The grant contains the server-verified stable actor alias and role. The
@@ -548,7 +548,11 @@ export class ServerProfileSessionManager {
548
548
  });
549
549
  // SMART/OpenID uses its own JSON/form protocol and must not be wrapped in
550
550
  // the encrypted resource transport selected for later business actions.
551
- const sdk = new ProfessionalSdk(this.createClient(profile.routeContext, idToken));
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.
554
+ const sdk = new ProfessionalSdk(operationalClient);
555
+ const smartSdk = new ProfessionalSdk(this.createClient(profile.routeContext, idToken));
552
556
  const digitalTwin = new DigitalTwinSdk(operationalClient, profile.actorDid);
553
557
  let digitalTwinAccessToken;
554
558
  const request = async (smart) => {
@@ -567,7 +571,7 @@ export class ServerProfileSessionManager {
567
571
  ...input.professionalProof,
568
572
  role,
569
573
  });
570
- return sdk.requestSmartToken({
574
+ return smartSdk.requestSmartToken({
571
575
  ...profile.routeContext,
572
576
  actorDid: profile.actorDid,
573
577
  subjectDid: String(smart.subjectDid || '').trim() || undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-sdk-node-ts",
3
- "version": "2.5.2",
3
+ "version": "2.6.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.6.2",
42
- "gdc-sdk-core-ts": "2.4.0"
41
+ "gdc-common-utils-ts": "2.7.0",
42
+ "gdc-sdk-core-ts": "2.4.1"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^20.14.10",
@@ -1,16 +0,0 @@
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>;
@@ -1,34 +0,0 @@
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
- }