gdc-sdk-node-ts 2.9.18 → 2.9.20

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
@@ -663,6 +663,48 @@ export async function GET(request: Request) {
663
663
  The application validates which systems are allowed for the specific coded
664
664
  form field. This MVP service performs no external terminology request.
665
665
 
666
+ ### Governed ValueSet options through the terminology service
667
+
668
+ For creation controls, the authenticated BFF can resolve the ValueSet bound to
669
+ a canonical flat claim without exposing its service token or terminology
670
+ server to the browser:
671
+
672
+ ```ts
673
+ import {
674
+ AllergyIntoleranceClaim,
675
+ MemoryTerminologyValueSetCache,
676
+ TerminologyServiceClient,
677
+ } from 'gdc-sdk-node-ts';
678
+
679
+ const terminology = new TerminologyServiceClient({
680
+ baseUrl: process.env.TERMINOLOGY_SERVICE_URL!,
681
+ serviceToken: process.env.TERMINOLOGY_SERVICE_TOKEN!,
682
+ cache: new MemoryTerminologyValueSetCache(),
683
+ });
684
+
685
+ const { document, cacheStatus } = await terminology.expandValueSetForClaim({
686
+ sector: 'health-care',
687
+ resourceType: 'AllergyIntolerance',
688
+ claim: AllergyIntoleranceClaim.Manifestation,
689
+ // Optional: language, jurisdiction, version, fhirVersion, offset and count.
690
+ });
691
+ ```
692
+
693
+ Omission means English, international/default terminology, the latest
694
+ installed terminology version and FHIR R4. Jurisdiction is a SNOMED edition
695
+ selector; it does not become part of LOINC or WHO ATC identity. The cache key
696
+ still separates language, SNOMED edition/version, claim and page.
697
+
698
+ `MemoryTerminologyValueSetCache.prime(...)` accepts immutable startup
699
+ snapshots. This permits an application to ship or load the English IPS page
700
+ from private storage and render it without a live terminology call. A local
701
+ language page is preferred when cached; if refresh fails, an English cached
702
+ page is returned with `cacheStatus: 'english-fallback-cache'`. Complete
703
+ catalogs and the service bearer credential remain server-side. A BFF may
704
+ replace the memory implementation with a GCS, Firestore or PostgreSQL adapter
705
+ implementing `TerminologyValueSetCache`; large complete catalogs should not be
706
+ stored in one Firestore document.
707
+
666
708
  ## API Index
667
709
 
668
710
  ## Full Public Surface
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from 'gdc-sdk-core-ts';
2
+ export { AllergyIntoleranceClaim } from 'gdc-common-utils-ts/models/interoperable-claims/allergy-intolerance-claims';
2
3
  export * from './runtime-contracts.js';
3
4
  export * from './identity-bootstrap.js';
4
5
  export * from './async-polling.js';
@@ -51,4 +52,5 @@ export * from './orchestration/professional-sdk.js';
51
52
  export * from './orchestration/digital-twin-sdk.js';
52
53
  export * from './legacy-compat.js';
53
54
  export * from './local-terminology-bff.js';
55
+ export * from './terminology-service-client.js';
54
56
  export * from './pdqm-patient-match.js';
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
2
  export * from 'gdc-sdk-core-ts';
3
+ // Explicitly use the direct common-utils release for newly promoted canonical claims.
4
+ export { AllergyIntoleranceClaim } from 'gdc-common-utils-ts/models/interoperable-claims/allergy-intolerance-claims';
3
5
  export * from './runtime-contracts.js';
4
6
  export * from './identity-bootstrap.js';
5
7
  export * from './async-polling.js';
@@ -52,4 +54,5 @@ export * from './orchestration/professional-sdk.js';
52
54
  export * from './orchestration/digital-twin-sdk.js';
53
55
  export * from './legacy-compat.js';
54
56
  export * from './local-terminology-bff.js';
57
+ export * from './terminology-service-client.js';
55
58
  export * from './pdqm-patient-match.js';
@@ -9,13 +9,20 @@ import type { NodeCapability } from '../session.js';
9
9
  import type { IndividualOrganizationLifecycleInput } from 'gdc-sdk-core-ts';
10
10
  import type { BlockchainArtifactRegistrationInput, ClinicalBundleSearchInput, ClinicalSectionUpdateInput, SubjectSectionUpdateInput, ClinicalSummaryReadResult, ClinicalSummaryRequestInput, ClinicalSummaryUpdateInput, CommunicationIngestionInput, CommunicationParticipantRuntimeSearchInput, DigitalTwinGenerationInput, DigitalTwinSecondaryUseConsentInput, DigitalTwinSecondaryUseConsentResult, DigitalTwinSubjectLinkPurgeInput, DigitalTwinSubjectLinkPurgeResult, GrantProfessionalAccessInput, GrantProfessionalAccessResult, IndividualMemberLifecycleInput, IndividualMemberLicenseAddInput, IndividualMemberLicenseInvitationInput, IndividualMemberLicenseTransitionInput, IpsOrFhirImportInput, LicenseListRuntimeSearchInput, LicenseOfferRuntimeSearchInput, LicenseOrderRuntimeSearchInput, RevokeProfessionalAccessInput, RevokeProfessionalAccessResult, ProfessionalAccessRequestDecisionInput, ProfessionalAccessRequestSearchInput, RelatedPersonUpsertInput } from '../resource-operations.js';
11
11
  import type { SmartTokenExchangeResult, SmartTokenRequestInput } from '../smart-token.js';
12
- /** Profile-owned defaults applied to subject-section mutations after unlock. */
12
+ /**
13
+ * Legacy compatibility default applied after unlock.
14
+ *
15
+ * New writes should pass the selected relationship explicitly as the
16
+ * document's `attester`; this default is retained so existing integrations do
17
+ * not break during the transition.
18
+ */
13
19
  export type IndividualControllerProfileDefaults = Readonly<{
14
20
  attester: SubjectSectionUpdateInput['attester'];
15
21
  }>;
16
22
  /**
17
- * High-level section input. An opened controller profile supplies its own
18
- * protected RESPRSN attester; standalone facades must still provide one.
23
+ * High-level section input. New writes pass the document attester explicitly.
24
+ * The optional shape remains compatible with opened facades created by older
25
+ * integrations, which still supply their protected RESPRSN assignment.
19
26
  */
20
27
  export type IndividualControllerSubjectSectionUpdateInput = Omit<SubjectSectionUpdateInput, 'attester'> & Partial<Pick<SubjectSectionUpdateInput, 'attester'>>;
21
28
  /**
@@ -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
+ }
@@ -56,7 +56,11 @@ export type ServerProfileRecord = Readonly<{
56
56
  clientId: string;
57
57
  /** Stable non-secret id of the browser/app installation registered by DCR. */
58
58
  clientInstanceId?: string;
59
- /** Profile identity used as Composition attester; never a per-write author. */
59
+ /**
60
+ * Authorized attester assignment retained for legacy profile/session
61
+ * compatibility. Every new document still carries its own explicit
62
+ * `Composition.attester` value.
63
+ */
60
64
  attester?: ServerProfileAttester;
61
65
  /** Stable FHIR creator/permission identity; channel and device values are aliases only. */
62
66
  clinicalCreatorBinding?: ClinicalCreatorBinding;
@@ -282,6 +286,8 @@ export type ServerAuthorizedSubjectSelectionInput = Readonly<{
282
286
  /** Server-owned relationship projection for one directory subject. */
283
287
  export type ServerAuthorizedSubjectGrant = Readonly<{
284
288
  subjectDid: string;
289
+ /** Canonical private subject used for SMART when the directory exposes a public card alias. */
290
+ transportSubjectDid?: string;
285
291
  actorMode: ServerActorMode;
286
292
  attester?: ServerProfileAttester;
287
293
  }>;
@@ -311,7 +317,7 @@ export type ResolvedServerProfileSession = Readonly<{
311
317
  accessToken: string;
312
318
  /** Relationship authorization selected for this subject, not for the wallet. */
313
319
  actorMode: ServerActorMode;
314
- /** Attester bound to this authenticated and unlocked profile. */
320
+ /** Attester selected for this subject relationship and current session. */
315
321
  attester?: ServerProfileAttester;
316
322
  secureTransportAdapter: SecureDidcommTransportAdapter;
317
323
  /** Storage adapter available only while the PIN-unlocked session is alive. */
@@ -360,6 +366,8 @@ export type OpenedServerIndividualController = Readonly<{
360
366
  sdk: IndividualControllerSdk;
361
367
  /** Returns the protected RelatedPerson URI for FHIR document attestation. */
362
368
  getAttesterUriForDocs(): string;
369
+ /** Returns the selected relationship to place explicitly on this document. */
370
+ getDocumentAttester(): ServerProfileAttester;
363
371
  }>;
364
372
  /** Session-bound individual-member facade using the same personal actor wallet. */
365
373
  export type OpenedServerIndividualMember = Readonly<{
@@ -3,6 +3,7 @@ import { randomBytes } from 'node:crypto';
3
3
  import { ActorKinds } from 'gdc-common-utils-ts/constants/actor-session';
4
4
  import { UrnPrefixes } from 'gdc-common-utils-ts/constants/urn';
5
5
  import { normalizeUuid } from 'gdc-common-utils-ts/utils/normalize-uuid';
6
+ import { parseIndividualMemberDidWeb } from 'gdc-common-utils-ts/utils/did';
6
7
  import { selectRelatedPersonListRecord, } from 'gdc-common-utils-ts/utils/related-person-list';
7
8
  import { CompositionAttesterModes, } from 'gdc-common-utils-ts/models/interoperable-claims/composition-claims';
8
9
  import { FhirIpsCreatorKinds, normalizeClinicalCreatorBinding, } from 'gdc-common-utils-ts/utils/fhir-ips-creator-identity';
@@ -619,8 +620,25 @@ export class ServerProfileSessionManager {
619
620
  const walletState = await this.resolveWalletState(input.ownerId, input.sessionId);
620
621
  const client = this.createRegisteredProfileClient(walletState.profile, walletState.wallet, walletState.context, idToken);
621
622
  const subjects = await client.listAuthorizedIndividualSubjects(walletState.profile.routeContext, { verifiedContact: input.verifiedContact });
622
- const authorizedSubjectDids = unique(subjects.map((subject) => subject.subjectDid));
623
- const authorizedSubjects = subjects.map(toServerAuthorizedSubjectGrant);
623
+ const directoryGrants = subjects.map(toServerAuthorizedSubjectGrant);
624
+ const canonicalControllerSubject = canonicalIndividualControllerSubject(actorSession.profile);
625
+ const enrolledSubjects = new Set(actorSession.profile.allowedSubjectDids);
626
+ const ownPublicGrant = canonicalControllerSubject && enrolledSubjects.has(canonicalControllerSubject)
627
+ ? directoryGrants.find((grant) => grant.subjectDid !== canonicalControllerSubject
628
+ && enrolledSubjects.has(grant.subjectDid)
629
+ && (grant.actorMode === 'self' || grant.actorMode === 'controller'))
630
+ : undefined;
631
+ const authorizedSubjects = ownPublicGrant && canonicalControllerSubject
632
+ ? [
633
+ ...directoryGrants.map((grant) => grant === ownPublicGrant
634
+ ? { ...grant, transportSubjectDid: canonicalControllerSubject }
635
+ : grant),
636
+ ...(directoryGrants.some((grant) => grant.subjectDid === canonicalControllerSubject)
637
+ ? []
638
+ : [{ ...ownPublicGrant, subjectDid: canonicalControllerSubject }]),
639
+ ]
640
+ : directoryGrants;
641
+ const authorizedSubjectDids = unique(authorizedSubjects.map((subject) => subject.subjectDid));
624
642
  const updatedProfile = {
625
643
  ...actorSession.profile,
626
644
  allowedSubjectDids: authorizedSubjectDids,
@@ -651,18 +669,19 @@ export class ServerProfileSessionManager {
651
669
  const stored = await this.options.store.getSession(input.sessionId);
652
670
  if (!stored || stored.ownerId !== input.ownerId)
653
671
  throw new Error('Profile session not found.');
654
- const subjectDid = String(input.subjectDid || '').trim();
655
- if (!subjectDid || !(stored.authorizedSubjectDids || []).includes(subjectDid)) {
672
+ const requestedSubjectDid = String(input.subjectDid || '').trim();
673
+ if (!requestedSubjectDid || !(stored.authorizedSubjectDids || []).includes(requestedSubjectDid)) {
656
674
  throw new Error('Subject is not authorized for this personal actor profile.');
657
675
  }
658
- const selectedGrant = stored.authorizedSubjects?.find((grant) => grant.subjectDid === subjectDid);
676
+ const selectedGrant = stored.authorizedSubjects?.find((grant) => grant.subjectDid === requestedSubjectDid);
659
677
  if (!selectedGrant) {
660
678
  throw new Error('Subject relationship metadata is missing from this personal actor session.');
661
679
  }
662
680
  const idToken = String(input.idToken || '').trim();
663
681
  if (!idToken)
664
682
  throw new Error('Subject selection requires idToken.');
665
- const scopes = unique(input.scopes);
683
+ const subjectDid = String(selectedGrant.transportSubjectDid || requestedSubjectDid).trim();
684
+ const scopes = unique(input.scopes).map((scope) => rewriteSmartScopeSubject(scope, requestedSubjectDid, subjectDid));
666
685
  if (!scopes.length)
667
686
  throw new Error('Subject selection requires scopes.');
668
687
  const walletState = await this.resolveWalletState(input.ownerId, input.sessionId);
@@ -847,6 +866,7 @@ export class ServerProfileSessionManager {
847
866
  }
848
867
  return reference;
849
868
  },
869
+ getDocumentAttester: () => profileAttester,
850
870
  };
851
871
  }
852
872
  /** Opens an accepted caregiver/member relationship after exact subject selection. */
@@ -1150,6 +1170,30 @@ export class ServerProfileSessionManager {
1150
1170
  }
1151
1171
  now() { return this.options.now?.() || new Date(); }
1152
1172
  }
1173
+ /** Resolves the private individual DID rooted in one registered controller actor. */
1174
+ function canonicalIndividualControllerSubject(profile) {
1175
+ if (profile.actorKind !== ActorKinds.IndividualController)
1176
+ return undefined;
1177
+ try {
1178
+ return parseIndividualMemberDidWeb(profile.actorDid).individualDidWeb;
1179
+ }
1180
+ catch {
1181
+ return undefined;
1182
+ }
1183
+ }
1184
+ /** Rebinds a selected public card alias to its canonical SMART subject. */
1185
+ function rewriteSmartScopeSubject(scope, requestedSubjectDid, transportSubjectDid) {
1186
+ const value = String(scope || '').trim();
1187
+ if (!value || requestedSubjectDid === transportSubjectDid)
1188
+ return value;
1189
+ const [permission, query = ''] = value.split('?', 2);
1190
+ const parameters = new URLSearchParams(query);
1191
+ if (parameters.get('subject') !== requestedSubjectDid) {
1192
+ throw new Error('Every SMART scope must be bound to the selected public subject alias.');
1193
+ }
1194
+ parameters.set('subject', transportSubjectDid);
1195
+ return `${permission}?${parameters.toString()}`;
1196
+ }
1153
1197
  /**
1154
1198
  * Binds every direct resource action and async poll to the actor and DCR client
1155
1199
  * stored in the server profile. The caller may author the business body and
@@ -0,0 +1,83 @@
1
+ /** Input resolved by an authenticated application BFF, never directly by a browser. */
2
+ export type TerminologyValueSetInput = Readonly<{
3
+ sector: string;
4
+ resourceType: string;
5
+ claim: string;
6
+ fhirVersion?: 'R4';
7
+ language?: string;
8
+ jurisdiction?: string;
9
+ /** Exact code-system version URI. Omission asks the service for its latest/default release. */
10
+ version?: string;
11
+ offset?: number;
12
+ count?: number;
13
+ }>;
14
+ export type TerminologyValueSetOption = Readonly<{
15
+ system: string;
16
+ code: string;
17
+ display: string;
18
+ localizedDisplay: string;
19
+ resolvedLanguage: string;
20
+ fallbackUsed: boolean;
21
+ }>;
22
+ export type TerminologyValueSetDocument = Readonly<{
23
+ jsonapi: Readonly<{
24
+ version: '1.1';
25
+ }>;
26
+ data: readonly Readonly<{
27
+ type: 'terminology-option';
28
+ id: string;
29
+ attributes: TerminologyValueSetOption;
30
+ }>[];
31
+ meta: Readonly<{
32
+ count: number;
33
+ total: number;
34
+ offset: number;
35
+ language: string;
36
+ resourceType: string;
37
+ field: string;
38
+ claim: string;
39
+ valueSet: Readonly<{
40
+ id: string;
41
+ url: string;
42
+ version: string;
43
+ system: string;
44
+ }>;
45
+ terminologyVersions?: Readonly<Record<string, string>>;
46
+ }>;
47
+ }>;
48
+ export type TerminologyCacheEntry = Readonly<{
49
+ document: TerminologyValueSetDocument;
50
+ storedAt: number;
51
+ }>;
52
+ export interface TerminologyValueSetCache {
53
+ get(key: string): TerminologyCacheEntry | undefined | Promise<TerminologyCacheEntry | undefined>;
54
+ set(key: string, entry: TerminologyCacheEntry): void | Promise<void>;
55
+ }
56
+ /** Stable key usable by in-memory, GCS, Firestore or PostgreSQL cache adapters. */
57
+ export declare function terminologyValueSetCacheKey(input: TerminologyValueSetInput): string;
58
+ /** Process-local cache suitable for warm BFF instances and immutable startup snapshots. */
59
+ export declare class MemoryTerminologyValueSetCache implements TerminologyValueSetCache {
60
+ #private;
61
+ get(key: string): TerminologyCacheEntry | undefined;
62
+ set(key: string, entry: TerminologyCacheEntry): void;
63
+ prime(input: TerminologyValueSetInput, document: TerminologyValueSetDocument, storedAt?: number): void;
64
+ }
65
+ export type TerminologyValueSetResult = Readonly<{
66
+ document: TerminologyValueSetDocument;
67
+ cacheStatus: 'remote' | 'fresh-cache' | 'stale-cache' | 'english-fallback-cache';
68
+ }>;
69
+ /** Server-only client for a product-neutral terminology service. */
70
+ export declare class TerminologyServiceClient {
71
+ #private;
72
+ constructor(options: Readonly<{
73
+ baseUrl: string;
74
+ serviceToken?: string;
75
+ fetchImplementation?: typeof fetch;
76
+ cache?: TerminologyValueSetCache;
77
+ cacheTtlMs?: number;
78
+ staleIfError?: boolean;
79
+ now?: () => number;
80
+ }>);
81
+ /** Resolves the ValueSet bound by the service to one canonical flat claim. */
82
+ expandValueSetForClaim(input: TerminologyValueSetInput): Promise<TerminologyValueSetResult>;
83
+ }
@@ -0,0 +1,155 @@
1
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
2
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
4
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
+ };
6
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
7
+ if (kind === "m") throw new TypeError("Private method is not writable");
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
10
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
11
+ };
12
+ var _MemoryTerminologyValueSetCache_entries, _TerminologyServiceClient_instances, _TerminologyServiceClient_baseUrl, _TerminologyServiceClient_serviceToken, _TerminologyServiceClient_fetch, _TerminologyServiceClient_cache, _TerminologyServiceClient_cacheTtlMs, _TerminologyServiceClient_staleIfError, _TerminologyServiceClient_now, _TerminologyServiceClient_request, _TerminologyServiceClient_validateInput, _TerminologyServiceClient_isDocument;
13
+ const DEFAULT_TTL_MS = 5 * 60 * 1000;
14
+ function normalizedInput(input) {
15
+ return {
16
+ sector: input.sector.trim().toLowerCase(),
17
+ resourceType: input.resourceType.trim(),
18
+ claim: input.claim.trim(),
19
+ fhirVersion: input.fhirVersion ?? 'R4',
20
+ language: (input.language?.trim() || 'en').replace(/_/g, '-').toLowerCase(),
21
+ ...(input.jurisdiction?.trim() ? { jurisdiction: input.jurisdiction.trim().toUpperCase() } : {}),
22
+ ...(input.version?.trim() ? { version: input.version.trim() } : {}),
23
+ offset: input.offset ?? 0,
24
+ count: input.count ?? 50,
25
+ };
26
+ }
27
+ /** Stable key usable by in-memory, GCS, Firestore or PostgreSQL cache adapters. */
28
+ export function terminologyValueSetCacheKey(input) {
29
+ const value = normalizedInput(input);
30
+ return JSON.stringify([
31
+ value.fhirVersion, value.sector, value.resourceType, value.claim,
32
+ value.language, value.jurisdiction ?? '', value.version ?? 'latest',
33
+ value.offset, value.count,
34
+ ]);
35
+ }
36
+ /** Process-local cache suitable for warm BFF instances and immutable startup snapshots. */
37
+ export class MemoryTerminologyValueSetCache {
38
+ constructor() {
39
+ _MemoryTerminologyValueSetCache_entries.set(this, new Map());
40
+ }
41
+ get(key) {
42
+ return __classPrivateFieldGet(this, _MemoryTerminologyValueSetCache_entries, "f").get(key);
43
+ }
44
+ set(key, entry) {
45
+ __classPrivateFieldGet(this, _MemoryTerminologyValueSetCache_entries, "f").set(key, entry);
46
+ }
47
+ prime(input, document, storedAt = Date.now()) {
48
+ this.set(terminologyValueSetCacheKey(input), { document, storedAt });
49
+ }
50
+ }
51
+ _MemoryTerminologyValueSetCache_entries = new WeakMap();
52
+ /** Server-only client for a product-neutral terminology service. */
53
+ export class TerminologyServiceClient {
54
+ constructor(options) {
55
+ _TerminologyServiceClient_instances.add(this);
56
+ _TerminologyServiceClient_baseUrl.set(this, void 0);
57
+ _TerminologyServiceClient_serviceToken.set(this, void 0);
58
+ _TerminologyServiceClient_fetch.set(this, void 0);
59
+ _TerminologyServiceClient_cache.set(this, void 0);
60
+ _TerminologyServiceClient_cacheTtlMs.set(this, void 0);
61
+ _TerminologyServiceClient_staleIfError.set(this, void 0);
62
+ _TerminologyServiceClient_now.set(this, void 0);
63
+ __classPrivateFieldSet(this, _TerminologyServiceClient_baseUrl, options.baseUrl.replace(/\/$/, ''), "f");
64
+ __classPrivateFieldSet(this, _TerminologyServiceClient_serviceToken, options.serviceToken?.trim() || undefined, "f");
65
+ __classPrivateFieldSet(this, _TerminologyServiceClient_fetch, options.fetchImplementation ?? fetch, "f");
66
+ __classPrivateFieldSet(this, _TerminologyServiceClient_cache, options.cache ?? new MemoryTerminologyValueSetCache(), "f");
67
+ __classPrivateFieldSet(this, _TerminologyServiceClient_cacheTtlMs, options.cacheTtlMs ?? DEFAULT_TTL_MS, "f");
68
+ __classPrivateFieldSet(this, _TerminologyServiceClient_staleIfError, options.staleIfError ?? true, "f");
69
+ __classPrivateFieldSet(this, _TerminologyServiceClient_now, options.now ?? Date.now, "f");
70
+ }
71
+ /** Resolves the ValueSet bound by the service to one canonical flat claim. */
72
+ async expandValueSetForClaim(input) {
73
+ __classPrivateFieldGet(this, _TerminologyServiceClient_instances, "m", _TerminologyServiceClient_validateInput).call(this, input);
74
+ const key = terminologyValueSetCacheKey(input);
75
+ const cached = await __classPrivateFieldGet(this, _TerminologyServiceClient_cache, "f").get(key);
76
+ if (cached && __classPrivateFieldGet(this, _TerminologyServiceClient_now, "f").call(this) - cached.storedAt <= __classPrivateFieldGet(this, _TerminologyServiceClient_cacheTtlMs, "f")) {
77
+ return { document: cached.document, cacheStatus: 'fresh-cache' };
78
+ }
79
+ try {
80
+ const document = await __classPrivateFieldGet(this, _TerminologyServiceClient_instances, "m", _TerminologyServiceClient_request).call(this, input);
81
+ await __classPrivateFieldGet(this, _TerminologyServiceClient_cache, "f").set(key, { document, storedAt: __classPrivateFieldGet(this, _TerminologyServiceClient_now, "f").call(this) });
82
+ return { document, cacheStatus: 'remote' };
83
+ }
84
+ catch (error) {
85
+ if (cached && __classPrivateFieldGet(this, _TerminologyServiceClient_staleIfError, "f")) {
86
+ return { document: cached.document, cacheStatus: 'stale-cache' };
87
+ }
88
+ const requestedLanguage = input.language?.trim().toLowerCase();
89
+ if (__classPrivateFieldGet(this, _TerminologyServiceClient_staleIfError, "f") && requestedLanguage && requestedLanguage !== 'en') {
90
+ const english = await __classPrivateFieldGet(this, _TerminologyServiceClient_cache, "f").get(terminologyValueSetCacheKey({ ...input, language: 'en' }));
91
+ if (english) {
92
+ return { document: english.document, cacheStatus: 'english-fallback-cache' };
93
+ }
94
+ }
95
+ throw new Error('Terminology service request failed');
96
+ }
97
+ }
98
+ }
99
+ _TerminologyServiceClient_baseUrl = new WeakMap(), _TerminologyServiceClient_serviceToken = new WeakMap(), _TerminologyServiceClient_fetch = new WeakMap(), _TerminologyServiceClient_cache = new WeakMap(), _TerminologyServiceClient_cacheTtlMs = new WeakMap(), _TerminologyServiceClient_staleIfError = new WeakMap(), _TerminologyServiceClient_now = new WeakMap(), _TerminologyServiceClient_instances = new WeakSet(), _TerminologyServiceClient_request = async function _TerminologyServiceClient_request(input) {
100
+ const url = new URL(`${__classPrivateFieldGet(this, _TerminologyServiceClient_baseUrl, "f")}/v1/terminology/value-set-options`);
101
+ url.searchParams.set('sector', input.sector.trim());
102
+ url.searchParams.set('resourceType', input.resourceType.trim());
103
+ url.searchParams.set('claim', input.claim.trim());
104
+ url.searchParams.set('offset', String(input.offset ?? 0));
105
+ url.searchParams.set('count', String(input.count ?? 50));
106
+ if (input.fhirVersion)
107
+ url.searchParams.set('fhirVersion', input.fhirVersion);
108
+ if (input.language?.trim())
109
+ url.searchParams.set('language', input.language.trim());
110
+ if (input.jurisdiction?.trim())
111
+ url.searchParams.set('jurisdiction', input.jurisdiction.trim());
112
+ if (input.version?.trim())
113
+ url.searchParams.set('version', input.version.trim());
114
+ const response = await __classPrivateFieldGet(this, _TerminologyServiceClient_fetch, "f").call(this, url, {
115
+ headers: {
116
+ accept: 'application/vnd.api+json',
117
+ ...(__classPrivateFieldGet(this, _TerminologyServiceClient_serviceToken, "f") ? { authorization: `Bearer ${__classPrivateFieldGet(this, _TerminologyServiceClient_serviceToken, "f")}` } : {}),
118
+ },
119
+ });
120
+ if (!response.ok)
121
+ throw new Error(`Terminology service returned HTTP ${response.status}`);
122
+ const document = await response.json();
123
+ if (!__classPrivateFieldGet(this, _TerminologyServiceClient_instances, "m", _TerminologyServiceClient_isDocument).call(this, document, input))
124
+ throw new Error('Terminology service returned an invalid document');
125
+ return document;
126
+ }, _TerminologyServiceClient_validateInput = function _TerminologyServiceClient_validateInput(input) {
127
+ if (!input.sector.trim())
128
+ throw new Error('sector is required');
129
+ if (!input.resourceType.trim())
130
+ throw new Error('resourceType is required');
131
+ if (!input.claim.trim())
132
+ throw new Error('claim is required');
133
+ if (input.offset !== undefined && (!Number.isInteger(input.offset) || input.offset < 0)) {
134
+ throw new Error('offset must be a non-negative integer');
135
+ }
136
+ if (input.count !== undefined && (!Number.isInteger(input.count) || input.count < 1 || input.count > 100)) {
137
+ throw new Error('count must be an integer from 1 to 100');
138
+ }
139
+ }, _TerminologyServiceClient_isDocument = function _TerminologyServiceClient_isDocument(value, input) {
140
+ if (!value || typeof value !== 'object')
141
+ return false;
142
+ const document = value;
143
+ if (document.jsonapi?.version !== '1.1' || !Array.isArray(document.data) || !document.meta)
144
+ return false;
145
+ if (document.meta.resourceType !== input.resourceType.trim() || document.meta.claim !== input.claim.trim())
146
+ return false;
147
+ return document.data.every((resource) => (resource?.type === 'terminology-option'
148
+ && typeof resource.id === 'string'
149
+ && typeof resource.attributes?.system === 'string'
150
+ && typeof resource.attributes?.code === 'string'
151
+ && typeof resource.attributes?.display === 'string'
152
+ && typeof resource.attributes?.localizedDisplay === 'string'
153
+ && typeof resource.attributes?.resolvedLanguage === 'string'
154
+ && typeof resource.attributes?.fallbackUsed === 'boolean'));
155
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-sdk-node-ts",
3
- "version": "2.9.18",
3
+ "version": "2.9.20",
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,7 +38,7 @@
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.9.16",
41
+ "gdc-common-utils-ts": "2.9.19",
42
42
  "gdc-sdk-core-ts": "2.9.10"
43
43
  },
44
44
  "devDependencies": {