@stll/folio-core 0.10.0 → 0.12.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 (217) hide show
  1. package/dist/ai-edits/apply.js +602 -50
  2. package/dist/ai-edits/block-identity.d.ts +4 -0
  3. package/dist/ai-edits/block-identity.js +9 -0
  4. package/dist/ai-edits/blockRange.d.ts +28 -1
  5. package/dist/ai-edits/blockRange.js +67 -1
  6. package/dist/ai-edits/headless.d.ts +4 -4
  7. package/dist/ai-edits/headless.js +9 -9
  8. package/dist/ai-edits/index.d.ts +2 -2
  9. package/dist/ai-edits/index.js +2 -2
  10. package/dist/ai-edits/read.d.ts +7 -7
  11. package/dist/ai-edits/read.js +110 -3
  12. package/dist/ai-edits/table-cell-mutations.d.ts +48 -0
  13. package/dist/ai-edits/table-cell-mutations.js +237 -0
  14. package/dist/ai-edits/table-mutation-plan.d.ts +38 -0
  15. package/dist/ai-edits/table-mutation-plan.js +90 -0
  16. package/dist/ai-edits/table-row-column-mutations.d.ts +100 -0
  17. package/dist/ai-edits/table-row-column-mutations.js +300 -0
  18. package/dist/ai-edits/table-targets.d.ts +32 -0
  19. package/dist/ai-edits/table-targets.js +70 -0
  20. package/dist/ai-edits/types.d.ts +34 -0
  21. package/dist/compat/eigenpal.js +1 -1
  22. package/dist/content-controls/mutateContentControls.js +1 -1
  23. package/dist/controller/activeEditorStory.d.ts +36 -0
  24. package/dist/controller/activeEditorStory.js +30 -0
  25. package/dist/controller/collaborationModules.d.ts +7 -0
  26. package/dist/controller/collaborationModules.js +15 -0
  27. package/dist/controller/contentControlWidgetController.d.ts +55 -0
  28. package/dist/controller/contentControlWidgetController.js +141 -0
  29. package/dist/controller/folioEditor.d.ts +1 -1
  30. package/dist/controller/fontReadiness.js +2 -0
  31. package/dist/controller/headerFooterEditorManager.d.ts +41 -0
  32. package/dist/controller/headerFooterEditorManager.js +182 -0
  33. package/dist/controller/hiddenEditorApi.js +2 -2
  34. package/dist/controller/hiddenEditorManager.d.ts +13 -2
  35. package/dist/controller/hiddenEditorManager.js +32 -11
  36. package/dist/controller/layoutPipeline.d.ts +2 -0
  37. package/dist/controller/layoutPipeline.js +16 -1
  38. package/dist/controller/noteEditorManager.d.ts +36 -0
  39. package/dist/controller/noteEditorManager.js +210 -0
  40. package/dist/document-operations.d.ts +35 -3
  41. package/dist/document-operations.js +161 -3
  42. package/dist/docx/blockContentParser.js +2 -2
  43. package/dist/docx/commentParser.js +6 -1
  44. package/dist/docx/conformance.js +8 -1
  45. package/dist/docx/documentGrid.d.ts +8 -0
  46. package/dist/docx/documentGrid.js +8 -0
  47. package/dist/docx/drawingUtils.js +1 -1
  48. package/dist/docx/ensureParaIds.js +1 -1
  49. package/dist/docx/fieldParser.js +1 -1
  50. package/dist/docx/notePropertiesParser.js +1 -1
  51. package/dist/docx/numberingParser.js +1 -1
  52. package/dist/docx/paragraphParser.js +63 -58
  53. package/dist/docx/paragraphTextBoxEnrichment.d.ts +2 -1
  54. package/dist/docx/paragraphTextBoxEnrichment.js +67 -13
  55. package/dist/docx/paragraphTraversal.js +1 -1
  56. package/dist/docx/parser.js +3 -3
  57. package/dist/docx/rezip.js +1 -1
  58. package/dist/docx/runParser.js +4 -2
  59. package/dist/docx/sdtProperties.js +1 -1
  60. package/dist/docx/sectionParser.js +1 -1
  61. package/dist/docx/selectiveSave.js +1 -1
  62. package/dist/docx/serializer/commentSerializer.js +1 -2
  63. package/dist/docx/serializer/documentSerializer.js +2 -2
  64. package/dist/docx/serializer/headerFooterSerializer.js +2 -2
  65. package/dist/docx/serializer/noteSerializer.js +2 -2
  66. package/dist/docx/serializer/paragraphSerializer.js +7 -1
  67. package/dist/docx/serializer/runSerializer.js +4 -2
  68. package/dist/docx/serializer/stylesSerializer.js +1 -1
  69. package/dist/docx/serializer/tableSerializer.d.ts +5 -4
  70. package/dist/docx/serializer/tableSerializer.js +17 -13
  71. package/dist/docx/server/applyDocxXmlPatchProposal.d.ts +87 -0
  72. package/dist/docx/server/applyDocxXmlPatchProposal.js +204 -0
  73. package/dist/docx/server/boundedArchive.d.ts +14 -3
  74. package/dist/docx/server/boundedArchive.js +56 -11
  75. package/dist/docx/server/evaluateDocxXmlPatchProposal.d.ts +85 -0
  76. package/dist/docx/server/evaluateDocxXmlPatchProposal.js +361 -0
  77. package/dist/docx/server/inspectDocxPackage.d.ts +70 -0
  78. package/dist/docx/server/inspectDocxPackage.js +217 -0
  79. package/dist/docx/server/validateDocxConformance.d.ts +41 -0
  80. package/dist/docx/server/validateDocxConformance.js +336 -0
  81. package/dist/docx/settingsParser.js +11 -1
  82. package/dist/docx/shapeParser.js +1 -1
  83. package/dist/docx/styleParser.js +4 -2
  84. package/dist/docx/tableParser.js +18 -7
  85. package/dist/docx/textBoxParser.d.ts +3 -3
  86. package/dist/docx/textBoxParser.js +16 -12
  87. package/dist/docx/xmlSafety.d.ts +5 -0
  88. package/dist/docx/xmlSafety.js +9 -0
  89. package/dist/fields/resolveFieldValues.js +1 -3
  90. package/dist/i18n/messages/catalogs.gen.d.ts +17 -0
  91. package/dist/i18n/messages/catalogs.gen.js +17 -0
  92. package/dist/i18n/messages/messages.gen.d.ts +1 -0
  93. package/dist/index.js +1 -1
  94. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  95. package/dist/layout-bridge/convert/footnoteLayout.js +47 -40
  96. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  97. package/dist/layout-bridge/convert/headerFooterLayout.js +3 -0
  98. package/dist/layout-bridge/convert/templatePreviewFlow.js +3 -3
  99. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -2
  100. package/dist/layout-bridge/convert/toFlowBlocks.js +136 -20
  101. package/dist/layout-bridge/dom/noteStoryDom.d.ts +6 -0
  102. package/dist/layout-bridge/dom/noteStoryDom.js +17 -0
  103. package/dist/layout-bridge/engine/clickToPosition.js +2 -2
  104. package/dist/layout-engine/index.d.ts +2 -2
  105. package/dist/layout-engine/index.js +43 -28
  106. package/dist/layout-engine/justificationCompatibility.d.ts +6 -0
  107. package/dist/layout-engine/justificationCompatibility.js +8 -0
  108. package/dist/layout-engine/keep-together.js +11 -1
  109. package/dist/layout-engine/measure/cache.js +21 -5
  110. package/dist/layout-engine/measure/lineBreakProvider.js +53 -34
  111. package/dist/layout-engine/measure/measureBlocks.js +6 -3
  112. package/dist/layout-engine/measure/measureContainer.js +1 -1
  113. package/dist/layout-engine/measure/measureHelpers.d.ts +1 -1
  114. package/dist/layout-engine/measure/measureHelpers.js +3 -3
  115. package/dist/layout-engine/measure/measureParagraph.js +69 -25
  116. package/dist/layout-engine/measure/measureProvider.js +4 -3
  117. package/dist/layout-engine/measure/tableCellFlow.js +5 -2
  118. package/dist/layout-engine/measure/textBoxParagraphLayout.d.ts +9 -7
  119. package/dist/layout-engine/measure/textBoxParagraphLayout.js +22 -6
  120. package/dist/layout-engine/measure/textMeasurementPolicy.d.ts +3 -1
  121. package/dist/layout-engine/measure/textMeasurementPolicy.js +7 -1
  122. package/dist/layout-engine/paginator.js +5 -1
  123. package/dist/layout-engine/paragraphSpacing.d.ts +28 -4
  124. package/dist/layout-engine/paragraphSpacing.js +107 -13
  125. package/dist/layout-engine/renderedBreakReconciliation.js +1 -1
  126. package/dist/layout-engine/types.d.ts +26 -13
  127. package/dist/layout-painter/registry/modules/textBox.js +5 -1
  128. package/dist/layout-painter/renderPage.d.ts +5 -2
  129. package/dist/layout-painter/renderPage.js +56 -12
  130. package/dist/layout-painter/renderParagraph.js +22 -6
  131. package/dist/layout-painter/renderTable.d.ts +5 -1
  132. package/dist/layout-painter/renderTable.js +5 -2
  133. package/dist/layout-painter/renderTextBox.d.ts +2 -1
  134. package/dist/layout-painter/renderTextBox.js +25 -14
  135. package/dist/managers/DocumentLoaderManager.js +1 -1
  136. package/dist/managers/FindReplaceManager.d.ts +8 -0
  137. package/dist/managers/FindReplaceManager.js +18 -0
  138. package/dist/managers/TableSelectionManager.d.ts +6 -0
  139. package/dist/managers/TableSelectionManager.js +14 -0
  140. package/dist/model.js +1 -1
  141. package/dist/prosemirror/attrs/index.d.ts +4 -2
  142. package/dist/prosemirror/attrs/index.js +154 -18
  143. package/dist/prosemirror/commands/comments.d.ts +3 -3
  144. package/dist/prosemirror/commands/comments.js +248 -12
  145. package/dist/prosemirror/commands/contentControls.js +1 -1
  146. package/dist/prosemirror/commands/index.js +1 -1
  147. package/dist/prosemirror/commands/pageBreak.js +1 -1
  148. package/dist/prosemirror/commands/propertyChangeScope.d.ts +2 -2
  149. package/dist/prosemirror/commands/propertyChangeScope.js +3 -2
  150. package/dist/prosemirror/commands/tableCellMergeResolution.d.ts +9 -0
  151. package/dist/prosemirror/commands/tableCellMergeResolution.js +181 -0
  152. package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -1
  153. package/dist/prosemirror/conversion/fromProseDoc.js +286 -61
  154. package/dist/prosemirror/conversion/sdtAttrs.d.ts +9 -0
  155. package/dist/prosemirror/conversion/sdtAttrs.js +49 -0
  156. package/dist/prosemirror/conversion/toProseDoc.d.ts +2 -1
  157. package/dist/prosemirror/conversion/toProseDoc.js +347 -98
  158. package/dist/prosemirror/extensions/ExtensionManager.js +1 -1
  159. package/dist/prosemirror/extensions/StarterKit.js +6 -1
  160. package/dist/prosemirror/extensions/core/ParagraphExtension.js +4 -1
  161. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +1 -0
  162. package/dist/prosemirror/extensions/features/ListExtension.js +1 -1
  163. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +10 -1
  164. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +93 -2
  165. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +2 -1
  166. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +6 -2
  167. package/dist/prosemirror/extensions/marks/CharacterSpacingExtension.js +1 -1
  168. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.d.ts +9 -1
  169. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +24 -1
  170. package/dist/prosemirror/extensions/nodes/FieldExtension.js +1 -1
  171. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -0
  172. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +22 -0
  173. package/dist/prosemirror/extensions/nodes/TableExtension.js +9 -3
  174. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +13 -0
  175. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +41 -0
  176. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +5 -2
  177. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +4 -1
  178. package/dist/prosemirror/findReplaceSelection.d.ts +6 -1
  179. package/dist/prosemirror/findReplaceSelection.js +57 -13
  180. package/dist/prosemirror/plugins/contentControlWidgets.d.ts +34 -8
  181. package/dist/prosemirror/plugins/contentControlWidgets.js +125 -91
  182. package/dist/prosemirror/plugins/selectionTracker.js +1 -0
  183. package/dist/prosemirror/plugins/suggestionMode.js +1 -1
  184. package/dist/prosemirror/schema/index.d.ts +2 -2
  185. package/dist/prosemirror/schema/marks.d.ts +5 -1
  186. package/dist/prosemirror/schema/nodes.d.ts +64 -7
  187. package/dist/prosemirror/selectionState.js +1 -0
  188. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  189. package/dist/prosemirror/tableCellMergeRevision.d.ts +9 -0
  190. package/dist/prosemirror/tableCellMergeRevision.js +22 -0
  191. package/dist/prosemirror/textBoxAnchorAttrs.d.ts +9 -0
  192. package/dist/prosemirror/textBoxAnchorAttrs.js +33 -0
  193. package/dist/prosemirror/utils/extractTrackedChanges.d.ts +1 -1
  194. package/dist/prosemirror/utils/extractTrackedChanges.js +62 -14
  195. package/dist/prosemirror/validation.js +9 -1
  196. package/dist/render-dom/BodySelectionOverlay.d.ts +31 -0
  197. package/dist/render-dom/BodySelectionOverlay.js +76 -0
  198. package/dist/render-dom/HeaderFooterSelectionOverlay.d.ts +28 -0
  199. package/dist/render-dom/HeaderFooterSelectionOverlay.js +121 -0
  200. package/dist/render-dom/RemoteSelectionOverlay.d.ts +30 -0
  201. package/dist/render-dom/RemoteSelectionOverlay.js +67 -0
  202. package/dist/render-dom/RenderedDomContext.d.ts +40 -0
  203. package/dist/render-dom/RenderedDomContext.js +126 -0
  204. package/dist/render-dom/resolveSidebarItemPositions.d.ts +33 -0
  205. package/dist/render-dom/resolveSidebarItemPositions.js +45 -0
  206. package/dist/server.d.ts +6 -2
  207. package/dist/server.js +5 -1
  208. package/dist/utils/findReplace.d.ts +32 -0
  209. package/dist/utils/findReplace.js +118 -0
  210. package/dist/utils/fontFamilyMerge.js +9 -11
  211. package/dist/utils/fontLoader.js +3 -2
  212. package/dist/utils/fontResolver.js +22 -4
  213. package/dist/utils/fontWeights.d.ts +1 -1
  214. package/dist/utils/fontWeights.js +1 -1
  215. package/dist/utils/headerFooter.js +24 -1
  216. package/dist/utils/paragraphFormattingMerge.js +1 -0
  217. package/package.json +1 -1
@@ -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 };
@@ -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 };
@@ -0,0 +1,217 @@
1
+ import { getAttributeAnyPrefix, getLocalName, parseXmlDocument } from "../xmlParser.js";
2
+ import { DocxArchiveError, loadDocxArchive } from "./boundedArchive.js";
3
+ import { TaggedError } from "better-result";
4
+ import { createHash } from "node:crypto";
5
+ //#region src/docx/server/inspectDocxPackage.ts
6
+ const FOLIO_DOCX_PACKAGE_INSPECTION_VERSION = 1;
7
+ const FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS = Object.freeze({
8
+ maxXmlParts: 16,
9
+ maxXmlPartBytes: 8 * 1024 * 1024,
10
+ maxXmlTotalBytes: 16 * 1024 * 1024
11
+ });
12
+ const FOLIO_DOCX_PACKAGE_INSPECTION_ERROR_CODES = Object.freeze([
13
+ "invalid-limits",
14
+ "too-many-xml-parts",
15
+ "duplicate-xml-part",
16
+ "part-not-found",
17
+ "part-not-xml",
18
+ "xml-part-too-large",
19
+ "xml-total-too-large",
20
+ "xml-decode-failed"
21
+ ]);
22
+ var FolioDocxPackageInspectionError = class extends TaggedError("FolioDocxPackageInspectionError")() {};
23
+ const CONTENT_TYPES_PATH = "[Content_Types].xml";
24
+ const CONTENT_TYPES_READ_LIMIT = 4 * 1024 * 1024;
25
+ const XML_PATH_PATTERN = /(?:\.xml|\.rels)$/iu;
26
+ const XML_CONTENT_TYPES = /* @__PURE__ */ new Set(["application/xml", "text/xml"]);
27
+ const comparePackagePaths = (left, right) => {
28
+ if (left < right) return -1;
29
+ return left === right ? 0 : 1;
30
+ };
31
+ const decodeXml = (bytes, part) => {
32
+ let encoding = "utf-8";
33
+ if (bytes.length >= 2 && (bytes[0] === 255 && bytes[1] === 254 || bytes[0] === 60 && bytes[1] === 0)) encoding = "utf-16le";
34
+ else if (bytes.length >= 2 && (bytes[0] === 254 && bytes[1] === 255 || bytes[0] === 0 && bytes[1] === 60)) encoding = "utf-16be";
35
+ try {
36
+ return new TextDecoder(encoding, { fatal: true }).decode(bytes);
37
+ } catch (cause) {
38
+ throw new FolioDocxPackageInspectionError({
39
+ message: `Failed to decode XML package part "${part}"`,
40
+ code: "xml-decode-failed",
41
+ part,
42
+ cause
43
+ });
44
+ }
45
+ };
46
+ const parseContentTypes = (xml) => {
47
+ const declarations = {
48
+ defaults: /* @__PURE__ */ new Map(),
49
+ overrides: /* @__PURE__ */ new Map()
50
+ };
51
+ if (xml === null) return declarations;
52
+ const root = parseXmlDocument(xml);
53
+ if (root === null || getLocalName(root.name ?? "") !== "Types") return declarations;
54
+ for (const element of root.elements ?? []) {
55
+ if (element.type !== "element") continue;
56
+ const localName = getLocalName(element.name ?? "");
57
+ if (localName === "Default") {
58
+ const extension = getAttributeAnyPrefix(element, "Extension")?.toLowerCase();
59
+ const contentType = getAttributeAnyPrefix(element, "ContentType");
60
+ if (extension !== void 0 && contentType !== null) declarations.defaults.set(extension, contentType);
61
+ continue;
62
+ }
63
+ if (localName !== "Override") continue;
64
+ const partName = getAttributeAnyPrefix(element, "PartName")?.replace(/^\//u, "");
65
+ const contentType = getAttributeAnyPrefix(element, "ContentType");
66
+ if (partName !== void 0 && contentType !== null) declarations.overrides.set(partName, contentType);
67
+ }
68
+ return declarations;
69
+ };
70
+ const extensionFor = (path) => {
71
+ const filename = path.slice(path.lastIndexOf("/") + 1);
72
+ const separator = filename.lastIndexOf(".");
73
+ return separator === -1 ? null : filename.slice(separator + 1).toLowerCase();
74
+ };
75
+ const contentTypeFor = (path, declarations) => {
76
+ const override = declarations.overrides.get(path);
77
+ if (override !== void 0) return override;
78
+ const extension = extensionFor(path);
79
+ return extension === null ? null : declarations.defaults.get(extension) ?? null;
80
+ };
81
+ const isXmlPart = (path, contentType) => {
82
+ if (contentType !== null) return XML_CONTENT_TYPES.has(contentType) || contentType.endsWith("+xml");
83
+ return XML_PATH_PATTERN.test(path);
84
+ };
85
+ const toPackagePart = (entry, declarations) => {
86
+ const contentType = contentTypeFor(entry.path, declarations);
87
+ return {
88
+ path: entry.path,
89
+ kind: isXmlPart(entry.path, contentType) ? "xml" : "binary",
90
+ contentType,
91
+ declaredUncompressedBytes: entry.declaredUncompressedBytes
92
+ };
93
+ };
94
+ const requireNonNegativeInteger = (value, name) => {
95
+ if (Number.isSafeInteger(value) && value >= 0) return value;
96
+ throw new FolioDocxPackageInspectionError({
97
+ message: `${name} must be a non-negative safe integer`,
98
+ code: "invalid-limits"
99
+ });
100
+ };
101
+ const resolveLimits = (limits) => ({
102
+ maxXmlParts: requireNonNegativeInteger(limits?.maxXmlParts ?? FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS.maxXmlParts, "maxXmlParts"),
103
+ maxXmlPartBytes: requireNonNegativeInteger(limits?.maxXmlPartBytes ?? FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS.maxXmlPartBytes, "maxXmlPartBytes"),
104
+ maxXmlTotalBytes: requireNonNegativeInteger(limits?.maxXmlTotalBytes ?? FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS.maxXmlTotalBytes, "maxXmlTotalBytes")
105
+ });
106
+ const validateRequestedParts = ({ requestedPaths, partsByPath, limits }) => {
107
+ if (requestedPaths.length > limits.maxXmlParts) throw new FolioDocxPackageInspectionError({
108
+ message: `Requested ${requestedPaths.length} XML parts (max ${limits.maxXmlParts})`,
109
+ code: "too-many-xml-parts"
110
+ });
111
+ const seen = /* @__PURE__ */ new Set();
112
+ let declaredTotalBytes = 0;
113
+ for (const path of requestedPaths) {
114
+ if (seen.has(path)) throw new FolioDocxPackageInspectionError({
115
+ message: `XML package part "${path}" was requested more than once`,
116
+ code: "duplicate-xml-part",
117
+ part: path
118
+ });
119
+ seen.add(path);
120
+ const part = partsByPath.get(path);
121
+ if (part === void 0) throw new FolioDocxPackageInspectionError({
122
+ message: `Package part "${path}" does not exist`,
123
+ code: "part-not-found",
124
+ part: path
125
+ });
126
+ if (part.kind !== "xml") throw new FolioDocxPackageInspectionError({
127
+ message: `Package part "${path}" is not declared as XML`,
128
+ code: "part-not-xml",
129
+ part: path
130
+ });
131
+ const declaredBytes = part.declaredUncompressedBytes;
132
+ if (declaredBytes === null) continue;
133
+ if (declaredBytes > limits.maxXmlPartBytes) throw new FolioDocxPackageInspectionError({
134
+ message: `XML package part "${path}" exceeds the per-part inspection limit`,
135
+ code: "xml-part-too-large",
136
+ part: path
137
+ });
138
+ declaredTotalBytes += declaredBytes;
139
+ if (declaredTotalBytes > limits.maxXmlTotalBytes) throw new FolioDocxPackageInspectionError({
140
+ message: "Requested XML package parts exceed the cumulative inspection limit",
141
+ code: "xml-total-too-large"
142
+ });
143
+ }
144
+ };
145
+ const readRequestedXmlParts = async ({ paths, partsByPath, limits, readEntryUint8 }) => {
146
+ const selected = [];
147
+ let totalBytes = 0;
148
+ for (const path of paths) {
149
+ const remainingTotalBytes = limits.maxXmlTotalBytes - totalBytes;
150
+ const readLimit = Math.min(limits.maxXmlPartBytes, remainingTotalBytes);
151
+ let bytes;
152
+ try {
153
+ bytes = await readEntryUint8(path, { maxBytes: readLimit });
154
+ } catch (cause) {
155
+ if (cause instanceof DocxArchiveError && cause.reason === "entry-too-large") {
156
+ const cumulativeLimitReached = readLimit < limits.maxXmlPartBytes;
157
+ throw new FolioDocxPackageInspectionError({
158
+ message: cumulativeLimitReached ? "Requested XML package parts exceed the cumulative inspection limit" : `XML package part "${path}" exceeds the per-part inspection limit`,
159
+ code: cumulativeLimitReached ? "xml-total-too-large" : "xml-part-too-large",
160
+ part: path,
161
+ cause
162
+ });
163
+ }
164
+ throw cause;
165
+ }
166
+ if (bytes === null) throw new FolioDocxPackageInspectionError({
167
+ message: `Package part "${path}" does not exist`,
168
+ code: "part-not-found",
169
+ part: path
170
+ });
171
+ totalBytes += bytes.byteLength;
172
+ if (totalBytes > limits.maxXmlTotalBytes) throw new FolioDocxPackageInspectionError({
173
+ message: "Requested XML package parts exceed the cumulative inspection limit",
174
+ code: "xml-total-too-large"
175
+ });
176
+ selected.push({
177
+ path,
178
+ contentType: partsByPath.get(path)?.contentType ?? null,
179
+ byteLength: bytes.byteLength,
180
+ sha256: createHash("sha256").update(bytes).digest("hex"),
181
+ text: decodeXml(bytes, path)
182
+ });
183
+ }
184
+ return selected;
185
+ };
186
+ /**
187
+ * Inspect package metadata and only the explicitly requested XML part bodies.
188
+ * This reports archive contents, not package conformance; use the conformance
189
+ * report when an operation requires a validity decision.
190
+ */
191
+ const inspectDocxPackage = async (bytes, options = {}) => {
192
+ const limits = resolveLimits(options.limits);
193
+ const requestedPaths = options.xmlParts ?? [];
194
+ const archive = await loadDocxArchive(bytes, options.archive);
195
+ const contentTypesBytes = await archive.readEntryUint8(CONTENT_TYPES_PATH, { maxBytes: CONTENT_TYPES_READ_LIMIT });
196
+ const declarations = parseContentTypes(contentTypesBytes === null ? null : decodeXml(contentTypesBytes, CONTENT_TYPES_PATH));
197
+ const parts = archive.entryMetadata.filter(({ directory }) => !directory).map((entry) => toPackagePart(entry, declarations)).toSorted(({ path: left }, { path: right }) => comparePackagePaths(left, right));
198
+ const partsByPath = new Map(parts.map((part) => [part.path, part]));
199
+ validateRequestedParts({
200
+ requestedPaths,
201
+ partsByPath,
202
+ limits
203
+ });
204
+ return {
205
+ version: 1,
206
+ parts,
207
+ xmlParts: await readRequestedXmlParts({
208
+ paths: requestedPaths,
209
+ partsByPath,
210
+ limits,
211
+ readEntryUint8: archive.readEntryUint8
212
+ }),
213
+ limits
214
+ };
215
+ };
216
+ //#endregion
217
+ export { FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS, FOLIO_DOCX_PACKAGE_INSPECTION_ERROR_CODES, FOLIO_DOCX_PACKAGE_INSPECTION_VERSION, FolioDocxPackageInspectionError, inspectDocxPackage, readRequestedXmlParts };