@firebreak/vitals 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Vitals (Node.js)
2
2
 
3
- Structured deep healthcheck endpoints for Node.js services. Register health checks, run them concurrently with timeouts, and expose them via Express.
3
+ Structured deep healthcheck endpoints for Node.js services. Register health checks, run them concurrently with timeouts, and expose them via Express or Next.js.
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install vitals
8
+ npm install @firebreak/vitals
9
9
  ```
10
10
 
11
11
  Peer dependencies are optional — install only what you need:
@@ -16,40 +16,51 @@ npm install ioredis # for Redis checks
16
16
  npm install express # for Express integration
17
17
  ```
18
18
 
19
- ## Quick Start
19
+ ## Quick Start (Express)
20
20
 
21
21
  ```typescript
22
- import { HealthcheckRegistry, Status } from 'vitals';
23
- import { createHealthcheckMiddleware } from 'vitals/express';
24
- import { pgPoolCheck } from 'vitals/checks/postgres';
25
- import { ioredisClientCheck } from 'vitals/checks/redis';
22
+ import { HealthcheckRegistry } from '@firebreak/vitals';
23
+ import { createHealthcheckMiddleware } from '@firebreak/vitals/express';
24
+ import { pgPoolCheck } from '@firebreak/vitals/checks/postgres';
25
+ import { ioredisClientCheck } from '@firebreak/vitals/checks/redis';
26
+ import { httpCheck } from '@firebreak/vitals/checks/http';
26
27
  import express from 'express';
27
28
 
28
29
  const registry = new HealthcheckRegistry({ defaultTimeout: 5000 });
29
30
 
30
- // Add checks using an existing connection pool / client
31
31
  registry.add('postgres', pgPoolCheck(pool));
32
32
  registry.add('redis', ioredisClientCheck(redisClient));
33
+ registry.add('api', httpCheck('https://api.example.com/health'));
33
34
 
34
- // Or add a custom check
35
- registry.add('api', async () => {
36
- const start = Date.now();
37
- const res = await fetch('https://api.example.com/ping');
38
- return {
39
- status: res.ok ? Status.HEALTHY : Status.OUTAGE,
40
- latencyMs: Date.now() - start,
41
- message: res.ok ? '' : `HTTP ${res.status}`,
42
- };
43
- });
44
-
45
- // Mount as Express middleware
46
35
  const app = express();
47
- app.get('/healthcheck/deep', createHealthcheckMiddleware({
36
+ app.get('/vitals', createHealthcheckMiddleware({
48
37
  registry,
49
- token: process.env.HEALTHCHECK_TOKEN,
38
+ token: process.env.VITALS_TOKEN,
50
39
  }));
51
40
  ```
52
41
 
42
+ ## Quick Start (Next.js)
43
+
44
+ ```typescript
45
+ // app/vitals/route.ts
46
+ import { HealthcheckRegistry } from '@firebreak/vitals';
47
+ import { createNextHandler } from '@firebreak/vitals/next';
48
+ import { pgClientCheck } from '@firebreak/vitals/checks/postgres';
49
+ import { httpCheck } from '@firebreak/vitals/checks/http';
50
+
51
+ const registry = new HealthcheckRegistry({ defaultTimeout: 5000 });
52
+
53
+ if (process.env.DATABASE_URL) {
54
+ registry.add('postgres', pgClientCheck(process.env.DATABASE_URL));
55
+ }
56
+ registry.add('api', httpCheck(`${process.env.API_BASE_URL}/health`));
57
+
58
+ export const GET = createNextHandler({
59
+ registry,
60
+ token: process.env.VITALS_TOKEN,
61
+ });
62
+ ```
63
+
53
64
  ## API
54
65
 
55
66
  ### `HealthcheckRegistry`
@@ -65,18 +76,19 @@ Register a named async check function. Options: `{ timeout?: number }`.
65
76
  ```typescript
66
77
  registry.add('service', async () => ({
67
78
  status: Status.HEALTHY,
68
- latencyMs: 0,
69
79
  message: '',
70
80
  }), { timeout: 3000 });
71
81
  ```
72
82
 
83
+ Check functions return a `CheckInput` with `status` and `message`. The `latencyMs` field is optional — the registry measures wall-clock latency automatically using `performance.now()`.
84
+
73
85
  **`registry.check(name, checkFn?, options?)`**
74
86
 
75
87
  Decorator-style registration. Can be used as a method decorator or called directly.
76
88
 
77
89
  **`registry.run()`**
78
90
 
79
- Execute all registered checks concurrently. Returns a `HealthcheckResponse` with the worst overall status.
91
+ Execute all registered checks concurrently. Returns a `HealthcheckResponse` with the worst overall status and registry-measured latency for each check.
80
92
 
81
93
  ```typescript
82
94
  const response = await registry.run();
@@ -89,7 +101,7 @@ const json = toJson(response);
89
101
  ### Status
90
102
 
91
103
  ```typescript
92
- import { Status, statusToLabel, statusFromString, httpStatusCode } from 'vitals';
104
+ import { Status, statusToLabel, statusFromString, httpStatusCode } from '@firebreak/vitals';
93
105
 
94
106
  Status.HEALTHY // 2
95
107
  Status.DEGRADED // 1
@@ -103,10 +115,26 @@ httpStatusCode(Status.OUTAGE) // 503
103
115
 
104
116
  ### Built-in Checks
105
117
 
118
+ #### HTTP
119
+
120
+ ```typescript
121
+ import { httpCheck } from '@firebreak/vitals/checks/http';
122
+
123
+ // Basic URL check — returns HEALTHY if response is 2xx
124
+ registry.add('api', httpCheck('https://api.example.com/health'));
125
+
126
+ // With options
127
+ registry.add('api', httpCheck('https://api.example.com/health', {
128
+ method: 'HEAD',
129
+ headers: { 'X-Api-Key': 'secret' },
130
+ timeout: 3000, // default: 5000ms
131
+ }));
132
+ ```
133
+
106
134
  #### PostgreSQL
107
135
 
108
136
  ```typescript
109
- import { pgClientCheck, pgPoolCheck } from 'vitals/checks/postgres';
137
+ import { pgClientCheck, pgPoolCheck } from '@firebreak/vitals/checks/postgres';
110
138
 
111
139
  // Fresh connection each time (good for validating connectivity)
112
140
  registry.add('pg', pgClientCheck('postgresql://localhost:5432/mydb'));
@@ -118,7 +146,7 @@ registry.add('pg', pgPoolCheck(pool));
118
146
  #### Redis
119
147
 
120
148
  ```typescript
121
- import { ioredisCheck, ioredisClientCheck } from 'vitals/checks/redis';
149
+ import { ioredisCheck, ioredisClientCheck } from '@firebreak/vitals/checks/redis';
122
150
 
123
151
  // Fresh connection each time
124
152
  registry.add('redis', ioredisCheck('redis://localhost:6379'));
@@ -127,48 +155,89 @@ registry.add('redis', ioredisCheck('redis://localhost:6379'));
127
155
  registry.add('redis', ioredisClientCheck(client));
128
156
  ```
129
157
 
158
+ ### Custom Checks
159
+
160
+ Check functions only need to return `status` and `message`. The registry measures latency automatically:
161
+
162
+ ```typescript
163
+ registry.add('databricks', async () => {
164
+ try {
165
+ await databricksService.query('SELECT 1');
166
+ return { status: Status.HEALTHY, message: '' };
167
+ } catch (error) {
168
+ return { status: Status.OUTAGE, message: error.message };
169
+ }
170
+ });
171
+ ```
172
+
130
173
  ### Sync Checks
131
174
 
132
175
  Wrap synchronous functions to use as health checks:
133
176
 
134
177
  ```typescript
135
- import { syncCheck, Status } from 'vitals';
136
-
137
- registry.add('disk', syncCheck(() => {
138
- const free = checkDiskSpace('/');
139
- return {
140
- status: free > 1_000_000_000 ? Status.HEALTHY : Status.DEGRADED,
141
- latencyMs: 0,
142
- message: `${free} bytes free`,
143
- };
144
- }));
178
+ import { syncCheck, Status } from '@firebreak/vitals';
179
+
180
+ registry.add('disk', syncCheck(() => ({
181
+ status: checkDiskSpace('/') > 1_000_000_000 ? Status.HEALTHY : Status.DEGRADED,
182
+ message: '',
183
+ })));
145
184
  ```
146
185
 
147
- ### Express Integration
186
+ ### Framework Integrations
187
+
188
+ #### Express
148
189
 
149
190
  ```typescript
150
- import { createHealthcheckMiddleware } from 'vitals/express';
191
+ import { createHealthcheckMiddleware } from '@firebreak/vitals/express';
151
192
 
152
- app.get('/healthcheck/deep', createHealthcheckMiddleware({
193
+ app.get('/vitals', createHealthcheckMiddleware({
153
194
  registry,
154
195
  token: 'my-secret-token', // optional — omit to disable auth
155
196
  queryParamName: 'token', // default: 'token'
156
197
  }));
157
198
  ```
158
199
 
200
+ #### Next.js (App Router)
201
+
202
+ ```typescript
203
+ import { createNextHandler } from '@firebreak/vitals/next';
204
+
205
+ export const GET = createNextHandler({
206
+ registry,
207
+ token: process.env.VITALS_TOKEN,
208
+ });
209
+ ```
210
+
211
+ #### Framework-Agnostic Handler
212
+
213
+ For other frameworks, use the generic handler directly:
214
+
215
+ ```typescript
216
+ import { createHealthcheckHandler } from '@firebreak/vitals';
217
+
218
+ const handle = createHealthcheckHandler({ registry, token: process.env.VITALS_TOKEN });
219
+
220
+ // In any framework:
221
+ const result = await handle({
222
+ queryParams: { token: 'abc' },
223
+ authorizationHeader: 'Bearer abc',
224
+ });
225
+ // result = { status: 200, body: { status: 'healthy', ... } }
226
+ ```
227
+
228
+ ### Authentication
229
+
159
230
  When a token is configured, requests must provide it via:
160
231
  - Query parameter: `?token=my-secret-token`
161
232
  - Bearer header: `Authorization: Bearer my-secret-token`
162
233
 
163
- ### Authentication
234
+ Tokens are compared using timing-safe SHA-256 comparison.
164
235
 
165
236
  ```typescript
166
- import { verifyToken, extractToken } from 'vitals';
237
+ import { verifyToken, extractToken } from '@firebreak/vitals';
167
238
 
168
- // Timing-safe token comparison
169
239
  verifyToken('provided-token', 'expected-token'); // boolean
170
240
 
171
- // Extract token from request
172
241
  const token = extractToken({
173
242
  queryParams: { token: 'abc' },
174
243
  authorizationHeader: 'Bearer abc',
@@ -185,12 +254,12 @@ const token = extractToken({
185
254
  "checks": {
186
255
  "postgres": {
187
256
  "status": "healthy",
188
- "latencyMs": 4.2,
257
+ "latencyMs": 4,
189
258
  "message": ""
190
259
  },
191
260
  "redis": {
192
261
  "status": "healthy",
193
- "latencyMs": 1.1,
262
+ "latencyMs": 1,
194
263
  "message": ""
195
264
  }
196
265
  }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/checks/http.ts
21
+ var http_exports = {};
22
+ __export(http_exports, {
23
+ httpCheck: () => httpCheck
24
+ });
25
+ module.exports = __toCommonJS(http_exports);
26
+
27
+ // src/types.ts
28
+ var Status = {
29
+ HEALTHY: 2,
30
+ DEGRADED: 1,
31
+ OUTAGE: 0
32
+ };
33
+
34
+ // src/checks/http.ts
35
+ function httpCheck(url, options) {
36
+ const { method = "GET", headers, timeout = 5e3 } = options ?? {};
37
+ return async () => {
38
+ try {
39
+ const controller = new AbortController();
40
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
41
+ try {
42
+ const res = await fetch(url, {
43
+ method,
44
+ headers,
45
+ signal: controller.signal
46
+ });
47
+ return {
48
+ status: res.ok ? Status.HEALTHY : Status.OUTAGE,
49
+ message: res.ok ? "" : `HTTP ${res.status}`
50
+ };
51
+ } finally {
52
+ clearTimeout(timeoutId);
53
+ }
54
+ } catch (error) {
55
+ const message = error instanceof Error ? error.name === "AbortError" ? `Request to ${url} timed out after ${timeout}ms` : error.message : String(error);
56
+ return { status: Status.OUTAGE, message };
57
+ }
58
+ };
59
+ }
60
+ // Annotate the CommonJS export names for ESM import in node:
61
+ 0 && (module.exports = {
62
+ httpCheck
63
+ });
@@ -0,0 +1,17 @@
1
+ import { A as AsyncCheckFn } from '../core-BJ2Z0rRi.cjs';
2
+
3
+ interface HttpCheckOptions {
4
+ /** HTTP method to use. Defaults to 'GET'. */
5
+ method?: string;
6
+ /** Request headers to include. */
7
+ headers?: Record<string, string>;
8
+ /** Timeout in milliseconds for the fetch call. Defaults to 5000. */
9
+ timeout?: number;
10
+ }
11
+ /**
12
+ * Creates a healthcheck function that fetches a URL and checks for an ok response.
13
+ * Returns HEALTHY if the response status is 2xx, OUTAGE otherwise.
14
+ */
15
+ declare function httpCheck(url: string, options?: HttpCheckOptions): AsyncCheckFn;
16
+
17
+ export { AsyncCheckFn, type HttpCheckOptions, httpCheck };
@@ -0,0 +1,17 @@
1
+ import { A as AsyncCheckFn } from '../core-BJ2Z0rRi.js';
2
+
3
+ interface HttpCheckOptions {
4
+ /** HTTP method to use. Defaults to 'GET'. */
5
+ method?: string;
6
+ /** Request headers to include. */
7
+ headers?: Record<string, string>;
8
+ /** Timeout in milliseconds for the fetch call. Defaults to 5000. */
9
+ timeout?: number;
10
+ }
11
+ /**
12
+ * Creates a healthcheck function that fetches a URL and checks for an ok response.
13
+ * Returns HEALTHY if the response status is 2xx, OUTAGE otherwise.
14
+ */
15
+ declare function httpCheck(url: string, options?: HttpCheckOptions): AsyncCheckFn;
16
+
17
+ export { AsyncCheckFn, type HttpCheckOptions, httpCheck };
@@ -0,0 +1,36 @@
1
+ // src/types.ts
2
+ var Status = {
3
+ HEALTHY: 2,
4
+ DEGRADED: 1,
5
+ OUTAGE: 0
6
+ };
7
+
8
+ // src/checks/http.ts
9
+ function httpCheck(url, options) {
10
+ const { method = "GET", headers, timeout = 5e3 } = options ?? {};
11
+ return async () => {
12
+ try {
13
+ const controller = new AbortController();
14
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
15
+ try {
16
+ const res = await fetch(url, {
17
+ method,
18
+ headers,
19
+ signal: controller.signal
20
+ });
21
+ return {
22
+ status: res.ok ? Status.HEALTHY : Status.OUTAGE,
23
+ message: res.ok ? "" : `HTTP ${res.status}`
24
+ };
25
+ } finally {
26
+ clearTimeout(timeoutId);
27
+ }
28
+ } catch (error) {
29
+ const message = error instanceof Error ? error.name === "AbortError" ? `Request to ${url} timed out after ${timeout}ms` : error.message : String(error);
30
+ return { status: Status.OUTAGE, message };
31
+ }
32
+ };
33
+ }
34
+ export {
35
+ httpCheck
36
+ };
@@ -34,7 +34,6 @@ __export(postgres_exports, {
34
34
  pgPoolCheck: () => pgPoolCheck
35
35
  });
36
36
  module.exports = __toCommonJS(postgres_exports);
37
- var import_node_perf_hooks = require("perf_hooks");
38
37
 
39
38
  // src/types.ts
40
39
  var Status = {
@@ -46,18 +45,15 @@ var Status = {
46
45
  // src/checks/postgres.ts
47
46
  function pgClientCheck(connectionString, options) {
48
47
  return async () => {
49
- const start = import_node_perf_hooks.performance.now();
50
48
  const { Client } = await import("pg");
51
49
  const client = new Client({ connectionString, ...options });
52
50
  try {
53
51
  await client.connect();
54
52
  await client.query("SELECT 1");
55
- const latencyMs = Math.round(import_node_perf_hooks.performance.now() - start);
56
- return { status: Status.HEALTHY, latencyMs, message: "" };
53
+ return { status: Status.HEALTHY, message: "" };
57
54
  } catch (error) {
58
- const latencyMs = Math.round(import_node_perf_hooks.performance.now() - start);
59
55
  const message = error instanceof Error ? error.message : String(error);
60
- return { status: Status.OUTAGE, latencyMs, message };
56
+ return { status: Status.OUTAGE, message };
61
57
  } finally {
62
58
  client.end().catch(() => {
63
59
  });
@@ -66,17 +62,14 @@ function pgClientCheck(connectionString, options) {
66
62
  }
67
63
  function pgPoolCheck(pool) {
68
64
  return async () => {
69
- const start = import_node_perf_hooks.performance.now();
70
65
  let client;
71
66
  try {
72
67
  client = await pool.connect();
73
68
  await client.query("SELECT 1");
74
- const latencyMs = Math.round(import_node_perf_hooks.performance.now() - start);
75
- return { status: Status.HEALTHY, latencyMs, message: "" };
69
+ return { status: Status.HEALTHY, message: "" };
76
70
  } catch (error) {
77
- const latencyMs = Math.round(import_node_perf_hooks.performance.now() - start);
78
71
  const message = error instanceof Error ? error.message : String(error);
79
- return { status: Status.OUTAGE, latencyMs, message };
72
+ return { status: Status.OUTAGE, message };
80
73
  } finally {
81
74
  client?.release();
82
75
  }
@@ -1,4 +1,4 @@
1
- import { A as AsyncCheckFn } from '../core-9-MXAO0I.cjs';
1
+ import { A as AsyncCheckFn } from '../core-BJ2Z0rRi.cjs';
2
2
  import { Pool } from 'pg';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { A as AsyncCheckFn } from '../core-9-MXAO0I.js';
1
+ import { A as AsyncCheckFn } from '../core-BJ2Z0rRi.js';
2
2
  import { Pool } from 'pg';
3
3
 
4
4
  /**
@@ -1,6 +1,3 @@
1
- // src/checks/postgres.ts
2
- import { performance } from "perf_hooks";
3
-
4
1
  // src/types.ts
5
2
  var Status = {
6
3
  HEALTHY: 2,
@@ -11,18 +8,15 @@ var Status = {
11
8
  // src/checks/postgres.ts
12
9
  function pgClientCheck(connectionString, options) {
13
10
  return async () => {
14
- const start = performance.now();
15
11
  const { Client } = await import("pg");
16
12
  const client = new Client({ connectionString, ...options });
17
13
  try {
18
14
  await client.connect();
19
15
  await client.query("SELECT 1");
20
- const latencyMs = Math.round(performance.now() - start);
21
- return { status: Status.HEALTHY, latencyMs, message: "" };
16
+ return { status: Status.HEALTHY, message: "" };
22
17
  } catch (error) {
23
- const latencyMs = Math.round(performance.now() - start);
24
18
  const message = error instanceof Error ? error.message : String(error);
25
- return { status: Status.OUTAGE, latencyMs, message };
19
+ return { status: Status.OUTAGE, message };
26
20
  } finally {
27
21
  client.end().catch(() => {
28
22
  });
@@ -31,17 +25,14 @@ function pgClientCheck(connectionString, options) {
31
25
  }
32
26
  function pgPoolCheck(pool) {
33
27
  return async () => {
34
- const start = performance.now();
35
28
  let client;
36
29
  try {
37
30
  client = await pool.connect();
38
31
  await client.query("SELECT 1");
39
- const latencyMs = Math.round(performance.now() - start);
40
- return { status: Status.HEALTHY, latencyMs, message: "" };
32
+ return { status: Status.HEALTHY, message: "" };
41
33
  } catch (error) {
42
- const latencyMs = Math.round(performance.now() - start);
43
34
  const message = error instanceof Error ? error.message : String(error);
44
- return { status: Status.OUTAGE, latencyMs, message };
35
+ return { status: Status.OUTAGE, message };
45
36
  } finally {
46
37
  client?.release();
47
38
  }
@@ -34,7 +34,6 @@ __export(redis_exports, {
34
34
  ioredisClientCheck: () => ioredisClientCheck
35
35
  });
36
36
  module.exports = __toCommonJS(redis_exports);
37
- var import_node_perf_hooks = require("perf_hooks");
38
37
 
39
38
  // src/types.ts
40
39
  var Status = {
@@ -46,7 +45,6 @@ var Status = {
46
45
  // src/checks/redis.ts
47
46
  function ioredisCheck(url, options) {
48
47
  return async () => {
49
- const start = import_node_perf_hooks.performance.now();
50
48
  const { default: RedisClient } = await import("ioredis");
51
49
  const client = new RedisClient(url, {
52
50
  lazyConnect: true,
@@ -60,12 +58,10 @@ function ioredisCheck(url, options) {
60
58
  if (result !== "PONG") {
61
59
  throw new Error(`PING returned ${result}`);
62
60
  }
63
- const latencyMs = Math.round(import_node_perf_hooks.performance.now() - start);
64
- return { status: Status.HEALTHY, latencyMs, message: "" };
61
+ return { status: Status.HEALTHY, message: "" };
65
62
  } catch (error) {
66
- const latencyMs = Math.round(import_node_perf_hooks.performance.now() - start);
67
63
  const message = error instanceof Error ? error.message : String(error);
68
- return { status: Status.OUTAGE, latencyMs, message };
64
+ return { status: Status.OUTAGE, message };
69
65
  } finally {
70
66
  client.disconnect();
71
67
  }
@@ -73,18 +69,15 @@ function ioredisCheck(url, options) {
73
69
  }
74
70
  function ioredisClientCheck(client) {
75
71
  return async () => {
76
- const start = import_node_perf_hooks.performance.now();
77
72
  try {
78
73
  const result = await client.ping();
79
74
  if (result !== "PONG") {
80
75
  throw new Error(`PING returned ${result}`);
81
76
  }
82
- const latencyMs = Math.round(import_node_perf_hooks.performance.now() - start);
83
- return { status: Status.HEALTHY, latencyMs, message: "" };
77
+ return { status: Status.HEALTHY, message: "" };
84
78
  } catch (error) {
85
- const latencyMs = Math.round(import_node_perf_hooks.performance.now() - start);
86
79
  const message = error instanceof Error ? error.message : String(error);
87
- return { status: Status.OUTAGE, latencyMs, message };
80
+ return { status: Status.OUTAGE, message };
88
81
  }
89
82
  };
90
83
  }
@@ -1,4 +1,4 @@
1
- import { A as AsyncCheckFn } from '../core-9-MXAO0I.cjs';
1
+ import { A as AsyncCheckFn } from '../core-BJ2Z0rRi.cjs';
2
2
  import Redis from 'ioredis';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { A as AsyncCheckFn } from '../core-9-MXAO0I.js';
1
+ import { A as AsyncCheckFn } from '../core-BJ2Z0rRi.js';
2
2
  import Redis from 'ioredis';
3
3
 
4
4
  /**
@@ -1,6 +1,3 @@
1
- // src/checks/redis.ts
2
- import { performance } from "perf_hooks";
3
-
4
1
  // src/types.ts
5
2
  var Status = {
6
3
  HEALTHY: 2,
@@ -11,7 +8,6 @@ var Status = {
11
8
  // src/checks/redis.ts
12
9
  function ioredisCheck(url, options) {
13
10
  return async () => {
14
- const start = performance.now();
15
11
  const { default: RedisClient } = await import("ioredis");
16
12
  const client = new RedisClient(url, {
17
13
  lazyConnect: true,
@@ -25,12 +21,10 @@ function ioredisCheck(url, options) {
25
21
  if (result !== "PONG") {
26
22
  throw new Error(`PING returned ${result}`);
27
23
  }
28
- const latencyMs = Math.round(performance.now() - start);
29
- return { status: Status.HEALTHY, latencyMs, message: "" };
24
+ return { status: Status.HEALTHY, message: "" };
30
25
  } catch (error) {
31
- const latencyMs = Math.round(performance.now() - start);
32
26
  const message = error instanceof Error ? error.message : String(error);
33
- return { status: Status.OUTAGE, latencyMs, message };
27
+ return { status: Status.OUTAGE, message };
34
28
  } finally {
35
29
  client.disconnect();
36
30
  }
@@ -38,18 +32,15 @@ function ioredisCheck(url, options) {
38
32
  }
39
33
  function ioredisClientCheck(client) {
40
34
  return async () => {
41
- const start = performance.now();
42
35
  try {
43
36
  const result = await client.ping();
44
37
  if (result !== "PONG") {
45
38
  throw new Error(`PING returned ${result}`);
46
39
  }
47
- const latencyMs = Math.round(performance.now() - start);
48
- return { status: Status.HEALTHY, latencyMs, message: "" };
40
+ return { status: Status.HEALTHY, message: "" };
49
41
  } catch (error) {
50
- const latencyMs = Math.round(performance.now() - start);
51
42
  const message = error instanceof Error ? error.message : String(error);
52
- return { status: Status.OUTAGE, latencyMs, message };
43
+ return { status: Status.OUTAGE, message };
53
44
  }
54
45
  };
55
46
  }
@@ -7,6 +7,16 @@ type StatusValue = (typeof Status)[keyof typeof Status];
7
7
  type StatusLabel = 'healthy' | 'degraded' | 'outage';
8
8
  declare function statusToLabel(status: StatusValue): StatusLabel;
9
9
  declare function statusFromString(value: string): StatusValue;
10
+ /**
11
+ * What a check function returns. `latencyMs` is optional because the registry
12
+ * always measures wall-clock latency and overwrites this value.
13
+ */
14
+ interface CheckInput {
15
+ readonly status: StatusValue;
16
+ readonly latencyMs?: number;
17
+ readonly message: string;
18
+ }
19
+ /** The final result stored per-check after the registry measures latency. */
10
20
  interface CheckResult {
11
21
  readonly status: StatusValue;
12
22
  readonly latencyMs: number;
@@ -29,8 +39,8 @@ interface HealthcheckResponseJson {
29
39
  declare function toJson(response: HealthcheckResponse): HealthcheckResponseJson;
30
40
  declare function httpStatusCode(status: StatusValue): 200 | 503;
31
41
 
32
- type AsyncCheckFn = () => Promise<CheckResult>;
33
- type SyncCheckFn = () => CheckResult;
42
+ type AsyncCheckFn = () => Promise<CheckInput>;
43
+ type SyncCheckFn = () => CheckInput;
34
44
  interface RegistryOptions {
35
45
  defaultTimeout?: number;
36
46
  }
@@ -52,4 +62,4 @@ declare class HealthcheckRegistry {
52
62
  }
53
63
  declare function syncCheck(fn: SyncCheckFn): AsyncCheckFn;
54
64
 
55
- export { type AsyncCheckFn as A, type CheckResult as C, HealthcheckRegistry as H, Status as S, type HealthcheckResponse as a, type HealthcheckResponseJson as b, type StatusLabel as c, type StatusValue as d, type SyncCheckFn as e, statusToLabel as f, syncCheck as g, httpStatusCode as h, statusFromString as s, toJson as t };
65
+ export { type AsyncCheckFn as A, type CheckInput as C, HealthcheckRegistry as H, Status as S, type CheckResult as a, type HealthcheckResponse as b, type HealthcheckResponseJson as c, type StatusLabel as d, type StatusValue as e, type SyncCheckFn as f, statusToLabel as g, httpStatusCode as h, syncCheck as i, statusFromString as s, toJson as t };
@@ -7,6 +7,16 @@ type StatusValue = (typeof Status)[keyof typeof Status];
7
7
  type StatusLabel = 'healthy' | 'degraded' | 'outage';
8
8
  declare function statusToLabel(status: StatusValue): StatusLabel;
9
9
  declare function statusFromString(value: string): StatusValue;
10
+ /**
11
+ * What a check function returns. `latencyMs` is optional because the registry
12
+ * always measures wall-clock latency and overwrites this value.
13
+ */
14
+ interface CheckInput {
15
+ readonly status: StatusValue;
16
+ readonly latencyMs?: number;
17
+ readonly message: string;
18
+ }
19
+ /** The final result stored per-check after the registry measures latency. */
10
20
  interface CheckResult {
11
21
  readonly status: StatusValue;
12
22
  readonly latencyMs: number;
@@ -29,8 +39,8 @@ interface HealthcheckResponseJson {
29
39
  declare function toJson(response: HealthcheckResponse): HealthcheckResponseJson;
30
40
  declare function httpStatusCode(status: StatusValue): 200 | 503;
31
41
 
32
- type AsyncCheckFn = () => Promise<CheckResult>;
33
- type SyncCheckFn = () => CheckResult;
42
+ type AsyncCheckFn = () => Promise<CheckInput>;
43
+ type SyncCheckFn = () => CheckInput;
34
44
  interface RegistryOptions {
35
45
  defaultTimeout?: number;
36
46
  }
@@ -52,4 +62,4 @@ declare class HealthcheckRegistry {
52
62
  }
53
63
  declare function syncCheck(fn: SyncCheckFn): AsyncCheckFn;
54
64
 
55
- export { type AsyncCheckFn as A, type CheckResult as C, HealthcheckRegistry as H, Status as S, type HealthcheckResponse as a, type HealthcheckResponseJson as b, type StatusLabel as c, type StatusValue as d, type SyncCheckFn as e, statusToLabel as f, syncCheck as g, httpStatusCode as h, statusFromString as s, toJson as t };
65
+ export { type AsyncCheckFn as A, type CheckInput as C, HealthcheckRegistry as H, Status as S, type CheckResult as a, type HealthcheckResponse as b, type HealthcheckResponseJson as c, type StatusLabel as d, type StatusValue as e, type SyncCheckFn as f, statusToLabel as g, httpStatusCode as h, syncCheck as i, statusFromString as s, toJson as t };
@@ -0,0 +1,25 @@
1
+ import { H as HealthcheckRegistry, c as HealthcheckResponseJson } from './core-BJ2Z0rRi.cjs';
2
+
3
+ interface HealthcheckHandlerOptions {
4
+ registry: HealthcheckRegistry;
5
+ token?: string | null;
6
+ queryParamName?: string;
7
+ }
8
+ interface HealthcheckRequest {
9
+ queryParams?: Record<string, string | string[] | undefined>;
10
+ authorizationHeader?: string | null;
11
+ }
12
+ interface HealthcheckHandlerResult {
13
+ status: number;
14
+ body: HealthcheckResponseJson | {
15
+ error: string;
16
+ };
17
+ }
18
+ /**
19
+ * Framework-agnostic healthcheck handler. Takes a simple request object
20
+ * and returns a status code + JSON body. Framework integrations (Express,
21
+ * Next.js, etc.) are thin wrappers around this function.
22
+ */
23
+ declare function createHealthcheckHandler(options: HealthcheckHandlerOptions): (req: HealthcheckRequest) => Promise<HealthcheckHandlerResult>;
24
+
25
+ export { type HealthcheckHandlerOptions as H, type HealthcheckHandlerResult as a, type HealthcheckRequest as b, createHealthcheckHandler as c };
@@ -0,0 +1,25 @@
1
+ import { H as HealthcheckRegistry, c as HealthcheckResponseJson } from './core-BJ2Z0rRi.js';
2
+
3
+ interface HealthcheckHandlerOptions {
4
+ registry: HealthcheckRegistry;
5
+ token?: string | null;
6
+ queryParamName?: string;
7
+ }
8
+ interface HealthcheckRequest {
9
+ queryParams?: Record<string, string | string[] | undefined>;
10
+ authorizationHeader?: string | null;
11
+ }
12
+ interface HealthcheckHandlerResult {
13
+ status: number;
14
+ body: HealthcheckResponseJson | {
15
+ error: string;
16
+ };
17
+ }
18
+ /**
19
+ * Framework-agnostic healthcheck handler. Takes a simple request object
20
+ * and returns a status code + JSON body. Framework integrations (Express,
21
+ * Next.js, etc.) are thin wrappers around this function.
22
+ */
23
+ declare function createHealthcheckHandler(options: HealthcheckHandlerOptions): (req: HealthcheckRequest) => Promise<HealthcheckHandlerResult>;
24
+
25
+ export { type HealthcheckHandlerOptions as H, type HealthcheckHandlerResult as a, type HealthcheckRequest as b, createHealthcheckHandler as c };
package/dist/index.cjs CHANGED
@@ -22,6 +22,7 @@ var src_exports = {};
22
22
  __export(src_exports, {
23
23
  HealthcheckRegistry: () => HealthcheckRegistry,
24
24
  Status: () => Status,
25
+ createHealthcheckHandler: () => createHealthcheckHandler,
25
26
  extractToken: () => extractToken,
26
27
  httpStatusCode: () => httpStatusCode,
27
28
  statusFromString: () => statusFromString,
@@ -186,10 +187,33 @@ function extractToken(options) {
186
187
  }
187
188
  return null;
188
189
  }
190
+
191
+ // src/handler.ts
192
+ function createHealthcheckHandler(options) {
193
+ const { registry, token = null, queryParamName = "token" } = options;
194
+ return async (req) => {
195
+ if (token !== null) {
196
+ const provided = extractToken({
197
+ queryParams: req.queryParams,
198
+ authorizationHeader: req.authorizationHeader,
199
+ queryParamName
200
+ });
201
+ if (provided === null || !verifyToken(provided, token)) {
202
+ return { status: 403, body: { error: "Forbidden" } };
203
+ }
204
+ }
205
+ const response = await registry.run();
206
+ return {
207
+ status: httpStatusCode(response.status),
208
+ body: toJson(response)
209
+ };
210
+ };
211
+ }
189
212
  // Annotate the CommonJS export names for ESM import in node:
190
213
  0 && (module.exports = {
191
214
  HealthcheckRegistry,
192
215
  Status,
216
+ createHealthcheckHandler,
193
217
  extractToken,
194
218
  httpStatusCode,
195
219
  statusFromString,
package/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
- export { A as AsyncCheckFn, C as CheckResult, H as HealthcheckRegistry, a as HealthcheckResponse, b as HealthcheckResponseJson, S as Status, c as StatusLabel, d as StatusValue, e as SyncCheckFn, h as httpStatusCode, s as statusFromString, f as statusToLabel, g as syncCheck, t as toJson } from './core-9-MXAO0I.cjs';
1
+ export { A as AsyncCheckFn, C as CheckInput, a as CheckResult, H as HealthcheckRegistry, b as HealthcheckResponse, c as HealthcheckResponseJson, S as Status, d as StatusLabel, e as StatusValue, f as SyncCheckFn, h as httpStatusCode, s as statusFromString, g as statusToLabel, i as syncCheck, t as toJson } from './core-BJ2Z0rRi.cjs';
2
+ export { H as HealthcheckHandlerOptions, a as HealthcheckHandlerResult, b as HealthcheckRequest, c as createHealthcheckHandler } from './handler-Bccbso4b.cjs';
2
3
 
3
4
  declare function verifyToken(provided: string, expected: string): boolean;
4
5
  declare function extractToken(options: {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export { A as AsyncCheckFn, C as CheckResult, H as HealthcheckRegistry, a as HealthcheckResponse, b as HealthcheckResponseJson, S as Status, c as StatusLabel, d as StatusValue, e as SyncCheckFn, h as httpStatusCode, s as statusFromString, f as statusToLabel, g as syncCheck, t as toJson } from './core-9-MXAO0I.js';
1
+ export { A as AsyncCheckFn, C as CheckInput, a as CheckResult, H as HealthcheckRegistry, b as HealthcheckResponse, c as HealthcheckResponseJson, S as Status, d as StatusLabel, e as StatusValue, f as SyncCheckFn, h as httpStatusCode, s as statusFromString, g as statusToLabel, i as syncCheck, t as toJson } from './core-BJ2Z0rRi.js';
2
+ export { H as HealthcheckHandlerOptions, a as HealthcheckHandlerResult, b as HealthcheckRequest, c as createHealthcheckHandler } from './handler-D0nYVQvu.js';
2
3
 
3
4
  declare function verifyToken(provided: string, expected: string): boolean;
4
5
  declare function extractToken(options: {
package/dist/index.mjs CHANGED
@@ -152,9 +152,32 @@ function extractToken(options) {
152
152
  }
153
153
  return null;
154
154
  }
155
+
156
+ // src/handler.ts
157
+ function createHealthcheckHandler(options) {
158
+ const { registry, token = null, queryParamName = "token" } = options;
159
+ return async (req) => {
160
+ if (token !== null) {
161
+ const provided = extractToken({
162
+ queryParams: req.queryParams,
163
+ authorizationHeader: req.authorizationHeader,
164
+ queryParamName
165
+ });
166
+ if (provided === null || !verifyToken(provided, token)) {
167
+ return { status: 403, body: { error: "Forbidden" } };
168
+ }
169
+ }
170
+ const response = await registry.run();
171
+ return {
172
+ status: httpStatusCode(response.status),
173
+ body: toJson(response)
174
+ };
175
+ };
176
+ }
155
177
  export {
156
178
  HealthcheckRegistry,
157
179
  Status,
180
+ createHealthcheckHandler,
158
181
  extractToken,
159
182
  httpStatusCode,
160
183
  statusFromString,
@@ -76,24 +76,38 @@ function httpStatusCode(status) {
76
76
  return status === Status.HEALTHY ? 200 : 503;
77
77
  }
78
78
 
79
+ // src/handler.ts
80
+ function createHealthcheckHandler(options) {
81
+ const { registry, token = null, queryParamName = "token" } = options;
82
+ return async (req) => {
83
+ if (token !== null) {
84
+ const provided = extractToken({
85
+ queryParams: req.queryParams,
86
+ authorizationHeader: req.authorizationHeader,
87
+ queryParamName
88
+ });
89
+ if (provided === null || !verifyToken(provided, token)) {
90
+ return { status: 403, body: { error: "Forbidden" } };
91
+ }
92
+ }
93
+ const response = await registry.run();
94
+ return {
95
+ status: httpStatusCode(response.status),
96
+ body: toJson(response)
97
+ };
98
+ };
99
+ }
100
+
79
101
  // src/integrations/express.ts
80
102
  function createHealthcheckMiddleware(options) {
81
- const { registry, token = null, queryParamName = "token" } = options;
103
+ const handle = createHealthcheckHandler(options);
82
104
  return async (req, res) => {
83
105
  try {
84
- if (token !== null) {
85
- const provided = extractToken({
86
- queryParams: req.query,
87
- authorizationHeader: req.headers.authorization ?? null,
88
- queryParamName
89
- });
90
- if (provided === null || !verifyToken(provided, token)) {
91
- res.status(403).json({ error: "Forbidden" });
92
- return;
93
- }
94
- }
95
- const response = await registry.run();
96
- res.status(httpStatusCode(response.status)).json(toJson(response));
106
+ const result = await handle({
107
+ queryParams: req.query,
108
+ authorizationHeader: req.headers.authorization ?? null
109
+ });
110
+ res.status(result.status).json(result.body);
97
111
  } catch {
98
112
  res.status(500).json({ error: "Internal Server Error" });
99
113
  }
@@ -1,11 +1,8 @@
1
1
  import { RequestHandler } from 'express';
2
- import { H as HealthcheckRegistry } from '../core-9-MXAO0I.cjs';
2
+ import { H as HealthcheckHandlerOptions } from '../handler-Bccbso4b.cjs';
3
+ import '../core-BJ2Z0rRi.cjs';
3
4
 
4
- interface HealthcheckMiddlewareOptions {
5
- registry: HealthcheckRegistry;
6
- token?: string | null;
7
- queryParamName?: string;
8
- }
5
+ type HealthcheckMiddlewareOptions = HealthcheckHandlerOptions;
9
6
  declare function createHealthcheckMiddleware(options: HealthcheckMiddlewareOptions): RequestHandler;
10
7
 
11
8
  export { type HealthcheckMiddlewareOptions, createHealthcheckMiddleware };
@@ -1,11 +1,8 @@
1
1
  import { RequestHandler } from 'express';
2
- import { H as HealthcheckRegistry } from '../core-9-MXAO0I.js';
2
+ import { H as HealthcheckHandlerOptions } from '../handler-D0nYVQvu.js';
3
+ import '../core-BJ2Z0rRi.js';
3
4
 
4
- interface HealthcheckMiddlewareOptions {
5
- registry: HealthcheckRegistry;
6
- token?: string | null;
7
- queryParamName?: string;
8
- }
5
+ type HealthcheckMiddlewareOptions = HealthcheckHandlerOptions;
9
6
  declare function createHealthcheckMiddleware(options: HealthcheckMiddlewareOptions): RequestHandler;
10
7
 
11
8
  export { type HealthcheckMiddlewareOptions, createHealthcheckMiddleware };
@@ -50,24 +50,38 @@ function httpStatusCode(status) {
50
50
  return status === Status.HEALTHY ? 200 : 503;
51
51
  }
52
52
 
53
+ // src/handler.ts
54
+ function createHealthcheckHandler(options) {
55
+ const { registry, token = null, queryParamName = "token" } = options;
56
+ return async (req) => {
57
+ if (token !== null) {
58
+ const provided = extractToken({
59
+ queryParams: req.queryParams,
60
+ authorizationHeader: req.authorizationHeader,
61
+ queryParamName
62
+ });
63
+ if (provided === null || !verifyToken(provided, token)) {
64
+ return { status: 403, body: { error: "Forbidden" } };
65
+ }
66
+ }
67
+ const response = await registry.run();
68
+ return {
69
+ status: httpStatusCode(response.status),
70
+ body: toJson(response)
71
+ };
72
+ };
73
+ }
74
+
53
75
  // src/integrations/express.ts
54
76
  function createHealthcheckMiddleware(options) {
55
- const { registry, token = null, queryParamName = "token" } = options;
77
+ const handle = createHealthcheckHandler(options);
56
78
  return async (req, res) => {
57
79
  try {
58
- if (token !== null) {
59
- const provided = extractToken({
60
- queryParams: req.query,
61
- authorizationHeader: req.headers.authorization ?? null,
62
- queryParamName
63
- });
64
- if (provided === null || !verifyToken(provided, token)) {
65
- res.status(403).json({ error: "Forbidden" });
66
- return;
67
- }
68
- }
69
- const response = await registry.run();
70
- res.status(httpStatusCode(response.status)).json(toJson(response));
80
+ const result = await handle({
81
+ queryParams: req.query,
82
+ authorizationHeader: req.headers.authorization ?? null
83
+ });
84
+ res.status(result.status).json(result.body);
71
85
  } catch {
72
86
  res.status(500).json({ error: "Internal Server Error" });
73
87
  }
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/integrations/next.ts
21
+ var next_exports = {};
22
+ __export(next_exports, {
23
+ createNextHandler: () => createNextHandler
24
+ });
25
+ module.exports = __toCommonJS(next_exports);
26
+
27
+ // src/auth.ts
28
+ var import_node_crypto = require("crypto");
29
+ function verifyToken(provided, expected) {
30
+ if (!provided || !expected) return false;
31
+ const providedHash = (0, import_node_crypto.createHash)("sha256").update(provided).digest();
32
+ const expectedHash = (0, import_node_crypto.createHash)("sha256").update(expected).digest();
33
+ return (0, import_node_crypto.timingSafeEqual)(providedHash, expectedHash);
34
+ }
35
+ function extractToken(options) {
36
+ const { queryParams, authorizationHeader, queryParamName = "token" } = options;
37
+ if (queryParams) {
38
+ const value = queryParams[queryParamName];
39
+ const str = Array.isArray(value) ? value[0] : value;
40
+ if (str) return str;
41
+ }
42
+ if (authorizationHeader?.startsWith("Bearer ")) {
43
+ const token = authorizationHeader.slice("Bearer ".length);
44
+ if (token) return token;
45
+ }
46
+ return null;
47
+ }
48
+
49
+ // src/types.ts
50
+ var Status = {
51
+ HEALTHY: 2,
52
+ DEGRADED: 1,
53
+ OUTAGE: 0
54
+ };
55
+ function statusToLabel(status) {
56
+ const labels = {
57
+ [Status.HEALTHY]: "healthy",
58
+ [Status.DEGRADED]: "degraded",
59
+ [Status.OUTAGE]: "outage"
60
+ };
61
+ return labels[status];
62
+ }
63
+ function toJson(response) {
64
+ return {
65
+ status: statusToLabel(response.status),
66
+ timestamp: response.timestamp,
67
+ checks: Object.fromEntries(
68
+ Object.entries(response.checks).map(([name, result]) => [
69
+ name,
70
+ { status: statusToLabel(result.status), latencyMs: result.latencyMs, message: result.message }
71
+ ])
72
+ )
73
+ };
74
+ }
75
+ function httpStatusCode(status) {
76
+ return status === Status.HEALTHY ? 200 : 503;
77
+ }
78
+
79
+ // src/handler.ts
80
+ function createHealthcheckHandler(options) {
81
+ const { registry, token = null, queryParamName = "token" } = options;
82
+ return async (req) => {
83
+ if (token !== null) {
84
+ const provided = extractToken({
85
+ queryParams: req.queryParams,
86
+ authorizationHeader: req.authorizationHeader,
87
+ queryParamName
88
+ });
89
+ if (provided === null || !verifyToken(provided, token)) {
90
+ return { status: 403, body: { error: "Forbidden" } };
91
+ }
92
+ }
93
+ const response = await registry.run();
94
+ return {
95
+ status: httpStatusCode(response.status),
96
+ body: toJson(response)
97
+ };
98
+ };
99
+ }
100
+
101
+ // src/integrations/next.ts
102
+ function createNextHandler(options) {
103
+ const handle = createHealthcheckHandler(options);
104
+ return async (request) => {
105
+ try {
106
+ const url = new URL(request.url);
107
+ const queryParams = Object.fromEntries(url.searchParams);
108
+ const result = await handle({
109
+ queryParams,
110
+ authorizationHeader: request.headers.get("authorization")
111
+ });
112
+ return Response.json(result.body, { status: result.status });
113
+ } catch {
114
+ return Response.json({ error: "Internal Server Error" }, { status: 500 });
115
+ }
116
+ };
117
+ }
118
+ // Annotate the CommonJS export names for ESM import in node:
119
+ 0 && (module.exports = {
120
+ createNextHandler
121
+ });
@@ -0,0 +1,24 @@
1
+ import { H as HealthcheckHandlerOptions } from '../handler-Bccbso4b.cjs';
2
+ import '../core-BJ2Z0rRi.cjs';
3
+
4
+ type NextHealthcheckOptions = HealthcheckHandlerOptions;
5
+ /**
6
+ * Creates a Next.js App Router GET handler for the healthcheck endpoint.
7
+ *
8
+ * Usage in `app/vitals/route.ts`:
9
+ * ```ts
10
+ * import { HealthcheckRegistry } from '@firebreak/vitals';
11
+ * import { createNextHandler } from '@firebreak/vitals/next';
12
+ *
13
+ * const registry = new HealthcheckRegistry();
14
+ * // ... register checks ...
15
+ *
16
+ * export const GET = createNextHandler({
17
+ * registry,
18
+ * token: process.env.VITALS_TOKEN,
19
+ * });
20
+ * ```
21
+ */
22
+ declare function createNextHandler(options: NextHealthcheckOptions): (request: Request) => Promise<Response>;
23
+
24
+ export { type NextHealthcheckOptions, createNextHandler };
@@ -0,0 +1,24 @@
1
+ import { H as HealthcheckHandlerOptions } from '../handler-D0nYVQvu.js';
2
+ import '../core-BJ2Z0rRi.js';
3
+
4
+ type NextHealthcheckOptions = HealthcheckHandlerOptions;
5
+ /**
6
+ * Creates a Next.js App Router GET handler for the healthcheck endpoint.
7
+ *
8
+ * Usage in `app/vitals/route.ts`:
9
+ * ```ts
10
+ * import { HealthcheckRegistry } from '@firebreak/vitals';
11
+ * import { createNextHandler } from '@firebreak/vitals/next';
12
+ *
13
+ * const registry = new HealthcheckRegistry();
14
+ * // ... register checks ...
15
+ *
16
+ * export const GET = createNextHandler({
17
+ * registry,
18
+ * token: process.env.VITALS_TOKEN,
19
+ * });
20
+ * ```
21
+ */
22
+ declare function createNextHandler(options: NextHealthcheckOptions): (request: Request) => Promise<Response>;
23
+
24
+ export { type NextHealthcheckOptions, createNextHandler };
@@ -0,0 +1,94 @@
1
+ // src/auth.ts
2
+ import { createHash, timingSafeEqual } from "crypto";
3
+ function verifyToken(provided, expected) {
4
+ if (!provided || !expected) return false;
5
+ const providedHash = createHash("sha256").update(provided).digest();
6
+ const expectedHash = createHash("sha256").update(expected).digest();
7
+ return timingSafeEqual(providedHash, expectedHash);
8
+ }
9
+ function extractToken(options) {
10
+ const { queryParams, authorizationHeader, queryParamName = "token" } = options;
11
+ if (queryParams) {
12
+ const value = queryParams[queryParamName];
13
+ const str = Array.isArray(value) ? value[0] : value;
14
+ if (str) return str;
15
+ }
16
+ if (authorizationHeader?.startsWith("Bearer ")) {
17
+ const token = authorizationHeader.slice("Bearer ".length);
18
+ if (token) return token;
19
+ }
20
+ return null;
21
+ }
22
+
23
+ // src/types.ts
24
+ var Status = {
25
+ HEALTHY: 2,
26
+ DEGRADED: 1,
27
+ OUTAGE: 0
28
+ };
29
+ function statusToLabel(status) {
30
+ const labels = {
31
+ [Status.HEALTHY]: "healthy",
32
+ [Status.DEGRADED]: "degraded",
33
+ [Status.OUTAGE]: "outage"
34
+ };
35
+ return labels[status];
36
+ }
37
+ function toJson(response) {
38
+ return {
39
+ status: statusToLabel(response.status),
40
+ timestamp: response.timestamp,
41
+ checks: Object.fromEntries(
42
+ Object.entries(response.checks).map(([name, result]) => [
43
+ name,
44
+ { status: statusToLabel(result.status), latencyMs: result.latencyMs, message: result.message }
45
+ ])
46
+ )
47
+ };
48
+ }
49
+ function httpStatusCode(status) {
50
+ return status === Status.HEALTHY ? 200 : 503;
51
+ }
52
+
53
+ // src/handler.ts
54
+ function createHealthcheckHandler(options) {
55
+ const { registry, token = null, queryParamName = "token" } = options;
56
+ return async (req) => {
57
+ if (token !== null) {
58
+ const provided = extractToken({
59
+ queryParams: req.queryParams,
60
+ authorizationHeader: req.authorizationHeader,
61
+ queryParamName
62
+ });
63
+ if (provided === null || !verifyToken(provided, token)) {
64
+ return { status: 403, body: { error: "Forbidden" } };
65
+ }
66
+ }
67
+ const response = await registry.run();
68
+ return {
69
+ status: httpStatusCode(response.status),
70
+ body: toJson(response)
71
+ };
72
+ };
73
+ }
74
+
75
+ // src/integrations/next.ts
76
+ function createNextHandler(options) {
77
+ const handle = createHealthcheckHandler(options);
78
+ return async (request) => {
79
+ try {
80
+ const url = new URL(request.url);
81
+ const queryParams = Object.fromEntries(url.searchParams);
82
+ const result = await handle({
83
+ queryParams,
84
+ authorizationHeader: request.headers.get("authorization")
85
+ });
86
+ return Response.json(result.body, { status: result.status });
87
+ } catch {
88
+ return Response.json({ error: "Internal Server Error" }, { status: 500 });
89
+ }
90
+ };
91
+ }
92
+ export {
93
+ createNextHandler
94
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firebreak/vitals",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Deep healthcheck endpoints following the HEALTHCHECK_SPEC format",
5
5
  "type": "module",
6
6
  "exports": {
@@ -24,6 +24,16 @@
24
24
  "default": "./dist/checks/postgres.cjs"
25
25
  }
26
26
  },
27
+ "./checks/http": {
28
+ "import": {
29
+ "types": "./dist/checks/http.d.ts",
30
+ "default": "./dist/checks/http.mjs"
31
+ },
32
+ "require": {
33
+ "types": "./dist/checks/http.d.cts",
34
+ "default": "./dist/checks/http.cjs"
35
+ }
36
+ },
27
37
  "./checks/redis": {
28
38
  "import": {
29
39
  "types": "./dist/checks/redis.d.ts",
@@ -43,6 +53,16 @@
43
53
  "types": "./dist/integrations/express.d.cts",
44
54
  "default": "./dist/integrations/express.cjs"
45
55
  }
56
+ },
57
+ "./next": {
58
+ "import": {
59
+ "types": "./dist/integrations/next.d.ts",
60
+ "default": "./dist/integrations/next.mjs"
61
+ },
62
+ "require": {
63
+ "types": "./dist/integrations/next.d.cts",
64
+ "default": "./dist/integrations/next.cjs"
65
+ }
46
66
  }
47
67
  },
48
68
  "main": "./dist/index.cjs",