@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.
@@ -0,0 +1,170 @@
1
+ /**
2
+ * @fileoverview PkiResource — PKI infrastructure endpoints
3
+ *
4
+ * Covers:
5
+ * - crl() GET /sdk/v1/pki/crl — X.509 v2 CRL (RFC 5280)
6
+ * - ocsp() GET /sdk/v1/pki/ocsp/:sn — OCSP response (RFC 6960)
7
+ * - chain() GET /sdk/v1/pki/chain — CA certificate chain (PEM bundle)
8
+ * - info() GET /sdk/v1/pki/info — CA hierarchy metadata
9
+ *
10
+ * Typical use cases:
11
+ * - Cache the CRL locally in HIE systems for offline revocation checking
12
+ * - Perform in-process OCSP stapling
13
+ * - Download CA chain to install in a local trust store
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ class PkiResource {
19
+ /** @param {import('../lib/HttpClient').HttpClient} http */
20
+ constructor(http) {
21
+ this._http = http;
22
+ }
23
+
24
+ // ── CRL ────────────────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * Download the current Certificate Revocation List from CertySign Intermediate CA.
28
+ *
29
+ * The CRL is an RFC 5280 X.509 v2 CRL, signed by the Intermediate CA.
30
+ * - Valid for 7 days
31
+ * - Issued every 24 hours
32
+ * - Includes reasonCode per revoked certificate
33
+ *
34
+ * @param {'der'|'pem'|'json'} [format] - Response format (default: 'pem')
35
+ * @returns {Promise<Buffer|string|Object>}
36
+ * - 'der' → Buffer containing DER-encoded CRL binary
37
+ * - 'pem' → string with PEM-encoded CRL
38
+ * - 'json' → structured JSON with revocation list
39
+ *
40
+ * @example
41
+ * // Download DER CRL and write to disk for caddy/nginx OCSP stapling cache
42
+ * const derBuf = await client.pki.crl('der');
43
+ * fs.writeFileSync('/etc/pki/certysign-intermediate.crl', derBuf);
44
+ *
45
+ * @example
46
+ * // Get JSON for application-level revocation checks
47
+ * const { data } = await client.pki.crl('json');
48
+ * const revokedSerials = new Set(data.crl.map(e => e.serialNumber));
49
+ * if (revokedSerials.has(myCertSn)) throw new Error('Certificate has been revoked');
50
+ */
51
+ async crl(format = 'pem') {
52
+ if (format === 'der') {
53
+ const response = await this._http.get('/sdk/v1/pki/crl', {
54
+ accept: 'application/pkix-crl',
55
+ responseType: 'arraybuffer'
56
+ });
57
+ return Buffer.isBuffer(response) ? response : Buffer.from(response);
58
+ }
59
+
60
+ if (format === 'pem') {
61
+ return this._http.get('/sdk/v1/pki/crl', {
62
+ accept: 'application/x-pem-file',
63
+ responseType: 'text'
64
+ });
65
+ }
66
+
67
+ // Default: JSON
68
+ return this._http.get('/sdk/v1/pki/crl');
69
+ }
70
+
71
+ // ── OCSP ───────────────────────────────────────────────────────────────────
72
+
73
+ /**
74
+ * Query the OCSP responder for a specific certificate.
75
+ *
76
+ * Returns an RFC 6960 BasicOCSPResponse — signed by CertySign Intermediate CA
77
+ * using SHA-256 with RSA. The CertID uses SHA-1 hashes as required by RFC 6960.
78
+ *
79
+ * @param {string} serialNumber - Certificate serial number (hex string)
80
+ * @param {'der'|'json'} [format] - Response format (default: 'json')
81
+ * @returns {Promise<Buffer|OcspJsonResult>}
82
+ *
83
+ * @example
84
+ * // Check revocation status for a certificate
85
+ * const { data } = await client.pki.ocsp('A1B2C3D4E5F6...', 'json');
86
+ * console.log(data.status); // 'good', 'revoked', or 'unknown'
87
+ * console.log(data.thisUpdate); // ISO 8601
88
+ * console.log(data.nextUpdate); // ISO 8601 — cache until this time
89
+ *
90
+ * @example
91
+ * // Get DER response for OCSP stapling
92
+ * const derBuf = await client.pki.ocsp('A1B2C3D4E5F6...', 'der');
93
+ */
94
+ async ocsp(serialNumber, format = 'json') {
95
+ if (!serialNumber) throw new Error('pki.ocsp: serialNumber is required');
96
+
97
+ if (format === 'der') {
98
+ const response = await this._http.get(`/sdk/v1/pki/ocsp/${encodeURIComponent(serialNumber)}`, {
99
+ accept: 'application/ocsp-response',
100
+ responseType: 'arraybuffer'
101
+ });
102
+ return Buffer.isBuffer(response) ? response : Buffer.from(response);
103
+ }
104
+
105
+ return this._http.get(`/sdk/v1/pki/ocsp/${encodeURIComponent(serialNumber)}`);
106
+ }
107
+
108
+ // ── CA Chain ───────────────────────────────────────────────────────────────
109
+
110
+ /**
111
+ * Download the CertySign CA certificate chain as a PEM bundle.
112
+ *
113
+ * The bundle contains (in order):
114
+ * 1. CertySign Intermediate CA certificate
115
+ * 2. CertySign Root CA certificate
116
+ *
117
+ * Install this bundle in your trust store to verify all certificates
118
+ * issued by CertySign.
119
+ *
120
+ * @returns {Promise<string>} PEM-encoded certificate chain
121
+ *
122
+ * @example
123
+ * const chain = await client.pki.chain();
124
+ * fs.writeFileSync('/etc/ssl/certs/certysign-chain.pem', chain);
125
+ *
126
+ * // Verify a PEM cert against the chain (using openssl):
127
+ * // openssl verify -CAfile certysign-chain.pem your-cert.pem
128
+ */
129
+ chain() {
130
+ return this._http.get('/sdk/v1/pki/chain', {
131
+ accept: 'application/x-pem-file',
132
+ responseType: 'text'
133
+ });
134
+ }
135
+
136
+ // ── CA Info ────────────────────────────────────────────────────────────────
137
+
138
+ /**
139
+ * Get metadata about the CertySign CA hierarchy.
140
+ *
141
+ * Returns information about the Root CA and Intermediate CA including:
142
+ * - Subject DN
143
+ * - Validity period
144
+ * - Key algorithm and size
145
+ * - CRL and OCSP URLs
146
+ * - Serial number
147
+ *
148
+ * @returns {Promise<CaInfoResult>}
149
+ *
150
+ * @example
151
+ * const { data } = await client.pki.info();
152
+ * console.log(data.intermediateCA.subject.commonName);
153
+ * console.log(data.intermediateCA.crlUrl);
154
+ * console.log(data.status.initialized);
155
+ */
156
+ info() {
157
+ return this._http.get('/sdk/v1/pki/info');
158
+ }
159
+ }
160
+
161
+ /**
162
+ * @typedef {Object} OcspJsonResult
163
+ * @property {boolean} success
164
+ * @property {{ status: 'good'|'revoked'|'unknown', serialNumber: string,
165
+ * producedAt: string, thisUpdate: string, nextUpdate: string,
166
+ * responderId: string, revocationDate: string|null,
167
+ * revocationReason: string|null, der: string|null }} data
168
+ */
169
+
170
+ module.exports = { PkiResource };
@@ -0,0 +1,192 @@
1
+ /**
2
+ * @fileoverview SigningResource — document signing operations
3
+ *
4
+ * Covers:
5
+ * - quickSign() POST /sdk/v1/sign
6
+ * - batchSign() POST /sdk/v1/sign/batch
7
+ * - verifyById() GET /sdk/v1/verify/:envelopeId
8
+ * - verifyDocument() POST /sdk/v1/verify
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const FormData = require('form-data');
16
+
17
+ class SigningResource {
18
+ /** @param {import('../lib/HttpClient').HttpClient} http */
19
+ constructor(http) {
20
+ this._http = http;
21
+ }
22
+
23
+ // ── Quick Sign ─────────────────────────────────────────────────────────────
24
+
25
+ /**
26
+ * Sign a single document in one call.
27
+ *
28
+ * Uploads the document, creates an envelope, sends it, and signs it —
29
+ * all in a single API round-trip. Ideal for automation pipelines.
30
+ *
31
+ * @param {QuickSignOptions} options
32
+ * @returns {Promise<QuickSignResult>}
33
+ *
34
+ * @example
35
+ * const result = await client.sign.quickSign({
36
+ * document: fs.readFileSync('./claim-form.pdf'),
37
+ * filename: 'claim-form.pdf',
38
+ * signerName: 'Dr. Amina Okonkwo',
39
+ * signerEmail: 'amina.okonkwo@dha.go.ke',
40
+ * reason: 'Health records approval — DHA Kenya',
41
+ * location: 'Nairobi, Kenya',
42
+ * metadata: { claimId: 'CLM-2026-003421', department: 'PHO' }
43
+ * });
44
+ *
45
+ * console.log(result.data.envelopeId); // Track the signed envelope
46
+ * console.log(result.data.certificate.serialNumber); // Per-doc X.509 cert
47
+ */
48
+ async quickSign(options) {
49
+ const {
50
+ document,
51
+ filename = 'document.pdf',
52
+ signerName,
53
+ signerEmail,
54
+ reason = 'Digital signature',
55
+ location = 'Nairobi, Kenya',
56
+ metadata = {}
57
+ } = options;
58
+
59
+ if (!document) throw new Error('quickSign: document (Buffer|ReadStream) is required');
60
+ if (!signerName) throw new Error('quickSign: signerName is required');
61
+
62
+ const form = new FormData();
63
+ _appendDocument(form, document, filename);
64
+ form.append('signerName', signerName);
65
+ if (signerEmail) form.append('signerEmail', signerEmail);
66
+ form.append('reason', reason);
67
+ form.append('location', location);
68
+ if (Object.keys(metadata).length) {
69
+ form.append('metadata', JSON.stringify(metadata));
70
+ }
71
+
72
+ return this._http.post('/sdk/v1/sign', { formData: form });
73
+ }
74
+
75
+ // ── Batch Sign ─────────────────────────────────────────────────────────────
76
+
77
+ /**
78
+ * Sign multiple documents in a single API call.
79
+ *
80
+ * Returns one result per document. Each document gets its own
81
+ * envelope and per-document X.509 certificate.
82
+ *
83
+ * @param {BatchSignOptions} options
84
+ * @returns {Promise<BatchSignResult>}
85
+ *
86
+ * @example
87
+ * const result = await client.sign.batchSign({
88
+ * documents: [
89
+ * { data: fs.readFileSync('./invoice-001.pdf'), filename: 'invoice-001.pdf' },
90
+ * { data: fs.readFileSync('./invoice-002.pdf'), filename: 'invoice-002.pdf' }
91
+ * ],
92
+ * signerName: 'NHIF Finance System',
93
+ * signerEmail: 'invoicing@nhif.or.ke',
94
+ * reason: 'Batch invoice processing'
95
+ * });
96
+ */
97
+ async batchSign(options) {
98
+ const {
99
+ documents,
100
+ signerName,
101
+ signerEmail,
102
+ reason = 'Batch digital signature',
103
+ location = 'Nairobi, Kenya',
104
+ metadata = {}
105
+ } = options;
106
+
107
+ if (!documents?.length) throw new Error('batchSign: documents array is required');
108
+ if (!signerName) throw new Error('batchSign: signerName is required');
109
+
110
+ const form = new FormData();
111
+ for (const doc of documents) {
112
+ _appendDocument(form, doc.data, doc.filename || 'document.pdf');
113
+ }
114
+ form.append('signerName', signerName);
115
+ if (signerEmail) form.append('signerEmail', signerEmail);
116
+ form.append('reason', reason);
117
+ form.append('location', location);
118
+ if (Object.keys(metadata).length) {
119
+ form.append('metadata', JSON.stringify(metadata));
120
+ }
121
+
122
+ return this._http.post('/sdk/v1/sign/batch', { formData: form });
123
+ }
124
+
125
+ // ── Verify ─────────────────────────────────────────────────────────────────
126
+
127
+ /**
128
+ * Verify the signature and certificate status of a previously signed envelope.
129
+ *
130
+ * @param {string} envelopeId
131
+ * @returns {Promise<VerifyResult>}
132
+ *
133
+ * @example
134
+ * const result = await client.sign.verifyById('env_abc123');
135
+ * if (result.data.valid) {
136
+ * console.log('Signature verified:', result.data.signerName);
137
+ * }
138
+ */
139
+ verifyById(envelopeId) {
140
+ if (!envelopeId) throw new Error('verifyById: envelopeId is required');
141
+ return this._http.get(`/sdk/v1/verify/${encodeURIComponent(envelopeId)}`);
142
+ }
143
+
144
+ /**
145
+ * Verify a document by uploading it (checks embedded signature).
146
+ *
147
+ * @param {Buffer|ReadStream} document
148
+ * @param {string} [filename]
149
+ * @returns {Promise<VerifyResult>}
150
+ */
151
+ verifyDocument(document, filename = 'document.pdf') {
152
+ const form = new FormData();
153
+ _appendDocument(form, document, filename);
154
+ return this._http.post('/sdk/v1/verify', { formData: form });
155
+ }
156
+ }
157
+
158
+ // ── Helpers ──────────────────────────────────────────────────────────────────
159
+
160
+ function _appendDocument(form, document, filename) {
161
+ if (Buffer.isBuffer(document)) {
162
+ form.append('document', document, { filename, contentType: 'application/pdf' });
163
+ } else if (document && typeof document.pipe === 'function') {
164
+ // ReadStream
165
+ form.append('document', document, { filename, contentType: 'application/pdf' });
166
+ } else {
167
+ throw new Error(`Document must be a Buffer or ReadStream, got ${typeof document}`);
168
+ }
169
+ }
170
+
171
+ /**
172
+ * @typedef {Object} QuickSignOptions
173
+ * @property {Buffer|import('fs').ReadStream} document - PDF file contents
174
+ * @property {string} [filename] - Filename for storage (default: 'document.pdf')
175
+ * @property {string} signerName - Full name of the signer
176
+ * @property {string} [signerEmail] - Email address of the signer
177
+ * @property {string} [reason] - Reason for signing
178
+ * @property {string} [location] - Physical signing location
179
+ * @property {Object} [metadata] - Custom key/value metadata stored with the envelope
180
+ */
181
+
182
+ /**
183
+ * @typedef {Object} BatchSignOptions
184
+ * @property {{ data: Buffer|import('fs').ReadStream, filename?: string }[]} documents
185
+ * @property {string} signerName
186
+ * @property {string} [signerEmail]
187
+ * @property {string} [reason]
188
+ * @property {string} [location]
189
+ * @property {Object} [metadata]
190
+ */
191
+
192
+ module.exports = { SigningResource };