@emeryld/rrroutes-openapi 2.3.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -10
- package/dist/docs/LeafDocsPage.d.ts +4 -22
- package/dist/docs/docs.d.ts +5 -7
- package/dist/docs/schemaIntrospection.d.ts +1 -1
- package/dist/docs/serializer.d.ts +8 -7
- package/dist/index.cjs +456 -559
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +41 -79
- package/dist/index.mjs +458 -558
- package/dist/index.mjs.map +1 -1
- package/dist/public/assets/docs.js +260 -21
- package/dist/web/app.d.ts +1 -8
- package/dist/web/main.d.ts +1 -1
- package/dist/web/utils/grouping.d.ts +2 -8
- package/dist/web/utils/security.d.ts +21 -0
- package/dist/web/utils/types.d.ts +17 -0
- package/dist/web/v2/AppShell.d.ts +7 -0
- package/dist/web/v2/components/JsonInput.d.ts +10 -0
- package/dist/web/v2/components/JsonViewer.d.ts +12 -0
- package/dist/web/v2/components/MethodBadge.d.ts +4 -0
- package/dist/web/v2/components/New/HttpMethodChip.d.ts +7 -0
- package/dist/web/v2/components/New/ListToolBar.d.ts +11 -0
- package/dist/web/v2/components/New/MethodFiltersChips.d.ts +7 -0
- package/dist/web/v2/components/New/RequestStatusChip.d.ts +6 -0
- package/dist/web/v2/components/New/SplitPageLayout.d.ts +7 -0
- package/dist/web/v2/components/New/StabilityChip.d.ts +7 -0
- package/dist/web/v2/components/New/StatusRangeFilter.d.ts +8 -0
- package/dist/web/v2/components/RecordItem.d.ts +34 -0
- package/dist/web/v2/components/ResizableSidePanel.d.ts +12 -0
- package/dist/web/v2/components/SchemaTable.d.ts +5 -0
- package/dist/web/v2/components/SectionHeader.d.ts +9 -0
- package/dist/web/v2/endpoints/EndpointDetailsPanel.d.ts +5 -0
- package/dist/web/v2/endpoints/EndpointList.d.ts +12 -0
- package/dist/web/v2/endpoints/EndpointsPage.d.ts +1 -0
- package/dist/web/v2/endpoints/endpoints.utils.d.ts +3 -0
- package/dist/web/v2/stores/clientStore.d.ts +48 -0
- package/dist/web/v2/stores/endpointsStore.d.ts +20 -0
- package/dist/web/v2/stores/logsStore.d.ts +5 -0
- package/dist/web/v2/theme.d.ts +21 -0
- package/dist/web/v2/types/types.base.d.ts +30 -0
- package/dist/web/v2/types/types.cacheLog.d.ts +165 -0
- package/dist/web/v2/types/types.endpoint.d.ts +326 -0
- package/dist/web/v2/types/types.log.d.ts +119 -0
- package/dist/web/v2/types/types.preset.d.ts +251 -0
- package/dist/web/v2/types/types.requestLog.d.ts +264 -0
- package/package.json +15 -5
- package/dist/docs/presets.d.ts +0 -14
- package/dist/public/assets/docs.css +0 -1
- package/dist/web/components/Analytics.d.ts +0 -68
- package/dist/web/components/CopyablePre.d.ts +0 -7
- package/dist/web/components/EndpointCard.d.ts +0 -10
- package/dist/web/components/Filters.d.ts +0 -9
- package/dist/web/components/FiltersBar.d.ts +0 -25
- package/dist/web/components/HelperEnumInput.d.ts +0 -10
- package/dist/web/components/HistoryView.d.ts +0 -7
- package/dist/web/components/LogsView.d.ts +0 -1
- package/dist/web/components/PlaygroundOverlay.d.ts +0 -35
- package/dist/web/components/PresetsView.d.ts +0 -10
- package/dist/web/components/RequestLogs.d.ts +0 -10
- package/dist/web/components/SchemaTable.d.ts +0 -4
- package/dist/web/components/ui/Button.d.ts +0 -8
- package/dist/web/components/ui/Clickable.d.ts +0 -7
- package/dist/web/components/ui/Tag.d.ts +0 -9
- package/dist/web/components/ui/Text.d.ts +0 -8
- package/dist/web/components/ui/index.d.ts +0 -4
- package/dist/web/historyStore.d.ts +0 -68
- package/dist/web/logsStore.d.ts +0 -51
- package/dist/web/types.d.ts +0 -5
- package/dist/webhooks.d.ts +0 -181
package/dist/index.cjs
CHANGED
|
@@ -31,8 +31,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
// src/index.ts
|
|
32
32
|
var index_exports = {};
|
|
33
33
|
__export(index_exports, {
|
|
34
|
+
introspectSchema: () => introspectSchema,
|
|
34
35
|
mountRRRoutesDocs: () => mountRRRoutesDocs,
|
|
35
36
|
renderLeafDocsHTML: () => renderLeafDocsHTML2,
|
|
37
|
+
requiredRoutes: () => leaves,
|
|
36
38
|
serializeLeaf: () => serializeLeaf
|
|
37
39
|
});
|
|
38
40
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -45,6 +47,9 @@ var import_node_url = require("url");
|
|
|
45
47
|
// src/docs/LeafDocsPage.tsx
|
|
46
48
|
var import_server = require("react-dom/server");
|
|
47
49
|
|
|
50
|
+
// src/docs/serializer.ts
|
|
51
|
+
var import_rrroutes_contract = require("@emeryld/rrroutes-contract");
|
|
52
|
+
|
|
48
53
|
// src/docs/schemaIntrospection.ts
|
|
49
54
|
var z = __toESM(require("zod"), 1);
|
|
50
55
|
function getDef(schema) {
|
|
@@ -190,10 +195,10 @@ function serializeLeaf(leaf) {
|
|
|
190
195
|
hasQuery: !!cfg.querySchema,
|
|
191
196
|
hasParams: !!cfg.paramsSchema,
|
|
192
197
|
hasOutput: !!cfg.outputSchema,
|
|
193
|
-
bodySchema: introspectSchema(cfg.bodySchema),
|
|
194
|
-
querySchema: introspectSchema(cfg.querySchema),
|
|
195
|
-
paramsSchema: introspectSchema(cfg.paramsSchema),
|
|
196
|
-
outputSchema: introspectSchema(cfg.outputSchema)
|
|
198
|
+
bodySchema: cfg.bodySchema ? introspectSchema((0, import_rrroutes_contract.routeSchemaParse)(cfg.bodySchema)) : void 0,
|
|
199
|
+
querySchema: cfg.querySchema ? introspectSchema((0, import_rrroutes_contract.routeSchemaParse)(cfg.querySchema)) : void 0,
|
|
200
|
+
paramsSchema: cfg.paramsSchema ? introspectSchema((0, import_rrroutes_contract.routeSchemaParse)(cfg.paramsSchema)) : void 0,
|
|
201
|
+
outputSchema: cfg.outputSchema ? introspectSchema((0, import_rrroutes_contract.routeSchemaParse)(cfg.outputSchema)) : void 0
|
|
197
202
|
}
|
|
198
203
|
};
|
|
199
204
|
}
|
|
@@ -210,25 +215,17 @@ function normalizeDocsBase(base) {
|
|
|
210
215
|
if (base === "/") return "/";
|
|
211
216
|
return base.endsWith("/") && base.length > 1 ? base.slice(0, -1) : base;
|
|
212
217
|
}
|
|
213
|
-
function normalizeBaseUrlSuffix(suffix) {
|
|
214
|
-
if (!suffix) return "";
|
|
215
|
-
const trimmed = suffix.endsWith("/") && suffix.length > 1 ? suffix.slice(0, -1) : suffix;
|
|
216
|
-
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
217
|
-
}
|
|
218
218
|
var DocsDocument = ({
|
|
219
219
|
leavesJson,
|
|
220
|
-
presetsJson,
|
|
221
220
|
assetBase,
|
|
222
221
|
docsBase,
|
|
223
|
-
historyJson,
|
|
224
|
-
logsJson,
|
|
225
|
-
baseUrlSuffix,
|
|
226
|
-
webhooks,
|
|
227
222
|
cspNonce
|
|
228
223
|
}) => {
|
|
229
224
|
const cssHref = `${assetBase}/docs.css`;
|
|
230
225
|
const jsSrc = `${assetBase}/docs.js`;
|
|
231
|
-
const configJson = serializeConfig({
|
|
226
|
+
const configJson = serializeConfig({
|
|
227
|
+
docsBasePath: docsBase
|
|
228
|
+
});
|
|
232
229
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("html", { lang: "en", children: [
|
|
233
230
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("head", { children: [
|
|
234
231
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("meta", { charSet: "UTF-8" }),
|
|
@@ -247,33 +244,6 @@ var DocsDocument = ({
|
|
|
247
244
|
dangerouslySetInnerHTML: { __html: leavesJson }
|
|
248
245
|
}
|
|
249
246
|
),
|
|
250
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
251
|
-
"script",
|
|
252
|
-
{
|
|
253
|
-
id: "preset-data",
|
|
254
|
-
type: "application/json",
|
|
255
|
-
nonce: cspNonce,
|
|
256
|
-
dangerouslySetInnerHTML: { __html: presetsJson }
|
|
257
|
-
}
|
|
258
|
-
),
|
|
259
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
260
|
-
"script",
|
|
261
|
-
{
|
|
262
|
-
id: "history-data",
|
|
263
|
-
type: "application/json",
|
|
264
|
-
nonce: cspNonce,
|
|
265
|
-
dangerouslySetInnerHTML: { __html: historyJson }
|
|
266
|
-
}
|
|
267
|
-
),
|
|
268
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
269
|
-
"script",
|
|
270
|
-
{
|
|
271
|
-
id: "logs-data",
|
|
272
|
-
type: "application/json",
|
|
273
|
-
nonce: cspNonce,
|
|
274
|
-
dangerouslySetInnerHTML: { __html: logsJson }
|
|
275
|
-
}
|
|
276
|
-
),
|
|
277
247
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
278
248
|
"script",
|
|
279
249
|
{
|
|
@@ -287,298 +257,481 @@ var DocsDocument = ({
|
|
|
287
257
|
] })
|
|
288
258
|
] });
|
|
289
259
|
};
|
|
290
|
-
function serializeLeaves(
|
|
291
|
-
return JSON.stringify(
|
|
292
|
-
}
|
|
293
|
-
function serializePresets(presets) {
|
|
294
|
-
return JSON.stringify(Array.isArray(presets) ? presets : []).replace(/<\//g, "<\\/");
|
|
295
|
-
}
|
|
296
|
-
function serializeHistorySeeds(historySeeds) {
|
|
297
|
-
return JSON.stringify(Array.isArray(historySeeds) ? historySeeds : []).replace(/<\//g, "<\\/");
|
|
298
|
-
}
|
|
299
|
-
function serializeLogSeeds(logSeeds) {
|
|
300
|
-
return JSON.stringify(Array.isArray(logSeeds) ? logSeeds : []).replace(/<\//g, "<\\/");
|
|
260
|
+
function serializeLeaves(leaves2) {
|
|
261
|
+
return JSON.stringify(leaves2.map(serializeLeaf)).replace(/<\//g, "<\\/");
|
|
301
262
|
}
|
|
302
263
|
function serializeConfig(config) {
|
|
303
264
|
return JSON.stringify(config).replace(/<\//g, "<\\/");
|
|
304
265
|
}
|
|
305
|
-
function createLeafDocsDocument(
|
|
266
|
+
function createLeafDocsDocument(leaves2, options = {}) {
|
|
306
267
|
const assetBase = normalizeBase(options.assetBasePath ?? DEFAULT_ASSET_BASE);
|
|
307
|
-
const leavesJson = serializeLeaves(
|
|
308
|
-
const presetsJson = serializePresets(options.presets);
|
|
268
|
+
const leavesJson = serializeLeaves(leaves2);
|
|
309
269
|
const docsBase = normalizeDocsBase(options.docsBasePath);
|
|
310
|
-
const historyJson = serializeHistorySeeds(options.historySeeds);
|
|
311
|
-
const logsJson = serializeLogSeeds(options.logSeeds);
|
|
312
|
-
const baseUrlSuffix = normalizeBaseUrlSuffix(options.baseUrlSuffix);
|
|
313
|
-
const webhooks = options.webhooks;
|
|
314
270
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
315
271
|
DocsDocument,
|
|
316
272
|
{
|
|
317
273
|
leavesJson,
|
|
318
|
-
presetsJson,
|
|
319
274
|
assetBase,
|
|
320
275
|
docsBase,
|
|
321
|
-
historyJson,
|
|
322
|
-
logsJson,
|
|
323
|
-
baseUrlSuffix,
|
|
324
|
-
webhooks,
|
|
325
276
|
cspNonce: options.cspNonce
|
|
326
277
|
}
|
|
327
278
|
);
|
|
328
279
|
}
|
|
329
|
-
function renderLeafDocsHTML(
|
|
330
|
-
const doc = createLeafDocsDocument(
|
|
280
|
+
function renderLeafDocsHTML(leaves2, options = {}) {
|
|
281
|
+
const doc = createLeafDocsDocument(leaves2, options);
|
|
331
282
|
const html = (0, import_server.renderToStaticMarkup)(doc);
|
|
332
283
|
return `<!DOCTYPE html>${html}`;
|
|
333
284
|
}
|
|
334
285
|
|
|
335
286
|
// src/docs/docs.ts
|
|
336
|
-
function renderLeafDocsHTML2(
|
|
337
|
-
return renderLeafDocsHTML(
|
|
287
|
+
function renderLeafDocsHTML2(leaves2, options = {}) {
|
|
288
|
+
return renderLeafDocsHTML(leaves2, options);
|
|
338
289
|
}
|
|
339
290
|
|
|
340
|
-
// src/
|
|
341
|
-
var
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
291
|
+
// src/web/utils/security.ts
|
|
292
|
+
var import_node_net = __toESM(require("net"), 1);
|
|
293
|
+
function createPasswordGuard(password, realm) {
|
|
294
|
+
const trimmed = password.trim();
|
|
295
|
+
return (req, res, next) => {
|
|
296
|
+
const provided = extractPassword(req.headers.authorization);
|
|
297
|
+
if (provided && provided === trimmed) {
|
|
298
|
+
return next();
|
|
299
|
+
}
|
|
300
|
+
applyDocsSecurityHeaders(res);
|
|
301
|
+
res.setHeader("WWW-Authenticate", `Basic realm="${realm}"`);
|
|
302
|
+
res.status(401).send(
|
|
303
|
+
renderAuthErrorPage(
|
|
304
|
+
"Docs are password protected. Provide the configured password."
|
|
305
|
+
)
|
|
306
|
+
);
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function createCookieGuard(cookieName, cookieSecret) {
|
|
310
|
+
return (req, res, next) => {
|
|
311
|
+
const cookies = req.cookies;
|
|
312
|
+
const value = cookies?.[cookieName];
|
|
313
|
+
const valid = cookieSecret ? value === cookieSecret : Boolean(value);
|
|
314
|
+
if (valid) {
|
|
315
|
+
return next();
|
|
316
|
+
}
|
|
317
|
+
applyDocsSecurityHeaders(res);
|
|
318
|
+
res.status(401).send(
|
|
319
|
+
renderAuthErrorPage(
|
|
320
|
+
"Docs are protected. You must be authenticated to access this page."
|
|
321
|
+
)
|
|
322
|
+
);
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function createMissingPasswordGuard() {
|
|
326
|
+
return (_req, res) => {
|
|
327
|
+
applyDocsSecurityHeaders(res);
|
|
328
|
+
res.status(500).send(renderAuthErrorPage("Provide auth configuration to mounted docs"));
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function extractPassword(authHeader) {
|
|
332
|
+
if (!authHeader) return void 0;
|
|
333
|
+
const header = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
|
334
|
+
if (typeof header !== "string" || !header.startsWith("Basic "))
|
|
335
|
+
return void 0;
|
|
336
|
+
const token = header.slice("Basic ".length);
|
|
337
|
+
try {
|
|
338
|
+
const decoded = Buffer.from(token, "base64").toString("utf8");
|
|
339
|
+
const parts = decoded.split(":");
|
|
340
|
+
parts.shift();
|
|
341
|
+
return parts.join(":");
|
|
342
|
+
} catch {
|
|
343
|
+
return void 0;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function createIpAllowListGuard(allowed) {
|
|
347
|
+
const ranges = allowed.map((raw) => raw.trim()).filter(Boolean).map(parseIpPattern).filter((r) => r !== null);
|
|
348
|
+
return (req, res, next) => {
|
|
349
|
+
const rawIp = req.ip || req.connection && req.connection.remoteAddress || "";
|
|
350
|
+
const ip = normalizeIp(rawIp);
|
|
351
|
+
if (!ip || !isIpAllowed(ip, ranges)) {
|
|
352
|
+
applyDocsSecurityHeaders(res);
|
|
353
|
+
res.status(403).send(
|
|
354
|
+
renderAuthErrorPage(
|
|
355
|
+
"Access to docs is restricted from this IP address."
|
|
356
|
+
)
|
|
357
|
+
);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
next();
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function normalizeIp(ip) {
|
|
364
|
+
if (!ip) return "";
|
|
365
|
+
if (ip.startsWith("::ffff:")) return ip.slice(7);
|
|
366
|
+
if (ip === "::1") return "127.0.0.1";
|
|
367
|
+
return ip;
|
|
368
|
+
}
|
|
369
|
+
function parseIpPattern(raw) {
|
|
370
|
+
if (raw.includes("/")) {
|
|
371
|
+
const cidr = parseCidr(raw);
|
|
372
|
+
if (!cidr) return null;
|
|
373
|
+
return { kind: "cidr", base: cidr.base, mask: cidr.mask };
|
|
374
|
+
}
|
|
375
|
+
return { kind: "exact", value: normalizeIp(raw) };
|
|
376
|
+
}
|
|
377
|
+
function parseCidr(raw) {
|
|
378
|
+
const [baseIp, bitsStr] = raw.split("/");
|
|
379
|
+
const bits = Number(bitsStr);
|
|
380
|
+
if (!Number.isInteger(bits) || bits < 0 || bits > 32) return null;
|
|
381
|
+
if (import_node_net.default.isIP(baseIp) !== 4) return null;
|
|
382
|
+
const baseLong = ipToLong(baseIp);
|
|
383
|
+
if (baseLong == null) return null;
|
|
384
|
+
const mask = bits === 0 ? 0 : ~0 << 32 - bits >>> 0;
|
|
385
|
+
return { base: (baseLong & mask) >>> 0, mask };
|
|
386
|
+
}
|
|
387
|
+
function ipToLong(ip) {
|
|
388
|
+
const parts = ip.split(".").map((n) => Number(n));
|
|
389
|
+
if (parts.length !== 4) return null;
|
|
390
|
+
if (parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return null;
|
|
391
|
+
return (parts[0] << 24 >>> 0) + (parts[1] << 16 >>> 0) + (parts[2] << 8 >>> 0) + parts[3];
|
|
392
|
+
}
|
|
393
|
+
function isIpAllowed(ip, ranges) {
|
|
394
|
+
const ipv4 = import_node_net.default.isIP(ip) === 4 ? ipToLong(ip) : null;
|
|
395
|
+
for (const r of ranges) {
|
|
396
|
+
if (r.kind === "exact") {
|
|
397
|
+
if (ip === r.value) return true;
|
|
398
|
+
} else if (r.kind === "cidr" && ipv4 != null) {
|
|
399
|
+
if ((ipv4 & r.mask) === r.base) return true;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
function renderAuthErrorPage(message) {
|
|
405
|
+
return `<!DOCTYPE html>
|
|
406
|
+
<html lang="en">
|
|
407
|
+
<head>
|
|
408
|
+
<meta charset="UTF-8" />
|
|
409
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
410
|
+
<title>RRRoutes docs locked</title>
|
|
411
|
+
<style>
|
|
412
|
+
body { margin:0; font-family: system-ui, -apple-system, Segoe UI, sans-serif; background: #0f172a; color: #e2e8f0; display:flex; align-items:center; justify-content:center; min-height:100vh; }
|
|
413
|
+
.card { padding:32px; border:1px solid #1e293b; border-radius:12px; max-width:420px; background: rgba(15,23,42,0.8); box-shadow:0 15px 45px rgba(0,0,0,0.35); }
|
|
414
|
+
h1 { margin:0 0 12px; font-size:22px; }
|
|
415
|
+
p { margin:0; line-height:1.5; color:#cbd5e1; }
|
|
416
|
+
code { background: rgba(226,232,240,0.1); padding: 2px 4px; border-radius: 4px; }
|
|
417
|
+
</style>
|
|
418
|
+
</head>
|
|
419
|
+
<body>
|
|
420
|
+
<div class="card">
|
|
421
|
+
<h1>Docs locked</h1>
|
|
422
|
+
<p>${message}</p>
|
|
423
|
+
</div>
|
|
424
|
+
</body>
|
|
425
|
+
</html>`;
|
|
426
|
+
}
|
|
427
|
+
function applyDocsSecurityHeaders(res) {
|
|
428
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
429
|
+
res.setHeader("Referrer-Policy", "same-origin");
|
|
430
|
+
res.setHeader("X-Frame-Options", "SAMEORIGIN");
|
|
431
|
+
res.setHeader("Cache-Control", "no-store");
|
|
432
|
+
res.setHeader(
|
|
433
|
+
"Strict-Transport-Security",
|
|
434
|
+
"max-age=31536000; includeSubDomains"
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/web/utils/types.ts
|
|
439
|
+
var import_rrroutes_contract7 = require("@emeryld/rrroutes-contract");
|
|
440
|
+
|
|
441
|
+
// src/web/v2/types/types.cacheLog.ts
|
|
442
|
+
var import_rrroutes_contract2 = require("@emeryld/rrroutes-contract");
|
|
443
|
+
var import_zod2 = __toESM(require("zod"), 1);
|
|
444
|
+
|
|
445
|
+
// src/web/v2/types/types.base.ts
|
|
446
|
+
var import_zod = __toESM(require("zod"), 1);
|
|
447
|
+
var METHODS = ["get", "post", "put", "patch", "delete"];
|
|
448
|
+
var baseEntitySchema = import_zod.default.object({
|
|
449
|
+
id: import_zod.default.string(),
|
|
450
|
+
name: import_zod.default.string(),
|
|
451
|
+
description: import_zod.default.string().optional(),
|
|
452
|
+
groupId: import_zod.default.string().optional(),
|
|
453
|
+
tags: import_zod.default.string().array().optional(),
|
|
454
|
+
createdAt: import_zod.default.number(),
|
|
455
|
+
updatedAt: import_zod.default.number()
|
|
357
456
|
});
|
|
358
|
-
var
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
457
|
+
var baseQuerySchema = import_zod.default.object({
|
|
458
|
+
beforeDate: import_zod.default.string().optional(),
|
|
459
|
+
afterDate: import_zod.default.string().optional(),
|
|
460
|
+
orderBy: import_zod.default.enum(["timestamp", "duration", "level", "path"]).default("timestamp"),
|
|
461
|
+
orderDirection: import_zod.default.enum(["asc", "desc"]).default("desc"),
|
|
462
|
+
searchQuery: import_zod.default.string().optional(),
|
|
463
|
+
groups: import_zod.default.string().array().optional(),
|
|
464
|
+
tags: import_zod.default.string().array().optional(),
|
|
465
|
+
cursor: import_zod.default.string().optional()
|
|
366
466
|
});
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
467
|
+
|
|
468
|
+
// src/web/v2/types/types.cacheLog.ts
|
|
469
|
+
var operationEnum = import_zod2.default.enum(["hit", "miss", "set", "delete"]);
|
|
470
|
+
var cacheLogSchema = baseEntitySchema.extend({
|
|
471
|
+
operation: operationEnum,
|
|
472
|
+
// on hit, value = value retrieved
|
|
473
|
+
// on miss, value = null
|
|
474
|
+
// on set, value = value set
|
|
475
|
+
// on delete, value = value deleted
|
|
476
|
+
value: import_zod2.default.any().nullable(),
|
|
477
|
+
size: import_zod2.default.number().optional()
|
|
378
478
|
});
|
|
379
|
-
var
|
|
380
|
-
|
|
381
|
-
limit: import_zod.z.number().int().positive().optional(),
|
|
382
|
-
types: import_zod.z.array(logTypeSchema).optional(),
|
|
383
|
-
tags: import_zod.z.array(import_zod.z.string()).optional(),
|
|
384
|
-
requestId: import_zod.z.string().optional(),
|
|
385
|
-
text: import_zod.z.string().optional(),
|
|
386
|
-
from: import_zod.z.number().optional(),
|
|
387
|
-
to: import_zod.z.number().optional(),
|
|
388
|
-
sortDir: import_zod.z.enum(["asc", "desc"]).optional()
|
|
479
|
+
var cacheLogQuerySchema = baseQuerySchema.extend({
|
|
480
|
+
operations: operationEnum.array().optional()
|
|
389
481
|
});
|
|
390
|
-
var
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
482
|
+
var cacheLeaves = (0, import_rrroutes_contract2.resource)("cache").get({
|
|
483
|
+
feed: true,
|
|
484
|
+
outputSchema: cacheLogSchema.array(),
|
|
485
|
+
querySchema: cacheLogQuerySchema,
|
|
486
|
+
outputMetaSchema: import_zod2.default.object({
|
|
487
|
+
totalCount: import_zod2.default.number().optional()
|
|
488
|
+
})
|
|
489
|
+
}).post({
|
|
490
|
+
querySchema: cacheLogQuerySchema
|
|
491
|
+
}).done();
|
|
492
|
+
|
|
493
|
+
// src/web/v2/types/types.endpoint.ts
|
|
494
|
+
var import_rrroutes_contract5 = require("@emeryld/rrroutes-contract");
|
|
495
|
+
var import_zod5 = __toESM(require("zod"), 1);
|
|
496
|
+
|
|
497
|
+
// src/web/v2/types/types.requestLog.ts
|
|
498
|
+
var import_rrroutes_contract4 = require("@emeryld/rrroutes-contract");
|
|
499
|
+
var import_zod4 = __toESM(require("zod"), 1);
|
|
500
|
+
|
|
501
|
+
// src/web/v2/types/types.log.ts
|
|
502
|
+
var import_rrroutes_contract3 = require("@emeryld/rrroutes-contract");
|
|
503
|
+
var import_zod3 = __toESM(require("zod"), 1);
|
|
504
|
+
var levelSchema = import_zod3.default.enum(["info", "warning", "error", "debug", "trace"]);
|
|
505
|
+
var logSchema = baseEntitySchema.extend({
|
|
506
|
+
level: levelSchema,
|
|
507
|
+
meta: import_zod3.default.json()
|
|
508
|
+
});
|
|
509
|
+
var logQuerySchema = baseQuerySchema.extend({
|
|
510
|
+
level: levelSchema.array().optional()
|
|
395
511
|
});
|
|
396
|
-
var
|
|
397
|
-
|
|
512
|
+
var logLeaves = (0, import_rrroutes_contract3.resource)("logs").get({
|
|
513
|
+
feed: true,
|
|
514
|
+
outputSchema: logSchema.array(),
|
|
515
|
+
querySchema: logQuerySchema,
|
|
516
|
+
outputMetaSchema: import_zod3.default.object({
|
|
517
|
+
totalCount: import_zod3.default.number().optional()
|
|
518
|
+
})
|
|
519
|
+
}).done();
|
|
520
|
+
|
|
521
|
+
// src/web/v2/types/types.requestLog.ts
|
|
522
|
+
var requestSchema = baseEntitySchema.extend({
|
|
523
|
+
status: import_zod4.default.number(),
|
|
524
|
+
body: import_zod4.default.any().optional(),
|
|
525
|
+
fullUrl: import_zod4.default.string(),
|
|
526
|
+
path: import_zod4.default.string(),
|
|
527
|
+
method: import_zod4.default.enum(METHODS),
|
|
528
|
+
query: import_zod4.default.record(import_zod4.default.string(), import_zod4.default.any()).optional(),
|
|
529
|
+
params: import_zod4.default.record(import_zod4.default.string(), import_zod4.default.any()).optional(),
|
|
530
|
+
output: import_zod4.default.any().optional(),
|
|
531
|
+
headers: import_zod4.default.record(import_zod4.default.string(), import_zod4.default.any()).optional(),
|
|
532
|
+
error: import_zod4.default.string().optional(),
|
|
533
|
+
durationMs: import_zod4.default.number()
|
|
534
|
+
});
|
|
535
|
+
var requestQuerySchema = baseQuerySchema.extend({
|
|
536
|
+
methods: import_zod4.default.enum(METHODS).array().default([]),
|
|
537
|
+
statuses: import_zod4.default.number().array().default([]),
|
|
538
|
+
path: import_zod4.default.string().optional()
|
|
539
|
+
});
|
|
540
|
+
var requestLogLeaves = (0, import_rrroutes_contract4.resource)("requests").get({
|
|
541
|
+
feed: true,
|
|
542
|
+
outputSchema: requestSchema.array(),
|
|
543
|
+
querySchema: requestQuerySchema,
|
|
544
|
+
outputMetaSchema: import_zod4.default.object({
|
|
545
|
+
totalCount: import_zod4.default.number().optional()
|
|
546
|
+
})
|
|
547
|
+
}).sub(
|
|
548
|
+
(0, import_rrroutes_contract4.resource)(":requestId", void 0, import_zod4.default.string()).get({
|
|
549
|
+
outputSchema: requestSchema.extend({
|
|
550
|
+
// Related by groupId
|
|
551
|
+
// Do I just use the existing feed endpoints with filter: groupId=?
|
|
552
|
+
logs: import_zod4.default.array(logSchema),
|
|
553
|
+
caches: import_zod4.default.array(cacheLogSchema)
|
|
554
|
+
})
|
|
555
|
+
}).done()
|
|
556
|
+
).done();
|
|
557
|
+
|
|
558
|
+
// src/web/v2/types/types.endpoint.ts
|
|
559
|
+
var nodeKind = [
|
|
560
|
+
"object",
|
|
561
|
+
"string",
|
|
562
|
+
"number",
|
|
563
|
+
"array",
|
|
564
|
+
"enum",
|
|
565
|
+
"literal",
|
|
566
|
+
"union"
|
|
567
|
+
];
|
|
568
|
+
var serializableSchemaSchema = import_zod5.default.lazy(
|
|
569
|
+
() => import_zod5.default.object({
|
|
570
|
+
kind: import_zod5.default.enum(nodeKind),
|
|
571
|
+
optional: import_zod5.default.boolean().optional(),
|
|
572
|
+
nullable: import_zod5.default.boolean().optional(),
|
|
573
|
+
description: import_zod5.default.string().optional(),
|
|
574
|
+
// object
|
|
575
|
+
properties: import_zod5.default.record(import_zod5.default.string(), serializableSchemaSchema).optional(),
|
|
576
|
+
// array
|
|
577
|
+
element: serializableSchemaSchema.optional(),
|
|
578
|
+
// union
|
|
579
|
+
union: import_zod5.default.array(serializableSchemaSchema).optional(),
|
|
580
|
+
// literal
|
|
581
|
+
literal: import_zod5.default.unknown().optional(),
|
|
582
|
+
// enum
|
|
583
|
+
enumValues: import_zod5.default.array(import_zod5.default.string()).optional()
|
|
584
|
+
})
|
|
585
|
+
);
|
|
586
|
+
var STABILITIES = [
|
|
587
|
+
"experimental",
|
|
588
|
+
"beta",
|
|
589
|
+
"stable",
|
|
590
|
+
"deprecated"
|
|
591
|
+
];
|
|
592
|
+
var stabilityEnum = import_zod5.default.enum(STABILITIES);
|
|
593
|
+
var endpointSchema = baseEntitySchema.extend({
|
|
594
|
+
method: import_zod5.default.enum(METHODS),
|
|
595
|
+
path: import_zod5.default.string(),
|
|
596
|
+
contract: import_zod5.default.object({
|
|
597
|
+
body: serializableSchemaSchema.optional(),
|
|
598
|
+
query: serializableSchemaSchema.optional(),
|
|
599
|
+
output: serializableSchemaSchema.optional(),
|
|
600
|
+
params: serializableSchemaSchema.optional(),
|
|
601
|
+
bodyFiles: import_zod5.default.object({ name: import_zod5.default.string(), maxCount: import_zod5.default.number() })
|
|
602
|
+
}),
|
|
603
|
+
feed: import_zod5.default.boolean().optional(),
|
|
604
|
+
summary: import_zod5.default.string().optional(),
|
|
605
|
+
stability: stabilityEnum,
|
|
606
|
+
hidden: import_zod5.default.boolean().optional(),
|
|
607
|
+
meta: import_zod5.default.record(import_zod5.default.string(), import_zod5.default.string())
|
|
608
|
+
});
|
|
609
|
+
var endpointFilterSchema = baseQuerySchema.extend({
|
|
610
|
+
methods: import_zod5.default.enum(METHODS).array().optional(),
|
|
611
|
+
path: import_zod5.default.string().optional(),
|
|
612
|
+
stability: stabilityEnum.array().optional()
|
|
613
|
+
});
|
|
614
|
+
var endpointLeaves = (0, import_rrroutes_contract5.resource)("endpoints").get({
|
|
615
|
+
feed: true,
|
|
616
|
+
querySchema: endpointFilterSchema,
|
|
617
|
+
outputSchema: endpointSchema.array(),
|
|
618
|
+
outputMetaSchema: import_zod5.default.object({
|
|
619
|
+
totalCount: import_zod5.default.number().optional()
|
|
620
|
+
})
|
|
621
|
+
}).sub(
|
|
622
|
+
(0, import_rrroutes_contract5.resource)(":endpointId", void 0, import_zod5.default.string()).get({
|
|
623
|
+
outputSchema: endpointSchema.extend({
|
|
624
|
+
// Related by groupId. Just use the existing feed endpoints with filter: groupId=?
|
|
625
|
+
requests: import_zod5.default.array(requestSchema),
|
|
626
|
+
// Summary stats: return with the feed?
|
|
627
|
+
volumeTS: import_zod5.default.array(
|
|
628
|
+
import_zod5.default.object({
|
|
629
|
+
timestamp: import_zod5.default.string(),
|
|
630
|
+
count: import_zod5.default.number()
|
|
631
|
+
})
|
|
632
|
+
),
|
|
633
|
+
averageDurationMs: import_zod5.default.number(),
|
|
634
|
+
successRate: import_zod5.default.number(),
|
|
635
|
+
// Add id as query param to the existing feed endpoints? This way "requests" field can also be only Ids
|
|
636
|
+
latestErrorRequestIds: import_zod5.default.array(import_zod5.default.string())
|
|
637
|
+
})
|
|
638
|
+
}).done()
|
|
639
|
+
).done();
|
|
640
|
+
|
|
641
|
+
// src/web/v2/types/types.preset.ts
|
|
642
|
+
var import_rrroutes_contract6 = require("@emeryld/rrroutes-contract");
|
|
643
|
+
var import_zod6 = __toESM(require("zod"), 1);
|
|
644
|
+
var presetSchema = baseEntitySchema.extend({
|
|
645
|
+
operations: import_zod6.default.array(
|
|
646
|
+
import_zod6.default.object({
|
|
647
|
+
endpointId: import_zod6.default.string().optional(),
|
|
648
|
+
method: import_zod6.default.enum(METHODS),
|
|
649
|
+
path: import_zod6.default.string(),
|
|
650
|
+
body: import_zod6.default.json().optional(),
|
|
651
|
+
extraHeaders: import_zod6.default.record(import_zod6.default.string(), import_zod6.default.any()).optional(),
|
|
652
|
+
query: import_zod6.default.record(import_zod6.default.string(), import_zod6.default.any()).optional()
|
|
653
|
+
})
|
|
654
|
+
)
|
|
655
|
+
});
|
|
656
|
+
var presetQuerySchema = baseQuerySchema.extend({
|
|
657
|
+
name: import_zod6.default.string().optional(),
|
|
658
|
+
tags: import_zod6.default.string().array().optional(),
|
|
659
|
+
group: import_zod6.default.string().optional()
|
|
660
|
+
});
|
|
661
|
+
var presetLeaves = (0, import_rrroutes_contract6.resource)("presets").get({
|
|
662
|
+
feed: true,
|
|
663
|
+
querySchema: presetQuerySchema.array(),
|
|
664
|
+
outputMetaSchema: import_zod6.default.object({
|
|
665
|
+
totalCount: import_zod6.default.number().optional()
|
|
666
|
+
}),
|
|
667
|
+
outputSchema: presetSchema
|
|
668
|
+
}).post({
|
|
669
|
+
bodySchema: presetSchema,
|
|
670
|
+
outputSchema: presetSchema
|
|
671
|
+
}).put({
|
|
672
|
+
bodySchema: presetSchema,
|
|
673
|
+
outputSchema: presetSchema
|
|
674
|
+
}).done();
|
|
675
|
+
|
|
676
|
+
// src/web/utils/types.ts
|
|
677
|
+
var allLeaves = (0, import_rrroutes_contract7.resource)().sub(
|
|
678
|
+
(0, import_rrroutes_contract7.resource)("___rrroutes").sub(
|
|
679
|
+
endpointLeaves,
|
|
680
|
+
requestLogLeaves,
|
|
681
|
+
logLeaves,
|
|
682
|
+
cacheLeaves,
|
|
683
|
+
presetLeaves
|
|
684
|
+
).done()
|
|
685
|
+
).done();
|
|
686
|
+
var leaves = (0, import_rrroutes_contract7.finalize)(allLeaves);
|
|
398
687
|
|
|
399
688
|
// src/index.ts
|
|
400
|
-
|
|
689
|
+
function resolvePublicDir() {
|
|
690
|
+
const moduleDir = typeof __dirname !== "undefined" ? __dirname : import_node_path.default.dirname((0, import_node_url.fileURLToPath)(__import_meta_url));
|
|
691
|
+
const fromModule = import_node_path.default.resolve(moduleDir, "../public");
|
|
692
|
+
if (import_node_fs.default.existsSync(fromModule)) return fromModule;
|
|
693
|
+
const fallback = import_node_path.default.resolve(moduleDir, "../dist/public");
|
|
694
|
+
if (import_node_fs.default.existsSync(fallback)) return fallback;
|
|
695
|
+
return fromModule;
|
|
696
|
+
}
|
|
401
697
|
function mountRRRoutesDocs({
|
|
402
698
|
router,
|
|
403
|
-
leaves,
|
|
404
|
-
|
|
405
|
-
options = {}
|
|
699
|
+
leaves: leaves2,
|
|
700
|
+
auth = {}
|
|
406
701
|
}) {
|
|
407
|
-
const
|
|
408
|
-
const docsPath = options.path ?? "/__rrroutes/docs";
|
|
409
|
-
const normalizedDocsPath = trimTrailingSlash(docsPath);
|
|
410
|
-
const assetsMountPath = trimTrailingSlash(
|
|
411
|
-
options.assetBasePath ?? `${normalizedDocsPath}/assets`
|
|
412
|
-
);
|
|
413
|
-
const webhookBaseInput = options.logWebhook?.basePath ?? `${normalizedDocsPath}/webhooks`;
|
|
414
|
-
const webhookBasePath = trimTrailingSlash(
|
|
415
|
-
webhookBaseInput.startsWith("/") ? webhookBaseInput : `/${webhookBaseInput}`
|
|
416
|
-
);
|
|
417
|
-
const defaultHistoryLimit = 200;
|
|
418
|
-
const defaultLogLimit = 400;
|
|
419
|
-
const inMemoryHistory = normalizeHistorySeeds(options.historySeeds, defaultHistoryLimit);
|
|
420
|
-
const inMemoryLogs = normalizeLogSeeds(options.logSeeds, defaultLogLimit);
|
|
421
|
-
const webhookPaths = {
|
|
422
|
-
history: `${webhookBasePath}/history`,
|
|
423
|
-
logs: `${webhookBasePath}/logs`
|
|
424
|
-
};
|
|
425
|
-
const webhookSchemas = {
|
|
426
|
-
history: {
|
|
427
|
-
query: historyFeedQuerySchema,
|
|
428
|
-
response: historyWebhookResponseSchema,
|
|
429
|
-
entry: historyFeedEntrySchema
|
|
430
|
-
},
|
|
431
|
-
logs: {
|
|
432
|
-
query: logFeedQuerySchema,
|
|
433
|
-
response: logWebhookResponseSchema,
|
|
434
|
-
entry: logFeedEntrySchema
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
const webhookLeaves = {
|
|
438
|
-
history: {
|
|
439
|
-
method: "get",
|
|
440
|
-
path: webhookPaths.history,
|
|
441
|
-
cfg: {
|
|
442
|
-
summary: "RRRoutes docs history feed",
|
|
443
|
-
description: "Returns request history for the docs UI.",
|
|
444
|
-
querySchema: historyFeedQuerySchema,
|
|
445
|
-
outputSchema: historyWebhookResponseSchema,
|
|
446
|
-
tags: ["rrroutes", "docs"]
|
|
447
|
-
}
|
|
448
|
-
},
|
|
449
|
-
logs: {
|
|
450
|
-
method: "get",
|
|
451
|
-
path: webhookPaths.logs,
|
|
452
|
-
cfg: {
|
|
453
|
-
summary: "RRRoutes docs request logs",
|
|
454
|
-
description: "Returns request logs for the docs UI.",
|
|
455
|
-
querySchema: logFeedQuerySchema,
|
|
456
|
-
outputSchema: logWebhookResponseSchema,
|
|
457
|
-
tags: ["rrroutes", "docs"]
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
};
|
|
702
|
+
const docsPath = "/__rrroutes/docs";
|
|
461
703
|
const publicDir = resolvePublicDir();
|
|
462
704
|
const assetsDir = import_node_path.default.join(publicDir, "assets");
|
|
463
|
-
const cspEnabled =
|
|
464
|
-
|
|
465
|
-
const
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const timestamp = Date.now();
|
|
477
|
-
const durationMs = Math.max(timestamp - start, 0);
|
|
478
|
-
const methodUpper = String(req.method || "GET").toUpperCase();
|
|
479
|
-
const pathOnly = req.path || req.originalUrl || "";
|
|
480
|
-
const status = res.statusCode;
|
|
481
|
-
const errorMsg = status >= 400 ? `${status}` : void 0;
|
|
482
|
-
if (usingFakeHistory) {
|
|
483
|
-
inMemoryHistory.unshift({
|
|
484
|
-
id: (0, import_crypto.randomBytes)(8).toString("hex"),
|
|
485
|
-
requestId: requestId ? String(requestId) : void 0,
|
|
486
|
-
timestamp,
|
|
487
|
-
method: methodUpper,
|
|
488
|
-
path: pathOnly,
|
|
489
|
-
fullUrl: req.originalUrl || pathOnly,
|
|
490
|
-
params: {},
|
|
491
|
-
query: coerceQueryRecord(req.query),
|
|
492
|
-
body: coercePayload(req.body),
|
|
493
|
-
output: "",
|
|
494
|
-
status,
|
|
495
|
-
durationMs,
|
|
496
|
-
error: errorMsg
|
|
497
|
-
});
|
|
498
|
-
if (inMemoryHistory.length > defaultHistoryLimit) inMemoryHistory.length = defaultHistoryLimit;
|
|
499
|
-
}
|
|
500
|
-
if (usingFakeLogs) {
|
|
501
|
-
const logType = status >= 500 ? "error" : status >= 400 ? "warn" : "info";
|
|
502
|
-
const metadata = JSON.stringify({
|
|
503
|
-
query: req.query,
|
|
504
|
-
durationMs
|
|
505
|
-
});
|
|
506
|
-
inMemoryLogs.unshift({
|
|
507
|
-
id: (0, import_crypto.randomBytes)(8).toString("hex"),
|
|
508
|
-
type: logType,
|
|
509
|
-
message: `${methodUpper} ${pathOnly} -> ${status}`,
|
|
510
|
-
timestamp,
|
|
511
|
-
requestId: requestId ? String(requestId) : void 0,
|
|
512
|
-
tags: [],
|
|
513
|
-
metadata
|
|
514
|
-
});
|
|
515
|
-
if (inMemoryLogs.length > defaultLogLimit) inMemoryLogs.length = defaultLogLimit;
|
|
516
|
-
}
|
|
517
|
-
});
|
|
518
|
-
next();
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
router.get(webhookPaths.history, async (req, res) => {
|
|
522
|
-
const handler = options.logWebhook?.history;
|
|
523
|
-
try {
|
|
524
|
-
const query = parseHistoryWebhookQuery(req);
|
|
525
|
-
if (!handler) {
|
|
526
|
-
const filtered2 = applyHistoryQuery(inMemoryHistory, query, defaultHistoryLimit);
|
|
527
|
-
res.json(filtered2);
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
const result = await handler({ query, req, res });
|
|
531
|
-
const normalized = normalizeWebhookPage(result);
|
|
532
|
-
const filtered = applyHistoryQuery(normalized.items, query, defaultHistoryLimit);
|
|
533
|
-
res.json(filtered);
|
|
534
|
-
} catch (err) {
|
|
535
|
-
console.error("Failed to serve history webhook", err);
|
|
536
|
-
res.status(500).json({ error: "Failed to load history feed" });
|
|
537
|
-
}
|
|
538
|
-
});
|
|
539
|
-
router.get(webhookPaths.logs, async (req, res) => {
|
|
540
|
-
const handler = options.logWebhook?.logs;
|
|
541
|
-
try {
|
|
542
|
-
const query = parseLogWebhookQuery(req);
|
|
543
|
-
if (!handler) {
|
|
544
|
-
const filtered2 = applyLogsQuery(inMemoryLogs, query, defaultLogLimit);
|
|
545
|
-
res.json(filtered2);
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
|
-
const result = await handler({ query, req, res });
|
|
549
|
-
const normalized = normalizeWebhookPage(result);
|
|
550
|
-
const filtered = applyLogsQuery(normalized.items, query, defaultLogLimit);
|
|
551
|
-
res.json(filtered);
|
|
552
|
-
} catch (err) {
|
|
553
|
-
console.error("Failed to serve log webhook", err);
|
|
554
|
-
res.status(500).json({ error: "Failed to load logs feed" });
|
|
555
|
-
}
|
|
705
|
+
const cspEnabled = auth.csp !== false;
|
|
706
|
+
const authEnabled = auth.enabled !== false;
|
|
707
|
+
const docsPassword = auth.password;
|
|
708
|
+
const authRealm = auth.realm || "RRRoutes Docs";
|
|
709
|
+
const allowedIps = auth.allowedIps ?? [];
|
|
710
|
+
const cookieName = auth.cookieName;
|
|
711
|
+
const cookieSecret = auth?.cookieSecret;
|
|
712
|
+
const customGuard = auth?.guardMiddleware;
|
|
713
|
+
const ipGuard = allowedIps.length > 0 ? createIpAllowListGuard(allowedIps) : void 0;
|
|
714
|
+
const authGuard = !authEnabled ? (_req, _res, next) => next() : customGuard ? customGuard : cookieName ? createCookieGuard(cookieName, cookieSecret) : docsPassword ? createPasswordGuard(docsPassword, authRealm) : createMissingPasswordGuard();
|
|
715
|
+
[docsPath, `${docsPath}/assets`, `__rrroutes/`].forEach((p) => {
|
|
716
|
+
if (ipGuard) router.use(p, ipGuard);
|
|
717
|
+
router.use(p, authGuard);
|
|
556
718
|
});
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
cspNonce: nonce,
|
|
571
|
-
assetBasePath: `${prefix}${assetsMountPath}`,
|
|
572
|
-
docsBasePath: `${prefix}${normalizedDocsPath}`,
|
|
573
|
-
baseUrlSuffix: prefix,
|
|
574
|
-
historySeeds: options.historySeeds,
|
|
575
|
-
logSeeds: options.logSeeds,
|
|
576
|
-
presets: normalizePresets(finalPresets),
|
|
577
|
-
webhooks: {
|
|
578
|
-
history: `${prefix}${webhookPaths.history}`,
|
|
579
|
-
logs: `${prefix}${webhookPaths.logs}`
|
|
719
|
+
router.use(
|
|
720
|
+
`${docsPath}/assets`,
|
|
721
|
+
(0, import_express.static)(assetsDir, { immutable: true, maxAge: "365d" })
|
|
722
|
+
);
|
|
723
|
+
const docsRoutePaths = [docsPath, `${docsPath}/`, `${docsPath}/*id`];
|
|
724
|
+
router.get(docsRoutePaths, (_req, res) => {
|
|
725
|
+
const nonce = cspEnabled ? (0, import_crypto.randomBytes)(16).toString("base64") : void 0;
|
|
726
|
+
const html = renderLeafDocsHTML2(
|
|
727
|
+
leaves2.filter((leaf) => leaf.cfg.docsHidden !== true),
|
|
728
|
+
{
|
|
729
|
+
cspNonce: nonce,
|
|
730
|
+
assetBasePath: `${`${docsPath}/assets`}`,
|
|
731
|
+
docsBasePath: `${docsPath}`
|
|
580
732
|
}
|
|
581
|
-
|
|
733
|
+
);
|
|
734
|
+
applyDocsSecurityHeaders(res);
|
|
582
735
|
if (cspEnabled && nonce) {
|
|
583
736
|
res.setHeader(
|
|
584
737
|
"Content-Security-Policy",
|
|
@@ -589,280 +742,24 @@ function mountRRRoutesDocs({
|
|
|
589
742
|
"img-src 'self' data:",
|
|
590
743
|
"connect-src 'self'",
|
|
591
744
|
"font-src 'self'",
|
|
592
|
-
"frame-ancestors 'self'"
|
|
745
|
+
"frame-ancestors 'self'",
|
|
746
|
+
"object-src 'none'",
|
|
747
|
+
"base-uri 'self'"
|
|
593
748
|
].join("; ")
|
|
594
749
|
);
|
|
595
750
|
}
|
|
596
751
|
res.send(html);
|
|
597
752
|
});
|
|
598
|
-
return { path: docsPath, webhooks: webhookPaths, webhookLeaves, webhookSchemas };
|
|
599
|
-
}
|
|
600
|
-
function resolvePublicDir() {
|
|
601
|
-
const moduleDir = typeof __dirname !== "undefined" ? __dirname : import_node_path.default.dirname((0, import_node_url.fileURLToPath)(__import_meta_url));
|
|
602
|
-
const fromModule = import_node_path.default.resolve(moduleDir, "../public");
|
|
603
|
-
if (import_node_fs.default.existsSync(fromModule)) return fromModule;
|
|
604
|
-
const fallback = import_node_path.default.resolve(moduleDir, "../dist/public");
|
|
605
|
-
if (import_node_fs.default.existsSync(fallback)) return fallback;
|
|
606
|
-
return fromModule;
|
|
607
|
-
}
|
|
608
|
-
function normalizePresets(presets) {
|
|
609
|
-
if (!Array.isArray(presets)) return [];
|
|
610
|
-
return presets.map((preset) => ({
|
|
611
|
-
name: preset.name,
|
|
612
|
-
description: preset.description,
|
|
613
|
-
tags: Array.isArray(preset.tags) ? preset.tags.slice() : [],
|
|
614
|
-
docsGroup: preset.docsGroup,
|
|
615
|
-
ops: Array.isArray(preset.ops) ? preset.ops.map((op) => ({
|
|
616
|
-
method: typeof op.method === "string" ? op.method.toUpperCase() : "",
|
|
617
|
-
path: typeof op.path === "string" ? op.path : "",
|
|
618
|
-
body: op.body,
|
|
619
|
-
query: op.query,
|
|
620
|
-
params: op.params
|
|
621
|
-
})) : []
|
|
622
|
-
}));
|
|
623
|
-
}
|
|
624
|
-
function parseHistoryWebhookQuery(req) {
|
|
625
|
-
const query = req.query || {};
|
|
626
|
-
const methods = parseStringList(query.methods);
|
|
627
|
-
const path2 = typeof query.path === "string" ? query.path : void 0;
|
|
628
|
-
const status = typeof query.status === "string" ? query.status : void 0;
|
|
629
|
-
const text = typeof query.text === "string" ? query.text : void 0;
|
|
630
|
-
const cursor = typeof query.cursor === "string" ? query.cursor : void 0;
|
|
631
|
-
const sortBy = isSortKey(query.sortBy) ? query.sortBy : void 0;
|
|
632
|
-
const sortDir = isSortDir(query.sortDir) ? query.sortDir : void 0;
|
|
633
|
-
const limit = parseLimit(query.limit);
|
|
634
|
-
const from = parseDateInput(query.from);
|
|
635
|
-
const to = parseDateInput(query.to);
|
|
636
753
|
return {
|
|
637
|
-
|
|
638
|
-
methods,
|
|
639
|
-
path: path2,
|
|
640
|
-
status,
|
|
641
|
-
text,
|
|
642
|
-
limit,
|
|
643
|
-
from,
|
|
644
|
-
to,
|
|
645
|
-
sortBy,
|
|
646
|
-
sortDir
|
|
754
|
+
path: docsPath
|
|
647
755
|
};
|
|
648
756
|
}
|
|
649
|
-
function parseLogWebhookQuery(req) {
|
|
650
|
-
const query = req.query || {};
|
|
651
|
-
const types = parseStringList(query.types);
|
|
652
|
-
const tags = parseStringList(query.tags);
|
|
653
|
-
const requestId = typeof query.requestId === "string" ? query.requestId : void 0;
|
|
654
|
-
const text = typeof query.text === "string" ? query.text : void 0;
|
|
655
|
-
const cursor = typeof query.cursor === "string" ? query.cursor : void 0;
|
|
656
|
-
const limit = parseLimit(query.limit);
|
|
657
|
-
const from = parseDateInput(query.from);
|
|
658
|
-
const to = parseDateInput(query.to);
|
|
659
|
-
const sortDir = isSortDir(query.sortDir) ? query.sortDir : void 0;
|
|
660
|
-
return {
|
|
661
|
-
cursor,
|
|
662
|
-
types,
|
|
663
|
-
tags,
|
|
664
|
-
requestId,
|
|
665
|
-
text,
|
|
666
|
-
limit,
|
|
667
|
-
from,
|
|
668
|
-
to,
|
|
669
|
-
sortDir
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
function parseStringList(value) {
|
|
673
|
-
if (typeof value !== "string") return void 0;
|
|
674
|
-
const parts = value.split(",").map((p) => p.trim()).filter(Boolean);
|
|
675
|
-
return parts.length ? parts : void 0;
|
|
676
|
-
}
|
|
677
|
-
function parseLimit(value) {
|
|
678
|
-
if (value === void 0) return void 0;
|
|
679
|
-
const num = Number(value);
|
|
680
|
-
if (!Number.isFinite(num) || num <= 0) return void 0;
|
|
681
|
-
return num;
|
|
682
|
-
}
|
|
683
|
-
function parseDateInput(value) {
|
|
684
|
-
if (typeof value !== "string") return void 0;
|
|
685
|
-
const numeric = Number(value);
|
|
686
|
-
if (Number.isFinite(numeric)) return numeric;
|
|
687
|
-
const timestamp = Date.parse(value);
|
|
688
|
-
if (Number.isNaN(timestamp)) return void 0;
|
|
689
|
-
return timestamp;
|
|
690
|
-
}
|
|
691
|
-
function isSortKey(value) {
|
|
692
|
-
return value === "timestamp" || value === "path" || value === "duration";
|
|
693
|
-
}
|
|
694
|
-
function isSortDir(value) {
|
|
695
|
-
return value === "asc" || value === "desc";
|
|
696
|
-
}
|
|
697
|
-
function normalizeWebhookPage(page) {
|
|
698
|
-
if (!page || typeof page !== "object") return { items: [] };
|
|
699
|
-
return {
|
|
700
|
-
items: Array.isArray(page.items) ? page.items : [],
|
|
701
|
-
nextCursor: page.nextCursor,
|
|
702
|
-
prevCursor: page.prevCursor,
|
|
703
|
-
total: page.total
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
function applyHistoryQuery(items, query, hardLimit) {
|
|
707
|
-
const fromTs = typeof query.from === "number" ? query.from : void 0;
|
|
708
|
-
const toTs = typeof query.to === "number" ? query.to : void 0;
|
|
709
|
-
const methods = query.methods ? new Set(query.methods.map((m) => m.toUpperCase())) : void 0;
|
|
710
|
-
const pathNeedle = (query.path || "").toLowerCase();
|
|
711
|
-
const textNeedle = (query.text || "").toLowerCase();
|
|
712
|
-
const statusNeedle = (query.status || "").trim();
|
|
713
|
-
const filtered = (Array.isArray(items) ? items : []).filter((entry) => {
|
|
714
|
-
if (methods?.size && !methods.has(String(entry.method || "").toUpperCase())) return false;
|
|
715
|
-
if (pathNeedle && !String(entry.path || "").toLowerCase().includes(pathNeedle)) return false;
|
|
716
|
-
if (statusNeedle) {
|
|
717
|
-
const statusStr = entry.status !== void 0 && entry.status !== null ? String(entry.status) : "ERR";
|
|
718
|
-
if (!statusStr.startsWith(statusNeedle)) return false;
|
|
719
|
-
}
|
|
720
|
-
if (Number.isFinite(fromTs) && entry.timestamp < fromTs) return false;
|
|
721
|
-
if (Number.isFinite(toTs) && entry.timestamp > toTs) return false;
|
|
722
|
-
if (textNeedle) {
|
|
723
|
-
const haystack = [
|
|
724
|
-
entry.path,
|
|
725
|
-
entry.fullUrl,
|
|
726
|
-
entry.body,
|
|
727
|
-
entry.output,
|
|
728
|
-
entry.error,
|
|
729
|
-
JSON.stringify(entry.params || {}),
|
|
730
|
-
JSON.stringify(entry.query || {})
|
|
731
|
-
].filter(Boolean).join(" ").toLowerCase();
|
|
732
|
-
if (!haystack.includes(textNeedle)) return false;
|
|
733
|
-
}
|
|
734
|
-
return true;
|
|
735
|
-
});
|
|
736
|
-
const sortBy = query.sortBy || "timestamp";
|
|
737
|
-
const direction = query.sortDir === "asc" ? 1 : -1;
|
|
738
|
-
const sorted = filtered.slice().sort((a, b) => {
|
|
739
|
-
let delta = 0;
|
|
740
|
-
if (sortBy === "path") {
|
|
741
|
-
delta = String(a.path || "").localeCompare(String(b.path || ""));
|
|
742
|
-
} else if (sortBy === "duration") {
|
|
743
|
-
delta = (a.durationMs || 0) - (b.durationMs || 0);
|
|
744
|
-
} else {
|
|
745
|
-
delta = (a.timestamp || 0) - (b.timestamp || 0);
|
|
746
|
-
}
|
|
747
|
-
return delta * direction;
|
|
748
|
-
});
|
|
749
|
-
return paginateItems(sorted, query.cursor, query.limit, hardLimit, 25);
|
|
750
|
-
}
|
|
751
|
-
function applyLogsQuery(items, query, hardLimit) {
|
|
752
|
-
const fromTs = typeof query.from === "number" ? query.from : void 0;
|
|
753
|
-
const toTs = typeof query.to === "number" ? query.to : void 0;
|
|
754
|
-
const textNeedle = (query.text || "").toLowerCase();
|
|
755
|
-
const requestIdNeedle = (query.requestId || "").toLowerCase();
|
|
756
|
-
const types = query.types ? new Set(query.types) : void 0;
|
|
757
|
-
const tags = query.tags ? new Set(query.tags) : void 0;
|
|
758
|
-
const filtered = (Array.isArray(items) ? items : []).filter((entry) => {
|
|
759
|
-
if (types?.size && !types.has(entry.type)) return false;
|
|
760
|
-
const entryTags = Array.isArray(entry.tags) ? entry.tags : [];
|
|
761
|
-
if (tags?.size && !entryTags.some((tag) => tags.has(tag))) return false;
|
|
762
|
-
if (requestIdNeedle && !(entry.requestId || "").toLowerCase().includes(requestIdNeedle))
|
|
763
|
-
return false;
|
|
764
|
-
if (Number.isFinite(fromTs) && entry.timestamp < fromTs) return false;
|
|
765
|
-
if (Number.isFinite(toTs) && entry.timestamp > toTs) return false;
|
|
766
|
-
if (textNeedle) {
|
|
767
|
-
const haystack = [
|
|
768
|
-
entry.message,
|
|
769
|
-
entry.requestId,
|
|
770
|
-
entryTags.join(" "),
|
|
771
|
-
typeof entry.metadata === "string" ? entry.metadata : JSON.stringify(entry.metadata || "")
|
|
772
|
-
].filter(Boolean).join(" ").toLowerCase();
|
|
773
|
-
if (!haystack.includes(textNeedle)) return false;
|
|
774
|
-
}
|
|
775
|
-
return true;
|
|
776
|
-
});
|
|
777
|
-
const direction = query.sortDir === "asc" ? 1 : -1;
|
|
778
|
-
const sorted = filtered.slice().sort((a, b) => (a.timestamp - b.timestamp) * direction);
|
|
779
|
-
return paginateItems(sorted, query.cursor, query.limit, hardLimit, 50);
|
|
780
|
-
}
|
|
781
|
-
function paginateItems(items, cursor, limit, hardLimit, fallbackLimit) {
|
|
782
|
-
const safeLimit = clampLimit(limit, hardLimit, fallbackLimit);
|
|
783
|
-
const start = parseCursor(cursor);
|
|
784
|
-
const end = start + safeLimit;
|
|
785
|
-
const slice = items.slice(start, end);
|
|
786
|
-
const nextCursor = end < items.length ? String(end) : void 0;
|
|
787
|
-
const prevCursor = start > 0 ? String(Math.max(start - safeLimit, 0)) : void 0;
|
|
788
|
-
return {
|
|
789
|
-
items: slice,
|
|
790
|
-
nextCursor,
|
|
791
|
-
prevCursor,
|
|
792
|
-
total: items.length
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
function clampLimit(value, hardLimit, fallback) {
|
|
796
|
-
if (!Number.isFinite(value)) return Math.min(hardLimit, fallback);
|
|
797
|
-
const safe = Math.max(1, Math.min(hardLimit, value));
|
|
798
|
-
return safe;
|
|
799
|
-
}
|
|
800
|
-
function parseCursor(cursor) {
|
|
801
|
-
const num = Number(cursor);
|
|
802
|
-
if (Number.isFinite(num) && num >= 0) return Math.floor(num);
|
|
803
|
-
return 0;
|
|
804
|
-
}
|
|
805
|
-
function normalizeHistorySeeds(seeds, hardLimit) {
|
|
806
|
-
if (!Array.isArray(seeds)) return [];
|
|
807
|
-
return seeds.slice(0, hardLimit).map((entry, idx) => ({
|
|
808
|
-
id: entry.id || (0, import_crypto.randomBytes)(8).toString("hex"),
|
|
809
|
-
requestId: typeof entry.requestId === "string" ? entry.requestId : void 0,
|
|
810
|
-
timestamp: entry.timestamp ?? Date.now() - idx * 1e3,
|
|
811
|
-
method: entry.method || "GET",
|
|
812
|
-
path: entry.path || "/",
|
|
813
|
-
fullUrl: entry.fullUrl || entry.path || "/",
|
|
814
|
-
params: entry.params || {},
|
|
815
|
-
query: entry.query || {},
|
|
816
|
-
body: entry.body || "",
|
|
817
|
-
output: entry.output || "",
|
|
818
|
-
status: entry.status,
|
|
819
|
-
durationMs: entry.durationMs ?? 0,
|
|
820
|
-
error: entry.error
|
|
821
|
-
})).filter((entry) => entry.method && entry.path);
|
|
822
|
-
}
|
|
823
|
-
function normalizeLogSeeds(seeds, hardLimit) {
|
|
824
|
-
if (!Array.isArray(seeds)) return [];
|
|
825
|
-
return seeds.slice(0, hardLimit).map((entry, idx) => ({
|
|
826
|
-
id: entry.id || (0, import_crypto.randomBytes)(8).toString("hex"),
|
|
827
|
-
type: entry.type || "info",
|
|
828
|
-
message: entry.message || "",
|
|
829
|
-
timestamp: entry.timestamp ?? Date.now() - idx * 1e3,
|
|
830
|
-
requestId: entry.requestId,
|
|
831
|
-
tags: Array.isArray(entry.tags) ? entry.tags : [],
|
|
832
|
-
metadata: entry.metadata
|
|
833
|
-
})).filter((entry) => entry.message);
|
|
834
|
-
}
|
|
835
|
-
function coerceQueryRecord(query) {
|
|
836
|
-
if (!query || typeof query !== "object") return {};
|
|
837
|
-
return Object.fromEntries(
|
|
838
|
-
Object.entries(query).map(([key, value]) => [key, coerceValue(value)])
|
|
839
|
-
);
|
|
840
|
-
}
|
|
841
|
-
function coercePayload(body) {
|
|
842
|
-
if (body === void 0 || body === null) return "";
|
|
843
|
-
if (typeof body === "string") return body;
|
|
844
|
-
try {
|
|
845
|
-
return JSON.stringify(body);
|
|
846
|
-
} catch {
|
|
847
|
-
return String(body);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
function coerceValue(value) {
|
|
851
|
-
if (value === void 0 || value === null) return "";
|
|
852
|
-
if (Array.isArray(value)) return value.map(coerceValue).join(",");
|
|
853
|
-
if (typeof value === "object") {
|
|
854
|
-
try {
|
|
855
|
-
return JSON.stringify(value);
|
|
856
|
-
} catch {
|
|
857
|
-
return String(value);
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
return String(value);
|
|
861
|
-
}
|
|
862
757
|
// Annotate the CommonJS export names for ESM import in node:
|
|
863
758
|
0 && (module.exports = {
|
|
759
|
+
introspectSchema,
|
|
864
760
|
mountRRRoutesDocs,
|
|
865
761
|
renderLeafDocsHTML,
|
|
762
|
+
requiredRoutes,
|
|
866
763
|
serializeLeaf
|
|
867
764
|
});
|
|
868
765
|
//# sourceMappingURL=index.cjs.map
|