@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.
- package/dist/{AbortError-CRSooKsk.cjs → AbortError-Cq2igpZ6.cjs} +2 -2
- package/dist/{AbortError-CRSooKsk.cjs.map → AbortError-Cq2igpZ6.cjs.map} +1 -1
- package/dist/{AbortError-8-9JAX1Y.js → AbortError-DHqPBnYc.js} +2 -2
- package/dist/{AbortError-8-9JAX1Y.js.map → AbortError-DHqPBnYc.js.map} +1 -1
- package/dist/{AnnotationListSnapshot-C1U3XUxP.d.cts → AnnotationListSnapshot-D3mFut1e.d.cts} +1323 -214
- package/dist/{AnnotationListSnapshot-C1U3XUxP.d.ts → AnnotationListSnapshot-D3mFut1e.d.ts} +1323 -214
- package/dist/{DocumentSecurityService-6rhztZlz.d.cts → DocumentSecurityService-ClTZc0zO.d.cts} +2 -2
- package/dist/{DocumentSecurityService-7nL9aY75.d.ts → DocumentSecurityService-_JRjqvoI.d.ts} +2 -2
- package/dist/{Engine-D3868xIU.d.cts → Engine-Bx0NOfmd.d.cts} +219 -11
- package/dist/{Engine-CWnAKccD.d.ts → Engine-DxtpCYcP.d.ts} +219 -11
- package/dist/{PageInsertBlankInput-CzShuSJI.cjs → PageInsertBlankInput-CqXBmFhN.cjs} +37 -2
- package/dist/PageInsertBlankInput-CqXBmFhN.cjs.map +1 -0
- package/dist/{PageInsertBlankInput-WKY0tr6S.js → PageInsertBlankInput-cHq9-i-z.js} +37 -2
- package/dist/PageInsertBlankInput-cHq9-i-z.js.map +1 -0
- package/dist/{builders-Do6_4KnS.cjs → builders-Bqm8yTpi.cjs} +1483 -11
- package/dist/builders-Bqm8yTpi.cjs.map +1 -0
- package/dist/{builders-Cdz34nbF.js → builders-Dqo2X-dt.js} +1364 -12
- package/dist/builders-Dqo2X-dt.js.map +1 -0
- package/dist/conformance.cjs +1630 -107
- package/dist/conformance.cjs.map +1 -1
- package/dist/conformance.d.cts +117 -3
- package/dist/conformance.d.ts +117 -3
- package/dist/conformance.js +1627 -108
- package/dist/conformance.js.map +1 -1
- package/dist/{fold-C7I5GpBz.cjs → fold-2V29cxCV.cjs} +2 -2
- package/dist/{fold-C7I5GpBz.cjs.map → fold-2V29cxCV.cjs.map} +1 -1
- package/dist/{fold-BfyxxwS6.js → fold-CdEJTBTn.js} +2 -2
- package/dist/{fold-BfyxxwS6.js.map → fold-CdEJTBTn.js.map} +1 -1
- package/dist/index.cjs +33 -4
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -5
- package/dist/{resolver-D-jumvgM.cjs → resolver-CYVNV4hn.cjs} +216 -10
- package/dist/resolver-CYVNV4hn.cjs.map +1 -0
- package/dist/{resolver-CHG4waXL.js → resolver-inAVzsWt.js} +157 -11
- package/dist/resolver-inAVzsWt.js.map +1 -0
- package/dist/runtime.cjs +34 -5
- package/dist/runtime.d.cts +253 -5
- package/dist/runtime.d.ts +253 -5
- package/dist/runtime.js +6 -6
- package/dist/{schemas-XGrNVMDd.js → schemas-BLARNCgx.js} +469 -7
- package/dist/schemas-BLARNCgx.js.map +1 -0
- package/dist/{schemas-CS5iPpX5.cjs → schemas-DIqFMmrC.cjs} +672 -6
- package/dist/schemas-DIqFMmrC.cjs.map +1 -0
- package/dist/wire.cjs +250 -12
- package/dist/wire.cjs.map +1 -1
- package/dist/wire.d.cts +444 -4
- package/dist/wire.d.ts +444 -4
- package/dist/wire.js +214 -13
- package/dist/wire.js.map +1 -1
- package/package.json +1 -1
- package/dist/PageInsertBlankInput-CzShuSJI.cjs.map +0 -1
- package/dist/PageInsertBlankInput-WKY0tr6S.js.map +0 -1
- package/dist/builders-Cdz34nbF.js.map +0 -1
- package/dist/builders-Do6_4KnS.cjs.map +0 -1
- package/dist/resolver-CHG4waXL.js.map +0 -1
- package/dist/resolver-D-jumvgM.cjs.map +0 -1
- package/dist/schemas-CS5iPpX5.cjs.map +0 -1
- package/dist/schemas-XGrNVMDd.js.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)
|
|
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-
|
|
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)
|
|
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-
|
|
510
|
+
//# sourceMappingURL=resolver-inAVzsWt.js.map
|