@josephomills/esign 0.2.2

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.
@@ -0,0 +1,553 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Lifecycle of a single signing request. PENDING/VIEWED are the only active
5
+ * states; SIGNED/DECLINED/EXPIRED/REVOKED are terminal. The submit handler is
6
+ * idempotent, so a no-op transition to the same state is always allowed.
7
+ */
8
+ type EsignStatus = "PENDING" | "VIEWED" | "SIGNED" | "DECLINED" | "EXPIRED" | "REVOKED";
9
+ declare const ESIGN_STATUSES: readonly EsignStatus[];
10
+ /** Active (still-actionable) states — eligible for view/sign/decline. */
11
+ declare const ACTIVE_STATUSES: readonly EsignStatus[];
12
+ /** Terminal states — no further transitions. */
13
+ declare const TERMINAL_STATUSES: readonly EsignStatus[];
14
+ declare function isActive(status: EsignStatus): boolean;
15
+ declare function isTerminal(status: EsignStatus): boolean;
16
+ /** Whether `from → to` is a legal move (a same-state no-op counts as legal). */
17
+ declare function canTransition(from: EsignStatus, to: EsignStatus): boolean;
18
+ /** Throw `EsignError('CONFLICT')` on an illegal transition. */
19
+ declare function assertTransition(from: EsignStatus, to: EsignStatus): void;
20
+
21
+ type EsignChannel = "EMAIL" | "TELEGRAM" | "SMS" | "WHATSAPP";
22
+ declare const ESIGN_CHANNELS: readonly EsignChannel[];
23
+ declare const esignChannelSchema: z.ZodEnum<["EMAIL", "TELEGRAM", "SMS", "WHATSAPP"]>;
24
+ interface Placement {
25
+ page: number;
26
+ x: number;
27
+ y: number;
28
+ w: number;
29
+ h: number;
30
+ }
31
+ declare const placementSchema: z.ZodObject<{
32
+ page: z.ZodNumber;
33
+ x: z.ZodNumber;
34
+ y: z.ZodNumber;
35
+ w: z.ZodNumber;
36
+ h: z.ZodNumber;
37
+ }, "strict", z.ZodTypeAny, {
38
+ page: number;
39
+ x: number;
40
+ y: number;
41
+ w: number;
42
+ h: number;
43
+ }, {
44
+ page: number;
45
+ x: number;
46
+ y: number;
47
+ w: number;
48
+ h: number;
49
+ }>;
50
+ /** A resolved recipient — the only thing the fan-out needs. subjectId = the host's profileId. */
51
+ interface Recipient {
52
+ name: string;
53
+ email?: string | null;
54
+ /** Phone for SMS (and WhatsApp when `whatsapp` is absent). */
55
+ phone?: string | null;
56
+ whatsapp?: string | null;
57
+ subjectType: string;
58
+ subjectId: string;
59
+ /** Snapshot of the targeted classification, persisted as SigningRequest.subjectGroup. */
60
+ group?: string | null;
61
+ }
62
+ /** A subject row rendered by the targeting picker. */
63
+ interface SubjectSummary {
64
+ subjectType: string;
65
+ subjectId: string;
66
+ label: string;
67
+ sublabel?: string;
68
+ group?: string | null;
69
+ groupLabel?: string | null;
70
+ eligible: boolean;
71
+ ineligibleReason?: string;
72
+ }
73
+ /** A subject that could not be sent to at resolve time. */
74
+ interface SkippedSubject {
75
+ subjectType: string;
76
+ subjectId: string;
77
+ label: string;
78
+ reason: string;
79
+ }
80
+ /** A declarative filter facet the picker renders as a control. */
81
+ interface SubjectFilterField {
82
+ key: string;
83
+ label: string;
84
+ kind: "enum" | "boolean" | "text" | "scope";
85
+ options?: {
86
+ value: string;
87
+ label: string;
88
+ }[];
89
+ }
90
+ type SubjectFilter = Record<string, string | boolean | string[] | undefined>;
91
+ /**
92
+ * How a target is expressed. "all" sends to the whole group (optionally a
93
+ * classification + filters); "ids" sends to an explicit subset.
94
+ */
95
+ type SubjectSelection = {
96
+ mode: "all";
97
+ type: string;
98
+ group?: string;
99
+ filter?: SubjectFilter;
100
+ } | {
101
+ mode: "ids";
102
+ type: string;
103
+ subjectIds: string[];
104
+ };
105
+ declare const subjectSelectionSchema: z.ZodType<SubjectSelection>;
106
+ /** Restrict a resend to a subset of the live group, diffed against existing requests. */
107
+ type ResendPolicy = "all" | "outstanding" | "olderVersion" | "uncovered";
108
+ /** Whether a green-check requires the current version or any version. */
109
+ type VersionPolicy = "current" | "any";
110
+ interface SigningDocumentDTO {
111
+ id: string;
112
+ scopeId: string | null;
113
+ documentType: string | null;
114
+ title: string;
115
+ audience: SubjectSelection | null;
116
+ currentVersionId: string | null;
117
+ createdById: string | null;
118
+ createdAt: string;
119
+ updatedAt: string;
120
+ }
121
+ interface SigningDocumentVersionDTO {
122
+ id: string;
123
+ documentId: string;
124
+ version: number;
125
+ sourceObjectKey: string;
126
+ sourceSha256: string;
127
+ placement: Placement;
128
+ changeNote: string | null;
129
+ createdById: string | null;
130
+ createdAt: string;
131
+ }
132
+ interface SigningCampaignDTO {
133
+ id: string;
134
+ documentId: string;
135
+ documentVersionId: string;
136
+ scopeId: string | null;
137
+ note: string | null;
138
+ emailReceipt: boolean;
139
+ targeting: SubjectSelection;
140
+ expiresAt: string;
141
+ createdById: string | null;
142
+ createdAt: string;
143
+ }
144
+ interface SigningRequestDTO {
145
+ id: string;
146
+ campaignId: string;
147
+ documentId: string;
148
+ documentVersionId: string;
149
+ scopeId: string | null;
150
+ subjectType: string;
151
+ subjectId: string;
152
+ subjectGroup: string | null;
153
+ recipientName: string;
154
+ recipientEmail: string | null;
155
+ recipientWhatsapp: string | null;
156
+ channels: EsignChannel[];
157
+ status: EsignStatus;
158
+ viewedAt: string | null;
159
+ signedAt: string | null;
160
+ declinedAt: string | null;
161
+ expiresAt: string;
162
+ revokedAt: string | null;
163
+ sealedObjectKey: string | null;
164
+ sealedSha256: string | null;
165
+ createdAt: string;
166
+ }
167
+ interface SigningAuditEventDTO {
168
+ id: string;
169
+ requestId: string;
170
+ seq: number;
171
+ type: string;
172
+ payload: unknown;
173
+ prevHash: string | null;
174
+ hash: string;
175
+ occurredAt: string;
176
+ }
177
+ type SubjectSigningState = "NONE" | "PENDING" | "VIEWED" | "SIGNED" | "DECLINED" | "NEEDS_RESIGN";
178
+ interface SubjectSigningStatus {
179
+ state: SubjectSigningState;
180
+ signedAt: string | null;
181
+ signedVersion: number | null;
182
+ requestId: string | null;
183
+ sealedObjectKey: string | null;
184
+ }
185
+ type StatusCountMap = Record<EsignStatus, number>;
186
+ interface OutstandingRequest {
187
+ id: string;
188
+ campaignId: string;
189
+ documentId: string;
190
+ subjectType: string;
191
+ subjectId: string;
192
+ recipientName: string;
193
+ channels: EsignChannel[];
194
+ status: Extract<EsignStatus, "PENDING" | "VIEWED">;
195
+ viewedAt: string | null;
196
+ createdAt: string;
197
+ expiresAt: string;
198
+ }
199
+ interface GroupBreakdown {
200
+ group: string | null;
201
+ counts: StatusCountMap;
202
+ total: number;
203
+ }
204
+ interface CampaignStatsRow {
205
+ campaignId: string;
206
+ counts: StatusCountMap;
207
+ total: number;
208
+ viewedNotSigned: number;
209
+ completionRate: number;
210
+ avgTimeToSignMs: number | null;
211
+ byGroup: GroupBreakdown[];
212
+ outstanding: OutstandingRequest[];
213
+ }
214
+ interface DocumentStatsRow {
215
+ documentId: string;
216
+ counts: StatusCountMap;
217
+ total: number;
218
+ completionRate: number;
219
+ byGroup: GroupBreakdown[];
220
+ bySubjectType: {
221
+ subjectType: string;
222
+ counts: StatusCountMap;
223
+ total: number;
224
+ }[];
225
+ }
226
+ interface ScopeStatsRow {
227
+ campaignsSent: number;
228
+ requestsSent: number;
229
+ counts: StatusCountMap;
230
+ completionRate: number;
231
+ bySubjectType: {
232
+ subjectType: string;
233
+ counts: StatusCountMap;
234
+ total: number;
235
+ }[];
236
+ oldestOutstanding: OutstandingRequest[];
237
+ }
238
+ interface StatsRange {
239
+ from?: Date;
240
+ to?: Date;
241
+ }
242
+ /** documentCoverage() result — re-derived from the live group every call. */
243
+ interface CoverageReport {
244
+ documentId: string;
245
+ totalNow: number;
246
+ signed: number;
247
+ outstanding: number;
248
+ needsResign: number;
249
+ uncovered: SubjectSummary[];
250
+ departed: {
251
+ subjectType: string;
252
+ subjectId: string;
253
+ }[];
254
+ }
255
+ /** A drawn or typed signature, as a PNG data URL produced by the signer UI. */
256
+ declare const submitSignatureSchema: z.ZodObject<{
257
+ signaturePng: z.ZodString;
258
+ signerName: z.ZodString;
259
+ consent: z.ZodLiteral<true>;
260
+ }, "strict", z.ZodTypeAny, {
261
+ signaturePng: string;
262
+ signerName: string;
263
+ consent: true;
264
+ }, {
265
+ signaturePng: string;
266
+ signerName: string;
267
+ consent: true;
268
+ }>;
269
+ type SubmitSignatureInput = z.infer<typeof submitSignatureSchema>;
270
+ declare const declineSchema: z.ZodObject<{
271
+ reason: z.ZodOptional<z.ZodString>;
272
+ }, "strict", z.ZodTypeAny, {
273
+ reason?: string | undefined;
274
+ }, {
275
+ reason?: string | undefined;
276
+ }>;
277
+ type DeclineInput = z.infer<typeof declineSchema>;
278
+
279
+ interface AuthPort<S> {
280
+ /** Resolve the request's principal, or throw EsignError('UNAUTHENTICATED'). */
281
+ requireAuth(req: Request): Promise<S>;
282
+ /** True when the scope may manage documents / send campaigns. */
283
+ isAdmin(scope: S): boolean;
284
+ /** Opaque user id used to attribute documents, versions, campaigns. */
285
+ userId(scope: S): string;
286
+ /** Optional opaque scope key (estate id / country id) for row scoping. */
287
+ scopeId?(scope: S): string | null;
288
+ }
289
+ interface StoragePort {
290
+ /** Short-lived signed PUT URL for the browser to upload a source PDF. */
291
+ presignPut(input: {
292
+ objectKey: string;
293
+ contentType: string;
294
+ ttlS: number;
295
+ }): Promise<{
296
+ uploadUrl: string;
297
+ }>;
298
+ /** Read an object's bytes (source PDF for sealing / serving). null = 404. */
299
+ get(objectKey: string): Promise<{
300
+ bytes: Uint8Array;
301
+ contentType?: string;
302
+ } | null>;
303
+ /** Write bytes server-side (the sealed PDF). */
304
+ put(input: {
305
+ objectKey: string;
306
+ bytes: Uint8Array;
307
+ contentType: string;
308
+ }): Promise<void>;
309
+ /** Best-effort delete. */
310
+ delete(objectKey: string): Promise<void>;
311
+ /** HMAC-stamp a fresh source-upload key so a presigned key can't be reused elsewhere. */
312
+ stampKey(parts: {
313
+ scopeId?: string | null;
314
+ documentId: string;
315
+ contentType: string;
316
+ }): string;
317
+ /** Verify a stamped key matches the expected document. */
318
+ verifyKey(stampedKey: string, parts: {
319
+ documentId: string;
320
+ }): boolean;
321
+ /** Deterministic key for a sealed request PDF. */
322
+ sealedKey(parts: {
323
+ scopeId?: string | null;
324
+ campaignId: string;
325
+ requestId: string;
326
+ }): string;
327
+ /** Optional upper bound for an uploaded source PDF (bytes). */
328
+ maxBytes?(): number;
329
+ }
330
+ interface NotifierAttachment {
331
+ filename: string;
332
+ content: Uint8Array;
333
+ contentType: string;
334
+ }
335
+ interface NotifierPort {
336
+ send(input: {
337
+ channel: EsignChannel;
338
+ recipient: string;
339
+ subject: string;
340
+ body: string;
341
+ actionUrl?: string;
342
+ idempotencyKey: string;
343
+ attachments?: NotifierAttachment[];
344
+ }): Promise<{
345
+ ok: boolean;
346
+ error?: string;
347
+ }>;
348
+ /** Channels with live credentials — the targeting UI offers only these. */
349
+ configuredChannels(): EsignChannel[];
350
+ }
351
+ interface SubjectTypeDescriptor {
352
+ type: string;
353
+ label: string;
354
+ pluralLabel?: string;
355
+ /** The primary classification facet (e.g. missionary support tier, employment type). */
356
+ groupField?: SubjectFilterField;
357
+ /** Secondary facets rendered as filter controls. */
358
+ filterFields?: SubjectFilterField[];
359
+ /** Paginated, channel-aware enumeration for the picker. */
360
+ listSubjects(args: {
361
+ scopeId?: string | null;
362
+ group?: string;
363
+ filter?: SubjectFilter;
364
+ channels?: EsignChannel[];
365
+ cursor?: string;
366
+ limit?: number;
367
+ }): Promise<{
368
+ subjects: SubjectSummary[];
369
+ nextCursor?: string;
370
+ total?: number;
371
+ }>;
372
+ /** Lean id-only enumeration for coverage diffs. */
373
+ listSubjectIds(args: {
374
+ scopeId?: string | null;
375
+ selection: SubjectSelection;
376
+ }): Promise<string[]>;
377
+ /** Counts for the review step without materializing recipients. */
378
+ countSubjects(args: {
379
+ scopeId?: string | null;
380
+ selection: SubjectSelection;
381
+ channels?: EsignChannel[];
382
+ }): Promise<{
383
+ total: number;
384
+ eligible: number;
385
+ ineligible: number;
386
+ }>;
387
+ /** Authoritative send-time resolution: who to send to, and who was skipped. */
388
+ resolveRecipients(args: {
389
+ scopeId?: string | null;
390
+ selection: SubjectSelection;
391
+ channels?: EsignChannel[];
392
+ }): Promise<{
393
+ recipients: Recipient[];
394
+ skipped: SkippedSubject[];
395
+ }>;
396
+ }
397
+ interface SubjectsPort {
398
+ types: SubjectTypeDescriptor[];
399
+ get(type: string): SubjectTypeDescriptor | undefined;
400
+ }
401
+ interface NewRequestRow {
402
+ /** Minted by the engine (createRequests returns void), so the fan-out knows it. */
403
+ id: string;
404
+ campaignId: string;
405
+ documentId: string;
406
+ documentVersionId: string;
407
+ scopeId: string | null;
408
+ subjectType: string;
409
+ subjectId: string;
410
+ subjectGroup: string | null;
411
+ recipientName: string;
412
+ recipientEmail: string | null;
413
+ recipientWhatsapp: string | null;
414
+ channels: EsignChannel[];
415
+ tokenHash: string;
416
+ expiresAt: Date;
417
+ }
418
+ interface NewAuditEventRow {
419
+ requestId: string;
420
+ seq: number;
421
+ type: string;
422
+ payload: unknown;
423
+ prevHash: string | null;
424
+ hash: string;
425
+ }
426
+ interface RequestSummary {
427
+ subjectId: string;
428
+ status: EsignStatus;
429
+ version: number;
430
+ signedAt: string | null;
431
+ }
432
+ interface PersistencePort {
433
+ createDocument(input: {
434
+ scopeId: string | null;
435
+ documentType: string | null;
436
+ title: string;
437
+ audience: SubjectSelection | null;
438
+ createdById: string | null;
439
+ }): Promise<SigningDocumentDTO>;
440
+ getDocument(id: string): Promise<SigningDocumentDTO | null>;
441
+ updateDocument(id: string, patch: Partial<Pick<SigningDocumentDTO, "title" | "audience" | "currentVersionId">>): Promise<SigningDocumentDTO>;
442
+ listDocuments(opts: {
443
+ scopeId?: string | null;
444
+ documentType?: string;
445
+ }): Promise<SigningDocumentDTO[]>;
446
+ createVersion(input: {
447
+ documentId: string;
448
+ version: number;
449
+ sourceObjectKey: string;
450
+ sourceSha256: string;
451
+ placement: Placement;
452
+ changeNote: string | null;
453
+ createdById: string | null;
454
+ }): Promise<SigningDocumentVersionDTO>;
455
+ getVersion(id: string): Promise<SigningDocumentVersionDTO | null>;
456
+ listVersions(documentId: string): Promise<SigningDocumentVersionDTO[]>;
457
+ latestVersion(documentId: string): Promise<SigningDocumentVersionDTO | null>;
458
+ createCampaign(input: {
459
+ documentId: string;
460
+ documentVersionId: string;
461
+ scopeId: string | null;
462
+ note: string | null;
463
+ emailReceipt: boolean;
464
+ targeting: SubjectSelection;
465
+ expiresAt: Date;
466
+ createdById: string | null;
467
+ }): Promise<SigningCampaignDTO>;
468
+ getCampaign(id: string): Promise<SigningCampaignDTO | null>;
469
+ listCampaigns(documentId: string): Promise<SigningCampaignDTO[]>;
470
+ createRequests(rows: NewRequestRow[]): Promise<void>;
471
+ getRequest(id: string): Promise<SigningRequestDTO | null>;
472
+ findRequestByTokenHash(tokenHash: string): Promise<SigningRequestDTO | null>;
473
+ updateRequest(id: string, patch: Partial<{
474
+ status: EsignStatus;
475
+ viewedAt: Date;
476
+ signedAt: Date;
477
+ declinedAt: Date;
478
+ declineReason: string;
479
+ revokedAt: Date;
480
+ signerName: string;
481
+ signerIp: string;
482
+ signerUserAgent: string;
483
+ sealedObjectKey: string;
484
+ sealedSha256: string;
485
+ finalAuditHash: string;
486
+ }>): Promise<SigningRequestDTO>;
487
+ listRequests(opts: {
488
+ campaignId?: string;
489
+ documentId?: string;
490
+ subjectType?: string;
491
+ subjectId?: string;
492
+ }): Promise<SigningRequestDTO[]>;
493
+ /** Latest request per subject for a document — powers coverage + version-aware status. */
494
+ requestSummaryBySubject(documentId: string): Promise<RequestSummary[]>;
495
+ appendAuditEvent(row: NewAuditEventRow): Promise<SigningAuditEventDTO>;
496
+ listAuditEvents(requestId: string): Promise<SigningAuditEventDTO[]>;
497
+ lastAuditHash(requestId: string): Promise<{
498
+ seq: number;
499
+ hash: string;
500
+ } | null>;
501
+ subjectSigningStatus(args: {
502
+ subjectType: string;
503
+ subjectId: string;
504
+ documentId?: string;
505
+ documentType?: string;
506
+ versionPolicy: VersionPolicy;
507
+ }): Promise<SubjectSigningStatus>;
508
+ campaignStats(campaignId: string): Promise<CampaignStatsRow>;
509
+ documentStats(documentId: string): Promise<DocumentStatsRow>;
510
+ scopeStats(scopeId: string | null, range?: StatsRange): Promise<ScopeStatsRow>;
511
+ outstandingRequests(filter: {
512
+ scopeId?: string | null;
513
+ documentId?: string;
514
+ campaignId?: string;
515
+ now: Date;
516
+ limit?: number;
517
+ }): Promise<OutstandingRequest[]>;
518
+ }
519
+ interface HooksPort {
520
+ onRequestViewed?(req: SigningRequestDTO): Promise<void> | void;
521
+ onRequestSigned?(event: {
522
+ subjectType: string;
523
+ subjectId: string;
524
+ documentId: string;
525
+ documentType: string | null;
526
+ requestId: string;
527
+ sealedObjectKey: string;
528
+ signedAt: string;
529
+ }): Promise<void> | void;
530
+ onRequestDeclined?(req: SigningRequestDTO): Promise<void> | void;
531
+ }
532
+ interface Clock {
533
+ now(): Date;
534
+ }
535
+ interface EsignConfig<S> {
536
+ persistence: PersistencePort;
537
+ storage: StoragePort;
538
+ notifier: NotifierPort;
539
+ auth: AuthPort<S>;
540
+ subjects: SubjectsPort;
541
+ hooks?: HooksPort;
542
+ clock?: Clock;
543
+ /** Absolute origin used to build the `/sign/<token>` link, e.g. https://estates.app. */
544
+ baseUrl: string;
545
+ /** Base path the host mounts the package routes under. Default `/api/esign`. */
546
+ routeBasePath?: string;
547
+ /** TTL (seconds) for presigned source-PDF uploads. Default 900. */
548
+ presignTtlS?: number;
549
+ /** Default signing-link lifetime in days. Default 30. */
550
+ defaultExpiryDays?: number;
551
+ }
552
+
553
+ export { submitSignatureSchema as $, ACTIVE_STATUSES as A, type StatusCountMap as B, type Clock as C, type DocumentStatsRow as D, type EsignChannel as E, type SubjectFilter as F, type GroupBreakdown as G, type HooksPort as H, type SubjectFilterField as I, type SubjectSigningState as J, type SubjectSummary as K, type SubmitSignatureInput as L, assertTransition as M, type NotifierPort as N, type OutstandingRequest as O, type PersistencePort as P, canTransition as Q, type ResendPolicy as R, type SubjectSelection as S, TERMINAL_STATUSES as T, declineSchema as U, type VersionPolicy as V, esignChannelSchema as W, isActive as X, isTerminal as Y, placementSchema as Z, subjectSelectionSchema as _, type SubjectsPort as a, type SkippedSubject as b, type Placement as c, type StoragePort as d, type SigningDocumentVersionDTO as e, type SigningDocumentDTO as f, type SigningRequestDTO as g, type EsignConfig as h, type SubjectSigningStatus as i, type CoverageReport as j, type CampaignStatsRow as k, type StatsRange as l, type ScopeStatsRow as m, type SubjectTypeDescriptor as n, type AuthPort as o, type DeclineInput as p, ESIGN_CHANNELS as q, ESIGN_STATUSES as r, type EsignStatus as s, type NewAuditEventRow as t, type NewRequestRow as u, type NotifierAttachment as v, type Recipient as w, type RequestSummary as x, type SigningAuditEventDTO as y, type SigningCampaignDTO as z };