@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,929 @@
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
+ import { createHash, createCipheriv, createDecipheriv, randomBytes, scrypt } from 'crypto';
17
+ import { EventEmitter } from 'events';
18
+ // ═══════════════════════════════════════════════════════════════════════════════
19
+ // HIPAA COMPLIANCE SERVICE
20
+ // ═══════════════════════════════════════════════════════════════════════════════
21
+ export class HIPAAComplianceService extends EventEmitter {
22
+ auditStore = [];
23
+ consents = new Map();
24
+ accessPolicies = [];
25
+ encryptionKeys = new Map();
26
+ activeKeyId;
27
+ config;
28
+ lastEntryHash;
29
+ constructor(config) {
30
+ super();
31
+ this.config = {
32
+ encryption: {
33
+ algorithm: 'aes-256-gcm',
34
+ keyDerivation: 'scrypt',
35
+ keyRotationDays: 90,
36
+ fieldLevelEncryption: true,
37
+ encryptedFields: ['ssn', 'dob', 'address', 'phone', 'genomics', 'mentalHealth'],
38
+ ...config?.encryption
39
+ },
40
+ masking: {
41
+ ssnPattern: 'last4',
42
+ dobPattern: 'age-only',
43
+ phonePattern: 'last4',
44
+ addressPattern: 'city-state',
45
+ mrnPattern: 'full',
46
+ genomicPattern: 'summary',
47
+ ...config?.masking
48
+ },
49
+ auditRetentionDays: config?.auditRetentionDays || 2190, // 6 years per HIPAA
50
+ emergencyAccessEnabled: config?.emergencyAccessEnabled ?? true
51
+ };
52
+ // Initialize encryption key
53
+ this.activeKeyId = this.generateKeyId();
54
+ this.initializeEncryptionKey();
55
+ }
56
+ // ═══════════════════════════════════════════════════════════════════════════════
57
+ // AUDIT LOGGING
58
+ // ═══════════════════════════════════════════════════════════════════════════════
59
+ /**
60
+ * Log an audit event
61
+ */
62
+ async logAuditEvent(entry) {
63
+ const id = this.generateAuditId();
64
+ const timestamp = new Date();
65
+ // Create the entry
66
+ const fullEntry = {
67
+ id,
68
+ timestamp,
69
+ ...entry,
70
+ previousEntryHash: this.lastEntryHash
71
+ };
72
+ // Calculate hash for tamper detection
73
+ fullEntry.entryHash = this.calculateEntryHash(fullEntry);
74
+ this.lastEntryHash = fullEntry.entryHash;
75
+ // Store the entry
76
+ this.auditStore.push(fullEntry);
77
+ // Emit event for external handlers
78
+ this.emit('audit-event', fullEntry);
79
+ // Check for suspicious patterns
80
+ await this.detectSuspiciousActivity(fullEntry);
81
+ return id;
82
+ }
83
+ /**
84
+ * Log PHI access event
85
+ */
86
+ async logPHIAccess(params) {
87
+ return this.logAuditEvent({
88
+ eventType: params.emergencyAccess ? 'emergency-access' : 'access',
89
+ action: params.action,
90
+ outcome: 'success',
91
+ actor: {
92
+ userId: params.userId,
93
+ userName: params.userName,
94
+ role: params.role,
95
+ ipAddress: params.ipAddress
96
+ },
97
+ resource: {
98
+ type: params.resourceType,
99
+ id: params.resourceId,
100
+ patientId: params.patientId
101
+ },
102
+ details: {
103
+ fieldsAccessed: params.fieldsAccessed,
104
+ reason: params.reason,
105
+ emergencyAccess: params.emergencyAccess,
106
+ consentId: params.consentId
107
+ },
108
+ security: {
109
+ authMethod: 'mfa', // Assuming MFA for PHI access
110
+ encryptionUsed: true,
111
+ integrityVerified: true,
112
+ accessLevel: params.emergencyAccess ? 'emergency' : 'normal'
113
+ }
114
+ });
115
+ }
116
+ /**
117
+ * Log PHI modification event
118
+ */
119
+ async logPHIModification(params) {
120
+ return this.logAuditEvent({
121
+ eventType: 'modification',
122
+ action: params.action,
123
+ outcome: 'success',
124
+ actor: {
125
+ userId: params.userId,
126
+ role: params.role,
127
+ ipAddress: params.ipAddress
128
+ },
129
+ resource: {
130
+ type: params.resourceType,
131
+ id: params.resourceId,
132
+ patientId: params.patientId
133
+ },
134
+ details: {
135
+ fieldsModified: params.fieldsModified,
136
+ previousValues: params.previousValues,
137
+ newValues: params.newValues,
138
+ reason: params.reason
139
+ },
140
+ security: {
141
+ authMethod: 'mfa',
142
+ encryptionUsed: true,
143
+ integrityVerified: true,
144
+ accessLevel: 'normal'
145
+ }
146
+ });
147
+ }
148
+ /**
149
+ * Log authentication event
150
+ */
151
+ async logAuthentication(params) {
152
+ return this.logAuditEvent({
153
+ eventType: 'authentication',
154
+ action: params.action,
155
+ outcome: params.success ? 'success' : 'failure',
156
+ actor: {
157
+ userId: params.userId,
158
+ role: 'unknown',
159
+ ipAddress: params.ipAddress
160
+ },
161
+ resource: {
162
+ type: 'system'
163
+ },
164
+ details: params.failureReason ? { reason: params.failureReason } : undefined,
165
+ security: {
166
+ authMethod: params.authMethod,
167
+ encryptionUsed: true,
168
+ integrityVerified: true,
169
+ accessLevel: 'normal'
170
+ }
171
+ });
172
+ }
173
+ /**
174
+ * Query audit logs
175
+ */
176
+ queryAuditLogs(params) {
177
+ let filtered = [...this.auditStore];
178
+ if (params.startDate) {
179
+ filtered = filtered.filter(e => e.timestamp >= params.startDate);
180
+ }
181
+ if (params.endDate) {
182
+ filtered = filtered.filter(e => e.timestamp <= params.endDate);
183
+ }
184
+ if (params.patientId) {
185
+ filtered = filtered.filter(e => e.resource.patientId === params.patientId);
186
+ }
187
+ if (params.userId) {
188
+ filtered = filtered.filter(e => e.actor.userId === params.userId);
189
+ }
190
+ if (params.eventType) {
191
+ filtered = filtered.filter(e => e.eventType === params.eventType);
192
+ }
193
+ if (params.action) {
194
+ filtered = filtered.filter(e => e.action === params.action);
195
+ }
196
+ if (params.outcome) {
197
+ filtered = filtered.filter(e => e.outcome === params.outcome);
198
+ }
199
+ // Sort by timestamp descending
200
+ filtered.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
201
+ const total = filtered.length;
202
+ const offset = params.offset || 0;
203
+ const limit = params.limit || 100;
204
+ return {
205
+ entries: filtered.slice(offset, offset + limit),
206
+ total
207
+ };
208
+ }
209
+ /**
210
+ * Get accounting of disclosures for a patient (HIPAA requirement)
211
+ */
212
+ getAccountingOfDisclosures(patientId, startDate, endDate) {
213
+ return this.auditStore.filter(entry => entry.resource.patientId === patientId &&
214
+ entry.eventType === 'disclosure' &&
215
+ (!startDate || entry.timestamp >= startDate) &&
216
+ (!endDate || entry.timestamp <= endDate)).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
217
+ }
218
+ /**
219
+ * Verify audit log integrity
220
+ */
221
+ verifyAuditLogIntegrity() {
222
+ const errors = [];
223
+ for (let i = 0; i < this.auditStore.length; i++) {
224
+ const entry = this.auditStore[i];
225
+ // Verify hash
226
+ const { entryHash: _hash, ...entryWithoutHash } = entry;
227
+ const calculatedHash = this.calculateEntryHash(entryWithoutHash);
228
+ if (calculatedHash !== entry.entryHash) {
229
+ errors.push(`Entry ${entry.id} has been tampered with (hash mismatch)`);
230
+ }
231
+ // Verify chain
232
+ if (i > 0 && entry.previousEntryHash !== this.auditStore[i - 1].entryHash) {
233
+ errors.push(`Entry ${entry.id} has broken chain link`);
234
+ }
235
+ }
236
+ return {
237
+ valid: errors.length === 0,
238
+ errors
239
+ };
240
+ }
241
+ // ═══════════════════════════════════════════════════════════════════════════════
242
+ // CONSENT MANAGEMENT
243
+ // ═══════════════════════════════════════════════════════════════════════════════
244
+ /**
245
+ * Record a patient consent
246
+ */
247
+ async recordConsent(consent) {
248
+ const id = this.generateConsentId();
249
+ const now = new Date();
250
+ const fullConsent = {
251
+ id,
252
+ ...consent,
253
+ audit: {
254
+ createdAt: now,
255
+ createdBy: consent.capture.method === 'electronic' ? 'system' : 'staff'
256
+ }
257
+ };
258
+ // Store consent
259
+ const patientConsents = this.consents.get(consent.patientId) || [];
260
+ patientConsents.push(fullConsent);
261
+ this.consents.set(consent.patientId, patientConsents);
262
+ // Log the consent capture
263
+ await this.logAuditEvent({
264
+ eventType: 'consent-change',
265
+ action: 'consent-obtained',
266
+ outcome: 'success',
267
+ actor: {
268
+ userId: fullConsent.audit.createdBy,
269
+ role: 'system',
270
+ ipAddress: consent.capture.ipAddress || 'unknown'
271
+ },
272
+ resource: {
273
+ type: 'patient',
274
+ patientId: consent.patientId,
275
+ description: `${consent.consentType} consent`
276
+ },
277
+ details: {
278
+ consentId: id
279
+ },
280
+ security: {
281
+ authMethod: 'password',
282
+ encryptionUsed: true,
283
+ integrityVerified: true,
284
+ accessLevel: 'normal'
285
+ }
286
+ });
287
+ this.emit('consent-recorded', fullConsent);
288
+ return id;
289
+ }
290
+ /**
291
+ * Revoke a patient consent
292
+ */
293
+ async revokeConsent(consentId, revokedBy, reason) {
294
+ for (const [patientId, consents] of this.consents) {
295
+ const consent = consents.find(c => c.id === consentId);
296
+ if (consent) {
297
+ consent.status = 'revoked';
298
+ consent.audit.revokedAt = new Date();
299
+ consent.audit.revokedBy = revokedBy;
300
+ consent.audit.revocationReason = reason;
301
+ // Log the revocation
302
+ await this.logAuditEvent({
303
+ eventType: 'consent-change',
304
+ action: 'consent-revoked',
305
+ outcome: 'success',
306
+ actor: {
307
+ userId: revokedBy,
308
+ role: 'patient',
309
+ ipAddress: 'unknown'
310
+ },
311
+ resource: {
312
+ type: 'patient',
313
+ patientId,
314
+ description: `${consent.consentType} consent revoked`
315
+ },
316
+ details: {
317
+ consentId,
318
+ reason
319
+ },
320
+ security: {
321
+ authMethod: 'password',
322
+ encryptionUsed: true,
323
+ integrityVerified: true,
324
+ accessLevel: 'normal'
325
+ }
326
+ });
327
+ this.emit('consent-revoked', consent);
328
+ return true;
329
+ }
330
+ }
331
+ return false;
332
+ }
333
+ /**
334
+ * Check if a patient has consented to a specific use
335
+ */
336
+ checkConsent(patientId, purpose, dataCategory) {
337
+ const patientConsents = this.consents.get(patientId) || [];
338
+ // Find active consent that covers the purpose
339
+ const validConsent = patientConsents.find(c => c.status === 'active' &&
340
+ c.scope.purposes.includes(purpose) &&
341
+ (!dataCategory || c.scope.dataCategories.includes(dataCategory)) &&
342
+ (!c.validity.expirationDate || c.validity.expirationDate > new Date()));
343
+ if (validConsent) {
344
+ return {
345
+ hasConsent: true,
346
+ consentId: validConsent.id,
347
+ restrictions: validConsent.scope.excludedData
348
+ };
349
+ }
350
+ // Check if TPO (Treatment, Payment, Operations) - may not require explicit consent
351
+ if (['treatment', 'payment', 'healthcare-operations'].includes(purpose)) {
352
+ // TPO typically covered by Notice of Privacy Practices
353
+ return {
354
+ hasConsent: true,
355
+ restrictions: []
356
+ };
357
+ }
358
+ return {
359
+ hasConsent: false
360
+ };
361
+ }
362
+ /**
363
+ * Get all consents for a patient
364
+ */
365
+ getPatientConsents(patientId) {
366
+ return this.consents.get(patientId) || [];
367
+ }
368
+ // ═══════════════════════════════════════════════════════════════════════════════
369
+ // ACCESS CONTROL
370
+ // ═══════════════════════════════════════════════════════════════════════════════
371
+ /**
372
+ * Add an access policy
373
+ */
374
+ addAccessPolicy(policy) {
375
+ const id = this.generatePolicyId();
376
+ const fullPolicy = { id, ...policy };
377
+ this.accessPolicies.push(fullPolicy);
378
+ this.accessPolicies.sort((a, b) => b.priority - a.priority);
379
+ return id;
380
+ }
381
+ /**
382
+ * Check if access should be allowed
383
+ */
384
+ checkAccess(request) {
385
+ const time = request.time || new Date();
386
+ // Emergency access bypass (break-the-glass)
387
+ if (request.emergencyAccess && this.config.emergencyAccessEnabled) {
388
+ return {
389
+ allowed: true,
390
+ reason: 'Emergency access granted (break-the-glass)',
391
+ conditions: ['Must document emergency reason', 'Subject to post-hoc review'],
392
+ auditRequired: true
393
+ };
394
+ }
395
+ // Check consent if accessing patient data
396
+ if (request.patientId && request.purpose) {
397
+ const consentCheck = this.checkConsent(request.patientId, request.purpose, request.dataCategory);
398
+ if (!consentCheck.hasConsent) {
399
+ return {
400
+ allowed: false,
401
+ reason: `No consent for ${request.purpose} purpose`,
402
+ auditRequired: true
403
+ };
404
+ }
405
+ }
406
+ // Evaluate policies in priority order
407
+ for (const policy of this.accessPolicies) {
408
+ if (!policy.enabled)
409
+ continue;
410
+ // Check if policy applies to this subject
411
+ const subjectMatch = (!policy.subjects.roles || policy.subjects.roles.includes(request.role)) &&
412
+ (!policy.subjects.users || policy.subjects.users.includes(request.userId)) &&
413
+ (!policy.subjects.organizations || policy.subjects.organizations.includes(request.organization || ''));
414
+ if (!subjectMatch)
415
+ continue;
416
+ // Check if policy applies to this resource
417
+ const resourceMatch = (!policy.resources.types || policy.resources.types.includes(request.resourceType)) &&
418
+ (!policy.resources.patientIds || !request.patientId || policy.resources.patientIds.includes(request.patientId)) &&
419
+ (!policy.resources.dataCategories || !request.dataCategory || policy.resources.dataCategories.includes(request.dataCategory));
420
+ if (!resourceMatch)
421
+ continue;
422
+ // Check if action is permitted
423
+ if (!policy.permissions.actions.includes(request.action)) {
424
+ continue;
425
+ }
426
+ // Evaluate conditions
427
+ const conditionResults = this.evaluateConditions(policy.permissions.conditions || [], request, time);
428
+ if (conditionResults.allMet) {
429
+ return {
430
+ allowed: true,
431
+ policy: policy.id,
432
+ reason: `Access granted by policy: ${policy.name}`,
433
+ conditions: conditionResults.messages,
434
+ auditRequired: true
435
+ };
436
+ }
437
+ else if (conditionResults.requiresElevation) {
438
+ return {
439
+ allowed: false,
440
+ policy: policy.id,
441
+ reason: 'Access requires additional verification',
442
+ requiresElevation: true,
443
+ conditions: conditionResults.messages,
444
+ auditRequired: true
445
+ };
446
+ }
447
+ }
448
+ // Default deny
449
+ return {
450
+ allowed: false,
451
+ reason: 'No matching policy found - access denied by default',
452
+ auditRequired: true
453
+ };
454
+ }
455
+ evaluateConditions(conditions, request, time) {
456
+ const messages = [];
457
+ let requiresElevation = false;
458
+ for (const condition of conditions) {
459
+ switch (condition.type) {
460
+ case 'time-of-day':
461
+ const hour = time.getHours();
462
+ const startHour = condition.parameters.startHour || 0;
463
+ const endHour = condition.parameters.endHour || 24;
464
+ if (hour < startHour || hour >= endHour) {
465
+ messages.push(`Access outside permitted hours (${startHour}:00 - ${endHour}:00)`);
466
+ return { allMet: false, requiresElevation: false, messages };
467
+ }
468
+ break;
469
+ case 'ip-range':
470
+ // Simplified IP check - would need proper CIDR matching in production
471
+ const allowedRanges = condition.parameters.ranges;
472
+ if (request.ipAddress && !allowedRanges.some(r => request.ipAddress.startsWith(r))) {
473
+ messages.push('Access from unauthorized network location');
474
+ return { allMet: false, requiresElevation: false, messages };
475
+ }
476
+ break;
477
+ case 'mfa-required':
478
+ messages.push('Multi-factor authentication required');
479
+ requiresElevation = true;
480
+ break;
481
+ case 'purpose':
482
+ const allowedPurposes = condition.parameters.purposes;
483
+ if (!request.purpose || !allowedPurposes.includes(request.purpose)) {
484
+ messages.push(`Purpose '${request.purpose}' not permitted`);
485
+ return { allMet: false, requiresElevation: false, messages };
486
+ }
487
+ break;
488
+ case 'emergency-only':
489
+ if (!request.emergencyAccess) {
490
+ messages.push('Access restricted to emergency situations');
491
+ return { allMet: false, requiresElevation: false, messages };
492
+ }
493
+ break;
494
+ }
495
+ }
496
+ return {
497
+ allMet: !requiresElevation,
498
+ requiresElevation,
499
+ messages
500
+ };
501
+ }
502
+ // ═══════════════════════════════════════════════════════════════════════════════
503
+ // ENCRYPTION
504
+ // ═══════════════════════════════════════════════════════════════════════════════
505
+ /**
506
+ * Encrypt sensitive data
507
+ */
508
+ async encryptData(data, context) {
509
+ const keyEntry = this.encryptionKeys.get(this.activeKeyId);
510
+ if (!keyEntry) {
511
+ throw new Error('No active encryption key');
512
+ }
513
+ const iv = randomBytes(16);
514
+ const algorithm = this.config.encryption.algorithm;
515
+ if (algorithm === 'aes-256-gcm') {
516
+ const cipher = createCipheriv('aes-256-gcm', keyEntry.key, iv);
517
+ if (context) {
518
+ cipher.setAAD(Buffer.from(context));
519
+ }
520
+ const dataBuffer = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
521
+ const encrypted = Buffer.concat([cipher.update(dataBuffer), cipher.final()]);
522
+ const authTag = cipher.getAuthTag();
523
+ return {
524
+ ciphertext: encrypted.toString('base64'),
525
+ iv: iv.toString('base64'),
526
+ authTag: authTag.toString('base64'),
527
+ keyId: this.activeKeyId,
528
+ algorithm,
529
+ encryptedAt: new Date()
530
+ };
531
+ }
532
+ else {
533
+ // AES-256-CBC fallback
534
+ const cipher = createCipheriv('aes-256-cbc', keyEntry.key, iv);
535
+ const dataBuffer = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
536
+ const encrypted = Buffer.concat([cipher.update(dataBuffer), cipher.final()]);
537
+ return {
538
+ ciphertext: encrypted.toString('base64'),
539
+ iv: iv.toString('base64'),
540
+ keyId: this.activeKeyId,
541
+ algorithm: 'aes-256-cbc',
542
+ encryptedAt: new Date()
543
+ };
544
+ }
545
+ }
546
+ /**
547
+ * Decrypt data
548
+ */
549
+ async decryptData(encrypted, context) {
550
+ const keyEntry = this.encryptionKeys.get(encrypted.keyId);
551
+ if (!keyEntry) {
552
+ throw new Error(`Encryption key ${encrypted.keyId} not found`);
553
+ }
554
+ const iv = Buffer.from(encrypted.iv, 'base64');
555
+ const ciphertext = Buffer.from(encrypted.ciphertext, 'base64');
556
+ if (encrypted.algorithm === 'aes-256-gcm') {
557
+ const decipher = createDecipheriv('aes-256-gcm', keyEntry.key, iv);
558
+ if (encrypted.authTag) {
559
+ decipher.setAuthTag(Buffer.from(encrypted.authTag, 'base64'));
560
+ }
561
+ if (context) {
562
+ decipher.setAAD(Buffer.from(context));
563
+ }
564
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
565
+ }
566
+ else {
567
+ const decipher = createDecipheriv('aes-256-cbc', keyEntry.key, iv);
568
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
569
+ }
570
+ }
571
+ /**
572
+ * Encrypt specific PHI fields in an object
573
+ */
574
+ async encryptPHI(data) {
575
+ const encryptedFields = {};
576
+ const result = { ...data };
577
+ for (const field of this.config.encryption.encryptedFields || []) {
578
+ if (data[field] !== undefined) {
579
+ encryptedFields[field] = await this.encryptData(JSON.stringify(data[field]), field);
580
+ result[field] = '[ENCRYPTED]';
581
+ }
582
+ }
583
+ result._encrypted = encryptedFields;
584
+ return result;
585
+ }
586
+ /**
587
+ * Decrypt PHI fields in an object
588
+ */
589
+ async decryptPHI(data) {
590
+ if (!data._encrypted)
591
+ return data;
592
+ const result = { ...data };
593
+ for (const [field, encrypted] of Object.entries(data._encrypted)) {
594
+ const decrypted = await this.decryptData(encrypted, field);
595
+ result[field] = JSON.parse(decrypted.toString('utf8'));
596
+ }
597
+ delete result._encrypted;
598
+ return result;
599
+ }
600
+ /**
601
+ * Rotate encryption keys
602
+ */
603
+ async rotateEncryptionKey() {
604
+ // Mark current key as inactive
605
+ const currentKey = this.encryptionKeys.get(this.activeKeyId);
606
+ if (currentKey) {
607
+ currentKey.active = false;
608
+ }
609
+ // Generate new key
610
+ const newKeyId = this.generateKeyId();
611
+ this.initializeEncryptionKey(newKeyId);
612
+ this.activeKeyId = newKeyId;
613
+ // Log key rotation
614
+ await this.logAuditEvent({
615
+ eventType: 'system-event',
616
+ action: 'update',
617
+ outcome: 'success',
618
+ actor: {
619
+ userId: 'system',
620
+ role: 'system',
621
+ ipAddress: 'localhost'
622
+ },
623
+ resource: {
624
+ type: 'configuration',
625
+ description: 'Encryption key rotation'
626
+ },
627
+ details: {
628
+ previousValues: { keyId: currentKey ? this.activeKeyId : 'none' },
629
+ newValues: { keyId: newKeyId }
630
+ },
631
+ security: {
632
+ authMethod: 'certificate',
633
+ encryptionUsed: true,
634
+ integrityVerified: true,
635
+ accessLevel: 'elevated'
636
+ }
637
+ });
638
+ return newKeyId;
639
+ }
640
+ // ═══════════════════════════════════════════════════════════════════════════════
641
+ // DATA MASKING (Minimum Necessary)
642
+ // ═══════════════════════════════════════════════════════════════════════════════
643
+ /**
644
+ * Mask PHI according to minimum necessary standard
645
+ */
646
+ maskPHI(data, accessLevel) {
647
+ const masked = { ...data };
648
+ // Determine masking rules based on access level
649
+ const maskingRules = this.getMaskingRulesForLevel(accessLevel);
650
+ // Apply masking
651
+ if (masked.ssn && maskingRules.ssnPattern !== 'full') {
652
+ masked.ssn = this.maskSSN(masked.ssn, maskingRules.ssnPattern);
653
+ }
654
+ if (masked.dob && maskingRules.dobPattern !== 'full') {
655
+ masked.dob = this.maskDOB(masked.dob, maskingRules.dobPattern);
656
+ }
657
+ if (masked.phone && maskingRules.phonePattern !== 'full') {
658
+ masked.phone = this.maskPhone(masked.phone, maskingRules.phonePattern);
659
+ }
660
+ if (masked.address && maskingRules.addressPattern !== 'full') {
661
+ masked.address = this.maskAddress(masked.address, maskingRules.addressPattern);
662
+ }
663
+ if (masked.mrn && maskingRules.mrnPattern !== 'full') {
664
+ masked.mrn = this.maskMRN(masked.mrn, maskingRules.mrnPattern);
665
+ }
666
+ if (masked.genomics && maskingRules.genomicPattern !== 'full') {
667
+ masked.genomics = this.maskGenomics(masked.genomics, maskingRules.genomicPattern);
668
+ }
669
+ return masked;
670
+ }
671
+ getMaskingRulesForLevel(level) {
672
+ switch (level) {
673
+ case 'full':
674
+ return {
675
+ ssnPattern: 'full',
676
+ dobPattern: 'full',
677
+ phonePattern: 'full',
678
+ addressPattern: 'full',
679
+ mrnPattern: 'full',
680
+ genomicPattern: 'full'
681
+ };
682
+ case 'clinical':
683
+ return {
684
+ ssnPattern: 'last4',
685
+ dobPattern: 'full',
686
+ phonePattern: 'full',
687
+ addressPattern: 'full',
688
+ mrnPattern: 'full',
689
+ genomicPattern: 'full'
690
+ };
691
+ case 'billing':
692
+ return {
693
+ ssnPattern: 'last4',
694
+ dobPattern: 'full',
695
+ phonePattern: 'last4',
696
+ addressPattern: 'full',
697
+ mrnPattern: 'full',
698
+ genomicPattern: 'hidden'
699
+ };
700
+ case 'research':
701
+ return {
702
+ ssnPattern: 'hidden',
703
+ dobPattern: 'year-only',
704
+ phonePattern: 'hidden',
705
+ addressPattern: 'zip-only',
706
+ mrnPattern: 'hidden',
707
+ genomicPattern: 'summary'
708
+ };
709
+ case 'minimal':
710
+ default:
711
+ return {
712
+ ssnPattern: 'hidden',
713
+ dobPattern: 'age-only',
714
+ phonePattern: 'hidden',
715
+ addressPattern: 'hidden',
716
+ mrnPattern: 'hidden',
717
+ genomicPattern: 'hidden'
718
+ };
719
+ }
720
+ }
721
+ maskSSN(ssn, pattern) {
722
+ const cleaned = ssn.replace(/\D/g, '');
723
+ switch (pattern) {
724
+ case 'last4':
725
+ return `***-**-${cleaned.slice(-4)}`;
726
+ case 'hidden':
727
+ return '***-**-****';
728
+ default:
729
+ return ssn;
730
+ }
731
+ }
732
+ maskDOB(dob, pattern) {
733
+ const date = typeof dob === 'string' ? new Date(dob) : dob;
734
+ switch (pattern) {
735
+ case 'year-only':
736
+ return date.getFullYear().toString();
737
+ case 'age-only':
738
+ const age = Math.floor((Date.now() - date.getTime()) / (365.25 * 24 * 60 * 60 * 1000));
739
+ return `Age: ${age}`;
740
+ case 'hidden':
741
+ return '[REDACTED]';
742
+ default:
743
+ return typeof dob === 'string' ? dob : date.toISOString().split('T')[0];
744
+ }
745
+ }
746
+ maskPhone(phone, pattern) {
747
+ const cleaned = phone.replace(/\D/g, '');
748
+ switch (pattern) {
749
+ case 'last4':
750
+ return `(***) ***-${cleaned.slice(-4)}`;
751
+ case 'hidden':
752
+ return '(***) ***-****';
753
+ default:
754
+ return phone;
755
+ }
756
+ }
757
+ maskAddress(address, pattern) {
758
+ if (typeof address === 'string') {
759
+ switch (pattern) {
760
+ case 'city-state':
761
+ return '[City, State]';
762
+ case 'zip-only':
763
+ const zipMatch = address.match(/\d{5}(-\d{4})?/);
764
+ return zipMatch ? zipMatch[0] : '[ZIP]';
765
+ case 'hidden':
766
+ return '[REDACTED]';
767
+ default:
768
+ return address;
769
+ }
770
+ }
771
+ // Object address
772
+ switch (pattern) {
773
+ case 'city-state':
774
+ return { city: address.city, state: address.state };
775
+ case 'zip-only':
776
+ return { zip: address.zip || address.postalCode };
777
+ case 'hidden':
778
+ return '[REDACTED]';
779
+ default:
780
+ return address;
781
+ }
782
+ }
783
+ maskMRN(mrn, pattern) {
784
+ switch (pattern) {
785
+ case 'last4':
786
+ return `****${mrn.slice(-4)}`;
787
+ case 'hidden':
788
+ return '[REDACTED]';
789
+ default:
790
+ return mrn;
791
+ }
792
+ }
793
+ maskGenomics(genomics, pattern) {
794
+ switch (pattern) {
795
+ case 'summary':
796
+ if (Array.isArray(genomics)) {
797
+ return { count: genomics.length, summary: 'Genomic data available' };
798
+ }
799
+ return { summary: 'Genomic data available' };
800
+ case 'hidden':
801
+ return '[GENOMIC DATA REDACTED]';
802
+ default:
803
+ return genomics;
804
+ }
805
+ }
806
+ // ═══════════════════════════════════════════════════════════════════════════════
807
+ // HELPER METHODS
808
+ // ═══════════════════════════════════════════════════════════════════════════════
809
+ generateAuditId() {
810
+ return `AUD-${Date.now()}-${randomBytes(4).toString('hex')}`;
811
+ }
812
+ generateConsentId() {
813
+ return `CON-${Date.now()}-${randomBytes(4).toString('hex')}`;
814
+ }
815
+ generatePolicyId() {
816
+ return `POL-${Date.now()}-${randomBytes(4).toString('hex')}`;
817
+ }
818
+ generateKeyId() {
819
+ return `KEY-${Date.now()}-${randomBytes(4).toString('hex')}`;
820
+ }
821
+ async initializeEncryptionKey(keyId) {
822
+ const id = keyId || this.activeKeyId;
823
+ // In production, this would use a proper key management system (KMS)
824
+ // For now, derive from a master secret
825
+ const masterSecret = process.env.HIPAA_MASTER_KEY || 'DEFAULT_DEV_KEY_CHANGE_IN_PRODUCTION';
826
+ return new Promise((resolve, reject) => {
827
+ scrypt(masterSecret, id, 32, (err, derivedKey) => {
828
+ if (err) {
829
+ reject(err);
830
+ }
831
+ else {
832
+ this.encryptionKeys.set(id, {
833
+ key: derivedKey,
834
+ createdAt: new Date(),
835
+ active: true
836
+ });
837
+ resolve();
838
+ }
839
+ });
840
+ });
841
+ }
842
+ calculateEntryHash(entry) {
843
+ const content = JSON.stringify({
844
+ ...entry,
845
+ timestamp: entry.timestamp.toISOString()
846
+ });
847
+ return createHash('sha256').update(content).digest('hex');
848
+ }
849
+ async detectSuspiciousActivity(entry) {
850
+ // Check for patterns that might indicate security issues
851
+ const suspiciousPatterns = [
852
+ {
853
+ pattern: () => entry.action === 'login-failed',
854
+ alert: 'Failed login attempt'
855
+ },
856
+ {
857
+ pattern: () => entry.security.accessLevel === 'emergency',
858
+ alert: 'Emergency access used'
859
+ },
860
+ {
861
+ pattern: () => {
862
+ // Check for after-hours access
863
+ const hour = entry.timestamp.getHours();
864
+ return (hour < 6 || hour > 22) && entry.eventType === 'access';
865
+ },
866
+ alert: 'After-hours PHI access'
867
+ },
868
+ {
869
+ pattern: () => {
870
+ // Check for excessive access
871
+ const recentAccess = this.auditStore.filter(e => e.actor.userId === entry.actor.userId &&
872
+ e.eventType === 'access' &&
873
+ e.timestamp.getTime() > Date.now() - 60000 // Last minute
874
+ );
875
+ return recentAccess.length > 50;
876
+ },
877
+ alert: 'Excessive PHI access rate detected'
878
+ }
879
+ ];
880
+ for (const { pattern, alert } of suspiciousPatterns) {
881
+ if (pattern()) {
882
+ this.emit('security-alert', {
883
+ timestamp: new Date(),
884
+ alert,
885
+ auditEntryId: entry.id,
886
+ actor: entry.actor,
887
+ severity: 'warning'
888
+ });
889
+ }
890
+ }
891
+ }
892
+ /**
893
+ * Export audit logs for compliance reporting
894
+ */
895
+ exportAuditLogs(format, params) {
896
+ const { entries } = this.queryAuditLogs({
897
+ ...params,
898
+ limit: 100000
899
+ });
900
+ if (format === 'json') {
901
+ return JSON.stringify(entries, null, 2);
902
+ }
903
+ // CSV format
904
+ const headers = [
905
+ 'ID', 'Timestamp', 'Event Type', 'Action', 'Outcome',
906
+ 'User ID', 'User Role', 'IP Address',
907
+ 'Resource Type', 'Patient ID', 'Access Level'
908
+ ];
909
+ const rows = entries.map(e => [
910
+ e.id,
911
+ e.timestamp.toISOString(),
912
+ e.eventType,
913
+ e.action,
914
+ e.outcome,
915
+ e.actor.userId,
916
+ e.actor.role,
917
+ e.actor.ipAddress,
918
+ e.resource.type,
919
+ e.resource.patientId || '',
920
+ e.security.accessLevel
921
+ ]);
922
+ return [
923
+ headers.join(','),
924
+ ...rows.map(r => r.map(v => `"${v}"`).join(','))
925
+ ].join('\n');
926
+ }
927
+ }
928
+ export default HIPAAComplianceService;
929
+ //# sourceMappingURL=hipaa.js.map