@spectratools/cli-shared 0.1.0 → 0.1.2

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.
@@ -0,0 +1,136 @@
1
+ import {
2
+ HttpError
3
+ } from "./chunk-TKORXDDX.js";
4
+
5
+ // src/middleware/auth.ts
6
+ var MissingApiKeyError = class extends Error {
7
+ constructor(envVar) {
8
+ super(`Missing required API key. Set the ${envVar} environment variable.`);
9
+ this.name = "MissingApiKeyError";
10
+ }
11
+ };
12
+ function apiKeyAuth(envVar) {
13
+ const apiKey = process.env[envVar];
14
+ if (!apiKey) {
15
+ throw new MissingApiKeyError(envVar);
16
+ }
17
+ return { apiKey };
18
+ }
19
+
20
+ // src/middleware/pagination.ts
21
+ async function* paginateCursor(options) {
22
+ let cursor = null;
23
+ while (true) {
24
+ const { items, nextCursor } = await options.fetchPage(cursor);
25
+ for (const item of items) {
26
+ yield item;
27
+ }
28
+ if (!nextCursor) break;
29
+ cursor = nextCursor;
30
+ }
31
+ }
32
+ async function* paginateOffset(options) {
33
+ const limit = options.limit ?? 100;
34
+ let offset = 0;
35
+ while (true) {
36
+ const { items, total } = await options.fetchPage(offset, limit);
37
+ for (const item of items) {
38
+ yield item;
39
+ }
40
+ offset += items.length;
41
+ if (offset >= total || items.length === 0) break;
42
+ }
43
+ }
44
+
45
+ // src/middleware/rate-limit.ts
46
+ function createRateLimiter(options) {
47
+ const { requestsPerSecond } = options;
48
+ const intervalMs = 1e3 / requestsPerSecond;
49
+ const queue = [];
50
+ let tokens = requestsPerSecond;
51
+ let lastRefill = Date.now();
52
+ let processingInterval = null;
53
+ function refill() {
54
+ const now = Date.now();
55
+ const elapsed = now - lastRefill;
56
+ const newTokens = elapsed / 1e3 * requestsPerSecond;
57
+ tokens = Math.min(requestsPerSecond, tokens + newTokens);
58
+ lastRefill = now;
59
+ }
60
+ function processQueue() {
61
+ refill();
62
+ while (queue.length > 0 && tokens >= 1) {
63
+ tokens -= 1;
64
+ const resolve = queue.shift();
65
+ resolve?.();
66
+ }
67
+ if (queue.length === 0 && processingInterval !== null) {
68
+ clearInterval(processingInterval);
69
+ processingInterval = null;
70
+ }
71
+ }
72
+ return () => {
73
+ return new Promise((resolve) => {
74
+ refill();
75
+ if (tokens >= 1) {
76
+ tokens -= 1;
77
+ resolve();
78
+ return;
79
+ }
80
+ queue.push(resolve);
81
+ if (processingInterval === null) {
82
+ processingInterval = setInterval(processQueue, intervalMs);
83
+ }
84
+ });
85
+ };
86
+ }
87
+ function withRateLimit(fn, acquire) {
88
+ return acquire().then(() => fn());
89
+ }
90
+
91
+ // src/middleware/retry.ts
92
+ function sleep(ms) {
93
+ return new Promise((resolve) => setTimeout(resolve, ms));
94
+ }
95
+ function jitter(ms) {
96
+ return ms * (0.5 + Math.random() * 0.5);
97
+ }
98
+ function parseRetryAfter(headers) {
99
+ const retryAfter = headers.get("Retry-After");
100
+ if (!retryAfter) return null;
101
+ const seconds = Number(retryAfter);
102
+ if (!Number.isNaN(seconds)) return seconds * 1e3;
103
+ const date = Date.parse(retryAfter);
104
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
105
+ return null;
106
+ }
107
+ async function withRetry(fn, options) {
108
+ const { maxRetries, baseMs, maxMs } = options;
109
+ let attempt = 0;
110
+ while (true) {
111
+ try {
112
+ return await fn();
113
+ } catch (err) {
114
+ if (attempt >= maxRetries) throw err;
115
+ let delayMs;
116
+ if (err instanceof HttpError && (err.status === 429 || err.status === 503)) {
117
+ const retryAfterMs = parseRetryAfter(err.headers);
118
+ delayMs = retryAfterMs ?? Math.min(baseMs * 2 ** attempt, maxMs);
119
+ } else {
120
+ delayMs = Math.min(baseMs * 2 ** attempt, maxMs);
121
+ }
122
+ await sleep(jitter(delayMs));
123
+ attempt++;
124
+ }
125
+ }
126
+ }
127
+
128
+ export {
129
+ MissingApiKeyError,
130
+ apiKeyAuth,
131
+ paginateCursor,
132
+ paginateOffset,
133
+ createRateLimiter,
134
+ withRateLimit,
135
+ withRetry
136
+ };
@@ -0,0 +1,32 @@
1
+ // src/utils/format.ts
2
+ import { Address } from "ox";
3
+ var ETH_DECIMALS = 18n;
4
+ var WEI_PER_ETH = 10n ** ETH_DECIMALS;
5
+ function weiToEth(wei, decimals = 6) {
6
+ const weiValue = typeof wei === "string" ? BigInt(wei) : wei;
7
+ const whole = weiValue / WEI_PER_ETH;
8
+ const frac = weiValue % WEI_PER_ETH;
9
+ const fracStr = frac.toString().padStart(18, "0").slice(0, decimals).replace(/0+$/, "");
10
+ return fracStr.length > 0 ? `${whole}.${fracStr}` : `${whole}`;
11
+ }
12
+ function isAddress(address) {
13
+ return Address.validate(address);
14
+ }
15
+ function checksumAddress(address) {
16
+ return Address.checksum(address);
17
+ }
18
+ function formatTimestamp(unixSeconds) {
19
+ return new Date(unixSeconds * 1e3).toISOString();
20
+ }
21
+ function truncate(str, prefixLen = 6, suffixLen = 4) {
22
+ if (str.length <= prefixLen + suffixLen + 3) return str;
23
+ return `${str.slice(0, prefixLen)}...${str.slice(-suffixLen)}`;
24
+ }
25
+
26
+ export {
27
+ weiToEth,
28
+ isAddress,
29
+ checksumAddress,
30
+ formatTimestamp,
31
+ truncate
32
+ };
@@ -0,0 +1,49 @@
1
+ // src/testing/mock-server.ts
2
+ import { createServer } from "http";
3
+ async function createMockServer() {
4
+ const routes = /* @__PURE__ */ new Map();
5
+ const requests = [];
6
+ const server = createServer((req, res) => {
7
+ let body = "";
8
+ req.on("data", (chunk) => {
9
+ body += chunk.toString();
10
+ });
11
+ req.on("end", () => {
12
+ const recorded = {
13
+ method: req.method ?? "GET",
14
+ url: req.url ?? "/",
15
+ headers: req.headers,
16
+ body
17
+ };
18
+ requests.push(recorded);
19
+ const key = `${recorded.method}:${recorded.url.split("?")[0]}`;
20
+ const mock = routes.get(key) ?? routes.get("*");
21
+ const status = mock?.status ?? 200;
22
+ const responseHeaders = mock?.headers ?? { "Content-Type": "application/json" };
23
+ const responseBody = mock?.body !== void 0 ? JSON.stringify(mock.body) : "{}";
24
+ res.writeHead(status, responseHeaders);
25
+ res.end(responseBody);
26
+ });
27
+ });
28
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
29
+ const address = server.address();
30
+ if (!address || typeof address === "string") {
31
+ throw new Error("Failed to get server address");
32
+ }
33
+ return {
34
+ url: `http://127.0.0.1:${address.port}`,
35
+ requests,
36
+ addRoute(method, path, response) {
37
+ routes.set(`${method.toUpperCase()}:${path}`, response);
38
+ },
39
+ close() {
40
+ return new Promise((resolve, reject) => {
41
+ server.close((err) => err ? reject(err) : resolve());
42
+ });
43
+ }
44
+ };
45
+ }
46
+
47
+ export {
48
+ createMockServer
49
+ };
@@ -0,0 +1,55 @@
1
+ // src/utils/http.ts
2
+ var HttpError = class extends Error {
3
+ constructor(status, statusText, body, headers = new Headers()) {
4
+ super(`HTTP ${status} ${statusText}: ${body}`);
5
+ this.status = status;
6
+ this.statusText = statusText;
7
+ this.body = body;
8
+ this.headers = headers;
9
+ this.name = "HttpError";
10
+ }
11
+ };
12
+ function serializeQuery(params) {
13
+ const parts = [];
14
+ for (const [key, value] of Object.entries(params)) {
15
+ if (value === void 0 || value === null) continue;
16
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
17
+ }
18
+ return parts.length > 0 ? `?${parts.join("&")}` : "";
19
+ }
20
+ function createHttpClient(options) {
21
+ const { baseUrl, defaultHeaders = {} } = options;
22
+ async function request(path, opts = {}) {
23
+ const { method = "GET", headers = {}, query = {}, body } = opts;
24
+ const qs = serializeQuery(query);
25
+ const url = `${baseUrl}${path}${qs}`;
26
+ const init = {
27
+ method
28
+ };
29
+ const mergedHeaders = {
30
+ ...defaultHeaders,
31
+ ...headers
32
+ };
33
+ if (body !== void 0) {
34
+ mergedHeaders["Content-Type"] ??= "application/json";
35
+ init.body = JSON.stringify(body);
36
+ }
37
+ init.headers = mergedHeaders;
38
+ const res = await fetch(url, init);
39
+ if (!res.ok) {
40
+ const text = await res.text();
41
+ throw new HttpError(res.status, res.statusText, text, res.headers);
42
+ }
43
+ const contentType = res.headers.get("content-type") ?? "";
44
+ if (contentType.includes("application/json")) {
45
+ return res.json();
46
+ }
47
+ return res.text();
48
+ }
49
+ return { request };
50
+ }
51
+
52
+ export {
53
+ HttpError,
54
+ createHttpClient
55
+ };
package/dist/index.d.ts CHANGED
@@ -1,124 +1,3 @@
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 };
1
+ export { ApiKeyAuthContext, CursorPaginationOptions, MissingApiKeyError, OffsetPaginationOptions, RateLimitOptions, RetryOptions, apiKeyAuth, createRateLimiter, paginateCursor, paginateOffset, withRateLimit, withRetry } from './middleware/index.js';
2
+ export { MockResponse, MockServer, RecordedRequest, createMockServer } from './testing/index.js';
3
+ export { HttpClientOptions, HttpError, RequestOptions, checksumAddress, createHttpClient, formatTimestamp, isAddress, truncate, weiToEth } from './utils/index.js';
package/dist/index.js CHANGED
@@ -1,244 +1,26 @@
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
- }
1
+ import {
2
+ MissingApiKeyError,
3
+ apiKeyAuth,
4
+ createRateLimiter,
5
+ paginateCursor,
6
+ paginateOffset,
7
+ withRateLimit,
8
+ withRetry
9
+ } from "./chunk-74BQLM22.js";
10
+ import {
11
+ createMockServer
12
+ } from "./chunk-QEANTXUE.js";
13
+ import {
14
+ checksumAddress,
15
+ formatTimestamp,
16
+ isAddress,
17
+ truncate,
18
+ weiToEth
19
+ } from "./chunk-OPDNZWSI.js";
20
+ import {
21
+ HttpError,
22
+ createHttpClient
23
+ } from "./chunk-TKORXDDX.js";
242
24
  export {
243
25
  HttpError,
244
26
  MissingApiKeyError,
@@ -248,6 +30,7 @@ export {
248
30
  createMockServer,
249
31
  createRateLimiter,
250
32
  formatTimestamp,
33
+ isAddress,
251
34
  paginateCursor,
252
35
  paginateOffset,
253
36
  truncate,
@@ -0,0 +1,59 @@
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
+ export { type ApiKeyAuthContext, type CursorPaginationOptions, MissingApiKeyError, type OffsetPaginationOptions, type RateLimitOptions, type RetryOptions, apiKeyAuth, createRateLimiter, paginateCursor, paginateOffset, withRateLimit, withRetry };
@@ -0,0 +1,19 @@
1
+ import {
2
+ MissingApiKeyError,
3
+ apiKeyAuth,
4
+ createRateLimiter,
5
+ paginateCursor,
6
+ paginateOffset,
7
+ withRateLimit,
8
+ withRetry
9
+ } from "../chunk-74BQLM22.js";
10
+ import "../chunk-TKORXDDX.js";
11
+ export {
12
+ MissingApiKeyError,
13
+ apiKeyAuth,
14
+ createRateLimiter,
15
+ paginateCursor,
16
+ paginateOffset,
17
+ withRateLimit,
18
+ withRetry
19
+ };
@@ -0,0 +1,24 @@
1
+ interface RecordedRequest {
2
+ method: string;
3
+ url: string;
4
+ headers: Record<string, string | string[] | undefined>;
5
+ body: string;
6
+ }
7
+ interface MockResponse {
8
+ status?: number;
9
+ headers?: Record<string, string>;
10
+ body?: unknown;
11
+ }
12
+ interface MockServer {
13
+ url: string;
14
+ requests: RecordedRequest[];
15
+ addRoute(method: string, path: string, response: MockResponse): void;
16
+ close(): Promise<void>;
17
+ }
18
+ /**
19
+ * Creates a lightweight HTTP mock server for integration testing.
20
+ * Records all incoming requests and returns configured fixture responses.
21
+ */
22
+ declare function createMockServer(): Promise<MockServer>;
23
+
24
+ export { type MockResponse, type MockServer, type RecordedRequest, createMockServer };
@@ -0,0 +1,6 @@
1
+ import {
2
+ createMockServer
3
+ } from "../chunk-QEANTXUE.js";
4
+ export {
5
+ createMockServer
6
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Converts wei (as bigint or string) to a human-readable ETH string.
3
+ */
4
+ declare function weiToEth(wei: bigint | string, decimals?: number): string;
5
+ /**
6
+ * Returns true if the value is a valid 0x-prefixed 20-byte hex address.
7
+ */
8
+ declare function isAddress(address: string): boolean;
9
+ /**
10
+ * Checksums an Ethereum address using EIP-55.
11
+ * Accepts lowercase or mixed-case hex addresses.
12
+ */
13
+ declare function checksumAddress(address: string): string;
14
+ /**
15
+ * Formats a Unix timestamp (seconds) to a human-readable string.
16
+ */
17
+ declare function formatTimestamp(unixSeconds: number): string;
18
+ /**
19
+ * Truncates a string in the middle, e.g. "0x1234...abcd".
20
+ */
21
+ declare function truncate(str: string, prefixLen?: number, suffixLen?: number): string;
22
+
23
+ interface HttpClientOptions {
24
+ baseUrl: string;
25
+ defaultHeaders?: Record<string, string>;
26
+ }
27
+ interface RequestOptions {
28
+ method?: string;
29
+ headers?: Record<string, string>;
30
+ query?: Record<string, string | number | boolean | undefined | null>;
31
+ body?: unknown;
32
+ }
33
+ declare class HttpError extends Error {
34
+ readonly status: number;
35
+ readonly statusText: string;
36
+ readonly body: string;
37
+ readonly headers: Headers;
38
+ constructor(status: number, statusText: string, body: string, headers?: Headers);
39
+ }
40
+ /**
41
+ * Typed fetch wrapper with base URL, default headers, query serialization, and error handling.
42
+ */
43
+ declare function createHttpClient(options: HttpClientOptions): {
44
+ request: <T>(path: string, opts?: RequestOptions) => Promise<T>;
45
+ };
46
+
47
+ export { type HttpClientOptions, HttpError, type RequestOptions, checksumAddress, createHttpClient, formatTimestamp, isAddress, truncate, weiToEth };
@@ -0,0 +1,20 @@
1
+ import {
2
+ checksumAddress,
3
+ formatTimestamp,
4
+ isAddress,
5
+ truncate,
6
+ weiToEth
7
+ } from "../chunk-OPDNZWSI.js";
8
+ import {
9
+ HttpError,
10
+ createHttpClient
11
+ } from "../chunk-TKORXDDX.js";
12
+ export {
13
+ HttpError,
14
+ checksumAddress,
15
+ createHttpClient,
16
+ formatTimestamp,
17
+ isAddress,
18
+ truncate,
19
+ weiToEth
20
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spectratools/cli-shared",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Shared middleware, utilities, and testing helpers for spectra CLI tools",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -10,20 +10,20 @@
10
10
  },
11
11
  "exports": {
12
12
  ".": {
13
- "types": "./src/index.ts",
14
- "default": "./src/index.ts"
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
15
  },
16
16
  "./middleware": {
17
- "types": "./src/middleware/index.ts",
18
- "default": "./src/middleware/index.ts"
17
+ "types": "./dist/middleware/index.d.ts",
18
+ "default": "./dist/middleware/index.js"
19
19
  },
20
20
  "./utils": {
21
- "types": "./src/utils/index.ts",
22
- "default": "./src/utils/index.ts"
21
+ "types": "./dist/utils/index.d.ts",
22
+ "default": "./dist/utils/index.js"
23
23
  },
24
24
  "./testing": {
25
- "types": "./src/testing/index.ts",
26
- "default": "./src/testing/index.ts"
25
+ "types": "./dist/testing/index.d.ts",
26
+ "default": "./dist/testing/index.js"
27
27
  }
28
28
  },
29
29
  "devDependencies": {