@emeryld/rrroutes-openapi 2.3.1 → 2.3.3

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