@embedpdf/plugin-signature 3.0.0-next.13 → 3.0.0-next.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -8
- package/dist/contract-Bu7_S84K.cjs +99 -0
- package/dist/contract-Bu7_S84K.cjs.map +1 -0
- package/dist/contract-GUWa_aBY.d.ts +299 -0
- package/dist/contract-NW5jPVqt.js +59 -0
- package/dist/contract-NW5jPVqt.js.map +1 -0
- package/dist/contract-c89MSPDK.d.cts +299 -0
- package/dist/contract.cjs +3 -1
- package/dist/contract.d.cts +2 -2
- package/dist/contract.d.ts +2 -2
- package/dist/contract.js +2 -2
- package/dist/index.cjs +814 -364
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -25
- package/dist/index.d.ts +31 -25
- package/dist/index.js +805 -364
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/dist/contract-BvHa9Zl_.d.cts +0 -176
- package/dist/contract-C3nQtwGf.js +0 -14
- package/dist/contract-C3nQtwGf.js.map +0 -1
- package/dist/contract-CojqGdMH.cjs +0 -43
- package/dist/contract-CojqGdMH.cjs.map +0 -1
- package/dist/contract-Dg50puf7.d.ts +0 -176
package/dist/index.cjs
CHANGED
|
@@ -1,12 +1,515 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_contract = require("./contract-
|
|
2
|
+
const require_contract = require("./contract-Bu7_S84K.cjs");
|
|
3
3
|
let _embedpdf_core = require("@embedpdf/core");
|
|
4
|
+
let _embedpdf_plugin_annotation_contract = require("@embedpdf/plugin-annotation/contract");
|
|
4
5
|
let _embedpdf_plugin_form_contract = require("@embedpdf/plugin-form/contract");
|
|
5
|
-
let
|
|
6
|
+
let _embedpdf_plugin_interaction_contract_host = require("@embedpdf/plugin-interaction/contract/host");
|
|
6
7
|
let _embedpdf_plugin_stamp_contract = require("@embedpdf/plugin-stamp/contract");
|
|
7
|
-
let
|
|
8
|
-
let _embedpdf_core_signature = require("@embedpdf/core-signature");
|
|
8
|
+
let _embedpdf_plugin_annotation_contract_host = require("@embedpdf/plugin-annotation/contract/host");
|
|
9
9
|
let _embedpdf_engine_core_runtime = require("@embedpdf/engine-core/runtime");
|
|
10
|
+
let _embedpdf_core_signature = require("@embedpdf/core-signature");
|
|
11
|
+
//#region src/tools/armed-mark.ts
|
|
12
|
+
/**
|
|
13
|
+
* The armed mark over a field. The annotation plugin places an armed stamp
|
|
14
|
+
* on click (`annotation-place`, priority 95) and selects an existing
|
|
15
|
+
* annotation under the pointer before that (`annotation-edit`, 100; a
|
|
16
|
+
* widget is an annotation). This handler sits above both, enabled for the
|
|
17
|
+
* armed stamp's tool only, and captures exactly when the armed asset is a
|
|
18
|
+
* person's mark (a library of kind `signatures`) and the pointer is over a
|
|
19
|
+
* signature field: then the destination decides, and the mark goes into the
|
|
20
|
+
* field (`placeMark` by mode) instead of onto the page. Everywhere else it
|
|
21
|
+
* declines and the click stays a stamp placement, as in Preview.
|
|
22
|
+
*/
|
|
23
|
+
function createArmedMarkHandler(documentId, signature, form, stamp) {
|
|
24
|
+
return {
|
|
25
|
+
id: "signature:armed-mark-on-field",
|
|
26
|
+
priority: _embedpdf_plugin_annotation_contract_host.ANNOTATION_EDIT_PRIORITY + 1,
|
|
27
|
+
enabledFor: (tool) => tool.id === _embedpdf_plugin_annotation_contract_host.ARMED_STAMP_TOOL_ID,
|
|
28
|
+
onDown: (sample) => {
|
|
29
|
+
if (!sample.page) return false;
|
|
30
|
+
const armed = stamp.getArmedAsset(documentId);
|
|
31
|
+
if (!armed || stamp.getLibrary(armed.libraryId)?.kind !== "signatures") return false;
|
|
32
|
+
const hit = form.getWidgetAt(sample.page.ref, sample.page.point);
|
|
33
|
+
if (!hit || hit.field.family !== "signature") return false;
|
|
34
|
+
if (signature.getSignature(hit.field.ref)?.signed || hit.field.valueEntry.kind !== "none") return true;
|
|
35
|
+
stamp.disarm(documentId);
|
|
36
|
+
signature.placeMark({ assetId: armed.id }, { field: hit.field.ref }).catch((error) => globalThis.console?.error("[signature] placing the mark failed:", error));
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/connect.ts
|
|
43
|
+
/**
|
|
44
|
+
* Wiring to sibling plugins. With the interaction hub and the stamp plugin
|
|
45
|
+
* present, an armed mark from a `signatures` library dropped over an
|
|
46
|
+
* unsigned signature field goes into the field instead of onto the page.
|
|
47
|
+
*/
|
|
48
|
+
function connectSiblings(ctx, signature) {
|
|
49
|
+
const interaction = ctx.tryGet(_embedpdf_plugin_interaction_contract_host.InteractionToken);
|
|
50
|
+
const stamp = ctx.tryGet(_embedpdf_plugin_stamp_contract.StampToken);
|
|
51
|
+
const documentId = ctx.documentId;
|
|
52
|
+
if (!interaction || !stamp || !documentId) return;
|
|
53
|
+
ctx.cleanup(interaction.registerHandler(createArmedMarkHandler(documentId, signature, ctx.get(_embedpdf_plugin_form_contract.FormToken), stamp)));
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/services/errors.ts
|
|
57
|
+
/**
|
|
58
|
+
* A capability verb: whatever the engine, a signer port or the validator
|
|
59
|
+
* rejects with reaches the caller as a `PluginError`.
|
|
60
|
+
*/
|
|
61
|
+
const verb = (run) => async (...args) => {
|
|
62
|
+
try {
|
|
63
|
+
return await run(...args);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw (0, _embedpdf_core.toPluginError)("signature", error);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/read/judging.ts
|
|
70
|
+
/**
|
|
71
|
+
* The judging verbs: re-reading the facts, validating, and the byte-level
|
|
72
|
+
* questions a verdict rests on (what changed after a signature, the exact
|
|
73
|
+
* revision it covers).
|
|
74
|
+
*/
|
|
75
|
+
function createJudging(ctx, { store }, { validate }, signatures, { getSignature, getVerdict }) {
|
|
76
|
+
const { requireSignatures } = store;
|
|
77
|
+
return { api: {
|
|
78
|
+
refresh: verb(async (options) => {
|
|
79
|
+
await ctx.cancellable(options?.signal, signatures.refresh());
|
|
80
|
+
return signatures.get().snapshot;
|
|
81
|
+
}),
|
|
82
|
+
validate: verb(validate),
|
|
83
|
+
validateField: verb(async (field, options) => {
|
|
84
|
+
await validate(options);
|
|
85
|
+
const verdict = getVerdict(field);
|
|
86
|
+
if (!verdict) throw new _embedpdf_core.PluginError("not-found", "signature", "no signed field at that address");
|
|
87
|
+
return verdict;
|
|
88
|
+
}),
|
|
89
|
+
analyzeChanges: verb(async (input, options) => ctx.cancellable(options?.signal, requireSignatures().analyze(input))),
|
|
90
|
+
readRevision: verb(async (target, options) => {
|
|
91
|
+
ctx.assertAllowed("doc.download", "signature.readRevision");
|
|
92
|
+
const service = requireSignatures();
|
|
93
|
+
let revisionIndex;
|
|
94
|
+
if ("revisionIndex" in target) revisionIndex = target.revisionIndex;
|
|
95
|
+
else {
|
|
96
|
+
const signature = getSignature(target);
|
|
97
|
+
if (!signature?.signed || signature.revisionIndex === null) throw new _embedpdf_core.PluginError("not-found", "signature", "no signed revision at that address");
|
|
98
|
+
revisionIndex = signature.revisionIndex;
|
|
99
|
+
}
|
|
100
|
+
return ctx.cancellable(options?.signal, service.downloadRevision(revisionIndex));
|
|
101
|
+
})
|
|
102
|
+
} };
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/model.ts
|
|
106
|
+
/**
|
|
107
|
+
* The signature plugin's state, in two kinds. Session state (the last
|
|
108
|
+
* validation, the sign-here target, the busy flag) changes only through the
|
|
109
|
+
* pure transitions below. The signature facts (the snapshot and the parked
|
|
110
|
+
* two-phase signing) are engine data: a mirror folds them from confirmed
|
|
111
|
+
* document events with {@link foldSignatureEvent}.
|
|
112
|
+
*/
|
|
113
|
+
const initialSignatureState = () => ({
|
|
114
|
+
verdicts: null,
|
|
115
|
+
target: null,
|
|
116
|
+
busy: false
|
|
117
|
+
});
|
|
118
|
+
const sameFieldRef = (left, right) => left.kind === "objectNumber" && right.kind === "objectNumber" ? left.objectNumber === right.objectNumber : left.kind === "fqn" && right.kind === "fqn" ? left.name === right.name : false;
|
|
119
|
+
function setVerdicts(state, verdicts) {
|
|
120
|
+
return state.verdicts === verdicts ? state : {
|
|
121
|
+
...state,
|
|
122
|
+
verdicts
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** Name the sign-here target; the same field (by ref) changes nothing. */
|
|
126
|
+
function setTarget(state, field) {
|
|
127
|
+
const current = state.target;
|
|
128
|
+
if (current === field || current && field && sameFieldRef(current, field)) return state;
|
|
129
|
+
return {
|
|
130
|
+
...state,
|
|
131
|
+
target: field
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function setBusy(state, busy) {
|
|
135
|
+
return state.busy === busy ? state : {
|
|
136
|
+
...state,
|
|
137
|
+
busy
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The verdicts that turned invalid because of unsaved edits: judged on the
|
|
142
|
+
* working copy, invalid now, and not already invalid in the previous
|
|
143
|
+
* validation (a signature that held, or was never judged).
|
|
144
|
+
*/
|
|
145
|
+
function newlyInvalidated(previous, next) {
|
|
146
|
+
return next.filter((verdict) => {
|
|
147
|
+
if (verdict.modifications.basis !== "working-copy" || verdict.summary !== "invalid") return false;
|
|
148
|
+
const before = previous?.find((earlier) => earlier.signature.index === verdict.signature.index);
|
|
149
|
+
return !before || before.summary !== "invalid";
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const emptySignatureRecord = () => ({
|
|
153
|
+
snapshot: null,
|
|
154
|
+
pending: null
|
|
155
|
+
});
|
|
156
|
+
const hasSignedField = (snapshot) => snapshot?.signatures.some((signature) => signature.signed) ?? false;
|
|
157
|
+
/** The result a `signatures.completed` event carries, without the event envelope. */
|
|
158
|
+
const completeResultOf = (event) => ({
|
|
159
|
+
status: event.status,
|
|
160
|
+
signature: event.signature,
|
|
161
|
+
version: event.version,
|
|
162
|
+
previous: event.previous,
|
|
163
|
+
protection: event.protection,
|
|
164
|
+
meta: event.meta
|
|
165
|
+
});
|
|
166
|
+
/**
|
|
167
|
+
* A completed signing, applied to the snapshot: the sealed field's facts and
|
|
168
|
+
* the protection now in force. The revisions change as well; the
|
|
169
|
+
* `document.versioned` event that follows every completion reloads them.
|
|
170
|
+
*/
|
|
171
|
+
function withCompletedSignature(snapshot, event) {
|
|
172
|
+
if (!snapshot) return null;
|
|
173
|
+
const sealed = event.signature;
|
|
174
|
+
return {
|
|
175
|
+
...snapshot,
|
|
176
|
+
signatures: snapshot.signatures.map((signature) => sameFieldRef(signature.field, sealed.field) ? sealed : signature),
|
|
177
|
+
protection: event.protection
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/** Events that change which signature fields exist; the snapshot lists fields, so it is re-read. */
|
|
181
|
+
const FIELD_SET_EVENTS = /* @__PURE__ */ new Set([
|
|
182
|
+
"forms.created",
|
|
183
|
+
"forms.deleted",
|
|
184
|
+
"forms.widgetAdded",
|
|
185
|
+
"forms.widgetRemoved",
|
|
186
|
+
"forms.repaired"
|
|
187
|
+
]);
|
|
188
|
+
/** Apply one confirmed document event to the signature facts. Pure, the same for every origin. */
|
|
189
|
+
function foldSignatureEvent(record, event) {
|
|
190
|
+
switch (event.type) {
|
|
191
|
+
case "signatures.prepared": return {
|
|
192
|
+
...record,
|
|
193
|
+
pending: {
|
|
194
|
+
signingId: event.signingId,
|
|
195
|
+
field: event.field
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
case "signatures.cancelled": return record.pending ? {
|
|
199
|
+
...record,
|
|
200
|
+
pending: null
|
|
201
|
+
} : record;
|
|
202
|
+
case "signatures.completed": return {
|
|
203
|
+
snapshot: withCompletedSignature(record.snapshot, event),
|
|
204
|
+
pending: null
|
|
205
|
+
};
|
|
206
|
+
default: return FIELD_SET_EVENTS.has(event.type) ? (0, _embedpdf_core.reload)() : record;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/read/signatures.ts
|
|
211
|
+
/** Reads over the signature facts and the last validation. The lists are
|
|
212
|
+
* memoized per snapshot so they stay reference-stable for selectors. */
|
|
213
|
+
const EMPTY_SIGNATURES = [];
|
|
214
|
+
const EMPTY_FIELDS = [];
|
|
215
|
+
/** The widget an address names, or null for a field ref. */
|
|
216
|
+
const widgetOf = (field) => {
|
|
217
|
+
if (!("kind" in field)) return field;
|
|
218
|
+
if (field.kind === "objectNumber" && "page" in field) return field;
|
|
219
|
+
return null;
|
|
220
|
+
};
|
|
221
|
+
const isFieldRef = (field) => "kind" in field && (field.kind === "fqn" || field.kind === "objectNumber" && !("page" in field));
|
|
222
|
+
function createSignatureReads(ctx, { authority, siblings }, signatures) {
|
|
223
|
+
const { form } = siblings;
|
|
224
|
+
const snapshot = () => signatures.get().snapshot;
|
|
225
|
+
const lists = (0, _embedpdf_core.memo)(() => [snapshot(), ctx.state.get().verdicts], (current, verdicts) => {
|
|
226
|
+
if (!current) return {
|
|
227
|
+
signed: EMPTY_SIGNATURES,
|
|
228
|
+
unsigned: EMPTY_FIELDS
|
|
229
|
+
};
|
|
230
|
+
const verdictOf = (signature) => verdicts?.find((verdict) => verdict.signature.index === signature.index) ?? null;
|
|
231
|
+
return {
|
|
232
|
+
signed: current.signatures.filter((signature) => signature.signed).map((signature) => ({
|
|
233
|
+
...signature,
|
|
234
|
+
verdict: verdictOf(signature)
|
|
235
|
+
})),
|
|
236
|
+
unsigned: current.signatures.filter((signature) => !signature.signed).map((signature) => signature.field)
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
const getSignature = (field) => {
|
|
240
|
+
const current = snapshot();
|
|
241
|
+
if (!current) return null;
|
|
242
|
+
const widget = widgetOf(field);
|
|
243
|
+
if (widget) {
|
|
244
|
+
const byWidget = current.signatures.find((signature) => signature.widget?.objectNumber === widget.objectNumber);
|
|
245
|
+
if (byWidget) return byWidget;
|
|
246
|
+
const owner = form.getFieldForWidget(widget);
|
|
247
|
+
return owner ? getSignature(owner.ref) : null;
|
|
248
|
+
}
|
|
249
|
+
if (!isFieldRef(field)) return null;
|
|
250
|
+
return current.signatures.find((signature) => field.kind === "fqn" ? signature.fieldName === field.name : sameFieldRef(signature.field, field)) ?? null;
|
|
251
|
+
};
|
|
252
|
+
const getVerdict = (field) => {
|
|
253
|
+
const signature = getSignature(field);
|
|
254
|
+
if (!signature) return null;
|
|
255
|
+
return ctx.state.get().verdicts?.find((verdict) => verdict.signature.index === signature.index) ?? null;
|
|
256
|
+
};
|
|
257
|
+
return {
|
|
258
|
+
snapshot,
|
|
259
|
+
getSignature,
|
|
260
|
+
getVerdict,
|
|
261
|
+
api: {
|
|
262
|
+
getSnapshot: snapshot,
|
|
263
|
+
getStatus: signatures.getStatus,
|
|
264
|
+
listSignatures: () => lists().signed,
|
|
265
|
+
listUnsignedFields: () => lists().unsigned,
|
|
266
|
+
getSignature,
|
|
267
|
+
listVerdicts: () => ctx.state.get().verdicts,
|
|
268
|
+
getVerdict,
|
|
269
|
+
getProtection: () => snapshot()?.protection ?? null,
|
|
270
|
+
getPending: () => signatures.get().pending,
|
|
271
|
+
isBusy: () => ctx.state.get().busy,
|
|
272
|
+
getMode: authority.mode,
|
|
273
|
+
getTarget: () => ctx.state.get().target
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
//#endregion
|
|
278
|
+
//#region src/services/authority.ts
|
|
279
|
+
/**
|
|
280
|
+
* The mode and the key, read from the settings where they're used, so a change
|
|
281
|
+
* applies at once; and who may sign or fill in which field, by the field's
|
|
282
|
+
* group (`ctx.allowsField`), as the engine checks it.
|
|
283
|
+
*/
|
|
284
|
+
function createAuthority(ctx, { form }) {
|
|
285
|
+
const settings = ctx.settings();
|
|
286
|
+
const mode = () => {
|
|
287
|
+
const { mode: chosen, key } = settings.get();
|
|
288
|
+
return chosen ?? (key ? "sign" : "visual");
|
|
289
|
+
};
|
|
290
|
+
const resolveKey = async (override) => {
|
|
291
|
+
const key = override ?? settings.get().key;
|
|
292
|
+
if (!key) throw new _embedpdf_core.PluginError("invalid-input", "signature", "no key: set the `key` setting or pass one with the call");
|
|
293
|
+
return typeof key === "function" ? key() : key;
|
|
294
|
+
};
|
|
295
|
+
/** Whether the session may `action` this field; `false` for a field the form doesn't have. */
|
|
296
|
+
const allowsField = (action, ref) => {
|
|
297
|
+
const field = form.get(ref);
|
|
298
|
+
return field !== null && ctx.allowsField(action, field);
|
|
299
|
+
};
|
|
300
|
+
/** Refuse a write to this field the session may not make, before anything starts. */
|
|
301
|
+
const assertAllowedField = (action, ref, operation) => {
|
|
302
|
+
const field = form.get(ref);
|
|
303
|
+
if (field) ctx.assertAllowedField(action, field, operation);
|
|
304
|
+
};
|
|
305
|
+
return {
|
|
306
|
+
mode,
|
|
307
|
+
resolveKey,
|
|
308
|
+
assertAllowedField,
|
|
309
|
+
canSign: (field) => allowsField("sign", field),
|
|
310
|
+
canFill: (field) => allowsField("fill", field),
|
|
311
|
+
canCertify: () => settings.get().allowCertify && ctx.allows("doc.sign.certify"),
|
|
312
|
+
canReadRevision: () => ctx.allows("doc.download")
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
//#endregion
|
|
316
|
+
//#region src/services/events.ts
|
|
317
|
+
function createEvents(ctx) {
|
|
318
|
+
const events = {
|
|
319
|
+
signed: ctx.events.source(),
|
|
320
|
+
filled: ctx.events.source(),
|
|
321
|
+
cleared: ctx.events.source(),
|
|
322
|
+
validated: ctx.events.source(),
|
|
323
|
+
protectionChanged: ctx.events.source(),
|
|
324
|
+
invalidationPredicted: ctx.events.source(),
|
|
325
|
+
targetChanged: ctx.events.source(),
|
|
326
|
+
signRequested: ctx.events.source(),
|
|
327
|
+
inspectionRequested: ctx.events.source()
|
|
328
|
+
};
|
|
329
|
+
ctx.state.onChange(({ previous, next }) => {
|
|
330
|
+
if (previous.target !== next.target) events.targetChanged.emit({ field: next.target });
|
|
331
|
+
if (previous.verdicts !== next.verdicts && next.verdicts) for (const verdict of newlyInvalidated(previous.verdicts, next.verdicts)) events.invalidationPredicted.emit({
|
|
332
|
+
field: verdict.signature.field,
|
|
333
|
+
detail: verdict.modifications.detail ?? ""
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
return events;
|
|
337
|
+
}
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/services/marks.ts
|
|
340
|
+
/** Marks → the one-page appearance bytes the engine draws into a field. */
|
|
341
|
+
function createMarks({ stamp }) {
|
|
342
|
+
const bytesOf = async (source) => new Uint8Array((await (0, _embedpdf_engine_core_runtime.resolveBinarySource)(source)).bytes);
|
|
343
|
+
const markBytes = async (mark) => {
|
|
344
|
+
if ("assetId" in mark) {
|
|
345
|
+
const bytes = stamp()?.readAssetBytes(mark.assetId);
|
|
346
|
+
if (!bytes) throw new _embedpdf_core.PluginError("not-found", "signature", `unknown mark '${mark.assetId}'`);
|
|
347
|
+
return bytes;
|
|
348
|
+
}
|
|
349
|
+
return bytesOf(mark.source);
|
|
350
|
+
};
|
|
351
|
+
return {
|
|
352
|
+
bytesOf,
|
|
353
|
+
markBytes
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region src/services/siblings.ts
|
|
358
|
+
/** The sibling planes: the form plugin (required), the stamp and annotation plugins (optional). */
|
|
359
|
+
function resolveSiblings(ctx) {
|
|
360
|
+
return {
|
|
361
|
+
form: ctx.get(_embedpdf_plugin_form_contract.FormToken),
|
|
362
|
+
stamp: () => ctx.tryGet(_embedpdf_plugin_stamp_contract.StampToken),
|
|
363
|
+
annotation: () => ctx.tryGet(_embedpdf_plugin_annotation_contract.AnnotationToken)
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region src/services/store.ts
|
|
368
|
+
/** The busy bracket and the engine doors every area needs. */
|
|
369
|
+
function createStore(ctx) {
|
|
370
|
+
const withBusy = async (work) => {
|
|
371
|
+
ctx.state.update(setBusy, true);
|
|
372
|
+
try {
|
|
373
|
+
return await work();
|
|
374
|
+
} finally {
|
|
375
|
+
ctx.state.update(setBusy, false);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const requireSignatures = () => {
|
|
379
|
+
const signatures = ctx.doc.signatures;
|
|
380
|
+
if (!signatures) throw new _embedpdf_core.PluginError("unsupported", "signature", "this engine does not implement signatures");
|
|
381
|
+
return signatures;
|
|
382
|
+
};
|
|
383
|
+
const documentId = () => {
|
|
384
|
+
const id = ctx.documentId;
|
|
385
|
+
if (!id) throw new _embedpdf_core.PluginError("not-ready", "signature", "no document id");
|
|
386
|
+
return id;
|
|
387
|
+
};
|
|
388
|
+
return {
|
|
389
|
+
withBusy,
|
|
390
|
+
requireSignatures,
|
|
391
|
+
documentId
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
//#endregion
|
|
395
|
+
//#region src/services/index.ts
|
|
396
|
+
/**
|
|
397
|
+
* Plugin-private services every area is built on (not the kernel): the
|
|
398
|
+
* events, the busy bracket and engine doors, authority, the sibling plugins
|
|
399
|
+
* and mark resolution.
|
|
400
|
+
*/
|
|
401
|
+
function createServices(ctx) {
|
|
402
|
+
const siblings = resolveSiblings(ctx);
|
|
403
|
+
return {
|
|
404
|
+
events: createEvents(ctx),
|
|
405
|
+
store: createStore(ctx),
|
|
406
|
+
authority: createAuthority(ctx, siblings),
|
|
407
|
+
siblings,
|
|
408
|
+
marks: createMarks(siblings)
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/sync/signatures.ts
|
|
413
|
+
/** Edits of the working copy: what a save would write changed, so a verdict may have too. */
|
|
414
|
+
const WORKING_COPY_EDITS = /* @__PURE__ */ new Set([
|
|
415
|
+
"annotations.created",
|
|
416
|
+
"annotations.updated",
|
|
417
|
+
"annotations.deleted",
|
|
418
|
+
"annotations.reordered",
|
|
419
|
+
"forms.valueSet",
|
|
420
|
+
"forms.updated"
|
|
421
|
+
]);
|
|
422
|
+
const sameProtection = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
423
|
+
function createSignaturesMirror(ctx, { events }, { validateNow, revalidateSoon }) {
|
|
424
|
+
const { signed, protectionChanged } = events;
|
|
425
|
+
/** Parked signings the engine answered it does not know: no event will clear them. */
|
|
426
|
+
const disowned = /* @__PURE__ */ new Set();
|
|
427
|
+
const mirror = ctx.mirror({
|
|
428
|
+
name: "signatures",
|
|
429
|
+
initial: emptySignatureRecord,
|
|
430
|
+
load: async (doc) => {
|
|
431
|
+
const snapshot = doc.signatures ? await doc.signatures.list() : null;
|
|
432
|
+
const pending = mirror.get().pending;
|
|
433
|
+
return { value: {
|
|
434
|
+
snapshot,
|
|
435
|
+
pending: pending && !disowned.has(pending.signingId) ? pending : null
|
|
436
|
+
} };
|
|
437
|
+
},
|
|
438
|
+
fold: foldSignatureEvent,
|
|
439
|
+
changed: ({ cause, event, previous, next }) => {
|
|
440
|
+
const protection = next.snapshot?.protection ?? null;
|
|
441
|
+
if (protection && !sameProtection(previous.snapshot?.protection ?? null, protection)) protectionChanged.emit({ protection });
|
|
442
|
+
if (event?.type === "signatures.completed") signed.emit({
|
|
443
|
+
field: event.signature.field,
|
|
444
|
+
result: completeResultOf(event),
|
|
445
|
+
origin: event.origin
|
|
446
|
+
});
|
|
447
|
+
if (!hasSignedField(next.snapshot)) return;
|
|
448
|
+
if (cause === "load") validateNow();
|
|
449
|
+
else if (previous.snapshot !== next.snapshot) revalidateSoon();
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
return {
|
|
453
|
+
mirror,
|
|
454
|
+
/**
|
|
455
|
+
* The engine does not know this parked signing (its cancel answered
|
|
456
|
+
* `unknown`), so no event will ever clear it: reload without it.
|
|
457
|
+
*/
|
|
458
|
+
disown(signingId) {
|
|
459
|
+
disowned.add(signingId);
|
|
460
|
+
return mirror.refresh();
|
|
461
|
+
},
|
|
462
|
+
connect() {
|
|
463
|
+
ctx.listen(ctx.doc.events, (event) => {
|
|
464
|
+
if (WORKING_COPY_EDITS.has(event.type) && hasSignedField(mirror.get().snapshot)) revalidateSoon();
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
//#endregion
|
|
470
|
+
//#region src/sync/validation.ts
|
|
471
|
+
const REVALIDATE_DELAY_MS = 300;
|
|
472
|
+
function createValidation(ctx, { events }) {
|
|
473
|
+
const { validated } = events;
|
|
474
|
+
const settings = ctx.settings();
|
|
475
|
+
const validate = async (options) => {
|
|
476
|
+
if (!ctx.doc.signatures) return [];
|
|
477
|
+
const { validateSignatures } = await ctx.cancellable(options?.signal, import("@embedpdf/core-signature"));
|
|
478
|
+
const verdicts = await ctx.cancellable(options?.signal, validateSignatures(ctx.doc, {
|
|
479
|
+
trust: settings.get().trust,
|
|
480
|
+
at: options?.at,
|
|
481
|
+
until: options?.until ?? "working-copy"
|
|
482
|
+
}));
|
|
483
|
+
ctx.state.update(setVerdicts, verdicts);
|
|
484
|
+
validated.emit({ verdicts });
|
|
485
|
+
return verdicts;
|
|
486
|
+
};
|
|
487
|
+
let cancelRevalidate = null;
|
|
488
|
+
const cancelScheduled = () => {
|
|
489
|
+
cancelRevalidate?.();
|
|
490
|
+
cancelRevalidate = null;
|
|
491
|
+
};
|
|
492
|
+
const validateInBackground = () => {
|
|
493
|
+
validate().catch((error) => globalThis.console?.error("[signature] validation failed:", error));
|
|
494
|
+
};
|
|
495
|
+
return {
|
|
496
|
+
validate,
|
|
497
|
+
/** Judge now; a scheduled re-judgement is superseded. */
|
|
498
|
+
validateNow: () => {
|
|
499
|
+
cancelScheduled();
|
|
500
|
+
validateInBackground();
|
|
501
|
+
},
|
|
502
|
+
/** Judge once the edits pause. */
|
|
503
|
+
revalidateSoon: () => {
|
|
504
|
+
cancelScheduled();
|
|
505
|
+
cancelRevalidate = ctx.clock.after(REVALIDATE_DELAY_MS, () => {
|
|
506
|
+
cancelRevalidate = null;
|
|
507
|
+
validateInBackground();
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
//#endregion
|
|
10
513
|
//#region src/blank-page.ts
|
|
11
514
|
const OBJECTS = [
|
|
12
515
|
"<< /Type /Catalog /Pages 2 0 R >>",
|
|
@@ -31,431 +534,376 @@ function blankPagePdf() {
|
|
|
31
534
|
return new Uint8Array(cached);
|
|
32
535
|
}
|
|
33
536
|
//#endregion
|
|
34
|
-
//#region src/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
45
|
-
|
|
537
|
+
//#region src/write/fill.ts
|
|
538
|
+
/** Visual fills: the mark becomes the widget's appearance, nothing is sealed. */
|
|
539
|
+
function createFills(ctx, { events, store, marks, authority }, { getSignature }, target) {
|
|
540
|
+
const { filled, cleared } = events;
|
|
541
|
+
const { withBusy } = store;
|
|
542
|
+
const { markBytes } = marks;
|
|
543
|
+
const setAppearance = async (field, pdf, options) => {
|
|
544
|
+
const forms = ctx.doc.forms;
|
|
545
|
+
if (!forms.setSignatureAppearance) throw new _embedpdf_core.PluginError("unsupported", "signature", "this engine cannot draw into a signature field");
|
|
546
|
+
const signature = getSignature(field);
|
|
547
|
+
if (signature?.signed) throw new _embedpdf_core.PluginError("conflict", "signature", `'${signature.fieldName}' is signed`);
|
|
548
|
+
await ctx.cancellable(options?.signal, forms.setSignatureAppearance(field, { pdf }));
|
|
46
549
|
};
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
};
|
|
54
|
-
};
|
|
55
|
-
const stamp = () => ctx.tryGet(_embedpdf_plugin_stamp_contract.StampToken);
|
|
56
|
-
const documentId = () => {
|
|
57
|
-
const id = ctx.documentId;
|
|
58
|
-
if (!id) throw new Error("[signature] no document id");
|
|
59
|
-
return id;
|
|
60
|
-
};
|
|
61
|
-
const mode = () => config.mode ?? (config.signer ? "sign" : "visual");
|
|
62
|
-
const resolveSigner = async () => {
|
|
63
|
-
const signer = config.signer;
|
|
64
|
-
if (!signer) throw new Error("[signature] no signer configured (mode \"sign\" needs one)");
|
|
65
|
-
return typeof signer === "function" ? signer() : signer;
|
|
66
|
-
};
|
|
67
|
-
const allows = (cap) => ctx.doc?.security.allows(cap) ?? false;
|
|
68
|
-
const refresh = async () => {
|
|
69
|
-
const doc = ctx.doc;
|
|
70
|
-
if (!doc?.signatures) {
|
|
71
|
-
ctx.dispatch({
|
|
72
|
-
type: "SNAPSHOT",
|
|
73
|
-
snapshot: null
|
|
74
|
-
});
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
const before = state().snapshot?.protection ?? null;
|
|
78
|
-
const snapshot = await doc.signatures.list();
|
|
79
|
-
ctx.dispatch({
|
|
80
|
-
type: "SNAPSHOT",
|
|
81
|
-
snapshot
|
|
82
|
-
});
|
|
83
|
-
if (!sameProtection(before, snapshot.protection)) changed.emit({
|
|
84
|
-
type: "protectionChanged",
|
|
85
|
-
protection: snapshot.protection
|
|
550
|
+
const fillField = async (field, mark, options) => {
|
|
551
|
+
authority.assertAllowedField("fill", field, "signature.fillField");
|
|
552
|
+
return withBusy(async () => {
|
|
553
|
+
await setAppearance(field, await markBytes(mark), options);
|
|
554
|
+
target.clearIfTarget(field);
|
|
555
|
+
filled.emit({ field });
|
|
86
556
|
});
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
trust: config.trust ?? null,
|
|
94
|
-
at: opts?.at,
|
|
95
|
-
until: opts?.until ?? "working-copy"
|
|
96
|
-
});
|
|
97
|
-
const before = state().verdicts;
|
|
98
|
-
ctx.dispatch({
|
|
99
|
-
type: "VERDICTS",
|
|
100
|
-
verdicts
|
|
101
|
-
});
|
|
102
|
-
changed.emit({
|
|
103
|
-
type: "validated",
|
|
104
|
-
verdicts
|
|
557
|
+
};
|
|
558
|
+
const clearField = async (field, options) => {
|
|
559
|
+
authority.assertAllowedField("fill", field, "signature.clearField");
|
|
560
|
+
return withBusy(async () => {
|
|
561
|
+
await setAppearance(field, blankPagePdf(), options);
|
|
562
|
+
cleared.emit({ field });
|
|
105
563
|
});
|
|
106
|
-
for (const v of verdicts) {
|
|
107
|
-
if (v.modifications.basis !== "working-copy" || v.summary !== "invalid") continue;
|
|
108
|
-
const was = before?.find((b) => b.signature.index === v.signature.index);
|
|
109
|
-
if (was && was.summary === "invalid") continue;
|
|
110
|
-
changed.emit({
|
|
111
|
-
type: "invalidating",
|
|
112
|
-
field: v.signature.field,
|
|
113
|
-
detail: v.modifications.detail ?? ""
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
return verdicts;
|
|
117
564
|
};
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
revalidateTimer = null;
|
|
124
|
-
validate().catch((error) => globalThis.console?.error("[signature] re-validation failed:", error));
|
|
125
|
-
}, 300);
|
|
126
|
-
};
|
|
127
|
-
ctx.cleanup(() => {
|
|
128
|
-
if (revalidateTimer) clearTimeout(revalidateTimer);
|
|
129
|
-
});
|
|
130
|
-
const analyze = (input) => requireSignatures().signatures.analyze(input);
|
|
131
|
-
const signatureOf = (field) => {
|
|
132
|
-
const snapshot = state().snapshot;
|
|
133
|
-
if (!snapshot) return null;
|
|
134
|
-
if ("annotObjectNumber" in field) {
|
|
135
|
-
const byWidget = snapshot.signatures.find((s) => s.widget?.annotObjectNumber === field.annotObjectNumber);
|
|
136
|
-
if (byWidget) return byWidget;
|
|
137
|
-
const owner = form.fieldForWidget(field.annotObjectNumber);
|
|
138
|
-
return owner ? signatureOf(owner.ref) : null;
|
|
565
|
+
return {
|
|
566
|
+
fillField,
|
|
567
|
+
api: {
|
|
568
|
+
fillField: verb(fillField),
|
|
569
|
+
clearField: verb(clearField)
|
|
139
570
|
}
|
|
140
|
-
return snapshot.signatures.find((s) => field.kind === "fqn" ? s.fieldName === field.name : sameRef(s.field, field)) ?? null;
|
|
141
571
|
};
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
572
|
+
}
|
|
573
|
+
//#endregion
|
|
574
|
+
//#region src/write/place.ts
|
|
575
|
+
/**
|
|
576
|
+
* The destination rule and the sign-here flow: a mark over a field is
|
|
577
|
+
* signed, drawn, or handed to the chrome by mode; a free placement is a
|
|
578
|
+
* stamp (one placement law, the stamp plugin's).
|
|
579
|
+
*/
|
|
580
|
+
function createTarget(ctx, { events }, { getSignature }) {
|
|
581
|
+
const { inspectionRequested } = events;
|
|
582
|
+
/** One field, whether a ref names it by object number or by name. */
|
|
583
|
+
const sameField = (left, right) => {
|
|
584
|
+
if (sameFieldRef(left, right)) return true;
|
|
585
|
+
const signature = getSignature(left);
|
|
586
|
+
return signature !== null && signature.index === getSignature(right)?.index;
|
|
146
587
|
};
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (!bytes) throw new Error(`[signature] unknown mark '${mark.assetId}'`);
|
|
152
|
-
return bytes;
|
|
153
|
-
}
|
|
154
|
-
return bytesOf(mark.source);
|
|
588
|
+
/** A field that was just signed or filled is no longer the target. */
|
|
589
|
+
const clearIfTarget = (field) => {
|
|
590
|
+
const current = ctx.state.get().target;
|
|
591
|
+
if (current && sameField(current, field)) ctx.state.update(setTarget, null);
|
|
155
592
|
};
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
return await work();
|
|
163
|
-
} finally {
|
|
164
|
-
ctx.dispatch({
|
|
165
|
-
type: "BUSY",
|
|
166
|
-
busy: false
|
|
167
|
-
});
|
|
593
|
+
return {
|
|
594
|
+
clearIfTarget,
|
|
595
|
+
api: {
|
|
596
|
+
setTarget: (field) => ctx.state.update(setTarget, field),
|
|
597
|
+
requestInspection: (field) => inspectionRequested.emit({ field })
|
|
168
598
|
}
|
|
169
599
|
};
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const appearance = input.appearance ? await bytesOf(input.appearance) : await markBytes(input.mark);
|
|
179
|
-
const result = await (0, _embedpdf_core_signature.sign)(doc, {
|
|
180
|
-
field: input.field,
|
|
181
|
-
signer,
|
|
182
|
-
attribution,
|
|
183
|
-
certify: input.certify,
|
|
184
|
-
lock: input.lock,
|
|
185
|
-
appearance: {
|
|
186
|
-
pdf: appearance,
|
|
187
|
-
pageIndex: 0
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
if (state().target && sameRef(state().target, input.field)) setTarget(null);
|
|
191
|
-
await refresh();
|
|
192
|
-
validate().catch((error) => globalThis.console?.error("[signature] validation after signing failed:", error));
|
|
193
|
-
changed.emit({
|
|
194
|
-
type: "signed",
|
|
195
|
-
field: input.field,
|
|
196
|
-
result
|
|
197
|
-
});
|
|
198
|
-
return result;
|
|
199
|
-
});
|
|
200
|
-
const setAppearance = async (field, pdf) => {
|
|
201
|
-
const doc = requireDoc();
|
|
202
|
-
if (!doc.forms.setSignatureAppearance) throw new Error("[signature] this engine cannot draw into a signature field");
|
|
203
|
-
const signature = signatureOf(field);
|
|
204
|
-
if (signature?.signed) throw new Error(`[signature] '${signature.fieldName}' is signed`);
|
|
205
|
-
await doc.forms.setSignatureAppearance(field, {
|
|
206
|
-
pdf,
|
|
207
|
-
pageIndex: 0
|
|
208
|
-
});
|
|
209
|
-
await form.refresh();
|
|
210
|
-
};
|
|
211
|
-
const fillField = (field, mark) => withBusy(async () => {
|
|
212
|
-
await setAppearance(field, await markBytes(mark));
|
|
213
|
-
if (state().target && sameRef(state().target, field)) setTarget(null);
|
|
214
|
-
changed.emit({
|
|
215
|
-
type: "filled",
|
|
216
|
-
field
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
const clearField = (field) => withBusy(async () => {
|
|
220
|
-
await setAppearance(field, blankPagePdf());
|
|
221
|
-
changed.emit({
|
|
222
|
-
type: "cleared",
|
|
223
|
-
field
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
const placeMark = async (mark, target) => {
|
|
600
|
+
}
|
|
601
|
+
function createPlacement(ctx, { events, store, authority, siblings }, { snapshot }, { sign }, { fillField }) {
|
|
602
|
+
const { signRequested } = events;
|
|
603
|
+
const { documentId } = store;
|
|
604
|
+
const { mode } = authority;
|
|
605
|
+
const { stamp, annotation } = siblings;
|
|
606
|
+
const settings = ctx.settings();
|
|
607
|
+
const placeMark = async (mark, target, options) => {
|
|
227
608
|
if (!("field" in target)) {
|
|
228
609
|
if ("assetId" in mark) {
|
|
229
610
|
const library = stamp();
|
|
230
|
-
if (!library) throw new
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
611
|
+
if (!library) throw new _embedpdf_core.PluginError("unsupported", "signature", "no stamp plugin to place an asset");
|
|
612
|
+
return {
|
|
613
|
+
kind: "placed",
|
|
614
|
+
annotation: (await library.placeAsset(mark.assetId, target, {
|
|
615
|
+
documentId: documentId(),
|
|
616
|
+
signal: options?.signal
|
|
617
|
+
})).annotation.ref
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
const annotations = annotation();
|
|
621
|
+
if (!annotations) throw new _embedpdf_core.PluginError("unsupported", "signature", "no annotation plugin to place a mark");
|
|
622
|
+
return {
|
|
623
|
+
kind: "placed",
|
|
624
|
+
annotation: (await annotations.stamps.place({ source: mark.source }, target, options)).annotation.ref
|
|
625
|
+
};
|
|
234
626
|
}
|
|
235
627
|
switch (mode()) {
|
|
236
628
|
case "sign":
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
type: "ask",
|
|
629
|
+
if (settings.get().allowCertify && !hasSignedField(snapshot())) {
|
|
630
|
+
signRequested.emit({
|
|
240
631
|
field: target.field,
|
|
241
632
|
mark
|
|
242
633
|
});
|
|
243
|
-
return
|
|
634
|
+
return {
|
|
635
|
+
kind: "requested",
|
|
636
|
+
field: target.field
|
|
637
|
+
};
|
|
244
638
|
}
|
|
245
|
-
|
|
639
|
+
return {
|
|
640
|
+
kind: "signed",
|
|
246
641
|
field: target.field,
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
642
|
+
result: await sign({
|
|
643
|
+
field: target.field,
|
|
644
|
+
mark
|
|
645
|
+
}, options)
|
|
646
|
+
};
|
|
250
647
|
case "visual":
|
|
251
|
-
await fillField(target.field, mark);
|
|
252
|
-
return
|
|
648
|
+
await fillField(target.field, mark, options);
|
|
649
|
+
return {
|
|
650
|
+
kind: "filled",
|
|
651
|
+
field: target.field
|
|
652
|
+
};
|
|
253
653
|
case "ask":
|
|
254
|
-
|
|
255
|
-
type: "ask",
|
|
654
|
+
signRequested.emit({
|
|
256
655
|
field: target.field,
|
|
257
656
|
mark
|
|
258
657
|
});
|
|
259
|
-
return
|
|
658
|
+
return {
|
|
659
|
+
kind: "requested",
|
|
660
|
+
field: target.field
|
|
661
|
+
};
|
|
260
662
|
}
|
|
261
663
|
};
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
664
|
+
return { api: { placeMark: verb(placeMark) } };
|
|
665
|
+
}
|
|
666
|
+
//#endregion
|
|
667
|
+
//#region src/write/sign.ts
|
|
668
|
+
/**
|
|
669
|
+
* The act of sealing. One-shot: `sign` (prepare, the key, complete,
|
|
670
|
+
* through `@embedpdf/core-signature`). Two-phase: `prepareSignature` parks
|
|
671
|
+
* the signing and hands out the digest; `completeSignature` seals it with a
|
|
672
|
+
* detached CMS; `cancelPending` cancels. The parked signing, the sealed
|
|
673
|
+
* field's facts and `onSigned` come from the confirmed events the engine
|
|
674
|
+
* publishes for each step (`sync/signatures.ts`), for every session alike.
|
|
675
|
+
*/
|
|
676
|
+
function createSigning(ctx, { store, authority, marks }, { api: reads }, target, signatures) {
|
|
677
|
+
const { withBusy, requireSignatures } = store;
|
|
678
|
+
const { resolveKey, assertAllowedField } = authority;
|
|
679
|
+
const { bytesOf, markBytes } = marks;
|
|
680
|
+
/** Two-phase signings this session prepared: the version each one must complete against. */
|
|
681
|
+
const prepared = /* @__PURE__ */ new Map();
|
|
682
|
+
const cancelled = (signal) => new _embedpdf_core.PluginError("operation-cancelled", "signature", "signing cancelled", { cause: signal.reason });
|
|
683
|
+
/**
|
|
684
|
+
* The document as one-shot signing sees it, with the caller's signal checked
|
|
685
|
+
* right before the seal: a signal that fired by then leaves nothing signed
|
|
686
|
+
* (the signing gives the prepared candidate up), and after it the seal is
|
|
687
|
+
* done.
|
|
688
|
+
*/
|
|
689
|
+
const sealUnlessCancelled = (signal) => {
|
|
690
|
+
const signatures = requireSignatures();
|
|
691
|
+
if (!signal) return ctx.doc;
|
|
692
|
+
return { signatures: {
|
|
693
|
+
prepare: (input) => signatures.prepare(input),
|
|
694
|
+
cancel: (signingId) => signatures.cancel(signingId),
|
|
695
|
+
complete: (input) => {
|
|
696
|
+
if (signal.aborted) return Promise.reject(cancelled(signal));
|
|
697
|
+
return signatures.complete(input);
|
|
698
|
+
}
|
|
699
|
+
} };
|
|
700
|
+
};
|
|
701
|
+
/**
|
|
702
|
+
* Refused before anything starts: signing the field (`doc.sign`, or
|
|
703
|
+
* `fields:sign` for its group), and `doc.sign.certify` for a certification.
|
|
704
|
+
*/
|
|
705
|
+
const assertMaySign = (input, operation) => {
|
|
706
|
+
assertAllowedField("sign", input.field, operation);
|
|
707
|
+
if (input.certify) ctx.assertAllowed("doc.sign.certify", operation);
|
|
708
|
+
};
|
|
709
|
+
const appearanceOf = async (input) => {
|
|
710
|
+
if (input.appearance) return bytesOf(input.appearance);
|
|
711
|
+
if (input.mark) return markBytes(input.mark);
|
|
712
|
+
return null;
|
|
713
|
+
};
|
|
714
|
+
const sign = async (input, options) => {
|
|
715
|
+
assertMaySign(input, "signature.sign");
|
|
716
|
+
if (options?.signal?.aborted) throw cancelled(options.signal);
|
|
717
|
+
return withBusy(async () => {
|
|
718
|
+
const doc = sealUnlessCancelled(options?.signal);
|
|
719
|
+
const { certificateCommonName, sign: signDocument } = await import("@embedpdf/core-signature");
|
|
720
|
+
const key = await resolveKey(input.key);
|
|
721
|
+
const subject = key.kind === "raw" && key.certificateChain[0] ? certificateCommonName(key.certificateChain[0]) : null;
|
|
722
|
+
const signer = {
|
|
723
|
+
...subject ? { name: subject } : {},
|
|
724
|
+
...input.signer
|
|
725
|
+
};
|
|
726
|
+
const appearance = await appearanceOf(input);
|
|
727
|
+
const result = await signDocument(doc, {
|
|
728
|
+
field: input.field,
|
|
729
|
+
key,
|
|
730
|
+
signer,
|
|
731
|
+
certify: input.certify,
|
|
732
|
+
lock: input.lock,
|
|
733
|
+
appearance: { pdf: appearance }
|
|
734
|
+
});
|
|
735
|
+
target.clearIfTarget(input.field);
|
|
736
|
+
return result;
|
|
268
737
|
});
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
738
|
+
};
|
|
739
|
+
const prepareSignature = async (input, options) => {
|
|
740
|
+
assertMaySign(input, "signature.prepareSignature");
|
|
741
|
+
return withBusy(async () => {
|
|
742
|
+
const signatures = requireSignatures();
|
|
743
|
+
const appearance = await appearanceOf(input);
|
|
744
|
+
const pending = signatures.prepare({
|
|
745
|
+
field: input.field,
|
|
746
|
+
signer: input.signer,
|
|
747
|
+
certify: input.certify,
|
|
748
|
+
lock: input.lock,
|
|
749
|
+
subFilter: input.subFilter,
|
|
750
|
+
digest: input.digest,
|
|
751
|
+
...appearance ? { appearance: { pdf: appearance } } : {}
|
|
752
|
+
});
|
|
753
|
+
const result = await ctx.cancellable(options?.signal, pending);
|
|
754
|
+
prepared.set(result.signingId, result);
|
|
755
|
+
return result;
|
|
272
756
|
});
|
|
273
757
|
};
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
ctx.dispatch({
|
|
289
|
-
type: "PENDING",
|
|
290
|
-
pending: null
|
|
291
|
-
});
|
|
292
|
-
return;
|
|
293
|
-
case "signature.completed":
|
|
294
|
-
ctx.dispatch({
|
|
295
|
-
type: "PENDING",
|
|
296
|
-
pending: null
|
|
297
|
-
});
|
|
298
|
-
return;
|
|
299
|
-
case "document.versioned":
|
|
300
|
-
case "stream.desynced":
|
|
301
|
-
refresh().then(() => validate()).catch((error) => globalThis.console?.error("[signature] refresh failed:", error));
|
|
302
|
-
return;
|
|
303
|
-
case "form.fieldCreated":
|
|
304
|
-
case "form.fieldDeleted":
|
|
305
|
-
case "form.widgetAttached":
|
|
306
|
-
case "form.widgetDetached":
|
|
307
|
-
case "form.imported":
|
|
308
|
-
case "form.repaired":
|
|
309
|
-
refresh().then(() => revalidateSoon()).catch((error) => globalThis.console?.error("[signature] refresh failed:", error));
|
|
310
|
-
return;
|
|
311
|
-
case "annotation.created":
|
|
312
|
-
case "annotation.updated":
|
|
313
|
-
case "annotation.deleted":
|
|
314
|
-
case "annotation.moved":
|
|
315
|
-
case "form.valueChanged":
|
|
316
|
-
case "form.fieldUpdated":
|
|
317
|
-
case "form.effectsApplied":
|
|
318
|
-
revalidateSoon();
|
|
319
|
-
return;
|
|
320
|
-
default: return;
|
|
321
|
-
}
|
|
758
|
+
const completeSignature = async (signingId, cms, options) => {
|
|
759
|
+
if (options?.signal?.aborted) throw cancelled(options.signal);
|
|
760
|
+
return withBusy(async () => {
|
|
761
|
+
const signatures = requireSignatures();
|
|
762
|
+
const parked = prepared.get(signingId);
|
|
763
|
+
if (!parked) throw new _embedpdf_core.PluginError("not-found", "signature", `no signing '${signingId}' was prepared in this session`);
|
|
764
|
+
const result = await signatures.complete({
|
|
765
|
+
signingId,
|
|
766
|
+
cms,
|
|
767
|
+
expectedVersion: parked.expectedVersion
|
|
768
|
+
});
|
|
769
|
+
prepared.delete(signingId);
|
|
770
|
+
target.clearIfTarget(result.signature.field);
|
|
771
|
+
return result;
|
|
322
772
|
});
|
|
323
|
-
|
|
324
|
-
|
|
773
|
+
};
|
|
774
|
+
const cancelPending = async (options) => {
|
|
775
|
+
const pending = reads.getPending();
|
|
776
|
+
if (!pending) return;
|
|
777
|
+
assertAllowedField("sign", pending.field, "signature.cancelPending");
|
|
778
|
+
const { status } = await ctx.cancellable(options?.signal, requireSignatures().cancel(pending.signingId));
|
|
779
|
+
prepared.delete(pending.signingId);
|
|
780
|
+
if (status === "unknown") await signatures.disown(pending.signingId);
|
|
781
|
+
};
|
|
325
782
|
return {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
signatureOf,
|
|
334
|
-
verdictOf,
|
|
335
|
-
canSign: () => allows("doc.sign") && config.signer != null,
|
|
336
|
-
canFill: () => allows("doc.forms.fill") && ctx.doc?.forms.setSignatureAppearance != null,
|
|
337
|
-
canCertify: () => config.allowCertify === true && allows("doc.sign.certify"),
|
|
338
|
-
signField,
|
|
339
|
-
fillField,
|
|
340
|
-
clearField,
|
|
341
|
-
placeMark,
|
|
342
|
-
setTarget,
|
|
343
|
-
inspect: (field) => changed.emit({
|
|
344
|
-
type: "inspect",
|
|
345
|
-
field
|
|
346
|
-
}),
|
|
347
|
-
refresh,
|
|
348
|
-
validate,
|
|
349
|
-
analyze,
|
|
350
|
-
revisionBytes: (revisionIndex) => requireSignatures().signatures.revisionBytes(revisionIndex),
|
|
351
|
-
onChanged: changed.on
|
|
783
|
+
sign,
|
|
784
|
+
api: {
|
|
785
|
+
sign: verb(sign),
|
|
786
|
+
prepareSignature: verb(prepareSignature),
|
|
787
|
+
completeSignature: verb(completeSignature),
|
|
788
|
+
cancelPending: verb(cancelPending)
|
|
789
|
+
}
|
|
352
790
|
};
|
|
353
791
|
}
|
|
354
792
|
//#endregion
|
|
355
|
-
//#region src/
|
|
793
|
+
//#region src/controller.ts
|
|
356
794
|
/**
|
|
357
|
-
* The
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
* armed stamp's tool only, and captures exactly when the armed asset is a
|
|
362
|
-
* person's mark (a library of kind `signatures`) and the pointer is over a
|
|
363
|
-
* signature field: then the destination decides — the mark goes INTO the
|
|
364
|
-
* field (`placeMark` by mode) instead of onto the page. Everywhere else it
|
|
365
|
-
* declines and the click stays a stamp placement, as in Preview.
|
|
795
|
+
* The signature controller: the composition root. It builds the plugin's
|
|
796
|
+
* services once, wires each area with the services it declares, and
|
|
797
|
+
* assembles the capability from the areas' API slices. No behavior lives
|
|
798
|
+
* here: every verb and read has a home in `read/`, `write/` or `sync/`.
|
|
366
799
|
*/
|
|
367
|
-
function
|
|
800
|
+
function createSignatureController(ctx) {
|
|
801
|
+
const services = createServices(ctx);
|
|
802
|
+
const { events, authority } = services;
|
|
803
|
+
const settings = ctx.settings();
|
|
804
|
+
const validation = createValidation(ctx, services);
|
|
805
|
+
const signatures = createSignaturesMirror(ctx, services, validation);
|
|
806
|
+
const reads = createSignatureReads(ctx, services, signatures.mirror);
|
|
807
|
+
const judging = createJudging(ctx, services, validation, signatures.mirror, reads);
|
|
808
|
+
const target = createTarget(ctx, services, reads);
|
|
809
|
+
const signing = createSigning(ctx, services, reads, target, signatures);
|
|
810
|
+
const fills = createFills(ctx, services, reads, target);
|
|
811
|
+
const placement = createPlacement(ctx, services, reads, signing, fills);
|
|
812
|
+
const api = (0, _embedpdf_core.composeApi)("signature", [
|
|
813
|
+
settings.api,
|
|
814
|
+
reads.api,
|
|
815
|
+
judging.api,
|
|
816
|
+
target.api,
|
|
817
|
+
signing.api,
|
|
818
|
+
fills.api,
|
|
819
|
+
placement.api,
|
|
820
|
+
{
|
|
821
|
+
canSign: authority.canSign,
|
|
822
|
+
canFill: authority.canFill,
|
|
823
|
+
canCertify: authority.canCertify,
|
|
824
|
+
canReadRevision: authority.canReadRevision,
|
|
825
|
+
onSigned: events.signed.on,
|
|
826
|
+
onFilled: events.filled.on,
|
|
827
|
+
onCleared: events.cleared.on,
|
|
828
|
+
onValidated: events.validated.on,
|
|
829
|
+
onProtectionChanged: events.protectionChanged.on,
|
|
830
|
+
onInvalidationPredicted: events.invalidationPredicted.on,
|
|
831
|
+
onTargetChanged: events.targetChanged.on,
|
|
832
|
+
onSignRequested: events.signRequested.on,
|
|
833
|
+
onInspectionRequested: events.inspectionRequested.on
|
|
834
|
+
}
|
|
835
|
+
]);
|
|
368
836
|
return {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if (!sample.page) return false;
|
|
374
|
-
const armed = stamp.armedAsset(documentId);
|
|
375
|
-
if (!armed || stamp.library(armed.libraryId)?.kind !== "signatures") return false;
|
|
376
|
-
const hit = form.widgetAt(sample.page.pon, sample.page.point);
|
|
377
|
-
if (!hit || hit.field.family !== "signature") return false;
|
|
378
|
-
if (signature.signatureOf(hit.field.ref)?.signed || hit.field.valueEntry.kind !== "none") return true;
|
|
379
|
-
stamp.disarm(documentId);
|
|
380
|
-
signature.placeMark({ assetId: armed.id }, { field: hit.field.ref }).catch((error) => globalThis.console?.error("[signature] placing the mark failed:", error));
|
|
381
|
-
return true;
|
|
837
|
+
api,
|
|
838
|
+
connect() {
|
|
839
|
+
signatures.connect();
|
|
840
|
+
connectSiblings(ctx, api);
|
|
382
841
|
}
|
|
383
842
|
};
|
|
384
843
|
}
|
|
385
844
|
//#endregion
|
|
386
|
-
//#region src/reducer.ts
|
|
387
|
-
const initialSignatureState = () => ({
|
|
388
|
-
snapshot: null,
|
|
389
|
-
verdicts: null,
|
|
390
|
-
target: null,
|
|
391
|
-
pending: null,
|
|
392
|
-
busy: false
|
|
393
|
-
});
|
|
394
|
-
function signatureReducer(state, action) {
|
|
395
|
-
switch (action.type) {
|
|
396
|
-
case "SNAPSHOT": return {
|
|
397
|
-
...state,
|
|
398
|
-
snapshot: action.snapshot
|
|
399
|
-
};
|
|
400
|
-
case "VERDICTS": return {
|
|
401
|
-
...state,
|
|
402
|
-
verdicts: action.verdicts
|
|
403
|
-
};
|
|
404
|
-
case "TARGET": return {
|
|
405
|
-
...state,
|
|
406
|
-
target: action.field
|
|
407
|
-
};
|
|
408
|
-
case "PENDING": return {
|
|
409
|
-
...state,
|
|
410
|
-
pending: action.pending
|
|
411
|
-
};
|
|
412
|
-
case "BUSY": return {
|
|
413
|
-
...state,
|
|
414
|
-
busy: action.busy
|
|
415
|
-
};
|
|
416
|
-
default: return state;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
//#endregion
|
|
420
845
|
//#region src/signature.plugin.ts
|
|
421
846
|
/**
|
|
422
|
-
* The signature plugin
|
|
847
|
+
* The signature plugin: the act of signing, document-scoped. It owns no
|
|
423
848
|
* mark: marks are stamp-library assets (or bytes the embedder brings), and
|
|
424
|
-
* the mark
|
|
425
|
-
* through `@embedpdf/core-signature` with the
|
|
426
|
-
* engine seals; on the cloud the server publishes the new version.
|
|
849
|
+
* the mark is the appearance the engine draws into the field. Signing goes
|
|
850
|
+
* through `@embedpdf/core-signature` with the `key` setting's signer port;
|
|
851
|
+
* the engine seals; on the cloud the server publishes the new version.
|
|
427
852
|
*
|
|
428
853
|
* With the interaction hub and the stamp plugin present, an armed mark from
|
|
429
854
|
* a `signatures` library dropped over an unsigned signature field goes into
|
|
430
|
-
* the field (sign
|
|
855
|
+
* the field (sign, visual fill or ask, by mode) instead of onto the page.
|
|
856
|
+
* `config` is the settings the app registers, over {@link SIGNATURE_DEFAULTS}.
|
|
431
857
|
*/
|
|
432
|
-
const signaturePlugin = (config
|
|
858
|
+
const signaturePlugin = (config) => (0, _embedpdf_core.definePlugin)({
|
|
433
859
|
id: "signature",
|
|
434
860
|
token: require_contract.SignatureToken,
|
|
435
861
|
scope: "document",
|
|
436
862
|
requires: [_embedpdf_plugin_form_contract.FormToken],
|
|
437
863
|
optional: [
|
|
438
|
-
|
|
864
|
+
_embedpdf_plugin_interaction_contract_host.InteractionToken,
|
|
439
865
|
_embedpdf_plugin_stamp_contract.StampToken,
|
|
440
866
|
_embedpdf_plugin_annotation_contract.AnnotationToken
|
|
441
867
|
],
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
868
|
+
state: initialSignatureState,
|
|
869
|
+
settings: {
|
|
870
|
+
defaults: require_contract.SIGNATURE_DEFAULTS,
|
|
871
|
+
registered: config,
|
|
872
|
+
whole: ["key", "trust"]
|
|
873
|
+
},
|
|
874
|
+
create: createSignatureController
|
|
875
|
+
});
|
|
876
|
+
//#endregion
|
|
877
|
+
//#region src/state.ts
|
|
878
|
+
/**
|
|
879
|
+
* The signatures page's State table as code: what `useSignatureState()`
|
|
880
|
+
* returns, and the same fields in every other framework.
|
|
881
|
+
*/
|
|
882
|
+
const NO_SIGNATURES = Object.freeze([]);
|
|
883
|
+
const signatureState = (0, _embedpdf_core.defineState)(require_contract.SignatureToken, {
|
|
884
|
+
read: (signature) => ({
|
|
885
|
+
signatures: signature.listSignatures(),
|
|
886
|
+
protection: signature.getProtection(),
|
|
887
|
+
target: signature.getTarget(),
|
|
888
|
+
busy: signature.isBusy(),
|
|
889
|
+
pending: signature.getPending(),
|
|
890
|
+
status: signature.getStatus()
|
|
891
|
+
}),
|
|
892
|
+
empty: {
|
|
893
|
+
signatures: NO_SIGNATURES,
|
|
894
|
+
protection: null,
|
|
895
|
+
target: null,
|
|
896
|
+
busy: false,
|
|
897
|
+
pending: null,
|
|
898
|
+
status: "idle"
|
|
451
899
|
}
|
|
452
900
|
});
|
|
453
901
|
//#endregion
|
|
454
902
|
exports.INITIALS_MARK_NAME = require_contract.INITIALS_MARK_NAME;
|
|
455
903
|
exports.SIGNATURES_LIBRARY_KIND = require_contract.SIGNATURES_LIBRARY_KIND;
|
|
904
|
+
exports.SIGNATURE_DEFAULTS = require_contract.SIGNATURE_DEFAULTS;
|
|
456
905
|
exports.SIGNATURE_MARK_NAME = require_contract.SIGNATURE_MARK_NAME;
|
|
457
906
|
exports.SignatureToken = require_contract.SignatureToken;
|
|
458
|
-
exports.createArmedMarkHandler = createArmedMarkHandler;
|
|
459
907
|
Object.defineProperty(exports, "createTestSigner", {
|
|
460
908
|
enumerable: true,
|
|
461
909
|
get: function() {
|
|
@@ -488,6 +936,8 @@ Object.defineProperty(exports, "remoteSigner", {
|
|
|
488
936
|
}
|
|
489
937
|
});
|
|
490
938
|
exports.signaturePlugin = signaturePlugin;
|
|
939
|
+
exports.signatureState = signatureState;
|
|
940
|
+
exports.signerRowsOf = require_contract.signerRowsOf;
|
|
491
941
|
Object.defineProperty(exports, "webCryptoSigner", {
|
|
492
942
|
enumerable: true,
|
|
493
943
|
get: function() {
|