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