@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 CHANGED
@@ -19,10 +19,15 @@ Official Node.js SDK for **CertySign Trust Services** — hash-based digital sig
19
19
  - [client.hasher — Local Document Hashing](#clienthasher--local-document-hashing)
20
20
  - [client.embedder — Local Signature Embedding](#clientembedder--local-signature-embedding)
21
21
  - [client.dashboard — SDK Analytics](#clientdashboard--sdk-analytics)
22
+ - [client.identity — Identity Verification](#clientidentity--identity-verification)
22
23
  - [client.certificates — X.509 Certificate Management](#clientcertificates--x509-certificate-management)
23
24
  - [client.pki — PKI Infrastructure](#clientpki--pki-infrastructure)
24
25
  - [client.envelopes — Envelope Management](#clientenvelopes--envelope-management)
25
- - [client.legacySign — Legacy Document Signing](#clientlegacysign--legacy-document-signing)
26
+ - [client.legacySign — Legacy Document Signing](#clientlegacysign--legacy-document-signing)
27
+ - [client.webhooks — Event Notifications](#clientwebhooks--event-notifications)
28
+ - [client.billing — Wallet, Quotes and Pricing](#clientbilling--wallet-quotes-and-pricing)
29
+ - [client.signers — Sponsored Signer Certificates](#clientsigners--sponsored-signer-certificates)
30
+ - [client.request — Any Endpoint](#clientrequest--any-endpoint)
26
31
  - [Complete Examples](#complete-examples)
27
32
  - [Error Handling](#error-handling)
28
33
  - [Rate Limiting](#rate-limiting)
@@ -862,6 +867,81 @@ const { data } = await client.dashboard.getDocuments({
862
867
 
863
868
  ---
864
869
 
870
+ ### `client.identity` — Identity Verification
871
+
872
+ Verify a national ID against the issuing authority (IPRS for Kenya) and get the
873
+ biographic record — the same pipeline CertySign uses to onboard its own signers.
874
+
875
+ **Registry-first:** an ID already verified anywhere on the CertySign network is served
876
+ from our registry (`source: 'registry'`) with no authority call. A *rejected* ID is
877
+ cached briefly too, so repeated lookups of the same bad number don't incur repeated
878
+ charges. Pass `forceRefresh: true` to bypass both.
879
+
880
+ Requires the **`identity:verify`** scope, which is **not granted by default** — enable it
881
+ under *Settings → Security → SDK API Keys → Permissions*.
882
+
883
+ > **Consent is mandatory.** This processes another person's personal data, so
884
+ > `consent.obtained: true` is required — the SDK throws *before* making the call if it's
885
+ > missing. Your attestation, API key and IP are written to your tenant's audit trail on
886
+ > every call, evidencing a lawful basis under the Kenya Data Protection Act 2019.
887
+ > Store the returned `identityReference` instead of the raw ID number — the full number
888
+ > is never returned to you.
889
+
890
+ #### `verify(params)` — Verify a national ID
891
+
892
+ ```js
893
+ const { data } = await client.identity.verify({
894
+ country: 'KE',
895
+ idNumber: '12345678',
896
+ idType: 'NATIONAL_ID', // optional — this is the default
897
+ consent: {
898
+ obtained: true, // REQUIRED
899
+ reference: 'loan-app-8821', // optional — your own consent record id
900
+ purpose: 'KYC onboarding' // optional
901
+ },
902
+ // Optional cross-check hints: firstName, lastName, dob
903
+ // forceRefresh: true // skip caches, go straight to the authority
904
+ });
905
+
906
+ // data.verified — true when the authority confirmed the ID
907
+ // data.decision — 'verified' | 'rejected' | 'provisional' | 'unknown'
908
+ // data.identityReference — privacy-preserving reference; store THIS
909
+ // data.idLast4 — last 4 digits only
910
+ // data.person — fullName, firstName, middleName, lastName,
911
+ // dob, gender, nationality
912
+ // data.source — 'registry' (cached) | 'authority' (fresh lookup)
913
+ // data.cached — true when served without an authority call
914
+ // data.resultCode — authority code, e.g. '1012' = valid ID
915
+ // data.checkedAt — ISO timestamp
916
+ ```
917
+
918
+ #### `isVerified(params)` — Boolean convenience wrapper
919
+
920
+ ```js
921
+ const ok = await client.identity.isVerified({
922
+ country: 'KE', idNumber: '12345678',
923
+ consent: { obtained: true, purpose: 'KYC onboarding' }
924
+ });
925
+ ```
926
+
927
+ > A thrown error means **"we couldn't check"**, which is *not* the same as
928
+ > **"not verified"**. Never collapse the two — treat an error as retry/escalate,
929
+ > not as a failed identity.
930
+
931
+ #### Errors
932
+
933
+ | Code | HTTP | Meaning |
934
+ |---|---|---|
935
+ | `CONSENT_REQUIRED` | 403 | Consent attestation missing |
936
+ | `INSUFFICIENT_BALANCE` | 402 | Checked *before* any authority call — you're never charged for a lookup you couldn't afford |
937
+ | `COUNTRY_NOT_SUPPORTED` / `ID_TYPE_NOT_SUPPORTED` | 422 | Only `KE` / `NATIONAL_ID` today |
938
+ | `VERIFICATION_UNAVAILABLE` | 502 | Authority unreachable — not billed, retry later |
939
+
940
+ **Billing:** one `ekyc_check` per distinct ID per day. A *negative* result is billable
941
+ (the authority was queried); an *outage* is never billed.
942
+
943
+ ---
944
+
865
945
  ### `client.certificates` — X.509 Certificate Management
866
946
 
867
947
  #### `getActive()` — Get your tenant's active signing certificate
@@ -1041,6 +1121,272 @@ Legacy methods: `quickSign()`, `batchSign()`, `verifyById()`, `verifyDocument()`
1041
1121
 
1042
1122
  ---
1043
1123
 
1124
+ ### `client.webhooks` — Event Notifications
1125
+
1126
+ Register an endpoint and CertySign calls you when something finishes, instead of you
1127
+ asking repeatedly. Requires the `webhook:manage` permission on your API key.
1128
+ **Deliveries are not charged**, and neither are the status endpoints.
1129
+
1130
+ #### `create({ url, events, description? })` — Register an endpoint
1131
+
1132
+ ```js
1133
+ const { data } = await client.webhooks.create({
1134
+ url: 'https://api.example.com/hooks/certysign',
1135
+ events: ['envelope.completed', 'envelope.declined'], // omit for all events
1136
+ description: 'Production order pipeline',
1137
+ });
1138
+
1139
+ // data.secret is returned ONCE and is never readable again. Store it now.
1140
+ process.env.CERTYSIGN_WEBHOOK_SECRET = data.secret;
1141
+ ```
1142
+
1143
+ Events: `envelope.sent`, `envelope.viewed`, `envelope.signed` (per signature),
1144
+ `envelope.completed`, `envelope.declined`, `envelope.voided`, `envelope.expired`,
1145
+ `signing_session.completed`, `sponsorship.created`,
1146
+ `sponsorship.certificate_issued`, `sponsorship.verification_rejected`,
1147
+ `sponsorship.failed`, `webhook.test`.
1148
+
1149
+ #### `list()`, `remove(webhookId)`, `test(webhookId)`
1150
+
1151
+ ```js
1152
+ const { data } = await client.webhooks.list();
1153
+ await client.webhooks.test(data.webhooks[0].webhookId); // sends a webhook.test delivery
1154
+ await client.webhooks.remove(data.webhooks[0].webhookId);
1155
+ ```
1156
+
1157
+ #### `deliveries({ webhookId?, status?, limit? })`, `redeliver(deliveryId)`
1158
+
1159
+ ```js
1160
+ // What did we send, and what did your endpoint say?
1161
+ const { data } = await client.webhooks.deliveries({ status: 'failed', limit: 50 });
1162
+ for (const d of data.deliveries) {
1163
+ console.log(d.event, d.responseStatus, d.error);
1164
+ await client.webhooks.redeliver(d.deliveryId);
1165
+ }
1166
+ ```
1167
+
1168
+ #### Verifying the signature
1169
+
1170
+ Use the built-in helper. It is timing-safe, enforces a freshness window, and reports
1171
+ the two mistakes that otherwise fail silently — signing the body alone (which leaves a
1172
+ captured delivery replayable forever) and verifying after `JSON.parse` (which changes
1173
+ the bytes the signature covers).
1174
+
1175
+ ```js
1176
+ const { CertySignClient } = require('@certysign/sdk');
1177
+
1178
+ // express.raw, NOT express.json — the signature covers the bytes as sent.
1179
+ app.post('/hooks/certysign', express.raw({ type: 'application/json' }), (req, res) => {
1180
+ const { valid, reason, event } = CertySignClient.verifyWebhookSignature({
1181
+ secret: process.env.CERTYSIGN_WEBHOOK_SECRET,
1182
+ headers: req.headers,
1183
+ rawBody: req.body, // Buffer or string, never a parsed object
1184
+ toleranceSeconds: 300, // default
1185
+ });
1186
+
1187
+ if (!valid) return res.status(401).send(reason);
1188
+
1189
+ if (alreadyHandled(event.eventId)) return res.sendStatus(200); // at-least-once
1190
+ handle(event);
1191
+ res.sendStatus(200); // answer fast; do the work afterwards
1192
+ });
1193
+ ```
1194
+
1195
+ If you would rather implement it yourself: the signature is HMAC-SHA256 over
1196
+ `"<timestamp>.<raw body>"` — **not the body alone** — compared timing-safely, over the
1197
+ raw bytes, before any JSON parsing.
1198
+
1199
+ ```js
1200
+ const crypto = require('crypto');
1201
+
1202
+ // express.raw, NOT express.json — the signature covers the bytes as sent.
1203
+ app.post('/hooks/certysign', express.raw({ type: 'application/json' }), (req, res) => {
1204
+ const ts = req.get('X-CertySign-Timestamp');
1205
+ const sig = (req.get('X-CertySign-Signature') || '').replace('sha256=', '');
1206
+
1207
+ // Reject anything older than 5 minutes.
1208
+ if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400);
1209
+
1210
+ const expected = crypto
1211
+ .createHmac('sha256', process.env.CERTYSIGN_WEBHOOK_SECRET)
1212
+ .update(ts + '.' + req.body.toString())
1213
+ .digest('hex');
1214
+
1215
+ if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
1216
+ return res.sendStatus(401);
1217
+ }
1218
+
1219
+ const event = JSON.parse(req.body.toString());
1220
+ if (alreadyHandled(event.eventId)) return res.sendStatus(200); // at-least-once
1221
+
1222
+ handle(event);
1223
+ res.sendStatus(200); // answer fast; do the work afterwards
1224
+ });
1225
+ ```
1226
+
1227
+ #### Delivery, retries and duplicates
1228
+
1229
+ - Respond **2xx** quickly. Anything else counts as a failure.
1230
+ - Retries on network errors, timeouts and 5xx: 10s, 1m, 5m, 30m, 2h, then stop.
1231
+ - A **4xx other than 429 is not retried** — your endpoint rejected the shape, and the
1232
+ same bytes would be rejected again.
1233
+ - Delivery is **at-least-once**. De-duplicate on `eventId`, which is stable across
1234
+ retries and redeliveries.
1235
+ - After 20 consecutive failures the webhook is disabled and the reason recorded.
1236
+
1237
+ **Webhooks do not replace polling.** A delivery you never received is unrecoverable
1238
+ unless you can ask for the current state, so the status endpoints remain — and they
1239
+ are free to call.
1240
+
1241
+ ---
1242
+
1243
+ ### `client.billing` — Wallet, Quotes and Pricing
1244
+
1245
+ Read-only, and every call here is free — checking a balance must never itself cost
1246
+ money.
1247
+
1248
+ #### `wallet()` — Token balance and standing
1249
+
1250
+ ```js
1251
+ const { data } = await client.billing.wallet();
1252
+ console.log(`${data.availableBalance} ${data.currency} available`);
1253
+ ```
1254
+
1255
+ Spend against **`availableBalance`**, not `balance`: `balance` includes tokens already
1256
+ reserved by operations in flight. A `status` of `'suspended'` means chargeable calls
1257
+ are refused even when the balance looks healthy, so check it before concluding you can
1258
+ afford something.
1259
+
1260
+ #### `quote({ operationType, operationSubType?, quantity?, additionalSigners? })`
1261
+
1262
+ What an operation will cost, and whether you can currently cover it. Call it **before**
1263
+ a large batch: one answer up front beats `INSUFFICIENT_BALANCE` halfway through, with
1264
+ some documents signed and charged and the rest not.
1265
+
1266
+ ```js
1267
+ const { data } = await client.billing.quote({
1268
+ operationType: 'document_signing', // or 'certificate_issuance'
1269
+ operationSubType: 'ADVANCED', // e.g. 'ADVANCED', 'ADES'
1270
+ quantity: documents.length,
1271
+ });
1272
+
1273
+ if (!data.canAfford) {
1274
+ throw new Error(`Need ${data.tokensTotal}, have ${data.availableBalance}`);
1275
+ }
1276
+ ```
1277
+
1278
+ #### `transactions({ limit?, skip?, type?, status? })`, `plan()`, `pricing()`
1279
+
1280
+ ```js
1281
+ const { data } = await client.billing.transactions({ limit: 100, type: 'debit' });
1282
+ const { data: plan } = await client.billing.plan(); // your tier and its limits
1283
+ const { data: rates } = await client.billing.pricing(); // the full price list
1284
+ ```
1285
+
1286
+ ---
1287
+
1288
+ ### `client.signers` — Sponsored Signer Certificates
1289
+
1290
+ A person cannot sign until they hold a certificate. These let you pay for one on their
1291
+ behalf and watch it become usable, instead of discovering at signing time that it is
1292
+ not there.
1293
+
1294
+ #### `signingStatus(email, { issueCompletionUrl? })` — Can this person sign yet?
1295
+
1296
+ Requires `cert:status`. Free to call, so it is safe in front of every send.
1297
+
1298
+ ```js
1299
+ const { data } = await client.signers.signingStatus('jane@example.com');
1300
+
1301
+ if (!data.canSign) {
1302
+ // data.reason is NO_SIGNING_CERTIFICATE or AWAITING_IDENTITY_VERIFICATION
1303
+ console.log(data.reason);
1304
+ }
1305
+ ```
1306
+
1307
+ Worth calling **before** a batch: it turns a per-document failure — discovered halfway
1308
+ through, after the wallet has been charged — into one answer up front.
1309
+
1310
+ For someone stuck at `AWAITING_IDENTITY_VERIFICATION`, ask for a link you can redirect
1311
+ them to, so they need not hunt for the invitation email:
1312
+
1313
+ ```js
1314
+ const { data } = await client.signers.signingStatus(email, { issueCompletionUrl: true });
1315
+ if (data.pendingSponsorship?.completionUrl) {
1316
+ return redirect(data.pendingSponsorship.completionUrl);
1317
+ }
1318
+ ```
1319
+
1320
+ `issueCompletionUrl` **mints a new link and the previous one stops working**, including
1321
+ the one already in their inbox. Ask for it when you are about to redirect someone, not
1322
+ on every poll. It needs `signer:sponsor`, because minting an onboarding credential is
1323
+ the same class of act as sponsoring one.
1324
+
1325
+ #### `sponsor({ email, name?, tier?, restrictToSponsor? })` — Pay for their certificate
1326
+
1327
+ Requires `signer:sponsor`, a paid plan and a funded wallet.
1328
+
1329
+ ```js
1330
+ const { data } = await client.signers.sponsor({
1331
+ email: 'jane@example.com',
1332
+ name: 'Jane Mwangi', // shown on the invitation; the certificate carries the
1333
+ // name on their verified ID
1334
+ tier: 'ades', // 'ses' | 'ades' (default) | 'qes'
1335
+ restrictToSponsor: true, // only signs your documents (default)
1336
+ });
1337
+
1338
+ return redirect(data.completionUrl);
1339
+ ```
1340
+
1341
+ They **cannot sign when this returns** — `status` is `awaiting_member` until they
1342
+ complete identity verification. Subscribe to `sponsorship.certificate_issued` rather
1343
+ than polling for it.
1344
+
1345
+ This also makes them a **member of your organisation** once the certificate issues:
1346
+ they get an account, appear in your member list, and their certificate falls under your
1347
+ governance.
1348
+
1349
+ `restrictToSponsor: false` gives them a certificate usable anywhere, including with
1350
+ your competitors. That is a deliberate choice, not a default.
1351
+
1352
+ #### `resendSetup(email)` — Send the setup link again
1353
+
1354
+ ```js
1355
+ await client.signers.resendSetup('jane@example.com');
1356
+ ```
1357
+
1358
+ **This rotates the token**, which invalidates the link in any email already sent. That
1359
+ is the point — an old link that still works is an old link someone else can use — but
1360
+ it does mean the earlier email stops working the moment you call this.
1361
+
1362
+ ---
1363
+
1364
+ ### `client.request` — Any Endpoint
1365
+
1366
+ The API can gain an endpoint at any time. This calls one that has no typed method yet,
1367
+ so you are never blocked waiting for an SDK release.
1368
+
1369
+ ```js
1370
+ const res = await client.request('POST', '/sdk/v1/some-new-endpoint', {
1371
+ data: { foo: 'bar' },
1372
+ params: { verbose: true }, // query string
1373
+ idempotencyKey: 'batch-2026-10-01',
1374
+ });
1375
+ ```
1376
+
1377
+ Authentication, retries, idempotency-conflict handling, the error shape and the
1378
+ User-Agent all behave exactly as they do for a typed resource. The only thing you give
1379
+ up is the named method and its documentation.
1380
+
1381
+ Prefer a typed resource where one exists — it records the shape and keeps working if a
1382
+ path changes. Use this when there is no resource yet.
1383
+
1384
+ > New response fields need nothing at all: responses are passed through untouched, so
1385
+ > a field added server-side is readable immediately on whatever version you have
1386
+ > installed.
1387
+
1388
+ ---
1389
+
1044
1390
  ## Complete Examples
1045
1391
 
1046
1392
  ### PDF: PAdES Sign with signCallback (Recommended)
@@ -1311,6 +1657,14 @@ For batch workloads, use `batchHashAndSign()` or `batchSignHashes()` to sign up
1311
1657
 
1312
1658
  ## Migration from v1
1313
1659
 
1660
+ ### What's New in v2.5.1
1661
+
1662
+ - **`client.webhooks`** — register an endpoint and be told when envelopes and
1663
+ sponsorships change state, instead of polling. See
1664
+ [client.webhooks](#clientwebhooks--event-notifications).
1665
+ - The `User-Agent` now reports the real package version. It had been pinned at
1666
+ `1.0.0` for the whole 2.x line, so server logs could not tell SDK versions apart.
1667
+
1314
1668
  ### What's New in v2.2.0
1315
1669
 
1316
1670
  | Feature | Description |
package/package.json CHANGED
@@ -1,54 +1,55 @@
1
- {
2
- "name": "@certysign/sdk",
3
- "version": "2.4.0",
4
- "description": "Official Node.js SDK for CertySign — hash-based digital signing, X.509 certificates, and PKI services. Documents never leave your system.",
5
- "main": "src/index.js",
6
- "types": "src/index.d.ts",
7
- "files": [
8
- "src/",
9
- "README.md"
10
- ],
11
- "scripts": {
12
- "test": "jest --coverage",
13
- "lint": "eslint src/**/*.js",
14
- "build:types": "npx tsc --emitDeclarationOnly"
15
- },
16
- "keywords": [
17
- "certysign",
18
- "digital-signature",
19
- "pki",
20
- "x509",
21
- "pdf-signing",
22
- "document-signing",
23
- "hash-based-signing",
24
- "kenya",
25
- "pades",
26
- "xmldsig",
27
- "otp"
28
- ],
29
- "author": "CertySign Limited <sdk@certysign.io>",
30
- "license": "MIT",
31
- "engines": {
32
- "node": ">=18.0.0"
33
- },
34
- "dependencies": {
35
- "axios": "^1.6.0",
36
- "form-data": "^4.0.0",
37
- "node-forge": "^1.3.3",
38
- "pdf-lib": "^1.17.1"
39
- },
40
- "devDependencies": {
41
- "jest": "^29.7.0"
42
- },
43
- "repository": {
44
- "type": "git",
45
- "url": "git+https://github.com/certysign/sdk-node.git"
46
- },
47
- "homepage": "https://docs.certysign.io/sdk",
48
- "bugs": {
49
- "url": "https://github.com/certysign/sdk-node/issues"
50
- },
51
- "publishConfig": {
52
- "access": "public"
53
- }
54
- }
1
+ {
2
+ "name": "@certysign/sdk",
3
+ "version": "2.5.1",
4
+ "description": "Official Node.js SDK for CertySign — hash-based digital signing, X.509 certificates, and PKI services. Documents never leave your system.",
5
+ "main": "src/index.js",
6
+ "types": "src/index.d.ts",
7
+ "files": [
8
+ "src/",
9
+ "README.md"
10
+ ],
11
+ "scripts": {
12
+ "test": "jest --coverage",
13
+ "lint": "eslint src/**/*.js",
14
+ "build:types": "npx tsc --emitDeclarationOnly",
15
+ "test:durability": "node test/durability.test.js"
16
+ },
17
+ "keywords": [
18
+ "certysign",
19
+ "digital-signature",
20
+ "pki",
21
+ "x509",
22
+ "pdf-signing",
23
+ "document-signing",
24
+ "hash-based-signing",
25
+ "kenya",
26
+ "pades",
27
+ "xmldsig",
28
+ "otp"
29
+ ],
30
+ "author": "CertySign Limited <sdk@certysign.io>",
31
+ "license": "MIT",
32
+ "engines": {
33
+ "node": ">=18.0.0"
34
+ },
35
+ "dependencies": {
36
+ "axios": "^1.6.0",
37
+ "form-data": "^4.0.0",
38
+ "node-forge": "^1.3.3",
39
+ "pdf-lib": "^1.17.1"
40
+ },
41
+ "devDependencies": {
42
+ "jest": "^29.7.0"
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/certysign/sdk-node.git"
47
+ },
48
+ "homepage": "https://docs.certysign.io/sdk",
49
+ "bugs": {
50
+ "url": "https://github.com/certysign/sdk-node/issues"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ }
55
+ }