@spectratools/cli-shared
v0.4.1
Published
Shared middleware, utilities, and testing helpers for spectra CLI tools
Downloads
529
Readme
@spectra-the-bot/cli-shared
Shared middleware, utilities, and testing helpers used across @spectra-the-bot CLI packages.
Installation
pnpm add @spectra-the-bot/cli-sharedImport paths
import { ... } from '@spectra-the-bot/cli-shared';
import { ... } from '@spectra-the-bot/cli-shared/middleware';
import { ... } from '@spectra-the-bot/cli-shared/utils';
import { ... } from '@spectra-the-bot/cli-shared/testing';Middleware
apiKeyAuth + MissingApiKeyError (auth)
Reads an API key from an env var and throws a typed error if missing.
import { apiKeyAuth, MissingApiKeyError } from '@spectra-the-bot/cli-shared/middleware';
try {
const { apiKey } = apiKeyAuth('ETHERSCAN_API_KEY');
// use apiKey in request headers
} catch (err) {
if (err instanceof MissingApiKeyError) {
console.error(err.message);
}
}withRetry (retry)
Wraps async calls with exponential backoff + jitter.
- Retries up to
maxRetries - Delay grows from
baseMstomaxMs - For
HttpError429/503, respectsRetry-Afterheaders
import { withRetry } from '@spectra-the-bot/cli-shared/middleware';
const result = await withRetry(
() => fetch('https://example.com/data').then((r) => r.json()),
{
maxRetries: 4,
baseMs: 200,
maxMs: 5_000,
},
);createRateLimiter + withRateLimit (rate-limit)
Token-bucket rate limiter for request throughput control.
import { createRateLimiter, withRateLimit } from '@spectra-the-bot/cli-shared/middleware';
const acquire = createRateLimiter({ requestsPerSecond: 5 });
const data = await withRateLimit(
() => fetch('https://example.com/items').then((r) => r.json()),
acquire,
);paginateCursor + paginateOffset (pagination)
Async iterators that flatten paged APIs.
Cursor pagination
import { paginateCursor } from '@spectra-the-bot/cli-shared/middleware';
for await (const item of paginateCursor({
fetchPage: async (cursor) => {
const res = await fetch(`/api/items?cursor=${cursor ?? ''}`).then((r) => r.json());
return { items: res.items, nextCursor: res.nextCursor };
},
})) {
console.log(item);
}Offset pagination
import { paginateOffset } from '@spectra-the-bot/cli-shared/middleware';
for await (const item of paginateOffset({
limit: 100,
fetchPage: async (offset, limit) => {
const res = await fetch(`/api/items?offset=${offset}&limit=${limit}`).then((r) => r.json());
return { items: res.items, total: res.total };
},
})) {
console.log(item);
}Utils
createHttpClient + HttpError (http client)
Typed fetch wrapper with:
baseUrl- default and per-request headers
- query serialization
- JSON request body encoding
HttpErroron non-2xx responses
import { createHttpClient, HttpError } from '@spectra-the-bot/cli-shared/utils';
type Proposal = { id: string; title: string };
const client = createHttpClient({
baseUrl: 'https://api.assembly.abs.xyz',
defaultHeaders: {
'X-Api-Key': process.env.ASSEMBLY_API_KEY ?? '',
},
});
try {
const proposals = await client.request<Proposal[]>('/v1/proposals', {
query: { status: 'active' },
});
console.log(proposals);
} catch (err) {
if (err instanceof HttpError) {
console.error(err.status, err.statusText, err.body);
}
}Format helpers (weiToEth, checksumAddress, formatTimestamp, truncate) (format)
import {
weiToEth,
checksumAddress,
formatTimestamp,
truncate,
} from '@spectra-the-bot/cli-shared/utils';
weiToEth('1234500000000000000');
// => "1.2345"
checksumAddress('0x742d35cc6634c0532925a3b844bc454e4438f44e');
// => EIP-55 checksummed address
formatTimestamp(1700000000);
// => "2023-11-14T22:13:20.000Z"
truncate('0x742d35cc6634c0532925a3b844bc454e4438f44e');
// => "0x742d...f44e"Testing
createMockServer (mock-server)
Spin up a lightweight local HTTP server for integration tests.
- Configure route responses (
addRoute) - Record incoming requests (
requests) - Cleanly close after tests (
close)
import { createMockServer } from '@spectra-the-bot/cli-shared/testing';
const server = await createMockServer();
server.addRoute('GET', '/v1/proposals', {
status: 200,
body: [{ id: '1', title: 'Test Proposal' }],
});
const res = await fetch(`${server.url}/v1/proposals`);
const data = await res.json();
console.log(data);
console.log(server.requests[0]); // method/url/headers/body
await server.close();Telemetry (OpenTelemetry)
Built-in OpenTelemetry instrumentation for distributed tracing and metrics. Zero overhead when disabled — OTEL modules are lazy-loaded only when telemetry is active.
Initialize telemetry
import { initTelemetry, shutdownTelemetry } from '@spectra-the-bot/cli-shared/telemetry';
// Initialize at CLI startup — no-op if OTEL env vars are not set
initTelemetry('my-cli');
// Shut down gracefully at exit — safe to call even if never initialized
await shutdownTelemetry();Telemetry is enabled when OTEL_EXPORTER_OTLP_ENDPOINT is set or SPECTRA_OTEL_ENABLED=true.
Command spans
import { createCommandSpan, withCommandSpan } from '@spectra-the-bot/cli-shared/telemetry';
// Option 1: Manual span management
const span = createCommandSpan('account balance', { address: '0x...', format: 'json' });
try {
// ... execute command
span.setStatus({ code: SpanStatusCode.OK });
} catch (err) {
recordError(span, err);
throw err;
} finally {
span.end();
}
// Option 2: Automatic span lifecycle
const result = await withCommandSpan('account balance', { address: '0x...' }, async () => {
// ... execute command
return data;
});Child spans
import { withSpan } from '@spectra-the-bot/cli-shared/telemetry';
const result = await withSpan('parse-response', async (span) => {
// span is automatically ended and errors are recorded
return parseData(raw);
});HTTP spans (automatic)
The createHttpClient utility automatically creates child spans for every HTTP request when OTEL is active:
import { createHttpClient } from '@spectra-the-bot/cli-shared/utils';
const client = createHttpClient({ baseUrl: 'https://api.example.com' });
// This request automatically creates an HTTP span with method, URL, and status code
const data = await client.request('/v1/items');Attribute sanitization
Span attributes are automatically sanitized — keys or values matching sensitive patterns (private keys, passwords, API keys, mnemonics, tokens, secrets) are stripped before being attached to spans.
import { sanitizeAttributes } from '@spectra-the-bot/cli-shared/telemetry';
const safe = sanitizeAttributes({
address: '0x1234', // ✓ kept
private_key: '0xdead', // ✗ stripped
api_key: 'sk-secret', // ✗ stripped
format: 'json', // ✓ kept
});
// => { address: '0x1234', format: 'json' }Export summary
- Middleware:
apiKeyAuth,MissingApiKeyError,withRetry,createRateLimiter,withRateLimit,paginateCursor,paginateOffset - Utils:
createHttpClient,HttpError,weiToEth,checksumAddress,formatTimestamp,truncate - Telemetry:
initTelemetry,shutdownTelemetry,createCommandSpan,withCommandSpan,withSpan,recordError,sanitizeAttributes,createHttpSpan,endHttpSpan,endHttpSpanWithError,extractPath - Testing:
createMockServer
