@certysign/sdk 1.0.0 → 2.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.
- package/README.md +994 -55
- package/package.json +15 -6
- package/src/index.js +55 -20
- package/src/lib/CertificateResource.js +24 -3
- package/src/lib/DashboardResource.js +91 -0
- package/src/lib/DocumentHasher.js +123 -0
- package/src/lib/HashSigningResource.js +220 -0
- package/src/lib/HttpClient.js +1 -1
- package/src/lib/SignatureEmbedder.js +723 -0
- package/src/lib/SigningSessionResource.js +183 -0
- package/examples/certificate-flow.js +0 -198
- package/examples/dha-integration.js +0 -208
- package/examples/nhif-batch-sign.js +0 -216
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* NHIF Kenya — Batch Invoice Signing Integration
|
|
3
|
-
*
|
|
4
|
-
* This example shows how NHIF's automated invoicing system uses batchSign()
|
|
5
|
-
* to sign hundreds of provider reimbursement invoices in a single API call
|
|
6
|
-
* during the nightly batch run.
|
|
7
|
-
*
|
|
8
|
-
* Workflow:
|
|
9
|
-
* 1. Collect pending invoices from the DB (up to 25 per API call)
|
|
10
|
-
* 2. Sign all documents in one batchSign() call
|
|
11
|
-
* 3. For each signed invoice: store envelopeId, certificate serial
|
|
12
|
-
* 4. Retrieve audit trails for compliance archiving
|
|
13
|
-
* 5. Download signed PDFs from the envelopes
|
|
14
|
-
*
|
|
15
|
-
* Rate limits:
|
|
16
|
-
* The SDK automatically handles 429 responses with exponential back-off.
|
|
17
|
-
* For very large batches, use the BATCH_SIZE constant to chunk requests.
|
|
18
|
-
*
|
|
19
|
-
* Prerequisites:
|
|
20
|
-
* npm install @certysign/sdk
|
|
21
|
-
* export CERTYSIGN_PUBLIC_KEY="cs_pk_..."
|
|
22
|
-
* export CERTYSIGN_SECRET_KEY="cs_sk_..."
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
'use strict';
|
|
26
|
-
|
|
27
|
-
const fs = require('fs');
|
|
28
|
-
const path = require('path');
|
|
29
|
-
const { CertySignClient, CertySignError } = require('@certysign/sdk');
|
|
30
|
-
|
|
31
|
-
// ── Config ───────────────────────────────────────────────────────────────────
|
|
32
|
-
|
|
33
|
-
const BATCH_SIZE = 10; // Sign up to 10 invoices per API call (max 25)
|
|
34
|
-
const SIGNER_NAME = 'NHIF Finance System';
|
|
35
|
-
const SIGNER_EMAIL = 'invoicing-batch@nhif.or.ke';
|
|
36
|
-
|
|
37
|
-
const client = new CertySignClient({
|
|
38
|
-
publicKey: process.env.CERTYSIGN_PUBLIC_KEY,
|
|
39
|
-
secretKey: process.env.CERTYSIGN_SECRET_KEY,
|
|
40
|
-
environment: 'production'
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// ── Simulated pending invoices ────────────────────────────────────────────────
|
|
44
|
-
|
|
45
|
-
function getPendingInvoices() {
|
|
46
|
-
// In production: fetch from your DB
|
|
47
|
-
return [
|
|
48
|
-
{ id: 'INV-2026-0441', provider: 'Aga Khan University Hospital', amount: 1_250_000 },
|
|
49
|
-
{ id: 'INV-2026-0442', provider: 'Nairobi Hospital', amount: 875_000 },
|
|
50
|
-
{ id: 'INV-2026-0443', provider: 'Karen Hospital', amount: 430_000 },
|
|
51
|
-
{ id: 'INV-2026-0444', provider: 'MP Shah Hospital', amount: 995_000 },
|
|
52
|
-
{ id: 'INV-2026-0445', provider: 'Mater Hospital', amount: 660_000 }
|
|
53
|
-
];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function loadInvoicePdf(invoice) {
|
|
57
|
-
// In production: load from document store / generate from template
|
|
58
|
-
const filePath = path.join(__dirname, `invoice-${invoice.id}.pdf`);
|
|
59
|
-
if (fs.existsSync(filePath)) return fs.readFileSync(filePath);
|
|
60
|
-
// Placeholder for demo
|
|
61
|
-
return Buffer.from(`%PDF-1.4 NHIF Invoice ${invoice.id} — ${invoice.provider}`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ── Chunk array helper ────────────────────────────────────────────────────────
|
|
65
|
-
|
|
66
|
-
function* chunks(arr, size) {
|
|
67
|
-
for (let i = 0; i < arr.length; i += size) {
|
|
68
|
-
yield arr.slice(i, i + size);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ── Batch signing ─────────────────────────────────────────────────────────────
|
|
73
|
-
|
|
74
|
-
async function runNightlyBatch() {
|
|
75
|
-
console.log('\n[NHIF] Starting nightly invoice signing batch');
|
|
76
|
-
console.log(`[NHIF] Timestamp: ${new Date().toISOString()}`);
|
|
77
|
-
console.log('─'.repeat(60));
|
|
78
|
-
|
|
79
|
-
const invoices = getPendingInvoices();
|
|
80
|
-
console.log(`[NHIF] ${invoices.length} invoices pending signature`);
|
|
81
|
-
|
|
82
|
-
const signedRecords = [];
|
|
83
|
-
const failedInvoices = [];
|
|
84
|
-
let batchNumber = 0;
|
|
85
|
-
|
|
86
|
-
for (const batch of chunks(invoices, BATCH_SIZE)) {
|
|
87
|
-
batchNumber++;
|
|
88
|
-
console.log(`\n[NHIF] Batch ${batchNumber} — signing ${batch.length} invoices...`);
|
|
89
|
-
|
|
90
|
-
// Build document list for this batch
|
|
91
|
-
const documents = batch.map(invoice => ({
|
|
92
|
-
data: loadInvoicePdf(invoice),
|
|
93
|
-
filename: `nhif-invoice-${invoice.id}.pdf`
|
|
94
|
-
}));
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
const result = await client.sign.batchSign({
|
|
98
|
-
documents,
|
|
99
|
-
signerName: SIGNER_NAME,
|
|
100
|
-
signerEmail: SIGNER_EMAIL,
|
|
101
|
-
reason: `NHIF provider reimbursement batch — ${new Date().toISOString().slice(0, 10)}`,
|
|
102
|
-
location: 'Nairobi, Kenya',
|
|
103
|
-
metadata: {
|
|
104
|
-
batchNumber: batchNumber,
|
|
105
|
-
invoiceCount: batch.length,
|
|
106
|
-
system: 'NHIF-FINANCE-BATCH'
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Map results back to invoice records
|
|
111
|
-
const results = result.data.results; // Array, one per document
|
|
112
|
-
for (let i = 0; i < batch.length; i++) {
|
|
113
|
-
const invoice = batch[i];
|
|
114
|
-
const res = results[i];
|
|
115
|
-
|
|
116
|
-
if (res.success) {
|
|
117
|
-
signedRecords.push({
|
|
118
|
-
invoiceId: invoice.id,
|
|
119
|
-
provider: invoice.provider,
|
|
120
|
-
amount: invoice.amount,
|
|
121
|
-
envelopeId: res.envelopeId,
|
|
122
|
-
certificateSerial: res.certificate?.serialNumber,
|
|
123
|
-
signedAt: new Date().toISOString(),
|
|
124
|
-
status: 'SIGNED'
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
console.log(` ✓ ${invoice.id} → env: ${res.envelopeId}`);
|
|
128
|
-
} else {
|
|
129
|
-
failedInvoices.push({ invoice, reason: res.error });
|
|
130
|
-
console.warn(` ✗ ${invoice.id} → ${res.error}`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
} catch (err) {
|
|
135
|
-
if (err instanceof CertySignError) {
|
|
136
|
-
console.error(`[NHIF] Batch ${batchNumber} API error (${err.statusCode}): ${err.message}`);
|
|
137
|
-
if (err.statusCode === 429) {
|
|
138
|
-
// Rate limited — wait and log (SDK already retried 3 times)
|
|
139
|
-
console.error('[NHIF] Rate limit exhausted — will retry this batch in next run');
|
|
140
|
-
}
|
|
141
|
-
batch.forEach(inv => failedInvoices.push({ invoice: inv, reason: err.message }));
|
|
142
|
-
} else {
|
|
143
|
-
throw err;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
149
|
-
// Step 2: Retrieve audit trails for compliance
|
|
150
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
151
|
-
console.log(`\n[NHIF] Retrieving audit trails for ${signedRecords.length} signed invoices...`);
|
|
152
|
-
|
|
153
|
-
for (const record of signedRecords.slice(0, 3)) { // Demo: first 3 only
|
|
154
|
-
try {
|
|
155
|
-
const auditResult = await client.envelopes.getAuditTrail(record.envelopeId);
|
|
156
|
-
const { auditTrail, chainIntegrity } = auditResult.data;
|
|
157
|
-
|
|
158
|
-
record.auditEntries = auditTrail.length;
|
|
159
|
-
record.auditChainValid = chainIntegrity?.valid;
|
|
160
|
-
|
|
161
|
-
console.log(` ✓ ${record.invoiceId}: ${auditTrail.length} events, chain=${chainIntegrity?.valid}`);
|
|
162
|
-
} catch (err) {
|
|
163
|
-
console.warn(` ✗ Audit trail for ${record.invoiceId}: ${err.message}`);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
168
|
-
// Step 3: Download first signed PDF as a demo
|
|
169
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
170
|
-
if (signedRecords.length > 0) {
|
|
171
|
-
const sample = signedRecords[0];
|
|
172
|
-
try {
|
|
173
|
-
console.log(`\n[NHIF] Downloading signed PDF for ${sample.invoiceId}...`);
|
|
174
|
-
// First get the envelope to find the document ID
|
|
175
|
-
const env = await client.envelopes.get(sample.envelopeId);
|
|
176
|
-
const docId = env.data?.envelope?.documents?.[0]?._id;
|
|
177
|
-
|
|
178
|
-
if (docId) {
|
|
179
|
-
const pdfBuffer = await client.envelopes.getDocument(sample.envelopeId, docId);
|
|
180
|
-
const outPath = path.join(__dirname, `signed-${sample.invoiceId}.pdf`);
|
|
181
|
-
fs.writeFileSync(outPath, pdfBuffer);
|
|
182
|
-
console.log(`[NHIF] ✓ Saved signed PDF to ${outPath} (${pdfBuffer.length} bytes)`);
|
|
183
|
-
}
|
|
184
|
-
} catch (err) {
|
|
185
|
-
console.warn(`[NHIF] Could not download signed PDF: ${err.message}`);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
190
|
-
// Summary
|
|
191
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
192
|
-
console.log('\n' + '─'.repeat(60));
|
|
193
|
-
console.log(`[NHIF] Batch complete`);
|
|
194
|
-
console.log(` Signed : ${signedRecords.length}`);
|
|
195
|
-
console.log(` Failed : ${failedInvoices.length}`);
|
|
196
|
-
|
|
197
|
-
if (failedInvoices.length) {
|
|
198
|
-
console.log('[NHIF] Failed invoices:');
|
|
199
|
-
failedInvoices.forEach(({ invoice, reason }) => {
|
|
200
|
-
console.log(` - ${invoice.id}: ${reason}`);
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return { signed: signedRecords, failed: failedInvoices };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// ── Run ───────────────────────────────────────────────────────────────────────
|
|
208
|
-
|
|
209
|
-
(async () => {
|
|
210
|
-
try {
|
|
211
|
-
await runNightlyBatch();
|
|
212
|
-
} catch (err) {
|
|
213
|
-
console.error('\n[NHIF] Fatal error:', err.message);
|
|
214
|
-
process.exit(1);
|
|
215
|
-
}
|
|
216
|
-
})();
|