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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { toMs, RANGES } from '../chunk-APC2HVSI.js';
|
|
2
|
+
|
|
3
|
+
// src/api/index.ts
|
|
4
|
+
var RUN_ALL_LOCK = "sermon:run:all";
|
|
5
|
+
function parseRange(value) {
|
|
6
|
+
return value && value in RANGES ? value : "24h";
|
|
7
|
+
}
|
|
8
|
+
function safeEqual(a, b) {
|
|
9
|
+
if (a.length !== b.length) return false;
|
|
10
|
+
let diff = 0;
|
|
11
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
12
|
+
return diff === 0;
|
|
13
|
+
}
|
|
14
|
+
function presentedSecret(request, url) {
|
|
15
|
+
const authorization = request.headers.get("authorization");
|
|
16
|
+
if (authorization?.toLowerCase().startsWith("bearer ")) return authorization.slice(7).trim();
|
|
17
|
+
return request.headers.get("x-sermon-secret") ?? url.searchParams.get("key") ?? void 0;
|
|
18
|
+
}
|
|
19
|
+
function describeError(error) {
|
|
20
|
+
if (!(error instanceof Error)) return String(error);
|
|
21
|
+
const parts = [error.stack ?? `${error.name}: ${error.message}`];
|
|
22
|
+
let cause = error.cause;
|
|
23
|
+
for (let depth = 0; cause && depth < 3; depth++) {
|
|
24
|
+
parts.push(`Caused by: ${cause instanceof Error ? `${cause.name}: ${cause.message}` : String(cause)}`);
|
|
25
|
+
cause = cause instanceof Error ? cause.cause : void 0;
|
|
26
|
+
}
|
|
27
|
+
return parts.join("\n");
|
|
28
|
+
}
|
|
29
|
+
function createStatusHandler(status, options = {}) {
|
|
30
|
+
const basePath = `/${(options.basePath ?? "/api").replace(/^\/+|\/+$/g, "")}`.replace(/^\/$/, "");
|
|
31
|
+
const runAllMs = toMs(options.rateLimits?.runAll ?? "60s");
|
|
32
|
+
const runOneMs = toMs(options.rateLimits?.runOne ?? "20s");
|
|
33
|
+
const refreshBudgetMs = toMs(options.refreshBudget ?? "8s");
|
|
34
|
+
const cache = options.cache === false ? void 0 : { status: 10, history: 30, ...options.cache };
|
|
35
|
+
const corsHeaders = (request) => {
|
|
36
|
+
if (!options.cors) return {};
|
|
37
|
+
const origin = options.cors === true ? request.headers.get("origin") ?? "*" : options.cors;
|
|
38
|
+
return {
|
|
39
|
+
"access-control-allow-origin": origin,
|
|
40
|
+
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
41
|
+
"access-control-allow-headers": "authorization, content-type, x-sermon-secret",
|
|
42
|
+
vary: "origin"
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
const json = (request, body, init = {}) => new Response(JSON.stringify(body), {
|
|
46
|
+
status: init.status ?? 200,
|
|
47
|
+
headers: {
|
|
48
|
+
"content-type": "application/json; charset=utf-8",
|
|
49
|
+
"cache-control": "no-store",
|
|
50
|
+
...corsHeaders(request),
|
|
51
|
+
...init.headers
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
const cached = (seconds) => seconds ? {
|
|
55
|
+
"cache-control": "no-cache",
|
|
56
|
+
"cdn-cache-control": `max-age=${seconds}, stale-while-revalidate=${seconds * 3}`
|
|
57
|
+
} : {};
|
|
58
|
+
const tooSoon = (request, message, retryAfterMs) => {
|
|
59
|
+
const retryAfter = Math.max(1, Math.ceil(retryAfterMs / 1e3));
|
|
60
|
+
return json(request, { error: message, retryAfter }, { status: 429, headers: { "retry-after": String(retryAfter) } });
|
|
61
|
+
};
|
|
62
|
+
async function refreshIfStale(context) {
|
|
63
|
+
const last = await status.lastFullRun();
|
|
64
|
+
if (last && Date.now() < last.at + status.intervalMs) return;
|
|
65
|
+
const tick = status.tick().catch(() => void 0);
|
|
66
|
+
const waitUntil = context?.waitUntil ?? options.waitUntil;
|
|
67
|
+
if (waitUntil) {
|
|
68
|
+
waitUntil(tick);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
await Promise.race([tick, new Promise((resolve) => setTimeout(resolve, refreshBudgetMs))]);
|
|
72
|
+
}
|
|
73
|
+
return async function handle(request, context) {
|
|
74
|
+
const url = new URL(request.url);
|
|
75
|
+
const method = request.method.toUpperCase();
|
|
76
|
+
if (basePath && url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) {
|
|
77
|
+
return json(request, { error: "Not found" }, { status: 404 });
|
|
78
|
+
}
|
|
79
|
+
if (method === "OPTIONS") return new Response(null, { status: 204, headers: corsHeaders(request) });
|
|
80
|
+
let segments;
|
|
81
|
+
try {
|
|
82
|
+
segments = url.pathname.slice(basePath.length).split("/").filter(Boolean).map((segment) => decodeURIComponent(segment));
|
|
83
|
+
} catch {
|
|
84
|
+
return json(request, { error: "Not found" }, { status: 404 });
|
|
85
|
+
}
|
|
86
|
+
const [first, second, third] = segments;
|
|
87
|
+
try {
|
|
88
|
+
if (method === "GET" && first === "status" && segments.length === 1) {
|
|
89
|
+
if (options.refreshOnRead) await refreshIfStale(context);
|
|
90
|
+
return json(request, await status.snapshot(), { headers: cached(cache?.status) });
|
|
91
|
+
}
|
|
92
|
+
if (method === "GET" && first === "history" && segments.length === 1) {
|
|
93
|
+
const range = parseRange(url.searchParams.get("range"));
|
|
94
|
+
return json(request, await status.history({ range }), { headers: cached(cache?.history) });
|
|
95
|
+
}
|
|
96
|
+
if (method === "GET" && first === "services" && second && third === "history" && segments.length === 3) {
|
|
97
|
+
if (!status.has(second)) return json(request, { error: "Unknown service" }, { status: 404 });
|
|
98
|
+
const range = parseRange(url.searchParams.get("range"));
|
|
99
|
+
return json(request, await status.serviceHistory(second, { range }), { headers: cached(cache?.history) });
|
|
100
|
+
}
|
|
101
|
+
if (method === "POST" && first === "run" && segments.length === 1) {
|
|
102
|
+
const lock = await status.lock(RUN_ALL_LOCK, runAllMs);
|
|
103
|
+
if (!lock.ok) return tooSoon(request, "A full run happened recently", lock.retryAfterMs);
|
|
104
|
+
await status.run({ trigger: "manual" });
|
|
105
|
+
return json(request, await status.snapshot());
|
|
106
|
+
}
|
|
107
|
+
if (method === "POST" && first === "run" && second && segments.length === 2) {
|
|
108
|
+
if (!status.has(second)) return json(request, { error: "Unknown service" }, { status: 404 });
|
|
109
|
+
const lock = await status.lock(`sermon:run:${second}`, runOneMs);
|
|
110
|
+
if (!lock.ok) return tooSoon(request, "This service was checked recently", lock.retryAfterMs);
|
|
111
|
+
await status.run({ keys: [second], trigger: "manual" });
|
|
112
|
+
return json(request, await status.snapshot());
|
|
113
|
+
}
|
|
114
|
+
if ((method === "GET" || method === "POST") && (first === "tick" || first === "cron") && segments.length === 1) {
|
|
115
|
+
const presented = presentedSecret(request, url);
|
|
116
|
+
const authorized = !!options.secret && !!presented && safeEqual(presented, options.secret);
|
|
117
|
+
if (options.secret && !authorized) return json(request, { error: "Unauthorized" }, { status: 401 });
|
|
118
|
+
const force = authorized && ["1", "true"].includes(url.searchParams.get("force") ?? "");
|
|
119
|
+
const result = await status.tick({ force });
|
|
120
|
+
if (!result.ran) return json(request, result);
|
|
121
|
+
await status.lock(RUN_ALL_LOCK, runAllMs);
|
|
122
|
+
return json(request, {
|
|
123
|
+
ran: true,
|
|
124
|
+
at: result.report.at,
|
|
125
|
+
score: result.report.score,
|
|
126
|
+
label: result.report.label,
|
|
127
|
+
statuses: Object.fromEntries(result.report.results.map((r) => [r.key, r.status]))
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return json(request, { error: "Not found" }, { status: 404 });
|
|
131
|
+
} catch (error) {
|
|
132
|
+
const message = status.redact(describeError(error));
|
|
133
|
+
if (options.onError) options.onError({ message, error });
|
|
134
|
+
else console.error(`[sermon-js] ${message}`);
|
|
135
|
+
return json(request, { error: "Internal error" }, { status: 500 });
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function toNodeListener(handler) {
|
|
140
|
+
return async function listener(req, res) {
|
|
141
|
+
try {
|
|
142
|
+
const host = (Array.isArray(req.headers.host) ? req.headers.host[0] : req.headers.host) ?? "localhost";
|
|
143
|
+
const headers = new Headers();
|
|
144
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
145
|
+
if (value !== void 0) headers.set(name, Array.isArray(value) ? value.join(", ") : value);
|
|
146
|
+
}
|
|
147
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
148
|
+
let body;
|
|
149
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
150
|
+
const decoder = new TextDecoder();
|
|
151
|
+
body = "";
|
|
152
|
+
for await (const chunk of req) body += decoder.decode(chunk, { stream: true });
|
|
153
|
+
body += decoder.decode();
|
|
154
|
+
}
|
|
155
|
+
const response = await handler(
|
|
156
|
+
new Request(`http://${host}${req.url ?? "/"}`, { method, headers, body: body || void 0 })
|
|
157
|
+
);
|
|
158
|
+
res.statusCode = response.status;
|
|
159
|
+
response.headers.forEach((value, name) => res.setHeader(name, value));
|
|
160
|
+
res.end(await response.text());
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error(`[sermon-js] ${error instanceof Error ? error.name : "Error"} while serving a request`);
|
|
163
|
+
res.statusCode = 500;
|
|
164
|
+
res.end('{"error":"Internal error"}');
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export { createStatusHandler, toNodeListener };
|
|
170
|
+
//# sourceMappingURL=index.js.map
|
|
171
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/api/index.ts"],"names":[],"mappings":"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","file":"index.js","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,178 @@
|
|
|
1
|
+
import { createHttp, combine, measure, normalizeReturn, errorMessage } from './chunk-XPXIDJUU.js';
|
|
2
|
+
|
|
3
|
+
// src/sanitize.ts
|
|
4
|
+
var MAX_LENGTH = 300;
|
|
5
|
+
var MIN_SECRET_LENGTH = 6;
|
|
6
|
+
var KEY_PATTERNS = [
|
|
7
|
+
// PEM blocks (private keys, certificates), whole or cut short
|
|
8
|
+
/-----BEGIN [A-Z0-9 ]+-----[\s\S]*?(-----END [A-Z0-9 ]+-----|$)/g,
|
|
9
|
+
// JSON Web Tokens (OAuth access and identity tokens)
|
|
10
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}(\.[A-Za-z0-9_-]*)?/g,
|
|
11
|
+
// Prefixed API keys: OpenAI/Anthropic/Stripe (sk-, pk_, rk_), GitHub, Slack, Fly, MailerSend.
|
|
12
|
+
// "*" is included because providers echo keys back partly masked (sk-proj-****abcd).
|
|
13
|
+
/\b(sk|pk|rk)[-_][A-Za-z0-9_*.-]{12,}/g,
|
|
14
|
+
/\b(gh[pousr]_|github_pat_)[A-Za-z0-9_*]{12,}/g,
|
|
15
|
+
/\bxox[abeoprs]-[A-Za-z0-9*-]{10,}/g,
|
|
16
|
+
/\b(FlyV1 |fm[12]_|fo1_)[A-Za-z0-9+/=_,*-]{12,}/g,
|
|
17
|
+
/\bmlsn\.[A-Za-z0-9*]{12,}/g,
|
|
18
|
+
// AWS access key ids, Google API keys, Google OAuth tokens
|
|
19
|
+
/\b(AKIA|ASIA)[0-9A-Z*]{12,}/g,
|
|
20
|
+
/\bAIza[0-9A-Za-z_*-]{20,}/g,
|
|
21
|
+
/\bya29\.[0-9A-Za-z_*.-]{12,}/g
|
|
22
|
+
];
|
|
23
|
+
function collectSecretValues(secrets) {
|
|
24
|
+
const values = /* @__PURE__ */ new Set();
|
|
25
|
+
const addOne = (value) => {
|
|
26
|
+
if (value.length >= MIN_SECRET_LENGTH) values.add(value);
|
|
27
|
+
};
|
|
28
|
+
const add = (value) => {
|
|
29
|
+
if (typeof value !== "string" || value.length < MIN_SECRET_LENGTH) return;
|
|
30
|
+
addOne(value);
|
|
31
|
+
try {
|
|
32
|
+
addOne(encodeURIComponent(value));
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
35
|
+
addOne(JSON.stringify(value).slice(1, -1));
|
|
36
|
+
try {
|
|
37
|
+
addOne(btoa(value).replace(/=+$/, ""));
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const walk = (value) => {
|
|
42
|
+
if (typeof value === "string") add(value);
|
|
43
|
+
else if (Array.isArray(value)) value.forEach(walk);
|
|
44
|
+
else if (value && typeof value === "object") Object.values(value).forEach(walk);
|
|
45
|
+
};
|
|
46
|
+
for (const secret of secrets) {
|
|
47
|
+
if (!secret) continue;
|
|
48
|
+
add(secret);
|
|
49
|
+
if (secret.trimStart().startsWith("{")) {
|
|
50
|
+
try {
|
|
51
|
+
walk(JSON.parse(secret));
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const credentials = /^[a-z][a-z0-9+.-]*:\/\/([^\s/@:]*):([^\s/@]*)@/i.exec(secret);
|
|
56
|
+
for (const part of credentials?.slice(1) ?? []) {
|
|
57
|
+
add(part);
|
|
58
|
+
try {
|
|
59
|
+
add(decodeURIComponent(part));
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return [...values].sort((a, b) => b.length - a.length);
|
|
65
|
+
}
|
|
66
|
+
function redactText({ text, secretValues }) {
|
|
67
|
+
let out = text;
|
|
68
|
+
for (const secret of secretValues) out = out.split(secret).join("***");
|
|
69
|
+
out = out.replace(/(\b[a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, "$1***@").replace(/(\b(?:https?|wss?):\/\/[^\s?]+)\?[^\s"')]*/gi, "$1?***").replace(/\b(Bearer|Token|Basic|DeepL-Auth-Key)\s+[A-Za-z0-9._~+/=*-]{8,}/gi, "$1 ***");
|
|
70
|
+
for (const pattern of KEY_PATTERNS) out = out.replace(pattern, "***");
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
function sanitizeError({
|
|
74
|
+
message,
|
|
75
|
+
secretValues
|
|
76
|
+
}) {
|
|
77
|
+
if (!message) return void 0;
|
|
78
|
+
const out = redactText({ text: message, secretValues });
|
|
79
|
+
return out.length > MAX_LENGTH ? `${out.slice(0, MAX_LENGTH)}\u2026` : out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/runner.ts
|
|
83
|
+
var GRACE_MS = 1e3;
|
|
84
|
+
async function runService({
|
|
85
|
+
service,
|
|
86
|
+
secretValues,
|
|
87
|
+
cache,
|
|
88
|
+
now = () => Date.now(),
|
|
89
|
+
fetch: fetchImpl,
|
|
90
|
+
hideResponseBodies
|
|
91
|
+
}) {
|
|
92
|
+
const checkedAt = now();
|
|
93
|
+
if (service.missing.length > 0) {
|
|
94
|
+
return { key: service.key, status: "unconfigured", checkedAt, probed: false };
|
|
95
|
+
}
|
|
96
|
+
const redact = (text) => redactText({ text, secretValues });
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
const ctx = {
|
|
99
|
+
secrets: service.secrets,
|
|
100
|
+
http: createHttp({ timeoutMs: service.timeoutMs, signal: controller.signal, fetch: fetchImpl, redact, hideResponseBodies }),
|
|
101
|
+
measure,
|
|
102
|
+
combine,
|
|
103
|
+
signal: controller.signal,
|
|
104
|
+
timeoutMs: service.timeoutMs,
|
|
105
|
+
slowMs: service.slowMs,
|
|
106
|
+
cache,
|
|
107
|
+
now: checkedAt
|
|
108
|
+
};
|
|
109
|
+
const start = performance.now();
|
|
110
|
+
let outcome;
|
|
111
|
+
let timer;
|
|
112
|
+
try {
|
|
113
|
+
const returned = await Promise.race([
|
|
114
|
+
Promise.resolve().then(() => service.check(ctx)),
|
|
115
|
+
new Promise((_, reject) => {
|
|
116
|
+
timer = setTimeout(() => {
|
|
117
|
+
controller.abort(new DOMException("Timed out", "TimeoutError"));
|
|
118
|
+
reject(new DOMException("Timed out", "TimeoutError"));
|
|
119
|
+
}, service.timeoutMs + GRACE_MS);
|
|
120
|
+
})
|
|
121
|
+
]);
|
|
122
|
+
outcome = normalizeReturn(returned);
|
|
123
|
+
if (outcome.latencyMs === void 0 && outcome.probed !== false && outcome.status !== "sleeping") {
|
|
124
|
+
outcome = { ...outcome, latencyMs: Math.round(performance.now() - start) };
|
|
125
|
+
}
|
|
126
|
+
} catch (error2) {
|
|
127
|
+
outcome = { status: "down", error: errorMessage(error2) };
|
|
128
|
+
} finally {
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
}
|
|
131
|
+
let { status, error } = outcome;
|
|
132
|
+
if (status === "up" && outcome.latencyMs !== void 0 && outcome.latencyMs > service.slowMs) {
|
|
133
|
+
status = "degraded";
|
|
134
|
+
error ??= `Slow response: ${outcome.latencyMs} ms (threshold ${service.slowMs} ms)`;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
key: service.key,
|
|
138
|
+
status,
|
|
139
|
+
checkedAt,
|
|
140
|
+
probed: outcome.probed ?? true,
|
|
141
|
+
latencyMs: outcome.latencyMs,
|
|
142
|
+
httpStatus: outcome.httpStatus,
|
|
143
|
+
error: sanitizeError({ message: error, secretValues }),
|
|
144
|
+
// Everything below is public too: no text leaves the runner without being redacted
|
|
145
|
+
metrics: outcome.metrics ? Object.fromEntries(
|
|
146
|
+
Object.entries(outcome.metrics).map(([name, value]) => [
|
|
147
|
+
redact(name),
|
|
148
|
+
typeof value === "string" ? redact(value) : value
|
|
149
|
+
])
|
|
150
|
+
) : void 0,
|
|
151
|
+
usage: outcome.usage?.length ? outcome.usage.map((usage) => ({
|
|
152
|
+
...usage,
|
|
153
|
+
label: redact(usage.label),
|
|
154
|
+
unit: usage.unit === void 0 ? void 0 : redact(usage.unit)
|
|
155
|
+
})) : void 0,
|
|
156
|
+
instances: outcome.instances?.length ? outcome.instances.map((instance) => ({
|
|
157
|
+
...instance,
|
|
158
|
+
key: redact(instance.key),
|
|
159
|
+
region: instance.region === void 0 ? void 0 : redact(instance.region),
|
|
160
|
+
id: instance.id === void 0 ? void 0 : redact(instance.id),
|
|
161
|
+
state: instance.state === void 0 ? void 0 : redact(instance.state),
|
|
162
|
+
error: sanitizeError({ message: instance.error, secretValues })
|
|
163
|
+
})) : void 0
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
async function runServices({
|
|
167
|
+
services,
|
|
168
|
+
cacheFor,
|
|
169
|
+
...rest
|
|
170
|
+
}) {
|
|
171
|
+
return Promise.all(
|
|
172
|
+
services.map((service) => runService({ service, cache: cacheFor(service.key), ...rest }))
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export { collectSecretValues, redactText, runService, runServices, sanitizeError };
|
|
177
|
+
//# sourceMappingURL=chunk-2M4UBCDM.js.map
|
|
178
|
+
//# sourceMappingURL=chunk-2M4UBCDM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sanitize.ts","../src/runner.ts"],"names":["error"],"mappings":"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","file":"chunk-2M4UBCDM.js","sourcesContent":["const MAX_LENGTH = 300;\nconst MIN_SECRET_LENGTH = 6;\n\n/** Shapes of well-known credentials, redacted even when the value was never declared as a secret */\nconst KEY_PATTERNS: RegExp[] = [\n // PEM blocks (private keys, certificates), whole or cut short\n /-----BEGIN [A-Z0-9 ]+-----[\\s\\S]*?(-----END [A-Z0-9 ]+-----|$)/g,\n // JSON Web Tokens (OAuth access and identity tokens)\n /\\beyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}(\\.[A-Za-z0-9_-]*)?/g,\n // Prefixed API keys: OpenAI/Anthropic/Stripe (sk-, pk_, rk_), GitHub, Slack, Fly, MailerSend.\n // \"*\" is included because providers echo keys back partly masked (sk-proj-****abcd).\n /\\b(sk|pk|rk)[-_][A-Za-z0-9_*.-]{12,}/g,\n /\\b(gh[pousr]_|github_pat_)[A-Za-z0-9_*]{12,}/g,\n /\\bxox[abeoprs]-[A-Za-z0-9*-]{10,}/g,\n /\\b(FlyV1 |fm[12]_|fo1_)[A-Za-z0-9+/=_,*-]{12,}/g,\n /\\bmlsn\\.[A-Za-z0-9*]{12,}/g,\n // AWS access key ids, Google API keys, Google OAuth tokens\n /\\b(AKIA|ASIA)[0-9A-Z*]{12,}/g,\n /\\bAIza[0-9A-Za-z_*-]{20,}/g,\n /\\bya29\\.[0-9A-Za-z_*.-]{12,}/g,\n];\n\n/**\n * Every value worth redacting from a set of secrets: the values themselves, the string fields\n * of JSON secrets (e.g. the private key inside a service-account JSON), the user and password\n * of connection strings, and the encoded forms a value takes in URLs, JSON and Basic auth.\n */\nexport function collectSecretValues(secrets: Iterable<string | undefined>): string[] {\n const values = new Set<string>();\n const addOne = (value: string) => {\n if (value.length >= MIN_SECRET_LENGTH) values.add(value);\n };\n const add = (value: unknown) => {\n if (typeof value !== \"string\" || value.length < MIN_SECRET_LENGTH) return;\n addOne(value);\n // The same value as it appears in a URL, inside JSON, and base64-encoded\n try {\n addOne(encodeURIComponent(value));\n } catch {\n // Lone surrogates can't be encoded: the raw value is still covered\n }\n addOne(JSON.stringify(value).slice(1, -1));\n try {\n addOne(btoa(value).replace(/=+$/, \"\"));\n } catch {\n // Not Latin-1: no base64 form to worry about\n }\n };\n const walk = (value: unknown) => {\n if (typeof value === \"string\") add(value);\n else if (Array.isArray(value)) value.forEach(walk);\n else if (value && typeof value === \"object\") Object.values(value).forEach(walk);\n };\n for (const secret of secrets) {\n if (!secret) continue;\n add(secret);\n if (secret.trimStart().startsWith(\"{\")) {\n try {\n walk(JSON.parse(secret));\n } catch {\n // Not JSON: the whole value is already covered\n }\n }\n // Connection strings: drivers report the user or the password on their own\n const credentials = /^[a-z][a-z0-9+.-]*:\\/\\/([^\\s/@:]*):([^\\s/@]*)@/i.exec(secret);\n for (const part of credentials?.slice(1) ?? []) {\n add(part);\n try {\n add(decodeURIComponent(part));\n } catch {\n // Not percent-encoded\n }\n }\n }\n // Longest first, so a secret containing another one is fully redacted\n return [...values].sort((a, b) => b.length - a.length);\n}\n\n/**\n * Strip secret values, URL credentials, query strings, bearer-like tokens and anything shaped\n * like a well-known credential. Safe for text of any length: nothing is cut here, so a secret\n * can never be half-removed.\n */\nexport function redactText({ text, secretValues }: { text: string; secretValues: string[] }): string {\n let out = text;\n for (const secret of secretValues) out = out.split(secret).join(\"***\");\n out = out\n // user:pass@host\n .replace(/(\\b[a-z][a-z0-9+.-]*:\\/\\/)[^\\s/@]+@/gi, \"$1***@\")\n // query strings\n .replace(/(\\b(?:https?|wss?):\\/\\/[^\\s?]+)\\?[^\\s\"')]*/gi, \"$1?***\")\n // bearer-ish tokens that slipped through\n .replace(/\\b(Bearer|Token|Basic|DeepL-Auth-Key)\\s+[A-Za-z0-9._~+/=*-]{8,}/gi, \"$1 ***\");\n for (const pattern of KEY_PATTERNS) out = out.replace(pattern, \"***\");\n return out;\n}\n\n/** Errors are shown on public pages: redact, then truncate (in that order) */\nexport function sanitizeError({\n message,\n secretValues,\n}: {\n message: string | undefined;\n secretValues: string[];\n}): string | undefined {\n if (!message) return undefined;\n const out = redactText({ text: message, secretValues });\n return out.length > MAX_LENGTH ? `${out.slice(0, MAX_LENGTH)}…` : out;\n}\n","import type { ResolvedService } from \"./config\";\nimport { combine, createHttp, errorMessage, measure, normalizeReturn } from \"./probe\";\nimport { redactText, sanitizeError } from \"./sanitize\";\nimport type { CheckCache, CheckContext, CheckOutcome, CheckResult, SecretsInput } from \"./types\";\n\n/** Extra time after `timeoutMs` before a check that ignores its abort signal is given up on */\nconst GRACE_MS = 1000;\n\nexport type RunServiceOptions = {\n service: ResolvedService;\n /** Every secret value of the whole configuration, for redaction */\n secretValues: string[];\n cache: CheckCache;\n now?: () => number;\n fetch?: typeof fetch;\n /** Report failed HTTP answers by status code only */\n hideResponseBodies?: boolean;\n};\n\n/**\n * Run one service and apply the rules shared by all of them:\n * - missing required secrets/fields → \"unconfigured\", the check is never called\n * - a throw or a timeout → down\n * - latency above `slowMs` turns \"up\" into \"degraded\"\n * - errors and every other text of the result are redacted before they leave the runner\n */\nexport async function runService({\n service,\n secretValues,\n cache,\n now = () => Date.now(),\n fetch: fetchImpl,\n hideResponseBodies,\n}: RunServiceOptions): Promise<CheckResult> {\n const checkedAt = now();\n if (service.missing.length > 0) {\n return { key: service.key, status: \"unconfigured\", checkedAt, probed: false };\n }\n\n const redact = (text: string) => redactText({ text, secretValues });\n const controller = new AbortController();\n const ctx: CheckContext<SecretsInput> = {\n secrets: service.secrets as Record<string, string>,\n http: createHttp({ timeoutMs: service.timeoutMs, signal: controller.signal, fetch: fetchImpl, redact, hideResponseBodies }),\n measure,\n combine,\n signal: controller.signal,\n timeoutMs: service.timeoutMs,\n slowMs: service.slowMs,\n cache,\n now: checkedAt,\n };\n\n const start = performance.now();\n let outcome: CheckOutcome;\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n const returned = await Promise.race([\n Promise.resolve().then(() => service.check(ctx)),\n new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n controller.abort(new DOMException(\"Timed out\", \"TimeoutError\"));\n reject(new DOMException(\"Timed out\", \"TimeoutError\"));\n }, service.timeoutMs + GRACE_MS);\n }),\n ]);\n outcome = normalizeReturn(returned);\n if (outcome.latencyMs === undefined && outcome.probed !== false && outcome.status !== \"sleeping\") {\n // Checks that don't measure themselves get the wall time of the whole check\n outcome = { ...outcome, latencyMs: Math.round(performance.now() - start) };\n }\n } catch (error) {\n outcome = { status: \"down\", error: errorMessage(error) };\n } finally {\n clearTimeout(timer);\n }\n\n let { status, error } = outcome;\n if (status === \"up\" && outcome.latencyMs !== undefined && outcome.latencyMs > service.slowMs) {\n status = \"degraded\";\n error ??= `Slow response: ${outcome.latencyMs} ms (threshold ${service.slowMs} ms)`;\n }\n\n return {\n key: service.key,\n status,\n checkedAt,\n probed: outcome.probed ?? true,\n latencyMs: outcome.latencyMs,\n httpStatus: outcome.httpStatus,\n error: sanitizeError({ message: error, secretValues }),\n // Everything below is public too: no text leaves the runner without being redacted\n metrics: outcome.metrics\n ? Object.fromEntries(\n Object.entries(outcome.metrics).map(([name, value]) => [\n redact(name),\n typeof value === \"string\" ? redact(value) : value,\n ])\n )\n : undefined,\n usage: outcome.usage?.length\n ? outcome.usage.map((usage) => ({\n ...usage,\n label: redact(usage.label),\n unit: usage.unit === undefined ? undefined : redact(usage.unit),\n }))\n : undefined,\n instances: outcome.instances?.length\n ? outcome.instances.map((instance) => ({\n ...instance,\n key: redact(instance.key),\n region: instance.region === undefined ? undefined : redact(instance.region),\n id: instance.id === undefined ? undefined : redact(instance.id),\n state: instance.state === undefined ? undefined : redact(instance.state),\n error: sanitizeError({ message: instance.error, secretValues }),\n }))\n : undefined,\n };\n}\n\nexport async function runServices({\n services,\n cacheFor,\n ...rest\n}: Omit<RunServiceOptions, \"service\" | \"cache\"> & {\n services: ResolvedService[];\n /** Each service gets its own cache namespace */\n cacheFor: (key: string) => CheckCache;\n}): Promise<CheckResult[]> {\n return Promise.all(\n services.map((service) => runService({ service, cache: cacheFor(service.key), ...rest }))\n );\n}\n"]}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// src/stores/redis-core.ts
|
|
2
|
+
function parse(value) {
|
|
3
|
+
if (typeof value !== "string") return value && typeof value === "object" ? value : void 0;
|
|
4
|
+
try {
|
|
5
|
+
return JSON.parse(value);
|
|
6
|
+
} catch {
|
|
7
|
+
return void 0;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function toRecord(reply) {
|
|
11
|
+
if (!reply) return {};
|
|
12
|
+
if (Array.isArray(reply)) {
|
|
13
|
+
const record = {};
|
|
14
|
+
for (let i = 0; i + 1 < reply.length; i += 2) record[String(reply[i])] = reply[i + 1];
|
|
15
|
+
return record;
|
|
16
|
+
}
|
|
17
|
+
if (reply instanceof Map) return Object.fromEntries(reply);
|
|
18
|
+
return reply;
|
|
19
|
+
}
|
|
20
|
+
var list = (reply) => Array.isArray(reply) ? reply : [];
|
|
21
|
+
function createRedisStore({ exec, prefix = "sermon" }) {
|
|
22
|
+
const k = {
|
|
23
|
+
latest: `${prefix}:latest`,
|
|
24
|
+
runs: `${prefix}:runs`,
|
|
25
|
+
samples: (key) => `${prefix}:samples:${key}`,
|
|
26
|
+
lock: (name) => `${prefix}:lock:${name}`,
|
|
27
|
+
cache: (key) => `${prefix}:cache:${key}`
|
|
28
|
+
};
|
|
29
|
+
const one = async (command) => (await exec([command]))[0];
|
|
30
|
+
return {
|
|
31
|
+
async saveRun({ summary, results, samples }) {
|
|
32
|
+
const commands = [];
|
|
33
|
+
if (results.length) {
|
|
34
|
+
commands.push(["HSET", k.latest, ...results.flatMap((result) => [result.key, JSON.stringify(result)])]);
|
|
35
|
+
}
|
|
36
|
+
for (const sample of samples) {
|
|
37
|
+
commands.push(["ZADD", k.samples(sample.key), sample.at, JSON.stringify(sample)]);
|
|
38
|
+
}
|
|
39
|
+
commands.push(["ZADD", k.runs, summary.at, JSON.stringify(summary)]);
|
|
40
|
+
await exec(commands);
|
|
41
|
+
},
|
|
42
|
+
async latest() {
|
|
43
|
+
const record = toRecord(await one(["HGETALL", k.latest]));
|
|
44
|
+
const latest = {};
|
|
45
|
+
for (const [key, value] of Object.entries(record)) {
|
|
46
|
+
const result = parse(value);
|
|
47
|
+
if (result) latest[key] = result;
|
|
48
|
+
}
|
|
49
|
+
return latest;
|
|
50
|
+
},
|
|
51
|
+
async runs({ since, until }) {
|
|
52
|
+
const reply = await one(["ZRANGEBYSCORE", k.runs, since, until]);
|
|
53
|
+
return list(reply).map((member) => parse(member)).filter((run) => !!run);
|
|
54
|
+
},
|
|
55
|
+
async samples(key, { since, until }) {
|
|
56
|
+
const reply = await one(["ZRANGEBYSCORE", k.samples(key), since, until]);
|
|
57
|
+
return list(reply).map((member) => parse(member)).filter((sample) => !!sample);
|
|
58
|
+
},
|
|
59
|
+
async prune({ before, keepKeys }) {
|
|
60
|
+
const keys = list(await one(["HKEYS", k.latest])).map(String);
|
|
61
|
+
const commands = [
|
|
62
|
+
["ZREMRANGEBYSCORE", k.runs, "-inf", `(${before}`],
|
|
63
|
+
...keys.map((key) => ["ZREMRANGEBYSCORE", k.samples(key), "-inf", `(${before}`])
|
|
64
|
+
];
|
|
65
|
+
if (keepKeys) {
|
|
66
|
+
const keep = new Set(keepKeys);
|
|
67
|
+
const removed = keys.filter((key) => !keep.has(key));
|
|
68
|
+
if (removed.length) {
|
|
69
|
+
commands.push(["HDEL", k.latest, ...removed], ["DEL", ...removed.map(k.samples)]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
await exec(commands);
|
|
73
|
+
},
|
|
74
|
+
async lock(name, ttlMs) {
|
|
75
|
+
const ttl = Math.max(1, Math.round(ttlMs));
|
|
76
|
+
const acquired = await one(["SET", k.lock(name), "1", "NX", "PX", ttl]);
|
|
77
|
+
if (acquired) return { ok: true };
|
|
78
|
+
const remaining = Number(await one(["PTTL", k.lock(name)]));
|
|
79
|
+
return { ok: false, retryAfterMs: remaining > 0 ? remaining : ttl };
|
|
80
|
+
},
|
|
81
|
+
async unlock(name) {
|
|
82
|
+
await one(["DEL", k.lock(name)]);
|
|
83
|
+
},
|
|
84
|
+
cache: {
|
|
85
|
+
async get(key) {
|
|
86
|
+
const value = await one(["GET", k.cache(key)]);
|
|
87
|
+
return typeof value === "string" ? value : value === null || value === void 0 ? null : JSON.stringify(value);
|
|
88
|
+
},
|
|
89
|
+
async set(key, value, ttlMs) {
|
|
90
|
+
await one(["SET", k.cache(key), value, "PX", Math.max(1, Math.round(ttlMs))]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { createRedisStore };
|
|
97
|
+
//# sourceMappingURL=chunk-AH372HLN.js.map
|
|
98
|
+
//# sourceMappingURL=chunk-AH372HLN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stores/redis-core.ts"],"names":[],"mappings":"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","file":"chunk-AH372HLN.js","sourcesContent":["import type { CheckSample, LockResult, RunSummary, StatusStore } from \"../store\";\nimport type { CheckResult } from \"../types\";\n\nexport type RedisCommand = Array<string | number>;\n\n/** Runs a batch of Redis commands and returns their replies in order */\nexport type RedisExec = (commands: RedisCommand[]) => Promise<unknown[]>;\n\nfunction parse<T>(value: unknown): T | undefined {\n if (typeof value !== \"string\") return value && typeof value === \"object\" ? (value as T) : undefined;\n try {\n return JSON.parse(value) as T;\n } catch {\n return undefined;\n }\n}\n\n/** HGETALL replies as a flat [field, value, …] array (RESP2, REST) or as an object (RESP3) */\nfunction toRecord(reply: unknown): Record<string, unknown> {\n if (!reply) return {};\n if (Array.isArray(reply)) {\n const record: Record<string, unknown> = {};\n for (let i = 0; i + 1 < reply.length; i += 2) record[String(reply[i])] = reply[i + 1];\n return record;\n }\n if (reply instanceof Map) return Object.fromEntries(reply);\n return reply as Record<string, unknown>;\n}\n\nconst list = (reply: unknown): unknown[] => (Array.isArray(reply) ? reply : []);\n\n/**\n * A `StatusStore` on top of plain Redis commands. Layout, under `prefix`:\n * - `latest` HASH service key → latest result (JSON)\n * - `runs` ZSET run summaries (JSON), scored by time\n * - `samples:<key>` ZSET samples of one service (JSON), scored by time\n * - `lock:<name>`, `cache:<key>` strings with a time-to-live\n */\nexport function createRedisStore({ exec, prefix = \"sermon\" }: { exec: RedisExec; prefix?: string }): StatusStore {\n const k = {\n latest: `${prefix}:latest`,\n runs: `${prefix}:runs`,\n samples: (key: string) => `${prefix}:samples:${key}`,\n lock: (name: string) => `${prefix}:lock:${name}`,\n cache: (key: string) => `${prefix}:cache:${key}`,\n };\n const one = async (command: RedisCommand) => (await exec([command]))[0];\n\n return {\n async saveRun({ summary, results, samples }) {\n const commands: RedisCommand[] = [];\n if (results.length) {\n commands.push([\"HSET\", k.latest, ...results.flatMap((result) => [result.key, JSON.stringify(result)])]);\n }\n for (const sample of samples) {\n commands.push([\"ZADD\", k.samples(sample.key), sample.at, JSON.stringify(sample)]);\n }\n commands.push([\"ZADD\", k.runs, summary.at, JSON.stringify(summary)]);\n await exec(commands);\n },\n\n async latest() {\n const record = toRecord(await one([\"HGETALL\", k.latest]));\n const latest: Record<string, CheckResult> = {};\n for (const [key, value] of Object.entries(record)) {\n const result = parse<CheckResult>(value);\n if (result) latest[key] = result;\n }\n return latest;\n },\n\n async runs({ since, until }) {\n const reply = await one([\"ZRANGEBYSCORE\", k.runs, since, until]);\n return list(reply)\n .map((member) => parse<RunSummary>(member))\n .filter((run): run is RunSummary => !!run);\n },\n\n async samples(key, { since, until }) {\n const reply = await one([\"ZRANGEBYSCORE\", k.samples(key), since, until]);\n return list(reply)\n .map((member) => parse<CheckSample>(member))\n .filter((sample): sample is CheckSample => !!sample);\n },\n\n async prune({ before, keepKeys }) {\n const keys = list(await one([\"HKEYS\", k.latest])).map(String);\n // \"(\" makes the bound exclusive: entries exactly at `before` are kept\n const commands: RedisCommand[] = [\n [\"ZREMRANGEBYSCORE\", k.runs, \"-inf\", `(${before}`],\n ...keys.map((key): RedisCommand => [\"ZREMRANGEBYSCORE\", k.samples(key), \"-inf\", `(${before}`]),\n ];\n if (keepKeys) {\n const keep = new Set(keepKeys);\n const removed = keys.filter((key) => !keep.has(key));\n if (removed.length) {\n commands.push([\"HDEL\", k.latest, ...removed], [\"DEL\", ...removed.map(k.samples)]);\n }\n }\n await exec(commands);\n },\n\n async lock(name, ttlMs): Promise<LockResult> {\n const ttl = Math.max(1, Math.round(ttlMs));\n const acquired = await one([\"SET\", k.lock(name), \"1\", \"NX\", \"PX\", ttl]);\n if (acquired) return { ok: true };\n const remaining = Number(await one([\"PTTL\", k.lock(name)]));\n return { ok: false, retryAfterMs: remaining > 0 ? remaining : ttl };\n },\n\n async unlock(name) {\n await one([\"DEL\", k.lock(name)]);\n },\n\n cache: {\n async get(key) {\n const value = await one([\"GET\", k.cache(key)]);\n return typeof value === \"string\" ? value : value === null || value === undefined ? null : JSON.stringify(value);\n },\n async set(key, value, ttlMs) {\n await one([\"SET\", k.cache(key), value, \"PX\", Math.max(1, Math.round(ttlMs))]);\n },\n },\n };\n}\n"]}
|