@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.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/ports-DtICqgEf.d.cts +553 -0
- package/dist/ports-DtICqgEf.d.ts +553 -0
- package/dist/prisma/index.cjs +510 -0
- package/dist/prisma/index.cjs.map +1 -0
- package/dist/prisma/index.d.cts +48 -0
- package/dist/prisma/index.d.ts +48 -0
- package/dist/prisma/index.js +508 -0
- package/dist/prisma/index.js.map +1 -0
- package/dist/server/index.cjs +1016 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +357 -0
- package/dist/server/index.d.ts +357 -0
- package/dist/server/index.js +974 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +84 -0
- package/prisma/esign-models.prisma +166 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joseph Mills
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @josephomills/esign
|
|
2
|
+
|
|
3
|
+
Reusable, host-agnostic **document e-signing** for Next.js + Prisma/Drizzle apps.
|
|
4
|
+
|
|
5
|
+
Send a PDF to people for online signing **without them logging in**, place the
|
|
6
|
+
signature with a drag-drop designer, capture a simple electronic signature, and
|
|
7
|
+
return a **Level-1 cryptographically-sealed PDF** — the final flattened bytes are
|
|
8
|
+
SHA-256 hashed and an appended audit-certificate page records a hash-chained
|
|
9
|
+
event log. Track who has / hasn't signed, version documents and resend, detect
|
|
10
|
+
subjects added after a send (coverage), and surface stats.
|
|
11
|
+
|
|
12
|
+
The package owns the engine; the host injects everything domain-specific through
|
|
13
|
+
ports — exactly like `@firstlovecenter/milestone-grid`:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { configureEsign } from "@josephomills/esign/server";
|
|
17
|
+
import { createPrismaPersistence } from "@josephomills/esign/server/prisma"; // E1
|
|
18
|
+
|
|
19
|
+
export const esign = configureEsign({
|
|
20
|
+
persistence: createPrismaPersistence(prisma),
|
|
21
|
+
storage, // StoragePort — wraps your R2/S3
|
|
22
|
+
notifier, // NotifierPort — wraps your email/WhatsApp senders
|
|
23
|
+
auth, // AuthPort
|
|
24
|
+
subjects, // SubjectsPort — registerSubjectTypes([...]) for group targeting
|
|
25
|
+
hooks, // optional — onRequestSigned → mark a milestone-grid requirement
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
pnpm add @josephomills/esign
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then paste the model fragment into your `schema.prisma` and migrate:
|
|
36
|
+
|
|
37
|
+
```prisma
|
|
38
|
+
// node_modules/@josephomills/esign/prisma/esign-models.prisma
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`subjectId` is host-managed (= your `profileId`) and intentionally has no foreign
|
|
42
|
+
key — pass any opaque string. `scopeId` is optional (estate / country / none).
|
|
43
|
+
|
|
44
|
+
## Status
|
|
45
|
+
|
|
46
|
+
Built in phases (see the host app's plan). **E0** ships the contract: ports,
|
|
47
|
+
DTOs, the token helper, the signing state machine, and the Prisma fragment.
|
|
48
|
+
|
|
49
|
+
## License
|
|
50
|
+
|
|
51
|
+
MIT © Joseph Mills
|
|
@@ -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 };
|