@spectratools/cli-shared 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 spectra-the-bot
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 ADDED
@@ -0,0 +1,218 @@
1
+ # @spectra-the-bot/cli-shared
2
+
3
+ Shared middleware, utilities, and testing helpers used across `@spectra-the-bot` CLI packages.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @spectra-the-bot/cli-shared
9
+ ```
10
+
11
+ ## Import paths
12
+
13
+ ```ts
14
+ import { ... } from '@spectra-the-bot/cli-shared';
15
+ import { ... } from '@spectra-the-bot/cli-shared/middleware';
16
+ import { ... } from '@spectra-the-bot/cli-shared/utils';
17
+ import { ... } from '@spectra-the-bot/cli-shared/testing';
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Middleware
23
+
24
+ ### `apiKeyAuth` + `MissingApiKeyError` (auth)
25
+
26
+ Reads an API key from an env var and throws a typed error if missing.
27
+
28
+ ```ts
29
+ import { apiKeyAuth, MissingApiKeyError } from '@spectra-the-bot/cli-shared/middleware';
30
+
31
+ try {
32
+ const { apiKey } = apiKeyAuth('ETHERSCAN_API_KEY');
33
+ // use apiKey in request headers
34
+ } catch (err) {
35
+ if (err instanceof MissingApiKeyError) {
36
+ console.error(err.message);
37
+ }
38
+ }
39
+ ```
40
+
41
+ ---
42
+
43
+ ### `withRetry` (retry)
44
+
45
+ Wraps async calls with exponential backoff + jitter.
46
+
47
+ - Retries up to `maxRetries`
48
+ - Delay grows from `baseMs` to `maxMs`
49
+ - For `HttpError` 429/503, respects `Retry-After` headers
50
+
51
+ ```ts
52
+ import { withRetry } from '@spectra-the-bot/cli-shared/middleware';
53
+
54
+ const result = await withRetry(
55
+ () => fetch('https://example.com/data').then((r) => r.json()),
56
+ {
57
+ maxRetries: 4,
58
+ baseMs: 200,
59
+ maxMs: 5_000,
60
+ },
61
+ );
62
+ ```
63
+
64
+ ---
65
+
66
+ ### `createRateLimiter` + `withRateLimit` (rate-limit)
67
+
68
+ Token-bucket rate limiter for request throughput control.
69
+
70
+ ```ts
71
+ import { createRateLimiter, withRateLimit } from '@spectra-the-bot/cli-shared/middleware';
72
+
73
+ const acquire = createRateLimiter({ requestsPerSecond: 5 });
74
+
75
+ const data = await withRateLimit(
76
+ () => fetch('https://example.com/items').then((r) => r.json()),
77
+ acquire,
78
+ );
79
+ ```
80
+
81
+ ---
82
+
83
+ ### `paginateCursor` + `paginateOffset` (pagination)
84
+
85
+ Async iterators that flatten paged APIs.
86
+
87
+ #### Cursor pagination
88
+
89
+ ```ts
90
+ import { paginateCursor } from '@spectra-the-bot/cli-shared/middleware';
91
+
92
+ for await (const item of paginateCursor({
93
+ fetchPage: async (cursor) => {
94
+ const res = await fetch(`/api/items?cursor=${cursor ?? ''}`).then((r) => r.json());
95
+ return { items: res.items, nextCursor: res.nextCursor };
96
+ },
97
+ })) {
98
+ console.log(item);
99
+ }
100
+ ```
101
+
102
+ #### Offset pagination
103
+
104
+ ```ts
105
+ import { paginateOffset } from '@spectra-the-bot/cli-shared/middleware';
106
+
107
+ for await (const item of paginateOffset({
108
+ limit: 100,
109
+ fetchPage: async (offset, limit) => {
110
+ const res = await fetch(`/api/items?offset=${offset}&limit=${limit}`).then((r) => r.json());
111
+ return { items: res.items, total: res.total };
112
+ },
113
+ })) {
114
+ console.log(item);
115
+ }
116
+ ```
117
+
118
+ ---
119
+
120
+ ## Utils
121
+
122
+ ### `createHttpClient` + `HttpError` (http client)
123
+
124
+ Typed fetch wrapper with:
125
+
126
+ - `baseUrl`
127
+ - default and per-request headers
128
+ - query serialization
129
+ - JSON request body encoding
130
+ - `HttpError` on non-2xx responses
131
+
132
+ ```ts
133
+ import { createHttpClient, HttpError } from '@spectra-the-bot/cli-shared/utils';
134
+
135
+ type Proposal = { id: string; title: string };
136
+
137
+ const client = createHttpClient({
138
+ baseUrl: 'https://api.assembly.abs.xyz',
139
+ defaultHeaders: {
140
+ 'X-Api-Key': process.env.ASSEMBLY_API_KEY ?? '',
141
+ },
142
+ });
143
+
144
+ try {
145
+ const proposals = await client.request<Proposal[]>('/v1/proposals', {
146
+ query: { status: 'active' },
147
+ });
148
+ console.log(proposals);
149
+ } catch (err) {
150
+ if (err instanceof HttpError) {
151
+ console.error(err.status, err.statusText, err.body);
152
+ }
153
+ }
154
+ ```
155
+
156
+ ---
157
+
158
+ ### Format helpers (`weiToEth`, `checksumAddress`, `formatTimestamp`, `truncate`) (format)
159
+
160
+ ```ts
161
+ import {
162
+ weiToEth,
163
+ checksumAddress,
164
+ formatTimestamp,
165
+ truncate,
166
+ } from '@spectra-the-bot/cli-shared/utils';
167
+
168
+ weiToEth('1234500000000000000');
169
+ // => "1.2345"
170
+
171
+ checksumAddress('0x742d35cc6634c0532925a3b844bc454e4438f44e');
172
+ // => EIP-55 checksummed address
173
+
174
+ formatTimestamp(1700000000);
175
+ // => "2023-11-14T22:13:20.000Z"
176
+
177
+ truncate('0x742d35cc6634c0532925a3b844bc454e4438f44e');
178
+ // => "0x742d...f44e"
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Testing
184
+
185
+ ### `createMockServer` (mock-server)
186
+
187
+ Spin up a lightweight local HTTP server for integration tests.
188
+
189
+ - Configure route responses (`addRoute`)
190
+ - Record incoming requests (`requests`)
191
+ - Cleanly close after tests (`close`)
192
+
193
+ ```ts
194
+ import { createMockServer } from '@spectra-the-bot/cli-shared/testing';
195
+
196
+ const server = await createMockServer();
197
+
198
+ server.addRoute('GET', '/v1/proposals', {
199
+ status: 200,
200
+ body: [{ id: '1', title: 'Test Proposal' }],
201
+ });
202
+
203
+ const res = await fetch(`${server.url}/v1/proposals`);
204
+ const data = await res.json();
205
+
206
+ console.log(data);
207
+ console.log(server.requests[0]); // method/url/headers/body
208
+
209
+ await server.close();
210
+ ```
211
+
212
+ ---
213
+
214
+ ## Export summary
215
+
216
+ - **Middleware**: `apiKeyAuth`, `MissingApiKeyError`, `withRetry`, `createRateLimiter`, `withRateLimit`, `paginateCursor`, `paginateOffset`
217
+ - **Utils**: `createHttpClient`, `HttpError`, `weiToEth`, `checksumAddress`, `formatTimestamp`, `truncate`
218
+ - **Testing**: `createMockServer`
@@ -0,0 +1,124 @@
1
+ declare class MissingApiKeyError extends Error {
2
+ constructor(envVar: string);
3
+ }
4
+ interface ApiKeyAuthContext {
5
+ apiKey: string;
6
+ }
7
+ /**
8
+ * Reads an API key from the environment and returns it.
9
+ * Throws MissingApiKeyError if the variable is not set.
10
+ */
11
+ declare function apiKeyAuth(envVar: string): ApiKeyAuthContext;
12
+
13
+ interface CursorPaginationOptions<T> {
14
+ fetchPage: (cursor: string | null) => Promise<{
15
+ items: T[];
16
+ nextCursor: string | null;
17
+ }>;
18
+ }
19
+ interface OffsetPaginationOptions<T> {
20
+ fetchPage: (offset: number, limit: number) => Promise<{
21
+ items: T[];
22
+ total: number;
23
+ }>;
24
+ limit?: number;
25
+ }
26
+ /**
27
+ * Async iterator for cursor-based pagination.
28
+ */
29
+ declare function paginateCursor<T>(options: CursorPaginationOptions<T>): AsyncGenerator<T>;
30
+ /**
31
+ * Async iterator for offset-based pagination.
32
+ */
33
+ declare function paginateOffset<T>(options: OffsetPaginationOptions<T>): AsyncGenerator<T>;
34
+
35
+ interface RateLimitOptions {
36
+ requestsPerSecond: number;
37
+ }
38
+ /**
39
+ * Token bucket rate limiter. Returns a function that must be called
40
+ * before each request; it resolves when the request is allowed.
41
+ */
42
+ declare function createRateLimiter(options: RateLimitOptions): () => Promise<void>;
43
+ /**
44
+ * Wraps a fetch-like function with token bucket rate limiting.
45
+ */
46
+ declare function withRateLimit<T>(fn: () => Promise<T>, acquire: () => Promise<void>): Promise<T>;
47
+
48
+ interface RetryOptions {
49
+ maxRetries: number;
50
+ baseMs: number;
51
+ maxMs: number;
52
+ }
53
+ /**
54
+ * Wraps a fetch-like function with exponential backoff retry logic.
55
+ * Respects Retry-After headers on 429/503 responses.
56
+ */
57
+ declare function withRetry<T>(fn: () => Promise<T>, options: RetryOptions): Promise<T>;
58
+
59
+ interface RecordedRequest {
60
+ method: string;
61
+ url: string;
62
+ headers: Record<string, string | string[] | undefined>;
63
+ body: string;
64
+ }
65
+ interface MockResponse {
66
+ status?: number;
67
+ headers?: Record<string, string>;
68
+ body?: unknown;
69
+ }
70
+ interface MockServer {
71
+ url: string;
72
+ requests: RecordedRequest[];
73
+ addRoute(method: string, path: string, response: MockResponse): void;
74
+ close(): Promise<void>;
75
+ }
76
+ /**
77
+ * Creates a lightweight HTTP mock server for integration testing.
78
+ * Records all incoming requests and returns configured fixture responses.
79
+ */
80
+ declare function createMockServer(): Promise<MockServer>;
81
+
82
+ /**
83
+ * Converts wei (as bigint or string) to a human-readable ETH string.
84
+ */
85
+ declare function weiToEth(wei: bigint | string, decimals?: number): string;
86
+ /**
87
+ * Checksums an Ethereum address using EIP-55.
88
+ * Accepts lowercase or mixed-case hex addresses.
89
+ */
90
+ declare function checksumAddress(address: string): string;
91
+ /**
92
+ * Formats a Unix timestamp (seconds) to a human-readable string.
93
+ */
94
+ declare function formatTimestamp(unixSeconds: number): string;
95
+ /**
96
+ * Truncates a string in the middle, e.g. "0x1234...abcd".
97
+ */
98
+ declare function truncate(str: string, prefixLen?: number, suffixLen?: number): string;
99
+
100
+ interface HttpClientOptions {
101
+ baseUrl: string;
102
+ defaultHeaders?: Record<string, string>;
103
+ }
104
+ interface RequestOptions {
105
+ method?: string;
106
+ headers?: Record<string, string>;
107
+ query?: Record<string, string | number | boolean | undefined | null>;
108
+ body?: unknown;
109
+ }
110
+ declare class HttpError extends Error {
111
+ readonly status: number;
112
+ readonly statusText: string;
113
+ readonly body: string;
114
+ readonly headers: Headers;
115
+ constructor(status: number, statusText: string, body: string, headers?: Headers);
116
+ }
117
+ /**
118
+ * Typed fetch wrapper with base URL, default headers, query serialization, and error handling.
119
+ */
120
+ declare function createHttpClient(options: HttpClientOptions): {
121
+ request: <T>(path: string, opts?: RequestOptions) => Promise<T>;
122
+ };
123
+
124
+ export { type ApiKeyAuthContext, type CursorPaginationOptions, type HttpClientOptions, HttpError, MissingApiKeyError, type MockResponse, type MockServer, type OffsetPaginationOptions, type RateLimitOptions, type RecordedRequest, type RequestOptions, type RetryOptions, apiKeyAuth, checksumAddress, createHttpClient, createMockServer, createRateLimiter, formatTimestamp, paginateCursor, paginateOffset, truncate, weiToEth, withRateLimit, withRetry };
package/dist/index.js ADDED
@@ -0,0 +1,257 @@
1
+ // src/middleware/auth.ts
2
+ var MissingApiKeyError = class extends Error {
3
+ constructor(envVar) {
4
+ super(`Missing required API key. Set the ${envVar} environment variable.`);
5
+ this.name = "MissingApiKeyError";
6
+ }
7
+ };
8
+ function apiKeyAuth(envVar) {
9
+ const apiKey = process.env[envVar];
10
+ if (!apiKey) {
11
+ throw new MissingApiKeyError(envVar);
12
+ }
13
+ return { apiKey };
14
+ }
15
+
16
+ // src/middleware/pagination.ts
17
+ async function* paginateCursor(options) {
18
+ let cursor = null;
19
+ while (true) {
20
+ const { items, nextCursor } = await options.fetchPage(cursor);
21
+ for (const item of items) {
22
+ yield item;
23
+ }
24
+ if (!nextCursor) break;
25
+ cursor = nextCursor;
26
+ }
27
+ }
28
+ async function* paginateOffset(options) {
29
+ const limit = options.limit ?? 100;
30
+ let offset = 0;
31
+ while (true) {
32
+ const { items, total } = await options.fetchPage(offset, limit);
33
+ for (const item of items) {
34
+ yield item;
35
+ }
36
+ offset += items.length;
37
+ if (offset >= total || items.length === 0) break;
38
+ }
39
+ }
40
+
41
+ // src/middleware/rate-limit.ts
42
+ function createRateLimiter(options) {
43
+ const { requestsPerSecond } = options;
44
+ const intervalMs = 1e3 / requestsPerSecond;
45
+ const queue = [];
46
+ let tokens = requestsPerSecond;
47
+ let lastRefill = Date.now();
48
+ let processingInterval = null;
49
+ function refill() {
50
+ const now = Date.now();
51
+ const elapsed = now - lastRefill;
52
+ const newTokens = elapsed / 1e3 * requestsPerSecond;
53
+ tokens = Math.min(requestsPerSecond, tokens + newTokens);
54
+ lastRefill = now;
55
+ }
56
+ function processQueue() {
57
+ refill();
58
+ while (queue.length > 0 && tokens >= 1) {
59
+ tokens -= 1;
60
+ const resolve = queue.shift();
61
+ resolve?.();
62
+ }
63
+ if (queue.length === 0 && processingInterval !== null) {
64
+ clearInterval(processingInterval);
65
+ processingInterval = null;
66
+ }
67
+ }
68
+ return () => {
69
+ return new Promise((resolve) => {
70
+ refill();
71
+ if (tokens >= 1) {
72
+ tokens -= 1;
73
+ resolve();
74
+ return;
75
+ }
76
+ queue.push(resolve);
77
+ if (processingInterval === null) {
78
+ processingInterval = setInterval(processQueue, intervalMs);
79
+ }
80
+ });
81
+ };
82
+ }
83
+ function withRateLimit(fn, acquire) {
84
+ return acquire().then(() => fn());
85
+ }
86
+
87
+ // src/utils/http.ts
88
+ var HttpError = class extends Error {
89
+ constructor(status, statusText, body, headers = new Headers()) {
90
+ super(`HTTP ${status} ${statusText}: ${body}`);
91
+ this.status = status;
92
+ this.statusText = statusText;
93
+ this.body = body;
94
+ this.headers = headers;
95
+ this.name = "HttpError";
96
+ }
97
+ };
98
+ function serializeQuery(params) {
99
+ const parts = [];
100
+ for (const [key, value] of Object.entries(params)) {
101
+ if (value === void 0 || value === null) continue;
102
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
103
+ }
104
+ return parts.length > 0 ? `?${parts.join("&")}` : "";
105
+ }
106
+ function createHttpClient(options) {
107
+ const { baseUrl, defaultHeaders = {} } = options;
108
+ async function request(path, opts = {}) {
109
+ const { method = "GET", headers = {}, query = {}, body } = opts;
110
+ const qs = serializeQuery(query);
111
+ const url = `${baseUrl}${path}${qs}`;
112
+ const init = {
113
+ method
114
+ };
115
+ const mergedHeaders = {
116
+ ...defaultHeaders,
117
+ ...headers
118
+ };
119
+ if (body !== void 0) {
120
+ mergedHeaders["Content-Type"] ??= "application/json";
121
+ init.body = JSON.stringify(body);
122
+ }
123
+ init.headers = mergedHeaders;
124
+ const res = await fetch(url, init);
125
+ if (!res.ok) {
126
+ const text = await res.text();
127
+ throw new HttpError(res.status, res.statusText, text, res.headers);
128
+ }
129
+ const contentType = res.headers.get("content-type") ?? "";
130
+ if (contentType.includes("application/json")) {
131
+ return res.json();
132
+ }
133
+ return res.text();
134
+ }
135
+ return { request };
136
+ }
137
+
138
+ // src/middleware/retry.ts
139
+ function sleep(ms) {
140
+ return new Promise((resolve) => setTimeout(resolve, ms));
141
+ }
142
+ function jitter(ms) {
143
+ return ms * (0.5 + Math.random() * 0.5);
144
+ }
145
+ function parseRetryAfter(headers) {
146
+ const retryAfter = headers.get("Retry-After");
147
+ if (!retryAfter) return null;
148
+ const seconds = Number(retryAfter);
149
+ if (!Number.isNaN(seconds)) return seconds * 1e3;
150
+ const date = Date.parse(retryAfter);
151
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
152
+ return null;
153
+ }
154
+ async function withRetry(fn, options) {
155
+ const { maxRetries, baseMs, maxMs } = options;
156
+ let attempt = 0;
157
+ while (true) {
158
+ try {
159
+ return await fn();
160
+ } catch (err) {
161
+ if (attempt >= maxRetries) throw err;
162
+ let delayMs;
163
+ if (err instanceof HttpError && (err.status === 429 || err.status === 503)) {
164
+ const retryAfterMs = parseRetryAfter(err.headers);
165
+ delayMs = retryAfterMs ?? Math.min(baseMs * 2 ** attempt, maxMs);
166
+ } else {
167
+ delayMs = Math.min(baseMs * 2 ** attempt, maxMs);
168
+ }
169
+ await sleep(jitter(delayMs));
170
+ attempt++;
171
+ }
172
+ }
173
+ }
174
+
175
+ // src/testing/mock-server.ts
176
+ import { createServer } from "http";
177
+ async function createMockServer() {
178
+ const routes = /* @__PURE__ */ new Map();
179
+ const requests = [];
180
+ const server = createServer((req, res) => {
181
+ let body = "";
182
+ req.on("data", (chunk) => {
183
+ body += chunk.toString();
184
+ });
185
+ req.on("end", () => {
186
+ const recorded = {
187
+ method: req.method ?? "GET",
188
+ url: req.url ?? "/",
189
+ headers: req.headers,
190
+ body
191
+ };
192
+ requests.push(recorded);
193
+ const key = `${recorded.method}:${recorded.url.split("?")[0]}`;
194
+ const mock = routes.get(key) ?? routes.get("*");
195
+ const status = mock?.status ?? 200;
196
+ const responseHeaders = mock?.headers ?? { "Content-Type": "application/json" };
197
+ const responseBody = mock?.body !== void 0 ? JSON.stringify(mock.body) : "{}";
198
+ res.writeHead(status, responseHeaders);
199
+ res.end(responseBody);
200
+ });
201
+ });
202
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
203
+ const address = server.address();
204
+ if (!address || typeof address === "string") {
205
+ throw new Error("Failed to get server address");
206
+ }
207
+ return {
208
+ url: `http://127.0.0.1:${address.port}`,
209
+ requests,
210
+ addRoute(method, path, response) {
211
+ routes.set(`${method.toUpperCase()}:${path}`, response);
212
+ },
213
+ close() {
214
+ return new Promise((resolve, reject) => {
215
+ server.close((err) => err ? reject(err) : resolve());
216
+ });
217
+ }
218
+ };
219
+ }
220
+
221
+ // src/utils/format.ts
222
+ import { Address } from "ox";
223
+ var ETH_DECIMALS = 18n;
224
+ var WEI_PER_ETH = 10n ** ETH_DECIMALS;
225
+ function weiToEth(wei, decimals = 6) {
226
+ const weiValue = typeof wei === "string" ? BigInt(wei) : wei;
227
+ const whole = weiValue / WEI_PER_ETH;
228
+ const frac = weiValue % WEI_PER_ETH;
229
+ const fracStr = frac.toString().padStart(18, "0").slice(0, decimals).replace(/0+$/, "");
230
+ return fracStr.length > 0 ? `${whole}.${fracStr}` : `${whole}`;
231
+ }
232
+ function checksumAddress(address) {
233
+ return Address.checksum(address);
234
+ }
235
+ function formatTimestamp(unixSeconds) {
236
+ return new Date(unixSeconds * 1e3).toISOString();
237
+ }
238
+ function truncate(str, prefixLen = 6, suffixLen = 4) {
239
+ if (str.length <= prefixLen + suffixLen + 3) return str;
240
+ return `${str.slice(0, prefixLen)}...${str.slice(-suffixLen)}`;
241
+ }
242
+ export {
243
+ HttpError,
244
+ MissingApiKeyError,
245
+ apiKeyAuth,
246
+ checksumAddress,
247
+ createHttpClient,
248
+ createMockServer,
249
+ createRateLimiter,
250
+ formatTimestamp,
251
+ paginateCursor,
252
+ paginateOffset,
253
+ truncate,
254
+ weiToEth,
255
+ withRateLimit,
256
+ withRetry
257
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@spectratools/cli-shared",
3
+ "version": "0.1.0",
4
+ "description": "Shared middleware, utilities, and testing helpers for spectra CLI tools",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "spectra-the-bot",
8
+ "engines": {
9
+ "node": ">=20"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./src/index.ts",
14
+ "default": "./src/index.ts"
15
+ },
16
+ "./middleware": {
17
+ "types": "./src/middleware/index.ts",
18
+ "default": "./src/middleware/index.ts"
19
+ },
20
+ "./utils": {
21
+ "types": "./src/utils/index.ts",
22
+ "default": "./src/utils/index.ts"
23
+ },
24
+ "./testing": {
25
+ "types": "./src/testing/index.ts",
26
+ "default": "./src/testing/index.ts"
27
+ }
28
+ },
29
+ "devDependencies": {
30
+ "typescript": "5.7.3",
31
+ "vitest": "2.1.8"
32
+ },
33
+ "dependencies": {
34
+ "ox": "^0.14.0"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "README.md"
39
+ ],
40
+ "main": "./dist/index.js",
41
+ "types": "./dist/index.d.ts",
42
+ "scripts": {
43
+ "build": "tsup",
44
+ "typecheck": "tsc --noEmit -p tsconfig.json",
45
+ "test": "vitest run"
46
+ }
47
+ }