@fin.cx/skr 1.0.0 → 1.2.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.
- package/dist_ts/index.d.ts +6 -0
- package/dist_ts/index.js +7 -1
- package/dist_ts/plugins.d.ts +8 -1
- package/dist_ts/plugins.js +10 -2
- package/dist_ts/skr.api.d.ts +70 -0
- package/dist_ts/skr.api.js +354 -3
- package/dist_ts/skr.classes.journalentry.d.ts +1 -0
- package/dist_ts/skr.classes.journalentry.js +55 -15
- package/dist_ts/skr.classes.ledger.d.ts +4 -0
- package/dist_ts/skr.classes.ledger.js +72 -1
- package/dist_ts/skr.classes.reports.js +56 -22
- package/dist_ts/skr.export.accounts.d.ts +53 -0
- package/dist_ts/skr.export.accounts.js +111 -0
- package/dist_ts/skr.export.balances.d.ts +59 -0
- package/dist_ts/skr.export.balances.js +205 -0
- package/dist_ts/skr.export.d.ts +110 -0
- package/dist_ts/skr.export.js +315 -0
- package/dist_ts/skr.export.ledger.d.ts +95 -0
- package/dist_ts/skr.export.ledger.js +164 -0
- package/dist_ts/skr.export.pdf.d.ts +82 -0
- package/dist_ts/skr.export.pdf.js +548 -0
- package/dist_ts/skr.invoice.adapter.d.ts +98 -0
- package/dist_ts/skr.invoice.adapter.js +476 -0
- package/dist_ts/skr.invoice.booking.d.ts +102 -0
- package/dist_ts/skr.invoice.booking.js +556 -0
- package/dist_ts/skr.invoice.entity.d.ts +287 -0
- package/dist_ts/skr.invoice.entity.js +2 -0
- package/dist_ts/skr.invoice.mapper.d.ts +69 -0
- package/dist_ts/skr.invoice.mapper.js +401 -0
- package/dist_ts/skr.invoice.storage.d.ts +140 -0
- package/dist_ts/skr.invoice.storage.js +529 -0
- package/dist_ts/skr.security.d.ts +65 -0
- package/dist_ts/skr.security.js +319 -0
- package/dist_ts/skr.types.d.ts +1 -0
- package/package.json +18 -12
- package/readme.md +461 -132
- package/ts/index.ts +6 -0
- package/ts/plugins.ts +22 -1
- package/ts/skr.api.ts +489 -2
- package/ts/skr.classes.journalentry.ts +63 -14
- package/ts/skr.classes.ledger.ts +85 -0
- package/ts/skr.classes.reports.ts +64 -21
- package/ts/skr.export.accounts.ts +154 -0
- package/ts/skr.export.balances.ts +270 -0
- package/ts/skr.export.ledger.ts +249 -0
- package/ts/skr.export.pdf.ts +601 -0
- package/ts/skr.export.ts +443 -0
- package/ts/skr.invoice.adapter.ts +581 -0
- package/ts/skr.invoice.booking.ts +738 -0
- package/ts/skr.invoice.entity.ts +351 -0
- package/ts/skr.invoice.mapper.ts +486 -0
- package/ts/skr.invoice.storage.ts +710 -0
- package/ts/skr.security.ts +405 -0
- package/ts/skr.types.ts +1 -0
package/ts/skr.export.ts
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import type { IAccountData, ITransactionData, IJournalEntry, TSKRType } from './skr.types.js';
|
|
4
|
+
|
|
5
|
+
export interface IExportOptions {
|
|
6
|
+
exportPath: string;
|
|
7
|
+
fiscalYear: number;
|
|
8
|
+
dateFrom: Date;
|
|
9
|
+
dateTo: Date;
|
|
10
|
+
includeDocuments?: boolean;
|
|
11
|
+
generatePdfReports?: boolean;
|
|
12
|
+
signExport?: boolean;
|
|
13
|
+
timestampExport?: boolean;
|
|
14
|
+
companyInfo?: {
|
|
15
|
+
name: string;
|
|
16
|
+
taxId: string;
|
|
17
|
+
registrationNumber?: string;
|
|
18
|
+
address?: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IExportMetadata {
|
|
23
|
+
exportVersion: string;
|
|
24
|
+
exportTimestamp: string;
|
|
25
|
+
generator: {
|
|
26
|
+
name: string;
|
|
27
|
+
version: string;
|
|
28
|
+
};
|
|
29
|
+
company?: {
|
|
30
|
+
name: string;
|
|
31
|
+
taxId: string;
|
|
32
|
+
registrationNumber?: string;
|
|
33
|
+
address?: string;
|
|
34
|
+
};
|
|
35
|
+
fiscalYear: number;
|
|
36
|
+
dateRange: {
|
|
37
|
+
from: string;
|
|
38
|
+
to: string;
|
|
39
|
+
};
|
|
40
|
+
skrType: TSKRType;
|
|
41
|
+
schemaVersion: string;
|
|
42
|
+
crypto: {
|
|
43
|
+
digestAlgorithms: string[];
|
|
44
|
+
signatureType?: string;
|
|
45
|
+
timestampPolicy?: string;
|
|
46
|
+
merkleTree: boolean;
|
|
47
|
+
};
|
|
48
|
+
options: {
|
|
49
|
+
packagedAs: 'bagit';
|
|
50
|
+
compression: 'none' | 'deflate';
|
|
51
|
+
deduplication: boolean;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface IBagItManifest {
|
|
56
|
+
[filePath: string]: string; // filePath -> SHA256 hash
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface IDocumentIndex {
|
|
60
|
+
contentHash: string;
|
|
61
|
+
sizeBytes: number;
|
|
62
|
+
mimeType: string;
|
|
63
|
+
createdAt: string;
|
|
64
|
+
originalFilename?: string;
|
|
65
|
+
pdfaAvailable: boolean;
|
|
66
|
+
zugferdXml?: string;
|
|
67
|
+
retentionClass: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class SkrExport {
|
|
71
|
+
private logger: plugins.smartlog.ConsoleLog;
|
|
72
|
+
private options: IExportOptions;
|
|
73
|
+
private exportDir: string;
|
|
74
|
+
private manifest: IBagItManifest = {};
|
|
75
|
+
private tagManifest: IBagItManifest = {};
|
|
76
|
+
|
|
77
|
+
constructor(options: IExportOptions) {
|
|
78
|
+
this.options = options;
|
|
79
|
+
this.logger = new plugins.smartlog.ConsoleLog();
|
|
80
|
+
this.exportDir = path.join(options.exportPath, `jahresabschluss_${options.fiscalYear}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Creates the BagIt directory structure for the export
|
|
85
|
+
*/
|
|
86
|
+
public async createBagItStructure(): Promise<void> {
|
|
87
|
+
this.logger.log('info', 'Creating BagIt directory structure...');
|
|
88
|
+
|
|
89
|
+
// Create main directories
|
|
90
|
+
await plugins.smartfile.fs.ensureDir(this.exportDir);
|
|
91
|
+
await plugins.smartfile.fs.ensureDir(path.join(this.exportDir, 'data'));
|
|
92
|
+
await plugins.smartfile.fs.ensureDir(path.join(this.exportDir, 'data', 'metadata'));
|
|
93
|
+
await plugins.smartfile.fs.ensureDir(path.join(this.exportDir, 'data', 'metadata', 'schemas'));
|
|
94
|
+
await plugins.smartfile.fs.ensureDir(path.join(this.exportDir, 'data', 'metadata', 'schemas', 'v1'));
|
|
95
|
+
await plugins.smartfile.fs.ensureDir(path.join(this.exportDir, 'data', 'metadata', 'signatures'));
|
|
96
|
+
await plugins.smartfile.fs.ensureDir(path.join(this.exportDir, 'data', 'accounting'));
|
|
97
|
+
await plugins.smartfile.fs.ensureDir(path.join(this.exportDir, 'data', 'accounting', 'ebilanz'));
|
|
98
|
+
await plugins.smartfile.fs.ensureDir(path.join(this.exportDir, 'data', 'documents'));
|
|
99
|
+
await plugins.smartfile.fs.ensureDir(path.join(this.exportDir, 'data', 'documents', 'by-hash'));
|
|
100
|
+
await plugins.smartfile.fs.ensureDir(path.join(this.exportDir, 'data', 'reports'));
|
|
101
|
+
|
|
102
|
+
// Create BagIt declaration file
|
|
103
|
+
await this.createBagItDeclaration();
|
|
104
|
+
|
|
105
|
+
// Create README
|
|
106
|
+
await this.createReadme();
|
|
107
|
+
|
|
108
|
+
this.logger.log('ok', 'BagIt structure created successfully');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Creates the bagit.txt declaration file
|
|
113
|
+
*/
|
|
114
|
+
private async createBagItDeclaration(): Promise<void> {
|
|
115
|
+
const bagitContent = `BagIt-Version: 1.0
|
|
116
|
+
Tag-File-Character-Encoding: UTF-8`;
|
|
117
|
+
|
|
118
|
+
const filePath = path.join(this.exportDir, 'bagit.txt');
|
|
119
|
+
await plugins.smartfile.memory.toFs(bagitContent, filePath);
|
|
120
|
+
|
|
121
|
+
// Add to tag manifest
|
|
122
|
+
const hash = await this.hashFile(filePath);
|
|
123
|
+
this.tagManifest['bagit.txt'] = hash;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Creates the README.txt file with Verfahrensdokumentation
|
|
128
|
+
*/
|
|
129
|
+
private async createReadme(): Promise<void> {
|
|
130
|
+
const readmeContent = `SKR Jahresabschluss Export - Verfahrensdokumentation
|
|
131
|
+
=====================================================
|
|
132
|
+
|
|
133
|
+
Dieses Archiv enthält einen revisionssicheren Export des Jahresabschlusses
|
|
134
|
+
gemäß den Grundsätzen ordnungsmäßiger Buchführung (GoBD).
|
|
135
|
+
|
|
136
|
+
Export-Datum: ${new Date().toISOString()}
|
|
137
|
+
Geschäftsjahr: ${this.options.fiscalYear}
|
|
138
|
+
Zeitraum: ${this.options.dateFrom.toISOString()} bis ${this.options.dateTo.toISOString()}
|
|
139
|
+
|
|
140
|
+
STRUKTUR DES ARCHIVS
|
|
141
|
+
--------------------
|
|
142
|
+
- /data/accounting/: Buchhaltungsdaten (Journale, Konten, Salden)
|
|
143
|
+
- /data/documents/: Belegdokumente (content-adressiert)
|
|
144
|
+
- /data/reports/: Finanzberichte (PDF/A-3)
|
|
145
|
+
- /data/metadata/: Export-Metadaten und Schemas
|
|
146
|
+
- /data/metadata/signatures/: Digitale Signaturen und Zeitstempel
|
|
147
|
+
|
|
148
|
+
INTEGRITÄTSSICHERUNG
|
|
149
|
+
--------------------
|
|
150
|
+
- Alle Dateien sind mit SHA-256 gehasht (siehe manifest-sha256.txt)
|
|
151
|
+
- Optional: Digitale Signatur (CAdES) über Manifest
|
|
152
|
+
- Optional: RFC 3161 Zeitstempel
|
|
153
|
+
|
|
154
|
+
AUFBEWAHRUNG
|
|
155
|
+
------------
|
|
156
|
+
Dieses Archiv muss gemäß § 147 AO für 10 Jahre revisionssicher aufbewahrt werden.
|
|
157
|
+
Empfohlen wird die Speicherung auf WORM-Medien.
|
|
158
|
+
|
|
159
|
+
REIMPORT
|
|
160
|
+
--------
|
|
161
|
+
Das Archiv kann mit der SKR-Software vollständig reimportiert werden.
|
|
162
|
+
Die Datenintegrität wird beim Import automatisch verifiziert.
|
|
163
|
+
|
|
164
|
+
COMPLIANCE
|
|
165
|
+
----------
|
|
166
|
+
- GoBD-konform
|
|
167
|
+
- E-Bilanz-fähig (XBRL)
|
|
168
|
+
- ZUGFeRD/Factur-X kompatibel
|
|
169
|
+
- PDF/A-3 für Langzeitarchivierung
|
|
170
|
+
|
|
171
|
+
© ${new Date().getFullYear()} ${this.options.companyInfo?.name || 'Export System'}`;
|
|
172
|
+
|
|
173
|
+
const filePath = path.join(this.exportDir, 'readme.txt');
|
|
174
|
+
await plugins.smartfile.memory.toFs(readmeContent, filePath);
|
|
175
|
+
|
|
176
|
+
// Add to tag manifest
|
|
177
|
+
const hash = await this.hashFile(filePath);
|
|
178
|
+
this.tagManifest['readme.txt'] = hash;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Creates the export metadata JSON file
|
|
183
|
+
*/
|
|
184
|
+
public async createExportMetadata(skrType: TSKRType): Promise<void> {
|
|
185
|
+
const metadata: IExportMetadata = {
|
|
186
|
+
exportVersion: '1.0.0',
|
|
187
|
+
exportTimestamp: new Date().toISOString(),
|
|
188
|
+
generator: {
|
|
189
|
+
name: '@fin.cx/skr',
|
|
190
|
+
version: '1.1.0' // Should be read from package.json
|
|
191
|
+
},
|
|
192
|
+
company: this.options.companyInfo,
|
|
193
|
+
fiscalYear: this.options.fiscalYear,
|
|
194
|
+
dateRange: {
|
|
195
|
+
from: this.options.dateFrom.toISOString(),
|
|
196
|
+
to: this.options.dateTo.toISOString()
|
|
197
|
+
},
|
|
198
|
+
skrType: skrType,
|
|
199
|
+
schemaVersion: '1.0',
|
|
200
|
+
crypto: {
|
|
201
|
+
digestAlgorithms: ['sha256'],
|
|
202
|
+
signatureType: this.options.signExport ? 'CAdES' : undefined,
|
|
203
|
+
timestampPolicy: this.options.timestampExport ? 'RFC3161' : undefined,
|
|
204
|
+
merkleTree: true
|
|
205
|
+
},
|
|
206
|
+
options: {
|
|
207
|
+
packagedAs: 'bagit',
|
|
208
|
+
compression: 'none',
|
|
209
|
+
deduplication: true
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const filePath = path.join(this.exportDir, 'data', 'metadata', 'export.json');
|
|
214
|
+
await plugins.smartfile.memory.toFs(JSON.stringify(metadata, null, 2), filePath);
|
|
215
|
+
|
|
216
|
+
// Add to manifest
|
|
217
|
+
const hash = await this.hashFile(filePath);
|
|
218
|
+
this.manifest['data/metadata/export.json'] = hash;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Creates JSON schemas for the export data structures
|
|
223
|
+
*/
|
|
224
|
+
public async createSchemas(): Promise<void> {
|
|
225
|
+
// Ledger schema
|
|
226
|
+
const ledgerSchema = {
|
|
227
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
228
|
+
"title": "Ledger Entry",
|
|
229
|
+
"type": "object",
|
|
230
|
+
"properties": {
|
|
231
|
+
"schema_version": { "type": "string" },
|
|
232
|
+
"entry_id": { "type": "string", "format": "uuid" },
|
|
233
|
+
"booking_date": { "type": "string", "format": "date" },
|
|
234
|
+
"posting_date": { "type": "string", "format": "date" },
|
|
235
|
+
"currency": { "type": "string" },
|
|
236
|
+
"journal": { "type": "string" },
|
|
237
|
+
"description": { "type": "string" },
|
|
238
|
+
"lines": {
|
|
239
|
+
"type": "array",
|
|
240
|
+
"items": {
|
|
241
|
+
"type": "object",
|
|
242
|
+
"properties": {
|
|
243
|
+
"posting_id": { "type": "string" },
|
|
244
|
+
"account_code": { "type": "string" },
|
|
245
|
+
"debit": { "type": "string" },
|
|
246
|
+
"credit": { "type": "string" },
|
|
247
|
+
"tax_code": { "type": "string" },
|
|
248
|
+
"document_refs": {
|
|
249
|
+
"type": "array",
|
|
250
|
+
"items": {
|
|
251
|
+
"type": "object",
|
|
252
|
+
"properties": {
|
|
253
|
+
"content_hash": { "type": "string" },
|
|
254
|
+
"doc_role": { "type": "string" },
|
|
255
|
+
"mime": { "type": "string" }
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
"required": ["posting_id", "account_code", "debit", "credit"]
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
"created_at": { "type": "string", "format": "date-time" },
|
|
264
|
+
"user": { "type": "string" }
|
|
265
|
+
},
|
|
266
|
+
"required": ["schema_version", "entry_id", "booking_date", "lines"]
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// Accounts schema
|
|
270
|
+
const accountsSchema = {
|
|
271
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
272
|
+
"title": "Accounts CSV",
|
|
273
|
+
"type": "object",
|
|
274
|
+
"properties": {
|
|
275
|
+
"account_code": { "type": "string" },
|
|
276
|
+
"name": { "type": "string" },
|
|
277
|
+
"type": { "type": "string" },
|
|
278
|
+
"parent": { "type": "string" },
|
|
279
|
+
"skr_set": { "type": "string" },
|
|
280
|
+
"tax_code_default": { "type": "string" },
|
|
281
|
+
"active_from": { "type": "string", "format": "date" },
|
|
282
|
+
"active_to": { "type": "string", "format": "date" }
|
|
283
|
+
},
|
|
284
|
+
"required": ["account_code", "name", "type", "skr_set"]
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Save schemas
|
|
288
|
+
const schemasDir = path.join(this.exportDir, 'data', 'metadata', 'schemas', 'v1');
|
|
289
|
+
|
|
290
|
+
await plugins.smartfile.memory.toFs(
|
|
291
|
+
JSON.stringify(ledgerSchema, null, 2),
|
|
292
|
+
path.join(schemasDir, 'ledger.schema.json')
|
|
293
|
+
);
|
|
294
|
+
this.manifest['data/metadata/schemas/v1/ledger.schema.json'] = await this.hashFile(
|
|
295
|
+
path.join(schemasDir, 'ledger.schema.json')
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
await plugins.smartfile.memory.toFs(
|
|
299
|
+
JSON.stringify(accountsSchema, null, 2),
|
|
300
|
+
path.join(schemasDir, 'accounts.schema.json')
|
|
301
|
+
);
|
|
302
|
+
this.manifest['data/metadata/schemas/v1/accounts.schema.json'] = await this.hashFile(
|
|
303
|
+
path.join(schemasDir, 'accounts.schema.json')
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Writes the BagIt manifest files
|
|
309
|
+
*/
|
|
310
|
+
public async writeManifests(): Promise<void> {
|
|
311
|
+
// Write data manifest
|
|
312
|
+
let manifestContent = '';
|
|
313
|
+
for (const [filePath, hash] of Object.entries(this.manifest)) {
|
|
314
|
+
manifestContent += `${hash} ${filePath}\n`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const manifestPath = path.join(this.exportDir, 'manifest-sha256.txt');
|
|
318
|
+
await plugins.smartfile.memory.toFs(manifestContent, manifestPath);
|
|
319
|
+
|
|
320
|
+
// Add manifest to tag manifest
|
|
321
|
+
this.tagManifest['manifest-sha256.txt'] = await this.hashFile(manifestPath);
|
|
322
|
+
|
|
323
|
+
// Write tag manifest
|
|
324
|
+
let tagManifestContent = '';
|
|
325
|
+
for (const [filePath, hash] of Object.entries(this.tagManifest)) {
|
|
326
|
+
tagManifestContent += `${hash} ${filePath}\n`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
await plugins.smartfile.memory.toFs(
|
|
330
|
+
tagManifestContent,
|
|
331
|
+
path.join(this.exportDir, 'tagmanifest-sha256.txt')
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Calculates SHA-256 hash of a file
|
|
337
|
+
*/
|
|
338
|
+
private async hashFile(filePath: string): Promise<string> {
|
|
339
|
+
const fileContent = await plugins.smartfile.fs.toBuffer(filePath);
|
|
340
|
+
return await plugins.smarthash.sha256FromBuffer(fileContent);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Stores a document in content-addressed storage
|
|
345
|
+
*/
|
|
346
|
+
public async storeDocument(content: Buffer, originalFilename?: string): Promise<string> {
|
|
347
|
+
const hash = await plugins.smarthash.sha256FromBuffer(content);
|
|
348
|
+
|
|
349
|
+
// Create path based on hash (first 2 chars as directory)
|
|
350
|
+
const hashPrefix = hash.substring(0, 2);
|
|
351
|
+
const hashDir = path.join(this.exportDir, 'data', 'documents', 'by-hash', hashPrefix);
|
|
352
|
+
await plugins.smartfile.fs.ensureDir(hashDir);
|
|
353
|
+
|
|
354
|
+
const docPath = path.join(hashDir, hash);
|
|
355
|
+
|
|
356
|
+
// Only store if not already exists (deduplication)
|
|
357
|
+
if (!(await plugins.smartfile.fs.fileExists(docPath))) {
|
|
358
|
+
await plugins.smartfile.memory.toFs(content, docPath);
|
|
359
|
+
this.manifest[`data/documents/by-hash/${hashPrefix}/${hash}`] = hash;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return hash;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Creates a Merkle tree from all file hashes
|
|
367
|
+
*/
|
|
368
|
+
public async createMerkleTree(): Promise<string> {
|
|
369
|
+
const leaves = Object.values(this.manifest).map(hash =>
|
|
370
|
+
Buffer.from(hash, 'hex')
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// Create a sync hash function wrapper for MerkleTree
|
|
374
|
+
const hashFn = (data: Buffer) => {
|
|
375
|
+
// Convert async to sync by using crypto directly
|
|
376
|
+
const crypto = require('crypto');
|
|
377
|
+
return crypto.createHash('sha256').update(data).digest();
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const tree = new plugins.MerkleTree(leaves, hashFn, {
|
|
381
|
+
sortPairs: true
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const root = tree.getRoot().toString('hex');
|
|
385
|
+
|
|
386
|
+
// Save Merkle tree data
|
|
387
|
+
const merkleData = {
|
|
388
|
+
root: root,
|
|
389
|
+
leaves: Object.entries(this.manifest).map(([path, hash]) => ({
|
|
390
|
+
path,
|
|
391
|
+
hash
|
|
392
|
+
})),
|
|
393
|
+
timestamp: new Date().toISOString()
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const merklePath = path.join(this.exportDir, 'data', 'metadata', 'merkle-tree.json');
|
|
397
|
+
await plugins.smartfile.memory.toFs(JSON.stringify(merkleData, null, 2), merklePath);
|
|
398
|
+
this.manifest['data/metadata/merkle-tree.json'] = await this.hashFile(merklePath);
|
|
399
|
+
|
|
400
|
+
return root;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Validates the BagIt structure
|
|
405
|
+
*/
|
|
406
|
+
public async validateBagIt(): Promise<boolean> {
|
|
407
|
+
this.logger.log('info', 'Validating BagIt structure...');
|
|
408
|
+
|
|
409
|
+
// Check required files exist
|
|
410
|
+
const requiredFiles = [
|
|
411
|
+
'bagit.txt',
|
|
412
|
+
'manifest-sha256.txt',
|
|
413
|
+
'tagmanifest-sha256.txt',
|
|
414
|
+
'readme.txt'
|
|
415
|
+
];
|
|
416
|
+
|
|
417
|
+
for (const file of requiredFiles) {
|
|
418
|
+
const filePath = path.join(this.exportDir, file);
|
|
419
|
+
if (!(await plugins.smartfile.fs.fileExists(filePath))) {
|
|
420
|
+
this.logger.log('error', `Required file missing: ${file}`);
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Verify all manifest entries
|
|
426
|
+
for (const [relPath, expectedHash] of Object.entries(this.manifest)) {
|
|
427
|
+
const fullPath = path.join(this.exportDir, relPath);
|
|
428
|
+
if (!(await plugins.smartfile.fs.fileExists(fullPath))) {
|
|
429
|
+
this.logger.log('error', `Manifest file missing: ${relPath}`);
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const actualHash = await this.hashFile(fullPath);
|
|
434
|
+
if (actualHash !== expectedHash) {
|
|
435
|
+
this.logger.log('error', `Hash mismatch for ${relPath}`);
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
this.logger.log('ok', 'BagIt validation successful');
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
}
|