stegdoc 5.6.0 → 6.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.
@@ -657,9 +657,12 @@ function decodeLogLines(allRows) {
657
657
  const headerStr = headerPayload.toString('utf8');
658
658
 
659
659
  // Parse header: "STGD05|<metaLen>|<encLen>|{metadataJson}{encryptionMeta}"
660
- const markerIdx = headerStr.indexOf('STGD05|');
660
+ const markerIdx = headerStr.search(/STGD0[56]\|/);
661
661
  if (markerIdx === -1) {
662
- throw new Error('Invalid v5 format: magic marker not found. This may not be a stegdoc v5 file.');
662
+ throw new Error('Invalid log-embed format: magic marker not found. This may not be a stegdoc file.');
663
+ }
664
+ if (headerStr.startsWith('STGD06|', markerIdx)) {
665
+ throw new Error('This file uses the v6 format, which the JavaScript decoder cannot read. The native engine is required.');
663
666
  }
664
667
 
665
668
  const afterMarker = headerStr.slice(markerIdx + 7);
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ const { hostTarget, packageName } = require('./bindings');
6
+
7
+ const NATIVE_DIR = path.join(__dirname, '..', '..', 'native');
8
+
9
+ let binding;
10
+ let resolved = false;
11
+
12
+ /**
13
+ * Load the native format engine, or return null when it is not available.
14
+ *
15
+ * Resolution order:
16
+ * 1. `STEGDOC_NATIVE_PATH`, an explicit `.node` file.
17
+ * 2. The platform package (`@stegdoc/binding-<suffix>`) from the published
18
+ * install's optionalDependencies.
19
+ * 3. `native/stegdoc.node`, the local `pnpm build:native` output.
20
+ *
21
+ * The binding is required for log-embed (v5/v6) decode and is the preferred
22
+ * encoder. The CommonJS implementation remains only for the legacy v3/v4
23
+ * formats, which the native engine does not cover. Set
24
+ * `STEGDOC_DISABLE_NATIVE=1` to force the JS paths.
25
+ *
26
+ * @returns {object|null} The binding, or null when unavailable.
27
+ */
28
+ function loadNative() {
29
+ if (resolved) return binding;
30
+ resolved = true;
31
+
32
+ if (process.env.STEGDOC_DISABLE_NATIVE === '1') {
33
+ binding = null;
34
+ return binding;
35
+ }
36
+
37
+ const explicit = process.env.STEGDOC_NATIVE_PATH;
38
+ if (explicit) {
39
+ binding = require(explicit);
40
+ return binding;
41
+ }
42
+
43
+ const target = hostTarget();
44
+ if (target) {
45
+ try {
46
+ binding = require(packageName(target));
47
+ return binding;
48
+ } catch {
49
+ // Not installed; fall through to the development build.
50
+ }
51
+ }
52
+
53
+ try {
54
+ binding = require(path.join(NATIVE_DIR, 'stegdoc.node'));
55
+ } catch {
56
+ binding = null;
57
+ }
58
+ return binding;
59
+ }
60
+
61
+ /**
62
+ * @returns {boolean} Whether the native engine is loaded.
63
+ */
64
+ function nativeAvailable() {
65
+ return loadNative() !== null;
66
+ }
67
+
68
+ module.exports = { loadNative, nativeAvailable };
@@ -1,65 +1,6 @@
1
1
  const { Transform, Writable } = require('stream');
2
2
  const crypto = require('crypto');
3
3
 
4
- /**
5
- * Transform stream that converts binary input to base64 text output.
6
- * Buffers incomplete 3-byte groups across chunk boundaries.
7
- */
8
- class Base64EncodeTransform extends Transform {
9
- constructor() {
10
- super();
11
- this._remainder = Buffer.alloc(0);
12
- }
13
-
14
- _transform(chunk, encoding, callback) {
15
- const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
16
- const combined = this._remainder.length > 0 ? Buffer.concat([this._remainder, buf]) : buf;
17
- const usable = combined.length - (combined.length % 3);
18
- if (usable > 0) {
19
- this.push(combined.slice(0, usable).toString('base64'));
20
- }
21
- this._remainder = usable < combined.length ? combined.slice(usable) : Buffer.alloc(0);
22
- callback();
23
- }
24
-
25
- _flush(callback) {
26
- if (this._remainder.length > 0) {
27
- this.push(this._remainder.toString('base64'));
28
- this._remainder = Buffer.alloc(0);
29
- }
30
- callback();
31
- }
32
- }
33
-
34
- /**
35
- * Transform stream that converts base64 text input to binary output.
36
- * Buffers incomplete 4-char groups across chunk boundaries.
37
- */
38
- class Base64DecodeTransform extends Transform {
39
- constructor() {
40
- super();
41
- this._remainder = '';
42
- }
43
-
44
- _transform(chunk, encoding, callback) {
45
- const str = this._remainder + (Buffer.isBuffer(chunk) ? chunk.toString() : chunk);
46
- const usable = str.length - (str.length % 4);
47
- if (usable > 0) {
48
- this.push(Buffer.from(str.slice(0, usable), 'base64'));
49
- }
50
- this._remainder = usable < str.length ? str.slice(usable) : '';
51
- callback();
52
- }
53
-
54
- _flush(callback) {
55
- if (this._remainder.length > 0) {
56
- this.push(Buffer.from(this._remainder, 'base64'));
57
- this._remainder = '';
58
- }
59
- callback();
60
- }
61
- }
62
-
63
4
  /**
64
5
  * Transform stream that passes data through unchanged while computing SHA-256 hash.
65
6
  * Access the hex hash via .digest after the stream has ended.
@@ -90,51 +31,6 @@ class HashPassthrough extends Transform {
90
31
  }
91
32
  }
92
33
 
93
- /**
94
- * Writable stream that collects string output up to maxBytes.
95
- * Calls an async onChunkReady callback when a chunk is full, applying
96
- * backpressure to pause upstream until the callback resolves.
97
- */
98
- class ChunkCollector extends Writable {
99
- constructor(maxBytes, onChunkReady) {
100
- super({ decodeStrings: false });
101
- this._maxBytes = maxBytes;
102
- this._buffer = '';
103
- this._chunkIndex = 0;
104
- this._onChunkReady = onChunkReady;
105
- }
106
-
107
- async _write(chunk, encoding, callback) {
108
- try {
109
- this._buffer += typeof chunk === 'string' ? chunk : chunk.toString();
110
- while (this._buffer.length >= this._maxBytes) {
111
- const piece = this._buffer.slice(0, this._maxBytes);
112
- this._buffer = this._buffer.slice(this._maxBytes);
113
- await this._onChunkReady(piece, this._chunkIndex++);
114
- }
115
- callback();
116
- } catch (err) {
117
- callback(err);
118
- }
119
- }
120
-
121
- async _final(callback) {
122
- try {
123
- if (this._buffer.length > 0) {
124
- await this._onChunkReady(this._buffer, this._chunkIndex++);
125
- this._buffer = '';
126
- }
127
- callback();
128
- } catch (err) {
129
- callback(err);
130
- }
131
- }
132
-
133
- get totalChunks() {
134
- return this._chunkIndex;
135
- }
136
- }
137
-
138
34
  /**
139
35
  * Writable stream that collects binary Buffer output up to maxBytes.
140
36
  * Calls an async onChunkReady callback with a Buffer when full.
@@ -221,10 +117,7 @@ class ProgressTransform extends Transform {
221
117
  }
222
118
 
223
119
  module.exports = {
224
- Base64EncodeTransform,
225
- Base64DecodeTransform,
226
120
  HashPassthrough,
227
- ChunkCollector,
228
121
  BinaryChunkCollector,
229
122
  ProgressTransform,
230
123
  };
package/src/lib/utils.js CHANGED
@@ -213,6 +213,25 @@ function detectFormat(filename) {
213
213
  return null;
214
214
  }
215
215
 
216
+ /**
217
+ * Reduce a metadata-supplied name to a bare, safe filename.
218
+ *
219
+ * `originalFilename` is unauthenticated, so it is never joined onto a directory
220
+ * unchecked (FORMAT.md 11.1). Any directory component is discarded and the
221
+ * remainder must not be empty, `.`, `..`, or contain NUL.
222
+ *
223
+ * @param {string} name - Metadata-supplied filename
224
+ * @returns {string} A bare filename safe to join onto a directory
225
+ * @throws {Error} If the name has no usable filename component
226
+ */
227
+ function safeFilename(name) {
228
+ const last = String(name == null ? '' : name).split(/[/\\]/).pop();
229
+ if (!last || last === '.' || last === '..' || last.includes('\0')) {
230
+ throw new Error(`Unsafe output filename in metadata: ${JSON.stringify(name)}`);
231
+ }
232
+ return last;
233
+ }
234
+
216
235
  module.exports = {
217
236
  generateHash,
218
237
  generateContentHash,
@@ -221,6 +240,7 @@ module.exports = {
221
240
  generateFilename,
222
241
  parseFilename,
223
242
  detectFormat,
243
+ safeFilename,
224
244
  // Legacy aliases for backward compatibility
225
245
  generateDocxFilename,
226
246
  parseDocxFilename,
@@ -1,16 +1,11 @@
1
- const ExcelJS = require('exceljs');
2
1
  const fs = require('fs');
3
2
  const path = require('path');
4
3
  const AdmZip = require('adm-zip');
5
- const { generateDecoyHeaders, generateDecoyData, calculateDecoyRowCount, rowToArray, resetTimeWindow } = require('./decoy-generator');
6
4
  const { generateLogHeaders, encodePayloadToLogLines, decodeLogLines, resetTimeState } = require('./log-generator');
7
5
  const { parseXmlFromZip, ensureArray, extractTextContent } = require('./xml-utils');
6
+ const { createXlsxRaw } = require('./xlsx-writer');
8
7
 
9
- // Constants for data storage
10
- const HIDDEN_SHEET_NAME = 'Data';
11
- const VISIBLE_SHEET_NAME = 'Server Metrics';
12
8
  const V5_SHEET_NAME = 'Access Logs';
13
- const CELL_CHUNK_SIZE = 32000; // Max characters per cell (~32KB)
14
9
 
15
10
  // ─── v5 Log-Embed Format ───────────────────────────────────────────────────
16
11
 
@@ -29,88 +24,23 @@ const CELL_CHUNK_SIZE = 32000; // Max characters per cell (~32KB)
29
24
  async function createXlsxPartV5(options) {
30
25
  const { payloadBuffer, encryptionMeta, metadataJson, outputPath } = options;
31
26
 
32
- // Ensure output directory exists
33
- const outputDir = path.dirname(outputPath);
34
- if (!fs.existsSync(outputDir)) {
35
- fs.mkdirSync(outputDir, { recursive: true });
36
- }
37
-
38
- const workbook = new ExcelJS.stream.xlsx.WorkbookWriter({
39
- filename: outputPath,
40
- useSharedStrings: false,
41
- });
42
-
43
- workbook.creator = 'Microsoft Excel';
44
- workbook.lastModifiedBy = 'Microsoft Excel';
45
- workbook.created = new Date();
46
- workbook.modified = new Date();
47
-
48
- // === Single Sheet: Access Logs ===
49
- const sheet = workbook.addWorksheet(V5_SHEET_NAME, {
50
- properties: { tabColor: { argb: '2F5496' } },
51
- });
52
-
53
- // Column widths for log data
54
- sheet.columns = [
55
- { width: 16 }, // Remote Address
56
- { width: 28 }, // Timestamp
57
- { width: 8 }, // Method
58
- { width: 90 }, // Request (contains URL with payload)
59
- { width: 7 }, // Status
60
- { width: 8 }, // Bytes
61
- { width: 65 }, // Referer
62
- { width: 85 }, // User-Agent
63
- { width: 38 }, // X-Request-ID
64
- { width: 34 }, // X-Trace-ID
65
- ];
66
-
67
- // Header row with styling
68
- const headers = generateLogHeaders();
69
- const headerRow = sheet.addRow(headers);
70
- for (let col = 1; col <= headers.length; col++) {
71
- const cell = headerRow.getCell(col);
72
- cell.font = { bold: true, size: 10, color: { argb: 'FFFFFF' }, name: 'Consolas' };
73
- cell.fill = {
74
- type: 'pattern',
75
- pattern: 'solid',
76
- fgColor: { argb: '2F5496' },
77
- };
78
- }
79
- headerRow.commit();
80
-
81
27
  // Generate all log lines (header + data + filler)
82
28
  const { headerRows, dataRows, fillerRows } = encodePayloadToLogLines(
83
29
  payloadBuffer, metadataJson, encryptionMeta
84
30
  );
85
31
 
86
- // Write header lines (metadata)
87
- for (const row of headerRows) {
88
- const r = sheet.addRow(row);
89
- r.commit();
90
- }
91
-
92
- // Write data lines (payload)
93
- for (const row of dataRows) {
94
- const r = sheet.addRow(row);
95
- r.commit();
96
- }
97
-
98
- // Write filler lines (realistic padding)
99
- for (const row of fillerRows) {
100
- const r = sheet.addRow(row);
101
- r.commit();
102
- }
103
-
104
- const totalRows = headerRows.length + dataRows.length + fillerRows.length;
105
-
106
- // Add filters
107
- sheet.autoFilter = {
108
- from: 'A1',
109
- to: `J${totalRows + 1}`,
110
- };
111
-
112
- await sheet.commit();
113
- await workbook.commit();
32
+ const headers = generateLogHeaders();
33
+ const allRows = [...headerRows, ...dataRows, ...fillerRows];
34
+
35
+ // Build XLSX from raw XML (no ExcelJS — passes air gap filters)
36
+ createXlsxRaw({
37
+ headers,
38
+ rows: allRows,
39
+ sheetName: V5_SHEET_NAME,
40
+ outputPath,
41
+ colWidths: [16, 28, 8, 90, 7, 8, 65, 85, 38, 34],
42
+ autoFilter: true,
43
+ });
114
44
 
115
45
  return outputPath;
116
46
  }
@@ -130,17 +60,17 @@ async function readXlsxV5(xlsxPath) {
130
60
  const fileBuffer = fs.readFileSync(xlsxPath);
131
61
  const zip = new AdmZip(fileBuffer);
132
62
 
133
- // Parse shared strings (only if present v5 files created with useSharedStrings:false may not have them)
63
+ // Parse shared strings handles both normal and namespace-prefixed XML (ns0:si, ns0:t)
134
64
  let sharedStrings = null;
135
65
  const ssEntry = zip.getEntry('xl/sharedStrings.xml');
136
66
  if (ssEntry) {
137
67
  sharedStrings = [];
138
68
  const ssXml = ssEntry.getData().toString('utf8');
139
- // Fast extract: match each <si><t>...</t></si> or <si><t ...>...</t></si>
140
- const siRegex = /<si><t[^>]*>([^<]*)<\/t><\/si>/g;
69
+ // Match <si><t>...</t></si> or <ns0:si><ns0:t>...</ns0:t></ns0:si> (any nsN prefix)
70
+ const siRegex = /<(?:\w+:)?si><(?:\w+:)?t[^>]*>([^<]*)<\/(?:\w+:)?t><\/(?:\w+:)?si>/g;
141
71
  let match;
142
72
  while ((match = siRegex.exec(ssXml)) !== null) {
143
- sharedStrings.push(match[1]);
73
+ sharedStrings.push(decodeXmlEntities(match[1]));
144
74
  }
145
75
  }
146
76
 
@@ -151,10 +81,10 @@ async function readXlsxV5(xlsxPath) {
151
81
  }
152
82
  const sheetXml = sheetEntry.getData().toString('utf8');
153
83
 
154
- // Fast row extraction using regex — much faster than full DOM parsing
84
+ // Fast row extraction using regex — handles both normal and namespace-prefixed XML
155
85
  const allRows = [];
156
- const rowRegex = /<row [^>]*>(.*?)<\/row>/gs;
157
- const cellRegex = /<c r="([A-Z]+)\d+"(?: t="([^"]*)")?[^>]*>(?:<v>([^<]*)<\/v>|<is><t[^>]*>([^<]*)<\/t><\/is>)?<\/c>/g;
86
+ const rowRegex = /<(?:\w+:)?row [^>]*>(.*?)<\/(?:\w+:)?row>/gs;
87
+ const cellRegex = /<(?:\w+:)?c r="([A-Z]+)\d+"(?: t="([^"]*)")?[^>]*>(?:<(?:\w+:)?v>([^<]*)<\/(?:\w+:)?v>|<(?:\w+:)?is><(?:\w+:)?t[^>]*>([^<]*)<\/(?:\w+:)?t><\/(?:\w+:)?is>)?<\/(?:\w+:)?c>/g;
158
88
 
159
89
  let rowMatch;
160
90
  while ((rowMatch = rowRegex.exec(sheetXml)) !== null) {
@@ -251,201 +181,7 @@ function detectXlsxVersion(xlsxPath) {
251
181
  return 'v5';
252
182
  }
253
183
 
254
- // ─── v3/v4 Legacy Format ────────────────────────────────────────────────────
255
-
256
- /**
257
- * Create an XLSX file using streaming WorkbookWriter (v4 format).
258
- * Memory-efficient: rows are freed after commit.
259
- * @param {object} options
260
- * @param {string} options.base64Content - Base64 content to store in this part
261
- * @param {string} options.encryptionMeta - Packed encryption metadata (iv:salt:authTag) or ''
262
- * @param {string} options.metadataJson - Serialized metadata JSON string
263
- * @param {string} options.outputPath - Output file path
264
- * @returns {Promise<string>} Path to created file
265
- */
266
- async function createXlsxPartStreaming(options) {
267
- const { base64Content, encryptionMeta, metadataJson, outputPath } = options;
268
-
269
- // Ensure output directory exists
270
- const outputDir = path.dirname(outputPath);
271
- if (!fs.existsSync(outputDir)) {
272
- fs.mkdirSync(outputDir, { recursive: true });
273
- }
274
-
275
- const workbook = new ExcelJS.stream.xlsx.WorkbookWriter({
276
- filename: outputPath,
277
- useSharedStrings: false, // Inline strings for easier post-processing
278
- });
279
-
280
- workbook.creator = 'Microsoft Excel';
281
- workbook.lastModifiedBy = 'Microsoft Excel';
282
- workbook.created = new Date();
283
- workbook.modified = new Date();
284
-
285
- // === Sheet 1: Visible decoy data (server metrics) ===
286
- const visibleSheet = workbook.addWorksheet(VISIBLE_SHEET_NAME, {
287
- properties: { tabColor: { argb: '4472C4' } },
288
- });
289
-
290
- // Set column widths before adding rows
291
- visibleSheet.columns = [
292
- { width: 20 }, // Timestamp
293
- { width: 16 }, // Server ID
294
- { width: 12 }, // Status
295
- { width: 8 }, // CPU %
296
- { width: 10 }, // Memory %
297
- { width: 8 }, // Disk %
298
- { width: 14 }, // Network (MB/s)
299
- { width: 10 }, // Requests
300
- { width: 14 }, // Resp Time (ms)
301
- { width: 12 }, // Uptime (hrs)
302
- ];
303
-
304
- // Add headers
305
- const headers = generateDecoyHeaders();
306
- const headerRow = visibleSheet.addRow(headers);
307
-
308
- const headerCount = headers.length;
309
- for (let col = 1; col <= headerCount; col++) {
310
- const cell = headerRow.getCell(col);
311
- cell.font = { bold: true, size: 11, color: { argb: 'FFFFFF' } };
312
- cell.fill = {
313
- type: 'pattern',
314
- pattern: 'solid',
315
- fgColor: { argb: '2E7D32' },
316
- };
317
- }
318
- headerRow.commit();
319
-
320
- // Generate and write decoy data rows, committing each to free memory
321
- const payloadSize = base64Content.length;
322
- const rowCount = calculateDecoyRowCount(payloadSize);
323
- const decoyData = generateDecoyData(rowCount);
324
-
325
- for (const row of decoyData) {
326
- const dataRow = visibleSheet.addRow(rowToArray(row));
327
- dataRow.commit();
328
- }
329
-
330
- // Add filters
331
- visibleSheet.autoFilter = {
332
- from: 'A1',
333
- to: `J${rowCount + 1}`,
334
- };
335
-
336
- await visibleSheet.commit();
337
-
338
- // === Sheet 2: Hidden payload (veryHidden) ===
339
- const hiddenSheet = workbook.addWorksheet(HIDDEN_SHEET_NAME, {
340
- state: 'veryHidden',
341
- });
342
-
343
- // Split base64 content into cell-sized chunks
344
- const chunks = splitIntoChunks(base64Content, CELL_CHUNK_SIZE);
345
-
346
- // Row 1: metadata
347
- const metaRow = hiddenSheet.getRow(1);
348
- metaRow.getCell(1).value = encryptionMeta;
349
- metaRow.getCell(2).value = metadataJson;
350
- metaRow.getCell(3).value = chunks.length.toString();
351
- metaRow.commit();
352
-
353
- // Write cell chunks in rows starting from row 2, columns A-Z
354
- const totalChunks = chunks.length;
355
- const totalRows = Math.ceil(totalChunks / 26);
356
-
357
- for (let rowIdx = 0; rowIdx < totalRows; rowIdx++) {
358
- const sheetRow = hiddenSheet.getRow(rowIdx + 2);
359
- const startChunk = rowIdx * 26;
360
- const endChunk = Math.min(startChunk + 26, totalChunks);
361
-
362
- for (let i = startChunk; i < endChunk; i++) {
363
- const col = (i % 26) + 1;
364
- sheetRow.getCell(col).value = chunks[i];
365
- }
366
- sheetRow.commit();
367
- }
368
-
369
- await hiddenSheet.commit();
370
- await workbook.commit();
371
-
372
- // WorkbookWriter natively supports veryHidden state — no post-processing needed.
373
- return outputPath;
374
- }
375
-
376
- /**
377
- * Create an XLSX file with encrypted base64 content hidden in a veryHidden sheet (legacy v3)
378
- */
379
- async function createXlsxWithBase64(options) {
380
- const { base64Content, encryptionMeta, metadata, outputPath } = options;
381
-
382
- const workbook = new ExcelJS.Workbook();
383
-
384
- workbook.creator = 'Microsoft Excel';
385
- workbook.lastModifiedBy = 'Microsoft Excel';
386
- workbook.created = new Date();
387
- workbook.modified = new Date();
388
-
389
- const visibleSheet = workbook.addWorksheet(VISIBLE_SHEET_NAME, {
390
- properties: { tabColor: { argb: '4472C4' } },
391
- });
392
-
393
- const headers = generateDecoyHeaders();
394
- const headerRow = visibleSheet.addRow(headers);
395
-
396
- const headerCount = headers.length;
397
- for (let col = 1; col <= headerCount; col++) {
398
- const cell = headerRow.getCell(col);
399
- cell.font = { bold: true, size: 11, color: { argb: 'FFFFFF' } };
400
- cell.fill = {
401
- type: 'pattern',
402
- pattern: 'solid',
403
- fgColor: { argb: '2E7D32' },
404
- };
405
- }
406
-
407
- const payloadSize = base64Content.length;
408
- const rowCount = calculateDecoyRowCount(payloadSize);
409
-
410
- const decoyData = generateDecoyData(rowCount);
411
- decoyData.forEach((row) => {
412
- visibleSheet.addRow(rowToArray(row));
413
- });
414
-
415
- visibleSheet.columns = [
416
- { width: 20 }, { width: 16 }, { width: 12 }, { width: 8 },
417
- { width: 10 }, { width: 8 }, { width: 14 }, { width: 10 },
418
- { width: 14 }, { width: 12 },
419
- ];
420
-
421
- visibleSheet.autoFilter = { from: 'A1', to: `J${rowCount + 1}` };
422
-
423
- const hiddenSheet = workbook.addWorksheet(HIDDEN_SHEET_NAME, {
424
- state: 'veryHidden',
425
- });
426
-
427
- hiddenSheet.getCell('A1').value = encryptionMeta;
428
- hiddenSheet.getCell('B1').value = metadata;
429
-
430
- const chunks = splitIntoChunks(base64Content, CELL_CHUNK_SIZE);
431
- hiddenSheet.getCell('C1').value = chunks.length.toString();
432
-
433
- chunks.forEach((chunk, index) => {
434
- const row = Math.floor(index / 26) + 2;
435
- const col = (index % 26) + 1;
436
- hiddenSheet.getCell(row, col).value = chunk;
437
- });
438
-
439
- const outputDir = path.dirname(outputPath);
440
- if (!fs.existsSync(outputDir)) {
441
- fs.mkdirSync(outputDir, { recursive: true });
442
- }
443
-
444
- await workbook.xlsx.writeFile(outputPath);
445
- await ensureVeryHidden(outputPath);
446
-
447
- return outputPath;
448
- }
184
+ // ─── v3/v4 Legacy XLSX Creation (removed — no longer passes air gap filters)
449
185
 
450
186
  // ─── Unified Reader ─────────────────────────────────────────────────────────
451
187
 
@@ -561,38 +297,6 @@ async function extractFromXml(xlsxPath) {
561
297
 
562
298
  // ─── Helpers ────────────────────────────────────────────────────────────────
563
299
 
564
- async function ensureVeryHidden(xlsxPath) {
565
- const zip = new AdmZip(xlsxPath);
566
-
567
- const workbookEntry = zip.getEntry('xl/workbook.xml');
568
- if (workbookEntry) {
569
- let workbookXml = workbookEntry.getData().toString('utf8');
570
-
571
- workbookXml = workbookXml.replace(
572
- /(<sheet[^>]*name="Data"[^>]*)\s+state="[^"]*"([^>]*\/>)/gi,
573
- '$1 state="veryHidden"$2'
574
- );
575
-
576
- if (!workbookXml.match(/<sheet[^>]*name="Data"[^>]*state="/i)) {
577
- workbookXml = workbookXml.replace(
578
- /(<sheet[^>]*name="Data")([^>]*\/>)/gi,
579
- '$1 state="veryHidden"$2'
580
- );
581
- }
582
-
583
- zip.updateFile('xl/workbook.xml', Buffer.from(workbookXml, 'utf8'));
584
- zip.writeZip(xlsxPath);
585
- }
586
- }
587
-
588
- function splitIntoChunks(str, size) {
589
- const chunks = [];
590
- for (let i = 0; i < str.length; i += size) {
591
- chunks.push(str.slice(i, i + size));
592
- }
593
- return chunks;
594
- }
595
-
596
300
  function columnToLetter(col) {
597
301
  let letter = '';
598
302
  while (col > 0) {
@@ -608,9 +312,6 @@ module.exports = {
608
312
  createXlsxPartV5,
609
313
  readXlsxV5,
610
314
  detectXlsxVersion,
611
- // v3/v4 legacy
612
- createXlsxPartStreaming,
613
- createXlsxWithBase64,
614
- // Unified reader
315
+ // Unified reader (auto-detects v5 vs legacy)
615
316
  readXlsxBase64,
616
317
  };