@stll/folio-core 0.3.1 → 0.5.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 (120) hide show
  1. package/dist/ai-edits/apply.d.ts +3 -9
  2. package/dist/ai-edits/apply.js +94 -14
  3. package/dist/ai-edits/blockRange.d.ts +33 -0
  4. package/dist/ai-edits/blockRange.js +58 -0
  5. package/dist/ai-edits/headless.d.ts +35 -5
  6. package/dist/ai-edits/headless.js +92 -22
  7. package/dist/ai-edits/index.d.ts +5 -3
  8. package/dist/ai-edits/index.js +3 -2
  9. package/dist/ai-edits/snapshot.d.ts +14 -2
  10. package/dist/ai-edits/snapshot.js +12 -1
  11. package/dist/ai-edits/types.d.ts +42 -3
  12. package/dist/compat/eigenpal.d.ts +3 -2
  13. package/dist/compat/eigenpal.js +6 -5
  14. package/dist/controller/layoutPipeline.d.ts +1 -0
  15. package/dist/controller/layoutPipeline.js +35 -115
  16. package/dist/controller/layoutScheduler.d.ts +2 -1
  17. package/dist/controller/layoutScheduler.js +6 -0
  18. package/dist/document-operations.d.ts +122 -0
  19. package/dist/document-operations.js +575 -0
  20. package/dist/docx/blockContentParser.js +17 -7
  21. package/dist/docx/encryption/agileDecryption.d.ts +6 -0
  22. package/dist/docx/encryption/agileDecryption.js +209 -0
  23. package/dist/docx/encryption/compoundFile.d.ts +20 -0
  24. package/dist/docx/encryption/compoundFile.js +251 -0
  25. package/dist/docx/encryption/containerFormat.d.ts +15 -0
  26. package/dist/docx/encryption/containerFormat.js +40 -0
  27. package/dist/docx/encryption/cryptoBytes.d.ts +11 -0
  28. package/dist/docx/encryption/cryptoBytes.js +55 -0
  29. package/dist/docx/encryption/encryptionInfo.d.ts +32 -0
  30. package/dist/docx/encryption/encryptionInfo.js +128 -0
  31. package/dist/docx/encryption/errors.d.ts +18 -0
  32. package/dist/docx/encryption/errors.js +13 -0
  33. package/dist/docx/encryption/index.d.ts +4 -0
  34. package/dist/docx/encryption/index.js +4 -0
  35. package/dist/docx/encryption/openEncryptedDocx.d.ts +28 -0
  36. package/dist/docx/encryption/openEncryptedDocx.js +65 -0
  37. package/dist/docx/footnoteParser.d.ts +1 -17
  38. package/dist/docx/footnoteParser.js +1 -27
  39. package/dist/docx/groupDrawingParser.d.ts +8 -0
  40. package/dist/docx/groupDrawingParser.js +131 -0
  41. package/dist/docx/index.d.ts +3 -1
  42. package/dist/docx/index.js +3 -1
  43. package/dist/docx/numberingParser.d.ts +3 -1
  44. package/dist/docx/numberingParser.js +8 -2
  45. package/dist/docx/paragraphParser.js +9 -3
  46. package/dist/docx/parser.d.ts +4 -5
  47. package/dist/docx/parser.js +8 -2
  48. package/dist/docx/runParser.js +8 -1
  49. package/dist/docx/selectiveXmlPatch.js +4 -10
  50. package/dist/docx/serializer/numberingSerializer.js +3 -1
  51. package/dist/docx/settingsParser.d.ts +6 -3
  52. package/dist/docx/settingsParser.js +2 -0
  53. package/dist/docx/unzip.d.ts +7 -5
  54. package/dist/docx/unzip.js +8 -2
  55. package/dist/docx/xmlParser.js +11 -4
  56. package/dist/index.d.ts +3 -2
  57. package/dist/index.js +6 -5
  58. package/dist/layout-bridge/convert/footnoteLayout.d.ts +37 -1
  59. package/dist/layout-bridge/convert/footnoteLayout.js +138 -14
  60. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +6 -8
  61. package/dist/layout-bridge/convert/headerFooterLayout.js +21 -13
  62. package/dist/layout-bridge/convert/toFlowBlocks.js +58 -19
  63. package/dist/layout-engine/index.d.ts +6 -5
  64. package/dist/layout-engine/index.js +116 -27
  65. package/dist/layout-engine/keep-together.d.ts +13 -8
  66. package/dist/layout-engine/keep-together.js +40 -25
  67. package/dist/layout-engine/layoutInstrumentation.d.ts +11 -1
  68. package/dist/layout-engine/layoutInstrumentation.js +10 -1
  69. package/dist/layout-engine/measure/measureBlocks.js +43 -19
  70. package/dist/layout-engine/measure/measureParagraph.js +91 -14
  71. package/dist/layout-engine/paginator.d.ts +2 -1
  72. package/dist/layout-engine/paginator.js +9 -3
  73. package/dist/layout-engine/textBoxGroup.d.ts +7 -0
  74. package/dist/layout-engine/textBoxGroup.js +8 -0
  75. package/dist/layout-engine/types.d.ts +33 -9
  76. package/dist/layout-engine/types.js +20 -2
  77. package/dist/layout-painter/renderPage.d.ts +15 -1
  78. package/dist/layout-painter/renderPage.js +29 -12
  79. package/dist/layout-painter/renderParagraph.js +44 -9
  80. package/dist/layout-painter/renderTable.js +27 -7
  81. package/dist/managers/DocumentLoaderManager.d.ts +3 -1
  82. package/dist/managers/DocumentLoaderManager.js +13 -5
  83. package/dist/managers/TableSelectionManager.js +1 -1
  84. package/dist/model.js +1 -1
  85. package/dist/paged-layout/pageText.d.ts +14 -0
  86. package/dist/paged-layout/pageText.js +21 -0
  87. package/dist/prosemirror/attrs/index.js +3 -0
  88. package/dist/prosemirror/commands/comments.js +75 -5
  89. package/dist/prosemirror/commands/hyperlink.js +4 -2
  90. package/dist/prosemirror/commands/image.js +2 -1
  91. package/dist/prosemirror/commands/index.js +1 -1
  92. package/dist/prosemirror/commands/propertyChangeScope.d.ts +68 -0
  93. package/dist/prosemirror/commands/propertyChangeScope.js +242 -0
  94. package/dist/prosemirror/conversion/fromProseDoc.js +21 -5
  95. package/dist/prosemirror/conversion/toProseDoc.js +43 -15
  96. package/dist/prosemirror/extensions/core/ParagraphExtension.js +3 -0
  97. package/dist/prosemirror/extensions/features/ListExtension.js +6 -0
  98. package/dist/prosemirror/extensions/nodes/TableExtension.js +12 -11
  99. package/dist/prosemirror/findReplaceSelection.js +2 -1
  100. package/dist/prosemirror/index.js +1 -1
  101. package/dist/prosemirror/insertOperations.js +1 -1
  102. package/dist/prosemirror/schema/nodes.d.ts +14 -7
  103. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  104. package/dist/prosemirror/utils/tabCalculator.js +4 -2
  105. package/dist/redline.d.ts +20 -0
  106. package/dist/redline.js +127 -0
  107. package/dist/server.d.ts +7 -3
  108. package/dist/server.js +6 -2
  109. package/dist/symbols.d.ts +35 -0
  110. package/dist/symbols.js +512 -0
  111. package/dist/utils/clipboard.js +3 -3
  112. package/dist/utils/fontLoader.js +3 -0
  113. package/dist/utils/fontResolver.js +65 -1
  114. package/dist/utils/zoom.d.ts +21 -0
  115. package/dist/utils/zoom.js +21 -0
  116. package/dist/version-comparison.d.ts +92 -0
  117. package/dist/version-comparison.js +436 -0
  118. package/package.json +7 -3
  119. package/dist/paged-layout/headerFooterMargins.d.ts +0 -73
  120. package/dist/paged-layout/headerFooterMargins.js +0 -120
@@ -0,0 +1,209 @@
1
+ import { bytesEqual, joinBytes, padToBlock, passwordToUtf16Le, toArrayBuffer, writeUint32Le } from "./cryptoBytes.js";
2
+ import { DOCX_ENCRYPTION_ERROR_CODES, DocxEncryptionError } from "./errors.js";
3
+ //#region src/docx/encryption/agileDecryption.ts
4
+ /**
5
+ * Agile Encryption decryption (Office 2010+) using Web Crypto.
6
+ *
7
+ * Algorithm steps follow [MS-OFFCRYPTO] sections 2.3.4.9 through 2.3.4.15.
8
+ *
9
+ * @see https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/
10
+ */
11
+ /** [MS-OFFCRYPTO] 2.3.4.15 — block keys used during password verification. */
12
+ const PASSWORD_INPUT_BLOCK_KEY = new Uint8Array([
13
+ 254,
14
+ 167,
15
+ 210,
16
+ 118,
17
+ 59,
18
+ 75,
19
+ 158,
20
+ 121
21
+ ]);
22
+ const PASSWORD_DIGEST_BLOCK_KEY = new Uint8Array([
23
+ 215,
24
+ 170,
25
+ 15,
26
+ 109,
27
+ 48,
28
+ 97,
29
+ 52,
30
+ 78
31
+ ]);
32
+ const INTERMEDIATE_KEY_BLOCK_KEY = new Uint8Array([
33
+ 20,
34
+ 110,
35
+ 11,
36
+ 231,
37
+ 171,
38
+ 172,
39
+ 208,
40
+ 214
41
+ ]);
42
+ const INTEGRITY_KEY_BLOCK_KEY = new Uint8Array([
43
+ 95,
44
+ 178,
45
+ 173,
46
+ 1,
47
+ 12,
48
+ 185,
49
+ 225,
50
+ 246
51
+ ]);
52
+ const INTEGRITY_DIGEST_BLOCK_KEY = new Uint8Array([
53
+ 160,
54
+ 103,
55
+ 127,
56
+ 2,
57
+ 178,
58
+ 44,
59
+ 132,
60
+ 51
61
+ ]);
62
+ const PACKAGE_PLAINTEXT_SIZE_BYTES = 8;
63
+ const PACKAGE_SEGMENT_BYTES = 4096;
64
+ const subtleCrypto = () => {
65
+ const crypto = globalThis.crypto?.subtle;
66
+ if (!crypto) throw new DocxEncryptionError({
67
+ code: DOCX_ENCRYPTION_ERROR_CODES.DECRYPTION_FAILED,
68
+ message: "Web Crypto API (SubtleCrypto) is not available in this environment"
69
+ });
70
+ return crypto;
71
+ };
72
+ const webHashName = (officeName) => {
73
+ const resolved = {
74
+ SHA1: "SHA-1",
75
+ "SHA-1": "SHA-1",
76
+ SHA256: "SHA-256",
77
+ "SHA-256": "SHA-256",
78
+ SHA384: "SHA-384",
79
+ "SHA-384": "SHA-384",
80
+ SHA512: "SHA-512",
81
+ "SHA-512": "SHA-512"
82
+ }[officeName];
83
+ if (!resolved) throw new DocxEncryptionError({
84
+ code: DOCX_ENCRYPTION_ERROR_CODES.DECRYPTION_FAILED,
85
+ message: `Unsupported hash algorithm: ${officeName}`
86
+ });
87
+ return resolved;
88
+ };
89
+ const digest = async (algorithm, input) => {
90
+ const output = await subtleCrypto().digest(algorithm, toArrayBuffer(input));
91
+ return new Uint8Array(output);
92
+ };
93
+ const importAesKey = (rawKey, usages) => subtleCrypto().importKey("raw", toArrayBuffer(rawKey), { name: "AES-CBC" }, false, usages);
94
+ const decryptAesCbc = async (rawKey, iv, ciphertext) => {
95
+ const blockSize = 16;
96
+ if (ciphertext.length === 0 || ciphertext.length % blockSize !== 0) throw new DocxEncryptionError({
97
+ code: DOCX_ENCRYPTION_ERROR_CODES.DECRYPTION_FAILED,
98
+ message: "AES ciphertext length must be a positive multiple of the block size"
99
+ });
100
+ const subtle = subtleCrypto();
101
+ const key = await importAesKey(rawKey, ["encrypt", "decrypt"]);
102
+ const lastBlock = ciphertext.subarray(ciphertext.length - blockSize);
103
+ const pkcs7PadPlaintext = new Uint8Array(blockSize);
104
+ pkcs7PadPlaintext.fill(blockSize);
105
+ const syntheticPadding = new Uint8Array(await subtle.encrypt({
106
+ name: "AES-CBC",
107
+ iv: toArrayBuffer(lastBlock)
108
+ }, key, pkcs7PadPlaintext)).subarray(0, blockSize);
109
+ const paddedCiphertext = new Uint8Array(ciphertext.length + blockSize);
110
+ paddedCiphertext.set(ciphertext);
111
+ paddedCiphertext.set(syntheticPadding, ciphertext.length);
112
+ const decrypted = await subtle.decrypt({
113
+ name: "AES-CBC",
114
+ iv: toArrayBuffer(iv)
115
+ }, key, toArrayBuffer(paddedCiphertext));
116
+ return new Uint8Array(decrypted).subarray(0, ciphertext.length);
117
+ };
118
+ /** [MS-OFFCRYPTO] 2.3.4.9 — iterated hash of password + salt. */
119
+ const hashPassword = async (password, salt, iterations, hashName) => {
120
+ const algorithm = webHashName(hashName);
121
+ let state = await digest(algorithm, joinBytes([salt, passwordToUtf16Le(password)]));
122
+ for (let i = 0; i < iterations; i++) state = await digest(algorithm, joinBytes([writeUint32Le(i), state]));
123
+ return state;
124
+ };
125
+ /** [MS-OFFCRYPTO] 2.3.4.9 — finalize a derived key with a block key suffix. */
126
+ const finalizeDerivedKey = async (passwordHash, blockKey, hashName, keyBits) => {
127
+ const algorithm = webHashName(hashName);
128
+ return (await digest(algorithm, joinBytes([passwordHash, blockKey]))).subarray(0, keyBits / 8);
129
+ };
130
+ const segmentIv = async (packageSalt, segmentIndex, hashName, blockBytes) => {
131
+ return (await digest(webHashName(hashName), joinBytes([packageSalt, writeUint32Le(segmentIndex)]))).subarray(0, blockBytes);
132
+ };
133
+ const integrityIv = async (packageSalt, blockKey, hashName, blockBytes) => {
134
+ return (await digest(webHashName(hashName), joinBytes([packageSalt, blockKey]))).subarray(0, blockBytes);
135
+ };
136
+ const unlockIntermediateKey = async (password, material) => {
137
+ const passwordHash = await hashPassword(password, material.passwordSalt, material.passwordIterations, material.passwordHash);
138
+ const verifierKey = await finalizeDerivedKey(passwordHash, PASSWORD_INPUT_BLOCK_KEY, material.passwordHash, material.passwordKeyBits);
139
+ const digestKey = await finalizeDerivedKey(passwordHash, PASSWORD_DIGEST_BLOCK_KEY, material.passwordHash, material.passwordKeyBits);
140
+ const intermediateKey = await finalizeDerivedKey(passwordHash, INTERMEDIATE_KEY_BLOCK_KEY, material.passwordHash, material.passwordKeyBits);
141
+ const verifierIv = padToBlock(material.passwordSalt, material.passwordBlockBytes);
142
+ const verifierPlaintext = await decryptAesCbc(verifierKey, verifierIv, material.encryptedVerifierInput);
143
+ const digestPlaintext = await decryptAesCbc(digestKey, verifierIv, material.encryptedVerifierDigest);
144
+ const actualDigest = await digest(webHashName(material.passwordHash), verifierPlaintext);
145
+ if (!bytesEqual(actualDigest, digestPlaintext.subarray(0, actualDigest.length))) throw new DocxEncryptionError({
146
+ code: DOCX_ENCRYPTION_ERROR_CODES.PASSWORD_INVALID,
147
+ message: "The password is incorrect"
148
+ });
149
+ return decryptAesCbc(intermediateKey, verifierIv, material.encryptedIntermediateKey);
150
+ };
151
+ const assertPackageIntegrity = async (intermediateKey, material, encryptedPackage) => {
152
+ const ivForKey = await integrityIv(material.packageSalt, INTEGRITY_KEY_BLOCK_KEY, material.packageHash, material.cipherBlockBytes);
153
+ const ivForDigest = await integrityIv(material.packageSalt, INTEGRITY_DIGEST_BLOCK_KEY, material.packageHash, material.cipherBlockBytes);
154
+ const integrityKey = await decryptAesCbc(intermediateKey, ivForKey, material.encryptedIntegrityKey);
155
+ const expectedDigest = await decryptAesCbc(intermediateKey, ivForDigest, material.encryptedIntegrityDigest);
156
+ const hmacKey = await subtleCrypto().importKey("raw", toArrayBuffer(integrityKey), {
157
+ name: "HMAC",
158
+ hash: webHashName(material.packageHash)
159
+ }, false, ["sign"]);
160
+ const actualDigest = new Uint8Array(await subtleCrypto().sign("HMAC", hmacKey, toArrayBuffer(encryptedPackage)));
161
+ if (!bytesEqual(actualDigest, expectedDigest.subarray(0, actualDigest.length))) throw new DocxEncryptionError({
162
+ code: DOCX_ENCRYPTION_ERROR_CODES.DECRYPTION_FAILED,
163
+ message: "Data integrity check failed — the encrypted package may be corrupt or tampered with"
164
+ });
165
+ };
166
+ const decryptEncryptedPackage = async (intermediateKey, material, encryptedPackage) => {
167
+ if (encryptedPackage.length < PACKAGE_PLAINTEXT_SIZE_BYTES) throw new DocxEncryptionError({
168
+ code: DOCX_ENCRYPTION_ERROR_CODES.DECRYPTION_FAILED,
169
+ message: "EncryptedPackage stream is too short to contain a size prefix"
170
+ });
171
+ const plaintextSizeBig = new DataView(encryptedPackage.buffer, encryptedPackage.byteOffset, PACKAGE_PLAINTEXT_SIZE_BYTES).getBigUint64(0, true);
172
+ if (plaintextSizeBig > BigInt(Number.MAX_SAFE_INTEGER)) throw new DocxEncryptionError({
173
+ code: DOCX_ENCRYPTION_ERROR_CODES.DECRYPTION_FAILED,
174
+ message: "EncryptedPackage plaintext size exceeds JavaScript's safe integer range"
175
+ });
176
+ const plaintextSize = Number(plaintextSizeBig);
177
+ const ciphertext = encryptedPackage.subarray(PACKAGE_PLAINTEXT_SIZE_BYTES);
178
+ const segmentCount = Math.ceil(ciphertext.length / PACKAGE_SEGMENT_BYTES);
179
+ const segments = [];
180
+ for (let index = 0; index < segmentCount; index++) {
181
+ const start = index * PACKAGE_SEGMENT_BYTES;
182
+ const end = Math.min(start + PACKAGE_SEGMENT_BYTES, ciphertext.length);
183
+ const chunk = ciphertext.subarray(start, end);
184
+ const iv = await segmentIv(material.packageSalt, index, material.packageHash, material.cipherBlockBytes);
185
+ segments.push(await decryptAesCbc(intermediateKey, iv, chunk));
186
+ }
187
+ const plaintext = joinBytes(segments);
188
+ if (plaintextSize > plaintext.length) throw new DocxEncryptionError({
189
+ code: DOCX_ENCRYPTION_ERROR_CODES.DECRYPTION_FAILED,
190
+ message: "EncryptedPackage plaintext size exceeds decrypted payload length"
191
+ });
192
+ return plaintext.subarray(0, plaintextSize);
193
+ };
194
+ const decryptAgileEncryptedPackage = async (password, material, encryptedPackage) => {
195
+ try {
196
+ const intermediateKey = await unlockIntermediateKey(password, material);
197
+ await assertPackageIntegrity(intermediateKey, material, encryptedPackage);
198
+ return await decryptEncryptedPackage(intermediateKey, material, encryptedPackage);
199
+ } catch (error) {
200
+ if (error instanceof DocxEncryptionError) throw error;
201
+ throw new DocxEncryptionError({
202
+ code: DOCX_ENCRYPTION_ERROR_CODES.DECRYPTION_FAILED,
203
+ message: "Decryption failed unexpectedly",
204
+ cause: error
205
+ });
206
+ }
207
+ };
208
+ //#endregion
209
+ export { decryptAgileEncryptedPackage };
@@ -0,0 +1,20 @@
1
+ //#region src/docx/encryption/compoundFile.d.ts
2
+ /**
3
+ * Read-only MS-CFB compound file access for encrypted OOXML packages.
4
+ *
5
+ * Encrypted `.docx` files store `EncryptionInfo` and `EncryptedPackage` streams
6
+ * at the compound-file root. This reader implements only what MS-OFFCRYPTO needs.
7
+ *
8
+ * @see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/
9
+ */
10
+ type CompoundFileReader = {
11
+ readStream: (absolutePath: string) => Uint8Array | null;
12
+ };
13
+ declare const openCompoundFile: (data: ArrayBuffer | Uint8Array) => CompoundFileReader;
14
+ type EncryptedPackageStreams = {
15
+ encryptionInfo: Uint8Array;
16
+ encryptedPackage: Uint8Array;
17
+ };
18
+ declare const readEncryptedPackageStreams: (data: ArrayBuffer | Uint8Array) => EncryptedPackageStreams;
19
+ //#endregion
20
+ export { CompoundFileReader, EncryptedPackageStreams, openCompoundFile, readEncryptedPackageStreams };
@@ -0,0 +1,251 @@
1
+ import { joinBytes } from "./cryptoBytes.js";
2
+ //#region src/docx/encryption/compoundFile.ts
3
+ /**
4
+ * Read-only MS-CFB compound file access for encrypted OOXML packages.
5
+ *
6
+ * Encrypted `.docx` files store `EncryptionInfo` and `EncryptedPackage` streams
7
+ * at the compound-file root. This reader implements only what MS-OFFCRYPTO needs.
8
+ *
9
+ * @see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/
10
+ */
11
+ const HEADER_BYTES = 512;
12
+ const DIRECTORY_RECORD_BYTES = 128;
13
+ const HEADER_DIFAT_SLOTS = 109;
14
+ const SIGNATURE = [
15
+ 208,
16
+ 207,
17
+ 17,
18
+ 224,
19
+ 161,
20
+ 177,
21
+ 26,
22
+ 225
23
+ ];
24
+ const LITTLE_ENDIAN_MARK = 65534;
25
+ const SECTOR_FREE = 4294967295;
26
+ const SECTOR_END = 4294967294;
27
+ const SECTOR_DIFAT = 4294967292;
28
+ const DIR_NONE = 4294967295;
29
+ const ENTRY_KIND = {
30
+ STORAGE: 1,
31
+ STREAM: 2,
32
+ ROOT: 5
33
+ };
34
+ const viewOf = (bytes) => new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
35
+ const u16 = (view, offset) => view.getUint16(offset, true);
36
+ const u32 = (view, offset) => view.getUint32(offset, true);
37
+ const u64 = (view, offset) => {
38
+ const low = BigInt(view.getUint32(offset, true));
39
+ return BigInt(view.getUint32(offset + 4, true)) << 32n | low;
40
+ };
41
+ const decodeName = (nameBytes) => {
42
+ const decoded = new TextDecoder("utf-16le").decode(nameBytes);
43
+ let end = decoded.length;
44
+ while (end > 0 && decoded.charCodeAt(end - 1) === 0) end--;
45
+ return decoded.slice(0, end);
46
+ };
47
+ const sectorOffset = (sectorId, sectorBytes) => (sectorId + 1) * sectorBytes;
48
+ const readHeader = (file) => {
49
+ if (file.length < HEADER_BYTES) throw new Error(`Compound file header truncated (${file.length} bytes)`);
50
+ for (let i = 0; i < SIGNATURE.length; i++) if (file[i] !== SIGNATURE[i]) throw new Error("Invalid compound file signature");
51
+ const view = viewOf(file);
52
+ const major = u16(view, 26);
53
+ const endian = u16(view, 28);
54
+ const sectorShift = u16(view, 30);
55
+ const miniShift = u16(view, 32);
56
+ const sectorBytes = 1 << sectorShift;
57
+ const miniSectorBytes = 1 << miniShift;
58
+ if (endian !== LITTLE_ENDIAN_MARK) throw new Error(`Unsupported compound file endianness: 0x${endian.toString(16)}`);
59
+ if (!(major === 3 && sectorBytes === 512 || major === 4 && sectorBytes === 4096)) throw new Error(`Unsupported compound file version ${major} (sector size ${sectorBytes})`);
60
+ const headerDifat = [];
61
+ for (let slot = 0; slot < HEADER_DIFAT_SLOTS; slot++) {
62
+ const id = u32(view, 76 + slot * 4);
63
+ if (id !== SECTOR_FREE) headerDifat.push(id);
64
+ }
65
+ return {
66
+ sectorBytes,
67
+ miniSectorBytes,
68
+ fatSectorCount: u32(view, 44),
69
+ directoryStartSector: u32(view, 48),
70
+ miniCutoff: u32(view, 56),
71
+ miniFatStart: u32(view, 60),
72
+ miniFatSectorCount: u32(view, 64),
73
+ difatStart: u32(view, 68),
74
+ difatSectorCount: u32(view, 72),
75
+ headerDifat
76
+ };
77
+ };
78
+ const loadSector = (file, header, sectorId) => {
79
+ if (sectorId >= SECTOR_DIFAT) throw new Error(`Invalid sector id 0x${sectorId.toString(16)}`);
80
+ const offset = sectorOffset(sectorId, header.sectorBytes);
81
+ if (offset + header.sectorBytes > file.length) throw new Error(`Sector ${sectorId} extends past end of file`);
82
+ return file.subarray(offset, offset + header.sectorBytes);
83
+ };
84
+ const loadFatSectorIds = (file, header) => {
85
+ const ids = [...header.headerDifat];
86
+ let chain = header.difatStart;
87
+ for (let step = 0; step < header.difatSectorCount; step++) {
88
+ if (chain === SECTOR_END || chain === SECTOR_FREE) break;
89
+ const sector = loadSector(file, header, chain);
90
+ const view = viewOf(sector);
91
+ const slotsPerSector = header.sectorBytes / 4 - 1;
92
+ for (let slot = 0; slot < slotsPerSector; slot++) {
93
+ const id = u32(view, slot * 4);
94
+ if (id !== SECTOR_FREE) ids.push(id);
95
+ }
96
+ chain = u32(view, header.sectorBytes - 4);
97
+ }
98
+ if (ids.length < header.fatSectorCount) throw new Error("DIFAT chain shorter than declared FAT sector count");
99
+ return ids.slice(0, header.fatSectorCount);
100
+ };
101
+ const loadFat = (file, header) => {
102
+ const table = [];
103
+ for (const fatSectorId of loadFatSectorIds(file, header)) {
104
+ const sector = loadSector(file, header, fatSectorId);
105
+ const view = viewOf(sector);
106
+ const entries = header.sectorBytes / 4;
107
+ for (let i = 0; i < entries; i++) table.push(u32(view, i * 4));
108
+ }
109
+ return table;
110
+ };
111
+ const followSectorChain = (file, header, fat, start) => {
112
+ if (start === SECTOR_END || start === SECTOR_FREE) return /* @__PURE__ */ new Uint8Array(0);
113
+ const parts = [];
114
+ const seen = /* @__PURE__ */ new Set();
115
+ let sectorId = start;
116
+ while (sectorId !== SECTOR_END) {
117
+ if (seen.has(sectorId)) throw new Error(`Sector chain loop at sector ${sectorId}`);
118
+ if (sectorId >= fat.length) throw new Error(`Sector ${sectorId} outside FAT`);
119
+ seen.add(sectorId);
120
+ parts.push(loadSector(file, header, sectorId));
121
+ const next = fat[sectorId];
122
+ if (next === void 0) throw new Error(`FAT missing link after sector ${sectorId}`);
123
+ sectorId = next;
124
+ }
125
+ return joinBytes(parts);
126
+ };
127
+ const loadDirectory = (file, header, fat) => {
128
+ const raw = followSectorChain(file, header, fat, header.directoryStartSector);
129
+ const records = [];
130
+ for (let pos = 0; pos + DIRECTORY_RECORD_BYTES <= raw.length; pos += DIRECTORY_RECORD_BYTES) {
131
+ const slice = raw.subarray(pos, pos + DIRECTORY_RECORD_BYTES);
132
+ const view = viewOf(slice);
133
+ const nameChars = Math.max(0, Math.min(u16(view, 64), 64) - 2);
134
+ records.push({
135
+ name: decodeName(slice.subarray(0, nameChars)),
136
+ kind: slice[66] ?? 0,
137
+ leftId: u32(view, 68),
138
+ rightId: u32(view, 72),
139
+ childId: u32(view, 76),
140
+ firstSector: u32(view, 116),
141
+ byteLength: (() => {
142
+ const size = u64(view, 120);
143
+ if (size > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`Stream too large: ${size.toString()}`);
144
+ return Number(size);
145
+ })()
146
+ });
147
+ }
148
+ return records;
149
+ };
150
+ /** Breadth-first sibling-tree walk (MS-CFB red-black sibling links). */
151
+ const walkSiblingTree = (records, rootId, visit) => {
152
+ const queue = [];
153
+ const enqueueSubtree = (id) => {
154
+ if (id === DIR_NONE || id >= records.length) return;
155
+ const stack = [id];
156
+ const queued = /* @__PURE__ */ new Set();
157
+ while (stack.length > 0) {
158
+ const current = stack.pop();
159
+ if (queued.has(current)) throw new Error(`Directory sibling loop at entry ${current}`);
160
+ queued.add(current);
161
+ queue.push(current);
162
+ const node = records[current];
163
+ if (!node) continue;
164
+ if (node.leftId !== DIR_NONE) stack.push(node.leftId);
165
+ if (node.rightId !== DIR_NONE) stack.push(node.rightId);
166
+ }
167
+ };
168
+ enqueueSubtree(rootId);
169
+ for (const id of queue) {
170
+ const record = records[id];
171
+ if (record) visit(record);
172
+ }
173
+ };
174
+ const resolvePath = (records, path) => {
175
+ const parts = path.split("/").filter(Boolean);
176
+ const root = records[0];
177
+ if (!root) return null;
178
+ let current = root;
179
+ for (const part of parts) {
180
+ if (current.kind !== ENTRY_KIND.ROOT && current.kind !== ENTRY_KIND.STORAGE) return null;
181
+ let match = null;
182
+ walkSiblingTree(records, current.childId, (candidate) => {
183
+ if (candidate.name.toLowerCase() === part.toLowerCase()) match = candidate;
184
+ });
185
+ if (!match) return null;
186
+ current = match;
187
+ }
188
+ return current;
189
+ };
190
+ const loadMiniFat = (file, header, fat) => {
191
+ if (header.miniFatSectorCount === 0 || header.miniFatStart === SECTOR_END) return [];
192
+ const bytes = followSectorChain(file, header, fat, header.miniFatStart);
193
+ const view = viewOf(bytes);
194
+ const count = Math.floor(bytes.length / 4);
195
+ const entries = [];
196
+ for (let i = 0; i < count; i++) entries.push(u32(view, i * 4));
197
+ return entries;
198
+ };
199
+ const followMiniChain = (container, miniFat, miniSectorBytes, start) => {
200
+ if (start === SECTOR_END || start === SECTOR_FREE) return /* @__PURE__ */ new Uint8Array(0);
201
+ const parts = [];
202
+ const seen = /* @__PURE__ */ new Set();
203
+ let sectorId = start;
204
+ while (sectorId !== SECTOR_END) {
205
+ if (seen.has(sectorId)) throw new Error(`Mini sector chain loop at ${sectorId}`);
206
+ if (sectorId >= miniFat.length) throw new Error(`Mini sector ${sectorId} outside MiniFAT`);
207
+ seen.add(sectorId);
208
+ const offset = sectorId * miniSectorBytes;
209
+ if (offset + miniSectorBytes > container.length) throw new Error(`Mini sector ${sectorId} extends past mini stream`);
210
+ parts.push(container.subarray(offset, offset + miniSectorBytes));
211
+ const next = miniFat[sectorId];
212
+ if (next === void 0) throw new Error(`MiniFAT missing link after mini sector ${sectorId}`);
213
+ sectorId = next;
214
+ }
215
+ return joinBytes(parts);
216
+ };
217
+ const openCompoundFile = (data) => {
218
+ const file = data instanceof Uint8Array ? data : new Uint8Array(data);
219
+ const header = readHeader(file);
220
+ const fat = loadFat(file, header);
221
+ const directory = loadDirectory(file, header, fat);
222
+ const root = directory[0];
223
+ if (!root || root.kind !== ENTRY_KIND.ROOT) throw new Error("Compound file root entry missing");
224
+ const miniFat = loadMiniFat(file, header, fat);
225
+ const miniContainer = root.byteLength > 0 && root.firstSector !== SECTOR_END ? followSectorChain(file, header, fat, root.firstSector).subarray(0, root.byteLength) : /* @__PURE__ */ new Uint8Array(0);
226
+ return { readStream(absolutePath) {
227
+ const entry = resolvePath(directory, absolutePath);
228
+ if (!entry || entry.kind !== ENTRY_KIND.STREAM) return null;
229
+ if (entry.byteLength < header.miniCutoff) return followMiniChain(miniContainer, miniFat, header.miniSectorBytes, entry.firstSector).subarray(0, entry.byteLength);
230
+ return followSectorChain(file, header, fat, entry.firstSector).subarray(0, entry.byteLength);
231
+ } };
232
+ };
233
+ const readEncryptedPackageStreams = (data) => {
234
+ const file = data instanceof Uint8Array ? data : new Uint8Array(data);
235
+ let reader;
236
+ try {
237
+ reader = openCompoundFile(file);
238
+ } catch (cause) {
239
+ throw new Error("Failed to parse encrypted OOXML compound file", { cause });
240
+ }
241
+ const encryptionInfo = reader.readStream("/EncryptionInfo");
242
+ const encryptedPackage = reader.readStream("/EncryptedPackage");
243
+ if (!encryptionInfo) throw new Error("Compound file missing EncryptionInfo stream");
244
+ if (!encryptedPackage) throw new Error("Compound file missing EncryptedPackage stream");
245
+ return {
246
+ encryptionInfo,
247
+ encryptedPackage
248
+ };
249
+ };
250
+ //#endregion
251
+ export { openCompoundFile, readEncryptedPackageStreams };
@@ -0,0 +1,15 @@
1
+ //#region src/docx/encryption/containerFormat.d.ts
2
+ /**
3
+ * OOXML container format detection (ZIP vs encrypted OLE compound file).
4
+ *
5
+ * @see https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/
6
+ */
7
+ declare const DOCX_CONTAINER_TYPES: {
8
+ readonly ZIP: "zip";
9
+ readonly CFB: "cfb";
10
+ readonly UNKNOWN: "unknown";
11
+ };
12
+ type DocxContainerType = (typeof DOCX_CONTAINER_TYPES)[keyof typeof DOCX_CONTAINER_TYPES];
13
+ declare const detectDocxContainerType: (data: ArrayBuffer | Uint8Array) => DocxContainerType;
14
+ //#endregion
15
+ export { DOCX_CONTAINER_TYPES, DocxContainerType, detectDocxContainerType };
@@ -0,0 +1,40 @@
1
+ //#region src/docx/encryption/containerFormat.ts
2
+ /**
3
+ * OOXML container format detection (ZIP vs encrypted OLE compound file).
4
+ *
5
+ * @see https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/
6
+ */
7
+ const DOCX_CONTAINER_TYPES = {
8
+ ZIP: "zip",
9
+ CFB: "cfb",
10
+ UNKNOWN: "unknown"
11
+ };
12
+ const ZIP_SIGNATURE = [
13
+ 80,
14
+ 75,
15
+ 3,
16
+ 4
17
+ ];
18
+ const OLE_SIGNATURE = [
19
+ 208,
20
+ 207,
21
+ 17,
22
+ 224,
23
+ 161,
24
+ 177,
25
+ 26,
26
+ 225
27
+ ];
28
+ const startsWith = (bytes, signature) => {
29
+ if (bytes.length < signature.length) return false;
30
+ for (let i = 0; i < signature.length; i++) if (bytes[i] !== signature[i]) return false;
31
+ return true;
32
+ };
33
+ const detectDocxContainerType = (data) => {
34
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
35
+ if (startsWith(bytes, ZIP_SIGNATURE)) return DOCX_CONTAINER_TYPES.ZIP;
36
+ if (startsWith(bytes, OLE_SIGNATURE)) return DOCX_CONTAINER_TYPES.CFB;
37
+ return DOCX_CONTAINER_TYPES.UNKNOWN;
38
+ };
39
+ //#endregion
40
+ export { DOCX_CONTAINER_TYPES, detectDocxContainerType };
@@ -0,0 +1,11 @@
1
+ //#region src/docx/encryption/cryptoBytes.d.ts
2
+ /** Byte helpers shared by MS-CFB and MS-OFFCRYPTO agile decryption. */
3
+ declare const joinBytes: (parts: readonly Uint8Array[]) => Uint8Array;
4
+ declare const writeUint32Le: (value: number) => Uint8Array;
5
+ declare const passwordToUtf16Le: (password: string) => Uint8Array;
6
+ declare const bytesEqual: (left: Uint8Array, right: Uint8Array) => boolean;
7
+ declare const toArrayBuffer: (bytes: Uint8Array) => ArrayBuffer;
8
+ declare const decodeBase64: (encoded: string) => Uint8Array;
9
+ declare const padToBlock: (bytes: Uint8Array, blockSize: number, padByte?: number) => Uint8Array;
10
+ //#endregion
11
+ export { bytesEqual, decodeBase64, joinBytes, padToBlock, passwordToUtf16Le, toArrayBuffer, writeUint32Le };
@@ -0,0 +1,55 @@
1
+ //#region src/docx/encryption/cryptoBytes.ts
2
+ /** Byte helpers shared by MS-CFB and MS-OFFCRYPTO agile decryption. */
3
+ const joinBytes = (parts) => {
4
+ let total = 0;
5
+ for (const part of parts) total += part.length;
6
+ const out = new Uint8Array(total);
7
+ let offset = 0;
8
+ for (const part of parts) {
9
+ out.set(part, offset);
10
+ offset += part.length;
11
+ }
12
+ return out;
13
+ };
14
+ const writeUint32Le = (value) => {
15
+ const out = /* @__PURE__ */ new Uint8Array(4);
16
+ out[0] = value & 255;
17
+ out[1] = value >>> 8 & 255;
18
+ out[2] = value >>> 16 & 255;
19
+ out[3] = value >>> 24 & 255;
20
+ return out;
21
+ };
22
+ const passwordToUtf16Le = (password) => {
23
+ const out = new Uint8Array(password.length * 2);
24
+ for (let i = 0; i < password.length; i++) {
25
+ const code = password.charCodeAt(i);
26
+ out[i * 2] = code & 255;
27
+ out[i * 2 + 1] = code >> 8;
28
+ }
29
+ return out;
30
+ };
31
+ const bytesEqual = (left, right) => {
32
+ if (left.length !== right.length) return false;
33
+ let mismatch = 0;
34
+ for (let i = 0; i < left.length; i++) mismatch |= left[i] ^ right[i];
35
+ return mismatch === 0;
36
+ };
37
+ const toArrayBuffer = (bytes) => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
38
+ const decodeBase64 = (encoded) => {
39
+ if (typeof atob === "function") {
40
+ const binary = atob(encoded);
41
+ const out = new Uint8Array(binary.length);
42
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
43
+ return out;
44
+ }
45
+ return new Uint8Array(Buffer.from(encoded, "base64"));
46
+ };
47
+ const padToBlock = (bytes, blockSize, padByte = 54) => {
48
+ if (bytes.length >= blockSize) return bytes.subarray(0, blockSize);
49
+ const out = new Uint8Array(blockSize);
50
+ out.set(bytes);
51
+ out.fill(padByte, bytes.length);
52
+ return out;
53
+ };
54
+ //#endregion
55
+ export { bytesEqual, decodeBase64, joinBytes, padToBlock, passwordToUtf16Le, toArrayBuffer, writeUint32Le };
@@ -0,0 +1,32 @@
1
+ //#region src/docx/encryption/encryptionInfo.d.ts
2
+ /**
3
+ * Parse the EncryptionInfo stream for Agile Encryption (Office 2010+).
4
+ *
5
+ * @see https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/87020a34-e73f-4139-99bc-bbdf6cf6fa55
6
+ */
7
+ type AgileKeyMaterial = {
8
+ packageSalt: Uint8Array;
9
+ packageKeyBits: number;
10
+ packageHash: string;
11
+ packageHashBytes: number;
12
+ cipherBlockBytes: number;
13
+ cipherName: string;
14
+ cipherMode: string;
15
+ passwordIterations: number;
16
+ passwordSalt: Uint8Array;
17
+ passwordHash: string;
18
+ passwordKeyBits: number;
19
+ passwordBlockBytes: number;
20
+ encryptedVerifierInput: Uint8Array;
21
+ encryptedVerifierDigest: Uint8Array;
22
+ encryptedIntermediateKey: Uint8Array;
23
+ encryptedIntegrityKey: Uint8Array;
24
+ encryptedIntegrityDigest: Uint8Array;
25
+ };
26
+ type AgileEncryptionDescriptor = {
27
+ scheme: "agile";
28
+ material: AgileKeyMaterial;
29
+ };
30
+ declare const parseAgileEncryptionInfo: (stream: Uint8Array) => AgileEncryptionDescriptor;
31
+ //#endregion
32
+ export { AgileEncryptionDescriptor, AgileKeyMaterial, parseAgileEncryptionInfo };