vector-framework 1.2.0 → 1.2.1

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 (75) hide show
  1. package/README.md +19 -0
  2. package/dist/auth/protected.d.ts +1 -0
  3. package/dist/auth/protected.d.ts.map +1 -1
  4. package/dist/auth/protected.js +3 -0
  5. package/dist/auth/protected.js.map +1 -1
  6. package/dist/cache/manager.d.ts +1 -0
  7. package/dist/cache/manager.d.ts.map +1 -1
  8. package/dist/cache/manager.js +3 -0
  9. package/dist/cache/manager.js.map +1 -1
  10. package/dist/cli/graceful-shutdown.d.ts +15 -0
  11. package/dist/cli/graceful-shutdown.d.ts.map +1 -0
  12. package/dist/cli/graceful-shutdown.js +42 -0
  13. package/dist/cli/graceful-shutdown.js.map +1 -0
  14. package/dist/cli/index.js +37 -43
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/cli.js +877 -218
  17. package/dist/core/config-loader.d.ts.map +1 -1
  18. package/dist/core/config-loader.js +5 -2
  19. package/dist/core/config-loader.js.map +1 -1
  20. package/dist/core/server.d.ts +3 -0
  21. package/dist/core/server.d.ts.map +1 -1
  22. package/dist/core/server.js +227 -7
  23. package/dist/core/server.js.map +1 -1
  24. package/dist/core/vector.d.ts +4 -2
  25. package/dist/core/vector.d.ts.map +1 -1
  26. package/dist/core/vector.js +32 -2
  27. package/dist/core/vector.js.map +1 -1
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +147 -41
  31. package/dist/index.js.map +1 -1
  32. package/dist/index.mjs +147 -41
  33. package/dist/openapi/docs-ui.d.ts +1 -1
  34. package/dist/openapi/docs-ui.d.ts.map +1 -1
  35. package/dist/openapi/docs-ui.js +147 -35
  36. package/dist/openapi/docs-ui.js.map +1 -1
  37. package/dist/openapi/generator.d.ts.map +1 -1
  38. package/dist/openapi/generator.js +233 -4
  39. package/dist/openapi/generator.js.map +1 -1
  40. package/dist/start-vector.d.ts +3 -0
  41. package/dist/start-vector.d.ts.map +1 -0
  42. package/dist/start-vector.js +38 -0
  43. package/dist/start-vector.js.map +1 -0
  44. package/dist/types/index.d.ts +25 -0
  45. package/dist/types/index.d.ts.map +1 -1
  46. package/dist/utils/logger.js +1 -1
  47. package/dist/utils/validation.d.ts.map +1 -1
  48. package/dist/utils/validation.js +2 -0
  49. package/dist/utils/validation.js.map +1 -1
  50. package/package.json +3 -1
  51. package/src/auth/protected.ts +4 -0
  52. package/src/cache/manager.ts +4 -0
  53. package/src/cli/graceful-shutdown.ts +60 -0
  54. package/src/cli/index.ts +42 -49
  55. package/src/core/config-loader.ts +5 -2
  56. package/src/core/server.ts +289 -7
  57. package/src/core/vector.ts +38 -4
  58. package/src/index.ts +4 -3
  59. package/src/openapi/assets/favicon/android-chrome-192x192.png +0 -0
  60. package/src/openapi/assets/favicon/android-chrome-512x512.png +0 -0
  61. package/src/openapi/assets/favicon/apple-touch-icon.png +0 -0
  62. package/src/openapi/assets/favicon/favicon-16x16.png +0 -0
  63. package/src/openapi/assets/favicon/favicon-32x32.png +0 -0
  64. package/src/openapi/assets/favicon/favicon.ico +0 -0
  65. package/src/openapi/assets/favicon/site.webmanifest +11 -0
  66. package/src/openapi/assets/logo.svg +12 -0
  67. package/src/openapi/assets/logo_dark.svg +6 -0
  68. package/src/openapi/assets/logo_icon.png +0 -0
  69. package/src/openapi/assets/logo_white.svg +6 -0
  70. package/src/openapi/docs-ui.ts +153 -35
  71. package/src/openapi/generator.ts +231 -4
  72. package/src/start-vector.ts +50 -0
  73. package/src/types/index.ts +34 -0
  74. package/src/utils/logger.ts +1 -1
  75. package/src/utils/validation.ts +2 -0
@@ -1,13 +1,23 @@
1
- export function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
1
+ export function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPath, logoWhitePath, appleTouchIconPath, favicon32Path, favicon16Path, webManifestPath) {
2
2
  const specJson = JSON.stringify(spec).replace(/<\/script/gi, '<\\/script');
3
3
  const openapiPathJson = JSON.stringify(openapiPath);
4
4
  const tailwindScriptPathJson = JSON.stringify(tailwindScriptPath);
5
+ const logoDarkPathJson = JSON.stringify(logoDarkPath);
6
+ const logoWhitePathJson = JSON.stringify(logoWhitePath);
7
+ const appleTouchIconPathJson = JSON.stringify(appleTouchIconPath);
8
+ const favicon32PathJson = JSON.stringify(favicon32Path);
9
+ const favicon16PathJson = JSON.stringify(favicon16Path);
10
+ const webManifestPathJson = JSON.stringify(webManifestPath);
5
11
  return `<!DOCTYPE html>
6
12
  <html lang="en">
7
13
  <head>
8
14
  <meta charset="UTF-8">
9
15
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
10
16
  <title>Vector API Documentation</title>
17
+ <link rel="apple-touch-icon" sizes="180x180" href=${appleTouchIconPathJson}>
18
+ <link rel="icon" type="image/png" sizes="32x32" href=${favicon32PathJson}>
19
+ <link rel="icon" type="image/png" sizes="16x16" href=${favicon16PathJson}>
20
+ <link rel="manifest" href=${webManifestPathJson}>
11
21
  <script>
12
22
  if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
13
23
  document.documentElement.classList.add('dark');
@@ -23,7 +33,12 @@ export function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
23
33
  theme: {
24
34
  extend: {
25
35
  colors: {
26
- brand: '#6366F1',
36
+ brand: {
37
+ DEFAULT: '#00A1FF',
38
+ mint: '#00FF8F',
39
+ soft: '#E4F5FF',
40
+ deep: '#007BC5',
41
+ },
27
42
  dark: { bg: '#0A0A0A', surface: '#111111', border: '#1F1F1F', text: '#EDEDED' },
28
43
  light: { bg: '#FFFFFF', surface: '#F9F9F9', border: '#E5E5E5', text: '#111111' }
29
44
  },
@@ -90,32 +105,44 @@ export function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
90
105
  from { opacity: 0; transform: translateX(-6px); }
91
106
  to { opacity: 1; transform: translateX(0); }
92
107
  }
108
+ @keyframes spin {
109
+ to { transform: rotate(360deg); }
110
+ }
111
+ .button-spinner {
112
+ display: inline-block;
113
+ width: 0.875rem;
114
+ height: 0.875rem;
115
+ border: 2px solid currentColor;
116
+ border-right-color: transparent;
117
+ border-radius: 9999px;
118
+ animation: spin 700ms linear infinite;
119
+ }
93
120
  @media (prefers-reduced-motion: reduce) {
94
121
  *, *::before, *::after {
95
122
  animation: none !important;
96
123
  transition: none !important;
97
124
  }
98
125
  }
99
- .json-key { color: #0f766e; }
100
- .json-string { color: #0369a1; }
101
- .json-number { color: #7c3aed; }
102
- .json-boolean { color: #b45309; }
103
- .json-null { color: #be123c; }
104
- .dark .json-key { color: #5eead4; }
105
- .dark .json-string { color: #7dd3fc; }
106
- .dark .json-number { color: #c4b5fd; }
107
- .dark .json-boolean { color: #fcd34d; }
108
- .dark .json-null { color: #fda4af; }
126
+ .json-key { color: #007bc5; }
127
+ .json-string { color: #334155; }
128
+ .json-number { color: #00a1ff; }
129
+ .json-boolean { color: #475569; }
130
+ .json-null { color: #64748b; }
131
+ .dark .json-key { color: #7dc9ff; }
132
+ .dark .json-string { color: #d1d9e6; }
133
+ .dark .json-number { color: #7dc9ff; }
134
+ .dark .json-boolean { color: #93a4bf; }
135
+ .dark .json-null { color: #7c8ba3; }
109
136
  </style>
110
137
  </head>
111
138
  <body class="bg-light-bg text-light-text dark:bg-dark-bg dark:text-dark-text font-sans antialiased flex h-screen overflow-hidden">
112
139
  <div id="mobile-backdrop" class="fixed inset-0 z-30 bg-black/40 opacity-0 pointer-events-none transition-opacity duration-300 md:hidden"></div>
113
140
  <aside id="docs-sidebar" class="fixed inset-y-0 left-0 z-40 w-72 md:w-64 border-r border-light-border dark:border-dark-border bg-light-surface dark:bg-dark-surface flex flex-col flex-shrink-0 transition-transform duration-300 ease-out -translate-x-full md:translate-x-0 md:static md:z-auto transition-colors duration-150">
114
141
  <div class="h-14 flex items-center px-5 border-b border-light-border dark:border-dark-border">
115
- <svg class="w-5 h-5 text-brand" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
116
- <polygon points="3 21 12 2 21 21 12 15 3 21"></polygon>
117
- </svg>
118
- <span class="ml-2.5 font-bold tracking-tight text-lg">Vector</span>
142
+ <div class="flex items-center">
143
+ <img src=${logoDarkPathJson} alt="Vector" class="h-6 w-auto block dark:hidden" />
144
+ <img src=${logoWhitePathJson} alt="Vector" class="h-6 w-auto hidden dark:block" />
145
+ </div>
119
146
  <button id="sidebar-close" class="ml-auto p-1.5 rounded-full border border-light-border dark:border-dark-border bg-light-bg/90 dark:bg-dark-bg/90 opacity-90 hover:opacity-100 transition md:hidden" aria-label="Close Menu" title="Close Menu">
120
147
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
121
148
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
@@ -146,8 +173,8 @@ export function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
146
173
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
147
174
  </svg>
148
175
  </button>
149
- <svg class="w-5 h-5 text-brand" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="3 21 12 2 21 21 12 15 3 21"></polygon></svg>
150
- <span class="font-bold tracking-tight">Vector</span>
176
+ <img src=${logoDarkPathJson} alt="Vector" class="h-5 w-auto block dark:hidden" />
177
+ <img src=${logoWhitePathJson} alt="Vector" class="h-5 w-auto hidden dark:block" />
151
178
  </div>
152
179
  <div class="flex-1"></div>
153
180
  <button id="theme-toggle" class="p-2 rounded-md hover:bg-black/5 dark:hover:bg-white/5 transition-colors" aria-label="Toggle Dark Mode">
@@ -172,17 +199,22 @@ export function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
172
199
  <div class="grid grid-cols-1 lg:grid-cols-12 gap-10">
173
200
  <div class="lg:col-span-5 space-y-8" id="params-column"></div>
174
201
  <div class="lg:col-span-7">
175
- <div class="rounded-lg border border-light-border dark:border-[#1F1F1F] bg-light-bg dark:bg-[#111111] overflow-hidden group">
176
- <div class="flex items-center justify-between px-4 py-2 border-b border-light-border dark:border-[#1F1F1F] bg-light-surface dark:bg-[#0A0A0A]">
177
- <span class="text-xs font-mono text-light-text/70 dark:text-[#EDEDED]/70">cURL</span>
178
- <button class="text-xs text-light-text/50 hover:text-light-text dark:text-[#EDEDED]/50 dark:hover:text-[#EDEDED] transition-colors" id="copy-curl">Copy</button>
202
+ <div class="rounded-lg border border-light-border dark:border-dark-border bg-light-bg dark:bg-dark-bg overflow-hidden group">
203
+ <div class="flex items-center justify-between px-4 py-2 border-b border-light-border dark:border-dark-border bg-light-surface dark:bg-dark-surface">
204
+ <span class="text-xs font-mono text-light-text/70 dark:text-dark-text/70">cURL</span>
205
+ <button class="text-xs text-light-text/50 hover:text-light-text dark:text-dark-text/50 dark:hover:text-dark-text transition-colors" id="copy-curl">Copy</button>
179
206
  </div>
180
- <pre class="p-4 text-sm font-mono text-light-text dark:text-[#EDEDED] overflow-x-auto leading-relaxed"><code id="curl-code"></code></pre>
207
+ <pre class="p-4 text-sm font-mono text-light-text dark:text-dark-text overflow-x-auto leading-relaxed"><code id="curl-code"></code></pre>
181
208
  </div>
182
209
  <div class="mt-4 p-4 rounded-lg border border-light-border dark:border-dark-border bg-light-surface dark:bg-dark-surface">
183
210
  <div class="flex items-center justify-between mb-3">
184
211
  <h4 class="text-sm font-medium">Try it out</h4>
185
- <button id="send-btn" class="px-4 py-1.5 bg-teal-600 text-white text-sm font-semibold rounded hover:bg-teal-500 transition-colors">Submit</button>
212
+ <button id="send-btn" class="px-4 py-1.5 bg-brand text-white text-sm font-semibold rounded hover:bg-brand-deep transition-colors">
213
+ <span class="inline-flex items-center gap-2">
214
+ <span id="send-btn-spinner" class="button-spinner hidden" aria-hidden="true"></span>
215
+ <span id="send-btn-label">Submit</span>
216
+ </span>
217
+ </button>
186
218
  </div>
187
219
  <div class="space-y-4">
188
220
  <div>
@@ -216,7 +248,7 @@ export function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
216
248
  </div>
217
249
  </div>
218
250
 
219
- <div>
251
+ <div id="response-section">
220
252
  <div class="flex items-center justify-between mb-2">
221
253
  <p class="text-xs font-semibold uppercase tracking-wider opacity-60">Response</p>
222
254
  </div>
@@ -243,7 +275,7 @@ export function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
243
275
  <div class="flex items-center justify-between mb-3">
244
276
  <h3 id="expand-modal-title" class="text-sm font-semibold">Expanded View</h3>
245
277
  <div class="flex items-center gap-2">
246
- <button id="expand-apply" class="hidden text-sm px-3 py-1.5 rounded bg-teal-600 text-white font-semibold hover:bg-teal-500 transition-colors">Apply</button>
278
+ <button id="expand-apply" class="hidden text-sm px-3 py-1.5 rounded bg-brand text-white font-semibold hover:bg-brand-deep transition-colors">Apply</button>
247
279
  <button id="expand-close" class="p-1.5 rounded-full border border-light-border dark:border-dark-border bg-light-bg/90 dark:bg-dark-bg/90 opacity-90 hover:opacity-100 hover:border-brand/60 transition-colors" aria-label="Close Modal" title="Close Modal">
248
280
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
249
281
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
@@ -262,12 +294,13 @@ export function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
262
294
  <script>
263
295
  const spec = ${specJson};
264
296
  const openapiPath = ${openapiPathJson};
297
+ const methodBadgeDefault = "bg-black/5 text-light-text/80 dark:bg-white/10 dark:text-dark-text/80";
265
298
  const methodBadge = {
266
- GET: "bg-green-100 text-green-700 dark:bg-green-500/10 dark:text-green-400",
267
- POST: "bg-blue-100 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400",
268
- PUT: "bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400",
269
- PATCH: "bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400",
270
- DELETE: "bg-red-100 text-red-700 dark:bg-red-500/10 dark:text-red-400",
299
+ GET: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
300
+ POST: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
301
+ PUT: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
302
+ PATCH: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
303
+ DELETE: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
271
304
  };
272
305
 
273
306
  function getOperations() {
@@ -521,6 +554,16 @@ export function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
521
554
  return;
522
555
  }
523
556
  for (const [tag, ops] of groups.entries()) {
557
+ ops.sort((a, b) => {
558
+ const byName = a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
559
+ if (byName !== 0) return byName;
560
+
561
+ const byPath = a.path.localeCompare(b.path, undefined, { sensitivity: "base" });
562
+ if (byPath !== 0) return byPath;
563
+
564
+ return a.method.localeCompare(b.method, undefined, { sensitivity: "base" });
565
+ });
566
+
524
567
  const block = document.createElement("div");
525
568
  block.innerHTML = '<h3 class="px-2 mb-2 font-semibold text-xs uppercase tracking-wider opacity-50"></h3><ul class="space-y-0.5"></ul>';
526
569
  block.querySelector("h3").textContent = tag;
@@ -532,14 +575,14 @@ export function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
532
575
  const a = document.createElement("a");
533
576
  a.href = "#";
534
577
  a.className = op === selected
535
- ? "block px-2 py-1.5 rounded-md bg-black/5 dark:bg-white/5 text-brand font-medium transition-colors"
578
+ ? "block px-2 py-1.5 rounded-md bg-brand-soft/70 dark:bg-brand/20 text-brand-deep dark:text-brand font-medium transition-colors"
536
579
  : "block px-2 py-1.5 rounded-md hover:bg-black/5 dark:hover:bg-white/5 transition-colors";
537
580
 
538
581
  const row = document.createElement("span");
539
582
  row.className = "flex items-center gap-2";
540
583
 
541
584
  const method = document.createElement("span");
542
- method.className = "px-1.5 py-0.5 rounded text-[10px] font-mono font-semibold " + (methodBadge[op.method] || "bg-zinc-100 text-zinc-700 dark:bg-zinc-500/10 dark:text-zinc-400");
585
+ method.className = "px-1.5 py-0.5 rounded text-[10px] font-mono font-semibold " + (methodBadge[op.method] || methodBadgeDefault);
543
586
  method.textContent = op.method;
544
587
 
545
588
  const name = document.createElement("span");
@@ -678,6 +721,55 @@ export function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
678
721
  );
679
722
  }
680
723
 
724
+ function renderResponseSchemasSection(responses) {
725
+ if (!responses || typeof responses !== "object") return "";
726
+
727
+ const statusCodes = Object.keys(responses).sort((a, b) => {
728
+ const aNum = Number(a);
729
+ const bNum = Number(b);
730
+ if (Number.isInteger(aNum) && Number.isInteger(bNum)) return aNum - bNum;
731
+ if (Number.isInteger(aNum)) return -1;
732
+ if (Number.isInteger(bNum)) return 1;
733
+ return a.localeCompare(b);
734
+ });
735
+
736
+ let sections = "";
737
+ for (const statusCode of statusCodes) {
738
+ const responseDef = responses[statusCode];
739
+ if (!responseDef || typeof responseDef !== "object") continue;
740
+
741
+ const jsonSchema =
742
+ responseDef.content &&
743
+ responseDef.content["application/json"] &&
744
+ responseDef.content["application/json"].schema;
745
+
746
+ if (!jsonSchema || typeof jsonSchema !== "object") continue;
747
+
748
+ const rootChildren = buildSchemaChildren(jsonSchema);
749
+ if (!rootChildren.length) continue;
750
+
751
+ let rows = "";
752
+ for (const child of rootChildren) {
753
+ rows += renderSchemaFieldNode(child, 0);
754
+ }
755
+
756
+ sections +=
757
+ '<div class="mb-4"><h4 class="text-xs font-mono uppercase tracking-wider opacity-70 mb-2">Status ' +
758
+ escapeHtml(statusCode) +
759
+ "</h4>" +
760
+ rows +
761
+ "</div>";
762
+ }
763
+
764
+ if (!sections) return "";
765
+
766
+ return (
767
+ '<div><h3 class="text-sm font-semibold mb-3 flex items-center border-b border-light-border dark:border-dark-border pb-2">Response Schemas</h3>' +
768
+ sections +
769
+ "</div>"
770
+ );
771
+ }
772
+
681
773
  function renderTryItParameterInputs(pathParams, queryParams) {
682
774
  const container = document.getElementById("request-param-inputs");
683
775
  if (!container || !selected) return;
@@ -961,6 +1053,19 @@ export function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
961
1053
  }
962
1054
  }
963
1055
 
1056
+ function setSubmitLoading(isLoading) {
1057
+ const sendButton = document.getElementById("send-btn");
1058
+ const spinner = document.getElementById("send-btn-spinner");
1059
+ const label = document.getElementById("send-btn-label");
1060
+ if (!sendButton) return;
1061
+
1062
+ sendButton.disabled = isLoading;
1063
+ sendButton.classList.toggle("opacity-80", isLoading);
1064
+ sendButton.classList.toggle("cursor-wait", isLoading);
1065
+ if (spinner) spinner.classList.toggle("hidden", !isLoading);
1066
+ if (label) label.textContent = isLoading ? "Sending..." : "Submit";
1067
+ }
1068
+
964
1069
  function updateRequestPreview() {
965
1070
  if (!selected) return;
966
1071
 
@@ -1024,7 +1129,7 @@ export function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
1024
1129
  document.getElementById("tag-description").textContent = op.description || "Interactive API documentation.";
1025
1130
  const methodNode = document.getElementById("endpoint-method");
1026
1131
  methodNode.textContent = selected.method;
1027
- methodNode.className = "px-2.5 py-0.5 rounded-full text-xs font-mono font-medium " + (methodBadge[selected.method] || "bg-zinc-100 text-zinc-700 dark:bg-zinc-500/10 dark:text-zinc-400");
1132
+ methodNode.className = "px-2.5 py-0.5 rounded-full text-xs font-mono font-medium " + (methodBadge[selected.method] || methodBadgeDefault);
1028
1133
  document.getElementById("endpoint-title").textContent = selected.name;
1029
1134
  document.getElementById("endpoint-path").textContent = selected.path;
1030
1135
 
@@ -1037,6 +1142,7 @@ export function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
1037
1142
  html += renderParamSection("Header Parameters", headers);
1038
1143
 
1039
1144
  html += renderRequestBodySchemaSection(reqSchema);
1145
+ html += renderResponseSchemasSection(op.responses);
1040
1146
  document.getElementById("params-column").innerHTML = html || '<div class="text-sm opacity-70">No parameters</div>';
1041
1147
  renderTryItParameterInputs(path, query);
1042
1148
  renderHeaderInputs();
@@ -1087,14 +1193,18 @@ export function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
1087
1193
  headers["Content-Type"] = "application/json";
1088
1194
  }
1089
1195
 
1196
+ setSubmitLoading(true);
1090
1197
  try {
1198
+ const requestStart = performance.now();
1091
1199
  const response = await fetch(requestPath, { method: selected.method, headers, body: body || undefined });
1092
1200
  const text = await response.text();
1201
+ const responseTimeMs = Math.round(performance.now() - requestStart);
1093
1202
  const contentType = response.headers.get("content-type") || "unknown";
1094
1203
  const formattedResponse = formatResponseText(text);
1095
1204
  const headerText =
1096
1205
  "Status: " + response.status + " " + response.statusText + "\\n" +
1097
- "Content-Type: " + contentType + "\\n\\n";
1206
+ "Content-Type: " + contentType + "\\n" +
1207
+ "Response Time: " + responseTimeMs + " ms\\n\\n";
1098
1208
  setResponseContent(
1099
1209
  headerText,
1100
1210
  formattedResponse.text,
@@ -1102,6 +1212,8 @@ export function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
1102
1212
  );
1103
1213
  } catch (error) {
1104
1214
  setResponseContent("", "Request failed: " + String(error), false);
1215
+ } finally {
1216
+ setSubmitLoading(false);
1105
1217
  }
1106
1218
  });
1107
1219
 
@@ -1 +1 @@
1
- {"version":3,"file":"docs-ui.js","sourceRoot":"","sources":["../../src/openapi/docs-ui.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,qBAAqB,CACnC,IAA6B,EAC7B,WAAmB,EACnB,kBAA0B;IAE1B,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;IAC3E,MAAM,eAAe,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IACpD,MAAM,sBAAsB,GAAG,IAAI,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;IAElE,OAAO;;;;;;;;;;;;;gBAaO,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAqPnB,QAAQ;0BACD,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAuhCjC,CAAC;AACT,CAAC"}
1
+ {"version":3,"file":"docs-ui.js","sourceRoot":"","sources":["../../src/openapi/docs-ui.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,qBAAqB,CACnC,IAA6B,EAC7B,WAAmB,EACnB,kBAA0B,EAC1B,YAAoB,EACpB,aAAqB,EACrB,kBAA0B,EAC1B,aAAqB,EACrB,aAAqB,EACrB,eAAuB;IAEvB,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;IAC3E,MAAM,eAAe,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IACpD,MAAM,sBAAsB,GAAG,IAAI,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;IAClE,MAAM,gBAAgB,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;IACtD,MAAM,iBAAiB,GAAG,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;IACxD,MAAM,sBAAsB,GAAG,IAAI,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;IAClE,MAAM,iBAAiB,GAAG,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;IACxD,MAAM,iBAAiB,GAAG,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;IACxD,MAAM,mBAAmB,GAAG,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;IAE5D,OAAO;;;;;;sDAM6C,sBAAsB;yDACnB,iBAAiB;yDACjB,iBAAiB;8BAC5C,mBAAmB;;;;;;;;gBAQjC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAmHnB,gBAAgB;mBAChB,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAgCjB,gBAAgB;mBAChB,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAsHjB,QAAQ;0BACD,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAumCjC,CAAC;AACT,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/openapi/generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,KAAK,EAAE,kBAAkB,EAAoD,MAAM,UAAU,CAAC;AAIrG,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,kBAAkB,CAAC;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAwSD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,yBAAyB,EAAE,EACnC,OAAO,EAAE,wBAAwB,GAChC,uBAAuB,CA8CzB"}
1
+ {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/openapi/generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,KAAK,EAAE,kBAAkB,EAAoD,MAAM,UAAU,CAAC;AAIrG,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,kBAAkB,CAAC;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AA2gBD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,yBAAyB,EAAE,EACnC,OAAO,EAAE,wBAAwB,GAChC,uBAAuB,CA8CzB"}
@@ -100,8 +100,9 @@ function convertInputSchema(routePath, inputSchema, target, warnings) {
100
100
  return inputSchema['~standard'].jsonSchema.input({ target });
101
101
  }
102
102
  catch (error) {
103
- warnings.push(`[OpenAPI] Failed input schema conversion for ${routePath}: ${error instanceof Error ? error.message : String(error)}`);
104
- return null;
103
+ warnings.push(`[OpenAPI] Failed input schema conversion for ${routePath}: ${error instanceof Error ? error.message : String(error)}. Falling back to a permissive JSON Schema.`);
104
+ const fallback = buildFallbackJSONSchema(inputSchema);
105
+ return isEmptyObjectSchema(fallback) ? null : fallback;
105
106
  }
106
107
  }
107
108
  function convertOutputSchema(routePath, statusCode, outputSchema, target, warnings) {
@@ -112,13 +113,241 @@ function convertOutputSchema(routePath, statusCode, outputSchema, target, warnin
112
113
  return outputSchema['~standard'].jsonSchema.output({ target });
113
114
  }
114
115
  catch (error) {
115
- warnings.push(`[OpenAPI] Failed output schema conversion for ${routePath} (${statusCode}): ${error instanceof Error ? error.message : String(error)}`);
116
- return null;
116
+ warnings.push(`[OpenAPI] Failed output schema conversion for ${routePath} (${statusCode}): ${error instanceof Error ? error.message : String(error)}. Falling back to a permissive JSON Schema.`);
117
+ return buildFallbackJSONSchema(outputSchema);
117
118
  }
118
119
  }
119
120
  function isRecord(value) {
120
121
  return !!value && typeof value === 'object' && !Array.isArray(value);
121
122
  }
123
+ function isEmptyObjectSchema(value) {
124
+ return isRecord(value) && Object.keys(value).length === 0;
125
+ }
126
+ // Best-effort extraction of internal schema definition metadata from common
127
+ // standards-compatible validators. If unavailable, callers should fall back to {}.
128
+ function getValidatorSchemaDef(schema) {
129
+ if (!schema || typeof schema !== 'object')
130
+ return null;
131
+ const value = schema;
132
+ if (isRecord(value._def))
133
+ return value._def;
134
+ if (isRecord(value._zod) && isRecord(value._zod.def)) {
135
+ return value._zod.def;
136
+ }
137
+ return null;
138
+ }
139
+ function getSchemaKind(def) {
140
+ if (!def)
141
+ return null;
142
+ const typeName = def.typeName;
143
+ if (typeof typeName === 'string')
144
+ return typeName;
145
+ const type = def.type;
146
+ if (typeof type === 'string')
147
+ return type;
148
+ return null;
149
+ }
150
+ function pickSchemaChild(def) {
151
+ const candidates = ['innerType', 'schema', 'type', 'out', 'in', 'left', 'right'];
152
+ for (const key of candidates) {
153
+ if (key in def)
154
+ return def[key];
155
+ }
156
+ return undefined;
157
+ }
158
+ function pickSchemaObjectCandidate(def, keys) {
159
+ for (const key of keys) {
160
+ const value = def[key];
161
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
162
+ return value;
163
+ }
164
+ }
165
+ return undefined;
166
+ }
167
+ function isOptionalWrapperKind(kind) {
168
+ if (!kind)
169
+ return false;
170
+ const lower = kind.toLowerCase();
171
+ return lower.includes('optional') || lower.includes('default') || lower.includes('catch');
172
+ }
173
+ function unwrapOptionalForRequired(schema) {
174
+ let current = schema;
175
+ let optional = false;
176
+ let guard = 0;
177
+ while (guard < 8) {
178
+ guard += 1;
179
+ const def = getValidatorSchemaDef(current);
180
+ const kind = getSchemaKind(def);
181
+ if (!def || !isOptionalWrapperKind(kind))
182
+ break;
183
+ optional = true;
184
+ const inner = pickSchemaChild(def);
185
+ if (!inner)
186
+ break;
187
+ current = inner;
188
+ }
189
+ return { schema: current, optional };
190
+ }
191
+ function getObjectShape(def) {
192
+ const rawShape = def.shape;
193
+ if (typeof rawShape === 'function') {
194
+ try {
195
+ const resolved = rawShape();
196
+ return isRecord(resolved) ? resolved : {};
197
+ }
198
+ catch {
199
+ return {};
200
+ }
201
+ }
202
+ return isRecord(rawShape) ? rawShape : {};
203
+ }
204
+ function mapPrimitiveKind(kind) {
205
+ const lower = kind.toLowerCase();
206
+ if (lower.includes('string'))
207
+ return { type: 'string' };
208
+ if (lower.includes('number'))
209
+ return { type: 'number' };
210
+ if (lower.includes('boolean'))
211
+ return { type: 'boolean' };
212
+ if (lower.includes('bigint'))
213
+ return { type: 'string' };
214
+ if (lower.includes('null'))
215
+ return { type: 'null' };
216
+ if (lower.includes('any') || lower.includes('unknown') || lower.includes('never'))
217
+ return {};
218
+ if (lower.includes('date'))
219
+ return { type: 'string', format: 'date-time' };
220
+ if (lower.includes('custom'))
221
+ return { type: 'object', additionalProperties: true };
222
+ return null;
223
+ }
224
+ // Universal fallback schema builder used when converter functions throw.
225
+ // This keeps docs generation resilient and preserves routes in OpenAPI output.
226
+ function buildIntrospectedFallbackJSONSchema(schema, seen = new WeakSet()) {
227
+ if (!schema || typeof schema !== 'object')
228
+ return {};
229
+ if (seen.has(schema))
230
+ return {};
231
+ seen.add(schema);
232
+ const def = getValidatorSchemaDef(schema);
233
+ const kind = getSchemaKind(def);
234
+ if (!def || !kind)
235
+ return {};
236
+ const primitive = mapPrimitiveKind(kind);
237
+ if (primitive)
238
+ return primitive;
239
+ const lower = kind.toLowerCase();
240
+ if (lower.includes('object')) {
241
+ const shape = getObjectShape(def);
242
+ const properties = {};
243
+ const required = [];
244
+ for (const [key, child] of Object.entries(shape)) {
245
+ const unwrapped = unwrapOptionalForRequired(child);
246
+ properties[key] = buildIntrospectedFallbackJSONSchema(unwrapped.schema, seen);
247
+ if (!unwrapped.optional)
248
+ required.push(key);
249
+ }
250
+ const out = {
251
+ type: 'object',
252
+ properties,
253
+ additionalProperties: true,
254
+ };
255
+ if (required.length > 0) {
256
+ out.required = required;
257
+ }
258
+ return out;
259
+ }
260
+ if (lower.includes('array')) {
261
+ const itemSchema = pickSchemaObjectCandidate(def, ['element', 'items', 'innerType', 'type']) ?? {};
262
+ return {
263
+ type: 'array',
264
+ items: buildIntrospectedFallbackJSONSchema(itemSchema, seen),
265
+ };
266
+ }
267
+ if (lower.includes('record')) {
268
+ const valueType = def.valueType ?? def.valueSchema;
269
+ return {
270
+ type: 'object',
271
+ additionalProperties: valueType ? buildIntrospectedFallbackJSONSchema(valueType, seen) : true,
272
+ };
273
+ }
274
+ if (lower.includes('tuple')) {
275
+ const items = Array.isArray(def.items)
276
+ ? def.items
277
+ : [];
278
+ const prefixItems = items.map((item) => buildIntrospectedFallbackJSONSchema(item, seen));
279
+ return {
280
+ type: 'array',
281
+ prefixItems,
282
+ minItems: prefixItems.length,
283
+ maxItems: prefixItems.length,
284
+ };
285
+ }
286
+ if (lower.includes('union')) {
287
+ const options = def.options ?? def.schemas ?? [];
288
+ if (!Array.isArray(options) || options.length === 0)
289
+ return {};
290
+ return {
291
+ anyOf: options.map((option) => buildIntrospectedFallbackJSONSchema(option, seen)),
292
+ };
293
+ }
294
+ if (lower.includes('intersection')) {
295
+ const left = def.left;
296
+ const right = def.right;
297
+ if (!left || !right)
298
+ return {};
299
+ return {
300
+ allOf: [buildIntrospectedFallbackJSONSchema(left, seen), buildIntrospectedFallbackJSONSchema(right, seen)],
301
+ };
302
+ }
303
+ if (lower.includes('enum')) {
304
+ const values = def.values;
305
+ if (Array.isArray(values))
306
+ return { enum: values };
307
+ if (values && typeof values === 'object')
308
+ return { enum: Object.values(values) };
309
+ return {};
310
+ }
311
+ if (lower.includes('literal')) {
312
+ const value = def.value;
313
+ if (value === undefined)
314
+ return {};
315
+ const valueType = value === null ? 'null' : typeof value;
316
+ if (valueType === 'string' || valueType === 'number' || valueType === 'boolean' || valueType === 'null') {
317
+ return { type: valueType, const: value };
318
+ }
319
+ return { const: value };
320
+ }
321
+ if (lower.includes('nullable')) {
322
+ const inner = pickSchemaChild(def);
323
+ if (!inner)
324
+ return {};
325
+ return {
326
+ anyOf: [buildIntrospectedFallbackJSONSchema(inner, seen), { type: 'null' }],
327
+ };
328
+ }
329
+ if (lower.includes('lazy')) {
330
+ const getter = def.getter;
331
+ if (typeof getter !== 'function')
332
+ return {};
333
+ try {
334
+ return buildIntrospectedFallbackJSONSchema(getter(), seen);
335
+ }
336
+ catch {
337
+ return {};
338
+ }
339
+ }
340
+ const child = pickSchemaChild(def);
341
+ if (child)
342
+ return buildIntrospectedFallbackJSONSchema(child, seen);
343
+ return {};
344
+ }
345
+ function buildFallbackJSONSchema(schema) {
346
+ const def = getValidatorSchemaDef(schema);
347
+ if (!def)
348
+ return {};
349
+ return buildIntrospectedFallbackJSONSchema(schema);
350
+ }
122
351
  function addStructuredInputToOperation(operation, inputJSONSchema) {
123
352
  if (!isRecord(inputJSONSchema))
124
353
  return;