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

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 (59) hide show
  1. package/dist/{AbortError-CRSooKsk.cjs → AbortError-Cq2igpZ6.cjs} +2 -2
  2. package/dist/{AbortError-CRSooKsk.cjs.map → AbortError-Cq2igpZ6.cjs.map} +1 -1
  3. package/dist/{AbortError-8-9JAX1Y.js → AbortError-DHqPBnYc.js} +2 -2
  4. package/dist/{AbortError-8-9JAX1Y.js.map → AbortError-DHqPBnYc.js.map} +1 -1
  5. package/dist/{AnnotationListSnapshot-C1U3XUxP.d.cts → AnnotationListSnapshot-D3mFut1e.d.cts} +1323 -214
  6. package/dist/{AnnotationListSnapshot-C1U3XUxP.d.ts → AnnotationListSnapshot-D3mFut1e.d.ts} +1323 -214
  7. package/dist/{DocumentSecurityService-6rhztZlz.d.cts → DocumentSecurityService-ClTZc0zO.d.cts} +2 -2
  8. package/dist/{DocumentSecurityService-7nL9aY75.d.ts → DocumentSecurityService-_JRjqvoI.d.ts} +2 -2
  9. package/dist/{Engine-D3868xIU.d.cts → Engine-Bx0NOfmd.d.cts} +219 -11
  10. package/dist/{Engine-CWnAKccD.d.ts → Engine-DxtpCYcP.d.ts} +219 -11
  11. package/dist/{PageInsertBlankInput-CzShuSJI.cjs → PageInsertBlankInput-CqXBmFhN.cjs} +37 -2
  12. package/dist/PageInsertBlankInput-CqXBmFhN.cjs.map +1 -0
  13. package/dist/{PageInsertBlankInput-WKY0tr6S.js → PageInsertBlankInput-cHq9-i-z.js} +37 -2
  14. package/dist/PageInsertBlankInput-cHq9-i-z.js.map +1 -0
  15. package/dist/{builders-Do6_4KnS.cjs → builders-Bqm8yTpi.cjs} +1483 -11
  16. package/dist/builders-Bqm8yTpi.cjs.map +1 -0
  17. package/dist/{builders-Cdz34nbF.js → builders-Dqo2X-dt.js} +1364 -12
  18. package/dist/builders-Dqo2X-dt.js.map +1 -0
  19. package/dist/conformance.cjs +1630 -107
  20. package/dist/conformance.cjs.map +1 -1
  21. package/dist/conformance.d.cts +117 -3
  22. package/dist/conformance.d.ts +117 -3
  23. package/dist/conformance.js +1627 -108
  24. package/dist/conformance.js.map +1 -1
  25. package/dist/{fold-C7I5GpBz.cjs → fold-2V29cxCV.cjs} +2 -2
  26. package/dist/{fold-C7I5GpBz.cjs.map → fold-2V29cxCV.cjs.map} +1 -1
  27. package/dist/{fold-BfyxxwS6.js → fold-CdEJTBTn.js} +2 -2
  28. package/dist/{fold-BfyxxwS6.js.map → fold-CdEJTBTn.js.map} +1 -1
  29. package/dist/index.cjs +33 -4
  30. package/dist/index.d.cts +2 -2
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +5 -5
  33. package/dist/{resolver-D-jumvgM.cjs → resolver-CYVNV4hn.cjs} +216 -10
  34. package/dist/resolver-CYVNV4hn.cjs.map +1 -0
  35. package/dist/{resolver-CHG4waXL.js → resolver-inAVzsWt.js} +157 -11
  36. package/dist/resolver-inAVzsWt.js.map +1 -0
  37. package/dist/runtime.cjs +34 -5
  38. package/dist/runtime.d.cts +253 -5
  39. package/dist/runtime.d.ts +253 -5
  40. package/dist/runtime.js +6 -6
  41. package/dist/{schemas-XGrNVMDd.js → schemas-BLARNCgx.js} +469 -7
  42. package/dist/schemas-BLARNCgx.js.map +1 -0
  43. package/dist/{schemas-CS5iPpX5.cjs → schemas-DIqFMmrC.cjs} +672 -6
  44. package/dist/schemas-DIqFMmrC.cjs.map +1 -0
  45. package/dist/wire.cjs +250 -12
  46. package/dist/wire.cjs.map +1 -1
  47. package/dist/wire.d.cts +444 -4
  48. package/dist/wire.d.ts +444 -4
  49. package/dist/wire.js +214 -13
  50. package/dist/wire.js.map +1 -1
  51. package/package.json +1 -1
  52. package/dist/PageInsertBlankInput-CzShuSJI.cjs.map +0 -1
  53. package/dist/PageInsertBlankInput-WKY0tr6S.js.map +0 -1
  54. package/dist/builders-Cdz34nbF.js.map +0 -1
  55. package/dist/builders-Do6_4KnS.cjs.map +0 -1
  56. package/dist/resolver-CHG4waXL.js.map +0 -1
  57. package/dist/resolver-D-jumvgM.cjs.map +0 -1
  58. package/dist/schemas-CS5iPpX5.cjs.map +0 -1
  59. package/dist/schemas-XGrNVMDd.js.map +0 -1
@@ -1,7 +1,7 @@
1
- import { f as pdfQuadBounds, i as EngineError, n as foldText, u as isRotatedGeometryRun } from "./fold-BfyxxwS6.js";
2
- import { f as EngineErrorCode, o as sliceTextByChars } from "./PageInsertBlankInput-WKY0tr6S.js";
3
- import { t as AbortError } from "./AbortError-8-9JAX1Y.js";
4
- import { A as FormSetValueResultSchema, B as PageDeleteResultSchema, O as FormImportResultSchema, Ot as FormSnapshotSchema, Pt as AnnotationDTOSchema, U as PageFlattenResultSchema, W as PageGeometrySnapshotSchema, Y as PageListSnapshotSchema, Z as PageMoveResultSchema, a as AnnotationCreateResultSchema, at as PageTextSnapshotSchema, c as AnnotationListPageSnapshotSchema, d as AnnotationUpdateResultSchema, k as FormRepairResultSchema, l as AnnotationListSnapshotAllPagesSchema, mt as SearchSliceSchema, nt as PageRotateResultSchema, o as AnnotationDeleteResultSchema, pr as DocumentActionsSnapshotSchema, st as RedactionApplyResultSchema, u as AnnotationMoveResultSchema } from "./schemas-XGrNVMDd.js";
1
+ import { f as pdfQuadBounds, i as EngineError, n as foldText, u as isRotatedGeometryRun } from "./fold-CdEJTBTn.js";
2
+ import { f as EngineErrorCode, o as sliceTextByChars } from "./PageInsertBlankInput-cHq9-i-z.js";
3
+ import { t as AbortError } from "./AbortError-DHqPBnYc.js";
4
+ import { B as FormImportResultSchema, Ct as PageTextSnapshotSchema, Et as RedactionApplyResultSchema, H as FormSetValueResultSchema, Nt as SearchSliceSchema, V as FormRepairResultSchema, at as PageGeometrySnapshotSchema, bt as PageRotateResultSchema, cn as FormSnapshotSchema, d as AnnotationListPageSnapshotSchema, f as AnnotationListSnapshotAllPagesSchema, ft as PageMoveResultSchema, it as PageFlattenResultSchema, l as AnnotationFlattenResultSchema, m as AnnotationUpdateResultSchema, mn as AnnotationDTOSchema, mt as PageNameResultSchema, o as AnnotationCreateResultSchema, p as AnnotationMoveResultSchema, qr as DocumentActionsSnapshotSchema, s as AnnotationDeleteResultSchema, tt as PageDeleteResultSchema, ut as PageListSnapshotSchema } from "./schemas-BLARNCgx.js";
5
5
  //#region src/conformance/runMetadataConformance.ts
6
6
  /**
7
7
  * The conformance harness is transport-agnostic. It takes the test runner as
@@ -18,7 +18,7 @@ function runMetadataConformance(runner, opts) {
18
18
  if (engine) await engine.destroy();
19
19
  });
20
20
  test("reads metadata from sample fixture", async () => {
21
- const doc = await openFixture$17(engine, opts);
21
+ const doc = await openFixture$20(engine, opts);
22
22
  try {
23
23
  const meta = await doc.metadata.read();
24
24
  expect(meta).toMatchObject(opts.fixture.expected);
@@ -28,7 +28,7 @@ function runMetadataConformance(runner, opts) {
28
28
  }
29
29
  });
30
30
  test("abort() before completion rejects with AbortError", async () => {
31
- const doc = await openFixture$17(engine, opts);
31
+ const doc = await openFixture$20(engine, opts);
32
32
  try {
33
33
  const p = doc.metadata.read();
34
34
  p.abort("test");
@@ -38,7 +38,7 @@ function runMetadataConformance(runner, opts) {
38
38
  }
39
39
  });
40
40
  test("aborting one read does not affect a concurrent one", async () => {
41
- const doc = await openFixture$17(engine, opts);
41
+ const doc = await openFixture$20(engine, opts);
42
42
  try {
43
43
  const a = doc.metadata.read();
44
44
  const b = doc.metadata.read();
@@ -50,7 +50,7 @@ function runMetadataConformance(runner, opts) {
50
50
  }
51
51
  });
52
52
  test("metadata after close throws DocNotOpen", async () => {
53
- const doc = await openFixture$17(engine, opts);
53
+ const doc = await openFixture$20(engine, opts);
54
54
  await doc.close();
55
55
  let caught;
56
56
  try {
@@ -62,7 +62,7 @@ function runMetadataConformance(runner, opts) {
62
62
  expect(EngineError.is(caught, EngineErrorCode.DocNotOpen)).toBe(true);
63
63
  });
64
64
  test("update() sets standard + custom fields and a subsequent read observes them", async () => {
65
- const doc = await openFixture$17(engine, opts);
65
+ const doc = await openFixture$20(engine, opts);
66
66
  try {
67
67
  const result = await doc.metadata.update({
68
68
  title: "Conformance Title",
@@ -81,7 +81,7 @@ function runMetadataConformance(runner, opts) {
81
81
  }
82
82
  });
83
83
  test("update() clears a field with null", async () => {
84
- const doc = await openFixture$17(engine, opts);
84
+ const doc = await openFixture$20(engine, opts);
85
85
  try {
86
86
  await doc.metadata.update({ title: "To Be Cleared" });
87
87
  const set = await doc.metadata.read();
@@ -94,7 +94,7 @@ function runMetadataConformance(runner, opts) {
94
94
  }
95
95
  });
96
96
  test("update() after close throws DocNotOpen", async () => {
97
- const doc = await openFixture$17(engine, opts);
97
+ const doc = await openFixture$20(engine, opts);
98
98
  await doc.close();
99
99
  let caught;
100
100
  try {
@@ -107,7 +107,7 @@ function runMetadataConformance(runner, opts) {
107
107
  });
108
108
  });
109
109
  }
110
- async function openFixture$17(engine, opts) {
110
+ async function openFixture$20(engine, opts) {
111
111
  if (opts.openKind === "bytes") {
112
112
  const bytes = await opts.fixture.bytes();
113
113
  return engine.open({
@@ -134,7 +134,7 @@ function runAnnotationReadConformance(runner, opts) {
134
134
  if (engine) await engine.destroy();
135
135
  });
136
136
  test("listRawAll returns one entry per page with valid PageState", async () => {
137
- const doc = await openFixture$16(engine, opts);
137
+ const doc = await openFixture$19(engine, opts);
138
138
  try {
139
139
  const snap = await doc.annotations.listRawAll();
140
140
  expect(AnnotationListSnapshotAllPagesSchema.safeParse(snap).success).toBe(true);
@@ -148,7 +148,7 @@ function runAnnotationReadConformance(runner, opts) {
148
148
  }
149
149
  });
150
150
  test("listRaw on the test page returns the expected counts and DTO shape", async () => {
151
- const doc = await openFixture$16(engine, opts);
151
+ const doc = await openFixture$19(engine, opts);
152
152
  try {
153
153
  const snap = await doc.annotations.listRaw(opts.fixture.pageObjectNumber);
154
154
  expect(AnnotationListPageSnapshotSchema.safeParse(snap).success).toBe(true);
@@ -178,7 +178,7 @@ function runAnnotationReadConformance(runner, opts) {
178
178
  }
179
179
  });
180
180
  test("shape/vertex/line annotations expose their family fields", async () => {
181
- const doc = await openFixture$16(engine, opts);
181
+ const doc = await openFixture$19(engine, opts);
182
182
  try {
183
183
  const snap = await doc.annotations.listRaw(opts.fixture.pageObjectNumber);
184
184
  for (const a of snap.annotations) {
@@ -237,7 +237,7 @@ function runAnnotationReadConformance(runner, opts) {
237
237
  }
238
238
  });
239
239
  test("full page list dispatches per-subtype and matches raw counts", async () => {
240
- const doc = await openFixture$16(engine, opts);
240
+ const doc = await openFixture$19(engine, opts);
241
241
  try {
242
242
  const full = await doc.page(opts.fixture.pageObjectNumber).annotations.list();
243
243
  expect(AnnotationListPageSnapshotSchema.safeParse(full).success).toBe(true);
@@ -247,7 +247,7 @@ function runAnnotationReadConformance(runner, opts) {
247
247
  }
248
248
  });
249
249
  test("every annotation DTO satisfies AnnotationDTOSchema (discriminated union)", async () => {
250
- const doc = await openFixture$16(engine, opts);
250
+ const doc = await openFixture$19(engine, opts);
251
251
  try {
252
252
  const snap = await doc.annotations.listRaw(opts.fixture.pageObjectNumber);
253
253
  for (const a of snap.annotations) {
@@ -287,7 +287,7 @@ function runAnnotationReadConformance(runner, opts) {
287
287
  });
288
288
  if (opts.fixture.expectsWeakAnnotation) {
289
289
  test("weak annotations carry identityQuality: weak and ref.kind: index", async () => {
290
- const doc = await openFixture$16(engine, opts);
290
+ const doc = await openFixture$19(engine, opts);
291
291
  try {
292
292
  const weak = (await doc.annotations.listRaw(opts.fixture.pageObjectNumber)).annotations.find((a) => a.identityQuality === "weak");
293
293
  expect(weak !== void 0).toBe(true);
@@ -297,7 +297,7 @@ function runAnnotationReadConformance(runner, opts) {
297
297
  }
298
298
  });
299
299
  test("a stale revision token throws InvalidReference", async () => {
300
- const doc = await openFixture$16(engine, opts);
300
+ const doc = await openFixture$19(engine, opts);
301
301
  try {
302
302
  const snap = await doc.page(opts.fixture.pageObjectNumber).annotations.list();
303
303
  const weak = snap.annotations.find((a) => a.identityQuality === "weak");
@@ -313,7 +313,7 @@ function runAnnotationReadConformance(runner, opts) {
313
313
  });
314
314
  }
315
315
  test("abort() on listRaw rejects with AbortError", async () => {
316
- const doc = await openFixture$16(engine, opts);
316
+ const doc = await openFixture$19(engine, opts);
317
317
  try {
318
318
  const p = doc.annotations.listRaw(opts.fixture.pageObjectNumber);
319
319
  p.abort("test");
@@ -323,7 +323,7 @@ function runAnnotationReadConformance(runner, opts) {
323
323
  }
324
324
  });
325
325
  test("listRaw on an unknown pageObjectNumber throws NotFound", async () => {
326
- const doc = await openFixture$16(engine, opts);
326
+ const doc = await openFixture$19(engine, opts);
327
327
  try {
328
328
  let caught;
329
329
  try {
@@ -339,7 +339,7 @@ function runAnnotationReadConformance(runner, opts) {
339
339
  });
340
340
  });
341
341
  }
342
- async function openFixture$16(engine, opts) {
342
+ async function openFixture$19(engine, opts) {
343
343
  if (opts.openKind === "bytes") {
344
344
  const bytes = await opts.fixture.bytes();
345
345
  return engine.open({
@@ -388,7 +388,7 @@ function runActionsConformance(runner, opts) {
388
388
  if (engine) await engine.destroy();
389
389
  });
390
390
  test("reads catalog additional actions and preserves /CO identity", async () => {
391
- const doc = await open(engine, opts, opts.fixtures.document);
391
+ const doc = await open$1(engine, opts, opts.fixtures.document);
392
392
  try {
393
393
  expect(Boolean(doc.actions)).toBe(true);
394
394
  const snapshot = await doc.actions.read();
@@ -409,7 +409,7 @@ function runActionsConformance(runner, opts) {
409
409
  }
410
410
  });
411
411
  test("attaches page /AA to PageLayout", async () => {
412
- const doc = await open(engine, opts, opts.fixtures.page);
412
+ const doc = await open$1(engine, opts, opts.fixtures.page);
413
413
  try {
414
414
  const pages = await doc.pages.list();
415
415
  expect(pages.pages[0].actions?.open?.root?.type).toBe("goto-embedded");
@@ -421,7 +421,7 @@ function runActionsConformance(runner, opts) {
421
421
  }
422
422
  });
423
423
  test("attaches activation actions to AnnotationBase, including links", async () => {
424
- const doc = await open(engine, opts, opts.fixtures.annotation);
424
+ const doc = await open$1(engine, opts, opts.fixtures.annotation);
425
425
  try {
426
426
  const firstPage = (await doc.pages.list()).pages[0];
427
427
  const snapshot = await doc.page(firstPage.pageObjectNumber).annotations.list();
@@ -438,7 +438,7 @@ function runActionsConformance(runner, opts) {
438
438
  test("classifies a /S /JavaScript link target, script staying on base.actions", async () => {
439
439
  const fixture = opts.fixtures.javascriptLink;
440
440
  if (!fixture) return;
441
- const doc = await open(engine, opts, fixture);
441
+ const doc = await open$1(engine, opts, fixture);
442
442
  try {
443
443
  const firstPage = (await doc.pages.list()).pages[0];
444
444
  const link = (await doc.page(firstPage.pageObjectNumber).annotations.list()).annotations.find((annotation) => annotation.subtype === "link");
@@ -451,7 +451,7 @@ function runActionsConformance(runner, opts) {
451
451
  }
452
452
  });
453
453
  test("keeps field actions separate from merged widget events", async () => {
454
- const doc = await open(engine, opts, opts.fixtures.field);
454
+ const doc = await open$1(engine, opts, opts.fixtures.field);
455
455
  try {
456
456
  const form = await doc.forms.list();
457
457
  expect(form.fields).toHaveLength(1);
@@ -465,7 +465,7 @@ function runActionsConformance(runner, opts) {
465
465
  }
466
466
  });
467
467
  test("carries interpreter payloads on every executable node", async () => {
468
- const doc = await open(engine, opts, opts.fixtures.payloads);
468
+ const doc = await open$1(engine, opts, opts.fixtures.payloads);
469
469
  try {
470
470
  const page = (await doc.pages.list()).pages[0];
471
471
  const snapshot = await doc.page(page.pageObjectNumber).annotations.list();
@@ -626,7 +626,7 @@ function runActionsConformance(runner, opts) {
626
626
  }
627
627
  });
628
628
  test("degrades unreadable payloads to unknown with payload-dropped", async () => {
629
- const doc = await open(engine, opts, opts.fixtures.payloads);
629
+ const doc = await open$1(engine, opts, opts.fixtures.payloads);
630
630
  try {
631
631
  const page = (await doc.pages.list()).pages[0];
632
632
  const snapshot = await doc.page(page.pageObjectNumber).annotations.list();
@@ -650,7 +650,7 @@ function runActionsConformance(runner, opts) {
650
650
  }
651
651
  });
652
652
  test("reads a destination-form /OpenAction as openDestination", async () => {
653
- const doc = await open(engine, opts, opts.fixtures.openDestination);
653
+ const doc = await open$1(engine, opts, opts.fixtures.openDestination);
654
654
  try {
655
655
  expect(Boolean(doc.actions)).toBe(true);
656
656
  const snapshot = await doc.actions.read();
@@ -670,7 +670,7 @@ function runActionsConformance(runner, opts) {
670
670
  });
671
671
  });
672
672
  }
673
- async function open(engine, opts, fixture) {
673
+ async function open$1(engine, opts, fixture) {
674
674
  if (opts.openKind === "bytes") return engine.open({
675
675
  kind: "bytes",
676
676
  id: fixture.id,
@@ -808,7 +808,7 @@ function runAnnotationMutationConformance(runner, opts) {
808
808
  if (engine) await engine.destroy();
809
809
  });
810
810
  test("create appends without shifting indices and leaves weak refs valid", async () => {
811
- const doc = await openFixture$15(engine, opts);
811
+ const doc = await openFixture$18(engine, opts);
812
812
  try {
813
813
  const page = doc.page(fix.pageObjectNumber);
814
814
  const before = await page.annotations.list();
@@ -844,7 +844,7 @@ function runAnnotationMutationConformance(runner, opts) {
844
844
  }
845
845
  });
846
846
  test("create + update honor annotation flags (set on create, merge on patch)", async () => {
847
- const doc = await openFixture$15(engine, opts);
847
+ const doc = await openFixture$18(engine, opts);
848
848
  try {
849
849
  const page = doc.page(fix.pageObjectNumber);
850
850
  const draft = {
@@ -879,7 +879,7 @@ function runAnnotationMutationConformance(runner, opts) {
879
879
  }
880
880
  });
881
881
  test("create shape annotations (circle + square) round-trip shape fields", async () => {
882
- const doc = await openFixture$15(engine, opts);
882
+ const doc = await openFixture$18(engine, opts);
883
883
  try {
884
884
  const page = doc.page(fix.pageObjectNumber);
885
885
  const circleDraft = {
@@ -960,7 +960,7 @@ function runAnnotationMutationConformance(runner, opts) {
960
960
  }
961
961
  });
962
962
  test("cloudy border (/BE) + rect differences (/RD) are tri-state", async () => {
963
- const doc = await openFixture$15(engine, opts);
963
+ const doc = await openFixture$18(engine, opts);
964
964
  try {
965
965
  const page = doc.page(fix.pageObjectNumber);
966
966
  const plainDraft = {
@@ -1039,7 +1039,7 @@ function runAnnotationMutationConformance(runner, opts) {
1039
1039
  }
1040
1040
  });
1041
1041
  test("update echoes the appearance verdict: moves preserve /AP, restyles re-bake", async () => {
1042
- const doc = await openFixture$15(engine, opts);
1042
+ const doc = await openFixture$18(engine, opts);
1043
1043
  try {
1044
1044
  const page = doc.page(fix.pageObjectNumber);
1045
1045
  const draft = {
@@ -1114,7 +1114,7 @@ function runAnnotationMutationConformance(runner, opts) {
1114
1114
  }
1115
1115
  });
1116
1116
  test("a partial /DA patch preserves the unpatched font members", async () => {
1117
- const doc = await openFixture$15(engine, opts);
1117
+ const doc = await openFixture$18(engine, opts);
1118
1118
  try {
1119
1119
  const page = doc.page(fix.pageObjectNumber);
1120
1120
  const draft = {
@@ -1170,7 +1170,7 @@ function runAnnotationMutationConformance(runner, opts) {
1170
1170
  }
1171
1171
  });
1172
1172
  test("box transform is tri-state: rect-only moves keep rotation, null flattens", async () => {
1173
- const doc = await openFixture$15(engine, opts);
1173
+ const doc = await openFixture$18(engine, opts);
1174
1174
  try {
1175
1175
  const page = doc.page(fix.pageObjectNumber);
1176
1176
  const draft = {
@@ -1231,7 +1231,7 @@ function runAnnotationMutationConformance(runner, opts) {
1231
1231
  }
1232
1232
  });
1233
1233
  test("create vertex + line annotations (polygon/polyline/line) round-trip geometry", async () => {
1234
- const doc = await openFixture$15(engine, opts);
1234
+ const doc = await openFixture$18(engine, opts);
1235
1235
  try {
1236
1236
  const page = doc.page(fix.pageObjectNumber);
1237
1237
  const polygonDraft = {
@@ -1362,7 +1362,7 @@ function runAnnotationMutationConformance(runner, opts) {
1362
1362
  }
1363
1363
  });
1364
1364
  test("create free-text + callout round-trip text/colour/intent fields", async () => {
1365
- const doc = await openFixture$15(engine, opts);
1365
+ const doc = await openFixture$18(engine, opts);
1366
1366
  try {
1367
1367
  const page = doc.page(fix.pageObjectNumber);
1368
1368
  const freeTextDraft = {
@@ -1458,7 +1458,7 @@ function runAnnotationMutationConformance(runner, opts) {
1458
1458
  }
1459
1459
  });
1460
1460
  test("update a free-text patches alignment + colours and is non-structural", async () => {
1461
- const doc = await openFixture$15(engine, opts);
1461
+ const doc = await openFixture$18(engine, opts);
1462
1462
  try {
1463
1463
  const page = doc.page(fix.pageObjectNumber);
1464
1464
  const created = await page.annotations.create({
@@ -1518,8 +1518,289 @@ function runAnnotationMutationConformance(runner, opts) {
1518
1518
  await doc.close();
1519
1519
  }
1520
1520
  });
1521
+ test("rich text: a plain draft reads back body-style paragraphs and a body from its font", async () => {
1522
+ const doc = await openFixture$18(engine, opts);
1523
+ try {
1524
+ const created = await doc.page(fix.pageObjectNumber).annotations.create({
1525
+ subtype: "free-text",
1526
+ intent: "free-text",
1527
+ contents: "Plain\rtext",
1528
+ rect: shapeRect,
1529
+ fontFamily: "helvetica-bold",
1530
+ fontSize: 18,
1531
+ textAlign: "center",
1532
+ color: {
1533
+ r: 0,
1534
+ g: 0,
1535
+ b: 0
1536
+ }
1537
+ });
1538
+ expect(created.created.subtype).toBe("free-text");
1539
+ if (created.created.subtype === "free-text") {
1540
+ const rich = created.created.richText;
1541
+ expect(created.created.fontFamily).toBe("helvetica-bold");
1542
+ expect(rich.body.family).toBe("Helvetica");
1543
+ expect(rich.body.weight).toBe(700);
1544
+ expect(rich.body.italic).toBe(false);
1545
+ expect(rich.body.size).toBe(18);
1546
+ expect(rich.body.align).toBe("center");
1547
+ expect(rich.paragraphs).toEqual([{ runs: [{ text: "Plain" }] }, { runs: [{ text: "text" }] }]);
1548
+ }
1549
+ } finally {
1550
+ await doc.close();
1551
+ }
1552
+ });
1553
+ test("rich text: a draft with runs round-trips the body, the runs and the plain projection", async () => {
1554
+ const doc = await openFixture$18(engine, opts);
1555
+ try {
1556
+ const page = doc.page(fix.pageObjectNumber);
1557
+ const created = await page.annotations.create({
1558
+ subtype: "free-text",
1559
+ intent: "free-text",
1560
+ rect: shapeRect,
1561
+ fontFamily: "helvetica",
1562
+ fontSize: 12,
1563
+ textAlign: "left",
1564
+ color: {
1565
+ r: 0,
1566
+ g: 0,
1567
+ b: 255
1568
+ },
1569
+ richText: {
1570
+ body: {
1571
+ family: "Helvetica",
1572
+ size: 18,
1573
+ color: "#102030"
1574
+ },
1575
+ paragraphs: [{ runs: [
1576
+ { text: "Hello " },
1577
+ {
1578
+ text: "bold",
1579
+ style: { weight: 700 }
1580
+ },
1581
+ {
1582
+ text: " red",
1583
+ style: { color: "#FF0000" }
1584
+ }
1585
+ ] }, {
1586
+ align: "center",
1587
+ runs: [{ text: "H" }, {
1588
+ text: "2",
1589
+ style: { script: "sub" }
1590
+ }]
1591
+ }]
1592
+ }
1593
+ });
1594
+ expect(AnnotationCreateResultSchema.safeParse(created).success).toBe(true);
1595
+ expect(created.created.subtype).toBe("free-text");
1596
+ if (created.created.subtype === "free-text") {
1597
+ const dto = created.created;
1598
+ expect(dto.contents).toBe("Hello bold red\rH2");
1599
+ expect(dto.fontFamily).toBe("helvetica");
1600
+ expect(dto.fontSize).toBe(18);
1601
+ expect(dto.color).toMatchObject({
1602
+ r: 0,
1603
+ g: 0,
1604
+ b: 255
1605
+ });
1606
+ expect(dto.richText.body.color).toBe("#102030");
1607
+ expect(dto.richText.paragraphs[0].runs).toEqual([
1608
+ { text: "Hello " },
1609
+ {
1610
+ text: "bold",
1611
+ style: { weight: 700 }
1612
+ },
1613
+ {
1614
+ text: " red",
1615
+ style: { color: "#FF0000" }
1616
+ }
1617
+ ]);
1618
+ expect(dto.richText.paragraphs[1].align).toBe("center");
1619
+ expect(dto.richText.paragraphs[1].runs[1]).toEqual({
1620
+ text: "2",
1621
+ style: { script: "sub" }
1622
+ });
1623
+ }
1624
+ const listed = (await page.annotations.list()).annotations.find((a) => a.index === created.created.index);
1625
+ expect(listed?.subtype).toBe("free-text");
1626
+ if (listed?.subtype === "free-text") expect(listed.richText.paragraphs[0].runs[1]).toEqual({
1627
+ text: "bold",
1628
+ style: { weight: 700 }
1629
+ });
1630
+ } finally {
1631
+ await doc.close();
1632
+ }
1633
+ });
1634
+ test("rich text: paragraphs-only patch keeps the body; contents-only rewrites body-style paragraphs", async () => {
1635
+ const doc = await openFixture$18(engine, opts);
1636
+ try {
1637
+ const page = doc.page(fix.pageObjectNumber);
1638
+ const ref = (await page.annotations.create({
1639
+ subtype: "free-text",
1640
+ intent: "free-text",
1641
+ rect: shapeRect,
1642
+ fontFamily: "helvetica",
1643
+ fontSize: 14,
1644
+ textAlign: "center",
1645
+ color: {
1646
+ r: 0,
1647
+ g: 0,
1648
+ b: 0
1649
+ },
1650
+ richText: {
1651
+ body: {
1652
+ family: "Helvetica",
1653
+ size: 14
1654
+ },
1655
+ paragraphs: [{ runs: [{ text: "a" }, {
1656
+ text: "b",
1657
+ style: { weight: 700 }
1658
+ }] }]
1659
+ }
1660
+ })).created.ref;
1661
+ const restyled = await page.annotations.update(ref, {
1662
+ subtype: "free-text",
1663
+ richText: { paragraphs: [{ runs: [{
1664
+ text: "ab",
1665
+ style: { italic: true }
1666
+ }, { text: "c" }] }] }
1667
+ });
1668
+ expect(AnnotationUpdateResultSchema.safeParse(restyled).success).toBe(true);
1669
+ expect(restyled.updated.subtype).toBe("free-text");
1670
+ if (restyled.updated.subtype === "free-text") {
1671
+ expect(restyled.updated.contents).toBe("abc");
1672
+ expect(restyled.updated.richText.body.size).toBe(14);
1673
+ expect(restyled.updated.richText.body.align).toBe("center");
1674
+ expect(restyled.updated.textAlign).toBe("center");
1675
+ expect(restyled.updated.richText.paragraphs[0].runs[0]).toEqual({
1676
+ text: "ab",
1677
+ style: { italic: true }
1678
+ });
1679
+ }
1680
+ const rewritten = await page.annotations.update(ref, {
1681
+ subtype: "free-text",
1682
+ contents: "one\rtwo"
1683
+ });
1684
+ expect(rewritten.updated.subtype).toBe("free-text");
1685
+ if (rewritten.updated.subtype === "free-text") {
1686
+ expect(rewritten.updated.contents).toBe("one\rtwo");
1687
+ expect(rewritten.updated.richText.paragraphs).toEqual([{ runs: [{ text: "one" }] }, { runs: [{ text: "two" }] }]);
1688
+ expect(rewritten.updated.richText.body.size).toBe(14);
1689
+ }
1690
+ expect(rewritten.meta.weakRefsInvalidated).toBe(false);
1691
+ } finally {
1692
+ await doc.close();
1693
+ }
1694
+ });
1695
+ test("rich text: fontSize / fontColor / fontFamily move the body while runs keep their deltas", async () => {
1696
+ const doc = await openFixture$18(engine, opts);
1697
+ try {
1698
+ const page = doc.page(fix.pageObjectNumber);
1699
+ const created = await page.annotations.create({
1700
+ subtype: "free-text",
1701
+ intent: "free-text",
1702
+ rect: shapeRect,
1703
+ fontFamily: "helvetica",
1704
+ fontSize: 14,
1705
+ textAlign: "left",
1706
+ color: {
1707
+ r: 0,
1708
+ g: 0,
1709
+ b: 0
1710
+ },
1711
+ richText: {
1712
+ body: {
1713
+ family: "Helvetica",
1714
+ size: 14
1715
+ },
1716
+ paragraphs: [{ runs: [{ text: "a" }, {
1717
+ text: "b",
1718
+ style: { size: 30 }
1719
+ }] }]
1720
+ }
1721
+ });
1722
+ const updated = await page.annotations.update(created.created.ref, {
1723
+ subtype: "free-text",
1724
+ fontSize: 20,
1725
+ fontColor: {
1726
+ r: 255,
1727
+ g: 0,
1728
+ b: 0
1729
+ },
1730
+ fontFamily: "times-bold"
1731
+ });
1732
+ expect(updated.updated.subtype).toBe("free-text");
1733
+ if (updated.updated.subtype === "free-text") {
1734
+ const dto = updated.updated;
1735
+ expect(dto.fontSize).toBe(20);
1736
+ expect(dto.fontFamily).toBe("times-bold");
1737
+ expect(dto.richText.body.size).toBe(20);
1738
+ expect(dto.richText.body.color).toBe("#FF0000");
1739
+ expect(dto.richText.body.family).toBe("Times");
1740
+ expect(dto.richText.body.weight).toBe(700);
1741
+ expect(dto.richText.paragraphs[0].runs[1]).toEqual({
1742
+ text: "b",
1743
+ style: { size: 30 }
1744
+ });
1745
+ }
1746
+ } finally {
1747
+ await doc.close();
1748
+ }
1749
+ });
1750
+ test("rich text: contents and richText that disagree reject with InvalidArg; agreeing writes the runs", async () => {
1751
+ const doc = await openFixture$18(engine, opts);
1752
+ try {
1753
+ const page = doc.page(fix.pageObjectNumber);
1754
+ const created = await page.annotations.create({
1755
+ subtype: "free-text",
1756
+ intent: "free-text",
1757
+ contents: "x",
1758
+ rect: shapeRect,
1759
+ fontFamily: "helvetica",
1760
+ fontSize: 14,
1761
+ textAlign: "left",
1762
+ color: {
1763
+ r: 0,
1764
+ g: 0,
1765
+ b: 0
1766
+ }
1767
+ });
1768
+ const ref = created.created.ref;
1769
+ let caught;
1770
+ try {
1771
+ await page.annotations.update(ref, {
1772
+ subtype: "free-text",
1773
+ contents: "stale",
1774
+ richText: { paragraphs: [{ runs: [{ text: "fresh" }] }] }
1775
+ });
1776
+ } catch (err) {
1777
+ caught = err;
1778
+ }
1779
+ expect(EngineError.is(caught, EngineErrorCode.InvalidArg)).toBe(true);
1780
+ const after = (await page.annotations.list()).annotations.find((a) => a.index === created.created.index);
1781
+ expect(after?.contents).toBe("x");
1782
+ const agreed = await page.annotations.update(ref, {
1783
+ subtype: "free-text",
1784
+ contents: "fresh",
1785
+ richText: { paragraphs: [{ runs: [{ text: "fr" }, {
1786
+ text: "esh",
1787
+ style: { weight: 700 }
1788
+ }] }] }
1789
+ });
1790
+ expect(agreed.updated.subtype).toBe("free-text");
1791
+ if (agreed.updated.subtype === "free-text") {
1792
+ expect(agreed.updated.contents).toBe("fresh");
1793
+ expect(agreed.updated.richText.paragraphs[0].runs[1]).toEqual({
1794
+ text: "esh",
1795
+ style: { weight: 700 }
1796
+ });
1797
+ }
1798
+ } finally {
1799
+ await doc.close();
1800
+ }
1801
+ });
1521
1802
  test("create redact (area + text) round-trips label + colour fields", async () => {
1522
- const doc = await openFixture$15(engine, opts);
1803
+ const doc = await openFixture$18(engine, opts);
1523
1804
  try {
1524
1805
  const page = doc.page(fix.pageObjectNumber);
1525
1806
  const areaDraft = {
@@ -1599,7 +1880,7 @@ function runAnnotationMutationConformance(runner, opts) {
1599
1880
  }
1600
1881
  });
1601
1882
  test("update a redact patches the label and clears it, non-structurally", async () => {
1602
- const doc = await openFixture$15(engine, opts);
1883
+ const doc = await openFixture$18(engine, opts);
1603
1884
  try {
1604
1885
  const page = doc.page(fix.pageObjectNumber);
1605
1886
  const created = await page.annotations.create({
@@ -1676,7 +1957,7 @@ function runAnnotationMutationConformance(runner, opts) {
1676
1957
  }
1677
1958
  });
1678
1959
  test("create + update a caret round-trips color/opacity/rect differences", async () => {
1679
- const doc = await openFixture$15(engine, opts);
1960
+ const doc = await openFixture$18(engine, opts);
1680
1961
  try {
1681
1962
  const page = doc.page(fix.pageObjectNumber);
1682
1963
  const caretDraft = {
@@ -1756,7 +2037,7 @@ function runAnnotationMutationConformance(runner, opts) {
1756
2037
  }
1757
2038
  });
1758
2039
  test("update an ink annotation patches strokes + color and is non-structural", async () => {
1759
- const doc = await openFixture$15(engine, opts);
2040
+ const doc = await openFixture$18(engine, opts);
1760
2041
  try {
1761
2042
  const page = doc.page(fix.pageObjectNumber);
1762
2043
  const created = await page.annotations.create({
@@ -1807,7 +2088,7 @@ function runAnnotationMutationConformance(runner, opts) {
1807
2088
  }
1808
2089
  });
1809
2090
  test("ink highlight intent + blend round-trip and unrelated patches preserve blend", async () => {
1810
- const doc = await openFixture$15(engine, opts);
2091
+ const doc = await openFixture$18(engine, opts);
1811
2092
  try {
1812
2093
  const page = doc.page(fix.pageObjectNumber);
1813
2094
  const created = await page.annotations.create({
@@ -1850,7 +2131,7 @@ function runAnnotationMutationConformance(runner, opts) {
1850
2131
  }
1851
2132
  });
1852
2133
  test("update a polyline patches vertices + line endings and is non-structural", async () => {
1853
- const doc = await openFixture$15(engine, opts);
2134
+ const doc = await openFixture$18(engine, opts);
1854
2135
  try {
1855
2136
  const page = doc.page(fix.pageObjectNumber);
1856
2137
  const created = await page.annotations.create({
@@ -1899,7 +2180,7 @@ function runAnnotationMutationConformance(runner, opts) {
1899
2180
  }
1900
2181
  });
1901
2182
  test("update a shape annotation patches color and is non-structural", async () => {
1902
- const doc = await openFixture$15(engine, opts);
2183
+ const doc = await openFixture$18(engine, opts);
1903
2184
  try {
1904
2185
  const page = doc.page(fix.pageObjectNumber);
1905
2186
  const created = await page.annotations.create({
@@ -1948,7 +2229,7 @@ function runAnnotationMutationConformance(runner, opts) {
1948
2229
  }
1949
2230
  });
1950
2231
  if (opts.supportsAppearanceRasters) test("creating a shape bakes an /AP appearance the reader can rasterize", async () => {
1951
- const doc = await openFixture$15(engine, opts);
2232
+ const doc = await openFixture$18(engine, opts);
1952
2233
  try {
1953
2234
  const page = doc.page(fix.pageObjectNumber);
1954
2235
  const created = await page.annotations.create({
@@ -1978,7 +2259,7 @@ function runAnnotationMutationConformance(runner, opts) {
1978
2259
  }
1979
2260
  });
1980
2261
  test("update on a durable annotation is non-structural and never touches /NM", async () => {
1981
- const doc = await openFixture$15(engine, opts);
2262
+ const doc = await openFixture$18(engine, opts);
1982
2263
  try {
1983
2264
  const page = doc.page(fix.pageObjectNumber);
1984
2265
  const before = await page.annotations.list();
@@ -2004,7 +2285,7 @@ function runAnnotationMutationConformance(runner, opts) {
2004
2285
  }
2005
2286
  });
2006
2287
  if (fix.expectsWeakAnnotation) test("update on a weak annotation stamps a UUID v4 /NM and upgrades the ref", async () => {
2007
- const doc = await openFixture$15(engine, opts);
2288
+ const doc = await openFixture$18(engine, opts);
2008
2289
  try {
2009
2290
  const page = doc.page(fix.pageObjectNumber);
2010
2291
  const before = await page.annotations.list();
@@ -2034,7 +2315,7 @@ function runAnnotationMutationConformance(runner, opts) {
2034
2315
  }
2035
2316
  });
2036
2317
  test("delete by objectNumber removes the annotation and reports a stable id", async () => {
2037
- const doc = await openFixture$15(engine, opts);
2318
+ const doc = await openFixture$18(engine, opts);
2038
2319
  try {
2039
2320
  const page = doc.page(fix.pageObjectNumber);
2040
2321
  const draft = {
@@ -2064,7 +2345,7 @@ function runAnnotationMutationConformance(runner, opts) {
2064
2345
  }
2065
2346
  });
2066
2347
  if (fix.expectsWeakAnnotation) test("delete by index of a weak annotation reports deleted: null and refetch reason", async () => {
2067
- const doc = await openFixture$15(engine, opts);
2348
+ const doc = await openFixture$18(engine, opts);
2068
2349
  try {
2069
2350
  const page = doc.page(fix.pageObjectNumber);
2070
2351
  const weak = (await page.annotations.list()).annotations.find((a) => a.identityQuality === "weak");
@@ -2087,7 +2368,7 @@ function runAnnotationMutationConformance(runner, opts) {
2087
2368
  }
2088
2369
  });
2089
2370
  test("abort on create rejects with AbortError", async () => {
2090
- const doc = await openFixture$15(engine, opts);
2371
+ const doc = await openFixture$18(engine, opts);
2091
2372
  try {
2092
2373
  const page = doc.page(fix.pageObjectNumber);
2093
2374
  const draft = {
@@ -2103,7 +2384,7 @@ function runAnnotationMutationConformance(runner, opts) {
2103
2384
  }
2104
2385
  });
2105
2386
  test("update with a stale index revision throws InvalidReference", async () => {
2106
- const doc = await openFixture$15(engine, opts);
2387
+ const doc = await openFixture$18(engine, opts);
2107
2388
  try {
2108
2389
  const page = doc.page(fix.pageObjectNumber);
2109
2390
  const weak = (await page.annotations.list()).annotations.find((a) => a.identityQuality === "weak");
@@ -2130,7 +2411,7 @@ function runAnnotationMutationConformance(runner, opts) {
2130
2411
  }
2131
2412
  });
2132
2413
  test("move single durable annotation reorders within the page (single-as-batch)", async () => {
2133
- const doc = await openFixture$15(engine, opts);
2414
+ const doc = await openFixture$18(engine, opts);
2134
2415
  try {
2135
2416
  const page = doc.page(fix.pageObjectNumber);
2136
2417
  const aDraft = {
@@ -2174,7 +2455,7 @@ function runAnnotationMutationConformance(runner, opts) {
2174
2455
  }
2175
2456
  });
2176
2457
  test("move multi-block preserves caller-supplied order at the destination", async () => {
2177
- const doc = await openFixture$15(engine, opts);
2458
+ const doc = await openFixture$18(engine, opts);
2178
2459
  try {
2179
2460
  const page = doc.page(fix.pageObjectNumber);
2180
2461
  const ids = await Promise.all([
@@ -2213,7 +2494,7 @@ function runAnnotationMutationConformance(runner, opts) {
2213
2494
  }
2214
2495
  });
2215
2496
  if (fix.expectsWeakAnnotation) test("move on a weak annotation upgrades it to durable /NM (one rev bump for batch)", async () => {
2216
- const doc = await openFixture$15(engine, opts);
2497
+ const doc = await openFixture$18(engine, opts);
2217
2498
  try {
2218
2499
  const page = doc.page(fix.pageObjectNumber);
2219
2500
  const before = await page.annotations.list();
@@ -2238,7 +2519,7 @@ function runAnnotationMutationConformance(runner, opts) {
2238
2519
  }
2239
2520
  });
2240
2521
  test("move with a stale index revision rejects (locked rev-token guard)", async () => {
2241
- const doc = await openFixture$15(engine, opts);
2522
+ const doc = await openFixture$18(engine, opts);
2242
2523
  try {
2243
2524
  const page = doc.page(fix.pageObjectNumber);
2244
2525
  const a = await page.annotations.create({
@@ -2276,7 +2557,7 @@ function runAnnotationMutationConformance(runner, opts) {
2276
2557
  }
2277
2558
  });
2278
2559
  test("move with out-of-range toIndex rejects with InvalidArg", async () => {
2279
- const doc = await openFixture$15(engine, opts);
2560
+ const doc = await openFixture$18(engine, opts);
2280
2561
  try {
2281
2562
  const page = doc.page(fix.pageObjectNumber);
2282
2563
  const a = await page.annotations.create({
@@ -2300,7 +2581,7 @@ function runAnnotationMutationConformance(runner, opts) {
2300
2581
  }
2301
2582
  });
2302
2583
  test("move with duplicate refs rejects with InvalidArg", async () => {
2303
- const doc = await openFixture$15(engine, opts);
2584
+ const doc = await openFixture$18(engine, opts);
2304
2585
  try {
2305
2586
  const page = doc.page(fix.pageObjectNumber);
2306
2587
  const a = await page.annotations.create({
@@ -2323,7 +2604,7 @@ function runAnnotationMutationConformance(runner, opts) {
2323
2604
  }
2324
2605
  });
2325
2606
  test("abort on move rejects with AbortError", async () => {
2326
- const doc = await openFixture$15(engine, opts);
2607
+ const doc = await openFixture$18(engine, opts);
2327
2608
  try {
2328
2609
  const page = doc.page(fix.pageObjectNumber);
2329
2610
  const a = await page.annotations.create({
@@ -2344,7 +2625,7 @@ function runAnnotationMutationConformance(runner, opts) {
2344
2625
  }
2345
2626
  });
2346
2627
  test("a page move does NOT bump per-page RevisionTokens (weak refs survive reorder)", async () => {
2347
- const doc = await openFixture$15(engine, opts);
2628
+ const doc = await openFixture$18(engine, opts);
2348
2629
  try {
2349
2630
  const list = await doc.pages.list();
2350
2631
  if (list.pages.length < 1) return;
@@ -2359,7 +2640,7 @@ function runAnnotationMutationConformance(runner, opts) {
2359
2640
  }
2360
2641
  });
2361
2642
  test("create a reply links /IRT and defaults /RT to \"reply\", reporting the parent", async () => {
2362
- const doc = await openFixture$15(engine, opts);
2643
+ const doc = await openFixture$18(engine, opts);
2363
2644
  try {
2364
2645
  const page = doc.page(fix.pageObjectNumber);
2365
2646
  const parent = await page.annotations.create({
@@ -2393,7 +2674,7 @@ function runAnnotationMutationConformance(runner, opts) {
2393
2674
  }
2394
2675
  });
2395
2676
  test("create a grouped subordinate writes /RT /Group", async () => {
2396
- const doc = await openFixture$15(engine, opts);
2677
+ const doc = await openFixture$18(engine, opts);
2397
2678
  try {
2398
2679
  const page = doc.page(fix.pageObjectNumber);
2399
2680
  const primary = await page.annotations.create({
@@ -2424,7 +2705,7 @@ function runAnnotationMutationConformance(runner, opts) {
2424
2705
  }
2425
2706
  });
2426
2707
  test("create link annotations round-trip uri, goto/xyz, goto/fitH, and dead targets", async () => {
2427
- const doc = await openFixture$15(engine, opts);
2708
+ const doc = await openFixture$18(engine, opts);
2428
2709
  try {
2429
2710
  const page = doc.page(fix.pageObjectNumber);
2430
2711
  const uri = await page.annotations.create({
@@ -2501,7 +2782,7 @@ function runAnnotationMutationConformance(runner, opts) {
2501
2782
  }
2502
2783
  });
2503
2784
  test("a link patch retargets in both directions (uri→goto, goto→uri) and moves the rect", async () => {
2504
- const doc = await openFixture$15(engine, opts);
2785
+ const doc = await openFixture$18(engine, opts);
2505
2786
  try {
2506
2787
  const page = doc.page(fix.pageObjectNumber);
2507
2788
  const created = await page.annotations.create({
@@ -2556,7 +2837,7 @@ function runAnnotationMutationConformance(runner, opts) {
2556
2837
  }
2557
2838
  });
2558
2839
  test("a link patch clears the target with null — a dead link on re-read", async () => {
2559
- const doc = await openFixture$15(engine, opts);
2840
+ const doc = await openFixture$18(engine, opts);
2560
2841
  try {
2561
2842
  const page = doc.page(fix.pageObjectNumber);
2562
2843
  const created = await page.annotations.create({
@@ -2595,7 +2876,7 @@ function runAnnotationMutationConformance(runner, opts) {
2595
2876
  }
2596
2877
  });
2597
2878
  test("a link grouped to a highlight round-trips /IRT + /RT /Group with its target", async () => {
2598
- const doc = await openFixture$15(engine, opts);
2879
+ const doc = await openFixture$18(engine, opts);
2599
2880
  try {
2600
2881
  const page = doc.page(fix.pageObjectNumber);
2601
2882
  const parent = await page.annotations.create({
@@ -2629,7 +2910,7 @@ function runAnnotationMutationConformance(runner, opts) {
2629
2910
  }
2630
2911
  });
2631
2912
  test("replace-text round-trips /IT and groups StrikeOut under its Caret", async () => {
2632
- const doc = await openFixture$15(engine, opts);
2913
+ const doc = await openFixture$18(engine, opts);
2633
2914
  try {
2634
2915
  const page = doc.page(fix.pageObjectNumber);
2635
2916
  const caret = await page.annotations.create({
@@ -2681,7 +2962,7 @@ function runAnnotationMutationConformance(runner, opts) {
2681
2962
  }
2682
2963
  });
2683
2964
  test("patch inReplyTo: null clears /IRT and /RT (back to top-level)", async () => {
2684
- const doc = await openFixture$15(engine, opts);
2965
+ const doc = await openFixture$18(engine, opts);
2685
2966
  try {
2686
2967
  const page = doc.page(fix.pageObjectNumber);
2687
2968
  const parent = await page.annotations.create({
@@ -2708,7 +2989,7 @@ function runAnnotationMutationConformance(runner, opts) {
2708
2989
  }
2709
2990
  });
2710
2991
  test("create with a cross-page /IRT parent throws InvalidArg", async () => {
2711
- const doc = await openFixture$15(engine, opts);
2992
+ const doc = await openFixture$18(engine, opts);
2712
2993
  try {
2713
2994
  const page = doc.page(fix.pageObjectNumber);
2714
2995
  const parent = await page.annotations.create({
@@ -2739,7 +3020,7 @@ function runAnnotationMutationConformance(runner, opts) {
2739
3020
  }
2740
3021
  });
2741
3022
  test("a review-status reply round-trips /State + /StateModel + /Subj", async () => {
2742
- const doc = await openFixture$15(engine, opts);
3023
+ const doc = await openFixture$18(engine, opts);
2743
3024
  try {
2744
3025
  const page = doc.page(fix.pageObjectNumber);
2745
3026
  const target = await page.annotations.create({
@@ -2773,7 +3054,7 @@ function runAnnotationMutationConformance(runner, opts) {
2773
3054
  }
2774
3055
  });
2775
3056
  test("a draft /State without /StateModel is rejected with InvalidArg", async () => {
2776
- const doc = await openFixture$15(engine, opts);
3057
+ const doc = await openFixture$18(engine, opts);
2777
3058
  try {
2778
3059
  const page = doc.page(fix.pageObjectNumber);
2779
3060
  let caught;
@@ -2792,7 +3073,7 @@ function runAnnotationMutationConformance(runner, opts) {
2792
3073
  }
2793
3074
  });
2794
3075
  test("null-clear patches truly remove /State, /StateModel, /Subj and /Contents", async () => {
2795
- const doc = await openFixture$15(engine, opts);
3076
+ const doc = await openFixture$18(engine, opts);
2796
3077
  try {
2797
3078
  const page = doc.page(fix.pageObjectNumber);
2798
3079
  const note = await page.annotations.create({
@@ -2839,7 +3120,7 @@ function runAnnotationMutationConformance(runner, opts) {
2839
3120
  }
2840
3121
  });
2841
3122
  test("custom state models round-trip verbatim", async () => {
2842
- const doc = await openFixture$15(engine, opts);
3123
+ const doc = await openFixture$18(engine, opts);
2843
3124
  try {
2844
3125
  const page = doc.page(fix.pageObjectNumber);
2845
3126
  const custom = await page.annotations.create({
@@ -2867,7 +3148,7 @@ async function beginWeakEditIfRequired(doc, pageObjectNumber, fix) {
2867
3148
  if (doc.capabilities.weakAnnotationEditSessions !== "required" || !fix.expectsWeakAnnotation) return null;
2868
3149
  return doc.annotations.beginWeakEdit([pageObjectNumber]);
2869
3150
  }
2870
- async function openFixture$15(engine, opts) {
3151
+ async function openFixture$18(engine, opts) {
2871
3152
  if (opts.openKind === "bytes") {
2872
3153
  const bytes = await opts.fixture.bytes();
2873
3154
  return engine.open({
@@ -3457,7 +3738,7 @@ function runAnnotationAppearanceConformance(runner, opts) {
3457
3738
  if (engine) await engine.destroy();
3458
3739
  });
3459
3740
  test("renders the expected set of appearances with valid output", async () => {
3460
- const doc = await openFixture$14(engine, opts);
3741
+ const doc = await openFixture$17(engine, opts);
3461
3742
  try {
3462
3743
  const { pageState, appearances } = await collect(doc, opts, useRaw);
3463
3744
  expect(pageState.pageObjectNumber).toBe(opts.fixture.pageObjectNumber);
@@ -3484,7 +3765,7 @@ function runAnnotationAppearanceConformance(runner, opts) {
3484
3765
  }
3485
3766
  });
3486
3767
  if (opts.fixture.expectsWeakAppearance) test("weak (index-only) annotations are emitted, not dropped", async () => {
3487
- const doc = await openFixture$14(engine, opts);
3768
+ const doc = await openFixture$17(engine, opts);
3488
3769
  try {
3489
3770
  const { appearances } = await collect(doc, opts, useRaw);
3490
3771
  const weak = appearances.find((a) => a.ref.kind === "index");
@@ -3494,7 +3775,7 @@ function runAnnotationAppearanceConformance(runner, opts) {
3494
3775
  }
3495
3776
  });
3496
3777
  if (useRaw) test("rendered appearances are not blank (non-zero alpha)", async () => {
3497
- const doc = await openFixture$14(engine, opts);
3778
+ const doc = await openFixture$17(engine, opts);
3498
3779
  try {
3499
3780
  const { appearances } = await collect(doc, opts, useRaw);
3500
3781
  const anyVisible = appearances.some((a) => a.raster !== null && hasOpaquePixel(a.raster));
@@ -3504,7 +3785,7 @@ function runAnnotationAppearanceConformance(runner, opts) {
3504
3785
  }
3505
3786
  });
3506
3787
  test("abort() rejects with AbortError", async () => {
3507
- const doc = await openFixture$14(engine, opts);
3788
+ const doc = await openFixture$17(engine, opts);
3508
3789
  try {
3509
3790
  const page = doc.page(opts.fixture.pageObjectNumber);
3510
3791
  const p = useRaw ? page.annotations.renderAppearances({ scale: 1 }) : page.annotations.renderAppearanceImages({
@@ -3559,7 +3840,7 @@ function hasOpaquePixel(raster) {
3559
3840
  }
3560
3841
  return false;
3561
3842
  }
3562
- async function openFixture$14(engine, opts) {
3843
+ async function openFixture$17(engine, opts) {
3563
3844
  if (opts.openKind === "bytes") {
3564
3845
  const bytes = await opts.fixture.bytes();
3565
3846
  return engine.open({
@@ -3628,7 +3909,7 @@ function runPageReorderConformance(runner, opts) {
3628
3909
  if (engine) await engine.destroy();
3629
3910
  });
3630
3911
  test("pages.list() returns every page in display order, durable PONs only", async () => {
3631
- const doc = await openFixture$13(engine, opts);
3912
+ const doc = await openFixture$16(engine, opts);
3632
3913
  try {
3633
3914
  const list = await doc.pages.list();
3634
3915
  expect(PageListSnapshotSchema.safeParse(list).success).toBe(true);
@@ -3647,7 +3928,7 @@ function runPageReorderConformance(runner, opts) {
3647
3928
  }
3648
3929
  });
3649
3930
  test("pages.move() reorders pages and returns the full post-move order", async () => {
3650
- const doc = await openFixture$13(engine, opts);
3931
+ const doc = await openFixture$16(engine, opts);
3651
3932
  try {
3652
3933
  const before = await doc.pages.list();
3653
3934
  if (before.pages.length < 3) return;
@@ -3671,7 +3952,7 @@ function runPageReorderConformance(runner, opts) {
3671
3952
  }
3672
3953
  });
3673
3954
  test("weak index-based annotation refs survive a page reorder", async () => {
3674
- const doc = await openFixture$13(engine, opts);
3955
+ const doc = await openFixture$16(engine, opts);
3675
3956
  try {
3676
3957
  const list = await doc.pages.list();
3677
3958
  if (list.pages.length < 2) return;
@@ -3708,7 +3989,7 @@ function runPageReorderConformance(runner, opts) {
3708
3989
  }
3709
3990
  });
3710
3991
  test("pages.move() rejects duplicate PONs with InvalidArg", async () => {
3711
- const doc = await openFixture$13(engine, opts);
3992
+ const doc = await openFixture$16(engine, opts);
3712
3993
  try {
3713
3994
  const list = await doc.pages.list();
3714
3995
  if (list.pages.length < 1) return;
@@ -3725,7 +4006,7 @@ function runPageReorderConformance(runner, opts) {
3725
4006
  }
3726
4007
  });
3727
4008
  test("pages.move() rejects out-of-range destIndex with InvalidArg", async () => {
3728
- const doc = await openFixture$13(engine, opts);
4009
+ const doc = await openFixture$16(engine, opts);
3729
4010
  try {
3730
4011
  const list = await doc.pages.list();
3731
4012
  if (list.pages.length < 1) return;
@@ -3742,7 +4023,7 @@ function runPageReorderConformance(runner, opts) {
3742
4023
  }
3743
4024
  });
3744
4025
  test("pages.move() rejects unknown PON with NotFound or InvalidArg", async () => {
3745
- const doc = await openFixture$13(engine, opts);
4026
+ const doc = await openFixture$16(engine, opts);
3746
4027
  try {
3747
4028
  const list = await doc.pages.list();
3748
4029
  let bogus = 0;
@@ -3760,7 +4041,7 @@ function runPageReorderConformance(runner, opts) {
3760
4041
  }
3761
4042
  });
3762
4043
  test("abort on pages.move() rejects with AbortError", async () => {
3763
- const doc = await openFixture$13(engine, opts);
4044
+ const doc = await openFixture$16(engine, opts);
3764
4045
  try {
3765
4046
  const list = await doc.pages.list();
3766
4047
  if (list.pages.length < 1) return;
@@ -3774,7 +4055,7 @@ function runPageReorderConformance(runner, opts) {
3774
4055
  });
3775
4056
  });
3776
4057
  }
3777
- async function openFixture$13(engine, opts) {
4058
+ async function openFixture$16(engine, opts) {
3778
4059
  if (opts.openKind === "bytes") {
3779
4060
  const bytes = await opts.fixture.bytes();
3780
4061
  return engine.open({
@@ -3848,7 +4129,7 @@ function runPageRotateConformance(runner, opts) {
3848
4129
  if (engine) await engine.destroy();
3849
4130
  });
3850
4131
  test("pages.rotate() sets the absolute rotation and returns the full layout", async () => {
3851
- const doc = await openFixture$12(engine, opts);
4132
+ const doc = await openFixture$15(engine, opts);
3852
4133
  try {
3853
4134
  const before = await doc.pages.list();
3854
4135
  const target = before.pages[0];
@@ -3867,7 +4148,7 @@ function runPageRotateConformance(runner, opts) {
3867
4148
  }
3868
4149
  });
3869
4150
  test("absolute rotation is idempotent: setting the same value twice is a no-op", async () => {
3870
- const doc = await openFixture$12(engine, opts);
4151
+ const doc = await openFixture$15(engine, opts);
3871
4152
  try {
3872
4153
  const pon = (await doc.pages.list()).pages[0].pageObjectNumber;
3873
4154
  const first = await doc.pages.rotate([pon], 180);
@@ -3878,7 +4159,7 @@ function runPageRotateConformance(runner, opts) {
3878
4159
  }
3879
4160
  });
3880
4161
  test("one rotation applies to EVERY listed page (the multi-select gesture)", async () => {
3881
- const doc = await openFixture$12(engine, opts);
4162
+ const doc = await openFixture$15(engine, opts);
3882
4163
  try {
3883
4164
  const list = await doc.pages.list();
3884
4165
  if (list.pages.length < 2) return;
@@ -3890,7 +4171,7 @@ function runPageRotateConformance(runner, opts) {
3890
4171
  }
3891
4172
  });
3892
4173
  test("weak index-based annotation refs survive a rotate (no revision bump)", async () => {
3893
- const doc = await openFixture$12(engine, opts);
4174
+ const doc = await openFixture$15(engine, opts);
3894
4175
  try {
3895
4176
  const hostPon = (await doc.pages.list()).pages[0].pageObjectNumber;
3896
4177
  const hostPage = doc.page(hostPon);
@@ -3920,7 +4201,7 @@ function runPageRotateConformance(runner, opts) {
3920
4201
  }
3921
4202
  });
3922
4203
  test("pages.rotate() rejects an invalid rotation value with InvalidArg", async () => {
3923
- const doc = await openFixture$12(engine, opts);
4204
+ const doc = await openFixture$15(engine, opts);
3924
4205
  try {
3925
4206
  const pon = (await doc.pages.list()).pages[0].pageObjectNumber;
3926
4207
  let caught;
@@ -3935,7 +4216,7 @@ function runPageRotateConformance(runner, opts) {
3935
4216
  }
3936
4217
  });
3937
4218
  test("pages.rotate() rejects duplicate PONs with InvalidArg", async () => {
3938
- const doc = await openFixture$12(engine, opts);
4219
+ const doc = await openFixture$15(engine, opts);
3939
4220
  try {
3940
4221
  const pon = (await doc.pages.list()).pages[0].pageObjectNumber;
3941
4222
  let caught;
@@ -3950,7 +4231,7 @@ function runPageRotateConformance(runner, opts) {
3950
4231
  }
3951
4232
  });
3952
4233
  test("pages.rotate() rejects unknown PON with NotFound or InvalidArg", async () => {
3953
- const doc = await openFixture$12(engine, opts);
4234
+ const doc = await openFixture$15(engine, opts);
3954
4235
  try {
3955
4236
  const list = await doc.pages.list();
3956
4237
  let bogus = 0;
@@ -3968,7 +4249,7 @@ function runPageRotateConformance(runner, opts) {
3968
4249
  }
3969
4250
  });
3970
4251
  test("abort on pages.rotate() rejects with AbortError", async () => {
3971
- const doc = await openFixture$12(engine, opts);
4252
+ const doc = await openFixture$15(engine, opts);
3972
4253
  try {
3973
4254
  const pon = (await doc.pages.list()).pages[0].pageObjectNumber;
3974
4255
  const p = doc.pages.rotate([pon], 90);
@@ -3980,7 +4261,7 @@ function runPageRotateConformance(runner, opts) {
3980
4261
  });
3981
4262
  });
3982
4263
  }
3983
- async function openFixture$12(engine, opts) {
4264
+ async function openFixture$15(engine, opts) {
3984
4265
  if (opts.openKind === "bytes") {
3985
4266
  const bytes = await opts.fixture.bytes();
3986
4267
  return engine.open({
@@ -4043,7 +4324,7 @@ function runPageDeleteConformance(runner, opts) {
4043
4324
  if (engine) await engine.destroy();
4044
4325
  });
4045
4326
  test("pages.delete() removes the page and returns the surviving layout", async () => {
4046
- const doc = await openFixture$11(engine, opts);
4327
+ const doc = await openFixture$14(engine, opts);
4047
4328
  try {
4048
4329
  const before = await doc.pages.list();
4049
4330
  if (before.pages.length < 2) return;
@@ -4064,7 +4345,7 @@ function runPageDeleteConformance(runner, opts) {
4064
4345
  }
4065
4346
  });
4066
4347
  test("addressing a deleted page afterwards is a clean NotFound", async () => {
4067
- const doc = await openFixture$11(engine, opts);
4348
+ const doc = await openFixture$14(engine, opts);
4068
4349
  try {
4069
4350
  const before = await doc.pages.list();
4070
4351
  if (before.pages.length < 2) return;
@@ -4082,7 +4363,7 @@ function runPageDeleteConformance(runner, opts) {
4082
4363
  }
4083
4364
  });
4084
4365
  test("surviving pages keep their weak index-based annotation refs", async () => {
4085
- const doc = await openFixture$11(engine, opts);
4366
+ const doc = await openFixture$14(engine, opts);
4086
4367
  try {
4087
4368
  const list = await doc.pages.list();
4088
4369
  if (list.pages.length < 2) return;
@@ -4115,7 +4396,7 @@ function runPageDeleteConformance(runner, opts) {
4115
4396
  }
4116
4397
  });
4117
4398
  test("deleting EVERY page is rejected with InvalidArg", async () => {
4118
- const doc = await openFixture$11(engine, opts);
4399
+ const doc = await openFixture$14(engine, opts);
4119
4400
  try {
4120
4401
  const list = await doc.pages.list();
4121
4402
  let caught;
@@ -4130,7 +4411,7 @@ function runPageDeleteConformance(runner, opts) {
4130
4411
  }
4131
4412
  });
4132
4413
  test("pages.delete() rejects duplicate PONs with InvalidArg", async () => {
4133
- const doc = await openFixture$11(engine, opts);
4414
+ const doc = await openFixture$14(engine, opts);
4134
4415
  try {
4135
4416
  const pon = (await doc.pages.list()).pages[0].pageObjectNumber;
4136
4417
  let caught;
@@ -4145,7 +4426,7 @@ function runPageDeleteConformance(runner, opts) {
4145
4426
  }
4146
4427
  });
4147
4428
  test("pages.delete() rejects unknown PON with NotFound or InvalidArg", async () => {
4148
- const doc = await openFixture$11(engine, opts);
4429
+ const doc = await openFixture$14(engine, opts);
4149
4430
  try {
4150
4431
  const list = await doc.pages.list();
4151
4432
  let bogus = 0;
@@ -4163,7 +4444,7 @@ function runPageDeleteConformance(runner, opts) {
4163
4444
  }
4164
4445
  });
4165
4446
  test("abort on pages.delete() rejects with AbortError", async () => {
4166
- const doc = await openFixture$11(engine, opts);
4447
+ const doc = await openFixture$14(engine, opts);
4167
4448
  try {
4168
4449
  const list = await doc.pages.list();
4169
4450
  if (list.pages.length < 2) return;
@@ -4177,7 +4458,224 @@ function runPageDeleteConformance(runner, opts) {
4177
4458
  });
4178
4459
  });
4179
4460
  }
4180
- async function openFixture$11(engine, opts) {
4461
+ async function openFixture$14(engine, opts) {
4462
+ if (opts.openKind === "bytes") {
4463
+ const bytes = await opts.fixture.bytes();
4464
+ return engine.open({
4465
+ kind: "bytes",
4466
+ id: opts.fixture.id,
4467
+ bytes
4468
+ });
4469
+ }
4470
+ return engine.open({
4471
+ kind: "id",
4472
+ id: opts.fixture.cloudId ?? opts.fixture.id
4473
+ });
4474
+ }
4475
+ //#endregion
4476
+ //#region src/conformance/runNamedPagesConformance.ts
4477
+ /**
4478
+ * Named-pages conformance: the catalog's `/Names /Pages` registry as LAYOUT
4479
+ * data. Locks the invariants both engines must share — do not loosen
4480
+ * without re-reading `NamedPageEntry` and `PageNameResult`:
4481
+ *
4482
+ * 1. `pages.list().namedPages` is present (possibly empty) and every
4483
+ * registration resolves to a page in `pages`, a template, or is
4484
+ * reported dangling — never silently dropped.
4485
+ * 2. `pages.setName()` creates or replaces by decoded key (idempotent),
4486
+ * `replace` renames in one job, and the result IS the new layout.
4487
+ * 3. `pages.removeName()` drops the registration and nothing else.
4488
+ * 4. Deleting a page removes every registration pointing at it — inside
4489
+ * the delete, with no plugin in the loop.
4490
+ * 5. Non-ASCII keys round-trip unchanged (Acrobat writes UTF-16 + BOM).
4491
+ *
4492
+ * Skips cleanly when the engine does not implement the optional members.
4493
+ */
4494
+ function runNamedPagesConformance(runner, opts) {
4495
+ const { describe, test, beforeAll, afterAll, expect } = runner;
4496
+ describe(`named pages conformance: ${opts.label}`, () => {
4497
+ let engine;
4498
+ beforeAll(async () => {
4499
+ engine = await opts.makeEngine();
4500
+ });
4501
+ afterAll(async () => {
4502
+ if (engine) await engine.destroy();
4503
+ });
4504
+ test("pages.list() carries namedPages and every entry resolves", async () => {
4505
+ const doc = await openFixture$13(engine, opts);
4506
+ try {
4507
+ const layout = await doc.pages.list();
4508
+ PageListSnapshotSchema.parse(layout);
4509
+ expect(Array.isArray(layout.namedPages)).toBe(true);
4510
+ const pons = new Set(layout.pages.map((page) => page.pageObjectNumber));
4511
+ for (const entry of layout.namedPages ?? []) {
4512
+ expect(entry.name.length > 0).toBe(true);
4513
+ if (entry.target.kind === "page") expect(pons.has(entry.target.pageObjectNumber)).toBe(true);
4514
+ }
4515
+ } finally {
4516
+ await doc.close();
4517
+ }
4518
+ });
4519
+ test("pages.setName() registers, replaces by key, and renames via `replace`", async () => {
4520
+ const doc = await openFixture$13(engine, opts);
4521
+ try {
4522
+ if (!doc.pages.setName) return;
4523
+ const before = await doc.pages.list();
4524
+ const [first, second] = before.pages;
4525
+ const baseline = (before.namedPages ?? []).length;
4526
+ const created = await doc.pages.setName({
4527
+ name: "Approved=Goedgekeurd",
4528
+ pageObjectNumber: first.pageObjectNumber
4529
+ });
4530
+ PageNameResultSchema.parse(created);
4531
+ expect(created.layout.pages.length).toBe(before.pages.length);
4532
+ expect(find(created, "Approved=Goedgekeurd")).toEqual({
4533
+ kind: "page",
4534
+ pageObjectNumber: first.pageObjectNumber
4535
+ });
4536
+ expect((created.layout.namedPages ?? []).length).toBe(baseline + 1);
4537
+ if (created.cache) {
4538
+ expect(created.cache.docVersion > created.cache.previousDocVersion).toBe(true);
4539
+ expect(created.cache.layoutVersion > 0).toBe(true);
4540
+ }
4541
+ const listed = await doc.pages.list();
4542
+ expect(listed.namedPages).toEqual(created.layout.namedPages);
4543
+ const replaced = await doc.pages.setName({
4544
+ name: "Approved=Goedgekeurd",
4545
+ pageObjectNumber: second.pageObjectNumber
4546
+ });
4547
+ expect(find(replaced, "Approved=Goedgekeurd")).toEqual({
4548
+ kind: "page",
4549
+ pageObjectNumber: second.pageObjectNumber
4550
+ });
4551
+ expect((replaced.layout.namedPages ?? []).length).toBe(baseline + 1);
4552
+ const renamed = await doc.pages.setName({
4553
+ name: "Approved=Approved",
4554
+ pageObjectNumber: second.pageObjectNumber,
4555
+ replace: "Approved=Goedgekeurd"
4556
+ });
4557
+ expect(find(renamed, "Approved=Goedgekeurd")).toBe(void 0);
4558
+ expect(find(renamed, "Approved=Approved")).toEqual({
4559
+ kind: "page",
4560
+ pageObjectNumber: second.pageObjectNumber
4561
+ });
4562
+ expect((renamed.layout.namedPages ?? []).length).toBe(baseline + 1);
4563
+ } finally {
4564
+ await doc.close();
4565
+ }
4566
+ });
4567
+ test("non-ASCII keys round-trip through set → list", async () => {
4568
+ const doc = await openFixture$13(engine, opts);
4569
+ try {
4570
+ if (!doc.pages.setName) return;
4571
+ const before = await doc.pages.list();
4572
+ const key = "Stämpel=Stämpel 日本";
4573
+ const result = await doc.pages.setName({
4574
+ name: key,
4575
+ pageObjectNumber: before.pages[0].pageObjectNumber
4576
+ });
4577
+ expect(find(result, key)).toEqual({
4578
+ kind: "page",
4579
+ pageObjectNumber: before.pages[0].pageObjectNumber
4580
+ });
4581
+ const listed = await doc.pages.list();
4582
+ expect((listed.namedPages ?? []).some((entry) => entry.name === key)).toBe(true);
4583
+ } finally {
4584
+ await doc.close();
4585
+ }
4586
+ });
4587
+ test("pages.removeName() drops the registration and keeps the page", async () => {
4588
+ const doc = await openFixture$13(engine, opts);
4589
+ try {
4590
+ if (!doc.pages.setName || !doc.pages.removeName) return;
4591
+ const before = await doc.pages.list();
4592
+ await doc.pages.setName({
4593
+ name: "Draft=Concept",
4594
+ pageObjectNumber: before.pages[0].pageObjectNumber
4595
+ });
4596
+ const removed = await doc.pages.removeName({ name: "Draft=Concept" });
4597
+ PageNameResultSchema.parse(removed);
4598
+ expect(find(removed, "Draft=Concept")).toBe(void 0);
4599
+ expect(removed.layout.pages.length).toBe(before.pages.length);
4600
+ await expect(doc.pages.removeName({ name: "Draft=Concept" })).rejects.toMatchObject({ code: EngineErrorCode.NotFound });
4601
+ } finally {
4602
+ await doc.close();
4603
+ }
4604
+ });
4605
+ test("pages.setName() rejects an empty name and an unknown page", async () => {
4606
+ const doc = await openFixture$13(engine, opts);
4607
+ try {
4608
+ if (!doc.pages.setName) return;
4609
+ const before = await doc.pages.list();
4610
+ await expect(doc.pages.setName({
4611
+ name: "",
4612
+ pageObjectNumber: before.pages[0].pageObjectNumber
4613
+ })).rejects.toMatchObject({ code: EngineErrorCode.InvalidArg });
4614
+ let unknownPage = null;
4615
+ try {
4616
+ await doc.pages.setName({
4617
+ name: "Ghost=Ghost",
4618
+ pageObjectNumber: 987654321
4619
+ });
4620
+ } catch (error) {
4621
+ unknownPage = error;
4622
+ }
4623
+ expect(EngineError.is(unknownPage, EngineErrorCode.NotFound) || EngineError.is(unknownPage, EngineErrorCode.InvalidArg)).toBe(true);
4624
+ } finally {
4625
+ await doc.close();
4626
+ }
4627
+ });
4628
+ test("deleting a page removes every registration pointing at it", async () => {
4629
+ const doc = await openFixture$13(engine, opts);
4630
+ try {
4631
+ if (!doc.pages.setName) return;
4632
+ const before = await doc.pages.list();
4633
+ if (before.pages.length < 2) return;
4634
+ const victim = before.pages[1].pageObjectNumber;
4635
+ const survivor = before.pages[0].pageObjectNumber;
4636
+ await doc.pages.setName({
4637
+ name: "Victim=One",
4638
+ pageObjectNumber: victim
4639
+ });
4640
+ await doc.pages.setName({
4641
+ name: "Victim=Two",
4642
+ pageObjectNumber: victim
4643
+ });
4644
+ await doc.pages.setName({
4645
+ name: "Survivor",
4646
+ pageObjectNumber: survivor
4647
+ });
4648
+ const deleted = await doc.pages.delete([victim]);
4649
+ const names = (deleted.layout.namedPages ?? []).map((entry) => entry.name);
4650
+ expect(names.includes("Victim=One")).toBe(false);
4651
+ expect(names.includes("Victim=Two")).toBe(false);
4652
+ expect(names).toContain("Survivor");
4653
+ expect((deleted.layout.namedPages ?? []).some((entry) => entry.target.kind === "dangling")).toBe(false);
4654
+ } finally {
4655
+ await doc.close();
4656
+ }
4657
+ });
4658
+ test("abort on pages.setName() rejects with AbortError", async () => {
4659
+ const doc = await openFixture$13(engine, opts);
4660
+ try {
4661
+ if (!doc.pages.setName) return;
4662
+ const before = await doc.pages.list();
4663
+ const promise = doc.pages.setName({
4664
+ name: "Aborted=Aborted",
4665
+ pageObjectNumber: before.pages[0].pageObjectNumber
4666
+ });
4667
+ promise.abort();
4668
+ await expect(promise).rejects.toBeInstanceOf(AbortError);
4669
+ } finally {
4670
+ await doc.close();
4671
+ }
4672
+ });
4673
+ });
4674
+ }
4675
+ function find(result, name) {
4676
+ return (result.layout.namedPages ?? []).find((entry) => entry.name === name)?.target;
4677
+ }
4678
+ async function openFixture$13(engine, opts) {
4181
4679
  if (opts.openKind === "bytes") {
4182
4680
  const bytes = await opts.fixture.bytes();
4183
4681
  return engine.open({
@@ -4192,6 +4690,243 @@ async function openFixture$11(engine, opts) {
4192
4690
  });
4193
4691
  }
4194
4692
  //#endregion
4693
+ //#region src/conformance/runAnnotationFlattenConformance.ts
4694
+ const square$1 = (left, bottom) => ({
4695
+ subtype: "square",
4696
+ rect: {
4697
+ left,
4698
+ bottom,
4699
+ right: left + 40,
4700
+ top: bottom + 40
4701
+ },
4702
+ color: {
4703
+ r: 20,
4704
+ g: 40,
4705
+ b: 220
4706
+ },
4707
+ strokeWidth: 2
4708
+ });
4709
+ /**
4710
+ * Selective annotation flatten: `page(pon).annotations.flatten(refs)` is
4711
+ * `pages.flatten` for a chosen set. Locks, on every engine:
4712
+ * 1. only the given refs are painted and removed; the rest of the page's
4713
+ * annotations are untouched;
4714
+ * 2. per-ref status (`applied` / `skipped`), one `annotations.flattened`
4715
+ * event, a `meta` with that page's new pins, layout unchanged;
4716
+ * 3. a hidden ref is `skipped`; a ref on another page rejects `InvalidArg`
4717
+ * with nothing mutated.
4718
+ * Skips cleanly when the engine does not implement the optional member.
4719
+ */
4720
+ function runAnnotationFlattenConformance(runner, opts) {
4721
+ const { describe, test, beforeAll, afterAll, expect } = runner;
4722
+ describe(`annotation flatten conformance: ${opts.label}`, () => {
4723
+ let engine;
4724
+ beforeAll(async () => {
4725
+ engine = await opts.makeEngine();
4726
+ });
4727
+ afterAll(async () => {
4728
+ if (engine) await engine.destroy();
4729
+ });
4730
+ test("flattens exactly the given refs and reports per-ref status", async () => {
4731
+ const doc = await openFixture$12(engine, opts);
4732
+ try {
4733
+ const layoutBefore = await doc.pages.list();
4734
+ const pon = layoutBefore.pages[0].pageObjectNumber;
4735
+ const page = doc.page(pon);
4736
+ if (!page.annotations.flatten) return;
4737
+ const a = (await page.annotations.create(square$1(20, 20))).created.ref;
4738
+ const b = (await page.annotations.create(square$1(80, 20))).created.ref;
4739
+ const c = (await page.annotations.create(square$1(140, 20))).created.ref;
4740
+ await page.annotations.update(c, {
4741
+ subtype: "square",
4742
+ flags: { hidden: true }
4743
+ });
4744
+ const before = await page.annotations.list();
4745
+ const events = [];
4746
+ const unsubscribe = doc.events.subscribe((event) => {
4747
+ if (event.type === "annotations.flattened") events.push(event);
4748
+ });
4749
+ const result = await page.annotations.flatten([a, c], "display");
4750
+ unsubscribe();
4751
+ expect(AnnotationFlattenResultSchema.safeParse(result).success).toBe(true);
4752
+ expect(result.pageObjectNumber).toBe(pon);
4753
+ expect(result.usage).toBe("display");
4754
+ expect(result.results.map((item) => item.status)).toEqual(["applied", "skipped"]);
4755
+ expect(result.meta === null).toBe(false);
4756
+ expect(events).toHaveLength(1);
4757
+ expect(await doc.pages.list()).toEqual(layoutBefore);
4758
+ const after = await page.annotations.list();
4759
+ expect(after.annotations.length).toBe(before.annotations.length - 1);
4760
+ expect(after.annotations.some((dto) => sameRef(dto.ref, b))).toBe(true);
4761
+ expect(after.annotations.some((dto) => sameRef(dto.ref, c))).toBe(true);
4762
+ expect(after.annotations.some((dto) => sameRef(dto.ref, a))).toBe(false);
4763
+ expect(after.pageState.revision.generation > before.pageState.revision.generation).toBe(true);
4764
+ } finally {
4765
+ await doc.close();
4766
+ }
4767
+ });
4768
+ test("a ref on another page rejects InvalidArg and mutates nothing", async () => {
4769
+ const doc = await openFixture$12(engine, opts);
4770
+ try {
4771
+ const layout = await doc.pages.list();
4772
+ if (layout.pages.length < 2) return;
4773
+ const page0 = doc.page(layout.pages[0].pageObjectNumber);
4774
+ const page1 = doc.page(layout.pages[1].pageObjectNumber);
4775
+ if (!page0.annotations.flatten) return;
4776
+ const own = (await page0.annotations.create(square$1(20, 100))).created.ref;
4777
+ const foreign = (await page1.annotations.create(square$1(20, 100))).created.ref;
4778
+ const countBefore = (await page0.annotations.list()).annotations.length;
4779
+ await expect(page0.annotations.flatten([own, foreign])).rejects.toMatchObject({ code: EngineErrorCode.InvalidArg });
4780
+ expect((await page0.annotations.list()).annotations.length).toBe(countBefore);
4781
+ } finally {
4782
+ await doc.close();
4783
+ }
4784
+ });
4785
+ test("an empty ref list rejects InvalidArg; an unknown ref rejects", async () => {
4786
+ const doc = await openFixture$12(engine, opts);
4787
+ try {
4788
+ const pon = (await doc.pages.list()).pages[0].pageObjectNumber;
4789
+ const page = doc.page(pon);
4790
+ if (!page.annotations.flatten) return;
4791
+ await expect(page.annotations.flatten([])).rejects.toMatchObject({ code: EngineErrorCode.InvalidArg });
4792
+ let unknown = null;
4793
+ try {
4794
+ await page.annotations.flatten([{
4795
+ kind: "objectNumber",
4796
+ pageObjectNumber: pon,
4797
+ annotObjectNumber: 987654321
4798
+ }]);
4799
+ } catch (error) {
4800
+ unknown = error;
4801
+ }
4802
+ expect(EngineError.is(unknown, EngineErrorCode.NotFound) || EngineError.is(unknown, EngineErrorCode.InvalidArg) || EngineError.is(unknown, EngineErrorCode.InvalidReference)).toBe(true);
4803
+ } finally {
4804
+ await doc.close();
4805
+ }
4806
+ });
4807
+ });
4808
+ }
4809
+ function sameRef(left, right) {
4810
+ if (left.kind === "objectNumber" && right.kind === "objectNumber") return left.annotObjectNumber === right.annotObjectNumber;
4811
+ return JSON.stringify(left) === JSON.stringify(right);
4812
+ }
4813
+ async function openFixture$12(engine, opts) {
4814
+ if (opts.openKind === "bytes") return engine.open({
4815
+ kind: "bytes",
4816
+ id: opts.fixture.id,
4817
+ bytes: await opts.fixture.bytes()
4818
+ });
4819
+ return engine.open({
4820
+ kind: "id",
4821
+ id: opts.fixture.cloudId ?? opts.fixture.id
4822
+ });
4823
+ }
4824
+ //#endregion
4825
+ //#region src/conformance/runAnnotationAppearanceExportConformance.ts
4826
+ const PDF_MAGIC$1 = [
4827
+ 37,
4828
+ 80,
4829
+ 68,
4830
+ 70
4831
+ ];
4832
+ const square = (left, bottom, size = 40) => ({
4833
+ subtype: "square",
4834
+ rect: {
4835
+ left,
4836
+ bottom,
4837
+ right: left + size,
4838
+ top: bottom + size
4839
+ },
4840
+ color: {
4841
+ r: 220,
4842
+ g: 20,
4843
+ b: 20
4844
+ },
4845
+ strokeWidth: 2
4846
+ });
4847
+ /**
4848
+ * `page(pon).annotations.exportAppearance(refs)`: the chosen annotations'
4849
+ * appearances as ONE single-page PDF sized to their union /Rect. Locks:
4850
+ * 1. bytes are a PDF; the source page is untouched (no mutation, no
4851
+ * revision bump);
4852
+ * 2. re-opened (local engines), the page is exactly the union size and the
4853
+ * requested count of annotations is gone — they are content now;
4854
+ * 3. a ref on another page rejects `InvalidArg`; an empty list rejects.
4855
+ * Skips cleanly when the engine does not implement the optional member.
4856
+ */
4857
+ function runAnnotationAppearanceExportConformance(runner, opts) {
4858
+ const { describe, test, beforeAll, afterAll, expect } = runner;
4859
+ describe(`annotation appearance export conformance: ${opts.label}`, () => {
4860
+ let engine;
4861
+ beforeAll(async () => {
4862
+ engine = await opts.makeEngine();
4863
+ });
4864
+ afterAll(async () => {
4865
+ if (engine) await engine.destroy();
4866
+ });
4867
+ test("returns a single-page PDF sized to the union rect; source untouched", async () => {
4868
+ const doc = await openFixture$11(engine, opts);
4869
+ let exported = null;
4870
+ try {
4871
+ const pon = (await doc.pages.list()).pages[0].pageObjectNumber;
4872
+ const page = doc.page(pon);
4873
+ if (!page.annotations.exportAppearance) return;
4874
+ const a = (await page.annotations.create(square(20, 20))).created.ref;
4875
+ const b = (await page.annotations.create(square(100, 60, 30))).created.ref;
4876
+ const before = await page.annotations.list();
4877
+ const bytes = await page.annotations.exportAppearance([a, b]);
4878
+ expect(bytes.length > 4).toBe(true);
4879
+ expect([...bytes.subarray(0, 4)]).toEqual(PDF_MAGIC$1);
4880
+ const after = await page.annotations.list();
4881
+ expect(after.annotations.length).toBe(before.annotations.length);
4882
+ expect(after.pageState.revision.generation).toBe(before.pageState.revision.generation);
4883
+ if (opts.openKind !== "bytes") return;
4884
+ exported = await engine.open({
4885
+ kind: "bytes",
4886
+ id: `${opts.fixture.id}-appearance`,
4887
+ bytes
4888
+ });
4889
+ const layout = await exported.pages.list();
4890
+ expect(layout.pageCount).toBe(1);
4891
+ expect(Math.round(layout.pages[0].size.width)).toBe(110);
4892
+ expect(Math.round(layout.pages[0].size.height)).toBe(70);
4893
+ const exportedPage = exported.page(layout.pages[0].pageObjectNumber);
4894
+ expect((await exportedPage.annotations.list()).annotations).toHaveLength(0);
4895
+ } finally {
4896
+ if (exported) await exported.close();
4897
+ await doc.close();
4898
+ }
4899
+ });
4900
+ test("a ref on another page and an empty list reject InvalidArg", async () => {
4901
+ const doc = await openFixture$11(engine, opts);
4902
+ try {
4903
+ const layout = await doc.pages.list();
4904
+ const page0 = doc.page(layout.pages[0].pageObjectNumber);
4905
+ if (!page0.annotations.exportAppearance) return;
4906
+ await expect(page0.annotations.exportAppearance([])).rejects.toMatchObject({ code: EngineErrorCode.InvalidArg });
4907
+ if (layout.pages.length < 2) return;
4908
+ const page1 = doc.page(layout.pages[1].pageObjectNumber);
4909
+ const own = (await page0.annotations.create(square(20, 100))).created.ref;
4910
+ const foreign = (await page1.annotations.create(square(20, 100))).created.ref;
4911
+ await expect(page0.annotations.exportAppearance([own, foreign])).rejects.toMatchObject({ code: EngineErrorCode.InvalidArg });
4912
+ } finally {
4913
+ await doc.close();
4914
+ }
4915
+ });
4916
+ });
4917
+ }
4918
+ async function openFixture$11(engine, opts) {
4919
+ if (opts.openKind === "bytes") return engine.open({
4920
+ kind: "bytes",
4921
+ id: opts.fixture.id,
4922
+ bytes: await opts.fixture.bytes()
4923
+ });
4924
+ return engine.open({
4925
+ kind: "id",
4926
+ id: opts.fixture.cloudId ?? opts.fixture.id
4927
+ });
4928
+ }
4929
+ //#endregion
4195
4930
  //#region src/conformance/runPageFlattenConformance.ts
4196
4931
  /** Transport-neutral coverage for the content + annotation flatten rail. */
4197
4932
  function runPageFlattenConformance(runner, opts) {
@@ -6220,6 +6955,790 @@ function diffAnnotation(i, a, b, errs) {
6220
6955
  if (a.nm !== b.nm) errs.push(`annotations[${i}].nm mismatch: ${a.nm} vs ${b.nm}`);
6221
6956
  }
6222
6957
  //#endregion
6223
- export { TEXT_DIVERGENCE_CASES, diffAnnotationListSnapshot, diffAnnotationListSnapshotAll, runActionsConformance, runAnnotationAppearanceConformance, runAnnotationMutationConformance, runAnnotationReadConformance, runAttachmentConformance, runDocumentEventsConformance, runFormConformance, runMetadataConformance, runPageDeleteConformance, runPageExtractConformance, runPageFlattenConformance, runPageGeometryOrientationConformance, runPageInsertBlankConformance, runPageInsertConformance, runPageReorderConformance, runPageRotateConformance, runPageTextConformance, runPieceInfoConformance, runRedactionApplyConformance, runSearchConformance, runTextDivergenceConformance };
6958
+ //#region src/conformance/runSignatureConformance.ts
6959
+ /**
6960
+ * Digital signatures, read side. The service is OPTIONAL on the contract
6961
+ * (`downloadLayer?` pattern) — the suite runs only where the engine
6962
+ * provides it.
6963
+ *
6964
+ * Invariants:
6965
+ * 1. Byte facts describe the LOADED bytes: a revision is a byte prefix,
6966
+ * a whole-revision signature seals exactly one, the digest over its
6967
+ * `/ByteRange` is what the file's bytes hash to, and an unsaved edit
6968
+ * changes none of it.
6969
+ * 2. Protection is derived from the signatures in the file and applied
6970
+ * as document-derived capabilities: a certification with permission 2
6971
+ * takes page assembly and annotation writes away, a FieldMDP freezes
6972
+ * the fields it names, a rewrite save is refused. Under
6973
+ * `signedDocumentPolicy: 'permit'` none of that applies and the
6974
+ * analysis is unchanged.
6975
+ * 3. The same answers come from a plain session and from a layer
6976
+ * session over the same bytes.
6977
+ */
6978
+ function runSignatureConformance(runner, opts) {
6979
+ const { describe, test, beforeAll, afterAll, expect } = runner;
6980
+ describe(`signatures conformance: ${opts.label}`, () => {
6981
+ let engine;
6982
+ let permitEngine = null;
6983
+ let supported = false;
6984
+ beforeAll(async () => {
6985
+ engine = await opts.makeEngine();
6986
+ const probe = await open(engine, opts, opts.fixtures.unsignedForm);
6987
+ supported = probe.signatures !== void 0;
6988
+ await probe.close();
6989
+ if (supported && opts.makePermitEngine) permitEngine = await opts.makePermitEngine();
6990
+ });
6991
+ afterAll(async () => {
6992
+ if (permitEngine) await permitEngine.destroy();
6993
+ if (engine) await engine.destroy();
6994
+ });
6995
+ test("an unsigned form has no signatures and no protection; version() hashes the loaded bytes", async () => {
6996
+ if (!supported) return;
6997
+ const bytes = await opts.fixtures.unsignedForm.bytes();
6998
+ const doc = await open(engine, opts, opts.fixtures.unsignedForm);
6999
+ try {
7000
+ const snapshot = await doc.signatures.list();
7001
+ expect(snapshot.chainValid).toBe(true);
7002
+ expect(snapshot.signatures).toEqual([]);
7003
+ expect(snapshot.revisions.length >= 1).toBe(true);
7004
+ expect(snapshot.protection.judged).toBeNull();
7005
+ expect(snapshot.protection.enforced).toBeNull();
7006
+ expect(snapshot.protection.certification).toBeNull();
7007
+ expect(snapshot.protection.fieldLocks).toEqual([]);
7008
+ expect(doc.security.allows("doc.pages.assemble")).toBe(true);
7009
+ if (doc.version) {
7010
+ const version = await doc.version();
7011
+ expect(version.byteLength).toBe(bytes.byteLength);
7012
+ expect(version.sha256).toBe(await sha256Hex(bytes));
7013
+ }
7014
+ let caught;
7015
+ try {
7016
+ await doc.signatures.contents({
7017
+ kind: "fqn",
7018
+ name: "no-such-field"
7019
+ });
7020
+ } catch (err) {
7021
+ caught = err;
7022
+ }
7023
+ expect(EngineError.is(caught, EngineErrorCode.NotFound)).toBe(true);
7024
+ } finally {
7025
+ await doc.close();
7026
+ }
7027
+ });
7028
+ test("two approval signatures: revisions, coverage, digests, contents, revision bytes", async () => {
7029
+ if (!supported) return;
7030
+ const fixture = opts.fixtures.twoApprovals;
7031
+ const bytes = await fixture.bytes();
7032
+ const doc = await open(engine, opts, fixture);
7033
+ try {
7034
+ const snapshot = await doc.signatures.list();
7035
+ expect(snapshot.chainValid).toBe(true);
7036
+ expect(snapshot.revisions).toHaveLength(3);
7037
+ expect(snapshot.signatures).toHaveLength(2);
7038
+ expect(snapshot.revisions[2].end).toBe(bytes.byteLength);
7039
+ for (const [i, sig] of snapshot.signatures.entries()) {
7040
+ expect(sig.index).toBe(i);
7041
+ expect(sig.signed).toBe(true);
7042
+ expect(sig.kind).toBe("signature");
7043
+ expect(sig.field.kind).toBe("objectNumber");
7044
+ expect(sig.fieldName).toBe(fixture.fieldNames[i]);
7045
+ expect(sig.coverage).toBe("whole-revision");
7046
+ expect(sig.revisionIndex).toBe(i + 1);
7047
+ expect(snapshot.revisions[i + 1].signatureIndex).toBe(i);
7048
+ expect(sig.subFilter).toBe("ETSI.CAdES.detached");
7049
+ expect(typeof sig.signer.claimedTime).toBe("string");
7050
+ expect(sig.docMdp).toBeNull();
7051
+ expect(sig.catalogCertification).toBe(false);
7052
+ expect(sig.contentsSize > 0).toBe(true);
7053
+ expect(sig.byteRange[0]).toBe(0);
7054
+ expect(sig.byteRange[2] + sig.byteRange[3]).toBe(snapshot.revisions[i + 1].end);
7055
+ }
7056
+ expect(snapshot.protection.judged).toBe("annotate");
7057
+ expect(snapshot.protection.enforced).toBeNull();
7058
+ expect(snapshot.protection.certification).toBeNull();
7059
+ expect(snapshot.protection.fieldLocks).toEqual([]);
7060
+ const first = snapshot.signatures[0];
7061
+ const contents = await doc.signatures.contents(first.field);
7062
+ expect(contents.byteLength).toBe(first.contentsSize);
7063
+ expect(contents[0]).toBe(48);
7064
+ const [a, b, c, d] = first.byteRange;
7065
+ const sealed = await doc.signatures.revisionBytes(1);
7066
+ expect(sealed.byteLength).toBe(snapshot.revisions[1].end);
7067
+ expect(bytesEqual(sealed, bytes.subarray(0, sealed.byteLength))).toBe(true);
7068
+ const signedBytes = concat(sealed.subarray(a, a + b), sealed.subarray(c, c + d));
7069
+ const digest = await doc.signatures.digest(first.field, "sha256");
7070
+ expect(digest.byteLength).toBe(32);
7071
+ expect(toHex(digest)).toBe(await sha256Hex(signedBytes));
7072
+ expect((await doc.signatures.digest(first.field, "sha512")).byteLength).toBe(64);
7073
+ const byName = await doc.signatures.digest({
7074
+ kind: "fqn",
7075
+ name: first.fieldName
7076
+ }, "sha256");
7077
+ expect(toHex(byName)).toBe(toHex(digest));
7078
+ const text = (await doc.forms.list()).fields.find((f) => f.name === fixture.textField);
7079
+ expect(text).toBeTruthy();
7080
+ await doc.forms.setValue({
7081
+ kind: "objectNumber",
7082
+ fieldObjectNumber: text.fieldObjectNumber
7083
+ }, {
7084
+ type: "text",
7085
+ value: "unsaved"
7086
+ });
7087
+ const again = await doc.signatures.list();
7088
+ expect(again.revisions).toEqual(snapshot.revisions);
7089
+ expect(again.signatures.map((s) => [
7090
+ s.coverage,
7091
+ s.revisionIndex,
7092
+ s.byteRange
7093
+ ])).toEqual(snapshot.signatures.map((s) => [
7094
+ s.coverage,
7095
+ s.revisionIndex,
7096
+ s.byteRange
7097
+ ]));
7098
+ expect(toHex(await doc.signatures.digest(first.field, "sha256"))).toBe(toHex(digest));
7099
+ if (doc.version) expect((await doc.version()).sha256).toBe(await sha256Hex(bytes));
7100
+ } finally {
7101
+ await doc.close();
7102
+ }
7103
+ });
7104
+ test("a certification takes structural capabilities away; permit lifts it", async () => {
7105
+ if (!supported) return;
7106
+ const fixture = opts.fixtures.certified;
7107
+ const doc = await open(engine, opts, fixture);
7108
+ try {
7109
+ const snapshot = await doc.signatures.list();
7110
+ const signed = snapshot.signatures.filter((s) => s.signed);
7111
+ expect(signed).toHaveLength(1);
7112
+ const sig = signed[0];
7113
+ expect(sig.fieldName).toBe(fixture.fieldName);
7114
+ expect(sig.coverage).toBe("whole-revision");
7115
+ expect(sig.docMdp).toBe(2);
7116
+ expect(sig.catalogCertification).toBe(true);
7117
+ expect(snapshot.protection.judged).toBe("fill");
7118
+ expect(snapshot.protection.enforced).toBe("fill");
7119
+ expect(snapshot.protection.certification).toEqual({
7120
+ signatureIndex: sig.index,
7121
+ permission: 2
7122
+ });
7123
+ expect(doc.security.allows("doc.forms.read")).toBe(true);
7124
+ expect(doc.security.allows("doc.forms.fill")).toBe(true);
7125
+ expect(doc.security.allows("doc.annotate.modify")).toBe(false);
7126
+ expect(doc.security.allows("doc.pages.assemble")).toBe(false);
7127
+ expect(doc.security.allows("doc.redact")).toBe(false);
7128
+ const list = await doc.pages.list();
7129
+ let caught;
7130
+ try {
7131
+ await doc.pages.delete([list.pages[0].pageObjectNumber]);
7132
+ } catch (err) {
7133
+ caught = err;
7134
+ }
7135
+ expect(EngineError.is(caught, EngineErrorCode.ProtectedDocument)).toBe(true);
7136
+ caught = void 0;
7137
+ try {
7138
+ await doc.download({ mode: "rewrite" });
7139
+ } catch (err) {
7140
+ caught = err;
7141
+ }
7142
+ expect(EngineError.is(caught, EngineErrorCode.ProtectedDocument)).toBe(true);
7143
+ const saved = await doc.download();
7144
+ expect(saved.byteLength >= snapshot.revisions[1].end).toBe(true);
7145
+ } finally {
7146
+ await doc.close();
7147
+ }
7148
+ if (!permitEngine) return;
7149
+ const permitted = await open(permitEngine, opts, fixture);
7150
+ try {
7151
+ const snapshot = await permitted.signatures.list();
7152
+ expect(snapshot.protection.judged).toBe("fill");
7153
+ expect(snapshot.protection.enforced).toBe("fill");
7154
+ expect(permitted.security.allows("doc.pages.assemble")).toBe(true);
7155
+ const rewritten = await permitted.download({ mode: "rewrite" });
7156
+ expect(rewritten.byteLength > 0).toBe(true);
7157
+ } finally {
7158
+ await permitted.close();
7159
+ }
7160
+ });
7161
+ test("a FieldMDP freezes the fields it names; permit lifts it", async () => {
7162
+ if (!supported) return;
7163
+ const fixture = opts.fixtures.fieldMdp;
7164
+ const doc = await open(engine, opts, fixture);
7165
+ const lockedRef = {
7166
+ kind: "fqn",
7167
+ name: fixture.lockedField
7168
+ };
7169
+ try {
7170
+ const snapshot = await doc.signatures.list();
7171
+ const sig = snapshot.signatures[0];
7172
+ expect(sig.fieldName).toBe(fixture.fieldName);
7173
+ expect(sig.fieldMdp).toEqual({
7174
+ action: "include",
7175
+ fields: [fixture.lockedField]
7176
+ });
7177
+ expect(snapshot.protection.judged).toBe("annotate");
7178
+ expect(snapshot.protection.enforced).toBeNull();
7179
+ expect(snapshot.protection.fieldLocks[0]).toEqual({
7180
+ signatureIndex: 0,
7181
+ source: "fieldmdp",
7182
+ spec: {
7183
+ action: "include",
7184
+ fields: [fixture.lockedField]
7185
+ }
7186
+ });
7187
+ for (const lock of snapshot.protection.fieldLocks) {
7188
+ expect(lock.signatureIndex).toBe(0);
7189
+ expect(lock.spec).toEqual({
7190
+ action: "include",
7191
+ fields: [fixture.lockedField]
7192
+ });
7193
+ }
7194
+ expect(doc.security.allows("doc.forms.fill")).toBe(true);
7195
+ let caught;
7196
+ try {
7197
+ await doc.forms.setValue(lockedRef, {
7198
+ type: "text",
7199
+ value: "changed"
7200
+ });
7201
+ } catch (err) {
7202
+ caught = err;
7203
+ }
7204
+ expect(EngineError.is(caught, EngineErrorCode.ProtectedDocument)).toBe(true);
7205
+ const field = await doc.forms.get(lockedRef);
7206
+ expect(field.family).toBe("text");
7207
+ expect(field.value === "changed").toBe(false);
7208
+ } finally {
7209
+ await doc.close();
7210
+ }
7211
+ if (!permitEngine) return;
7212
+ const permitted = await open(permitEngine, opts, fixture);
7213
+ try {
7214
+ const result = await permitted.forms.setValue(lockedRef, {
7215
+ type: "text",
7216
+ value: "changed"
7217
+ });
7218
+ expect(result.field.name).toBe(fixture.lockedField);
7219
+ } finally {
7220
+ await permitted.close();
7221
+ }
7222
+ });
7223
+ test("well-formed ranges that seal no revision are partial, with no revision index", async () => {
7224
+ if (!supported) return;
7225
+ const doc = await open(engine, opts, opts.fixtures.partialChain);
7226
+ try {
7227
+ const snapshot = await doc.signatures.list();
7228
+ expect(snapshot.chainValid).toBe(true);
7229
+ expect(snapshot.revisions).toHaveLength(3);
7230
+ expect(snapshot.signatures).toHaveLength(2);
7231
+ for (const sig of snapshot.signatures) {
7232
+ expect(sig.signed).toBe(true);
7233
+ expect(sig.coverage).toBe("partial");
7234
+ expect(sig.revisionIndex).toBeNull();
7235
+ expect(sig.contentsSize).toBe(13);
7236
+ expect((await doc.signatures.contents(sig.field)).byteLength).toBe(13);
7237
+ expect((await doc.signatures.digest(sig.field, "sha256")).byteLength).toBe(32);
7238
+ }
7239
+ expect(snapshot.revisions.every((r) => r.signatureIndex === null)).toBe(true);
7240
+ expect(snapshot.protection.judged).toBe("annotate");
7241
+ expect(snapshot.protection.enforced).toBeNull();
7242
+ let caught;
7243
+ try {
7244
+ await doc.signatures.revisionBytes(3);
7245
+ } catch (err) {
7246
+ caught = err;
7247
+ }
7248
+ expect(EngineError.is(caught, EngineErrorCode.NotFound)).toBe(true);
7249
+ } finally {
7250
+ await doc.close();
7251
+ }
7252
+ });
7253
+ if (opts.openKind !== "id") {
7254
+ runSigningTests(runner, opts, () => engine, () => supported);
7255
+ runAnalysisTests(runner, opts, () => engine, () => permitEngine, () => supported);
7256
+ }
7257
+ });
7258
+ }
7259
+ function runAnalysisTests(runner, opts, engineOf, permitEngineOf, supported) {
7260
+ const { test, expect } = runner;
7261
+ test("analyze: a form fill and a second signature after an approval are permitted", async () => {
7262
+ if (!supported()) return;
7263
+ const doc = await open(engineOf(), opts, opts.fixtures.twoApprovals);
7264
+ try {
7265
+ const analysis = await doc.signatures.analyze({ since: { signatureIndex: 0 } });
7266
+ expect(analysis.mode).toBe("authoritative");
7267
+ expect(analysis.basis.source).toBe("persisted");
7268
+ expect(analysis.since).toEqual({
7269
+ revisionIndex: 1,
7270
+ signatureIndex: 0
7271
+ });
7272
+ expect(analysis.until.revisionIndex).toBe(2);
7273
+ expect(analysis.steps).toHaveLength(1);
7274
+ const [step] = analysis.steps;
7275
+ expect(step.levelInForce).toBe("annotate");
7276
+ expect(step.verdict).toBe("permitted");
7277
+ expect(step.changes.length > 0).toBe(true);
7278
+ const rules = new Set(step.findings.filter((f) => f.verdict === "permitted").map((f) => f.rule));
7279
+ expect(rules.has("form-fill")).toBe(true);
7280
+ expect(rules.has("signature-added")).toBe(true);
7281
+ expect(step.findings.filter((f) => f.verdict === "forbidden")).toEqual([]);
7282
+ expect(analysis.verdict).toBe("permitted");
7283
+ const latest = await doc.signatures.analyze({ since: { signatureIndex: 1 } });
7284
+ expect(latest.steps).toHaveLength(0);
7285
+ expect(latest.verdict).toBe("unchanged");
7286
+ const historic = await doc.signatures.analyze({
7287
+ since: { revisionIndex: 0 },
7288
+ until: { revisionIndex: 1 }
7289
+ });
7290
+ expect(historic.steps).toHaveLength(1);
7291
+ expect(historic.verdict).toBe("permitted");
7292
+ } finally {
7293
+ await doc.close();
7294
+ }
7295
+ });
7296
+ test("analyze: the working copy is one more revision; a partial signature cannot anchor", async () => {
7297
+ if (!supported()) return;
7298
+ const fx = opts.fixtures.unsignedSigField;
7299
+ const doc = await open(engineOf(), opts, fx);
7300
+ try {
7301
+ const prepared = await doc.signatures.prepare({
7302
+ field: {
7303
+ kind: "fqn",
7304
+ name: fx.fieldName
7305
+ },
7306
+ certify: { permission: 2 }
7307
+ });
7308
+ await doc.signatures.complete({
7309
+ signingId: prepared.signingId,
7310
+ cms: FAKE_CMS,
7311
+ expectedVersion: prepared.expectedVersion
7312
+ });
7313
+ const clean = await doc.signatures.analyze({
7314
+ since: { signatureIndex: 0 },
7315
+ until: "working-copy"
7316
+ });
7317
+ expect(clean.steps).toHaveLength(0);
7318
+ expect(clean.verdict).toBe("unchanged");
7319
+ await doc.forms.setValue({
7320
+ kind: "fqn",
7321
+ name: fx.textField
7322
+ }, {
7323
+ type: "text",
7324
+ value: "draft"
7325
+ });
7326
+ const persisted = await doc.signatures.analyze({ since: { signatureIndex: 0 } });
7327
+ expect(persisted.steps).toHaveLength(0);
7328
+ const working = await doc.signatures.analyze({
7329
+ since: { signatureIndex: 0 },
7330
+ until: "working-copy"
7331
+ });
7332
+ expect(working.basis.source).toBe("working-copy");
7333
+ expect(working.steps).toHaveLength(1);
7334
+ expect(working.steps[0].levelInForce).toBe("fill");
7335
+ expect(working.verdict).toBe("permitted");
7336
+ expect(working.steps[0].findings.some((f) => f.rule === "form-fill" && f.verdict === "permitted")).toBe(true);
7337
+ } finally {
7338
+ await doc.close();
7339
+ }
7340
+ const partial = await open(engineOf(), opts, opts.fixtures.partialChain);
7341
+ try {
7342
+ expect(await caughtCode(() => partial.signatures.analyze({ since: { signatureIndex: 0 } }))).toBe(EngineErrorCode.InvalidArg);
7343
+ const byRevision = await partial.signatures.analyze({ since: { revisionIndex: 0 } });
7344
+ expect(byRevision.later.revisionCount).toBe(2);
7345
+ expect(byRevision.steps).toHaveLength(0);
7346
+ const inFull = await partial.signatures.analyze({
7347
+ since: { revisionIndex: 0 },
7348
+ detail: "full"
7349
+ });
7350
+ expect(inFull.steps).toHaveLength(2);
7351
+ expect(inFull.current.verdict).toBe(byRevision.current.verdict);
7352
+ } finally {
7353
+ await partial.close();
7354
+ }
7355
+ });
7356
+ test("analyze: a page edit under a certification and a locked field edit are forbidden", async () => {
7357
+ if (!supported()) return;
7358
+ const permit = permitEngineOf();
7359
+ if (!permit) return;
7360
+ const certified = await open(permit, opts, opts.fixtures.certified);
7361
+ let reopened = null;
7362
+ try {
7363
+ const list = await certified.pages.list();
7364
+ await certified.pages.rotate([list.pages[0].pageObjectNumber], 90);
7365
+ const working = await certified.signatures.analyze({
7366
+ since: { signatureIndex: 0 },
7367
+ until: "working-copy"
7368
+ });
7369
+ expect(working.verdict).toBe("forbidden");
7370
+ const bytes = await certified.download();
7371
+ reopened = await reopen(permit, opts, `${opts.fixtures.certified.id}-tampered`, bytes);
7372
+ const analysis = await reopened.signatures.analyze({ since: { signatureIndex: 0 } });
7373
+ expect(analysis.verdict).toBe("forbidden");
7374
+ expect(analysis.steps[0].findings.some((f) => f.verdict === "forbidden")).toBe(true);
7375
+ } finally {
7376
+ if (reopened) await reopened.close();
7377
+ await certified.close();
7378
+ }
7379
+ const locked = await open(permit, opts, opts.fixtures.fieldMdp);
7380
+ try {
7381
+ await locked.forms.setValue({
7382
+ kind: "fqn",
7383
+ name: opts.fixtures.fieldMdp.lockedField
7384
+ }, {
7385
+ type: "text",
7386
+ value: "tampered"
7387
+ });
7388
+ const working = await locked.signatures.analyze({
7389
+ since: { signatureIndex: 0 },
7390
+ until: "working-copy"
7391
+ });
7392
+ expect(working.verdict).toBe("forbidden");
7393
+ expect(working.steps[0].findings.some((f) => f.rule === "field-lock")).toBe(true);
7394
+ } finally {
7395
+ await locked.close();
7396
+ }
7397
+ });
7398
+ }
7399
+ /** A minimal DER SEQUENCE standing in for a CMS: the engine's job ends at the bytes. */
7400
+ const FAKE_CMS = new Uint8Array([
7401
+ 48,
7402
+ 6,
7403
+ 2,
7404
+ 1,
7405
+ 1,
7406
+ 2,
7407
+ 1,
7408
+ 2
7409
+ ]);
7410
+ async function caughtCode(run) {
7411
+ try {
7412
+ await run();
7413
+ return null;
7414
+ } catch (err) {
7415
+ return err instanceof EngineError ? err.code : "not-an-engine-error";
7416
+ }
7417
+ }
7418
+ function runSigningTests(runner, opts, engineOf, supported) {
7419
+ const { test, expect } = runner;
7420
+ const fx = () => opts.fixtures.unsignedSigField;
7421
+ const sigRef = () => ({
7422
+ kind: "fqn",
7423
+ name: fx().fieldName
7424
+ });
7425
+ const textRef = () => ({
7426
+ kind: "fqn",
7427
+ name: fx().textField
7428
+ });
7429
+ test("sign: prepare seals a candidate, complete installs it, download returns it verbatim", async () => {
7430
+ if (!supported()) return;
7431
+ const engine = engineOf();
7432
+ const doc = await open(engine, opts, fx());
7433
+ const events = [];
7434
+ const unsubscribe = doc.events.subscribe((event) => events.push(event.type));
7435
+ let reopened = null;
7436
+ try {
7437
+ const v0 = await doc.version();
7438
+ const before = await doc.signatures.list();
7439
+ expect(before.revisions).toHaveLength(1);
7440
+ expect(before.signatures).toHaveLength(1);
7441
+ expect(before.signatures[0].signed).toBe(false);
7442
+ expect(before.protection.judged).toBeNull();
7443
+ const prepared = await doc.signatures.prepare({
7444
+ field: sigRef(),
7445
+ subFilter: "ETSI.CAdES.detached",
7446
+ digest: "sha256",
7447
+ contentsSize: 1024,
7448
+ attribution: { reason: "conformance" }
7449
+ });
7450
+ expect(prepared.digest.byteLength).toBe(32);
7451
+ expect(prepared.algorithm).toBe("sha256");
7452
+ expect(prepared.subFilter).toBe("ETSI.CAdES.detached");
7453
+ expect(prepared.contentsSize).toBe(1024);
7454
+ expect(prepared.byteRange[0]).toBe(0);
7455
+ expect(prepared.byteRange[2] > prepared.byteRange[1]).toBe(true);
7456
+ expect(prepared.expectedVersion.baseSha256).toBe(v0.sha256);
7457
+ expect(await caughtCode(() => doc.signatures.prepare({ field: sigRef() }))).toBe(EngineErrorCode.SigningPending);
7458
+ expect(await caughtCode(() => doc.forms.setValue(textRef(), {
7459
+ type: "text",
7460
+ value: "x"
7461
+ }))).toBe(EngineErrorCode.SigningPending);
7462
+ const during = await doc.signatures.list();
7463
+ expect(during.signatures[0].signed).toBe(false);
7464
+ expect((await doc.version()).sha256).toBe(v0.sha256);
7465
+ expect(await caughtCode(() => doc.signatures.complete({
7466
+ signingId: prepared.signingId,
7467
+ cms: FAKE_CMS,
7468
+ expectedVersion: {
7469
+ baseSha256: prepared.expectedVersion.baseSha256,
7470
+ editsVersion: prepared.expectedVersion.editsVersion + 1
7471
+ }
7472
+ }))).toBe(EngineErrorCode.SigningVersionMismatch);
7473
+ const result = await doc.signatures.complete({
7474
+ signingId: prepared.signingId,
7475
+ cms: FAKE_CMS,
7476
+ expectedVersion: prepared.expectedVersion
7477
+ });
7478
+ expect(result.status).toBe("completed");
7479
+ expect(result.signature.signed).toBe(true);
7480
+ expect(result.signature.fieldName).toBe(fx().fieldName);
7481
+ expect(result.signature.coverage).toBe("whole-revision");
7482
+ expect(result.signature.revisionIndex).toBe(1);
7483
+ expect(result.signature.subFilter).toBe("ETSI.CAdES.detached");
7484
+ expect(result.signature.signer.reason).toBe("conformance");
7485
+ expect(result.signature.contentsSize).toBe(FAKE_CMS.byteLength);
7486
+ expect(result.previous).toEqual(prepared.expectedVersion);
7487
+ expect(result.version.sha256 === v0.sha256).toBe(false);
7488
+ expect(result.protection.judged).toBe("annotate");
7489
+ expect(result.protection.enforced).toBeNull();
7490
+ expect(result.meta.affectedPages.length >= 1).toBe(true);
7491
+ expect(await doc.version()).toEqual(result.version);
7492
+ const after = await doc.signatures.list();
7493
+ expect(after.revisions).toHaveLength(2);
7494
+ expect(after.signatures[0].signed).toBe(true);
7495
+ expect(after.signatures[0].revisionIndex).toBe(1);
7496
+ expect(after.revisions[1].signatureIndex).toBe(0);
7497
+ expect(toHex(await doc.signatures.digest(sigRef(), "sha256"))).toBe(toHex(prepared.digest));
7498
+ expect(bytesEqual(await doc.signatures.contents(sigRef()), FAKE_CMS)).toBe(true);
7499
+ const bytes = await doc.download();
7500
+ expect(bytes.byteLength).toBe(result.version.byteLength);
7501
+ expect(await sha256Hex(bytes)).toBe(result.version.sha256);
7502
+ reopened = await reopen(engine, opts, `${fx().id}-signed`, bytes);
7503
+ const fresh = await reopened.signatures.list();
7504
+ expect(fresh.revisions).toHaveLength(2);
7505
+ expect(fresh.signatures[0].coverage).toBe("whole-revision");
7506
+ expect(toHex(await reopened.signatures.digest(sigRef(), "sha256"))).toBe(toHex(prepared.digest));
7507
+ const replay = await doc.signatures.complete({
7508
+ signingId: prepared.signingId,
7509
+ cms: FAKE_CMS,
7510
+ expectedVersion: prepared.expectedVersion
7511
+ });
7512
+ expect(replay.status).toBe("already-completed");
7513
+ expect(replay.version).toEqual(result.version);
7514
+ expect((await doc.signatures.abort(prepared.signingId)).status).toBe("already-completed");
7515
+ expect((await doc.signatures.abort("never-prepared")).status).toBe("unknown");
7516
+ await doc.forms.setValue(textRef(), {
7517
+ type: "text",
7518
+ value: "after"
7519
+ });
7520
+ expect(await caughtCode(() => doc.signatures.prepare({ field: sigRef() }))).toBe(EngineErrorCode.SignatureRefused);
7521
+ expect(events.filter((t) => t === "signature.prepared")).toHaveLength(1);
7522
+ expect(events.filter((t) => t === "signature.completed")).toHaveLength(1);
7523
+ expect(events.filter((t) => t === "document.versioned")).toHaveLength(1);
7524
+ } finally {
7525
+ unsubscribe();
7526
+ if (reopened) await reopened.close();
7527
+ await doc.close();
7528
+ }
7529
+ });
7530
+ test("sign: unsaved edits are sealed in the signature's own revision (layer session)", async () => {
7531
+ if (!supported()) return;
7532
+ const doc = await open(engineOf(), opts, fx());
7533
+ try {
7534
+ await doc.forms.setValue(textRef(), {
7535
+ type: "text",
7536
+ value: "unsaved"
7537
+ });
7538
+ const prepared = await doc.signatures.prepare({ field: sigRef() });
7539
+ const result = await doc.signatures.complete({
7540
+ signingId: prepared.signingId,
7541
+ cms: FAKE_CMS,
7542
+ expectedVersion: prepared.expectedVersion
7543
+ });
7544
+ expect(result.signature.revisionIndex).toBe(1);
7545
+ const after = await doc.signatures.list();
7546
+ expect(after.revisions).toHaveLength(2);
7547
+ const text = await doc.forms.get(textRef());
7548
+ expect(text.value).toBe("unsaved");
7549
+ const bytes = await doc.download();
7550
+ expect(await sha256Hex(bytes)).toBe(result.version.sha256);
7551
+ } finally {
7552
+ await doc.close();
7553
+ }
7554
+ });
7555
+ test("sign: a certification protects the document from the moment it completes", async () => {
7556
+ if (!supported()) return;
7557
+ const doc = await open(engineOf(), opts, fx());
7558
+ try {
7559
+ expect(doc.security.allows("doc.pages.assemble")).toBe(true);
7560
+ const prepared = await doc.signatures.prepare({
7561
+ field: sigRef(),
7562
+ certify: { permission: 2 },
7563
+ subFilter: "adbe.pkcs7.detached"
7564
+ });
7565
+ const result = await doc.signatures.complete({
7566
+ signingId: prepared.signingId,
7567
+ cms: FAKE_CMS,
7568
+ expectedVersion: prepared.expectedVersion
7569
+ });
7570
+ expect(result.signature.docMdp).toBe(2);
7571
+ expect(result.signature.catalogCertification).toBe(true);
7572
+ expect(result.protection.judged).toBe("fill");
7573
+ expect(result.protection.enforced).toBe("fill");
7574
+ expect(result.protection.certification).toEqual({
7575
+ signatureIndex: result.signature.index,
7576
+ permission: 2
7577
+ });
7578
+ expect(doc.security.allows("doc.pages.assemble")).toBe(false);
7579
+ expect(doc.security.allows("doc.forms.fill")).toBe(true);
7580
+ const list = await doc.pages.list();
7581
+ expect(await caughtCode(() => doc.pages.delete([list.pages[0].pageObjectNumber]))).toBe(EngineErrorCode.ProtectedDocument);
7582
+ expect(await caughtCode(() => doc.download({ mode: "rewrite" }))).toBe(EngineErrorCode.ProtectedDocument);
7583
+ await doc.forms.setValue(textRef(), {
7584
+ type: "text",
7585
+ value: "filled"
7586
+ });
7587
+ const after = await doc.signatures.list();
7588
+ expect(after.signatures[0].coverage).toBe("whole-revision");
7589
+ } finally {
7590
+ await doc.close();
7591
+ }
7592
+ });
7593
+ test("sign: a lock freezes the named fields and is mirrored on the field", async () => {
7594
+ if (!supported()) return;
7595
+ const doc = await open(engineOf(), opts, fx());
7596
+ try {
7597
+ const prepared = await doc.signatures.prepare({
7598
+ field: sigRef(),
7599
+ lock: {
7600
+ action: "include",
7601
+ fields: [fx().textField]
7602
+ }
7603
+ });
7604
+ const result = await doc.signatures.complete({
7605
+ signingId: prepared.signingId,
7606
+ cms: FAKE_CMS,
7607
+ expectedVersion: prepared.expectedVersion
7608
+ });
7609
+ expect(result.signature.fieldMdp).toEqual({
7610
+ action: "include",
7611
+ fields: [fx().textField]
7612
+ });
7613
+ expect(result.signature.lock).toEqual({
7614
+ action: "include",
7615
+ fields: [fx().textField]
7616
+ });
7617
+ expect(result.protection.fieldLocks.length >= 1).toBe(true);
7618
+ expect(await caughtCode(() => doc.forms.setValue(textRef(), {
7619
+ type: "text",
7620
+ value: "nope"
7621
+ }))).toBe(EngineErrorCode.ProtectedDocument);
7622
+ } finally {
7623
+ await doc.close();
7624
+ }
7625
+ });
7626
+ test("sign: abort discards the candidate and lifts the fence", async () => {
7627
+ if (!supported()) return;
7628
+ const doc = await open(engineOf(), opts, fx());
7629
+ try {
7630
+ const v0 = await doc.version();
7631
+ const prepared = await doc.signatures.prepare({ field: sigRef() });
7632
+ expect((await doc.signatures.abort(prepared.signingId)).status).toBe("aborted");
7633
+ expect((await doc.signatures.list()).signatures[0].signed).toBe(false);
7634
+ expect((await doc.version()).sha256).toBe(v0.sha256);
7635
+ await doc.forms.setValue(textRef(), {
7636
+ type: "text",
7637
+ value: "free again"
7638
+ });
7639
+ expect(await caughtCode(() => doc.signatures.complete({
7640
+ signingId: prepared.signingId,
7641
+ cms: FAKE_CMS,
7642
+ expectedVersion: prepared.expectedVersion
7643
+ }))).toBe(EngineErrorCode.NotFound);
7644
+ } finally {
7645
+ await doc.close();
7646
+ }
7647
+ });
7648
+ test("sign: artwork from a PDF page lands on the widget", async () => {
7649
+ if (!supported()) return;
7650
+ const doc = await open(engineOf(), opts, fx());
7651
+ try {
7652
+ const artwork = await opts.fixtures.artwork.bytes();
7653
+ const prepared = await doc.signatures.prepare({
7654
+ field: sigRef(),
7655
+ appearance: {
7656
+ pdf: artwork,
7657
+ pageIndex: 0
7658
+ }
7659
+ });
7660
+ const result = await doc.signatures.complete({
7661
+ signingId: prepared.signingId,
7662
+ cms: FAKE_CMS,
7663
+ expectedVersion: prepared.expectedVersion
7664
+ });
7665
+ expect(result.signature.coverage).toBe("whole-revision");
7666
+ expect(result.signature.widget).toBeTruthy();
7667
+ const widget = (await doc.page(result.signature.widget.pageObjectNumber).annotations.list()).annotations.find((a) => a.ref.kind === "objectNumber" && a.ref.annotObjectNumber === result.signature.widget.annotObjectNumber);
7668
+ expect(widget).toBeTruthy();
7669
+ } finally {
7670
+ await doc.close();
7671
+ }
7672
+ });
7673
+ if (opts.openKind === "layerBytes") test("sign: a layer session keeps its kind, with an empty layer over the new base", async () => {
7674
+ if (!supported()) return;
7675
+ const doc = await open(engineOf(), opts, fx());
7676
+ try {
7677
+ const prepared = await doc.signatures.prepare({ field: sigRef() });
7678
+ const result = await doc.signatures.complete({
7679
+ signingId: prepared.signingId,
7680
+ cms: FAKE_CMS,
7681
+ expectedVersion: prepared.expectedVersion
7682
+ });
7683
+ const layer = await doc.downloadLayer();
7684
+ expect(layer.byteLength > 0).toBe(true);
7685
+ expect(await sha256Hex(await doc.download())).toBe(result.version.sha256);
7686
+ } finally {
7687
+ await doc.close();
7688
+ }
7689
+ });
7690
+ }
7691
+ async function reopen(engine, opts, id, bytes) {
7692
+ if (opts.openKind === "layerBytes") return engine.open({
7693
+ kind: "layerBytes",
7694
+ id,
7695
+ baseBytes: bytes
7696
+ });
7697
+ return engine.open({
7698
+ kind: "bytes",
7699
+ id,
7700
+ bytes
7701
+ });
7702
+ }
7703
+ async function open(engine, opts, fixture) {
7704
+ if (opts.openKind === "bytes") return engine.open({
7705
+ kind: "bytes",
7706
+ id: fixture.id,
7707
+ bytes: await fixture.bytes()
7708
+ });
7709
+ if (opts.openKind === "layerBytes") return engine.open({
7710
+ kind: "layerBytes",
7711
+ id: `${fixture.id}-layer`,
7712
+ baseBytes: await fixture.bytes()
7713
+ });
7714
+ return engine.open({
7715
+ kind: "id",
7716
+ id: fixture.cloudId ?? fixture.id
7717
+ });
7718
+ }
7719
+ function concat(a, b) {
7720
+ const out = new Uint8Array(a.byteLength + b.byteLength);
7721
+ out.set(a, 0);
7722
+ out.set(b, a.byteLength);
7723
+ return out;
7724
+ }
7725
+ function bytesEqual(a, b) {
7726
+ if (a.byteLength !== b.byteLength) return false;
7727
+ for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;
7728
+ return true;
7729
+ }
7730
+ function toHex(bytes) {
7731
+ let out = "";
7732
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
7733
+ return out;
7734
+ }
7735
+ async function sha256Hex(bytes) {
7736
+ const copy = new Uint8Array(bytes.byteLength);
7737
+ copy.set(bytes);
7738
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", copy);
7739
+ return toHex(new Uint8Array(digest));
7740
+ }
7741
+ //#endregion
7742
+ export { TEXT_DIVERGENCE_CASES, diffAnnotationListSnapshot, diffAnnotationListSnapshotAll, runActionsConformance, runAnnotationAppearanceConformance, runAnnotationAppearanceExportConformance, runAnnotationFlattenConformance, runAnnotationMutationConformance, runAnnotationReadConformance, runAttachmentConformance, runDocumentEventsConformance, runFormConformance, runMetadataConformance, runNamedPagesConformance, runPageDeleteConformance, runPageExtractConformance, runPageFlattenConformance, runPageGeometryOrientationConformance, runPageInsertBlankConformance, runPageInsertConformance, runPageReorderConformance, runPageRotateConformance, runPageTextConformance, runPieceInfoConformance, runRedactionApplyConformance, runSearchConformance, runSignatureConformance, runTextDivergenceConformance };
6224
7743
 
6225
7744
  //# sourceMappingURL=conformance.js.map