@embedpdf/engine-core 3.0.0-next.12 → 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-x6tKulNV.d.cts → AnnotationListSnapshot-D3mFut1e.d.cts} +1155 -171
  6. package/dist/{AnnotationListSnapshot-x6tKulNV.d.ts → AnnotationListSnapshot-D3mFut1e.d.ts} +1155 -171
  7. package/dist/{DocumentSecurityService-CK0pejAK.d.cts → DocumentSecurityService-ClTZc0zO.d.cts} +2 -2
  8. package/dist/{DocumentSecurityService-9MawYxP-.d.ts → DocumentSecurityService-_JRjqvoI.d.ts} +2 -2
  9. package/dist/{Engine-Ds0ytbl6.d.ts → Engine-Bx0NOfmd.d.cts} +166 -11
  10. package/dist/{Engine--0XF0UAK.d.cts → Engine-DxtpCYcP.d.ts} +166 -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-8Hl1AYNU.cjs → builders-Bqm8yTpi.cjs} +1470 -4
  16. package/dist/builders-Bqm8yTpi.cjs.map +1 -0
  17. package/dist/{builders-DOjcKDWQ.js → builders-Dqo2X-dt.js} +1351 -5
  18. package/dist/builders-Dqo2X-dt.js.map +1 -0
  19. package/dist/conformance.cjs +1079 -13
  20. package/dist/conformance.cjs.map +1 -1
  21. package/dist/conformance.d.cts +70 -3
  22. package/dist/conformance.d.ts +70 -3
  23. package/dist/conformance.js +1079 -14
  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 +192 -5
  39. package/dist/runtime.d.ts +192 -5
  40. package/dist/runtime.js +6 -6
  41. package/dist/{schemas-DOyVEPk7.js → schemas-BLARNCgx.js} +420 -4
  42. package/dist/schemas-BLARNCgx.js.map +1 -0
  43. package/dist/{schemas-RChsAMWw.cjs → schemas-DIqFMmrC.cjs} +581 -3
  44. package/dist/schemas-DIqFMmrC.cjs.map +1 -0
  45. package/dist/wire.cjs +228 -12
  46. package/dist/wire.cjs.map +1 -1
  47. package/dist/wire.d.cts +415 -4
  48. package/dist/wire.d.ts +415 -4
  49. package/dist/wire.js +199 -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-8Hl1AYNU.cjs.map +0 -1
  55. package/dist/builders-DOjcKDWQ.js.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-DOyVEPk7.js.map +0 -1
  59. package/dist/schemas-RChsAMWw.cjs.map +0 -1
@@ -1,3 +1,142 @@
1
+ //#region src/signature/protection.ts
2
+ /**
3
+ * Bumped whenever the derivation below (or a judgement rule) changes meaning.
4
+ * Rides every protection and every analysis, and keys the cloud's immutable
5
+ * analysis URLs. v4 (the `signature-compat` corpus, 91 Acrobat observations):
6
+ * each signature is judged on the net state of the document against the
7
+ * revision it sealed; an object written again with the sealed value is not
8
+ * a modification; an approval signature permits commenting; field
9
+ * properties are judged per level; a lock binds only the signature that
10
+ * declares it; a changed shared resource must be allowed by every use.
11
+ */
12
+ const SIGNATURE_POLICY_VERSION = 4;
13
+ /**
14
+ * How a validator reads an approval signature. ISO 32000 defines permitted
15
+ * changes only for certification signatures; Acrobat reads an approval
16
+ * signature as "Form Fill-in, Signing and Commenting are allowed" (its own
17
+ * permission text under every such signature) and keeps it valid through a
18
+ * Square annotation added, deleted or recoloured afterwards (corpus
19
+ * `signature-compat` v3 cases 88, 90, 91: "Annotations Created/Deleted/
20
+ * Modified", signature valid). pyHanko judges stricter (fill only); Acrobat
21
+ * is the validator recipients use, so the baseline is `annotate`.
22
+ */
23
+ const APPROVAL_BASELINE = "annotate";
24
+ const LEVEL_RANK = {
25
+ none: 0,
26
+ lta: 1,
27
+ fill: 2,
28
+ annotate: 3
29
+ };
30
+ /** DocMDP `/P` → the modification level it permits. */
31
+ function levelFromPermission(permission) {
32
+ return permission === 1 ? "lta" : permission === 2 ? "fill" : "annotate";
33
+ }
34
+ function minLevel(a, b) {
35
+ return LEVEL_RANK[a] <= LEVEL_RANK[b] ? a : b;
36
+ }
37
+ /** Whether `level` (null = unsigned, everything allowed) permits changes of kind `needed`. */
38
+ function levelAllows(level, needed) {
39
+ return level === null || LEVEL_RANK[level] >= LEVEL_RANK[needed];
40
+ }
41
+ /** Hierarchical name match: `group` covers `group.total`. */
42
+ function lockNameCovers(lockedName, fieldName) {
43
+ return fieldName === lockedName || fieldName.startsWith(`${lockedName}.`);
44
+ }
45
+ function lockCovers(spec, fieldName) {
46
+ switch (spec.action) {
47
+ case "all": return true;
48
+ case "include": return spec.fields.some((name) => lockNameCovers(name, fieldName));
49
+ case "exclude": return !spec.fields.some((name) => lockNameCovers(name, fieldName));
50
+ }
51
+ }
52
+ /**
53
+ * Derive what the signatures already in a document mean for what comes
54
+ * after. Pure and versioned (`SIGNATURE_POLICY_VERSION`); the same function
55
+ * runs in the browser worker, on the native server, and in the conformance
56
+ * suite.
57
+ *
58
+ * enforced = min(certification P, every signed field's /Lock /P), or null
59
+ * when nothing was declared. What the engine refuses.
60
+ * judged = enforced, or the approval baseline when signatures exist
61
+ * without a declaration, or null when nothing is signed. What
62
+ * the revision analysis judges later changes against. A
63
+ * declaration governs the judgement too: a P=3 certifier really
64
+ * did allow annotations.
65
+ * fieldLocks = every signed signature's FieldMDP and the /Lock of every
66
+ * signed field. Unsigned fields' /Lock entries describe a
67
+ * FUTURE signature and lock nothing yet.
68
+ */
69
+ function deriveProtection(signatures) {
70
+ let signed = false;
71
+ let enforced = null;
72
+ let certification = null;
73
+ const fieldLocks = [];
74
+ for (const sig of signatures) {
75
+ if (!sig.signed) continue;
76
+ signed = true;
77
+ if (sig.catalogCertification && sig.docMdp !== null && certification === null) {
78
+ certification = {
79
+ signatureIndex: sig.index,
80
+ permission: sig.docMdp
81
+ };
82
+ enforced = minLevel(enforced ?? "annotate", levelFromPermission(sig.docMdp));
83
+ }
84
+ if (sig.fieldMdp) fieldLocks.push({
85
+ signatureIndex: sig.index,
86
+ source: "fieldmdp",
87
+ spec: sig.fieldMdp
88
+ });
89
+ if (sig.lock) {
90
+ fieldLocks.push({
91
+ signatureIndex: sig.index,
92
+ source: "lock",
93
+ spec: sig.lock
94
+ });
95
+ if (sig.lock.permission !== void 0) enforced = minLevel(enforced ?? "annotate", levelFromPermission(sig.lock.permission));
96
+ }
97
+ }
98
+ return {
99
+ enforced,
100
+ judged: signed ? enforced ?? "annotate" : null,
101
+ certification,
102
+ fieldLocks,
103
+ policyVersion: 4
104
+ };
105
+ }
106
+ /** The lock that freezes `fieldName`, if any. */
107
+ function fieldLockFor(protection, fieldName) {
108
+ for (const lock of protection.fieldLocks) if (lockCovers(lock.spec, fieldName)) return lock;
109
+ return null;
110
+ }
111
+ /**
112
+ * The capabilities a protection removes from every caller, admin scope
113
+ * included — document-derived authority, exactly like encryption bits.
114
+ *
115
+ * Only what was DECLARED is refused: a refusal is a promise a signer made,
116
+ * never a guess about a validator. A certification (or a lock with /P)
117
+ * declares what may follow, and everything outside that — page edits,
118
+ * redaction, field authoring, attachments, and below P=3 annotations, below
119
+ * P=2 form fill — is refused. A plain approval signature declares nothing:
120
+ * the same edits stay possible and are then JUDGED, the verdict saying the
121
+ * signature no longer holds, exactly as Acrobat does. The one exception is a
122
+ * rewrite: it does not invalidate signatures, it erases them, so any signed
123
+ * document refuses it. Per-field locks are enforced by the form mutator.
124
+ */
125
+ function protectedCapabilities(protection) {
126
+ const out = /* @__PURE__ */ new Set();
127
+ if (!protection || protection.judged === null) return out;
128
+ out.add("doc.download.flattened");
129
+ if (protection.enforced === null) return out;
130
+ out.add("doc.pages.modify");
131
+ out.add("doc.pages.assemble");
132
+ out.add("doc.redact");
133
+ out.add("doc.attachments.modify");
134
+ out.add("doc.forms.modify");
135
+ if (!levelAllows(protection.enforced, "annotate")) out.add("doc.annotate.modify");
136
+ if (!levelAllows(protection.enforced, "fill")) out.add("doc.forms.fill");
137
+ return out;
138
+ }
139
+ //#endregion
1
140
  //#region src/auth/scope/errors.ts
2
141
  /**
3
142
  * Thrown by {@link parseScope} / {@link validateScopeArray} when a scope
@@ -76,7 +215,9 @@ const KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
76
215
  "doc.annotate.modify",
77
216
  "doc.metadata.modify",
78
217
  "doc.attachments.modify",
79
- "doc.redact"
218
+ "doc.redact",
219
+ "doc.sign",
220
+ "doc.sign.certify"
80
221
  ]);
81
222
  /**
82
223
  * Parse a single scope string into a structured `ParsedScope`.
@@ -166,18 +307,19 @@ function parseCapability(raw) {
166
307
  * Wildcard `*` short-circuits to true. Anything not explicitly granted
167
308
  * (or expanded from `pdf.permissions`) is denied.
168
309
  */
169
- function checkCapability(capability, rawScope, pdfBits) {
310
+ function checkCapability(capability, rawScope, pdfBits, protection = null) {
311
+ if (protectedCapabilities(protection).has(capability)) return false;
170
312
  const parsed = rawScope.map(parseScope);
171
313
  if (parsed.some((s) => s.kind === "wildcard")) return true;
172
- return expandedCapabilities(parsed, pdfBits).has(capability);
314
+ return expandedCapabilities(parsed, pdfBits, protection).has(capability);
173
315
  }
174
316
  /**
175
317
  * True iff the scope grants AT LEAST ONE of `capabilities`. Convenience
176
318
  * for routes like `/text` (`doc.text.copy` OR `doc.text.search`) and
177
319
  * `/geometry` (`doc.text.select` OR `doc.text.search`).
178
320
  */
179
- function checkAnyCapability(capabilities, rawScope, pdfBits) {
180
- return capabilities.some((c) => checkCapability(c, rawScope, pdfBits));
321
+ function checkAnyCapability(capabilities, rawScope, pdfBits, protection = null) {
322
+ return capabilities.some((c) => checkCapability(c, rawScope, pdfBits, protection));
181
323
  }
182
324
  /**
183
325
  * True iff the scope grants the collab action against the target record.
@@ -225,7 +367,7 @@ function checkCollab(action, target, rawScope, identity, pdfBits) {
225
367
  * before calling this. Returning the expanded set is useful for the
226
368
  * `/access` response's `effectiveScope` and for advisory UI surfacing.
227
369
  */
228
- function expandedCapabilities(parsed, pdfBits) {
370
+ function expandedCapabilities(parsed, pdfBits, protection = null) {
229
371
  const out = /* @__PURE__ */ new Set();
230
372
  let hasAnnotationCollab = false;
231
373
  for (const s of parsed) if (s.kind === "capability") out.add(s.name);
@@ -238,6 +380,7 @@ function expandedCapabilities(parsed, pdfBits) {
238
380
  }
239
381
  if (out.has("doc.forms.fill")) out.add("doc.forms.read");
240
382
  if (hasAnnotationCollab) out.add("doc.annotate.read");
383
+ for (const removed of protectedCapabilities(protection)) out.delete(removed);
241
384
  return out;
242
385
  }
243
386
  /**
@@ -245,8 +388,8 @@ function expandedCapabilities(parsed, pdfBits) {
245
388
  * returns the expanded capability set. Exposed for the `/access`
246
389
  * endpoint and for the SDK helper that surfaces `effectiveScope`.
247
390
  */
248
- function expandRawScope(rawScope, pdfBits) {
249
- return expandedCapabilities(rawScope.map(parseScope), pdfBits);
391
+ function expandRawScope(rawScope, pdfBits, protection = null) {
392
+ return expandedCapabilities(rawScope.map(parseScope), pdfBits, protection);
250
393
  }
251
394
  /**
252
395
  * Test a single collab filter against a target record + the caller's
@@ -355,10 +498,19 @@ function addPdfPermissions(out, b) {
355
498
  }
356
499
  if (b.bit11) out.add("doc.pages.assemble");
357
500
  if (b.bit6) out.add("doc.annotate.modify");
358
- if (b.bit6 || b.bit9) out.add("doc.forms.fill");
501
+ if (b.bit6 || b.bit9) {
502
+ out.add("doc.forms.fill");
503
+ out.add("doc.sign");
504
+ }
359
505
  if (b.bit6 && b.bit4) out.add("doc.forms.modify");
360
506
  }
361
507
  //#endregion
508
+ Object.defineProperty(exports, "APPROVAL_BASELINE", {
509
+ enumerable: true,
510
+ get: function() {
511
+ return APPROVAL_BASELINE;
512
+ }
513
+ });
362
514
  Object.defineProperty(exports, "InvalidScope", {
363
515
  enumerable: true,
364
516
  get: function() {
@@ -377,6 +529,12 @@ Object.defineProperty(exports, "PermissionDenied", {
377
529
  return PermissionDenied;
378
530
  }
379
531
  });
532
+ Object.defineProperty(exports, "SIGNATURE_POLICY_VERSION", {
533
+ enumerable: true,
534
+ get: function() {
535
+ return SIGNATURE_POLICY_VERSION;
536
+ }
537
+ });
380
538
  Object.defineProperty(exports, "checkAnyCapability", {
381
539
  enumerable: true,
382
540
  get: function() {
@@ -401,6 +559,12 @@ Object.defineProperty(exports, "checkSetGroup", {
401
559
  return checkSetGroup;
402
560
  }
403
561
  });
562
+ Object.defineProperty(exports, "deriveProtection", {
563
+ enumerable: true,
564
+ get: function() {
565
+ return deriveProtection;
566
+ }
567
+ });
404
568
  Object.defineProperty(exports, "expandRawScope", {
405
569
  enumerable: true,
406
570
  get: function() {
@@ -413,18 +577,60 @@ Object.defineProperty(exports, "expandedCapabilities", {
413
577
  return expandedCapabilities;
414
578
  }
415
579
  });
580
+ Object.defineProperty(exports, "fieldLockFor", {
581
+ enumerable: true,
582
+ get: function() {
583
+ return fieldLockFor;
584
+ }
585
+ });
416
586
  Object.defineProperty(exports, "filterMatches", {
417
587
  enumerable: true,
418
588
  get: function() {
419
589
  return filterMatches;
420
590
  }
421
591
  });
592
+ Object.defineProperty(exports, "levelAllows", {
593
+ enumerable: true,
594
+ get: function() {
595
+ return levelAllows;
596
+ }
597
+ });
598
+ Object.defineProperty(exports, "levelFromPermission", {
599
+ enumerable: true,
600
+ get: function() {
601
+ return levelFromPermission;
602
+ }
603
+ });
604
+ Object.defineProperty(exports, "lockCovers", {
605
+ enumerable: true,
606
+ get: function() {
607
+ return lockCovers;
608
+ }
609
+ });
610
+ Object.defineProperty(exports, "lockNameCovers", {
611
+ enumerable: true,
612
+ get: function() {
613
+ return lockNameCovers;
614
+ }
615
+ });
616
+ Object.defineProperty(exports, "minLevel", {
617
+ enumerable: true,
618
+ get: function() {
619
+ return minLevel;
620
+ }
621
+ });
422
622
  Object.defineProperty(exports, "parseScope", {
423
623
  enumerable: true,
424
624
  get: function() {
425
625
  return parseScope;
426
626
  }
427
627
  });
628
+ Object.defineProperty(exports, "protectedCapabilities", {
629
+ enumerable: true,
630
+ get: function() {
631
+ return protectedCapabilities;
632
+ }
633
+ });
428
634
  Object.defineProperty(exports, "validateScopeArray", {
429
635
  enumerable: true,
430
636
  get: function() {
@@ -432,4 +638,4 @@ Object.defineProperty(exports, "validateScopeArray", {
432
638
  }
433
639
  });
434
640
 
435
- //# sourceMappingURL=resolver-D-jumvgM.cjs.map
641
+ //# sourceMappingURL=resolver-CYVNV4hn.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver-CYVNV4hn.cjs","names":[],"sources":["../src/signature/protection.ts","../src/auth/scope/errors.ts","../src/auth/scope/parser.ts","../src/auth/scope/resolver.ts"],"sourcesContent":["import type { DocCapability } from '../auth/scope/types';\nimport type {\n DocMdpPermission,\n DocumentFieldLock,\n DocumentProtection,\n FieldLockSpec,\n ModificationLevel,\n SignatureDTO,\n} from './types';\n\n/**\n * Bumped whenever the derivation below (or a judgement rule) changes meaning.\n * Rides every protection and every analysis, and keys the cloud's immutable\n * analysis URLs. v4 (the `signature-compat` corpus, 91 Acrobat observations):\n * each signature is judged on the net state of the document against the\n * revision it sealed; an object written again with the sealed value is not\n * a modification; an approval signature permits commenting; field\n * properties are judged per level; a lock binds only the signature that\n * declares it; a changed shared resource must be allowed by every use.\n */\nexport const SIGNATURE_POLICY_VERSION = 4;\n\n/**\n * How a validator reads an approval signature. ISO 32000 defines permitted\n * changes only for certification signatures; Acrobat reads an approval\n * signature as \"Form Fill-in, Signing and Commenting are allowed\" (its own\n * permission text under every such signature) and keeps it valid through a\n * Square annotation added, deleted or recoloured afterwards (corpus\n * `signature-compat` v3 cases 88, 90, 91: \"Annotations Created/Deleted/\n * Modified\", signature valid). pyHanko judges stricter (fill only); Acrobat\n * is the validator recipients use, so the baseline is `annotate`.\n */\nexport const APPROVAL_BASELINE: ModificationLevel = 'annotate';\n\nconst LEVEL_RANK: Record<ModificationLevel, number> = { none: 0, lta: 1, fill: 2, annotate: 3 };\n\n/** DocMDP `/P` → the modification level it permits. */\nexport function levelFromPermission(permission: DocMdpPermission): ModificationLevel {\n return permission === 1 ? 'lta' : permission === 2 ? 'fill' : 'annotate';\n}\n\nexport function minLevel(a: ModificationLevel, b: ModificationLevel): ModificationLevel {\n return LEVEL_RANK[a] <= LEVEL_RANK[b] ? a : b;\n}\n\n/** Whether `level` (null = unsigned, everything allowed) permits changes of kind `needed`. */\nexport function levelAllows(level: ModificationLevel | null, needed: ModificationLevel): boolean {\n return level === null || LEVEL_RANK[level] >= LEVEL_RANK[needed];\n}\n\n/** Hierarchical name match: `group` covers `group.total`. */\nexport function lockNameCovers(lockedName: string, fieldName: string): boolean {\n return fieldName === lockedName || fieldName.startsWith(`${lockedName}.`);\n}\n\nexport function lockCovers(spec: FieldLockSpec, fieldName: string): boolean {\n switch (spec.action) {\n case 'all':\n return true;\n case 'include':\n return spec.fields.some((name) => lockNameCovers(name, fieldName));\n case 'exclude':\n return !spec.fields.some((name) => lockNameCovers(name, fieldName));\n }\n}\n\n/**\n * Derive what the signatures already in a document mean for what comes\n * after. Pure and versioned (`SIGNATURE_POLICY_VERSION`); the same function\n * runs in the browser worker, on the native server, and in the conformance\n * suite.\n *\n * enforced = min(certification P, every signed field's /Lock /P), or null\n * when nothing was declared. What the engine refuses.\n * judged = enforced, or the approval baseline when signatures exist\n * without a declaration, or null when nothing is signed. What\n * the revision analysis judges later changes against. A\n * declaration governs the judgement too: a P=3 certifier really\n * did allow annotations.\n * fieldLocks = every signed signature's FieldMDP and the /Lock of every\n * signed field. Unsigned fields' /Lock entries describe a\n * FUTURE signature and lock nothing yet.\n */\nexport function deriveProtection(signatures: ReadonlyArray<SignatureDTO>): DocumentProtection {\n let signed = false;\n let enforced: ModificationLevel | null = null;\n let certification: DocumentProtection['certification'] = null;\n const fieldLocks: DocumentFieldLock[] = [];\n\n for (const sig of signatures) {\n if (!sig.signed) continue;\n signed = true;\n if (sig.catalogCertification && sig.docMdp !== null && certification === null) {\n certification = { signatureIndex: sig.index, permission: sig.docMdp };\n enforced = minLevel(enforced ?? 'annotate', levelFromPermission(sig.docMdp));\n }\n if (sig.fieldMdp) {\n fieldLocks.push({ signatureIndex: sig.index, source: 'fieldmdp', spec: sig.fieldMdp });\n }\n if (sig.lock) {\n fieldLocks.push({ signatureIndex: sig.index, source: 'lock', spec: sig.lock });\n if (sig.lock.permission !== undefined) {\n enforced = minLevel(enforced ?? 'annotate', levelFromPermission(sig.lock.permission));\n }\n }\n }\n\n const judged = signed ? (enforced ?? APPROVAL_BASELINE) : null;\n return { enforced, judged, certification, fieldLocks, policyVersion: SIGNATURE_POLICY_VERSION };\n}\n\n/** The lock that freezes `fieldName`, if any. */\nexport function fieldLockFor(\n protection: DocumentProtection,\n fieldName: string,\n): DocumentFieldLock | null {\n for (const lock of protection.fieldLocks) {\n if (lockCovers(lock.spec, fieldName)) return lock;\n }\n return null;\n}\n\n/**\n * The capabilities a protection removes from every caller, admin scope\n * included — document-derived authority, exactly like encryption bits.\n *\n * Only what was DECLARED is refused: a refusal is a promise a signer made,\n * never a guess about a validator. A certification (or a lock with /P)\n * declares what may follow, and everything outside that — page edits,\n * redaction, field authoring, attachments, and below P=3 annotations, below\n * P=2 form fill — is refused. A plain approval signature declares nothing:\n * the same edits stay possible and are then JUDGED, the verdict saying the\n * signature no longer holds, exactly as Acrobat does. The one exception is a\n * rewrite: it does not invalidate signatures, it erases them, so any signed\n * document refuses it. Per-field locks are enforced by the form mutator.\n */\nexport function protectedCapabilities(protection: DocumentProtection | null): Set<DocCapability> {\n const out = new Set<DocCapability>();\n if (!protection || protection.judged === null) return out;\n out.add('doc.download.flattened');\n if (protection.enforced === null) return out; // nothing declared: judged, not refused\n out.add('doc.pages.modify');\n out.add('doc.pages.assemble');\n out.add('doc.redact');\n out.add('doc.attachments.modify');\n out.add('doc.forms.modify');\n if (!levelAllows(protection.enforced, 'annotate')) out.add('doc.annotate.modify');\n if (!levelAllows(protection.enforced, 'fill')) out.add('doc.forms.fill');\n return out;\n}\n","/**\n * Thrown by {@link parseScope} / {@link validateScopeArray} when a scope\n * string does not match any known grammar (capability, collab, virtual,\n * or wildcard).\n *\n * Carries the offending scope verbatim so route/JWT-layer error\n * handlers can echo it to the customer.\n */\nexport class InvalidScope extends Error {\n constructor(\n public readonly scope: string,\n reason: string,\n ) {\n super(`invalid scope \"${scope}\": ${reason}`);\n this.name = 'InvalidScope';\n }\n}\n\n/**\n * Thrown by route guards and engine-local enforcement when a capability\n * or collab action is denied for the current request/handle.\n *\n * `required` names what the caller needed (e.g., \"doc.render\" or\n * \"annotations:update\"). `context` is an optional label such as \"local\"\n * or \"/v1/docs/.../pages/2/render\".\n */\nexport class PermissionDenied extends Error {\n constructor(\n public readonly required: string,\n public readonly context?: string,\n ) {\n super(`permission denied${context ? ` (${context})` : ''}: ${required}`);\n this.name = 'PermissionDenied';\n }\n}\n\n/**\n * Thrown at engine-local open time when the supplied scope includes\n * collab filters (`:self`, `:group=...`) but the identity claims needed\n * to resolve them are missing. Fails loudly at open so the configuration\n * mistake is visible immediately instead of producing silent denies at\n * every annotation mutation.\n */\nexport class MissingIdentity extends Error {\n constructor(scope: string) {\n super(`scope \"${scope}\" requires identity claims (user_id and/or groups)`);\n this.name = 'MissingIdentity';\n }\n}\n","import { InvalidScope } from './errors';\nimport type { CollabAction, CollabFilter, DocCapability, ParsedScope } from './types';\n\n/**\n * Closed set of recognized capability strings. Membership is the\n * authoritative source of truth — adding a new capability requires\n * extending the `DocCapability` union AND adding it here. The parser\n * rejects anything outside this set.\n *\n * Removed legacy names (e.g., `doc.read`, `doc.edit-pages`, `doc.save`)\n * are deliberately absent so any JWT carrying them fails at verify\n * time with a clear error.\n */\nconst KNOWN_CAPABILITIES: ReadonlySet<DocCapability> = new Set([\n 'doc.open',\n 'doc.render',\n 'doc.text.select',\n 'doc.text.copy',\n 'doc.text.search',\n 'doc.content.copy',\n 'doc.download',\n 'doc.download.flattened',\n 'doc.print',\n 'doc.print.high',\n 'doc.pages.modify',\n 'doc.pages.assemble',\n 'doc.forms.read',\n 'doc.forms.fill',\n 'doc.forms.modify',\n 'doc.forms.submit',\n 'doc.annotate.read',\n 'doc.annotate.modify',\n 'doc.metadata.modify',\n 'doc.attachments.modify',\n 'doc.redact',\n 'doc.sign',\n 'doc.sign.certify',\n]);\n\n/**\n * Parse a single scope string into a structured `ParsedScope`.\n *\n * Recognised shapes:\n * - `\"*\"` → wildcard\n * - `\"pdf.permissions\"` → virtual (expands at resolve time)\n * - `\"a.b.c...\"` (dotted) → capability\n * - `\"entity:action:filter\"` (colons) → collab\n *\n * Throws {@link InvalidScope} for any string that fails to match one of\n * those shapes, or whose capability/entity/action/filter is not in the\n * closed enum.\n */\nexport function parseScope(raw: string): ParsedScope {\n if (raw === '*') return { kind: 'wildcard' };\n if (raw === 'pdf.permissions') return { kind: 'virtual', name: 'pdf.permissions' };\n if (raw.includes(':')) return parseCollab(raw);\n return parseCapability(raw);\n}\n\n/**\n * Validate every entry in a scope array by attempting to parse it.\n * Throws on the first invalid string. Discards the parse output —\n * callers re-parse at resolution time to keep the array as the\n * authoritative payload.\n */\nexport function validateScopeArray(raw: ReadonlyArray<string>): void {\n for (const s of raw) parseScope(s);\n}\n\nfunction parseCollab(raw: string): ParsedScope {\n // Split on the FIRST two colons only. Filter values may contain\n // colons (UUIDs with `urn:uuid:...`, subject ids like `auth0|user:1`).\n const idx1 = raw.indexOf(':');\n const idx2 = raw.indexOf(':', idx1 + 1);\n if (idx1 === -1 || idx2 === -1) {\n throw new InvalidScope(raw, 'collab scope requires entity:action:filter');\n }\n const entity = raw.slice(0, idx1);\n const action = raw.slice(idx1 + 1, idx2);\n const filterStr = raw.slice(idx2 + 1);\n\n if (entity !== 'annotations') {\n throw new InvalidScope(raw, `unknown collab entity: ${entity}`);\n }\n if (\n action !== 'create' &&\n action !== 'update' &&\n action !== 'delete' &&\n action !== 'set-group' &&\n action !== '*'\n ) {\n throw new InvalidScope(raw, `unknown collab action: ${action}`);\n }\n const filter = parseFilter(filterStr, raw);\n\n // `set-group` is a group-assignment authority, not a per-record collab\n // filter. The only meaningful filter values are `all` (assign to any\n // group) and `group=X` (assign to group X). `:self` and `:createdBy=Y`\n // can't be sensibly applied to \"what group is this annotation in\", so\n // we reject them at parse time rather than silently always-denying.\n if (action === 'set-group' && (filter.kind === 'self' || filter.kind === 'createdBy')) {\n throw new InvalidScope(\n raw,\n `set-group only supports :all or :group=<id> filters (got :${filter.kind})`,\n );\n }\n\n return {\n kind: 'collab',\n entity,\n action: action as CollabAction | '*',\n filter,\n };\n}\n\nfunction parseFilter(s: string, raw: string): CollabFilter {\n if (s === 'all') return { kind: 'all' };\n if (s === 'self') return { kind: 'self' };\n if (s.startsWith('createdBy=')) {\n const v = s.slice('createdBy='.length);\n if (!v) throw new InvalidScope(raw, 'createdBy= requires a value');\n return { kind: 'createdBy', userId: v };\n }\n if (s.startsWith('group=')) {\n const v = s.slice('group='.length);\n if (!v) throw new InvalidScope(raw, 'group= requires a value');\n return { kind: 'group', groupId: v };\n }\n throw new InvalidScope(raw, `unknown filter: ${s}`);\n}\n\n// Capability shape: lowercase dotted segments, e.g. \"doc.text.copy\".\n// Validates shape first, then membership in the closed set.\nconst CAPABILITY_SHAPE = /^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$/;\n\nfunction parseCapability(raw: string): ParsedScope {\n if (!CAPABILITY_SHAPE.test(raw)) {\n throw new InvalidScope(raw, 'invalid capability name shape');\n }\n if (!KNOWN_CAPABILITIES.has(raw as DocCapability)) {\n throw new InvalidScope(raw, `unknown capability: ${raw}`);\n }\n return { kind: 'capability', name: raw as DocCapability };\n}\n","import { parseScope } from './parser';\nimport type {\n CollabAction,\n CollabFilter,\n DocCapability,\n IdentityClaims,\n ParsedScope,\n PdfBits,\n} from './types';\nimport { protectedCapabilities } from '../../signature/protection';\nimport type { DocumentProtection } from '../../signature/types';\n\n/**\n * Resolved collab subject — the per-record identity bits used to test\n * collab filters against. Sourced from the target record's stored\n * `/EMBD_Metadata/UserID` and `/GroupID` at mutation time.\n */\nexport interface CollabTarget {\n userId?: string;\n groupId?: string;\n}\n\n/**\n * True iff the given capability is granted by the scope array plus the\n * PDF bits visible via `pdf.permissions` expansion.\n *\n * Wildcard `*` short-circuits to true. Anything not explicitly granted\n * (or expanded from `pdf.permissions`) is denied.\n */\nexport function checkCapability(\n capability: DocCapability,\n rawScope: ReadonlyArray<string>,\n pdfBits: PdfBits,\n protection: DocumentProtection | null = null,\n): boolean {\n // Document-derived authority comes first: what the signatures in the\n // file forbid, no scope grants — the wildcard included.\n if (protectedCapabilities(protection).has(capability)) return false;\n const parsed = rawScope.map(parseScope);\n if (parsed.some((s) => s.kind === 'wildcard')) return true;\n return expandedCapabilities(parsed, pdfBits, protection).has(capability);\n}\n\n/**\n * True iff the scope grants AT LEAST ONE of `capabilities`. Convenience\n * for routes like `/text` (`doc.text.copy` OR `doc.text.search`) and\n * `/geometry` (`doc.text.select` OR `doc.text.search`).\n */\nexport function checkAnyCapability(\n capabilities: ReadonlyArray<DocCapability>,\n rawScope: ReadonlyArray<string>,\n pdfBits: PdfBits,\n protection: DocumentProtection | null = null,\n): boolean {\n return capabilities.some((c) => checkCapability(c, rawScope, pdfBits, protection));\n}\n\n/**\n * True iff the scope grants the collab action against the target record.\n *\n * Narrowing model. For each action independently:\n * 1. wildcard `*` → allow (global escape hatch)\n * 2. if any collab scope applies to this action → NARROW: only those\n * collab filters decide. If none match the target, deny — even if\n * `doc.annotate.modify` is also present. This is what makes\n * `[modify, update:self]` correctly mean \"edit own only\" rather\n * than \"edit anyone via modify-bypass.\"\n * 3. otherwise, if `doc.annotate.modify` is present → allow. This is\n * the broad PDF-bit-derived default for create/update/delete when\n * no per-action collab filter has been written.\n * 4. otherwise → deny.\n *\n * For create: `target` should be built by the caller from JWT identity\n * (`{ userId: caller.user_id, groupId: caller.group_id }`). `:self` and\n * `:all` then trivially pass; `:group=X` is the meaningful filter (only\n * matches when the caller's default group is X).\n *\n * For update / delete: `target` is the existing row's owner.\n */\nexport function checkCollab(\n action: CollabAction,\n target: CollabTarget,\n rawScope: ReadonlyArray<string>,\n identity: IdentityClaims,\n pdfBits: PdfBits,\n): boolean {\n const parsed = rawScope.map(parseScope);\n if (parsed.some((s) => s.kind === 'wildcard')) return true;\n\n const applicableCollab = parsed.filter(\n (s): s is Extract<ParsedScope, { kind: 'collab' }> =>\n s.kind === 'collab' &&\n s.entity === 'annotations' &&\n (s.action === action || s.action === '*'),\n );\n\n if (applicableCollab.length > 0) {\n // Narrow: presence of any applicable collab scope shadows\n // `modify`-as-default for this action.\n return applicableCollab.some((s) => filterMatches(s.filter, target, identity));\n }\n\n // No collab scope for this action — fall back to the broad default.\n return expandedCapabilities(parsed, pdfBits).has('doc.annotate.modify');\n}\n\n/**\n * Compute the full set of granted capabilities after expanding\n * `pdf.permissions` and applying implication rules.\n *\n * Implications applied:\n * - `doc.annotate.modify` implies `doc.annotate.read` (you can't\n * sensibly modify what you can't see).\n * - `doc.forms.modify` implies `doc.forms.fill` and `doc.forms.read`.\n * - `doc.forms.fill` implies `doc.forms.read`.\n * - any annotation collab scope implies `doc.annotate.read`, because\n * mutation routes need to see the target row to evaluate the\n * collab filter against its current owner.\n *\n * Does NOT short-circuit on wildcard — callers do that themselves\n * before calling this. Returning the expanded set is useful for the\n * `/access` response's `effectiveScope` and for advisory UI surfacing.\n */\nexport function expandedCapabilities(\n parsed: ReadonlyArray<ParsedScope>,\n pdfBits: PdfBits,\n protection: DocumentProtection | null = null,\n): Set<DocCapability> {\n const out = new Set<DocCapability>();\n let hasAnnotationCollab = false;\n\n for (const s of parsed) {\n if (s.kind === 'capability') {\n out.add(s.name);\n } else if (s.kind === 'virtual' && s.name === 'pdf.permissions') {\n addPdfPermissions(out, pdfBits);\n } else if (s.kind === 'collab' && s.entity === 'annotations') {\n hasAnnotationCollab = true;\n }\n }\n\n // Implications (apply after the explicit additions above)\n if (out.has('doc.annotate.modify')) out.add('doc.annotate.read');\n if (out.has('doc.forms.modify')) {\n out.add('doc.forms.fill');\n out.add('doc.forms.read');\n }\n if (out.has('doc.forms.fill')) out.add('doc.forms.read');\n if (hasAnnotationCollab) out.add('doc.annotate.read');\n\n // Subtraction last: a signed document's own restrictions win over any\n // grant or implication.\n for (const removed of protectedCapabilities(protection)) out.delete(removed);\n\n return out;\n}\n\n/**\n * Convenience wrapper: takes the raw scope array, parses each entry,\n * returns the expanded capability set. Exposed for the `/access`\n * endpoint and for the SDK helper that surfaces `effectiveScope`.\n */\nexport function expandRawScope(\n rawScope: ReadonlyArray<string>,\n pdfBits: PdfBits,\n protection: DocumentProtection | null = null,\n): Set<DocCapability> {\n return expandedCapabilities(rawScope.map(parseScope), pdfBits, protection);\n}\n\n/**\n * Test a single collab filter against a target record + the caller's\n * identity. Pure function — no side effects, no implicit rules.\n *\n * all → always matches\n * self → matches if identity.user_id === target.userId\n * createdBy=<X> → matches if target.userId === X\n * group=<X> → matches if target.groupId === X\n * AND identity.groups includes X\n *\n * The group-membership check on `group=X` prevents a token from\n * matching annotations in a group it doesn't belong to, even if the\n * target row carries that groupId.\n */\n/**\n * Authority to assign a specific groupId to an annotation.\n *\n * Set-group is decoupled from `doc.annotate.modify`. The reasoning:\n * capabilities like `modify` describe row access — what kind of write\n * you can do to which existing rows — and map to PDF permission bits.\n * Set-group is a cloud-only *assignment authority*: which destination\n * group can you put an annotation into? There is no PDF-bit\n * counterpart, so it doesn't inherit from `modify`.\n *\n * Resolution order:\n * 1. newGroupId === callerDefaultGroupId → true (no real reassignment\n * is happening; the annotation gets the caller's default group)\n * 2. wildcard `*` → true (global escape hatch)\n * 3. `annotations:set-group:all` → true\n * 4. `annotations:set-group:group=<newGroupId>` → true\n * 5. `annotations:*:all` or `annotations:*:group=<newGroupId>` → true\n * (action wildcard includes set-group)\n * 6. otherwise → false\n *\n * Note: independent from membership. A user with\n * `set-group:group=legal` can assign annotations to the legal group\n * even if they're not a member — that's the whole point.\n */\nexport function checkSetGroup(\n newGroupId: string,\n callerDefaultGroupId: string | undefined,\n rawScope: ReadonlyArray<string>,\n _pdfBits: PdfBits,\n): boolean {\n // No authority needed when the caller is assigning their default group.\n if (newGroupId === callerDefaultGroupId) return true;\n\n const parsed = rawScope.map(parseScope);\n if (parsed.some((s) => s.kind === 'wildcard')) return true;\n\n for (const s of parsed) {\n if (s.kind !== 'collab') continue;\n if (s.entity !== 'annotations') continue;\n if (s.action !== 'set-group' && s.action !== '*') continue;\n if (s.filter.kind === 'all') return true;\n if (s.filter.kind === 'group' && s.filter.groupId === newGroupId) return true;\n // :self and :createdBy are rejected at parse time for set-group, so\n // they never appear here. The action-wildcard path could carry those\n // filters legitimately for other actions — silently skip those, they\n // can't satisfy a set-group check.\n }\n return false;\n}\n\nexport function filterMatches(\n filter: CollabFilter,\n target: CollabTarget,\n id: IdentityClaims,\n): boolean {\n switch (filter.kind) {\n case 'all':\n return true;\n case 'self':\n return !!id.user_id && target.userId === id.user_id;\n case 'createdBy':\n return target.userId === filter.userId;\n case 'group':\n return target.groupId === filter.groupId && (id.groups?.includes(filter.groupId) ?? false);\n }\n}\n\n/**\n * Translate `pdf.permissions` (virtual scope) into the concrete\n * capabilities it represents under the current PDF bit configuration.\n *\n * Always adds `doc.open` and `doc.render` — these are cloud-only\n * capabilities with no PDF bit, but `pdf.permissions` is meant to be\n * the \"give the user a working session\" shorthand. Without them, a\n * token with just `['pdf.permissions']` would be useless.\n *\n * Reads are unconditional: ISO 32000 / Acrobat let any reader see\n * existing annotations and form values regardless of permission bits.\n * Bit 6 governs *writing*, not visibility.\n *\n * Bit-derived expansions follow ISO 32000:\n * bit 5 → doc.text.{select, copy, search}, doc.content.copy\n * bit 3 → doc.print\n * bit 12 → doc.print.high (requires bit 3 also set)\n * bit 4 → doc.pages.modify, doc.redact, doc.metadata.modify, doc.attachments.modify\n * bit 11 → doc.pages.assemble\n * bit 6 → doc.annotate.modify\n * bit 6/9 → doc.forms.fill\n * bit 6+4 → doc.forms.modify\n *\n * Note: this same expansion lives in builders.ts as\n * `materializePdfPermissions` for SDK-side use. The two MUST stay in\n * sync; a test in resolver.test.ts pins them together.\n */\nfunction addPdfPermissions(out: Set<DocCapability>, b: PdfBits): void {\n // Always — pdf.permissions means \"give me a working session\"\n out.add('doc.open');\n out.add('doc.render');\n // Reading existing annotations and form values is unconditional —\n // PDF bit 6 governs writes, not visibility.\n out.add('doc.annotate.read');\n out.add('doc.forms.read');\n\n if (b.bit5) {\n out.add('doc.text.select');\n out.add('doc.text.copy');\n out.add('doc.text.search');\n out.add('doc.content.copy');\n }\n if (b.bit3) out.add('doc.print');\n if (b.bit12 && b.bit3) out.add('doc.print.high');\n if (b.bit4) {\n out.add('doc.pages.modify');\n out.add('doc.redact');\n out.add('doc.metadata.modify');\n out.add('doc.attachments.modify');\n }\n if (b.bit11) out.add('doc.pages.assemble');\n if (b.bit6) {\n out.add('doc.annotate.modify');\n }\n if (b.bit6 || b.bit9) {\n out.add('doc.forms.fill');\n out.add('doc.sign');\n }\n if (b.bit6 && b.bit4) out.add('doc.forms.modify');\n}\n"],"mappings":"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"}
@@ -1,3 +1,142 @@
1
+ //#region src/signature/protection.ts
2
+ /**
3
+ * Bumped whenever the derivation below (or a judgement rule) changes meaning.
4
+ * Rides every protection and every analysis, and keys the cloud's immutable
5
+ * analysis URLs. v4 (the `signature-compat` corpus, 91 Acrobat observations):
6
+ * each signature is judged on the net state of the document against the
7
+ * revision it sealed; an object written again with the sealed value is not
8
+ * a modification; an approval signature permits commenting; field
9
+ * properties are judged per level; a lock binds only the signature that
10
+ * declares it; a changed shared resource must be allowed by every use.
11
+ */
12
+ const SIGNATURE_POLICY_VERSION = 4;
13
+ /**
14
+ * How a validator reads an approval signature. ISO 32000 defines permitted
15
+ * changes only for certification signatures; Acrobat reads an approval
16
+ * signature as "Form Fill-in, Signing and Commenting are allowed" (its own
17
+ * permission text under every such signature) and keeps it valid through a
18
+ * Square annotation added, deleted or recoloured afterwards (corpus
19
+ * `signature-compat` v3 cases 88, 90, 91: "Annotations Created/Deleted/
20
+ * Modified", signature valid). pyHanko judges stricter (fill only); Acrobat
21
+ * is the validator recipients use, so the baseline is `annotate`.
22
+ */
23
+ const APPROVAL_BASELINE = "annotate";
24
+ const LEVEL_RANK = {
25
+ none: 0,
26
+ lta: 1,
27
+ fill: 2,
28
+ annotate: 3
29
+ };
30
+ /** DocMDP `/P` → the modification level it permits. */
31
+ function levelFromPermission(permission) {
32
+ return permission === 1 ? "lta" : permission === 2 ? "fill" : "annotate";
33
+ }
34
+ function minLevel(a, b) {
35
+ return LEVEL_RANK[a] <= LEVEL_RANK[b] ? a : b;
36
+ }
37
+ /** Whether `level` (null = unsigned, everything allowed) permits changes of kind `needed`. */
38
+ function levelAllows(level, needed) {
39
+ return level === null || LEVEL_RANK[level] >= LEVEL_RANK[needed];
40
+ }
41
+ /** Hierarchical name match: `group` covers `group.total`. */
42
+ function lockNameCovers(lockedName, fieldName) {
43
+ return fieldName === lockedName || fieldName.startsWith(`${lockedName}.`);
44
+ }
45
+ function lockCovers(spec, fieldName) {
46
+ switch (spec.action) {
47
+ case "all": return true;
48
+ case "include": return spec.fields.some((name) => lockNameCovers(name, fieldName));
49
+ case "exclude": return !spec.fields.some((name) => lockNameCovers(name, fieldName));
50
+ }
51
+ }
52
+ /**
53
+ * Derive what the signatures already in a document mean for what comes
54
+ * after. Pure and versioned (`SIGNATURE_POLICY_VERSION`); the same function
55
+ * runs in the browser worker, on the native server, and in the conformance
56
+ * suite.
57
+ *
58
+ * enforced = min(certification P, every signed field's /Lock /P), or null
59
+ * when nothing was declared. What the engine refuses.
60
+ * judged = enforced, or the approval baseline when signatures exist
61
+ * without a declaration, or null when nothing is signed. What
62
+ * the revision analysis judges later changes against. A
63
+ * declaration governs the judgement too: a P=3 certifier really
64
+ * did allow annotations.
65
+ * fieldLocks = every signed signature's FieldMDP and the /Lock of every
66
+ * signed field. Unsigned fields' /Lock entries describe a
67
+ * FUTURE signature and lock nothing yet.
68
+ */
69
+ function deriveProtection(signatures) {
70
+ let signed = false;
71
+ let enforced = null;
72
+ let certification = null;
73
+ const fieldLocks = [];
74
+ for (const sig of signatures) {
75
+ if (!sig.signed) continue;
76
+ signed = true;
77
+ if (sig.catalogCertification && sig.docMdp !== null && certification === null) {
78
+ certification = {
79
+ signatureIndex: sig.index,
80
+ permission: sig.docMdp
81
+ };
82
+ enforced = minLevel(enforced ?? "annotate", levelFromPermission(sig.docMdp));
83
+ }
84
+ if (sig.fieldMdp) fieldLocks.push({
85
+ signatureIndex: sig.index,
86
+ source: "fieldmdp",
87
+ spec: sig.fieldMdp
88
+ });
89
+ if (sig.lock) {
90
+ fieldLocks.push({
91
+ signatureIndex: sig.index,
92
+ source: "lock",
93
+ spec: sig.lock
94
+ });
95
+ if (sig.lock.permission !== void 0) enforced = minLevel(enforced ?? "annotate", levelFromPermission(sig.lock.permission));
96
+ }
97
+ }
98
+ return {
99
+ enforced,
100
+ judged: signed ? enforced ?? "annotate" : null,
101
+ certification,
102
+ fieldLocks,
103
+ policyVersion: 4
104
+ };
105
+ }
106
+ /** The lock that freezes `fieldName`, if any. */
107
+ function fieldLockFor(protection, fieldName) {
108
+ for (const lock of protection.fieldLocks) if (lockCovers(lock.spec, fieldName)) return lock;
109
+ return null;
110
+ }
111
+ /**
112
+ * The capabilities a protection removes from every caller, admin scope
113
+ * included — document-derived authority, exactly like encryption bits.
114
+ *
115
+ * Only what was DECLARED is refused: a refusal is a promise a signer made,
116
+ * never a guess about a validator. A certification (or a lock with /P)
117
+ * declares what may follow, and everything outside that — page edits,
118
+ * redaction, field authoring, attachments, and below P=3 annotations, below
119
+ * P=2 form fill — is refused. A plain approval signature declares nothing:
120
+ * the same edits stay possible and are then JUDGED, the verdict saying the
121
+ * signature no longer holds, exactly as Acrobat does. The one exception is a
122
+ * rewrite: it does not invalidate signatures, it erases them, so any signed
123
+ * document refuses it. Per-field locks are enforced by the form mutator.
124
+ */
125
+ function protectedCapabilities(protection) {
126
+ const out = /* @__PURE__ */ new Set();
127
+ if (!protection || protection.judged === null) return out;
128
+ out.add("doc.download.flattened");
129
+ if (protection.enforced === null) return out;
130
+ out.add("doc.pages.modify");
131
+ out.add("doc.pages.assemble");
132
+ out.add("doc.redact");
133
+ out.add("doc.attachments.modify");
134
+ out.add("doc.forms.modify");
135
+ if (!levelAllows(protection.enforced, "annotate")) out.add("doc.annotate.modify");
136
+ if (!levelAllows(protection.enforced, "fill")) out.add("doc.forms.fill");
137
+ return out;
138
+ }
139
+ //#endregion
1
140
  //#region src/auth/scope/errors.ts
2
141
  /**
3
142
  * Thrown by {@link parseScope} / {@link validateScopeArray} when a scope
@@ -76,7 +215,9 @@ const KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
76
215
  "doc.annotate.modify",
77
216
  "doc.metadata.modify",
78
217
  "doc.attachments.modify",
79
- "doc.redact"
218
+ "doc.redact",
219
+ "doc.sign",
220
+ "doc.sign.certify"
80
221
  ]);
81
222
  /**
82
223
  * Parse a single scope string into a structured `ParsedScope`.
@@ -166,18 +307,19 @@ function parseCapability(raw) {
166
307
  * Wildcard `*` short-circuits to true. Anything not explicitly granted
167
308
  * (or expanded from `pdf.permissions`) is denied.
168
309
  */
169
- function checkCapability(capability, rawScope, pdfBits) {
310
+ function checkCapability(capability, rawScope, pdfBits, protection = null) {
311
+ if (protectedCapabilities(protection).has(capability)) return false;
170
312
  const parsed = rawScope.map(parseScope);
171
313
  if (parsed.some((s) => s.kind === "wildcard")) return true;
172
- return expandedCapabilities(parsed, pdfBits).has(capability);
314
+ return expandedCapabilities(parsed, pdfBits, protection).has(capability);
173
315
  }
174
316
  /**
175
317
  * True iff the scope grants AT LEAST ONE of `capabilities`. Convenience
176
318
  * for routes like `/text` (`doc.text.copy` OR `doc.text.search`) and
177
319
  * `/geometry` (`doc.text.select` OR `doc.text.search`).
178
320
  */
179
- function checkAnyCapability(capabilities, rawScope, pdfBits) {
180
- return capabilities.some((c) => checkCapability(c, rawScope, pdfBits));
321
+ function checkAnyCapability(capabilities, rawScope, pdfBits, protection = null) {
322
+ return capabilities.some((c) => checkCapability(c, rawScope, pdfBits, protection));
181
323
  }
182
324
  /**
183
325
  * True iff the scope grants the collab action against the target record.
@@ -225,7 +367,7 @@ function checkCollab(action, target, rawScope, identity, pdfBits) {
225
367
  * before calling this. Returning the expanded set is useful for the
226
368
  * `/access` response's `effectiveScope` and for advisory UI surfacing.
227
369
  */
228
- function expandedCapabilities(parsed, pdfBits) {
370
+ function expandedCapabilities(parsed, pdfBits, protection = null) {
229
371
  const out = /* @__PURE__ */ new Set();
230
372
  let hasAnnotationCollab = false;
231
373
  for (const s of parsed) if (s.kind === "capability") out.add(s.name);
@@ -238,6 +380,7 @@ function expandedCapabilities(parsed, pdfBits) {
238
380
  }
239
381
  if (out.has("doc.forms.fill")) out.add("doc.forms.read");
240
382
  if (hasAnnotationCollab) out.add("doc.annotate.read");
383
+ for (const removed of protectedCapabilities(protection)) out.delete(removed);
241
384
  return out;
242
385
  }
243
386
  /**
@@ -245,8 +388,8 @@ function expandedCapabilities(parsed, pdfBits) {
245
388
  * returns the expanded capability set. Exposed for the `/access`
246
389
  * endpoint and for the SDK helper that surfaces `effectiveScope`.
247
390
  */
248
- function expandRawScope(rawScope, pdfBits) {
249
- return expandedCapabilities(rawScope.map(parseScope), pdfBits);
391
+ function expandRawScope(rawScope, pdfBits, protection = null) {
392
+ return expandedCapabilities(rawScope.map(parseScope), pdfBits, protection);
250
393
  }
251
394
  /**
252
395
  * Test a single collab filter against a target record + the caller's
@@ -355,10 +498,13 @@ function addPdfPermissions(out, b) {
355
498
  }
356
499
  if (b.bit11) out.add("doc.pages.assemble");
357
500
  if (b.bit6) out.add("doc.annotate.modify");
358
- if (b.bit6 || b.bit9) out.add("doc.forms.fill");
501
+ if (b.bit6 || b.bit9) {
502
+ out.add("doc.forms.fill");
503
+ out.add("doc.sign");
504
+ }
359
505
  if (b.bit6 && b.bit4) out.add("doc.forms.modify");
360
506
  }
361
507
  //#endregion
362
- export { expandRawScope as a, parseScope as c, MissingIdentity as d, PermissionDenied as f, checkSetGroup as i, validateScopeArray as l, checkCapability as n, expandedCapabilities as o, checkCollab as r, filterMatches as s, checkAnyCapability as t, InvalidScope as u };
508
+ export { protectedCapabilities as S, levelAllows as _, expandRawScope as a, lockNameCovers as b, parseScope as c, MissingIdentity as d, PermissionDenied as f, fieldLockFor as g, deriveProtection as h, checkSetGroup as i, validateScopeArray as l, SIGNATURE_POLICY_VERSION as m, checkCapability as n, expandedCapabilities as o, APPROVAL_BASELINE as p, checkCollab as r, filterMatches as s, checkAnyCapability as t, InvalidScope as u, levelFromPermission as v, minLevel as x, lockCovers as y };
363
509
 
364
- //# sourceMappingURL=resolver-CHG4waXL.js.map
510
+ //# sourceMappingURL=resolver-inAVzsWt.js.map