@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
@@ -0,0 +1,262 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { DicomError, DicomMetadata, Config } from '../types/index.js';
4
+ import daikon from 'daikon';
5
+
6
+ const DICOM_MAGIC_OFFSET = 128;
7
+ const DICOM_MAGIC_SIGNATURE = 'DICM';
8
+
9
+ function toArrayBuffer(buf: Buffer): ArrayBuffer {
10
+ const ab = new ArrayBuffer(buf.byteLength);
11
+ const view = new Uint8Array(ab);
12
+ view.set(buf);
13
+ return ab;
14
+ }
15
+
16
+ export class DicomProcessor {
17
+ /**
18
+ * Process a DICOM directory and extract metadata
19
+ */
20
+ async processDicomFolder(
21
+ dicomPath: string,
22
+ config?: Config
23
+ ): Promise<DicomMetadata> {
24
+ try {
25
+ const dicomFiles = await this.findDicomFiles(dicomPath);
26
+
27
+ if (dicomFiles.length === 0) {
28
+ // Fallback to config data if no DICOM files found
29
+ return this.createMetadataFromConfig(config);
30
+ }
31
+
32
+ // Parse files with Daikon and group by series
33
+ const images: any[] = [];
34
+ const seriesMap: Map<string, any[]> = new Map();
35
+ const modalitiesSet: Set<string> = new Set();
36
+
37
+ for (const f of dicomFiles) {
38
+ try {
39
+ const buf = await fs.readFile(f);
40
+ const view = new DataView(toArrayBuffer(buf));
41
+ const image = daikon.Series.parseImage(view);
42
+ if (!image) continue;
43
+ images.push(image);
44
+
45
+ const seriesId = image.getSeriesId ? image.getSeriesId() : path.dirname(f);
46
+ const arr = seriesMap.get(seriesId) || [];
47
+ arr.push(image);
48
+ seriesMap.set(seriesId, arr);
49
+
50
+ // Try to read modality if available
51
+ try {
52
+ const modality = (image.getModality && image.getModality()) || this.readTag(image, 0x0008, 0x0060) || 'UNKNOWN';
53
+ if (modality) modalitiesSet.add(String(modality));
54
+ } catch {
55
+ // ignore modality errors
56
+ }
57
+ } catch {
58
+ // skip unreadable file
59
+ }
60
+ }
61
+
62
+ const series: DicomMetadata['series'] = [];
63
+ for (const [sid, imgs] of seriesMap.entries()) {
64
+ series.push({
65
+ series_uid: String(sid),
66
+ modality: 'UNKNOWN',
67
+ instance_count: imgs.length,
68
+ });
69
+ }
70
+
71
+ // Attempt to extract some study-level tags from the first image
72
+ let study_description: string | undefined = undefined;
73
+ let study_uid: string | undefined = undefined;
74
+ let study_date: string | undefined = undefined;
75
+ let patient_name: string | undefined = config?.patient.name;
76
+ let patient_id: string | undefined = config?.patient.id;
77
+ let patient_dob: string | undefined = config?.patient.dob;
78
+ let patient_sex: string | undefined = config?.patient.sex;
79
+
80
+ const first = images[0];
81
+ if (first) {
82
+ study_description = this.readTag(first, 0x0008, 0x1030) || 'Study';
83
+ study_uid = this.readTag(first, 0x0020, 0x000D) || this.generateStudyUID();
84
+ const sd = this.readTag(first, 0x0008, 0x0020);
85
+ study_date = sd && String(sd).length === 8 ? DicomProcessor.formatDicomDate(String(sd)) : new Date().toISOString().split('T')[0];
86
+ patient_name = this.readTag(first, 0x0010, 0x0010) || patient_name;
87
+ patient_id = this.readTag(first, 0x0010, 0x0020) || patient_id;
88
+ const dob = this.readTag(first, 0x0010, 0x0030);
89
+ patient_dob = dob && String(dob).length === 8 ? DicomProcessor.formatDicomDate(String(dob)) : patient_dob;
90
+ patient_sex = this.readTag(first, 0x0010, 0x0040) || patient_sex;
91
+ }
92
+
93
+ const metadata: DicomMetadata = {
94
+ patient_name,
95
+ patient_id,
96
+ patient_dob,
97
+ patient_sex,
98
+ study_description,
99
+ study_uid,
100
+ study_date,
101
+ modalities: Array.from(modalitiesSet.size ? modalitiesSet : new Set(['UNKNOWN'])),
102
+ series_count: seriesMap.size || 1,
103
+ instance_count: images.length,
104
+ series,
105
+ };
106
+
107
+ return metadata;
108
+ } catch (error) {
109
+ throw new DicomError(
110
+ `Failed to process DICOM folder: ${dicomPath}`,
111
+ error instanceof Error ? error : new Error(String(error))
112
+ );
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Recursively find DICOM files in a directory
118
+ */
119
+ private async findDicomFiles(dirPath: string): Promise<string[]> {
120
+ const dicomFiles: string[] = [];
121
+
122
+ try {
123
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
124
+
125
+ for (const entry of entries) {
126
+ const fullPath = path.join(dirPath, entry.name);
127
+
128
+ if (entry.isDirectory()) {
129
+ // Recursively search subdirectories
130
+ const subDirFiles = await this.findDicomFiles(fullPath);
131
+ dicomFiles.push(...subDirFiles);
132
+ } else if (entry.isFile()) {
133
+ if (await this.isDicomFile(fullPath)) {
134
+ dicomFiles.push(fullPath);
135
+ }
136
+ }
137
+ }
138
+ } catch (error) {
139
+ throw new DicomError(
140
+ `Failed to read directory: ${dirPath}`,
141
+ error instanceof Error ? error : new Error(String(error))
142
+ );
143
+ }
144
+
145
+ return dicomFiles;
146
+ }
147
+
148
+ /**
149
+ * Check if a file is a DICOM file by checking the magic signature
150
+ */
151
+ private async isDicomFile(filePath: string): Promise<boolean> {
152
+ try {
153
+ // Check file extension first (quick check)
154
+ const ext = path.extname(filePath).toLowerCase();
155
+ if (['.dcm', '.dicom'].includes(ext)) {
156
+ return true;
157
+ }
158
+
159
+ // Check for DICM magic number at offset 128
160
+ const fileHandle = await fs.open(filePath, 'r');
161
+ const buffer = Buffer.alloc(4);
162
+ await fileHandle.read(buffer, 0, 4, DICOM_MAGIC_OFFSET);
163
+ await fileHandle.close();
164
+
165
+ return buffer.toString('ascii') === DICOM_MAGIC_SIGNATURE;
166
+ } catch {
167
+ // If we can't read the file, assume it's not a DICOM file
168
+ return false;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Create metadata from config when DICOM parsing fails or no files found
174
+ */
175
+ private createMetadataFromConfig(config?: Config): DicomMetadata {
176
+ return {
177
+ patient_name: config?.patient.name,
178
+ patient_id: config?.patient.id,
179
+ patient_dob: config?.patient.dob,
180
+ patient_sex: config?.patient.sex,
181
+ study_description: 'Study from configuration',
182
+ study_uid: this.generateStudyUID(),
183
+ study_date: new Date().toISOString().split('T')[0],
184
+ modalities: ['UNKNOWN'],
185
+ series_count: 1,
186
+ instance_count: 0,
187
+ series: [],
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Generate a study UID (placeholder implementation)
193
+ */
194
+ private generateStudyUID(): string {
195
+ // Simple UID generation - in production would use proper DICOM UID format
196
+ const timestamp = Date.now();
197
+ const random = Math.floor(Math.random() * 10000);
198
+ return `1.2.3.${timestamp}.${random}`;
199
+ }
200
+
201
+ /**
202
+ * Estimate series count based on file naming patterns or directory structure
203
+ */
204
+ private estimateSeriesCount(files: string[]): number {
205
+ // Simple estimation based on directory structure
206
+ const directories = new Set<string>();
207
+
208
+ for (const file of files) {
209
+ directories.add(path.dirname(file));
210
+ }
211
+
212
+ return Math.max(1, directories.size);
213
+ }
214
+
215
+ /**
216
+ * Attempt to read a DICOM tag value using Daikon's API (best-effort)
217
+ */
218
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
219
+ private readTag(image: any, group: number, element: number): string | undefined {
220
+ try {
221
+ if (image && image.getTag && typeof image.getTag === 'function') {
222
+ const tag = image.getTag(group, element);
223
+ if (tag && tag.value && tag.value.length) {
224
+ return String(tag.value[0]);
225
+ }
226
+ }
227
+ } catch {
228
+ // ignore
229
+ }
230
+ return undefined;
231
+ }
232
+
233
+ /**
234
+ * Format DICOM date to ISO format
235
+ */
236
+ static formatDicomDate(dicomDate: string): string {
237
+ if (!dicomDate || dicomDate.length !== 8) {
238
+ return new Date().toISOString().split('T')[0];
239
+ }
240
+
241
+ const year = dicomDate.substring(0, 4);
242
+ const month = dicomDate.substring(4, 6);
243
+ const day = dicomDate.substring(6, 8);
244
+
245
+ return `${year}-${month}-${day}`;
246
+ }
247
+
248
+ /**
249
+ * Format DICOM person name to readable format
250
+ */
251
+ static formatDicomPersonName(dicomName: string): string {
252
+ if (!dicomName) return '';
253
+
254
+ // DICOM names are in format: "Last^First^Middle^Prefix^Suffix"
255
+ const parts = dicomName.split('^');
256
+ const last = parts[0] || '';
257
+ const first = parts[1] || '';
258
+ const middle = parts[2] || '';
259
+
260
+ return [first, middle, last].filter(Boolean).join(' ').trim();
261
+ }
262
+ }
package/src/index.ts ADDED
@@ -0,0 +1,30 @@
1
+ // JMIX TypeScript Library - Main Entry Point
2
+ // JSON Medical Interchange format for secure medical data exchange
3
+
4
+ export { JmixBuilder, type JmixBuilderOptions } from './JmixBuilder.js';
5
+ export {
6
+ SchemaValidator,
7
+ type SchemaValidatorOptions,
8
+ } from './validation/SchemaValidator.js';
9
+ export { DicomProcessor } from './dicom/DicomProcessor.js';
10
+
11
+ // Export all types
12
+ export * from './types/index.js';
13
+
14
+ // Re-export key types for convenience
15
+ export type {
16
+ JmixEnvelope,
17
+ Config,
18
+ Manifest,
19
+ Metadata,
20
+ Audit,
21
+ Files,
22
+ DicomMetadata,
23
+ Patient,
24
+ ContactVariant,
25
+ Contact,
26
+ Security,
27
+ Consent,
28
+ Encryption,
29
+ SenderAssertion,
30
+ } from './types/index.js';
@@ -0,0 +1 @@
1
+ declare module 'daikon';
@@ -0,0 +1,201 @@
1
+ // JMIX Core Types - TypeScript interfaces for JMIX envelope components
2
+
3
+ export interface Contact {
4
+ system?: string;
5
+ value: string;
6
+ use_field?: string;
7
+ }
8
+
9
+ export interface ContactVariant {
10
+ name: string;
11
+ id: string;
12
+ contact: string | Contact;
13
+ }
14
+
15
+ export interface PatientIdentifier {
16
+ system: string;
17
+ value: string;
18
+ use_field?: string;
19
+ }
20
+
21
+ export interface Patient {
22
+ name: string;
23
+ id: string;
24
+ dob?: string;
25
+ sex?: 'M' | 'F' | 'O' | 'U';
26
+ identifiers?: PatientIdentifier[];
27
+ }
28
+
29
+ export interface Consent {
30
+ status: 'granted' | 'denied' | 'pending';
31
+ scope?: string[];
32
+ method?: string;
33
+ }
34
+
35
+ export interface Security {
36
+ classification: 'public' | 'internal' | 'confidential' | 'restricted';
37
+ // Optional SHA-256 digest of the payload/ directory, formatted as "sha256:<hex>"
38
+ payload_hash?: string;
39
+ }
40
+
41
+ export interface Encryption {
42
+ algorithm: 'AES-256-GCM';
43
+ ephemeral_public_key: string; // base64 encoded
44
+ iv: string; // base64 encoded
45
+ auth_tag: string; // base64 encoded
46
+ }
47
+
48
+ export interface SenderAssertion {
49
+ signing_key: {
50
+ alg: string;
51
+ public_key: string;
52
+ fingerprint: string;
53
+ };
54
+ key_reference?: string;
55
+ signed_fields: string[];
56
+ signature: string;
57
+ expires_at?: string;
58
+ directory_attestation?: {
59
+ provider: string;
60
+ attestation_signature: string;
61
+ attestation_timestamp: string;
62
+ attestation_public_key: string;
63
+ };
64
+ }
65
+
66
+ export interface Manifest {
67
+ jmix_version: string;
68
+ envelope_id: string;
69
+ created_at: string;
70
+ sender: ContactVariant & { assertion?: SenderAssertion };
71
+ requester?: ContactVariant & { assertion?: SenderAssertion };
72
+ receivers: ContactVariant[];
73
+ patient: Patient;
74
+ security: Security;
75
+ consent?: Consent;
76
+ custom_tags?: string[];
77
+ report?: {
78
+ file: string;
79
+ };
80
+ deid_keys?: string[];
81
+ encryption?: Encryption;
82
+ }
83
+
84
+ export interface DicomMetadata {
85
+ patient_name?: string;
86
+ patient_id?: string;
87
+ patient_dob?: string;
88
+ patient_sex?: string;
89
+ study_description?: string;
90
+ study_uid?: string;
91
+ study_date?: string;
92
+ modalities: string[];
93
+ series_count: number;
94
+ instance_count: number;
95
+ series?: Array<{
96
+ series_uid: string;
97
+ series_description?: string;
98
+ modality: string;
99
+ instance_count: number;
100
+ }>;
101
+ }
102
+
103
+ export interface Metadata {
104
+ patient: Patient;
105
+ study: {
106
+ description?: string;
107
+ uid?: string;
108
+ date?: string;
109
+ };
110
+ dicom: DicomMetadata;
111
+ custom_metadata?: Record<string, unknown>;
112
+ }
113
+
114
+ export interface AuditEntityRef {
115
+ id: string;
116
+ name?: string;
117
+ }
118
+
119
+ export interface AuditEntry {
120
+ event: string;
121
+ by: AuditEntityRef;
122
+ to?: AuditEntityRef;
123
+ timestamp: string; // date-time
124
+ assertion?: SenderAssertion;
125
+ }
126
+
127
+ export interface Audit {
128
+ audit: AuditEntry[];
129
+ }
130
+
131
+ export interface FileEntry {
132
+ path: string;
133
+ size: number;
134
+ hash: string;
135
+ mime_type?: string;
136
+ description?: string;
137
+ }
138
+
139
+ export interface Files {
140
+ payload_directory: string;
141
+ files: FileEntry[];
142
+ total_size: number;
143
+ file_count: number;
144
+ }
145
+
146
+ export interface JmixEnvelope {
147
+ manifest: Manifest;
148
+ metadata: Metadata;
149
+ audit: Audit;
150
+ files?: Files;
151
+ }
152
+
153
+ export interface Config {
154
+ version: string;
155
+ sender: ContactVariant;
156
+ requester?: ContactVariant;
157
+ receivers: ContactVariant[];
158
+ patient: Patient;
159
+ security: Security;
160
+ consent?: Consent;
161
+ custom_tags?: string[];
162
+ report?: {
163
+ file: string;
164
+ };
165
+ deid_keys?: string[];
166
+ }
167
+
168
+ // Error types
169
+ export class JmixError extends Error {
170
+ constructor(
171
+ message: string,
172
+ public readonly cause?: Error
173
+ ) {
174
+ super(message);
175
+ this.name = 'JmixError';
176
+ }
177
+ }
178
+
179
+ export class ValidationError extends JmixError {
180
+ constructor(
181
+ message: string,
182
+ public readonly errors: string[] = []
183
+ ) {
184
+ super(message);
185
+ this.name = 'ValidationError';
186
+ }
187
+ }
188
+
189
+ export class CryptographyError extends JmixError {
190
+ constructor(message: string, cause?: Error) {
191
+ super(message, cause);
192
+ this.name = 'CryptographyError';
193
+ }
194
+ }
195
+
196
+ export class DicomError extends JmixError {
197
+ constructor(message: string, cause?: Error) {
198
+ super(message, cause);
199
+ this.name = 'DicomError';
200
+ }
201
+ }
@@ -0,0 +1,156 @@
1
+ import Ajv from 'ajv';
2
+ import addFormats from 'ajv-formats';
3
+ import * as fs from 'fs/promises';
4
+ import * as path from 'path';
5
+ import { ValidationError } from '../types/index.js';
6
+
7
+ export interface SchemaValidatorOptions {
8
+ schemaPath?: string;
9
+ strictMode?: boolean;
10
+ }
11
+
12
+ export class SchemaValidator {
13
+ private ajv: Ajv;
14
+ private schemaPath: string;
15
+ private schemaCache: Map<string, object> = new Map();
16
+
17
+ constructor(options: SchemaValidatorOptions = {}) {
18
+ this.schemaPath =
19
+ options.schemaPath ||
20
+ process.env.JMIX_SCHEMA_PATH ||
21
+ path.resolve(process.cwd(), '../jmix/schemas');
22
+
23
+ this.ajv = new Ajv({
24
+ allErrors: true,
25
+ strict: options.strictMode ?? true,
26
+ loadSchema: this.loadSchema.bind(this),
27
+ });
28
+
29
+ addFormats(this.ajv);
30
+ }
31
+
32
+ private async loadSchema(uri: string): Promise<object> {
33
+ if (this.schemaCache.has(uri)) {
34
+ return this.schemaCache.get(uri)!;
35
+ }
36
+
37
+ try {
38
+ const schemaContent = await fs.readFile(uri, 'utf-8');
39
+ const schema = JSON.parse(schemaContent);
40
+ this.schemaCache.set(uri, schema);
41
+ return schema;
42
+ } catch (error) {
43
+ throw new ValidationError(`Failed to load schema from ${uri}`, [
44
+ error instanceof Error ? error.message : String(error),
45
+ ]);
46
+ }
47
+ }
48
+
49
+ private getSchemaPath(schemaName: string): string {
50
+ return path.join(this.schemaPath, `${schemaName}.schema.json`);
51
+ }
52
+
53
+ async validateManifest(data: unknown): Promise<void> {
54
+ await this.validate('manifest', data);
55
+ }
56
+
57
+ async validateMetadata(data: unknown): Promise<void> {
58
+ await this.validate('metadata', data);
59
+ }
60
+
61
+ async validateAudit(data: unknown): Promise<void> {
62
+ await this.validate('audit', data);
63
+ }
64
+
65
+ async validateFiles(data: unknown): Promise<void> {
66
+ await this.validate('files', data);
67
+ }
68
+
69
+ private async validate(schemaName: string, data: unknown): Promise<void> {
70
+ const schemaPath = this.getSchemaPath(schemaName);
71
+
72
+ try {
73
+ // Check if schema file exists
74
+ await fs.access(schemaPath);
75
+ } catch {
76
+ // Schema file doesn't exist, skip validation gracefully
77
+ console.warn(`Schema validation skipped: ${schemaPath} not found`);
78
+ return;
79
+ }
80
+
81
+ try {
82
+ const schema = await this.loadSchema(schemaPath);
83
+ const validate = this.ajv.compile(schema);
84
+ const valid = validate(data);
85
+
86
+ if (!valid) {
87
+ const errors =
88
+ validate.errors?.map((error) => {
89
+ const instancePath = error.instancePath || 'root';
90
+ return `${instancePath}: ${error.message}`;
91
+ }) || [];
92
+
93
+ throw new ValidationError(`${schemaName} validation failed`, errors);
94
+ }
95
+ } catch (error) {
96
+ if (
97
+ error instanceof ValidationError &&
98
+ error.message.includes('validation failed')
99
+ ) {
100
+ throw error;
101
+ }
102
+
103
+ // For schema loading errors, just skip validation gracefully
104
+ console.warn(
105
+ `Schema validation error for ${schemaName}, skipping: ${error instanceof Error ? error.message : String(error)}`
106
+ );
107
+ return;
108
+ }
109
+ }
110
+
111
+ async validateEnvelope(envelope: {
112
+ manifest: unknown;
113
+ metadata: unknown;
114
+ audit: unknown;
115
+ files?: unknown;
116
+ }): Promise<void> {
117
+ const validationPromises = [
118
+ this.validateManifest(envelope.manifest),
119
+ this.validateMetadata(envelope.metadata),
120
+ this.validateAudit(envelope.audit),
121
+ ];
122
+
123
+ if (envelope.files) {
124
+ validationPromises.push(this.validateFiles(envelope.files));
125
+ }
126
+
127
+ try {
128
+ await Promise.all(validationPromises);
129
+ } catch (error) {
130
+ if (error instanceof ValidationError) {
131
+ throw error;
132
+ }
133
+ throw new ValidationError('Envelope validation failed', [
134
+ error instanceof Error ? error.message : String(error),
135
+ ]);
136
+ }
137
+ }
138
+
139
+ // Check if schema files are available
140
+ async isSchemaAvailable(schemaName?: string): Promise<boolean> {
141
+ if (schemaName) {
142
+ try {
143
+ await fs.access(this.getSchemaPath(schemaName));
144
+ return true;
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+
150
+ // Check if any schema files are available
151
+ const schemaNames = ['manifest', 'metadata', 'audit', 'files'];
152
+ const checks = schemaNames.map((name) => this.isSchemaAvailable(name));
153
+ const results = await Promise.all(checks);
154
+ return results.some((available) => available);
155
+ }
156
+ }
@@ -0,0 +1,10 @@
1
+ import { JmixBuilder } from './dist/index.js';
2
+ import nacl from 'tweetnacl';
3
+
4
+ (async () => {
5
+ const config = await JmixBuilder.loadConfig('./samples/sample_config.json');
6
+ const builder = new JmixBuilder();
7
+ const kp = nacl.box.keyPair();
8
+ const out = await builder.packageEncryptedToDirectory('./samples/study_1', config, './tmp', Buffer.from(kp.publicKey).toString('base64'));
9
+ console.log('RESULT', out);
10
+ })().catch(e => { console.error('ERR', e); process.exit(1); });