@stll/folio-core 0.11.0 → 0.13.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 (263) hide show
  1. package/dist/ai-edits/apply.d.ts +2 -1
  2. package/dist/ai-edits/apply.js +693 -63
  3. package/dist/ai-edits/block-identity.d.ts +4 -0
  4. package/dist/ai-edits/block-identity.js +9 -0
  5. package/dist/ai-edits/blockRange.d.ts +28 -1
  6. package/dist/ai-edits/blockRange.js +68 -2
  7. package/dist/ai-edits/clean-text.js +2 -1
  8. package/dist/ai-edits/headless.d.ts +4 -4
  9. package/dist/ai-edits/headless.js +9 -9
  10. package/dist/ai-edits/index.d.ts +2 -2
  11. package/dist/ai-edits/index.js +2 -2
  12. package/dist/ai-edits/read.d.ts +7 -7
  13. package/dist/ai-edits/read.js +110 -3
  14. package/dist/ai-edits/snapshot.js +21 -3
  15. package/dist/ai-edits/table-cell-mutations.d.ts +48 -0
  16. package/dist/ai-edits/table-cell-mutations.js +238 -0
  17. package/dist/ai-edits/table-mutation-plan.d.ts +38 -0
  18. package/dist/ai-edits/table-mutation-plan.js +90 -0
  19. package/dist/ai-edits/table-row-column-mutations.d.ts +108 -0
  20. package/dist/ai-edits/table-row-column-mutations.js +300 -0
  21. package/dist/ai-edits/table-targets.d.ts +32 -0
  22. package/dist/ai-edits/table-targets.js +70 -0
  23. package/dist/ai-edits/types.d.ts +62 -1
  24. package/dist/ai-edits/word-diff.js +21 -0
  25. package/dist/compat/eigenpal.d.ts +3 -3
  26. package/dist/compat/eigenpal.js +3 -3
  27. package/dist/content-controls/mutateContentControls.js +1 -1
  28. package/dist/controller/activeEditorStory.d.ts +36 -0
  29. package/dist/controller/activeEditorStory.js +30 -0
  30. package/dist/controller/contentControlWidgetController.d.ts +55 -0
  31. package/dist/controller/contentControlWidgetController.js +141 -0
  32. package/dist/controller/fontReadiness.js +2 -0
  33. package/dist/controller/headerFooterEditorManager.js +2 -2
  34. package/dist/controller/hiddenEditorApi.js +2 -2
  35. package/dist/controller/hiddenEditorManager.d.ts +12 -1
  36. package/dist/controller/hiddenEditorManager.js +44 -12
  37. package/dist/controller/layoutPipeline.d.ts +2 -0
  38. package/dist/controller/layoutPipeline.js +17 -2
  39. package/dist/controller/noteEditorManager.d.ts +36 -0
  40. package/dist/controller/noteEditorManager.js +210 -0
  41. package/dist/document-operations.d.ts +43 -11
  42. package/dist/document-operations.js +186 -18
  43. package/dist/docx/blockContentParser.js +21 -7
  44. package/dist/docx/commentParser.js +6 -1
  45. package/dist/docx/commentRangeIntegrity.d.ts +6 -0
  46. package/dist/docx/commentRangeIntegrity.js +186 -0
  47. package/dist/docx/conformance.js +62 -1
  48. package/dist/docx/documentGrid.d.ts +8 -0
  49. package/dist/docx/documentGrid.js +8 -0
  50. package/dist/docx/drawingUtils.js +1 -1
  51. package/dist/docx/encryption/agileDecryption.js +12 -2
  52. package/dist/docx/encryption/compoundFile.js +6 -1
  53. package/dist/docx/encryption/encryptionInfo.d.ts +9 -1
  54. package/dist/docx/encryption/encryptionInfo.js +18 -2
  55. package/dist/docx/encryption/openEncryptedDocx.js +1 -1
  56. package/dist/docx/ensureParaIds.js +8 -7
  57. package/dist/docx/fieldParser.js +1 -1
  58. package/dist/docx/groupDrawingParser.js +10 -5
  59. package/dist/docx/hyperlinkParser.js +1 -1
  60. package/dist/docx/imageParser.js +10 -6
  61. package/dist/docx/metadataPrivacy.js +17 -3
  62. package/dist/docx/notePropertiesParser.js +1 -1
  63. package/dist/docx/numberingParser.js +1 -1
  64. package/dist/docx/paragraphParser.js +70 -61
  65. package/dist/docx/paragraphTextBoxEnrichment.d.ts +2 -1
  66. package/dist/docx/paragraphTextBoxEnrichment.js +67 -13
  67. package/dist/docx/paragraphTraversal.js +1 -1
  68. package/dist/docx/parser.js +3 -3
  69. package/dist/docx/rezip.js +20 -9
  70. package/dist/docx/runParser.js +17 -6
  71. package/dist/docx/sdtProperties.js +1 -1
  72. package/dist/docx/sectionParser.js +18 -5
  73. package/dist/docx/selectiveSave.js +5 -1
  74. package/dist/docx/selectiveXmlPatch.d.ts +22 -1
  75. package/dist/docx/selectiveXmlPatch.js +61 -1
  76. package/dist/docx/serializer/blockSdtSerializer.js +3 -2
  77. package/dist/docx/serializer/commentSerializer.js +5 -6
  78. package/dist/docx/serializer/documentSerializer.js +2 -2
  79. package/dist/docx/serializer/headerFooterSerializer.js +2 -2
  80. package/dist/docx/serializer/noteSerializer.js +2 -2
  81. package/dist/docx/serializer/paragraphSerializer.js +23 -15
  82. package/dist/docx/serializer/runSerializer.js +25 -21
  83. package/dist/docx/serializer/sectionPropertiesSerializer.js +2 -1
  84. package/dist/docx/serializer/stylesSerializer.js +1 -1
  85. package/dist/docx/serializer/tableSerializer.d.ts +5 -4
  86. package/dist/docx/serializer/tableSerializer.js +26 -20
  87. package/dist/docx/serializer/xmlUtils.d.ts +13 -1
  88. package/dist/docx/serializer/xmlUtils.js +27 -1
  89. package/dist/docx/server/applyDocxXmlPatchProposal.d.ts +87 -0
  90. package/dist/docx/server/applyDocxXmlPatchProposal.js +204 -0
  91. package/dist/docx/server/boundedArchive.d.ts +14 -3
  92. package/dist/docx/server/boundedArchive.js +56 -11
  93. package/dist/docx/server/evaluateDocxXmlPatchProposal.d.ts +85 -0
  94. package/dist/docx/server/evaluateDocxXmlPatchProposal.js +361 -0
  95. package/dist/docx/server/extractDocxText.js +55 -8
  96. package/dist/docx/server/inspectDocxPackage.d.ts +70 -0
  97. package/dist/docx/server/inspectDocxPackage.js +217 -0
  98. package/dist/docx/server/validateDocxConformance.d.ts +41 -0
  99. package/dist/docx/server/validateDocxConformance.js +336 -0
  100. package/dist/docx/settingsParser.js +26 -3
  101. package/dist/docx/shapeParser.js +1 -1
  102. package/dist/docx/styleParser.js +4 -2
  103. package/dist/docx/tableParser.d.ts +8 -1
  104. package/dist/docx/tableParser.js +31 -10
  105. package/dist/docx/textBoxParser.d.ts +3 -3
  106. package/dist/docx/textBoxParser.js +16 -12
  107. package/dist/docx/unzip.js +1 -0
  108. package/dist/docx/vmlImageParser.js +11 -3
  109. package/dist/docx/xmlParser.js +11 -1
  110. package/dist/docx/xmlSafety.d.ts +5 -0
  111. package/dist/docx/xmlSafety.js +9 -0
  112. package/dist/fields/resolveFieldValues.js +1 -3
  113. package/dist/fonts/embeddedFonts.d.ts +33 -5
  114. package/dist/fonts/embeddedFonts.js +37 -8
  115. package/dist/i18n/messages/catalogs.gen.d.ts +17 -0
  116. package/dist/i18n/messages/catalogs.gen.js +17 -0
  117. package/dist/i18n/messages/messages.gen.d.ts +1 -0
  118. package/dist/index.d.ts +3 -3
  119. package/dist/index.js +3 -3
  120. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  121. package/dist/layout-bridge/convert/footnoteLayout.js +47 -40
  122. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  123. package/dist/layout-bridge/convert/headerFooterLayout.js +3 -0
  124. package/dist/layout-bridge/convert/templatePreviewFlow.js +3 -3
  125. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -2
  126. package/dist/layout-bridge/convert/toFlowBlocks.js +151 -20
  127. package/dist/layout-bridge/dom/noteStoryDom.d.ts +6 -0
  128. package/dist/layout-bridge/dom/noteStoryDom.js +17 -0
  129. package/dist/layout-bridge/engine/clickToPosition.js +2 -2
  130. package/dist/layout-engine/index.d.ts +2 -2
  131. package/dist/layout-engine/index.js +58 -30
  132. package/dist/layout-engine/justificationCompatibility.d.ts +6 -0
  133. package/dist/layout-engine/justificationCompatibility.js +8 -0
  134. package/dist/layout-engine/keep-together.js +11 -1
  135. package/dist/layout-engine/measure/cache.js +23 -5
  136. package/dist/layout-engine/measure/effectiveLineBreakPolicy.js +8 -2
  137. package/dist/layout-engine/measure/lineBreakProvider.d.ts +2 -2
  138. package/dist/layout-engine/measure/lineBreakProvider.js +56 -37
  139. package/dist/layout-engine/measure/measureBlocks.js +15 -4
  140. package/dist/layout-engine/measure/measureContainer.js +1 -1
  141. package/dist/layout-engine/measure/measureHelpers.d.ts +11 -2
  142. package/dist/layout-engine/measure/measureHelpers.js +15 -4
  143. package/dist/layout-engine/measure/measureParagraph.js +85 -25
  144. package/dist/layout-engine/measure/measureProvider.js +4 -3
  145. package/dist/layout-engine/measure/tableCellFlow.js +5 -2
  146. package/dist/layout-engine/measure/textBoxParagraphLayout.d.ts +9 -7
  147. package/dist/layout-engine/measure/textBoxParagraphLayout.js +22 -6
  148. package/dist/layout-engine/measure/textMeasurementPolicy.d.ts +3 -1
  149. package/dist/layout-engine/measure/textMeasurementPolicy.js +7 -1
  150. package/dist/layout-engine/paginator.js +5 -1
  151. package/dist/layout-engine/paragraphSpacing.d.ts +28 -4
  152. package/dist/layout-engine/paragraphSpacing.js +107 -13
  153. package/dist/layout-engine/renderedBreakReconciliation.js +1 -1
  154. package/dist/layout-engine/types.d.ts +48 -15
  155. package/dist/layout-painter/registry/modules/textBox.js +5 -1
  156. package/dist/layout-painter/renderImage.d.ts +12 -1
  157. package/dist/layout-painter/renderImage.js +21 -4
  158. package/dist/layout-painter/renderPage.d.ts +5 -2
  159. package/dist/layout-painter/renderPage.js +59 -14
  160. package/dist/layout-painter/renderParagraph.js +55 -15
  161. package/dist/layout-painter/renderTable.d.ts +5 -1
  162. package/dist/layout-painter/renderTable.js +7 -3
  163. package/dist/layout-painter/renderTextBox.d.ts +2 -1
  164. package/dist/layout-painter/renderTextBox.js +25 -14
  165. package/dist/layout-painter/renderWatermark.d.ts +1 -1
  166. package/dist/layout-painter/renderWatermark.js +2 -1
  167. package/dist/managers/DocumentLoaderManager.js +1 -1
  168. package/dist/markdown/fromMarkdown.js +1 -1
  169. package/dist/model.js +1 -1
  170. package/dist/prosemirror/attrs/index.d.ts +4 -2
  171. package/dist/prosemirror/attrs/index.js +175 -18
  172. package/dist/prosemirror/commands/comments.d.ts +79 -4
  173. package/dist/prosemirror/commands/comments.js +622 -13
  174. package/dist/prosemirror/commands/contentControls.js +1 -1
  175. package/dist/prosemirror/commands/hyperlink.js +15 -1
  176. package/dist/prosemirror/commands/image.js +7 -1
  177. package/dist/prosemirror/commands/index.d.ts +2 -2
  178. package/dist/prosemirror/commands/index.js +2 -2
  179. package/dist/prosemirror/commands/pageBreak.js +1 -1
  180. package/dist/prosemirror/commands/propertyChangeScope.d.ts +2 -2
  181. package/dist/prosemirror/commands/propertyChangeScope.js +3 -2
  182. package/dist/prosemirror/commands/tableCellMergeResolution.d.ts +9 -0
  183. package/dist/prosemirror/commands/tableCellMergeResolution.js +181 -0
  184. package/dist/prosemirror/commentIdAllocator.js +13 -3
  185. package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -1
  186. package/dist/prosemirror/conversion/fromProseDoc.js +404 -62
  187. package/dist/prosemirror/conversion/index.js +1 -1
  188. package/dist/prosemirror/conversion/sdtAttrs.d.ts +9 -0
  189. package/dist/prosemirror/conversion/sdtAttrs.js +49 -0
  190. package/dist/prosemirror/conversion/toProseDoc.d.ts +2 -1
  191. package/dist/prosemirror/conversion/toProseDoc.js +364 -103
  192. package/dist/prosemirror/extensions/ExtensionManager.js +1 -1
  193. package/dist/prosemirror/extensions/StarterKit.js +6 -1
  194. package/dist/prosemirror/extensions/core/ParagraphExtension.js +6 -2
  195. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +1 -0
  196. package/dist/prosemirror/extensions/features/ImagePasteExtension.js +10 -2
  197. package/dist/prosemirror/extensions/features/ListExtension.js +1 -1
  198. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +10 -1
  199. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +93 -2
  200. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +2 -1
  201. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +6 -2
  202. package/dist/prosemirror/extensions/features/pasteCleanup.js +15 -2
  203. package/dist/prosemirror/extensions/marks/CharacterSpacingExtension.js +1 -1
  204. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +30 -4
  205. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.d.ts +9 -1
  206. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +70 -11
  207. package/dist/prosemirror/extensions/nodes/FieldExtension.js +1 -1
  208. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -0
  209. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +22 -0
  210. package/dist/prosemirror/extensions/nodes/TableExtension.d.ts +1 -1
  211. package/dist/prosemirror/extensions/nodes/TableExtension.js +14 -6
  212. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +13 -0
  213. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +41 -0
  214. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +5 -2
  215. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +4 -1
  216. package/dist/prosemirror/index.d.ts +1 -1
  217. package/dist/prosemirror/index.js +1 -1
  218. package/dist/prosemirror/plugins/contentControlWidgets.d.ts +36 -10
  219. package/dist/prosemirror/plugins/contentControlWidgets.js +128 -94
  220. package/dist/prosemirror/plugins/revisionIds.d.ts +23 -0
  221. package/dist/prosemirror/plugins/revisionIds.js +57 -0
  222. package/dist/prosemirror/plugins/selectionTracker.js +1 -0
  223. package/dist/prosemirror/plugins/suggestionMode.js +5 -4
  224. package/dist/prosemirror/runFormattingMarkNames.d.ts +14 -0
  225. package/dist/prosemirror/runFormattingMarkNames.js +40 -0
  226. package/dist/prosemirror/schema/index.d.ts +2 -2
  227. package/dist/prosemirror/schema/marks.d.ts +30 -3
  228. package/dist/prosemirror/schema/marks.js +15 -0
  229. package/dist/prosemirror/schema/nodes.d.ts +111 -6
  230. package/dist/prosemirror/selectionState.js +1 -0
  231. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  232. package/dist/prosemirror/tableCellMergeRevision.d.ts +9 -0
  233. package/dist/prosemirror/tableCellMergeRevision.js +22 -0
  234. package/dist/prosemirror/textBoxAnchorAttrs.d.ts +9 -0
  235. package/dist/prosemirror/textBoxAnchorAttrs.js +33 -0
  236. package/dist/prosemirror/utils/extractTrackedChanges.d.ts +1 -1
  237. package/dist/prosemirror/utils/extractTrackedChanges.js +62 -14
  238. package/dist/prosemirror/validation.js +9 -1
  239. package/dist/render-dom/RemoteSelectionOverlay.js +2 -2
  240. package/dist/server.d.ts +7 -3
  241. package/dist/server.js +7 -3
  242. package/dist/utils/clipboard.d.ts +10 -1
  243. package/dist/utils/clipboard.js +33 -6
  244. package/dist/utils/colorResolver.d.ts +8 -1
  245. package/dist/utils/colorResolver.js +35 -5
  246. package/dist/utils/fontFamilyMerge.js +9 -11
  247. package/dist/utils/fontLoader.js +3 -2
  248. package/dist/utils/fontResolver.d.ts +2 -1
  249. package/dist/utils/fontResolver.js +31 -7
  250. package/dist/utils/fontWeights.d.ts +1 -1
  251. package/dist/utils/fontWeights.js +1 -1
  252. package/dist/utils/hexId.d.ts +11 -1
  253. package/dist/utils/hexId.js +11 -1
  254. package/dist/utils/paragraphFormattingMerge.js +1 -0
  255. package/dist/utils/sanitizeImageSrc.d.ts +21 -0
  256. package/dist/utils/sanitizeImageSrc.js +30 -0
  257. package/dist/utils/stripXmlDeclarations.d.ts +11 -0
  258. package/dist/utils/stripXmlDeclarations.js +35 -0
  259. package/dist/version-comparison.d.ts +22 -2
  260. package/dist/version-comparison.js +41 -12
  261. package/dist/watermark/index.d.ts +11 -1
  262. package/dist/watermark/index.js +22 -1
  263. package/package.json +3 -2
@@ -0,0 +1,361 @@
1
+ import { DocxArchiveError } from "./boundedArchive.js";
2
+ import { getDocxXmlSafetyIssue } from "../xmlSafety.js";
3
+ import { FolioDocxPackageInspectionError, inspectDocxPackage } from "./inspectDocxPackage.js";
4
+ import { TaggedError, panic } from "better-result";
5
+ import { createHash } from "node:crypto";
6
+ //#region src/docx/server/evaluateDocxXmlPatchProposal.ts
7
+ const FOLIO_DOCX_XML_PATCH_PROPOSAL_VERSION = 1;
8
+ const FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE = "folio-xml-patch-proposal-v1";
9
+ const FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS = Object.freeze({
10
+ maxReplacements: 16,
11
+ maxPartBytes: 8 * 1024 * 1024,
12
+ maxTotalBytes: 16 * 1024 * 1024
13
+ });
14
+ const FOLIO_DOCX_XML_PATCH_PROPOSAL_ISSUE_CODES = Object.freeze([
15
+ "invalid-proposal",
16
+ "unsupported-version",
17
+ "too-many-replacements",
18
+ "duplicate-part",
19
+ "invalid-part-path",
20
+ "invalid-base-sha256",
21
+ "part-not-allowed",
22
+ "part-not-found",
23
+ "part-not-xml",
24
+ "replacement-too-large",
25
+ "replacements-too-large",
26
+ "xml-doctype-forbidden",
27
+ "xml-not-well-formed",
28
+ "xml-encoding-mismatch",
29
+ "base-hash-mismatch",
30
+ "package-inspection-failed"
31
+ ]);
32
+ var InvalidFolioDocxXmlPatchProposalError = class extends TaggedError("InvalidFolioDocxXmlPatchProposalError")() {};
33
+ var InvalidFolioDocxXmlPatchProposalOptionsError = class extends TaggedError("InvalidFolioDocxXmlPatchProposalOptionsError")() {};
34
+ const SHA256_PATTERN = /^[\da-f]{64}$/u;
35
+ const XML_DECLARED_ENCODING_PATTERN = /^\uFEFF?<\?xml[^>]*\bencoding\s*=\s*["']([^"']+)["'][^>]*\?>/iu;
36
+ const MAX_PACKAGE_PATH_CODE_UNITS = 1024;
37
+ const prepareXmlReplacement = ({ replacement, encodedByPath, inspectedByPath }) => {
38
+ const replacementBytes = encodedByPath.get(replacement.path);
39
+ const current = inspectedByPath.get(replacement.path);
40
+ if (replacementBytes === void 0 || current === void 0) return panic("Accepted XML replacement is missing prepared package data");
41
+ return {
42
+ path: replacement.path,
43
+ baseSha256: replacement.baseSha256,
44
+ currentSha256: current.sha256,
45
+ replacementSha256: createHash("sha256").update(replacementBytes).digest("hex"),
46
+ replacementByteLength: replacementBytes.byteLength,
47
+ encoding: "utf-8"
48
+ };
49
+ };
50
+ const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
51
+ const invalidProposal = (path, reason) => {
52
+ throw new InvalidFolioDocxXmlPatchProposalError({
53
+ message: `Invalid XML patch proposal at ${path}: ${reason}.`,
54
+ path,
55
+ reason
56
+ });
57
+ };
58
+ const assertAllowedKeys = (value, path, allowedKeys) => {
59
+ const allowed = new Set(allowedKeys);
60
+ const unexpected = Object.keys(value).find((key) => !allowed.has(key));
61
+ if (unexpected !== void 0) invalidProposal(`${path}.${unexpected}`, "unexpected property");
62
+ };
63
+ const readString = (value, key, path) => {
64
+ const result = value[key];
65
+ if (typeof result !== "string") return invalidProposal(`${path}.${key}`, "expected a string");
66
+ return result;
67
+ };
68
+ const parseXmlReplacement = (value, index) => {
69
+ const path = `$.replacements[${index}]`;
70
+ if (!isPlainObject(value)) return invalidProposal(path, "expected an object");
71
+ assertAllowedKeys(value, path, [
72
+ "path",
73
+ "baseSha256",
74
+ "replacementXml"
75
+ ]);
76
+ return {
77
+ path: readString(value, "path", path),
78
+ baseSha256: readString(value, "baseSha256", path),
79
+ replacementXml: readString(value, "replacementXml", path)
80
+ };
81
+ };
82
+ const parseFolioDocxXmlPatchProposal = (value) => {
83
+ if (!isPlainObject(value)) return invalidProposal("$", "expected an object");
84
+ assertAllowedKeys(value, "$", ["version", "replacements"]);
85
+ const version = value["version"];
86
+ if (typeof version !== "number" || !Number.isSafeInteger(version)) return invalidProposal("$.version", "expected an integer contract version");
87
+ if (version !== 1) return invalidProposal("$.version", "unsupported contract version");
88
+ const replacements = value["replacements"];
89
+ if (!Array.isArray(replacements) || replacements.length === 0) return invalidProposal("$.replacements", "expected a non-empty array");
90
+ if (replacements.length > FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS.maxReplacements) return invalidProposal("$.replacements", `expected at most ${FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS.maxReplacements} replacements`);
91
+ const firstReplacement = replacements.at(0);
92
+ if (firstReplacement === void 0) return invalidProposal("$.replacements", "expected a non-empty array");
93
+ return {
94
+ version: 1,
95
+ replacements: [parseXmlReplacement(firstReplacement, 0), ...replacements.slice(1).map((replacement, index) => parseXmlReplacement(replacement, index + 1))]
96
+ };
97
+ };
98
+ const resolveLimit = ({ value, fallback, path }) => {
99
+ const limit = value ?? fallback;
100
+ if (!Number.isSafeInteger(limit) || limit < 0) throw new InvalidFolioDocxXmlPatchProposalOptionsError({
101
+ message: `${path} must be a non-negative safe integer.`,
102
+ path
103
+ });
104
+ return limit;
105
+ };
106
+ const resolveLimits = (limits) => {
107
+ const maxReplacements = resolveLimit({
108
+ value: limits?.maxReplacements,
109
+ fallback: FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS.maxReplacements,
110
+ path: "limits.maxReplacements"
111
+ });
112
+ if (maxReplacements > FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS.maxReplacements) throw new InvalidFolioDocxXmlPatchProposalOptionsError({
113
+ message: `limits.maxReplacements must not exceed ${FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS.maxReplacements}.`,
114
+ path: "limits.maxReplacements"
115
+ });
116
+ return {
117
+ maxReplacements,
118
+ maxPartBytes: resolveLimit({
119
+ value: limits?.maxPartBytes,
120
+ fallback: FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS.maxPartBytes,
121
+ path: "limits.maxPartBytes"
122
+ }),
123
+ maxTotalBytes: resolveLimit({
124
+ value: limits?.maxTotalBytes,
125
+ fallback: FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS.maxTotalBytes,
126
+ path: "limits.maxTotalBytes"
127
+ })
128
+ };
129
+ };
130
+ const isNormalizedPackagePath = (path) => path.length > 0 && path.length <= MAX_PACKAGE_PATH_CODE_UNITS && !path.startsWith("/") && !path.endsWith("/") && !path.includes("\\") && path.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
131
+ const resolveAllowedParts = (allowedParts) => {
132
+ const resolved = /* @__PURE__ */ new Set();
133
+ for (const [index, path] of allowedParts.entries()) {
134
+ if (typeof path !== "string" || !isNormalizedPackagePath(path)) throw new InvalidFolioDocxXmlPatchProposalOptionsError({
135
+ message: `allowedParts[${index}] must be a normalized package path.`,
136
+ path: `allowedParts[${index}]`
137
+ });
138
+ if (resolved.has(path)) throw new InvalidFolioDocxXmlPatchProposalOptionsError({
139
+ message: `allowedParts[${index}] must be unique.`,
140
+ path: `allowedParts[${index}]`
141
+ });
142
+ resolved.add(path);
143
+ }
144
+ return resolved;
145
+ };
146
+ const rejectedEvaluation = ({ issues, limits }) => ({
147
+ version: 1,
148
+ profile: FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE,
149
+ status: "rejected",
150
+ producesOutput: false,
151
+ issues,
152
+ replacements: [],
153
+ limits
154
+ });
155
+ const acceptedEvaluation = ({ replacements, limits }) => ({
156
+ version: 1,
157
+ profile: FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE,
158
+ status: "accepted",
159
+ producesOutput: false,
160
+ issues: [],
161
+ replacements,
162
+ limits
163
+ });
164
+ const inspectionIssue = (error) => {
165
+ if (error.code === "part-not-found" || error.code === "part-not-xml") return {
166
+ code: error.code,
167
+ message: error.message,
168
+ ...error.part !== void 0 && { part: error.part }
169
+ };
170
+ return {
171
+ code: "package-inspection-failed",
172
+ message: "The package could not be inspected within the configured limits.",
173
+ ...error.part !== void 0 && { part: error.part }
174
+ };
175
+ };
176
+ const evaluateDocxXmlPatchProposal = async ({ bytes, proposal: rawProposal, allowedParts, archive, limits: rawLimits }) => {
177
+ const limits = resolveLimits(rawLimits);
178
+ const allowed = resolveAllowedParts(allowedParts);
179
+ const rawReplacements = isPlainObject(rawProposal) ? rawProposal["replacements"] : void 0;
180
+ if (isPlainObject(rawProposal) && rawProposal["version"] === 1 && Array.isArray(rawReplacements) && rawReplacements.length > limits.maxReplacements) return rejectedEvaluation({
181
+ issues: [{
182
+ code: "too-many-replacements",
183
+ message: `Proposal contains ${rawReplacements.length} replacements (max ${limits.maxReplacements}).`,
184
+ proposalPath: "$.replacements"
185
+ }],
186
+ limits
187
+ });
188
+ let proposal;
189
+ try {
190
+ proposal = parseFolioDocxXmlPatchProposal(rawProposal);
191
+ } catch (error) {
192
+ if (!(error instanceof InvalidFolioDocxXmlPatchProposalError)) throw error;
193
+ return rejectedEvaluation({
194
+ issues: [{
195
+ code: error.path === "$.version" && isPlainObject(rawProposal) && typeof rawProposal["version"] === "number" && Number.isSafeInteger(rawProposal["version"]) ? "unsupported-version" : "invalid-proposal",
196
+ message: error.message,
197
+ proposalPath: error.path
198
+ }],
199
+ limits
200
+ });
201
+ }
202
+ const issues = [];
203
+ const seen = /* @__PURE__ */ new Set();
204
+ const encodedByPath = /* @__PURE__ */ new Map();
205
+ let encodedTotalBytes = 0;
206
+ let totalCodeUnits = 0;
207
+ let totalLimitReported = false;
208
+ for (const [index, replacement] of proposal.replacements.entries()) {
209
+ const proposalPath = `$.replacements[${index}]`;
210
+ if (seen.has(replacement.path)) issues.push({
211
+ code: "duplicate-part",
212
+ message: "Each package part may appear only once in a proposal.",
213
+ proposalPath: `${proposalPath}.path`,
214
+ part: replacement.path
215
+ });
216
+ seen.add(replacement.path);
217
+ if (!isNormalizedPackagePath(replacement.path)) issues.push({
218
+ code: "invalid-part-path",
219
+ message: "Replacement paths must be normalized package paths.",
220
+ proposalPath: `${proposalPath}.path`,
221
+ part: replacement.path
222
+ });
223
+ else if (!allowed.has(replacement.path)) issues.push({
224
+ code: "part-not-allowed",
225
+ message: "The server-side policy does not allow replacing this package part.",
226
+ proposalPath: `${proposalPath}.path`,
227
+ part: replacement.path
228
+ });
229
+ if (replacement.baseSha256.length !== 64 || !SHA256_PATTERN.test(replacement.baseSha256)) issues.push({
230
+ code: "invalid-base-sha256",
231
+ message: "baseSha256 must be a lowercase hexadecimal SHA-256 digest.",
232
+ proposalPath: `${proposalPath}.baseSha256`,
233
+ part: replacement.path
234
+ });
235
+ const replacementCodeUnits = replacement.replacementXml.length;
236
+ const exceedsPartLowerBound = replacementCodeUnits > limits.maxPartBytes;
237
+ if (exceedsPartLowerBound) issues.push({
238
+ code: "replacement-too-large",
239
+ message: `Replacement XML exceeds the ${limits.maxPartBytes}-byte per-part limit.`,
240
+ proposalPath: `${proposalPath}.replacementXml`,
241
+ part: replacement.path
242
+ });
243
+ totalCodeUnits += replacementCodeUnits;
244
+ if (totalCodeUnits > limits.maxTotalBytes && !totalLimitReported) {
245
+ totalLimitReported = true;
246
+ issues.push({
247
+ code: "replacements-too-large",
248
+ message: `Replacement XML exceeds the ${limits.maxTotalBytes}-byte cumulative limit.`,
249
+ proposalPath: "$.replacements"
250
+ });
251
+ }
252
+ if (exceedsPartLowerBound || totalLimitReported) continue;
253
+ const replacementBytes = new TextEncoder().encode(replacement.replacementXml);
254
+ const exceedsEncodedPart = replacementBytes.byteLength > limits.maxPartBytes;
255
+ const nextEncodedTotalBytes = encodedTotalBytes + replacementBytes.byteLength;
256
+ if (exceedsEncodedPart) issues.push({
257
+ code: "replacement-too-large",
258
+ message: `Replacement XML exceeds the ${limits.maxPartBytes}-byte per-part limit.`,
259
+ proposalPath: `${proposalPath}.replacementXml`,
260
+ part: replacement.path
261
+ });
262
+ if (nextEncodedTotalBytes > limits.maxTotalBytes) {
263
+ totalLimitReported = true;
264
+ issues.push({
265
+ code: "replacements-too-large",
266
+ message: `Replacement XML exceeds the ${limits.maxTotalBytes}-byte cumulative limit.`,
267
+ proposalPath: "$.replacements"
268
+ });
269
+ }
270
+ encodedTotalBytes = nextEncodedTotalBytes;
271
+ if (exceedsEncodedPart || totalLimitReported) continue;
272
+ encodedByPath.set(replacement.path, replacementBytes);
273
+ const safetyIssue = getDocxXmlSafetyIssue(replacement.replacementXml);
274
+ if (safetyIssue === "doctype-forbidden") issues.push({
275
+ code: "xml-doctype-forbidden",
276
+ message: "Replacement XML must not declare a document type.",
277
+ proposalPath: `${proposalPath}.replacementXml`,
278
+ part: replacement.path
279
+ });
280
+ else if (safetyIssue === "not-well-formed") issues.push({
281
+ code: "xml-not-well-formed",
282
+ message: "Replacement XML must be well formed.",
283
+ proposalPath: `${proposalPath}.replacementXml`,
284
+ part: replacement.path
285
+ });
286
+ else {
287
+ const declaredEncoding = replacement.replacementXml.match(XML_DECLARED_ENCODING_PATTERN)?.at(1);
288
+ if (declaredEncoding !== void 0 && declaredEncoding.toLowerCase() !== "utf-8" && declaredEncoding.toLowerCase() !== "utf8") issues.push({
289
+ code: "xml-encoding-mismatch",
290
+ message: "Replacement XML declarations must use UTF-8 encoding.",
291
+ proposalPath: `${proposalPath}.replacementXml`,
292
+ part: replacement.path
293
+ });
294
+ }
295
+ }
296
+ if (issues.length > 0) {
297
+ const firstIssue = issues.at(0);
298
+ if (firstIssue === void 0) return panic("Rejected XML patch proposal is missing an issue");
299
+ return rejectedEvaluation({
300
+ issues: [firstIssue, ...issues.slice(1)],
301
+ limits
302
+ });
303
+ }
304
+ let inspected;
305
+ try {
306
+ inspected = await inspectDocxPackage(bytes, {
307
+ ...archive === void 0 ? {} : { archive },
308
+ xmlParts: proposal.replacements.map(({ path }) => path),
309
+ limits: {
310
+ maxXmlParts: limits.maxReplacements,
311
+ maxXmlPartBytes: limits.maxPartBytes,
312
+ maxXmlTotalBytes: limits.maxTotalBytes
313
+ }
314
+ });
315
+ } catch (error) {
316
+ if (error instanceof FolioDocxPackageInspectionError) return rejectedEvaluation({
317
+ issues: [inspectionIssue(error)],
318
+ limits
319
+ });
320
+ if (error instanceof DocxArchiveError) return rejectedEvaluation({
321
+ issues: [{
322
+ code: "package-inspection-failed",
323
+ message: "The package could not be inspected within the configured limits."
324
+ }],
325
+ limits
326
+ });
327
+ throw error;
328
+ }
329
+ const inspectedByPath = new Map(inspected.xmlParts.map((part) => [part.path, part]));
330
+ for (const [index, replacement] of proposal.replacements.entries()) {
331
+ const current = inspectedByPath.get(replacement.path);
332
+ if (current === void 0 || current.sha256 === replacement.baseSha256) continue;
333
+ issues.push({
334
+ code: "base-hash-mismatch",
335
+ message: "The package part changed after the proposal base was inspected.",
336
+ proposalPath: `$.replacements[${index}].baseSha256`,
337
+ part: replacement.path
338
+ });
339
+ }
340
+ if (issues.length > 0) {
341
+ const firstIssue = issues.at(0);
342
+ if (firstIssue === void 0) return panic("Rejected XML patch proposal is missing an issue");
343
+ return rejectedEvaluation({
344
+ issues: [firstIssue, ...issues.slice(1)],
345
+ limits
346
+ });
347
+ }
348
+ const preparedReplacements = proposal.replacements.map((replacement) => prepareXmlReplacement({
349
+ replacement,
350
+ encodedByPath,
351
+ inspectedByPath
352
+ }));
353
+ const firstPreparedReplacement = preparedReplacements.at(0);
354
+ if (firstPreparedReplacement === void 0) return panic("Accepted XML patch proposal is missing a prepared replacement");
355
+ return acceptedEvaluation({
356
+ replacements: [firstPreparedReplacement, ...preparedReplacements.slice(1)],
357
+ limits
358
+ });
359
+ };
360
+ //#endregion
361
+ export { FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS, FOLIO_DOCX_XML_PATCH_PROPOSAL_ISSUE_CODES, FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE, FOLIO_DOCX_XML_PATCH_PROPOSAL_VERSION, InvalidFolioDocxXmlPatchProposalError, InvalidFolioDocxXmlPatchProposalOptionsError, evaluateDocxXmlPatchProposal, parseFolioDocxXmlPatchProposal };
@@ -1,7 +1,8 @@
1
- import { findAllDeep, findChild, findDeep, getAttributeAnyPrefix, getLocalName, getTextContent, parseXml } from "../xmlParser.js";
1
+ import { findAllDeep, findChild, findDeep, getAttribute, getAttributeAnyPrefix, getLocalName, getTextContent, parseXml } from "../xmlParser.js";
2
+ import { RELATIONSHIP_TYPES, parseRelationships } from "../relsParser.js";
2
3
  import { loadDocxArchive } from "./boundedArchive.js";
3
4
  //#region src/docx/server/extractDocxText.ts
4
- const HEADER_FOOTER_PATH = /^word\/(?:header|footer)\d+\.xml$/u;
5
+ const DOCUMENT_RELS_PATH = "word/_rels/document.xml.rels";
5
6
  const childElements = (element) => element.elements?.filter((child) => child.type === "element") ?? [];
6
7
  const collectText = (element) => {
7
8
  let text = "";
@@ -86,12 +87,10 @@ const extractContainer = ({ container, source, startIndex }) => {
86
87
  charCount
87
88
  };
88
89
  };
89
- const extractParts = async ({ archive, source, rootName, startIndex }) => {
90
+ const extractParts = async ({ archive, source, rootName, startIndex, paths }) => {
90
91
  const paragraphs = [];
91
92
  let charCount = 0;
92
93
  let nextIndex = startIndex;
93
- const prefix = `word/${source}`;
94
- const paths = archive.entries.filter((path) => HEADER_FOOTER_PATH.test(path) && path.startsWith(prefix)).toSorted();
95
94
  for (const path of paths) {
96
95
  const xml = await archive.readEntryString(path);
97
96
  if (xml === null) continue;
@@ -111,6 +110,50 @@ const extractParts = async ({ archive, source, rootName, startIndex }) => {
111
110
  charCount
112
111
  };
113
112
  };
113
+ /** A `word/_rels/document.xml.rels` `Target` is relative to `word/`; resolve it to a full archive-entry path. */
114
+ const resolveWordPartPath = (target) => target.startsWith("/") ? target.slice(1) : `word/${target}`;
115
+ /**
116
+ * Resolve the header/footer parts actually wired into the document via
117
+ * `word/_rels/document.xml.rels` + each section's `w:headerReference` /
118
+ * `w:footerReference`, instead of extracting every `word/header*.xml` /
119
+ * `word/footer*.xml` entry by filename. A DOCX can carry an orphaned
120
+ * header/footer part (stale, or planted by an attacker) that no section
121
+ * references — reading it unconditionally would surface prompt-injection or
122
+ * stale content that Word itself never renders.
123
+ */
124
+ const resolveReferencedHeaderFooterParts = async (archive, documentRoot) => {
125
+ const relsXml = await archive.readEntryString(DOCUMENT_RELS_PATH);
126
+ if (relsXml === null) return {
127
+ headers: [],
128
+ footers: []
129
+ };
130
+ const relationships = parseRelationships(relsXml);
131
+ const headerRIds = /* @__PURE__ */ new Set();
132
+ const footerRIds = /* @__PURE__ */ new Set();
133
+ for (const sectPr of findAllDeep(documentRoot, "w", "sectPr")) {
134
+ for (const ref of findAllDeep(sectPr, "w", "headerReference")) {
135
+ const rId = getAttribute(ref, "r", "id");
136
+ if (rId !== null) headerRIds.add(rId);
137
+ }
138
+ for (const ref of findAllDeep(sectPr, "w", "footerReference")) {
139
+ const rId = getAttribute(ref, "r", "id");
140
+ if (rId !== null) footerRIds.add(rId);
141
+ }
142
+ }
143
+ const resolvePaths = (rIds, relationshipType) => {
144
+ const paths = /* @__PURE__ */ new Set();
145
+ for (const rId of rIds) {
146
+ const relationship = relationships.get(rId);
147
+ if (!relationship || relationship.type !== relationshipType || relationship.targetMode === "External") continue;
148
+ paths.add(resolveWordPartPath(relationship.target));
149
+ }
150
+ return [...paths].toSorted();
151
+ };
152
+ return {
153
+ headers: resolvePaths(headerRIds, RELATIONSHIP_TYPES.header),
154
+ footers: resolvePaths(footerRIds, RELATIONSHIP_TYPES.footer)
155
+ };
156
+ };
114
157
  const createEmptyResult = () => ({
115
158
  paragraphs: [],
116
159
  charCount: 0,
@@ -121,13 +164,16 @@ const extractDocxText = async (bytes) => {
121
164
  const archive = await loadDocxArchive(bytes);
122
165
  const documentXml = await archive.readEntryString("word/document.xml");
123
166
  if (documentXml === null) return createEmptyResult();
124
- const body = findDeep(parseXml(documentXml), "w", "body");
167
+ const root = parseXml(documentXml);
168
+ const body = findDeep(root, "w", "body");
125
169
  if (!body) return createEmptyResult();
170
+ const referencedParts = await resolveReferencedHeaderFooterParts(archive, root);
126
171
  const headers = await extractParts({
127
172
  archive,
128
173
  source: "header",
129
174
  rootName: "hdr",
130
- startIndex: 0
175
+ startIndex: 0,
176
+ paths: referencedParts.headers
131
177
  });
132
178
  const bodyResult = extractContainer({
133
179
  container: body,
@@ -138,7 +184,8 @@ const extractDocxText = async (bytes) => {
138
184
  archive,
139
185
  source: "footer",
140
186
  rootName: "ftr",
141
- startIndex: headers.paragraphs.length + bodyResult.paragraphs.length
187
+ startIndex: headers.paragraphs.length + bodyResult.paragraphs.length,
188
+ paths: referencedParts.footers
142
189
  });
143
190
  return {
144
191
  paragraphs: [
@@ -0,0 +1,70 @@
1
+ import { DocxArchiveOptions } from "./boundedArchive.js";
2
+
3
+ //#region src/docx/server/inspectDocxPackage.d.ts
4
+ declare const FOLIO_DOCX_PACKAGE_INSPECTION_VERSION: 1;
5
+ declare const FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS: Readonly<{
6
+ readonly maxXmlParts: 16;
7
+ readonly maxXmlPartBytes: number;
8
+ readonly maxXmlTotalBytes: number;
9
+ }>;
10
+ declare const FOLIO_DOCX_PACKAGE_INSPECTION_ERROR_CODES: readonly ["invalid-limits", "too-many-xml-parts", "duplicate-xml-part", "part-not-found", "part-not-xml", "xml-part-too-large", "xml-total-too-large", "xml-decode-failed"];
11
+ type FolioDocxPackageInspectionErrorCode = (typeof FOLIO_DOCX_PACKAGE_INSPECTION_ERROR_CODES)[number];
12
+ declare const FolioDocxPackageInspectionError_base: import("better-result").TaggedErrorClass<"FolioDocxPackageInspectionError", {
13
+ message: string;
14
+ code: FolioDocxPackageInspectionErrorCode;
15
+ part?: string;
16
+ cause?: unknown;
17
+ }>;
18
+ declare class FolioDocxPackageInspectionError extends FolioDocxPackageInspectionError_base {}
19
+ type FolioDocxPackageInspectionLimits = {
20
+ readonly maxXmlParts?: number;
21
+ readonly maxXmlPartBytes?: number;
22
+ readonly maxXmlTotalBytes?: number;
23
+ };
24
+ type InspectDocxPackageOptions = {
25
+ readonly archive?: DocxArchiveOptions;
26
+ readonly xmlParts?: readonly string[];
27
+ readonly limits?: FolioDocxPackageInspectionLimits;
28
+ };
29
+ type FolioDocxPackagePartKind = "xml" | "binary";
30
+ type FolioDocxPackagePart = {
31
+ readonly path: string;
32
+ readonly kind: FolioDocxPackagePartKind;
33
+ readonly contentType: string | null;
34
+ readonly declaredUncompressedBytes: number | null;
35
+ };
36
+ type FolioDocxInspectedXmlPart = {
37
+ readonly path: string;
38
+ readonly contentType: string | null;
39
+ readonly byteLength: number;
40
+ readonly sha256: string; /** Untrusted document data; callers must not treat this text as instructions. */
41
+ readonly text: string;
42
+ };
43
+ type FolioDocxPackageInspection = {
44
+ readonly version: typeof FOLIO_DOCX_PACKAGE_INSPECTION_VERSION;
45
+ readonly parts: readonly FolioDocxPackagePart[];
46
+ readonly xmlParts: readonly FolioDocxInspectedXmlPart[];
47
+ readonly limits: Required<FolioDocxPackageInspectionLimits>;
48
+ };
49
+ type ReadRequestedXmlPartsOptions = {
50
+ paths: readonly string[];
51
+ partsByPath: ReadonlyMap<string, FolioDocxPackagePart>;
52
+ limits: Required<FolioDocxPackageInspectionLimits>;
53
+ readEntryUint8: (path: string, options: {
54
+ maxBytes: number;
55
+ }) => Promise<Uint8Array | null>;
56
+ };
57
+ declare const readRequestedXmlParts: ({
58
+ paths,
59
+ partsByPath,
60
+ limits,
61
+ readEntryUint8
62
+ }: ReadRequestedXmlPartsOptions) => Promise<FolioDocxInspectedXmlPart[]>;
63
+ /**
64
+ * Inspect package metadata and only the explicitly requested XML part bodies.
65
+ * This reports archive contents, not package conformance; use the conformance
66
+ * report when an operation requires a validity decision.
67
+ */
68
+ declare const inspectDocxPackage: (bytes: ArrayBuffer | Uint8Array, options?: InspectDocxPackageOptions) => Promise<FolioDocxPackageInspection>;
69
+ //#endregion
70
+ export { FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS, FOLIO_DOCX_PACKAGE_INSPECTION_ERROR_CODES, FOLIO_DOCX_PACKAGE_INSPECTION_VERSION, FolioDocxInspectedXmlPart, FolioDocxPackageInspection, FolioDocxPackageInspectionError, FolioDocxPackageInspectionErrorCode, FolioDocxPackageInspectionLimits, FolioDocxPackagePart, FolioDocxPackagePartKind, InspectDocxPackageOptions, inspectDocxPackage, readRequestedXmlParts };