@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/README.md +355 -1
- package/package.json +55 -54
- package/src/index.js +372 -214
- package/src/lib/BillingResource.js +90 -0
- package/src/lib/EnvelopeResource.js +306 -249
- package/src/lib/HashSigningResource.js +9 -2
- package/src/lib/HttpClient.js +219 -198
- package/src/lib/IdentityResource.js +153 -0
- package/src/lib/SignerResource.js +120 -0
- package/src/lib/SigningResource.js +11 -7
- package/src/lib/WebhookResource.js +88 -0
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 {
|
|
29
|
-
const {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* - client.
|
|
40
|
-
* - client.
|
|
41
|
-
* - client.
|
|
42
|
-
* - client.
|
|
43
|
-
* - client.
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (!
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
this.
|
|
119
|
-
this.
|
|
120
|
-
this.
|
|
121
|
-
this.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
this.
|
|
126
|
-
|
|
127
|
-
this.
|
|
128
|
-
this.
|
|
129
|
-
this.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
*
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
+
};
|