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