npm package: @firebreak/vitals
Description: Deep healthcheck endpoints following the HEALTHCHECK_SPEC format
Latest version: 3.0.1

# Vitals (Node.js)

Structured healthcheck endpoints for Node.js services. Register health checks, run them concurrently with timeouts, and expose them via Express or Next.js. Health checks always run — a configurable detail policy controls whether callers see a summary status or full diagnostic results.

## Install

```bash
npm install @firebreak/vitals
```

Peer dependencies are optional — install only what you need:

```bash
npm install pg        # for PostgreSQL checks
npm install ioredis   # for Redis checks
npm install express   # for Express integration
```

## Quick Start (Express)

```typescript
import { HealthcheckRegistry, extractToken, verifyToken } from '@firebreak/vitals';
import { createHealthcheckMiddleware } from '@firebreak/vitals/express';
import { pgPoolCheck } from '@firebreak/vitals/checks/postgres';
import { ioredisClientCheck } from '@firebreak/vitals/checks/redis';
import { httpCheck } from '@firebreak/vitals/checks/http';
import express from 'express';

const registry = new HealthcheckRegistry({ defaultTimeout: 5000 });

registry.add('postgres', pgPoolCheck(pool));
registry.add('redis', ioredisClientCheck(redisClient));
registry.add('api', httpCheck('https://api.example.com/health'));

const app = express();
app.get('/vitals', createHealthcheckMiddleware({
  registry,
  resolveDetail: (req) => {
    const token = extractToken({
      queryParams: req.queryParams as Record<string, string>,
      authorizationHeader: req.authorizationHeader as string | null,
    });
    if (token && verifyToken(token, process.env.VITALS_TOKEN!)) return 'full';
    return 'summary';
  },
  metadata: {
    build: process.env.BUILD_SHA,
    buildTimestamp: process.env.BUILD_TIMESTAMP,
  },
}));
```

Hit `/vitals` without a valid token to get a summary response with real health status:

```json
{ "status": "healthy", "timestamp": "...", "build": "stg-45d76e5", "buildTimestamp": "2025-02-25T19:41:56Z" }
```

When `resolveDetail` returns `'full'`, the full response with check results is returned.

## Quick Start (Next.js)

```typescript
// app/vitals/route.ts
import { HealthcheckRegistry, extractToken, verifyToken } from '@firebreak/vitals';
import { createNextHandler } from '@firebreak/vitals/next';
import { pgClientCheck } from '@firebreak/vitals/checks/postgres';
import { httpCheck } from '@firebreak/vitals/checks/http';

const registry = new HealthcheckRegistry({ defaultTimeout: 5000 });

if (process.env.DATABASE_URL) {
  registry.add('postgres', pgClientCheck(process.env.DATABASE_URL));
}
registry.add('api', httpCheck(`${process.env.API_BASE_URL}/health`));

export const GET = createNextHandler({
  registry,
  resolveDetail: (req) => {
    const token = extractToken({
      queryParams: req.queryParams as Record<string, string>,
      authorizationHeader: req.authorizationHeader as string | null,
    });
    if (token && verifyToken(token, process.env.VITALS_TOKEN!)) return 'full';
    return 'summary';
  },
  metadata: {
    build: process.env.BUILD_SHA,
  },
});
```

## API

### `HealthcheckRegistry`

```typescript
const registry = new HealthcheckRegistry({ defaultTimeout: 5000 });
```

**`registry.add(name, checkFn, options?)`**

Register a named async check function. Options: `{ timeout?: number }`.

```typescript
registry.add('service', async () => ({
  status: Status.HEALTHY,
  message: '',
}), { timeout: 3000 });
```

Check functions return a `CheckInput` with `status` and `message`. The `latencyMs` field is optional — the registry measures wall-clock latency automatically using `performance.now()`.

**`registry.check(name, checkFn?, options?)`**

Decorator-style registration. Can be used as a method decorator or called directly.

**`registry.run()`**

Execute all registered checks concurrently. Returns a `HealthcheckResponse` with the worst overall status and registry-measured latency for each check.

```typescript
const response = await registry.run();
// { status: 2, timestamp: '...', checks: { service: { status: 2, latencyMs: 12, message: '' } } }

const json = toJson(response);
// { status: 'healthy', timestamp: '...', checks: { ... } }
```

### Status

```typescript
import { Status, statusToLabel, statusFromString, httpStatusCode } from '@firebreak/vitals';

Status.HEALTHY  // 2
Status.DEGRADED // 1
Status.OUTAGE   // 0

statusToLabel(Status.HEALTHY) // 'healthy'
statusFromString('degraded')  // Status.DEGRADED
httpStatusCode(Status.HEALTHY) // 200
httpStatusCode(Status.OUTAGE)  // 503
```

### Built-in Checks

#### HTTP

```typescript
import { httpCheck } from '@firebreak/vitals/checks/http';

// Basic URL check — returns HEALTHY if response is 2xx
registry.add('api', httpCheck('https://api.example.com/health'));

// With options
registry.add('api', httpCheck('https://api.example.com/health', {
  method: 'HEAD',
  headers: { 'X-Api-Key': 'secret' },
  timeout: 3000, // default: 5000ms
}));
```

#### PostgreSQL

```typescript
import { pgClientCheck, pgPoolCheck } from '@firebreak/vitals/checks/postgres';

// Fresh connection each time (good for validating connectivity)
registry.add('pg', pgClientCheck('postgresql://localhost:5432/mydb'));

// Existing pg.Pool (good for production — reuses connections)
registry.add('pg', pgPoolCheck(pool));
```

#### Redis

```typescript
import { ioredisCheck, ioredisClientCheck } from '@firebreak/vitals/checks/redis';

// Fresh connection each time
registry.add('redis', ioredisCheck('redis://localhost:6379'));

// Existing ioredis client
registry.add('redis', ioredisClientCheck(client));
```

### Custom Checks

Check functions only need to return `status` and `message`. The registry measures latency automatically:

```typescript
registry.add('databricks', async () => {
  try {
    await databricksService.query('SELECT 1');
    return { status: Status.HEALTHY, message: '' };
  } catch (error) {
    return { status: Status.OUTAGE, message: error.message };
  }
});
```

### Sync Checks

Wrap synchronous functions to use as health checks:

```typescript
import { syncCheck, Status } from '@firebreak/vitals';

registry.add('disk', syncCheck(() => ({
  status: checkDiskSpace('/') > 1_000_000_000 ? Status.HEALTHY : Status.DEGRADED,
  message: '',
})));
```

### Framework Integrations

#### Express

```typescript
import { createHealthcheckMiddleware } from '@firebreak/vitals/express';

app.get('/vitals', createHealthcheckMiddleware({
  registry,
  resolveDetail: (req) => {    // optional — omit for always-summary
    const token = extractToken({
      queryParams: req.queryParams as Record<string, string>,
      authorizationHeader: req.authorizationHeader as string | null,
    });
    if (token && verifyToken(token, 'my-secret-token')) return 'full';
    return 'summary';
  },
  metadata: {                  // optional — included in summary & full responses
    build: 'stg-45d76e5',
  },
}));
```

#### Next.js (App Router)

```typescript
import { createNextHandler } from '@firebreak/vitals/next';

export const GET = createNextHandler({
  registry,
  resolveDetail: (req) => {
    const token = extractToken({
      queryParams: req.queryParams as Record<string, string>,
      authorizationHeader: req.authorizationHeader as string | null,
    });
    if (token && verifyToken(token, process.env.VITALS_TOKEN!)) return 'full';
    return 'summary';
  },
  metadata: { build: process.env.BUILD_SHA },
});
```

#### Framework-Agnostic Handler

For other frameworks, use the generic handler directly:

```typescript
import { createHealthcheckHandler, extractToken, verifyToken } from '@firebreak/vitals';

const handle = createHealthcheckHandler({
  registry,
  resolveDetail: (req) => {
    const token = extractToken({
      queryParams: req.queryParams as Record<string, string>,
      authorizationHeader: req.authorizationHeader as string | null,
    });
    if (token && verifyToken(token, process.env.VITALS_TOKEN!)) return 'full';
    return 'summary';
  },
  metadata: { build: 'stg-45d76e5' },
});

// resolveDetail returns 'summary' → summary response (real status, no checks)
const summary = await handle({});
// { status: 200, body: { status: 'healthy', timestamp: '...', build: 'stg-45d76e5' } }

// resolveDetail returns 'full' → full response with check details
const full = await handle({ queryParams: { token: '...' } });
// { status: 200, body: { status: 'healthy', timestamp: '...', build: 'stg-45d76e5', checks: { ... } } }
```

### Summary / Full Responses

Health checks always run. The `resolveDetail` option controls how much detail is exposed in the response:

| `resolveDetail` returns | Response |
|-------------------------|----------|
| `'summary'` (or omitted) | **Summary** — real HTTP status with `{ status: "healthy"/"degraded"/"outage", timestamp, ...metadata }` |
| `'full'` | **Full** — real HTTP status with check results + metadata |

When `resolveDetail` is not provided, the handler returns a summary response.

The `resolveDetail` function receives the request object (`{ ip, headers, queryParams, authorizationHeader }`) and returns `'full'` or `'summary'` (or a promise of either). This lets you implement any detail policy — token-based, IP-based, header-based, or always-full for internal services:

```typescript
// Always full (internal service behind a private network)
createHealthcheckHandler({
  registry,
  resolveDetail: () => 'full',
});
```

Both summary and full responses reflect real health status and return the correct HTTP status code (200 for healthy, 503 for degraded/outage). The summary response lets load balancers and uptime monitors get real health status without exposing internal check details. The full response adds the `checks` object with per-check diagnostics.

### Metadata

Attach static key-value pairs that appear in both summary and full responses:

```typescript
createHealthcheckHandler({
  registry,
  metadata: {
    build: 'stg-45d76e5',
    buildTimestamp: '2025-02-25T19:41:56Z',
    region: 'us-east-1',
  },
});
```

Metadata values can be `string`, `number`, or `boolean`. Keys `status`, `timestamp`, `checks`, and `cachedAt` are reserved -- passing them throws at handler creation time.

#### `summaryMetadata`

By default, summary responses include all `metadata` fields. Use `summaryMetadata` to override what appears in summary responses while keeping `metadata` for full responses:

```typescript
createHealthcheckHandler({
  registry,
  metadata: {
    build: 'stg-45d76e5',
    buildTimestamp: '2025-02-25T19:41:56Z',
    region: 'us-east-1',
  },
  // Summary responses only include status + timestamp (no build info)
  summaryMetadata: {},
});
```

You can also include a subset of fields:

```typescript
createHealthcheckHandler({
  registry,
  metadata: { build: 'stg-45d76e5', region: 'us-east-1', version: '1.0.0' },
  // Summary responses include only version
  summaryMetadata: { version: '1.0.0' },
});
```

When `summaryMetadata` is omitted, summary responses fall back to using `metadata`.

### Utilities

`extractToken` and `verifyToken` are exported helpers for use inside your `resolveDetail` function.

```typescript
import { verifyToken, extractToken } from '@firebreak/vitals';

// Extract a token from query params (?token=...) or Authorization header
const token = extractToken({
  queryParams: { token: 'abc' },
  authorizationHeader: 'Bearer abc',
});

// Timing-safe SHA-256 comparison
verifyToken('provided-token', 'expected-token'); // boolean
```

## Response Format

**Summary response** (`resolveDetail` returns `'summary'` or is omitted):

```json
{
  "status": "healthy",
  "timestamp": "2025-02-26T12:00:00.000Z",
  "build": "stg-45d76e5",
  "buildTimestamp": "2025-02-25T19:41:56Z"
}
```

**Full response** (`resolveDetail` returns `'full'`):

```json
{
  "status": "healthy",
  "timestamp": "2025-02-26T12:00:00.000Z",
  "build": "stg-45d76e5",
  "buildTimestamp": "2025-02-25T19:41:56Z",
  "checks": {
    "postgres": {
      "status": "healthy",
      "latencyMs": 4,
      "message": ""
    },
    "redis": {
      "status": "healthy",
      "latencyMs": 1,
      "message": ""
    }
  }
}
```

Both summary and full responses return real HTTP status codes:

| Status | HTTP Code |
|--------|-----------|
| `"healthy"` | `200` |
| `"degraded"` | `503` |
| `"outage"` | `503` |

## Requirements

- Node.js >= 20.0.0