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