@erosolaraijs/cure 1.0.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +120 -0
  3. package/dist/bin/cure.d.ts +10 -0
  4. package/dist/bin/cure.d.ts.map +1 -0
  5. package/dist/bin/cure.js +169 -0
  6. package/dist/bin/cure.js.map +1 -0
  7. package/dist/capabilities/cancerTreatmentCapability.d.ts +167 -0
  8. package/dist/capabilities/cancerTreatmentCapability.d.ts.map +1 -0
  9. package/dist/capabilities/cancerTreatmentCapability.js +912 -0
  10. package/dist/capabilities/cancerTreatmentCapability.js.map +1 -0
  11. package/dist/capabilities/index.d.ts +2 -0
  12. package/dist/capabilities/index.d.ts.map +1 -0
  13. package/dist/capabilities/index.js +3 -0
  14. package/dist/capabilities/index.js.map +1 -0
  15. package/dist/compliance/hipaa.d.ts +337 -0
  16. package/dist/compliance/hipaa.d.ts.map +1 -0
  17. package/dist/compliance/hipaa.js +929 -0
  18. package/dist/compliance/hipaa.js.map +1 -0
  19. package/dist/examples/cancerTreatmentDemo.d.ts +21 -0
  20. package/dist/examples/cancerTreatmentDemo.d.ts.map +1 -0
  21. package/dist/examples/cancerTreatmentDemo.js +216 -0
  22. package/dist/examples/cancerTreatmentDemo.js.map +1 -0
  23. package/dist/integrations/clinicalTrials/clinicalTrialsGov.d.ts +265 -0
  24. package/dist/integrations/clinicalTrials/clinicalTrialsGov.d.ts.map +1 -0
  25. package/dist/integrations/clinicalTrials/clinicalTrialsGov.js +808 -0
  26. package/dist/integrations/clinicalTrials/clinicalTrialsGov.js.map +1 -0
  27. package/dist/integrations/ehr/fhir.d.ts +455 -0
  28. package/dist/integrations/ehr/fhir.d.ts.map +1 -0
  29. package/dist/integrations/ehr/fhir.js +859 -0
  30. package/dist/integrations/ehr/fhir.js.map +1 -0
  31. package/dist/integrations/genomics/genomicPlatforms.d.ts +362 -0
  32. package/dist/integrations/genomics/genomicPlatforms.d.ts.map +1 -0
  33. package/dist/integrations/genomics/genomicPlatforms.js +1079 -0
  34. package/dist/integrations/genomics/genomicPlatforms.js.map +1 -0
  35. package/package.json +52 -0
  36. package/src/bin/cure.ts +182 -0
  37. package/src/capabilities/cancerTreatmentCapability.ts +1161 -0
  38. package/src/capabilities/index.ts +2 -0
  39. package/src/compliance/hipaa.ts +1365 -0
  40. package/src/examples/cancerTreatmentDemo.ts +241 -0
  41. package/src/integrations/clinicalTrials/clinicalTrialsGov.ts +1143 -0
  42. package/src/integrations/ehr/fhir.ts +1304 -0
  43. package/src/integrations/genomics/genomicPlatforms.ts +1480 -0
  44. package/src/ml/outcomePredictor.ts +1301 -0
  45. package/src/safety/drugInteractions.ts +942 -0
  46. package/src/validation/retrospectiveValidator.ts +887 -0
@@ -0,0 +1,1304 @@
1
+ /**
2
+ * HL7 FHIR Integration Layer
3
+ *
4
+ * Connects to Electronic Health Record systems using the FHIR R4 standard.
5
+ * Supports Epic, Cerner, AllScripts, and other FHIR-compliant EHR systems.
6
+ *
7
+ * IMPORTANT: This module requires proper HIPAA compliance setup before use.
8
+ * All PHI access must be logged and patient consent must be verified.
9
+ */
10
+
11
+ import { EventEmitter } from 'events';
12
+
13
+ // ═══════════════════════════════════════════════════════════════════════════════
14
+ // FHIR R4 TYPE DEFINITIONS
15
+ // ═══════════════════════════════════════════════════════════════════════════════
16
+
17
+ export interface FHIRConfig {
18
+ baseUrl: string;
19
+ clientId: string;
20
+ clientSecret?: string;
21
+ scopes: string[];
22
+ authType: 'smart-on-fhir' | 'basic' | 'oauth2' | 'client-credentials';
23
+ ehrVendor: 'epic' | 'cerner' | 'allscripts' | 'meditech' | 'generic';
24
+ timeout?: number;
25
+ retryAttempts?: number;
26
+ }
27
+
28
+ export interface FHIRPatient {
29
+ resourceType: 'Patient';
30
+ id: string;
31
+ identifier: FHIRIdentifier[];
32
+ name: FHIRHumanName[];
33
+ gender: 'male' | 'female' | 'other' | 'unknown';
34
+ birthDate: string;
35
+ address?: FHIRAddress[];
36
+ telecom?: FHIRContactPoint[];
37
+ maritalStatus?: FHIRCodeableConcept;
38
+ communication?: { language: FHIRCodeableConcept; preferred?: boolean }[];
39
+ extension?: FHIRExtension[];
40
+ }
41
+
42
+ export interface FHIRIdentifier {
43
+ system: string;
44
+ value: string;
45
+ type?: FHIRCodeableConcept;
46
+ use?: 'usual' | 'official' | 'temp' | 'secondary' | 'old';
47
+ }
48
+
49
+ export interface FHIRHumanName {
50
+ use?: 'usual' | 'official' | 'temp' | 'nickname' | 'anonymous' | 'old' | 'maiden';
51
+ family?: string;
52
+ given?: string[];
53
+ prefix?: string[];
54
+ suffix?: string[];
55
+ }
56
+
57
+ export interface FHIRAddress {
58
+ use?: 'home' | 'work' | 'temp' | 'old' | 'billing';
59
+ type?: 'postal' | 'physical' | 'both';
60
+ line?: string[];
61
+ city?: string;
62
+ state?: string;
63
+ postalCode?: string;
64
+ country?: string;
65
+ }
66
+
67
+ export interface FHIRContactPoint {
68
+ system?: 'phone' | 'fax' | 'email' | 'pager' | 'url' | 'sms' | 'other';
69
+ value?: string;
70
+ use?: 'home' | 'work' | 'temp' | 'old' | 'mobile';
71
+ }
72
+
73
+ export interface FHIRCodeableConcept {
74
+ coding?: FHIRCoding[];
75
+ text?: string;
76
+ }
77
+
78
+ export interface FHIRCoding {
79
+ system?: string;
80
+ version?: string;
81
+ code?: string;
82
+ display?: string;
83
+ }
84
+
85
+ export interface FHIRExtension {
86
+ url: string;
87
+ valueString?: string;
88
+ valueCode?: string;
89
+ valueDecimal?: number;
90
+ valueBoolean?: boolean;
91
+ valueCoding?: FHIRCoding;
92
+ }
93
+
94
+ export interface FHIRCondition {
95
+ resourceType: 'Condition';
96
+ id: string;
97
+ clinicalStatus: FHIRCodeableConcept;
98
+ verificationStatus: FHIRCodeableConcept;
99
+ category: FHIRCodeableConcept[];
100
+ severity?: FHIRCodeableConcept;
101
+ code: FHIRCodeableConcept;
102
+ bodySite?: FHIRCodeableConcept[];
103
+ subject: FHIRReference;
104
+ onsetDateTime?: string;
105
+ recordedDate?: string;
106
+ stage?: {
107
+ summary?: FHIRCodeableConcept;
108
+ assessment?: FHIRReference[];
109
+ type?: FHIRCodeableConcept;
110
+ }[];
111
+ evidence?: {
112
+ code?: FHIRCodeableConcept[];
113
+ detail?: FHIRReference[];
114
+ }[];
115
+ }
116
+
117
+ export interface FHIRObservation {
118
+ resourceType: 'Observation';
119
+ id: string;
120
+ status: 'registered' | 'preliminary' | 'final' | 'amended' | 'corrected' | 'cancelled' | 'entered-in-error' | 'unknown';
121
+ category?: FHIRCodeableConcept[];
122
+ code: FHIRCodeableConcept;
123
+ subject: FHIRReference;
124
+ effectiveDateTime?: string;
125
+ valueQuantity?: FHIRQuantity;
126
+ valueCodeableConcept?: FHIRCodeableConcept;
127
+ valueString?: string;
128
+ interpretation?: FHIRCodeableConcept[];
129
+ referenceRange?: {
130
+ low?: FHIRQuantity;
131
+ high?: FHIRQuantity;
132
+ type?: FHIRCodeableConcept;
133
+ text?: string;
134
+ }[];
135
+ component?: {
136
+ code: FHIRCodeableConcept;
137
+ valueQuantity?: FHIRQuantity;
138
+ valueCodeableConcept?: FHIRCodeableConcept;
139
+ valueString?: string;
140
+ }[];
141
+ }
142
+
143
+ export interface FHIRQuantity {
144
+ value?: number;
145
+ comparator?: '<' | '<=' | '>=' | '>';
146
+ unit?: string;
147
+ system?: string;
148
+ code?: string;
149
+ }
150
+
151
+ export interface FHIRReference {
152
+ reference?: string;
153
+ type?: string;
154
+ identifier?: FHIRIdentifier;
155
+ display?: string;
156
+ }
157
+
158
+ export interface FHIRMedicationRequest {
159
+ resourceType: 'MedicationRequest';
160
+ id: string;
161
+ status: 'active' | 'on-hold' | 'cancelled' | 'completed' | 'entered-in-error' | 'stopped' | 'draft' | 'unknown';
162
+ intent: 'proposal' | 'plan' | 'order' | 'original-order' | 'reflex-order' | 'filler-order' | 'instance-order' | 'option';
163
+ medicationCodeableConcept?: FHIRCodeableConcept;
164
+ medicationReference?: FHIRReference;
165
+ subject: FHIRReference;
166
+ authoredOn?: string;
167
+ requester?: FHIRReference;
168
+ dosageInstruction?: FHIRDosage[];
169
+ }
170
+
171
+ export interface FHIRDosage {
172
+ sequence?: number;
173
+ text?: string;
174
+ timing?: {
175
+ repeat?: {
176
+ frequency?: number;
177
+ period?: number;
178
+ periodUnit?: 's' | 'min' | 'h' | 'd' | 'wk' | 'mo' | 'a';
179
+ };
180
+ };
181
+ route?: FHIRCodeableConcept;
182
+ doseAndRate?: {
183
+ doseQuantity?: FHIRQuantity;
184
+ rateQuantity?: FHIRQuantity;
185
+ }[];
186
+ }
187
+
188
+ export interface FHIRDiagnosticReport {
189
+ resourceType: 'DiagnosticReport';
190
+ id: string;
191
+ status: 'registered' | 'partial' | 'preliminary' | 'final' | 'amended' | 'corrected' | 'appended' | 'cancelled' | 'entered-in-error' | 'unknown';
192
+ category?: FHIRCodeableConcept[];
193
+ code: FHIRCodeableConcept;
194
+ subject: FHIRReference;
195
+ effectiveDateTime?: string;
196
+ issued?: string;
197
+ performer?: FHIRReference[];
198
+ result?: FHIRReference[];
199
+ conclusion?: string;
200
+ conclusionCode?: FHIRCodeableConcept[];
201
+ presentedForm?: {
202
+ contentType?: string;
203
+ data?: string;
204
+ url?: string;
205
+ title?: string;
206
+ }[];
207
+ }
208
+
209
+ export interface FHIRProcedure {
210
+ resourceType: 'Procedure';
211
+ id: string;
212
+ status: 'preparation' | 'in-progress' | 'not-done' | 'on-hold' | 'stopped' | 'completed' | 'entered-in-error' | 'unknown';
213
+ code: FHIRCodeableConcept;
214
+ subject: FHIRReference;
215
+ performedDateTime?: string;
216
+ performedPeriod?: { start?: string; end?: string };
217
+ performer?: { actor: FHIRReference; function?: FHIRCodeableConcept }[];
218
+ bodySite?: FHIRCodeableConcept[];
219
+ outcome?: FHIRCodeableConcept;
220
+ report?: FHIRReference[];
221
+ }
222
+
223
+ export interface FHIRBundle {
224
+ resourceType: 'Bundle';
225
+ id?: string;
226
+ type: 'document' | 'message' | 'transaction' | 'transaction-response' | 'batch' | 'batch-response' | 'history' | 'searchset' | 'collection';
227
+ total?: number;
228
+ link?: { relation: string; url: string }[];
229
+ entry?: {
230
+ fullUrl?: string;
231
+ resource?: FHIRResource;
232
+ search?: { mode?: 'match' | 'include' | 'outcome'; score?: number };
233
+ }[];
234
+ }
235
+
236
+ export type FHIRResource = FHIRPatient | FHIRCondition | FHIRObservation |
237
+ FHIRMedicationRequest | FHIRDiagnosticReport | FHIRProcedure | FHIRBundle;
238
+
239
+ // ═══════════════════════════════════════════════════════════════════════════════
240
+ // CANCER-SPECIFIC FHIR PROFILES
241
+ // ═══════════════════════════════════════════════════════════════════════════════
242
+
243
+ export interface CancerDiagnosis {
244
+ patientId: string;
245
+ conditionId: string;
246
+ cancerType: string;
247
+ icdCode: string;
248
+ snomedCode?: string;
249
+ histology?: string;
250
+ primarySite?: string;
251
+ laterality?: 'left' | 'right' | 'bilateral' | 'not-applicable';
252
+ stage?: {
253
+ system: 'ajcc' | 'figo' | 'rai' | 'binet' | 'iss' | 'other';
254
+ stage: string;
255
+ tnm?: { t?: string; n?: string; m?: string };
256
+ grade?: string;
257
+ };
258
+ diagnosisDate: Date;
259
+ verificationStatus: 'confirmed' | 'provisional' | 'differential' | 'refuted';
260
+ }
261
+
262
+ export interface CancerBiomarkers {
263
+ patientId: string;
264
+ collectionDate: Date;
265
+ biomarkers: {
266
+ name: string;
267
+ value: string | number;
268
+ unit?: string;
269
+ status: 'positive' | 'negative' | 'equivocal' | 'not-tested';
270
+ method?: string;
271
+ loincCode?: string;
272
+ }[];
273
+ genomicAlterations?: {
274
+ gene: string;
275
+ alteration: string;
276
+ type: 'mutation' | 'amplification' | 'deletion' | 'fusion' | 'rearrangement';
277
+ variantAlleleFrequency?: number;
278
+ pathogenicity?: 'pathogenic' | 'likely-pathogenic' | 'vus' | 'likely-benign' | 'benign';
279
+ }[];
280
+ tumorMutationalBurden?: {
281
+ value: number;
282
+ unit: 'mutations/Mb';
283
+ status: 'high' | 'intermediate' | 'low';
284
+ threshold?: number;
285
+ };
286
+ microsatelliteInstability?: {
287
+ status: 'MSI-H' | 'MSI-L' | 'MSS';
288
+ method: 'PCR' | 'NGS' | 'IHC';
289
+ };
290
+ pdl1Expression?: {
291
+ score: number;
292
+ scoreType: 'TPS' | 'CPS' | 'IC';
293
+ antibody?: string;
294
+ };
295
+ hrdStatus?: {
296
+ status: 'positive' | 'negative';
297
+ score?: number;
298
+ components?: {
299
+ loh?: number;
300
+ tai?: number;
301
+ lst?: number;
302
+ };
303
+ };
304
+ }
305
+
306
+ export interface TreatmentHistory {
307
+ patientId: string;
308
+ treatments: {
309
+ id: string;
310
+ type: 'chemotherapy' | 'immunotherapy' | 'targeted-therapy' | 'radiation' | 'surgery' | 'hormone-therapy' | 'car-t' | 'other';
311
+ regimen?: string;
312
+ drugs?: { name: string; dose?: string; route?: string; rxNormCode?: string }[];
313
+ startDate: Date;
314
+ endDate?: Date;
315
+ status: 'planned' | 'active' | 'completed' | 'stopped' | 'on-hold';
316
+ cycles?: number;
317
+ response?: 'CR' | 'PR' | 'SD' | 'PD' | 'NE';
318
+ responseDate?: Date;
319
+ reasonStopped?: string;
320
+ adverseEvents?: { name: string; grade: number; ctcaeCode?: string }[];
321
+ }[];
322
+ }
323
+
324
+ // ═══════════════════════════════════════════════════════════════════════════════
325
+ // FHIR CLIENT IMPLEMENTATION
326
+ // ═══════════════════════════════════════════════════════════════════════════════
327
+
328
+ export class FHIRClient extends EventEmitter {
329
+ private config: FHIRConfig;
330
+ private accessToken?: string;
331
+ private tokenExpiry?: Date;
332
+ private auditLogger?: (event: AuditEvent) => Promise<void>;
333
+
334
+ constructor(config: FHIRConfig) {
335
+ super();
336
+ this.config = {
337
+ timeout: 30000,
338
+ retryAttempts: 3,
339
+ ...config
340
+ };
341
+ }
342
+
343
+ /**
344
+ * Set audit logger for HIPAA compliance
345
+ */
346
+ setAuditLogger(logger: (event: AuditEvent) => Promise<void>): void {
347
+ this.auditLogger = logger;
348
+ }
349
+
350
+ /**
351
+ * Authenticate with the FHIR server
352
+ */
353
+ async authenticate(): Promise<void> {
354
+ const startTime = Date.now();
355
+
356
+ try {
357
+ switch (this.config.authType) {
358
+ case 'smart-on-fhir':
359
+ await this.smartOnFhirAuth();
360
+ break;
361
+ case 'client-credentials':
362
+ await this.clientCredentialsAuth();
363
+ break;
364
+ case 'oauth2':
365
+ await this.oauth2Auth();
366
+ break;
367
+ case 'basic':
368
+ // Basic auth doesn't require pre-authentication
369
+ break;
370
+ default:
371
+ throw new Error(`Unsupported auth type: ${this.config.authType}`);
372
+ }
373
+
374
+ await this.logAudit({
375
+ action: 'authenticate',
376
+ resourceType: 'System',
377
+ outcome: 'success',
378
+ duration: Date.now() - startTime
379
+ });
380
+ } catch (error) {
381
+ await this.logAudit({
382
+ action: 'authenticate',
383
+ resourceType: 'System',
384
+ outcome: 'failure',
385
+ duration: Date.now() - startTime,
386
+ errorMessage: error instanceof Error ? error.message : 'Unknown error'
387
+ });
388
+ throw error;
389
+ }
390
+ }
391
+
392
+ private async smartOnFhirAuth(): Promise<void> {
393
+ // SMART on FHIR authentication flow
394
+ // This is a simplified version - production would need full OAuth2 flow
395
+ const tokenEndpoint = await this.discoverTokenEndpoint();
396
+
397
+ const response = await this.httpRequest(tokenEndpoint, {
398
+ method: 'POST',
399
+ headers: {
400
+ 'Content-Type': 'application/x-www-form-urlencoded'
401
+ },
402
+ body: new URLSearchParams({
403
+ grant_type: 'client_credentials',
404
+ client_id: this.config.clientId,
405
+ client_secret: this.config.clientSecret || '',
406
+ scope: this.config.scopes.join(' ')
407
+ }).toString()
408
+ });
409
+
410
+ const data = JSON.parse(response);
411
+ this.accessToken = data.access_token;
412
+ this.tokenExpiry = new Date(Date.now() + (data.expires_in * 1000));
413
+ }
414
+
415
+ private async clientCredentialsAuth(): Promise<void> {
416
+ const tokenEndpoint = `${this.config.baseUrl}/oauth2/token`;
417
+
418
+ const response = await this.httpRequest(tokenEndpoint, {
419
+ method: 'POST',
420
+ headers: {
421
+ 'Content-Type': 'application/x-www-form-urlencoded',
422
+ 'Authorization': `Basic ${Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64')}`
423
+ },
424
+ body: new URLSearchParams({
425
+ grant_type: 'client_credentials',
426
+ scope: this.config.scopes.join(' ')
427
+ }).toString()
428
+ });
429
+
430
+ const data = JSON.parse(response);
431
+ this.accessToken = data.access_token;
432
+ this.tokenExpiry = new Date(Date.now() + (data.expires_in * 1000));
433
+ }
434
+
435
+ private async oauth2Auth(): Promise<void> {
436
+ // OAuth2 flow - similar to client credentials for server-to-server
437
+ await this.clientCredentialsAuth();
438
+ }
439
+
440
+ private async discoverTokenEndpoint(): Promise<string> {
441
+ const wellKnown = `${this.config.baseUrl}/.well-known/smart-configuration`;
442
+
443
+ try {
444
+ const response = await this.httpRequest(wellKnown, { method: 'GET' });
445
+ const config = JSON.parse(response);
446
+ return config.token_endpoint;
447
+ } catch {
448
+ // Fall back to standard OAuth2 endpoint
449
+ return `${this.config.baseUrl}/oauth2/token`;
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Get a patient by ID
455
+ */
456
+ async getPatient(patientId: string): Promise<FHIRPatient> {
457
+ return await this.read<FHIRPatient>('Patient', patientId);
458
+ }
459
+
460
+ /**
461
+ * Search for patients
462
+ */
463
+ async searchPatients(params: Record<string, string>): Promise<FHIRBundle> {
464
+ return await this.search('Patient', params);
465
+ }
466
+
467
+ /**
468
+ * Get cancer diagnosis for a patient
469
+ */
470
+ async getCancerDiagnosis(patientId: string): Promise<CancerDiagnosis[]> {
471
+ const bundle = await this.search('Condition', {
472
+ patient: patientId,
473
+ category: 'encounter-diagnosis',
474
+ 'code:below': '363346000' // SNOMED CT code for malignant neoplasm
475
+ });
476
+
477
+ const diagnoses: CancerDiagnosis[] = [];
478
+
479
+ for (const entry of bundle.entry || []) {
480
+ const condition = entry.resource as FHIRCondition;
481
+ if (condition.resourceType === 'Condition') {
482
+ diagnoses.push(this.mapConditionToCancerDiagnosis(condition, patientId));
483
+ }
484
+ }
485
+
486
+ return diagnoses;
487
+ }
488
+
489
+ /**
490
+ * Get biomarkers and genomic data for a patient
491
+ */
492
+ async getBiomarkers(patientId: string): Promise<CancerBiomarkers | null> {
493
+ // Get lab observations
494
+ const labBundle = await this.search('Observation', {
495
+ patient: patientId,
496
+ category: 'laboratory',
497
+ _sort: '-date',
498
+ _count: '100'
499
+ });
500
+
501
+ // Get genomic observations
502
+ const genomicBundle = await this.search('Observation', {
503
+ patient: patientId,
504
+ category: 'genomic-variant',
505
+ _sort: '-date',
506
+ _count: '100'
507
+ });
508
+
509
+ // Get diagnostic reports (for NGS results)
510
+ const reportBundle = await this.search('DiagnosticReport', {
511
+ patient: patientId,
512
+ category: 'GE', // Genetics
513
+ _sort: '-date',
514
+ _count: '50'
515
+ });
516
+
517
+ return this.aggregateBiomarkerData(patientId, labBundle, genomicBundle, reportBundle);
518
+ }
519
+
520
+ /**
521
+ * Get treatment history for a patient
522
+ */
523
+ async getTreatmentHistory(patientId: string): Promise<TreatmentHistory> {
524
+ const [medications, procedures] = await Promise.all([
525
+ this.search('MedicationRequest', {
526
+ patient: patientId,
527
+ _sort: '-authoredon',
528
+ _count: '200'
529
+ }),
530
+ this.search('Procedure', {
531
+ patient: patientId,
532
+ _sort: '-date',
533
+ _count: '200'
534
+ })
535
+ ]);
536
+
537
+ return this.aggregateTreatmentHistory(patientId, medications, procedures);
538
+ }
539
+
540
+ /**
541
+ * Get all observations for a patient
542
+ */
543
+ async getObservations(patientId: string, category?: string): Promise<FHIRObservation[]> {
544
+ const params: Record<string, string> = {
545
+ patient: patientId,
546
+ _sort: '-date',
547
+ _count: '100'
548
+ };
549
+
550
+ if (category) {
551
+ params.category = category;
552
+ }
553
+
554
+ const bundle = await this.search('Observation', params);
555
+ return (bundle.entry || [])
556
+ .map(e => e.resource)
557
+ .filter((r): r is FHIRObservation => r?.resourceType === 'Observation');
558
+ }
559
+
560
+ /**
561
+ * Get all medications for a patient
562
+ */
563
+ async getMedications(patientId: string): Promise<FHIRMedicationRequest[]> {
564
+ const bundle = await this.search('MedicationRequest', {
565
+ patient: patientId,
566
+ _sort: '-authoredon'
567
+ });
568
+
569
+ return (bundle.entry || [])
570
+ .map(e => e.resource)
571
+ .filter((r): r is FHIRMedicationRequest => r?.resourceType === 'MedicationRequest');
572
+ }
573
+
574
+ /**
575
+ * Get diagnostic reports for a patient
576
+ */
577
+ async getDiagnosticReports(patientId: string): Promise<FHIRDiagnosticReport[]> {
578
+ const bundle = await this.search('DiagnosticReport', {
579
+ patient: patientId,
580
+ _sort: '-date'
581
+ });
582
+
583
+ return (bundle.entry || [])
584
+ .map(e => e.resource)
585
+ .filter((r): r is FHIRDiagnosticReport => r?.resourceType === 'DiagnosticReport');
586
+ }
587
+
588
+ /**
589
+ * Create a comprehensive cancer patient summary
590
+ */
591
+ async getComprehensiveCancerSummary(patientId: string): Promise<{
592
+ patient: FHIRPatient;
593
+ diagnoses: CancerDiagnosis[];
594
+ biomarkers: CancerBiomarkers | null;
595
+ treatmentHistory: TreatmentHistory;
596
+ recentLabs: FHIRObservation[];
597
+ performanceStatus?: { score: number; scale: 'ECOG' | 'KPS'; date: Date };
598
+ }> {
599
+ const [patient, diagnoses, biomarkers, treatmentHistory, recentLabs] = await Promise.all([
600
+ this.getPatient(patientId),
601
+ this.getCancerDiagnosis(patientId),
602
+ this.getBiomarkers(patientId),
603
+ this.getTreatmentHistory(patientId),
604
+ this.getObservations(patientId, 'laboratory')
605
+ ]);
606
+
607
+ // Get performance status (ECOG/KPS)
608
+ const performanceObs = await this.search('Observation', {
609
+ patient: patientId,
610
+ code: '89247-1', // LOINC for ECOG performance status
611
+ _sort: '-date',
612
+ _count: '1'
613
+ });
614
+
615
+ let performanceStatus: { score: number; scale: 'ECOG' | 'KPS'; date: Date } | undefined;
616
+ const perfEntry = performanceObs.entry?.[0]?.resource as FHIRObservation;
617
+ if (perfEntry?.valueQuantity?.value !== undefined) {
618
+ performanceStatus = {
619
+ score: perfEntry.valueQuantity.value,
620
+ scale: 'ECOG',
621
+ date: new Date(perfEntry.effectiveDateTime || Date.now())
622
+ };
623
+ }
624
+
625
+ return {
626
+ patient,
627
+ diagnoses,
628
+ biomarkers,
629
+ treatmentHistory,
630
+ recentLabs: recentLabs.slice(0, 20),
631
+ performanceStatus
632
+ };
633
+ }
634
+
635
+ // ═══════════════════════════════════════════════════════════════════════════════
636
+ // CORE FHIR OPERATIONS
637
+ // ═══════════════════════════════════════════════════════════════════════════════
638
+
639
+ /**
640
+ * Read a resource by ID
641
+ */
642
+ async read<T extends FHIRResource>(resourceType: string, id: string): Promise<T> {
643
+ const url = `${this.config.baseUrl}/${resourceType}/${id}`;
644
+ const startTime = Date.now();
645
+
646
+ try {
647
+ const response = await this.httpRequest(url, {
648
+ method: 'GET',
649
+ headers: await this.getAuthHeaders()
650
+ });
651
+
652
+ await this.logAudit({
653
+ action: 'read',
654
+ resourceType,
655
+ resourceId: id,
656
+ outcome: 'success',
657
+ duration: Date.now() - startTime
658
+ });
659
+
660
+ return JSON.parse(response) as T;
661
+ } catch (error) {
662
+ await this.logAudit({
663
+ action: 'read',
664
+ resourceType,
665
+ resourceId: id,
666
+ outcome: 'failure',
667
+ duration: Date.now() - startTime,
668
+ errorMessage: error instanceof Error ? error.message : 'Unknown error'
669
+ });
670
+ throw error;
671
+ }
672
+ }
673
+
674
+ /**
675
+ * Search for resources
676
+ */
677
+ async search(resourceType: string, params: Record<string, string>): Promise<FHIRBundle> {
678
+ const searchParams = new URLSearchParams(params);
679
+ const url = `${this.config.baseUrl}/${resourceType}?${searchParams.toString()}`;
680
+ const startTime = Date.now();
681
+
682
+ try {
683
+ const response = await this.httpRequest(url, {
684
+ method: 'GET',
685
+ headers: await this.getAuthHeaders()
686
+ });
687
+
688
+ await this.logAudit({
689
+ action: 'search',
690
+ resourceType,
691
+ outcome: 'success',
692
+ duration: Date.now() - startTime,
693
+ details: { searchParams: params }
694
+ });
695
+
696
+ return JSON.parse(response) as FHIRBundle;
697
+ } catch (error) {
698
+ await this.logAudit({
699
+ action: 'search',
700
+ resourceType,
701
+ outcome: 'failure',
702
+ duration: Date.now() - startTime,
703
+ errorMessage: error instanceof Error ? error.message : 'Unknown error'
704
+ });
705
+ throw error;
706
+ }
707
+ }
708
+
709
+ /**
710
+ * Create a resource
711
+ */
712
+ async create<T extends FHIRResource>(resource: T): Promise<T> {
713
+ const url = `${this.config.baseUrl}/${resource.resourceType}`;
714
+ const startTime = Date.now();
715
+
716
+ try {
717
+ const response = await this.httpRequest(url, {
718
+ method: 'POST',
719
+ headers: {
720
+ ...await this.getAuthHeaders(),
721
+ 'Content-Type': 'application/fhir+json'
722
+ },
723
+ body: JSON.stringify(resource)
724
+ });
725
+
726
+ await this.logAudit({
727
+ action: 'create',
728
+ resourceType: resource.resourceType,
729
+ outcome: 'success',
730
+ duration: Date.now() - startTime
731
+ });
732
+
733
+ return JSON.parse(response) as T;
734
+ } catch (error) {
735
+ await this.logAudit({
736
+ action: 'create',
737
+ resourceType: resource.resourceType,
738
+ outcome: 'failure',
739
+ duration: Date.now() - startTime,
740
+ errorMessage: error instanceof Error ? error.message : 'Unknown error'
741
+ });
742
+ throw error;
743
+ }
744
+ }
745
+
746
+ /**
747
+ * Update a resource
748
+ */
749
+ async update<T extends FHIRResource>(resource: T & { id: string }): Promise<T> {
750
+ const url = `${this.config.baseUrl}/${resource.resourceType}/${resource.id}`;
751
+ const startTime = Date.now();
752
+
753
+ try {
754
+ const response = await this.httpRequest(url, {
755
+ method: 'PUT',
756
+ headers: {
757
+ ...await this.getAuthHeaders(),
758
+ 'Content-Type': 'application/fhir+json'
759
+ },
760
+ body: JSON.stringify(resource)
761
+ });
762
+
763
+ await this.logAudit({
764
+ action: 'update',
765
+ resourceType: resource.resourceType,
766
+ resourceId: resource.id,
767
+ outcome: 'success',
768
+ duration: Date.now() - startTime
769
+ });
770
+
771
+ return JSON.parse(response) as T;
772
+ } catch (error) {
773
+ await this.logAudit({
774
+ action: 'update',
775
+ resourceType: resource.resourceType,
776
+ resourceId: resource.id,
777
+ outcome: 'failure',
778
+ duration: Date.now() - startTime,
779
+ errorMessage: error instanceof Error ? error.message : 'Unknown error'
780
+ });
781
+ throw error;
782
+ }
783
+ }
784
+
785
+ // ═══════════════════════════════════════════════════════════════════════════════
786
+ // HELPER METHODS
787
+ // ═══════════════════════════════════════════════════════════════════════════════
788
+
789
+ private async getAuthHeaders(): Promise<Record<string, string>> {
790
+ // Check if token needs refresh
791
+ if (this.tokenExpiry && new Date() >= this.tokenExpiry) {
792
+ await this.authenticate();
793
+ }
794
+
795
+ const headers: Record<string, string> = {
796
+ 'Accept': 'application/fhir+json'
797
+ };
798
+
799
+ if (this.accessToken) {
800
+ headers['Authorization'] = `Bearer ${this.accessToken}`;
801
+ } else if (this.config.authType === 'basic') {
802
+ headers['Authorization'] = `Basic ${Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64')}`;
803
+ }
804
+
805
+ return headers;
806
+ }
807
+
808
+ private async httpRequest(url: string, options: {
809
+ method: string;
810
+ headers?: Record<string, string>;
811
+ body?: string;
812
+ }): Promise<string> {
813
+ // Use native fetch in Node.js 18+
814
+ const response = await fetch(url, {
815
+ method: options.method,
816
+ headers: options.headers,
817
+ body: options.body,
818
+ signal: AbortSignal.timeout(this.config.timeout || 30000)
819
+ });
820
+
821
+ if (!response.ok) {
822
+ const errorBody = await response.text();
823
+ throw new Error(`FHIR request failed: ${response.status} ${response.statusText} - ${errorBody}`);
824
+ }
825
+
826
+ return await response.text();
827
+ }
828
+
829
+ private mapConditionToCancerDiagnosis(condition: FHIRCondition, patientId: string): CancerDiagnosis {
830
+ const icdCoding = condition.code.coding?.find(c =>
831
+ c.system?.includes('icd-10') || c.system?.includes('icd-9')
832
+ );
833
+ const snomedCoding = condition.code.coding?.find(c =>
834
+ c.system?.includes('snomed')
835
+ );
836
+
837
+ const stageInfo = condition.stage?.[0];
838
+ let stage: CancerDiagnosis['stage'];
839
+
840
+ if (stageInfo?.summary) {
841
+ const stageCode = stageInfo.summary.coding?.[0]?.code || stageInfo.summary.text || '';
842
+ stage = {
843
+ system: this.determineStageSystem(stageCode),
844
+ stage: stageCode
845
+ };
846
+ }
847
+
848
+ return {
849
+ patientId,
850
+ conditionId: condition.id,
851
+ cancerType: condition.code.text || condition.code.coding?.[0]?.display || 'Unknown',
852
+ icdCode: icdCoding?.code || '',
853
+ snomedCode: snomedCoding?.code,
854
+ primarySite: condition.bodySite?.[0]?.text || condition.bodySite?.[0]?.coding?.[0]?.display,
855
+ stage,
856
+ diagnosisDate: new Date(condition.onsetDateTime || condition.recordedDate || Date.now()),
857
+ verificationStatus: this.mapVerificationStatus(condition.verificationStatus)
858
+ };
859
+ }
860
+
861
+ private determineStageSystem(stageCode: string): 'ajcc' | 'figo' | 'rai' | 'binet' | 'iss' | 'other' {
862
+ const code = stageCode.toUpperCase();
863
+ if (code.includes('AJCC') || /^[IV]+[ABC]?$/.test(code) || code.includes('TNM')) {
864
+ return 'ajcc';
865
+ }
866
+ if (code.includes('FIGO')) return 'figo';
867
+ if (code.includes('RAI')) return 'rai';
868
+ if (code.includes('BINET')) return 'binet';
869
+ if (code.includes('ISS')) return 'iss';
870
+ return 'other';
871
+ }
872
+
873
+ private mapVerificationStatus(status: FHIRCodeableConcept): CancerDiagnosis['verificationStatus'] {
874
+ const code = status.coding?.[0]?.code?.toLowerCase() || '';
875
+ if (code.includes('confirmed')) return 'confirmed';
876
+ if (code.includes('provisional')) return 'provisional';
877
+ if (code.includes('differential')) return 'differential';
878
+ if (code.includes('refuted')) return 'refuted';
879
+ return 'confirmed';
880
+ }
881
+
882
+ private aggregateBiomarkerData(
883
+ patientId: string,
884
+ labBundle: FHIRBundle,
885
+ genomicBundle: FHIRBundle,
886
+ reportBundle: FHIRBundle
887
+ ): CancerBiomarkers | null {
888
+ const biomarkers: CancerBiomarkers['biomarkers'] = [];
889
+ const genomicAlterations: CancerBiomarkers['genomicAlterations'] = [];
890
+ let latestDate = new Date(0);
891
+
892
+ // Process lab observations
893
+ for (const entry of labBundle.entry || []) {
894
+ const obs = entry.resource as FHIRObservation;
895
+ if (obs.resourceType !== 'Observation') continue;
896
+
897
+ const date = new Date(obs.effectiveDateTime || Date.now());
898
+ if (date > latestDate) latestDate = date;
899
+
900
+ // Map common cancer biomarkers
901
+ const biomarker = this.mapObservationToBiomarker(obs);
902
+ if (biomarker) biomarkers.push(biomarker);
903
+ }
904
+
905
+ // Process genomic observations
906
+ for (const entry of genomicBundle.entry || []) {
907
+ const obs = entry.resource as FHIRObservation;
908
+ if (obs.resourceType !== 'Observation') continue;
909
+
910
+ const date = new Date(obs.effectiveDateTime || Date.now());
911
+ if (date > latestDate) latestDate = date;
912
+
913
+ const alteration = this.mapObservationToGenomicAlteration(obs);
914
+ if (alteration) genomicAlterations.push(alteration);
915
+ }
916
+
917
+ // Process diagnostic reports for additional genomic data
918
+ for (const entry of reportBundle.entry || []) {
919
+ const report = entry.resource as FHIRDiagnosticReport;
920
+ if (report.resourceType !== 'DiagnosticReport') continue;
921
+
922
+ const date = new Date(report.effectiveDateTime || Date.now());
923
+ if (date > latestDate) latestDate = date;
924
+ }
925
+
926
+ if (biomarkers.length === 0 && genomicAlterations.length === 0) {
927
+ return null;
928
+ }
929
+
930
+ // Extract special biomarkers
931
+ const tmbObs = this.findBiomarker(biomarkers, ['TMB', 'tumor mutational burden']);
932
+ const msiObs = this.findBiomarker(biomarkers, ['MSI', 'microsatellite']);
933
+ const pdl1Obs = this.findBiomarker(biomarkers, ['PD-L1', 'PDL1']);
934
+ const hrdObs = this.findBiomarker(biomarkers, ['HRD', 'homologous recombination']);
935
+
936
+ return {
937
+ patientId,
938
+ collectionDate: latestDate,
939
+ biomarkers,
940
+ genomicAlterations: genomicAlterations.length > 0 ? genomicAlterations : undefined,
941
+ tumorMutationalBurden: tmbObs ? {
942
+ value: typeof tmbObs.value === 'number' ? tmbObs.value : parseFloat(String(tmbObs.value)) || 0,
943
+ unit: 'mutations/Mb',
944
+ status: this.categorizeTMB(tmbObs.value)
945
+ } : undefined,
946
+ microsatelliteInstability: msiObs ? {
947
+ status: this.categorizeMSI(msiObs.value),
948
+ method: 'NGS'
949
+ } : undefined,
950
+ pdl1Expression: pdl1Obs ? {
951
+ score: typeof pdl1Obs.value === 'number' ? pdl1Obs.value : parseFloat(String(pdl1Obs.value)) || 0,
952
+ scoreType: 'TPS'
953
+ } : undefined,
954
+ hrdStatus: hrdObs ? {
955
+ status: hrdObs.status === 'positive' ? 'positive' : 'negative',
956
+ score: typeof hrdObs.value === 'number' ? hrdObs.value : undefined
957
+ } : undefined
958
+ };
959
+ }
960
+
961
+ private mapObservationToBiomarker(obs: FHIRObservation): CancerBiomarkers['biomarkers'][0] | null {
962
+ const name = obs.code.text || obs.code.coding?.[0]?.display;
963
+ if (!name) return null;
964
+
965
+ let value: string | number;
966
+ let status: 'positive' | 'negative' | 'equivocal' | 'not-tested';
967
+
968
+ if (obs.valueQuantity?.value !== undefined) {
969
+ value = obs.valueQuantity.value;
970
+ status = 'positive'; // Will be refined based on interpretation
971
+ } else if (obs.valueCodeableConcept) {
972
+ value = obs.valueCodeableConcept.text || obs.valueCodeableConcept.coding?.[0]?.display || '';
973
+ status = this.interpretBiomarkerStatus(value);
974
+ } else if (obs.valueString) {
975
+ value = obs.valueString;
976
+ status = this.interpretBiomarkerStatus(value);
977
+ } else {
978
+ return null;
979
+ }
980
+
981
+ // Check interpretation if available
982
+ if (obs.interpretation?.[0]?.coding?.[0]?.code) {
983
+ const interpCode = obs.interpretation[0].coding[0].code;
984
+ if (interpCode === 'POS' || interpCode === 'H') status = 'positive';
985
+ else if (interpCode === 'NEG' || interpCode === 'N') status = 'negative';
986
+ else if (interpCode === 'IND') status = 'equivocal';
987
+ }
988
+
989
+ return {
990
+ name,
991
+ value,
992
+ unit: obs.valueQuantity?.unit,
993
+ status,
994
+ loincCode: obs.code.coding?.find(c => c.system?.includes('loinc'))?.code
995
+ };
996
+ }
997
+
998
+ private mapObservationToGenomicAlteration(obs: FHIRObservation): CancerBiomarkers['genomicAlterations'][0] | null {
999
+ // This would need to be expanded to handle the full mCode/genomics-reporting IG
1000
+ const geneComponent = obs.component?.find(c =>
1001
+ c.code.coding?.some(coding => coding.code === '48018-6') // Gene studied
1002
+ );
1003
+ const variantComponent = obs.component?.find(c =>
1004
+ c.code.coding?.some(coding => coding.code === '81252-9') // DNA change
1005
+ );
1006
+
1007
+ if (!geneComponent || !variantComponent) return null;
1008
+
1009
+ const gene = geneComponent.valueCodeableConcept?.text ||
1010
+ geneComponent.valueCodeableConcept?.coding?.[0]?.display || '';
1011
+ const alteration = variantComponent.valueString ||
1012
+ variantComponent.valueCodeableConcept?.text || '';
1013
+
1014
+ if (!gene || !alteration) return null;
1015
+
1016
+ return {
1017
+ gene,
1018
+ alteration,
1019
+ type: this.determineAlterationType(alteration)
1020
+ };
1021
+ }
1022
+
1023
+ private determineAlterationType(alteration: string): 'mutation' | 'amplification' | 'deletion' | 'fusion' | 'rearrangement' {
1024
+ const lower = alteration.toLowerCase();
1025
+ if (lower.includes('amp') || lower.includes('gain')) return 'amplification';
1026
+ if (lower.includes('del') || lower.includes('loss')) return 'deletion';
1027
+ if (lower.includes('fusion') || lower.includes('::')) return 'fusion';
1028
+ if (lower.includes('rearr') || lower.includes('transloc')) return 'rearrangement';
1029
+ return 'mutation';
1030
+ }
1031
+
1032
+ private interpretBiomarkerStatus(value: string | number): 'positive' | 'negative' | 'equivocal' | 'not-tested' {
1033
+ const lower = String(value).toLowerCase();
1034
+ if (lower.includes('positive') || lower.includes('detected') || lower === 'yes') return 'positive';
1035
+ if (lower.includes('negative') || lower.includes('not detected') || lower === 'no') return 'negative';
1036
+ if (lower.includes('equivocal') || lower.includes('indeterminate') || lower.includes('borderline')) return 'equivocal';
1037
+ return 'positive'; // Default to positive if we have a value
1038
+ }
1039
+
1040
+ private findBiomarker(biomarkers: CancerBiomarkers['biomarkers'], keywords: string[]): CancerBiomarkers['biomarkers'][0] | undefined {
1041
+ return biomarkers.find(b =>
1042
+ keywords.some(k => b.name.toLowerCase().includes(k.toLowerCase()))
1043
+ );
1044
+ }
1045
+
1046
+ private categorizeTMB(value: string | number): 'high' | 'intermediate' | 'low' {
1047
+ const numValue = typeof value === 'number' ? value : parseFloat(String(value));
1048
+ if (isNaN(numValue)) return 'low';
1049
+ if (numValue >= 10) return 'high';
1050
+ if (numValue >= 6) return 'intermediate';
1051
+ return 'low';
1052
+ }
1053
+
1054
+ private categorizeMSI(value: string | number): 'MSI-H' | 'MSI-L' | 'MSS' {
1055
+ const strValue = String(value).toUpperCase();
1056
+ if (strValue.includes('MSI-H') || strValue.includes('HIGH') || strValue.includes('UNSTABLE')) return 'MSI-H';
1057
+ if (strValue.includes('MSI-L') || strValue.includes('LOW')) return 'MSI-L';
1058
+ return 'MSS';
1059
+ }
1060
+
1061
+ private aggregateTreatmentHistory(
1062
+ patientId: string,
1063
+ medications: FHIRBundle,
1064
+ procedures: FHIRBundle
1065
+ ): TreatmentHistory {
1066
+ const treatments: TreatmentHistory['treatments'] = [];
1067
+
1068
+ // Process medications
1069
+ for (const entry of medications.entry || []) {
1070
+ const med = entry.resource as FHIRMedicationRequest;
1071
+ if (med.resourceType !== 'MedicationRequest') continue;
1072
+
1073
+ const drugName = med.medicationCodeableConcept?.text ||
1074
+ med.medicationCodeableConcept?.coding?.[0]?.display ||
1075
+ 'Unknown medication';
1076
+
1077
+ const treatment = this.categorizeTreatment(drugName);
1078
+
1079
+ treatments.push({
1080
+ id: med.id,
1081
+ type: treatment.type,
1082
+ regimen: treatment.regimen,
1083
+ drugs: [{
1084
+ name: drugName,
1085
+ dose: med.dosageInstruction?.[0]?.text,
1086
+ route: med.dosageInstruction?.[0]?.route?.text,
1087
+ rxNormCode: med.medicationCodeableConcept?.coding?.find(c =>
1088
+ c.system?.includes('rxnorm')
1089
+ )?.code
1090
+ }],
1091
+ startDate: new Date(med.authoredOn || Date.now()),
1092
+ status: this.mapMedicationStatus(med.status)
1093
+ });
1094
+ }
1095
+
1096
+ // Process procedures (surgery, radiation)
1097
+ for (const entry of procedures.entry || []) {
1098
+ const proc = entry.resource as FHIRProcedure;
1099
+ if (proc.resourceType !== 'Procedure') continue;
1100
+
1101
+ const procName = proc.code.text || proc.code.coding?.[0]?.display || 'Unknown procedure';
1102
+ const isSurgery = this.isSurgicalProcedure(procName);
1103
+ const isRadiation = this.isRadiationProcedure(procName);
1104
+
1105
+ if (isSurgery || isRadiation) {
1106
+ treatments.push({
1107
+ id: proc.id,
1108
+ type: isRadiation ? 'radiation' : 'surgery',
1109
+ drugs: [],
1110
+ startDate: new Date(proc.performedDateTime || proc.performedPeriod?.start || Date.now()),
1111
+ endDate: proc.performedPeriod?.end ? new Date(proc.performedPeriod.end) : undefined,
1112
+ status: this.mapProcedureStatus(proc.status)
1113
+ });
1114
+ }
1115
+ }
1116
+
1117
+ // Sort by date
1118
+ treatments.sort((a, b) => b.startDate.getTime() - a.startDate.getTime());
1119
+
1120
+ return { patientId, treatments };
1121
+ }
1122
+
1123
+ private categorizeTreatment(drugName: string): { type: TreatmentHistory['treatments'][0]['type']; regimen?: string } {
1124
+ const lower = drugName.toLowerCase();
1125
+
1126
+ // Immunotherapy
1127
+ const immunotherapyDrugs = ['pembrolizumab', 'nivolumab', 'ipilimumab', 'atezolizumab', 'durvalumab',
1128
+ 'avelumab', 'cemiplimab', 'dostarlimab', 'relatlimab', 'tremelimumab'];
1129
+ if (immunotherapyDrugs.some(d => lower.includes(d))) {
1130
+ return { type: 'immunotherapy' };
1131
+ }
1132
+
1133
+ // Targeted therapy
1134
+ const targetedDrugs = ['imatinib', 'erlotinib', 'gefitinib', 'osimertinib', 'crizotinib', 'alectinib',
1135
+ 'palbociclib', 'ribociclib', 'abemaciclib', 'olaparib', 'rucaparib', 'niraparib', 'trastuzumab',
1136
+ 'pertuzumab', 'lapatinib', 'vemurafenib', 'dabrafenib', 'trametinib', 'sotorasib', 'adagrasib',
1137
+ 'venetoclax', 'ibrutinib', 'acalabrutinib', 'lenvatinib', 'sorafenib', 'regorafenib', 'cabozantinib',
1138
+ 'bevacizumab', 'cetuximab', 'panitumumab', 'rituximab'];
1139
+ if (targetedDrugs.some(d => lower.includes(d))) {
1140
+ return { type: 'targeted-therapy' };
1141
+ }
1142
+
1143
+ // Hormone therapy
1144
+ const hormoneDrugs = ['tamoxifen', 'letrozole', 'anastrozole', 'exemestane', 'fulvestrant',
1145
+ 'enzalutamide', 'abiraterone', 'apalutamide', 'darolutamide', 'lupron', 'leuprolide'];
1146
+ if (hormoneDrugs.some(d => lower.includes(d))) {
1147
+ return { type: 'hormone-therapy' };
1148
+ }
1149
+
1150
+ // CAR-T
1151
+ const carTDrugs = ['tisagenlecleucel', 'axicabtagene', 'brexucabtagene', 'lisocabtagene',
1152
+ 'idecabtagene', 'ciltacabtagene', 'kymriah', 'yescarta', 'tecartus', 'breyanzi', 'abecma', 'carvykti'];
1153
+ if (carTDrugs.some(d => lower.includes(d))) {
1154
+ return { type: 'car-t' };
1155
+ }
1156
+
1157
+ // Chemotherapy (catch-all for cytotoxic agents)
1158
+ const chemoDrugs = ['carboplatin', 'cisplatin', 'oxaliplatin', 'paclitaxel', 'docetaxel',
1159
+ 'doxorubicin', 'epirubicin', 'cyclophosphamide', 'fluorouracil', '5-fu', 'capecitabine',
1160
+ 'gemcitabine', 'pemetrexed', 'etoposide', 'irinotecan', 'vincristine', 'vinblastine',
1161
+ 'methotrexate', 'cytarabine', 'azacitidine', 'decitabine', 'temozolomide'];
1162
+ if (chemoDrugs.some(d => lower.includes(d))) {
1163
+ return { type: 'chemotherapy' };
1164
+ }
1165
+
1166
+ return { type: 'other' };
1167
+ }
1168
+
1169
+ private mapMedicationStatus(status: FHIRMedicationRequest['status']): TreatmentHistory['treatments'][0]['status'] {
1170
+ switch (status) {
1171
+ case 'active': return 'active';
1172
+ case 'completed': return 'completed';
1173
+ case 'stopped': return 'stopped';
1174
+ case 'on-hold': return 'on-hold';
1175
+ case 'draft': return 'planned';
1176
+ default: return 'active';
1177
+ }
1178
+ }
1179
+
1180
+ private mapProcedureStatus(status: FHIRProcedure['status']): TreatmentHistory['treatments'][0]['status'] {
1181
+ switch (status) {
1182
+ case 'completed': return 'completed';
1183
+ case 'in-progress': return 'active';
1184
+ case 'preparation': return 'planned';
1185
+ case 'on-hold': return 'on-hold';
1186
+ case 'stopped': return 'stopped';
1187
+ default: return 'completed';
1188
+ }
1189
+ }
1190
+
1191
+ private isSurgicalProcedure(name: string): boolean {
1192
+ const surgeryKeywords = ['surgery', 'resection', 'excision', 'mastectomy', 'lobectomy',
1193
+ 'colectomy', 'gastrectomy', 'prostatectomy', 'nephrectomy', 'hysterectomy',
1194
+ 'lymphadenectomy', 'biopsy', 'debulking', 'whipple', 'hepatectomy'];
1195
+ return surgeryKeywords.some(k => name.toLowerCase().includes(k));
1196
+ }
1197
+
1198
+ private isRadiationProcedure(name: string): boolean {
1199
+ const radiationKeywords = ['radiation', 'radiotherapy', 'sbrt', 'imrt', 'proton',
1200
+ 'brachytherapy', 'cyberknife', 'gamma knife', 'external beam'];
1201
+ return radiationKeywords.some(k => name.toLowerCase().includes(k));
1202
+ }
1203
+
1204
+ private async logAudit(event: Partial<AuditEvent>): Promise<void> {
1205
+ if (!this.auditLogger) return;
1206
+
1207
+ const fullEvent: AuditEvent = {
1208
+ timestamp: new Date(),
1209
+ userId: 'system',
1210
+ ipAddress: '0.0.0.0',
1211
+ action: event.action || 'unknown',
1212
+ resourceType: event.resourceType || 'unknown',
1213
+ outcome: event.outcome || 'unknown',
1214
+ ...event
1215
+ };
1216
+
1217
+ try {
1218
+ await this.auditLogger(fullEvent);
1219
+ } catch (error) {
1220
+ console.error('Failed to log audit event:', error);
1221
+ }
1222
+ }
1223
+ }
1224
+
1225
+ // ═══════════════════════════════════════════════════════════════════════════════
1226
+ // AUDIT EVENT TYPE
1227
+ // ═══════════════════════════════════════════════════════════════════════════════
1228
+
1229
+ export interface AuditEvent {
1230
+ timestamp: Date;
1231
+ userId: string;
1232
+ ipAddress: string;
1233
+ action: string;
1234
+ resourceType: string;
1235
+ resourceId?: string;
1236
+ outcome: 'success' | 'failure' | 'unknown';
1237
+ duration?: number;
1238
+ errorMessage?: string;
1239
+ details?: Record<string, any>;
1240
+ }
1241
+
1242
+ // ═══════════════════════════════════════════════════════════════════════════════
1243
+ // EHR VENDOR-SPECIFIC ADAPTERS
1244
+ // ═══════════════════════════════════════════════════════════════════════════════
1245
+
1246
+ export class EpicFHIRClient extends FHIRClient {
1247
+ constructor(config: Omit<FHIRConfig, 'ehrVendor'>) {
1248
+ super({ ...config, ehrVendor: 'epic' });
1249
+ }
1250
+
1251
+ /**
1252
+ * Epic-specific: Get MyChart patient context
1253
+ */
1254
+ async getMyChartContext(launchToken: string): Promise<{
1255
+ patient: string;
1256
+ encounter?: string;
1257
+ practitioner?: string;
1258
+ }> {
1259
+ // Epic SMART launch context parsing
1260
+ const decoded = Buffer.from(launchToken, 'base64').toString();
1261
+ return JSON.parse(decoded);
1262
+ }
1263
+ }
1264
+
1265
+ export class CernerFHIRClient extends FHIRClient {
1266
+ constructor(config: Omit<FHIRConfig, 'ehrVendor'>) {
1267
+ super({ ...config, ehrVendor: 'cerner' });
1268
+ }
1269
+
1270
+ /**
1271
+ * Cerner-specific: Handle Millennium-specific extensions
1272
+ */
1273
+ parseMillenniumExtensions(resource: FHIRResource): Record<string, any> {
1274
+ const extensions: Record<string, any> = {};
1275
+
1276
+ if ('extension' in resource && resource.extension) {
1277
+ for (const ext of resource.extension) {
1278
+ if (ext.url.includes('cerner.com')) {
1279
+ const key = ext.url.split('/').pop() || ext.url;
1280
+ extensions[key] = ext.valueString || ext.valueCode || ext.valueBoolean || ext.valueCoding;
1281
+ }
1282
+ }
1283
+ }
1284
+
1285
+ return extensions;
1286
+ }
1287
+ }
1288
+
1289
+ // ═══════════════════════════════════════════════════════════════════════════════
1290
+ // FACTORY FUNCTION
1291
+ // ═══════════════════════════════════════════════════════════════════════════════
1292
+
1293
+ export function createFHIRClient(config: FHIRConfig): FHIRClient {
1294
+ switch (config.ehrVendor) {
1295
+ case 'epic':
1296
+ return new EpicFHIRClient(config);
1297
+ case 'cerner':
1298
+ return new CernerFHIRClient(config);
1299
+ default:
1300
+ return new FHIRClient(config);
1301
+ }
1302
+ }
1303
+
1304
+ export default FHIRClient;