@dreaminhex/hexdb 0.0.0-stage → 1.0.0

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
@@ -1,3 +1,20 @@
1
- # Temporary Holding Version
1
+ # @dreaminhex/hexdb
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 JavaScript and TypeScript client for [HexDB](https://github.com/dreaminhex/hexdb), a document database with REST, GraphQL and read-only SQL, encryption at rest and replication with automatic failover.
4
+
5
+ It covers documents (insert, get, replace, patch, delete), queries with filters, sorting and paging, counts, aggregations, upserts, transactions, idempotency keys, the change feed, streams, functions and GraphQL. It retries `429` and `503` answers for reads, and for writes that carry an idempotency key. Runs on Node.js 18+, Deno, Bun and in browsers, with no dependencies.
6
+
7
+ ```sh
8
+ npm install @dreaminhex/hexdb
9
+ ```
10
+
11
+ ```ts
12
+ import { HexDB } from "@dreaminhex/hexdb"
13
+
14
+ const db = new HexDB({ url: "http://127.0.0.1:7700", apiKey: process.env.HEXDB_API_KEY })
15
+ const orders = db.tessellation("orders")
16
+ await orders.insert({ customer: "ada", total: 12 })
17
+ const page = await orders.query({ filter: { total: { $gt: 10 } }, sort: "-total" })
18
+ ```
19
+
20
+ Create an API key on the admin UI's Account page. The [driver documentation](https://github.com/dreaminhex/hexdb/tree/main/drivers) and the [manual](https://github.com/dreaminhex/hexdb/blob/main/MANUAL.md) cover the rest. Licensed under the Apache License 2.0.
@@ -0,0 +1,210 @@
1
+ export interface Options {
2
+ /** Base URL of any hex, e.g. http://127.0.0.1:7700 (replicas forward writes to the Overseer). */
3
+ url: string;
4
+ /** An API key (hxk_...) or a session token. */
5
+ apiKey?: string;
6
+ /** Retries for 429 and 503 answers (honoring Retry-After). Default 3. */
7
+ retries?: number;
8
+ /** Per-request timeout in milliseconds. Default 30,000. */
9
+ timeoutMs?: number;
10
+ /** A fetch implementation (defaults to the global one). */
11
+ fetch?: typeof fetch;
12
+ }
13
+ /** An error answer from HexDB: `status` is the HTTP status, `code` HexDB's error code. */
14
+ export declare class HexDBError extends Error {
15
+ readonly status: number;
16
+ readonly code: string;
17
+ readonly retryAfter?: number | undefined;
18
+ constructor(status: number, code: string, message: string, retryAfter?: number | undefined);
19
+ }
20
+ /** A stored document: its fields plus `id` (and `_expires_at` with a TTL). */
21
+ export type Document = {
22
+ id: string;
23
+ _expires_at?: string;
24
+ [field: string]: unknown;
25
+ };
26
+ export interface SortKey {
27
+ field: string;
28
+ descending?: boolean;
29
+ }
30
+ export interface Query {
31
+ /** A filter like `{ status: "paid", total: { $gte: 10 } }` (see the HexDB manual). */
32
+ filter?: Record<string, unknown>;
33
+ /** "-total,name" or sort keys. */
34
+ sort?: string | SortKey[];
35
+ limit?: number;
36
+ offset?: number;
37
+ /** Continue after this document ID (unsorted queries). */
38
+ after?: string;
39
+ /** Only these (dotted) fields. */
40
+ fields?: string[];
41
+ }
42
+ export interface QueryPage {
43
+ documents: Document[];
44
+ total: number;
45
+ next: string | null;
46
+ plan?: {
47
+ indexes: string[];
48
+ scanned: number;
49
+ };
50
+ }
51
+ export interface WriteOptions {
52
+ /** Makes retries safe: the same key returns the first result instead of writing again. */
53
+ idempotencyKey?: string;
54
+ /** Expire the document after this many seconds. */
55
+ ttlSeconds?: number;
56
+ }
57
+ export interface Change {
58
+ seq: number;
59
+ timestamp: string;
60
+ op: "put" | "delete" | "drop_tessellation";
61
+ tessellation: string;
62
+ id?: string;
63
+ document?: Document;
64
+ }
65
+ export interface StreamMessage {
66
+ offset: string;
67
+ time: number;
68
+ payload: unknown;
69
+ key?: string | null;
70
+ headers?: Record<string, string>;
71
+ }
72
+ type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
73
+ export declare class HexDB {
74
+ private readonly base;
75
+ private token?;
76
+ private readonly retries;
77
+ private readonly timeoutMs;
78
+ private readonly fetcher;
79
+ constructor(options: Options);
80
+ /** Send any request to the REST API and return the parsed JSON (undefined for 204). */
81
+ request<T = unknown>(method: Method, path: string, body?: unknown, headers?: Record<string, string>, timeoutMs?: number): Promise<T>;
82
+ /** Sign in with a login and password (and a one-time code with MFA); later requests use the session. */
83
+ login(login: string, password: string, code?: string): Promise<{
84
+ user: unknown;
85
+ expires_at: number;
86
+ }>;
87
+ /** End the session (a no-op for API keys). */
88
+ logout(): Promise<void>;
89
+ /** Whether the server is up (and, signed in, which hex answered). */
90
+ health(): Promise<{
91
+ status: string;
92
+ name?: string;
93
+ hex_type?: string;
94
+ version?: string;
95
+ }>;
96
+ /** Server status and metrics (needs the status permission). */
97
+ status(): Promise<Record<string, unknown>>;
98
+ /** A tessellation (collection of documents). */
99
+ tessellation(name: string): Tessellation;
100
+ /** Tessellations you can read. */
101
+ tessellations(): Promise<{
102
+ name: string;
103
+ kind: string;
104
+ created: string | null;
105
+ indexes: string[];
106
+ }[]>;
107
+ /**
108
+ * Operations across tessellations, all or nothing:
109
+ * `[{ op: "insert" | "replace" | "patch" | "delete" | "get" | "check", tessellation, id?, data?, if_version? }]`.
110
+ */
111
+ transaction(operations: Record<string, unknown>[], options?: {
112
+ idempotencyKey?: string;
113
+ }): Promise<{
114
+ results: unknown[];
115
+ writes: number;
116
+ }>;
117
+ /** Run a GraphQL query or mutation. Throws on GraphQL errors. */
118
+ graphql<T = Record<string, unknown>>(query: string, variables?: Record<string, unknown>): Promise<T>;
119
+ /**
120
+ * Committed changes after `after` (default: from now), as they happen.
121
+ * Long-polls; stops when `signal` aborts.
122
+ */
123
+ changes(options?: {
124
+ after?: number;
125
+ tessellation?: string;
126
+ signal?: AbortSignal;
127
+ }): AsyncGenerator<Change>;
128
+ /** A stream (publish/subscribe). */
129
+ stream(name: string): Stream;
130
+ /** Run a saved function with parameters; returns its result. */
131
+ runFunction<T = unknown>(name: string, params?: Record<string, unknown>): Promise<T>;
132
+ }
133
+ export declare class Tessellation {
134
+ private readonly db;
135
+ readonly name: string;
136
+ constructor(db: HexDB, name: string);
137
+ private get path();
138
+ /** Insert a document; returns it with its new `id`. */
139
+ insert(doc: Record<string, unknown>, options?: WriteOptions): Promise<Document>;
140
+ /** Insert up to 1000 documents atomically; returns their IDs in order. */
141
+ insertMany(docs: Record<string, unknown>[], options?: WriteOptions): Promise<string[]>;
142
+ /** A document by ID, or null. */
143
+ get(id: string, fields?: string[]): Promise<Document | null>;
144
+ /** Replace a document's fields. */
145
+ replace(id: string, doc: Record<string, unknown>, options?: WriteOptions): Promise<Document>;
146
+ /** Merge fields into a document (null removes a field). */
147
+ patch(id: string, changes: Record<string, unknown>, options?: WriteOptions): Promise<Document>;
148
+ /** Delete a document; false if it didn't exist. */
149
+ delete(id: string, options?: {
150
+ idempotencyKey?: string;
151
+ }): Promise<boolean>;
152
+ /** Documents matching a filter, one page at a time. */
153
+ query(query?: Query): Promise<QueryPage>;
154
+ /** Every matching document, page by page (unsorted, in ID order). */
155
+ iterate(query?: Omit<Query, "sort" | "offset" | "after">): AsyncGenerator<Document>;
156
+ /** How many documents match (all of them without a filter). */
157
+ count(filter?: Record<string, unknown>): Promise<number>;
158
+ /** Group and summarize: `{ group_by: ["status"], aggregates: { total: { $sum: "amount" } } }`. */
159
+ aggregate(spec: Record<string, unknown>): Promise<{
160
+ rows: Record<string, unknown>[];
161
+ total_groups: number;
162
+ matched: number;
163
+ }>;
164
+ /** Insert or replace documents matched by key fields (e.g. an ID from another system). */
165
+ upsert(key: string | string[], docs: Record<string, unknown>[], options?: {
166
+ idempotencyKey?: string;
167
+ }): Promise<{
168
+ inserted: number;
169
+ replaced: number;
170
+ ids: string[];
171
+ }>;
172
+ /** Merge `update` into every document matching `filter`. */
173
+ updateWhere(filter: Record<string, unknown>, update: Record<string, unknown>): Promise<{
174
+ matched: number;
175
+ modified: number;
176
+ }>;
177
+ }
178
+ export declare class Stream {
179
+ private readonly db;
180
+ readonly name: string;
181
+ constructor(db: HexDB, name: string);
182
+ private get path();
183
+ /** Publish messages; returns their offsets. */
184
+ publish(...messages: {
185
+ payload: unknown;
186
+ key?: string;
187
+ headers?: Record<string, string>;
188
+ }[]): Promise<string[]>;
189
+ /** Messages after an offset or a group's committed offset; waits up to `wait` seconds for new ones. */
190
+ read(options?: {
191
+ after?: string;
192
+ group?: string;
193
+ limit?: number;
194
+ wait?: number;
195
+ }): Promise<{
196
+ messages: StreamMessage[];
197
+ next: string | null;
198
+ }>;
199
+ /** Commit a consumer group's offset. */
200
+ commit(group: string, offset: string): Promise<void>;
201
+ /**
202
+ * Consume as a group: calls `handle` for each message in order, committing
203
+ * after each batch it handled. Stops when `signal` aborts.
204
+ */
205
+ consume(group: string, handle: (message: StreamMessage) => Promise<void> | void, options?: {
206
+ signal?: AbortSignal;
207
+ batch?: number;
208
+ }): Promise<void>;
209
+ }
210
+ export default HexDB;
package/dist/index.js ADDED
@@ -0,0 +1,286 @@
1
+ // HexDB client for Node.js (18+), Deno, Bun and browsers. No dependencies:
2
+ // it speaks HexDB's REST API with fetch.
3
+ //
4
+ // import { HexDB } from "@dreaminhex/hexdb"
5
+ // const db = new HexDB({ url: "http://127.0.0.1:7700", apiKey: process.env.HEXDB_API_KEY })
6
+ // const orders = db.tessellation("orders")
7
+ // const { id } = await orders.insert({ customer: "ada", total: 12 })
8
+ // const page = await orders.query({ filter: { total: { $gt: 10 } }, sort: "-total" })
9
+ /** An error answer from HexDB: `status` is the HTTP status, `code` HexDB's error code. */
10
+ export class HexDBError extends Error {
11
+ status;
12
+ code;
13
+ retryAfter;
14
+ constructor(status, code, message, retryAfter) {
15
+ super(message);
16
+ this.status = status;
17
+ this.code = code;
18
+ this.retryAfter = retryAfter;
19
+ this.name = "HexDBError";
20
+ }
21
+ }
22
+ const enc = encodeURIComponent;
23
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
24
+ export class HexDB {
25
+ base;
26
+ token;
27
+ retries;
28
+ timeoutMs;
29
+ fetcher;
30
+ constructor(options) {
31
+ this.base = options.url.replace(/\/+$/, "");
32
+ this.token = options.apiKey;
33
+ this.retries = options.retries ?? 3;
34
+ this.timeoutMs = options.timeoutMs ?? 30_000;
35
+ this.fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
36
+ }
37
+ /** Send any request to the REST API and return the parsed JSON (undefined for 204). */
38
+ async request(method, path, body, headers = {}, timeoutMs = this.timeoutMs) {
39
+ for (let attempt = 0;; attempt++) {
40
+ const controller = new AbortController();
41
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
42
+ let response;
43
+ try {
44
+ response = await this.fetcher(this.base + path, {
45
+ method,
46
+ headers: {
47
+ Accept: "application/json",
48
+ ...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
49
+ ...(body === undefined ? {} : { "Content-Type": "application/json" }),
50
+ ...headers,
51
+ },
52
+ body: body === undefined ? undefined : JSON.stringify(body),
53
+ signal: controller.signal,
54
+ });
55
+ }
56
+ catch (e) {
57
+ throw new HexDBError(0, "unreachable", `Couldn't reach HexDB at ${this.base}: ${e instanceof Error ? e.message : String(e)}`);
58
+ }
59
+ finally {
60
+ clearTimeout(timer);
61
+ }
62
+ if (response.status === 204)
63
+ return undefined;
64
+ const text = await response.text();
65
+ const json = text ? safeJson(text) : undefined;
66
+ if (response.ok)
67
+ return json;
68
+ const retryAfter = Number(response.headers.get("retry-after")) || undefined;
69
+ // Writes are retried only when they carry an idempotency key.
70
+ const retryable = (response.status === 429 || response.status === 503) && (method === "GET" || "Idempotency-Key" in headers);
71
+ if (retryable && attempt < this.retries) {
72
+ await sleep((retryAfter ?? 2 ** attempt) * 1000);
73
+ continue;
74
+ }
75
+ const error = json?.error;
76
+ throw new HexDBError(response.status, error?.code ?? "error", error?.message ?? (text || response.statusText), retryAfter);
77
+ }
78
+ }
79
+ /** Sign in with a login and password (and a one-time code with MFA); later requests use the session. */
80
+ async login(login, password, code) {
81
+ const result = await this.request("POST", "/auth/login", { login, password, code, return_token: true });
82
+ this.token = result.token;
83
+ return { user: result.user, expires_at: result.expires_at };
84
+ }
85
+ /** End the session (a no-op for API keys). */
86
+ async logout() {
87
+ await this.request("POST", "/auth/logout");
88
+ this.token = undefined;
89
+ }
90
+ /** Whether the server is up (and, signed in, which hex answered). */
91
+ health() {
92
+ return this.request("GET", "/health");
93
+ }
94
+ /** Server status and metrics (needs the status permission). */
95
+ status() {
96
+ return this.request("GET", "/status");
97
+ }
98
+ /** A tessellation (collection of documents). */
99
+ tessellation(name) {
100
+ return new Tessellation(this, name);
101
+ }
102
+ /** Tessellations you can read. */
103
+ async tessellations() {
104
+ return (await this.request("GET", "/tessellations")).tessellations;
105
+ }
106
+ /**
107
+ * Operations across tessellations, all or nothing:
108
+ * `[{ op: "insert" | "replace" | "patch" | "delete" | "get" | "check", tessellation, id?, data?, if_version? }]`.
109
+ */
110
+ transaction(operations, options = {}) {
111
+ return this.request("POST", "/transactions", { operations }, idempotency(options.idempotencyKey));
112
+ }
113
+ /** Run a GraphQL query or mutation. Throws on GraphQL errors. */
114
+ async graphql(query, variables) {
115
+ const result = await this.request("POST", "/graphql", { query, variables });
116
+ if (result.errors?.length) {
117
+ const first = result.errors[0];
118
+ throw new HexDBError(200, first.extensions?.code ?? "GRAPHQL", first.message);
119
+ }
120
+ return result.data;
121
+ }
122
+ /**
123
+ * Committed changes after `after` (default: from now), as they happen.
124
+ * Long-polls; stops when `signal` aborts.
125
+ */
126
+ async *changes(options = {}) {
127
+ let cursor = options.after;
128
+ while (!options.signal?.aborted) {
129
+ const params = new URLSearchParams({ wait: "30", limit: "500" });
130
+ if (cursor !== undefined)
131
+ params.set("after", String(cursor));
132
+ if (options.tessellation)
133
+ params.set("tessellation", options.tessellation);
134
+ const page = await this.request("GET", `/changes?${params}`, undefined, {}, 60_000);
135
+ cursor = page.last_seq;
136
+ for (const change of page.changes)
137
+ yield change;
138
+ }
139
+ }
140
+ /** A stream (publish/subscribe). */
141
+ stream(name) {
142
+ return new Stream(this, name);
143
+ }
144
+ /** Run a saved function with parameters; returns its result. */
145
+ async runFunction(name, params = {}) {
146
+ return (await this.request("POST", `/functions/${enc(name)}/run`, { params })).result;
147
+ }
148
+ }
149
+ function safeJson(text) {
150
+ try {
151
+ return JSON.parse(text);
152
+ }
153
+ catch {
154
+ return text;
155
+ }
156
+ }
157
+ function idempotency(key) {
158
+ return key ? { "Idempotency-Key": key } : {};
159
+ }
160
+ function ttl(options) {
161
+ return options.ttlSeconds ? `?ttl=${options.ttlSeconds}` : "";
162
+ }
163
+ export class Tessellation {
164
+ db;
165
+ name;
166
+ constructor(db, name) {
167
+ this.db = db;
168
+ this.name = name;
169
+ }
170
+ get path() {
171
+ return `/${enc(this.name)}`;
172
+ }
173
+ /** Insert a document; returns it with its new `id`. */
174
+ insert(doc, options = {}) {
175
+ return this.db.request("POST", `${this.path}${ttl(options)}`, doc, idempotency(options.idempotencyKey));
176
+ }
177
+ /** Insert up to 1000 documents atomically; returns their IDs in order. */
178
+ async insertMany(docs, options = {}) {
179
+ return (await this.db.request("POST", `${this.path}/_bulk${ttl(options)}`, docs, idempotency(options.idempotencyKey))).ids;
180
+ }
181
+ /** A document by ID, or null. */
182
+ async get(id, fields) {
183
+ try {
184
+ return await this.db.request("GET", `${this.path}/${enc(id)}${fields ? `?fields=${enc(fields.join(","))}` : ""}`);
185
+ }
186
+ catch (e) {
187
+ if (e instanceof HexDBError && e.status === 404)
188
+ return null;
189
+ throw e;
190
+ }
191
+ }
192
+ /** Replace a document's fields. */
193
+ replace(id, doc, options = {}) {
194
+ return this.db.request("PUT", `${this.path}/${enc(id)}${ttl(options)}`, doc, idempotency(options.idempotencyKey));
195
+ }
196
+ /** Merge fields into a document (null removes a field). */
197
+ patch(id, changes, options = {}) {
198
+ return this.db.request("PATCH", `${this.path}/${enc(id)}${ttl(options)}`, changes, idempotency(options.idempotencyKey));
199
+ }
200
+ /** Delete a document; false if it didn't exist. */
201
+ async delete(id, options = {}) {
202
+ try {
203
+ await this.db.request("DELETE", `${this.path}/${enc(id)}`, undefined, idempotency(options.idempotencyKey));
204
+ return true;
205
+ }
206
+ catch (e) {
207
+ if (e instanceof HexDBError && e.status === 404)
208
+ return false;
209
+ throw e;
210
+ }
211
+ }
212
+ /** Documents matching a filter, one page at a time. */
213
+ query(query = {}) {
214
+ return this.db.request("POST", `${this.path}/_query`, query);
215
+ }
216
+ /** Every matching document, page by page (unsorted, in ID order). */
217
+ async *iterate(query = {}) {
218
+ let after;
219
+ for (;;) {
220
+ const page = await this.query({ ...query, limit: query.limit ?? 500, after });
221
+ for (const doc of page.documents)
222
+ yield doc;
223
+ if (!page.next)
224
+ return;
225
+ after = page.next;
226
+ }
227
+ }
228
+ /** How many documents match (all of them without a filter). */
229
+ async count(filter) {
230
+ const path = filter ? `${this.path}/count?filter=${enc(JSON.stringify(filter))}` : `${this.path}/count`;
231
+ return (await this.db.request("GET", path)).count;
232
+ }
233
+ /** Group and summarize: `{ group_by: ["status"], aggregates: { total: { $sum: "amount" } } }`. */
234
+ aggregate(spec) {
235
+ return this.db.request("POST", `${this.path}/_aggregate`, spec);
236
+ }
237
+ /** Insert or replace documents matched by key fields (e.g. an ID from another system). */
238
+ upsert(key, docs, options = {}) {
239
+ return this.db.request("POST", `${this.path}/_upsert`, { key, documents: docs }, idempotency(options.idempotencyKey));
240
+ }
241
+ /** Merge `update` into every document matching `filter`. */
242
+ updateWhere(filter, update) {
243
+ return this.db.request("POST", `${this.path}/_update`, { filter, update });
244
+ }
245
+ }
246
+ export class Stream {
247
+ db;
248
+ name;
249
+ constructor(db, name) {
250
+ this.db = db;
251
+ this.name = name;
252
+ }
253
+ get path() {
254
+ return `/streams/${enc(this.name)}`;
255
+ }
256
+ /** Publish messages; returns their offsets. */
257
+ async publish(...messages) {
258
+ return (await this.db.request("POST", `${this.path}/messages`, messages)).offsets;
259
+ }
260
+ /** Messages after an offset or a group's committed offset; waits up to `wait` seconds for new ones. */
261
+ read(options = {}) {
262
+ const params = new URLSearchParams();
263
+ for (const [k, v] of Object.entries(options))
264
+ if (v !== undefined)
265
+ params.set(k, String(v));
266
+ return this.db.request("GET", `${this.path}/messages?${params}`, undefined, {}, ((options.wait ?? 0) + 30) * 1000);
267
+ }
268
+ /** Commit a consumer group's offset. */
269
+ commit(group, offset) {
270
+ return this.db.request("POST", `${this.path}/groups/${enc(group)}/commit`, { offset });
271
+ }
272
+ /**
273
+ * Consume as a group: calls `handle` for each message in order, committing
274
+ * after each batch it handled. Stops when `signal` aborts.
275
+ */
276
+ async consume(group, handle, options = {}) {
277
+ while (!options.signal?.aborted) {
278
+ const { messages, next } = await this.read({ group, limit: options.batch ?? 100, wait: 30 });
279
+ for (const message of messages)
280
+ await handle(message);
281
+ if (messages.length > 0 && next)
282
+ await this.commit(group, next);
283
+ }
284
+ }
285
+ }
286
+ export default HexDB;
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
- {
2
- "name": "@dreaminhex/hexdb",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@dreaminhex/hexdb",
3
+ "version": "1.0.0",
4
+ "description": "Client for HexDB, the hexagonal document database.",
5
+ "license": "Apache-2.0",
6
+ "author": "Dream In Hex",
7
+ "homepage": "https://hexdb-website.vercel.app",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/dreaminhex/hexdb.git",
11
+ "directory": "drivers/node"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/dreaminhex/hexdb/issues"
15
+ },
16
+ "keywords": [
17
+ "hexdb",
18
+ "database",
19
+ "document",
20
+ "nosql",
21
+ "client"
22
+ ],
23
+ "type": "module",
24
+ "main": "dist/index.js",
25
+ "types": "dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "README.md"
35
+ ],
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc -p tsconfig.json",
41
+ "test": "npm run build && node --test test/client.test.mjs"
42
+ },
43
+ "devDependencies": {
44
+ "typescript": "~5.9.3",
45
+ "@types/node": "^22.15.18"
46
+ }
47
+ }