@didit-protocol/sdk-node 0.0.0-stage → 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Didit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,362 @@
1
- # Temporary Holding Version
1
+ # Didit TypeScript / JavaScript SDK
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The official Didit TypeScript and JavaScript library.
4
+ Identity verification (KYC), business verification (KYB), AML screening, biometrics, transaction monitoring, Travel Rule and customer authentication, from your backend.
5
+
6
+ - Every server endpoint of the [Didit API](https://docs.didit.me), with full types, generated from the OpenAPI contract.
7
+ - Webhook verification with typed events, and Didit Authentication token verification.
8
+ - No runtime dependencies. Runs on Node.js 18 or later, Bun, Deno and edge runtimes with `fetch` and Web Crypto (Cloudflare Workers, Vercel Edge).
9
+ - ESM and CommonJS, TypeScript strict.
10
+
11
+ ## Installation
12
+
13
+ ```sh
14
+ npm install @didit-protocol/sdk-node
15
+ # or: pnpm add @didit-protocol/sdk-node / yarn add @didit-protocol/sdk-node / bun add @didit-protocol/sdk-node
16
+ ```
17
+
18
+ ## Quickstart
19
+
20
+ Create a verification session, send the user to its URL, read the decision, and verify the webhook Didit sends when the status changes.
21
+
22
+ ```ts
23
+ import Didit from "@didit-protocol/sdk-node";
24
+
25
+ const didit = new Didit(); // reads DIDIT_API_KEY
26
+
27
+ // 1. Create a session and send the user to session.url.
28
+ const session = await didit.sessions.create({
29
+ workflow_id: "11111111-2222-3333-4444-555555555555",
30
+ vendor_data: "user-42",
31
+ callback: "https://example.com/verification/done",
32
+ });
33
+ console.log(session.url);
34
+
35
+ // 2. Later, read the decision.
36
+ const decision = await didit.sessions.retrieveDecision(session.session_id);
37
+ console.log(decision.status, decision.created_at); // "Approved", a Date
38
+ ```
39
+
40
+ ```ts
41
+ // 3. Verify the webhook (Express). Pass the raw body, not parsed JSON.
42
+ import express from "express";
43
+ import { webhooks, WebhookVerificationError } from "@didit-protocol/sdk-node";
44
+
45
+ const app = express();
46
+ app.post("/webhooks/didit", express.raw({ type: "application/json" }), async (req, res) => {
47
+ // express.raw leaves req.body empty for other content types.
48
+ if (!Buffer.isBuffer(req.body)) return res.status(400).send("expected a JSON body");
49
+ try {
50
+ const event = await webhooks.verify(req.body, req.headers, process.env.DIDIT_WEBHOOK_SECRET!);
51
+ if (event.webhook_type === "status.updated") {
52
+ console.log(event.session_id, event.status);
53
+ }
54
+ res.sendStatus(200);
55
+ } catch (error) {
56
+ if (error instanceof WebhookVerificationError) return res.status(400).send(error.reason);
57
+ throw error;
58
+ }
59
+ });
60
+ ```
61
+
62
+ More runnable examples, including webhook receivers for Express, Fastify, Hono, Next.js, Cloudflare Workers, Bun and Deno, are in [`examples/`](examples/).
63
+
64
+ ## Configuration
65
+
66
+ ```ts
67
+ const didit = new Didit({
68
+ apiKey: "...", // default: DIDIT_API_KEY
69
+ organizationId: "...", // default: DIDIT_ORGANIZATION_ID
70
+ applicationId: "...", // default: DIDIT_APPLICATION_ID
71
+ timeout: 60_000, // milliseconds per attempt
72
+ maxRetries: 2,
73
+ });
74
+ ```
75
+
76
+ | Option | Default | Notes |
77
+ | --------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
78
+ | `apiKey` | `DIDIT_API_KEY` | Sent as `x-api-key`, only to `baseURL`. A client without a key can still use `didit.account` and verify webhooks. |
79
+ | `baseURL` | `https://verification.didit.me` | |
80
+ | `organizationId`, `applicationId` | `DIDIT_ORGANIZATION_ID`, `DIDIT_APPLICATION_ID` | Needed by application-scoped methods (cases, networks, ongoing monitoring and others; see [api.md](api.md)). |
81
+ | `accountBaseURL` | `https://apx.didit.me/auth/v2` | The account management API (`didit.account`). |
82
+ | `accountToken` | none | The `access_token` from `didit.account.login(...)`. |
83
+ | `timeout` | `60_000` | Milliseconds, per attempt, including reading the body. |
84
+ | `maxRetries` | `2` | See [Retries](#retries). |
85
+ | `defaultHeaders` | none | Added to every request. They cannot replace the credentials (`x-api-key`, `Authorization`). |
86
+ | `fetch` | global `fetch` | Your own `fetch` (proxies, instrumentation, tests). |
87
+ | `fetchOptions` | none | Extra `fetch` options for every request, such as an undici `dispatcher`. A `signal` here cancels every request. |
88
+ | `logLevel`, `logger` | `DIDIT_LOG`, `console` | `debug` logs one line per request. |
89
+
90
+ The environment variables are read on Node.js, Bun and Deno (with `--allow-env`). Edge runtimes such as Cloudflare Workers pass configuration to the handler instead, so give it to the client: `new Didit({ apiKey: env.DIDIT_API_KEY })`.
91
+
92
+ Create one client and reuse it. `didit.withOptions({ ... })` returns a new client with some options changed. Every method also takes request options as its last argument:
93
+
94
+ ```ts
95
+ await didit.sessions.create(
96
+ { workflow_id },
97
+ {
98
+ timeout: 10_000,
99
+ maxRetries: 0,
100
+ idempotencyKey: "order-123",
101
+ headers: { "X-Trace": "abc" },
102
+ signal,
103
+ },
104
+ );
105
+ ```
106
+
107
+ `signal` cancels the request, its retries and the wait between them; a `signal` in `fetchOptions` does the same.
108
+
109
+ ## Using the API
110
+
111
+ Resources and methods follow the API: `didit.<resource>.<method>(...)`, with nested resources such as `didit.sessions.imports` or `didit.cases.notes`.
112
+ Path parameters are positional; everything else goes in one params object whose fields keep the API's names (snake_case).
113
+ [api.md](api.md) lists all 257 methods with their HTTP routes and types.
114
+
115
+ ```ts
116
+ import { fileFromPath } from "@didit-protocol/sdk-node";
117
+
118
+ await didit.idVerification.verify({ front_image: await fileFromPath("id-front.jpg") });
119
+ await didit.aml.screen({ full_name: "Ada Lovelace", date_of_birth: "1815-12-10" });
120
+ await didit.users.updateStatus("user-42", { status: "FLAGGED" });
121
+ await didit.cases.notes.create(caseUuid, { comment: "Checked with the customer" });
122
+ ```
123
+
124
+ Every request and response has a type, exported from the package:
125
+
126
+ ```ts
127
+ import type {
128
+ SessionsCreateParams,
129
+ SessionsCreateResponse,
130
+ TransactionDetail,
131
+ } from "@didit-protocol/sdk-node";
132
+ ```
133
+
134
+ Enums are open (`"Approved" | "Declined" | (string & {})`): a value added to the API later never breaks your build.
135
+ Fields the API adds later are kept on the objects you receive.
136
+ Optional fields are `?:`; fields the API can send as `null` include `| null`.
137
+ Date-time fields are `Date` objects (a date-time the API sends without a time zone is read as UTC); in parameters you can pass a `Date` or an ISO 8601 string.
138
+
139
+ The account management API (organizations and applications) is under `didit.account`:
140
+
141
+ ```ts
142
+ const { access_token } = await didit.account.login({ email, password });
143
+ const account = didit.withOptions({ accountToken: access_token });
144
+ const organizations = await account.account.organizations.list();
145
+ ```
146
+
147
+ ### Pagination
148
+
149
+ Paginated list methods (a "page" in [api.md](api.md)) return a page with `count`, `next`, `previous` and `results`. Iterate with `for await` to walk every item of every page:
150
+
151
+ ```ts
152
+ for await (const session of didit.sessions.list({ limit: 100 })) {
153
+ console.log(session.session_id);
154
+ }
155
+
156
+ const page = await didit.sessions.list({ limit: 100 });
157
+ if (page.hasNextPage()) {
158
+ const next = await page.getNextPage();
159
+ }
160
+ ```
161
+
162
+ The SDK follows the API's `next` links and refuses a link to another origin, so your API key never leaves the API.
163
+
164
+ ### Files
165
+
166
+ Upload parameters accept a `Blob` or `File`, a `Uint8Array` or `ArrayBuffer`, a web `ReadableStream`, a Node.js stream such as `fs.createReadStream("id.jpg")`, or `{ data, filename, contentType }`.
167
+ `fileFromPath(path)` reads a file on Node.js, Bun and Deno; `toFile(bytes, filename)` names bytes.
168
+ An upload without a name is sent as `file` plus the extension its content type or first bytes give (`file.jpg`, `file.pdf`).
169
+ A request with a file is sent as multipart form data: objects in it travel as JSON text, and `null` fields are left out (multipart has no null).
170
+
171
+ Binary responses (session and user PDFs, network reports, wallet screening PDFs, the import template, case attachments) return a `Blob`:
172
+
173
+ ```ts
174
+ import { writeFile } from "node:fs/promises";
175
+
176
+ const pdf = await didit.sessions.generatePdf(sessionId);
177
+ await writeFile("report.pdf", new Uint8Array(await pdf.arrayBuffer()));
178
+ ```
179
+
180
+ ## Errors
181
+
182
+ | Error | When |
183
+ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
184
+ | `DiditError` | Base class of every error below. |
185
+ | `APIConnectionError` / `APITimeoutError` | No response (network failure, timeout). |
186
+ | `APIStatusError` | Any non-2xx answer, with `status`, `body` (parsed JSON when possible), `headers` and `requestId`. |
187
+ | `BadRequestError` 400, `AuthenticationError` 401, `PermissionDeniedError` 403, `NotFoundError` 404, `ConflictError` 409, `UnprocessableEntityError` 422, `RateLimitError` 429, `InternalServerError` 5xx | Subclasses of `APIStatusError`. |
188
+ | `APIResponseValidationError` | A 2xx answer the SDK cannot use (not JSON, a pagination link to another origin). |
189
+ | `WebhookVerificationError` | A webhook failed verification; `reason` says why. |
190
+ | `TokenVerificationError` | An Authentication token failed verification; `reason` says why. |
191
+ | `ConfigurationError` | Missing API key, organization or application id, an empty webhook secret, an invalid option. Thrown before anything is sent. |
192
+
193
+ ```ts
194
+ import { APIStatusError, NotFoundError } from "@didit-protocol/sdk-node";
195
+
196
+ try {
197
+ await didit.sessions.retrieveDecision(sessionId);
198
+ } catch (error) {
199
+ if (error instanceof NotFoundError) {
200
+ // ...
201
+ } else if (error instanceof APIStatusError) {
202
+ console.error(error.status, error.body, error.requestId);
203
+ } else {
204
+ throw error;
205
+ }
206
+ }
207
+ ```
208
+
209
+ The API key never appears in an error message, a log line or the logged client.
210
+
211
+ ### Retries
212
+
213
+ Failed requests are retried up to `maxRetries` times (default 2) with exponential backoff from 0.5 s, full jitter, capped at 8 s, honouring `Retry-After` (capped at 60 s).
214
+ `GET`, `HEAD`, `PUT` and `DELETE` are retried on connection errors, timeouts, 408, 429 and 5xx.
215
+ `POST` and `PATCH` are never retried once the request may have reached the server: only on 429 or when the connection failed before anything was sent.
216
+
217
+ ### Raw responses
218
+
219
+ ```ts
220
+ const { data, response, requestId } = await didit.sessions.create({ workflow_id }).withResponse();
221
+ const raw: Response = await didit.sessions.generatePdf(sessionId).asResponse(); // unread, for streaming
222
+ ```
223
+
224
+ ## Webhooks
225
+
226
+ `webhooks.verify(rawBody, headers, secret, options?)` checks the signature and the timestamp and returns a typed event; it throws `WebhookVerificationError` otherwise.
227
+ It is also available as `didit.webhooks.verify(...)`.
228
+
229
+ - `rawBody`: the exact body bytes (a `Buffer`, `Uint8Array`, `ArrayBuffer` or string). Configure your framework to give you the raw body.
230
+ - `headers`: a `Headers` object, Node.js `req.headers`, or `[name, value]` pairs.
231
+ - `secret`: the destination's secret, or a list of secrets during a rotation.
232
+ - `options.tolerance`: seconds between the delivery's timestamp and now (default 300).
233
+
234
+ Didit signs three ways: `X-Signature` (the raw body), `X-Signature-V2` (a canonical JSON form that survives middleware that re-encodes JSON) and the deprecated `X-Signature-Simple`; during a secret rotation the `-Previous` variants are signed with the old secret. Any of the first two, with either secret, is accepted.
235
+
236
+ ```ts
237
+ const event = await webhooks.verify(rawBody, headers, [currentSecret, previousSecret]);
238
+ switch (event.webhook_type) {
239
+ case "status.updated":
240
+ case "data.updated":
241
+ console.log(event.session_id, event.status, event.decision?.status);
242
+ break;
243
+ case "transaction.status.updated":
244
+ console.log(event.transaction_id, event.status, event.score);
245
+ break;
246
+ default:
247
+ console.log("unhandled event", event.webhook_type);
248
+ }
249
+ ```
250
+
251
+ Events are a union discriminated by `webhook_type`: session (`status.updated`, `data.updated`), user, business, transaction and Travel Rule events, `activity.created`, `workflow.compliance_changed`, `kyb.registry_search.resolved`, and `UnknownEvent` for any other type.
252
+ Each event also has `event.verifiedBy` (which signature matched) and `event.bodyAuthenticated`.
253
+ The event is the body as sent, except that the session `decision` is the generated decision type, so its date-times are `Date` objects.
254
+
255
+ The KYB registry search callback (`kyb.registry_search.resolved`) is delivered unsigned today (`X-Didit-Unsigned-Callback: true`), so `verify` rejects it with `missing_signature`. Do not trust its body: read the result from the API by its `request_id` instead.
256
+
257
+ If your framework only gives you the parsed JSON body, `webhooks.verifyParsed(payload, headers, secret)` checks `X-Signature-V2` only, and fails for integers above 2^53 that `JSON.parse` has already rounded. Prefer the raw body.
258
+ `X-Signature-Simple` signs the envelope only, not the body; it is accepted with `{ allowSimple: true }`, and then `event.bodyAuthenticated` is `false`: re-fetch the record from the API before acting on it.
259
+
260
+ ## Transactions, wallet screening and Travel Rule
261
+
262
+ ```ts
263
+ const txn = await didit.transactions.create({
264
+ transaction_id: "txn-1001",
265
+ transaction_category: "finance",
266
+ transaction_details: { direction: "outbound", amount: 2500, currency: "EUR" },
267
+ subject: { vendor_data: "user-42", full_name: "Ada Lovelace" },
268
+ });
269
+ console.log(txn.status, txn.score); // "APPROVED" | "IN_REVIEW" | "DECLINED" | ...
270
+
271
+ const screening = await didit.walletScreening.screen({
272
+ wallet_address: "0x...",
273
+ blockchain: "ETH",
274
+ });
275
+ for await (const rule of didit.transactions.rules.list()) console.log(rule.title, rule.mode);
276
+ const settings = await didit.travelRule.retrieveSettings();
277
+ const vasps = await didit.travelRule.listVasps({ limit: 20 });
278
+ ```
279
+
280
+ The `transaction.created`, `transaction.status.updated` and `travel_rule.status.updated` webhooks are typed events.
281
+
282
+ ## Authentication
283
+
284
+ Didit Authentication gives each application its own OpenID Connect issuer for its end users. The `Authentication` helper verifies the ES256 tokens it signs and calls its token, session and step-up endpoints from your backend. It never uses your API key.
285
+
286
+ ```ts
287
+ import { Authentication, TokenVerificationError } from "@didit-protocol/sdk-node";
288
+
289
+ const auth = Authentication.fromPublishableKey(process.env.DIDIT_PUBLISHABLE_KEY!, {
290
+ clientId: process.env.DIDIT_APPLICATION_ID!,
291
+ });
292
+ // or: new Authentication({ issuer: "https://acme.auth.didit.me", clientId: "<application id>" })
293
+
294
+ try {
295
+ const claims = await auth.verifyAccessToken(accessToken);
296
+ console.log(claims.sub, claims.org?.role);
297
+ } catch (error) {
298
+ if (error instanceof TokenVerificationError)
299
+ console.log(error.reason); // e.g. "expired"
300
+ else throw error;
301
+ }
302
+ ```
303
+
304
+ - Options: `issuer` (or the publishable key), `clientId` (the application id: the default audience, and sent as `client_id` to the token endpoint), `leeway` (seconds, default 60), `timeout` (milliseconds per issuer request, default 10 000), `fetch`, `logLevel`, `logger`.
305
+ - `verifyAccessToken(token, { audience? })` and `verifyIdToken(token, { nonce, audience? })`: ES256 only; issuer, audience, expiry, not-before and issued-at are checked with 60 s leeway. Keys come from the issuer's JWKS, cached and refreshed when a new key appears. Pass `nonce: null` when no nonce was sent (an ID token from `refresh`); the token must then carry none. An access token an agent uses for a person also verifies: its `act` claim names the agent.
306
+ - `createPkce()`, `createState()` and `createNonce()` are standalone functions (`import { createPkce } from "@didit-protocol/sdk-node"`) for the authorization code flow; then `auth.exchangeCode({ code, codeVerifier, redirectUri })`.
307
+ - `refresh(refreshToken)`: serialise refreshes for one session; reusing a consumed refresh token revokes the session.
308
+ - `getSession(accessToken)` for an instant revocation check, `revokeSession(accessToken)`.
309
+ - `createStepUp(accessToken, { action, amount?, currency?, returnTo? })` and `retrieveStepUp(accessToken, id)`: act only on status `approved`. The issuer currently requires `returnTo` and answers 400 without it.
310
+
311
+ Authentication is in early access and reaches production with the 2026-11-01 Didit release.
312
+
313
+ ## Logging
314
+
315
+ Set `DIDIT_LOG=debug` (or `logLevel: "debug"`) to log one line per HTTP request: method, path, status, duration and request id. Bodies, headers, query strings and credentials are never logged.
316
+
317
+ ## Supported runtimes
318
+
319
+ | Runtime | Versions |
320
+ | ------------- | ------------------------------------------------------------------------------------------- |
321
+ | Node.js | 18, 20, 22, 24 |
322
+ | Bun | 1.x |
323
+ | Deno | 2.x |
324
+ | Edge runtimes | Cloudflare Workers, Vercel Edge and other runtimes with `fetch`, Web Crypto and Web Streams |
325
+
326
+ CI runs the test suite on every Node.js version and a smoke test of the built package on Node.js 18, Bun, Deno and workerd (the Cloudflare Workers runtime).
327
+
328
+ The SDK refuses to send your API key anywhere but the API, but it is a server-side library: never ship your API key to a browser.
329
+
330
+ ## Language notes
331
+
332
+ Choices this SDK makes where the shared design leaves them to each language:
333
+
334
+ - **Names.** Resources and methods are camelCase (`didit.idVerification.verify`, `retrieveDecision`); request and response fields keep the API's snake_case names. A query parameter named like a body field takes a `_query` suffix in the params object (`node_id_query`). Fields the SDK adds are camelCase: `requestId` on errors and raw responses, `verifiedBy` and `bodyAuthenticated` on webhook events.
335
+ - **Options.** Options are objects, not positional arguments: `new Didit({ apiKey })`, `new Authentication({ issuer })`, `verifyIdToken(token, { nonce, audience })`, `exchangeCode({ code, codeVerifier, redirectUri })`, `createStepUp(token, { action, ... })`. The design's `http_client` is the `fetch` option (plus `fetchOptions`); `logLevel` and `logger` choose where debug lines go. On edge runtimes, pass the API key as an option (environment variables are read only on Node.js, Bun and Deno).
336
+ - **Errors.** `APITimeoutError` extends `APIConnectionError`. `TokenVerificationError` (Authentication) and `APIResponseValidationError` (a 2xx answer the SDK cannot use) complete the design's table.
337
+ - **Times.** `timeout` is in milliseconds; webhook `tolerance` and Authentication `leeway` are in seconds, like the timestamps they compare.
338
+ - **Dates.** Response date-times are `Date` objects, which keep millisecond precision (the API sends microseconds); one without a time zone is read as UTC. Date-only fields (`format: date`) stay `YYYY-MM-DD` strings. Webhook events and Authentication claims are the JSON payloads as sent, so their timestamps stay numbers or strings, except a session event's `decision`, which is the generated decision type with `Date` objects.
339
+ - **Async.** Every API method, `webhooks.verify` and the Authentication helpers return promises (Web Crypto is asynchronous). `createPkce`, `createState` and `createNonce` are asynchronous for the same reason.
340
+ - **Results.** Methods return an `APIPromise`: await it for the data, or call `.withResponse()` / `.asResponse()`.
341
+ - **Absent and null.** Optional fields are `?:`; nullable fields add `| null`; a field you leave `undefined` is not sent.
342
+ - **Large integers.** Webhook signatures are checked on the raw body with a parser that keeps every number's digits, so integers above 2^53 are verified exactly. The typed event you get back is parsed with `JSON.parse`, like every response.
343
+ - **CommonJS.** `const { Didit } = require("@didit-protocol/sdk-node")`; the ESM default export is also `require(...).default`.
344
+ - **Files.** Paths are read with `fileFromPath(path)`; everything else is passed as bytes, a Blob or a stream.
345
+ - **Client header.** `User-Agent: didit-node/<version> (<runtime> <version>; <os>)` and `X-Didit-Client: node/<version>`, after the repository name, on every runtime.
346
+
347
+ ## Versioning
348
+
349
+ This package follows [semantic versioning](https://semver.org). A breaking change to the public SDK surface is a major version (a minor version while the version is 0.x).
350
+ Releases and `CHANGELOG.md` are produced by release-please from conventional commits.
351
+ When the API removes or renames an endpoint, the SDK keeps the old method as deprecated for one more minor version.
352
+
353
+ ## Links
354
+
355
+ - API reference and guides: https://docs.didit.me
356
+ - Design contract shared by every Didit server SDK (Python, TypeScript, Go, Java, PHP, Ruby, .NET): [docs/DESIGN.md](docs/DESIGN.md)
357
+ - Contributing (regenerate, test, release): [CONTRIBUTING.md](CONTRIBUTING.md)
358
+ - Security: [SECURITY.md](SECURITY.md)
359
+
360
+ ## License
361
+
362
+ MIT