@spectratools/cli-shared 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,28 @@
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 checksumAddress(address) {
13
+ return Address.checksum(address);
14
+ }
15
+ function formatTimestamp(unixSeconds) {
16
+ return new Date(unixSeconds * 1e3).toISOString();
17
+ }
18
+ function truncate(str, prefixLen = 6, suffixLen = 4) {
19
+ if (str.length <= prefixLen + suffixLen + 3) return str;
20
+ return `${str.slice(0, prefixLen)}...${str.slice(-suffixLen)}`;
21
+ }
22
+
23
+ export {
24
+ weiToEth,
25
+ checksumAddress,
26
+ formatTimestamp,
27
+ truncate
28
+ };
@@ -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, truncate, weiToEth } from './utils/index.js';
package/dist/index.js CHANGED
@@ -1,244 +1,25 @@
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
+ checksumAddress,
12
+ formatTimestamp,
13
+ truncate,
14
+ weiToEth
15
+ } from "./chunk-IW62SQFU.js";
16
+ import {
17
+ HttpError,
18
+ createHttpClient
19
+ } from "./chunk-TKORXDDX.js";
20
+ import {
21
+ createMockServer
22
+ } from "./chunk-QEANTXUE.js";
242
23
  export {
243
24
  HttpError,
244
25
  MissingApiKeyError,
@@ -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,43 @@
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
+ * Checksums an Ethereum address using EIP-55.
7
+ * Accepts lowercase or mixed-case hex addresses.
8
+ */
9
+ declare function checksumAddress(address: string): string;
10
+ /**
11
+ * Formats a Unix timestamp (seconds) to a human-readable string.
12
+ */
13
+ declare function formatTimestamp(unixSeconds: number): string;
14
+ /**
15
+ * Truncates a string in the middle, e.g. "0x1234...abcd".
16
+ */
17
+ declare function truncate(str: string, prefixLen?: number, suffixLen?: number): string;
18
+
19
+ interface HttpClientOptions {
20
+ baseUrl: string;
21
+ defaultHeaders?: Record<string, string>;
22
+ }
23
+ interface RequestOptions {
24
+ method?: string;
25
+ headers?: Record<string, string>;
26
+ query?: Record<string, string | number | boolean | undefined | null>;
27
+ body?: unknown;
28
+ }
29
+ declare class HttpError extends Error {
30
+ readonly status: number;
31
+ readonly statusText: string;
32
+ readonly body: string;
33
+ readonly headers: Headers;
34
+ constructor(status: number, statusText: string, body: string, headers?: Headers);
35
+ }
36
+ /**
37
+ * Typed fetch wrapper with base URL, default headers, query serialization, and error handling.
38
+ */
39
+ declare function createHttpClient(options: HttpClientOptions): {
40
+ request: <T>(path: string, opts?: RequestOptions) => Promise<T>;
41
+ };
42
+
43
+ export { type HttpClientOptions, HttpError, type RequestOptions, checksumAddress, createHttpClient, formatTimestamp, truncate, weiToEth };
@@ -0,0 +1,18 @@
1
+ import {
2
+ checksumAddress,
3
+ formatTimestamp,
4
+ truncate,
5
+ weiToEth
6
+ } from "../chunk-IW62SQFU.js";
7
+ import {
8
+ HttpError,
9
+ createHttpClient
10
+ } from "../chunk-TKORXDDX.js";
11
+ export {
12
+ HttpError,
13
+ checksumAddress,
14
+ createHttpClient,
15
+ formatTimestamp,
16
+ truncate,
17
+ weiToEth
18
+ };
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.1",
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": {