API Client Library
@atlascrew/crucible-client is a typed TypeScript client for the Crucible REST and WebSocket APIs. It wraps every endpoint with a strongly-typed method, handles errors consistently, and provides a typed WebSocket client with auto-reconnect.
Why use the client library?
- Typed end-to-end — every request and response has a TypeScript interface
- Zero runtime dependencies — uses native
fetchandWebSocket(Node 22+) - Namespaced methods that mirror REST resources (
client.scenarios.list(),client.executions.pause(id)) - Structured error handling via
CrucibleApiErrorwith status codes - Typed WebSocket events with auto-reconnect and jittered backoff
Installation
npm install @atlascrew/crucible-client
Requires Node.js 22+ or a modern browser environment.
Quick start
import { CrucibleClient } from '@atlascrew/crucible-client';
const client = new CrucibleClient({
baseUrl: 'http://localhost:3000',
});
// List all scenarios
const scenarios = await client.scenarios.list();
// Start an assessment
const { executionId } = await client.assessments.start('owasp-api-1-broken-auth');
// Poll until it finishes
let execution = await client.executions.get(executionId);
while (execution.status === 'running' || execution.status === 'pending') {
await new Promise((r) => setTimeout(r, 2000));
execution = await client.executions.get(executionId);
}
console.log('Score:', execution.report?.score);
Client options
const client = new CrucibleClient({
baseUrl: 'http://localhost:3000', // Required
headers: { Authorization: 'Bearer ...' }, // Optional — sent with every request
timeout: 30_000, // Optional — ms before AbortSignal triggers
fetch: customFetch, // Optional — for testing/custom transports
});
REST API reference
All methods return Promise<T> and throw CrucibleApiError on non-2xx responses.
Health
await client.health();
// → { status, timestamp, scenarios, targetUrl }
Scenarios
await client.scenarios.list();
// → Scenario[]
await client.scenarios.update(id, scenarioData);
// → Scenario
Executions
// List with optional filters
await client.executions.list({
scenarioId: 'my-scenario',
status: ['running', 'completed'], // array or single value
mode: 'assessment', // 'simulation' | 'assessment'
since: Date.now() - 86400000, // ms timestamp
until: Date.now(),
limit: 50,
offset: 0,
});
// → ScenarioExecution[]
await client.executions.get(id);
// → ScenarioExecution
// Control individual executions
await client.executions.pause(id); // → { ok: true }
await client.executions.resume(id); // → { ok: true }
await client.executions.cancel(id); // → { ok: true }
await client.executions.restart(id); // → { executionId: string } (new ID)
// Bulk control
await client.executions.pauseAll(); // → { count: number }
await client.executions.resumeAll(); // → { count: number }
await client.executions.cancelAll(); // → { count: number }
Simulations and Assessments
// Simulation mode: runs the scenario without scoring
await client.simulations.start('my-scenario', { /* optional trigger data */ });
// → { executionId, mode: 'simulation', wsUrl }
// Assessment mode: runs and scores pass/fail
await client.assessments.start('my-scenario', { /* optional trigger data */ });
// → { executionId, mode: 'assessment', reportUrl }
Reports
// Get the parsed report object
await client.reports.get(id);
// → ScenarioExecution (with .report field populated)
// Download report files as Response objects (for streaming/saving)
const jsonRes = await client.reports.json(id);
const htmlRes = await client.reports.html(id);
const pdfRes = await client.reports.pdf(id);
// Save a PDF to disk
import { writeFile } from 'node:fs/promises';
const buffer = Buffer.from(await pdfRes.arrayBuffer());
await writeFile('report.pdf', buffer);
Error handling
Every non-2xx response throws a CrucibleApiError:
import { CrucibleApiError } from '@atlascrew/crucible-client';
try {
await client.executions.pause('non-existent');
} catch (error) {
if (error instanceof CrucibleApiError) {
console.log(error.status); // 404
console.log(error.statusText); // "Not Found"
console.log(error.message); // "Execution not found"
console.log(error.body); // { error: "Execution not found" }
}
}
Common status codes:
| Status | Meaning |
|---|---|
400 |
Invalid request (bad query params, invalid scenario data) |
404 |
Execution or scenario not found |
409 |
Invalid state transition (e.g. pausing a completed execution) |
500 |
Server error |
WebSocket events
The client library ships with a typed WebSocket wrapper:
const socket = client.connect();
// Typed event subscription — returns an unsubscribe function
const unsub = socket.on('execution:completed', (execution) => {
console.log('Finished:', execution.id, 'score:', execution.report?.score);
});
// All event types
socket.on('execution:started', (exec) => { /* ... */ });
socket.on('execution:updated', (exec) => { /* ... */ });
socket.on('execution:completed', (exec) => { /* ... */ });
socket.on('execution:failed', (exec) => { /* ... */ });
socket.on('execution:paused', (exec) => { /* ... */ });
socket.on('execution:cancelled', (exec) => { /* ... */ });
socket.on('execution:resumed', (exec) => { /* ... */ });
socket.on('execution:delta', (delta) => { /* partial update */ });
socket.on('status:update', (exec) => { /* snapshot on connect */ });
socket.on('terminal:output', ({ executionId, data }) => { /* ... */ });
// Connection lifecycle
socket.on('open', () => console.log('connected'));
socket.on('close', ({ code, reason }) => console.log('disconnected'));
socket.on('error', (event) => console.error(event));
// Send typed commands
const sent = socket.send({
type: 'SCENARIO_START',
payload: { scenarioId: 'my-scenario' },
});
// Returns true if sent, false if not connected
// Cleanup
unsub(); // remove a single listener
socket.close(); // disconnect and clear all listeners
Socket options
client.connect({
url: 'ws://custom-host:3000', // Default: derived from client's baseUrl
minReconnectDelay: 1000, // Default: 1s
maxReconnectDelay: 30000, // Default: 30s
autoReconnect: true, // Default: true
});
Reconnection uses exponential backoff with random jitter (50-100% of base delay) to prevent thundering-herd reconnects after server restarts.
Streaming an assessment to completion
Combine REST and WebSocket for a full lifecycle:
const client = new CrucibleClient({ baseUrl: 'http://localhost:3000' });
const socket = client.connect();
const { executionId } = await client.assessments.start('my-scenario');
await new Promise<void>((resolve, reject) => {
socket.on('execution:completed', (exec) => {
if (exec.id === executionId) resolve();
});
socket.on('execution:failed', (exec) => {
if (exec.id === executionId) reject(new Error(exec.error));
});
});
const report = await client.reports.get(executionId);
console.log('Final score:', report.report?.score);
socket.close();
Related
- CLI Reference — the
crucible-clitool uses this library internally - System Overview — REST endpoint and WebSocket protocol details