vector-framework 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) 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 +967 -222
  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 +4 -0
  21. package/dist/core/server.d.ts.map +1 -1
  22. package/dist/core/server.js +240 -9
  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/errors/index.cjs +2 -0
  29. package/dist/index.cjs +1434 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +12 -1327
  33. package/dist/index.js.map +1 -1
  34. package/dist/index.mjs +153 -46
  35. package/dist/openapi/docs-ui.d.ts +1 -1
  36. package/dist/openapi/docs-ui.d.ts.map +1 -1
  37. package/dist/openapi/docs-ui.js +147 -35
  38. package/dist/openapi/docs-ui.js.map +1 -1
  39. package/dist/openapi/generator.d.ts.map +1 -1
  40. package/dist/openapi/generator.js +318 -6
  41. package/dist/openapi/generator.js.map +1 -1
  42. package/dist/start-vector.d.ts +3 -0
  43. package/dist/start-vector.d.ts.map +1 -0
  44. package/dist/start-vector.js +38 -0
  45. package/dist/start-vector.js.map +1 -0
  46. package/dist/types/index.d.ts +25 -0
  47. package/dist/types/index.d.ts.map +1 -1
  48. package/dist/utils/logger.js +1 -1
  49. package/dist/utils/validation.d.ts.map +1 -1
  50. package/dist/utils/validation.js +2 -0
  51. package/dist/utils/validation.js.map +1 -1
  52. package/package.json +10 -14
  53. package/src/auth/protected.ts +4 -0
  54. package/src/cache/manager.ts +4 -0
  55. package/src/cli/graceful-shutdown.ts +60 -0
  56. package/src/cli/index.ts +42 -49
  57. package/src/core/config-loader.ts +5 -2
  58. package/src/core/server.ts +304 -9
  59. package/src/core/vector.ts +38 -4
  60. package/src/index.ts +4 -3
  61. package/src/openapi/assets/favicon/android-chrome-192x192.png +0 -0
  62. package/src/openapi/assets/favicon/android-chrome-512x512.png +0 -0
  63. package/src/openapi/assets/favicon/apple-touch-icon.png +0 -0
  64. package/src/openapi/assets/favicon/favicon-16x16.png +0 -0
  65. package/src/openapi/assets/favicon/favicon-32x32.png +0 -0
  66. package/src/openapi/assets/favicon/favicon.ico +0 -0
  67. package/src/openapi/assets/favicon/site.webmanifest +11 -0
  68. package/src/openapi/assets/logo.svg +12 -0
  69. package/src/openapi/assets/logo_dark.svg +6 -0
  70. package/src/openapi/assets/logo_icon.png +0 -0
  71. package/src/openapi/assets/logo_white.svg +6 -0
  72. package/src/openapi/docs-ui.ts +153 -35
  73. package/src/openapi/generator.ts +341 -6
  74. package/src/start-vector.ts +50 -0
  75. package/src/types/index.ts +34 -0
  76. package/src/utils/logger.ts +1 -1
  77. package/src/utils/validation.ts +2 -0
package/dist/cli.js CHANGED
@@ -5,29 +5,164 @@
5
5
  import { watch } from "fs";
6
6
  import { parseArgs } from "util";
7
7
 
8
- // src/auth/protected.ts
9
- class AuthManager {
10
- protectedHandler = null;
11
- setProtectedHandler(handler) {
12
- this.protectedHandler = handler;
8
+ // src/cli/option-resolution.ts
9
+ function resolveRoutesDir(configRoutesDir, hasRoutesOption, cliRoutes) {
10
+ if (hasRoutesOption) {
11
+ return cliRoutes;
13
12
  }
14
- async authenticate(request) {
15
- if (!this.protectedHandler) {
16
- throw new Error("Protected handler not configured. Use vector.protected() to set authentication handler.");
13
+ return configRoutesDir ?? cliRoutes;
14
+ }
15
+ function parseAndValidatePort(value) {
16
+ const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
17
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
18
+ throw new Error(`Invalid port value: ${String(value)}`);
19
+ }
20
+ return parsed;
21
+ }
22
+ function resolvePort(configPort, hasPortOption, cliPort) {
23
+ if (hasPortOption) {
24
+ return parseAndValidatePort(cliPort);
25
+ }
26
+ const resolved = configPort ?? cliPort;
27
+ return parseAndValidatePort(resolved);
28
+ }
29
+ function resolveHost(configHost, hasHostOption, cliHost) {
30
+ const resolved = hasHostOption ? cliHost : configHost ?? cliHost;
31
+ if (typeof resolved !== "string" || resolved.length === 0) {
32
+ throw new Error(`Invalid host value: ${String(resolved)}`);
33
+ }
34
+ return resolved;
35
+ }
36
+
37
+ // src/cli/graceful-shutdown.ts
38
+ function installGracefulShutdownHandlers(options) {
39
+ const on = options.on ?? ((event, listener) => process.on(event, listener));
40
+ const off = options.off ?? ((event, listener) => process.off(event, listener));
41
+ const exit = options.exit ?? ((code) => process.exit(code));
42
+ const logError = options.logError ?? console.error;
43
+ let shuttingDown = false;
44
+ const handleSignal = async (signal) => {
45
+ if (shuttingDown) {
46
+ return;
17
47
  }
48
+ shuttingDown = true;
18
49
  try {
19
- const authUser = await this.protectedHandler(request);
20
- request.authUser = authUser;
21
- return authUser;
50
+ const target = options.getTarget();
51
+ if (target) {
52
+ if (typeof target.shutdown === "function") {
53
+ await target.shutdown();
54
+ } else if (typeof target.stop === "function") {
55
+ target.stop();
56
+ }
57
+ }
58
+ exit(0);
22
59
  } catch (error) {
23
- throw new Error(`Authentication failed: ${error instanceof Error ? error.message : String(error)}`);
60
+ logError(`[vector] Graceful shutdown failed after ${signal}:`, error);
61
+ exit(1);
24
62
  }
63
+ };
64
+ const onSigint = () => {
65
+ handleSignal("SIGINT");
66
+ };
67
+ const onSigterm = () => {
68
+ handleSignal("SIGTERM");
69
+ };
70
+ on("SIGINT", onSigint);
71
+ on("SIGTERM", onSigterm);
72
+ return () => {
73
+ off("SIGINT", onSigint);
74
+ off("SIGTERM", onSigterm);
75
+ };
76
+ }
77
+
78
+ // src/core/config-loader.ts
79
+ import { existsSync } from "fs";
80
+ import { resolve, isAbsolute } from "path";
81
+
82
+ // src/utils/path.ts
83
+ function toFileUrl(path) {
84
+ return process.platform === "win32" ? `file:///${path.replace(/\\/g, "/")}` : path;
85
+ }
86
+ function buildRouteRegex(path) {
87
+ return RegExp(`^${path.replace(/\/+(\/|$)/g, "$1").replace(/(\/?\.?):(\w+)\+/g, "($1(?<$2>[\\s\\S]+))").replace(/(\/?\.?):(\w+)/g, "($1(?<$2>[^$1/]+?))").replace(/\./g, "\\.").replace(/(\/?)\*/g, "($1.*)?")}/*$`);
88
+ }
89
+
90
+ // src/core/config-loader.ts
91
+ class ConfigLoader {
92
+ configPath;
93
+ config = null;
94
+ configSource = "default";
95
+ constructor(configPath) {
96
+ const path = configPath || "vector.config.ts";
97
+ this.configPath = isAbsolute(path) ? path : resolve(process.cwd(), path);
25
98
  }
26
- isAuthenticated(request) {
27
- return !!request.authUser;
99
+ async load() {
100
+ if (existsSync(this.configPath)) {
101
+ try {
102
+ const userConfigPath = toFileUrl(this.configPath);
103
+ const userConfig = await import(userConfigPath);
104
+ this.config = userConfig.default || userConfig;
105
+ this.configSource = "user";
106
+ } catch (error) {
107
+ const msg = error instanceof Error ? error.message : String(error);
108
+ console.error(`[vector] Failed to load config from ${this.configPath}: ${msg}`);
109
+ console.error("[vector] Server is using default configuration. Fix your config file and restart.");
110
+ this.config = {};
111
+ }
112
+ } else {
113
+ this.config = {};
114
+ }
115
+ return await this.buildLegacyConfig();
28
116
  }
29
- getUser(request) {
30
- return request.authUser || null;
117
+ getConfigSource() {
118
+ return this.configSource;
119
+ }
120
+ async buildLegacyConfig() {
121
+ const config = {};
122
+ if (this.config) {
123
+ config.port = this.config.port;
124
+ config.hostname = this.config.hostname;
125
+ config.reusePort = this.config.reusePort;
126
+ config.development = this.config.development;
127
+ config.routesDir = this.config.routesDir || "./routes";
128
+ config.routeExcludePatterns = this.config.routeExcludePatterns;
129
+ config.idleTimeout = this.config.idleTimeout;
130
+ config.defaults = this.config.defaults;
131
+ config.openapi = this.config.openapi;
132
+ config.startup = this.config.startup;
133
+ config.shutdown = this.config.shutdown;
134
+ }
135
+ config.autoDiscover = true;
136
+ if (this.config?.cors) {
137
+ if (typeof this.config.cors === "boolean") {
138
+ config.cors = this.config.cors ? {
139
+ origin: "*",
140
+ credentials: true,
141
+ allowHeaders: "Content-Type, Authorization",
142
+ allowMethods: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
143
+ exposeHeaders: "Authorization",
144
+ maxAge: 86400
145
+ } : undefined;
146
+ } else {
147
+ config.cors = this.config.cors;
148
+ }
149
+ }
150
+ if (this.config?.before) {
151
+ config.before = this.config.before;
152
+ }
153
+ if (this.config?.after) {
154
+ config.finally = this.config.after;
155
+ }
156
+ return config;
157
+ }
158
+ async loadAuthHandler() {
159
+ return this.config?.auth || null;
160
+ }
161
+ async loadCacheHandler() {
162
+ return this.config?.cache || null;
163
+ }
164
+ getConfig() {
165
+ return this.config;
31
166
  }
32
167
  }
33
168
 
@@ -113,6 +248,35 @@ var STATIC_RESPONSES = {
113
248
  })
114
249
  };
115
250
 
251
+ // src/auth/protected.ts
252
+ class AuthManager {
253
+ protectedHandler = null;
254
+ setProtectedHandler(handler) {
255
+ this.protectedHandler = handler;
256
+ }
257
+ clearProtectedHandler() {
258
+ this.protectedHandler = null;
259
+ }
260
+ async authenticate(request) {
261
+ if (!this.protectedHandler) {
262
+ throw new Error("Protected handler not configured. Use vector.protected() to set authentication handler.");
263
+ }
264
+ try {
265
+ const authUser = await this.protectedHandler(request);
266
+ request.authUser = authUser;
267
+ return authUser;
268
+ } catch (error) {
269
+ throw new Error(`Authentication failed: ${error instanceof Error ? error.message : String(error)}`);
270
+ }
271
+ }
272
+ isAuthenticated(request) {
273
+ return !!request.authUser;
274
+ }
275
+ getUser(request) {
276
+ return request.authUser || null;
277
+ }
278
+ }
279
+
116
280
  // src/cache/manager.ts
117
281
  class CacheManager {
118
282
  cacheHandler = null;
@@ -122,6 +286,9 @@ class CacheManager {
122
286
  setCacheHandler(handler) {
123
287
  this.cacheHandler = handler;
124
288
  }
289
+ clearCacheHandler() {
290
+ this.cacheHandler = null;
291
+ }
125
292
  async get(key, factory, ttl = DEFAULT_CONFIG.CACHE_TTL) {
126
293
  if (ttl <= 0) {
127
294
  return factory();
@@ -294,8 +461,8 @@ ${routeEntries.join(`,
294
461
  }
295
462
 
296
463
  // src/dev/route-scanner.ts
297
- import { existsSync, promises as fs2 } from "fs";
298
- import { join, relative as relative2, resolve, sep } from "path";
464
+ import { existsSync as existsSync2, promises as fs2 } from "fs";
465
+ import { join, relative as relative2, resolve as resolve2, sep } from "path";
299
466
 
300
467
  class RouteScanner {
301
468
  routesDir;
@@ -317,12 +484,12 @@ class RouteScanner {
317
484
  "*.d.ts"
318
485
  ];
319
486
  constructor(routesDir = "./routes", excludePatterns) {
320
- this.routesDir = resolve(process.cwd(), routesDir);
487
+ this.routesDir = resolve2(process.cwd(), routesDir);
321
488
  this.excludePatterns = excludePatterns || RouteScanner.DEFAULT_EXCLUDE_PATTERNS;
322
489
  }
323
490
  async scan() {
324
491
  const routes = [];
325
- if (!existsSync(this.routesDir)) {
492
+ if (!existsSync2(this.routesDir)) {
326
493
  return [];
327
494
  }
328
495
  try {
@@ -465,14 +632,6 @@ class MiddlewareManager {
465
632
  }
466
633
  }
467
634
 
468
- // src/utils/path.ts
469
- function toFileUrl(path) {
470
- return process.platform === "win32" ? `file:///${path.replace(/\\/g, "/")}` : path;
471
- }
472
- function buildRouteRegex(path) {
473
- return RegExp(`^${path.replace(/\/+(\/|$)/g, "$1").replace(/(\/?\.?):(\w+)\+/g, "($1(?<$2>[\\s\\S]+))").replace(/(\/?\.?):(\w+)/g, "($1(?<$2>[^$1/]+?))").replace(/\./g, "\\.").replace(/(\/?)\*/g, "($1.*)?")}/*$`);
474
- }
475
-
476
635
  // src/http.ts
477
636
  function stringifyData(data) {
478
637
  const val = data ?? null;
@@ -1143,7 +1302,7 @@ class VectorRouter {
1143
1302
  }
1144
1303
 
1145
1304
  // src/core/server.ts
1146
- import { existsSync as existsSync2 } from "fs";
1305
+ import { existsSync as existsSync3 } from "fs";
1147
1306
  import { join as join2 } from "path";
1148
1307
 
1149
1308
  // src/utils/cors.ts
@@ -1231,16 +1390,26 @@ function cors(config) {
1231
1390
  }
1232
1391
 
1233
1392
  // src/openapi/docs-ui.ts
1234
- function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
1393
+ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPath, logoWhitePath, appleTouchIconPath, favicon32Path, favicon16Path, webManifestPath) {
1235
1394
  const specJson = JSON.stringify(spec).replace(/<\/script/gi, "<\\/script");
1236
1395
  const openapiPathJson = JSON.stringify(openapiPath);
1237
1396
  const tailwindScriptPathJson = JSON.stringify(tailwindScriptPath);
1397
+ const logoDarkPathJson = JSON.stringify(logoDarkPath);
1398
+ const logoWhitePathJson = JSON.stringify(logoWhitePath);
1399
+ const appleTouchIconPathJson = JSON.stringify(appleTouchIconPath);
1400
+ const favicon32PathJson = JSON.stringify(favicon32Path);
1401
+ const favicon16PathJson = JSON.stringify(favicon16Path);
1402
+ const webManifestPathJson = JSON.stringify(webManifestPath);
1238
1403
  return `<!DOCTYPE html>
1239
1404
  <html lang="en">
1240
1405
  <head>
1241
1406
  <meta charset="UTF-8">
1242
1407
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1243
1408
  <title>Vector API Documentation</title>
1409
+ <link rel="apple-touch-icon" sizes="180x180" href=${appleTouchIconPathJson}>
1410
+ <link rel="icon" type="image/png" sizes="32x32" href=${favicon32PathJson}>
1411
+ <link rel="icon" type="image/png" sizes="16x16" href=${favicon16PathJson}>
1412
+ <link rel="manifest" href=${webManifestPathJson}>
1244
1413
  <script>
1245
1414
  if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
1246
1415
  document.documentElement.classList.add('dark');
@@ -1256,7 +1425,12 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
1256
1425
  theme: {
1257
1426
  extend: {
1258
1427
  colors: {
1259
- brand: '#6366F1',
1428
+ brand: {
1429
+ DEFAULT: '#00A1FF',
1430
+ mint: '#00FF8F',
1431
+ soft: '#E4F5FF',
1432
+ deep: '#007BC5',
1433
+ },
1260
1434
  dark: { bg: '#0A0A0A', surface: '#111111', border: '#1F1F1F', text: '#EDEDED' },
1261
1435
  light: { bg: '#FFFFFF', surface: '#F9F9F9', border: '#E5E5E5', text: '#111111' }
1262
1436
  },
@@ -1323,32 +1497,44 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
1323
1497
  from { opacity: 0; transform: translateX(-6px); }
1324
1498
  to { opacity: 1; transform: translateX(0); }
1325
1499
  }
1500
+ @keyframes spin {
1501
+ to { transform: rotate(360deg); }
1502
+ }
1503
+ .button-spinner {
1504
+ display: inline-block;
1505
+ width: 0.875rem;
1506
+ height: 0.875rem;
1507
+ border: 2px solid currentColor;
1508
+ border-right-color: transparent;
1509
+ border-radius: 9999px;
1510
+ animation: spin 700ms linear infinite;
1511
+ }
1326
1512
  @media (prefers-reduced-motion: reduce) {
1327
1513
  *, *::before, *::after {
1328
1514
  animation: none !important;
1329
1515
  transition: none !important;
1330
1516
  }
1331
1517
  }
1332
- .json-key { color: #0f766e; }
1333
- .json-string { color: #0369a1; }
1334
- .json-number { color: #7c3aed; }
1335
- .json-boolean { color: #b45309; }
1336
- .json-null { color: #be123c; }
1337
- .dark .json-key { color: #5eead4; }
1338
- .dark .json-string { color: #7dd3fc; }
1339
- .dark .json-number { color: #c4b5fd; }
1340
- .dark .json-boolean { color: #fcd34d; }
1341
- .dark .json-null { color: #fda4af; }
1518
+ .json-key { color: #007bc5; }
1519
+ .json-string { color: #334155; }
1520
+ .json-number { color: #00a1ff; }
1521
+ .json-boolean { color: #475569; }
1522
+ .json-null { color: #64748b; }
1523
+ .dark .json-key { color: #7dc9ff; }
1524
+ .dark .json-string { color: #d1d9e6; }
1525
+ .dark .json-number { color: #7dc9ff; }
1526
+ .dark .json-boolean { color: #93a4bf; }
1527
+ .dark .json-null { color: #7c8ba3; }
1342
1528
  </style>
1343
1529
  </head>
1344
1530
  <body class="bg-light-bg text-light-text dark:bg-dark-bg dark:text-dark-text font-sans antialiased flex h-screen overflow-hidden">
1345
1531
  <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>
1346
1532
  <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">
1347
1533
  <div class="h-14 flex items-center px-5 border-b border-light-border dark:border-dark-border">
1348
- <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">
1349
- <polygon points="3 21 12 2 21 21 12 15 3 21"></polygon>
1350
- </svg>
1351
- <span class="ml-2.5 font-bold tracking-tight text-lg">Vector</span>
1534
+ <div class="flex items-center">
1535
+ <img src=${logoDarkPathJson} alt="Vector" class="h-6 w-auto block dark:hidden" />
1536
+ <img src=${logoWhitePathJson} alt="Vector" class="h-6 w-auto hidden dark:block" />
1537
+ </div>
1352
1538
  <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">
1353
1539
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1354
1540
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
@@ -1379,8 +1565,8 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
1379
1565
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
1380
1566
  </svg>
1381
1567
  </button>
1382
- <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>
1383
- <span class="font-bold tracking-tight">Vector</span>
1568
+ <img src=${logoDarkPathJson} alt="Vector" class="h-5 w-auto block dark:hidden" />
1569
+ <img src=${logoWhitePathJson} alt="Vector" class="h-5 w-auto hidden dark:block" />
1384
1570
  </div>
1385
1571
  <div class="flex-1"></div>
1386
1572
  <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">
@@ -1405,17 +1591,22 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
1405
1591
  <div class="grid grid-cols-1 lg:grid-cols-12 gap-10">
1406
1592
  <div class="lg:col-span-5 space-y-8" id="params-column"></div>
1407
1593
  <div class="lg:col-span-7">
1408
- <div class="rounded-lg border border-light-border dark:border-[#1F1F1F] bg-light-bg dark:bg-[#111111] overflow-hidden group">
1409
- <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]">
1410
- <span class="text-xs font-mono text-light-text/70 dark:text-[#EDEDED]/70">cURL</span>
1411
- <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>
1594
+ <div class="rounded-lg border border-light-border dark:border-dark-border bg-light-bg dark:bg-dark-bg overflow-hidden group">
1595
+ <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">
1596
+ <span class="text-xs font-mono text-light-text/70 dark:text-dark-text/70">cURL</span>
1597
+ <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>
1412
1598
  </div>
1413
- <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>
1599
+ <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>
1414
1600
  </div>
1415
1601
  <div class="mt-4 p-4 rounded-lg border border-light-border dark:border-dark-border bg-light-surface dark:bg-dark-surface">
1416
1602
  <div class="flex items-center justify-between mb-3">
1417
1603
  <h4 class="text-sm font-medium">Try it out</h4>
1418
- <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>
1604
+ <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">
1605
+ <span class="inline-flex items-center gap-2">
1606
+ <span id="send-btn-spinner" class="button-spinner hidden" aria-hidden="true"></span>
1607
+ <span id="send-btn-label">Submit</span>
1608
+ </span>
1609
+ </button>
1419
1610
  </div>
1420
1611
  <div class="space-y-4">
1421
1612
  <div>
@@ -1449,7 +1640,7 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
1449
1640
  </div>
1450
1641
  </div>
1451
1642
 
1452
- <div>
1643
+ <div id="response-section">
1453
1644
  <div class="flex items-center justify-between mb-2">
1454
1645
  <p class="text-xs font-semibold uppercase tracking-wider opacity-60">Response</p>
1455
1646
  </div>
@@ -1476,7 +1667,7 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
1476
1667
  <div class="flex items-center justify-between mb-3">
1477
1668
  <h3 id="expand-modal-title" class="text-sm font-semibold">Expanded View</h3>
1478
1669
  <div class="flex items-center gap-2">
1479
- <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>
1670
+ <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>
1480
1671
  <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">
1481
1672
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1482
1673
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
@@ -1495,12 +1686,13 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
1495
1686
  <script>
1496
1687
  const spec = ${specJson};
1497
1688
  const openapiPath = ${openapiPathJson};
1689
+ const methodBadgeDefault = "bg-black/5 text-light-text/80 dark:bg-white/10 dark:text-dark-text/80";
1498
1690
  const methodBadge = {
1499
- GET: "bg-green-100 text-green-700 dark:bg-green-500/10 dark:text-green-400",
1500
- POST: "bg-blue-100 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400",
1501
- PUT: "bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400",
1502
- PATCH: "bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400",
1503
- DELETE: "bg-red-100 text-red-700 dark:bg-red-500/10 dark:text-red-400",
1691
+ GET: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
1692
+ POST: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
1693
+ PUT: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
1694
+ PATCH: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
1695
+ DELETE: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
1504
1696
  };
1505
1697
 
1506
1698
  function getOperations() {
@@ -1754,6 +1946,16 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
1754
1946
  return;
1755
1947
  }
1756
1948
  for (const [tag, ops] of groups.entries()) {
1949
+ ops.sort((a, b) => {
1950
+ const byName = a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
1951
+ if (byName !== 0) return byName;
1952
+
1953
+ const byPath = a.path.localeCompare(b.path, undefined, { sensitivity: "base" });
1954
+ if (byPath !== 0) return byPath;
1955
+
1956
+ return a.method.localeCompare(b.method, undefined, { sensitivity: "base" });
1957
+ });
1958
+
1757
1959
  const block = document.createElement("div");
1758
1960
  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>';
1759
1961
  block.querySelector("h3").textContent = tag;
@@ -1765,14 +1967,14 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
1765
1967
  const a = document.createElement("a");
1766
1968
  a.href = "#";
1767
1969
  a.className = op === selected
1768
- ? "block px-2 py-1.5 rounded-md bg-black/5 dark:bg-white/5 text-brand font-medium transition-colors"
1970
+ ? "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"
1769
1971
  : "block px-2 py-1.5 rounded-md hover:bg-black/5 dark:hover:bg-white/5 transition-colors";
1770
1972
 
1771
1973
  const row = document.createElement("span");
1772
1974
  row.className = "flex items-center gap-2";
1773
1975
 
1774
1976
  const method = document.createElement("span");
1775
- 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");
1977
+ method.className = "px-1.5 py-0.5 rounded text-[10px] font-mono font-semibold " + (methodBadge[op.method] || methodBadgeDefault);
1776
1978
  method.textContent = op.method;
1777
1979
 
1778
1980
  const name = document.createElement("span");
@@ -1911,6 +2113,55 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
1911
2113
  );
1912
2114
  }
1913
2115
 
2116
+ function renderResponseSchemasSection(responses) {
2117
+ if (!responses || typeof responses !== "object") return "";
2118
+
2119
+ const statusCodes = Object.keys(responses).sort((a, b) => {
2120
+ const aNum = Number(a);
2121
+ const bNum = Number(b);
2122
+ if (Number.isInteger(aNum) && Number.isInteger(bNum)) return aNum - bNum;
2123
+ if (Number.isInteger(aNum)) return -1;
2124
+ if (Number.isInteger(bNum)) return 1;
2125
+ return a.localeCompare(b);
2126
+ });
2127
+
2128
+ let sections = "";
2129
+ for (const statusCode of statusCodes) {
2130
+ const responseDef = responses[statusCode];
2131
+ if (!responseDef || typeof responseDef !== "object") continue;
2132
+
2133
+ const jsonSchema =
2134
+ responseDef.content &&
2135
+ responseDef.content["application/json"] &&
2136
+ responseDef.content["application/json"].schema;
2137
+
2138
+ if (!jsonSchema || typeof jsonSchema !== "object") continue;
2139
+
2140
+ const rootChildren = buildSchemaChildren(jsonSchema);
2141
+ if (!rootChildren.length) continue;
2142
+
2143
+ let rows = "";
2144
+ for (const child of rootChildren) {
2145
+ rows += renderSchemaFieldNode(child, 0);
2146
+ }
2147
+
2148
+ sections +=
2149
+ '<div class="mb-4"><h4 class="text-xs font-mono uppercase tracking-wider opacity-70 mb-2">Status ' +
2150
+ escapeHtml(statusCode) +
2151
+ "</h4>" +
2152
+ rows +
2153
+ "</div>";
2154
+ }
2155
+
2156
+ if (!sections) return "";
2157
+
2158
+ return (
2159
+ '<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>' +
2160
+ sections +
2161
+ "</div>"
2162
+ );
2163
+ }
2164
+
1914
2165
  function renderTryItParameterInputs(pathParams, queryParams) {
1915
2166
  const container = document.getElementById("request-param-inputs");
1916
2167
  if (!container || !selected) return;
@@ -2194,6 +2445,19 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
2194
2445
  }
2195
2446
  }
2196
2447
 
2448
+ function setSubmitLoading(isLoading) {
2449
+ const sendButton = document.getElementById("send-btn");
2450
+ const spinner = document.getElementById("send-btn-spinner");
2451
+ const label = document.getElementById("send-btn-label");
2452
+ if (!sendButton) return;
2453
+
2454
+ sendButton.disabled = isLoading;
2455
+ sendButton.classList.toggle("opacity-80", isLoading);
2456
+ sendButton.classList.toggle("cursor-wait", isLoading);
2457
+ if (spinner) spinner.classList.toggle("hidden", !isLoading);
2458
+ if (label) label.textContent = isLoading ? "Sending..." : "Submit";
2459
+ }
2460
+
2197
2461
  function updateRequestPreview() {
2198
2462
  if (!selected) return;
2199
2463
 
@@ -2257,7 +2521,7 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
2257
2521
  document.getElementById("tag-description").textContent = op.description || "Interactive API documentation.";
2258
2522
  const methodNode = document.getElementById("endpoint-method");
2259
2523
  methodNode.textContent = selected.method;
2260
- 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");
2524
+ methodNode.className = "px-2.5 py-0.5 rounded-full text-xs font-mono font-medium " + (methodBadge[selected.method] || methodBadgeDefault);
2261
2525
  document.getElementById("endpoint-title").textContent = selected.name;
2262
2526
  document.getElementById("endpoint-path").textContent = selected.path;
2263
2527
 
@@ -2270,6 +2534,7 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
2270
2534
  html += renderParamSection("Header Parameters", headers);
2271
2535
 
2272
2536
  html += renderRequestBodySchemaSection(reqSchema);
2537
+ html += renderResponseSchemasSection(op.responses);
2273
2538
  document.getElementById("params-column").innerHTML = html || '<div class="text-sm opacity-70">No parameters</div>';
2274
2539
  renderTryItParameterInputs(path, query);
2275
2540
  renderHeaderInputs();
@@ -2320,14 +2585,18 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
2320
2585
  headers["Content-Type"] = "application/json";
2321
2586
  }
2322
2587
 
2588
+ setSubmitLoading(true);
2323
2589
  try {
2590
+ const requestStart = performance.now();
2324
2591
  const response = await fetch(requestPath, { method: selected.method, headers, body: body || undefined });
2325
2592
  const text = await response.text();
2593
+ const responseTimeMs = Math.round(performance.now() - requestStart);
2326
2594
  const contentType = response.headers.get("content-type") || "unknown";
2327
2595
  const formattedResponse = formatResponseText(text);
2328
2596
  const headerText =
2329
2597
  "Status: " + response.status + " " + response.statusText + "\\n" +
2330
- "Content-Type: " + contentType + "\\n\\n";
2598
+ "Content-Type: " + contentType + "\\n" +
2599
+ "Response Time: " + responseTimeMs + " ms\\n\\n";
2331
2600
  setResponseContent(
2332
2601
  headerText,
2333
2602
  formattedResponse.text,
@@ -2335,6 +2604,8 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath) {
2335
2604
  );
2336
2605
  } catch (error) {
2337
2606
  setResponseContent("", "Request failed: " + String(error), false);
2607
+ } finally {
2608
+ setSubmitLoading(false);
2338
2609
  }
2339
2610
  });
2340
2611
 
@@ -2630,29 +2901,325 @@ function getResponseDescription(status) {
2630
2901
  }
2631
2902
  function convertInputSchema(routePath, inputSchema, target, warnings) {
2632
2903
  if (!isJSONSchemaCapable(inputSchema)) {
2633
- return null;
2904
+ const fallback = buildFallbackJSONSchema(inputSchema);
2905
+ return isEmptyObjectSchema(fallback) ? null : fallback;
2634
2906
  }
2635
2907
  try {
2636
2908
  return inputSchema["~standard"].jsonSchema.input({ target });
2637
2909
  } catch (error) {
2638
- warnings.push(`[OpenAPI] Failed input schema conversion for ${routePath}: ${error instanceof Error ? error.message : String(error)}`);
2639
- return null;
2910
+ const alternate = tryAlternateTargetConversion(inputSchema, "input", target, error, routePath, undefined, warnings);
2911
+ if (alternate) {
2912
+ return alternate;
2913
+ }
2914
+ warnings.push(`[OpenAPI] Failed input schema conversion for ${routePath}: ${error instanceof Error ? error.message : String(error)}. Falling back to a permissive JSON Schema.`);
2915
+ const fallback = buildFallbackJSONSchema(inputSchema);
2916
+ return isEmptyObjectSchema(fallback) ? null : fallback;
2640
2917
  }
2641
2918
  }
2642
2919
  function convertOutputSchema(routePath, statusCode, outputSchema, target, warnings) {
2643
2920
  if (!isJSONSchemaCapable(outputSchema)) {
2644
- return null;
2921
+ const fallback = buildFallbackJSONSchema(outputSchema);
2922
+ return isEmptyObjectSchema(fallback) ? null : fallback;
2645
2923
  }
2646
2924
  try {
2647
2925
  return outputSchema["~standard"].jsonSchema.output({ target });
2648
2926
  } catch (error) {
2649
- warnings.push(`[OpenAPI] Failed output schema conversion for ${routePath} (${statusCode}): ${error instanceof Error ? error.message : String(error)}`);
2650
- return null;
2927
+ const alternate = tryAlternateTargetConversion(outputSchema, "output", target, error, routePath, statusCode, warnings);
2928
+ if (alternate) {
2929
+ return alternate;
2930
+ }
2931
+ warnings.push(`[OpenAPI] Failed output schema conversion for ${routePath} (${statusCode}): ${error instanceof Error ? error.message : String(error)}. Falling back to a permissive JSON Schema.`);
2932
+ return buildFallbackJSONSchema(outputSchema);
2651
2933
  }
2652
2934
  }
2653
2935
  function isRecord(value) {
2654
2936
  return !!value && typeof value === "object" && !Array.isArray(value);
2655
2937
  }
2938
+ function isEmptyObjectSchema(value) {
2939
+ return isRecord(value) && Object.keys(value).length === 0;
2940
+ }
2941
+ function tryAlternateTargetConversion(schema, kind, target, originalError, routePath, statusCode, warnings) {
2942
+ if (!isJSONSchemaCapable(schema)) {
2943
+ return null;
2944
+ }
2945
+ const message = originalError instanceof Error ? originalError.message : String(originalError);
2946
+ const unsupportedOpenAPITarget = target === "openapi-3.0" && message.includes("target 'openapi-3.0' is not supported") && message.includes("draft-2020-12") && message.includes("draft-07");
2947
+ if (!unsupportedOpenAPITarget) {
2948
+ return null;
2949
+ }
2950
+ try {
2951
+ const converted = schema["~standard"].jsonSchema[kind]({ target: "draft-07" });
2952
+ warnings.push(kind === "input" ? `[OpenAPI] ${routePath} converter does not support openapi-3.0 target; using draft-07 conversion output.` : `[OpenAPI] ${routePath} (${statusCode}) converter does not support openapi-3.0 target; using draft-07 conversion output.`);
2953
+ return converted;
2954
+ } catch {
2955
+ return null;
2956
+ }
2957
+ }
2958
+ function getValidatorSchemaDef(schema) {
2959
+ if (!schema || typeof schema !== "object")
2960
+ return null;
2961
+ const value = schema;
2962
+ if (isRecord(value._def))
2963
+ return value._def;
2964
+ if (isRecord(value._zod) && isRecord(value._zod.def)) {
2965
+ return value._zod.def;
2966
+ }
2967
+ if (value.kind === "schema" && typeof value.type === "string") {
2968
+ return value;
2969
+ }
2970
+ return null;
2971
+ }
2972
+ function getSchemaKind(def) {
2973
+ if (!def)
2974
+ return null;
2975
+ const typeName = def.typeName;
2976
+ if (typeof typeName === "string")
2977
+ return typeName;
2978
+ const type = def.type;
2979
+ if (typeof type === "string")
2980
+ return type;
2981
+ return null;
2982
+ }
2983
+ function pickSchemaChild(def) {
2984
+ const candidates = ["innerType", "schema", "type", "out", "in", "left", "right", "wrapped", "element"];
2985
+ for (const key of candidates) {
2986
+ if (key in def)
2987
+ return def[key];
2988
+ }
2989
+ return;
2990
+ }
2991
+ function pickSchemaObjectCandidate(def, keys) {
2992
+ for (const key of keys) {
2993
+ const value = def[key];
2994
+ if (value && typeof value === "object" && !Array.isArray(value)) {
2995
+ return value;
2996
+ }
2997
+ }
2998
+ return;
2999
+ }
3000
+ function isOptionalWrapperKind(kind) {
3001
+ if (!kind)
3002
+ return false;
3003
+ const lower = kind.toLowerCase();
3004
+ return lower.includes("optional") || lower.includes("default") || lower.includes("catch");
3005
+ }
3006
+ function unwrapOptionalForRequired(schema) {
3007
+ let current = schema;
3008
+ let optional = false;
3009
+ let guard = 0;
3010
+ while (guard < 8) {
3011
+ guard += 1;
3012
+ const def = getValidatorSchemaDef(current);
3013
+ const kind = getSchemaKind(def);
3014
+ if (!def || !isOptionalWrapperKind(kind))
3015
+ break;
3016
+ optional = true;
3017
+ const inner = pickSchemaChild(def);
3018
+ if (!inner)
3019
+ break;
3020
+ current = inner;
3021
+ }
3022
+ return { schema: current, optional };
3023
+ }
3024
+ function getObjectShape(def) {
3025
+ const entries = def.entries;
3026
+ if (isRecord(entries)) {
3027
+ return entries;
3028
+ }
3029
+ const rawShape = def.shape;
3030
+ if (typeof rawShape === "function") {
3031
+ try {
3032
+ const resolved = rawShape();
3033
+ return isRecord(resolved) ? resolved : {};
3034
+ } catch {
3035
+ return {};
3036
+ }
3037
+ }
3038
+ return isRecord(rawShape) ? rawShape : {};
3039
+ }
3040
+ function extractEnumValues(def) {
3041
+ const values = def.values;
3042
+ if (Array.isArray(values))
3043
+ return values;
3044
+ if (values && typeof values === "object")
3045
+ return Object.values(values);
3046
+ const entries = def.entries;
3047
+ if (entries && typeof entries === "object")
3048
+ return Object.values(entries);
3049
+ const enumObject = def.enum;
3050
+ if (enumObject && typeof enumObject === "object")
3051
+ return Object.values(enumObject);
3052
+ const options = def.options;
3053
+ if (Array.isArray(options)) {
3054
+ return options.map((item) => {
3055
+ if (item && typeof item === "object" && "unit" in item) {
3056
+ return item.unit;
3057
+ }
3058
+ return item;
3059
+ }).filter((item) => item !== undefined);
3060
+ }
3061
+ return [];
3062
+ }
3063
+ function mapPrimitiveKind(kind) {
3064
+ const lower = kind.toLowerCase();
3065
+ if (lower.includes("string"))
3066
+ return { type: "string" };
3067
+ if (lower.includes("number"))
3068
+ return { type: "number" };
3069
+ if (lower.includes("boolean"))
3070
+ return { type: "boolean" };
3071
+ if (lower.includes("bigint"))
3072
+ return { type: "string" };
3073
+ if (lower === "null" || lower.includes("zodnull"))
3074
+ return { type: "null" };
3075
+ if (lower.includes("any") || lower.includes("unknown") || lower.includes("never"))
3076
+ return {};
3077
+ if (lower.includes("date"))
3078
+ return { type: "string", format: "date-time" };
3079
+ if (lower.includes("custom"))
3080
+ return { type: "object", additionalProperties: true };
3081
+ return null;
3082
+ }
3083
+ function buildIntrospectedFallbackJSONSchema(schema, seen = new WeakSet) {
3084
+ if (!schema || typeof schema !== "object")
3085
+ return {};
3086
+ if (seen.has(schema))
3087
+ return {};
3088
+ seen.add(schema);
3089
+ const def = getValidatorSchemaDef(schema);
3090
+ const kind = getSchemaKind(def);
3091
+ if (!def || !kind)
3092
+ return {};
3093
+ const primitive = mapPrimitiveKind(kind);
3094
+ if (primitive)
3095
+ return primitive;
3096
+ const lower = kind.toLowerCase();
3097
+ if (lower.includes("object")) {
3098
+ const shape = getObjectShape(def);
3099
+ const properties = {};
3100
+ const required = [];
3101
+ for (const [key, child2] of Object.entries(shape)) {
3102
+ const unwrapped = unwrapOptionalForRequired(child2);
3103
+ properties[key] = buildIntrospectedFallbackJSONSchema(unwrapped.schema, seen);
3104
+ if (!unwrapped.optional)
3105
+ required.push(key);
3106
+ }
3107
+ const out = {
3108
+ type: "object",
3109
+ properties,
3110
+ additionalProperties: true
3111
+ };
3112
+ if (required.length > 0) {
3113
+ out.required = required;
3114
+ }
3115
+ return out;
3116
+ }
3117
+ if (lower.includes("array")) {
3118
+ const itemSchema = pickSchemaObjectCandidate(def, ["element", "items", "innerType", "type"]) ?? {};
3119
+ return {
3120
+ type: "array",
3121
+ items: buildIntrospectedFallbackJSONSchema(itemSchema, seen)
3122
+ };
3123
+ }
3124
+ if (lower.includes("record")) {
3125
+ const valueType = def.valueType ?? def.valueSchema;
3126
+ return {
3127
+ type: "object",
3128
+ additionalProperties: valueType ? buildIntrospectedFallbackJSONSchema(valueType, seen) : true
3129
+ };
3130
+ }
3131
+ if (lower.includes("tuple")) {
3132
+ const items = Array.isArray(def.items) ? def.items : [];
3133
+ const prefixItems = items.map((item) => buildIntrospectedFallbackJSONSchema(item, seen));
3134
+ return {
3135
+ type: "array",
3136
+ prefixItems,
3137
+ minItems: prefixItems.length,
3138
+ maxItems: prefixItems.length
3139
+ };
3140
+ }
3141
+ if (lower.includes("union")) {
3142
+ const options = def.options ?? def.schemas ?? [];
3143
+ if (!Array.isArray(options) || options.length === 0)
3144
+ return {};
3145
+ return {
3146
+ anyOf: options.map((option) => buildIntrospectedFallbackJSONSchema(option, seen))
3147
+ };
3148
+ }
3149
+ if (lower.includes("intersection")) {
3150
+ const left = def.left;
3151
+ const right = def.right;
3152
+ if (!left || !right)
3153
+ return {};
3154
+ return {
3155
+ allOf: [buildIntrospectedFallbackJSONSchema(left, seen), buildIntrospectedFallbackJSONSchema(right, seen)]
3156
+ };
3157
+ }
3158
+ if (lower.includes("enum")) {
3159
+ const enumValues = extractEnumValues(def);
3160
+ if (enumValues.length > 0) {
3161
+ const allString = enumValues.every((v) => typeof v === "string");
3162
+ const allNumber = enumValues.every((v) => typeof v === "number");
3163
+ const allBoolean = enumValues.every((v) => typeof v === "boolean");
3164
+ if (allString)
3165
+ return { type: "string", enum: enumValues };
3166
+ if (allNumber)
3167
+ return { type: "number", enum: enumValues };
3168
+ if (allBoolean)
3169
+ return { type: "boolean", enum: enumValues };
3170
+ return { enum: enumValues };
3171
+ }
3172
+ return {};
3173
+ }
3174
+ if (lower.includes("picklist")) {
3175
+ const enumValues = extractEnumValues(def);
3176
+ if (enumValues.length > 0) {
3177
+ const allString = enumValues.every((v) => typeof v === "string");
3178
+ if (allString)
3179
+ return { type: "string", enum: enumValues };
3180
+ return { enum: enumValues };
3181
+ }
3182
+ return {};
3183
+ }
3184
+ if (lower.includes("literal")) {
3185
+ const value = def.value;
3186
+ if (value === undefined)
3187
+ return {};
3188
+ const valueType = value === null ? "null" : typeof value;
3189
+ if (valueType === "string" || valueType === "number" || valueType === "boolean" || valueType === "null") {
3190
+ return { type: valueType, const: value };
3191
+ }
3192
+ return { const: value };
3193
+ }
3194
+ if (lower.includes("nullable")) {
3195
+ const inner = pickSchemaChild(def);
3196
+ if (!inner)
3197
+ return {};
3198
+ return {
3199
+ anyOf: [buildIntrospectedFallbackJSONSchema(inner, seen), { type: "null" }]
3200
+ };
3201
+ }
3202
+ if (lower.includes("lazy")) {
3203
+ const getter = def.getter;
3204
+ if (typeof getter !== "function")
3205
+ return {};
3206
+ try {
3207
+ return buildIntrospectedFallbackJSONSchema(getter(), seen);
3208
+ } catch {
3209
+ return {};
3210
+ }
3211
+ }
3212
+ const child = pickSchemaChild(def);
3213
+ if (child)
3214
+ return buildIntrospectedFallbackJSONSchema(child, seen);
3215
+ return {};
3216
+ }
3217
+ function buildFallbackJSONSchema(schema) {
3218
+ const def = getValidatorSchemaDef(schema);
3219
+ if (!def)
3220
+ return {};
3221
+ return buildIntrospectedFallbackJSONSchema(schema);
3222
+ }
2656
3223
  function addStructuredInputToOperation(operation, inputJSONSchema) {
2657
3224
  if (!isRecord(inputJSONSchema))
2658
3225
  return;
@@ -2803,38 +3370,155 @@ function generateOpenAPIDocument(routes, options) {
2803
3370
 
2804
3371
  // src/core/server.ts
2805
3372
  var OPENAPI_TAILWIND_ASSET_PATH = "/_vector/openapi/tailwindcdn.js";
3373
+ var OPENAPI_LOGO_DARK_ASSET_PATH = "/_vector/openapi/logo_dark.svg";
3374
+ var OPENAPI_LOGO_WHITE_ASSET_PATH = "/_vector/openapi/logo_white.svg";
3375
+ var OPENAPI_APPLE_TOUCH_ICON_ASSET_PATH = "/_vector/openapi/favicon/apple-touch-icon.png";
3376
+ var OPENAPI_FAVICON_32_ASSET_PATH = "/_vector/openapi/favicon/favicon-32x32.png";
3377
+ var OPENAPI_FAVICON_16_ASSET_PATH = "/_vector/openapi/favicon/favicon-16x16.png";
3378
+ var OPENAPI_FAVICON_ICO_ASSET_PATH = "/_vector/openapi/favicon/favicon.ico";
3379
+ var OPENAPI_WEBMANIFEST_ASSET_PATH = "/_vector/openapi/favicon/site.webmanifest";
3380
+ var OPENAPI_ANDROID_192_ASSET_PATH = "/_vector/openapi/favicon/android-chrome-192x192.png";
3381
+ var OPENAPI_ANDROID_512_ASSET_PATH = "/_vector/openapi/favicon/android-chrome-512x512.png";
2806
3382
  var OPENAPI_TAILWIND_ASSET_RELATIVE_CANDIDATES = [
2807
3383
  "../openapi/assets/tailwindcdn.js",
2808
3384
  "../src/openapi/assets/tailwindcdn.js",
2809
3385
  "../../src/openapi/assets/tailwindcdn.js"
2810
3386
  ];
3387
+ var OPENAPI_LOGO_DARK_ASSET_RELATIVE_CANDIDATES = [
3388
+ "../openapi/assets/logo_dark.svg",
3389
+ "../src/openapi/assets/logo_dark.svg",
3390
+ "../../src/openapi/assets/logo_dark.svg"
3391
+ ];
3392
+ var OPENAPI_LOGO_WHITE_ASSET_RELATIVE_CANDIDATES = [
3393
+ "../openapi/assets/logo_white.svg",
3394
+ "../src/openapi/assets/logo_white.svg",
3395
+ "../../src/openapi/assets/logo_white.svg"
3396
+ ];
2811
3397
  var OPENAPI_TAILWIND_ASSET_CWD_CANDIDATES = [
2812
3398
  "src/openapi/assets/tailwindcdn.js",
2813
3399
  "openapi/assets/tailwindcdn.js",
2814
3400
  "dist/openapi/assets/tailwindcdn.js"
2815
3401
  ];
3402
+ var OPENAPI_LOGO_DARK_ASSET_CWD_CANDIDATES = [
3403
+ "src/openapi/assets/logo_dark.svg",
3404
+ "openapi/assets/logo_dark.svg",
3405
+ "dist/openapi/assets/logo_dark.svg"
3406
+ ];
3407
+ var OPENAPI_LOGO_WHITE_ASSET_CWD_CANDIDATES = [
3408
+ "src/openapi/assets/logo_white.svg",
3409
+ "openapi/assets/logo_white.svg",
3410
+ "dist/openapi/assets/logo_white.svg"
3411
+ ];
3412
+ var OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES = [
3413
+ "../openapi/assets/favicon",
3414
+ "../src/openapi/assets/favicon",
3415
+ "../../src/openapi/assets/favicon"
3416
+ ];
3417
+ var OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES = [
3418
+ "src/openapi/assets/favicon",
3419
+ "openapi/assets/favicon",
3420
+ "dist/openapi/assets/favicon"
3421
+ ];
2816
3422
  var OPENAPI_TAILWIND_ASSET_INLINE_FALLBACK = "/* OpenAPI docs runtime asset missing: tailwind disabled */";
2817
- function resolveOpenAPITailwindAssetFile() {
2818
- for (const relativePath of OPENAPI_TAILWIND_ASSET_RELATIVE_CANDIDATES) {
3423
+ function buildOpenAPIAssetCandidatePaths(bases, filename) {
3424
+ return bases.map((base) => `${base}/${filename}`);
3425
+ }
3426
+ function resolveOpenAPIAssetFile(relativeCandidates, cwdCandidates) {
3427
+ for (const relativePath of relativeCandidates) {
2819
3428
  try {
2820
3429
  const fileUrl = new URL(relativePath, import.meta.url);
2821
- if (existsSync2(fileUrl)) {
3430
+ if (existsSync3(fileUrl)) {
2822
3431
  return Bun.file(fileUrl);
2823
3432
  }
2824
3433
  } catch {}
2825
3434
  }
2826
3435
  const cwd = process.cwd();
2827
- for (const relativePath of OPENAPI_TAILWIND_ASSET_CWD_CANDIDATES) {
3436
+ for (const relativePath of cwdCandidates) {
2828
3437
  const absolutePath = join2(cwd, relativePath);
2829
- if (existsSync2(absolutePath)) {
3438
+ if (existsSync3(absolutePath)) {
2830
3439
  return Bun.file(absolutePath);
2831
3440
  }
2832
3441
  }
2833
3442
  return null;
2834
3443
  }
2835
- var OPENAPI_TAILWIND_ASSET_FILE = resolveOpenAPITailwindAssetFile();
3444
+ var OPENAPI_TAILWIND_ASSET_FILE = resolveOpenAPIAssetFile(OPENAPI_TAILWIND_ASSET_RELATIVE_CANDIDATES, OPENAPI_TAILWIND_ASSET_CWD_CANDIDATES);
3445
+ var OPENAPI_LOGO_DARK_ASSET_FILE = resolveOpenAPIAssetFile(OPENAPI_LOGO_DARK_ASSET_RELATIVE_CANDIDATES, OPENAPI_LOGO_DARK_ASSET_CWD_CANDIDATES);
3446
+ var OPENAPI_LOGO_WHITE_ASSET_FILE = resolveOpenAPIAssetFile(OPENAPI_LOGO_WHITE_ASSET_RELATIVE_CANDIDATES, OPENAPI_LOGO_WHITE_ASSET_CWD_CANDIDATES);
3447
+ var OPENAPI_APPLE_TOUCH_ICON_ASSET_FILE = resolveOpenAPIAssetFile(buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, "apple-touch-icon.png"), buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, "apple-touch-icon.png"));
3448
+ var OPENAPI_FAVICON_32_ASSET_FILE = resolveOpenAPIAssetFile(buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, "favicon-32x32.png"), buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, "favicon-32x32.png"));
3449
+ var OPENAPI_FAVICON_16_ASSET_FILE = resolveOpenAPIAssetFile(buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, "favicon-16x16.png"), buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, "favicon-16x16.png"));
3450
+ var OPENAPI_FAVICON_ICO_ASSET_FILE = resolveOpenAPIAssetFile(buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, "favicon.ico"), buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, "favicon.ico"));
3451
+ var OPENAPI_WEBMANIFEST_ASSET_FILE = resolveOpenAPIAssetFile(buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, "site.webmanifest"), buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, "site.webmanifest"));
3452
+ var OPENAPI_ANDROID_192_ASSET_FILE = resolveOpenAPIAssetFile(buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, "android-chrome-192x192.png"), buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, "android-chrome-192x192.png"));
3453
+ var OPENAPI_ANDROID_512_ASSET_FILE = resolveOpenAPIAssetFile(buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, "android-chrome-512x512.png"), buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, "android-chrome-512x512.png"));
3454
+ var OPENAPI_FAVICON_ASSETS = [
3455
+ {
3456
+ path: OPENAPI_APPLE_TOUCH_ICON_ASSET_PATH,
3457
+ file: OPENAPI_APPLE_TOUCH_ICON_ASSET_FILE,
3458
+ contentType: "image/png",
3459
+ filename: "apple-touch-icon.png"
3460
+ },
3461
+ {
3462
+ path: OPENAPI_FAVICON_32_ASSET_PATH,
3463
+ file: OPENAPI_FAVICON_32_ASSET_FILE,
3464
+ contentType: "image/png",
3465
+ filename: "favicon-32x32.png"
3466
+ },
3467
+ {
3468
+ path: OPENAPI_FAVICON_16_ASSET_PATH,
3469
+ file: OPENAPI_FAVICON_16_ASSET_FILE,
3470
+ contentType: "image/png",
3471
+ filename: "favicon-16x16.png"
3472
+ },
3473
+ {
3474
+ path: OPENAPI_FAVICON_ICO_ASSET_PATH,
3475
+ file: OPENAPI_FAVICON_ICO_ASSET_FILE,
3476
+ contentType: "image/x-icon",
3477
+ filename: "favicon.ico"
3478
+ },
3479
+ {
3480
+ path: OPENAPI_WEBMANIFEST_ASSET_PATH,
3481
+ file: OPENAPI_WEBMANIFEST_ASSET_FILE,
3482
+ contentType: "application/manifest+json; charset=utf-8",
3483
+ filename: "site.webmanifest"
3484
+ },
3485
+ {
3486
+ path: OPENAPI_ANDROID_192_ASSET_PATH,
3487
+ file: OPENAPI_ANDROID_192_ASSET_FILE,
3488
+ contentType: "image/png",
3489
+ filename: "android-chrome-192x192.png"
3490
+ },
3491
+ {
3492
+ path: OPENAPI_ANDROID_512_ASSET_PATH,
3493
+ file: OPENAPI_ANDROID_512_ASSET_FILE,
3494
+ contentType: "image/png",
3495
+ filename: "android-chrome-512x512.png"
3496
+ }
3497
+ ];
2836
3498
  var DOCS_HTML_CACHE_CONTROL = "public, max-age=0, must-revalidate";
2837
3499
  var DOCS_ASSET_CACHE_CONTROL = "public, max-age=31536000, immutable";
3500
+ var DOCS_ASSET_ERROR_CACHE_CONTROL = "no-store";
3501
+ function escapeRegex(value) {
3502
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3503
+ }
3504
+ function wildcardPatternToRegex(pattern) {
3505
+ let regexSource = "^";
3506
+ for (const char of pattern) {
3507
+ if (char === "*") {
3508
+ regexSource += ".*";
3509
+ continue;
3510
+ }
3511
+ regexSource += escapeRegex(char);
3512
+ }
3513
+ regexSource += "$";
3514
+ return new RegExp(regexSource);
3515
+ }
3516
+ function matchesExposePath(path, exposePathPattern) {
3517
+ if (!exposePathPattern.includes("*")) {
3518
+ return path === exposePathPattern;
3519
+ }
3520
+ return wildcardPatternToRegex(exposePathPattern).test(path);
3521
+ }
2838
3522
 
2839
3523
  class VectorServer {
2840
3524
  server = null;
@@ -2845,6 +3529,8 @@ class VectorServer {
2845
3529
  openapiDocsHtmlCache = null;
2846
3530
  openapiWarningsLogged = false;
2847
3531
  openapiTailwindMissingLogged = false;
3532
+ openapiLogoDarkMissingLogged = false;
3533
+ openapiLogoWhiteMissingLogged = false;
2848
3534
  corsHandler = null;
2849
3535
  corsHeadersEntries = null;
2850
3536
  constructor(router, config) {
@@ -2894,9 +3580,10 @@ class VectorServer {
2894
3580
  }
2895
3581
  const openapiObject = openapi || {};
2896
3582
  const docsValue = openapiObject.docs;
2897
- const docs = typeof docsValue === "boolean" ? { enabled: docsValue, path: "/docs" } : {
3583
+ const docs = typeof docsValue === "boolean" ? { enabled: docsValue, path: "/docs", exposePaths: undefined } : {
2898
3584
  enabled: docsValue?.enabled === true,
2899
- path: docsValue?.path || "/docs"
3585
+ path: docsValue?.path || "/docs",
3586
+ exposePaths: Array.isArray(docsValue?.exposePaths) ? docsValue.exposePaths.map((path) => typeof path === "string" ? path.trim() : "").filter((path) => path.length > 0) : undefined
2900
3587
  };
2901
3588
  return {
2902
3589
  enabled: openapiObject.enabled ?? defaultEnabled,
@@ -2909,6 +3596,15 @@ class VectorServer {
2909
3596
  isDocsReservedPath(path) {
2910
3597
  return path === this.openapiConfig.path || this.openapiConfig.docs.enabled && path === this.openapiConfig.docs.path;
2911
3598
  }
3599
+ shouldLogOpenAPIConversionWarnings() {
3600
+ const nodeEnv = "development";
3601
+ const isDevelopment = this.config.development !== false && nodeEnv !== "production";
3602
+ if (!isDevelopment) {
3603
+ return false;
3604
+ }
3605
+ const logLevel = process.env.LOG_LEVEL;
3606
+ return typeof logLevel === "string" && logLevel.toLowerCase() === "debug";
3607
+ }
2912
3608
  getOpenAPIDocument() {
2913
3609
  if (this.openapiDocCache) {
2914
3610
  return this.openapiDocCache;
@@ -2919,19 +3615,39 @@ class VectorServer {
2919
3615
  info: this.openapiConfig.info
2920
3616
  });
2921
3617
  if (!this.openapiWarningsLogged && result.warnings.length > 0) {
2922
- for (const warning of result.warnings) {
2923
- console.warn(warning);
3618
+ if (this.shouldLogOpenAPIConversionWarnings()) {
3619
+ for (const warning of result.warnings) {
3620
+ console.warn(warning);
3621
+ }
2924
3622
  }
2925
3623
  this.openapiWarningsLogged = true;
2926
3624
  }
2927
3625
  this.openapiDocCache = result.document;
2928
3626
  return this.openapiDocCache;
2929
3627
  }
3628
+ getOpenAPIDocumentForDocs() {
3629
+ const exposePaths = this.openapiConfig.docs.exposePaths;
3630
+ const document = this.getOpenAPIDocument();
3631
+ if (!Array.isArray(exposePaths) || exposePaths.length === 0) {
3632
+ return document;
3633
+ }
3634
+ const existingPaths = document.paths && typeof document.paths === "object" && !Array.isArray(document.paths) ? document.paths : {};
3635
+ const filteredPaths = {};
3636
+ for (const [path, value] of Object.entries(existingPaths)) {
3637
+ if (exposePaths.some((pattern) => matchesExposePath(path, pattern))) {
3638
+ filteredPaths[path] = value;
3639
+ }
3640
+ }
3641
+ return {
3642
+ ...document,
3643
+ paths: filteredPaths
3644
+ };
3645
+ }
2930
3646
  getOpenAPIDocsHtmlCacheEntry() {
2931
3647
  if (this.openapiDocsHtmlCache) {
2932
3648
  return this.openapiDocsHtmlCache;
2933
3649
  }
2934
- const html = renderOpenAPIDocsHtml(this.getOpenAPIDocument(), this.openapiConfig.path, OPENAPI_TAILWIND_ASSET_PATH);
3650
+ const html = renderOpenAPIDocsHtml(this.getOpenAPIDocumentForDocs(), this.openapiConfig.path, OPENAPI_TAILWIND_ASSET_PATH, OPENAPI_LOGO_DARK_ASSET_PATH, OPENAPI_LOGO_WHITE_ASSET_PATH, OPENAPI_APPLE_TOUCH_ICON_ASSET_PATH, OPENAPI_FAVICON_32_ASSET_PATH, OPENAPI_FAVICON_16_ASSET_PATH, OPENAPI_WEBMANIFEST_ASSET_PATH);
2935
3651
  const gzip = Bun.gzipSync(html);
2936
3652
  const etag = `"${Bun.hash(html).toString(16)}"`;
2937
3653
  this.openapiDocsHtmlCache = { html, gzip, etag };
@@ -2949,6 +3665,11 @@ class VectorServer {
2949
3665
  if (this.openapiConfig.docs.enabled) {
2950
3666
  reserved.add(this.openapiConfig.docs.path);
2951
3667
  reserved.add(OPENAPI_TAILWIND_ASSET_PATH);
3668
+ reserved.add(OPENAPI_LOGO_DARK_ASSET_PATH);
3669
+ reserved.add(OPENAPI_LOGO_WHITE_ASSET_PATH);
3670
+ for (const asset of OPENAPI_FAVICON_ASSETS) {
3671
+ reserved.add(asset.path);
3672
+ }
2952
3673
  }
2953
3674
  const methodConflicts = this.router.getRouteDefinitions().filter((route) => reserved.has(route.path)).map((route) => `${route.method} ${route.path}`);
2954
3675
  const staticConflicts = Object.entries(this.router.getRouteTable()).filter(([path, value]) => reserved.has(path) && value instanceof Response).map(([path]) => `STATIC ${path}`);
@@ -3021,6 +3742,71 @@ class VectorServer {
3021
3742
  }
3022
3743
  });
3023
3744
  }
3745
+ if (this.openapiConfig.docs.enabled && pathname === OPENAPI_LOGO_DARK_ASSET_PATH) {
3746
+ if (!OPENAPI_LOGO_DARK_ASSET_FILE) {
3747
+ if (!this.openapiLogoDarkMissingLogged) {
3748
+ this.openapiLogoDarkMissingLogged = true;
3749
+ console.warn('[OpenAPI] Missing docs runtime asset "logo_dark.svg".');
3750
+ }
3751
+ return new Response("OpenAPI docs runtime asset missing: logo_dark.svg", {
3752
+ status: 404,
3753
+ headers: {
3754
+ "content-type": "text/plain; charset=utf-8",
3755
+ "cache-control": DOCS_ASSET_ERROR_CACHE_CONTROL
3756
+ }
3757
+ });
3758
+ }
3759
+ return new Response(OPENAPI_LOGO_DARK_ASSET_FILE, {
3760
+ status: 200,
3761
+ headers: {
3762
+ "content-type": "image/svg+xml; charset=utf-8",
3763
+ "cache-control": DOCS_ASSET_CACHE_CONTROL
3764
+ }
3765
+ });
3766
+ }
3767
+ if (this.openapiConfig.docs.enabled && pathname === OPENAPI_LOGO_WHITE_ASSET_PATH) {
3768
+ if (!OPENAPI_LOGO_WHITE_ASSET_FILE) {
3769
+ if (!this.openapiLogoWhiteMissingLogged) {
3770
+ this.openapiLogoWhiteMissingLogged = true;
3771
+ console.warn('[OpenAPI] Missing docs runtime asset "logo_white.svg".');
3772
+ }
3773
+ return new Response("OpenAPI docs runtime asset missing: logo_white.svg", {
3774
+ status: 404,
3775
+ headers: {
3776
+ "content-type": "text/plain; charset=utf-8",
3777
+ "cache-control": DOCS_ASSET_ERROR_CACHE_CONTROL
3778
+ }
3779
+ });
3780
+ }
3781
+ return new Response(OPENAPI_LOGO_WHITE_ASSET_FILE, {
3782
+ status: 200,
3783
+ headers: {
3784
+ "content-type": "image/svg+xml; charset=utf-8",
3785
+ "cache-control": DOCS_ASSET_CACHE_CONTROL
3786
+ }
3787
+ });
3788
+ }
3789
+ if (this.openapiConfig.docs.enabled) {
3790
+ const faviconAsset = OPENAPI_FAVICON_ASSETS.find((asset) => asset.path === pathname);
3791
+ if (faviconAsset) {
3792
+ if (!faviconAsset.file) {
3793
+ return new Response(`OpenAPI docs runtime asset missing: ${faviconAsset.filename}`, {
3794
+ status: 404,
3795
+ headers: {
3796
+ "content-type": "text/plain; charset=utf-8",
3797
+ "cache-control": DOCS_ASSET_ERROR_CACHE_CONTROL
3798
+ }
3799
+ });
3800
+ }
3801
+ return new Response(faviconAsset.file, {
3802
+ status: 200,
3803
+ headers: {
3804
+ "content-type": faviconAsset.contentType,
3805
+ "cache-control": DOCS_ASSET_CACHE_CONTROL
3806
+ }
3807
+ });
3808
+ }
3809
+ }
3024
3810
  return null;
3025
3811
  }
3026
3812
  normalizeCorsOptions(options) {
@@ -3071,7 +3857,7 @@ class VectorServer {
3071
3857
  reusePort: this.config.reusePort !== false,
3072
3858
  routes: this.router.getRouteTable(),
3073
3859
  fetch: fallbackFetch,
3074
- idleTimeout: this.config.idleTimeout || 60,
3860
+ idleTimeout: this.config.idleTimeout ?? 60,
3075
3861
  error: (error, request) => {
3076
3862
  console.error("[ERROR] Server error:", error);
3077
3863
  return this.applyCors(new Response("Internal Server Error", { status: 500 }), request);
@@ -3134,6 +3920,7 @@ class Vector {
3134
3920
  routeGenerator = null;
3135
3921
  _protectedHandler = null;
3136
3922
  _cacheHandler = null;
3923
+ shutdownPromise = null;
3137
3924
  constructor() {
3138
3925
  this.middlewareManager = new MiddlewareManager;
3139
3926
  this.authManager = new AuthManager;
@@ -3148,14 +3935,22 @@ class Vector {
3148
3935
  }
3149
3936
  setProtectedHandler(handler) {
3150
3937
  this._protectedHandler = handler;
3151
- this.authManager.setProtectedHandler(handler);
3938
+ if (handler) {
3939
+ this.authManager.setProtectedHandler(handler);
3940
+ return;
3941
+ }
3942
+ this.authManager.clearProtectedHandler();
3152
3943
  }
3153
3944
  getProtectedHandler() {
3154
3945
  return this._protectedHandler;
3155
3946
  }
3156
3947
  setCacheHandler(handler) {
3157
3948
  this._cacheHandler = handler;
3158
- this.cacheManager.setCacheHandler(handler);
3949
+ if (handler) {
3950
+ this.cacheManager.setCacheHandler(handler);
3951
+ return;
3952
+ }
3953
+ this.cacheManager.clearCacheHandler();
3159
3954
  }
3160
3955
  getCacheHandler() {
3161
3956
  return this._cacheHandler;
@@ -3178,6 +3973,9 @@ class Vector {
3178
3973
  if (config?.finally) {
3179
3974
  this.middlewareManager.addFinally(...config.finally);
3180
3975
  }
3976
+ if (typeof this.config.startup === "function") {
3977
+ await this.config.startup();
3978
+ }
3181
3979
  if (this.config.autoDiscover !== false) {
3182
3980
  await this.discoverRoutes();
3183
3981
  }
@@ -3260,6 +4058,22 @@ class Vector {
3260
4058
  this.server = null;
3261
4059
  }
3262
4060
  }
4061
+ async shutdown() {
4062
+ if (this.shutdownPromise) {
4063
+ return this.shutdownPromise;
4064
+ }
4065
+ this.shutdownPromise = (async () => {
4066
+ this.stop();
4067
+ if (typeof this.config.shutdown === "function") {
4068
+ await this.config.shutdown();
4069
+ }
4070
+ })();
4071
+ try {
4072
+ await this.shutdownPromise;
4073
+ } finally {
4074
+ this.shutdownPromise = null;
4075
+ }
4076
+ }
3263
4077
  getServer() {
3264
4078
  return this.server;
3265
4079
  }
@@ -3278,111 +4092,40 @@ class Vector {
3278
4092
  }
3279
4093
  var getVectorInstance = Vector.getInstance;
3280
4094
 
3281
- // src/core/config-loader.ts
3282
- import { existsSync as existsSync3 } from "fs";
3283
- import { resolve as resolve2, isAbsolute } from "path";
3284
- class ConfigLoader {
3285
- configPath;
3286
- config = null;
3287
- configSource = "default";
3288
- constructor(configPath) {
3289
- const path = configPath || "vector.config.ts";
3290
- this.configPath = isAbsolute(path) ? path : resolve2(process.cwd(), path);
3291
- }
3292
- async load() {
3293
- if (existsSync3(this.configPath)) {
3294
- try {
3295
- const userConfigPath = toFileUrl(this.configPath);
3296
- const userConfig = await import(userConfigPath);
3297
- this.config = userConfig.default || userConfig;
3298
- this.configSource = "user";
3299
- } catch (error) {
3300
- const msg = error instanceof Error ? error.message : String(error);
3301
- console.error(`[Vector] Failed to load config from ${this.configPath}: ${msg}`);
3302
- console.error("[Vector] Server is using default configuration. Fix your config file and restart.");
3303
- this.config = {};
3304
- }
3305
- } else {
3306
- this.config = {};
3307
- }
3308
- return await this.buildLegacyConfig();
3309
- }
3310
- getConfigSource() {
3311
- return this.configSource;
3312
- }
3313
- async buildLegacyConfig() {
3314
- const config = {};
3315
- if (this.config) {
3316
- config.port = this.config.port;
3317
- config.hostname = this.config.hostname;
3318
- config.reusePort = this.config.reusePort;
3319
- config.development = this.config.development;
3320
- config.routesDir = this.config.routesDir || "./routes";
3321
- config.idleTimeout = this.config.idleTimeout;
3322
- config.defaults = this.config.defaults;
3323
- config.openapi = this.config.openapi;
3324
- }
3325
- config.autoDiscover = true;
3326
- if (this.config?.cors) {
3327
- if (typeof this.config.cors === "boolean") {
3328
- config.cors = this.config.cors ? {
3329
- origin: "*",
3330
- credentials: true,
3331
- allowHeaders: "Content-Type, Authorization",
3332
- allowMethods: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
3333
- exposeHeaders: "Authorization",
3334
- maxAge: 86400
3335
- } : undefined;
3336
- } else {
3337
- config.cors = this.config.cors;
3338
- }
3339
- }
3340
- if (this.config?.before) {
3341
- config.before = this.config.before;
3342
- }
3343
- if (this.config?.after) {
3344
- config.finally = this.config.after;
3345
- }
3346
- return config;
3347
- }
3348
- async loadAuthHandler() {
3349
- return this.config?.auth || null;
3350
- }
3351
- async loadCacheHandler() {
3352
- return this.config?.cache || null;
3353
- }
3354
- getConfig() {
3355
- return this.config;
3356
- }
3357
- }
3358
-
3359
- // src/cli/option-resolution.ts
3360
- function resolveRoutesDir(configRoutesDir, hasRoutesOption, cliRoutes) {
3361
- if (hasRoutesOption) {
3362
- return cliRoutes;
3363
- }
3364
- return configRoutesDir ?? cliRoutes;
3365
- }
3366
- function parseAndValidatePort(value) {
3367
- const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
3368
- if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
3369
- throw new Error(`Invalid port value: ${String(value)}`);
3370
- }
3371
- return parsed;
3372
- }
3373
- function resolvePort(configPort, hasPortOption, cliPort) {
3374
- if (hasPortOption) {
3375
- return parseAndValidatePort(cliPort);
3376
- }
3377
- const resolved = configPort ?? cliPort;
3378
- return parseAndValidatePort(resolved);
3379
- }
3380
- function resolveHost(configHost, hasHostOption, cliHost) {
3381
- const resolved = hasHostOption ? cliHost : configHost ?? cliHost;
3382
- if (typeof resolved !== "string" || resolved.length === 0) {
3383
- throw new Error(`Invalid host value: ${String(resolved)}`);
3384
- }
3385
- return resolved;
4095
+ // src/start-vector.ts
4096
+ async function startVector(options = {}) {
4097
+ const configLoader = new ConfigLoader(options.configPath);
4098
+ const loadedConfig = await configLoader.load();
4099
+ const configSource = configLoader.getConfigSource();
4100
+ let config = { ...loadedConfig };
4101
+ if (options.mutateConfig) {
4102
+ config = await options.mutateConfig(config, { configSource });
4103
+ }
4104
+ if (options.config) {
4105
+ config = { ...config, ...options.config };
4106
+ }
4107
+ if (options.autoDiscover !== undefined) {
4108
+ config.autoDiscover = options.autoDiscover;
4109
+ }
4110
+ const vector = getVectorInstance();
4111
+ const resolvedProtectedHandler = options.protectedHandler !== undefined ? options.protectedHandler : await configLoader.loadAuthHandler();
4112
+ const resolvedCacheHandler = options.cacheHandler !== undefined ? options.cacheHandler : await configLoader.loadCacheHandler();
4113
+ vector.setProtectedHandler(resolvedProtectedHandler ?? null);
4114
+ vector.setCacheHandler(resolvedCacheHandler ?? null);
4115
+ const server = await vector.startServer(config);
4116
+ const effectiveConfig = {
4117
+ ...config,
4118
+ port: server.port ?? config.port ?? DEFAULT_CONFIG.PORT,
4119
+ hostname: server.hostname || config.hostname || DEFAULT_CONFIG.HOSTNAME,
4120
+ reusePort: config.reusePort !== false,
4121
+ idleTimeout: config.idleTimeout ?? 60
4122
+ };
4123
+ return {
4124
+ server,
4125
+ config: effectiveConfig,
4126
+ stop: () => vector.stop(),
4127
+ shutdown: () => vector.shutdown()
4128
+ };
3386
4129
  }
3387
4130
 
3388
4131
  // src/cli/index.ts
@@ -3429,7 +4172,8 @@ var hasPortOption = args.some((arg) => arg === "--port" || arg === "-p" || arg.s
3429
4172
  async function runDev() {
3430
4173
  const isDev = command === "dev";
3431
4174
  let server = null;
3432
- let vector = null;
4175
+ let app = null;
4176
+ let removeShutdownHandlers = null;
3433
4177
  async function startServer() {
3434
4178
  const timeoutPromise = new Promise((_, reject) => {
3435
4179
  setTimeout(() => {
@@ -3438,34 +4182,30 @@ async function runDev() {
3438
4182
  });
3439
4183
  const serverStartPromise = (async () => {
3440
4184
  const explicitConfigPath = values.config;
3441
- const configLoader = new ConfigLoader(explicitConfigPath);
3442
- const loadedConfig = await configLoader.load();
3443
- const config = { ...loadedConfig };
3444
- config.port = resolvePort(config.port, hasPortOption, values.port);
3445
- config.hostname = resolveHost(config.hostname, hasHostOption, values.host);
3446
- config.routesDir = resolveRoutesDir(config.routesDir, hasRoutesOption, values.routes);
3447
- config.development = config.development ?? isDev;
3448
- config.autoDiscover = true;
3449
- if (config.cors === undefined && values.cors) {
3450
- config.cors = {
3451
- origin: "*",
3452
- credentials: true,
3453
- allowHeaders: "Content-Type, Authorization",
3454
- allowMethods: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
3455
- exposeHeaders: "Authorization",
3456
- maxAge: 86400
3457
- };
3458
- }
3459
- vector = getVectorInstance();
3460
- const authHandler = await configLoader.loadAuthHandler();
3461
- if (authHandler) {
3462
- vector.setProtectedHandler(authHandler);
3463
- }
3464
- const cacheHandler = await configLoader.loadCacheHandler();
3465
- if (cacheHandler) {
3466
- vector.setCacheHandler(cacheHandler);
3467
- }
3468
- server = await vector.startServer(config);
4185
+ app = await startVector({
4186
+ configPath: explicitConfigPath,
4187
+ mutateConfig: (loadedConfig) => {
4188
+ const config2 = { ...loadedConfig };
4189
+ config2.port = resolvePort(config2.port, hasPortOption, values.port);
4190
+ config2.hostname = resolveHost(config2.hostname, hasHostOption, values.host);
4191
+ config2.routesDir = resolveRoutesDir(config2.routesDir, hasRoutesOption, values.routes);
4192
+ config2.development = config2.development ?? isDev;
4193
+ config2.autoDiscover = true;
4194
+ if (config2.cors === undefined && values.cors) {
4195
+ config2.cors = {
4196
+ origin: "*",
4197
+ credentials: true,
4198
+ allowHeaders: "Content-Type, Authorization",
4199
+ allowMethods: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
4200
+ exposeHeaders: "Authorization",
4201
+ maxAge: 86400
4202
+ };
4203
+ }
4204
+ return config2;
4205
+ }
4206
+ });
4207
+ server = app.server;
4208
+ const config = app.config;
3469
4209
  if (!server || !server.port) {
3470
4210
  throw new Error("Server started but is not responding correctly");
3471
4211
  }
@@ -3474,13 +4214,18 @@ async function runDev() {
3474
4214
  console.log(`
3475
4215
  Listening on ${cyan}http://${config.hostname}:${config.port}${reset}
3476
4216
  `);
3477
- return { server, vector, config };
4217
+ return { server, app, config };
3478
4218
  })();
3479
4219
  return await Promise.race([serverStartPromise, timeoutPromise]);
3480
4220
  }
3481
4221
  try {
3482
4222
  const result = await startServer();
3483
4223
  server = result.server;
4224
+ if (!removeShutdownHandlers) {
4225
+ removeShutdownHandlers = installGracefulShutdownHandlers({
4226
+ getTarget: () => app
4227
+ });
4228
+ }
3484
4229
  if (isDev && values.watch) {
3485
4230
  try {
3486
4231
  let reloadTimeout = null;
@@ -3504,14 +4249,14 @@ Listening on ${cyan}http://${config.hostname}:${config.port}${reset}
3504
4249
  isReloading = true;
3505
4250
  lastReloadTime = Date.now();
3506
4251
  changedFiles.clear();
3507
- if (vector) {
3508
- vector.stop();
4252
+ if (app) {
4253
+ app.stop();
3509
4254
  }
3510
4255
  await new Promise((resolve3) => setTimeout(resolve3, 100));
3511
4256
  try {
3512
4257
  const result2 = await startServer();
3513
4258
  server = result2.server;
3514
- vector = result2.vector;
4259
+ app = result2.app;
3515
4260
  } catch (error) {
3516
4261
  console.error(`
3517
4262
  [Reload Error]`, error.message || error);