gdc-sdk-node-ts 2.9.20 → 2.9.22

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.
@@ -461,16 +461,14 @@ export class ServerProfileSessionManager {
461
461
  const walletKeyDerivationId = normalizedWalletKeyDerivationId(profile.walletKeyDerivationId, profile.profileId);
462
462
  const wallet = await this.createWallet(walletKeyDerivationId, seed);
463
463
  await requireRegisteredProfileKeys(wallet, walletContext(walletKeyDerivationId), profile);
464
- const updated = {
465
- ...profile,
464
+ const updated = withProfileUnlockState(profile, {
466
465
  protectedWalletSeed: await protectServerProfileSecret(seed, input.newPin, `${profile.profileId}:wallet-seed`, this.options.sealer, this.options.profileProtection),
467
466
  ...(profile.protectedVpToken ? {
468
467
  protectedVpToken: await protectServerProfileSecret(String(input.authorizedVpToken), input.newPin, `${profile.profileId}:vp-token`, this.options.sealer, this.options.profileProtection),
469
468
  } : {}),
470
469
  failedUnlocks: 0,
471
- lockedUntil: undefined,
472
470
  updatedAt: this.now().toISOString(),
473
- };
471
+ });
474
472
  await this.options.store.putProfile(updated);
475
473
  await this.options.store.deleteSessionsForProfile(profile.profileId);
476
474
  return updated;
@@ -498,7 +496,11 @@ export class ServerProfileSessionManager {
498
496
  const lockedUntil = failures >= max
499
497
  ? new Date(now.getTime() + (this.options.lockSeconds ?? 300) * 1000).toISOString()
500
498
  : undefined;
501
- await this.options.store.putProfile({ ...profile, failedUnlocks: failures, lockedUntil, updatedAt: now.toISOString() });
499
+ await this.options.store.putProfile(withProfileUnlockState(profile, {
500
+ failedUnlocks: failures,
501
+ lockedUntil,
502
+ updatedAt: now.toISOString(),
503
+ }));
502
504
  throw new Error('Profile PIN rejected.');
503
505
  }
504
506
  profile = await this.ensureRequiredStorageProfile(profile, seed);
@@ -531,7 +533,10 @@ export class ServerProfileSessionManager {
531
533
  throw new Error('SMART token exchange failed.');
532
534
  const sessionId = randomBytes(32).toString('base64url');
533
535
  const expiresAt = new Date(now.getTime() + (this.options.sessionTtlSeconds ?? 300) * 1000);
534
- await this.options.store.putProfile({ ...profile, failedUnlocks: 0, lockedUntil: undefined, updatedAt: now.toISOString() });
536
+ await this.options.store.putProfile(withProfileUnlockState(profile, {
537
+ failedUnlocks: 0,
538
+ updatedAt: now.toISOString(),
539
+ }));
535
540
  await this.options.store.putSession({
536
541
  sessionId,
537
542
  ownerId: input.ownerId,
@@ -577,22 +582,19 @@ export class ServerProfileSessionManager {
577
582
  const lockedUntil = failures >= max
578
583
  ? new Date(now.getTime() + (this.options.lockSeconds ?? 300) * 1000).toISOString()
579
584
  : undefined;
580
- await this.options.store.putProfile({
581
- ...profile,
585
+ await this.options.store.putProfile(withProfileUnlockState(profile, {
582
586
  failedUnlocks: failures,
583
587
  lockedUntil,
584
588
  updatedAt: now.toISOString(),
585
- });
589
+ }));
586
590
  throw new Error('Profile PIN rejected.');
587
591
  }
588
592
  profile = await this.ensureRequiredStorageProfile(profile, seed);
589
593
  const sessionId = randomBytes(32).toString('base64url');
590
- await this.options.store.putProfile({
591
- ...profile,
594
+ await this.options.store.putProfile(withProfileUnlockState(profile, {
592
595
  failedUnlocks: 0,
593
- lockedUntil: undefined,
594
596
  updatedAt: now.toISOString(),
595
- });
597
+ }));
596
598
  await this.options.store.putSession({
597
599
  sessionId,
598
600
  ownerId: input.ownerId,
@@ -1354,6 +1356,20 @@ function sameGovernedRole(left, right) {
1354
1356
  && leftRole.codingSystem === rightRole.codingSystem
1355
1357
  && leftRole.code === rightRole.code);
1356
1358
  }
1359
+ /**
1360
+ * Resets or advances the persisted PIN lock state without writing an
1361
+ * `undefined` optional property. Firestore rejects `undefined`, so durable
1362
+ * adapters must receive an omitted `lockedUntil` field when no lock exists.
1363
+ */
1364
+ function withProfileUnlockState(profile, updates) {
1365
+ const { lockedUntil: _previousLock, ...unlockedProfile } = profile;
1366
+ const { lockedUntil, ...definedUpdates } = updates;
1367
+ return {
1368
+ ...unlockedProfile,
1369
+ ...definedUpdates,
1370
+ ...(lockedUntil ? { lockedUntil } : {}),
1371
+ };
1372
+ }
1357
1373
  function unique(values) {
1358
1374
  return [...new Set(values.map((value) => String(value).trim()).filter(Boolean))].sort();
1359
1375
  }
@@ -1,7 +1,7 @@
1
1
  /** Input resolved by an authenticated application BFF, never directly by a browser. */
2
2
  export type TerminologyValueSetInput = Readonly<{
3
- sector: string;
4
- resourceType: string;
3
+ /** Optional per-request override. Dedicated BFFs normally configure this once on the client. */
4
+ sector?: string;
5
5
  claim: string;
6
6
  fhirVersion?: 'R4';
7
7
  language?: string;
@@ -11,6 +11,7 @@ export type TerminologyValueSetInput = Readonly<{
11
11
  offset?: number;
12
12
  count?: number;
13
13
  }>;
14
+ export type TerminologyValueSetCodeListInput = Omit<TerminologyValueSetInput, 'offset' | 'count'>;
14
15
  export type TerminologyValueSetOption = Readonly<{
15
16
  system: string;
16
17
  code: string;
@@ -66,11 +67,23 @@ export type TerminologyValueSetResult = Readonly<{
66
67
  document: TerminologyValueSetDocument;
67
68
  cacheStatus: 'remote' | 'fresh-cache' | 'stale-cache' | 'english-fallback-cache';
68
69
  }>;
70
+ /** Frontend-ready code list. Coding system and code remain separate FHIR Coding values. */
71
+ export type TerminologyValueSetCodeList = Readonly<{
72
+ codingSystem: string;
73
+ codeToDisplay: Readonly<Record<string, string>>;
74
+ requestedLanguage: string;
75
+ resolvedLanguage: string;
76
+ fallbackUsed: boolean;
77
+ valueSet: TerminologyValueSetDocument['meta']['valueSet'];
78
+ terminologyVersions?: Readonly<Record<string, string>>;
79
+ }>;
69
80
  /** Server-only client for a product-neutral terminology service. */
70
81
  export declare class TerminologyServiceClient {
71
82
  #private;
72
83
  constructor(options: Readonly<{
73
84
  baseUrl: string;
85
+ /** Default sector for a dedicated human-health or animal-health BFF. */
86
+ sector?: string;
74
87
  serviceToken?: string;
75
88
  fetchImplementation?: typeof fetch;
76
89
  cache?: TerminologyValueSetCache;
@@ -80,4 +93,6 @@ export declare class TerminologyServiceClient {
80
93
  }>);
81
94
  /** Resolves the ValueSet bound by the service to one canonical flat claim. */
82
95
  expandValueSetForClaim(input: TerminologyValueSetInput): Promise<TerminologyValueSetResult>;
96
+ /** Resolves every page and returns the simple code-to-display object expected by forms. */
97
+ getValueSetCodeListForClaim(input: TerminologyValueSetCodeListInput): Promise<TerminologyValueSetCodeList>;
83
98
  }
@@ -9,12 +9,10 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
9
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
10
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
11
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;
12
+ var _MemoryTerminologyValueSetCache_entries, _TerminologyServiceClient_instances, _TerminologyServiceClient_baseUrl, _TerminologyServiceClient_serviceToken, _TerminologyServiceClient_sector, _TerminologyServiceClient_fetch, _TerminologyServiceClient_cache, _TerminologyServiceClient_cacheTtlMs, _TerminologyServiceClient_staleIfError, _TerminologyServiceClient_now, _TerminologyServiceClient_request, _TerminologyServiceClient_validateInput, _TerminologyServiceClient_isDocument;
13
13
  const DEFAULT_TTL_MS = 5 * 60 * 1000;
14
14
  function normalizedInput(input) {
15
15
  return {
16
- sector: input.sector.trim().toLowerCase(),
17
- resourceType: input.resourceType.trim(),
18
16
  claim: input.claim.trim(),
19
17
  fhirVersion: input.fhirVersion ?? 'R4',
20
18
  language: (input.language?.trim() || 'en').replace(/_/g, '-').toLowerCase(),
@@ -26,13 +24,27 @@ function normalizedInput(input) {
26
24
  }
27
25
  /** Stable key usable by in-memory, GCS, Firestore or PostgreSQL cache adapters. */
28
26
  export function terminologyValueSetCacheKey(input) {
29
- const value = normalizedInput(input);
27
+ const normalizedTerminologyContext = normalizedInput(input);
30
28
  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,
29
+ normalizedTerminologyContext.fhirVersion,
30
+ input.sector?.trim().toLowerCase() ?? '',
31
+ normalizedTerminologyContext.claim,
32
+ normalizedTerminologyContext.language,
33
+ normalizedTerminologyContext.jurisdiction ?? '',
34
+ normalizedTerminologyContext.version ?? 'latest',
35
+ normalizedTerminologyContext.offset,
36
+ normalizedTerminologyContext.count,
34
37
  ]);
35
38
  }
39
+ function resourceTypeFromClaim(claim) {
40
+ const canonicalClaim = claim.trim();
41
+ const separatorIndex = canonicalClaim.indexOf('.');
42
+ const resourceType = canonicalClaim.slice(0, separatorIndex);
43
+ if (separatorIndex < 1 || !/^[A-Z][A-Za-z0-9]*$/.test(resourceType)) {
44
+ throw new Error('claim must start with its FHIR resource type');
45
+ }
46
+ return resourceType;
47
+ }
36
48
  /** Process-local cache suitable for warm BFF instances and immutable startup snapshots. */
37
49
  export class MemoryTerminologyValueSetCache {
38
50
  constructor() {
@@ -55,6 +67,7 @@ export class TerminologyServiceClient {
55
67
  _TerminologyServiceClient_instances.add(this);
56
68
  _TerminologyServiceClient_baseUrl.set(this, void 0);
57
69
  _TerminologyServiceClient_serviceToken.set(this, void 0);
70
+ _TerminologyServiceClient_sector.set(this, void 0);
58
71
  _TerminologyServiceClient_fetch.set(this, void 0);
59
72
  _TerminologyServiceClient_cache.set(this, void 0);
60
73
  _TerminologyServiceClient_cacheTtlMs.set(this, void 0);
@@ -62,6 +75,7 @@ export class TerminologyServiceClient {
62
75
  _TerminologyServiceClient_now.set(this, void 0);
63
76
  __classPrivateFieldSet(this, _TerminologyServiceClient_baseUrl, options.baseUrl.replace(/\/$/, ''), "f");
64
77
  __classPrivateFieldSet(this, _TerminologyServiceClient_serviceToken, options.serviceToken?.trim() || undefined, "f");
78
+ __classPrivateFieldSet(this, _TerminologyServiceClient_sector, options.sector?.trim() || undefined, "f");
65
79
  __classPrivateFieldSet(this, _TerminologyServiceClient_fetch, options.fetchImplementation ?? fetch, "f");
66
80
  __classPrivateFieldSet(this, _TerminologyServiceClient_cache, options.cache ?? new MemoryTerminologyValueSetCache(), "f");
67
81
  __classPrivateFieldSet(this, _TerminologyServiceClient_cacheTtlMs, options.cacheTtlMs ?? DEFAULT_TTL_MS, "f");
@@ -71,13 +85,17 @@ export class TerminologyServiceClient {
71
85
  /** Resolves the ValueSet bound by the service to one canonical flat claim. */
72
86
  async expandValueSetForClaim(input) {
73
87
  __classPrivateFieldGet(this, _TerminologyServiceClient_instances, "m", _TerminologyServiceClient_validateInput).call(this, input);
74
- const key = terminologyValueSetCacheKey(input);
88
+ const resolvedTerminologyContext = {
89
+ ...input,
90
+ sector: input.sector?.trim() || __classPrivateFieldGet(this, _TerminologyServiceClient_sector, "f"),
91
+ };
92
+ const key = terminologyValueSetCacheKey(resolvedTerminologyContext);
75
93
  const cached = await __classPrivateFieldGet(this, _TerminologyServiceClient_cache, "f").get(key);
76
94
  if (cached && __classPrivateFieldGet(this, _TerminologyServiceClient_now, "f").call(this) - cached.storedAt <= __classPrivateFieldGet(this, _TerminologyServiceClient_cacheTtlMs, "f")) {
77
95
  return { document: cached.document, cacheStatus: 'fresh-cache' };
78
96
  }
79
97
  try {
80
- const document = await __classPrivateFieldGet(this, _TerminologyServiceClient_instances, "m", _TerminologyServiceClient_request).call(this, input);
98
+ const document = await __classPrivateFieldGet(this, _TerminologyServiceClient_instances, "m", _TerminologyServiceClient_request).call(this, resolvedTerminologyContext);
81
99
  await __classPrivateFieldGet(this, _TerminologyServiceClient_cache, "f").set(key, { document, storedAt: __classPrivateFieldGet(this, _TerminologyServiceClient_now, "f").call(this) });
82
100
  return { document, cacheStatus: 'remote' };
83
101
  }
@@ -87,7 +105,10 @@ export class TerminologyServiceClient {
87
105
  }
88
106
  const requestedLanguage = input.language?.trim().toLowerCase();
89
107
  if (__classPrivateFieldGet(this, _TerminologyServiceClient_staleIfError, "f") && requestedLanguage && requestedLanguage !== 'en') {
90
- const english = await __classPrivateFieldGet(this, _TerminologyServiceClient_cache, "f").get(terminologyValueSetCacheKey({ ...input, language: 'en' }));
108
+ const english = await __classPrivateFieldGet(this, _TerminologyServiceClient_cache, "f").get(terminologyValueSetCacheKey({
109
+ ...resolvedTerminologyContext,
110
+ language: 'en',
111
+ }));
91
112
  if (english) {
92
113
  return { document: english.document, cacheStatus: 'english-fallback-cache' };
93
114
  }
@@ -95,11 +116,61 @@ export class TerminologyServiceClient {
95
116
  throw new Error('Terminology service request failed');
96
117
  }
97
118
  }
119
+ /** Resolves every page and returns the simple code-to-display object expected by forms. */
120
+ async getValueSetCodeListForClaim(input) {
121
+ const pageSize = 100;
122
+ let nextOffset = 0;
123
+ let totalOptionCount = 0;
124
+ let codingSystem = '';
125
+ let resolvedLanguage = '';
126
+ let fallbackUsed = false;
127
+ let resolvedValueSet;
128
+ let terminologyVersions;
129
+ const codeToDisplay = {};
130
+ do {
131
+ const valueSetPage = await this.expandValueSetForClaim({
132
+ ...input,
133
+ offset: nextOffset,
134
+ count: pageSize,
135
+ });
136
+ const { document } = valueSetPage;
137
+ totalOptionCount = document.meta.total;
138
+ resolvedLanguage || (resolvedLanguage = document.meta.language);
139
+ resolvedValueSet ?? (resolvedValueSet = document.meta.valueSet);
140
+ terminologyVersions ?? (terminologyVersions = document.meta.terminologyVersions);
141
+ if (resolvedValueSet.system !== document.meta.valueSet.system) {
142
+ throw new Error('Terminology service returned inconsistent ValueSet pages');
143
+ }
144
+ for (const terminologyOptionResource of document.data) {
145
+ const terminologyOption = terminologyOptionResource.attributes;
146
+ if (codingSystem && codingSystem !== terminologyOption.system) {
147
+ throw new Error('Terminology ValueSet contains more than one coding system');
148
+ }
149
+ codingSystem || (codingSystem = terminologyOption.system);
150
+ codeToDisplay[terminologyOption.code] = terminologyOption.localizedDisplay;
151
+ fallbackUsed || (fallbackUsed = terminologyOption.fallbackUsed);
152
+ }
153
+ if (document.data.length === 0 && nextOffset < totalOptionCount) {
154
+ throw new Error('Terminology service returned an incomplete ValueSet page');
155
+ }
156
+ nextOffset += document.data.length;
157
+ } while (nextOffset < totalOptionCount);
158
+ if (!resolvedValueSet)
159
+ throw new Error('Terminology service returned no ValueSet metadata');
160
+ return {
161
+ codingSystem: codingSystem || resolvedValueSet.system,
162
+ codeToDisplay,
163
+ requestedLanguage: (input.language?.trim() || 'en').replace(/_/g, '-').toLowerCase(),
164
+ resolvedLanguage: resolvedLanguage || 'en',
165
+ fallbackUsed,
166
+ valueSet: resolvedValueSet,
167
+ ...(terminologyVersions ? { terminologyVersions } : {}),
168
+ };
169
+ }
98
170
  }
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) {
171
+ _TerminologyServiceClient_baseUrl = new WeakMap(), _TerminologyServiceClient_serviceToken = new WeakMap(), _TerminologyServiceClient_sector = 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
172
  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());
173
+ url.searchParams.set('sector', input.sector?.trim() || __classPrivateFieldGet(this, _TerminologyServiceClient_sector, "f") || '');
103
174
  url.searchParams.set('claim', input.claim.trim());
104
175
  url.searchParams.set('offset', String(input.offset ?? 0));
105
176
  url.searchParams.set('count', String(input.count ?? 50));
@@ -124,12 +195,11 @@ _TerminologyServiceClient_baseUrl = new WeakMap(), _TerminologyServiceClient_ser
124
195
  throw new Error('Terminology service returned an invalid document');
125
196
  return document;
126
197
  }, _TerminologyServiceClient_validateInput = function _TerminologyServiceClient_validateInput(input) {
127
- if (!input.sector.trim())
198
+ if (!(input.sector?.trim() || __classPrivateFieldGet(this, _TerminologyServiceClient_sector, "f")))
128
199
  throw new Error('sector is required');
129
- if (!input.resourceType.trim())
130
- throw new Error('resourceType is required');
131
200
  if (!input.claim.trim())
132
201
  throw new Error('claim is required');
202
+ resourceTypeFromClaim(input.claim);
133
203
  if (input.offset !== undefined && (!Number.isInteger(input.offset) || input.offset < 0)) {
134
204
  throw new Error('offset must be a non-negative integer');
135
205
  }
@@ -142,7 +212,7 @@ _TerminologyServiceClient_baseUrl = new WeakMap(), _TerminologyServiceClient_ser
142
212
  const document = value;
143
213
  if (document.jsonapi?.version !== '1.1' || !Array.isArray(document.data) || !document.meta)
144
214
  return false;
145
- if (document.meta.resourceType !== input.resourceType.trim() || document.meta.claim !== input.claim.trim())
215
+ if (document.meta.resourceType !== resourceTypeFromClaim(input.claim) || document.meta.claim !== input.claim.trim())
146
216
  return false;
147
217
  return document.data.every((resource) => (resource?.type === 'terminology-option'
148
218
  && typeof resource.id === 'string'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-sdk-node-ts",
3
- "version": "2.9.20",
3
+ "version": "2.9.22",
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.19",
41
+ "gdc-common-utils-ts": "2.9.20",
42
42
  "gdc-sdk-core-ts": "2.9.10"
43
43
  },
44
44
  "devDependencies": {