@embedpdf/engine-core 3.0.0-next.13 → 3.0.0-next.15

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 (135) hide show
  1. package/dist/AnnotationList-CkijRHet.d.cts +27163 -0
  2. package/dist/AnnotationList-CkijRHet.d.ts +27163 -0
  3. package/dist/Change-CKoxfTNY.js +889 -0
  4. package/dist/Change-CKoxfTNY.js.map +1 -0
  5. package/dist/Change-CjImsrN0.cjs +1020 -0
  6. package/dist/Change-CjImsrN0.cjs.map +1 -0
  7. package/dist/DocumentRenderService-BfFdFSwI.cjs +120 -0
  8. package/dist/DocumentRenderService-BfFdFSwI.cjs.map +1 -0
  9. package/dist/DocumentRenderService-DV3J9lMz.js +85 -0
  10. package/dist/DocumentRenderService-DV3J9lMz.js.map +1 -0
  11. package/dist/{DocumentSecurityService-_JRjqvoI.d.ts → DocumentSecurityService-CU1M6Q7m.d.ts} +100 -112
  12. package/dist/{DocumentSecurityService-ClTZc0zO.d.cts → DocumentSecurityService-e6vp2de8.d.cts} +100 -112
  13. package/dist/LocalDocumentHandle-D02FMm-Z.cjs +53 -0
  14. package/dist/LocalDocumentHandle-D02FMm-Z.cjs.map +1 -0
  15. package/dist/LocalDocumentHandle-DAPdaOt_.js +30 -0
  16. package/dist/LocalDocumentHandle-DAPdaOt_.js.map +1 -0
  17. package/dist/LocalEngine-DFoBm4B3.d.cts +1565 -0
  18. package/dist/LocalEngine-DPriFujS.d.ts +1565 -0
  19. package/dist/PageInsertBlankInput-CXHAM7l6.cjs +17 -0
  20. package/dist/PageInsertBlankInput-CXHAM7l6.cjs.map +1 -0
  21. package/dist/PageInsertBlankInput-DhVYVJ_S.js +12 -0
  22. package/dist/PageInsertBlankInput-DhVYVJ_S.js.map +1 -0
  23. package/dist/authority-B3BklsB4.js +2239 -0
  24. package/dist/authority-B3BklsB4.js.map +1 -0
  25. package/dist/authority-DLMNR7ff.cjs +2676 -0
  26. package/dist/authority-DLMNR7ff.cjs.map +1 -0
  27. package/dist/builders-CNj0POeY.js +161 -0
  28. package/dist/builders-CNj0POeY.js.map +1 -0
  29. package/dist/builders-Cgdp_YCn.cjs +184 -0
  30. package/dist/builders-Cgdp_YCn.cjs.map +1 -0
  31. package/dist/calibrate-7T3fnP8-.js +2404 -0
  32. package/dist/calibrate-7T3fnP8-.js.map +1 -0
  33. package/dist/calibrate-DJBXnhLE.cjs +2919 -0
  34. package/dist/calibrate-DJBXnhLE.cjs.map +1 -0
  35. package/dist/charmap-Cprti1ta.js +239 -0
  36. package/dist/charmap-Cprti1ta.js.map +1 -0
  37. package/dist/charmap-aYlefQP9.cjs +304 -0
  38. package/dist/charmap-aYlefQP9.cjs.map +1 -0
  39. package/dist/conformance.cjs +16753 -5247
  40. package/dist/conformance.cjs.map +1 -1
  41. package/dist/conformance.d.cts +928 -165
  42. package/dist/conformance.d.ts +928 -165
  43. package/dist/conformance.js +16731 -5268
  44. package/dist/conformance.js.map +1 -1
  45. package/dist/format-Bp3QFSH1.cjs +574 -0
  46. package/dist/format-Bp3QFSH1.cjs.map +1 -0
  47. package/dist/format-Dgg428Kb.js +461 -0
  48. package/dist/format-Dgg428Kb.js.map +1 -0
  49. package/dist/index.cjs +330 -103
  50. package/dist/index.d.cts +2 -2
  51. package/dist/index.d.ts +2 -2
  52. package/dist/index.js +14 -5
  53. package/dist/pageRefs-B3ur7Ckm.cjs +51 -0
  54. package/dist/pageRefs-B3ur7Ckm.cjs.map +1 -0
  55. package/dist/pageRefs-DkbuxSCV.js +40 -0
  56. package/dist/pageRefs-DkbuxSCV.js.map +1 -0
  57. package/dist/public.cjs +57 -0
  58. package/dist/public.d.cts +5 -0
  59. package/dist/public.d.ts +5 -0
  60. package/dist/public.js +9 -0
  61. package/dist/resolveAnnotationDraft-7Ke4nV7F.cjs +3646 -0
  62. package/dist/resolveAnnotationDraft-7Ke4nV7F.cjs.map +1 -0
  63. package/dist/resolveAnnotationDraft-CUpf0SMh.js +3167 -0
  64. package/dist/resolveAnnotationDraft-CUpf0SMh.js.map +1 -0
  65. package/dist/{resolver-inAVzsWt.js → resolver-CzxmD9Ck.js} +294 -144
  66. package/dist/resolver-CzxmD9Ck.js.map +1 -0
  67. package/dist/{resolver-CYVNV4hn.cjs → resolver-D2OCtE8F.cjs} +347 -143
  68. package/dist/resolver-D2OCtE8F.cjs.map +1 -0
  69. package/dist/runtime-Cl0v56mm.d.ts +2032 -0
  70. package/dist/runtime-p1WDzDo4.d.cts +2032 -0
  71. package/dist/runtime.cjs +733 -284
  72. package/dist/runtime.cjs.map +1 -1
  73. package/dist/runtime.d.cts +5 -1348
  74. package/dist/runtime.d.ts +5 -1348
  75. package/dist/runtime.js +396 -179
  76. package/dist/runtime.js.map +1 -1
  77. package/dist/schema-C-gU4vj7.cjs +3034 -0
  78. package/dist/schema-C-gU4vj7.cjs.map +1 -0
  79. package/dist/schema-DzlUNk38.js +2081 -0
  80. package/dist/schema-DzlUNk38.js.map +1 -0
  81. package/dist/schemas-Berf1swF.cjs +2481 -0
  82. package/dist/schemas-Berf1swF.cjs.map +1 -0
  83. package/dist/schemas-D6xOGjqg.js +1654 -0
  84. package/dist/schemas-D6xOGjqg.js.map +1 -0
  85. package/dist/subtype-CG4WpIRj.js +129 -0
  86. package/dist/subtype-CG4WpIRj.js.map +1 -0
  87. package/dist/subtype-aDt5TFmK.cjs +158 -0
  88. package/dist/subtype-aDt5TFmK.cjs.map +1 -0
  89. package/dist/validation.cjs +578 -0
  90. package/dist/validation.cjs.map +1 -0
  91. package/dist/validation.d.cts +174 -0
  92. package/dist/validation.d.ts +174 -0
  93. package/dist/validation.js +571 -0
  94. package/dist/validation.js.map +1 -0
  95. package/dist/value-BFS1R8ek.cjs +435 -0
  96. package/dist/value-BFS1R8ek.cjs.map +1 -0
  97. package/dist/value-Dhc8eYcx.js +340 -0
  98. package/dist/value-Dhc8eYcx.js.map +1 -0
  99. package/dist/viewport-BJW13EGZ.js +540 -0
  100. package/dist/viewport-BJW13EGZ.js.map +1 -0
  101. package/dist/viewport-DlL5Bxbg.cjs +671 -0
  102. package/dist/viewport-DlL5Bxbg.cjs.map +1 -0
  103. package/dist/wire.cjs +1056 -379
  104. package/dist/wire.cjs.map +1 -1
  105. package/dist/wire.d.cts +15069 -725
  106. package/dist/wire.d.ts +15069 -725
  107. package/dist/wire.js +825 -239
  108. package/dist/wire.js.map +1 -1
  109. package/package.json +10 -1
  110. package/dist/AbortError-Cq2igpZ6.cjs +0 -38
  111. package/dist/AbortError-Cq2igpZ6.cjs.map +0 -1
  112. package/dist/AbortError-DHqPBnYc.js +0 -27
  113. package/dist/AbortError-DHqPBnYc.js.map +0 -1
  114. package/dist/AnnotationListSnapshot-D3mFut1e.d.cts +0 -6871
  115. package/dist/AnnotationListSnapshot-D3mFut1e.d.ts +0 -6871
  116. package/dist/Engine-Bx0NOfmd.d.cts +0 -1264
  117. package/dist/Engine-DxtpCYcP.d.ts +0 -1264
  118. package/dist/PageInsertBlankInput-CqXBmFhN.cjs +0 -449
  119. package/dist/PageInsertBlankInput-CqXBmFhN.cjs.map +0 -1
  120. package/dist/PageInsertBlankInput-cHq9-i-z.js +0 -378
  121. package/dist/PageInsertBlankInput-cHq9-i-z.js.map +0 -1
  122. package/dist/builders-Bqm8yTpi.cjs +0 -3564
  123. package/dist/builders-Bqm8yTpi.cjs.map +0 -1
  124. package/dist/builders-Dqo2X-dt.js +0 -3121
  125. package/dist/builders-Dqo2X-dt.js.map +0 -1
  126. package/dist/fold-2V29cxCV.cjs +0 -447
  127. package/dist/fold-2V29cxCV.cjs.map +0 -1
  128. package/dist/fold-CdEJTBTn.js +0 -334
  129. package/dist/fold-CdEJTBTn.js.map +0 -1
  130. package/dist/resolver-CYVNV4hn.cjs.map +0 -1
  131. package/dist/resolver-inAVzsWt.js.map +0 -1
  132. package/dist/schemas-BLARNCgx.js +0 -3113
  133. package/dist/schemas-BLARNCgx.js.map +0 -1
  134. package/dist/schemas-DIqFMmrC.cjs +0 -4468
  135. package/dist/schemas-DIqFMmrC.cjs.map +0 -1
@@ -1,3564 +0,0 @@
1
- const require_fold = require("./fold-2V29cxCV.cjs");
2
- const require_PageInsertBlankInput = require("./PageInsertBlankInput-CqXBmFhN.cjs");
3
- const require_resolver = require("./resolver-CYVNV4hn.cjs");
4
- //#region src/dto/PdfAction.ts
5
- /**
6
- * The one Table-240 decode, shared by the reader, the schema's producers,
7
- * and tests — so bit semantics cannot fork.
8
- */
9
- const decodeSubmitFormFlags = (raw) => {
10
- const bit = (n) => (raw & 1 << n - 1) !== 0;
11
- const format = bit(9) ? "pdf" : bit(6) ? "xfdf" : bit(3) ? "html" : "fdf";
12
- return {
13
- raw,
14
- exclude: bit(1),
15
- includeNoValueFields: bit(2),
16
- format,
17
- method: bit(4) && (format === "html" || format === "pdf") ? "get" : "post",
18
- submitCoordinates: bit(5),
19
- includeAppendSaves: bit(7),
20
- includeAnnotations: bit(8),
21
- canonicalFormat: bit(10),
22
- exclNonUserAnnots: bit(11),
23
- exclFKey: bit(12),
24
- embedForm: bit(14)
25
- };
26
- };
27
- //#endregion
28
- //#region src/dto/PageRender.ts
29
- function createPageImageHandle(result, blobSource) {
30
- return {
31
- ...result,
32
- async objectUrl(signal) {
33
- if (typeof Blob === "undefined" || typeof URL === "undefined" || !URL.createObjectURL) throw new require_fold.EngineError(require_PageInsertBlankInput.EngineErrorCode.RuntimeUnavailable, "Object URLs are not available in this environment");
34
- const blob = await blobSource.blob(signal);
35
- const url = URL.createObjectURL(blob);
36
- return {
37
- url,
38
- revoke: () => URL.revokeObjectURL(url)
39
- };
40
- }
41
- };
42
- }
43
- //#endregion
44
- //#region src/dto/PdfSaveMode.ts
45
- const DEFAULT_PDF_SAVE_MODE = "incremental";
46
- //#endregion
47
- //#region src/dto/RichText.ts
48
- /** The plain-text projection of a rich document: paragraphs joined by `\r`,
49
- * runs concatenated — what the engine writes to `/Contents`. */
50
- function richTextPlainText(doc) {
51
- return doc.paragraphs.map((p) => p.runs.map((r) => r.text).join("")).join("\r");
52
- }
53
- /** Plain text as rich paragraphs: one per line break, one run each, no
54
- * overrides (the engine's own `/Contents` fallback shape). */
55
- function richTextParagraphsFromPlainText(text) {
56
- return text.split(/\r\n|\r|\n/).map((line) => ({ runs: [{ text: line }] }));
57
- }
58
- //#endregion
59
- //#region src/identity/PageObjectNumber.ts
60
- /**
61
- * Type guard for the rare malformed-PDF case where a page is a direct
62
- * object. The engine treats `0` as malformed and throws `EngineError(NotFound)`
63
- * before any page-level operation.
64
- */
65
- function isValidPageObjectNumber(value) {
66
- return typeof value === "number" && Number.isInteger(value) && value > 0;
67
- }
68
- //#endregion
69
- //#region src/identity/AnnotationStableId.ts
70
- /**
71
- * URL-safe encoding of a stable id, used by the cloud HTTP surface as the
72
- * `:annotKey` route parameter. Decoded by the server back into an
73
- * `AnnotationStableId` via `decodeStableIdKey`.
74
- *
75
- * Format:
76
- * `{ kind: 'objectNumber', value: 42 }` -> `'obj:42'`
77
- * `{ kind: 'nm', value: 'foo bar' }` -> `'nm:foo bar'`
78
- *
79
- * The caller is responsible for `encodeURIComponent`-ing the result before
80
- * splicing it into a URL path; `wirePaths.layerAnnotationByKey` already does
81
- * that. /NM values are opaque strings and may contain anything; the
82
- * `nm:` prefix lets the decoder distinguish them from numeric ids
83
- * unambiguously.
84
- */
85
- function encodeStableIdKey(id) {
86
- if (id.kind === "objectNumber") {
87
- if (!Number.isInteger(id.value) || id.value <= 0) throw new RangeError(`encodeStableIdKey: objectNumber must be a positive integer, got ${id.value}`);
88
- return `obj:${id.value}`;
89
- }
90
- return `nm:${id.value}`;
91
- }
92
- /**
93
- * Inverse of `encodeStableIdKey`. Returns `null` for malformed input so
94
- * the server can answer 400 InvalidArg with a useful message instead of
95
- * throwing.
96
- *
97
- * The input is the already-`decodeURIComponent`-ed segment from the route
98
- * path. Empty `/NM` values are rejected: `nm:` with no suffix is not a
99
- * valid identity.
100
- */
101
- function decodeStableIdKey(key) {
102
- if (key.startsWith("obj:")) {
103
- const rest = key.slice(4);
104
- const n = Number.parseInt(rest, 10);
105
- if (!Number.isInteger(n) || n <= 0 || String(n) !== rest) return null;
106
- return {
107
- kind: "objectNumber",
108
- value: n
109
- };
110
- }
111
- if (key.startsWith("nm:")) {
112
- const value = key.slice(3);
113
- if (value.length === 0) return null;
114
- return {
115
- kind: "nm",
116
- value
117
- };
118
- }
119
- return null;
120
- }
121
- //#endregion
122
- //#region src/revision/RevisionToken.ts
123
- function revisionTokensEqual(a, b) {
124
- return a.docSessionId === b.docSessionId && a.pageObjectNumber === b.pageObjectNumber && a.generation === b.generation;
125
- }
126
- //#endregion
127
- //#region src/revision/WeakAnnotationState.ts
128
- const UNKNOWN_WEAK_ANNOTATION_STATE = { kind: "unknown" };
129
- function knownWeakAnnotationState(hasAnyWeakAnnotations) {
130
- return {
131
- kind: "known",
132
- hasAnyWeakAnnotations
133
- };
134
- }
135
- //#endregion
136
- //#region src/annotation/primitives.ts
137
- const NO_ANNOTATION_FLAGS = {
138
- invisible: false,
139
- hidden: false,
140
- print: false,
141
- noZoom: false,
142
- noRotate: false,
143
- noView: false,
144
- readOnly: false,
145
- locked: false,
146
- toggleNoView: false,
147
- lockedContents: false
148
- };
149
- //#endregion
150
- //#region src/resource/BinarySource.ts
151
- function isBlob(value) {
152
- return typeof Blob !== "undefined" && value instanceof Blob;
153
- }
154
- /**
155
- * Copy a Uint8Array view into a fresh, exactly-sized ArrayBuffer the call
156
- * owns. Always copies — even when the view spans its whole buffer — because
157
- * the result may be transferred to a worker (detaching it), and the view is
158
- * the caller's. Returning `view.buffer` here would let one transfer zero
159
- * the caller's bytes (a stamp library's Uint8Array became single-use).
160
- * Callers wanting a zero-copy hand-off need an explicit "give" form, as
161
- * `pages.insert` has for bare ArrayBuffers; `BinarySource` has none.
162
- */
163
- function toOwnedArrayBuffer(view) {
164
- const copy = new ArrayBuffer(view.byteLength);
165
- new Uint8Array(copy).set(view);
166
- return copy;
167
- }
168
- /**
169
- * Resolve any accepted `BinarySource` form into a `WireResource`.
170
- * Async because Blob bytes can only be read asynchronously.
171
- */
172
- async function resolveBinarySource(source) {
173
- if (source instanceof Uint8Array) return { bytes: toOwnedArrayBuffer(source) };
174
- if (isBlob(source)) {
175
- const blobName = blobFileName(source);
176
- return {
177
- bytes: await source.arrayBuffer(),
178
- mimeType: source.type || void 0,
179
- ...blobName !== void 0 ? { name: blobName } : {}
180
- };
181
- }
182
- const { data, mimeType, name } = source;
183
- const inner = data instanceof Uint8Array ? {
184
- bytes: toOwnedArrayBuffer(data),
185
- mimeType: void 0,
186
- name: void 0
187
- } : {
188
- bytes: await data.arrayBuffer(),
189
- mimeType: data.type || void 0,
190
- name: blobFileName(data)
191
- };
192
- const resolvedName = name ?? inner.name;
193
- return {
194
- bytes: inner.bytes,
195
- ...mimeType ?? inner.mimeType ? { mimeType: mimeType ?? inner.mimeType } : {},
196
- ...resolvedName !== void 0 ? { name: resolvedName } : {}
197
- };
198
- }
199
- /** A browser `File` is a Blob with a `name` — pick it up so attachment
200
- * drafts can pass a `File` directly without repeating the file name. */
201
- function blobFileName(blob) {
202
- const name = blob.name;
203
- return typeof name === "string" && name.length > 0 ? name : void 0;
204
- }
205
- //#endregion
206
- //#region src/resource/binaryMetadata.ts
207
- /**
208
- * Detect format (and intrinsic dimensions for rasters) of a binary buffer.
209
- *
210
- * @returns metadata, or `null` for unsupported/corrupt data
211
- */
212
- function sniffBinaryMetadata(buffer) {
213
- if (buffer.byteLength < 4) return null;
214
- const bytes = new Uint8Array(buffer);
215
- if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) return parsePng(buffer);
216
- if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return parseJpeg(buffer);
217
- if (bytes[0] === 37 && bytes[1] === 80 && bytes[2] === 68 && bytes[3] === 70) return { mimeType: "application/pdf" };
218
- return null;
219
- }
220
- function parsePng(buffer) {
221
- if (buffer.byteLength < 24) return null;
222
- const view = new DataView(buffer);
223
- const width = view.getUint32(16);
224
- const height = view.getUint32(20);
225
- if (width === 0 || height === 0) return null;
226
- return {
227
- mimeType: "image/png",
228
- width,
229
- height
230
- };
231
- }
232
- function parseJpeg(buffer) {
233
- const bytes = new Uint8Array(buffer);
234
- let offset = 2;
235
- while (offset + 4 < bytes.byteLength) {
236
- if (bytes[offset] !== 255) return null;
237
- const marker = bytes[offset + 1];
238
- if (marker >= 192 && marker <= 195) {
239
- if (offset + 9 > bytes.byteLength) return null;
240
- const view = new DataView(buffer, offset + 5);
241
- const height = view.getUint16(0);
242
- const width = view.getUint16(2);
243
- if (width === 0 || height === 0) return null;
244
- return {
245
- mimeType: "image/jpeg",
246
- width,
247
- height
248
- };
249
- }
250
- if (marker === 216 || marker === 217) offset += 2;
251
- else if (marker === 255) offset += 1;
252
- else {
253
- const segLen = bytes[offset + 2] << 8 | bytes[offset + 3];
254
- offset += 2 + segLen;
255
- }
256
- }
257
- return null;
258
- }
259
- //#endregion
260
- //#region src/annotation/kinds/file-attachment/normalize.ts
261
- /**
262
- * Resolve an attachment `file` into its wire halves: metadata into the
263
- * JSON body, bytes into the resource map. Unlike stamps there is NO
264
- * format allowlist — attaching arbitrary files is the point — so the
265
- * declared mime type wins (attachment formats cannot be reliably sniffed;
266
- * the writer falls back to `application/octet-stream` when absent).
267
- *
268
- * Shared by the file-attachment annotation draft (below) and the
269
- * document-level `attachments.create` mutation — one vocabulary, one
270
- * normalizer for both homes a file can enter a PDF through.
271
- */
272
- async function normalizeAttachmentFileSource(file, key) {
273
- const resolved = await resolveBinarySource(file.data);
274
- const name = file.name ?? resolved.name;
275
- if (!name) throw new require_fold.EngineError(require_PageInsertBlankInput.EngineErrorCode.InvalidArg, "attachment file requires a name — pass { data, name } or a File whose name is set");
276
- const mimeType = file.mimeType ?? resolved.mimeType;
277
- return {
278
- wireFile: {
279
- resource: key,
280
- name,
281
- ...mimeType !== void 0 ? { mimeType } : {},
282
- ...file.description !== void 0 ? { description: file.description } : {}
283
- },
284
- resource: {
285
- bytes: resolved.bytes,
286
- ...mimeType !== void 0 ? { mimeType } : {},
287
- name
288
- }
289
- };
290
- }
291
- async function normalizeFileAttachmentDraft(draft, allocateKey) {
292
- const { file, ...rest } = draft;
293
- const key = allocateKey();
294
- const { wireFile, resource } = await normalizeAttachmentFileSource(file, key);
295
- return {
296
- wire: {
297
- ...rest,
298
- file: wireFile
299
- },
300
- resources: { [key]: resource }
301
- };
302
- }
303
- //#endregion
304
- //#region src/annotation/kinds/stamp/normalize.ts
305
- /**
306
- * Resolve + validate a stamp `source`. Sniffing happens here — before any
307
- * transport — so callers get a fast `InvalidArg` instead of a worker/HTTP
308
- * round-trip. The sniffed mime type overwrites whatever was declared.
309
- */
310
- async function resolveStampSource(source) {
311
- const resolved = await resolveBinarySource(source);
312
- const meta = sniffBinaryMetadata(resolved.bytes);
313
- if (!meta) throw new require_fold.EngineError(require_PageInsertBlankInput.EngineErrorCode.InvalidArg, "stamp source must be PNG, JPEG, or single-page PDF bytes (format is detected from the data, not the declared mime type)");
314
- return {
315
- ...resolved,
316
- mimeType: meta.mimeType
317
- };
318
- }
319
- async function normalizeStampDraft(draft, allocateKey) {
320
- const { source, ...rest } = draft;
321
- const resolved = await resolveStampSource(source);
322
- const key = allocateKey();
323
- return {
324
- wire: {
325
- ...rest,
326
- source: { resource: key }
327
- },
328
- resources: { [key]: resolved }
329
- };
330
- }
331
- async function normalizeStampPatch(patch, allocateKey) {
332
- if (patch.source === void 0) {
333
- const { source: _source, ...rest } = patch;
334
- return {
335
- wire: rest,
336
- resources: {}
337
- };
338
- }
339
- const { source, ...rest } = patch;
340
- const resolved = await resolveStampSource(source);
341
- const key = allocateKey();
342
- return {
343
- wire: {
344
- ...rest,
345
- source: { resource: key }
346
- },
347
- resources: { [key]: resolved }
348
- };
349
- }
350
- //#endregion
351
- //#region src/annotation/normalize.ts
352
- function createResourceKeyAllocator() {
353
- let next = 0;
354
- return () => `r${next++}`;
355
- }
356
- async function normalizeAnnotationDraft(draft) {
357
- switch (draft.subtype) {
358
- case "stamp": return normalizeStampDraft(draft, createResourceKeyAllocator());
359
- case "file-attachment": return normalizeFileAttachmentDraft(draft, createResourceKeyAllocator());
360
- default: return {
361
- wire: draft,
362
- resources: {}
363
- };
364
- }
365
- }
366
- async function normalizeAnnotationPatch(patch) {
367
- switch (patch.subtype) {
368
- case "stamp": return normalizeStampPatch(patch, createResourceKeyAllocator());
369
- default: return {
370
- wire: patch,
371
- resources: {}
372
- };
373
- }
374
- }
375
- //#endregion
376
- //#region src/annotation/relationships.ts
377
- /**
378
- * Classify one annotation by its relationship edge alone. Mirrors the
379
- * spec's "practical parser rule": no `/IRT` is top-level; `/RT /Group` is
380
- * a grouped subordinate; anything else with an `/IRT` is a reply (the
381
- * engine has already normalized a missing `/RT` to `'reply'`).
382
- */
383
- function classifyRelation(a) {
384
- if (!a.inReplyTo) return "top-level";
385
- return a.replyType === "group" ? "grouped-subordinate" : "reply";
386
- }
387
- /**
388
- * Stable string key for an {@link AnnotationRef}, used to match a child's
389
- * `inReplyTo` against a candidate parent's `ref`. Both sides are produced
390
- * by the engine with the same identity precedence (objectNumber, then nm,
391
- * then index), so equal keys mean "same annotation".
392
- */
393
- function refKey(ref) {
394
- switch (ref.kind) {
395
- case "objectNumber": return `obj:${ref.pageObjectNumber}:${ref.annotObjectNumber}`;
396
- case "nm": return `nm:${ref.pageObjectNumber}:${ref.nm}`;
397
- case "index": return `idx:${ref.pageObjectNumber}:${ref.index}`;
398
- }
399
- }
400
- /**
401
- * Compose a flat annotation list (one page, or a whole document via
402
- * `listRawAll()` flattened) into {@link AnnotationThread}s in primary
403
- * order.
404
- *
405
- * Rules (ISO 32000 §12.5.6.2 + the spec's UI rule):
406
- * - Annotations with no `/IRT` become thread primaries, in input order.
407
- * - A `/RT /Group` child is attached to its primary's `groupedParts`.
408
- * - A `/RT /R` (or default) child is attached to its primary's `replies`.
409
- * - A child whose parent is not in the input set is treated as an
410
- * orphan primary (defensive — e.g. a partial/filtered page slice).
411
- *
412
- * Limitations (v1, matches the legacy stack): threading is one level deep
413
- * — a reply-to-a-reply is keyed to its direct parent, so it only appears
414
- * nested if that direct parent is itself a primary. Callers that want a
415
- * sidebar of "most annotations, not widgets/popups" should pre-filter the
416
- * input; this helper is intentionally unopinionated about eligibility.
417
- */
418
- function buildThreads(annotations) {
419
- const byKey = /* @__PURE__ */ new Map();
420
- for (const a of annotations) {
421
- byKey.set(refKey(a.ref), a);
422
- if (a.nm && a.nm.length > 0) byKey.set(`nm:${a.ref.pageObjectNumber}:${a.nm}`, a);
423
- }
424
- const threads = [];
425
- const threadByPrimaryKey = /* @__PURE__ */ new Map();
426
- const primaryThread = (primary) => {
427
- const key = refKey(primary.ref);
428
- let thread = threadByPrimaryKey.get(key);
429
- if (!thread) {
430
- thread = {
431
- primary,
432
- groupedParts: [],
433
- replies: []
434
- };
435
- threadByPrimaryKey.set(key, thread);
436
- threads.push(thread);
437
- }
438
- return thread;
439
- };
440
- for (const a of annotations) if (!a.inReplyTo) primaryThread(a);
441
- for (const a of annotations) {
442
- if (!a.inReplyTo) continue;
443
- const parent = byKey.get(refKey(a.inReplyTo));
444
- if (!parent || parent.inReplyTo) {
445
- primaryThread(a);
446
- continue;
447
- }
448
- const thread = primaryThread(parent);
449
- if (a.replyType === "group") thread.groupedParts.push(a);
450
- else thread.replies.push(a);
451
- }
452
- return threads;
453
- }
454
- //#endregion
455
- //#region src/annotation/comments.ts
456
- /** Subtypes that never participate in comment threads. `unsupported`
457
- * covers `/Popup` dictionaries (raw code 16) and unreadable foreign
458
- * blobs — neither makes a meaningful comment card. */
459
- const EXCLUDED_SUBTYPES = /* @__PURE__ */ new Set([
460
- "widget",
461
- "link",
462
- "unsupported"
463
- ]);
464
- const REVIEW_STATES = /* @__PURE__ */ new Set([
465
- "accepted",
466
- "rejected",
467
- "cancelled",
468
- "completed",
469
- "none"
470
- ]);
471
- const MARKED_STATES = /* @__PURE__ */ new Set(["marked", "unmarked"]);
472
- const nonEmpty = (v) => typeof v === "string" && v !== "";
473
- /**
474
- * An ISO 32000 §12.5.6.3 state annotation: a text annotation carrying a
475
- * non-empty `/State` or `/StateModel`. Classification never relies on
476
- * flags — producers disagree on how state annotations are flagged.
477
- */
478
- function isStateAnnotation(a) {
479
- return a.subtype === "text" && (nonEmpty(a.state) || nonEmpty(a.stateModel));
480
- }
481
- function buildCommentThreads(annotations, opts = {}) {
482
- const eligible = annotations.filter((a) => !EXCLUDED_SUBTYPES.has(a.subtype));
483
- const byKey = /* @__PURE__ */ new Map();
484
- for (const a of eligible) {
485
- const key = refKey(a.ref);
486
- if (!byKey.has(key)) byKey.set(key, a);
487
- if (a.nm) {
488
- const aliasKey = refKey({
489
- kind: "nm",
490
- pageObjectNumber: a.pageObjectNumber,
491
- nm: a.nm
492
- });
493
- if (!byKey.has(aliasKey)) byKey.set(aliasKey, a);
494
- }
495
- }
496
- const children = /* @__PURE__ */ new Map();
497
- for (const a of eligible) {
498
- if (!a.inReplyTo) continue;
499
- const parent = byKey.get(refKey(a.inReplyTo));
500
- if (!parent) continue;
501
- const parentKey = refKey(parent.ref);
502
- const list = children.get(parentKey);
503
- if (list) list.push(a);
504
- else children.set(parentKey, [a]);
505
- }
506
- const visited = /* @__PURE__ */ new Set();
507
- const threads = [];
508
- const walk = (root) => {
509
- const replies = [];
510
- const groupedParts = [];
511
- const states = [];
512
- visited.add(refKey(root.ref));
513
- const stack = [...children.get(refKey(root.ref)) ?? []];
514
- while (stack.length > 0) {
515
- const a = stack.pop();
516
- const key = refKey(a.ref);
517
- if (visited.has(key)) continue;
518
- visited.add(key);
519
- if (isStateAnnotation(a)) states.push(a);
520
- else if (classifyRelation(a) === "grouped-subordinate") groupedParts.push(a);
521
- else replies.push(a);
522
- stack.push(...children.get(key) ?? []);
523
- }
524
- replies.sort(chronological);
525
- states.sort(chronological);
526
- threads.push({
527
- root,
528
- pageObjectNumber: root.pageObjectNumber,
529
- replies,
530
- groupedParts,
531
- review: computeReview(states, opts)
532
- });
533
- };
534
- for (const a of eligible) if (!a.inReplyTo && !isStateAnnotation(a)) walk(a);
535
- for (const a of eligible) {
536
- if (visited.has(refKey(a.ref))) continue;
537
- if (isStateAnnotation(a)) continue;
538
- walk(a);
539
- }
540
- return threads;
541
- }
542
- /** `created ?? modified` ascending; undated last; z-order tiebreak. */
543
- function chronological(a, b) {
544
- const at = a.created ?? a.modified;
545
- const bt = b.created ?? b.modified;
546
- if (at !== null && bt !== null && at !== bt) return at < bt ? -1 : 1;
547
- if (at !== null && bt === null) return -1;
548
- if (at === null && bt !== null) return 1;
549
- return a.index - b.index;
550
- }
551
- function computeReview(states, opts) {
552
- const byReviewer = {};
553
- const markedLatest = {};
554
- let lastChange = null;
555
- for (const a of states) {
556
- const status = toReviewStatus(a);
557
- if (!status) continue;
558
- if (status.stateModel === "marked") {
559
- if (status.by !== null && isNewer(status, markedLatest[status.by])) markedLatest[status.by] = status;
560
- continue;
561
- }
562
- if (status.by !== null && isNewer(status, byReviewer[status.by])) byReviewer[status.by] = status;
563
- if (isNewer(status, lastChange)) lastChange = status;
564
- }
565
- const markedBy = Object.keys(markedLatest).filter((key) => markedLatest[key].state === "marked").sort();
566
- const review = {
567
- byReviewer,
568
- lastChange,
569
- markedBy,
570
- statusRefs: states.map((a) => a.ref)
571
- };
572
- if (opts.currentUserId !== void 0) review.mine = byReviewer[opts.currentUserId] ?? null;
573
- return review;
574
- }
575
- /**
576
- * Derive a `ReviewStatus` from one state annotation, or null when nothing
577
- * classifiable can be derived. ISO defaulting happens HERE, not in the
578
- * engine reader: a known model with an absent state means `none` /
579
- * `unmarked`; an absent model infers from a known state; a custom model
580
- * with no state has no derivable status.
581
- */
582
- function toReviewStatus(a) {
583
- if (a.subtype !== "text") return null;
584
- const rawState = nonEmpty(a.state) ? a.state : null;
585
- let stateModel = nonEmpty(a.stateModel) ? a.stateModel : null;
586
- if (stateModel === null && rawState !== null) if (REVIEW_STATES.has(rawState)) stateModel = "review";
587
- else if (MARKED_STATES.has(rawState)) stateModel = "marked";
588
- else return null;
589
- let state = rawState;
590
- if (state === null) if (stateModel === "review") state = "none";
591
- else if (stateModel === "marked") state = "unmarked";
592
- else return null;
593
- if (stateModel === null) return null;
594
- return {
595
- state,
596
- stateModel,
597
- by: a.userId ?? (nonEmpty(a.author) ? a.author : null),
598
- at: a.modified ?? a.created ?? null,
599
- ref: a.ref
600
- };
601
- }
602
- /** `a` newer than (or supersedes) `b`. `at` decides; dated beats undated.
603
- * States arrive pre-sorted by `chronological` (date, then z-order), so on
604
- * equal or missing dates the later entry wins via `>=` / `true`. */
605
- function isNewer(a, b) {
606
- if (!b) return true;
607
- if (a.at !== null && b.at !== null) return a.at >= b.at;
608
- if (a.at !== null) return true;
609
- if (b.at !== null) return false;
610
- return true;
611
- }
612
- //#endregion
613
- //#region src/mutation/AnnotationMutationImpactPolicy.ts
614
- function changesAnnotationList(_kind) {
615
- return true;
616
- }
617
- function shiftsExistingAnnotationIndices(kind) {
618
- return kind === "delete" || kind === "move";
619
- }
620
- function invalidatesWeakIndexRefs(kind, weakStateBefore) {
621
- if (!shiftsExistingAnnotationIndices(kind)) return false;
622
- if (weakStateBefore.kind !== "known") throw new Error(`invalidatesWeakIndexRefs requires a known WeakAnnotationState for ${kind}; got 'unknown'. Caller must scan the page before computing mutation impact.`);
623
- return weakStateBefore.hasAnyWeakAnnotations;
624
- }
625
- //#endregion
626
- //#region src/annotation/appearance.ts
627
- /**
628
- * Coordinate tolerance in PDF user-space points. PDF numbers round-trip
629
- * through f32 (~7 significant digits) and client-side space conversions, so
630
- * exact float equality would misclassify no-op writes; 1e-3 pt is far below
631
- * anything visible while far above accumulated f32 drift at page magnitudes.
632
- */
633
- const EPSILON = .001;
634
- /**
635
- * Keys that never affect `/AP` on any kind: the discriminator, behavioral
636
- * flags, reply relationships, grouping, and the conversation-plane
637
- * entries (`/Subj` subject line, `/State` + `/StateModel` review status —
638
- * dictionary-only per ISO 32000 §12.5.6.3, never painted).
639
- */
640
- const INERT_KEYS = /* @__PURE__ */ new Set([
641
- "subtype",
642
- "flags",
643
- "inReplyTo",
644
- "replyType",
645
- "groupId",
646
- "subject",
647
- "state",
648
- "stateModel"
649
- ]);
650
- /**
651
- * Kinds whose `/AP` paints `/Contents`. Everywhere else `contents` is popup
652
- * note text — editing a comment must never re-bake (or destroy) the shape's
653
- * appearance.
654
- */
655
- const CONTENTS_PAINTED = /* @__PURE__ */ new Set(["free-text", "redact"]);
656
- /**
657
- * The vertex family's `/EMBD_Metadata/Rotation` is an advisory scalar (the
658
- * vertices are already rotated); it never drives the AP generator, so patches
659
- * touching it are appearance-inert for these kinds.
660
- */
661
- const ADVISORY_ROTATION = /* @__PURE__ */ new Set([
662
- "line",
663
- "polyline",
664
- "polygon",
665
- "ink"
666
- ]);
667
- /**
668
- * The box kinds carry the `/EMBD_Metadata` transform pair (`rotation` +
669
- * `unrotatedRect`), tri-state on writes: omitted fields are PRESERVED, `null`
670
- * (or `0` for rotation) clears. Translation verification therefore compares
671
- * the after-state — `patch.rotation ?? current.rotation` — not the patch keys.
672
- */
673
- const BOX_TRANSFORM = /* @__PURE__ */ new Set([
674
- "square",
675
- "circle",
676
- "free-text",
677
- "stamp"
678
- ]);
679
- /**
680
- * Per-kind absolute-geometry fields (PDF user space) that a rigid translation
681
- * shifts together. A kind absent from this table never takes the translation
682
- * route. `rect` is required in every entry: a translation always moves `/Rect`.
683
- */
684
- const TRANSLATABLE_GEOMETRY = {
685
- square: ["rect", "unrotatedRect"],
686
- circle: ["rect", "unrotatedRect"],
687
- "free-text": [
688
- "rect",
689
- "unrotatedRect",
690
- "calloutLine"
691
- ],
692
- line: ["rect", "linePoints"],
693
- polygon: ["rect", "vertices"],
694
- polyline: ["rect", "vertices"],
695
- ink: ["rect", "inkList"],
696
- highlight: ["rect", "quadPoints"],
697
- underline: ["rect", "quadPoints"],
698
- squiggly: ["rect", "quadPoints"],
699
- strikeout: ["rect", "quadPoints"],
700
- redact: ["rect", "quadPoints"],
701
- caret: ["rect"],
702
- text: ["rect"],
703
- stamp: ["rect", "unrotatedRect"],
704
- "file-attachment": ["rect"],
705
- link: ["rect"]
706
- };
707
- const numEq = (a, b) => Math.abs(a - b) <= EPSILON;
708
- /** Degrees normalized to [0,360); absent reads as 0 (no rotation). */
709
- const normDeg = (v) => {
710
- return typeof v === "number" ? (v % 360 + 360) % 360 : 0;
711
- };
712
- /**
713
- * Semantic equality between a patch value and the current DTO value.
714
- * Numbers compare within {@link EPSILON}; `null` and `undefined` both mean
715
- * "entry absent" (the tri-state clear of an absent entry is a no-op); arrays
716
- * and objects compare structurally.
717
- */
718
- function semanticEqual(a, b) {
719
- if (a == null || b == null) return a == null && b == null;
720
- if (typeof a === "number" && typeof b === "number") return numEq(a, b);
721
- if (Array.isArray(a) || Array.isArray(b)) {
722
- if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
723
- return a.every((v, i) => semanticEqual(v, b[i]));
724
- }
725
- if (typeof a === "object" && typeof b === "object") {
726
- const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
727
- for (const k of keys) if (!semanticEqual(a[k], b[k])) return false;
728
- return true;
729
- }
730
- return a === b;
731
- }
732
- /**
733
- * Is `next` exactly `cur` shifted by `(dx, dy)`? Walks the value structurally:
734
- * numeric leaves under x-axis keys (`left`/`right`/`x`) must shift by `dx`,
735
- * y-axis keys (`bottom`/`top`/`y`) by `dy`, and any other numeric leaf must be
736
- * unchanged. Arrays walk elementwise (same length required).
737
- */
738
- function shiftedBy(cur, next, dx, dy) {
739
- if (cur == null || next == null) return cur == null && next == null;
740
- if (Array.isArray(cur) || Array.isArray(next)) {
741
- if (!Array.isArray(cur) || !Array.isArray(next) || cur.length !== next.length) return false;
742
- return cur.every((v, i) => shiftedBy(v, next[i], dx, dy));
743
- }
744
- if (typeof cur === "object" && typeof next === "object") {
745
- const keys = /* @__PURE__ */ new Set([...Object.keys(cur), ...Object.keys(next)]);
746
- for (const k of keys) {
747
- const c = cur[k];
748
- const n = next[k];
749
- if (typeof c === "number" && typeof n === "number") {
750
- if (!numEq(c + (k === "left" || k === "right" || k === "x" ? dx : k === "bottom" || k === "top" || k === "y" ? dy : 0), n)) return false;
751
- } else if (!shiftedBy(c, n, dx, dy)) return false;
752
- }
753
- return true;
754
- }
755
- return cur === next;
756
- }
757
- /**
758
- * Verify the touched geometry is one rigid translation of the current state.
759
- * Requires `patch.rect` (a translation always moves `/Rect`) with width and
760
- * height preserved; every other geometry field present on the annotation must
761
- * ride along shifted by the same delta — a rect move that leaves `vertices`
762
- * behind is NOT a translation (the dictionary would desync from the pixels).
763
- * For box kinds the `/EMBD_Metadata` transform group is checked as an
764
- * after-state: writes are tri-state (omitted preserves, null/0 clears), so an
765
- * omitted rotation keeps the current one — but a preserved-yet-unshifted
766
- * `unrotatedRect` still fails the congruence check via the field loop.
767
- */
768
- function isRigidTranslation(cur, pat, subtype, geometryKeys) {
769
- const curRect = cur.rect;
770
- const patRect = pat.rect;
771
- if (!curRect || !patRect) return false;
772
- const dx = patRect.left - curRect.left;
773
- const dy = patRect.bottom - curRect.bottom;
774
- if (!numEq(patRect.right - patRect.left, curRect.right - curRect.left)) return false;
775
- if (!numEq(patRect.top - patRect.bottom, curRect.top - curRect.bottom)) return false;
776
- if (BOX_TRANSFORM.has(subtype)) {
777
- const rotAfter = pat.rotation === void 0 ? cur.rotation : pat.rotation;
778
- if (!numEq(normDeg(cur.rotation), normDeg(rotAfter))) return false;
779
- }
780
- for (const key of geometryKeys) {
781
- if (key === "rect") continue;
782
- const c = cur[key];
783
- const p = pat[key];
784
- if (c == null && p == null) continue;
785
- if (c == null || p == null) return false;
786
- if (!shiftedBy(c, p, dx, dy)) return false;
787
- }
788
- return true;
789
- }
790
- /**
791
- * Classify an update patch against the annotation's current DTO.
792
- *
793
- * 1. Value-diff: drop keys whose value semantically equals the current one,
794
- * plus the always-inert metadata keys (and per-kind inert keys: `contents`
795
- * where it isn't painted, advisory `rotation` on the vertex family).
796
- * Nothing left → `'inert'`.
797
- * 2. If every remaining key is translatable geometry for this kind AND the
798
- * values are one rigid translation → `'translation'`.
799
- * 3. Anything else — style, text, unknown keys, unknown kinds → `'regenerate'`.
800
- */
801
- function appearanceImpactOf(current, patch) {
802
- if (patch.subtype !== current.subtype) return "regenerate";
803
- const cur = current;
804
- const pat = patch;
805
- const subtype = patch.subtype;
806
- const touched = [];
807
- for (const [key, value] of Object.entries(pat)) {
808
- if (value === void 0 || INERT_KEYS.has(key)) continue;
809
- if (key === "contents" && !CONTENTS_PAINTED.has(subtype)) continue;
810
- if (key === "rotation" && ADVISORY_ROTATION.has(subtype)) continue;
811
- if (!semanticEqual(value, cur[key])) touched.push(key);
812
- }
813
- if (touched.length === 0) return "inert";
814
- const geometryKeys = TRANSLATABLE_GEOMETRY[subtype];
815
- if (!geometryKeys) return "regenerate";
816
- if (!touched.every((k) => geometryKeys.includes(k))) return "regenerate";
817
- return isRigidTranslation(cur, pat, subtype, geometryKeys) ? "translation" : "regenerate";
818
- }
819
- //#endregion
820
- //#region src/identity/FormFieldRef.ts
821
- /**
822
- * URL-safe encoding of a `FormFieldRef`, used by the cloud HTTP surface as
823
- * the `:fieldKey` route parameter. Decoded by the server back into a
824
- * `FormFieldRef` via `decodeFieldRefKey`. Mirrors the annotation plane's
825
- * `encodeStableIdKey` (`obj:42` / `nm:…`) so the two member-key syntaxes
826
- * read the same on the wire:
827
- *
828
- * `{ kind: 'objectNumber', fieldObjectNumber: 12 }` -> `'obj:12'`
829
- * `{ kind: 'fqn', name: 'billing.name' }` -> `'fqn:billing.name'`
830
- *
831
- * The caller is responsible for `encodeURIComponent`-ing the result before
832
- * splicing it into a URL path; the `wirePaths.layerFormField*` builders
833
- * already do that. FQNs are opaque strings and may contain anything; the
834
- * `fqn:` prefix keeps them unambiguous from numeric ids.
835
- */
836
- function encodeFieldRefKey(ref) {
837
- if (ref.kind === "objectNumber") {
838
- if (!Number.isInteger(ref.fieldObjectNumber) || ref.fieldObjectNumber <= 0) throw new RangeError(`encodeFieldRefKey: fieldObjectNumber must be a positive integer, got ${ref.fieldObjectNumber}`);
839
- return `obj:${ref.fieldObjectNumber}`;
840
- }
841
- return `fqn:${ref.name}`;
842
- }
843
- /**
844
- * Inverse of `encodeFieldRefKey`. Returns `null` for malformed input so the
845
- * server can answer 400 InvalidArg with a useful message instead of
846
- * throwing. The input is the already-`decodeURIComponent`-ed segment from
847
- * the route path. Empty FQNs are rejected: `fqn:` with no suffix is not a
848
- * valid identity.
849
- */
850
- function decodeFieldRefKey(key) {
851
- if (key.startsWith("obj:")) {
852
- const rest = key.slice(4);
853
- const n = Number.parseInt(rest, 10);
854
- if (!Number.isInteger(n) || n <= 0 || String(n) !== rest) return null;
855
- return {
856
- kind: "objectNumber",
857
- fieldObjectNumber: n
858
- };
859
- }
860
- if (key.startsWith("fqn:")) {
861
- const name = key.slice(4);
862
- if (name.length === 0) return null;
863
- return {
864
- kind: "fqn",
865
- name
866
- };
867
- }
868
- return null;
869
- }
870
- //#endregion
871
- //#region src/signature/analysis/pdf-value.ts
872
- /**
873
- * Parse the fork's canonical shallow serialisation:
874
- * dict <</Key value/Key2 value>> (keys sorted, `#xx`-escaped)
875
- * array [item item]
876
- * ref n 0 R
877
- * stream stream(length,sha256hex)<<dict>>
878
- * name /Name (with #xx escapes)
879
- * string (literal with backslash escapes) or <hex>
880
- * number -12.5 bool true|false null
881
- */
882
- function parsePdfValue(text) {
883
- const parser = new Parser(text);
884
- const value = parser.value();
885
- parser.skipSpaces();
886
- if (!parser.atEnd()) throw new Error(`trailing input at ${parser.pos}: ${text.slice(parser.pos, parser.pos + 20)}`);
887
- return value;
888
- }
889
- var Parser = class {
890
- constructor(text) {
891
- this.text = text;
892
- require_fold._defineProperty(this, "pos", 0);
893
- }
894
- atEnd() {
895
- return this.pos >= this.text.length;
896
- }
897
- skipSpaces() {
898
- while (this.pos < this.text.length && this.text[this.pos] === " ") this.pos++;
899
- }
900
- value() {
901
- this.skipSpaces();
902
- const t = this.text;
903
- if (t.startsWith("<<", this.pos)) return this.dict();
904
- if (t.startsWith("stream(", this.pos)) return this.stream();
905
- if (t[this.pos] === "[") return this.array();
906
- if (t[this.pos] === "/") return {
907
- t: "name",
908
- v: this.name()
909
- };
910
- if (t[this.pos] === "(") return {
911
- t: "string",
912
- v: this.literalString()
913
- };
914
- if (t[this.pos] === "<") return {
915
- t: "string",
916
- v: this.hexString()
917
- };
918
- if (t.startsWith("true", this.pos)) {
919
- this.pos += 4;
920
- return {
921
- t: "bool",
922
- v: true
923
- };
924
- }
925
- if (t.startsWith("false", this.pos)) {
926
- this.pos += 5;
927
- return {
928
- t: "bool",
929
- v: false
930
- };
931
- }
932
- if (t.startsWith("null", this.pos)) {
933
- this.pos += 4;
934
- return { t: "null" };
935
- }
936
- const ref = /^(\d+) 0 R(?![0-9A-Za-z])/.exec(t.slice(this.pos));
937
- if (ref) {
938
- this.pos += ref[0].length;
939
- return {
940
- t: "ref",
941
- num: Number(ref[1])
942
- };
943
- }
944
- const num = /^[+-]?(?:\d+\.?\d*|\.\d+)/.exec(t.slice(this.pos));
945
- if (num) {
946
- this.pos += num[0].length;
947
- return {
948
- t: "number",
949
- v: Number(num[0])
950
- };
951
- }
952
- throw new Error(`unexpected input at ${this.pos}: ${t.slice(this.pos, this.pos + 20)}`);
953
- }
954
- dict() {
955
- return {
956
- t: "dict",
957
- entries: this.dictEntries()
958
- };
959
- }
960
- dictEntries() {
961
- this.expect("<<");
962
- const entries = {};
963
- for (;;) {
964
- this.skipSpaces();
965
- if (this.text.startsWith(">>", this.pos)) {
966
- this.pos += 2;
967
- return entries;
968
- }
969
- if (this.text[this.pos] !== "/") throw new Error(`expected a key at ${this.pos}`);
970
- const key = this.name();
971
- this.skipSpaces();
972
- entries[key] = this.value();
973
- }
974
- }
975
- stream() {
976
- const m = /^stream\((\d+),([0-9a-fA-F]*)\)/.exec(this.text.slice(this.pos));
977
- if (!m) throw new Error(`malformed stream signature at ${this.pos}`);
978
- this.pos += m[0].length;
979
- const dict = this.dictEntries();
980
- return {
981
- t: "stream",
982
- length: Number(m[1]),
983
- sha256: m[2].toLowerCase(),
984
- dict
985
- };
986
- }
987
- array() {
988
- this.expect("[");
989
- const items = [];
990
- for (;;) {
991
- this.skipSpaces();
992
- if (this.text[this.pos] === "]") {
993
- this.pos++;
994
- return {
995
- t: "array",
996
- items
997
- };
998
- }
999
- items.push(this.value());
1000
- }
1001
- }
1002
- name() {
1003
- this.expect("/");
1004
- let out = "";
1005
- while (this.pos < this.text.length) {
1006
- const ch = this.text[this.pos];
1007
- if (ch === " " || ch === "/" || ch === "[" || ch === "]" || ch === "<" || ch === ">" || ch === "(" || ch === ")") break;
1008
- if (ch === "#" && /^[0-9A-Fa-f]{2}/.test(this.text.slice(this.pos + 1, this.pos + 3))) {
1009
- out += String.fromCharCode(parseInt(this.text.slice(this.pos + 1, this.pos + 3), 16));
1010
- this.pos += 3;
1011
- continue;
1012
- }
1013
- out += ch;
1014
- this.pos++;
1015
- }
1016
- return out;
1017
- }
1018
- literalString() {
1019
- this.expect("(");
1020
- let depth = 1;
1021
- let out = "";
1022
- while (this.pos < this.text.length) {
1023
- const ch = this.text[this.pos++];
1024
- if (ch === "\\") {
1025
- const next = this.text[this.pos++];
1026
- switch (next) {
1027
- case "n":
1028
- out += "\n";
1029
- break;
1030
- case "r":
1031
- out += "\r";
1032
- break;
1033
- case "t":
1034
- out += " ";
1035
- break;
1036
- case "b":
1037
- out += "\b";
1038
- break;
1039
- case "f":
1040
- out += "\f";
1041
- break;
1042
- case "\n": break;
1043
- case "\r":
1044
- if (this.text[this.pos] === "\n") this.pos++;
1045
- break;
1046
- default: {
1047
- const oct = /^[0-7]{1,3}/.exec(this.text.slice(this.pos - 1));
1048
- if (oct) {
1049
- out += String.fromCharCode(parseInt(oct[0], 8));
1050
- this.pos += oct[0].length - 1;
1051
- } else out += next;
1052
- }
1053
- }
1054
- continue;
1055
- }
1056
- if (ch === "(") depth++;
1057
- if (ch === ")") {
1058
- depth--;
1059
- if (depth === 0) return out;
1060
- }
1061
- out += ch;
1062
- }
1063
- throw new Error("unterminated string");
1064
- }
1065
- hexString() {
1066
- this.expect("<");
1067
- const end = this.text.indexOf(">", this.pos);
1068
- if (end < 0) throw new Error("unterminated hex string");
1069
- const hex = this.text.slice(this.pos, end).replace(/\s+/g, "");
1070
- this.pos = end + 1;
1071
- let out = "";
1072
- for (let i = 0; i < hex.length; i += 2) out += String.fromCharCode(parseInt(hex.slice(i, i + 2).padEnd(2, "0"), 16));
1073
- return out;
1074
- }
1075
- expect(token) {
1076
- if (!this.text.startsWith(token, this.pos)) throw new Error(`expected ${token} at ${this.pos}`);
1077
- this.pos += token.length;
1078
- }
1079
- };
1080
- /** Structural equality of two parsed values. */
1081
- function pdfValueEquals(a, b) {
1082
- return stableStringify(a) === stableStringify(b);
1083
- }
1084
- function stableStringify(value) {
1085
- if (value === null) return "null";
1086
- switch (value.t) {
1087
- case "dict": return `<<${Object.keys(value.entries).sort().map((k) => `/${k} ${stableStringify(value.entries[k])}`).join("")}>>`;
1088
- case "stream": return `stream(${value.length},${value.sha256})${stableStringify({
1089
- t: "dict",
1090
- entries: value.dict
1091
- })}`;
1092
- case "array": return `[${value.items.map(stableStringify).join(" ")}]`;
1093
- case "ref": return `${value.num} 0 R`;
1094
- case "name": return `/${encodeName(value.v)}`;
1095
- case "string": return `(${value.v.replace(/[\\()]/g, (ch) => `\\${ch}`)})`;
1096
- case "number": return String(value.v);
1097
- case "bool": return value.v ? "true" : "false";
1098
- case "null": return "null";
1099
- }
1100
- }
1101
- function dictEntries(value) {
1102
- if (!value) return null;
1103
- if (value.t === "dict") return value.entries;
1104
- if (value.t === "stream") return value.dict;
1105
- return null;
1106
- }
1107
- /** Keys whose value differs (added, removed, or changed) between two dictionaries. */
1108
- function changedKeys(a, b) {
1109
- const ea = dictEntries(a) ?? {};
1110
- const eb = dictEntries(b) ?? {};
1111
- const out = /* @__PURE__ */ new Set();
1112
- for (const key of /* @__PURE__ */ new Set([...Object.keys(ea), ...Object.keys(eb)])) if (!pdfValueEquals(ea[key] ?? null, eb[key] ?? null)) out.add(key);
1113
- return out;
1114
- }
1115
- function refsOf(value) {
1116
- if (!value) return [];
1117
- if (value.t === "ref") return [value.num];
1118
- if (value.t === "array") return value.items.flatMap(refsOf);
1119
- return [];
1120
- }
1121
- /** `#xx`-escape a name the way the PDF writer does: delimiters, whitespace, `#`, and non-printables. */
1122
- function encodeName(name) {
1123
- let out = "";
1124
- for (const ch of name) {
1125
- const code = ch.charCodeAt(0);
1126
- if (code < 33 || code > 126 || "#/[]<>(){}%".includes(ch)) out += `#${code.toString(16).toUpperCase().padStart(2, "0")}`;
1127
- else out += ch;
1128
- }
1129
- return out;
1130
- }
1131
- //#endregion
1132
- //#region src/signature/analysis/verdict.ts
1133
- const RANK = {
1134
- unchanged: 0,
1135
- permitted: 1,
1136
- indeterminate: 2,
1137
- forbidden: 3
1138
- };
1139
- /** The worst of several verdicts (a certification replay's steps). */
1140
- function worstVerdict(steps) {
1141
- let worst = "unchanged";
1142
- for (const s of steps) if (RANK[s.verdict] > RANK[worst]) worst = s.verdict;
1143
- return worst;
1144
- }
1145
- /**
1146
- * One aggregation rule everywhere: a proven violation decides, whatever
1147
- * else could not be established; missing evidence never becomes a pass.
1148
- */
1149
- function conclude(findings, hasEffectiveChanges) {
1150
- if (findings.some((f) => f.verdict === "forbidden")) return "forbidden";
1151
- if (findings.some((f) => f.verdict === "incomplete")) return "indeterminate";
1152
- return hasEffectiveChanges ? "permitted" : "unchanged";
1153
- }
1154
- /** The finding that explains a verdict: the violation, else the gap, else the most notable allowance. */
1155
- function primaryFinding(findings) {
1156
- return findings.find((f) => f.verdict === "forbidden") ?? findings.find((f) => f.verdict === "incomplete") ?? findings.find((f) => f.verdict === "permitted");
1157
- }
1158
- function assessmentOf(step) {
1159
- const primary = primaryFinding(step.findings);
1160
- return {
1161
- verdict: step.verdict,
1162
- complete: !step.findings.some((f) => f.verdict === "incomplete"),
1163
- ...primary ? { primary } : {},
1164
- findings: step.findings
1165
- };
1166
- }
1167
- /**
1168
- * A certification window: the net verdict and every replayed step, under
1169
- * the same rule. A step's violation is a violation of the certification
1170
- * even when the final state no longer shows it (corpus v3/83).
1171
- */
1172
- function combine(net, steps) {
1173
- const findings = [...net.findings];
1174
- for (const s of steps) {
1175
- if (s === net) continue;
1176
- for (const f of s.findings) {
1177
- if (f.verdict === "permitted") continue;
1178
- findings.push({
1179
- ...f,
1180
- detail: `revision ${s.newer}: ${f.detail ?? f.rule}`
1181
- });
1182
- }
1183
- }
1184
- const verdict = conclude(findings, net.verdict !== "unchanged" || steps.some((s) => s.verdict !== "unchanged"));
1185
- const primary = primaryFinding(findings);
1186
- return {
1187
- verdict,
1188
- complete: !findings.some((f) => f.verdict === "incomplete"),
1189
- ...primary ? { primary } : {},
1190
- findings
1191
- };
1192
- }
1193
- //#endregion
1194
- //#region src/signature/analysis/evaluate.ts
1195
- const SIDES = ["old", "new"];
1196
- const TRAILER_KEYS = /* @__PURE__ */ new Set([
1197
- "Size",
1198
- "Prev",
1199
- "XRefStm",
1200
- "Info",
1201
- "ID",
1202
- "Root",
1203
- "Type",
1204
- "W",
1205
- "Index",
1206
- "Filter",
1207
- "DecodeParms",
1208
- "Length",
1209
- "Encrypt"
1210
- ]);
1211
- const CATALOG_KEYS = /* @__PURE__ */ new Set([
1212
- "Metadata",
1213
- "Extensions",
1214
- "Version",
1215
- "DSS",
1216
- "AcroForm",
1217
- "Perms"
1218
- ]);
1219
- const ACROFORM_KEYS = /* @__PURE__ */ new Set([
1220
- "DR",
1221
- "DA",
1222
- "Q",
1223
- "SigFlags",
1224
- "NeedAppearances",
1225
- "Fields"
1226
- ]);
1227
- /**
1228
- * What a form field may change after a signature, by the level in force.
1229
- * Every key here has a corpus case behind it (`signature-compat`); a key
1230
- * not listed is forbidden until one does.
1231
- *
1232
- * annotate (an approval signature: "Form Fill-in, Signing and Commenting
1233
- * are allowed"): everything but the field's identity and its place in
1234
- * the tree. v1/08 /V+/AP, 09 /Ff, 10 /Rect, 11 /F, 18 /TU, 21 /DA; 13 /T
1235
- * forbidden. /AA (v1/12) and /A are accepted by Acrobat and deliberately
1236
- * NOT listed: an action on a signed form is executable content.
1237
- *
1238
- * fill (a P=2 certification): value and appearance. v1/26 /V+/AP, v3/64
1239
- * /Ff (ReadOnly added), 65 /DA, 68 /AP stream; v3/66 /TU, 67 /F, v2/50
1240
- * /Rect forbidden.
1241
- */
1242
- const FIELD_KEYS_BY_LEVEL = {
1243
- annotate: /* @__PURE__ */ new Set([
1244
- "V",
1245
- "AS",
1246
- "AP",
1247
- "I",
1248
- "DA",
1249
- "Ff",
1250
- "RV",
1251
- "MK",
1252
- "Q",
1253
- "TU",
1254
- "F",
1255
- "Rect",
1256
- "BS",
1257
- "Opt",
1258
- "DV",
1259
- "MaxLen"
1260
- ]),
1261
- fill: /* @__PURE__ */ new Set([
1262
- "V",
1263
- "AS",
1264
- "AP",
1265
- "I",
1266
- "RV",
1267
- "DA",
1268
- "Ff"
1269
- ])
1270
- };
1271
- /** A separate widget dictionary: the presentation subset of the same tables. */
1272
- const WIDGET_KEYS_BY_LEVEL = {
1273
- annotate: /* @__PURE__ */ new Set([
1274
- "AS",
1275
- "AP",
1276
- "DA",
1277
- "MK",
1278
- "Q",
1279
- "TU",
1280
- "F",
1281
- "Rect",
1282
- "BS"
1283
- ]),
1284
- fill: /* @__PURE__ */ new Set([
1285
- "AS",
1286
- "AP",
1287
- "DA"
1288
- ])
1289
- };
1290
- /**
1291
- * Signing an EXISTING field: its value, appearance and ReadOnly at any
1292
- * level; after an approval signature also its tooltip and a /Lock installed
1293
- * with the signature (v1/18, 19: both valid; under P=2 the combination is
1294
- * rejected, v3/69, so neither is allowed at `fill`).
1295
- */
1296
- const SIGNED_FIELD_KEYS_BY_LEVEL = {
1297
- annotate: /* @__PURE__ */ new Set([
1298
- "V",
1299
- "AP",
1300
- "AS",
1301
- "Ff",
1302
- "TU",
1303
- "Lock"
1304
- ]),
1305
- fill: /* @__PURE__ */ new Set([
1306
- "V",
1307
- "AP",
1308
- "AS",
1309
- "Ff"
1310
- ])
1311
- };
1312
- const SIGNED_WIDGET_KEYS = /* @__PURE__ */ new Set(["AP", "AS"]);
1313
- const FF_READ_ONLY = 1;
1314
- function fieldKeysFor(level) {
1315
- return level === "annotate" ? FIELD_KEYS_BY_LEVEL.annotate : FIELD_KEYS_BY_LEVEL.fill;
1316
- }
1317
- function widgetKeysFor(level) {
1318
- return level === "annotate" ? WIDGET_KEYS_BY_LEVEL.annotate : WIDGET_KEYS_BY_LEVEL.fill;
1319
- }
1320
- function signedFieldKeysFor(level) {
1321
- return level === "annotate" ? SIGNED_FIELD_KEYS_BY_LEVEL.annotate : SIGNED_FIELD_KEYS_BY_LEVEL.fill;
1322
- }
1323
- /**
1324
- * Rules whose permitted findings do not make a revision a CHANGE of the
1325
- * document: an object written again with the sealed value, an object nobody
1326
- * references, a cross-reference container, the trailer's own bookkeeping.
1327
- * A window with nothing else is `unchanged` (Acrobat: "not modified").
1328
- */
1329
- const NON_EFFECTIVE_RULES = /* @__PURE__ */ new Set([
1330
- "identical-rewrite",
1331
- "orphan",
1332
- "xref-container",
1333
- "trailer"
1334
- ]);
1335
- /** The level a step runs at, and the locks it enforces: what the OLDER revision's signatures established. */
1336
- function restrictionsOf(before) {
1337
- const protection = require_resolver.deriveProtection(before.signatures);
1338
- return {
1339
- level: protection.judged ?? "annotate",
1340
- locks: protection.fieldLocks
1341
- };
1342
- }
1343
- /**
1344
- * Judge one pairwise step. Every reference to a changed object, in the
1345
- * older AND the newer revision, must be claimed by a rule that inspected
1346
- * that use; an unclaimed reference is `unexplained` and forbidden. A
1347
- * truncated value is `incomplete`, never permitted. Pure: JSON in, JSON
1348
- * out, the same code in the browser worker, on the server and in tests.
1349
- */
1350
- function evaluateStep(input) {
1351
- const changes = withEvidenceCheck(input.changes);
1352
- const { level, locks } = input.restrictions ?? restrictionsOf(input.before);
1353
- const levelInForce = input.levelOverride ?? level;
1354
- const ctx = new StepContext({
1355
- ...input,
1356
- changes
1357
- }, levelInForce, locks);
1358
- ruleRevisionHealth(ctx);
1359
- ruleXrefContainer(ctx);
1360
- ruleIdenticalRewrite(ctx);
1361
- ruleFieldLock(ctx);
1362
- ruleTrailer(ctx);
1363
- ruleInfo(ctx);
1364
- ruleMetadata(ctx);
1365
- ruleCatalog(ctx);
1366
- ruleAcroForm(ctx);
1367
- ruleDss(ctx);
1368
- ruleSignatureFieldAdded(ctx);
1369
- ruleSignatureAdded(ctx);
1370
- ruleFormFill(ctx);
1371
- ruleAnnotation(ctx);
1372
- ruleUnexplained(ctx);
1373
- const verdict = conclude(ctx.findings, ctx.findings.some((f) => f.verdict === "permitted" && !NON_EFFECTIVE_RULES.has(f.rule)));
1374
- return {
1375
- older: input.older,
1376
- newer: input.newer,
1377
- levelInForce,
1378
- locks,
1379
- verdict,
1380
- findings: ctx.findings,
1381
- changes
1382
- };
1383
- }
1384
- /**
1385
- * The evaluator never trusts its transport to be honest about gaps: a side
1386
- * that exists must carry a value, and an object whose value is missing is
1387
- * judged as truncated, whatever the flag says. Cross-reference containers
1388
- * are exempt (their members are judged on their own).
1389
- */
1390
- function withEvidenceCheck(changes) {
1391
- return changes.map((c) => {
1392
- if (c.value.truncated || c.kind === "xref" || c.kind === "objstm") return c;
1393
- const missingOld = c.present.old && (c.value.old === null || c.read?.old === "failed");
1394
- const missingNew = c.present.new && (c.value.new === null || c.read?.new === "failed");
1395
- return missingOld || missingNew ? {
1396
- ...c,
1397
- value: {
1398
- ...c.value,
1399
- truncated: true
1400
- }
1401
- } : c;
1402
- });
1403
- }
1404
- /**
1405
- * The transport's verdict on each revision as a whole. A sealed revision
1406
- * Acrobat cannot judge later changes to (sparse cross-reference table, a
1407
- * reachable bare-reference object) makes every step over it indeterminate:
1408
- * our verdict would otherwise disagree with the validator recipients use,
1409
- * and for a reason that has nothing to do with the change. An incomplete
1410
- * reachability walk on either side leaves every use unproven.
1411
- */
1412
- function ruleRevisionHealth(ctx) {
1413
- const health = ctx.input.health;
1414
- if (!health) return;
1415
- const base = health.old;
1416
- if (base.sparseXref || base.bareReferences > 0) {
1417
- const why = [base.sparseXref ? "its cross-reference table has no entry for some object numbers below /Size" : null, base.bareReferences > 0 ? `${base.bareReferences} reachable indirect object(s) consist of a bare reference` : null].filter((x) => x !== null).join("; ");
1418
- ctx.findings.push({
1419
- rule: "base-unverifiable",
1420
- verdict: "incomplete",
1421
- objectNumber: 0,
1422
- detail: `the signed revision cannot be verified against later changes by Acrobat (${why}); no verdict is given rather than one Acrobat would contradict`
1423
- });
1424
- }
1425
- for (const side of SIDES) if (!health[side].referrersComplete) ctx.findings.push({
1426
- rule: "unexplained",
1427
- verdict: "incomplete",
1428
- objectNumber: 0,
1429
- detail: `the ${side === "old" ? "older" : "newer"} revision's reachability walk hit its budget; not every use of a changed object is known`
1430
- });
1431
- }
1432
- /**
1433
- * Same effective value on both sides: same canonical serialisation (keys
1434
- * sorted, whitespace normalised - Acrobat compares dictionaries by value,
1435
- * corpus v3/61-62), same stream data (by bytes, v2/45), and both sides
1436
- * actually read. A failed read serialises as "null" and would match another
1437
- * failed read - evidence of nothing.
1438
- */
1439
- function sameEffectiveValue(c) {
1440
- return c.change === "modified" && !c.value.truncated && c.read?.old !== "failed" && c.read?.new !== "failed" && c.raw.old !== null && c.raw.new !== null && c.raw.old === c.raw.new && !c.streamDataChanged;
1441
- }
1442
- function indexRoles(s) {
1443
- const fieldByObj = /* @__PURE__ */ new Map();
1444
- const widgetToField = /* @__PURE__ */ new Map();
1445
- const sigFields = /* @__PURE__ */ new Set();
1446
- for (const f of s.fields) {
1447
- fieldByObj.set(f.objectNumber, f);
1448
- if (f.family === "signature") sigFields.add(f.objectNumber);
1449
- for (const w of f.widgets) widgetToField.set(w, f.objectNumber);
1450
- }
1451
- const signedFields = /* @__PURE__ */ new Set();
1452
- for (const sig of s.signatures) if (sig.signed && sig.field.kind === "objectNumber") signedFields.add(sig.field.fieldObjectNumber);
1453
- return {
1454
- fieldByObj,
1455
- widgetToField,
1456
- pages: new Set(s.pages),
1457
- signedFields,
1458
- sigFields
1459
- };
1460
- }
1461
- function edgeKey(e) {
1462
- return `${e.parent}:${e.label}`;
1463
- }
1464
- var StepContext = class {
1465
- constructor(input, level, locks) {
1466
- this.input = input;
1467
- this.level = level;
1468
- this.locks = locks;
1469
- require_fold._defineProperty(this, "findings", []);
1470
- require_fold._defineProperty(this, "byNum", /* @__PURE__ */ new Map());
1471
- require_fold._defineProperty(this, "before", void 0);
1472
- require_fold._defineProperty(this, "after", void 0);
1473
- require_fold._defineProperty(this, "claims", /* @__PURE__ */ new Map());
1474
- require_fold._defineProperty(this, "locked", /* @__PURE__ */ new Set());
1475
- for (const c of input.changes) this.byNum.set(c.objectNumber, c);
1476
- this.before = indexRoles(input.before);
1477
- this.after = indexRoles(input.after);
1478
- }
1479
- get changes() {
1480
- return this.input.changes;
1481
- }
1482
- allows(needed) {
1483
- return require_resolver.levelAllows(this.level, needed);
1484
- }
1485
- claimEdge(c, side, edge, rule, allow = true) {
1486
- if (this.locked.has(c.objectNumber)) return;
1487
- const key = `${c.objectNumber}:${side}:${edgeKey(edge)}`;
1488
- if (!this.claims.has(key)) this.claims.set(key, {
1489
- rule,
1490
- allow
1491
- });
1492
- }
1493
- claimAll(c, rule, allow = true) {
1494
- for (const side of SIDES) for (const e of c.usage[side]) this.claimEdge(c, side, e, rule, allow);
1495
- }
1496
- /**
1497
- * Claim the references that are the same in both revisions; a reference
1498
- * gained or lost stays unexplained. `allow: false` explains a condemned
1499
- * object's edges (one cause in the report, not a cascade) without letting
1500
- * anything hang off it.
1501
- */
1502
- claimStable(c, rule, allow = true) {
1503
- const oldKeys = new Set(c.usage.old.map(edgeKey));
1504
- const newKeys = new Set(c.usage.new.map(edgeKey));
1505
- for (const e of c.usage.old) if (newKeys.has(edgeKey(e))) this.claimEdge(c, "old", e, rule, allow);
1506
- for (const e of c.usage.new) if (oldKeys.has(edgeKey(e))) this.claimEdge(c, "new", e, rule, allow);
1507
- }
1508
- isClaimed(c, side, edge) {
1509
- return this.claims.has(`${c.objectNumber}:${side}:${edgeKey(edge)}`);
1510
- }
1511
- /** The use was inspected by a rule that allowed it. */
1512
- isAllowed(c, side, edge) {
1513
- return this.claims.get(`${c.objectNumber}:${side}:${edgeKey(edge)}`)?.allow === true;
1514
- }
1515
- permitted(c, rule, detail) {
1516
- if (this.locked.has(c.objectNumber)) return;
1517
- this.findings.push({
1518
- rule,
1519
- verdict: "permitted",
1520
- objectNumber: c.objectNumber,
1521
- detail
1522
- });
1523
- }
1524
- /**
1525
- * A violation is only proven on evidence: a rule condemning an object
1526
- * whose value could not be read or inspected is reporting what it could
1527
- * not see, and that is `incomplete`, never `forbidden`.
1528
- */
1529
- forbidden(c, rule, detail, edge) {
1530
- this.findings.push({
1531
- rule,
1532
- verdict: c.value.truncated ? "incomplete" : "forbidden",
1533
- objectNumber: c.objectNumber,
1534
- edge: edge ? edgeKey(edge) : void 0,
1535
- detail: c.value.truncated ? `${detail} (judged on missing evidence: not proven)` : detail
1536
- });
1537
- }
1538
- /**
1539
- * Claim every reference under a set of parents (an appearance subtree,
1540
- * the DSS): objects whose references come from `parents` through labels
1541
- * `accept`, then everything referenced from those objects, and so on.
1542
- * Runs per side, because a replaced appearance stream exists on one
1543
- * side only.
1544
- */
1545
- claimSubtree(side, roots, accept, rule) {
1546
- const joined = /* @__PURE__ */ new Set();
1547
- let grew = true;
1548
- while (grew) {
1549
- grew = false;
1550
- for (const c of this.changes) {
1551
- if (roots.has(c.objectNumber) || joined.has(c.objectNumber)) continue;
1552
- if (this.locked.has(c.objectNumber)) continue;
1553
- const edges = c.usage[side];
1554
- if (edges.length === 0) continue;
1555
- const inside = edges.filter((e) => roots.has(e.parent) && accept(e.label) || joined.has(e.parent));
1556
- if (inside.length === 0) continue;
1557
- for (const e of inside) this.claimEdge(c, side, e, rule);
1558
- if (edges.every((e) => inside.includes(e) || this.isAllowed(c, side, e))) {
1559
- joined.add(c.objectNumber);
1560
- grew = true;
1561
- }
1562
- }
1563
- }
1564
- }
1565
- isIdentical(c) {
1566
- return sameEffectiveValue(c);
1567
- }
1568
- /** Signatures signed in the newer revision but not the older: this step's signing events. */
1569
- newlySigned() {
1570
- const out = [];
1571
- for (const sig of this.input.after.signatures) {
1572
- if (!sig.signed || sig.field.kind !== "objectNumber") continue;
1573
- if (this.before.signedFields.has(sig.field.fieldObjectNumber)) continue;
1574
- out.push({
1575
- fieldObjectNumber: sig.field.fieldObjectNumber,
1576
- sig
1577
- });
1578
- }
1579
- return out;
1580
- }
1581
- };
1582
- function ruleXrefContainer(ctx) {
1583
- for (const c of ctx.changes) if (c.kind === "xref" || c.kind === "objstm") {
1584
- ctx.claimAll(c, "xref-container");
1585
- ctx.permitted(c, "xref-container", "cross-reference container; members are judged on their own");
1586
- }
1587
- }
1588
- /**
1589
- * An object written again with the value the sealed revision holds is not
1590
- * a modification: Acrobat compares objects by value and reports such a
1591
- * revision as "not modified" for every role tried - document information,
1592
- * a custom catalog entry, a page, the signed signature's own widget, a text
1593
- * field, an annotation, the encryption dictionary, reserialised with keys
1594
- * reordered, under a certification, under a field lock (corpus v1/02-07,
1595
- * 23, v2/47, v3/54-62, 70, 71, 77). Under P=1 nothing is established and
1596
- * the rewrite stays forbidden. Claimed here so the object-kind rules skip
1597
- * it; the verdict treats it as no effective change.
1598
- */
1599
- function ruleIdenticalRewrite(ctx) {
1600
- for (const c of ctx.changes) {
1601
- if (!ctx.isIdentical(c)) continue;
1602
- if (ctx.allows("fill")) {
1603
- ctx.claimAll(c, "identical-rewrite");
1604
- ctx.permitted(c, "identical-rewrite", "written again with the sealed value");
1605
- } else {
1606
- ctx.claimAll(c, "identical-rewrite", false);
1607
- ctx.forbidden(c, "identical-rewrite", `written again with the sealed value under level '${ctx.level}' (not established for this level)`);
1608
- }
1609
- }
1610
- }
1611
- function ruleTrailer(ctx) {
1612
- const c = ctx.byNum.get(0);
1613
- if (!c) return;
1614
- const changed = changedKeys(c.value.old, c.value.new);
1615
- const bad = [...changed].filter((k) => !TRAILER_KEYS.has(k));
1616
- const oldRoot = dictEntries(c.value.old)?.Root;
1617
- const newRoot = dictEntries(c.value.new)?.Root;
1618
- if (bad.length > 0) {
1619
- ctx.forbidden(c, "trailer", `trailer keys changed: ${bad.join(", ")}`);
1620
- return;
1621
- }
1622
- if (changed.has("Root") && !pdfValueEquals(oldRoot ?? null, newRoot ?? null)) {
1623
- ctx.forbidden(c, "trailer", "the trailer points at a different catalog");
1624
- return;
1625
- }
1626
- if (changed.has("Encrypt") && !pdfValueEquals(dictEntries(c.value.old)?.Encrypt ?? null, dictEntries(c.value.new)?.Encrypt ?? null)) {
1627
- ctx.forbidden(c, "trailer", "the trailer points at a different encryption dictionary");
1628
- return;
1629
- }
1630
- ctx.claimAll(c, "trailer");
1631
- ctx.permitted(c, "trailer");
1632
- }
1633
- function onlyEdges(c, test) {
1634
- const all = [...c.usage.old, ...c.usage.new];
1635
- return all.length > 0 && all.every(test);
1636
- }
1637
- function ruleInfo(ctx) {
1638
- for (const c of ctx.changes) {
1639
- if (ctx.isIdentical(c)) continue;
1640
- if (c.objectNumber !== 0 && onlyEdges(c, (e) => e.parent === 0 && e.label === "Info")) {
1641
- ctx.claimAll(c, "info");
1642
- ctx.permitted(c, "info", "document information dictionary");
1643
- }
1644
- }
1645
- }
1646
- function ruleMetadata(ctx) {
1647
- const roots = /* @__PURE__ */ new Set([ctx.input.before.root, ctx.input.after.root]);
1648
- for (const c of ctx.changes) {
1649
- if (ctx.isIdentical(c)) continue;
1650
- if (onlyEdges(c, (e) => roots.has(e.parent) && e.label === "Metadata")) {
1651
- ctx.claimAll(c, "metadata");
1652
- ctx.permitted(c, "metadata", "XMP metadata stream");
1653
- }
1654
- }
1655
- }
1656
- function ruleCatalog(ctx) {
1657
- const root = ctx.input.after.root;
1658
- const c = ctx.byNum.get(root);
1659
- if (!c || ctx.isIdentical(c)) return;
1660
- const changed = changedKeys(c.value.old, c.value.new);
1661
- const bad = [...changed].filter((k) => !CATALOG_KEYS.has(k));
1662
- if (bad.length > 0) {
1663
- ctx.forbidden(c, "catalog-housekeeping", `catalog keys changed: ${bad.join(", ")}`);
1664
- return;
1665
- }
1666
- if (changed.has("AcroForm") && !ctx.input.after.acroForm && !ctx.input.before.acroForm) {
1667
- const problem = acroFormProblem(ctx, dictEntries(c.value.old)?.AcroForm ?? null, dictEntries(c.value.new)?.AcroForm ?? null);
1668
- if (problem) {
1669
- ctx.forbidden(c, "acroform-housekeeping", problem);
1670
- return;
1671
- }
1672
- for (const side of SIDES) ctx.claimSubtree(side, /* @__PURE__ */ new Set([root]), (label) => label.startsWith("AcroForm/DR/"), "acroform-housekeeping");
1673
- }
1674
- if (changed.has("Perms")) {
1675
- const after = require_resolver.deriveProtection(ctx.input.after.signatures);
1676
- const before = require_resolver.deriveProtection(ctx.input.before.signatures);
1677
- if (!after.certification || before.judged !== null) {
1678
- ctx.forbidden(c, "catalog-housekeeping", "/Perms may only appear with the certification signature, in the first signed revision");
1679
- return;
1680
- }
1681
- }
1682
- ctx.claimAll(c, "catalog-housekeeping");
1683
- ctx.permitted(c, "catalog-housekeeping");
1684
- }
1685
- function ruleAcroForm(ctx) {
1686
- const acro = ctx.input.after.acroForm || ctx.input.before.acroForm;
1687
- if (!acro) return;
1688
- const c = ctx.byNum.get(acro);
1689
- if (!c || ctx.isIdentical(c)) return;
1690
- const problem = acroFormProblem(ctx, c.value.old, c.value.new);
1691
- if (problem) {
1692
- ctx.forbidden(c, "acroform-housekeeping", problem);
1693
- return;
1694
- }
1695
- ctx.claimAll(c, "acroform-housekeeping");
1696
- ctx.permitted(c, "acroform-housekeeping");
1697
- for (const side of SIDES) ctx.claimSubtree(side, /* @__PURE__ */ new Set([acro]), (label) => label.startsWith("DR/"), "acroform-housekeeping");
1698
- }
1699
- /** What is wrong with an AcroForm dictionary change, or null when it is housekeeping. */
1700
- function acroFormProblem(ctx, oldValue, newValue) {
1701
- const changed = changedKeys(oldValue, newValue);
1702
- const bad = [...changed].filter((k) => !ACROFORM_KEYS.has(k));
1703
- if (bad.length > 0) return `AcroForm keys changed: ${bad.join(", ")}`;
1704
- if (changed.has("Fields")) {
1705
- const oldRefs = refsOf(dictEntries(oldValue)?.Fields);
1706
- const newRefs = refsOf(dictEntries(newValue)?.Fields);
1707
- if (oldRefs.length > newRefs.length || oldRefs.some((n, i) => newRefs[i] !== n)) return "/AcroForm /Fields is not append-only";
1708
- const newSigFields = newSignatureFields(ctx);
1709
- const strangers = newRefs.slice(oldRefs.length).filter((n) => !newSigFields.has(n));
1710
- if (strangers.length > 0) return `/AcroForm /Fields gained non-signature fields: ${strangers.join(", ")}`;
1711
- }
1712
- return null;
1713
- }
1714
- /** The refs appended to an array-valued key when the old array is a prefix of the new one; null otherwise. */
1715
- function appendedRefs(c, key) {
1716
- const oldItems = dictEntries(c.value.old)?.[key];
1717
- const newItems = dictEntries(c.value.new)?.[key];
1718
- const oldRefs = refsOf(oldItems);
1719
- const newRefs = refsOf(newItems);
1720
- if (oldItems && oldItems.t !== "array") return null;
1721
- if (!newItems || newItems.t !== "array") return null;
1722
- if (oldRefs.length > newRefs.length) return null;
1723
- for (let i = 0; i < oldRefs.length; i++) if (oldRefs[i] !== newRefs[i]) return null;
1724
- return newRefs.slice(oldRefs.length);
1725
- }
1726
- function newSignatureFields(ctx) {
1727
- const out = /* @__PURE__ */ new Set();
1728
- for (const n of ctx.after.sigFields) if (!ctx.before.fieldByObj.has(n)) out.add(n);
1729
- return out;
1730
- }
1731
- function ruleDss(ctx) {
1732
- const roots = /* @__PURE__ */ new Set([ctx.input.before.root, ctx.input.after.root]);
1733
- for (const side of SIDES) ctx.claimSubtree(side, roots, (label) => label === "DSS" || label.startsWith("DSS/"), "dss");
1734
- for (const c of ctx.changes) if (c.usage.new.some((e) => roots.has(e.parent) && e.label === "DSS") && !ctx.findings.some((f) => f.objectNumber === c.objectNumber)) ctx.permitted(c, "dss", "document security store");
1735
- }
1736
- function ruleSignatureFieldAdded(ctx) {
1737
- const added = newSignatureFields(ctx);
1738
- if (added.size === 0) return;
1739
- const rule = "signature-field-added";
1740
- if (!ctx.allows("fill")) {
1741
- for (const n of added) {
1742
- const c = ctx.byNum.get(n);
1743
- if (c) ctx.forbidden(c, rule, `a signature field was added under level '${ctx.level}'`);
1744
- }
1745
- return;
1746
- }
1747
- const newWidgets = /* @__PURE__ */ new Set();
1748
- for (const n of added) {
1749
- const field = ctx.after.fieldByObj.get(n);
1750
- const c = ctx.byNum.get(n);
1751
- if (c) {
1752
- ctx.claimAll(c, rule);
1753
- ctx.permitted(c, rule, `new signature field "${field.name}"`);
1754
- }
1755
- for (const w of field.widgets) {
1756
- newWidgets.add(w);
1757
- if (w === n) continue;
1758
- const wc = ctx.byNum.get(w);
1759
- if (wc) {
1760
- ctx.claimAll(wc, rule);
1761
- ctx.permitted(wc, rule, `widget of new signature field "${field.name}"`);
1762
- }
1763
- }
1764
- }
1765
- for (const p of ctx.after.pages) {
1766
- const c = ctx.byNum.get(p);
1767
- if (!c || ctx.isIdentical(c)) continue;
1768
- const changed = changedKeys(c.value.old, c.value.new);
1769
- if (changed.size !== 1 || !changed.has("Annots")) continue;
1770
- const appended = appendedRefs(c, "Annots");
1771
- if (appended && appended.every((n) => newWidgets.has(n))) {
1772
- ctx.claimAll(c, rule);
1773
- ctx.permitted(c, rule, "page /Annots gained the new signature widget");
1774
- }
1775
- }
1776
- for (const c of ctx.changes) {
1777
- if (c.kind !== "dictionary" || ctx.isIdentical(c)) continue;
1778
- const changed = changedKeys(c.value.old, c.value.new);
1779
- if (changed.size !== 1 || !changed.has("Kids")) continue;
1780
- const appended = appendedRefs(c, "Kids");
1781
- if (appended && appended.length > 0 && appended.every((n) => added.has(n) || newWidgets.has(n))) {
1782
- ctx.claimAll(c, rule);
1783
- ctx.permitted(c, rule, "field /Kids gained the new signature field");
1784
- }
1785
- }
1786
- }
1787
- function ruleSignatureAdded(ctx) {
1788
- const rule = "signature-added";
1789
- const events = ctx.newlySigned();
1790
- if (events.length === 0) return;
1791
- const addedThisStep = newSignatureFields(ctx);
1792
- const beforeProtection = require_resolver.deriveProtection(ctx.input.before.signatures);
1793
- const afterProtection = require_resolver.deriveProtection(ctx.input.after.signatures);
1794
- const parents = /* @__PURE__ */ new Set();
1795
- const values = /* @__PURE__ */ new Set();
1796
- for (const { fieldObjectNumber, sig } of events) {
1797
- const needed = sig.kind === "timestamp" ? "lta" : "fill";
1798
- if (!ctx.allows(needed)) {
1799
- const c = ctx.byNum.get(fieldObjectNumber);
1800
- if (c) ctx.forbidden(c, rule, `a ${sig.kind} was added under level '${ctx.level}'`);
1801
- continue;
1802
- }
1803
- if (sig.catalogCertification && beforeProtection.judged !== null) {
1804
- const c = ctx.byNum.get(fieldObjectNumber);
1805
- if (c) ctx.forbidden(c, rule, "a certification signature after another signature");
1806
- continue;
1807
- }
1808
- const field = ctx.after.fieldByObj.get(fieldObjectNumber);
1809
- parents.add(fieldObjectNumber);
1810
- for (const w of field?.widgets ?? []) parents.add(w);
1811
- const fieldIsNew = addedThisStep.has(fieldObjectNumber);
1812
- const allowedFieldKeys = beforeProtection.judged === null ? /* @__PURE__ */ new Set([...signedFieldKeysFor(ctx.level), "Lock"]) : signedFieldKeysFor(ctx.level);
1813
- const fc = ctx.byNum.get(fieldObjectNumber);
1814
- if (fc && fieldIsNew) ctx.permitted(fc, rule, `new field "${sig.fieldName}" signed`);
1815
- else if (fc && !ctx.isIdentical(fc)) {
1816
- const changed = changedKeys(fc.value.old, fc.value.new);
1817
- const bad = [...changed].filter((k) => !allowedFieldKeys.has(k));
1818
- if (bad.length > 0) {
1819
- ctx.forbidden(fc, rule, `signature field keys changed: ${bad.join(", ")}`);
1820
- ctx.claimStable(fc, rule, false);
1821
- } else if (changed.has("Ff") && !readOnlyOnlyChange(fc, ctx.before.fieldByObj.get(fieldObjectNumber)?.flags)) {
1822
- ctx.forbidden(fc, rule, "signature field flags changed beyond ReadOnly");
1823
- ctx.claimStable(fc, rule, false);
1824
- } else {
1825
- ctx.claimStable(fc, rule);
1826
- ctx.permitted(fc, rule, `field "${sig.fieldName}" signed`);
1827
- }
1828
- }
1829
- for (const w of field?.widgets ?? []) {
1830
- if (w === fieldObjectNumber || fieldIsNew) continue;
1831
- const wc = ctx.byNum.get(w);
1832
- if (!wc || ctx.isIdentical(wc)) continue;
1833
- const bad = [...changedKeys(wc.value.old, wc.value.new)].filter((k) => !SIGNED_WIDGET_KEYS.has(k));
1834
- if (bad.length > 0) {
1835
- ctx.forbidden(wc, rule, `signature widget keys changed: ${bad.join(", ")}`);
1836
- ctx.claimStable(wc, rule, false);
1837
- continue;
1838
- }
1839
- ctx.claimStable(wc, rule);
1840
- ctx.permitted(wc, rule, "signature widget appearance");
1841
- }
1842
- for (const c of ctx.changes) {
1843
- if (c.change !== "added") continue;
1844
- const asValue = c.usage.new.filter((e) => e.parent === fieldObjectNumber && e.label === "V");
1845
- const asLock = c.usage.new.filter((e) => e.parent === fieldObjectNumber && e.label === "Lock");
1846
- const type = dictEntries(c.value.new)?.Type;
1847
- if (asValue.length > 0) {
1848
- if (!(type?.t === "name" && (type.v === "Sig" || type.v === "DocTimeStamp")) && type !== void 0) continue;
1849
- for (const e of c.usage.new) if (e.parent === fieldObjectNumber && e.label === "V" || e.label === "DocMDP") ctx.claimEdge(c, "new", e, rule);
1850
- ctx.permitted(c, rule, "signature value");
1851
- values.add(c.objectNumber);
1852
- } else if (asLock.length > 0) {
1853
- if (!(type === void 0 || type.t === "name" && type.v === "SigFieldLock")) continue;
1854
- for (const e of asLock) ctx.claimEdge(c, "new", e, rule);
1855
- ctx.permitted(c, rule, "field lock installed with the signature");
1856
- }
1857
- }
1858
- }
1859
- if (afterProtection.certification && beforeProtection.judged === null) {
1860
- const root = ctx.input.after.root;
1861
- for (const c of ctx.changes) if (c.change === "added" && onlyEdges(c, (e) => e.parent === root && e.label === "Perms")) {
1862
- ctx.claimAll(c, rule);
1863
- ctx.permitted(c, rule, "certification permissions");
1864
- }
1865
- }
1866
- for (const side of SIDES) ctx.claimSubtree(side, parents, (label) => label === "AP" || label.startsWith("AP/"), rule);
1867
- if (values.size > 0) ctx.claimSubtree("new", values, () => true, rule);
1868
- }
1869
- /**
1870
- * Did `/Ff` change by gaining ReadOnly and nothing else? `/Ff` is
1871
- * inheritable, and a lock writes `effective | ReadOnly` into the terminal
1872
- * dictionary, so a terminal that carried no `/Ff` of its own is judged
1873
- * against the effective flags the older revision's form model reported.
1874
- */
1875
- function readOnlyOnlyChange(c, effectiveBefore) {
1876
- const oldFf = dictEntries(c.value.old)?.Ff;
1877
- const newFf = dictEntries(c.value.new)?.Ff;
1878
- const before = oldFf?.t === "number" ? oldFf.v : effectiveBefore ?? 0;
1879
- const after = newFf?.t === "number" ? newFf.v : 0;
1880
- return (before | FF_READ_ONLY) === after;
1881
- }
1882
- function ruleFormFill(ctx) {
1883
- const rule = "form-fill";
1884
- const fieldKeys = fieldKeysFor(ctx.level);
1885
- const widgetKeys = widgetKeysFor(ctx.level);
1886
- const parents = /* @__PURE__ */ new Set();
1887
- const fields = new Map([...ctx.before.fieldByObj, ...ctx.after.fieldByObj]);
1888
- for (const [num, field] of fields) {
1889
- if (field.family === "signature") continue;
1890
- const fc = ctx.byNum.get(num);
1891
- const widgetChanges = field.widgets.filter((w) => w !== num).map((w) => ctx.byNum.get(w)).filter(Boolean);
1892
- const owners = /* @__PURE__ */ new Set([num, ...field.widgets]);
1893
- const isAp = (label) => label === "AP" || label.startsWith("AP/");
1894
- const appearanceOnly = !fc && widgetChanges.length === 0 ? ctx.changes.filter((c) => SIDES.some((side) => c.usage[side].some((e) => owners.has(e.parent) && isAp(e.label))) && SIDES.every((side) => c.usage[side].every((e) => !isAp(e.label) || owners.has(e.parent)))) : [];
1895
- if (!fc && widgetChanges.length === 0 && appearanceOnly.length === 0) continue;
1896
- if (!ctx.allows("fill")) {
1897
- for (const c of [
1898
- fc,
1899
- ...widgetChanges,
1900
- ...appearanceOnly
1901
- ]) if (c && !ctx.isIdentical(c)) ctx.forbidden(c, rule, `form fill under level '${ctx.level}'`);
1902
- continue;
1903
- }
1904
- if (appearanceOnly.length > 0) {
1905
- for (const c of appearanceOnly) ctx.permitted(c, rule, `appearance of field "${field.name}" regenerated`);
1906
- parents.add(num);
1907
- for (const w of field.widgets) parents.add(w);
1908
- continue;
1909
- }
1910
- let ok = true;
1911
- if (fc && !ctx.isIdentical(fc)) {
1912
- const changed = changedKeys(fc.value.old, fc.value.new);
1913
- const bad = [...changed].filter((k) => !fieldKeys.has(k));
1914
- if (bad.length > 0) {
1915
- ctx.forbidden(fc, rule, `field "${field.name}" keys changed: ${bad.join(", ")}`);
1916
- ctx.claimStable(fc, rule, false);
1917
- ok = false;
1918
- } else if (changed.has("Ff") && !readOnlyOnlyChange(fc, ctx.before.fieldByObj.get(num)?.flags)) {
1919
- ctx.forbidden(fc, rule, `field "${field.name}" flags changed beyond adding ReadOnly`);
1920
- ctx.claimStable(fc, rule, false);
1921
- ok = false;
1922
- } else {
1923
- ctx.claimStable(fc, rule);
1924
- const properties = [...changed].filter((k) => k !== "V" && k !== "AP" && k !== "AS" && k !== "I" && k !== "RV");
1925
- ctx.permitted(fc, rule, properties.length > 0 ? `field "${field.name}" ${changed.has("V") || changed.has("AS") ? "filled, " : ""}properties changed: ${properties.join(", ")}` : `field "${field.name}" filled`);
1926
- }
1927
- }
1928
- for (const wc of widgetChanges) {
1929
- if (ctx.isIdentical(wc)) continue;
1930
- const bad = [...changedKeys(wc.value.old, wc.value.new)].filter((k) => !widgetKeys.has(k));
1931
- if (bad.length > 0) {
1932
- ctx.forbidden(wc, rule, `widget of "${field.name}" keys changed: ${bad.join(", ")}`);
1933
- ctx.claimStable(wc, rule, false);
1934
- ok = false;
1935
- continue;
1936
- }
1937
- ctx.claimStable(wc, rule);
1938
- ctx.permitted(wc, rule, `widget of "${field.name}" appearance`);
1939
- }
1940
- if (ok) {
1941
- parents.add(num);
1942
- for (const w of field.widgets) parents.add(w);
1943
- }
1944
- }
1945
- for (const side of SIDES) ctx.claimSubtree(side, parents, (label) => label === "AP" || label.startsWith("AP/"), rule);
1946
- }
1947
- function ruleAnnotation(ctx) {
1948
- const rule = "annotation";
1949
- const pages = /* @__PURE__ */ new Set([...ctx.before.pages, ...ctx.after.pages]);
1950
- const widgets = /* @__PURE__ */ new Set([...ctx.before.widgetToField.keys(), ...ctx.after.widgetToField.keys()]);
1951
- const isWidget = (c) => {
1952
- if (widgets.has(c.objectNumber)) return true;
1953
- const sub = dictEntries(c.value.new ?? c.value.old)?.Subtype;
1954
- return sub?.t === "name" && sub.v === "Widget";
1955
- };
1956
- const annots = [];
1957
- for (const c of ctx.changes) {
1958
- if (c.kind !== "dictionary" || ctx.isIdentical(c)) continue;
1959
- if ([...c.usage.old, ...c.usage.new].some((e) => pages.has(e.parent) && e.label.startsWith("Annots/")) && !isWidget(c)) annots.push(c);
1960
- }
1961
- if (annots.length === 0) {}
1962
- if (!ctx.allows("annotate")) {
1963
- for (const c of annots) ctx.forbidden(c, rule, `annotation change under level '${ctx.level}'`);
1964
- return;
1965
- }
1966
- const parents = /* @__PURE__ */ new Set();
1967
- for (const c of annots) {
1968
- ctx.claimAll(c, rule);
1969
- ctx.permitted(c, rule, `${c.change} annotation`);
1970
- parents.add(c.objectNumber);
1971
- }
1972
- for (const p of pages) {
1973
- const c = ctx.byNum.get(p);
1974
- if (!c || ctx.isIdentical(c)) continue;
1975
- const changed = changedKeys(c.value.old, c.value.new);
1976
- if (changed.size === 1 && changed.has("Annots")) {
1977
- const removed = refsOf(dictEntries(c.value.old)?.Annots).filter((n) => !refsOf(dictEntries(c.value.new)?.Annots).includes(n));
1978
- const added = refsOf(dictEntries(c.value.new)?.Annots).filter((n) => !refsOf(dictEntries(c.value.old)?.Annots).includes(n));
1979
- if ([...removed, ...added].some((n) => widgets.has(n) && !ctx.findings.some((f) => f.objectNumber === n && f.verdict === "permitted"))) {
1980
- ctx.forbidden(c, rule, "page /Annots added or removed a form widget");
1981
- continue;
1982
- }
1983
- ctx.claimAll(c, rule);
1984
- ctx.permitted(c, rule, "page /Annots changed");
1985
- }
1986
- }
1987
- for (const side of SIDES) ctx.claimSubtree(side, parents, (label) => label === "AP" || label.startsWith("AP/") || label === "Popup" || label === "IRT", rule);
1988
- }
1989
- function ruleFieldLock(ctx) {
1990
- if (ctx.locks.length === 0) return;
1991
- const lockedFields = ctx.input.before.fields.filter((f) => ctx.locks.some((l) => require_resolver.lockCovers(l.spec, f.name)));
1992
- const lockedObjects = /* @__PURE__ */ new Set();
1993
- for (const f of lockedFields) {
1994
- lockedObjects.add(f.objectNumber);
1995
- for (const w of f.widgets) lockedObjects.add(w);
1996
- }
1997
- const frontier = new Set(lockedObjects);
1998
- let grew = true;
1999
- while (grew) {
2000
- grew = false;
2001
- for (const c of ctx.changes) {
2002
- if (frontier.has(c.objectNumber)) continue;
2003
- if (c.usage.old.length > 0 && c.usage.old.every((e) => frontier.has(e.parent))) {
2004
- frontier.add(c.objectNumber);
2005
- grew = true;
2006
- }
2007
- }
2008
- }
2009
- for (const c of ctx.changes) {
2010
- if (!frontier.has(c.objectNumber) || ctx.isIdentical(c)) continue;
2011
- const field = lockedFields.find((f) => f.objectNumber === c.objectNumber || f.widgets.includes(c.objectNumber));
2012
- if (field && field.objectNumber === c.objectNumber) {
2013
- const changed = changedKeys(c.value.old, c.value.new);
2014
- if (changed.size === 1 && changed.has("Ff") && readOnlyOnlyChange(c, ctx.before.fieldByObj.get(c.objectNumber)?.flags)) {
2015
- ctx.claimStable(c, "field-lock");
2016
- ctx.permitted(c, "field-lock", `ReadOnly set on locked field "${field.name}"`);
2017
- continue;
2018
- }
2019
- }
2020
- ctx.locked.add(c.objectNumber);
2021
- ctx.forbidden(c, "field-lock", field ? `field "${field.name}" is locked by the signature` : "part of a locked field changed");
2022
- }
2023
- }
2024
- function ruleUnexplained(ctx) {
2025
- for (const c of ctx.changes) {
2026
- if (c.value.truncated) {
2027
- ctx.findings.push({
2028
- rule: "unexplained",
2029
- verdict: "incomplete",
2030
- objectNumber: c.objectNumber,
2031
- detail: "value too large to inspect"
2032
- });
2033
- continue;
2034
- }
2035
- if (c.usageIncomplete) {
2036
- ctx.findings.push({
2037
- rule: "unexplained",
2038
- verdict: "incomplete",
2039
- objectNumber: c.objectNumber,
2040
- detail: "references could not be resolved within budget (depth, fan-out or total reads)"
2041
- });
2042
- continue;
2043
- }
2044
- if (ctx.locked.has(c.objectNumber)) continue;
2045
- for (const side of SIDES) for (const e of c.usage[side]) if (!ctx.isClaimed(c, side, e)) ctx.forbidden(c, "unexplained", `${side === "old" ? "was" : "is"} referenced from object ${e.parent} at ${e.label} and no rule explains it`, e);
2046
- if (c.usage.old.length === 0 && c.usage.new.length === 0 && !ctx.findings.some((f) => f.objectNumber === c.objectNumber)) ctx.permitted(c, "orphan", "referenced from nowhere reachable");
2047
- }
2048
- }
2049
- //#endregion
2050
- //#region src/signature/analysis/restrictions.ts
2051
- /**
2052
- * The restrictions a window is judged under, read from the SEALED
2053
- * revision's structure (what that revision declared, not what the final
2054
- * file says).
2055
- *
2056
- * - `judged` given (a signature-anchored analysis): its own FieldMDP and
2057
- * /Lock bind the window; other signatures' locks do not (corpus v3/85:
2058
- * a second signature is valid with a fill of a field the first locked).
2059
- * Every certification's level, own or earlier, applies (inherited:
2060
- * conservative until observed otherwise), as does a /Lock's /P.
2061
- * - no `judged` (a revision-anchored analysis): every signed signature's
2062
- * locks and levels, as before.
2063
- */
2064
- function restrictionsFor(before, judged) {
2065
- const anchors = [];
2066
- const locks = [];
2067
- let enforced = null;
2068
- let signed = false;
2069
- for (const sig of before.signatures) {
2070
- if (!sig.signed || sig.revisionIndex === null) continue;
2071
- signed = true;
2072
- const own = judged !== null && sig.index === judged.index;
2073
- if (sig.catalogCertification && sig.docMdp !== null) {
2074
- anchors.push({
2075
- signatureIndex: sig.index,
2076
- revisionIndex: sig.revisionIndex,
2077
- source: "docmdp",
2078
- own,
2079
- permission: sig.docMdp
2080
- });
2081
- enforced = require_resolver.minLevel(enforced ?? "annotate", require_resolver.levelFromPermission(sig.docMdp));
2082
- }
2083
- if (sig.lock?.permission !== void 0) enforced = require_resolver.minLevel(enforced ?? "annotate", require_resolver.levelFromPermission(sig.lock.permission));
2084
- if (judged !== null && !own) continue;
2085
- if (sig.fieldMdp) {
2086
- anchors.push({
2087
- signatureIndex: sig.index,
2088
- revisionIndex: sig.revisionIndex,
2089
- source: "fieldmdp",
2090
- own,
2091
- fields: sig.fieldMdp
2092
- });
2093
- locks.push({
2094
- signatureIndex: sig.index,
2095
- source: "fieldmdp",
2096
- spec: sig.fieldMdp
2097
- });
2098
- }
2099
- if (sig.lock) {
2100
- anchors.push({
2101
- signatureIndex: sig.index,
2102
- revisionIndex: sig.revisionIndex,
2103
- source: "lock",
2104
- own,
2105
- fields: sig.lock
2106
- });
2107
- locks.push({
2108
- signatureIndex: sig.index,
2109
- source: "lock",
2110
- spec: sig.lock
2111
- });
2112
- }
2113
- }
2114
- return {
2115
- anchors,
2116
- level: signed ? enforced ?? "annotate" : "annotate",
2117
- locks
2118
- };
2119
- }
2120
- //#endregion
2121
- //#region src/signature/analysis/resolve.ts
2122
- const DEFAULT_EDGE_RESOLVER_BUDGET = {
2123
- maxDepth: 16,
2124
- maxEdgesPerObject: 256,
2125
- maxReads: 2e4
2126
- };
2127
- const USAGE_INCOMPLETE = "incomplete";
2128
- /**
2129
- * Resolves the raw inbound edges of an object (from the fork's referrer
2130
- * index, which covers every reachable object of a revision) into edges
2131
- * from *anchors*: the changed objects and the structural objects of the
2132
- * revision (trailer, catalog, AcroForm, page tree root, pages, fields,
2133
- * widgets). An edge from an unchanged, non-structural parent is replaced by
2134
- * that parent's own anchored edges with the labels joined, so an
2135
- * appearance stream under an unchanged indirect `/AP` dictionary reads
2136
- * `widget:AP/N` rather than `apDict:N`.
2137
- *
2138
- * A graph traversal, not a path enumeration: results are memoised per
2139
- * object, so a lattice of shared unchanged objects costs its size, not the
2140
- * number of paths through it. Cycle edges contribute no anchors and are
2141
- * skipped; a result computed while skipping one is not memoised.
2142
- */
2143
- var EdgeResolver = class {
2144
- constructor(read, anchors, budget = DEFAULT_EDGE_RESOLVER_BUDGET) {
2145
- this.read = read;
2146
- this.anchors = anchors;
2147
- this.budget = budget;
2148
- require_fold._defineProperty(this, "memo", /* @__PURE__ */ new Map());
2149
- require_fold._defineProperty(this, "reads", 0);
2150
- require_fold._defineProperty(this, "exhausted", false);
2151
- }
2152
- /** Anchored edges of `objectNumber`, or `incomplete` when a budget was exceeded. */
2153
- resolve(objectNumber) {
2154
- if (this.exhausted) return USAGE_INCOMPLETE;
2155
- const result = this.resolveInner(objectNumber, /* @__PURE__ */ new Set());
2156
- return result === null ? USAGE_INCOMPLETE : result.edges;
2157
- }
2158
- resolveInner(n, active) {
2159
- const hit = this.memo.get(n);
2160
- if (hit) return hit;
2161
- if (active.size >= this.budget.maxDepth) return null;
2162
- if (this.reads >= this.budget.maxReads) {
2163
- this.exhausted = true;
2164
- return null;
2165
- }
2166
- this.reads += 1;
2167
- const raw = this.read(n);
2168
- active.add(n);
2169
- const edges = [];
2170
- let cyclic = false;
2171
- try {
2172
- for (const e of raw) {
2173
- if (this.anchors.has(e.parent)) edges.push(e);
2174
- else if (active.has(e.parent)) {
2175
- cyclic = true;
2176
- continue;
2177
- } else {
2178
- const up = this.resolveInner(e.parent, active);
2179
- if (up === null) return null;
2180
- cyclic || (cyclic = up.cyclic);
2181
- for (const u of up.edges) edges.push({
2182
- parent: u.parent,
2183
- label: `${u.label}/${e.label}`,
2184
- via: [...u.via ?? [], e.parent]
2185
- });
2186
- }
2187
- if (edges.length > this.budget.maxEdgesPerObject) return null;
2188
- }
2189
- } finally {
2190
- active.delete(n);
2191
- }
2192
- const resolution = {
2193
- edges,
2194
- cyclic
2195
- };
2196
- if (!cyclic) this.memo.set(n, resolution);
2197
- return resolution;
2198
- }
2199
- };
2200
- //#endregion
2201
- //#region src/search/literal.ts
2202
- /**
2203
- * The fold flavor a literal query needs. Haystack and needle MUST be
2204
- * folded with the same options; corpus caches key their pre-folded text
2205
- * on this (the default `{}` fold is the persistable one — non-default
2206
- * queries fold the original page text at query time).
2207
- */
2208
- function foldOptionsFor(query) {
2209
- return {
2210
- keepCase: !!query.matchCase,
2211
- keepMarks: !!query.matchDiacritics
2212
- };
2213
- }
2214
- const WORD_UNIT = /[\p{L}\p{N}_]/u;
2215
- const MARK = /\p{M}/u;
2216
- /** Whether the code point ENDING at `index` (exclusive) is a word char. */
2217
- function wordBefore(text, index) {
2218
- if (index <= 0) return false;
2219
- const unit = text.charCodeAt(index - 1);
2220
- const start = unit >= 56320 && unit <= 57343 && index >= 2 ? index - 2 : index - 1;
2221
- return WORD_UNIT.test(String.fromCodePoint(text.codePointAt(start)));
2222
- }
2223
- /** Whether the code point starting at `index` is a word char. */
2224
- function wordAt(text, index) {
2225
- if (index >= text.length) return false;
2226
- return WORD_UNIT.test(String.fromCodePoint(text.codePointAt(index)));
2227
- }
2228
- /**
2229
- * All non-overlapping literal matches, in original code-unit space.
2230
- * `haystack` must have been folded with `foldOptionsFor(query)`.
2231
- */
2232
- function matchLiteral(haystack, query) {
2233
- const needle = require_fold.foldText(query.text, foldOptionsFor(query)).folded;
2234
- if (needle.trim().length === 0) return [];
2235
- const out = [];
2236
- let from = 0;
2237
- while (from <= haystack.folded.length - needle.length) {
2238
- const at = haystack.folded.indexOf(needle, from);
2239
- if (at < 0) break;
2240
- if (query.wholeWord && (wordBefore(haystack.folded, at) || wordAt(haystack.folded, at + needle.length))) {
2241
- from = at + 1;
2242
- continue;
2243
- }
2244
- if (query.matchDiacritics && at + needle.length < haystack.folded.length) {
2245
- const after = haystack.folded.codePointAt(at + needle.length);
2246
- if (MARK.test(String.fromCodePoint(after))) {
2247
- from = at + 1;
2248
- continue;
2249
- }
2250
- }
2251
- out.push(require_fold.toOriginalRange(haystack, at, needle.length));
2252
- from = at + needle.length;
2253
- }
2254
- return out;
2255
- }
2256
- //#endregion
2257
- //#region src/search/regex.ts
2258
- /**
2259
- * The portable search-regex dialect: JavaScript `u`-mode syntax MINUS the
2260
- * features RE2 refuses (backreferences, lookaround). The local engine
2261
- * executes patterns with JS `RegExp`; the server executes the SAME
2262
- * pattern with RE2, whose linear-time guarantee is the ReDoS defense.
2263
- * Restricting both sides to the common subset keeps one query string
2264
- * valid — and equally powerful — everywhere.
2265
- */
2266
- const SEARCH_REGEX_MAX_LENGTH = 512;
2267
- /**
2268
- * Validate a pattern against the dialect. Cheap and dependency-free —
2269
- * viewers call it on keystroke for early feedback; engines re-validate
2270
- * and reject bad patterns with `InvalidArg`.
2271
- */
2272
- function validateSearchRegex(pattern) {
2273
- if (pattern.length === 0) return {
2274
- ok: false,
2275
- issue: "empty",
2276
- message: "Pattern is empty."
2277
- };
2278
- if (pattern.length > 512) return {
2279
- ok: false,
2280
- issue: "too-long",
2281
- message: `Pattern exceeds 512 characters.`
2282
- };
2283
- let inClass = false;
2284
- for (let i = 0; i < pattern.length; i++) {
2285
- const ch = pattern[i];
2286
- if (ch === "\\") {
2287
- const next = pattern[i + 1] ?? "";
2288
- if (!inClass && next >= "1" && next <= "9") return {
2289
- ok: false,
2290
- issue: "backreference",
2291
- message: `Backreference \\${next} is not supported.`
2292
- };
2293
- if (!inClass && next === "k" && pattern[i + 2] === "<") return {
2294
- ok: false,
2295
- issue: "backreference",
2296
- message: "Named backreferences (\\k<…>) are not supported."
2297
- };
2298
- i++;
2299
- continue;
2300
- }
2301
- if (inClass) {
2302
- if (ch === "]") inClass = false;
2303
- continue;
2304
- }
2305
- if (ch === "[") {
2306
- inClass = true;
2307
- continue;
2308
- }
2309
- if (ch === "(" && pattern[i + 1] === "?") {
2310
- const tag = pattern[i + 2] ?? "";
2311
- if (tag === "=" || tag === "!") return {
2312
- ok: false,
2313
- issue: "lookaround",
2314
- message: "Lookahead ((?=…), (?!…)) is not supported."
2315
- };
2316
- if (tag === "<" && (pattern[i + 3] === "=" || pattern[i + 3] === "!")) return {
2317
- ok: false,
2318
- issue: "lookaround",
2319
- message: "Lookbehind ((?<=…), (?<!…)) is not supported."
2320
- };
2321
- }
2322
- }
2323
- try {
2324
- new RegExp(pattern, "mu");
2325
- } catch (error) {
2326
- return {
2327
- ok: false,
2328
- issue: "syntax",
2329
- message: error.message
2330
- };
2331
- }
2332
- return { ok: true };
2333
- }
2334
- function validateSearchQuery(query) {
2335
- if (!query.regex) return { ok: true };
2336
- if (query.matchDiacritics) return {
2337
- ok: false,
2338
- issue: "diacritics-with-regex",
2339
- message: "Diacritic-sensitive matching is not available for regex patterns (regex runs on the raw text plane)."
2340
- };
2341
- return validateSearchRegex(query.text);
2342
- }
2343
- /**
2344
- * All matches of a dialect-valid pattern over the RAW page text (regex
2345
- * does not run against folded text — case handling is the `i` flag, and
2346
- * diacritic folding never applies). `wholeWord` is a boundary POST-FILTER
2347
- * using the same Unicode word test as the literal path — never `\b`,
2348
- * whose ASCII word set would give the toggle different semantics in regex
2349
- * mode ("caf" whole-word matching inside "café"). Filtering after the
2350
- * match also survives the move to RE2 server-side. Zero-length matches
2351
- * are skipped: search UI cannot highlight nothing, and skipping them is
2352
- * also the infinite-loop guard for patterns like `a*`.
2353
- */
2354
- function matchRegex(text, query) {
2355
- const valid = validateSearchQuery({
2356
- ...query,
2357
- regex: true
2358
- });
2359
- if (!valid.ok) throw new Error(`Invalid search query (${valid.issue}): ${valid.message}`);
2360
- const re = new RegExp(query.text, query.matchCase ? "gmu" : "gimu");
2361
- const out = [];
2362
- let m;
2363
- while ((m = re.exec(text)) !== null) {
2364
- if (m[0].length === 0) {
2365
- const unit = text.charCodeAt(re.lastIndex);
2366
- re.lastIndex += unit >= 55296 && unit <= 56319 ? 2 : 1;
2367
- continue;
2368
- }
2369
- if (query.wholeWord && (wordBefore(text, m.index) || wordAt(text, m.index + m[0].length))) continue;
2370
- out.push({
2371
- start: m.index,
2372
- length: m[0].length
2373
- });
2374
- }
2375
- return out;
2376
- }
2377
- //#endregion
2378
- //#region src/search/matcher.ts
2379
- /**
2380
- * The whole match stage in one pure call: page text in, original-space
2381
- * match ranges out. Engines with a pre-folded corpus for the default fold
2382
- * skip this and call `matchLiteral` on the cached `FoldedText` directly;
2383
- * everything else (non-default fold options, regex, tests) comes through
2384
- * here.
2385
- */
2386
- function matchPageText(text, query) {
2387
- if (query.regex) return matchRegex(text, query);
2388
- return matchLiteral(require_fold.foldText(text, foldOptionsFor(query)), query);
2389
- }
2390
- //#endregion
2391
- //#region src/search/snippet.ts
2392
- /** Default context on each side of a match, in code units. */
2393
- const SEARCH_SNIPPET_CONTEXT = 48;
2394
- const WHITESPACE = /\s/;
2395
- const WHITESPACE_ALL = /\s/g;
2396
- const isLowSurrogate = (text, index) => {
2397
- const unit = text.charCodeAt(index);
2398
- return unit >= 56320 && unit <= 57343;
2399
- };
2400
- /**
2401
- * The `'full'`-mode excerpt around one match: up to `context` code units
2402
- * of page text on each side, trimmed to a whitespace boundary when one
2403
- * exists inside the window (so snippets start and end on whole words),
2404
- * never splitting a surrogate pair, whitespace flattened 1:1.
2405
- */
2406
- function buildSnippet(text, range, context = 48) {
2407
- const matchEnd = range.start + range.length;
2408
- let start = Math.max(0, range.start - context);
2409
- if (start > 0) {
2410
- for (let i = start; i < range.start; i++) if (WHITESPACE.test(text[i])) {
2411
- start = i + 1;
2412
- break;
2413
- }
2414
- if (isLowSurrogate(text, start)) start++;
2415
- }
2416
- let end = Math.min(text.length, matchEnd + context);
2417
- if (end < text.length) {
2418
- for (let i = end - 1; i >= matchEnd; i--) if (WHITESPACE.test(text[i])) {
2419
- end = i;
2420
- break;
2421
- }
2422
- if (isLowSurrogate(text, end)) end--;
2423
- }
2424
- return {
2425
- text: text.slice(start, end).replace(WHITESPACE_ALL, " "),
2426
- matchStart: range.start - start,
2427
- matchLength: range.length
2428
- };
2429
- }
2430
- //#endregion
2431
- //#region src/text/layout.ts
2432
- const FLAG_EMPTY = 2;
2433
- const isBoundary = (flags) => (flags & 3) !== 0;
2434
- const isEmpty = (flags) => (flags & FLAG_EMPTY) !== 0;
2435
- const CHAR_DISTANCE_FACTOR = 2.5;
2436
- const FONT_SIZE_RATIO_THRESHOLD = 1.5;
2437
- const VERTICAL_OVERLAP_THRESHOLD = .8;
2438
- const LINE_OVERLAP_THRESHOLD = .5;
2439
- /** Baseline directions within ~0.5° share a frame. */
2440
- const FRAME_DOT_TOLERANCE = Math.cos(.0087);
2441
- const EDGE_EPSILON = 1e-6;
2442
- const IDENTITY_FRAME = {
2443
- baseline: {
2444
- x: 1,
2445
- y: 0
2446
- },
2447
- ascent: {
2448
- x: 0,
2449
- y: 1
2450
- }
2451
- };
2452
- const toFrame = (f, p) => ({
2453
- x: p.x * f.baseline.x + p.y * f.baseline.y,
2454
- y: p.x * f.ascent.x + p.y * f.ascent.y
2455
- });
2456
- const fromFrame = (f, p) => ({
2457
- x: f.baseline.x * p.x + f.ascent.x * p.y,
2458
- y: f.baseline.y * p.x + f.ascent.y * p.y
2459
- });
2460
- const ZERO_RECT = {
2461
- left: 0,
2462
- bottom: 0,
2463
- right: 0,
2464
- top: 0
2465
- };
2466
- /** Frame-local AABB of a page-space cell (exact under the frame's rotation;
2467
- * shear residue is the documented in-frame envelope). */
2468
- function frameBoxOfQuad(f, q) {
2469
- const pts = [
2470
- q.p1,
2471
- q.p2,
2472
- q.p3,
2473
- q.p4
2474
- ].map((p) => toFrame(f, p));
2475
- const xs = pts.map((p) => p.x);
2476
- const ys = pts.map((p) => p.y);
2477
- return {
2478
- left: Math.min(...xs),
2479
- bottom: Math.min(...ys),
2480
- right: Math.max(...xs),
2481
- top: Math.max(...ys)
2482
- };
2483
- }
2484
- /** Find-or-create the frame for a rotated run, keyed by the baseline
2485
- * direction + ascent handedness of its first classifiable glyph's edges. */
2486
- function frameForRun(frames, run) {
2487
- const seed = run.glyphs.find((g) => !isEmpty(g.flags));
2488
- if (!seed) return 0;
2489
- const q = seed.looseQuad;
2490
- const bx = q.p2.x - q.p1.x;
2491
- const by = q.p2.y - q.p1.y;
2492
- const len = Math.hypot(bx, by);
2493
- if (len <= EDGE_EPSILON) return 0;
2494
- const baseline = {
2495
- x: bx / len,
2496
- y: by / len
2497
- };
2498
- const ax = q.p1.x - q.p3.x;
2499
- const ay = q.p1.y - q.p3.y;
2500
- const sign = baseline.x * ay - baseline.y * ax >= 0 ? 1 : -1;
2501
- const ascent = {
2502
- x: -baseline.y * sign,
2503
- y: baseline.x * sign
2504
- };
2505
- for (let i = 0; i < frames.length; i++) {
2506
- const f = frames[i];
2507
- const dot = f.baseline.x * baseline.x + f.baseline.y * baseline.y;
2508
- const sameHandedness = f.ascent.x * ascent.x + f.ascent.y * ascent.y > 0;
2509
- if (dot >= FRAME_DOT_TOLERANCE && sameHandedness) return i;
2510
- }
2511
- frames.push({
2512
- baseline,
2513
- ascent
2514
- });
2515
- return frames.length - 1;
2516
- }
2517
- /**
2518
- * Flatten a page's geometry snapshot into the canonical layout. Upright runs
2519
- * copy their wire boxes verbatim (frame 0 — byte-identical to the
2520
- * pre-orientation behavior, wire run rects included); rotated runs project
2521
- * their glyph cells into their cluster's frame.
2522
- */
2523
- function buildPageTextLayout(snapshot) {
2524
- const glyphs = [];
2525
- const runs = [];
2526
- const frames = [IDENTITY_FRAME];
2527
- for (const run of snapshot.runs) {
2528
- if (!require_fold.isRotatedGeometryRun(run)) {
2529
- for (const g of run.glyphs) glyphs.push({
2530
- loose: g.looseBox,
2531
- tight: g.tightBox,
2532
- flags: g.flags
2533
- });
2534
- runs.push({
2535
- charStart: run.charStart,
2536
- count: run.glyphs.length,
2537
- rect: run.rect,
2538
- fontSize: run.fontSize,
2539
- frame: 0
2540
- });
2541
- continue;
2542
- }
2543
- const frame = frameForRun(frames, run);
2544
- const f = frames[frame];
2545
- let runBox = null;
2546
- for (const g of run.glyphs) {
2547
- if (isEmpty(g.flags)) {
2548
- glyphs.push({
2549
- loose: ZERO_RECT,
2550
- flags: g.flags
2551
- });
2552
- continue;
2553
- }
2554
- const loose = frameBoxOfQuad(f, g.looseQuad);
2555
- const tight = g.tightQuad ? frameBoxOfQuad(f, g.tightQuad) : void 0;
2556
- glyphs.push({
2557
- loose,
2558
- tight,
2559
- flags: g.flags
2560
- });
2561
- runBox = runBox ? {
2562
- left: Math.min(runBox.left, loose.left),
2563
- bottom: Math.min(runBox.bottom, loose.bottom),
2564
- right: Math.max(runBox.right, loose.right),
2565
- top: Math.max(runBox.top, loose.top)
2566
- } : loose;
2567
- }
2568
- runs.push({
2569
- charStart: run.charStart,
2570
- count: run.glyphs.length,
2571
- rect: runBox ?? ZERO_RECT,
2572
- fontSize: run.fontSize,
2573
- frame
2574
- });
2575
- }
2576
- return {
2577
- glyphs,
2578
- runs,
2579
- frames
2580
- };
2581
- }
2582
- const inRect = (r, p) => p.x >= r.left && p.x <= r.right && p.y >= r.bottom && p.y <= r.top;
2583
- function avgGlyphHeight(layout) {
2584
- let total = 0;
2585
- let count = 0;
2586
- for (const g of layout.glyphs) {
2587
- if (isEmpty(g.flags)) continue;
2588
- total += g.loose.top - g.loose.bottom;
2589
- count++;
2590
- }
2591
- return count === 0 ? 0 : total / count;
2592
- }
2593
- /** The point expressed in every frame (index-aligned with `layout.frames`).
2594
- * Frame 0 returns the point itself — no float round-trip on the fast path. */
2595
- function pointPerFrame(layout, p) {
2596
- return layout.frames.map((f, i) => i === 0 ? p : toFrame(f, p));
2597
- }
2598
- /**
2599
- * The glyph (char) index at a page-space point, or null when nothing is near
2600
- * (so callers can show the pointer cursor off-text). PDFium `GetIndexAtPos`:
2601
- * exact tight-box containment first, then a tolerance pass (closest by
2602
- * Manhattan distance within `toleranceFactor × average glyph height`). Each
2603
- * run tests the point in ITS OWN frame, so rotated text hit-tests exactly.
2604
- */
2605
- function textGlyphAt(layout, point, toleranceFactor = 1.5) {
2606
- const pts = pointPerFrame(layout, point);
2607
- for (const run of layout.runs) {
2608
- const q = pts[run.frame];
2609
- if (!inRect(run.rect, q)) continue;
2610
- for (let i = 0; i < run.count; i++) {
2611
- const g = layout.glyphs[run.charStart + i];
2612
- if (inRect(g.tight ?? g.loose, q)) return run.charStart + i;
2613
- }
2614
- }
2615
- if (toleranceFactor <= 0) return null;
2616
- const half = avgGlyphHeight(layout) * toleranceFactor / 2;
2617
- let best = -1;
2618
- let bestDist = Infinity;
2619
- for (const run of layout.runs) {
2620
- const q = pts[run.frame];
2621
- const r = run.rect;
2622
- if (q.x < r.left - half || q.x > r.right + half || q.y < r.bottom - half || q.y > r.top + half) continue;
2623
- for (let i = 0; i < run.count; i++) {
2624
- const g = layout.glyphs[run.charStart + i];
2625
- if (isEmpty(g.flags)) continue;
2626
- const b = g.tight ?? g.loose;
2627
- if (q.x < b.left - half || q.x > b.right + half || q.y < b.bottom - half || q.y > b.top + half) continue;
2628
- const dx = Math.min(Math.abs(q.x - b.left), Math.abs(q.x - b.right));
2629
- const dy = Math.min(Math.abs(q.y - b.bottom), Math.abs(q.y - b.top));
2630
- if (dx + dy < bestDist) {
2631
- bestDist = dx + dy;
2632
- best = run.charStart + i;
2633
- }
2634
- }
2635
- }
2636
- return best >= 0 ? best : null;
2637
- }
2638
- /** Double-click: the word around `glyph` (walk to space/empty glyphs both ways). */
2639
- function expandTextRangeToWord(layout, glyph) {
2640
- const n = layout.glyphs.length;
2641
- if (glyph < 0 || glyph >= n) return [glyph, glyph];
2642
- let from = glyph;
2643
- while (from > 0 && !isBoundary(layout.glyphs[from - 1].flags)) from--;
2644
- let to = glyph;
2645
- while (to < n - 1 && !isBoundary(layout.glyphs[to + 1].flags)) to++;
2646
- return [from, to];
2647
- }
2648
- /** Triple-click: the full visual line — SAME-FRAME runs whose vertical extent
2649
- * overlaps the anchor run's. A differently-oriented run is a line boundary. */
2650
- function expandTextRangeToLine(layout, glyph) {
2651
- const ri = layout.runs.findIndex((r) => glyph >= r.charStart && glyph < r.charStart + r.count);
2652
- if (ri < 0) return [glyph, glyph];
2653
- const anchor = layout.runs[ri];
2654
- const bottom = anchor.rect.bottom;
2655
- const top = anchor.rect.top;
2656
- let from = anchor.charStart;
2657
- let to = anchor.charStart + anchor.count - 1;
2658
- for (let r = ri - 1; r >= 0; r--) {
2659
- const run = layout.runs[r];
2660
- if (isZero(run.rect)) continue;
2661
- if (run.frame !== anchor.frame) break;
2662
- if (!overlapV(run.rect.bottom, run.rect.top, bottom, top)) break;
2663
- from = run.charStart;
2664
- }
2665
- for (let r = ri + 1; r < layout.runs.length; r++) {
2666
- const run = layout.runs[r];
2667
- if (isZero(run.rect)) continue;
2668
- if (run.frame !== anchor.frame) break;
2669
- if (!overlapV(run.rect.bottom, run.rect.top, bottom, top)) break;
2670
- to = run.charStart + run.count - 1;
2671
- }
2672
- return [from, to];
2673
- }
2674
- /**
2675
- * A single glyph's oriented cell in page space, or null for degenerate
2676
- * glyphs — the anchor for caret placement at a selection boundary.
2677
- */
2678
- function textGlyphQuad(layout, glyph) {
2679
- const g = layout.glyphs[glyph];
2680
- if (!g || isEmpty(g.flags)) return null;
2681
- const b = g.loose;
2682
- if (b.right <= b.left || b.top <= b.bottom) return null;
2683
- const run = layout.runs.find((r) => glyph >= r.charStart && glyph < r.charStart + r.count);
2684
- const f = layout.frames[run?.frame ?? 0];
2685
- return quadOfFrameRect(f, run?.frame ?? 0, b);
2686
- }
2687
- /**
2688
- * Merged visual-line segments for the half-open char range
2689
- * `[charStart, charStart + charCount)` — THE canonical segmentation. Chars
2690
- * outside the layout are ignored.
2691
- */
2692
- function textSegmentsForRange(layout, charStart, charCount) {
2693
- if (charCount <= 0 || layout.glyphs.length === 0) return [];
2694
- const lo = Math.max(0, charStart);
2695
- const hi = Math.min(layout.glyphs.length - 1, charStart + charCount - 1);
2696
- if (hi < lo) return [];
2697
- const subRuns = [];
2698
- for (const run of layout.runs) {
2699
- const runEnd = run.charStart + run.count - 1;
2700
- if (runEnd < lo || run.charStart > hi) continue;
2701
- const s = Math.max(lo, run.charStart);
2702
- const e = Math.min(hi, runEnd);
2703
- let left = Infinity;
2704
- let right = -Infinity;
2705
- let bottom = Infinity;
2706
- let top = -Infinity;
2707
- let charCountAcc = 0;
2708
- let widthSum = 0;
2709
- let prevRight = -Infinity;
2710
- let firstX = 0;
2711
- let lastX = 0;
2712
- const flush = () => {
2713
- if (charCountAcc > 0 && left !== Infinity) subRuns.push({
2714
- rect: {
2715
- left,
2716
- bottom,
2717
- right,
2718
- top
2719
- },
2720
- charCount: charCountAcc,
2721
- fontSize: run.fontSize,
2722
- frame: run.frame,
2723
- firstX,
2724
- lastX
2725
- });
2726
- left = Infinity;
2727
- right = -Infinity;
2728
- bottom = Infinity;
2729
- top = -Infinity;
2730
- charCountAcc = 0;
2731
- widthSum = 0;
2732
- prevRight = -Infinity;
2733
- };
2734
- for (let ci = s; ci <= e; ci++) {
2735
- const g = layout.glyphs[ci];
2736
- if (isEmpty(g.flags)) continue;
2737
- const b = g.loose;
2738
- if (charCountAcc > 0 && prevRight > -Infinity) {
2739
- const avg = widthSum / charCountAcc;
2740
- if (avg > 0 && Math.abs(b.left - prevRight) > CHAR_DISTANCE_FACTOR * avg) flush();
2741
- }
2742
- const centerX = (b.left + b.right) / 2;
2743
- if (charCountAcc === 0) firstX = centerX;
2744
- lastX = centerX;
2745
- left = Math.min(left, b.left);
2746
- right = Math.max(right, b.right);
2747
- bottom = Math.min(bottom, b.bottom);
2748
- top = Math.max(top, b.top);
2749
- charCountAcc++;
2750
- widthSum += b.right - b.left;
2751
- prevRight = b.right;
2752
- }
2753
- flush();
2754
- }
2755
- return mergeAdjacentSubRuns(subRuns).map((m) => materializeSegment(layout, m));
2756
- }
2757
- function mergeAdjacentSubRuns(runs) {
2758
- const out = [];
2759
- let prev = null;
2760
- let cur = null;
2761
- for (const run of runs) {
2762
- if (prev && cur && prev.frame === run.frame && shouldMerge(prev, run)) cur = {
2763
- rect: {
2764
- left: Math.min(cur.rect.left, run.rect.left),
2765
- bottom: Math.min(cur.rect.bottom, run.rect.bottom),
2766
- right: Math.max(cur.rect.right, run.rect.right),
2767
- top: Math.max(cur.rect.top, run.rect.top)
2768
- },
2769
- frame: cur.frame,
2770
- firstX: cur.firstX,
2771
- lastX: run.lastX
2772
- };
2773
- else {
2774
- if (cur) out.push(cur);
2775
- cur = {
2776
- rect: run.rect,
2777
- frame: run.frame,
2778
- firstX: run.firstX,
2779
- lastX: run.lastX
2780
- };
2781
- }
2782
- prev = run;
2783
- }
2784
- if (cur && cur.rect.right > cur.rect.left && cur.rect.top > cur.rect.bottom) out.push(cur);
2785
- return out;
2786
- }
2787
- /** Segment quad for a frame-local rect (frame 0: the rect's own corners,
2788
- * no float round-trip). Slot order US, UE, LS, LE; frame is y-up, so the
2789
- * upper corners sit at the frame TOP. */
2790
- function quadOfFrameRect(f, frameIndex, r) {
2791
- if (frameIndex === 0) return {
2792
- p1: {
2793
- x: r.left,
2794
- y: r.top
2795
- },
2796
- p2: {
2797
- x: r.right,
2798
- y: r.top
2799
- },
2800
- p3: {
2801
- x: r.left,
2802
- y: r.bottom
2803
- },
2804
- p4: {
2805
- x: r.right,
2806
- y: r.bottom
2807
- }
2808
- };
2809
- return {
2810
- p1: fromFrame(f, {
2811
- x: r.left,
2812
- y: r.top
2813
- }),
2814
- p2: fromFrame(f, {
2815
- x: r.right,
2816
- y: r.top
2817
- }),
2818
- p3: fromFrame(f, {
2819
- x: r.left,
2820
- y: r.bottom
2821
- }),
2822
- p4: fromFrame(f, {
2823
- x: r.right,
2824
- y: r.bottom
2825
- })
2826
- };
2827
- }
2828
- function materializeSegment(layout, m) {
2829
- const frame = layout.frames[m.frame];
2830
- const quad = quadOfFrameRect(frame, m.frame, m.rect);
2831
- return {
2832
- quad,
2833
- rect: m.frame === 0 ? m.rect : {
2834
- left: Math.min(quad.p1.x, quad.p2.x, quad.p3.x, quad.p4.x),
2835
- bottom: Math.min(quad.p1.y, quad.p2.y, quad.p3.y, quad.p4.y),
2836
- right: Math.max(quad.p1.x, quad.p2.x, quad.p3.x, quad.p4.x),
2837
- top: Math.max(quad.p1.y, quad.p2.y, quad.p3.y, quad.p4.y)
2838
- },
2839
- advance: m.lastX >= m.firstX ? 1 : -1
2840
- };
2841
- }
2842
- function shouldMerge(a, b) {
2843
- if (a.fontSize != null && b.fontSize != null && a.fontSize > 0 && b.fontSize > 0) {
2844
- if (Math.max(a.fontSize, b.fontSize) / Math.min(a.fontSize, b.fontSize) > FONT_SIZE_RATIO_THRESHOLD) return false;
2845
- }
2846
- if (verticalOverlap(a.rect, b.rect) < VERTICAL_OVERLAP_THRESHOLD) return false;
2847
- const aw = (a.rect.right - a.rect.left) / a.charCount;
2848
- const bw = (b.rect.right - b.rect.left) / b.charCount;
2849
- const aL = a.rect.left - aw;
2850
- const aR = a.rect.right + aw;
2851
- const bL = b.rect.left - bw;
2852
- return aL < b.rect.right + bw && aR > bL;
2853
- }
2854
- function verticalOverlap(a, b) {
2855
- const ah = a.top - a.bottom;
2856
- const bh = b.top - b.bottom;
2857
- if (ah <= 0 || bh <= 0) return 0;
2858
- const u = Math.max(a.top, b.top) - Math.min(a.bottom, b.bottom);
2859
- if (u === ah || u === bh) return 1;
2860
- return Math.max(0, Math.min(a.top, b.top) - Math.max(a.bottom, b.bottom)) / u;
2861
- }
2862
- const isZero = (r) => r.right === r.left && r.top === r.bottom;
2863
- function overlapV(bottom1, top1, bottom2, top2) {
2864
- const u = Math.max(top1, top2) - Math.min(bottom1, bottom2);
2865
- if (u === 0) return false;
2866
- return Math.max(0, Math.min(top1, top2) - Math.max(bottom1, bottom2)) / u >= LINE_OVERLAP_THRESHOLD;
2867
- }
2868
- //#endregion
2869
- //#region src/search/rects.ts
2870
- /**
2871
- * Match-range → canonical visual-line segments. Thin wrappers over the ONE
2872
- * text layout (`src/text/layout.ts`) — search owns matching and cursors,
2873
- * never segmentation, so a match highlights exactly like a selection of the
2874
- * same characters. No frame or merge logic lives here.
2875
- */
2876
- /** Canonical segments for `[charStart, charStart + charCount)`. */
2877
- function searchSegmentsForRange(snapshot, charStart, charCount) {
2878
- return textSegmentsForRange(buildPageTextLayout(snapshot), charStart, charCount);
2879
- }
2880
- /** The segments' AABBs — for consumers that genuinely want boxes. */
2881
- function searchRectsForRange(snapshot, charStart, charCount) {
2882
- return searchSegmentsForRange(snapshot, charStart, charCount).map((s) => s.rect);
2883
- }
2884
- //#endregion
2885
- //#region src/search/epoch.ts
2886
- /**
2887
- * The search CONTENT EPOCH: a deterministic fingerprint of everything a
2888
- * search result depends on — page order/structure (`layoutVersion`) and
2889
- * each page's text content (`contentVersion`), nothing else. Annotation
2890
- * and metadata churn deliberately do NOT move it, so cached search
2891
- * responses in a collaborative document survive comment storms.
2892
- *
2893
- * Both sides compute it from the manifest they already hold: the client
2894
- * to mint versioned search URLs, the server to validate them (mismatch →
2895
- * NotFound, the standard stale-versioned-read signal). FNV-1a 64-bit —
2896
- * dependency-free and stable; not cryptographic, and doesn't need to be:
2897
- * an adversary can only vary his OWN authorized query results.
2898
- */
2899
- function searchContentEpoch(manifest) {
2900
- let input = `${manifest.layoutVersion}`;
2901
- for (const page of manifest.pages) input += `|${page.state.pageObjectNumber}:${page.cache.contentVersion}`;
2902
- return fnv1a64(input);
2903
- }
2904
- const FNV_OFFSET = 14695981039346656037n;
2905
- const FNV_PRIME = 1099511628211n;
2906
- const MASK_64 = 18446744073709551615n;
2907
- function fnv1a64(input) {
2908
- let hash = FNV_OFFSET;
2909
- for (let i = 0; i < input.length; i++) {
2910
- hash ^= BigInt(input.charCodeAt(i));
2911
- hash = hash * FNV_PRIME & MASK_64;
2912
- }
2913
- return hash.toString(16).padStart(16, "0");
2914
- }
2915
- /**
2916
- * Canonicalize a query for CACHE KEYING: default-fold literal queries are
2917
- * pre-folded ("Café", "café" and "CAFE" are the same search, so they
2918
- * should be the same cache entry). Semantics are unchanged — the fold is
2919
- * idempotent and exactly what the matcher applies to the needle anyway.
2920
- * Case- or diacritic-sensitive literals and regex patterns pass through
2921
- * untouched (their raw form IS the query).
2922
- */
2923
- function canonicalSearchQuery(query) {
2924
- if (query.regex || query.matchCase || query.matchDiacritics) return query;
2925
- const canonical = { text: require_fold.foldText(query.text).folded };
2926
- if (query.wholeWord) canonical.wholeWord = true;
2927
- return canonical;
2928
- }
2929
- //#endregion
2930
- //#region src/auth/scope/pdf-bits.ts
2931
- /**
2932
- * ISO 32000 user-access permission bit positions, expressed as integer
2933
- * masks. Bit numbers in the spec are 1-indexed, so bit 3 = `1 << 2`.
2934
- *
2935
- * Bits 1, 2, 7, 8, and 13+ are reserved and not surfaced here.
2936
- */
2937
- const PDF_BITS = {
2938
- /** Bit 3 — print the document. */
2939
- PRINT: 4,
2940
- /** Bit 4 — modify content streams. */
2941
- MODIFY: 8,
2942
- /** Bit 5 — copy/extract text and graphics. */
2943
- COPY: 16,
2944
- /** Bit 6 — annotate + fill forms (+ modify form fields with bit 4). */
2945
- ANNOTATE_FILL: 32,
2946
- /** Bit 9 — fill existing forms (even if bit 6 is clear). */
2947
- FILL_FORMS: 256,
2948
- /** Bit 10 — accessibility extraction (deprecated in PDF 2.0). */
2949
- ACCESSIBILITY: 512,
2950
- /** Bit 11 — assemble document (insert/rotate/delete pages, outline). */
2951
- ASSEMBLE: 1024,
2952
- /** Bit 12 — high-quality print (refines bit 3). */
2953
- PRINT_HIGH: 2048
2954
- };
2955
- /**
2956
- * Decode a raw PDFium permission integer into a typed boolean view.
2957
- *
2958
- * `null` input (the documents row column is nullable until probed)
2959
- * yields all-false bits — i.e., a maximally restrictive interpretation.
2960
- * That keeps `pdf.permissions` expansion safe on unprobed documents.
2961
- */
2962
- function decodePdfBits(raw) {
2963
- if (raw === null) return {
2964
- bit3: false,
2965
- bit4: false,
2966
- bit5: false,
2967
- bit6: false,
2968
- bit9: false,
2969
- bit10: false,
2970
- bit11: false,
2971
- bit12: false
2972
- };
2973
- return {
2974
- bit3: (raw & PDF_BITS.PRINT) !== 0,
2975
- bit4: (raw & PDF_BITS.MODIFY) !== 0,
2976
- bit5: (raw & PDF_BITS.COPY) !== 0,
2977
- bit6: (raw & PDF_BITS.ANNOTATE_FILL) !== 0,
2978
- bit9: (raw & PDF_BITS.FILL_FORMS) !== 0,
2979
- bit10: (raw & PDF_BITS.ACCESSIBILITY) !== 0,
2980
- bit11: (raw & PDF_BITS.ASSEMBLE) !== 0,
2981
- bit12: (raw & PDF_BITS.PRINT_HIGH) !== 0
2982
- };
2983
- }
2984
- //#endregion
2985
- //#region src/auth/scope/builders.ts
2986
- /**
2987
- * Typed capability constants for customer-side JWT minting.
2988
- *
2989
- * The shape mirrors the dotted capability name so it reads naturally:
2990
- * caps.doc.text.copy() // "doc.text.copy"
2991
- * caps.doc.download() // "doc.download"
2992
- * caps.doc.download.flattened() // "doc.download.flattened"
2993
- * caps.doc.print.high() // "doc.print.high"
2994
- *
2995
- * Each leaf is a function returning the literal string (as `const`) so
2996
- * TypeScript catches typos at the call site. Wrappers like `download`
2997
- * and `print` are `Object.assign`'d to be callable AND carry child
2998
- * properties for the refinement capabilities.
2999
- */
3000
- const caps = { doc: {
3001
- open: () => "doc.open",
3002
- render: () => "doc.render",
3003
- text: {
3004
- select: () => "doc.text.select",
3005
- copy: () => "doc.text.copy",
3006
- search: () => "doc.text.search"
3007
- },
3008
- content: { copy: () => "doc.content.copy" },
3009
- download: Object.assign(() => "doc.download", { flattened: () => "doc.download.flattened" }),
3010
- print: Object.assign(() => "doc.print", { high: () => "doc.print.high" }),
3011
- pages: {
3012
- modify: () => "doc.pages.modify",
3013
- assemble: () => "doc.pages.assemble"
3014
- },
3015
- forms: {
3016
- read: () => "doc.forms.read",
3017
- fill: () => "doc.forms.fill",
3018
- modify: () => "doc.forms.modify"
3019
- },
3020
- annotate: {
3021
- read: () => "doc.annotate.read",
3022
- modify: () => "doc.annotate.modify"
3023
- },
3024
- metadata: { modify: () => "doc.metadata.modify" },
3025
- attachments: { modify: () => "doc.attachments.modify" },
3026
- redact: () => "doc.redact"
3027
- } };
3028
- /**
3029
- * Builder factory for collab scope filters. Each entity:action pair
3030
- * gets a sub-object with the four filter constructors.
3031
- *
3032
- * collab.annotations.create.self() → "annotations:create:self"
3033
- * collab.annotations.create.group("legal") → "annotations:create:group=legal"
3034
- * collab.annotations.update.group("4") → "annotations:update:group=4"
3035
- * collab.annotations.delete.createdBy("u-7") → "annotations:delete:createdBy=u-7"
3036
- * collab.annotations.setGroup.group("legal") → "annotations:set-group:group=legal"
3037
- * collab.annotations.all.all() → "annotations:*:all" (action wildcard)
3038
- *
3039
- * On create, the filter is evaluated against the caller's own identity
3040
- * (no impersonation), so `:self` and `:all` trivially pass; `:group=X`
3041
- * is the meaningful filter — it constrains creation to callers whose
3042
- * default group is X.
3043
- */
3044
- const collab = { annotations: {
3045
- create: makeFilterBuilder("annotations", "create"),
3046
- update: makeFilterBuilder("annotations", "update"),
3047
- delete: makeFilterBuilder("annotations", "delete"),
3048
- setGroup: makeSetGroupBuilder(),
3049
- /** Action wildcard — matches create, update, delete, AND set-group with the given filter. */
3050
- all: makeFilterBuilder("annotations", "*")
3051
- } };
3052
- function makeFilterBuilder(entity, action) {
3053
- return {
3054
- all: () => `${entity}:${action}:all`,
3055
- self: () => `${entity}:${action}:self`,
3056
- createdBy: (userId) => `${entity}:${action}:createdBy=${userId}`,
3057
- group: (groupId) => `${entity}:${action}:group=${groupId}`
3058
- };
3059
- }
3060
- function makeSetGroupBuilder() {
3061
- return {
3062
- all: () => "annotations:set-group:all",
3063
- group: (groupId) => `annotations:set-group:group=${groupId}`
3064
- };
3065
- }
3066
- /**
3067
- * Builder for the `pdf.permissions` virtual scope. Returns the literal
3068
- * string; provided as a helper for parity with caps/collab in JWT
3069
- * construction code.
3070
- */
3071
- const pdfPermissions = () => "pdf.permissions";
3072
- /**
3073
- * Materialise `pdf.permissions` into the concrete capability list it
3074
- * would expand to under the given PDF bit configuration.
3075
- *
3076
- * Useful when the customer wants to start from PDF defaults and then
3077
- * subtract specific capabilities — easier to build the JWT scope array
3078
- * by filtering an explicit list than by composing virtual + denials.
3079
- *
3080
- * const defaults = materializePdfPermissions(pdfBits);
3081
- * const scope = [
3082
- * ...defaults.filter(s => s !== 'doc.text.copy'),
3083
- * 'doc.download',
3084
- * ];
3085
- *
3086
- * IMPORTANT: this MUST stay in sync with `addPdfPermissions` inside
3087
- * resolver.ts. A test pins them together.
3088
- */
3089
- function materializePdfPermissions(b) {
3090
- const out = /* @__PURE__ */ new Set();
3091
- out.add("doc.open");
3092
- out.add("doc.render");
3093
- out.add("doc.annotate.read");
3094
- out.add("doc.forms.read");
3095
- if (b.bit5) {
3096
- out.add("doc.text.select");
3097
- out.add("doc.text.copy");
3098
- out.add("doc.text.search");
3099
- out.add("doc.content.copy");
3100
- }
3101
- if (b.bit3) out.add("doc.print");
3102
- if (b.bit12 && b.bit3) out.add("doc.print.high");
3103
- if (b.bit4) {
3104
- out.add("doc.pages.modify");
3105
- out.add("doc.redact");
3106
- out.add("doc.metadata.modify");
3107
- out.add("doc.attachments.modify");
3108
- }
3109
- if (b.bit11) out.add("doc.pages.assemble");
3110
- if (b.bit6) out.add("doc.annotate.modify");
3111
- if (b.bit6 || b.bit9) {
3112
- out.add("doc.forms.fill");
3113
- out.add("doc.sign");
3114
- }
3115
- if (b.bit6 && b.bit4) out.add("doc.forms.modify");
3116
- return [...out];
3117
- }
3118
- //#endregion
3119
- Object.defineProperty(exports, "DEFAULT_EDGE_RESOLVER_BUDGET", {
3120
- enumerable: true,
3121
- get: function() {
3122
- return DEFAULT_EDGE_RESOLVER_BUDGET;
3123
- }
3124
- });
3125
- Object.defineProperty(exports, "DEFAULT_PDF_SAVE_MODE", {
3126
- enumerable: true,
3127
- get: function() {
3128
- return DEFAULT_PDF_SAVE_MODE;
3129
- }
3130
- });
3131
- Object.defineProperty(exports, "EdgeResolver", {
3132
- enumerable: true,
3133
- get: function() {
3134
- return EdgeResolver;
3135
- }
3136
- });
3137
- Object.defineProperty(exports, "NO_ANNOTATION_FLAGS", {
3138
- enumerable: true,
3139
- get: function() {
3140
- return NO_ANNOTATION_FLAGS;
3141
- }
3142
- });
3143
- Object.defineProperty(exports, "PDF_BITS", {
3144
- enumerable: true,
3145
- get: function() {
3146
- return PDF_BITS;
3147
- }
3148
- });
3149
- Object.defineProperty(exports, "SEARCH_REGEX_MAX_LENGTH", {
3150
- enumerable: true,
3151
- get: function() {
3152
- return SEARCH_REGEX_MAX_LENGTH;
3153
- }
3154
- });
3155
- Object.defineProperty(exports, "SEARCH_SNIPPET_CONTEXT", {
3156
- enumerable: true,
3157
- get: function() {
3158
- return SEARCH_SNIPPET_CONTEXT;
3159
- }
3160
- });
3161
- Object.defineProperty(exports, "UNKNOWN_WEAK_ANNOTATION_STATE", {
3162
- enumerable: true,
3163
- get: function() {
3164
- return UNKNOWN_WEAK_ANNOTATION_STATE;
3165
- }
3166
- });
3167
- Object.defineProperty(exports, "USAGE_INCOMPLETE", {
3168
- enumerable: true,
3169
- get: function() {
3170
- return USAGE_INCOMPLETE;
3171
- }
3172
- });
3173
- Object.defineProperty(exports, "appearanceImpactOf", {
3174
- enumerable: true,
3175
- get: function() {
3176
- return appearanceImpactOf;
3177
- }
3178
- });
3179
- Object.defineProperty(exports, "assessmentOf", {
3180
- enumerable: true,
3181
- get: function() {
3182
- return assessmentOf;
3183
- }
3184
- });
3185
- Object.defineProperty(exports, "buildCommentThreads", {
3186
- enumerable: true,
3187
- get: function() {
3188
- return buildCommentThreads;
3189
- }
3190
- });
3191
- Object.defineProperty(exports, "buildPageTextLayout", {
3192
- enumerable: true,
3193
- get: function() {
3194
- return buildPageTextLayout;
3195
- }
3196
- });
3197
- Object.defineProperty(exports, "buildSnippet", {
3198
- enumerable: true,
3199
- get: function() {
3200
- return buildSnippet;
3201
- }
3202
- });
3203
- Object.defineProperty(exports, "buildThreads", {
3204
- enumerable: true,
3205
- get: function() {
3206
- return buildThreads;
3207
- }
3208
- });
3209
- Object.defineProperty(exports, "canonicalSearchQuery", {
3210
- enumerable: true,
3211
- get: function() {
3212
- return canonicalSearchQuery;
3213
- }
3214
- });
3215
- Object.defineProperty(exports, "caps", {
3216
- enumerable: true,
3217
- get: function() {
3218
- return caps;
3219
- }
3220
- });
3221
- Object.defineProperty(exports, "changedKeys", {
3222
- enumerable: true,
3223
- get: function() {
3224
- return changedKeys;
3225
- }
3226
- });
3227
- Object.defineProperty(exports, "changesAnnotationList", {
3228
- enumerable: true,
3229
- get: function() {
3230
- return changesAnnotationList;
3231
- }
3232
- });
3233
- Object.defineProperty(exports, "classifyRelation", {
3234
- enumerable: true,
3235
- get: function() {
3236
- return classifyRelation;
3237
- }
3238
- });
3239
- Object.defineProperty(exports, "collab", {
3240
- enumerable: true,
3241
- get: function() {
3242
- return collab;
3243
- }
3244
- });
3245
- Object.defineProperty(exports, "combine", {
3246
- enumerable: true,
3247
- get: function() {
3248
- return combine;
3249
- }
3250
- });
3251
- Object.defineProperty(exports, "conclude", {
3252
- enumerable: true,
3253
- get: function() {
3254
- return conclude;
3255
- }
3256
- });
3257
- Object.defineProperty(exports, "createPageImageHandle", {
3258
- enumerable: true,
3259
- get: function() {
3260
- return createPageImageHandle;
3261
- }
3262
- });
3263
- Object.defineProperty(exports, "decodeFieldRefKey", {
3264
- enumerable: true,
3265
- get: function() {
3266
- return decodeFieldRefKey;
3267
- }
3268
- });
3269
- Object.defineProperty(exports, "decodePdfBits", {
3270
- enumerable: true,
3271
- get: function() {
3272
- return decodePdfBits;
3273
- }
3274
- });
3275
- Object.defineProperty(exports, "decodeStableIdKey", {
3276
- enumerable: true,
3277
- get: function() {
3278
- return decodeStableIdKey;
3279
- }
3280
- });
3281
- Object.defineProperty(exports, "decodeSubmitFormFlags", {
3282
- enumerable: true,
3283
- get: function() {
3284
- return decodeSubmitFormFlags;
3285
- }
3286
- });
3287
- Object.defineProperty(exports, "dictEntries", {
3288
- enumerable: true,
3289
- get: function() {
3290
- return dictEntries;
3291
- }
3292
- });
3293
- Object.defineProperty(exports, "encodeFieldRefKey", {
3294
- enumerable: true,
3295
- get: function() {
3296
- return encodeFieldRefKey;
3297
- }
3298
- });
3299
- Object.defineProperty(exports, "encodeStableIdKey", {
3300
- enumerable: true,
3301
- get: function() {
3302
- return encodeStableIdKey;
3303
- }
3304
- });
3305
- Object.defineProperty(exports, "evaluateStep", {
3306
- enumerable: true,
3307
- get: function() {
3308
- return evaluateStep;
3309
- }
3310
- });
3311
- Object.defineProperty(exports, "expandTextRangeToLine", {
3312
- enumerable: true,
3313
- get: function() {
3314
- return expandTextRangeToLine;
3315
- }
3316
- });
3317
- Object.defineProperty(exports, "expandTextRangeToWord", {
3318
- enumerable: true,
3319
- get: function() {
3320
- return expandTextRangeToWord;
3321
- }
3322
- });
3323
- Object.defineProperty(exports, "foldOptionsFor", {
3324
- enumerable: true,
3325
- get: function() {
3326
- return foldOptionsFor;
3327
- }
3328
- });
3329
- Object.defineProperty(exports, "invalidatesWeakIndexRefs", {
3330
- enumerable: true,
3331
- get: function() {
3332
- return invalidatesWeakIndexRefs;
3333
- }
3334
- });
3335
- Object.defineProperty(exports, "isStateAnnotation", {
3336
- enumerable: true,
3337
- get: function() {
3338
- return isStateAnnotation;
3339
- }
3340
- });
3341
- Object.defineProperty(exports, "isValidPageObjectNumber", {
3342
- enumerable: true,
3343
- get: function() {
3344
- return isValidPageObjectNumber;
3345
- }
3346
- });
3347
- Object.defineProperty(exports, "knownWeakAnnotationState", {
3348
- enumerable: true,
3349
- get: function() {
3350
- return knownWeakAnnotationState;
3351
- }
3352
- });
3353
- Object.defineProperty(exports, "matchLiteral", {
3354
- enumerable: true,
3355
- get: function() {
3356
- return matchLiteral;
3357
- }
3358
- });
3359
- Object.defineProperty(exports, "matchPageText", {
3360
- enumerable: true,
3361
- get: function() {
3362
- return matchPageText;
3363
- }
3364
- });
3365
- Object.defineProperty(exports, "matchRegex", {
3366
- enumerable: true,
3367
- get: function() {
3368
- return matchRegex;
3369
- }
3370
- });
3371
- Object.defineProperty(exports, "materializePdfPermissions", {
3372
- enumerable: true,
3373
- get: function() {
3374
- return materializePdfPermissions;
3375
- }
3376
- });
3377
- Object.defineProperty(exports, "normalizeAnnotationDraft", {
3378
- enumerable: true,
3379
- get: function() {
3380
- return normalizeAnnotationDraft;
3381
- }
3382
- });
3383
- Object.defineProperty(exports, "normalizeAnnotationPatch", {
3384
- enumerable: true,
3385
- get: function() {
3386
- return normalizeAnnotationPatch;
3387
- }
3388
- });
3389
- Object.defineProperty(exports, "normalizeAttachmentFileSource", {
3390
- enumerable: true,
3391
- get: function() {
3392
- return normalizeAttachmentFileSource;
3393
- }
3394
- });
3395
- Object.defineProperty(exports, "parsePdfValue", {
3396
- enumerable: true,
3397
- get: function() {
3398
- return parsePdfValue;
3399
- }
3400
- });
3401
- Object.defineProperty(exports, "pdfPermissions", {
3402
- enumerable: true,
3403
- get: function() {
3404
- return pdfPermissions;
3405
- }
3406
- });
3407
- Object.defineProperty(exports, "pdfValueEquals", {
3408
- enumerable: true,
3409
- get: function() {
3410
- return pdfValueEquals;
3411
- }
3412
- });
3413
- Object.defineProperty(exports, "primaryFinding", {
3414
- enumerable: true,
3415
- get: function() {
3416
- return primaryFinding;
3417
- }
3418
- });
3419
- Object.defineProperty(exports, "refKey", {
3420
- enumerable: true,
3421
- get: function() {
3422
- return refKey;
3423
- }
3424
- });
3425
- Object.defineProperty(exports, "refsOf", {
3426
- enumerable: true,
3427
- get: function() {
3428
- return refsOf;
3429
- }
3430
- });
3431
- Object.defineProperty(exports, "resolveBinarySource", {
3432
- enumerable: true,
3433
- get: function() {
3434
- return resolveBinarySource;
3435
- }
3436
- });
3437
- Object.defineProperty(exports, "restrictionsFor", {
3438
- enumerable: true,
3439
- get: function() {
3440
- return restrictionsFor;
3441
- }
3442
- });
3443
- Object.defineProperty(exports, "restrictionsOf", {
3444
- enumerable: true,
3445
- get: function() {
3446
- return restrictionsOf;
3447
- }
3448
- });
3449
- Object.defineProperty(exports, "revisionTokensEqual", {
3450
- enumerable: true,
3451
- get: function() {
3452
- return revisionTokensEqual;
3453
- }
3454
- });
3455
- Object.defineProperty(exports, "richTextParagraphsFromPlainText", {
3456
- enumerable: true,
3457
- get: function() {
3458
- return richTextParagraphsFromPlainText;
3459
- }
3460
- });
3461
- Object.defineProperty(exports, "richTextPlainText", {
3462
- enumerable: true,
3463
- get: function() {
3464
- return richTextPlainText;
3465
- }
3466
- });
3467
- Object.defineProperty(exports, "sameEffectiveValue", {
3468
- enumerable: true,
3469
- get: function() {
3470
- return sameEffectiveValue;
3471
- }
3472
- });
3473
- Object.defineProperty(exports, "searchContentEpoch", {
3474
- enumerable: true,
3475
- get: function() {
3476
- return searchContentEpoch;
3477
- }
3478
- });
3479
- Object.defineProperty(exports, "searchRectsForRange", {
3480
- enumerable: true,
3481
- get: function() {
3482
- return searchRectsForRange;
3483
- }
3484
- });
3485
- Object.defineProperty(exports, "searchSegmentsForRange", {
3486
- enumerable: true,
3487
- get: function() {
3488
- return searchSegmentsForRange;
3489
- }
3490
- });
3491
- Object.defineProperty(exports, "semanticEqual", {
3492
- enumerable: true,
3493
- get: function() {
3494
- return semanticEqual;
3495
- }
3496
- });
3497
- Object.defineProperty(exports, "shiftsExistingAnnotationIndices", {
3498
- enumerable: true,
3499
- get: function() {
3500
- return shiftsExistingAnnotationIndices;
3501
- }
3502
- });
3503
- Object.defineProperty(exports, "sniffBinaryMetadata", {
3504
- enumerable: true,
3505
- get: function() {
3506
- return sniffBinaryMetadata;
3507
- }
3508
- });
3509
- Object.defineProperty(exports, "stableStringify", {
3510
- enumerable: true,
3511
- get: function() {
3512
- return stableStringify;
3513
- }
3514
- });
3515
- Object.defineProperty(exports, "textGlyphAt", {
3516
- enumerable: true,
3517
- get: function() {
3518
- return textGlyphAt;
3519
- }
3520
- });
3521
- Object.defineProperty(exports, "textGlyphQuad", {
3522
- enumerable: true,
3523
- get: function() {
3524
- return textGlyphQuad;
3525
- }
3526
- });
3527
- Object.defineProperty(exports, "textSegmentsForRange", {
3528
- enumerable: true,
3529
- get: function() {
3530
- return textSegmentsForRange;
3531
- }
3532
- });
3533
- Object.defineProperty(exports, "validateSearchQuery", {
3534
- enumerable: true,
3535
- get: function() {
3536
- return validateSearchQuery;
3537
- }
3538
- });
3539
- Object.defineProperty(exports, "validateSearchRegex", {
3540
- enumerable: true,
3541
- get: function() {
3542
- return validateSearchRegex;
3543
- }
3544
- });
3545
- Object.defineProperty(exports, "wordAt", {
3546
- enumerable: true,
3547
- get: function() {
3548
- return wordAt;
3549
- }
3550
- });
3551
- Object.defineProperty(exports, "wordBefore", {
3552
- enumerable: true,
3553
- get: function() {
3554
- return wordBefore;
3555
- }
3556
- });
3557
- Object.defineProperty(exports, "worstVerdict", {
3558
- enumerable: true,
3559
- get: function() {
3560
- return worstVerdict;
3561
- }
3562
- });
3563
-
3564
- //# sourceMappingURL=builders-Bqm8yTpi.cjs.map