@aurabx/jmix-js 0.1.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 (73) hide show
  1. package/.ai/security.md +184 -0
  2. package/.eslintrc.cjs +23 -0
  3. package/.idea/jmix-ts.iml +13 -0
  4. package/.idea/modules.xml +8 -0
  5. package/.idea/vcs.xml +6 -0
  6. package/.prettierrc +9 -0
  7. package/README.md +335 -0
  8. package/WARP.md +143 -0
  9. package/dist/JmixBuilder.d.ts +82 -0
  10. package/dist/JmixBuilder.d.ts.map +1 -0
  11. package/dist/JmixBuilder.js +353 -0
  12. package/dist/JmixBuilder.js.map +1 -0
  13. package/dist/crypto/PayloadDecryptor.d.ts +12 -0
  14. package/dist/crypto/PayloadDecryptor.d.ts.map +1 -0
  15. package/dist/crypto/PayloadDecryptor.js +39 -0
  16. package/dist/crypto/PayloadDecryptor.js.map +1 -0
  17. package/dist/crypto/PayloadEncryptor.d.ts +21 -0
  18. package/dist/crypto/PayloadEncryptor.d.ts.map +1 -0
  19. package/dist/crypto/PayloadEncryptor.js +79 -0
  20. package/dist/crypto/PayloadEncryptor.js.map +1 -0
  21. package/dist/demo-decrypt-existing.js +27 -0
  22. package/dist/demo-decrypt.js +39 -0
  23. package/dist/demo-no-validation.js +95 -0
  24. package/dist/demo-package-encrypted.js +34 -0
  25. package/dist/demo-package.js +28 -0
  26. package/dist/demo-verify-hash.js +30 -0
  27. package/dist/demo.js +73 -0
  28. package/dist/dicom/DicomProcessor.d.ts +40 -0
  29. package/dist/dicom/DicomProcessor.d.ts.map +1 -0
  30. package/dist/dicom/DicomProcessor.js +231 -0
  31. package/dist/dicom/DicomProcessor.js.map +1 -0
  32. package/dist/index.d.ts +6 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +8 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/types/index.d.ts +165 -0
  37. package/dist/types/index.d.ts.map +1 -0
  38. package/dist/types/index.js +31 -0
  39. package/dist/types/index.js.map +1 -0
  40. package/dist/validation/SchemaValidator.d.ts +25 -0
  41. package/dist/validation/SchemaValidator.d.ts.map +1 -0
  42. package/dist/validation/SchemaValidator.js +125 -0
  43. package/dist/validation/SchemaValidator.js.map +1 -0
  44. package/jest.config.json +20 -0
  45. package/package.json +74 -0
  46. package/samples/sample_config.json +52 -0
  47. package/samples/study_1/series_1/CT.1.1.dcm +0 -0
  48. package/samples/study_1/series_1/CT.1.2.dcm +0 -0
  49. package/samples/study_1/series_1/CT.1.3.dcm +0 -0
  50. package/samples/study_1/series_1/CT.1.4.dcm +0 -0
  51. package/samples/study_1/series_1/CT.1.5.dcm +0 -0
  52. package/samples/study_1/series_2/CT.2.1.dcm +0 -0
  53. package/samples/study_1/series_2/CT.2.2.dcm +0 -0
  54. package/samples/study_1/series_2/CT.2.3.dcm +0 -0
  55. package/samples/study_1/series_2/CT.2.4.dcm +0 -0
  56. package/samples/study_1/series_2/CT.2.5.dcm +0 -0
  57. package/samples/study_1/series_3/CT.3.1.dcm +0 -0
  58. package/samples/study_1/series_3/CT.3.2.dcm +0 -0
  59. package/samples/study_1/series_3/CT.3.3.dcm +0 -0
  60. package/samples/study_1/series_3/CT.3.4.dcm +0 -0
  61. package/samples/study_1/series_3/CT.3.5.dcm +0 -0
  62. package/src/JmixBuilder.ts +572 -0
  63. package/src/crypto/PayloadDecryptor.ts +45 -0
  64. package/src/crypto/PayloadEncryptor.ts +97 -0
  65. package/src/dicom/DicomProcessor.ts +262 -0
  66. package/src/index.ts +30 -0
  67. package/src/types/daikon.d.ts +1 -0
  68. package/src/types/index.ts +201 -0
  69. package/src/validation/SchemaValidator.ts +156 -0
  70. package/test-encrypt.js +10 -0
  71. package/tests/JmixBuilder.test.ts +170 -0
  72. package/tests/SchemaValidator.test.ts +196 -0
  73. package/tsconfig.json +21 -0
package/WARP.md ADDED
@@ -0,0 +1,143 @@
1
+ # WARP.md
2
+
3
+ This file provides guidance to WARP (warp.dev) when working with code in this repository.
4
+
5
+ Repository: jmix-ts - TypeScript library for JMIX (JSON Medical Interchange) format
6
+
7
+ ## What This Library Does
8
+
9
+ JMIX-TS builds secure JMIX envelopes from DICOM directories, supporting:
10
+ - AES-256-GCM encryption with X25519 key agreement and HKDF key derivation
11
+ - Three JSON components: manifest.json (routing/security), metadata.json (clinical), audit.json (trail)
12
+ - Deterministic payload hash verification over plaintext payload directories
13
+ - Full-featured packaging including original DICOM files and encrypted payload support
14
+
15
+ ## Prerequisites
16
+
17
+ - Node.js 20+ LTS
18
+ - npm or compatible package manager
19
+ - TypeScript 5+
20
+
21
+ ## Essential Commands
22
+
23
+ ```bash
24
+ # Install dependencies
25
+ npm install
26
+
27
+ # Build the library
28
+ npm run build
29
+
30
+ # Run all tests (Jest with ESM support)
31
+ npm test
32
+
33
+ # Run specific test
34
+ npm test -- JmixBuilder.test.ts
35
+
36
+ # Test with coverage report (outputs to coverage/)
37
+ npm run test:coverage
38
+
39
+ # Watch mode for development
40
+ npm run test:watch
41
+
42
+ # Type checking only
43
+ npm run typecheck
44
+
45
+ # Format code
46
+ npm run format:fix
47
+
48
+ # Clean build artifacts and temporary files
49
+ npm run clean
50
+ ```
51
+
52
+ ## Demo Commands
53
+
54
+ ```bash
55
+ # Basic package creation (plaintext)
56
+ npm run demo:package
57
+
58
+ # Encrypted package with generated keys
59
+ npm run demo:package:encrypted
60
+
61
+ # Decrypt and verify workflow
62
+ npm run demo:decrypt
63
+
64
+ # Verify payload hash
65
+ npm run demo:verify:hash
66
+ ```
67
+
68
+ ## High-Level Architecture
69
+
70
+ **Core Flow**: JmixBuilder orchestrates DICOM processing → JSON component generation → schema validation → file output/packaging.
71
+
72
+ ```
73
+ DICOM Directory → DicomProcessor → JmixBuilder → {manifest, metadata, audit} → SchemaValidator → Output
74
+
75
+ PayloadEncryptor/Decryptor → Encrypted JMIX Package
76
+ ```
77
+
78
+ **Key Components**:
79
+ - `JmixBuilder`: Main orchestrator with buildFromDicom(), packageToDirectory(), and packageEncryptedToDirectory()
80
+ - `DicomProcessor`: Recursively scans directories, validates DICOM magic signatures, extracts metadata
81
+ - `SchemaValidator`: Ajv-based validation against JSON schemas in configurable path (default: ../jmix/schemas)
82
+ - `PayloadEncryptor/Decryptor`: AES-256-GCM with X25519+HKDF, base64-encoded ephemeral keys/IV/auth tags
83
+ - Deterministic payload hashing: SHA-256 over sorted (path + newline + bytes) for all payload files
84
+
85
+ ## Configuration
86
+
87
+ ### Schema Path
88
+ - Default: `../jmix/schemas` (works in monorepo structure)
89
+ - Override: `JMIX_SCHEMA_PATH` environment variable
90
+ - Constructor: `new SchemaValidator({ schemaPath: '/custom/path' })`
91
+ - Gracefully skips validation when schemas not found
92
+
93
+ ### Output Directory
94
+ - Default: `./tmp` (not system /tmp)
95
+ - Constructor: `new JmixBuilder({ outputPath: './custom-tmp' })`
96
+
97
+ ## Testing Strategy
98
+
99
+ - Uses sample DICOM files in `samples/` directory
100
+ - All test outputs written to `./tmp` directory
101
+ - Comprehensive coverage: unit tests, integration tests, schema validation
102
+ - Real DICOM file processing with fallback to configuration data
103
+ - Mock-free approach using actual sample data
104
+
105
+ ## Cryptography Implementation
106
+
107
+ Based on JMIX security whitepaper (see `.ai/security.md`):
108
+ - **Algorithm**: AES-256-GCM
109
+ - **Key Exchange**: X25519 (Curve25519) ECDH
110
+ - **Key Derivation**: HKDF-SHA256 with info="JMIX-Payload-Encryption"
111
+ - **Forward Secrecy**: Ephemeral sender keys for each envelope
112
+ - **Authentication**: GCM authentication tag prevents tampering
113
+
114
+ Encryption creates `payload.encrypted` from tar-compressed payload directory, with all crypto material (ephemeral_public_key, iv, auth_tag) stored base64-encoded in manifest.security.encryption.
115
+
116
+ ## File Structure Reference
117
+
118
+ **Generated JMIX Envelope**:
119
+ ```
120
+ <envelope_id>.JMIX/
121
+ ├── manifest.json # Routing, security, patient info
122
+ ├── audit.json # Audit trail and events
123
+ └── payload/ # OR payload.encrypted (when encrypted)
124
+ ├── metadata.json # Clinical metadata and DICOM info
125
+ └── dicom/ # Original DICOM files (structure preserved)
126
+ ```
127
+
128
+ ## Development Notes
129
+
130
+ - **ESM modules**: Uses import/export syntax, .js extensions in TypeScript imports
131
+ - **Jest configuration**: Supports ESM with ts-jest preset
132
+ - **Schema validation**: Optional and graceful - continues if schemas missing
133
+ - **Error handling**: Structured error types (JmixError, ValidationError, DicomError, CryptographyError)
134
+ - **DICOM detection**: Magic number 'DICM' at byte offset 128 plus .dcm/.dicom extensions
135
+
136
+ ## Monorepo Context
137
+
138
+ This library expects to work within a larger jmix monorepo structure where:
139
+ - Schema definitions are available at `../jmix/schemas`
140
+ - Related Rust CLI tools may consume this library's output
141
+ - Sample data in `samples/` is shared across implementations
142
+
143
+ For more detailed guidance on general WARP usage patterns, see `.ai/WARP.md`.
@@ -0,0 +1,82 @@
1
+ import { SchemaValidator, SchemaValidatorOptions } from './validation/SchemaValidator.js';
2
+ import { DicomProcessor } from './dicom/DicomProcessor.js';
3
+ import { JmixEnvelope, Config } from './types/index.js';
4
+ export interface JmixBuilderOptions {
5
+ schemaValidatorOptions?: SchemaValidatorOptions;
6
+ outputPath?: string;
7
+ }
8
+ export declare class JmixBuilder {
9
+ private validator;
10
+ private dicomProcessor;
11
+ private outputPath;
12
+ constructor(options?: JmixBuilderOptions);
13
+ /**
14
+ * Build a JMIX envelope from DICOM files and configuration
15
+ */
16
+ buildFromDicom(dicomPath: string, config: Config): Promise<JmixEnvelope>;
17
+ /**
18
+ * Save envelope to JSON files
19
+ */
20
+ saveToFiles(envelope: JmixEnvelope, outputPath?: string): Promise<void>;
21
+ /**
22
+ * Build manifest component
23
+ */
24
+ private buildManifest;
25
+ /**
26
+ * Build metadata component
27
+ */
28
+ private buildMetadata;
29
+ /**
30
+ * Build audit component (audit trail)
31
+ */
32
+ private buildAudit;
33
+ /**
34
+ * Load configuration from JSON file
35
+ */
36
+ static loadConfig(configPath: string): Promise<Config>;
37
+ /**
38
+ * Get the schema validator instance
39
+ */
40
+ getValidator(): SchemaValidator;
41
+ /**
42
+ * Get the DICOM processor instance
43
+ */
44
+ getDicomProcessor(): DicomProcessor;
45
+ /**
46
+ * Package an ENCRYPTED JMIX envelope directory:
47
+ * - Builds plaintext payload/ first (metadata.json + dicom/)
48
+ * - Computes payload_hash over payload/
49
+ * - Tars payload/, encrypts to payload.encrypted with AES-256-GCM via X25519+HKDF
50
+ * - Removes plaintext payload/ directory
51
+ * Layout:
52
+ * <outputRoot>/<envelope_id>.JMIX/
53
+ * manifest.json (includes security.encryption & payload_hash)
54
+ * audit.json
55
+ * payload.encrypted
56
+ */
57
+ packageEncryptedToDirectory(dicomPath: string, config: Config, outputRoot: string, recipientPublicKeyBase64: string): Promise<string>;
58
+ /**
59
+ * Package a JMIX envelope to a directory structure that includes original DICOM files.
60
+ * Layout:
61
+ * <outputRoot>/<envelope_id>.JMIX/
62
+ * manifest.json
63
+ * audit.json
64
+ * payload/
65
+ * metadata.json
66
+ * dicom/ ... (copied from dicomPath, preserving structure)
67
+ * files/ (optional in future)
68
+ * files.json (optional in future)
69
+ */
70
+ packageToDirectory(dicomPath: string, config: Config, outputRoot: string): Promise<string>;
71
+ private walkFiles;
72
+ private isLikelyDicom;
73
+ private computePayloadHash;
74
+ /**
75
+ * Decrypt an encrypted JMIX envelope in place: restores a plaintext payload/ directory.
76
+ * - envelopeDir: path to <id>.JMIX directory
77
+ * - recipientPrivateKeyBase64: base64 Curve25519 private key (32 bytes)
78
+ * Returns the extracted payload directory path.
79
+ */
80
+ decryptEnvelope(envelopeDir: string, recipientPrivateKeyBase64: string): Promise<string>;
81
+ }
82
+ //# sourceMappingURL=JmixBuilder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JmixBuilder.d.ts","sourceRoot":"","sources":["../src/JmixBuilder.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,eAAe,EACf,sBAAsB,EACvB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAI3D,OAAO,EACL,YAAY,EACZ,MAAM,EAMP,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,kBAAkB;IACjC,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;IAChD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,UAAU,CAAS;gBAEf,OAAO,GAAE,kBAAuB;IAM5C;;OAEG;IACG,cAAc,CAClB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,YAAY,CAAC;IA2CxB;;OAEG;IACG,WAAW,CACf,QAAQ,EAAE,YAAY,EACtB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC;IA4ChB;;OAEG;IACH,OAAO,CAAC,aAAa;IAmCrB;;OAEG;IACH,OAAO,CAAC,aAAa;IAarB;;OAEG;IACH,OAAO,CAAC,UAAU;IAqBlB;;OAEG;WACU,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAa5D;;OAEG;IACH,YAAY,IAAI,eAAe;IAI/B;;OAEG;IACH,iBAAiB,IAAI,cAAc;IAInC;;;;;;;;;;;OAWG;IACG,2BAA2B,CAC/B,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,wBAAwB,EAAE,MAAM,GAC/B,OAAO,CAAC,MAAM,CAAC;IAyElB;;;;;;;;;;;OAWG;IACG,kBAAkB,CACtB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC;YAoDJ,SAAS;YAeT,aAAa;YAmBb,kBAAkB;IAgBhC;;;;;OAKG;IACG,eAAe,CACnB,WAAW,EAAE,MAAM,EACnB,yBAAyB,EAAE,MAAM,GAChC,OAAO,CAAC,MAAM,CAAC;CA6CnB"}
@@ -0,0 +1,353 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { randomUUID, createHash } from 'crypto';
4
+ import { SchemaValidator, } from './validation/SchemaValidator.js';
5
+ import { DicomProcessor } from './dicom/DicomProcessor.js';
6
+ import { PayloadEncryptor } from './crypto/PayloadEncryptor.js';
7
+ import tar from 'tar';
8
+ import { PayloadDecryptor } from './crypto/PayloadDecryptor.js';
9
+ import { JmixError, } from './types/index.js';
10
+ export class JmixBuilder {
11
+ validator;
12
+ dicomProcessor;
13
+ outputPath;
14
+ constructor(options = {}) {
15
+ this.validator = new SchemaValidator(options.schemaValidatorOptions);
16
+ this.dicomProcessor = new DicomProcessor();
17
+ this.outputPath = options.outputPath || './tmp';
18
+ }
19
+ /**
20
+ * Build a JMIX envelope from DICOM files and configuration
21
+ */
22
+ async buildFromDicom(dicomPath, config) {
23
+ try {
24
+ // Generate transmission ID and timestamp
25
+ const transmissionId = randomUUID();
26
+ const timestamp = new Date().toISOString();
27
+ // Extract DICOM metadata
28
+ const dicomMetadata = await this.dicomProcessor.processDicomFolder(dicomPath, config);
29
+ // Build envelope components
30
+ const manifest = this.buildManifest(config, timestamp);
31
+ const metadata = this.buildMetadata(config, dicomMetadata);
32
+ const audit = this.buildAudit(transmissionId, timestamp, config);
33
+ const envelope = {
34
+ manifest,
35
+ metadata,
36
+ audit,
37
+ };
38
+ // Validate the envelope components
39
+ await this.validator.validateEnvelope({
40
+ manifest,
41
+ metadata,
42
+ audit,
43
+ });
44
+ return envelope;
45
+ }
46
+ catch (error) {
47
+ throw new JmixError(`Failed to build JMIX envelope from ${dicomPath}`, error instanceof Error ? error : new Error(String(error)));
48
+ }
49
+ }
50
+ /**
51
+ * Save envelope to JSON files
52
+ */
53
+ async saveToFiles(envelope, outputPath) {
54
+ const finalOutputPath = outputPath || this.outputPath;
55
+ try {
56
+ // Ensure output directory exists
57
+ await fs.mkdir(finalOutputPath, { recursive: true });
58
+ // Write manifest.json
59
+ await fs.writeFile(path.join(finalOutputPath, 'manifest.json'), JSON.stringify(envelope.manifest, null, 2), 'utf-8');
60
+ // Write metadata.json
61
+ await fs.writeFile(path.join(finalOutputPath, 'metadata.json'), JSON.stringify(envelope.metadata, null, 2), 'utf-8');
62
+ // Write audit.json (audit trail)
63
+ await fs.writeFile(path.join(finalOutputPath, 'audit.json'), JSON.stringify(envelope.audit, null, 2), 'utf-8');
64
+ // Write files.json if present
65
+ if (envelope.files) {
66
+ await fs.writeFile(path.join(finalOutputPath, 'files.json'), JSON.stringify(envelope.files, null, 2), 'utf-8');
67
+ }
68
+ }
69
+ catch (error) {
70
+ throw new JmixError(`Failed to save envelope to ${finalOutputPath}`, error instanceof Error ? error : new Error(String(error)));
71
+ }
72
+ }
73
+ /**
74
+ * Build manifest component
75
+ */
76
+ buildManifest(config, timestamp) {
77
+ const manifest = {
78
+ jmix_version: config.version,
79
+ envelope_id: randomUUID(),
80
+ created_at: timestamp,
81
+ sender: config.sender,
82
+ receivers: config.receivers,
83
+ patient: config.patient,
84
+ security: config.security,
85
+ };
86
+ // Add optional fields
87
+ if (config.requester) {
88
+ manifest.requester = config.requester;
89
+ }
90
+ if (config.consent) {
91
+ manifest.consent = config.consent;
92
+ }
93
+ if (config.custom_tags) {
94
+ manifest.custom_tags = config.custom_tags;
95
+ }
96
+ if (config.report) {
97
+ manifest.report = config.report;
98
+ }
99
+ if (config.deid_keys) {
100
+ manifest.deid_keys = config.deid_keys;
101
+ }
102
+ return manifest;
103
+ }
104
+ /**
105
+ * Build metadata component
106
+ */
107
+ buildMetadata(config, dicomMetadata) {
108
+ return {
109
+ patient: config.patient,
110
+ study: {
111
+ description: dicomMetadata.study_description,
112
+ uid: dicomMetadata.study_uid,
113
+ date: dicomMetadata.study_date,
114
+ },
115
+ dicom: dicomMetadata,
116
+ custom_metadata: {},
117
+ };
118
+ }
119
+ /**
120
+ * Build audit component (audit trail)
121
+ */
122
+ buildAudit(transmissionId, timestamp, config) {
123
+ const firstReceiver = config.receivers && config.receivers[0];
124
+ const entry = {
125
+ event: 'envelope_created',
126
+ timestamp,
127
+ by: { id: config.sender.id, name: config.sender.name },
128
+ to: firstReceiver ? { id: firstReceiver.id, name: firstReceiver.name } : undefined,
129
+ };
130
+ const audit = {
131
+ audit: [entry],
132
+ };
133
+ return audit;
134
+ }
135
+ /**
136
+ * Load configuration from JSON file
137
+ */
138
+ static async loadConfig(configPath) {
139
+ try {
140
+ const configContent = await fs.readFile(configPath, 'utf-8');
141
+ return JSON.parse(configContent);
142
+ }
143
+ catch (error) {
144
+ throw new JmixError(`Failed to load configuration from ${configPath}`, error instanceof Error ? error : new Error(String(error)));
145
+ }
146
+ }
147
+ /**
148
+ * Get the schema validator instance
149
+ */
150
+ getValidator() {
151
+ return this.validator;
152
+ }
153
+ /**
154
+ * Get the DICOM processor instance
155
+ */
156
+ getDicomProcessor() {
157
+ return this.dicomProcessor;
158
+ }
159
+ /**
160
+ * Package an ENCRYPTED JMIX envelope directory:
161
+ * - Builds plaintext payload/ first (metadata.json + dicom/)
162
+ * - Computes payload_hash over payload/
163
+ * - Tars payload/, encrypts to payload.encrypted with AES-256-GCM via X25519+HKDF
164
+ * - Removes plaintext payload/ directory
165
+ * Layout:
166
+ * <outputRoot>/<envelope_id>.JMIX/
167
+ * manifest.json (includes security.encryption & payload_hash)
168
+ * audit.json
169
+ * payload.encrypted
170
+ */
171
+ async packageEncryptedToDirectory(dicomPath, config, outputRoot, recipientPublicKeyBase64) {
172
+ // Build the envelope first
173
+ const envelope = await this.buildFromDicom(dicomPath, config);
174
+ const packageRoot = path.resolve(outputRoot, `${envelope.manifest.envelope_id}.JMIX`);
175
+ const payloadDir = path.join(packageRoot, 'payload');
176
+ const dicomOutDir = path.join(payloadDir, 'dicom');
177
+ // Create directory structure and copy DICOM
178
+ await fs.mkdir(dicomOutDir, { recursive: true });
179
+ const allInputFiles = await this.walkFiles(dicomPath);
180
+ for (const absFile of allInputFiles) {
181
+ if (!(await this.isLikelyDicom(absFile)))
182
+ continue;
183
+ const rel = path.relative(dicomPath, absFile);
184
+ const dest = path.join(dicomOutDir, rel);
185
+ await fs.mkdir(path.dirname(dest), { recursive: true });
186
+ await fs.copyFile(absFile, dest);
187
+ }
188
+ // Write payload/metadata.json
189
+ await fs.mkdir(payloadDir, { recursive: true });
190
+ await fs.writeFile(path.join(payloadDir, 'metadata.json'), JSON.stringify(envelope.metadata, null, 2), 'utf-8');
191
+ // Compute payload hash over plaintext payload
192
+ const payloadHash = await this.computePayloadHash(payloadDir);
193
+ // Tar the payload directory to a temporary tar file under the package root
194
+ const tmpTarPath = path.join(packageRoot, 'payload.tmp.tar');
195
+ await tar.c({ cwd: packageRoot, file: tmpTarPath, portable: true, gzip: false }, ['payload']);
196
+ // Read tar and encrypt
197
+ const tarBuf = await fs.readFile(tmpTarPath);
198
+ const { ciphertext, result } = PayloadEncryptor.encryptTar(tarBuf, recipientPublicKeyBase64);
199
+ const encryptedPath = path.join(packageRoot, 'payload.encrypted');
200
+ await fs.writeFile(encryptedPath, ciphertext);
201
+ // Cleanup plaintext tar and payload directory
202
+ await fs.rm(tmpTarPath, { force: true });
203
+ await fs.rm(payloadDir, { recursive: true, force: true });
204
+ // Update manifest security
205
+ envelope.manifest.security = {
206
+ ...envelope.manifest.security,
207
+ payload_hash: payloadHash,
208
+ encryption: {
209
+ algorithm: 'AES-256-GCM',
210
+ ephemeral_public_key: result.ephemeral_public_key,
211
+ iv: result.iv,
212
+ auth_tag: result.auth_tag,
213
+ },
214
+ };
215
+ // Write manifest.json and audit.json
216
+ await fs.mkdir(packageRoot, { recursive: true });
217
+ await fs.writeFile(path.join(packageRoot, 'manifest.json'), JSON.stringify(envelope.manifest, null, 2), 'utf-8');
218
+ await fs.writeFile(path.join(packageRoot, 'audit.json'), JSON.stringify(envelope.audit, null, 2), 'utf-8');
219
+ return packageRoot;
220
+ }
221
+ /**
222
+ * Package a JMIX envelope to a directory structure that includes original DICOM files.
223
+ * Layout:
224
+ * <outputRoot>/<envelope_id>.JMIX/
225
+ * manifest.json
226
+ * audit.json
227
+ * payload/
228
+ * metadata.json
229
+ * dicom/ ... (copied from dicomPath, preserving structure)
230
+ * files/ (optional in future)
231
+ * files.json (optional in future)
232
+ */
233
+ async packageToDirectory(dicomPath, config, outputRoot) {
234
+ // Build the envelope first
235
+ const envelope = await this.buildFromDicom(dicomPath, config);
236
+ const packageRoot = path.resolve(outputRoot, `${envelope.manifest.envelope_id}.JMIX`);
237
+ const payloadDir = path.join(packageRoot, 'payload');
238
+ const dicomOutDir = path.join(payloadDir, 'dicom');
239
+ // Create directory structure
240
+ await fs.mkdir(dicomOutDir, { recursive: true });
241
+ // Copy DICOM files preserving relative structure
242
+ const allInputFiles = await this.walkFiles(dicomPath);
243
+ for (const absFile of allInputFiles) {
244
+ if (!(await this.isLikelyDicom(absFile)))
245
+ continue;
246
+ const rel = path.relative(dicomPath, absFile);
247
+ const dest = path.join(dicomOutDir, rel);
248
+ await fs.mkdir(path.dirname(dest), { recursive: true });
249
+ await fs.copyFile(absFile, dest);
250
+ }
251
+ // Write payload/metadata.json
252
+ await fs.mkdir(payloadDir, { recursive: true });
253
+ await fs.writeFile(path.join(payloadDir, 'metadata.json'), JSON.stringify(envelope.metadata, null, 2), 'utf-8');
254
+ // Compute payload hash over all files under payload/
255
+ const payloadHash = await this.computePayloadHash(payloadDir);
256
+ envelope.manifest.security = {
257
+ ...envelope.manifest.security,
258
+ payload_hash: payloadHash,
259
+ };
260
+ // Write manifest.json and audit.json at package root
261
+ await fs.writeFile(path.join(packageRoot, 'manifest.json'), JSON.stringify(envelope.manifest, null, 2), 'utf-8');
262
+ await fs.writeFile(path.join(packageRoot, 'audit.json'), JSON.stringify(envelope.audit, null, 2), 'utf-8');
263
+ return packageRoot;
264
+ }
265
+ // Recursively list all files under a directory (files only)
266
+ async walkFiles(root) {
267
+ const out = [];
268
+ const entries = await fs.readdir(root, { withFileTypes: true });
269
+ for (const entry of entries) {
270
+ const full = path.join(root, entry.name);
271
+ if (entry.isDirectory()) {
272
+ out.push(...(await this.walkFiles(full)));
273
+ }
274
+ else if (entry.isFile()) {
275
+ out.push(full);
276
+ }
277
+ }
278
+ return out;
279
+ }
280
+ // Basic DICOM detection: extension or DICM magic at offset 128
281
+ async isLikelyDicom(filePath) {
282
+ try {
283
+ const ext = path.extname(filePath).toLowerCase();
284
+ if (ext === '.dcm' || ext === '.dicom')
285
+ return true;
286
+ const fh = await fs.open(filePath, 'r');
287
+ try {
288
+ const buf = Buffer.alloc(4);
289
+ await fh.read(buf, 0, 4, 128);
290
+ return buf.toString('ascii') === 'DICM';
291
+ }
292
+ finally {
293
+ await fh.close();
294
+ }
295
+ }
296
+ catch {
297
+ return false;
298
+ }
299
+ }
300
+ // Deterministic payload hash: sha256 over ordered (path + newline + bytes) for all files under payload/
301
+ async computePayloadHash(payloadDir) {
302
+ const all = await this.walkFiles(payloadDir);
303
+ const rels = all
304
+ .map((abs) => ({ abs, rel: path.posix.join(...path.relative(payloadDir, abs).split(path.sep)) }))
305
+ .sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
306
+ const hash = createHash('sha256');
307
+ for (const f of rels) {
308
+ hash.update(Buffer.from(f.rel + '\n', 'utf-8'));
309
+ const data = await fs.readFile(f.abs);
310
+ hash.update(data);
311
+ }
312
+ const digestHex = hash.digest('hex');
313
+ return `sha256:${digestHex}`;
314
+ }
315
+ /**
316
+ * Decrypt an encrypted JMIX envelope in place: restores a plaintext payload/ directory.
317
+ * - envelopeDir: path to <id>.JMIX directory
318
+ * - recipientPrivateKeyBase64: base64 Curve25519 private key (32 bytes)
319
+ * Returns the extracted payload directory path.
320
+ */
321
+ async decryptEnvelope(envelopeDir, recipientPrivateKeyBase64) {
322
+ const manifestPath = path.join(envelopeDir, 'manifest.json');
323
+ const encryptedPath = path.join(envelopeDir, 'payload.encrypted');
324
+ // Read manifest and encryption info
325
+ const manifestRaw = await fs.readFile(manifestPath, 'utf-8');
326
+ const manifest = JSON.parse(manifestRaw);
327
+ const enc = manifest?.security?.encryption;
328
+ if (!enc || !enc.ephemeral_public_key || !enc.iv || !enc.auth_tag) {
329
+ throw new Error('Envelope is not encrypted or missing encryption metadata');
330
+ }
331
+ // Read ciphertext
332
+ const ciphertext = await fs.readFile(encryptedPath);
333
+ // Decrypt to tar buffer
334
+ const tarBuf = PayloadDecryptor.decryptToBuffer(ciphertext, enc.ephemeral_public_key, enc.iv, enc.auth_tag, recipientPrivateKeyBase64);
335
+ // Write tar to temp and extract to payload/
336
+ const tmpTar = path.join(envelopeDir, 'payload.decrypted.tar');
337
+ await fs.writeFile(tmpTar, tarBuf);
338
+ const payloadDir = path.join(envelopeDir, 'payload');
339
+ await fs.mkdir(payloadDir, { recursive: true });
340
+ await tar.x({ cwd: envelopeDir, file: tmpTar });
341
+ // Remove temp tar
342
+ await fs.rm(tmpTar, { force: true });
343
+ // Optional: verify payload hash
344
+ if (manifest?.security?.payload_hash) {
345
+ const computed = await this.computePayloadHash(payloadDir);
346
+ if (computed !== manifest.security.payload_hash) {
347
+ throw new Error(`Payload hash mismatch: expected ${manifest.security.payload_hash} but got ${computed}`);
348
+ }
349
+ }
350
+ return payloadDir;
351
+ }
352
+ }
353
+ //# sourceMappingURL=JmixBuilder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JmixBuilder.js","sourceRoot":"","sources":["../src/JmixBuilder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export declare class PayloadDecryptor {
2
+ /**
3
+ * Decrypt AES-256-GCM ciphertext using X25519 + HKDF-SHA256 key derivation.
4
+ * - ephPubB64: base64 sender ephemeral public key (32 bytes)
5
+ * - ivB64: base64 12-byte IV
6
+ * - tagB64: base64 16-byte auth tag
7
+ * - recipientPrivB64: base64 32-byte recipient private key
8
+ * Returns plaintext Buffer (tar data).
9
+ */
10
+ static decryptToBuffer(ciphertext: Buffer, ephPubB64: string, ivB64: string, tagB64: string, recipientPrivB64: string): Buffer;
11
+ }
12
+ //# sourceMappingURL=PayloadDecryptor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PayloadDecryptor.d.ts","sourceRoot":"","sources":["../../src/crypto/PayloadDecryptor.ts"],"names":[],"mappings":"AAGA,qBAAa,gBAAgB;IAC3B;;;;;;;OAOG;IACH,MAAM,CAAC,eAAe,CACpB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,GACvB,MAAM;CA0BV"}
@@ -0,0 +1,39 @@
1
+ import { createDecipheriv, hkdfSync } from 'crypto';
2
+ import nacl from 'tweetnacl';
3
+ export class PayloadDecryptor {
4
+ /**
5
+ * Decrypt AES-256-GCM ciphertext using X25519 + HKDF-SHA256 key derivation.
6
+ * - ephPubB64: base64 sender ephemeral public key (32 bytes)
7
+ * - ivB64: base64 12-byte IV
8
+ * - tagB64: base64 16-byte auth tag
9
+ * - recipientPrivB64: base64 32-byte recipient private key
10
+ * Returns plaintext Buffer (tar data).
11
+ */
12
+ static decryptToBuffer(ciphertext, ephPubB64, ivB64, tagB64, recipientPrivB64) {
13
+ const ephPub = Buffer.from(ephPubB64, 'base64');
14
+ const iv = Buffer.from(ivB64, 'base64');
15
+ const tag = Buffer.from(tagB64, 'base64');
16
+ const recipPriv = Buffer.from(recipientPrivB64, 'base64');
17
+ if (ephPub.length !== 32)
18
+ throw new Error('Invalid ephemeral public key');
19
+ if (recipPriv.length !== 32)
20
+ throw new Error('Invalid recipient private key');
21
+ if (iv.length !== 12)
22
+ throw new Error('Invalid IV length (expected 12)');
23
+ if (tag.length !== 16)
24
+ throw new Error('Invalid auth tag length (expected 16)');
25
+ // X25519 shared secret
26
+ const shared = nacl.scalarMult(new Uint8Array(recipPriv), new Uint8Array(ephPub));
27
+ // Derive 32-byte AES key with HKDF-SHA256
28
+ const salt = Buffer.alloc(0);
29
+ const info = Buffer.from('JMIX-Payload-Encryption');
30
+ const hkdfOut = hkdfSync('sha256', Buffer.from(shared), salt, info, 32);
31
+ const key = Buffer.from(hkdfOut);
32
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
33
+ decipher.setAuthTag(tag);
34
+ const dec1 = decipher.update(ciphertext);
35
+ const dec2 = decipher.final();
36
+ return Buffer.concat([dec1, dec2]);
37
+ }
38
+ }
39
+ //# sourceMappingURL=PayloadDecryptor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PayloadDecryptor.js","sourceRoot":"","sources":["../../src/crypto/PayloadDecryptor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACpD,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,OAAO,gBAAgB;IAC3B;;;;;;;OAOG;IACH,MAAM,CAAC,eAAe,CACpB,UAAkB,EAClB,SAAiB,EACjB,KAAa,EACb,MAAc,EACd,gBAAwB;QAExB,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAChD,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QACxC,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QAC1C,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,QAAQ,CAAC,CAAC;QAE1D,IAAI,MAAM,CAAC,MAAM,KAAK,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;QAC1E,IAAI,SAAS,CAAC,MAAM,KAAK,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QAC9E,IAAI,EAAE,CAAC,MAAM,KAAK,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;QACzE,IAAI,GAAG,CAAC,MAAM,KAAK,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;QAEhF,uBAAuB;QACvB,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC,SAAS,CAAC,EAAE,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;QAElF,0CAA0C;QAC1C,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;QACpD,MAAM,OAAO,GAAG,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;QACxE,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAEjC,MAAM,QAAQ,GAAG,gBAAgB,CAAC,aAAa,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;QAC1D,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QACzB,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACzC,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAC;QAC9B,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IACrC,CAAC;CACF"}