@certysign/sdk 2.4.0 → 2.5.1

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/src/index.js CHANGED
@@ -1,214 +1,372 @@
1
- /**
2
- * @fileoverview CertySign SDK for Node.js
3
- *
4
- * Official client library for CertySign Trust Services — digital document
5
- * signing, X.509 certificate issuance, and PKI operations for East Africa.
6
- *
7
- * @example Basic setup
8
- * ```js
9
- * const { CertySignClient } = require('@certysign/sdk');
10
- *
11
- * const client = new CertySignClient({
12
- * publicKey: 'cs_pk_...',
13
- * secretKey: 'cs_sk_...'
14
- * });
15
- * ```
16
- */
17
-
18
- 'use strict';
19
-
20
- const { HttpClient, CertySignError } = require('./lib/HttpClient');
21
- const { SigningResource } = require('./lib/SigningResource');
22
- const { HashSigningResource } = require('./lib/HashSigningResource');
23
- const { CertificateResource } = require('./lib/CertificateResource');
24
- const { PkiResource } = require('./lib/PkiResource');
25
- const { EnvelopeResource } = require('./lib/EnvelopeResource');
26
- const { SigningSessionResource } = require('./lib/SigningSessionResource');
27
- const { DashboardResource } = require('./lib/DashboardResource');
28
- const { DocumentHasher } = require('./lib/DocumentHasher');
29
- const { SignatureEmbedder } = require('./lib/SignatureEmbedder');
30
-
31
- /**
32
- * CertySign API client.
33
- *
34
- * Resources are accessed as properties:
35
- * - client.sign — hash-based document signing (documents stay local)
36
- * - client.sessions — multi-recipient signing sessions with OTP verification
37
- * - client.dashboard — SDK usage analytics (stats, recipients, documents)
38
- * - client.certificates — X.509 certificate lifecycle (issue, verify, status, getActive)
39
- * - client.pki — PKI infrastructure (CRL, OCSP, CA chain, info)
40
- * - client.envelopes — envelope management (create, upload, send, sign, audit)
41
- * - client.hasher — local document hashing utility
42
- * - client.embedder — local signature embedding (PDF, XML, JSON)
43
- * - client.legacySign — legacy file-upload signing (deprecated)
44
- *
45
- * @example Hash-based signing (documents never leave your system)
46
- * const { CertySignClient } = require('@certysign/sdk');
47
- *
48
- * const client = new CertySignClient({
49
- * publicKey: process.env.CERTYSIGN_PUBLIC_KEY,
50
- * secretKey: process.env.CERTYSIGN_SECRET_KEY,
51
- * environment: 'production'
52
- * });
53
- *
54
- * // 1. Hash locally → sign remotely → embed locally
55
- * const result = await client.sign.hashAndSign({
56
- * document: fs.readFileSync('./contract.pdf'),
57
- * fileName: 'contract.pdf',
58
- * reason: 'Contract approval'
59
- * });
60
- *
61
- * // 2. Embed signature into the PDF on your system
62
- * const signedPdf = await client.embedder.embedInPdf(
63
- * fs.readFileSync('./contract.pdf'),
64
- * {
65
- * signature: result.data.signature,
66
- * certificate: result.data.certificate,
67
- * chain: result.data.chain,
68
- * signerName: 'Dr. Amina Okonkwo',
69
- * reason: 'Contract approval',
70
- * certSerialNumber: result.data.certSerialNumber
71
- * }
72
- * );
73
- * fs.writeFileSync('./contract-signed.pdf', signedPdf);
74
- */
75
- class CertySignClient {
76
- /**
77
- * @param {ClientOptions} options
78
- */
79
- constructor(options = {}) {
80
- const {
81
- publicKey,
82
- secretKey,
83
- baseUrl,
84
- environment = 'production',
85
- timeout,
86
- retries,
87
- debug = false,
88
- tsaUrl,
89
- } = options;
90
-
91
- if (!publicKey) throw new Error('CertySignClient: publicKey is required');
92
- if (!secretKey) throw new Error('CertySignClient: secretKey is required');
93
-
94
- // Resolve base URL from environment if not explicitly provided
95
- const resolvedBaseUrl = baseUrl ?? CertySignClient.BASE_URLS[environment];
96
- if (!resolvedBaseUrl) {
97
- throw new Error(
98
- `CertySignClient: unknown environment "${environment}". ` +
99
- `Expected one of: ${Object.keys(CertySignClient.BASE_URLS).join(', ')} ` +
100
- `or provide baseUrl directly.`
101
- );
102
- }
103
-
104
- this._http = new HttpClient({
105
- publicKey,
106
- secretKey,
107
- baseUrl: resolvedBaseUrl,
108
- timeout,
109
- retries,
110
- debug
111
- });
112
-
113
- // ── Resource objects ──
114
- this.sign = new HashSigningResource(this._http); // Hash-based signing (documents stay local)
115
- this.legacySign = new SigningResource(this._http); // Legacy file-upload signing
116
- this.certificates = new CertificateResource(this._http);
117
- this.pki = new PkiResource(this._http);
118
- this.envelopes = new EnvelopeResource(this._http);
119
- this.sessions = new SigningSessionResource(this._http); // Multi-recipient signing sessions
120
- this.dashboard = new DashboardResource(this._http); // SDK usage analytics
121
- this.hasher = new DocumentHasher(); // Local document hashing
122
-
123
- // TSA URL: explicit > environment-derived
124
- const resolvedTsaUrl = tsaUrl ?? CertySignClient.TSA_URLS[environment] ?? null;
125
- this.embedder = new SignatureEmbedder({ tsaUrl: resolvedTsaUrl }); // PAdES-T when TSA URL available
126
-
127
- this.publicKey = publicKey;
128
- this.environment = environment;
129
- this.baseUrl = resolvedBaseUrl;
130
- this.tsaUrl = resolvedTsaUrl;
131
- }
132
-
133
- /**
134
- * Environment → base URL mapping.
135
- * Override any entry via the `baseUrl` constructor option.
136
- */
137
- static get BASE_URLS() {
138
- return {
139
- production: 'https://core.certysign.io',
140
- staging: 'https://service.certysign.io',
141
- development: 'http://localhost:8000',
142
- test: 'http://localhost:8000'
143
- };
144
- }
145
-
146
- /**
147
- * Environment → TSA URL mapping.
148
- * Override via the `tsaUrl` constructor option.
149
- */
150
- static get TSA_URLS() {
151
- return {
152
- production: 'https://tsa.certysign.io',
153
- staging: 'https://tsa-staging.certysign.io',
154
- development: 'http://localhost:5015',
155
- test: 'http://localhost:5015'
156
- };
157
- }
158
-
159
- /**
160
- * Verify that the API key is valid and return the key metadata.
161
- * Useful as a "ping" / health check at startup.
162
- *
163
- * @returns {Promise<{ success: boolean, data: { keyName, permissions, environment, tenantId } }>}
164
- *
165
- * @example
166
- * const { data } = await client.ping();
167
- * console.log('Connected as:', data.keyName);
168
- * console.log('Permissions:', data.permissions);
169
- */
170
- async ping() {
171
- // Issue a lightweight request; GET /pki/info is cheap and requires pki:info
172
- try {
173
- const result = await this._http.get('/sdk/v1/pki/info');
174
- return {
175
- success: true,
176
- data: {
177
- keyName: result.data?.keyName,
178
- permissions: result.data?.permissions,
179
- environment: result.data?.environment ?? this.environment,
180
- tenantId: result.data?.tenantId,
181
- caInitialized: result.data?.status?.initialized
182
- }
183
- };
184
- } catch (err) {
185
- return { success: false, error: err.message, code: err.code };
186
- }
187
- }
188
- }
189
-
190
- /**
191
- * @typedef {Object} ClientOptions
192
- * @property {string} publicKey - API public key (cs_pk_...)
193
- * @property {string} secretKey - API secret key (cs_sk_...)
194
- * @property {'production'|'staging'|'development'|'test'} [environment] - Target environment (default: 'production')
195
- * @property {string} [baseUrl] - Override the API base URL (e.g. for self-hosted)
196
- * @property {number} [timeout] - Request timeout in ms (default: 30000)
197
- * @property {number} [retries] - Max retries on transient errors (default: 3)
198
- * @property {boolean} [debug] - Log HTTP requests/responses (default: false)
199
- */
200
-
201
- module.exports = {
202
- CertySignClient,
203
- CertySignError,
204
- DocumentHasher,
205
- SignatureEmbedder,
206
- HashSigningResource,
207
- SigningSessionResource,
208
- DashboardResource,
209
- // Legacy exports
210
- SigningResource,
211
- CertificateResource,
212
- PkiResource,
213
- EnvelopeResource
214
- };
1
+ /**
2
+ * @fileoverview CertySign SDK for Node.js
3
+ *
4
+ * Official client library for CertySign Trust Services — digital document
5
+ * signing, X.509 certificate issuance, and PKI operations for East Africa.
6
+ *
7
+ * @example Basic setup
8
+ * ```js
9
+ * const { CertySignClient } = require('@certysign/sdk');
10
+ *
11
+ * const client = new CertySignClient({
12
+ * publicKey: 'cs_pk_...',
13
+ * secretKey: 'cs_sk_...'
14
+ * });
15
+ * ```
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ const { HttpClient, CertySignError } = require('./lib/HttpClient');
21
+ const { SigningResource } = require('./lib/SigningResource');
22
+ const { HashSigningResource } = require('./lib/HashSigningResource');
23
+ const { CertificateResource } = require('./lib/CertificateResource');
24
+ const { PkiResource } = require('./lib/PkiResource');
25
+ const { EnvelopeResource } = require('./lib/EnvelopeResource');
26
+ const { SigningSessionResource } = require('./lib/SigningSessionResource');
27
+ const { DashboardResource } = require('./lib/DashboardResource');
28
+ const { IdentityResource } = require('./lib/IdentityResource');
29
+ const { BillingResource } = require('./lib/BillingResource');
30
+ const { SignerResource } = require('./lib/SignerResource');
31
+ const { WebhookResource } = require('./lib/WebhookResource');
32
+ const { DocumentHasher } = require('./lib/DocumentHasher');
33
+ const { SignatureEmbedder } = require('./lib/SignatureEmbedder');
34
+
35
+ /**
36
+ * CertySign API client.
37
+ *
38
+ * Resources are accessed as properties:
39
+ * - client.sign — hash-based document signing (documents stay local)
40
+ * - client.sessions — multi-recipient signing sessions with OTP verification
41
+ * - client.dashboard — SDK usage analytics (stats, recipients, documents)
42
+ * - client.certificates — X.509 certificate lifecycle (issue, verify, status, getActive)
43
+ * - client.pki — PKI infrastructure (CRL, OCSP, CA chain, info)
44
+ * - client.envelopes — envelope management (create, upload, send, sign, audit)
45
+ * - client.hasher — local document hashing utility
46
+ * - client.embedder — local signature embedding (PDF, XML, JSON)
47
+ * - client.legacySign — legacy file-upload signing (deprecated)
48
+ *
49
+ * @example Hash-based signing (documents never leave your system)
50
+ * const { CertySignClient } = require('@certysign/sdk');
51
+ *
52
+ * const client = new CertySignClient({
53
+ * publicKey: process.env.CERTYSIGN_PUBLIC_KEY,
54
+ * secretKey: process.env.CERTYSIGN_SECRET_KEY,
55
+ * environment: 'production'
56
+ * });
57
+ *
58
+ * // 1. Hash locally → sign remotely → embed locally
59
+ * const result = await client.sign.hashAndSign({
60
+ * document: fs.readFileSync('./contract.pdf'),
61
+ * fileName: 'contract.pdf',
62
+ * reason: 'Contract approval'
63
+ * });
64
+ *
65
+ * // 2. Embed signature into the PDF on your system
66
+ * const signedPdf = await client.embedder.embedInPdf(
67
+ * fs.readFileSync('./contract.pdf'),
68
+ * {
69
+ * signature: result.data.signature,
70
+ * certificate: result.data.certificate,
71
+ * chain: result.data.chain,
72
+ * signerName: 'Dr. Amina Okonkwo',
73
+ * reason: 'Contract approval',
74
+ * certSerialNumber: result.data.certSerialNumber
75
+ * }
76
+ * );
77
+ * fs.writeFileSync('./contract-signed.pdf', signedPdf);
78
+ */
79
+ class CertySignClient {
80
+ /**
81
+ * @param {ClientOptions} options
82
+ */
83
+ constructor(options = {}) {
84
+ const {
85
+ publicKey,
86
+ secretKey,
87
+ baseUrl,
88
+ environment = 'production',
89
+ timeout,
90
+ retries,
91
+ debug = false,
92
+ tsaUrl,
93
+ } = options;
94
+
95
+ if (!publicKey) throw new Error('CertySignClient: publicKey is required');
96
+ if (!secretKey) throw new Error('CertySignClient: secretKey is required');
97
+
98
+ // Resolve base URL from environment if not explicitly provided
99
+ const resolvedBaseUrl = baseUrl ?? CertySignClient.BASE_URLS[environment];
100
+ if (!resolvedBaseUrl) {
101
+ throw new Error(
102
+ `CertySignClient: unknown environment "${environment}". ` +
103
+ `Expected one of: ${Object.keys(CertySignClient.BASE_URLS).join(', ')} ` +
104
+ `or provide baseUrl directly.`
105
+ );
106
+ }
107
+
108
+ this._http = new HttpClient({
109
+ publicKey,
110
+ secretKey,
111
+ baseUrl: resolvedBaseUrl,
112
+ timeout,
113
+ retries,
114
+ debug
115
+ });
116
+
117
+ // ── Resource objects ──
118
+ this.sign = new HashSigningResource(this._http); // Hash-based signing (documents stay local)
119
+ this.legacySign = new SigningResource(this._http); // Legacy file-upload signing
120
+ this.certificates = new CertificateResource(this._http);
121
+ this.pki = new PkiResource(this._http);
122
+ this.envelopes = new EnvelopeResource(this._http);
123
+ this.sessions = new SigningSessionResource(this._http); // Multi-recipient signing sessions
124
+ this.dashboard = new DashboardResource(this._http); // SDK usage analytics
125
+ this.identity = new IdentityResource(this._http); // National-ID verification (IPRS)
126
+ this.billing = new BillingResource(this._http); // Balance, quotes, plan (read-only)
127
+ this.signers = new SignerResource(this._http); // Sponsor + check the people you sign for
128
+ this.webhooks = new WebhookResource(this._http); // Be told, instead of polling
129
+ this.hasher = new DocumentHasher(); // Local document hashing
130
+
131
+ // TSA URL: explicit > environment-derived
132
+ const resolvedTsaUrl = tsaUrl ?? CertySignClient.TSA_URLS[environment] ?? null;
133
+ this.embedder = new SignatureEmbedder({ tsaUrl: resolvedTsaUrl }); // PAdES-T when TSA URL available
134
+
135
+ this.publicKey = publicKey;
136
+ this.environment = environment;
137
+ this.baseUrl = resolvedBaseUrl;
138
+ this.tsaUrl = resolvedTsaUrl;
139
+ }
140
+
141
+ /**
142
+ * Environment → base URL mapping.
143
+ * Override any entry via the `baseUrl` constructor option.
144
+ */
145
+ static get BASE_URLS() {
146
+ return {
147
+ production: 'https://core.certysign.io',
148
+ staging: 'https://service.certysign.io',
149
+ development: 'http://localhost:8000',
150
+ test: 'http://localhost:8000'
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Environment → TSA URL mapping.
156
+ * Override via the `tsaUrl` constructor option.
157
+ */
158
+ static get TSA_URLS() {
159
+ return {
160
+ production: 'https://tsa.certysign.io',
161
+ staging: 'https://tsa-staging.certysign.io',
162
+ development: 'http://localhost:5015',
163
+ test: 'http://localhost:5015'
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Check that the API key works, as a startup health check.
169
+ *
170
+ * There is no dedicated identity endpoint, so this issues the cheapest
171
+ * authenticated call there is — GET /sdk/v1/pki/info — and reports whether it was
172
+ * accepted. A true `ok` means the credentials authenticated and the key holds
173
+ * `pki:info`; it does NOT prove any other scope is granted.
174
+ *
175
+ * It used to claim to return keyName, permissions and tenantId. /pki/info returns
176
+ * CA metadata and has never carried any of them, so every one of those fields came
177
+ * back undefined — a health check that silently reported nothing about the key it
178
+ * was supposed to be checking.
179
+ *
180
+ * @returns {Promise<{ ok: boolean, environment: string, caInitialized?: boolean, error?: string, code?: string }>}
181
+ *
182
+ * @example
183
+ * const res = await client.ping();
184
+ * if (!res.ok) throw new Error(`CertySign unreachable: ${res.error}`);
185
+ */
186
+ async ping() {
187
+ try {
188
+ const result = await this._http.get('/sdk/v1/pki/info');
189
+ const info = result?.data ?? result;
190
+ return {
191
+ ok: true,
192
+ environment: this.environment,
193
+ caInitialized: info?.initialized ?? info?.status?.initialized,
194
+ };
195
+ } catch (err) {
196
+ return { ok: false, environment: this.environment, error: err.message, code: err.code };
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Call any CertySign endpoint, including ones this SDK version has no method for.
202
+ *
203
+ * This exists so the backend can ship an endpoint and you can use it the same
204
+ * day, without waiting for an SDK release. Everything a typed resource gives you
205
+ * still applies: authentication, retries, idempotency-conflict handling, the
206
+ * error shape and the User-Agent. The only thing you give up is the named
207
+ * method and its JSDoc.
208
+ *
209
+ * Prefer a typed resource where one exists — it documents the shape and will keep
210
+ * working if a path changes. Reach for this when there is no resource yet.
211
+ *
212
+ * @param {'GET'|'POST'|'PUT'|'PATCH'|'DELETE'} method
213
+ * @param {string} path e.g. '/sdk/v1/some-new-endpoint'
214
+ * @param {object} [options]
215
+ * @param {object} [options.data] JSON body
216
+ * @param {object} [options.params] query string
217
+ * @param {object} [options.headers] extra headers
218
+ * @param {string} [options.idempotencyKey]
219
+ * @returns {Promise<any>} the parsed response body
220
+ *
221
+ * @example
222
+ * // An endpoint added to the API after this SDK was published:
223
+ * const res = await client.request('POST', '/sdk/v1/envelopes/bulk-void', {
224
+ * data: { envelopeIds: ids, reason: 'duplicate batch' },
225
+ * });
226
+ */
227
+ request(method, path, options = {}) {
228
+ if (!method) throw new Error('client.request: method is required (GET, POST, PUT, PATCH, DELETE)');
229
+ if (!path) throw new Error('client.request: path is required (e.g. "/sdk/v1/...")');
230
+ if (!String(path).startsWith('/')) {
231
+ throw new Error(`client.request: path must start with "/" — got "${path}"`);
232
+ }
233
+ return this._http.request(String(method).toUpperCase(), path, options);
234
+ }
235
+
236
+ /**
237
+ * Verify an incoming webhook delivery.
238
+ *
239
+ * Every integrator otherwise hand-writes this, and the two ways to get it wrong
240
+ * both fail silently rather than loudly:
241
+ *
242
+ * - signing the body alone instead of `timestamp + "." + body`, which leaves a
243
+ * captured delivery replayable forever; and
244
+ * - verifying after `JSON.parse`, because re-serialising changes the bytes the
245
+ * signature was computed over.
246
+ *
247
+ * So pass the RAW body: a Buffer or the original string, never a parsed object.
248
+ * With Express that means `express.raw({ type: 'application/json' })`, not
249
+ * `express.json()`.
250
+ *
251
+ * Comparison is timing-safe, and a delivery older than `toleranceSeconds` is
252
+ * rejected even when the signature is good.
253
+ *
254
+ * @param {object} opts
255
+ * @param {string} opts.secret the whsec_... shown once at creation
256
+ * @param {object} opts.headers the request headers (case-insensitive)
257
+ * @param {Buffer|string} opts.rawBody the body exactly as received
258
+ * @param {number} [opts.toleranceSeconds=300] reject deliveries older than this
259
+ * @returns {{ valid: boolean, reason?: string, event?: object }}
260
+ * `event` is the parsed payload, present only when valid is true.
261
+ *
262
+ * @example
263
+ * app.post('/hooks', express.raw({ type: 'application/json' }), (req, res) => {
264
+ * const { valid, reason, event } = CertySignClient.verifyWebhookSignature({
265
+ * secret: process.env.CERTYSIGN_WEBHOOK_SECRET,
266
+ * headers: req.headers,
267
+ * rawBody: req.body,
268
+ * });
269
+ * if (!valid) return res.status(401).send(reason);
270
+ *
271
+ * if (alreadyHandled(event.eventId)) return res.sendStatus(200); // at-least-once
272
+ * handle(event);
273
+ * res.sendStatus(200);
274
+ * });
275
+ */
276
+ static verifyWebhookSignature({ secret, headers, rawBody, toleranceSeconds = 300 } = {}) {
277
+ const crypto = require('crypto');
278
+
279
+ if (!secret) return { valid: false, reason: 'secret is required' };
280
+ if (!headers) return { valid: false, reason: 'headers are required' };
281
+ if (rawBody === undefined || rawBody === null) {
282
+ return { valid: false, reason: 'rawBody is required' };
283
+ }
284
+
285
+ // A parsed object means express.json() ran and the original bytes are gone.
286
+ // Say so plainly, because the signature would simply never match and the
287
+ // cause is not obvious from a bare "invalid signature".
288
+ if (typeof rawBody === 'object' && !Buffer.isBuffer(rawBody)) {
289
+ return {
290
+ valid: false,
291
+ reason: 'rawBody looks parsed, not raw. The signature covers the bytes as sent, '
292
+ + "so use express.raw({ type: 'application/json' }) and pass req.body "
293
+ + 'unmodified.',
294
+ };
295
+ }
296
+
297
+ // Headers arrive lower-cased from Node, but a framework may not.
298
+ const pick = (name) => {
299
+ const want = name.toLowerCase();
300
+ if (typeof headers.get === 'function') return headers.get(name) ?? undefined;
301
+ const hit = Object.keys(headers).find((k) => k.toLowerCase() === want);
302
+ return hit ? headers[hit] : undefined;
303
+ };
304
+
305
+ const timestamp = pick('X-CertySign-Timestamp');
306
+ const signature = String(pick('X-CertySign-Signature') || '').replace(/^sha256=/, '');
307
+
308
+ if (!timestamp) return { valid: false, reason: 'missing X-CertySign-Timestamp header' };
309
+ if (!signature) return { valid: false, reason: 'missing X-CertySign-Signature header' };
310
+
311
+ const ts = Number(timestamp);
312
+ if (!Number.isFinite(ts)) return { valid: false, reason: 'X-CertySign-Timestamp is not a number' };
313
+
314
+ if (toleranceSeconds > 0) {
315
+ const age = Math.abs(Date.now() / 1000 - ts);
316
+ if (age > toleranceSeconds) {
317
+ return { valid: false, reason: `delivery is ${Math.round(age)}s old, outside the ${toleranceSeconds}s tolerance` };
318
+ }
319
+ }
320
+
321
+ const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(String(rawBody), 'utf8');
322
+ const expected = crypto
323
+ .createHmac('sha256', secret)
324
+ .update(Buffer.concat([Buffer.from(`${timestamp}.`, 'utf8'), body]))
325
+ .digest('hex');
326
+
327
+ // timingSafeEqual throws on a length mismatch, so compare digests of a fixed
328
+ // length rather than the raw hex.
329
+ const a = crypto.createHash('sha256').update(signature, 'utf8').digest();
330
+ const b = crypto.createHash('sha256').update(expected, 'utf8').digest();
331
+ if (!crypto.timingSafeEqual(a, b)) return { valid: false, reason: 'signature does not match' };
332
+
333
+ try {
334
+ return { valid: true, event: JSON.parse(body.toString('utf8')) };
335
+ } catch {
336
+ // Signature is good, so this came from us; the caller still gets the bytes.
337
+ return { valid: true, event: undefined, reason: 'signature valid but body is not JSON' };
338
+ }
339
+ }
340
+ }
341
+
342
+ /**
343
+ * @typedef {Object} ClientOptions
344
+ * @property {string} publicKey - API public key (cs_pk_...)
345
+ * @property {string} secretKey - API secret key (cs_sk_...)
346
+ * @property {'production'|'staging'|'development'|'test'} [environment] - Target environment (default: 'production')
347
+ * @property {string} [baseUrl] - Override the API base URL (e.g. for self-hosted)
348
+ * @property {number} [timeout] - Request timeout in ms (default: 30000)
349
+ * @property {number} [retries] - Max retries on transient errors (default: 3)
350
+ * @property {boolean} [debug] - Log HTTP requests/responses (default: false)
351
+ */
352
+
353
+ module.exports = {
354
+ CertySignClient,
355
+ CertySignError,
356
+ DocumentHasher,
357
+ SignatureEmbedder,
358
+ HashSigningResource,
359
+ SigningSessionResource,
360
+ DashboardResource,
361
+ IdentityResource,
362
+ // Added in 2.5.0. Exported like every other resource so a caller can type-check
363
+ // or extend them; the client wires them up as client.signers / client.billing.
364
+ SignerResource,
365
+ BillingResource,
366
+ WebhookResource,
367
+ // Legacy exports
368
+ SigningResource,
369
+ CertificateResource,
370
+ PkiResource,
371
+ EnvelopeResource
372
+ };