sermon-js 0.0.0-stage → 0.2.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.
Files changed (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +239 -2
  3. package/dist/api/index.cjs +174 -0
  4. package/dist/api/index.cjs.map +1 -0
  5. package/dist/api/index.d.cts +77 -0
  6. package/dist/api/index.d.ts +77 -0
  7. package/dist/api/index.js +171 -0
  8. package/dist/api/index.js.map +1 -0
  9. package/dist/chunk-2M4UBCDM.js +178 -0
  10. package/dist/chunk-2M4UBCDM.js.map +1 -0
  11. package/dist/chunk-AH372HLN.js +98 -0
  12. package/dist/chunk-AH372HLN.js.map +1 -0
  13. package/dist/chunk-APC2HVSI.js +154 -0
  14. package/dist/chunk-APC2HVSI.js.map +1 -0
  15. package/dist/chunk-D7BBJTYU.cjs +335 -0
  16. package/dist/chunk-D7BBJTYU.cjs.map +1 -0
  17. package/dist/chunk-ELV4ZU6G.cjs +51 -0
  18. package/dist/chunk-ELV4ZU6G.cjs.map +1 -0
  19. package/dist/chunk-EMP7XVKC.js +334 -0
  20. package/dist/chunk-EMP7XVKC.js.map +1 -0
  21. package/dist/chunk-I3346MUL.js +49 -0
  22. package/dist/chunk-I3346MUL.js.map +1 -0
  23. package/dist/chunk-NP3YYVRZ.cjs +163 -0
  24. package/dist/chunk-NP3YYVRZ.cjs.map +1 -0
  25. package/dist/chunk-PARE6EUZ.cjs +184 -0
  26. package/dist/chunk-PARE6EUZ.cjs.map +1 -0
  27. package/dist/chunk-XHXEKB3Q.cjs +241 -0
  28. package/dist/chunk-XHXEKB3Q.cjs.map +1 -0
  29. package/dist/chunk-XPXIDJUU.js +229 -0
  30. package/dist/chunk-XPXIDJUU.js.map +1 -0
  31. package/dist/chunk-ZI442ZA5.cjs +100 -0
  32. package/dist/chunk-ZI442ZA5.cjs.map +1 -0
  33. package/dist/config-BEbP-uLf.d.cts +125 -0
  34. package/dist/config-w1tm1bdu.d.ts +125 -0
  35. package/dist/generic-BJmlgtUM.d.ts +31 -0
  36. package/dist/generic-lmrGxMDC.d.cts +31 -0
  37. package/dist/index.cjs +407 -0
  38. package/dist/index.cjs.map +1 -0
  39. package/dist/index.d.cts +93 -0
  40. package/dist/index.d.ts +93 -0
  41. package/dist/index.js +330 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/presets/index.cjs +1012 -0
  44. package/dist/presets/index.cjs.map +1 -0
  45. package/dist/presets/index.d.cts +516 -0
  46. package/dist/presets/index.d.ts +516 -0
  47. package/dist/presets/index.js +971 -0
  48. package/dist/presets/index.js.map +1 -0
  49. package/dist/react/heroui/index.cjs +822 -0
  50. package/dist/react/heroui/index.cjs.map +1 -0
  51. package/dist/react/heroui/index.d.cts +122 -0
  52. package/dist/react/heroui/index.d.ts +122 -0
  53. package/dist/react/heroui/index.js +821 -0
  54. package/dist/react/heroui/index.js.map +1 -0
  55. package/dist/react/heroui/styles.css +11 -0
  56. package/dist/react/index.cjs +71 -0
  57. package/dist/react/index.cjs.map +1 -0
  58. package/dist/react/index.d.cts +53 -0
  59. package/dist/react/index.d.ts +53 -0
  60. package/dist/react/index.js +70 -0
  61. package/dist/react/index.js.map +1 -0
  62. package/dist/status-DsWzgb9Q.d.cts +146 -0
  63. package/dist/status-zd04ZQ98.d.ts +146 -0
  64. package/dist/store-DybhDphS.d.cts +77 -0
  65. package/dist/store-_GuzSP0z.d.ts +77 -0
  66. package/dist/stores/mongodb.cjs +99 -0
  67. package/dist/stores/mongodb.cjs.map +1 -0
  68. package/dist/stores/mongodb.d.cts +40 -0
  69. package/dist/stores/mongodb.d.ts +40 -0
  70. package/dist/stores/mongodb.js +97 -0
  71. package/dist/stores/mongodb.js.map +1 -0
  72. package/dist/stores/redis.cjs +23 -0
  73. package/dist/stores/redis.cjs.map +1 -0
  74. package/dist/stores/redis.d.cts +31 -0
  75. package/dist/stores/redis.d.ts +31 -0
  76. package/dist/stores/redis.js +21 -0
  77. package/dist/stores/redis.js.map +1 -0
  78. package/dist/stores/sql.cjs +164 -0
  79. package/dist/stores/sql.cjs.map +1 -0
  80. package/dist/stores/sql.d.cts +39 -0
  81. package/dist/stores/sql.d.ts +39 -0
  82. package/dist/stores/sql.js +162 -0
  83. package/dist/stores/sql.js.map +1 -0
  84. package/dist/stores/upstash.cjs +27 -0
  85. package/dist/stores/upstash.cjs.map +1 -0
  86. package/dist/stores/upstash.d.cts +19 -0
  87. package/dist/stores/upstash.d.ts +19 -0
  88. package/dist/stores/upstash.js +25 -0
  89. package/dist/stores/upstash.js.map +1 -0
  90. package/dist/testing/index.cjs +209 -0
  91. package/dist/testing/index.cjs.map +1 -0
  92. package/dist/testing/index.d.cts +66 -0
  93. package/dist/testing/index.d.ts +66 -0
  94. package/dist/testing/index.js +205 -0
  95. package/dist/testing/index.js.map +1 -0
  96. package/dist/tree-BXbXfjF1.d.cts +236 -0
  97. package/dist/tree-BXbXfjF1.d.ts +236 -0
  98. package/dist/types-CctvUAs2.d.cts +192 -0
  99. package/dist/types-CctvUAs2.d.ts +192 -0
  100. 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
- # Temporary Holding Version
1
+ # sermon-js
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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/search providers, Vercel, MongoDB, Redis, PostgreSQL
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 };