sdk reference / javascript

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.

bash
npm install @attestd/sdk
client

new Client(options)

All options are passed as a single object. Only apiKey is required.

OptionDescription
apiKeyRequired. Your Attestd API key.
baseUrlBase URL override. Defaults to "https://api.attestd.io".
timeoutRequest timeout in milliseconds. Default: 10000.
maxRetriesRetry attempts on 5xx errors. Default: 3. Set to 0 to disable.
cachePolicyResult cache policy: "development" (24h), "runtime" (5min, default), "ci" (never expire), or "none".
fetchfetch override for testing. Pass mock.fn from @attestd/sdk/testing.
check.ts
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

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.

cache.ts
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);
batch

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.

batch.ts
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}`);
  }
}
catalog and quota

client.products()

Returns the full Attestd product catalog. Maps to GET /v1/products.

products.ts
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.

cve.ts
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.

usage.ts
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

typescript
interface ProductEntry {
  slug: string;
  displayName: string;
}

interface SupplyChainEntry {
  package: string;
  ecosystem: string;
  displayName: string | null;
}

interface ProductsResult {
  cveProducts: ProductEntry[];
  supplyChainPackages: SupplyChainEntry[];
  total: number;
}
typescript
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;
}
typescript
interface UsageResult {
  tier: string;
  keyCallsThisMonth: number;
  accountCallsThisMonth: number;
  includedCalls: number;
  billingPeriodStart: Date;
  billingPeriodEnd: Date;
  overageCalls: number;
  estimatedOverageUsd: number;
}
models

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.

typescript
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;
}
response

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.).

typescript
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:

check_supply_chain.ts
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 === null

Note: riskState can be 'none' while supplyChain.compromised is true. These are two independent signals. See the Supply Chain Integrity guide.

response

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.

typescript
interface TyposquatSignal {
  detected: boolean;
  resembles: string | null;
  confidence: number;   // 0.0–1.0
  ecosystem: string;    // 'pypi' | 'npm'
  kind: 'typosquat' | 'hallucination';
  likelyIntended: string[];
}
response

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.

typescript
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)
}
errors

Error types

All error classes extend AttestdError extends Error. Each uses Object.setPrototypeOf in its constructor so instanceof works correctly in transpiled CommonJS environments.

ClassWhen thrown
AttestdAuthErrorHTTP 401. Invalid or missing API key.
AttestdRateLimitErrorHTTP 429. Has retryAfter property (seconds).
AttestdUnsupportedProductErrorsupported: false. Has product and version properties.
AttestdAPIErrorHTTP 5xx, network failure, timeout, or malformed response. statusCode is 0 for transport errors.
AttestdErrorBase 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).

errors.ts
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}`);
  }
}
testing

@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.

gate.test.ts
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.

retry.test.ts
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.

fixtures.ts
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' });
related