@allus-fyi/company-data 0.0.3

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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +706 -0
  3. package/dist/cjs/buffer.js +352 -0
  4. package/dist/cjs/client.js +396 -0
  5. package/dist/cjs/config.js +241 -0
  6. package/dist/cjs/crypto.js +288 -0
  7. package/dist/cjs/errors.js +96 -0
  8. package/dist/cjs/http.js +272 -0
  9. package/dist/cjs/index.js +74 -0
  10. package/dist/cjs/models.js +300 -0
  11. package/dist/cjs/package.json +1 -0
  12. package/dist/cjs/pump.js +279 -0
  13. package/dist/cjs/webhooks.js +335 -0
  14. package/dist/cjs/xml.js +257 -0
  15. package/dist/esm/buffer.js +348 -0
  16. package/dist/esm/client.js +392 -0
  17. package/dist/esm/config.js +237 -0
  18. package/dist/esm/crypto.js +281 -0
  19. package/dist/esm/errors.js +86 -0
  20. package/dist/esm/http.js +267 -0
  21. package/dist/esm/index.js +37 -0
  22. package/dist/esm/models.js +292 -0
  23. package/dist/esm/package.json +1 -0
  24. package/dist/esm/pump.js +275 -0
  25. package/dist/esm/webhooks.js +329 -0
  26. package/dist/esm/xml.js +252 -0
  27. package/dist/types/buffer.d.ts +109 -0
  28. package/dist/types/client.d.ts +150 -0
  29. package/dist/types/config.d.ts +86 -0
  30. package/dist/types/crypto.d.ts +125 -0
  31. package/dist/types/errors.d.ts +73 -0
  32. package/dist/types/http.d.ts +80 -0
  33. package/dist/types/index.d.ts +36 -0
  34. package/dist/types/models.d.ts +154 -0
  35. package/dist/types/pump.d.ts +118 -0
  36. package/dist/types/webhooks.d.ts +99 -0
  37. package/dist/types/xml.d.ts +42 -0
  38. package/docs/config.md +93 -0
  39. package/docs/errors.md +87 -0
  40. package/docs/model.md +141 -0
  41. package/docs/pump.md +130 -0
  42. package/docs/webhooks.md +140 -0
  43. package/package.json +54 -0
package/docs/model.md ADDED
@@ -0,0 +1,141 @@
1
+ # Output model reference
2
+
3
+ The conclusions — the only objects you work with. Importable from
4
+ `@allus/company-data`. Each carries `.raw` (the underlying hardened API object;
5
+ never contains the person's source field).
6
+
7
+ ## `RequestField`
8
+
9
+ Your request-field **definition** — your config, never the person's fields.
10
+ Returned by `client.requestFields()`.
11
+
12
+ ```ts
13
+ class RequestField {
14
+ slug: string; // the stable, company-set key — the contract for value access
15
+ label: string; // the human label (rename freely; the slug stays)
16
+ type: string; // email|phone|url|text|address|bank|creditcard|date|date_of_birth|photo|document|legal_document
17
+ oneTime: boolean; // a one-time snapshot vs a live (auto-updating) answer
18
+ mandatory: boolean; // mandatory-to-provide OR mandatory-to-stay-connected (the API's two flags, folded)
19
+ raw: Record<string, unknown>;
20
+ }
21
+ ```
22
+
23
+ ## `Connection`
24
+
25
+ A connected person — identity + the slug-keyed value map. No source field
26
+ anywhere; `values` is keyed by **your** request slug.
27
+
28
+ ```ts
29
+ class Connection {
30
+ id: string;
31
+ personId: string;
32
+ displayName: string | null; // null on connection(id) (the list endpoint carries it)
33
+ connectedAt: Date | null; // likewise null on connection(id)
34
+ values: Record<string, Value>; // {<your_slug>: Value}
35
+ raw: Record<string, unknown>;
36
+ }
37
+ ```
38
+
39
+ ```ts
40
+ conn.values['work_email'].value // "alice@acme.com"
41
+ conn.values['mobile'] // undefined if the person didn't answer that slot
42
+ ```
43
+
44
+ ## `Value`
45
+
46
+ One answer for one of your request slots.
47
+
48
+ ```ts
49
+ class Value {
50
+ value: unknown; // typed plaintext (see below)
51
+ live: boolean; // true = "keep connected" (auto-updates); false = one-time snapshot
52
+ updatedAt: Date | null; // when this answer last changed
53
+ raw: Record<string, unknown>;
54
+ }
55
+ ```
56
+
57
+ ### `value` types (resolved from the field's `type`)
58
+
59
+ | Field type | JS `value` | Notes |
60
+ |------------|------------|-------|
61
+ | `email`, `phone`, `url`, `text` | `string` | The decrypted plaintext. |
62
+ | `address`, `bank`, `creditcard` | `object` | The decrypted plaintext is a JSON object → parsed. A non-JSON structured value throws `DecryptError`. |
63
+ | `date`, `date_of_birth` | `Date` | Parsed from ISO `YYYY-MM-DD` (UTC midnight, the leading 10 chars); falls back to the raw string if unparseable. |
64
+ | `photo`, `document`, `legal_document` | `BinaryHandle` | Lazy — nothing fetched/decrypted until `.bytes()`/`.save()`. |
65
+ | unanswered / no value | `null` | The slot has no answer. |
66
+
67
+ ## `BinaryHandle`
68
+
69
+ A lazy handle for a binary value. No network or decryption happens at construction.
70
+
71
+ ```ts
72
+ class BinaryHandle {
73
+ get valueUrl(): string | null; // the opaque slot-keyed file URL (read-only)
74
+ bytes(): Promise<Buffer>; // fetch (if needed) → decrypt → decoded primary file bytes
75
+ save(path: string): Promise<number>; // write bytes() to path; resolves to bytes written
76
+ static parseEnvelopeBytes(envelopeJson: string): Buffer; // envelope string → file bytes
77
+ }
78
+ ```
79
+
80
+ On first `.bytes()`/`.save()`:
81
+
82
+ 1. GET the slot-keyed file endpoint → the API serves `{"encrypted": true, "value": <wrapper>}`.
83
+ 2. Decrypt the inner `{"_enc":1,…}` wrapper with the service key → a JSON file-envelope string (`{"full": "data:…", "thumb": …}` for photos, `{"file": "data:…", …}` for documents).
84
+ 3. Base64-decode the primary data URI (`full` for photos, `file` for documents) → a `Buffer`. Cached on the handle (repeated calls don't re-fetch).
85
+
86
+ `.save()` is crash-safe (temp file → fsync → atomic rename — never a truncated
87
+ output). An unanswered binary slot yields an empty handle; calling `.bytes()` on it
88
+ throws `DecryptError`.
89
+
90
+ ## `Change`
91
+
92
+ A change-feed / webhook event. Returned by the pump (`processChanges`, `drainBatch`)
93
+ and the webhook helpers.
94
+
95
+ ```ts
96
+ class Change {
97
+ id: string; // the stable server change-row id — YOUR dedup key
98
+ event: string; // see the event table
99
+ personId: string | null;
100
+ shareCode: string | null; // the person's profile share code (every event; may be null)
101
+ slug: string | null; // field_updated/field_deleted/consent_* only
102
+ value: unknown; // field_updated only; typed exactly like Value.value
103
+ live: boolean | null; // field_updated only
104
+ at: Date | null; // the change time (no separate updatedAt on a change)
105
+ raw: Record<string, unknown>;
106
+ }
107
+ ```
108
+
109
+ ### Events
110
+
111
+ | `event` | Carries |
112
+ |---------|---------|
113
+ | `connection_created` | identity only (no slot/value) |
114
+ | `connection_deleted` | identity only (no slot/value) |
115
+ | `field_updated` | `slug` + decrypted `value` (+ `live`); binary → a lazy `BinaryHandle` |
116
+ | `field_deleted` | `slug`, no value |
117
+ | `consent_accepted` / `consent_declined` | `slug` |
118
+
119
+ `Change.id` is captured before the server's drain-delete, so it survives a crash +
120
+ replay unchanged — dedup on it.
121
+
122
+ ## `LogEntry`
123
+
124
+ A service activity-log entry — ops events only (email / purge / webhook), never
125
+ person field data.
126
+
127
+ ```ts
128
+ class LogEntry {
129
+ type: string;
130
+ message: string | null;
131
+ metadata: unknown;
132
+ at: Date | null;
133
+ raw: Record<string, unknown>;
134
+ }
135
+ ```
136
+
137
+ ## `.raw`
138
+
139
+ Every model has a `.raw` property: the underlying (hardened) API object, for
140
+ debugging or an edge case the SDK didn't model. It never contains the person's
141
+ source field — the hardened API doesn't return it.
package/docs/pump.md ADDED
@@ -0,0 +1,130 @@
1
+ # The changes pump
2
+
3
+ The changes feed is a server-side **drain-on-fetch queue**:
4
+ `GET /api/company-data/changes?limit=N` returns up to N events (default 100, max
5
+ 500) **and deletes exactly those rows in the same transaction**. There is no
6
+ offset/cursor/page, and the API keeps no copy after a fetch. So a consumer must:
7
+
8
+ * not lose a drained batch if it crashes mid-batch (the API already deleted it), and
9
+ * not materialize a huge backlog in memory.
10
+
11
+ `client.processChanges(handler)` (delegating to the `Pump`) does both.
12
+
13
+ ## `processChanges(handler, options?)`
14
+
15
+ ```ts
16
+ processChanges(
17
+ handler: (change: Change) => void | Promise<void>,
18
+ options?: {
19
+ batchSize?: number; // clamped to [1, 500]; default 100
20
+ maxRetries?: number; // default 3
21
+ onError?: 'deadletter' | 'halt'; // default 'deadletter'
22
+ backoff?: (attempt: number) => number; // attempt(1-based) -> seconds
23
+ },
24
+ ): Promise<void>
25
+ ```
26
+
27
+ Drains the feed through `handler` one `Change` at a time, **until the feed is empty,
28
+ then resolves**. No follow/daemon mode — schedule re-runs yourself. The handler may
29
+ be sync or async.
30
+
31
+ ## The cycle
32
+
33
+ 1. **Replay first** — deliver any un-acked events already in the local buffer (a previous crashed run), oldest-first.
34
+ 2. **Drain** — when the buffer is empty, fetch one batch (≤ `batchSize`, ≤ 500) and **persist it to the durable buffer (fsync) BEFORE handing anything out**.
35
+ 3. **Deliver one-by-one** — for each buffered event, oldest-first: decrypt its value *at delivery* (never on disk), build the typed `Change`, call `handler(change)`.
36
+ 4. **Ack / retry / dead-letter** — on handler success, remove the event from the buffer (ack). On a handler error, retry with `backoff` up to `maxRetries`; then:
37
+ * `onError="deadletter"` (default) → move it to the dead-letter store, log it, and continue (one poison event never wedges the stream);
38
+ * `onError="halt"` → re-throw the handler's error (the event stays un-acked in the buffer for the next run).
39
+ A **`DecryptError`** (corrupt/truncated ciphertext, rotated key) is special: the decrypt runs *inside* the delivery attempt, and an undecryptable event is **dead-lettered immediately** — re-decrypting can't fix it, so it does **not** burn `maxRetries`. Under `onError="halt"` it re-throws like a handler error. Either way it never propagates out of `processChanges` and wedges step-1 replay.
40
+ 5. Repeat until a drain returns empty **and** the buffer is drained → resolve.
41
+
42
+ ## Crash safety · at-least-once · idempotency
43
+
44
+ A batch is durably buffered *before* any delivery, and acked per-item only *after*
45
+ the handler succeeds. A crash between a handler's success and its ack re-delivers
46
+ that event on the next run. Delivery is therefore **at-least-once**:
47
+
48
+ > **Your handler must be idempotent. Dedup on `Change.id`** (the stable server
49
+ > change-row id, captured before the server delete).
50
+
51
+ ## The durable buffer (on disk)
52
+
53
+ Under `cacheDir`:
54
+
55
+ ```
56
+ <cacheDir>/pending/<seq>_<change_id>.json # un-acked events, oldest-first
57
+ <cacheDir>/deadletter/<seq>_<change_id>.json # events that exhausted retries
58
+ ```
59
+
60
+ * Stored events keep their **ciphertext** `value`/`value_url` — **no plaintext PII is ever written to disk**. Decryption happens only at delivery.
61
+ * `<seq>` is a zero-padded, monotonically increasing sequence, so lexicographic filename order == oldest-first (stable even if `at` timestamps are equal/missing).
62
+ * Writes are crash-safe: temp file → `fsyncSync` → atomic rename → dir fsync. A crash never leaves a half-written file.
63
+ * Re-instantiating the buffer on the same `cacheDir` recovers whatever is on disk — that recovery **is** the replay-on-restart.
64
+
65
+ ## Options
66
+
67
+ | Option | Default | Meaning |
68
+ |--------|---------|---------|
69
+ | `batchSize` | 100 | Events per drain; clamped to `[1, 500]`. |
70
+ | `maxRetries` | 3 | Handler retries before dead-letter/halt. |
71
+ | `onError` | `"deadletter"` | `"deadletter"` (continue) or `"halt"` (re-throw). Any other value throws `TypeError`. |
72
+ | `backoff` | exponential, capped 30s | `attempt -> seconds` between retries. |
73
+
74
+ > `logger` is **not** a `processChanges` option — pass it to the `Client`
75
+ > constructor (`Client.fromConfig('allus.json', { logger: myLogger })`). Every
76
+ > drain, deliver, ack, retry, dead-letter, and replay is logged (any
77
+ > console-compatible sink with `debug`/`info`/`warn`/`error`).
78
+
79
+ ## Durability guarantees
80
+
81
+ Held by the pump across all six SDKs, validated by the test suite:
82
+
83
+ 1. **Decrypt inside the delivery attempt** — a poison/undecryptable event is dead-lettered immediately, never wedges replay, and does not burn retries.
84
+ 2. **A re-failing dead-letter is updated in place** within `deadletter/` (atomic temp+fsync+rename) — never routed back through `pending/` (which has a crash window that could resurrect a dead-letter as a live event).
85
+ 3. **Stored attempt count is monotonic** across separate retry runs (`max(existing, new)`).
86
+ 4. **At-least-once dead-lettering** — the new dead-letter copy is written *before* the pending copy is unlinked, so a crash between leaves the event in both dirs (harmless re-delivery, absorbed by the id-dedup handler). This is intentional — do not "fix" it by deleting-first.
87
+
88
+ ## No follow mode — schedule re-runs
89
+
90
+ ```ts
91
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
92
+ for (;;) {
93
+ await client.processChanges(handle); // resolves when the feed empties
94
+ await sleep(5000); // the feed is cheap to poll (see rate limits)
95
+ }
96
+ ```
97
+
98
+ A cron job, a worker loop, or any scheduler works equally well.
99
+
100
+ ## Dead-letter inspect / re-drive
101
+
102
+ ```ts
103
+ client.deadLetters(): DeadLetterRecord[]
104
+ client.retryDeadLetters(handler, options?): Promise<number>
105
+ ```
106
+
107
+ * `deadLetters()` — each record is the stored (ciphertext) event with a flattened `error` and `attempts`, plus its `id`.
108
+ * `retryDeadLetters(handler)` — re-drives every dead-lettered event through `handler`. On success the record is removed. On repeated failure (or a `DecryptError`) the dead-letter record is **updated in place** with the new error + attempt count and stays in `deadletter/` (`"deadletter"`), or the error re-throws (`"halt"`). Resolves to the count successfully re-driven.
109
+
110
+ A re-failing dead-letter never re-enters `pending/` — it is rewritten in place
111
+ within `deadletter/`, so a crash mid-re-drive can't resurrect it as a live event on
112
+ the next run. Dead letters are **never silently dropped** and **never re-fetched
113
+ from the API** (it already deleted them) — the local store is their only home,
114
+ which is exactly why it's durable.
115
+
116
+ ```ts
117
+ for (const dl of client.deadLetters()) console.log(dl.id, dl.error, dl.attempts);
118
+ const fixed = await client.retryDeadLetters(handle); // after fixing the handler bug
119
+ ```
120
+
121
+ ## Advanced: `drainBatch(max?)`
122
+
123
+ ```ts
124
+ client.drainBatch(max?: number): Promise<Change[]>
125
+ ```
126
+
127
+ A raw, **UNBUFFERED** drain: fetches one batch (clamped ≤ 500) and returns the
128
+ decrypted `Change`s directly — it does **not** persist anything to the buffer, so
129
+ **you own durability** if you use it (a crash loses what the API already deleted).
130
+ Prefer `processChanges` for safe consumption.
@@ -0,0 +1,140 @@
1
+ # Webhook receiver helpers
2
+
3
+ The lower-latency push alternative to polling the changes feed. The platform POSTs
4
+ each change event to your configured webhook URL with:
5
+
6
+ * `X-Allus-Webhook-Id` — which webhook this is (selects the HMAC secret from config).
7
+ * `X-Allus-Signature` — `HMAC-SHA256(rawBody, secret)` as lowercase hex.
8
+ * the body — the same slug-keyed `Change` shape as the pull feed (JSON or XML). If `encrypt_payload` is on, the body is replaced by a `{"_enc":1,…}` envelope encrypted to the company **account** key (and the HMAC is over that envelope).
9
+
10
+ **All secrets/keys come from config — these helpers take NO key or secret
11
+ arguments.** Always pass the **raw request body bytes** (a `Buffer`); don't
12
+ re-serialize a parsed body — the HMAC is over the exact bytes sent.
13
+
14
+ ## Client methods (the usual form)
15
+
16
+ ```ts
17
+ client.verifyWebhook(rawBody: Buffer | Uint8Array | string, headers): boolean
18
+ client.parseWebhook(rawBody, headers): Change
19
+ client.handleWebhook(rawBody, headers): Change // verify + parse
20
+ ```
21
+
22
+ | Method | Returns | Errors |
23
+ |--------|---------|--------|
24
+ | `verifyWebhook` | `boolean` — recomputes `HMAC-SHA256(rawBody, secret)` and constant-time-compares to `X-Allus-Signature`. `false` on missing signature / unknown id / mismatch. | **Never throws** for a bad signature. |
25
+ | `parseWebhook` | a typed `Change`. Does **not** verify. Handles JSON, XML, and the `encrypt_payload` account-key envelope. | `WebhookError` on a malformed/unparseable body or envelope. |
26
+ | `handleWebhook` | a typed `Change` — verify **then** parse. | `WebhookError` on a bad/unknown signature, or any `parseWebhook` error. |
27
+
28
+ > The client webhook methods are **synchronous** but need the request-fields catalog
29
+ > (to type the value). Call `await client.requestFields()` once at startup so the
30
+ > catalog is cached — the methods then make no network calls. `headers` may be a
31
+ > plain object or a Node `IncomingHttpHeaders` (case-insensitive lookup; array
32
+ > header values use the first element).
33
+
34
+ ## Standalone functions
35
+
36
+ The same three are importable as module functions. They take the `config` and the
37
+ decrypt/type closures explicitly — used by `Client` internally; you'll normally use
38
+ the client methods inside an app.
39
+
40
+ ```ts
41
+ import { verifyWebhook, parseWebhook, handleWebhook } from '@allus/company-data';
42
+
43
+ verifyWebhook(rawBody, headers, config): boolean
44
+ parseWebhook(rawBody, headers, config, { typeForSlug, decryptValue, binaryFetch?, accountKey? }): Change
45
+ handleWebhook(rawBody, headers, config, { typeForSlug, decryptValue, binaryFetch?, accountKey? }): Change
46
+ ```
47
+
48
+ ## In a web route
49
+
50
+ ### Express
51
+
52
+ ```ts
53
+ import express from 'express';
54
+ import { Client, WebhookError } from '@allus/company-data';
55
+
56
+ const app = express();
57
+ const client = Client.fromConfig('allus.json');
58
+ await client.requestFields(); // warm the catalog (the webhook methods are sync)
59
+
60
+ // Capture the RAW body bytes — do NOT let a JSON parser replace them.
61
+ app.post('/allus/webhook', express.raw({ type: '*/*' }), (req, res) => {
62
+ let change;
63
+ try {
64
+ change = client.handleWebhook(req.body, req.headers);
65
+ } catch (e) {
66
+ if (e instanceof WebhookError) return res.sendStatus(401);
67
+ throw e;
68
+ }
69
+ if (!seen(change.id)) { // idempotency — same rule as the pump
70
+ applyChange(change);
71
+ recordSeen(change.id);
72
+ }
73
+ res.sendStatus(204);
74
+ });
75
+ ```
76
+
77
+ ### Fastify
78
+
79
+ ```ts
80
+ import Fastify from 'fastify';
81
+ import { Client, WebhookError } from '@allus/company-data';
82
+
83
+ const app = Fastify();
84
+ const client = Client.fromConfig('allus.json');
85
+ await client.requestFields();
86
+
87
+ // Keep the raw body: a contentTypeParser that returns the Buffer untouched.
88
+ app.addContentTypeParser('*', { parseAs: 'buffer' }, (_req, body, done) => done(null, body));
89
+
90
+ app.post('/allus/webhook', (req, reply) => {
91
+ let change;
92
+ try {
93
+ change = client.handleWebhook(req.body as Buffer, req.headers);
94
+ } catch (e) {
95
+ if (e instanceof WebhookError) return reply.code(401).send();
96
+ throw e;
97
+ }
98
+ if (!seen(change.id)) { applyChange(change); recordSeen(change.id); }
99
+ return reply.code(204).send();
100
+ });
101
+ ```
102
+
103
+ Split the steps if you prefer:
104
+
105
+ ```ts
106
+ if (!client.verifyWebhook(rawBody, headers)) return res.sendStatus(401);
107
+ const change = client.parseWebhook(rawBody, headers);
108
+ ```
109
+
110
+ ## Config-driven secrets
111
+
112
+ Per-webhook HMAC secrets live in the config `webhooks` map, keyed by webhook id;
113
+ the SDK reads `X-Allus-Webhook-Id` and looks up the matching secret. A
114
+ single-webhook service can use the flat `"webhook_secret": "…"` shortcut (or
115
+ `ALLUS_WEBHOOK_SECRET`). An unknown/unconfigured id ⇒ `verifyWebhook` returns
116
+ `false` (and `handleWebhook` throws `WebhookError`).
117
+
118
+ ## The `encrypt_payload` account-key envelope
119
+
120
+ If a webhook has `encrypt_payload` enabled, the whole body is a `{"_enc":1,…}`
121
+ envelope encrypted to your company **account** key, and the HMAC is over that
122
+ envelope. `parseWebhook`/`handleWebhook`:
123
+
124
+ 1. Unwrap the envelope with the configured `account_private_key` + `account_passphrase` (loaded once at `Client` construction — no per-webhook PBKDF2).
125
+ 2. Parse the inner payload (JSON or XML per `format`).
126
+ 3. Decrypt the inner field `value` (a service-key wrapper) with the service key.
127
+
128
+ So an `encrypt_payload` `Change` is identical to a plain one. Receiving such a
129
+ webhook without an `account_private_key` configured throws `WebhookError`.
130
+
131
+ > The envelope uses RSA-OAEP-**SHA1** (OpenSSL's default), distinct from the
132
+ > OAEP-SHA256 used for person field values. The SDK has two OAEP code paths and
133
+ > handles this difference internally — you only supply the account key in config.
134
+
135
+ ## XXE safety
136
+
137
+ XML webhook bodies (and the inner payload after an envelope unwrap) are parsed by
138
+ the same **XXE-safe** parser the HTTP layer uses: no DOCTYPE/DTD processing, no
139
+ custom or external entities. The HMAC is always computed over the **raw bytes**,
140
+ never the parsed tree.
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@allus-fyi/company-data",
3
+ "version": "0.0.3",
4
+ "description": "TypeScript/Node SDK for the allus company-data API: typed, plaintext, slug-keyed conclusions with transparent decryption.",
5
+ "license": "MIT",
6
+ "author": "allme.fyi",
7
+ "keywords": [
8
+ "allus",
9
+ "allme",
10
+ "company-data",
11
+ "sdk"
12
+ ],
13
+ "homepage": "https://github.com/allus-fyi/company-data-typescript#readme",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/allus-fyi/company-data-typescript.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/allus-fyi/company-data-typescript/issues"
20
+ },
21
+ "type": "module",
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/types/index.d.ts",
28
+ "import": "./dist/esm/index.js",
29
+ "require": "./dist/cjs/index.js"
30
+ }
31
+ },
32
+ "main": "./dist/cjs/index.js",
33
+ "module": "./dist/esm/index.js",
34
+ "types": "./dist/types/index.d.ts",
35
+ "files": [
36
+ "dist",
37
+ "README.md",
38
+ "docs"
39
+ ],
40
+ "scripts": {
41
+ "clean": "rimraf dist",
42
+ "build": "npm run clean && npm run build:esm && npm run build:cjs && npm run build:types && node scripts/fixup-cjs.mjs",
43
+ "build:esm": "tsc -p tsconfig.esm.json",
44
+ "build:cjs": "tsc -p tsconfig.cjs.json",
45
+ "build:types": "tsc -p tsconfig.types.json",
46
+ "test": "node --test --import tsx test/*.test.ts"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^22.0.0",
50
+ "rimraf": "^5.0.0",
51
+ "tsx": "^4.19.0",
52
+ "typescript": "^5.6.0"
53
+ }
54
+ }