@vicacha-devs/strapi-plugin-certificates 0.0.0-stage → 1.1.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.
Files changed (70) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +17 -2
  3. package/dist/admin/CertificatesPage-CFDIEBN_.js +783 -0
  4. package/dist/admin/CertificatesPage-DeGmoX7h.cjs +783 -0
  5. package/dist/admin/SettingsPage-CsWFO1XB.js +447 -0
  6. package/dist/admin/SettingsPage-D7Ob1gV0.cjs +447 -0
  7. package/dist/admin/ar-Bv3T0_7C.cjs +258 -0
  8. package/dist/admin/ar-CDFHNnFd.js +258 -0
  9. package/dist/admin/de-BjMxn-Lt.js +258 -0
  10. package/dist/admin/de-DB5eW-yI.cjs +258 -0
  11. package/dist/admin/en-BI5Z3lPW.js +258 -0
  12. package/dist/admin/en-CsYV0VDf.cjs +258 -0
  13. package/dist/admin/es-B7uy9a-2.js +258 -0
  14. package/dist/admin/es-DEp3j3kD.cjs +258 -0
  15. package/dist/admin/fr-D7y038Pm.cjs +258 -0
  16. package/dist/admin/fr-DxbtdEIA.js +258 -0
  17. package/dist/admin/id-DfW4ARVm.js +258 -0
  18. package/dist/admin/id-Wuw9jl-Y.cjs +258 -0
  19. package/dist/admin/index-BQdS3gc3.cjs +707 -0
  20. package/dist/admin/index-BYO34da4.js +708 -0
  21. package/dist/admin/index.cjs +5 -0
  22. package/dist/admin/index.js +5 -0
  23. package/dist/admin/index.mjs +5 -0
  24. package/dist/admin/pt-C0QaikKS.js +258 -0
  25. package/dist/admin/pt-Dj2oc3Ga.cjs +258 -0
  26. package/dist/admin/src/components/IssueCertificateModal.d.ts +9 -0
  27. package/dist/admin/src/components/SettingsTabHelpers.d.ts +13 -0
  28. package/dist/admin/src/index.d.ts +11 -0
  29. package/dist/admin/src/pages/CertificatesPage.d.ts +3 -0
  30. package/dist/admin/src/pages/SettingsPage.d.ts +3 -0
  31. package/dist/admin/src/translations/cmLabels.d.ts +1 -0
  32. package/dist/admin/src/translations/cmPlaceholders.d.ts +1 -0
  33. package/dist/admin/tr-BbeagXRY.js +258 -0
  34. package/dist/admin/tr-DlGl_XMF.cjs +258 -0
  35. package/dist/admin/ur-Bwn4TrjF.js +258 -0
  36. package/dist/admin/ur-qIlUtgqb.cjs +258 -0
  37. package/dist/admin/zh-Cmg5mEvP.js +258 -0
  38. package/dist/admin/zh-D-3QDsuQ.cjs +258 -0
  39. package/dist/package.json +3 -0
  40. package/dist/server/index.cjs +2282 -0
  41. package/dist/server/index.js +2282 -0
  42. package/dist/server/index.mjs +2282 -0
  43. package/dist/server/src/bootstrap.d.ts +5 -0
  44. package/dist/server/src/content-types/certificate/schema.json.d.ts +165 -0
  45. package/dist/server/src/content-types/index.d.ts +277 -0
  46. package/dist/server/src/content-types/template/schema.json.d.ts +118 -0
  47. package/dist/server/src/controllers/admin.d.ts +31 -0
  48. package/dist/server/src/controllers/content-api.d.ts +28 -0
  49. package/dist/server/src/controllers/index.d.ts +30 -0
  50. package/dist/server/src/index.d.ts +413 -0
  51. package/dist/server/src/register.d.ts +5 -0
  52. package/dist/server/src/routes/admin.d.ts +12 -0
  53. package/dist/server/src/routes/content-api.d.ts +12 -0
  54. package/dist/server/src/routes/index.d.ts +25 -0
  55. package/dist/server/src/services/docusign.d.ts +76 -0
  56. package/dist/server/src/services/index.d.ts +76 -0
  57. package/dist/server/src/services/issuance.d.ts +54 -0
  58. package/dist/server/src/services/issuanceHelpers.d.ts +47 -0
  59. package/dist/server/src/services/settings.d.ts +17 -0
  60. package/dist/server/src/services/urlHelpers.d.ts +2 -0
  61. package/dist/shared/calendarSystems.d.ts +3 -0
  62. package/dist/shared/cryptoEngine.d.ts +27 -0
  63. package/dist/shared/documentRenderer.d.ts +12 -0
  64. package/dist/shared/index.cjs +636 -0
  65. package/dist/shared/index.d.ts +5 -0
  66. package/dist/shared/index.js +613 -0
  67. package/dist/shared/index.mjs +613 -0
  68. package/dist/shared/pluginId.d.ts +2 -0
  69. package/dist/shared/types.d.ts +194 -0
  70. package/package.json +112 -3
@@ -0,0 +1,636 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const shared = require("@vicacha-devs/strapi-plugin-calendar/shared");
4
+ const node_crypto = require("node:crypto");
5
+ const PLUGIN_ID = "certificates";
6
+ function formatSecondaryCalendars(date, systems, locale = "en") {
7
+ const result = {};
8
+ const dateStr = date.toISOString().slice(0, 10);
9
+ for (const sys of systems) {
10
+ if (!sys || !sys.enabled || !sys.id) continue;
11
+ const formatted = shared.formatDateInCalendarSystem(dateStr, sys, locale, {
12
+ compact: false
13
+ });
14
+ const compact = shared.formatDateInCalendarSystem(dateStr, sys, locale, {
15
+ compact: true
16
+ });
17
+ const parts = shared.extractCalendarParts(
18
+ date,
19
+ sys.intlCalendar,
20
+ sys.dayOffset ?? 0
21
+ );
22
+ if (formatted) {
23
+ result[sys.id] = {
24
+ calendarId: sys.id,
25
+ label: sys.label || sys.id,
26
+ formatted,
27
+ compact: compact || formatted,
28
+ intlCalendar: sys.intlCalendar,
29
+ year: parts?.year,
30
+ month: parts?.month,
31
+ day: parts?.day,
32
+ isOverridden: false
33
+ };
34
+ }
35
+ }
36
+ return result;
37
+ }
38
+ function computePublicKeyFingerprint(publicKeyPem) {
39
+ const normalized = String(publicKeyPem || "").trim();
40
+ if (!normalized) return "";
41
+ const digest = node_crypto.createHash("sha256").update(normalized, "utf8").digest("hex");
42
+ return `SHA256:${digest.slice(0, 32).toUpperCase()}`;
43
+ }
44
+ function generateEd25519KeyPair() {
45
+ const { publicKey, privateKey } = node_crypto.generateKeyPairSync("ed25519", {
46
+ publicKeyEncoding: { type: "spki", format: "pem" },
47
+ privateKeyEncoding: { type: "pkcs8", format: "pem" }
48
+ });
49
+ return {
50
+ publicKeyPem: publicKey,
51
+ privateKeyPem: privateKey,
52
+ fingerprint: computePublicKeyFingerprint(publicKey)
53
+ };
54
+ }
55
+ function sanitizeSerialPrefix(prefix) {
56
+ const cleaned = String(prefix || "CERT").toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, 6);
57
+ return cleaned || "CERT";
58
+ }
59
+ function generateSerialNumber(prefix = "CERT", year) {
60
+ const safePrefix = sanitizeSerialPrefix(prefix);
61
+ const targetYear = typeof year === "number" && Number.isFinite(year) ? year : (/* @__PURE__ */ new Date()).getUTCFullYear();
62
+ const entropy = node_crypto.randomBytes(4).toString("hex").toUpperCase();
63
+ return `${safePrefix}-${targetYear}-${entropy}`;
64
+ }
65
+ function generateVerificationToken() {
66
+ return node_crypto.randomBytes(24).toString("hex");
67
+ }
68
+ function normalizeNationalId(rawId) {
69
+ return String(rawId || "").toUpperCase().replace(/[^A-Z0-9]/g, "");
70
+ }
71
+ function maskNationalId(rawId) {
72
+ const normalized = normalizeNationalId(rawId);
73
+ if (!normalized) return "";
74
+ if (normalized.length <= 4) {
75
+ return `***-${normalized}`;
76
+ }
77
+ const tail = normalized.slice(-4);
78
+ return `***-***-${tail}`;
79
+ }
80
+ function hashNationalId(rawId, serialNumber) {
81
+ const normalized = normalizeNationalId(rawId);
82
+ if (!normalized) return "";
83
+ const cleanSerial = String(serialNumber || "").trim().toUpperCase();
84
+ return node_crypto.createHash("sha256").update(`${cleanSerial}:${normalized}`, "utf8").digest("hex");
85
+ }
86
+ function verifyNationalIdHash(rawId, serialNumber, expectedHash) {
87
+ if (!expectedHash || !rawId) return false;
88
+ const computed = hashNationalId(rawId, serialNumber);
89
+ if (!computed) return false;
90
+ return verifyTokenConstantTime(computed, expectedHash);
91
+ }
92
+ function maskRecipientName(fullName) {
93
+ const parts = String(fullName || "").trim().split(/\s+/).filter(Boolean);
94
+ if (parts.length === 0) return "***";
95
+ if (parts.length === 1) {
96
+ const word = parts[0];
97
+ return word.length <= 2 ? `${word[0]}*` : `${word.slice(0, 2)}***`;
98
+ }
99
+ const first = parts[0];
100
+ const initials = parts.slice(1).map((part) => `${part.charAt(0).toUpperCase()}.`).join(" ");
101
+ return `${first} ${initials}`;
102
+ }
103
+ function cleanStr(val) {
104
+ return String(val ?? "").trim();
105
+ }
106
+ function serializeCanonicalPayload(payload) {
107
+ const signatories = payload.signatories ?? [];
108
+ const witnesses = payload.witnesses ?? [];
109
+ const ordered = {
110
+ version: "1.0",
111
+ serialNumber: cleanStr(payload.serialNumber),
112
+ templateSlug: cleanStr(payload.templateSlug),
113
+ organizationName: cleanStr(payload.organizationName),
114
+ recipientFullName: cleanStr(payload.recipientFullName),
115
+ recipientChosenName: cleanStr(payload.recipientChosenName),
116
+ nationalIdHash: cleanStr(payload.nationalIdHash),
117
+ issuedAt: cleanStr(payload.issuedAt),
118
+ expiresAt: cleanStr(payload.expiresAt),
119
+ signatories: signatories.map((s) => ({
120
+ name: cleanStr(s.name),
121
+ title: cleanStr(s.title)
122
+ })),
123
+ witnesses: witnesses.map((w) => ({
124
+ fullName: cleanStr(w.fullName),
125
+ role: cleanStr(w.role)
126
+ }))
127
+ };
128
+ return JSON.stringify(ordered);
129
+ }
130
+ function computeCanonicalPayloadHash(payload) {
131
+ const canonicalJson = serializeCanonicalPayload(payload);
132
+ return node_crypto.createHash("sha256").update(canonicalJson, "utf8").digest("hex");
133
+ }
134
+ function signCanonicalPayload(payload, privateKeyPem) {
135
+ const canonicalJson = serializeCanonicalPayload(payload);
136
+ const payloadHash = node_crypto.createHash("sha256").update(canonicalJson, "utf8").digest("hex");
137
+ const dataBuffer = Buffer.from(canonicalJson, "utf8");
138
+ const signature = node_crypto.sign(null, dataBuffer, privateKeyPem).toString("base64url");
139
+ return {
140
+ canonicalJson,
141
+ payloadHash,
142
+ signature
143
+ };
144
+ }
145
+ function verifyCanonicalPayloadSignature(payload, signatureBase64Url, publicKeyPem) {
146
+ try {
147
+ if (!signatureBase64Url || !publicKeyPem) return false;
148
+ const canonicalJson = serializeCanonicalPayload(payload);
149
+ const dataBuffer = Buffer.from(canonicalJson, "utf8");
150
+ const sigBuffer = Buffer.from(signatureBase64Url, "base64url");
151
+ return node_crypto.verify(null, dataBuffer, publicKeyPem, sigBuffer);
152
+ } catch {
153
+ return false;
154
+ }
155
+ }
156
+ function verifyTokenConstantTime(providedToken, expectedToken) {
157
+ if (!providedToken || !expectedToken) return false;
158
+ const a = Buffer.from(String(providedToken), "utf8");
159
+ const b = Buffer.from(String(expectedToken), "utf8");
160
+ if (a.length !== b.length || a.length === 0) {
161
+ return false;
162
+ }
163
+ return node_crypto.timingSafeEqual(a, b);
164
+ }
165
+ function computeDocuSignWebhookHmac(rawPayload, webhookSecret) {
166
+ return node_crypto.createHmac("sha256", webhookSecret).update(rawPayload, "utf8").digest("base64");
167
+ }
168
+ function verifyDocuSignWebhookHmac(rawPayload, signatureHeaderBase64, webhookSecret) {
169
+ if (!rawPayload || !signatureHeaderBase64 || !webhookSecret) {
170
+ return false;
171
+ }
172
+ const computedBase64 = computeDocuSignWebhookHmac(rawPayload, webhookSecret);
173
+ return verifyTokenConstantTime(
174
+ String(signatureHeaderBase64).trim(),
175
+ computedBase64
176
+ );
177
+ }
178
+ function computeDeterministicHex(input) {
179
+ const out = [];
180
+ for (let pass = 0; pass < 8; pass++) {
181
+ let h = (2166136261 ^ pass) >>> 0;
182
+ for (let i = 0; i < input.length; i++) {
183
+ h ^= input.charCodeAt(i);
184
+ h = Math.imul(h, 16777619) >>> 0;
185
+ }
186
+ out.push(h.toString(16).padStart(8, "0"));
187
+ }
188
+ return out.join("");
189
+ }
190
+ function escapeHtml(input) {
191
+ return String(input ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
192
+ }
193
+ function sanitizeHexColor(input, fallback = "#14532d") {
194
+ const cleaned = String(input || "").trim();
195
+ if (/^#[0-9a-fA-F]{6}$/.test(cleaned)) {
196
+ return cleaned;
197
+ }
198
+ return fallback;
199
+ }
200
+ function interpolateBodyTemplate(templateText, variables) {
201
+ const safeTemplate = escapeHtml(templateText);
202
+ return safeTemplate.replace(
203
+ /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g,
204
+ (_match, key) => {
205
+ const rawVal = variables[key] ?? "";
206
+ return `<strong>${escapeHtml(rawVal)}</strong>`;
207
+ }
208
+ );
209
+ }
210
+ function renderVerificationMatrixSvg(seedText, accentColor = "#14532d") {
211
+ const safeColor = sanitizeHexColor(accentColor);
212
+ const hashHex = computeDeterministicHex(seedText);
213
+ const size = 9;
214
+ const cells = [];
215
+ for (let row = 0; row < size; row++) {
216
+ for (let col = 0; col < size; col++) {
217
+ const isCornerFinder = row < 3 && col < 3 || row < 3 && col >= size - 3 || row >= size - 3 && col < 3;
218
+ const hexIdx = (row * size + col) % hashHex.length;
219
+ const nibble = parseInt(hashHex[hexIdx], 16);
220
+ const filled = isCornerFinder || nibble % 2 === 0;
221
+ if (filled) {
222
+ const x = 6 + col * 8;
223
+ const y = 6 + row * 8;
224
+ cells.push(
225
+ `<rect x="${x}" y="${y}" width="6.5" height="6.5" rx="1" fill="${safeColor}" />`
226
+ );
227
+ }
228
+ }
229
+ }
230
+ return [
231
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 84 84" width="84" height="84" role="img" aria-label="Cryptographic Verification Matrix">',
232
+ `<rect x="1" y="1" width="82" height="82" rx="6" fill="#ffffff" stroke="${safeColor}" stroke-width="2" />`,
233
+ ...cells,
234
+ "</svg>"
235
+ ].join("");
236
+ }
237
+ function buildTemplateVariables(certificate, template, issuedDateFormatted, secondaryFormattedList) {
238
+ return {
239
+ recipientFullName: certificate.recipientFullName,
240
+ recipientChosenName: certificate.recipientChosenName || certificate.recipientFullName,
241
+ chosenName: certificate.recipientChosenName || "",
242
+ nationalIdMasked: certificate.nationalIdMasked || "N/A",
243
+ recipientNationality: certificate.recipientNationality || "",
244
+ issuedDateFormatted,
245
+ secondaryDateFormatted: secondaryFormattedList || issuedDateFormatted,
246
+ serialNumber: certificate.serialNumber,
247
+ organizationName: template.organizationName,
248
+ ...certificate.customData
249
+ };
250
+ }
251
+ function renderSignatoriesHtml(signatories) {
252
+ return (signatories || []).map(
253
+ (sig, idx) => `
254
+ <div class="sig-block">
255
+ <div class="sig-line"><span class="esign-anchor">/sig_${idx + 1}/</span></div>
256
+ <div class="sig-name">${escapeHtml(sig.name)}</div>
257
+ <div class="sig-title">${escapeHtml(sig.title)}</div>
258
+ ${sig.role ? `<div class="sig-role">${escapeHtml(sig.role)}</div>` : ""}
259
+ </div>`
260
+ ).join("");
261
+ }
262
+ function renderWitnessesHtml(witnesses) {
263
+ const list = witnesses || [];
264
+ if (list.length === 0) return "";
265
+ const cards = list.map(
266
+ (w, idx) => `
267
+ <div class="witness-card">
268
+ <div class="sig-line"><span class="esign-anchor">/wit_${idx + 1}/</span></div>
269
+ <div class="witness-name">${escapeHtml(w.fullName)}</div>
270
+ <div class="witness-meta">
271
+ ${w.role ? `<span>${escapeHtml(w.role)}</span>` : ""}
272
+ ${w.documentIdMasked ? `<span>ID: ${escapeHtml(w.documentIdMasked)}</span>` : ""}
273
+ </div>
274
+ </div>`
275
+ ).join("");
276
+ return `
277
+ <div class="witness-section">
278
+ <div class="witness-heading">Official Witnesses</div>
279
+ <div class="witness-grid">${cards}
280
+ </div>
281
+ </div>`;
282
+ }
283
+ function renderOptionalHtmlFragments(certificate, template, secondaryFormattedList) {
284
+ return {
285
+ statusBanner: certificate.status !== "issued" ? `<div class="watermark-alert">STATUS: ${escapeHtml(certificate.status.toUpperCase())}</div>` : "",
286
+ headerHtml: template.headerText ? `<div class="header-Invocation">${escapeHtml(template.headerText)}</div>` : "",
287
+ subheaderHtml: template.organizationSubheader ? `<div class="org-subheader">${escapeHtml(template.organizationSubheader)}</div>` : "",
288
+ chosenNameHtml: certificate.recipientChosenName ? `<div class="recipient-chosen">Chosen Name / Title: ${escapeHtml(certificate.recipientChosenName)}</div>` : "",
289
+ secondaryDateHtml: secondaryFormattedList ? `<span><strong>Secondary Calendar:</strong> ${escapeHtml(secondaryFormattedList)}</span>` : "",
290
+ nationalIdHtml: certificate.nationalIdMasked ? `<span><strong>Document ID:</strong> ${escapeHtml(certificate.nationalIdMasked)}</span>` : "",
291
+ esignLineHtml: certificate.esignEnvelopeId ? `<div>E-Sign Envelope (${escapeHtml(certificate.esignProvider || "docusign")}): ${escapeHtml(certificate.esignEnvelopeId)} [${escapeHtml(certificate.esignEnvelopeStatus || "sent")}]</div>` : ""
292
+ };
293
+ }
294
+ function renderPrintableCertificateHtml(params) {
295
+ const { certificate, template, verificationUrl } = params;
296
+ const accent = sanitizeHexColor(template.accentColor, "#14532d");
297
+ const issuedDateFormatted = certificate.issuedAt.slice(0, 10);
298
+ const secondaryFormattedList = Object.values(certificate.secondaryDates || {}).map((d) => `${d.label}: ${d.formatted}`).join(" • ");
299
+ const variables = buildTemplateVariables(
300
+ certificate,
301
+ template,
302
+ issuedDateFormatted,
303
+ secondaryFormattedList
304
+ );
305
+ const renderedBody = interpolateBodyTemplate(
306
+ template.bodyTemplate,
307
+ variables
308
+ );
309
+ const matrixSvg = renderVerificationMatrixSvg(
310
+ `${certificate.serialNumber}:${certificate.cryptographicSignature}`,
311
+ accent
312
+ );
313
+ const signatoriesHtml = renderSignatoriesHtml(certificate.signatories);
314
+ const witnessesHtml = renderWitnessesHtml(certificate.witnesses);
315
+ const fragments = renderOptionalHtmlFragments(
316
+ certificate,
317
+ template,
318
+ secondaryFormattedList
319
+ );
320
+ return `<!DOCTYPE html>
321
+ <html lang="en">
322
+ <head>
323
+ <meta charset="utf-8" />
324
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
325
+ <title>${escapeHtml(template.titleText)} — ${escapeHtml(certificate.serialNumber)}</title>
326
+ <style>
327
+ @page { size: A4 landscape; margin: 12mm; }
328
+ * { box-sizing: border-box; }
329
+ body {
330
+ margin: 0;
331
+ padding: 24px;
332
+ font-family: Georgia, "Times New Roman", serif;
333
+ background: #f8fafc;
334
+ color: #0f172a;
335
+ }
336
+ .certificate-frame {
337
+ max-width: 1020px;
338
+ margin: 0 auto;
339
+ background: #ffffff;
340
+ border: 3px solid ${accent};
341
+ padding: 10px;
342
+ box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08);
343
+ position: relative;
344
+ }
345
+ .certificate-inner {
346
+ border: 1px solid ${accent};
347
+ padding: 36px 48px;
348
+ position: relative;
349
+ }
350
+ .watermark-alert {
351
+ background: #fef2f2;
352
+ color: #991b1b;
353
+ border: 2px solid #dc2626;
354
+ padding: 8px 16px;
355
+ text-align: center;
356
+ font-weight: bold;
357
+ letter-spacing: 0.1em;
358
+ margin-bottom: 16px;
359
+ }
360
+ .header-Invocation {
361
+ text-align: center;
362
+ font-size: 20px;
363
+ color: ${accent};
364
+ margin-bottom: 8px;
365
+ font-weight: 600;
366
+ }
367
+ .org-name {
368
+ text-align: center;
369
+ font-size: 24px;
370
+ font-weight: 700;
371
+ letter-spacing: 0.06em;
372
+ text-transform: uppercase;
373
+ color: #0f172a;
374
+ }
375
+ .org-subheader {
376
+ text-align: center;
377
+ font-size: 13px;
378
+ color: #475569;
379
+ margin-top: 4px;
380
+ margin-bottom: 24px;
381
+ }
382
+ .cert-title {
383
+ text-align: center;
384
+ font-size: 30px;
385
+ color: ${accent};
386
+ margin: 18px 0 24px;
387
+ font-weight: 700;
388
+ border-bottom: 2px solid ${accent};
389
+ display: inline-block;
390
+ padding-bottom: 6px;
391
+ }
392
+ .title-wrap { text-align: center; }
393
+ .recipient-banner {
394
+ text-align: center;
395
+ margin: 16px 0 20px;
396
+ }
397
+ .recipient-name {
398
+ font-size: 28px;
399
+ font-weight: 700;
400
+ color: #0f172a;
401
+ }
402
+ .recipient-chosen {
403
+ font-size: 18px;
404
+ color: ${accent};
405
+ margin-top: 4px;
406
+ font-style: italic;
407
+ }
408
+ .cert-body {
409
+ font-size: 16px;
410
+ line-height: 1.75;
411
+ text-align: center;
412
+ max-width: 820px;
413
+ margin: 0 auto 28px;
414
+ color: #1e293b;
415
+ }
416
+ .dates-bar {
417
+ display: flex;
418
+ justify-content: center;
419
+ gap: 28px;
420
+ font-size: 13px;
421
+ color: #334155;
422
+ margin-bottom: 28px;
423
+ flex-wrap: wrap;
424
+ }
425
+ .sig-row {
426
+ display: flex;
427
+ justify-content: space-around;
428
+ gap: 24px;
429
+ margin-top: 32px;
430
+ margin-bottom: 24px;
431
+ flex-wrap: wrap;
432
+ }
433
+ .sig-block {
434
+ min-width: 200px;
435
+ text-align: center;
436
+ }
437
+ .sig-line {
438
+ border-bottom: 1px solid #334155;
439
+ height: 36px;
440
+ margin-bottom: 8px;
441
+ }
442
+ .esign-anchor {
443
+ color: #ffffff;
444
+ font-size: 1px;
445
+ line-height: 1px;
446
+ opacity: 0.01;
447
+ user-select: none;
448
+ }
449
+ .sig-name { font-weight: 700; font-size: 15px; }
450
+ .sig-title { font-size: 13px; color: #475569; }
451
+ .sig-role { font-size: 12px; color: ${accent}; }
452
+ .witness-section {
453
+ border-top: 1px dashed #cbd5e1;
454
+ padding-top: 14px;
455
+ margin-top: 16px;
456
+ margin-bottom: 20px;
457
+ }
458
+ .witness-heading {
459
+ text-align: center;
460
+ font-size: 12px;
461
+ text-transform: uppercase;
462
+ letter-spacing: 0.08em;
463
+ color: #64748b;
464
+ margin-bottom: 10px;
465
+ }
466
+ .witness-grid {
467
+ display: flex;
468
+ justify-content: center;
469
+ gap: 24px;
470
+ flex-wrap: wrap;
471
+ }
472
+ .witness-card {
473
+ font-size: 13px;
474
+ text-align: center;
475
+ padding: 6px 14px;
476
+ background: #f8fafc;
477
+ border-radius: 6px;
478
+ border: 1px solid #e2e8f0;
479
+ }
480
+ .witness-name { font-weight: 600; }
481
+ .witness-meta { font-size: 11px; color: #64748b; display: flex; gap: 8px; justify-content: center; }
482
+ .crypto-footer {
483
+ display: flex;
484
+ align-items: center;
485
+ justify-content: space-between;
486
+ border-top: 2px solid #e2e8f0;
487
+ padding-top: 16px;
488
+ margin-top: 16px;
489
+ gap: 16px;
490
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
491
+ font-size: 11px;
492
+ color: #475569;
493
+ }
494
+ .crypto-details { line-height: 1.5; }
495
+ @media print {
496
+ body { background: #ffffff; padding: 0; }
497
+ .certificate-frame { box-shadow: none; }
498
+ }
499
+ </style>
500
+ </head>
501
+ <body>
502
+ <div class="certificate-frame">
503
+ <div class="certificate-inner">
504
+ ${fragments.statusBanner}
505
+ ${fragments.headerHtml}
506
+ <div class="org-name">${escapeHtml(template.organizationName)}</div>
507
+ ${fragments.subheaderHtml}
508
+ <div class="title-wrap">
509
+ <div class="cert-title">${escapeHtml(template.titleText)}</div>
510
+ </div>
511
+ <div class="recipient-banner">
512
+ <div class="recipient-name">${escapeHtml(certificate.recipientFullName)}</div>
513
+ ${fragments.chosenNameHtml}
514
+ </div>
515
+ <div class="cert-body">${renderedBody}</div>
516
+ <div class="dates-bar">
517
+ <span><strong>Serial No:</strong> ${escapeHtml(certificate.serialNumber)}</span>
518
+ <span><strong>Issued Date:</strong> ${escapeHtml(issuedDateFormatted)}</span>
519
+ ${fragments.secondaryDateHtml}
520
+ ${fragments.nationalIdHtml}
521
+ </div>
522
+ <div class="sig-row">${signatoriesHtml}</div>
523
+ ${witnessesHtml}
524
+ <div class="crypto-footer">
525
+ <div class="crypto-details">
526
+ <div><strong>CRYPTOGRAPHICALLY SIGNED RECORD (Ed25519)</strong></div>
527
+ <div>Serial: ${escapeHtml(certificate.serialNumber)} | Key Fingerprint: ${escapeHtml(certificate.publicKeyFingerprint)}</div>
528
+ <div>Payload SHA-256: ${escapeHtml(certificate.canonicalPayloadHash.slice(0, 32))}...</div>
529
+ <div>Signature (Ed25519): ${escapeHtml(certificate.cryptographicSignature.slice(0, 40))}...</div>
530
+ ${fragments.esignLineHtml}
531
+ <div>Verify Online: ${escapeHtml(verificationUrl)}</div>
532
+ </div>
533
+ <div>${matrixSvg}</div>
534
+ </div>
535
+ </div>
536
+ </div>
537
+ </body>
538
+ </html>`;
539
+ }
540
+ function escapeCsvCell(value) {
541
+ let str = String(value ?? "").replace(/\r?\n/g, " ").trim();
542
+ if (/^[=+\-@\t\r]/.test(str)) {
543
+ str = `'${str}`;
544
+ }
545
+ if (str.includes(",") || str.includes('"') || str.includes("'")) {
546
+ return `"${str.replace(/"/g, '""')}"`;
547
+ }
548
+ return str;
549
+ }
550
+ function exportCertificatesToCsv(records) {
551
+ const headers = [
552
+ "Serial Number",
553
+ "Template",
554
+ "Status",
555
+ "Recipient Full Name",
556
+ "Chosen Name / Title",
557
+ "Masked ID",
558
+ "Issued At",
559
+ "Expires At",
560
+ "Signatories",
561
+ "Witnesses Count",
562
+ "Key Fingerprint"
563
+ ];
564
+ const rows = records.map((r) => [
565
+ escapeCsvCell(r.serialNumber),
566
+ escapeCsvCell(r.templateName || r.templateSlug),
567
+ escapeCsvCell(r.status),
568
+ escapeCsvCell(r.recipientFullName),
569
+ escapeCsvCell(r.recipientChosenName || ""),
570
+ escapeCsvCell(r.nationalIdMasked || ""),
571
+ escapeCsvCell(r.issuedAt),
572
+ escapeCsvCell(r.expiresAt || ""),
573
+ escapeCsvCell((r.signatories || []).map((s) => s.name).join("; ")),
574
+ escapeCsvCell((r.witnesses || []).length),
575
+ escapeCsvCell(r.publicKeyFingerprint)
576
+ ]);
577
+ return [headers.join(","), ...rows.map((row) => row.join(","))].join("\n");
578
+ }
579
+ Object.defineProperty(exports, "SUPPORTED_INTL_CALENDARS", {
580
+ enumerable: true,
581
+ get: () => shared.SUPPORTED_INTL_CALENDARS
582
+ });
583
+ Object.defineProperty(exports, "applyDayOffset", {
584
+ enumerable: true,
585
+ get: () => shared.applyDayOffset
586
+ });
587
+ Object.defineProperty(exports, "extractCalendarParts", {
588
+ enumerable: true,
589
+ get: () => shared.extractCalendarParts
590
+ });
591
+ Object.defineProperty(exports, "formatDateInCalendarSystem", {
592
+ enumerable: true,
593
+ get: () => shared.formatDateInCalendarSystem
594
+ });
595
+ Object.defineProperty(exports, "getRegisteredCalendarFormatters", {
596
+ enumerable: true,
597
+ get: () => shared.getRegisteredCalendarFormatters
598
+ });
599
+ Object.defineProperty(exports, "registerCalendarSystemFormatter", {
600
+ enumerable: true,
601
+ get: () => shared.registerCalendarSystemFormatter
602
+ });
603
+ Object.defineProperty(exports, "resolveOccurrenceAlternateDates", {
604
+ enumerable: true,
605
+ get: () => shared.resolveOccurrenceAlternateDates
606
+ });
607
+ Object.defineProperty(exports, "unregisterCalendarSystemFormatter", {
608
+ enumerable: true,
609
+ get: () => shared.unregisterCalendarSystemFormatter
610
+ });
611
+ exports.PLUGIN_ID = PLUGIN_ID;
612
+ exports.computeCanonicalPayloadHash = computeCanonicalPayloadHash;
613
+ exports.computeDocuSignWebhookHmac = computeDocuSignWebhookHmac;
614
+ exports.computePublicKeyFingerprint = computePublicKeyFingerprint;
615
+ exports.escapeCsvCell = escapeCsvCell;
616
+ exports.escapeHtml = escapeHtml;
617
+ exports.exportCertificatesToCsv = exportCertificatesToCsv;
618
+ exports.formatSecondaryCalendars = formatSecondaryCalendars;
619
+ exports.generateEd25519KeyPair = generateEd25519KeyPair;
620
+ exports.generateSerialNumber = generateSerialNumber;
621
+ exports.generateVerificationToken = generateVerificationToken;
622
+ exports.hashNationalId = hashNationalId;
623
+ exports.interpolateBodyTemplate = interpolateBodyTemplate;
624
+ exports.maskNationalId = maskNationalId;
625
+ exports.maskRecipientName = maskRecipientName;
626
+ exports.normalizeNationalId = normalizeNationalId;
627
+ exports.renderPrintableCertificateHtml = renderPrintableCertificateHtml;
628
+ exports.renderVerificationMatrixSvg = renderVerificationMatrixSvg;
629
+ exports.sanitizeHexColor = sanitizeHexColor;
630
+ exports.sanitizeSerialPrefix = sanitizeSerialPrefix;
631
+ exports.serializeCanonicalPayload = serializeCanonicalPayload;
632
+ exports.signCanonicalPayload = signCanonicalPayload;
633
+ exports.verifyCanonicalPayloadSignature = verifyCanonicalPayloadSignature;
634
+ exports.verifyDocuSignWebhookHmac = verifyDocuSignWebhookHmac;
635
+ exports.verifyNationalIdHash = verifyNationalIdHash;
636
+ exports.verifyTokenConstantTime = verifyTokenConstantTime;
@@ -0,0 +1,5 @@
1
+ export * from './pluginId';
2
+ export * from './calendarSystems';
3
+ export * from './types';
4
+ export * from './cryptoEngine';
5
+ export * from './documentRenderer';