@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,1365 @@
1
+ /**
2
+ * HIPAA Compliance Layer
3
+ *
4
+ * Provides comprehensive HIPAA compliance features:
5
+ * - Audit logging (access, modification, disclosure)
6
+ * - Patient consent management
7
+ * - Data encryption (at rest and in transit)
8
+ * - Access control and authentication
9
+ * - Break-the-glass emergency access
10
+ * - Minimum necessary standard enforcement
11
+ * - Business Associate Agreement tracking
12
+ *
13
+ * IMPORTANT: This module implements the technical safeguards required by HIPAA.
14
+ * Administrative and physical safeguards must be implemented organizationally.
15
+ */
16
+
17
+ import { createHash, createCipheriv, createDecipheriv, randomBytes, scrypt } from 'crypto';
18
+ import { EventEmitter } from 'events';
19
+
20
+ // ═══════════════════════════════════════════════════════════════════════════════
21
+ // AUDIT LOG TYPES
22
+ // ═══════════════════════════════════════════════════════════════════════════════
23
+
24
+ export interface AuditLogEntry {
25
+ id: string;
26
+ timestamp: Date;
27
+ eventType: AuditEventType;
28
+ action: AuditAction;
29
+ outcome: 'success' | 'failure' | 'error';
30
+
31
+ // Actor information
32
+ actor: {
33
+ userId: string;
34
+ userName?: string;
35
+ role: string;
36
+ organization?: string;
37
+ ipAddress: string;
38
+ userAgent?: string;
39
+ sessionId?: string;
40
+ };
41
+
42
+ // Resource information
43
+ resource: {
44
+ type: 'patient' | 'record' | 'report' | 'order' | 'document' | 'system' | 'configuration';
45
+ id?: string;
46
+ patientId?: string;
47
+ description?: string;
48
+ };
49
+
50
+ // Additional context
51
+ details?: {
52
+ fieldsAccessed?: string[];
53
+ fieldsModified?: string[];
54
+ previousValues?: Record<string, any>;
55
+ newValues?: Record<string, any>;
56
+ query?: string;
57
+ reason?: string;
58
+ emergencyAccess?: boolean;
59
+ consentId?: string;
60
+ };
61
+
62
+ // Security metadata
63
+ security: {
64
+ authMethod: 'password' | 'mfa' | 'sso' | 'certificate' | 'api-key' | 'oauth';
65
+ encryptionUsed: boolean;
66
+ integrityVerified: boolean;
67
+ accessLevel: 'normal' | 'elevated' | 'emergency';
68
+ };
69
+
70
+ // Hash for tamper detection
71
+ entryHash?: string;
72
+ previousEntryHash?: string;
73
+ }
74
+
75
+ export type AuditEventType =
76
+ | 'authentication'
77
+ | 'authorization'
78
+ | 'access'
79
+ | 'modification'
80
+ | 'disclosure'
81
+ | 'deletion'
82
+ | 'export'
83
+ | 'print'
84
+ | 'query'
85
+ | 'emergency-access'
86
+ | 'consent-change'
87
+ | 'system-event';
88
+
89
+ export type AuditAction =
90
+ | 'login' | 'logout' | 'login-failed' | 'session-timeout'
91
+ | 'view' | 'search' | 'download' | 'print' | 'export'
92
+ | 'create' | 'update' | 'delete' | 'restore'
93
+ | 'share' | 'transmit' | 'receive'
94
+ | 'grant-access' | 'revoke-access'
95
+ | 'consent-obtained' | 'consent-revoked'
96
+ | 'emergency-override' | 'break-the-glass';
97
+
98
+ // ═══════════════════════════════════════════════════════════════════════════════
99
+ // CONSENT MANAGEMENT TYPES
100
+ // ═══════════════════════════════════════════════════════════════════════════════
101
+
102
+ export interface PatientConsent {
103
+ id: string;
104
+ patientId: string;
105
+ consentType: ConsentType;
106
+ status: 'active' | 'revoked' | 'expired' | 'pending';
107
+
108
+ scope: {
109
+ dataCategories: DataCategory[];
110
+ purposes: ConsentPurpose[];
111
+ recipients?: string[];
112
+ excludedProviders?: string[];
113
+ excludedData?: string[];
114
+ };
115
+
116
+ validity: {
117
+ effectiveDate: Date;
118
+ expirationDate?: Date;
119
+ autoRenew?: boolean;
120
+ };
121
+
122
+ capture: {
123
+ method: 'written' | 'electronic' | 'verbal' | 'implied';
124
+ documentId?: string;
125
+ witnessName?: string;
126
+ witnessDate?: Date;
127
+ ipAddress?: string;
128
+ deviceInfo?: string;
129
+ };
130
+
131
+ audit: {
132
+ createdAt: Date;
133
+ createdBy: string;
134
+ modifiedAt?: Date;
135
+ modifiedBy?: string;
136
+ revokedAt?: Date;
137
+ revokedBy?: string;
138
+ revocationReason?: string;
139
+ };
140
+ }
141
+
142
+ export type ConsentType =
143
+ | 'treatment' // Consent for treatment
144
+ | 'payment' // Consent for payment processing
145
+ | 'healthcare-ops' // Consent for healthcare operations
146
+ | 'research' // Consent for research use
147
+ | 'marketing' // Consent for marketing communications
148
+ | 'disclosure' // Consent for specific disclosure
149
+ | 'genetic-testing' // Consent for genetic testing
150
+ | 'clinical-trial' // Consent for clinical trial participation
151
+ | 'data-sharing' // Consent for data sharing with third parties
152
+ | 'hie' // Consent for Health Information Exchange
153
+ | 'psychotherapy-notes' // Special consent for psychotherapy notes
154
+ | 'substance-abuse'; // 42 CFR Part 2 consent for substance abuse records
155
+
156
+ export type DataCategory =
157
+ | 'demographics'
158
+ | 'diagnoses'
159
+ | 'medications'
160
+ | 'lab-results'
161
+ | 'imaging'
162
+ | 'procedures'
163
+ | 'genomics'
164
+ | 'mental-health'
165
+ | 'substance-abuse'
166
+ | 'hiv-status'
167
+ | 'sexual-health'
168
+ | 'domestic-violence'
169
+ | 'psychotherapy-notes'
170
+ | 'billing';
171
+
172
+ export type ConsentPurpose =
173
+ | 'treatment'
174
+ | 'payment'
175
+ | 'healthcare-operations'
176
+ | 'research'
177
+ | 'public-health'
178
+ | 'legal'
179
+ | 'insurance'
180
+ | 'marketing'
181
+ | 'fundraising'
182
+ | 'care-coordination'
183
+ | 'quality-improvement';
184
+
185
+ // ═══════════════════════════════════════════════════════════════════════════════
186
+ // ACCESS CONTROL TYPES
187
+ // ═══════════════════════════════════════════════════════════════════════════════
188
+
189
+ export interface AccessPolicy {
190
+ id: string;
191
+ name: string;
192
+ description?: string;
193
+
194
+ subjects: {
195
+ roles?: string[];
196
+ users?: string[];
197
+ organizations?: string[];
198
+ departments?: string[];
199
+ };
200
+
201
+ resources: {
202
+ types?: string[];
203
+ patientIds?: string[];
204
+ dataCategories?: DataCategory[];
205
+ };
206
+
207
+ permissions: {
208
+ actions: ('read' | 'write' | 'delete' | 'share' | 'print' | 'export')[];
209
+ conditions?: AccessCondition[];
210
+ };
211
+
212
+ priority: number;
213
+ enabled: boolean;
214
+ }
215
+
216
+ export interface AccessCondition {
217
+ type: 'time-of-day' | 'ip-range' | 'location' | 'mfa-required' | 'relationship' | 'purpose' | 'emergency-only';
218
+ parameters: Record<string, any>;
219
+ }
220
+
221
+ export interface AccessDecision {
222
+ allowed: boolean;
223
+ policy?: string;
224
+ reason: string;
225
+ conditions?: string[];
226
+ requiresElevation?: boolean;
227
+ auditRequired: boolean;
228
+ }
229
+
230
+ // ═══════════════════════════════════════════════════════════════════════════════
231
+ // ENCRYPTION TYPES
232
+ // ═══════════════════════════════════════════════════════════════════════════════
233
+
234
+ export interface EncryptionConfig {
235
+ algorithm: 'aes-256-gcm' | 'aes-256-cbc' | 'chacha20-poly1305';
236
+ keyDerivation: 'scrypt' | 'pbkdf2' | 'argon2';
237
+ keyRotationDays: number;
238
+ fieldLevelEncryption: boolean;
239
+ encryptedFields?: string[];
240
+ }
241
+
242
+ export interface EncryptedData {
243
+ ciphertext: string;
244
+ iv: string;
245
+ authTag?: string;
246
+ keyId: string;
247
+ algorithm: string;
248
+ encryptedAt: Date;
249
+ }
250
+
251
+ export interface DataMaskingConfig {
252
+ ssnPattern: 'full' | 'last4' | 'hidden';
253
+ dobPattern: 'full' | 'year-only' | 'age-only' | 'hidden';
254
+ phonePattern: 'full' | 'last4' | 'hidden';
255
+ addressPattern: 'full' | 'city-state' | 'zip-only' | 'hidden';
256
+ mrnPattern: 'full' | 'last4' | 'hidden';
257
+ genomicPattern: 'full' | 'summary' | 'hidden';
258
+ }
259
+
260
+ // ═══════════════════════════════════════════════════════════════════════════════
261
+ // HIPAA COMPLIANCE SERVICE
262
+ // ═══════════════════════════════════════════════════════════════════════════════
263
+
264
+ export class HIPAAComplianceService extends EventEmitter {
265
+ private auditStore: AuditLogEntry[] = [];
266
+ private consents: Map<string, PatientConsent[]> = new Map();
267
+ private accessPolicies: AccessPolicy[] = [];
268
+ private encryptionKeys: Map<string, { key: Buffer; createdAt: Date; active: boolean }> = new Map();
269
+ private activeKeyId: string;
270
+ private config: {
271
+ encryption: EncryptionConfig;
272
+ masking: DataMaskingConfig;
273
+ auditRetentionDays: number;
274
+ emergencyAccessEnabled: boolean;
275
+ };
276
+ private lastEntryHash?: string;
277
+
278
+ constructor(config?: Partial<{
279
+ encryption: Partial<EncryptionConfig>;
280
+ masking: Partial<DataMaskingConfig>;
281
+ auditRetentionDays: number;
282
+ emergencyAccessEnabled: boolean;
283
+ }>) {
284
+ super();
285
+
286
+ this.config = {
287
+ encryption: {
288
+ algorithm: 'aes-256-gcm',
289
+ keyDerivation: 'scrypt',
290
+ keyRotationDays: 90,
291
+ fieldLevelEncryption: true,
292
+ encryptedFields: ['ssn', 'dob', 'address', 'phone', 'genomics', 'mentalHealth'],
293
+ ...config?.encryption
294
+ },
295
+ masking: {
296
+ ssnPattern: 'last4',
297
+ dobPattern: 'age-only',
298
+ phonePattern: 'last4',
299
+ addressPattern: 'city-state',
300
+ mrnPattern: 'full',
301
+ genomicPattern: 'summary',
302
+ ...config?.masking
303
+ },
304
+ auditRetentionDays: config?.auditRetentionDays || 2190, // 6 years per HIPAA
305
+ emergencyAccessEnabled: config?.emergencyAccessEnabled ?? true
306
+ };
307
+
308
+ // Initialize encryption key
309
+ this.activeKeyId = this.generateKeyId();
310
+ this.initializeEncryptionKey();
311
+ }
312
+
313
+ // ═══════════════════════════════════════════════════════════════════════════════
314
+ // AUDIT LOGGING
315
+ // ═══════════════════════════════════════════════════════════════════════════════
316
+
317
+ /**
318
+ * Log an audit event
319
+ */
320
+ async logAuditEvent(entry: Omit<AuditLogEntry, 'id' | 'timestamp' | 'entryHash' | 'previousEntryHash'>): Promise<string> {
321
+ const id = this.generateAuditId();
322
+ const timestamp = new Date();
323
+
324
+ // Create the entry
325
+ const fullEntry: AuditLogEntry = {
326
+ id,
327
+ timestamp,
328
+ ...entry,
329
+ previousEntryHash: this.lastEntryHash
330
+ };
331
+
332
+ // Calculate hash for tamper detection
333
+ fullEntry.entryHash = this.calculateEntryHash(fullEntry);
334
+ this.lastEntryHash = fullEntry.entryHash;
335
+
336
+ // Store the entry
337
+ this.auditStore.push(fullEntry);
338
+
339
+ // Emit event for external handlers
340
+ this.emit('audit-event', fullEntry);
341
+
342
+ // Check for suspicious patterns
343
+ await this.detectSuspiciousActivity(fullEntry);
344
+
345
+ return id;
346
+ }
347
+
348
+ /**
349
+ * Log PHI access event
350
+ */
351
+ async logPHIAccess(params: {
352
+ userId: string;
353
+ userName?: string;
354
+ role: string;
355
+ ipAddress: string;
356
+ patientId: string;
357
+ action: 'view' | 'download' | 'print' | 'export';
358
+ resourceType: AuditLogEntry['resource']['type'];
359
+ resourceId?: string;
360
+ fieldsAccessed?: string[];
361
+ reason?: string;
362
+ emergencyAccess?: boolean;
363
+ consentId?: string;
364
+ }): Promise<string> {
365
+ return this.logAuditEvent({
366
+ eventType: params.emergencyAccess ? 'emergency-access' : 'access',
367
+ action: params.action,
368
+ outcome: 'success',
369
+ actor: {
370
+ userId: params.userId,
371
+ userName: params.userName,
372
+ role: params.role,
373
+ ipAddress: params.ipAddress
374
+ },
375
+ resource: {
376
+ type: params.resourceType,
377
+ id: params.resourceId,
378
+ patientId: params.patientId
379
+ },
380
+ details: {
381
+ fieldsAccessed: params.fieldsAccessed,
382
+ reason: params.reason,
383
+ emergencyAccess: params.emergencyAccess,
384
+ consentId: params.consentId
385
+ },
386
+ security: {
387
+ authMethod: 'mfa', // Assuming MFA for PHI access
388
+ encryptionUsed: true,
389
+ integrityVerified: true,
390
+ accessLevel: params.emergencyAccess ? 'emergency' : 'normal'
391
+ }
392
+ });
393
+ }
394
+
395
+ /**
396
+ * Log PHI modification event
397
+ */
398
+ async logPHIModification(params: {
399
+ userId: string;
400
+ role: string;
401
+ ipAddress: string;
402
+ patientId: string;
403
+ action: 'create' | 'update' | 'delete';
404
+ resourceType: AuditLogEntry['resource']['type'];
405
+ resourceId?: string;
406
+ fieldsModified?: string[];
407
+ previousValues?: Record<string, any>;
408
+ newValues?: Record<string, any>;
409
+ reason?: string;
410
+ }): Promise<string> {
411
+ return this.logAuditEvent({
412
+ eventType: 'modification',
413
+ action: params.action,
414
+ outcome: 'success',
415
+ actor: {
416
+ userId: params.userId,
417
+ role: params.role,
418
+ ipAddress: params.ipAddress
419
+ },
420
+ resource: {
421
+ type: params.resourceType,
422
+ id: params.resourceId,
423
+ patientId: params.patientId
424
+ },
425
+ details: {
426
+ fieldsModified: params.fieldsModified,
427
+ previousValues: params.previousValues,
428
+ newValues: params.newValues,
429
+ reason: params.reason
430
+ },
431
+ security: {
432
+ authMethod: 'mfa',
433
+ encryptionUsed: true,
434
+ integrityVerified: true,
435
+ accessLevel: 'normal'
436
+ }
437
+ });
438
+ }
439
+
440
+ /**
441
+ * Log authentication event
442
+ */
443
+ async logAuthentication(params: {
444
+ userId: string;
445
+ ipAddress: string;
446
+ action: 'login' | 'logout' | 'login-failed' | 'session-timeout';
447
+ authMethod: AuditLogEntry['security']['authMethod'];
448
+ success: boolean;
449
+ failureReason?: string;
450
+ }): Promise<string> {
451
+ return this.logAuditEvent({
452
+ eventType: 'authentication',
453
+ action: params.action,
454
+ outcome: params.success ? 'success' : 'failure',
455
+ actor: {
456
+ userId: params.userId,
457
+ role: 'unknown',
458
+ ipAddress: params.ipAddress
459
+ },
460
+ resource: {
461
+ type: 'system'
462
+ },
463
+ details: params.failureReason ? { reason: params.failureReason } : undefined,
464
+ security: {
465
+ authMethod: params.authMethod,
466
+ encryptionUsed: true,
467
+ integrityVerified: true,
468
+ accessLevel: 'normal'
469
+ }
470
+ });
471
+ }
472
+
473
+ /**
474
+ * Query audit logs
475
+ */
476
+ queryAuditLogs(params: {
477
+ startDate?: Date;
478
+ endDate?: Date;
479
+ patientId?: string;
480
+ userId?: string;
481
+ eventType?: AuditEventType;
482
+ action?: AuditAction;
483
+ outcome?: 'success' | 'failure';
484
+ limit?: number;
485
+ offset?: number;
486
+ }): { entries: AuditLogEntry[]; total: number } {
487
+ let filtered = [...this.auditStore];
488
+
489
+ if (params.startDate) {
490
+ filtered = filtered.filter(e => e.timestamp >= params.startDate!);
491
+ }
492
+ if (params.endDate) {
493
+ filtered = filtered.filter(e => e.timestamp <= params.endDate!);
494
+ }
495
+ if (params.patientId) {
496
+ filtered = filtered.filter(e => e.resource.patientId === params.patientId);
497
+ }
498
+ if (params.userId) {
499
+ filtered = filtered.filter(e => e.actor.userId === params.userId);
500
+ }
501
+ if (params.eventType) {
502
+ filtered = filtered.filter(e => e.eventType === params.eventType);
503
+ }
504
+ if (params.action) {
505
+ filtered = filtered.filter(e => e.action === params.action);
506
+ }
507
+ if (params.outcome) {
508
+ filtered = filtered.filter(e => e.outcome === params.outcome);
509
+ }
510
+
511
+ // Sort by timestamp descending
512
+ filtered.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
513
+
514
+ const total = filtered.length;
515
+ const offset = params.offset || 0;
516
+ const limit = params.limit || 100;
517
+
518
+ return {
519
+ entries: filtered.slice(offset, offset + limit),
520
+ total
521
+ };
522
+ }
523
+
524
+ /**
525
+ * Get accounting of disclosures for a patient (HIPAA requirement)
526
+ */
527
+ getAccountingOfDisclosures(patientId: string, startDate?: Date, endDate?: Date): AuditLogEntry[] {
528
+ return this.auditStore.filter(entry =>
529
+ entry.resource.patientId === patientId &&
530
+ entry.eventType === 'disclosure' &&
531
+ (!startDate || entry.timestamp >= startDate) &&
532
+ (!endDate || entry.timestamp <= endDate)
533
+ ).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
534
+ }
535
+
536
+ /**
537
+ * Verify audit log integrity
538
+ */
539
+ verifyAuditLogIntegrity(): { valid: boolean; errors: string[] } {
540
+ const errors: string[] = [];
541
+
542
+ for (let i = 0; i < this.auditStore.length; i++) {
543
+ const entry = this.auditStore[i];
544
+
545
+ // Verify hash
546
+ const { entryHash: _hash, ...entryWithoutHash } = entry;
547
+ const calculatedHash = this.calculateEntryHash(entryWithoutHash);
548
+
549
+ if (calculatedHash !== entry.entryHash) {
550
+ errors.push(`Entry ${entry.id} has been tampered with (hash mismatch)`);
551
+ }
552
+
553
+ // Verify chain
554
+ if (i > 0 && entry.previousEntryHash !== this.auditStore[i - 1].entryHash) {
555
+ errors.push(`Entry ${entry.id} has broken chain link`);
556
+ }
557
+ }
558
+
559
+ return {
560
+ valid: errors.length === 0,
561
+ errors
562
+ };
563
+ }
564
+
565
+ // ═══════════════════════════════════════════════════════════════════════════════
566
+ // CONSENT MANAGEMENT
567
+ // ═══════════════════════════════════════════════════════════════════════════════
568
+
569
+ /**
570
+ * Record a patient consent
571
+ */
572
+ async recordConsent(consent: Omit<PatientConsent, 'id' | 'audit'>): Promise<string> {
573
+ const id = this.generateConsentId();
574
+ const now = new Date();
575
+
576
+ const fullConsent: PatientConsent = {
577
+ id,
578
+ ...consent,
579
+ audit: {
580
+ createdAt: now,
581
+ createdBy: consent.capture.method === 'electronic' ? 'system' : 'staff'
582
+ }
583
+ };
584
+
585
+ // Store consent
586
+ const patientConsents = this.consents.get(consent.patientId) || [];
587
+ patientConsents.push(fullConsent);
588
+ this.consents.set(consent.patientId, patientConsents);
589
+
590
+ // Log the consent capture
591
+ await this.logAuditEvent({
592
+ eventType: 'consent-change',
593
+ action: 'consent-obtained',
594
+ outcome: 'success',
595
+ actor: {
596
+ userId: fullConsent.audit.createdBy,
597
+ role: 'system',
598
+ ipAddress: consent.capture.ipAddress || 'unknown'
599
+ },
600
+ resource: {
601
+ type: 'patient',
602
+ patientId: consent.patientId,
603
+ description: `${consent.consentType} consent`
604
+ },
605
+ details: {
606
+ consentId: id
607
+ },
608
+ security: {
609
+ authMethod: 'password',
610
+ encryptionUsed: true,
611
+ integrityVerified: true,
612
+ accessLevel: 'normal'
613
+ }
614
+ });
615
+
616
+ this.emit('consent-recorded', fullConsent);
617
+
618
+ return id;
619
+ }
620
+
621
+ /**
622
+ * Revoke a patient consent
623
+ */
624
+ async revokeConsent(consentId: string, revokedBy: string, reason?: string): Promise<boolean> {
625
+ for (const [patientId, consents] of this.consents) {
626
+ const consent = consents.find(c => c.id === consentId);
627
+ if (consent) {
628
+ consent.status = 'revoked';
629
+ consent.audit.revokedAt = new Date();
630
+ consent.audit.revokedBy = revokedBy;
631
+ consent.audit.revocationReason = reason;
632
+
633
+ // Log the revocation
634
+ await this.logAuditEvent({
635
+ eventType: 'consent-change',
636
+ action: 'consent-revoked',
637
+ outcome: 'success',
638
+ actor: {
639
+ userId: revokedBy,
640
+ role: 'patient',
641
+ ipAddress: 'unknown'
642
+ },
643
+ resource: {
644
+ type: 'patient',
645
+ patientId,
646
+ description: `${consent.consentType} consent revoked`
647
+ },
648
+ details: {
649
+ consentId,
650
+ reason
651
+ },
652
+ security: {
653
+ authMethod: 'password',
654
+ encryptionUsed: true,
655
+ integrityVerified: true,
656
+ accessLevel: 'normal'
657
+ }
658
+ });
659
+
660
+ this.emit('consent-revoked', consent);
661
+
662
+ return true;
663
+ }
664
+ }
665
+ return false;
666
+ }
667
+
668
+ /**
669
+ * Check if a patient has consented to a specific use
670
+ */
671
+ checkConsent(patientId: string, purpose: ConsentPurpose, dataCategory?: DataCategory): {
672
+ hasConsent: boolean;
673
+ consentId?: string;
674
+ restrictions?: string[];
675
+ } {
676
+ const patientConsents = this.consents.get(patientId) || [];
677
+
678
+ // Find active consent that covers the purpose
679
+ const validConsent = patientConsents.find(c =>
680
+ c.status === 'active' &&
681
+ c.scope.purposes.includes(purpose) &&
682
+ (!dataCategory || c.scope.dataCategories.includes(dataCategory)) &&
683
+ (!c.validity.expirationDate || c.validity.expirationDate > new Date())
684
+ );
685
+
686
+ if (validConsent) {
687
+ return {
688
+ hasConsent: true,
689
+ consentId: validConsent.id,
690
+ restrictions: validConsent.scope.excludedData
691
+ };
692
+ }
693
+
694
+ // Check if TPO (Treatment, Payment, Operations) - may not require explicit consent
695
+ if (['treatment', 'payment', 'healthcare-operations'].includes(purpose)) {
696
+ // TPO typically covered by Notice of Privacy Practices
697
+ return {
698
+ hasConsent: true,
699
+ restrictions: []
700
+ };
701
+ }
702
+
703
+ return {
704
+ hasConsent: false
705
+ };
706
+ }
707
+
708
+ /**
709
+ * Get all consents for a patient
710
+ */
711
+ getPatientConsents(patientId: string): PatientConsent[] {
712
+ return this.consents.get(patientId) || [];
713
+ }
714
+
715
+ // ═══════════════════════════════════════════════════════════════════════════════
716
+ // ACCESS CONTROL
717
+ // ═══════════════════════════════════════════════════════════════════════════════
718
+
719
+ /**
720
+ * Add an access policy
721
+ */
722
+ addAccessPolicy(policy: Omit<AccessPolicy, 'id'>): string {
723
+ const id = this.generatePolicyId();
724
+ const fullPolicy: AccessPolicy = { id, ...policy };
725
+ this.accessPolicies.push(fullPolicy);
726
+ this.accessPolicies.sort((a, b) => b.priority - a.priority);
727
+ return id;
728
+ }
729
+
730
+ /**
731
+ * Check if access should be allowed
732
+ */
733
+ checkAccess(request: {
734
+ userId: string;
735
+ role: string;
736
+ organization?: string;
737
+ action: 'read' | 'write' | 'delete' | 'share' | 'print' | 'export';
738
+ resourceType: string;
739
+ patientId?: string;
740
+ dataCategory?: DataCategory;
741
+ purpose?: ConsentPurpose;
742
+ ipAddress?: string;
743
+ time?: Date;
744
+ emergencyAccess?: boolean;
745
+ }): AccessDecision {
746
+ const time = request.time || new Date();
747
+
748
+ // Emergency access bypass (break-the-glass)
749
+ if (request.emergencyAccess && this.config.emergencyAccessEnabled) {
750
+ return {
751
+ allowed: true,
752
+ reason: 'Emergency access granted (break-the-glass)',
753
+ conditions: ['Must document emergency reason', 'Subject to post-hoc review'],
754
+ auditRequired: true
755
+ };
756
+ }
757
+
758
+ // Check consent if accessing patient data
759
+ if (request.patientId && request.purpose) {
760
+ const consentCheck = this.checkConsent(
761
+ request.patientId,
762
+ request.purpose,
763
+ request.dataCategory
764
+ );
765
+
766
+ if (!consentCheck.hasConsent) {
767
+ return {
768
+ allowed: false,
769
+ reason: `No consent for ${request.purpose} purpose`,
770
+ auditRequired: true
771
+ };
772
+ }
773
+ }
774
+
775
+ // Evaluate policies in priority order
776
+ for (const policy of this.accessPolicies) {
777
+ if (!policy.enabled) continue;
778
+
779
+ // Check if policy applies to this subject
780
+ const subjectMatch =
781
+ (!policy.subjects.roles || policy.subjects.roles.includes(request.role)) &&
782
+ (!policy.subjects.users || policy.subjects.users.includes(request.userId)) &&
783
+ (!policy.subjects.organizations || policy.subjects.organizations.includes(request.organization || ''));
784
+
785
+ if (!subjectMatch) continue;
786
+
787
+ // Check if policy applies to this resource
788
+ const resourceMatch =
789
+ (!policy.resources.types || policy.resources.types.includes(request.resourceType)) &&
790
+ (!policy.resources.patientIds || !request.patientId || policy.resources.patientIds.includes(request.patientId)) &&
791
+ (!policy.resources.dataCategories || !request.dataCategory || policy.resources.dataCategories.includes(request.dataCategory));
792
+
793
+ if (!resourceMatch) continue;
794
+
795
+ // Check if action is permitted
796
+ if (!policy.permissions.actions.includes(request.action)) {
797
+ continue;
798
+ }
799
+
800
+ // Evaluate conditions
801
+ const conditionResults = this.evaluateConditions(policy.permissions.conditions || [], request, time);
802
+
803
+ if (conditionResults.allMet) {
804
+ return {
805
+ allowed: true,
806
+ policy: policy.id,
807
+ reason: `Access granted by policy: ${policy.name}`,
808
+ conditions: conditionResults.messages,
809
+ auditRequired: true
810
+ };
811
+ } else if (conditionResults.requiresElevation) {
812
+ return {
813
+ allowed: false,
814
+ policy: policy.id,
815
+ reason: 'Access requires additional verification',
816
+ requiresElevation: true,
817
+ conditions: conditionResults.messages,
818
+ auditRequired: true
819
+ };
820
+ }
821
+ }
822
+
823
+ // Default deny
824
+ return {
825
+ allowed: false,
826
+ reason: 'No matching policy found - access denied by default',
827
+ auditRequired: true
828
+ };
829
+ }
830
+
831
+ private evaluateConditions(
832
+ conditions: AccessCondition[],
833
+ request: any,
834
+ time: Date
835
+ ): { allMet: boolean; requiresElevation: boolean; messages: string[] } {
836
+ const messages: string[] = [];
837
+ let requiresElevation = false;
838
+
839
+ for (const condition of conditions) {
840
+ switch (condition.type) {
841
+ case 'time-of-day':
842
+ const hour = time.getHours();
843
+ const startHour = condition.parameters.startHour || 0;
844
+ const endHour = condition.parameters.endHour || 24;
845
+ if (hour < startHour || hour >= endHour) {
846
+ messages.push(`Access outside permitted hours (${startHour}:00 - ${endHour}:00)`);
847
+ return { allMet: false, requiresElevation: false, messages };
848
+ }
849
+ break;
850
+
851
+ case 'ip-range':
852
+ // Simplified IP check - would need proper CIDR matching in production
853
+ const allowedRanges = condition.parameters.ranges as string[];
854
+ if (request.ipAddress && !allowedRanges.some(r => request.ipAddress.startsWith(r))) {
855
+ messages.push('Access from unauthorized network location');
856
+ return { allMet: false, requiresElevation: false, messages };
857
+ }
858
+ break;
859
+
860
+ case 'mfa-required':
861
+ messages.push('Multi-factor authentication required');
862
+ requiresElevation = true;
863
+ break;
864
+
865
+ case 'purpose':
866
+ const allowedPurposes = condition.parameters.purposes as ConsentPurpose[];
867
+ if (!request.purpose || !allowedPurposes.includes(request.purpose)) {
868
+ messages.push(`Purpose '${request.purpose}' not permitted`);
869
+ return { allMet: false, requiresElevation: false, messages };
870
+ }
871
+ break;
872
+
873
+ case 'emergency-only':
874
+ if (!request.emergencyAccess) {
875
+ messages.push('Access restricted to emergency situations');
876
+ return { allMet: false, requiresElevation: false, messages };
877
+ }
878
+ break;
879
+ }
880
+ }
881
+
882
+ return {
883
+ allMet: !requiresElevation,
884
+ requiresElevation,
885
+ messages
886
+ };
887
+ }
888
+
889
+ // ═══════════════════════════════════════════════════════════════════════════════
890
+ // ENCRYPTION
891
+ // ═══════════════════════════════════════════════════════════════════════════════
892
+
893
+ /**
894
+ * Encrypt sensitive data
895
+ */
896
+ async encryptData(data: string | Buffer, context?: string): Promise<EncryptedData> {
897
+ const keyEntry = this.encryptionKeys.get(this.activeKeyId);
898
+ if (!keyEntry) {
899
+ throw new Error('No active encryption key');
900
+ }
901
+
902
+ const iv = randomBytes(16);
903
+ const algorithm = this.config.encryption.algorithm;
904
+
905
+ if (algorithm === 'aes-256-gcm') {
906
+ const cipher = createCipheriv('aes-256-gcm', keyEntry.key, iv);
907
+ if (context) {
908
+ cipher.setAAD(Buffer.from(context));
909
+ }
910
+
911
+ const dataBuffer = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
912
+ const encrypted = Buffer.concat([cipher.update(dataBuffer), cipher.final()]);
913
+ const authTag = cipher.getAuthTag();
914
+
915
+ return {
916
+ ciphertext: encrypted.toString('base64'),
917
+ iv: iv.toString('base64'),
918
+ authTag: authTag.toString('base64'),
919
+ keyId: this.activeKeyId,
920
+ algorithm,
921
+ encryptedAt: new Date()
922
+ };
923
+ } else {
924
+ // AES-256-CBC fallback
925
+ const cipher = createCipheriv('aes-256-cbc', keyEntry.key, iv);
926
+ const dataBuffer = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
927
+ const encrypted = Buffer.concat([cipher.update(dataBuffer), cipher.final()]);
928
+
929
+ return {
930
+ ciphertext: encrypted.toString('base64'),
931
+ iv: iv.toString('base64'),
932
+ keyId: this.activeKeyId,
933
+ algorithm: 'aes-256-cbc',
934
+ encryptedAt: new Date()
935
+ };
936
+ }
937
+ }
938
+
939
+ /**
940
+ * Decrypt data
941
+ */
942
+ async decryptData(encrypted: EncryptedData, context?: string): Promise<Buffer> {
943
+ const keyEntry = this.encryptionKeys.get(encrypted.keyId);
944
+ if (!keyEntry) {
945
+ throw new Error(`Encryption key ${encrypted.keyId} not found`);
946
+ }
947
+
948
+ const iv = Buffer.from(encrypted.iv, 'base64');
949
+ const ciphertext = Buffer.from(encrypted.ciphertext, 'base64');
950
+
951
+ if (encrypted.algorithm === 'aes-256-gcm') {
952
+ const decipher = createDecipheriv('aes-256-gcm', keyEntry.key, iv);
953
+ if (encrypted.authTag) {
954
+ decipher.setAuthTag(Buffer.from(encrypted.authTag, 'base64'));
955
+ }
956
+ if (context) {
957
+ decipher.setAAD(Buffer.from(context));
958
+ }
959
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
960
+ } else {
961
+ const decipher = createDecipheriv('aes-256-cbc', keyEntry.key, iv);
962
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
963
+ }
964
+ }
965
+
966
+ /**
967
+ * Encrypt specific PHI fields in an object
968
+ */
969
+ async encryptPHI<T extends Record<string, any>>(data: T): Promise<T & { _encrypted: Record<string, EncryptedData> }> {
970
+ const encryptedFields: Record<string, EncryptedData> = {};
971
+ const result = { ...data } as T & { _encrypted: Record<string, EncryptedData> };
972
+
973
+ for (const field of this.config.encryption.encryptedFields || []) {
974
+ if (data[field] !== undefined) {
975
+ encryptedFields[field] = await this.encryptData(JSON.stringify(data[field]), field);
976
+ (result as any)[field] = '[ENCRYPTED]';
977
+ }
978
+ }
979
+
980
+ result._encrypted = encryptedFields;
981
+ return result;
982
+ }
983
+
984
+ /**
985
+ * Decrypt PHI fields in an object
986
+ */
987
+ async decryptPHI<T extends Record<string, any>>(data: T & { _encrypted?: Record<string, EncryptedData> }): Promise<T> {
988
+ if (!data._encrypted) return data;
989
+
990
+ const result = { ...data };
991
+ for (const [field, encrypted] of Object.entries(data._encrypted)) {
992
+ const decrypted = await this.decryptData(encrypted, field);
993
+ (result as any)[field] = JSON.parse(decrypted.toString('utf8'));
994
+ }
995
+
996
+ delete (result as any)._encrypted;
997
+ return result;
998
+ }
999
+
1000
+ /**
1001
+ * Rotate encryption keys
1002
+ */
1003
+ async rotateEncryptionKey(): Promise<string> {
1004
+ // Mark current key as inactive
1005
+ const currentKey = this.encryptionKeys.get(this.activeKeyId);
1006
+ if (currentKey) {
1007
+ currentKey.active = false;
1008
+ }
1009
+
1010
+ // Generate new key
1011
+ const newKeyId = this.generateKeyId();
1012
+ this.initializeEncryptionKey(newKeyId);
1013
+ this.activeKeyId = newKeyId;
1014
+
1015
+ // Log key rotation
1016
+ await this.logAuditEvent({
1017
+ eventType: 'system-event',
1018
+ action: 'update',
1019
+ outcome: 'success',
1020
+ actor: {
1021
+ userId: 'system',
1022
+ role: 'system',
1023
+ ipAddress: 'localhost'
1024
+ },
1025
+ resource: {
1026
+ type: 'configuration',
1027
+ description: 'Encryption key rotation'
1028
+ },
1029
+ details: {
1030
+ previousValues: { keyId: currentKey ? this.activeKeyId : 'none' },
1031
+ newValues: { keyId: newKeyId }
1032
+ },
1033
+ security: {
1034
+ authMethod: 'certificate',
1035
+ encryptionUsed: true,
1036
+ integrityVerified: true,
1037
+ accessLevel: 'elevated'
1038
+ }
1039
+ });
1040
+
1041
+ return newKeyId;
1042
+ }
1043
+
1044
+ // ═══════════════════════════════════════════════════════════════════════════════
1045
+ // DATA MASKING (Minimum Necessary)
1046
+ // ═══════════════════════════════════════════════════════════════════════════════
1047
+
1048
+ /**
1049
+ * Mask PHI according to minimum necessary standard
1050
+ */
1051
+ maskPHI(data: Record<string, any>, accessLevel: 'full' | 'clinical' | 'billing' | 'research' | 'minimal'): Record<string, any> {
1052
+ const masked = { ...data };
1053
+
1054
+ // Determine masking rules based on access level
1055
+ const maskingRules = this.getMaskingRulesForLevel(accessLevel);
1056
+
1057
+ // Apply masking
1058
+ if (masked.ssn && maskingRules.ssnPattern !== 'full') {
1059
+ masked.ssn = this.maskSSN(masked.ssn, maskingRules.ssnPattern);
1060
+ }
1061
+ if (masked.dob && maskingRules.dobPattern !== 'full') {
1062
+ masked.dob = this.maskDOB(masked.dob, maskingRules.dobPattern);
1063
+ }
1064
+ if (masked.phone && maskingRules.phonePattern !== 'full') {
1065
+ masked.phone = this.maskPhone(masked.phone, maskingRules.phonePattern);
1066
+ }
1067
+ if (masked.address && maskingRules.addressPattern !== 'full') {
1068
+ masked.address = this.maskAddress(masked.address, maskingRules.addressPattern);
1069
+ }
1070
+ if (masked.mrn && maskingRules.mrnPattern !== 'full') {
1071
+ masked.mrn = this.maskMRN(masked.mrn, maskingRules.mrnPattern);
1072
+ }
1073
+ if (masked.genomics && maskingRules.genomicPattern !== 'full') {
1074
+ masked.genomics = this.maskGenomics(masked.genomics, maskingRules.genomicPattern);
1075
+ }
1076
+
1077
+ return masked;
1078
+ }
1079
+
1080
+ private getMaskingRulesForLevel(level: string): DataMaskingConfig {
1081
+ switch (level) {
1082
+ case 'full':
1083
+ return {
1084
+ ssnPattern: 'full',
1085
+ dobPattern: 'full',
1086
+ phonePattern: 'full',
1087
+ addressPattern: 'full',
1088
+ mrnPattern: 'full',
1089
+ genomicPattern: 'full'
1090
+ };
1091
+ case 'clinical':
1092
+ return {
1093
+ ssnPattern: 'last4',
1094
+ dobPattern: 'full',
1095
+ phonePattern: 'full',
1096
+ addressPattern: 'full',
1097
+ mrnPattern: 'full',
1098
+ genomicPattern: 'full'
1099
+ };
1100
+ case 'billing':
1101
+ return {
1102
+ ssnPattern: 'last4',
1103
+ dobPattern: 'full',
1104
+ phonePattern: 'last4',
1105
+ addressPattern: 'full',
1106
+ mrnPattern: 'full',
1107
+ genomicPattern: 'hidden'
1108
+ };
1109
+ case 'research':
1110
+ return {
1111
+ ssnPattern: 'hidden',
1112
+ dobPattern: 'year-only',
1113
+ phonePattern: 'hidden',
1114
+ addressPattern: 'zip-only',
1115
+ mrnPattern: 'hidden',
1116
+ genomicPattern: 'summary'
1117
+ };
1118
+ case 'minimal':
1119
+ default:
1120
+ return {
1121
+ ssnPattern: 'hidden',
1122
+ dobPattern: 'age-only',
1123
+ phonePattern: 'hidden',
1124
+ addressPattern: 'hidden',
1125
+ mrnPattern: 'hidden',
1126
+ genomicPattern: 'hidden'
1127
+ };
1128
+ }
1129
+ }
1130
+
1131
+ private maskSSN(ssn: string, pattern: DataMaskingConfig['ssnPattern']): string {
1132
+ const cleaned = ssn.replace(/\D/g, '');
1133
+ switch (pattern) {
1134
+ case 'last4':
1135
+ return `***-**-${cleaned.slice(-4)}`;
1136
+ case 'hidden':
1137
+ return '***-**-****';
1138
+ default:
1139
+ return ssn;
1140
+ }
1141
+ }
1142
+
1143
+ private maskDOB(dob: string | Date, pattern: DataMaskingConfig['dobPattern']): string {
1144
+ const date = typeof dob === 'string' ? new Date(dob) : dob;
1145
+ switch (pattern) {
1146
+ case 'year-only':
1147
+ return date.getFullYear().toString();
1148
+ case 'age-only':
1149
+ const age = Math.floor((Date.now() - date.getTime()) / (365.25 * 24 * 60 * 60 * 1000));
1150
+ return `Age: ${age}`;
1151
+ case 'hidden':
1152
+ return '[REDACTED]';
1153
+ default:
1154
+ return typeof dob === 'string' ? dob : date.toISOString().split('T')[0];
1155
+ }
1156
+ }
1157
+
1158
+ private maskPhone(phone: string, pattern: DataMaskingConfig['phonePattern']): string {
1159
+ const cleaned = phone.replace(/\D/g, '');
1160
+ switch (pattern) {
1161
+ case 'last4':
1162
+ return `(***) ***-${cleaned.slice(-4)}`;
1163
+ case 'hidden':
1164
+ return '(***) ***-****';
1165
+ default:
1166
+ return phone;
1167
+ }
1168
+ }
1169
+
1170
+ private maskAddress(address: any, pattern: DataMaskingConfig['addressPattern']): any {
1171
+ if (typeof address === 'string') {
1172
+ switch (pattern) {
1173
+ case 'city-state':
1174
+ return '[City, State]';
1175
+ case 'zip-only':
1176
+ const zipMatch = address.match(/\d{5}(-\d{4})?/);
1177
+ return zipMatch ? zipMatch[0] : '[ZIP]';
1178
+ case 'hidden':
1179
+ return '[REDACTED]';
1180
+ default:
1181
+ return address;
1182
+ }
1183
+ }
1184
+
1185
+ // Object address
1186
+ switch (pattern) {
1187
+ case 'city-state':
1188
+ return { city: address.city, state: address.state };
1189
+ case 'zip-only':
1190
+ return { zip: address.zip || address.postalCode };
1191
+ case 'hidden':
1192
+ return '[REDACTED]';
1193
+ default:
1194
+ return address;
1195
+ }
1196
+ }
1197
+
1198
+ private maskMRN(mrn: string, pattern: DataMaskingConfig['mrnPattern']): string {
1199
+ switch (pattern) {
1200
+ case 'last4':
1201
+ return `****${mrn.slice(-4)}`;
1202
+ case 'hidden':
1203
+ return '[REDACTED]';
1204
+ default:
1205
+ return mrn;
1206
+ }
1207
+ }
1208
+
1209
+ private maskGenomics(genomics: any, pattern: DataMaskingConfig['genomicPattern']): any {
1210
+ switch (pattern) {
1211
+ case 'summary':
1212
+ if (Array.isArray(genomics)) {
1213
+ return { count: genomics.length, summary: 'Genomic data available' };
1214
+ }
1215
+ return { summary: 'Genomic data available' };
1216
+ case 'hidden':
1217
+ return '[GENOMIC DATA REDACTED]';
1218
+ default:
1219
+ return genomics;
1220
+ }
1221
+ }
1222
+
1223
+ // ═══════════════════════════════════════════════════════════════════════════════
1224
+ // HELPER METHODS
1225
+ // ═══════════════════════════════════════════════════════════════════════════════
1226
+
1227
+ private generateAuditId(): string {
1228
+ return `AUD-${Date.now()}-${randomBytes(4).toString('hex')}`;
1229
+ }
1230
+
1231
+ private generateConsentId(): string {
1232
+ return `CON-${Date.now()}-${randomBytes(4).toString('hex')}`;
1233
+ }
1234
+
1235
+ private generatePolicyId(): string {
1236
+ return `POL-${Date.now()}-${randomBytes(4).toString('hex')}`;
1237
+ }
1238
+
1239
+ private generateKeyId(): string {
1240
+ return `KEY-${Date.now()}-${randomBytes(4).toString('hex')}`;
1241
+ }
1242
+
1243
+ private async initializeEncryptionKey(keyId?: string): Promise<void> {
1244
+ const id = keyId || this.activeKeyId;
1245
+
1246
+ // In production, this would use a proper key management system (KMS)
1247
+ // For now, derive from a master secret
1248
+ const masterSecret = process.env.HIPAA_MASTER_KEY || 'DEFAULT_DEV_KEY_CHANGE_IN_PRODUCTION';
1249
+
1250
+ return new Promise((resolve, reject) => {
1251
+ scrypt(masterSecret, id, 32, (err, derivedKey) => {
1252
+ if (err) {
1253
+ reject(err);
1254
+ } else {
1255
+ this.encryptionKeys.set(id, {
1256
+ key: derivedKey,
1257
+ createdAt: new Date(),
1258
+ active: true
1259
+ });
1260
+ resolve();
1261
+ }
1262
+ });
1263
+ });
1264
+ }
1265
+
1266
+ private calculateEntryHash(entry: Omit<AuditLogEntry, 'entryHash'>): string {
1267
+ const content = JSON.stringify({
1268
+ ...entry,
1269
+ timestamp: entry.timestamp.toISOString()
1270
+ });
1271
+ return createHash('sha256').update(content).digest('hex');
1272
+ }
1273
+
1274
+ private async detectSuspiciousActivity(entry: AuditLogEntry): Promise<void> {
1275
+ // Check for patterns that might indicate security issues
1276
+ const suspiciousPatterns: { pattern: () => boolean; alert: string }[] = [
1277
+ {
1278
+ pattern: () => entry.action === 'login-failed',
1279
+ alert: 'Failed login attempt'
1280
+ },
1281
+ {
1282
+ pattern: () => entry.security.accessLevel === 'emergency',
1283
+ alert: 'Emergency access used'
1284
+ },
1285
+ {
1286
+ pattern: () => {
1287
+ // Check for after-hours access
1288
+ const hour = entry.timestamp.getHours();
1289
+ return (hour < 6 || hour > 22) && entry.eventType === 'access';
1290
+ },
1291
+ alert: 'After-hours PHI access'
1292
+ },
1293
+ {
1294
+ pattern: () => {
1295
+ // Check for excessive access
1296
+ const recentAccess = this.auditStore.filter(e =>
1297
+ e.actor.userId === entry.actor.userId &&
1298
+ e.eventType === 'access' &&
1299
+ e.timestamp.getTime() > Date.now() - 60000 // Last minute
1300
+ );
1301
+ return recentAccess.length > 50;
1302
+ },
1303
+ alert: 'Excessive PHI access rate detected'
1304
+ }
1305
+ ];
1306
+
1307
+ for (const { pattern, alert } of suspiciousPatterns) {
1308
+ if (pattern()) {
1309
+ this.emit('security-alert', {
1310
+ timestamp: new Date(),
1311
+ alert,
1312
+ auditEntryId: entry.id,
1313
+ actor: entry.actor,
1314
+ severity: 'warning'
1315
+ });
1316
+ }
1317
+ }
1318
+ }
1319
+
1320
+ /**
1321
+ * Export audit logs for compliance reporting
1322
+ */
1323
+ exportAuditLogs(format: 'json' | 'csv', params?: {
1324
+ startDate?: Date;
1325
+ endDate?: Date;
1326
+ patientId?: string;
1327
+ }): string {
1328
+ const { entries } = this.queryAuditLogs({
1329
+ ...params,
1330
+ limit: 100000
1331
+ });
1332
+
1333
+ if (format === 'json') {
1334
+ return JSON.stringify(entries, null, 2);
1335
+ }
1336
+
1337
+ // CSV format
1338
+ const headers = [
1339
+ 'ID', 'Timestamp', 'Event Type', 'Action', 'Outcome',
1340
+ 'User ID', 'User Role', 'IP Address',
1341
+ 'Resource Type', 'Patient ID', 'Access Level'
1342
+ ];
1343
+
1344
+ const rows = entries.map(e => [
1345
+ e.id,
1346
+ e.timestamp.toISOString(),
1347
+ e.eventType,
1348
+ e.action,
1349
+ e.outcome,
1350
+ e.actor.userId,
1351
+ e.actor.role,
1352
+ e.actor.ipAddress,
1353
+ e.resource.type,
1354
+ e.resource.patientId || '',
1355
+ e.security.accessLevel
1356
+ ]);
1357
+
1358
+ return [
1359
+ headers.join(','),
1360
+ ...rows.map(r => r.map(v => `"${v}"`).join(','))
1361
+ ].join('\n');
1362
+ }
1363
+ }
1364
+
1365
+ export default HIPAAComplianceService;