JavaScript SDK Reference
The @attestd/sdk package is a TypeScript-first client for Node 18+. It has zero runtime dependencies and wraps the native fetch API. Dual ESM + CommonJS build works in any modern Node project or bundler.
npm install @attestd/sdknew Client(options)
All options are passed as a single object. Only apiKey is required.
| Option | Description |
|---|---|
apiKey | Required. Your Attestd API key. |
baseUrl | Base URL override. Defaults to "https://api.attestd.io". |
timeout | Request timeout in milliseconds. Default: 10000. |
maxRetries | Retry attempts on 5xx errors. Default: 3. Set to 0 to disable. |
cachePolicy | Result cache policy: "development" (24h), "runtime" (5min, default), "ci" (never expire), or "none". |
fetch | fetch override for testing. Pass mock.fn from @attestd/sdk/testing. |
import { Client } from '@attestd/sdk';
const client = new Client({ apiKey: process.env.ATTESTD_API_KEY });
const result = await client.check('nginx', '1.20.0');
console.log(result.riskState); // 'high'
console.log(result.activelyExploited); // false
console.log(result.cveIds); // ['CVE-2021-23017']client.check(product, version) returns Promise<RiskResult>. A single instance is safe to reuse across concurrent calls. Results are cached according to cachePolicy.
Cache policies and session stats
Results are cached in memory by product and version. Cache hits do not count against your monthly quota. Use invalidateCache after a patch deploy or advisory webhook. Use stats() to inspect apiCallsMade, cacheHits, batchSaves, and callsSaved. See Best Practices for CI vs production guidance.
import { Client } from '@attestd/sdk';
// runtime (default): 5-minute TTL. development: 24h. ci: never expire. none: always hit API.
const client = new Client({
apiKey: process.env.ATTESTD_API_KEY!,
cachePolicy: 'runtime',
});
const result = await client.check('nginx', '1.20.0');
await client.check('nginx', '1.20.0'); // cache hit, no API call
client.invalidateCache('nginx', '1.20.0');
await client.check('nginx', '1.20.0'); // fetches again
const stats = client.stats();
console.log(stats.apiCallsMade, stats.cacheHits, stats.callsSaved);client.checkBatch(items)
Check up to 100 product versions in one API call. Accepts an array of { product, version } objects (type BatchCheckItem[], exported from @attestd/sdk). Returns results in the same order. null means the product is not in Attestd's coverage. Each item counts as one API call against your quota. If the batch would exceed your quota, AttestdRateLimitError is thrown before any results are delivered and no calls are billed.
import { Client } from '@attestd/sdk';
const client = new Client({ apiKey: process.env.ATTESTD_API_KEY });
const deps = [
{ product: 'log4j', version: '2.14.1' },
{ product: 'nginx', version: '1.27.4' },
{ product: 'openssl', version: '3.0.7' },
];
const results = await client.checkBatch(deps);
for (const [item, result] of deps.map((d, i) => [d, results[i]] as const)) {
if (result === null) {
console.log(`${item.product} ${item.version}: not in coverage`);
} else if (result.riskState === 'critical' || result.riskState === 'high') {
throw new Error(`Deployment blocked: ${item.product} ${item.version} is ${result.riskState}`);
} else {
console.log(`${item.product} ${item.version}: ${result.riskState}`);
}
}client.products()
Returns the full Attestd product catalog. Maps to GET /v1/products.
import { Client } from '@attestd/sdk';
const client = new Client({ apiKey: process.env.ATTESTD_API_KEY! });
const catalog = await client.products();
console.log(catalog.total, catalog.cveProducts[0]?.slug);client.cve(cveId)
Returns CVSS, EPSS, and affected products for a single CVE. Throws AttestdAPIError with statusCode === 404 when not found.
import { Client, AttestdAPIError } from '@attestd/sdk';
const client = new Client({ apiKey: process.env.ATTESTD_API_KEY! });
try {
const detail = await client.cve('CVE-2021-44228');
console.log(detail.cvssScore, detail.epssScore);
} catch (err) {
if (err instanceof AttestdAPIError && err.statusCode === 404) {
console.log('CVE not in database');
}
}client.usage()
Returns quota usage: calls used, included cap, billing period dates, and overage estimate for the authenticated key.
import { Client } from '@attestd/sdk';
const client = new Client({ apiKey: process.env.ATTESTD_API_KEY! });
const usage = await client.usage();
console.log(usage.tier);
console.log(usage.keyCallsThisMonth, '/', usage.includedCalls);
console.log(usage.billingPeriodEnd);ProductsResult, CveDetail, UsageResult
interface ProductEntry {
slug: string;
displayName: string;
}
interface SupplyChainEntry {
package: string;
ecosystem: string;
displayName: string | null;
}
interface ProductsResult {
cveProducts: ProductEntry[];
supplyChainPackages: SupplyChainEntry[];
total: number;
}interface CveDetail {
cveId: string;
description: string | null;
cvssScore: number | null;
cvssVector: string | null;
activelyExploited: boolean;
remoteExploitable: boolean;
authenticationRequired: boolean;
affectedProducts: string[];
epssScore: number | null;
epssPercentile: number | null;
sourcePublishedAt: Date | null;
lastCheckedAt: Date | null;
}interface UsageResult {
tier: string;
keyCallsThisMonth: number;
accountCallsThisMonth: number;
includedCalls: number;
billingPeriodStart: Date;
billingPeriodEnd: Date;
overageCalls: number;
estimatedOverageUsd: number;
}RiskResult
Fields mirror the Python SDK in camelCase. All Date fields are parsed from ISO strings at response time, so you get real Date objects, not strings. See the Response Field Reference for field semantics.
interface RiskResult {
product: string;
version: string;
riskState: RiskState; // 'critical' | 'high' | 'elevated' | 'low' | 'none'
riskFactors: RiskFactor[];
activelyExploited: boolean;
remoteExploitable: boolean;
authenticationRequired: boolean;
patchAvailable: boolean;
fixedVersion: string | null;
confidence: number;
cveIds: string[];
maxEpss: number | null;
cves: CveSummary[];
supplyChain: SupplyChainSignal | null; // null for CVE-only products
typosquat: TyposquatSignal | null; // null when no resemblance detected
lastUpdated: Date;
}SupplyChainSignal
Supply chain integrity data for monitored PyPI and npm packages. Present on RiskResult as the supplyChain field. null for CVE-only products (nginx, PostgreSQL, etc.).
interface SupplyChainSignal {
compromised: boolean;
sources: string[];
malwareType: string | null;
description: string | null;
advisoryUrl: string | null;
compromisedAt: Date | null;
removedAt: Date | null;
/** true = attested; false = baseline drop; null = no baseline */
provenance: boolean | null;
}Usage example: check for compromised PyPI and npm packages:
import { Client } from '@attestd/sdk';
const client = new Client({ apiKey: process.env.ATTESTD_API_KEY });
// Check a monitored PyPI package
const pypiResult = await client.check('langchain', '0.1.0');
if (pypiResult.supplyChain?.compromised) {
console.error('SUPPLY CHAIN ALERT:', pypiResult.supplyChain.description);
process.exit(1);
}
// Check a monitored npm package (scoped names are URL-encoded automatically)
const npmResult = await client.check('@bitwarden/cli', '2026.4.0');
if (npmResult.supplyChain?.compromised) {
console.error('SUPPLY CHAIN ALERT:', npmResult.supplyChain.description);
process.exit(1);
}
// CVE-only products have supplyChain = null
const nginx = await client.check('nginx', '1.20.0');
// nginx.supplyChain === nullNote: riskState can be 'none' while supplyChain.compromised is true. These are two independent signals. See the Supply Chain Integrity guide.
TyposquatSignal
Package name integrity signal for typosquats and AI-hallucinated package names. Present on RiskResult as the typosquat field when populated. For unsupported products, the signal is attached to AttestdUnsupportedProductError.typosquat instead of returning a result. kind is 'typosquat' or 'hallucination'. See Response Fields for API semantics.
interface TyposquatSignal {
detected: boolean;
resembles: string | null;
confidence: number; // 0.0–1.0
ecosystem: string; // 'pypi' | 'npm'
kind: 'typosquat' | 'hallucination';
likelyIntended: string[];
}CveSummary
Per-CVE detail records returned on RiskResult.cves when the API request includes include=cves. Empty array on the default response. See Response Fields for subfield semantics.
interface CveSummary {
cveId: string;
cvssScore: number | null;
activelyExploited: boolean;
remoteExploitable: boolean;
epssScore: number | null; // EPSS probability (0.0–1.0)
epssPercentile: number | null; // EPSS percentile rank (0.0–1.0)
}Error types
All error classes extend AttestdError extends Error. Each uses Object.setPrototypeOf in its constructor so instanceof works correctly in transpiled CommonJS environments.
| Class | When thrown |
|---|---|
AttestdAuthError | HTTP 401. Invalid or missing API key. |
AttestdRateLimitError | HTTP 429. Has retryAfter property (seconds). |
AttestdUnsupportedProductError | supported: false. Has product and version properties. |
AttestdAPIError | HTTP 5xx, network failure, timeout, or malformed response. statusCode is 0 for transport errors. |
AttestdError | Base class. Catch this for a single broad handler. |
401 and 429 are never retried. Only transient 5xx errors and network failures are retried (up to maxRetries, exponential backoff: 1s, 2s, 4s).
import {
Client,
AttestdAuthError,
AttestdRateLimitError,
AttestdUnsupportedProductError,
AttestdAPIError,
} from '@attestd/sdk';
const client = new Client({ apiKey: process.env.ATTESTD_API_KEY });
try {
const result = await client.check('nginx', '1.20.0');
} catch (err) {
if (err instanceof AttestdAuthError) {
console.error('Invalid API key');
} else if (err instanceof AttestdRateLimitError) {
console.error(`Rate limited. Retry in ${err.retryAfter}s`);
} else if (err instanceof AttestdUnsupportedProductError) {
// Product is outside coverage. Not a safety clearance.
// Check typosquat first: the name itself may be the risk.
if (err.typosquat?.detected) {
console.warn(
`${err.product} fails name integrity (kind=${err.typosquat.kind}). Prefer ${err.typosquat.resembles}.`,
);
} else {
console.warn(`${err.product} is outside attestd coverage`);
}
throw err;
} else if (err instanceof AttestdAPIError) {
console.error(`API error (${err.statusCode}): ${err.message}`);
}
}@attestd/sdk/testing
A separate subpath export, not included in the main bundle. Import only in test files. Provides typed mock fetch implementations and fixture bodies so you can test your gate logic without making real API calls.
MockFetch
Returns the same response for every call.
import { Client } from '@attestd/sdk';
import { MockFetch, NGINX_VULNERABLE, NGINX_SAFE } from '@attestd/sdk/testing';
test('blocks deployment on high risk', async () => {
const mock = new MockFetch(200, NGINX_VULNERABLE);
const client = new Client({ apiKey: 'test', fetch: mock.fn });
const result = await client.check('nginx', '1.20.0');
expect(result.riskState).toBe('high');
expect(mock.callCount).toBe(1);
});
test('allows deployment when safe', async () => {
const mock = new MockFetch(200, NGINX_SAFE);
const client = new Client({ apiKey: 'test', fetch: mock.fn });
const result = await client.check('nginx', '1.27.4');
expect(result.riskState).toBe('none');
});SequentialMockFetch
Returns responses from a pre-defined sequence. Use for testing retry logic where early requests fail before a success.
import { Client } from '@attestd/sdk';
import { SequentialMockFetch, NGINX_SAFE } from '@attestd/sdk/testing';
test('retries on 503', async () => {
const mock = new SequentialMockFetch([
[503, {}], // first attempt fails
[503, {}], // second attempt fails
[200, NGINX_SAFE], // third attempt succeeds
]);
const client = new Client({ apiKey: 'test', fetch: mock.fn, maxRetries: 2 });
const result = await client.check('nginx', '1.27.4');
expect(result.riskState).toBe('none');
expect(mock.callCount).toBe(3);
});Canned response bodies
Ready-made response fixtures for common test scenarios. All are plain objects and can be spread to override individual fields.
import {
NGINX_SAFE, // riskState: 'none'
NGINX_VULNERABLE, // riskState: 'high'
LOG4J_CRITICAL, // riskState: 'critical', activelyExploited: true
UNSUPPORTED, // supported: false
LITELLM_COMPROMISED, // supplyChain.compromised: true (PyPI)
PYTORCH_LIGHTNING_COMPROMISED, // supplyChain.compromised: true (PyPI)
BITWARDEN_CLI_COMPROMISED, // supplyChain.compromised: true (npm)
} from '@attestd/sdk/testing';
// Override individual fields
const mock = new MockFetch(200, { ...NGINX_VULNERABLE, risk_state: 'critical' });