@maced/api-client
v0.9.1
Published
Typed TypeScript client for the Maced API (api.maced.ai).
Downloads
1,708
Maintainers
Readme
@maced/api-client
Typed TypeScript SDK for the Maced API.
⚠️ Backend only
Do not use this SDK in browsers, React Native, Electron, or any client
that ships to end users. API keys (mc_live_* / mc_dev_*) are secrets:
they trigger paid pentests and return private report content. Anyone who
sees your bundle can read the key.
If you want to surface Maced data in a frontend, put a thin endpoint on
your backend that calls @maced/api-client, and have your frontend
call that endpoint. Standard BFF pattern:
React / Mobile → your backend (uses @maced/api-client) → api.maced.ai
public private privateSame reason stripe is a separate SDK from @stripe/stripe-js.
- Fully typed — every endpoint, param, response, and error
- Ergonomic —
maced.pentests.create(...)+ full-verb escape hatch - Reliable — built-in retry, timeout, and request cancellation
- Observable — pluggable
onRequest/onResponse/onErrorhooks - Safe — throws
MacedApiErrorwith structured body + status helpers - Small — ~4kB packed, zero runtime deps beyond
openapi-fetch - Works everywhere — Node 18+, Bun, Deno, Cloudflare Workers, browsers
Install
bun add @maced/api-client
# or
pnpm add @maced/api-client
# or
npm install @maced/api-clientQuickstart
import { createMacedClient } from "@maced/api-client";
const maced = createMacedClient({ apiKey: process.env.MACED_API_KEY! });
// Create a pentest
const run = await maced.pentests.create({
targetUrl: "https://target.example.com",
});
// Wait for completion (polls /v1/pentests/:id until terminal)
const done = await maced.pentests.waitForCompletion(run.id, {
onProgress: (r) =>
console.log(`${r.progress.completedAgents}/${r.progress.totalAgents} agents`),
});
// Fetch the report
if (done.status === "completed") {
const report = await maced.pentests.report(done.id);
console.log(report.markdown);
}Get an API key at https://maced.ai/settings/api.
API
Client factory
createMacedClient(options: MacedClientOptions): MacedClient| option | type | default | notes |
|--------------|-----------------------|-----------------------|-------|
| apiKey | string required | — | mc_live_* or mc_dev_* |
| baseUrl | string | https://api.maced.ai| Point at staging, a fork, a proxy |
| timeoutMs | number | 30_000 | Per-request AbortSignal. 0 disables |
| retry | RetryOptions | 3 attempts, 250ms base| 408/425/429/5xx with exp. backoff + jitter. Honors Retry-After |
| logger | LoggingHooks | — | onRequest / onResponse / onError |
| userAgent | string | — | Appended to the SDK User-Agent |
| fetch | typeof fetch | globalThis.fetch | Custom transport / test override |
Pentests
maced.pentests.create(body) // → PentestCreated
maced.pentests.list({ status?, limit? }) // → Pentest[]
maced.pentests.get(id) // → PentestWithProgress
maced.pentests.report(id) // → PentestReport (markdown + cost + duration)
maced.pentests.events(id) // → PentestEvent[] (live agent events)
maced.pentests.issues(id) // → Issue[]
maced.pentests.progress(id) // → PentestProgress
maced.pentests.cancel(id) // → Pentest
maced.pentests.waitForCompletion(id, opts?) // polls until terminal
maced.pentests.streamEvents(id, opts?) // AsyncIterable<PentestEvent>Issues
maced.issues.list({ severity?, status?, pentestId? }) // → Issue[]
maced.issues.update(issueId, { status }) // → IssueDomains
maced.domains.list() // → VerifiedDomain[]
maced.domains.verify(domain) // → DomainVerificationChallenge
maced.domains.check(domain) // → { verified: boolean }Webhooks
maced.webhooks.getSecret() // → { secret }
verifyMacedWebhook(rawBody, signature, secret, opts?) // → MacedWebhookEvent (throws on bad sig)Usage
maced.usage.get() // → UsageSummary (calendar month-to-date, UTC)
maced.usage.get({ period: "week" }) // → last 7 days
maced.usage.get({ from: "...", to: "..." }) // → arbitrary ISO windowEscape hatch — raw typed fetch
const { data, error, response } = await maced.http.GET("/v1/pentests", {
params: { query: { status: "running" } },
});Returns an openapi-fetch { data, error, response } tuple — never throws. Use
this when you want to inspect the raw Response, forward it, or prefer Result
patterns over exceptions.
Error handling
Namespaced methods throw MacedApiError on non-2xx responses:
import { MacedApiError } from "@maced/api-client";
try {
await maced.pentests.create({ targetUrl: "javascript:alert(1)" });
} catch (err) {
if (err instanceof MacedApiError) {
console.log(err.status); // 400
console.log(err.body); // { error: "targetUrl must be http(s)" }
console.log(err.method); // "POST"
console.log(err.endpoint); // "/v1/pentests"
if (err.isUnauthorized) { /* rotate key */ }
if (err.isRateLimited) { /* back off — SDK already retried with backoff */ }
if (err.isServerError) { /* alert ops */ }
}
throw err;
}Recipes
Stream agent events live
for await (const event of maced.pentests.streamEvents(runId)) {
console.log(`[${event.agent}] ${event.description ?? event.tool}`);
}
// Terminates automatically when the run reaches completed / failed / cancelled.Built on polling + ID-based dedup. Each event yielded at most once. Cancel
with an AbortSignal:
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 5 * 60 * 1000);
for await (const event of maced.pentests.streamEvents(id, { signal: ctrl.signal })) {
//
}Idempotent pentest creation
If your caller might crash between receiving the run id and persisting it,
pass an idempotencyKey so retries return the same run instead of creating
duplicates. The server holds the replay window for 24h.
const run = await maced.pentests.create(
{ targetUrl: "https://target" },
{ idempotencyKey: crypto.randomUUID() },
);Build your own UI from structured findings
Every issue ships with structured reproduction + attack-path data so you can render a custom report, build a triage UI, or feed a SIEM — no markdown parsing required:
const issues = await maced.pentests.issues(runId);
for (const i of issues) {
console.log(`${i.severity.toUpperCase()} · ${i.title}`);
console.log(` CVSS: ${i.cvssScore ?? "—"} | CWE: ${i.cweId ?? "—"}`);
console.log(` Endpoint: ${i.affectedEndpoint ?? "—"}`);
// Step-by-step repro, each marked verified by the agent
for (const step of i.validationSteps ?? []) {
console.log(` ${step.verified ? "✓" : "·"} ${step.description}`);
}
// Narrative attack path
if (i.attackPath) {
console.log(` Likelihood: ${i.attackPath.likelihood} (${i.attackPath.likelihoodRationale})`);
console.log(` Impact: ${i.attackPath.impact} (${i.attackPath.impactRationale})`);
}
// Polymorphic evidence — HTTP transcripts, screenshot URLs, code snippets
for (const ev of i.evidence ?? []) {
switch (ev.type) {
case "http": console.log(` [HTTP] ${ev.method} ${ev.url} → ${ev.responseStatus}`); break;
case "screenshot": console.log(` [IMG] ${ev.screenshotUrl}`); break;
case "code": console.log(` [CODE] ${ev.filePath}:${ev.startLine}-${ev.endLine}`); break;
}
}
}Use i.summary for a 2-3 paragraph human-readable explanation, or
maced.pentests.report(id).markdown for the full narrative report ready
to render to PDF with your own branding.
Reseller pattern — attribute runs + meter end-customers
// On create — stash your customer ID + their webhook on each run
const run = await maced.pentests.create({
targetUrl: customerTarget,
webhookUrl: `https://my-backend.example.com/webhooks/maced`,
metadata: { customerId: "acme_123", plan: "enterprise" },
});
// When fetched back, metadata is typed + returned verbatim
const detail = await maced.pentests.get(run.id);
console.log(detail.metadata?.customerId); // "acme_123"
// Bill end-customers against actual Maced cost
const { totalCostUsd, completedRuns } = await maced.usage.get({ period: "month" });
const markup = 2.5;
chargeCustomer({ usd: totalCostUsd * markup });Receive signed webhooks
Fetch your org's signing secret once, store it somewhere safe (env var, secret manager), then verify every inbound webhook:
import { MacedClient, verifyMacedWebhook, MacedWebhookSignatureError } from "@maced/api-client";
// One-time, from a dev shell or deploy script
const maced = createMacedClient({ apiKey: process.env.MACED_API_KEY! });
const { secret } = await maced.webhooks.getSecret();
// → "whsec_5a8d..." — persist to your secret manager
// In your webhook handler (Next.js app router shown)
export async function POST(req: Request) {
const raw = await req.text();
const sig = req.headers.get("x-maced-signature");
try {
const event = await verifyMacedWebhook(raw, sig, process.env.MACED_WEBHOOK_SECRET!);
switch (event.event) {
case "pentest.completed":
await sendCustomerReport(event.runId, event.report);
break;
case "pentest.failed":
await alertOps(event.runId, event.error);
break;
}
return new Response(null, { status: 204 });
} catch (err) {
if (err instanceof MacedWebhookSignatureError) {
return new Response(err.code, { status: 400 });
}
throw err;
}
}Verification is HMAC-SHA256 over ${timestamp}.${rawBody}, timing-safe
comparison, 5-minute clock tolerance (replay protection).
Guard against accidental production calls
const maced = createMacedClient({ apiKey: process.env.MACED_API_KEY! });
if (process.env.NODE_ENV === "test" && maced.environment !== "test") {
throw new Error("Refusing to run tests against a live Maced key");
}Create a pentest and wait for the report
const run = await maced.pentests.create({
targetUrl: "https://target.example.com",
scope: "black-box",
notificationEmail: "[email protected]",
});
const done = await maced.pentests.waitForCompletion(run.id, {
intervalMs: 10_000,
timeoutMs: 60 * 60 * 1000, // 1h
onProgress: (r) => {
console.log(`${r.status}: ${r.progress.completedAgents}/${r.progress.totalAgents}`);
},
});
if (done.status !== "completed") {
throw new Error(`Pentest finished as ${done.status}`);
}
const report = await maced.pentests.report(done.id);
await Bun.write("./report.md", report.markdown);Triage critical issues across all pentests
const open = await maced.issues.list({ severity: "critical", status: "open" });
for (const issue of open) {
console.log(`[${issue.severity}] ${issue.title} — ${issue.runId}`);
}
// Close issues you've already fixed
await Promise.all(
resolvedIds.map((id) => maced.issues.update(id, { status: "resolved" })),
);Verify a domain
const { txtRecord, txtValue, verified } = await maced.domains.verify("acme.com");
if (!verified) {
console.log(`Publish a TXT record at ${txtRecord} with value ${txtValue}`);
}
// Later, after DNS propagates:
const { verified: ok } = await maced.domains.check("acme.com");Plug Sentry / PostHog for observability
import * as Sentry from "@sentry/node";
const maced = createMacedClient({
apiKey: process.env.MACED_API_KEY!,
logger: {
onError: ({ method, url, status }) =>
Sentry.captureMessage(`Maced API ${method} ${url} ${status}`),
onResponse: ({ durationMs, url }) => {
if (durationMs > 2_000) Sentry.captureMessage(`slow: ${url} ${durationMs}ms`);
},
},
});Disable retries for idempotency-sensitive flows
const maced = createMacedClient({
apiKey: process.env.MACED_API_KEY!,
retry: { maxAttempts: 1 },
});Cancel an in-flight request
const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);
const { data, error } = await maced.http.GET("/v1/pentests", {
signal: controller.signal,
});Environments
mc_live_* keys hit production pentests and consume credits. mc_dev_* keys
run simulated pentests against the same API surface — use them in CI and tests.
The client cares only about the key prefix — both hit api.maced.ai.
Runtime support
| Runtime | Status |
|--------------------|--------|
| Node 18+ | ✅ |
| Node 20 / 22 | ✅ |
| Bun | ✅ |
| Deno | ✅ (via npm:@maced/api-client) |
| Cloudflare Workers | ✅ |
| Edge / Vercel Edge | ✅ |
| Browsers | ❌ — see "Backend only" banner above |
| React Native | ❌ — same |
| Electron renderer | ❌ — same |
Versioning
Types are regenerated from the live OpenAPI spec on every API change via a GitHub Action. Each regeneration that produces a diff bumps a patch version and publishes with npm provenance.
Major versions (breaking changes to the SDK surface itself) follow semver.
Links
- API docs: https://api.maced.ai/docs
- OpenAPI spec: https://api.maced.ai/openapi.json
- Dashboard: https://maced.ai
- Generate keys: https://maced.ai/settings/api
- Issues: https://github.com/goatedventures/maced/issues
License
MIT
