sermon-js 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +239 -2
- package/dist/api/index.cjs +174 -0
- package/dist/api/index.cjs.map +1 -0
- package/dist/api/index.d.cts +77 -0
- package/dist/api/index.d.ts +77 -0
- package/dist/api/index.js +171 -0
- package/dist/api/index.js.map +1 -0
- package/dist/chunk-2M4UBCDM.js +178 -0
- package/dist/chunk-2M4UBCDM.js.map +1 -0
- package/dist/chunk-AH372HLN.js +98 -0
- package/dist/chunk-AH372HLN.js.map +1 -0
- package/dist/chunk-APC2HVSI.js +154 -0
- package/dist/chunk-APC2HVSI.js.map +1 -0
- package/dist/chunk-D7BBJTYU.cjs +335 -0
- package/dist/chunk-D7BBJTYU.cjs.map +1 -0
- package/dist/chunk-ELV4ZU6G.cjs +51 -0
- package/dist/chunk-ELV4ZU6G.cjs.map +1 -0
- package/dist/chunk-EMP7XVKC.js +334 -0
- package/dist/chunk-EMP7XVKC.js.map +1 -0
- package/dist/chunk-I3346MUL.js +49 -0
- package/dist/chunk-I3346MUL.js.map +1 -0
- package/dist/chunk-NP3YYVRZ.cjs +163 -0
- package/dist/chunk-NP3YYVRZ.cjs.map +1 -0
- package/dist/chunk-PARE6EUZ.cjs +184 -0
- package/dist/chunk-PARE6EUZ.cjs.map +1 -0
- package/dist/chunk-XHXEKB3Q.cjs +241 -0
- package/dist/chunk-XHXEKB3Q.cjs.map +1 -0
- package/dist/chunk-XPXIDJUU.js +229 -0
- package/dist/chunk-XPXIDJUU.js.map +1 -0
- package/dist/chunk-ZI442ZA5.cjs +100 -0
- package/dist/chunk-ZI442ZA5.cjs.map +1 -0
- package/dist/config-BEbP-uLf.d.cts +125 -0
- package/dist/config-w1tm1bdu.d.ts +125 -0
- package/dist/generic-BJmlgtUM.d.ts +31 -0
- package/dist/generic-lmrGxMDC.d.cts +31 -0
- package/dist/index.cjs +407 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +93 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +330 -0
- package/dist/index.js.map +1 -0
- package/dist/presets/index.cjs +867 -0
- package/dist/presets/index.cjs.map +1 -0
- package/dist/presets/index.d.cts +427 -0
- package/dist/presets/index.d.ts +427 -0
- package/dist/presets/index.js +831 -0
- package/dist/presets/index.js.map +1 -0
- package/dist/react/heroui/index.cjs +822 -0
- package/dist/react/heroui/index.cjs.map +1 -0
- package/dist/react/heroui/index.d.cts +122 -0
- package/dist/react/heroui/index.d.ts +122 -0
- package/dist/react/heroui/index.js +821 -0
- package/dist/react/heroui/index.js.map +1 -0
- package/dist/react/heroui/styles.css +11 -0
- package/dist/react/index.cjs +71 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +53 -0
- package/dist/react/index.d.ts +53 -0
- package/dist/react/index.js +70 -0
- package/dist/react/index.js.map +1 -0
- package/dist/status-DsWzgb9Q.d.cts +146 -0
- package/dist/status-zd04ZQ98.d.ts +146 -0
- package/dist/store-DybhDphS.d.cts +77 -0
- package/dist/store-_GuzSP0z.d.ts +77 -0
- package/dist/stores/mongodb.cjs +99 -0
- package/dist/stores/mongodb.cjs.map +1 -0
- package/dist/stores/mongodb.d.cts +40 -0
- package/dist/stores/mongodb.d.ts +40 -0
- package/dist/stores/mongodb.js +97 -0
- package/dist/stores/mongodb.js.map +1 -0
- package/dist/stores/redis.cjs +23 -0
- package/dist/stores/redis.cjs.map +1 -0
- package/dist/stores/redis.d.cts +31 -0
- package/dist/stores/redis.d.ts +31 -0
- package/dist/stores/redis.js +21 -0
- package/dist/stores/redis.js.map +1 -0
- package/dist/stores/sql.cjs +164 -0
- package/dist/stores/sql.cjs.map +1 -0
- package/dist/stores/sql.d.cts +39 -0
- package/dist/stores/sql.d.ts +39 -0
- package/dist/stores/sql.js +162 -0
- package/dist/stores/sql.js.map +1 -0
- package/dist/stores/upstash.cjs +27 -0
- package/dist/stores/upstash.cjs.map +1 -0
- package/dist/stores/upstash.d.cts +19 -0
- package/dist/stores/upstash.d.ts +19 -0
- package/dist/stores/upstash.js +25 -0
- package/dist/stores/upstash.js.map +1 -0
- package/dist/testing/index.cjs +209 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +66 -0
- package/dist/testing/index.d.ts +66 -0
- package/dist/testing/index.js +205 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/tree-BXbXfjF1.d.cts +236 -0
- package/dist/tree-BXbXfjF1.d.ts +236 -0
- package/dist/types-CctvUAs2.d.cts +192 -0
- package/dist/types-CctvUAs2.d.ts +192 -0
- package/package.json +233 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BSide-Tech
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,240 @@
|
|
|
1
|
-
#
|
|
1
|
+
# sermon-js
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Ser**vice **mon**itor: everything behind a status page, as a TypeScript library.
|
|
4
|
+
|
|
5
|
+
Describe the services you depend on, and get checks, scoring, history, an HTTP API and a
|
|
6
|
+
ready-made page. It runs anywhere: Node, Bun, Deno, Vercel, Netlify, Cloudflare Workers.
|
|
7
|
+
No hosting assumptions, no required database.
|
|
8
|
+
|
|
9
|
+
> **Status: pre-release.** Everything below is built and tested; the first npm release is
|
|
10
|
+
> pending. See [Roadmap](#roadmap).
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install sermon-js
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { custom, defineStatus, generic, group } from "sermon-js";
|
|
20
|
+
import { createStatusHandler } from "sermon-js/api";
|
|
21
|
+
|
|
22
|
+
export const status = defineStatus({
|
|
23
|
+
services: [
|
|
24
|
+
group({
|
|
25
|
+
title: "Our services",
|
|
26
|
+
services: [
|
|
27
|
+
generic.http({ name: "Web app", url: process.env.WEB_URL, critical: true }),
|
|
28
|
+
generic.http({
|
|
29
|
+
name: "API",
|
|
30
|
+
url: "https://api.example.com/health",
|
|
31
|
+
secrets: { token: process.env.API_TOKEN },
|
|
32
|
+
headers: (secrets) => ({ Authorization: `Bearer ${secrets.token}` }),
|
|
33
|
+
critical: true,
|
|
34
|
+
}),
|
|
35
|
+
],
|
|
36
|
+
}),
|
|
37
|
+
custom({
|
|
38
|
+
name: "Queue depth",
|
|
39
|
+
check: async ({ http }) => {
|
|
40
|
+
const res = await http("https://queue.internal/stats");
|
|
41
|
+
if (!res.ok) return res.outcome;
|
|
42
|
+
const { waiting, limit } = res.json as { waiting: number; limit: number };
|
|
43
|
+
return {
|
|
44
|
+
status: waiting > limit * 0.9 ? "degraded" : "up",
|
|
45
|
+
usage: [{ label: "Queue", used: waiting, limit, unit: "jobs" }],
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// A standard (Request) => Response handler: mount it in any framework
|
|
53
|
+
export const handler = createStatusHandler(status);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then trigger `status.tick()` on a schedule (see [Scheduling](docs/scheduling.md)) and read
|
|
57
|
+
`GET /api/status`.
|
|
58
|
+
|
|
59
|
+
## Describing services
|
|
60
|
+
|
|
61
|
+
`services` is a list of **presets** (see [all presets](docs/presets.md)), **custom checks** and
|
|
62
|
+
**groups**. Groups can nest. Every service takes the same five concerns:
|
|
63
|
+
|
|
64
|
+
| Concern | Option |
|
|
65
|
+
|---|---|
|
|
66
|
+
| How to call it | `name` |
|
|
67
|
+
| What it is | the function you call, plus the fields it needs (`url`, …) |
|
|
68
|
+
| Secrets it needs | `secrets: { … }`: redacted from everything public and from logs (see [Security](docs/security.md)) |
|
|
69
|
+
| What to track | `track: { … }`: options of the preset |
|
|
70
|
+
| Importance | `critical: true` weighs more in the score; one critical service down means "outage" |
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { deepgram, fly, statuspage } from "sermon-js/presets";
|
|
74
|
+
|
|
75
|
+
group({ title: "Platform", services: [
|
|
76
|
+
// Never wakes a sleeping machine; reports each region on its own
|
|
77
|
+
fly.app({ app: "my-api", secrets: { token: process.env.FLY_API_TOKEN }, critical: true }),
|
|
78
|
+
deepgram({ secrets: { apiKey: process.env.DEEPGRAM_API_KEY }, track: { balance: { warnBelow: 20 } } }),
|
|
79
|
+
statuspage({ name: "Fly.io", url: "https://status.flyio.net" }),
|
|
80
|
+
] })
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
A service whose required secret or field is empty (say, an environment variable that isn't
|
|
84
|
+
set yet) is listed as **not configured**: it never runs and never throws.
|
|
85
|
+
|
|
86
|
+
Services have no IDs. Keys are derived (`our-services:web-app`), unique and URL-safe.
|
|
87
|
+
|
|
88
|
+
### Custom checks
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
custom({
|
|
92
|
+
name: "Queue depth", // required
|
|
93
|
+
description: "Jobs waiting in the worker queue",
|
|
94
|
+
critical: false,
|
|
95
|
+
secrets: { apiKey: process.env.QUEUE_API_KEY }, // typed inside `check`
|
|
96
|
+
slowMs: 1500, // slower than this → degraded (default 2000)
|
|
97
|
+
timeoutMs: 10_000, // hard cap (default 10 s)
|
|
98
|
+
check: async ({ secrets, http, measure, combine, signal, cache }) => { … },
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`check` returns `true`/nothing (up), `false` (down), or an outcome:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
type CheckOutcome = {
|
|
106
|
+
status: "up" | "degraded" | "down" | "sleeping";
|
|
107
|
+
latencyMs?: number; // measured for you when omitted
|
|
108
|
+
httpStatus?: number;
|
|
109
|
+
error?: string; // shown publicly: secrets are redacted, long text truncated
|
|
110
|
+
metrics?: Record<string, string | number>; // small chips
|
|
111
|
+
usage?: { label; used; limit; unit?; resetsAt? }[]; // quota bars
|
|
112
|
+
instances?: { key; region?; state?; status; latencyMs?; error? }[]; // per machine/region
|
|
113
|
+
probed?: boolean; // false when the target was deliberately not touched
|
|
114
|
+
};
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Throwing means down. The helpers never throw: `http()` returns
|
|
118
|
+
`{ ok, status, latencyMs, json, text, outcome }` where `outcome` is ready to return.
|
|
119
|
+
|
|
120
|
+
### Statuses
|
|
121
|
+
|
|
122
|
+
| Status | Meaning | Score |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `up` | Reachable and healthy | 1 |
|
|
125
|
+
| `degraded` | Working with a warning: slow, rate limited, low quota, part of its instances down | 0.6 |
|
|
126
|
+
| `down` | Unreachable, erroring, rejected, crashed | 0 |
|
|
127
|
+
| `sleeping` | Deliberately not running (scale to zero), and not probed | 1 |
|
|
128
|
+
| `unconfigured` | A required secret or field is empty; never run | excluded |
|
|
129
|
+
|
|
130
|
+
Applied to every service: latency above `slowMs` turns up into degraded, HTTP 429 is
|
|
131
|
+
degraded, timeouts and thrown errors are down.
|
|
132
|
+
|
|
133
|
+
### Writing a preset
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { definePreset, type PresetOptions } from "sermon-js";
|
|
137
|
+
|
|
138
|
+
type Options = PresetOptions & {
|
|
139
|
+
app: string;
|
|
140
|
+
secrets: { token: string | undefined };
|
|
141
|
+
track?: { health?: string | false };
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const flyApp = definePreset<Options>()({
|
|
145
|
+
kind: "fly.app",
|
|
146
|
+
requiredSecrets: ["token"],
|
|
147
|
+
derive: (o) => ({ name: o.app, identity: `fly.app:${o.app}` }),
|
|
148
|
+
check: async ({ options, secrets, http }) => { … },
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`identity` gives the service a natural key, so its history survives renames and moves.
|
|
153
|
+
|
|
154
|
+
## The engine
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
await status.tick(); // run everything, only if due (idempotent)
|
|
158
|
+
await status.run(); // run everything now
|
|
159
|
+
await status.run({ keys: ["queue-depth"] });
|
|
160
|
+
await status.snapshot(); // tree + latest results + statistics + score
|
|
161
|
+
await status.history({ range: "24h" }); // score history in buckets ("24h" | "7d")
|
|
162
|
+
await status.serviceHistory("queue-depth");
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Options: `interval` (default `"5m"`), `retention` (`"7d"`), `scoring`, `store`, `hideResponseBodies`
|
|
166
|
+
(report failed answers by status code only; see [Security](docs/security.md)).
|
|
167
|
+
|
|
168
|
+
## HTTP API
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
createStatusHandler(status, {
|
|
172
|
+
basePath: "/api", // default
|
|
173
|
+
secret: process.env.TICK_SECRET, // protects /tick when set
|
|
174
|
+
refreshOnRead: false, // start a run when /status is read and data is stale
|
|
175
|
+
rateLimits: { runAll: "60s", runOne: "20s" },
|
|
176
|
+
cors: false,
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
| Route | |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `GET /status` | Snapshot |
|
|
183
|
+
| `GET /history?range=24h\|7d` | Score history |
|
|
184
|
+
| `GET /services/:key/history` | Samples of one service |
|
|
185
|
+
| `POST /run`, `POST /run/:key` | On-demand runs, rate limited |
|
|
186
|
+
| `GET\|POST /tick` | Run if due, for schedulers |
|
|
187
|
+
|
|
188
|
+
For plain Node servers: `http.createServer(toNodeListener(handler))`.
|
|
189
|
+
|
|
190
|
+
## Stores
|
|
191
|
+
|
|
192
|
+
History is kept in memory by default, which lasts as long as the process. For anything
|
|
193
|
+
serverless, or to survive restarts, pass a store:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { upstashStore } from "sermon-js/stores/upstash"; // Upstash / Vercel KV, fetch only
|
|
197
|
+
import { redisStore } from "sermon-js/stores/redis"; // any Redis client you already have
|
|
198
|
+
|
|
199
|
+
defineStatus({ store: upstashStore({ url, token }), services });
|
|
200
|
+
defineStatus({ store: redisStore({ client: new Redis(url) }), services }); // ioredis or node-redis
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { sqlStore } from "sermon-js/stores/sql"; // Postgres, MySQL/MariaDB, SQLite: your driver
|
|
205
|
+
import { mongoStore } from "sermon-js/stores/mongodb";
|
|
206
|
+
|
|
207
|
+
defineStatus({ store: sqlStore({ dialect: "postgres", query: async (sql, params) => (await pool.query(sql, params)).rows }), services });
|
|
208
|
+
defineStatus({ store: mongoStore({ db: client.db("status") }), services });
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Anything else is a small interface to implement and a conformance suite to check it with.
|
|
212
|
+
See [Stores](docs/stores.md).
|
|
213
|
+
|
|
214
|
+
## UI
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
import { StatusPage } from "sermon-js/react/heroui";
|
|
218
|
+
|
|
219
|
+
<StatusPage baseUrl="/api" brand={<Logo />} />
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
A complete page (score, history, collapsible sections, per-region details, usage bars,
|
|
223
|
+
on-demand checks, deep links, theme switch) on HeroUI and Tailwind, plus a headless layer of
|
|
224
|
+
hooks and helpers to build your own. See [UI kit](docs/ui.md) and
|
|
225
|
+
[`examples/vite`](examples/vite).
|
|
226
|
+
|
|
227
|
+
## Roadmap
|
|
228
|
+
|
|
229
|
+
- [x] Core: config tree, custom checks, `generic.http`, runner, scoring, history, statistics
|
|
230
|
+
- [x] Host-agnostic scheduling: `tick()`, `startScheduler`, `/tick`, refresh on read
|
|
231
|
+
- [x] HTTP API handler, Node adapter
|
|
232
|
+
- [x] Stores: memory, Upstash REST, any Redis client
|
|
233
|
+
- [x] Presets: Fly apps (sleep-safe, per region), GCP instances, AWS/S3, Statuspage, Pusher, AI/translation/video/email providers, MongoDB, Redis
|
|
234
|
+
- [x] Stores: SQL (Postgres, MySQL/MariaDB, SQLite), MongoDB
|
|
235
|
+
- [x] React UI kit (headless hooks + HeroUI components)
|
|
236
|
+
- [ ] First npm release
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
MIT
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkNP3YYVRZ_cjs = require('../chunk-NP3YYVRZ.cjs');
|
|
4
|
+
|
|
5
|
+
// src/api/index.ts
|
|
6
|
+
var RUN_ALL_LOCK = "sermon:run:all";
|
|
7
|
+
function parseRange(value) {
|
|
8
|
+
return value && value in chunkNP3YYVRZ_cjs.RANGES ? value : "24h";
|
|
9
|
+
}
|
|
10
|
+
function safeEqual(a, b) {
|
|
11
|
+
if (a.length !== b.length) return false;
|
|
12
|
+
let diff = 0;
|
|
13
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
14
|
+
return diff === 0;
|
|
15
|
+
}
|
|
16
|
+
function presentedSecret(request, url) {
|
|
17
|
+
const authorization = request.headers.get("authorization");
|
|
18
|
+
if (authorization?.toLowerCase().startsWith("bearer ")) return authorization.slice(7).trim();
|
|
19
|
+
return request.headers.get("x-sermon-secret") ?? url.searchParams.get("key") ?? void 0;
|
|
20
|
+
}
|
|
21
|
+
function describeError(error) {
|
|
22
|
+
if (!(error instanceof Error)) return String(error);
|
|
23
|
+
const parts = [error.stack ?? `${error.name}: ${error.message}`];
|
|
24
|
+
let cause = error.cause;
|
|
25
|
+
for (let depth = 0; cause && depth < 3; depth++) {
|
|
26
|
+
parts.push(`Caused by: ${cause instanceof Error ? `${cause.name}: ${cause.message}` : String(cause)}`);
|
|
27
|
+
cause = cause instanceof Error ? cause.cause : void 0;
|
|
28
|
+
}
|
|
29
|
+
return parts.join("\n");
|
|
30
|
+
}
|
|
31
|
+
function createStatusHandler(status, options = {}) {
|
|
32
|
+
const basePath = `/${(options.basePath ?? "/api").replace(/^\/+|\/+$/g, "")}`.replace(/^\/$/, "");
|
|
33
|
+
const runAllMs = chunkNP3YYVRZ_cjs.toMs(options.rateLimits?.runAll ?? "60s");
|
|
34
|
+
const runOneMs = chunkNP3YYVRZ_cjs.toMs(options.rateLimits?.runOne ?? "20s");
|
|
35
|
+
const refreshBudgetMs = chunkNP3YYVRZ_cjs.toMs(options.refreshBudget ?? "8s");
|
|
36
|
+
const cache = options.cache === false ? void 0 : { status: 10, history: 30, ...options.cache };
|
|
37
|
+
const corsHeaders = (request) => {
|
|
38
|
+
if (!options.cors) return {};
|
|
39
|
+
const origin = options.cors === true ? request.headers.get("origin") ?? "*" : options.cors;
|
|
40
|
+
return {
|
|
41
|
+
"access-control-allow-origin": origin,
|
|
42
|
+
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
43
|
+
"access-control-allow-headers": "authorization, content-type, x-sermon-secret",
|
|
44
|
+
vary: "origin"
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
const json = (request, body, init = {}) => new Response(JSON.stringify(body), {
|
|
48
|
+
status: init.status ?? 200,
|
|
49
|
+
headers: {
|
|
50
|
+
"content-type": "application/json; charset=utf-8",
|
|
51
|
+
"cache-control": "no-store",
|
|
52
|
+
...corsHeaders(request),
|
|
53
|
+
...init.headers
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
const cached = (seconds) => seconds ? {
|
|
57
|
+
"cache-control": "no-cache",
|
|
58
|
+
"cdn-cache-control": `max-age=${seconds}, stale-while-revalidate=${seconds * 3}`
|
|
59
|
+
} : {};
|
|
60
|
+
const tooSoon = (request, message, retryAfterMs) => {
|
|
61
|
+
const retryAfter = Math.max(1, Math.ceil(retryAfterMs / 1e3));
|
|
62
|
+
return json(request, { error: message, retryAfter }, { status: 429, headers: { "retry-after": String(retryAfter) } });
|
|
63
|
+
};
|
|
64
|
+
async function refreshIfStale(context) {
|
|
65
|
+
const last = await status.lastFullRun();
|
|
66
|
+
if (last && Date.now() < last.at + status.intervalMs) return;
|
|
67
|
+
const tick = status.tick().catch(() => void 0);
|
|
68
|
+
const waitUntil = context?.waitUntil ?? options.waitUntil;
|
|
69
|
+
if (waitUntil) {
|
|
70
|
+
waitUntil(tick);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
await Promise.race([tick, new Promise((resolve) => setTimeout(resolve, refreshBudgetMs))]);
|
|
74
|
+
}
|
|
75
|
+
return async function handle(request, context) {
|
|
76
|
+
const url = new URL(request.url);
|
|
77
|
+
const method = request.method.toUpperCase();
|
|
78
|
+
if (basePath && url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) {
|
|
79
|
+
return json(request, { error: "Not found" }, { status: 404 });
|
|
80
|
+
}
|
|
81
|
+
if (method === "OPTIONS") return new Response(null, { status: 204, headers: corsHeaders(request) });
|
|
82
|
+
let segments;
|
|
83
|
+
try {
|
|
84
|
+
segments = url.pathname.slice(basePath.length).split("/").filter(Boolean).map((segment) => decodeURIComponent(segment));
|
|
85
|
+
} catch {
|
|
86
|
+
return json(request, { error: "Not found" }, { status: 404 });
|
|
87
|
+
}
|
|
88
|
+
const [first, second, third] = segments;
|
|
89
|
+
try {
|
|
90
|
+
if (method === "GET" && first === "status" && segments.length === 1) {
|
|
91
|
+
if (options.refreshOnRead) await refreshIfStale(context);
|
|
92
|
+
return json(request, await status.snapshot(), { headers: cached(cache?.status) });
|
|
93
|
+
}
|
|
94
|
+
if (method === "GET" && first === "history" && segments.length === 1) {
|
|
95
|
+
const range = parseRange(url.searchParams.get("range"));
|
|
96
|
+
return json(request, await status.history({ range }), { headers: cached(cache?.history) });
|
|
97
|
+
}
|
|
98
|
+
if (method === "GET" && first === "services" && second && third === "history" && segments.length === 3) {
|
|
99
|
+
if (!status.has(second)) return json(request, { error: "Unknown service" }, { status: 404 });
|
|
100
|
+
const range = parseRange(url.searchParams.get("range"));
|
|
101
|
+
return json(request, await status.serviceHistory(second, { range }), { headers: cached(cache?.history) });
|
|
102
|
+
}
|
|
103
|
+
if (method === "POST" && first === "run" && segments.length === 1) {
|
|
104
|
+
const lock = await status.lock(RUN_ALL_LOCK, runAllMs);
|
|
105
|
+
if (!lock.ok) return tooSoon(request, "A full run happened recently", lock.retryAfterMs);
|
|
106
|
+
await status.run({ trigger: "manual" });
|
|
107
|
+
return json(request, await status.snapshot());
|
|
108
|
+
}
|
|
109
|
+
if (method === "POST" && first === "run" && second && segments.length === 2) {
|
|
110
|
+
if (!status.has(second)) return json(request, { error: "Unknown service" }, { status: 404 });
|
|
111
|
+
const lock = await status.lock(`sermon:run:${second}`, runOneMs);
|
|
112
|
+
if (!lock.ok) return tooSoon(request, "This service was checked recently", lock.retryAfterMs);
|
|
113
|
+
await status.run({ keys: [second], trigger: "manual" });
|
|
114
|
+
return json(request, await status.snapshot());
|
|
115
|
+
}
|
|
116
|
+
if ((method === "GET" || method === "POST") && (first === "tick" || first === "cron") && segments.length === 1) {
|
|
117
|
+
const presented = presentedSecret(request, url);
|
|
118
|
+
const authorized = !!options.secret && !!presented && safeEqual(presented, options.secret);
|
|
119
|
+
if (options.secret && !authorized) return json(request, { error: "Unauthorized" }, { status: 401 });
|
|
120
|
+
const force = authorized && ["1", "true"].includes(url.searchParams.get("force") ?? "");
|
|
121
|
+
const result = await status.tick({ force });
|
|
122
|
+
if (!result.ran) return json(request, result);
|
|
123
|
+
await status.lock(RUN_ALL_LOCK, runAllMs);
|
|
124
|
+
return json(request, {
|
|
125
|
+
ran: true,
|
|
126
|
+
at: result.report.at,
|
|
127
|
+
score: result.report.score,
|
|
128
|
+
label: result.report.label,
|
|
129
|
+
statuses: Object.fromEntries(result.report.results.map((r) => [r.key, r.status]))
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return json(request, { error: "Not found" }, { status: 404 });
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const message = status.redact(describeError(error));
|
|
135
|
+
if (options.onError) options.onError({ message, error });
|
|
136
|
+
else console.error(`[sermon-js] ${message}`);
|
|
137
|
+
return json(request, { error: "Internal error" }, { status: 500 });
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function toNodeListener(handler) {
|
|
142
|
+
return async function listener(req, res) {
|
|
143
|
+
try {
|
|
144
|
+
const host = (Array.isArray(req.headers.host) ? req.headers.host[0] : req.headers.host) ?? "localhost";
|
|
145
|
+
const headers = new Headers();
|
|
146
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
147
|
+
if (value !== void 0) headers.set(name, Array.isArray(value) ? value.join(", ") : value);
|
|
148
|
+
}
|
|
149
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
150
|
+
let body;
|
|
151
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
152
|
+
const decoder = new TextDecoder();
|
|
153
|
+
body = "";
|
|
154
|
+
for await (const chunk of req) body += decoder.decode(chunk, { stream: true });
|
|
155
|
+
body += decoder.decode();
|
|
156
|
+
}
|
|
157
|
+
const response = await handler(
|
|
158
|
+
new Request(`http://${host}${req.url ?? "/"}`, { method, headers, body: body || void 0 })
|
|
159
|
+
);
|
|
160
|
+
res.statusCode = response.status;
|
|
161
|
+
response.headers.forEach((value, name) => res.setHeader(name, value));
|
|
162
|
+
res.end(await response.text());
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error(`[sermon-js] ${error instanceof Error ? error.name : "Error"} while serving a request`);
|
|
165
|
+
res.statusCode = 500;
|
|
166
|
+
res.end('{"error":"Internal error"}');
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
exports.createStatusHandler = createStatusHandler;
|
|
172
|
+
exports.toNodeListener = toNodeListener;
|
|
173
|
+
//# sourceMappingURL=index.cjs.map
|
|
174
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/api/index.ts"],"names":["RANGES","toMs"],"mappings":"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","file":"index.cjs","sourcesContent":["import { toMs, type Duration } from \"../duration\";\nimport { RANGES } from \"../history\";\nimport type { Status } from \"../status\";\nimport type { HistoryRange } from \"../types\";\n\nexport type HandlerContext = {\n /** Keeps the platform alive for background work after the response (Workers, Vercel, Netlify…) */\n waitUntil?: (promise: Promise<unknown>) => void;\n};\n\nexport type StatusHandlerOptions = {\n /** Where the routes are mounted (default \"/api\"). The handler ignores anything before it. */\n basePath?: string;\n /**\n * Protects `/tick`. When set, callers must send it as `Authorization: Bearer <secret>`,\n * `x-sermon-secret: <secret>` or `?key=<secret>`. Without a secret, `/tick` is public but\n * harmless: it only runs when a run is due.\n */\n secret?: string;\n /** Minimum time between public on-demand runs */\n rateLimits?: {\n /** Full runs (default \"60s\") */\n runAll?: Duration;\n /** Single-service runs, per service (default \"20s\") */\n runOne?: Duration;\n };\n /**\n * When `/status` is read and the data is stale, start a `tick()`. With a `waitUntil` the\n * response isn't delayed; without one the handler waits for the run (up to `refreshBudget`).\n * Keeps a page fresh without any scheduler, as long as someone visits it.\n */\n refreshOnRead?: boolean;\n /** How long a read may wait for a refresh when there is no `waitUntil` (default \"8s\") */\n refreshBudget?: Duration;\n waitUntil?: HandlerContext[\"waitUntil\"];\n /**\n * Shared-cache lifetimes in seconds for CDNs and edges (default `{ status: 10, history: 30 }`).\n * Browsers are always told to revalidate. Pass `false` to disable shared caching.\n */\n cache?: { status?: number; history?: number } | false;\n /** Allow other origins to call the API: `true` for any, or one origin (default: same-origin only) */\n cors?: boolean | string;\n /**\n * Receives unexpected failures (e.g. the store is unreachable). `message` has every configured\n * secret and credential-shaped value removed; `error` is the raw value, which may contain\n * connection strings: don't print it. Default: `console.error` of `message` only.\n */\n onError?: (failure: { message: string; error: unknown }) => void;\n};\n\nexport type StatusHandler = (request: Request, context?: HandlerContext) => Promise<Response>;\n\nconst RUN_ALL_LOCK = \"sermon:run:all\";\n\nfunction parseRange(value: string | null): HistoryRange {\n return value && value in RANGES ? (value as HistoryRange) : \"24h\";\n}\n\n/** Constant-time comparison, so the secret can't be guessed byte by byte from response times */\nfunction safeEqual(a: string, b: string) {\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n return diff === 0;\n}\n\nfunction presentedSecret(request: Request, url: URL) {\n const authorization = request.headers.get(\"authorization\");\n if (authorization?.toLowerCase().startsWith(\"bearer \")) return authorization.slice(7).trim();\n return request.headers.get(\"x-sermon-secret\") ?? url.searchParams.get(\"key\") ?? undefined;\n}\n\n/**\n * Expose a status engine over HTTP. The result is a standard `(Request) => Response` function:\n * mount it in Next.js, Hono, Bun, Deno, Cloudflare Workers, Netlify or Vercel functions, or wrap\n * it with `toNodeListener` for plain Node servers.\n *\n * Routes (under `basePath`):\n * - `GET /status` snapshot: tree, latest results, statistics, score\n * - `GET /history?range=24h|7d` score history\n * - `GET /services/:key/history` samples of one service\n * - `POST /run` run everything now (rate limited)\n * - `POST /run/:key` run one service now (rate limited)\n * - `GET|POST /tick` run everything if due (for schedulers); `?force=1` with the secret\n */\n/** An error as loggable text: name, message, causes and stack frames */\nfunction describeError(error: unknown) {\n if (!(error instanceof Error)) return String(error);\n const parts = [error.stack ?? `${error.name}: ${error.message}`];\n let cause = (error as Error & { cause?: unknown }).cause;\n for (let depth = 0; cause && depth < 3; depth++) {\n parts.push(`Caused by: ${cause instanceof Error ? `${cause.name}: ${cause.message}` : String(cause)}`);\n cause = cause instanceof Error ? (cause as Error & { cause?: unknown }).cause : undefined;\n }\n return parts.join(\"\\n\");\n}\n\nexport function createStatusHandler(status: Status, options: StatusHandlerOptions = {}): StatusHandler {\n const basePath = `/${(options.basePath ?? \"/api\").replace(/^\\/+|\\/+$/g, \"\")}`.replace(/^\\/$/, \"\");\n const runAllMs = toMs(options.rateLimits?.runAll ?? \"60s\");\n const runOneMs = toMs(options.rateLimits?.runOne ?? \"20s\");\n const refreshBudgetMs = toMs(options.refreshBudget ?? \"8s\");\n const cache = options.cache === false ? undefined : { status: 10, history: 30, ...options.cache };\n\n const corsHeaders = (request: Request): Record<string, string> => {\n if (!options.cors) return {};\n const origin = options.cors === true ? (request.headers.get(\"origin\") ?? \"*\") : options.cors;\n return {\n \"access-control-allow-origin\": origin,\n \"access-control-allow-methods\": \"GET, POST, OPTIONS\",\n \"access-control-allow-headers\": \"authorization, content-type, x-sermon-secret\",\n vary: \"origin\",\n };\n };\n\n const json = (request: Request, body: unknown, init: { status?: number; headers?: Record<string, string> } = {}) =>\n new Response(JSON.stringify(body), {\n status: init.status ?? 200,\n headers: {\n \"content-type\": \"application/json; charset=utf-8\",\n \"cache-control\": \"no-store\",\n ...corsHeaders(request),\n ...init.headers,\n },\n });\n\n /** Short shared cache for CDNs only: browsers must always revalidate, or they show stale results */\n const cached = (seconds: number | undefined): Record<string, string> =>\n seconds\n ? {\n \"cache-control\": \"no-cache\",\n \"cdn-cache-control\": `max-age=${seconds}, stale-while-revalidate=${seconds * 3}`,\n }\n : {};\n\n const tooSoon = (request: Request, message: string, retryAfterMs: number) => {\n const retryAfter = Math.max(1, Math.ceil(retryAfterMs / 1000));\n return json(request, { error: message, retryAfter }, { status: 429, headers: { \"retry-after\": String(retryAfter) } });\n };\n\n async function refreshIfStale(context: HandlerContext | undefined) {\n const last = await status.lastFullRun();\n if (last && Date.now() < last.at + status.intervalMs) return;\n const tick = status.tick().catch(() => undefined);\n const waitUntil = context?.waitUntil ?? options.waitUntil;\n if (waitUntil) {\n waitUntil(tick);\n return;\n }\n await Promise.race([tick, new Promise((resolve) => setTimeout(resolve, refreshBudgetMs))]);\n }\n\n return async function handle(request, context) {\n const url = new URL(request.url);\n const method = request.method.toUpperCase();\n if (basePath && url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) {\n return json(request, { error: \"Not found\" }, { status: 404 });\n }\n if (method === \"OPTIONS\") return new Response(null, { status: 204, headers: corsHeaders(request) });\n\n let segments: string[];\n try {\n segments = url.pathname\n .slice(basePath.length)\n .split(\"/\")\n .filter(Boolean)\n .map((segment) => decodeURIComponent(segment));\n } catch {\n return json(request, { error: \"Not found\" }, { status: 404 });\n }\n const [first, second, third] = segments;\n\n try {\n if (method === \"GET\" && first === \"status\" && segments.length === 1) {\n if (options.refreshOnRead) await refreshIfStale(context);\n return json(request, await status.snapshot(), { headers: cached(cache?.status) });\n }\n\n if (method === \"GET\" && first === \"history\" && segments.length === 1) {\n const range = parseRange(url.searchParams.get(\"range\"));\n return json(request, await status.history({ range }), { headers: cached(cache?.history) });\n }\n\n if (method === \"GET\" && first === \"services\" && second && third === \"history\" && segments.length === 3) {\n if (!status.has(second)) return json(request, { error: \"Unknown service\" }, { status: 404 });\n const range = parseRange(url.searchParams.get(\"range\"));\n return json(request, await status.serviceHistory(second, { range }), { headers: cached(cache?.history) });\n }\n\n if (method === \"POST\" && first === \"run\" && segments.length === 1) {\n const lock = await status.lock(RUN_ALL_LOCK, runAllMs);\n if (!lock.ok) return tooSoon(request, \"A full run happened recently\", lock.retryAfterMs);\n await status.run({ trigger: \"manual\" });\n return json(request, await status.snapshot());\n }\n\n if (method === \"POST\" && first === \"run\" && second && segments.length === 2) {\n if (!status.has(second)) return json(request, { error: \"Unknown service\" }, { status: 404 });\n const lock = await status.lock(`sermon:run:${second}`, runOneMs);\n if (!lock.ok) return tooSoon(request, \"This service was checked recently\", lock.retryAfterMs);\n await status.run({ keys: [second], trigger: \"manual\" });\n return json(request, await status.snapshot());\n }\n\n // \"/cron\" is kept as an alias for schedulers configured before \"/tick\" existed\n if ((method === \"GET\" || method === \"POST\") && (first === \"tick\" || first === \"cron\") && segments.length === 1) {\n const presented = presentedSecret(request, url);\n const authorized = !!options.secret && !!presented && safeEqual(presented, options.secret);\n if (options.secret && !authorized) return json(request, { error: \"Unauthorized\" }, { status: 401 });\n\n // Forcing a run is only for callers that hold the secret\n const force = authorized && [\"1\", \"true\"].includes(url.searchParams.get(\"force\") ?? \"\");\n const result = await status.tick({ force });\n if (!result.ran) return json(request, result);\n // A scheduled run also counts for the public rate limit\n await status.lock(RUN_ALL_LOCK, runAllMs);\n return json(request, {\n ran: true,\n at: result.report.at,\n score: result.report.score,\n label: result.report.label,\n statuses: Object.fromEntries(result.report.results.map((r) => [r.key, r.status])),\n });\n }\n\n return json(request, { error: \"Not found\" }, { status: 404 });\n } catch (error) {\n // Never log the raw error: driver and store errors can carry connection strings\n const message = status.redact(describeError(error));\n if (options.onError) options.onError({ message, error });\n else console.error(`[sermon-js] ${message}`);\n return json(request, { error: \"Internal error\" }, { status: 500 });\n }\n };\n}\n\ntype NodeRequest = {\n method?: string;\n url?: string;\n headers: Record<string, string | string[] | undefined>;\n [Symbol.asyncIterator](): AsyncIterator<Uint8Array>;\n};\n\ntype NodeResponse = {\n statusCode: number;\n setHeader(name: string, value: string): unknown;\n end(body?: string): unknown;\n};\n\n/**\n * Adapt the handler to Node's `http.createServer` / Express / Connect:\n *\n * ```ts\n * http.createServer(toNodeListener(createStatusHandler(status))).listen(3000);\n * ```\n */\nexport function toNodeListener(handler: StatusHandler) {\n return async function listener(req: NodeRequest, res: NodeResponse) {\n try {\n const host = (Array.isArray(req.headers.host) ? req.headers.host[0] : req.headers.host) ?? \"localhost\";\n const headers = new Headers();\n for (const [name, value] of Object.entries(req.headers)) {\n if (value !== undefined) headers.set(name, Array.isArray(value) ? value.join(\", \") : value);\n }\n const method = (req.method ?? \"GET\").toUpperCase();\n let body: string | undefined;\n if (method !== \"GET\" && method !== \"HEAD\") {\n const decoder = new TextDecoder();\n body = \"\";\n for await (const chunk of req) body += decoder.decode(chunk, { stream: true });\n body += decoder.decode();\n }\n const response = await handler(\n new Request(`http://${host}${req.url ?? \"/\"}`, { method, headers, body: body || undefined })\n );\n res.statusCode = response.status;\n response.headers.forEach((value, name) => res.setHeader(name, value));\n res.end(await response.text());\n } catch (error) {\n // The handler reports its own failures; this only covers a broken request or response\n console.error(`[sermon-js] ${error instanceof Error ? error.name : \"Error\"} while serving a request`);\n res.statusCode = 500;\n res.end('{\"error\":\"Internal error\"}');\n }\n };\n}\n"]}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { D as Duration, S as Status } from '../status-DsWzgb9Q.cjs';
|
|
2
|
+
import '../config-BEbP-uLf.cjs';
|
|
3
|
+
import '../types-CctvUAs2.cjs';
|
|
4
|
+
import '../store-DybhDphS.cjs';
|
|
5
|
+
|
|
6
|
+
type HandlerContext = {
|
|
7
|
+
/** Keeps the platform alive for background work after the response (Workers, Vercel, Netlify…) */
|
|
8
|
+
waitUntil?: (promise: Promise<unknown>) => void;
|
|
9
|
+
};
|
|
10
|
+
type StatusHandlerOptions = {
|
|
11
|
+
/** Where the routes are mounted (default "/api"). The handler ignores anything before it. */
|
|
12
|
+
basePath?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Protects `/tick`. When set, callers must send it as `Authorization: Bearer <secret>`,
|
|
15
|
+
* `x-sermon-secret: <secret>` or `?key=<secret>`. Without a secret, `/tick` is public but
|
|
16
|
+
* harmless: it only runs when a run is due.
|
|
17
|
+
*/
|
|
18
|
+
secret?: string;
|
|
19
|
+
/** Minimum time between public on-demand runs */
|
|
20
|
+
rateLimits?: {
|
|
21
|
+
/** Full runs (default "60s") */
|
|
22
|
+
runAll?: Duration;
|
|
23
|
+
/** Single-service runs, per service (default "20s") */
|
|
24
|
+
runOne?: Duration;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* When `/status` is read and the data is stale, start a `tick()`. With a `waitUntil` the
|
|
28
|
+
* response isn't delayed; without one the handler waits for the run (up to `refreshBudget`).
|
|
29
|
+
* Keeps a page fresh without any scheduler, as long as someone visits it.
|
|
30
|
+
*/
|
|
31
|
+
refreshOnRead?: boolean;
|
|
32
|
+
/** How long a read may wait for a refresh when there is no `waitUntil` (default "8s") */
|
|
33
|
+
refreshBudget?: Duration;
|
|
34
|
+
waitUntil?: HandlerContext["waitUntil"];
|
|
35
|
+
/**
|
|
36
|
+
* Shared-cache lifetimes in seconds for CDNs and edges (default `{ status: 10, history: 30 }`).
|
|
37
|
+
* Browsers are always told to revalidate. Pass `false` to disable shared caching.
|
|
38
|
+
*/
|
|
39
|
+
cache?: {
|
|
40
|
+
status?: number;
|
|
41
|
+
history?: number;
|
|
42
|
+
} | false;
|
|
43
|
+
/** Allow other origins to call the API: `true` for any, or one origin (default: same-origin only) */
|
|
44
|
+
cors?: boolean | string;
|
|
45
|
+
/**
|
|
46
|
+
* Receives unexpected failures (e.g. the store is unreachable). `message` has every configured
|
|
47
|
+
* secret and credential-shaped value removed; `error` is the raw value, which may contain
|
|
48
|
+
* connection strings: don't print it. Default: `console.error` of `message` only.
|
|
49
|
+
*/
|
|
50
|
+
onError?: (failure: {
|
|
51
|
+
message: string;
|
|
52
|
+
error: unknown;
|
|
53
|
+
}) => void;
|
|
54
|
+
};
|
|
55
|
+
type StatusHandler = (request: Request, context?: HandlerContext) => Promise<Response>;
|
|
56
|
+
declare function createStatusHandler(status: Status, options?: StatusHandlerOptions): StatusHandler;
|
|
57
|
+
type NodeRequest = {
|
|
58
|
+
method?: string;
|
|
59
|
+
url?: string;
|
|
60
|
+
headers: Record<string, string | string[] | undefined>;
|
|
61
|
+
[Symbol.asyncIterator](): AsyncIterator<Uint8Array>;
|
|
62
|
+
};
|
|
63
|
+
type NodeResponse = {
|
|
64
|
+
statusCode: number;
|
|
65
|
+
setHeader(name: string, value: string): unknown;
|
|
66
|
+
end(body?: string): unknown;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Adapt the handler to Node's `http.createServer` / Express / Connect:
|
|
70
|
+
*
|
|
71
|
+
* ```ts
|
|
72
|
+
* http.createServer(toNodeListener(createStatusHandler(status))).listen(3000);
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
declare function toNodeListener(handler: StatusHandler): (req: NodeRequest, res: NodeResponse) => Promise<void>;
|
|
76
|
+
|
|
77
|
+
export { type HandlerContext, type StatusHandler, type StatusHandlerOptions, createStatusHandler, toNodeListener };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { D as Duration, S as Status } from '../status-zd04ZQ98.js';
|
|
2
|
+
import '../config-w1tm1bdu.js';
|
|
3
|
+
import '../types-CctvUAs2.js';
|
|
4
|
+
import '../store-_GuzSP0z.js';
|
|
5
|
+
|
|
6
|
+
type HandlerContext = {
|
|
7
|
+
/** Keeps the platform alive for background work after the response (Workers, Vercel, Netlify…) */
|
|
8
|
+
waitUntil?: (promise: Promise<unknown>) => void;
|
|
9
|
+
};
|
|
10
|
+
type StatusHandlerOptions = {
|
|
11
|
+
/** Where the routes are mounted (default "/api"). The handler ignores anything before it. */
|
|
12
|
+
basePath?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Protects `/tick`. When set, callers must send it as `Authorization: Bearer <secret>`,
|
|
15
|
+
* `x-sermon-secret: <secret>` or `?key=<secret>`. Without a secret, `/tick` is public but
|
|
16
|
+
* harmless: it only runs when a run is due.
|
|
17
|
+
*/
|
|
18
|
+
secret?: string;
|
|
19
|
+
/** Minimum time between public on-demand runs */
|
|
20
|
+
rateLimits?: {
|
|
21
|
+
/** Full runs (default "60s") */
|
|
22
|
+
runAll?: Duration;
|
|
23
|
+
/** Single-service runs, per service (default "20s") */
|
|
24
|
+
runOne?: Duration;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* When `/status` is read and the data is stale, start a `tick()`. With a `waitUntil` the
|
|
28
|
+
* response isn't delayed; without one the handler waits for the run (up to `refreshBudget`).
|
|
29
|
+
* Keeps a page fresh without any scheduler, as long as someone visits it.
|
|
30
|
+
*/
|
|
31
|
+
refreshOnRead?: boolean;
|
|
32
|
+
/** How long a read may wait for a refresh when there is no `waitUntil` (default "8s") */
|
|
33
|
+
refreshBudget?: Duration;
|
|
34
|
+
waitUntil?: HandlerContext["waitUntil"];
|
|
35
|
+
/**
|
|
36
|
+
* Shared-cache lifetimes in seconds for CDNs and edges (default `{ status: 10, history: 30 }`).
|
|
37
|
+
* Browsers are always told to revalidate. Pass `false` to disable shared caching.
|
|
38
|
+
*/
|
|
39
|
+
cache?: {
|
|
40
|
+
status?: number;
|
|
41
|
+
history?: number;
|
|
42
|
+
} | false;
|
|
43
|
+
/** Allow other origins to call the API: `true` for any, or one origin (default: same-origin only) */
|
|
44
|
+
cors?: boolean | string;
|
|
45
|
+
/**
|
|
46
|
+
* Receives unexpected failures (e.g. the store is unreachable). `message` has every configured
|
|
47
|
+
* secret and credential-shaped value removed; `error` is the raw value, which may contain
|
|
48
|
+
* connection strings: don't print it. Default: `console.error` of `message` only.
|
|
49
|
+
*/
|
|
50
|
+
onError?: (failure: {
|
|
51
|
+
message: string;
|
|
52
|
+
error: unknown;
|
|
53
|
+
}) => void;
|
|
54
|
+
};
|
|
55
|
+
type StatusHandler = (request: Request, context?: HandlerContext) => Promise<Response>;
|
|
56
|
+
declare function createStatusHandler(status: Status, options?: StatusHandlerOptions): StatusHandler;
|
|
57
|
+
type NodeRequest = {
|
|
58
|
+
method?: string;
|
|
59
|
+
url?: string;
|
|
60
|
+
headers: Record<string, string | string[] | undefined>;
|
|
61
|
+
[Symbol.asyncIterator](): AsyncIterator<Uint8Array>;
|
|
62
|
+
};
|
|
63
|
+
type NodeResponse = {
|
|
64
|
+
statusCode: number;
|
|
65
|
+
setHeader(name: string, value: string): unknown;
|
|
66
|
+
end(body?: string): unknown;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Adapt the handler to Node's `http.createServer` / Express / Connect:
|
|
70
|
+
*
|
|
71
|
+
* ```ts
|
|
72
|
+
* http.createServer(toNodeListener(createStatusHandler(status))).listen(3000);
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
declare function toNodeListener(handler: StatusHandler): (req: NodeRequest, res: NodeResponse) => Promise<void>;
|
|
76
|
+
|
|
77
|
+
export { type HandlerContext, type StatusHandler, type StatusHandlerOptions, createStatusHandler, toNodeListener };
|