@certysign/sdk 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.
package/README.md ADDED
@@ -0,0 +1,398 @@
1
+ # @certysign/sdk
2
+
3
+ Official Node.js SDK for **CertySign Trust Services** — digital document signing, X.509 certificate issuance, and PKI operations built for East Africa.
4
+
5
+ ## Installation
6
+
7
+ ### Local / internal use (current)
8
+
9
+ The SDK is not yet published to the public npm registry. Use one of the methods below.
10
+
11
+ **Option A — file path dependency** (recommended for services in this monorepo):
12
+
13
+ ```bash
14
+ # From any service directory, e.g. a DHA integration project:
15
+ npm install ../../sdk
16
+ ```
17
+
18
+ This adds the following to the consuming project's `package.json`:
19
+
20
+ ```json
21
+ "dependencies": {
22
+ "@certysign/sdk": "file:../../sdk"
23
+ }
24
+ ```
25
+
26
+ **Option B — `npm link`** (for local development outside the monorepo):
27
+
28
+ ```bash
29
+ # In the sdk/ directory — register it globally once:
30
+ cd sdk
31
+ npm link
32
+
33
+ # In any other project:
34
+ npm link @certysign/sdk
35
+ ```
36
+
37
+ **Option C — install directly from the folder path**:
38
+
39
+ ```bash
40
+ npm install /absolute/path/to/Certy_Sign/sdk
41
+ ```
42
+
43
+ ### Publishing to npm (when ready)
44
+
45
+ ```bash
46
+ # Log in to npm:
47
+ npm login
48
+
49
+ # From the sdk/ directory:
50
+ npm publish --access public
51
+ ```
52
+
53
+ After publishing, standard installation will work:
54
+
55
+ ```bash
56
+ npm install @certysign/sdk
57
+ ```
58
+
59
+ Node.js >= 18 required.
60
+
61
+ ## Quick start
62
+
63
+ ```js
64
+ const { CertySignClient } = require('@certysign/sdk');
65
+
66
+ const client = new CertySignClient({
67
+ publicKey: process.env.CERTYSIGN_PUBLIC_KEY, // cs_pk_...
68
+ secretKey: process.env.CERTYSIGN_SECRET_KEY, // cs_sk_...
69
+ environment: 'production' // 'staging' | 'development'
70
+ });
71
+
72
+ // Sign a document
73
+ const result = await client.sign.quickSign({
74
+ document: require('fs').readFileSync('./claim.pdf'),
75
+ filename: 'claim.pdf',
76
+ signerName: 'Dr. Amina Okonkwo',
77
+ signerEmail: 'amina.okonkwo@dha.go.ke',
78
+ reason: 'Health claim approval — DHA Kenya'
79
+ });
80
+
81
+ console.log(result.data.envelopeId);
82
+ console.log(result.data.certificate.serialNumber);
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Authentication
88
+
89
+ API keys are managed in the CertySign portal under **Settings → API Keys**.
90
+
91
+ Each key pair consists of:
92
+
93
+ | Value | Header | Format |
94
+ |-------|--------|--------|
95
+ | Public key | `X-API-Key-Id` | `cs_pk_<48 hex chars>` |
96
+ | Secret key | `X-API-Key-Secret` | `cs_sk_<64 hex chars>` |
97
+
98
+ The secret key is shown **once** at creation. Store it in a secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager) — never in source code.
99
+
100
+ ---
101
+
102
+ ## Environments
103
+
104
+ | Environment | Base URL |
105
+ |-------------|----------|
106
+ | `production` | `https://api.certysign.com` |
107
+ | `staging` | `https://api-staging.certysign.com` |
108
+ | `development` | `http://localhost:8000` |
109
+
110
+ ---
111
+
112
+ ## API Resources
113
+
114
+ ### `client.sign` — Document Signing
115
+
116
+ #### `quickSign(options)` — Sign a PDF in one call
117
+
118
+ ```js
119
+ const result = await client.sign.quickSign({
120
+ document: Buffer, // Required — PDF buffer or ReadStream
121
+ filename: 'document.pdf', // Optional — default: 'document.pdf'
122
+ signerName: 'Full Name', // Required
123
+ signerEmail: 'email@example.com', // Optional
124
+ reason: 'Approval', // Optional
125
+ location: 'Nairobi, Kenya', // Optional
126
+ metadata: { claimId: '...' } // Optional — stored with envelope
127
+ });
128
+ // result.data.envelopeId
129
+ // result.data.certificate.serialNumber
130
+ // result.data.signedDocumentUrl
131
+ ```
132
+
133
+ #### `batchSign(options)` — Sign multiple PDFs at once
134
+
135
+ ```js
136
+ const result = await client.sign.batchSign({
137
+ documents: [
138
+ { data: fs.readFileSync('./invoice-1.pdf'), filename: 'invoice-1.pdf' },
139
+ { data: fs.readFileSync('./invoice-2.pdf'), filename: 'invoice-2.pdf' }
140
+ ],
141
+ signerName: 'NHIF Finance System',
142
+ signerEmail: 'invoicing@nhif.or.ke',
143
+ reason: 'Batch provider reimbursement'
144
+ });
145
+ // result.data.results[].envelopeId
146
+ // result.data.results[].success
147
+ ```
148
+
149
+ #### `verifyById(envelopeId)` — Verify a signed envelope
150
+
151
+ ```js
152
+ const result = await client.sign.verifyById('env_abc123');
153
+ // result.data.valid true/false
154
+ // result.data.signerName
155
+ // result.data.signedAt
156
+ ```
157
+
158
+ #### `verifyDocument(document, filename)` — Verify by uploading
159
+
160
+ ```js
161
+ const result = await client.sign.verifyDocument(fs.readFileSync('./signed.pdf'));
162
+ ```
163
+
164
+ ---
165
+
166
+ ### `client.certificates` — X.509 Certificate Management
167
+
168
+ #### `issue(options)` — Issue a per-document certificate
169
+
170
+ ```js
171
+ const result = await client.certificates.issue({
172
+ commonName: 'DHA Claims System', // Required — Subject CN
173
+ organisation: 'Dept of Health Affairs', // Required — Subject O
174
+ country: 'KE', // Default: 'KE'
175
+ state: 'Nairobi', // Default: 'Nairobi'
176
+ locality: 'Nairobi', // Default: 'Nairobi'
177
+ email: 'pki@dha.go.ke',
178
+ validityDays: 365,
179
+ metadata: { claimId: '...' }
180
+ });
181
+ // result.data.serialNumber — store for future status checks
182
+ // result.data.fingerprint — SHA-256 hex fingerprint
183
+ // result.data.validFrom
184
+ // result.data.validUntil
185
+ // result.data.pemCertificate — PEM certificate (store this)
186
+ // result.data.pemChain — Full trust chain PEM
187
+ // result.data.privateKey — PEM private key (shown ONCE — store securely)
188
+ ```
189
+
190
+ Issued certificates include:
191
+ - **CRL Distribution Point** → `https://pki.certysign.com/crl/intermediate.crl`
192
+ - **OCSP (AIA)** → `https://pki.certysign.com/ocsp`
193
+ - **CA Issuers (AIA)** → `https://pki.certysign.com/certs/intermediate.crt`
194
+
195
+ #### `verify(serialNumber, atTime?)` — Verify certificate validity
196
+
197
+ ```js
198
+ // Current-time check
199
+ const result = await client.certificates.verify('A1B2C3...');
200
+
201
+ // Point-in-time check (was it valid when the document was signed?)
202
+ const result = await client.certificates.verify('A1B2C3...', new Date('2026-01-15T10:30:00Z'));
203
+
204
+ // result.data.valid
205
+ // result.data.chainVerified
206
+ // result.data.revocationChecked
207
+ // result.data.certStatus 'good' | 'revoked' | 'expired'
208
+ ```
209
+
210
+ #### `status(serialNumber)` — Current revocation status
211
+
212
+ ```js
213
+ const result = await client.certificates.status('A1B2C3...');
214
+ // result.data.status 'good' | 'revoked' | 'expired' | 'unknown'
215
+ // result.data.revocationDate ISO 8601 (if revoked)
216
+ // result.data.revocationReason RFC 5280 reason string (if revoked)
217
+ ```
218
+
219
+ ---
220
+
221
+ ### `client.pki` — PKI Infrastructure
222
+
223
+ #### `crl(format?)` — Download the Certificate Revocation List
224
+
225
+ ```js
226
+ // PEM (default)
227
+ const pem = await client.pki.crl('pem');
228
+ fs.writeFileSync('/etc/pki/certysign.crl.pem', pem);
229
+
230
+ // DER binary (for nginx/caddy stapling)
231
+ const der = await client.pki.crl('der');
232
+ fs.writeFileSync('/etc/pki/certysign.crl', der);
233
+
234
+ // JSON — for application-level revocation checks
235
+ const { data } = await client.pki.crl('json');
236
+ const revokedSerials = new Set(data.crl.map(e => e.serialNumber));
237
+ ```
238
+
239
+ CRL properties: RFC 5280 X.509 v2 CRL, signed by CertySign Intermediate CA, valid 7 days, issued every 24 hours.
240
+
241
+ #### `ocsp(serialNumber, format?)` — OCSP query
242
+
243
+ ```js
244
+ // JSON
245
+ const { data } = await client.pki.ocsp('A1B2C3...', 'json');
246
+ // data.status 'good' | 'revoked' | 'unknown'
247
+ // data.thisUpdate ISO 8601
248
+ // data.nextUpdate ISO 8601 ← cache response until this time
249
+ // data.responderId CA subject DN
250
+
251
+ // DER binary (RFC 6960 BasicOCSPResponse — for OCSP stapling)
252
+ const buf = await client.pki.ocsp('A1B2C3...', 'der');
253
+ ```
254
+
255
+ #### `chain()` — Download CA certificate chain
256
+
257
+ ```js
258
+ const pem = await client.pki.chain();
259
+ // Returns PEM bundle: Intermediate CA cert + Root CA cert
260
+ fs.writeFileSync('/etc/ssl/certs/certysign-chain.pem', pem);
261
+ ```
262
+
263
+ #### `info()` — CA hierarchy metadata
264
+
265
+ ```js
266
+ const { data } = await client.pki.info();
267
+ // data.rootCA.subject.commonName
268
+ // data.intermediateCA.validity.notAfter
269
+ // data.intermediateCA.crlUrl
270
+ // data.intermediateCA.ocspUrl
271
+ // data.status.initialized
272
+ ```
273
+
274
+ ---
275
+
276
+ ### `client.envelopes` — Envelope Management
277
+
278
+ For integrations that need full control over the envelope lifecycle (instead of `quickSign`).
279
+
280
+ ```js
281
+ // 1. Create envelope
282
+ const { data: { envelope } } = await client.envelopes.create({
283
+ title: 'NHIF Claims Q1-2026',
284
+ signers: [{ name: 'Director Finance', email: 'df@nhif.or.ke' }]
285
+ });
286
+
287
+ // 2. Upload documents
288
+ await client.envelopes.uploadDocuments(envelope._id, [
289
+ { data: fs.readFileSync('./report.pdf'), filename: 'Q1-report.pdf' }
290
+ ]);
291
+
292
+ // 3. Send envelope (transitions to 'sent' status)
293
+ await client.envelopes.send(envelope._id);
294
+
295
+ // 4. Sign
296
+ const signed = await client.envelopes.sign(envelope._id, {
297
+ reason: 'NHIF Q1 approval',
298
+ location: 'Nairobi, Kenya'
299
+ });
300
+
301
+ // 5. Download signed PDF
302
+ const pdfBuffer = await client.envelopes.getDocument(envelope._id, docId);
303
+
304
+ // 6. Audit trail
305
+ const { data } = await client.envelopes.getAuditTrail(envelope._id);
306
+ // data.auditTrail[].action, .timestamp, .eventHash, .chainHash
307
+ // data.chainIntegrity.valid
308
+ ```
309
+
310
+ ---
311
+
312
+ ## Error Handling
313
+
314
+ All SDK methods throw `CertySignError` on failure:
315
+
316
+ ```js
317
+ const { CertySignClient, CertySignError } = require('@certysign/sdk');
318
+
319
+ try {
320
+ await client.sign.quickSign({ ... });
321
+ } catch (err) {
322
+ if (err instanceof CertySignError) {
323
+ console.error(err.message); // Human-readable message
324
+ console.error(err.statusCode); // HTTP status (0 for network errors)
325
+ console.error(err.code); // Machine-readable code, e.g. 'INVALID_API_KEY'
326
+ console.error(err.details); // Full API error body
327
+ }
328
+ }
329
+ ```
330
+
331
+ Common error codes:
332
+
333
+ | Code | Status | Meaning |
334
+ |------|--------|---------|
335
+ | `INVALID_API_KEY` | 401 | Key not found or secret mismatch |
336
+ | `API_KEY_EXPIRED` | 401 | Key has passed its `expiresAt` date |
337
+ | `INSUFFICIENT_SDK_PERMISSIONS` | 403 | Key lacks required permission |
338
+ | `IP_NOT_ALLOWED` | 403 | Request IP not in IP allowlist |
339
+ | `RATE_LIMIT_EXCEEDED` | 429 | Requests per minute exceeded |
340
+ | `NETWORK_ERROR` | 0 | Could not reach the API |
341
+
342
+ The SDK automatically retries `429`, `502`, `503`, `504` responses up to 3 times with exponential back-off.
343
+
344
+ ---
345
+
346
+ ## Rate Limiting
347
+
348
+ Each API key has a configurable rate limit (requests per minute, set in the CertySign portal). When the limit is exceeded:
349
+
350
+ - HTTP `429` is returned
351
+ - `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` headers are set
352
+ - SDK retries automatically (up to 3 times) if the issue is transient
353
+ - After retries are exhausted, `CertySignError` with code `RATE_LIMIT_EXCEEDED` is thrown
354
+
355
+ For batch workloads, use `batchSign()` to sign multiple documents in a single API call rather than calling `quickSign()` in a loop.
356
+
357
+ ---
358
+
359
+ ## Examples
360
+
361
+ | File | Description |
362
+ |------|-------------|
363
+ | [examples/dha-integration.js](examples/dha-integration.js) | DHA Kenya claims quick-sign + certificate issuance |
364
+ | [examples/nhif-batch-sign.js](examples/nhif-batch-sign.js) | NHIF nightly invoice batch signing |
365
+ | [examples/certificate-flow.js](examples/certificate-flow.js) | Full X.509 lifecycle: issue, verify, CRL, OCSP, chain |
366
+
367
+ ---
368
+
369
+ ## PKI Trust Chain
370
+
371
+ ```
372
+ CertySign Root CA (C=KE, RSA-4096, 20yr)
373
+ └── CertySign Intermediate CA (C=KE, RSA-4096, 10yr, pathLen=0)
374
+ ├── Per-document X.509 certs (issued via /sdk/v1/certificates/issue)
375
+ └── Tenant signing certs (managed in CertySign portal)
376
+ ```
377
+
378
+ CA bundle: `https://pki.certysign.com/certs/chain.pem`
379
+ CRL: `https://pki.certysign.com/crl/intermediate.crl`
380
+ OCSP: `https://pki.certysign.com/ocsp`
381
+
382
+ ---
383
+
384
+ ## Security Best Practices
385
+
386
+ - **Rotate keys** every 90 days: use the CertySign portal → API Keys → Rotate
387
+ - **IP allowlist** every production key to your deployment's egress IPs
388
+ - **Use `staging` environment** for development and CI pipelines (separate key pair)
389
+ - **Store the private key** from `certificates.issue()` response in a HSM or secrets manager — CertySign does not retain it
390
+ - **Cache CRL / OCSP responses** up to `nextUpdate` to reduce latency in HIE systems
391
+
392
+ ---
393
+
394
+ ## Support
395
+
396
+ - Docs: https://docs.certysign.com/sdk
397
+ - Issues: https://github.com/certysign/sdk-node/issues
398
+ - Email: sdk@certysign.com
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Full PKI Certificate Lifecycle Example
3
+ *
4
+ * Demonstrates the complete X.509 certificate workflow:
5
+ * 1. Issue a per-document certificate
6
+ * 2. Check current status (should be 'good')
7
+ * 3. Verify the certificate (chain, revocation, validity)
8
+ * 4. Download CA chain and save it locally
9
+ * 5. Perform a point-in-time verification
10
+ * 6. Fetch and cache the CRL locally
11
+ * 7. Check a serial number directly against the CRL JSON
12
+ * 8. Perform an OCSP query
13
+ *
14
+ * This pattern is used by HIE systems that need to:
15
+ * - Verify document signing certificates before granting access
16
+ * - Cache CRL/OCSP responses to meet response time SLAs
17
+ *
18
+ * Prerequisites:
19
+ * npm install @certysign/sdk
20
+ * export CERTYSIGN_PUBLIC_KEY="cs_pk_..."
21
+ * export CERTYSIGN_SECRET_KEY="cs_sk_..."
22
+ */
23
+
24
+ 'use strict';
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+ const { CertySignClient, CertySignError } = require('@certysign/sdk');
29
+
30
+ const client = new CertySignClient({
31
+ publicKey: process.env.CERTYSIGN_PUBLIC_KEY,
32
+ secretKey: process.env.CERTYSIGN_SECRET_KEY,
33
+ environment: 'production'
34
+ });
35
+
36
+ async function runCertificateFlow() {
37
+ console.log('\n[PKI] CertySign Certificate Lifecycle Demo');
38
+ console.log('─'.repeat(60));
39
+
40
+ // ─────────────────────────────────────────────────────────────────────────
41
+ // 1. Issue a certificate
42
+ // ─────────────────────────────────────────────────────────────────────────
43
+ console.log('\n[PKI] 1. Issuing X.509 certificate...');
44
+
45
+ const issueResult = await client.certificates.issue({
46
+ commonName: 'HIE Integration Node — KNH',
47
+ organisation: 'Kenyatta National Hospital',
48
+ country: 'KE',
49
+ state: 'Nairobi',
50
+ locality: 'Nairobi',
51
+ email: 'pki@knh.or.ke',
52
+ validityDays: 365,
53
+ metadata: { system: 'HIE-NODE-KNH-PROD', issuedAt: new Date().toISOString() }
54
+ });
55
+
56
+ const cert = issueResult.data;
57
+ console.log(` serialNumber : ${cert.serialNumber}`);
58
+ console.log(` fingerprint : ${cert.fingerprint}`);
59
+ console.log(` validFrom : ${cert.validFrom}`);
60
+ console.log(` validUntil : ${cert.validUntil}`);
61
+ console.log(` Subject CN : ${cert.subject?.cn}`);
62
+
63
+ // Store private key securely (shown ONCE — CertySign does not retain it)
64
+ const keyPath = path.join(__dirname, 'hie-node-knh.key.pem');
65
+ fs.writeFileSync(keyPath, cert.privateKey, { mode: 0o600 });
66
+ console.log(` privateKey → ${keyPath} (mode 600)`);
67
+
68
+ const certPath = path.join(__dirname, 'hie-node-knh.crt.pem');
69
+ fs.writeFileSync(certPath, cert.pemCertificate);
70
+ console.log(` certificate → ${certPath}`);
71
+
72
+ const { serialNumber } = cert;
73
+
74
+ // ─────────────────────────────────────────────────────────────────────────
75
+ // 2. Real-time status check (should be 'good')
76
+ // ─────────────────────────────────────────────────────────────────────────
77
+ console.log('\n[PKI] 2. Certificate status check...');
78
+ const statusResult = await client.certificates.status(serialNumber);
79
+ const status = statusResult.data;
80
+ console.log(` status : ${status.status}`);
81
+ if (status.status === 'revoked') {
82
+ console.log(` revocationDate : ${status.revocationDate}`);
83
+ console.log(` reason : ${status.revocationReason}`);
84
+ }
85
+
86
+ // ─────────────────────────────────────────────────────────────────────────
87
+ // 3. Full certificate verification (chain + revocation)
88
+ // ─────────────────────────────────────────────────────────────────────────
89
+ console.log('\n[PKI] 3. Full certificate verification...');
90
+ const verifyResult = await client.certificates.verify(serialNumber);
91
+ const verification = verifyResult.data;
92
+ console.log(` valid : ${verification.valid}`);
93
+ console.log(` chainVerified : ${verification.chainVerified}`);
94
+ console.log(` revocationChecked : ${verification.revocationChecked}`);
95
+ console.log(` certStatus : ${verification.certStatus}`);
96
+
97
+ // ─────────────────────────────────────────────────────────────────────────
98
+ // 4. Download CA chain and install in local trust store
99
+ // ─────────────────────────────────────────────────────────────────────────
100
+ console.log('\n[PKI] 4. Downloading CA chain...');
101
+ const chainPem = await client.pki.chain();
102
+ const chainPath = path.join(__dirname, 'certysign-chain.pem');
103
+ fs.writeFileSync(chainPath, chainPem);
104
+ console.log(` CA chain → ${chainPath}`);
105
+ console.log(` (${chainPem.split('-----BEGIN CERTIFICATE-----').length - 1} cert(s) in bundle)`);
106
+
107
+ // ─────────────────────────────────────────────────────────────────────────
108
+ // 5. Point-in-time verification
109
+ // Used by HIE to check if a cert was valid at the time a document was signed
110
+ // ─────────────────────────────────────────────────────────────────────────
111
+ console.log('\n[PKI] 5. Point-in-time verification...');
112
+ const signingTime = new Date(Date.now() - 5 * 60 * 1000); // 5 minutes ago
113
+ const ptResult = await client.certificates.verify(serialNumber, signingTime);
114
+ console.log(` valid at ${signingTime.toISOString()} : ${ptResult.data.valid}`);
115
+
116
+ // ─────────────────────────────────────────────────────────────────────────
117
+ // 6. Fetch and cache CRL (JSON format for application-level checks)
118
+ // ─────────────────────────────────────────────────────────────────────────
119
+ console.log('\n[PKI] 6. Fetching CRL (JSON)...');
120
+ const crlJson = await client.pki.crl('json');
121
+ const crl = crlJson.data;
122
+ console.log(` issuer : ${crl.issuer}`);
123
+ console.log(` thisUpdate : ${crl.thisUpdate}`);
124
+ console.log(` nextUpdate : ${crl.nextUpdate}`);
125
+ console.log(` revokedCount : ${crl.revokedCount}`);
126
+
127
+ // Build a Set of revoked serial numbers for fast in-process lookups
128
+ const revokedSet = new Set((crl.crl || []).map(e => e.serialNumber));
129
+ console.log(` Revoked set : ${revokedSet.size} entries loaded`);
130
+
131
+ // Cache on disk until nextUpdate
132
+ fs.writeFileSync(
133
+ path.join(__dirname, 'crl-cache.json'),
134
+ JSON.stringify({ ...crl, cachedAt: new Date().toISOString() }, null, 2)
135
+ );
136
+
137
+ // Also download DER CRL for systems that need binary format
138
+ console.log('\n[PKI] 6b. Downloading DER CRL...');
139
+ const crlDer = await client.pki.crl('der');
140
+ fs.writeFileSync(path.join(__dirname, 'certysign-intermediate.crl'), crlDer);
141
+ console.log(` certysign-intermediate.crl — ${crlDer.length} bytes`);
142
+
143
+ // ─────────────────────────────────────────────────────────────────────────
144
+ // 7. In-process revocation check using cached CRL
145
+ // ─────────────────────────────────────────────────────────────────────────
146
+ console.log('\n[PKI] 7. Local CRL check...');
147
+ const isRevoked = revokedSet.has(serialNumber);
148
+ console.log(` Serial ${serialNumber.slice(0, 12)}... : ${isRevoked ? 'REVOKED' : 'not in CRL'}`);
149
+
150
+ // ─────────────────────────────────────────────────────────────────────────
151
+ // 8. OCSP query — freshest revocation status
152
+ // ─────────────────────────────────────────────────────────────────────────
153
+ console.log('\n[PKI] 8. OCSP query...');
154
+ const ocspResult = await client.pki.ocsp(serialNumber, 'json');
155
+ const ocsp = ocspResult.data;
156
+ console.log(` status : ${ocsp.status}`);
157
+ console.log(` producedAt : ${ocsp.producedAt}`);
158
+ console.log(` responderId : ${ocsp.responderId}`);
159
+ console.log(` nextUpdate : ${ocsp.nextUpdate}`);
160
+ console.log(` signatureAlgo : ${ocsp.signatureAlgorithm}`);
161
+ if (ocsp.certIdHash) {
162
+ console.log(` CertID algo : ${ocsp.certIdHash.algorithm}`);
163
+ console.log(` issuerNameHash : ${ocsp.certIdHash.issuerNameHash?.slice(0, 16)}...`);
164
+ }
165
+
166
+ // ─────────────────────────────────────────────────────────────────────────
167
+ // 9. CA Info — inspect the trust hierarchy
168
+ // ─────────────────────────────────────────────────────────────────────────
169
+ console.log('\n[PKI] 9. CA hierarchy info...');
170
+ const info = await client.pki.info();
171
+ const caInfo = info.data;
172
+ console.log(` Root CA : ${caInfo.rootCA?.subject?.commonName}`);
173
+ console.log(` Root CA expiry : ${caInfo.rootCA?.validity?.notAfter}`);
174
+ console.log(` Inter CA : ${caInfo.intermediateCA?.subject?.commonName}`);
175
+ console.log(` Inter CA expiry: ${caInfo.intermediateCA?.validity?.notAfter}`);
176
+ console.log(` CRL URL : ${caInfo.intermediateCA?.crlUrl}`);
177
+ console.log(` OCSP URL : ${caInfo.intermediateCA?.ocspUrl}`);
178
+ console.log(` Initialised : ${caInfo.status?.initialized}`);
179
+
180
+ console.log('\n[PKI] ✓ Certificate lifecycle demo complete');
181
+ console.log('─'.repeat(60));
182
+ }
183
+
184
+ // ── Run ───────────────────────────────────────────────────────────────────────
185
+
186
+ (async () => {
187
+ try {
188
+ await runCertificateFlow();
189
+ } catch (err) {
190
+ if (err instanceof CertySignError) {
191
+ console.error(`\n[PKI] API Error (${err.statusCode}): ${err.message}`);
192
+ if (err.details) console.error('[PKI] Details:', JSON.stringify(err.details, null, 2));
193
+ } else {
194
+ console.error('\n[PKI] Error:', err.message);
195
+ }
196
+ process.exit(1);
197
+ }
198
+ })();