@embedpdf/engine-core 3.0.0-next.14 → 3.0.0-next.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/dist/AnnotationList-DcyhF2kU.d.cts +27691 -0
  2. package/dist/AnnotationList-DcyhF2kU.d.ts +27691 -0
  3. package/dist/Change-Ct-dB8D5.cjs +1133 -0
  4. package/dist/Change-Ct-dB8D5.cjs.map +1 -0
  5. package/dist/Change-tMR34hTU.js +978 -0
  6. package/dist/Change-tMR34hTU.js.map +1 -0
  7. package/dist/DocumentRenderService-9IjSQEIU.cjs +121 -0
  8. package/dist/DocumentRenderService-9IjSQEIU.cjs.map +1 -0
  9. package/dist/DocumentRenderService-DFrZ9wfW.js +86 -0
  10. package/dist/DocumentRenderService-DFrZ9wfW.js.map +1 -0
  11. package/dist/{DocumentSecurityService-Ca6EK3-3.d.ts → DocumentSecurityService-D2zA4NKi.d.cts} +100 -112
  12. package/dist/{DocumentSecurityService-CIx1MT0T.d.cts → DocumentSecurityService-YJV9df8P.d.ts} +100 -112
  13. package/dist/LocalDocumentHandle-D02FMm-Z.cjs +53 -0
  14. package/dist/LocalDocumentHandle-D02FMm-Z.cjs.map +1 -0
  15. package/dist/LocalDocumentHandle-DAPdaOt_.js +30 -0
  16. package/dist/LocalDocumentHandle-DAPdaOt_.js.map +1 -0
  17. package/dist/LocalEngine-DBt2-GWF.d.ts +1649 -0
  18. package/dist/LocalEngine-IA9zh3Jh.d.cts +1649 -0
  19. package/dist/PageInsertBlankInput-CXHAM7l6.cjs +17 -0
  20. package/dist/PageInsertBlankInput-CXHAM7l6.cjs.map +1 -0
  21. package/dist/PageInsertBlankInput-DhVYVJ_S.js +12 -0
  22. package/dist/PageInsertBlankInput-DhVYVJ_S.js.map +1 -0
  23. package/dist/authority-BFp6XM04.cjs +2997 -0
  24. package/dist/authority-BFp6XM04.cjs.map +1 -0
  25. package/dist/authority-PuxpGFhJ.js +2506 -0
  26. package/dist/authority-PuxpGFhJ.js.map +1 -0
  27. package/dist/builders-DV62HA3I.js +165 -0
  28. package/dist/builders-DV62HA3I.js.map +1 -0
  29. package/dist/builders-DgTAj_tP.cjs +188 -0
  30. package/dist/builders-DgTAj_tP.cjs.map +1 -0
  31. package/dist/calibrate-DJbi1wuK.cjs +3012 -0
  32. package/dist/calibrate-DJbi1wuK.cjs.map +1 -0
  33. package/dist/calibrate-nQFgCuki.js +2461 -0
  34. package/dist/calibrate-nQFgCuki.js.map +1 -0
  35. package/dist/charmap-Cprti1ta.js +239 -0
  36. package/dist/charmap-Cprti1ta.js.map +1 -0
  37. package/dist/charmap-aYlefQP9.cjs +304 -0
  38. package/dist/charmap-aYlefQP9.cjs.map +1 -0
  39. package/dist/conformance.cjs +18146 -5258
  40. package/dist/conformance.cjs.map +1 -1
  41. package/dist/conformance.d.cts +1024 -170
  42. package/dist/conformance.d.ts +1024 -170
  43. package/dist/conformance.js +18148 -5309
  44. package/dist/conformance.js.map +1 -1
  45. package/dist/format-Bp3QFSH1.cjs +574 -0
  46. package/dist/format-Bp3QFSH1.cjs.map +1 -0
  47. package/dist/format-Dgg428Kb.js +461 -0
  48. package/dist/format-Dgg428Kb.js.map +1 -0
  49. package/dist/index.cjs +342 -116
  50. package/dist/index.d.cts +2 -2
  51. package/dist/index.d.ts +2 -2
  52. package/dist/index.js +14 -5
  53. package/dist/pageRefs-B3ur7Ckm.cjs +51 -0
  54. package/dist/pageRefs-B3ur7Ckm.cjs.map +1 -0
  55. package/dist/pageRefs-DkbuxSCV.js +40 -0
  56. package/dist/pageRefs-DkbuxSCV.js.map +1 -0
  57. package/dist/public.cjs +59 -0
  58. package/dist/public.d.cts +5 -0
  59. package/dist/public.d.ts +5 -0
  60. package/dist/public.js +9 -0
  61. package/dist/resolveAnnotationDraft-7Ke4nV7F.cjs +3646 -0
  62. package/dist/resolveAnnotationDraft-7Ke4nV7F.cjs.map +1 -0
  63. package/dist/resolveAnnotationDraft-CUpf0SMh.js +3167 -0
  64. package/dist/resolveAnnotationDraft-CUpf0SMh.js.map +1 -0
  65. package/dist/{resolver-inAVzsWt.js → resolver-BBggAxQc.js} +298 -145
  66. package/dist/resolver-BBggAxQc.js.map +1 -0
  67. package/dist/{resolver-CYVNV4hn.cjs → resolver-BWSWSF6R.cjs} +351 -144
  68. package/dist/resolver-BWSWSF6R.cjs.map +1 -0
  69. package/dist/runtime-BaDKjxiu.d.ts +1959 -0
  70. package/dist/runtime-CWEuiwbk.d.cts +1959 -0
  71. package/dist/runtime.cjs +986 -297
  72. package/dist/runtime.cjs.map +1 -1
  73. package/dist/runtime.d.cts +5 -1372
  74. package/dist/runtime.d.ts +5 -1372
  75. package/dist/runtime.js +634 -179
  76. package/dist/runtime.js.map +1 -1
  77. package/dist/schema-6S2VmlAj.js +2087 -0
  78. package/dist/schema-6S2VmlAj.js.map +1 -0
  79. package/dist/schema-miH6tWQC.cjs +3046 -0
  80. package/dist/schema-miH6tWQC.cjs.map +1 -0
  81. package/dist/schemas-BE2QGOit.js +1851 -0
  82. package/dist/schemas-BE2QGOit.js.map +1 -0
  83. package/dist/schemas-DljJR9ut.cjs +2798 -0
  84. package/dist/schemas-DljJR9ut.cjs.map +1 -0
  85. package/dist/subtype-CG4WpIRj.js +129 -0
  86. package/dist/subtype-CG4WpIRj.js.map +1 -0
  87. package/dist/subtype-aDt5TFmK.cjs +158 -0
  88. package/dist/subtype-aDt5TFmK.cjs.map +1 -0
  89. package/dist/validation.cjs +578 -0
  90. package/dist/validation.cjs.map +1 -0
  91. package/dist/validation.d.cts +174 -0
  92. package/dist/validation.d.ts +174 -0
  93. package/dist/validation.js +571 -0
  94. package/dist/validation.js.map +1 -0
  95. package/dist/value-BFS1R8ek.cjs +435 -0
  96. package/dist/value-BFS1R8ek.cjs.map +1 -0
  97. package/dist/value-Dhc8eYcx.js +340 -0
  98. package/dist/value-Dhc8eYcx.js.map +1 -0
  99. package/dist/viewport-CHG9qQUb.cjs +671 -0
  100. package/dist/viewport-CHG9qQUb.cjs.map +1 -0
  101. package/dist/viewport-DQR5LyX3.js +540 -0
  102. package/dist/viewport-DQR5LyX3.js.map +1 -0
  103. package/dist/wire.cjs +1242 -420
  104. package/dist/wire.cjs.map +1 -1
  105. package/dist/wire.d.cts +14840 -830
  106. package/dist/wire.d.ts +14840 -830
  107. package/dist/wire.js +985 -261
  108. package/dist/wire.js.map +1 -1
  109. package/package.json +10 -1
  110. package/dist/AbortError-DwuHPK_I.js +0 -27
  111. package/dist/AbortError-DwuHPK_I.js.map +0 -1
  112. package/dist/AbortError-P0RU0Mgg.cjs +0 -38
  113. package/dist/AbortError-P0RU0Mgg.cjs.map +0 -1
  114. package/dist/AnnotationListSnapshot-D9R5n2Kf.d.cts +0 -7101
  115. package/dist/AnnotationListSnapshot-D9R5n2Kf.d.ts +0 -7101
  116. package/dist/Engine-EWWHhEIi.d.ts +0 -1277
  117. package/dist/Engine-FNaAEjfb.d.cts +0 -1277
  118. package/dist/derive-BbsDk_Kz.cjs +0 -880
  119. package/dist/derive-BbsDk_Kz.cjs.map +0 -1
  120. package/dist/derive-DAFJolWu.js +0 -647
  121. package/dist/derive-DAFJolWu.js.map +0 -1
  122. package/dist/format-Bbphf4zR.js +0 -435
  123. package/dist/format-Bbphf4zR.js.map +0 -1
  124. package/dist/format-D_epWlob.cjs +0 -518
  125. package/dist/format-D_epWlob.cjs.map +0 -1
  126. package/dist/resolver-CYVNV4hn.cjs.map +0 -1
  127. package/dist/resolver-inAVzsWt.js.map +0 -1
  128. package/dist/schemas-CFQGvWiu.js +0 -3243
  129. package/dist/schemas-CFQGvWiu.js.map +0 -1
  130. package/dist/schemas-D0g6tKa9.cjs +0 -4694
  131. package/dist/schemas-D0g6tKa9.cjs.map +0 -1
  132. package/dist/viewport-BWeNHZmb.cjs +0 -3503
  133. package/dist/viewport-BWeNHZmb.cjs.map +0 -1
  134. package/dist/viewport-CvMcadfQ.js +0 -3072
  135. package/dist/viewport-CvMcadfQ.js.map +0 -1
@@ -1,8 +1,115 @@
1
+ import { h as EngineError, v as _defineProperty, y as EngineErrorCode } from "./format-Dgg428Kb.js";
2
+ import { r as materializePdfPermissions } from "./builders-DV62HA3I.js";
3
+ //#region src/auth/scope/errors.ts
4
+ /**
5
+ * Thrown by {@link parseScope} / {@link validateScopeArray} when a scope
6
+ * string does not match any known grammar (capability, collab, virtual,
7
+ * or wildcard). An `EngineError` with code `InvalidArg`.
8
+ *
9
+ * Carries the offending scope verbatim so route/JWT-layer error
10
+ * handlers can echo it to the customer.
11
+ */
12
+ var InvalidScope = class extends EngineError {
13
+ constructor(scope, reason) {
14
+ super(EngineErrorCode.InvalidArg, `invalid scope "${scope}": ${reason}`, { details: {
15
+ scope,
16
+ reason
17
+ } });
18
+ this.scope = scope;
19
+ _defineProperty(this, "name", "InvalidScope");
20
+ }
21
+ };
22
+ /**
23
+ * Thrown by route guards and engine-local enforcement when a capability
24
+ * or collab action is denied for the current request/handle. An
25
+ * `EngineError` with code `Forbidden`, on both engines.
26
+ *
27
+ * `required` (also `details.required`) names what the caller needed (e.g.
28
+ * "doc.render" or "annotations:update"). When any of several would have
29
+ * done, `anyOf` (also `details.anyOf`) lists them and `required` is the
30
+ * first. `context` is an optional label such as "engine-local" or a route.
31
+ */
32
+ var PermissionDenied = class extends EngineError {
33
+ constructor(required, context, anyOf, refs) {
34
+ super(EngineErrorCode.Forbidden, `permission denied${context ? ` (${context})` : ""}: ${anyOf ? `one of ${anyOf.join(", ")}` : required}${refs ? ` for ${refs.length} annotation${refs.length === 1 ? "" : "s"}` : ""}`, { details: {
35
+ required,
36
+ ...context === void 0 ? {} : { context },
37
+ ...anyOf === void 0 ? {} : { anyOf: [...anyOf] },
38
+ ...refs === void 0 ? {} : { refs: [...refs] }
39
+ } });
40
+ this.required = required;
41
+ this.context = context;
42
+ this.anyOf = anyOf;
43
+ this.refs = refs;
44
+ _defineProperty(this, "name", "PermissionDenied");
45
+ }
46
+ };
47
+ /**
48
+ * Thrown at engine-local open time when the supplied scope includes
49
+ * collab filters (`:self`, `:group=...`) but the identity claims needed
50
+ * to resolve them are missing. Fails loudly at open so the configuration
51
+ * mistake is visible immediately instead of producing silent denies at
52
+ * every annotation mutation. An `EngineError` with code `InvalidArg`.
53
+ */
54
+ var MissingIdentity = class extends EngineError {
55
+ constructor(scope) {
56
+ super(EngineErrorCode.InvalidArg, `scope "${scope}" requires an identity (userId and/or groups)`, { details: { scope } });
57
+ this.scope = scope;
58
+ _defineProperty(this, "name", "MissingIdentity");
59
+ }
60
+ };
61
+ //#endregion
62
+ //#region src/identity/AnnotationRef.ts
63
+ /**
64
+ * URL-safe encoding of a ref without its page, used by the cloud HTTP
65
+ * surface as the `:annotKey` route parameter beside `:pageKey`
66
+ * (`encodePageKey`). Decoded by `decodeAnnotKey`.
67
+ *
68
+ * `{ kind: 'objectNumber', objectNumber: 42 }` -> `'obj:42'`
69
+ * `{ kind: 'baseIndex', baseIndex: 2 }` -> `'base:2'`
70
+ */
71
+ function encodeAnnotKey(ref) {
72
+ if (ref.kind === "objectNumber") {
73
+ if (!Number.isInteger(ref.objectNumber) || ref.objectNumber <= 0) throw new RangeError(`encodeAnnotKey: objectNumber must be a positive integer, got ${ref.objectNumber}`);
74
+ return `obj:${ref.objectNumber}`;
75
+ }
76
+ if (!Number.isInteger(ref.baseIndex) || ref.baseIndex < 0) throw new RangeError(`encodeAnnotKey: baseIndex must be a non-negative integer, got ${ref.baseIndex}`);
77
+ return `base:${ref.baseIndex}`;
78
+ }
79
+ /**
80
+ * Inverse of `encodeAnnotKey`, on the page the route names. Returns `null`
81
+ * for malformed input so the server can answer 400 InvalidArg with a useful
82
+ * message instead of throwing. The input is the already-`decodeURIComponent`-ed
83
+ * segment from the route path.
84
+ */
85
+ function decodeAnnotKey(page, key) {
86
+ const number = (prefix, min) => {
87
+ if (!key.startsWith(prefix)) return null;
88
+ const rest = key.slice(prefix.length);
89
+ const n = Number.parseInt(rest, 10);
90
+ return Number.isInteger(n) && n >= min && String(n) === rest ? n : null;
91
+ };
92
+ const objectNumber = number("obj:", 1);
93
+ if (objectNumber !== null) return {
94
+ kind: "objectNumber",
95
+ page,
96
+ objectNumber
97
+ };
98
+ const baseIndex = number("base:", 0);
99
+ if (baseIndex !== null) return {
100
+ kind: "baseIndex",
101
+ page,
102
+ baseIndex
103
+ };
104
+ return null;
105
+ }
106
+ //#endregion
1
107
  //#region src/signature/protection.ts
2
108
  /**
3
109
  * Bumped whenever the derivation below (or a judgement rule) changes meaning.
4
110
  * Rides every protection and every analysis, and keys the cloud's immutable
5
- * analysis URLs. v4 (the `signature-compat` corpus, 91 Acrobat observations):
111
+ * analysis URLs. The current policy is calibrated on the 91 Acrobat
112
+ * observations in `packages/engine/main/test/fixtures/signature-compat`:
6
113
  * each signature is judged on the net state of the document against the
7
114
  * revision it sealed; an object written again with the sealed value is not
8
115
  * a modification; an approval signature permits commenting; field
@@ -15,8 +122,8 @@ const SIGNATURE_POLICY_VERSION = 4;
15
122
  * changes only for certification signatures; Acrobat reads an approval
16
123
  * signature as "Form Fill-in, Signing and Commenting are allowed" (its own
17
124
  * 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/
125
+ * Square annotation added, deleted or recoloured afterwards (fixtures
126
+ * `signature-compat/v3/88`, `/90` and `/91`: "Annotations Created/Deleted/
20
127
  * Modified", signature valid). pyHanko judges stricter (fill only); Acrobat
21
128
  * is the validator recipients use, so the baseline is `annotate`.
22
129
  */
@@ -64,7 +171,7 @@ function lockCovers(spec, fieldName) {
64
171
  * did allow annotations.
65
172
  * fieldLocks = every signed signature's FieldMDP and the /Lock of every
66
173
  * signed field. Unsigned fields' /Lock entries describe a
67
- * FUTURE signature and lock nothing yet.
174
+ * future signature and lock nothing yet.
68
175
  */
69
176
  function deriveProtection(signatures) {
70
177
  let signed = false;
@@ -109,15 +216,38 @@ function fieldLockFor(protection, fieldName) {
109
216
  return null;
110
217
  }
111
218
  /**
219
+ * Every capability a protection can remove: the most it ever takes away.
220
+ * A check of any other capability never needs the document's protection.
221
+ */
222
+ const PROTECTABLE_CAPABILITIES = [
223
+ "doc.download.flattened",
224
+ "doc.pages.modify",
225
+ "doc.pages.assemble",
226
+ "doc.redact",
227
+ "doc.attachments.modify",
228
+ "doc.outline.modify",
229
+ "doc.forms.modify",
230
+ "doc.forms.script",
231
+ "doc.annotate.modify",
232
+ "doc.annotate.import",
233
+ "doc.forms.fill",
234
+ "doc.forms.import"
235
+ ];
236
+ const PROTECTABLE = new Set(PROTECTABLE_CAPABILITIES);
237
+ /** Whether a protection can ever remove `capability`. */
238
+ function isProtectableCapability(capability) {
239
+ return PROTECTABLE.has(capability);
240
+ }
241
+ /**
112
242
  * The capabilities a protection removes from every caller, admin scope
113
243
  * included — document-derived authority, exactly like encryption bits.
114
244
  *
115
- * Only what was DECLARED is refused: a refusal is a promise a signer made,
245
+ * Only what was declared is refused: a refusal is a promise a signer made,
116
246
  * never a guess about a validator. A certification (or a lock with /P)
117
247
  * declares what may follow, and everything outside that — page edits,
118
- * redaction, field authoring, attachments, and below P=3 annotations, below
248
+ * redaction, field authoring, attachments, the outline, and below P=3 annotations, below
119
249
  * 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
250
+ * the same edits stay possible and are then judged, the verdict saying the
121
251
  * signature no longer holds, exactly as Acrobat does. The one exception is a
122
252
  * rewrite: it does not invalidate signatures, it erases them, so any signed
123
253
  * document refuses it. Per-field locks are enforced by the form mutator.
@@ -131,63 +261,29 @@ function protectedCapabilities(protection) {
131
261
  out.add("doc.pages.assemble");
132
262
  out.add("doc.redact");
133
263
  out.add("doc.attachments.modify");
264
+ out.add("doc.outline.modify");
134
265
  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
140
- //#region src/auth/scope/errors.ts
141
- /**
142
- * Thrown by {@link parseScope} / {@link validateScopeArray} when a scope
143
- * string does not match any known grammar (capability, collab, virtual,
144
- * or wildcard).
145
- *
146
- * Carries the offending scope verbatim so route/JWT-layer error
147
- * handlers can echo it to the customer.
148
- */
149
- var InvalidScope = class extends Error {
150
- constructor(scope, reason) {
151
- super(`invalid scope "${scope}": ${reason}`);
152
- this.scope = scope;
153
- this.name = "InvalidScope";
266
+ out.add("doc.forms.script");
267
+ if (!levelAllows(protection.enforced, "annotate")) {
268
+ out.add("doc.annotate.modify");
269
+ out.add("doc.annotate.import");
154
270
  }
155
- };
156
- /**
157
- * Thrown by route guards and engine-local enforcement when a capability
158
- * or collab action is denied for the current request/handle.
159
- *
160
- * `required` names what the caller needed (e.g., "doc.render" or
161
- * "annotations:update"). `context` is an optional label such as "local"
162
- * or "/v1/docs/.../pages/2/render".
163
- */
164
- var PermissionDenied = class extends Error {
165
- constructor(required, context) {
166
- super(`permission denied${context ? ` (${context})` : ""}: ${required}`);
167
- this.required = required;
168
- this.context = context;
169
- this.name = "PermissionDenied";
170
- }
171
- };
172
- /**
173
- * Thrown at engine-local open time when the supplied scope includes
174
- * collab filters (`:self`, `:group=...`) but the identity claims needed
175
- * to resolve them are missing. Fails loudly at open so the configuration
176
- * mistake is visible immediately instead of producing silent denies at
177
- * every annotation mutation.
178
- */
179
- var MissingIdentity = class extends Error {
180
- constructor(scope) {
181
- super(`scope "${scope}" requires identity claims (user_id and/or groups)`);
182
- this.name = "MissingIdentity";
271
+ if (!levelAllows(protection.enforced, "fill")) {
272
+ out.add("doc.forms.fill");
273
+ out.add("doc.forms.import");
183
274
  }
184
- };
275
+ return out;
276
+ }
277
+ /** Why `protection` refuses `capability`: the message of a `ProtectedDocument` refusal. */
278
+ function describeProtection(capability, protection) {
279
+ return `the document is signed: ${protection.certification ? `certification signature ${protection.certification.signatureIndex} (permission ${protection.certification.permission})` : `an existing signature (declared level '${protection.enforced ?? "none declared"}')`} forbids '${capability}'`;
280
+ }
185
281
  //#endregion
186
282
  //#region src/auth/scope/parser.ts
187
283
  /**
188
284
  * Closed set of recognized capability strings. Membership is the
189
285
  * authoritative source of truth — adding a new capability requires
190
- * extending the `DocCapability` union AND adding it here. The parser
286
+ * extending the `DocCapability` union and adding it here. The parser
191
287
  * rejects anything outside this set.
192
288
  *
193
289
  * Removed legacy names (e.g., `doc.read`, `doc.edit-pages`, `doc.save`)
@@ -210,11 +306,15 @@ const KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
210
306
  "doc.forms.read",
211
307
  "doc.forms.fill",
212
308
  "doc.forms.modify",
309
+ "doc.forms.import",
310
+ "doc.forms.script",
213
311
  "doc.forms.submit",
214
312
  "doc.annotate.read",
215
313
  "doc.annotate.modify",
314
+ "doc.annotate.import",
216
315
  "doc.metadata.modify",
217
316
  "doc.attachments.modify",
317
+ "doc.outline.modify",
218
318
  "doc.redact",
219
319
  "doc.sign",
220
320
  "doc.sign.certify"
@@ -257,6 +357,7 @@ function parseCollab(raw) {
257
357
  const entity = raw.slice(0, idx1);
258
358
  const action = raw.slice(idx1 + 1, idx2);
259
359
  const filterStr = raw.slice(idx2 + 1);
360
+ if (entity === "fields") return parseFieldScope(raw, action, filterStr);
260
361
  if (entity !== "annotations") throw new InvalidScope(raw, `unknown collab entity: ${entity}`);
261
362
  if (action !== "create" && action !== "update" && action !== "delete" && action !== "set-group" && action !== "*") throw new InvalidScope(raw, `unknown collab action: ${action}`);
262
363
  const filter = parseFilter(filterStr, raw);
@@ -268,6 +369,22 @@ function parseCollab(raw) {
268
369
  filter
269
370
  };
270
371
  }
372
+ /**
373
+ * `fields:<fill|sign|set-group|*>:<all|group=NAME>`. A field's group is who
374
+ * fills it, and a field records no creator, so `self` and `createdBy=` are
375
+ * refused.
376
+ */
377
+ function parseFieldScope(raw, action, filterStr) {
378
+ if (action !== "fill" && action !== "sign" && action !== "set-group" && action !== "*") throw new InvalidScope(raw, `unknown fields action: ${action}`);
379
+ const filter = parseFilter(filterStr, raw);
380
+ if (filter.kind !== "all" && filter.kind !== "group") throw new InvalidScope(raw, `fields only support :all or :group=<id> filters (got :${filter.kind})`);
381
+ return {
382
+ kind: "collab",
383
+ entity: "fields",
384
+ action,
385
+ filter
386
+ };
387
+ }
271
388
  function parseFilter(s, raw) {
272
389
  if (s === "all") return { kind: "all" };
273
390
  if (s === "self") return { kind: "self" };
@@ -300,6 +417,13 @@ function parseCapability(raw) {
300
417
  }
301
418
  //#endregion
302
419
  //#region src/auth/scope/resolver.ts
420
+ /** The collab target of a record read with nullable owner fields (an annotation). */
421
+ function collabTargetOf(owner) {
422
+ return {
423
+ ...owner.userId ? { userId: owner.userId } : {},
424
+ ...owner.groupId ? { groupId: owner.groupId } : {}
425
+ };
426
+ }
303
427
  /**
304
428
  * True iff the given capability is granted by the scope array plus the
305
429
  * PDF bits visible via `pdf.permissions` expansion.
@@ -314,9 +438,9 @@ function checkCapability(capability, rawScope, pdfBits, protection = null) {
314
438
  return expandedCapabilities(parsed, pdfBits, protection).has(capability);
315
439
  }
316
440
  /**
317
- * True iff the scope grants AT LEAST ONE of `capabilities`. Convenience
318
- * for routes like `/text` (`doc.text.copy` OR `doc.text.search`) and
319
- * `/geometry` (`doc.text.select` OR `doc.text.search`).
441
+ * True iff the scope grants at least one of `capabilities`. Convenience
442
+ * for routes like `/text` (`doc.text.copy` or `doc.text.search`) and
443
+ * `/geometry` (`doc.text.select` or `doc.text.search`).
320
444
  */
321
445
  function checkAnyCapability(capabilities, rawScope, pdfBits, protection = null) {
322
446
  return capabilities.some((c) => checkCapability(c, rawScope, pdfBits, protection));
@@ -326,7 +450,7 @@ function checkAnyCapability(capabilities, rawScope, pdfBits, protection = null)
326
450
  *
327
451
  * Narrowing model. For each action independently:
328
452
  * 1. wildcard `*` → allow (global escape hatch)
329
- * 2. if any collab scope applies to this action → NARROW: only those
453
+ * 2. if any collab scope applies to this action → narrow: only those
330
454
  * collab filters decide. If none match the target, deny — even if
331
455
  * `doc.annotate.modify` is also present. This is what makes
332
456
  * `[modify, update:self]` correctly mean "edit own only" rather
@@ -337,7 +461,7 @@ function checkAnyCapability(capabilities, rawScope, pdfBits, protection = null)
337
461
  * 4. otherwise → deny.
338
462
  *
339
463
  * For create: `target` should be built by the caller from JWT identity
340
- * (`{ userId: caller.user_id, groupId: caller.group_id }`). `:self` and
464
+ * (`{ userId: caller.userId, groupId: caller.groupId }`). `:self` and
341
465
  * `:all` then trivially pass; `:group=X` is the meaningful filter (only
342
466
  * matches when the caller's default group is X).
343
467
  *
@@ -362,17 +486,22 @@ function checkCollab(action, target, rawScope, identity, pdfBits) {
362
486
  * - any annotation collab scope implies `doc.annotate.read`, because
363
487
  * mutation routes need to see the target row to evaluate the
364
488
  * collab filter against its current owner.
489
+ * - any field collab scope implies `doc.forms.read`: filling in or
490
+ * signing a field means seeing the form. Groups limit writes, never
491
+ * reads.
365
492
  *
366
- * Does NOT short-circuit on wildcard — callers do that themselves
493
+ * Does not short-circuit on wildcard — callers do that themselves
367
494
  * before calling this. Returning the expanded set is useful for the
368
495
  * `/access` response's `effectiveScope` and for advisory UI surfacing.
369
496
  */
370
497
  function expandedCapabilities(parsed, pdfBits, protection = null) {
371
498
  const out = /* @__PURE__ */ new Set();
372
499
  let hasAnnotationCollab = false;
500
+ let hasFieldCollab = false;
373
501
  for (const s of parsed) if (s.kind === "capability") out.add(s.name);
374
502
  else if (s.kind === "virtual" && s.name === "pdf.permissions") addPdfPermissions(out, pdfBits);
375
503
  else if (s.kind === "collab" && s.entity === "annotations") hasAnnotationCollab = true;
504
+ else if (s.kind === "collab" && s.entity === "fields") hasFieldCollab = true;
376
505
  if (out.has("doc.annotate.modify")) out.add("doc.annotate.read");
377
506
  if (out.has("doc.forms.modify")) {
378
507
  out.add("doc.forms.fill");
@@ -380,6 +509,7 @@ function expandedCapabilities(parsed, pdfBits, protection = null) {
380
509
  }
381
510
  if (out.has("doc.forms.fill")) out.add("doc.forms.read");
382
511
  if (hasAnnotationCollab) out.add("doc.annotate.read");
512
+ if (hasFieldCollab) out.add("doc.forms.read");
383
513
  for (const removed of protectedCapabilities(protection)) out.delete(removed);
384
514
  return out;
385
515
  }
@@ -392,119 +522,142 @@ function expandRawScope(rawScope, pdfBits, protection = null) {
392
522
  return expandedCapabilities(rawScope.map(parseScope), pdfBits, protection);
393
523
  }
394
524
  /**
395
- * Test a single collab filter against a target record + the caller's
396
- * identity. Pure function — no side effects, no implicit rules.
525
+ * Authority to put a record of `entity` in a specific group: an annotation,
526
+ * or a form field.
397
527
  *
398
- * all → always matches
399
- * self → matches if identity.user_id === target.userId
400
- * createdBy=<X> → matches if target.userId === X
401
- * group=<X> → matches if target.groupId === X
402
- * AND identity.groups includes X
403
- *
404
- * The group-membership check on `group=X` prevents a token from
405
- * matching annotations in a group it doesn't belong to, even if the
406
- * target row carries that groupId.
407
- */
408
- /**
409
- * Authority to assign a specific groupId to an annotation.
410
- *
411
- * Set-group is decoupled from `doc.annotate.modify`. The reasoning:
412
- * capabilities like `modify` describe row access — what kind of write
413
- * you can do to which existing rows — and map to PDF permission bits.
414
- * Set-group is a cloud-only *assignment authority*: which destination
415
- * group can you put an annotation into? There is no PDF-bit
416
- * counterpart, so it doesn't inherit from `modify`.
528
+ * Set-group is decoupled from the write capabilities (`doc.annotate.modify`,
529
+ * `doc.forms.modify`). The reasoning: those describe row access — what kind
530
+ * of write you can do to which existing rows — and map to PDF permission
531
+ * bits. Set-group is a cloud-only *assignment authority*: which destination
532
+ * group can you put a record into? There is no PDF-bit counterpart, so it
533
+ * doesn't inherit from them.
417
534
  *
418
535
  * Resolution order:
419
536
  * 1. newGroupId === callerDefaultGroupId → true (no real reassignment
420
- * is happening; the annotation gets the caller's default group)
537
+ * is happening; the record gets the caller's default group)
421
538
  * 2. wildcard `*` → true (global escape hatch)
422
- * 3. `annotations:set-group:all` → true
423
- * 4. `annotations:set-group:group=<newGroupId>` → true
424
- * 5. `annotations:*:all` or `annotations:*:group=<newGroupId>` → true
539
+ * 3. `<entity>:set-group:all` → true
540
+ * 4. `<entity>:set-group:group=<newGroupId>` → true
541
+ * 5. `<entity>:*:all` or `<entity>:*:group=<newGroupId>` → true
425
542
  * (action wildcard includes set-group)
426
543
  * 6. otherwise → false
427
544
  *
428
- * Note: independent from membership. A user with
429
- * `set-group:group=legal` can assign annotations to the legal group
430
- * even if they're not a member — that's the whole point.
545
+ * A user with `set-group:group=legal` can put records in the legal group
546
+ * whatever their own group is — that's the whole point.
431
547
  */
432
- function checkSetGroup(newGroupId, callerDefaultGroupId, rawScope, _pdfBits) {
548
+ function checkSetGroup(entity, newGroupId, callerDefaultGroupId, rawScope) {
433
549
  if (newGroupId === callerDefaultGroupId) return true;
434
550
  const parsed = rawScope.map(parseScope);
435
551
  if (parsed.some((s) => s.kind === "wildcard")) return true;
436
552
  for (const s of parsed) {
437
553
  if (s.kind !== "collab") continue;
438
- if (s.entity !== "annotations") continue;
554
+ if (s.entity !== entity) continue;
439
555
  if (s.action !== "set-group" && s.action !== "*") continue;
440
556
  if (s.filter.kind === "all") return true;
441
557
  if (s.filter.kind === "group" && s.filter.groupId === newGroupId) return true;
442
558
  }
443
559
  return false;
444
560
  }
561
+ /**
562
+ * True iff the scope lets the caller `action` (fill in, or sign) a form
563
+ * field in `groupId` (`null` for a field in no group).
564
+ *
565
+ * Narrowing, per action, as for annotations:
566
+ * 1. wildcard `*` → allow;
567
+ * 2. if any `fields:<action>` scope exists (or `fields:*`) → only those
568
+ * decide: the field must match one of their filters, even when the
569
+ * broad capability is also present. A field in no group then matches
570
+ * only `:all`;
571
+ * 3. otherwise the broad capability decides, for every field:
572
+ * `doc.forms.fill` to fill, `doc.sign` to sign.
573
+ *
574
+ * Nothing here narrows form design (`doc.forms.modify`).
575
+ */
576
+ function checkFieldAction(action, groupId, rawScope, pdfBits, protection = null) {
577
+ const broad = action === "fill" ? "doc.forms.fill" : "doc.sign";
578
+ if (protectedCapabilities(protection).has(broad)) return false;
579
+ const parsed = rawScope.map(parseScope);
580
+ if (parsed.some((s) => s.kind === "wildcard")) return true;
581
+ const applicable = parsed.filter((s) => s.kind === "collab" && s.entity === "fields" && (s.action === action || s.action === "*"));
582
+ if (applicable.length > 0) return applicable.some((s) => s.filter.kind === "all" || groupId !== null && s.filter.groupId === groupId);
583
+ return expandedCapabilities(parsed, pdfBits, protection).has(broad);
584
+ }
585
+ /**
586
+ * {@link checkFieldAction} and {@link checkSetGroup} as lists: for each
587
+ * action, every field or the groups the scope names, so a designer can
588
+ * offer exactly the groups a field may go in, and a filler see whose fields
589
+ * are theirs. The same rules, read the other way round.
590
+ */
591
+ function fieldRightsOf(rawScope, pdfBits, protection, callerDefaultGroupId) {
592
+ const parsed = rawScope.map(parseScope);
593
+ const wildcard = parsed.some((s) => s.kind === "wildcard");
594
+ const fieldScopes = (action) => parsed.filter((s) => s.kind === "collab" && s.entity === "fields" && (s.action === action || s.action === "*"));
595
+ const fromScopes = (scopes, extra = []) => {
596
+ if (scopes.some((s) => s.filter.kind === "all")) return { all: true };
597
+ const groups = scopes.flatMap((s) => s.filter.kind === "group" ? [s.filter.groupId] : []);
598
+ return {
599
+ all: false,
600
+ groups: [.../* @__PURE__ */ new Set([...groups, ...extra])]
601
+ };
602
+ };
603
+ const NONE = {
604
+ all: false,
605
+ groups: []
606
+ };
607
+ const writing = (action) => {
608
+ const broad = action === "fill" ? "doc.forms.fill" : "doc.sign";
609
+ if (protectedCapabilities(protection).has(broad)) return NONE;
610
+ if (wildcard) return { all: true };
611
+ const scopes = fieldScopes(action);
612
+ if (scopes.length > 0) return fromScopes(scopes);
613
+ return expandedCapabilities(parsed, pdfBits, protection).has(broad) ? { all: true } : NONE;
614
+ };
615
+ const own = callerDefaultGroupId !== void 0 ? [callerDefaultGroupId] : [];
616
+ return {
617
+ fill: writing("fill"),
618
+ sign: writing("sign"),
619
+ setGroup: wildcard ? { all: true } : fromScopes(fieldScopes("set-group"), own)
620
+ };
621
+ }
622
+ /**
623
+ * Whether the scope lets the caller `action` some form field: the broad
624
+ * capability, or any `fields:<action>` scope. What a form verb checks
625
+ * before its fields are known; each field is checked in the write.
626
+ */
627
+ function checkAnyFieldAction(action, rawScope, pdfBits, protection = null) {
628
+ const broad = action === "fill" ? "doc.forms.fill" : "doc.sign";
629
+ if (protectedCapabilities(protection).has(broad)) return false;
630
+ const parsed = rawScope.map(parseScope);
631
+ if (parsed.some((s) => s.kind === "wildcard")) return true;
632
+ if (parsed.some((s) => s.kind === "collab" && s.entity === "fields" && (s.action === action || s.action === "*"))) return true;
633
+ return expandedCapabilities(parsed, pdfBits, protection).has(broad);
634
+ }
635
+ /**
636
+ * Test a single collab filter against a target record. Every filter but
637
+ * `self` compares one fact on the record with the value the scope names:
638
+ *
639
+ * all → always matches
640
+ * self → matches if identity.userId === target.userId
641
+ * createdBy=<X> → matches if target.userId === X
642
+ * group=<X> → matches if target.groupId === X
643
+ */
445
644
  function filterMatches(filter, target, id) {
446
645
  switch (filter.kind) {
447
646
  case "all": return true;
448
- case "self": return !!id.user_id && target.userId === id.user_id;
647
+ case "self": return !!id.userId && target.userId === id.userId;
449
648
  case "createdBy": return target.userId === filter.userId;
450
- case "group": return target.groupId === filter.groupId && (id.groups?.includes(filter.groupId) ?? false);
649
+ case "group": return target.groupId === filter.groupId;
451
650
  }
452
651
  }
453
652
  /**
454
653
  * Translate `pdf.permissions` (virtual scope) into the concrete
455
- * capabilities it represents under the current PDF bit configuration.
456
- *
457
- * Always adds `doc.open` and `doc.render` — these are cloud-only
458
- * capabilities with no PDF bit, but `pdf.permissions` is meant to be
459
- * the "give the user a working session" shorthand. Without them, a
460
- * token with just `['pdf.permissions']` would be useless.
461
- *
462
- * Reads are unconditional: ISO 32000 / Acrobat let any reader see
463
- * existing annotations and form values regardless of permission bits.
464
- * Bit 6 governs *writing*, not visibility.
465
- *
466
- * Bit-derived expansions follow ISO 32000:
467
- * bit 5 → doc.text.{select, copy, search}, doc.content.copy
468
- * bit 3 → doc.print
469
- * bit 12 → doc.print.high (requires bit 3 also set)
470
- * bit 4 → doc.pages.modify, doc.redact, doc.metadata.modify, doc.attachments.modify
471
- * bit 11 → doc.pages.assemble
472
- * bit 6 → doc.annotate.modify
473
- * bit 6/9 → doc.forms.fill
474
- * bit 6+4 → doc.forms.modify
475
- *
476
- * Note: this same expansion lives in builders.ts as
477
- * `materializePdfPermissions` for SDK-side use. The two MUST stay in
478
- * sync; a test in resolver.test.ts pins them together.
654
+ * capabilities it represents under the current PDF bit configuration:
655
+ * {@link materializePdfPermissions}, the one reading of the bits.
479
656
  */
480
657
  function addPdfPermissions(out, b) {
481
- out.add("doc.open");
482
- out.add("doc.render");
483
- out.add("doc.annotate.read");
484
- out.add("doc.forms.read");
485
- if (b.bit5) {
486
- out.add("doc.text.select");
487
- out.add("doc.text.copy");
488
- out.add("doc.text.search");
489
- out.add("doc.content.copy");
490
- }
491
- if (b.bit3) out.add("doc.print");
492
- if (b.bit12 && b.bit3) out.add("doc.print.high");
493
- if (b.bit4) {
494
- out.add("doc.pages.modify");
495
- out.add("doc.redact");
496
- out.add("doc.metadata.modify");
497
- out.add("doc.attachments.modify");
498
- }
499
- if (b.bit11) out.add("doc.pages.assemble");
500
- if (b.bit6) out.add("doc.annotate.modify");
501
- if (b.bit6 || b.bit9) {
502
- out.add("doc.forms.fill");
503
- out.add("doc.sign");
504
- }
505
- if (b.bit6 && b.bit4) out.add("doc.forms.modify");
658
+ for (const capability of materializePdfPermissions(b)) out.add(capability);
506
659
  }
507
660
  //#endregion
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 };
661
+ export { MissingIdentity as A, lockCovers as C, decodeAnnotKey as D, protectedCapabilities as E, encodeAnnotKey as O, levelFromPermission as S, minLevel as T, deriveProtection as _, checkFieldAction as a, isProtectableCapability as b, expandRawScope as c, filterMatches as d, parseScope as f, SIGNATURE_POLICY_VERSION as g, PROTECTABLE_CAPABILITIES as h, checkCollab as i, PermissionDenied as j, InvalidScope as k, expandedCapabilities as l, APPROVAL_BASELINE as m, checkAnyFieldAction as n, checkSetGroup as o, validateScopeArray as p, checkCapability as r, collabTargetOf as s, checkAnyCapability as t, fieldRightsOf as u, describeProtection as v, lockNameCovers as w, levelAllows as x, fieldLockFor as y };
509
662
 
510
- //# sourceMappingURL=resolver-inAVzsWt.js.map
663
+ //# sourceMappingURL=resolver-BBggAxQc.js.map