@josephomills/esign 0.8.0 → 0.9.0

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.
@@ -113,6 +113,58 @@ function toErrorResponse(err) {
113
113
  { status: 500 }
114
114
  );
115
115
  }
116
+
117
+ // src/shared/fields.ts
118
+ var DATE_FORMATS = [
119
+ "DD/MM/YYYY",
120
+ "MM/DD/YYYY",
121
+ "YYYY-MM-DD",
122
+ "D MMM YYYY",
123
+ "Do MMM YYYY",
124
+ "Do MMMM YYYY",
125
+ "MMMM D, YYYY"
126
+ ];
127
+ var DEFAULT_DATE_FORMAT = "Do MMMM YYYY";
128
+ var MONTHS_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
129
+ var MONTHS_LONG = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
130
+ function ordinal(n) {
131
+ const s = ["th", "st", "nd", "rd"];
132
+ const v = n % 100;
133
+ return `${n}${s[(v - 20) % 10] ?? s[v] ?? s[0]}`;
134
+ }
135
+ function formatPickedDate(iso, format) {
136
+ const [y, m, d] = iso.split("-").map(Number);
137
+ if (!y || !m || !d) return iso;
138
+ const dd = String(d).padStart(2, "0");
139
+ const mm = String(m).padStart(2, "0");
140
+ switch (format) {
141
+ case "DD/MM/YYYY":
142
+ return `${dd}/${mm}/${y}`;
143
+ case "MM/DD/YYYY":
144
+ return `${mm}/${dd}/${y}`;
145
+ case "YYYY-MM-DD":
146
+ return `${y}-${mm}-${dd}`;
147
+ case "D MMM YYYY":
148
+ return `${d} ${MONTHS_SHORT[m - 1]} ${y}`;
149
+ case "Do MMM YYYY":
150
+ return `${ordinal(d)} ${MONTHS_SHORT[m - 1]} ${y}`;
151
+ case "Do MMMM YYYY":
152
+ return `${ordinal(d)} ${MONTHS_LONG[m - 1]} ${y}`;
153
+ case "MMMM D, YYYY":
154
+ return `${MONTHS_LONG[m - 1]} ${d}, ${y}`;
155
+ }
156
+ }
157
+ var DATE_FORMAT_LABELS = {
158
+ "DD/MM/YYYY": "02/10/2026",
159
+ "MM/DD/YYYY": "10/02/2026",
160
+ "YYYY-MM-DD": "2026-10-02",
161
+ "D MMM YYYY": "2 Oct 2026",
162
+ "Do MMM YYYY": "2nd Oct 2026",
163
+ "Do MMMM YYYY": "2nd October 2026",
164
+ "MMMM D, YYYY": "October 2, 2026"
165
+ };
166
+
167
+ // src/server/types.ts
116
168
  var ESIGN_CHANNELS = [
117
169
  "EMAIL",
118
170
  "TELEGRAM",
@@ -128,11 +180,18 @@ var placementSchema = zod.z.object({
128
180
  w: zod.z.number().min(0.01).max(1),
129
181
  h: zod.z.number().min(0.01).max(1)
130
182
  }).strict();
131
- var signatureFieldSchema = placementSchema.extend({
183
+ var isoDate = zod.z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
184
+ var documentFieldSchema = placementSchema.extend({
132
185
  id: zod.z.string().min(1).max(64),
133
- label: zod.z.string().trim().max(80)
186
+ label: zod.z.string().trim().max(80),
187
+ type: zod.z.enum(["signature", "date"]).default("signature"),
188
+ dateFormat: zod.z.enum(DATE_FORMATS).optional(),
189
+ minDate: isoDate.nullable().optional()
190
+ }).strict();
191
+ var signatureFieldSchema = documentFieldSchema;
192
+ var placementsSchema = zod.z.array(documentFieldSchema).min(1).max(20).refine((fields) => fields.some((f) => f.type === "signature"), {
193
+ message: "A document needs at least one signature field."
134
194
  });
135
- var placementsSchema = zod.z.array(signatureFieldSchema).min(1).max(20);
136
195
  function addressFor(recipient, channel) {
137
196
  switch (channel) {
138
197
  case "EMAIL":
@@ -162,7 +221,8 @@ var submitSignatureSchema = zod.z.object({
162
221
  signaturePng: zod.z.string().min(1).max(15e5),
163
222
  // ~1MB cap on the data URL
164
223
  signerName: zod.z.string().trim().min(1).max(200),
165
- consent: zod.z.literal(true)
224
+ consent: zod.z.literal(true),
225
+ dateValues: zod.z.record(zod.z.string().min(1).max(64), isoDate).optional()
166
226
  }).strict();
167
227
  var declineSchema = zod.z.object({ reason: zod.z.string().trim().max(500).optional() }).strict();
168
228
  var ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
@@ -652,15 +712,35 @@ async function sealPdf(input) {
652
712
  const png = await pdf.embedPng(input.signaturePng);
653
713
  const font = await pdf.embedFont(pdfLib.StandardFonts.Helvetica);
654
714
  const caption = `Signed by ${input.signerName} \xB7 ${input.signedAt}${input.signerIp ? ` \xB7 IP ${input.signerIp}` : ""}`;
655
- input.placements.forEach((placement, i) => {
656
- const pageIndex = Math.min(Math.max(placement.page - 1, 0), pages.length - 1);
715
+ let captionDrawn = false;
716
+ input.fields.forEach((field) => {
717
+ const pageIndex = Math.min(Math.max(field.page - 1, 0), pages.length - 1);
657
718
  const page = pages[pageIndex];
658
719
  const pageW = page.getWidth();
659
720
  const pageH = page.getHeight();
660
- const boxX = placement.x * pageW;
661
- const boxW = placement.w * pageW;
662
- const boxH = placement.h * pageH;
663
- const boxBottomY = pageH * (1 - placement.y - placement.h);
721
+ const boxX = field.x * pageW;
722
+ const boxW = field.w * pageW;
723
+ const boxH = field.h * pageH;
724
+ const boxBottomY = pageH * (1 - field.y - field.h);
725
+ if (field.type === "date") {
726
+ const iso = input.dateValues[field.id];
727
+ if (!iso) return;
728
+ const text = formatPickedDate(iso, field.dateFormat ?? DEFAULT_DATE_FORMAT);
729
+ let size = Math.min(boxH * 0.62, 15);
730
+ let textW = font.widthOfTextAtSize(text, size);
731
+ if (textW > boxW && textW > 0) {
732
+ size *= boxW / textW;
733
+ textW = font.widthOfTextAtSize(text, size);
734
+ }
735
+ page.drawText(text, {
736
+ x: boxX + Math.max(0, (boxW - textW) / 2),
737
+ y: boxBottomY + (boxH - size) / 2 + size * 0.22,
738
+ size,
739
+ font,
740
+ color: pdfLib.rgb(0.1, 0.1, 0.12)
741
+ });
742
+ return;
743
+ }
664
744
  const scale = Math.min(boxW / png.width, boxH / png.height);
665
745
  const drawW = png.width * scale;
666
746
  const drawH = png.height * scale;
@@ -670,7 +750,8 @@ async function sealPdf(input) {
670
750
  width: drawW,
671
751
  height: drawH
672
752
  });
673
- if (i === 0) {
753
+ if (!captionDrawn) {
754
+ captionDrawn = true;
674
755
  page.drawText(caption, {
675
756
  x: boxX,
676
757
  y: Math.max(boxBottomY - 11, 4),
@@ -786,6 +867,18 @@ async function submitSignature(deps, input) {
786
867
  const src = await storage.get(version.sourceObjectKey);
787
868
  if (!src) throw new EsignError("INTERNAL", "Source document missing.");
788
869
  const document = await persistence.getDocument(request.documentId);
870
+ const dateValues = input.dateValues ?? {};
871
+ const createdFloor = document?.createdAt ? document.createdAt.slice(0, 10) : null;
872
+ for (const field of version.placements) {
873
+ if (field.type !== "date") continue;
874
+ const value = dateValues[field.id];
875
+ const name = field.label || "date";
876
+ if (!value) throw new EsignError("INVALID_INPUT", `Please fill in the "${name}" date.`);
877
+ const floor = field.minDate ?? createdFloor;
878
+ if (floor && value < floor) {
879
+ throw new EsignError("INVALID_INPUT", `The "${name}" date can't be before ${floor}.`);
880
+ }
881
+ }
789
882
  const existing = (await persistence.listAuditEvents(request.id)).map(toLink);
790
883
  const nowIso = now.toISOString();
791
884
  const consented = await recordEvent(persistence, request.id, existing.at(-1) ?? null, {
@@ -800,7 +893,8 @@ async function submitSignature(deps, input) {
800
893
  const seal = await sealPdf({
801
894
  sourcePdf: src.bytes,
802
895
  signaturePng: input.signaturePng,
803
- placements: version.placements,
896
+ fields: version.placements,
897
+ dateValues,
804
898
  signerName: input.signerName,
805
899
  signedAt: nowIso,
806
900
  signerIp: input.ip,
@@ -921,6 +1015,7 @@ function createSignActionsRoute(deps) {
921
1015
  token,
922
1016
  signaturePng: pngFromDataUrl(body.signaturePng),
923
1017
  signerName: body.signerName,
1018
+ dateValues: body.dateValues,
924
1019
  ip: clientIp(req),
925
1020
  userAgent: req.headers.get("user-agent")
926
1021
  });
@@ -1014,6 +1109,9 @@ function registerSubjectTypes(types) {
1014
1109
  }
1015
1110
 
1016
1111
  exports.ACTIVE_STATUSES = ACTIVE_STATUSES;
1112
+ exports.DATE_FORMATS = DATE_FORMATS;
1113
+ exports.DATE_FORMAT_LABELS = DATE_FORMAT_LABELS;
1114
+ exports.DEFAULT_DATE_FORMAT = DEFAULT_DATE_FORMAT;
1017
1115
  exports.ESIGN_CHANNELS = ESIGN_CHANNELS;
1018
1116
  exports.ESIGN_STATUSES = ESIGN_STATUSES;
1019
1117
  exports.EsignError = EsignError;
@@ -1034,7 +1132,9 @@ exports.declineRequest = declineRequest;
1034
1132
  exports.declineSchema = declineSchema;
1035
1133
  exports.deleteDocument = deleteDocument;
1036
1134
  exports.documentCoverage = documentCoverage;
1135
+ exports.documentFieldSchema = documentFieldSchema;
1037
1136
  exports.esignChannelSchema = esignChannelSchema;
1137
+ exports.formatPickedDate = formatPickedDate;
1038
1138
  exports.getSigningView = getSigningView;
1039
1139
  exports.isActive = isActive;
1040
1140
  exports.isTerminal = isTerminal;