@emeryld/rrroutes-openapi 2.2.27 → 2.3.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -184,12 +184,14 @@ var DocsDocument = ({
184
184
  assetBase,
185
185
  docsBase,
186
186
  historyJson,
187
+ logsJson,
187
188
  baseUrlSuffix,
189
+ webhooks,
188
190
  cspNonce
189
191
  }) => {
190
192
  const cssHref = `${assetBase}/docs.css`;
191
193
  const jsSrc = `${assetBase}/docs.js`;
192
- const configJson = serializeConfig({ docsBasePath: docsBase, baseUrlSuffix });
194
+ const configJson = serializeConfig({ docsBasePath: docsBase, baseUrlSuffix, webhooks });
193
195
  return /* @__PURE__ */ jsxs("html", { lang: "en", children: [
194
196
  /* @__PURE__ */ jsxs("head", { children: [
195
197
  /* @__PURE__ */ jsx("meta", { charSet: "UTF-8" }),
@@ -226,6 +228,15 @@ var DocsDocument = ({
226
228
  dangerouslySetInnerHTML: { __html: historyJson }
227
229
  }
228
230
  ),
231
+ /* @__PURE__ */ jsx(
232
+ "script",
233
+ {
234
+ id: "logs-data",
235
+ type: "application/json",
236
+ nonce: cspNonce,
237
+ dangerouslySetInnerHTML: { __html: logsJson }
238
+ }
239
+ ),
229
240
  /* @__PURE__ */ jsx(
230
241
  "script",
231
242
  {
@@ -248,6 +259,9 @@ function serializePresets(presets) {
248
259
  function serializeHistorySeeds(historySeeds) {
249
260
  return JSON.stringify(Array.isArray(historySeeds) ? historySeeds : []).replace(/<\//g, "<\\/");
250
261
  }
262
+ function serializeLogSeeds(logSeeds) {
263
+ return JSON.stringify(Array.isArray(logSeeds) ? logSeeds : []).replace(/<\//g, "<\\/");
264
+ }
251
265
  function serializeConfig(config) {
252
266
  return JSON.stringify(config).replace(/<\//g, "<\\/");
253
267
  }
@@ -257,7 +271,9 @@ function createLeafDocsDocument(leaves, options = {}) {
257
271
  const presetsJson = serializePresets(options.presets);
258
272
  const docsBase = normalizeDocsBase(options.docsBasePath);
259
273
  const historyJson = serializeHistorySeeds(options.historySeeds);
274
+ const logsJson = serializeLogSeeds(options.logSeeds);
260
275
  const baseUrlSuffix = normalizeBaseUrlSuffix(options.baseUrlSuffix);
276
+ const webhooks = options.webhooks;
261
277
  return /* @__PURE__ */ jsx(
262
278
  DocsDocument,
263
279
  {
@@ -266,7 +282,9 @@ function createLeafDocsDocument(leaves, options = {}) {
266
282
  assetBase,
267
283
  docsBase,
268
284
  historyJson,
285
+ logsJson,
269
286
  baseUrlSuffix,
287
+ webhooks,
270
288
  cspNonce: options.cspNonce
271
289
  }
272
290
  );
@@ -282,6 +300,65 @@ function renderLeafDocsHTML2(leaves, options = {}) {
282
300
  return renderLeafDocsHTML(leaves, options);
283
301
  }
284
302
 
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
+
285
362
  // src/index.ts
286
363
  var trimTrailingSlash = (value) => value.endsWith("/") && value.length > 1 ? value.slice(0, -1) : value;
287
364
  function mountRRRoutesDocs({
@@ -296,10 +373,150 @@ function mountRRRoutesDocs({
296
373
  const assetsMountPath = trimTrailingSlash(
297
374
  options.assetBasePath ?? `${normalizedDocsPath}/assets`
298
375
  );
376
+ const webhookBaseInput = options.logWebhook?.basePath ?? `${normalizedDocsPath}/webhooks`;
377
+ const webhookBasePath = trimTrailingSlash(
378
+ webhookBaseInput.startsWith("/") ? webhookBaseInput : `/${webhookBaseInput}`
379
+ );
380
+ const defaultHistoryLimit = 200;
381
+ const defaultLogLimit = 400;
382
+ const inMemoryHistory = normalizeHistorySeeds(options.historySeeds, defaultHistoryLimit);
383
+ const inMemoryLogs = normalizeLogSeeds(options.logSeeds, defaultLogLimit);
384
+ const webhookPaths = {
385
+ history: `${webhookBasePath}/history`,
386
+ logs: `${webhookBasePath}/logs`
387
+ };
388
+ const webhookSchemas = {
389
+ history: {
390
+ query: historyFeedQuerySchema,
391
+ response: historyWebhookResponseSchema,
392
+ entry: historyFeedEntrySchema
393
+ },
394
+ logs: {
395
+ query: logFeedQuerySchema,
396
+ response: logWebhookResponseSchema,
397
+ entry: logFeedEntrySchema
398
+ }
399
+ };
400
+ const webhookLeaves = {
401
+ history: {
402
+ method: "get",
403
+ path: webhookPaths.history,
404
+ cfg: {
405
+ summary: "RRRoutes docs history feed",
406
+ description: "Returns request history for the docs UI.",
407
+ querySchema: historyFeedQuerySchema,
408
+ outputSchema: historyWebhookResponseSchema,
409
+ tags: ["rrroutes", "docs"]
410
+ }
411
+ },
412
+ logs: {
413
+ method: "get",
414
+ path: webhookPaths.logs,
415
+ cfg: {
416
+ summary: "RRRoutes docs request logs",
417
+ description: "Returns request logs for the docs UI.",
418
+ querySchema: logFeedQuerySchema,
419
+ outputSchema: logWebhookResponseSchema,
420
+ tags: ["rrroutes", "docs"]
421
+ }
422
+ }
423
+ };
299
424
  const publicDir = resolvePublicDir();
300
425
  const assetsDir = path.join(publicDir, "assets");
301
426
  const cspEnabled = options.csp !== false;
302
427
  router.use(assetsMountPath, expressStatic(assetsDir, { immutable: true, maxAge: "365d" }));
428
+ const usingFakeHistory = !options.logWebhook?.history;
429
+ const usingFakeLogs = !options.logWebhook?.logs;
430
+ if (usingFakeHistory || usingFakeLogs) {
431
+ router.use((req, res, next) => {
432
+ if (req.path.startsWith(webhookBasePath) || req.path.startsWith(assetsMountPath) || req.path.startsWith(normalizedDocsPath)) {
433
+ return next();
434
+ }
435
+ const start = Date.now();
436
+ const requestIdHeader = req.headers["x-request-id"] || req.headers["x-requestid"] || req.headers["x-request_id"];
437
+ const requestId = Array.isArray(requestIdHeader) ? requestIdHeader[0] : requestIdHeader;
438
+ res.once("finish", () => {
439
+ const timestamp = Date.now();
440
+ const durationMs = Math.max(timestamp - start, 0);
441
+ const methodUpper = String(req.method || "GET").toUpperCase();
442
+ const pathOnly = req.path || req.originalUrl || "";
443
+ const status = res.statusCode;
444
+ const errorMsg = status >= 400 ? `${status}` : void 0;
445
+ if (usingFakeHistory) {
446
+ inMemoryHistory.unshift({
447
+ id: randomBytes(8).toString("hex"),
448
+ requestId: requestId ? String(requestId) : void 0,
449
+ timestamp,
450
+ method: methodUpper,
451
+ path: pathOnly,
452
+ fullUrl: req.originalUrl || pathOnly,
453
+ params: {},
454
+ query: coerceQueryRecord(req.query),
455
+ body: coercePayload(req.body),
456
+ output: "",
457
+ status,
458
+ durationMs,
459
+ error: errorMsg
460
+ });
461
+ if (inMemoryHistory.length > defaultHistoryLimit) inMemoryHistory.length = defaultHistoryLimit;
462
+ }
463
+ if (usingFakeLogs) {
464
+ const logType = status >= 500 ? "error" : status >= 400 ? "warn" : "info";
465
+ const metadata = JSON.stringify({
466
+ query: req.query,
467
+ durationMs
468
+ });
469
+ inMemoryLogs.unshift({
470
+ id: randomBytes(8).toString("hex"),
471
+ type: logType,
472
+ message: `${methodUpper} ${pathOnly} -> ${status}`,
473
+ timestamp,
474
+ requestId: requestId ? String(requestId) : void 0,
475
+ tags: [],
476
+ metadata
477
+ });
478
+ if (inMemoryLogs.length > defaultLogLimit) inMemoryLogs.length = defaultLogLimit;
479
+ }
480
+ });
481
+ next();
482
+ });
483
+ }
484
+ router.get(webhookPaths.history, async (req, res) => {
485
+ const handler = options.logWebhook?.history;
486
+ try {
487
+ const query = parseHistoryWebhookQuery(req);
488
+ if (!handler) {
489
+ const filtered2 = applyHistoryQuery(inMemoryHistory, query, defaultHistoryLimit);
490
+ res.json(filtered2);
491
+ return;
492
+ }
493
+ const result = await handler({ query, req, res });
494
+ const normalized = normalizeWebhookPage(result);
495
+ const filtered = applyHistoryQuery(normalized.items, query, defaultHistoryLimit);
496
+ res.json(filtered);
497
+ } catch (err) {
498
+ console.error("Failed to serve history webhook", err);
499
+ res.status(500).json({ error: "Failed to load history feed" });
500
+ }
501
+ });
502
+ router.get(webhookPaths.logs, async (req, res) => {
503
+ const handler = options.logWebhook?.logs;
504
+ try {
505
+ const query = parseLogWebhookQuery(req);
506
+ if (!handler) {
507
+ const filtered2 = applyLogsQuery(inMemoryLogs, query, defaultLogLimit);
508
+ res.json(filtered2);
509
+ return;
510
+ }
511
+ const result = await handler({ query, req, res });
512
+ const normalized = normalizeWebhookPage(result);
513
+ const filtered = applyLogsQuery(normalized.items, query, defaultLogLimit);
514
+ res.json(filtered);
515
+ } catch (err) {
516
+ console.error("Failed to serve log webhook", err);
517
+ res.status(500).json({ error: "Failed to load logs feed" });
518
+ }
519
+ });
303
520
  const docsRoutePaths = [normalizedDocsPath, `${normalizedDocsPath}/`, `${normalizedDocsPath}/*id`];
304
521
  router.get(docsRoutePaths, (req, res) => {
305
522
  const preparedLeaves = Array.isArray(leaves) ? leaves.filter((leaf) => leaf.cfg.docsHidden !== true) : [];
@@ -318,7 +535,12 @@ function mountRRRoutesDocs({
318
535
  docsBasePath: `${prefix}${normalizedDocsPath}`,
319
536
  baseUrlSuffix: prefix,
320
537
  historySeeds: options.historySeeds,
321
- presets: normalizePresets(finalPresets)
538
+ logSeeds: options.logSeeds,
539
+ presets: normalizePresets(finalPresets),
540
+ webhooks: {
541
+ history: `${prefix}${webhookPaths.history}`,
542
+ logs: `${prefix}${webhookPaths.logs}`
543
+ }
322
544
  });
323
545
  if (cspEnabled && nonce) {
324
546
  res.setHeader(
@@ -336,7 +558,7 @@ function mountRRRoutesDocs({
336
558
  }
337
559
  res.send(html);
338
560
  });
339
- return { path: docsPath };
561
+ return { path: docsPath, webhooks: webhookPaths, webhookLeaves, webhookSchemas };
340
562
  }
341
563
  function resolvePublicDir() {
342
564
  const moduleDir = typeof __dirname !== "undefined" ? __dirname : path.dirname(fileURLToPath(import.meta.url));
@@ -362,6 +584,244 @@ function normalizePresets(presets) {
362
584
  })) : []
363
585
  }));
364
586
  }
587
+ function parseHistoryWebhookQuery(req) {
588
+ const query = req.query || {};
589
+ const methods = parseStringList(query.methods);
590
+ const path2 = typeof query.path === "string" ? query.path : void 0;
591
+ const status = typeof query.status === "string" ? query.status : void 0;
592
+ const text = typeof query.text === "string" ? query.text : void 0;
593
+ const cursor = typeof query.cursor === "string" ? query.cursor : void 0;
594
+ const sortBy = isSortKey(query.sortBy) ? query.sortBy : void 0;
595
+ const sortDir = isSortDir(query.sortDir) ? query.sortDir : void 0;
596
+ const limit = parseLimit(query.limit);
597
+ const from = parseDateInput(query.from);
598
+ const to = parseDateInput(query.to);
599
+ return {
600
+ cursor,
601
+ methods,
602
+ path: path2,
603
+ status,
604
+ text,
605
+ limit,
606
+ from,
607
+ to,
608
+ sortBy,
609
+ sortDir
610
+ };
611
+ }
612
+ function parseLogWebhookQuery(req) {
613
+ const query = req.query || {};
614
+ const types = parseStringList(query.types);
615
+ const tags = parseStringList(query.tags);
616
+ const requestId = typeof query.requestId === "string" ? query.requestId : void 0;
617
+ const text = typeof query.text === "string" ? query.text : void 0;
618
+ const cursor = typeof query.cursor === "string" ? query.cursor : void 0;
619
+ const limit = parseLimit(query.limit);
620
+ const from = parseDateInput(query.from);
621
+ const to = parseDateInput(query.to);
622
+ const sortDir = isSortDir(query.sortDir) ? query.sortDir : void 0;
623
+ return {
624
+ cursor,
625
+ types,
626
+ tags,
627
+ requestId,
628
+ text,
629
+ limit,
630
+ from,
631
+ to,
632
+ sortDir
633
+ };
634
+ }
635
+ function parseStringList(value) {
636
+ if (typeof value !== "string") return void 0;
637
+ const parts = value.split(",").map((p) => p.trim()).filter(Boolean);
638
+ return parts.length ? parts : void 0;
639
+ }
640
+ function parseLimit(value) {
641
+ if (value === void 0) return void 0;
642
+ const num = Number(value);
643
+ if (!Number.isFinite(num) || num <= 0) return void 0;
644
+ return num;
645
+ }
646
+ function parseDateInput(value) {
647
+ if (typeof value !== "string") return void 0;
648
+ const numeric = Number(value);
649
+ if (Number.isFinite(numeric)) return numeric;
650
+ const timestamp = Date.parse(value);
651
+ if (Number.isNaN(timestamp)) return void 0;
652
+ return timestamp;
653
+ }
654
+ function isSortKey(value) {
655
+ return value === "timestamp" || value === "path" || value === "duration";
656
+ }
657
+ function isSortDir(value) {
658
+ return value === "asc" || value === "desc";
659
+ }
660
+ function normalizeWebhookPage(page) {
661
+ if (!page || typeof page !== "object") return { items: [] };
662
+ return {
663
+ items: Array.isArray(page.items) ? page.items : [],
664
+ nextCursor: page.nextCursor,
665
+ prevCursor: page.prevCursor,
666
+ total: page.total
667
+ };
668
+ }
669
+ function applyHistoryQuery(items, query, hardLimit) {
670
+ const fromTs = typeof query.from === "number" ? query.from : void 0;
671
+ const toTs = typeof query.to === "number" ? query.to : void 0;
672
+ const methods = query.methods ? new Set(query.methods.map((m) => m.toUpperCase())) : void 0;
673
+ const pathNeedle = (query.path || "").toLowerCase();
674
+ const textNeedle = (query.text || "").toLowerCase();
675
+ const statusNeedle = (query.status || "").trim();
676
+ const filtered = (Array.isArray(items) ? items : []).filter((entry) => {
677
+ if (methods?.size && !methods.has(String(entry.method || "").toUpperCase())) return false;
678
+ if (pathNeedle && !String(entry.path || "").toLowerCase().includes(pathNeedle)) return false;
679
+ if (statusNeedle) {
680
+ const statusStr = entry.status !== void 0 && entry.status !== null ? String(entry.status) : "ERR";
681
+ if (!statusStr.startsWith(statusNeedle)) return false;
682
+ }
683
+ if (Number.isFinite(fromTs) && entry.timestamp < fromTs) return false;
684
+ if (Number.isFinite(toTs) && entry.timestamp > toTs) return false;
685
+ if (textNeedle) {
686
+ const haystack = [
687
+ entry.path,
688
+ entry.fullUrl,
689
+ entry.body,
690
+ entry.output,
691
+ entry.error,
692
+ JSON.stringify(entry.params || {}),
693
+ JSON.stringify(entry.query || {})
694
+ ].filter(Boolean).join(" ").toLowerCase();
695
+ if (!haystack.includes(textNeedle)) return false;
696
+ }
697
+ return true;
698
+ });
699
+ const sortBy = query.sortBy || "timestamp";
700
+ const direction = query.sortDir === "asc" ? 1 : -1;
701
+ const sorted = filtered.slice().sort((a, b) => {
702
+ let delta = 0;
703
+ if (sortBy === "path") {
704
+ delta = String(a.path || "").localeCompare(String(b.path || ""));
705
+ } else if (sortBy === "duration") {
706
+ delta = (a.durationMs || 0) - (b.durationMs || 0);
707
+ } else {
708
+ delta = (a.timestamp || 0) - (b.timestamp || 0);
709
+ }
710
+ return delta * direction;
711
+ });
712
+ return paginateItems(sorted, query.cursor, query.limit, hardLimit, 25);
713
+ }
714
+ function applyLogsQuery(items, query, hardLimit) {
715
+ const fromTs = typeof query.from === "number" ? query.from : void 0;
716
+ const toTs = typeof query.to === "number" ? query.to : void 0;
717
+ const textNeedle = (query.text || "").toLowerCase();
718
+ const requestIdNeedle = (query.requestId || "").toLowerCase();
719
+ const types = query.types ? new Set(query.types) : void 0;
720
+ const tags = query.tags ? new Set(query.tags) : void 0;
721
+ const filtered = (Array.isArray(items) ? items : []).filter((entry) => {
722
+ if (types?.size && !types.has(entry.type)) return false;
723
+ const entryTags = Array.isArray(entry.tags) ? entry.tags : [];
724
+ if (tags?.size && !entryTags.some((tag) => tags.has(tag))) return false;
725
+ if (requestIdNeedle && !(entry.requestId || "").toLowerCase().includes(requestIdNeedle))
726
+ return false;
727
+ if (Number.isFinite(fromTs) && entry.timestamp < fromTs) return false;
728
+ if (Number.isFinite(toTs) && entry.timestamp > toTs) return false;
729
+ if (textNeedle) {
730
+ const haystack = [
731
+ entry.message,
732
+ entry.requestId,
733
+ entryTags.join(" "),
734
+ typeof entry.metadata === "string" ? entry.metadata : JSON.stringify(entry.metadata || "")
735
+ ].filter(Boolean).join(" ").toLowerCase();
736
+ if (!haystack.includes(textNeedle)) return false;
737
+ }
738
+ return true;
739
+ });
740
+ const direction = query.sortDir === "asc" ? 1 : -1;
741
+ const sorted = filtered.slice().sort((a, b) => (a.timestamp - b.timestamp) * direction);
742
+ return paginateItems(sorted, query.cursor, query.limit, hardLimit, 50);
743
+ }
744
+ function paginateItems(items, cursor, limit, hardLimit, fallbackLimit) {
745
+ const safeLimit = clampLimit(limit, hardLimit, fallbackLimit);
746
+ const start = parseCursor(cursor);
747
+ const end = start + safeLimit;
748
+ const slice = items.slice(start, end);
749
+ const nextCursor = end < items.length ? String(end) : void 0;
750
+ const prevCursor = start > 0 ? String(Math.max(start - safeLimit, 0)) : void 0;
751
+ return {
752
+ items: slice,
753
+ nextCursor,
754
+ prevCursor,
755
+ total: items.length
756
+ };
757
+ }
758
+ function clampLimit(value, hardLimit, fallback) {
759
+ if (!Number.isFinite(value)) return Math.min(hardLimit, fallback);
760
+ const safe = Math.max(1, Math.min(hardLimit, value));
761
+ return safe;
762
+ }
763
+ function parseCursor(cursor) {
764
+ const num = Number(cursor);
765
+ if (Number.isFinite(num) && num >= 0) return Math.floor(num);
766
+ return 0;
767
+ }
768
+ function normalizeHistorySeeds(seeds, hardLimit) {
769
+ if (!Array.isArray(seeds)) return [];
770
+ return seeds.slice(0, hardLimit).map((entry, idx) => ({
771
+ id: entry.id || randomBytes(8).toString("hex"),
772
+ requestId: typeof entry.requestId === "string" ? entry.requestId : void 0,
773
+ timestamp: entry.timestamp ?? Date.now() - idx * 1e3,
774
+ method: entry.method || "GET",
775
+ path: entry.path || "/",
776
+ fullUrl: entry.fullUrl || entry.path || "/",
777
+ params: entry.params || {},
778
+ query: entry.query || {},
779
+ body: entry.body || "",
780
+ output: entry.output || "",
781
+ status: entry.status,
782
+ durationMs: entry.durationMs ?? 0,
783
+ error: entry.error
784
+ })).filter((entry) => entry.method && entry.path);
785
+ }
786
+ function normalizeLogSeeds(seeds, hardLimit) {
787
+ if (!Array.isArray(seeds)) return [];
788
+ return seeds.slice(0, hardLimit).map((entry, idx) => ({
789
+ id: entry.id || randomBytes(8).toString("hex"),
790
+ type: entry.type || "info",
791
+ message: entry.message || "",
792
+ timestamp: entry.timestamp ?? Date.now() - idx * 1e3,
793
+ requestId: entry.requestId,
794
+ tags: Array.isArray(entry.tags) ? entry.tags : [],
795
+ metadata: entry.metadata
796
+ })).filter((entry) => entry.message);
797
+ }
798
+ function coerceQueryRecord(query) {
799
+ if (!query || typeof query !== "object") return {};
800
+ return Object.fromEntries(
801
+ Object.entries(query).map(([key, value]) => [key, coerceValue(value)])
802
+ );
803
+ }
804
+ function coercePayload(body) {
805
+ if (body === void 0 || body === null) return "";
806
+ if (typeof body === "string") return body;
807
+ try {
808
+ return JSON.stringify(body);
809
+ } catch {
810
+ return String(body);
811
+ }
812
+ }
813
+ function coerceValue(value) {
814
+ if (value === void 0 || value === null) return "";
815
+ if (Array.isArray(value)) return value.map(coerceValue).join(",");
816
+ if (typeof value === "object") {
817
+ try {
818
+ return JSON.stringify(value);
819
+ } catch {
820
+ return String(value);
821
+ }
822
+ }
823
+ return String(value);
824
+ }
365
825
  export {
366
826
  mountRRRoutesDocs,
367
827
  renderLeafDocsHTML2 as renderLeafDocsHTML,