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