gdc-sdk-node-ts 2.4.32 → 2.4.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -113,6 +113,8 @@ If you need the shortest path:
113
113
 
114
114
  - main onboarding guide:
115
115
  [docs/101-SDK_END_TO_END.md](./docs/101-SDK_END_TO_END.md)
116
+ - wallet `context`, stable `runtimeId` and KMS-encrypted seed custody:
117
+ [docs/101-WALLET_CONTEXT_AND_KEY_CUSTODY.md](./docs/101-WALLET_CONTEXT_AND_KEY_CUSTODY.md)
116
118
  - first public actor surfaces:
117
119
  `HostOnboardingSdk`, `OrganizationControllerSdk`,
118
120
  `IndividualControllerSdk`, `ProfessionalSdk`
@@ -649,7 +651,7 @@ modules below.
649
651
  - functions: `createOrganizationEmployeeWithDeps(...)`, `importIpsOrFhirAndUpdateIndexWithDeps(...)`, `upsertRelatedPersonAndPollWithDeps(...)`, `ingestCommunicationAndUpdateIndexWithDeps(...)`, `searchClinicalBundleWithDeps(...)`, `searchLatestIpsWithDeps(...)`, `grantProfessionalAccessWithDeps(...)`, `setDigitalTwinSecondaryUseConsentWithDeps(...)`, `purgeDigitalTwinSubjectLinkWithDeps(...)`
650
652
  - [`src/digital-twin.ts`](src/digital-twin.ts)
651
653
  - types: `DigitalTwinSearchInput`, `DigitalTwinSelectionInput`, `DigitalTwinResearchTag`, `DigitalTwinMaterializationInput`
652
- - functions: `createDigitalTwinSecondaryUseConsentIdentifier()`, `searchDigitalTwinsWithDeps(...)`, `saveDigitalTwinSelectionWithDeps(...)`, `materializeDigitalTwinWithDeps(...)`
654
+ - functions: `searchDigitalTwinsWithDeps(...)`, `saveDigitalTwinSelectionWithDeps(...)`, `materializeDigitalTwinWithDeps(...)`
653
655
  - [`src/orchestration/digital-twin-sdk.ts`](src/orchestration/digital-twin-sdk.ts)
654
656
  - class: `DigitalTwinSdk` (`requestSmartToken`, `search`, `saveSelection`, `materialize`)
655
657
  - [`src/session.ts`](src/session.ts)
@@ -3,14 +3,6 @@ import type { MetaTagCoding } from 'gdc-common-utils-ts/models/confidential-stor
3
3
  import type { RouteContext } from './individual-onboarding.js';
4
4
  import type { PollOptions, SubmitAndPollResult } from './orchestration/client-port.js';
5
5
  export type DigitalTwinFhirFormat = 'org.hl7.fhir.r4' | 'org.hl7.fhir.api';
6
- /**
7
- * Creates the opaque FHIR identifier for the provider-level secondary-use
8
- * Consent. Call this exactly once when the server creates the subject's index
9
- * enrollment, persist the result there, and reuse it for every permit/deny.
10
- * It is intentionally random and must not be derived from a subject or tenant
11
- * identifier.
12
- */
13
- export declare function createDigitalTwinSecondaryUseConsentIdentifier(): string;
14
6
  /** Returns whether a value can be used as a pseudonymous digital-twin subject. */
15
7
  export declare function isDigitalTwinSubjectId(value: unknown): value is string;
16
8
  /** Rejects operational DIDs and malformed or caller-invented subject shapes. */
@@ -3,16 +3,6 @@ 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
5
  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
- * Creates the opaque FHIR identifier for the provider-level secondary-use
8
- * Consent. Call this exactly once when the server creates the subject's index
9
- * enrollment, persist the result there, and reuse it for every permit/deny.
10
- * It is intentionally random and must not be derived from a subject or tenant
11
- * identifier.
12
- */
13
- export function createDigitalTwinSecondaryUseConsentIdentifier() {
14
- return `urn:uuid:${randomUUID()}`;
15
- }
16
6
  /** Returns whether a value can be used as a pseudonymous digital-twin subject. */
17
7
  export function isDigitalTwinSubjectId(value) {
18
8
  return DIGITAL_TWIN_SUBJECT_URN_UUID.test(String(value || '').trim());
@@ -48,9 +48,11 @@ export function extractConsentRules(value) {
48
48
  ? record.claims
49
49
  : undefined;
50
50
  for (const candidate of [record, claims]) {
51
- if (!candidate || !looksLikeConsentRule(candidate))
51
+ if (!candidate)
52
+ continue;
53
+ const rule = normalizeConsentRule(candidate);
54
+ if (!looksLikeConsentRule(rule))
52
55
  continue;
53
- const rule = candidate;
54
56
  const key = String(rule[ClaimConsent.identifier] || JSON.stringify(rule));
55
57
  if (!seenRules.has(key)) {
56
58
  seenRules.add(key);
@@ -62,6 +64,18 @@ export function extractConsentRules(value) {
62
64
  visit(value);
63
65
  return candidates;
64
66
  }
67
+ function normalizeConsentRule(value) {
68
+ const normalized = { ...value };
69
+ const context = String(value['@context'] || '').trim();
70
+ if (context) {
71
+ const prefix = context.endsWith('.') ? context : `${context}.`;
72
+ for (const [key, claimValue] of Object.entries(value)) {
73
+ if (key.startsWith(prefix))
74
+ normalized[key.slice(prefix.length)] = claimValue;
75
+ }
76
+ }
77
+ return normalized;
78
+ }
65
79
  function looksLikeConsentRule(value) {
66
80
  return Boolean(String(value[ClaimConsent.subject] || '').trim()
67
81
  && String(value[ClaimConsent.actorIdentifier] || '').trim()
@@ -76,10 +76,12 @@ export declare class NodeManagedWallet implements IWallet {
76
76
  * representative; these runtime keys protect DIDComm communications.
77
77
  *
78
78
  * The caller owns durable wallet custody. When deterministic provisioning is
79
- * used, protect `seedMaterial` in the portal wallet store (for example behind
80
- * the user's PIN and a server-held KEK) so the same private keys can be
81
- * reconstructed after restart. ICA and GW receive only this returned public
82
- * JWKS.
79
+ * used, protect `seedMaterial` in the portal wallet store (for example with
80
+ * its KMS/KEK and optionally a user PIN) and persist the same non-secret
81
+ * `context.runtime.runtimeId`. A fresh `NodeManagedWallet` reconstructs the
82
+ * same private keys from that seed and context after restart, so private JWKs
83
+ * do not need separate persistence. ICA and GW receive only this returned
84
+ * public JWKS.
83
85
  */
84
86
  initializeCommunicationJsonWebKeySet(context: WalletExecutionContext, options?: NodeCommunicationWalletInitialization): Promise<JwkSet>;
85
87
  /**
@@ -143,10 +143,12 @@ export class NodeManagedWallet {
143
143
  * representative; these runtime keys protect DIDComm communications.
144
144
  *
145
145
  * The caller owns durable wallet custody. When deterministic provisioning is
146
- * used, protect `seedMaterial` in the portal wallet store (for example behind
147
- * the user's PIN and a server-held KEK) so the same private keys can be
148
- * reconstructed after restart. ICA and GW receive only this returned public
149
- * JWKS.
146
+ * used, protect `seedMaterial` in the portal wallet store (for example with
147
+ * its KMS/KEK and optionally a user PIN) and persist the same non-secret
148
+ * `context.runtime.runtimeId`. A fresh `NodeManagedWallet` reconstructs the
149
+ * same private keys from that seed and context after restart, so private JWKs
150
+ * do not need separate persistence. ICA and GW receive only this returned
151
+ * public JWKS.
150
152
  */
151
153
  async initializeCommunicationJsonWebKeySet(context, options = {}) {
152
154
  if (!context.runtime?.runtimeId) {
@@ -1,4 +1,3 @@
1
- import { type ConsentRule } from 'gdc-common-utils-ts/models/consent-rule';
2
1
  import { type IndividualControllerCredentialInput, type IndividualControllerVpPayloadInput, type IndividualSubjectCredentialInput } from 'gdc-common-utils-ts';
3
2
  import { type NodeRuntimeClient, type PollOptions, type SubmitAndPollResult, type SubmitPayload } from './client-port.js';
4
3
  import type { FamilyOrganizationSummary } from 'gdc-common-utils-ts/utils/family-organization-summary';
@@ -129,8 +128,8 @@ export declare class IndividualControllerSdk {
129
128
  * Enables or disables the subject's secondary-use digital-twin projection.
130
129
  * This is the canonical patient-side operation; application code must not
131
130
  * publish a canonical Composition directly into the research index. The
132
- * caller reuses the server-owned identifier created once with
133
- * `createDigitalTwinSecondaryUseConsentIdentifier()` during index enrollment.
131
+ * caller supplies only the portal/software/study reference. GW owns and
132
+ * reuses the underlying FHIR Consent identifier.
134
133
  */
135
134
  setDigitalTwinSecondaryUseConsent(ctx: RouteContext, input: DigitalTwinSecondaryUseConsentInput): Promise<DigitalTwinSecondaryUseConsentResult>;
136
135
  /**
@@ -139,15 +138,14 @@ export declare class IndividualControllerSdk {
139
138
  * data already shared for research.
140
139
  */
141
140
  purgeDigitalTwinSubjectLink(ctx: RouteContext, input: DigitalTwinSubjectLinkPurgeInput): Promise<DigitalTwinSubjectLinkPurgeResult>;
142
- /** Returns whether the subject currently permits projection by its index provider. */
141
+ /** Returns the current decision for one portal, software or research study. */
143
142
  getDigitalTwinSecondaryUseConsentStatus(ctx: RouteContext, input: Readonly<{
144
143
  subjectDid: string;
145
144
  indexProviderOrganizationDid: string;
146
- consentIdentifier: string;
145
+ researchUseReference: string;
147
146
  }>): Promise<Readonly<{
148
147
  exists: boolean;
149
148
  enabled: boolean;
150
- consent?: ConsentRule;
151
149
  }>>;
152
150
  /**
153
151
  * Searches indexed clinical bundles for the current subject/controller context.
@@ -192,8 +192,8 @@ export class IndividualControllerSdk {
192
192
  * Enables or disables the subject's secondary-use digital-twin projection.
193
193
  * This is the canonical patient-side operation; application code must not
194
194
  * publish a canonical Composition directly into the research index. The
195
- * caller reuses the server-owned identifier created once with
196
- * `createDigitalTwinSecondaryUseConsentIdentifier()` during index enrollment.
195
+ * caller supplies only the portal/software/study reference. GW owns and
196
+ * reuses the underlying FHIR Consent identifier.
197
197
  */
198
198
  setDigitalTwinSecondaryUseConsent(ctx, input) {
199
199
  assertFacadeCapability(this.capabilities, ActorCapabilities.IndividualGenerateDigitalTwin, ActorKinds.IndividualController, 'setDigitalTwinSecondaryUseConsent');
@@ -208,26 +208,27 @@ export class IndividualControllerSdk {
208
208
  assertFacadeCapability(this.capabilities, ActorCapabilities.IndividualGenerateDigitalTwin, ActorKinds.IndividualController, 'purgeDigitalTwinSubjectLink');
209
209
  return requireClientMethod(this.client, 'purgeDigitalTwinSubjectLink')(ctx, input);
210
210
  }
211
- /** Returns whether the subject currently permits projection by its index provider. */
211
+ /** Returns the current decision for one portal, software or research study. */
212
212
  async getDigitalTwinSecondaryUseConsentStatus(ctx, input) {
213
213
  assertFacadeCapability(this.capabilities, ActorCapabilities.IndividualGenerateDigitalTwin, ActorKinds.IndividualController, 'getDigitalTwinSecondaryUseConsentStatus');
214
- const activeConsents = await new GatewayActiveConsentProvider(this.client, ctx)
214
+ const consents = await new GatewayActiveConsentProvider(this.client, ctx)
215
215
  .getActiveConsentsForSubject(input.subjectDid);
216
- const consentIdentifier = String(input.consentIdentifier || '').trim();
217
- if (!consentIdentifier)
218
- throw new Error('consentIdentifier is required to distinguish the portal rule from study consents.');
216
+ const researchUseReference = String(input.researchUseReference || '').trim();
217
+ if (!researchUseReference)
218
+ throw new Error('researchUseReference is required to identify the portal, software or study consent.');
219
219
  const indexProviderOrganizationDid = String(input.indexProviderOrganizationDid || '').trim();
220
220
  if (!indexProviderOrganizationDid)
221
221
  throw new Error('indexProviderOrganizationDid is required.');
222
- const consent = activeConsents.find((rule) => {
222
+ const consent = consents.find((rule) => {
223
+ const claims = rule;
223
224
  const actions = String(rule[ClaimConsent.action] || '').split(',').map((value) => value.trim());
224
- return String(rule[ClaimConsent.identifier] || '').trim() === consentIdentifier
225
+ return String(claims[ClaimConsent.sourceReference] || '').trim() === researchUseReference
225
226
  && String(rule[ClaimConsent.actorIdentifier] || '').trim() === indexProviderOrganizationDid
226
227
  && String(rule[ClaimConsent.purpose] || '').trim().toUpperCase() === String(HealthcareConsentPurposes.Research).toUpperCase()
227
228
  && actions.includes(ServiceCapability.DigitalTwinReader);
228
229
  });
229
230
  return consent
230
- ? { exists: true, enabled: String(consent[ClaimConsent.decision] || '').trim().toLowerCase() === 'permit', consent }
231
+ ? { exists: true, enabled: String(consent[ClaimConsent.decision] || '').trim().toLowerCase() === 'permit' }
231
232
  : { exists: false, enabled: false };
232
233
  }
233
234
  /**
@@ -508,11 +508,11 @@ export type DigitalTwinSecondaryUseConsentInput = {
508
508
  indexProviderOrganizationDid: string;
509
509
  decision: 'permit' | 'deny';
510
510
  /**
511
- * Server-owned FHIR Consent identifier. Create it once with
512
- * `createDigitalTwinSecondaryUseConsentIdentifier()` in the index-enrollment
513
- * transaction, persist it there and never accept it from the browser.
511
+ * Stable URL or URI identifying the portal, software or research study whose
512
+ * permission is being changed. GW uses it as `Consent.source-reference` to
513
+ * find or create the rule; callers never manage `Consent.identifier`.
514
514
  */
515
- consentIdentifier: string;
515
+ researchUseReference: string;
516
516
  consentDate?: string;
517
517
  dataType?: string;
518
518
  pollOptions?: {
@@ -811,8 +811,9 @@ export async function grantProfessionalAccessWithDeps(routeCtx, input, deps) {
811
811
  * tenant-private subject alias.
812
812
  */
813
813
  export async function setDigitalTwinSecondaryUseConsentWithDeps(routeCtx, input, deps) {
814
- if (!String(input.consentIdentifier || '').trim()) {
815
- throw new Error('consentIdentifier is required for idempotent digital-twin consent updates.');
814
+ const researchUseReference = String(input.researchUseReference || '').trim();
815
+ if (!researchUseReference) {
816
+ throw new Error('researchUseReference is required to identify the portal, software or study consent.');
816
817
  }
817
818
  const indexProviderOrganizationDid = String(input.indexProviderOrganizationDid || '').trim();
818
819
  if (!indexProviderOrganizationDid) {
@@ -827,7 +828,9 @@ export async function setDigitalTwinSecondaryUseConsentWithDeps(routeCtx, input,
827
828
  if (context) {
828
829
  delete claims[`${context}.${ClaimConsent.attachmentContentType}`];
829
830
  delete claims[`${context}.${ClaimConsent.attachmentData}`];
831
+ delete claims[`${context}.${ClaimConsent.identifier}`];
830
832
  }
833
+ delete claims[ClaimConsent.identifier];
831
834
  const assigned = assignCidToClaimsId(claims);
832
835
  return { ...built, consentClaims: assigned.claims, claimsCid: assigned.cid };
833
836
  };
@@ -838,7 +841,7 @@ export async function setDigitalTwinSecondaryUseConsentWithDeps(routeCtx, input,
838
841
  purpose: HealthcareConsentPurposes.Research,
839
842
  actions: [ServiceCapability.DigitalTwinReader],
840
843
  decision: input.decision,
841
- consentIdentifier: input.consentIdentifier,
844
+ sourceReference: researchUseReference,
842
845
  consentDate: input.consentDate,
843
846
  dataType: input.dataType,
844
847
  pollOptions: input.pollOptions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-sdk-node-ts",
3
- "version": "2.4.32",
3
+ "version": "2.4.34",
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.",