@firebreak/vitals 1.0.1 → 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 +115 -46
- package/dist/checks/http.cjs +63 -0
- package/dist/checks/http.d.cts +17 -0
- package/dist/checks/http.d.ts +17 -0
- package/dist/checks/http.mjs +36 -0
- package/dist/checks/postgres.cjs +4 -11
- package/dist/checks/postgres.d.cts +1 -1
- package/dist/checks/postgres.d.ts +1 -1
- package/dist/checks/postgres.mjs +4 -13
- package/dist/checks/redis.cjs +4 -11
- package/dist/checks/redis.d.cts +1 -1
- package/dist/checks/redis.d.ts +1 -1
- package/dist/checks/redis.mjs +4 -13
- package/dist/{core-9-MXAO0I.d.cts → core-BJ2Z0rRi.d.cts} +13 -3
- package/dist/{core-9-MXAO0I.d.ts → core-BJ2Z0rRi.d.ts} +13 -3
- package/dist/handler-Bccbso4b.d.cts +25 -0
- package/dist/handler-D0nYVQvu.d.ts +25 -0
- package/dist/index.cjs +24 -0
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.mjs +23 -0
- package/dist/integrations/express.cjs +28 -14
- package/dist/integrations/express.d.cts +3 -6
- package/dist/integrations/express.d.ts +3 -6
- package/dist/integrations/express.mjs +28 -14
- package/dist/integrations/next.cjs +121 -0
- package/dist/integrations/next.d.cts +24 -0
- package/dist/integrations/next.d.ts +24 -0
- package/dist/integrations/next.mjs +94 -0
- package/package.json +21 -1
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
|
|
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('/
|
|
36
|
+
app.get('/vitals', createHealthcheckMiddleware({
|
|
48
37
|
registry,
|
|
49
|
-
token: process.env.
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
###
|
|
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('/
|
|
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
|
-
|
|
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
|
|
257
|
+
"latencyMs": 4,
|
|
189
258
|
"message": ""
|
|
190
259
|
},
|
|
191
260
|
"redis": {
|
|
192
261
|
"status": "healthy",
|
|
193
|
-
"latencyMs": 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
|
+
};
|
package/dist/checks/postgres.cjs
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
72
|
+
return { status: Status.OUTAGE, message };
|
|
80
73
|
} finally {
|
|
81
74
|
client?.release();
|
|
82
75
|
}
|
package/dist/checks/postgres.mjs
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
35
|
+
return { status: Status.OUTAGE, message };
|
|
45
36
|
} finally {
|
|
46
37
|
client?.release();
|
|
47
38
|
}
|
package/dist/checks/redis.cjs
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
80
|
+
return { status: Status.OUTAGE, message };
|
|
88
81
|
}
|
|
89
82
|
};
|
|
90
83
|
}
|
package/dist/checks/redis.d.cts
CHANGED
package/dist/checks/redis.d.ts
CHANGED
package/dist/checks/redis.mjs
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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<
|
|
33
|
-
type SyncCheckFn = () =>
|
|
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
|
|
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<
|
|
33
|
-
type SyncCheckFn = () =>
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
103
|
+
const handle = createHealthcheckHandler(options);
|
|
82
104
|
return async (req, res) => {
|
|
83
105
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
2
|
+
import { H as HealthcheckHandlerOptions } from '../handler-Bccbso4b.cjs';
|
|
3
|
+
import '../core-BJ2Z0rRi.cjs';
|
|
3
4
|
|
|
4
|
-
|
|
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
|
|
2
|
+
import { H as HealthcheckHandlerOptions } from '../handler-D0nYVQvu.js';
|
|
3
|
+
import '../core-BJ2Z0rRi.js';
|
|
3
4
|
|
|
4
|
-
|
|
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
|
|
77
|
+
const handle = createHealthcheckHandler(options);
|
|
56
78
|
return async (req, res) => {
|
|
57
79
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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.
|
|
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",
|