# Blast Radius API

Blast Radius answers one question for the npm ecosystem: when a package is compromised, who is actually exposed. The public API is the same code path the console uses — every response carries the latency of the real query that produced it, and nothing is ever invented. There are no rate tiers, no usage caps and no billing. Create a key and use it.

**Base URL** `http://blast-radius-web-zc8b.onrender.com/api/v1`  
**Cost** free, no rate limit, no usage cap  
**License** MIT

## Authentication

Send your key as a bearer token:

    Authorization: Bearer brk_live_...

or, if a header is inconvenient, as `?api_key=`. Keys are created in the dashboard, shown once, and stored only as a SHA-256 digest — a copy of the database yields no working key. Revoking a key takes effect on the next request.

## Quickstart

### curl

```bash
curl -H 'Authorization: Bearer brk_live_...' \
  'http://blast-radius-web-zc8b.onrender.com/api/v1/blast?name=debug&depth=5'
```

### JavaScript

```js
const res = await fetch('http://blast-radius-web-zc8b.onrender.com/api/v1/blast?name=debug&depth=5', {
  headers: { Authorization: 'Bearer brk_live_...' },
});
const { total, histogram, latency_ms } = await res.json();
console.log(`${total} packages exposed in ${latency_ms}ms`);
```

### Python

```python
import requests

r = requests.get(
    'http://blast-radius-web-zc8b.onrender.com/api/v1/blast',
    params={'name': 'debug', 'depth': 5},
    headers={'Authorization': 'Bearer brk_live_...'},
)
r.raise_for_status()
print(r.json()['total'], 'packages exposed')
```

### CI gate

```bash
# fail the build if anything in the lockfile is known-malicious
curl -sS -X POST 'http://blast-radius-web-zc8b.onrender.com/api/v1/audit' \
  -H 'Authorization: Bearer brk_live_...' \
  --data-binary @package-lock.json \
  | jq -e '.verdict != "COMPROMISED"'
```

## Endpoints

### GET `/api/v1/blast`

**Blast radius** — Who is transitively exposed if this package is compromised.

| Parameter | Type | Notes | Description |
| --- | --- | --- | --- |
| `name` | string | required | Package name, e.g. `debug`. |
| `depth` | integer | 1–5, default 5 | How many hops to walk. |
| `limit` | integer | default 5000 | Cap on returned victims. |

```bash
curl -H 'Authorization: Bearer brk_live_...' 'http://blast-radius-web-zc8b.onrender.com/api/v1/blast?name=debug&depth=5'
```

```json
{
  "total": 3688,
  "depth": 5,
  "queries": 6,
  "histogram": [
    {
      "depth": 1,
      "packages": 739
    },
    {
      "depth": 2,
      "packages": 1361
    }
  ],
  "victims": [
    "@11ty/eleventy-dev-server",
    "express"
  ],
  "truncated": false,
  "latency_ms": 1492.0,
  "ok": true
}
```

### GET `/api/v1/resolve`

**Semver resolution** — Of the packages that declare a range on this one, how many would actually have resolved the bad version, and how many were shielded by a pin.

| Parameter | Type | Notes | Description |
| --- | --- | --- | --- |
| `name` | string | required | Package name. |
| `bad_version` | string | required | The compromised version, e.g. `4.4.2`. |

```bash
curl -H 'Authorization: Bearer brk_live_...' 'http://blast-radius-web-zc8b.onrender.com/api/v1/resolve?name=debug&bad_version=4.4.2'
```

```json
{
  "exposed_count": 407,
  "shielded_count": 385,
  "checked": 3659,
  "exposed": [
    {
      "name": "@11ty/eleventy-dev-server",
      "ranges": [
        "^4.4.0"
      ]
    }
  ],
  "latency_ms": 88.4,
  "ok": true
}
```

### POST `/api/v1/lockfile`

**Lockfile check** — Post a package-lock.json body and find out whether a specific incident reaches you, and by which path. Nothing is stored.

| Parameter | Type | Notes | Description |
| --- | --- | --- | --- |
| `name` | string | required | The compromised package. |
| `bad_version` | string | optional | The compromised version. |
| `depth` | integer | default 5 | How far to walk. |

**Body** — The raw package-lock.json (v1, v2 or v3). yarn.lock and pnpm-lock.yaml also parse.

```bash
curl -X POST 'http://blast-radius-web-zc8b.onrender.com/api/v1/lockfile?name=debug&bad_version=4.4.2' \
  -H 'Authorization: Bearer brk_live_...' \
  --data-binary @package-lock.json
```

```json
{
  "verdict": "EXPOSED",
  "installed_version": "4.3.4",
  "paths": [
    {
      "path": [
        "your-app",
        "express",
        "debug"
      ]
    }
  ],
  "latency_ms": 210.5,
  "ok": true
}
```

### POST `/api/v1/audit`

**Malware audit** — Check every package in a lockfile against osv.dev, including MAL- identifiers for confirmed malicious packages.

| Parameter | Type | Notes | Description |
| --- | --- | --- | --- |
| `(none)` |  |  | The lockfile is the body. |

**Body** — The raw package-lock.json.

```bash
curl -X POST 'http://blast-radius-web-zc8b.onrender.com/api/v1/audit' \
  -H 'Authorization: Bearer brk_live_...' \
  --data-binary @package-lock.json
```

```json
{
  "verdict": "VULNERABLE",
  "checked": 812,
  "findings": [
    {
      "package": "event-stream",
      "version": "3.3.6",
      "advisories": [
        {
          "id": "MAL-2018-001",
          "kind": "malware"
        }
      ]
    }
  ],
  "latency_ms": 1840.2,
  "ok": true
}
```

### GET `/api/v1/maintainers`

**Maintainer pivot** — What else the people who publish this package control — the attacker's next move after one account falls.

| Parameter | Type | Notes | Description |
| --- | --- | --- | --- |
| `name` | string | required | Package name. |

```bash
curl -H 'Authorization: Bearer brk_live_...' 'http://blast-radius-web-zc8b.onrender.com/api/v1/maintainers?name=debug'
```

```json
{
  "maintainers": [
    "qix",
    "tootallnate"
  ],
  "also_controls": [
    {
      "package": "https-proxy-agent",
      "direct_dependents": 74
    }
  ],
  "latency_ms": 20.1,
  "ok": true
}
```

### GET `/api/v1/typosquats`

**Typosquat ring** — Names one edit away from this one that exist on npm right now.

| Parameter | Type | Notes | Description |
| --- | --- | --- | --- |
| `name` | string | required | Package name. |

```bash
curl -H 'Authorization: Bearer brk_live_...' 'http://blast-radius-web-zc8b.onrender.com/api/v1/typosquats?name=debug'
```

```json
{
  "candidates": 9,
  "existing": [
    {
      "name": "dbug",
      "latest": "0.4.2",
      "in_graph": false
    }
  ],
  "ok": true
}
```

### GET `/api/v1/subgraph`

**Subgraph** — The exposed set with per-package depth and dependent counts, plus the edges between them — enough to draw the graph yourself.

| Parameter | Type | Notes | Description |
| --- | --- | --- | --- |
| `name` | string | required | Package name. |
| `depth` | integer | 1–5, default 2 | How far to walk. |
| `limit` | integer | default 60 | Node cap. |

```bash
curl -H 'Authorization: Bearer brk_live_...' 'http://blast-radius-web-zc8b.onrender.com/api/v1/subgraph?name=debug&depth=2&limit=60'
```

```json
{
  "root": "debug",
  "nodes": [
    {
      "name": "express",
      "depth": 1,
      "dependents": 212
    }
  ],
  "edges": [
    {
      "from": "debug",
      "to": "express"
    }
  ],
  "ok": true
}
```

### GET `/api/v1/monitors`

**Monitors** — List the packages this key's account watches. POST to add one, DELETE /api/v1/monitors/{id} to stop. Each is re-measured on a timer and any movement raises an alert on the dashboard.

| Parameter | Type | Notes | Description |
| --- | --- | --- | --- |
| `package` | string | POST only | Package to watch. |

```bash
curl -H 'Authorization: Bearer brk_live_...' 'http://blast-radius-web-zc8b.onrender.com/api/v1/monitors'
```

```json
{
  "monitors": [
    {
      "id": "mon_ab12",
      "package": "debug",
      "last_total": 3688,
      "last_check_at": 1787270000.0
    }
  ],
  "ok": true
}
```

### GET `/api/v1/alerts`

**Alerts** — Everything the watch has raised for this account, newest first.

| Parameter | Type | Notes | Description |
| --- | --- | --- | --- |
| `limit` | integer | default 50 | How many to return. |

```bash
curl -H 'Authorization: Bearer brk_live_...' 'http://blast-radius-web-zc8b.onrender.com/api/v1/alerts?limit=20'
```

```json
{
  "alerts": [
    {
      "level": "high",
      "title": "debug blast radius grew by 141",
      "created_at": 1787270000.0
    }
  ],
  "ok": true
}
```

### GET `/api/v1/webhooks`

**Webhooks** — Endpoints this account's alerts are delivered to. POST to add one; the signing secret is returned once. Every delivery carries `X-BlastRadius-Signature: t=<unix>,v1=<hmac-sha256>` over `<timestamp>.<raw body>` — verify it before trusting a payload.

| Parameter | Type | Notes | Description |
| --- | --- | --- | --- |
| `url` | string | POST only | Where to deliver. Must be http(s). |

```bash
curl -H 'Authorization: Bearer brk_live_...' 'http://blast-radius-web-zc8b.onrender.com/api/v1/webhooks'
```

```json
{
  "webhooks": [
    {
      "id": "wh_ab12",
      "url": "https://hooks.example.com/br",
      "deliveries": 42,
      "failures": 0,
      "active": 1
    }
  ],
  "ok": true
}
```

### GET `/api/v1/whoami`

**Whoami** — Confirm a key works and see which account and key it belongs to. The cheapest possible integration test.

```bash
curl -H 'Authorization: Bearer brk_live_...' 'http://blast-radius-web-zc8b.onrender.com/api/v1/whoami'
```

```json
{
  "account": {
    "email": "you@example.com"
  },
  "key": {
    "name": "Default key",
    "prefix": "brk_live_a1b2c3"
  },
  "ok": true
}
```

## Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 200 | `ok` | The query ran. `latency_ms` is the measured time. |
| 401 | `missing_key / bad_key` | No key, an unknown key, or a revoked key. |
| 404 | `not_in_graph` | The package has not been crawled yet. The response says so rather than returning an empty result that reads as safety. |
| 429 | `rate_limited` | Only applied to anonymous browser traffic. Keyed API calls are not capped. |
| 503 | `graph_warming` | HydraDB is cold after a restart. Retry in a few seconds; the client in the console does this automatically. |
