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
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);
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);
98
+ }
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 = {};
24
114
  }
115
+ return await this.buildLegacyConfig();
25
116
  }
26
- isAuthenticated(request) {
27
- return !!request.authUser;
117
+ getConfigSource() {
118
+ return this.configSource;
28
119
  }
29
- getUser(request) {
30
- return request.authUser || null;
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
 
@@ -2635,8 +2906,9 @@ function convertInputSchema(routePath, inputSchema, target, warnings) {
2635
2906
  try {
2636
2907
  return inputSchema["~standard"].jsonSchema.input({ target });
2637
2908
  } catch (error) {
2638
- warnings.push(`[OpenAPI] Failed input schema conversion for ${routePath}: ${error instanceof Error ? error.message : String(error)}`);
2639
- return null;
2909
+ warnings.push(`[OpenAPI] Failed input schema conversion for ${routePath}: ${error instanceof Error ? error.message : String(error)}. Falling back to a permissive JSON Schema.`);
2910
+ const fallback = buildFallbackJSONSchema(inputSchema);
2911
+ return isEmptyObjectSchema(fallback) ? null : fallback;
2640
2912
  }
2641
2913
  }
2642
2914
  function convertOutputSchema(routePath, statusCode, outputSchema, target, warnings) {
@@ -2646,13 +2918,233 @@ function convertOutputSchema(routePath, statusCode, outputSchema, target, warnin
2646
2918
  try {
2647
2919
  return outputSchema["~standard"].jsonSchema.output({ target });
2648
2920
  } catch (error) {
2649
- warnings.push(`[OpenAPI] Failed output schema conversion for ${routePath} (${statusCode}): ${error instanceof Error ? error.message : String(error)}`);
2650
- return null;
2921
+ warnings.push(`[OpenAPI] Failed output schema conversion for ${routePath} (${statusCode}): ${error instanceof Error ? error.message : String(error)}. Falling back to a permissive JSON Schema.`);
2922
+ return buildFallbackJSONSchema(outputSchema);
2651
2923
  }
2652
2924
  }
2653
2925
  function isRecord(value) {
2654
2926
  return !!value && typeof value === "object" && !Array.isArray(value);
2655
2927
  }
2928
+ function isEmptyObjectSchema(value) {
2929
+ return isRecord(value) && Object.keys(value).length === 0;
2930
+ }
2931
+ function getValidatorSchemaDef(schema) {
2932
+ if (!schema || typeof schema !== "object")
2933
+ return null;
2934
+ const value = schema;
2935
+ if (isRecord(value._def))
2936
+ return value._def;
2937
+ if (isRecord(value._zod) && isRecord(value._zod.def)) {
2938
+ return value._zod.def;
2939
+ }
2940
+ return null;
2941
+ }
2942
+ function getSchemaKind(def) {
2943
+ if (!def)
2944
+ return null;
2945
+ const typeName = def.typeName;
2946
+ if (typeof typeName === "string")
2947
+ return typeName;
2948
+ const type = def.type;
2949
+ if (typeof type === "string")
2950
+ return type;
2951
+ return null;
2952
+ }
2953
+ function pickSchemaChild(def) {
2954
+ const candidates = ["innerType", "schema", "type", "out", "in", "left", "right"];
2955
+ for (const key of candidates) {
2956
+ if (key in def)
2957
+ return def[key];
2958
+ }
2959
+ return;
2960
+ }
2961
+ function pickSchemaObjectCandidate(def, keys) {
2962
+ for (const key of keys) {
2963
+ const value = def[key];
2964
+ if (value && typeof value === "object" && !Array.isArray(value)) {
2965
+ return value;
2966
+ }
2967
+ }
2968
+ return;
2969
+ }
2970
+ function isOptionalWrapperKind(kind) {
2971
+ if (!kind)
2972
+ return false;
2973
+ const lower = kind.toLowerCase();
2974
+ return lower.includes("optional") || lower.includes("default") || lower.includes("catch");
2975
+ }
2976
+ function unwrapOptionalForRequired(schema) {
2977
+ let current = schema;
2978
+ let optional = false;
2979
+ let guard = 0;
2980
+ while (guard < 8) {
2981
+ guard += 1;
2982
+ const def = getValidatorSchemaDef(current);
2983
+ const kind = getSchemaKind(def);
2984
+ if (!def || !isOptionalWrapperKind(kind))
2985
+ break;
2986
+ optional = true;
2987
+ const inner = pickSchemaChild(def);
2988
+ if (!inner)
2989
+ break;
2990
+ current = inner;
2991
+ }
2992
+ return { schema: current, optional };
2993
+ }
2994
+ function getObjectShape(def) {
2995
+ const rawShape = def.shape;
2996
+ if (typeof rawShape === "function") {
2997
+ try {
2998
+ const resolved = rawShape();
2999
+ return isRecord(resolved) ? resolved : {};
3000
+ } catch {
3001
+ return {};
3002
+ }
3003
+ }
3004
+ return isRecord(rawShape) ? rawShape : {};
3005
+ }
3006
+ function mapPrimitiveKind(kind) {
3007
+ const lower = kind.toLowerCase();
3008
+ if (lower.includes("string"))
3009
+ return { type: "string" };
3010
+ if (lower.includes("number"))
3011
+ return { type: "number" };
3012
+ if (lower.includes("boolean"))
3013
+ return { type: "boolean" };
3014
+ if (lower.includes("bigint"))
3015
+ return { type: "string" };
3016
+ if (lower.includes("null"))
3017
+ return { type: "null" };
3018
+ if (lower.includes("any") || lower.includes("unknown") || lower.includes("never"))
3019
+ return {};
3020
+ if (lower.includes("date"))
3021
+ return { type: "string", format: "date-time" };
3022
+ if (lower.includes("custom"))
3023
+ return { type: "object", additionalProperties: true };
3024
+ return null;
3025
+ }
3026
+ function buildIntrospectedFallbackJSONSchema(schema, seen = new WeakSet) {
3027
+ if (!schema || typeof schema !== "object")
3028
+ return {};
3029
+ if (seen.has(schema))
3030
+ return {};
3031
+ seen.add(schema);
3032
+ const def = getValidatorSchemaDef(schema);
3033
+ const kind = getSchemaKind(def);
3034
+ if (!def || !kind)
3035
+ return {};
3036
+ const primitive = mapPrimitiveKind(kind);
3037
+ if (primitive)
3038
+ return primitive;
3039
+ const lower = kind.toLowerCase();
3040
+ if (lower.includes("object")) {
3041
+ const shape = getObjectShape(def);
3042
+ const properties = {};
3043
+ const required = [];
3044
+ for (const [key, child2] of Object.entries(shape)) {
3045
+ const unwrapped = unwrapOptionalForRequired(child2);
3046
+ properties[key] = buildIntrospectedFallbackJSONSchema(unwrapped.schema, seen);
3047
+ if (!unwrapped.optional)
3048
+ required.push(key);
3049
+ }
3050
+ const out = {
3051
+ type: "object",
3052
+ properties,
3053
+ additionalProperties: true
3054
+ };
3055
+ if (required.length > 0) {
3056
+ out.required = required;
3057
+ }
3058
+ return out;
3059
+ }
3060
+ if (lower.includes("array")) {
3061
+ const itemSchema = pickSchemaObjectCandidate(def, ["element", "items", "innerType", "type"]) ?? {};
3062
+ return {
3063
+ type: "array",
3064
+ items: buildIntrospectedFallbackJSONSchema(itemSchema, seen)
3065
+ };
3066
+ }
3067
+ if (lower.includes("record")) {
3068
+ const valueType = def.valueType ?? def.valueSchema;
3069
+ return {
3070
+ type: "object",
3071
+ additionalProperties: valueType ? buildIntrospectedFallbackJSONSchema(valueType, seen) : true
3072
+ };
3073
+ }
3074
+ if (lower.includes("tuple")) {
3075
+ const items = Array.isArray(def.items) ? def.items : [];
3076
+ const prefixItems = items.map((item) => buildIntrospectedFallbackJSONSchema(item, seen));
3077
+ return {
3078
+ type: "array",
3079
+ prefixItems,
3080
+ minItems: prefixItems.length,
3081
+ maxItems: prefixItems.length
3082
+ };
3083
+ }
3084
+ if (lower.includes("union")) {
3085
+ const options = def.options ?? def.schemas ?? [];
3086
+ if (!Array.isArray(options) || options.length === 0)
3087
+ return {};
3088
+ return {
3089
+ anyOf: options.map((option) => buildIntrospectedFallbackJSONSchema(option, seen))
3090
+ };
3091
+ }
3092
+ if (lower.includes("intersection")) {
3093
+ const left = def.left;
3094
+ const right = def.right;
3095
+ if (!left || !right)
3096
+ return {};
3097
+ return {
3098
+ allOf: [buildIntrospectedFallbackJSONSchema(left, seen), buildIntrospectedFallbackJSONSchema(right, seen)]
3099
+ };
3100
+ }
3101
+ if (lower.includes("enum")) {
3102
+ const values = def.values;
3103
+ if (Array.isArray(values))
3104
+ return { enum: values };
3105
+ if (values && typeof values === "object")
3106
+ return { enum: Object.values(values) };
3107
+ return {};
3108
+ }
3109
+ if (lower.includes("literal")) {
3110
+ const value = def.value;
3111
+ if (value === undefined)
3112
+ return {};
3113
+ const valueType = value === null ? "null" : typeof value;
3114
+ if (valueType === "string" || valueType === "number" || valueType === "boolean" || valueType === "null") {
3115
+ return { type: valueType, const: value };
3116
+ }
3117
+ return { const: value };
3118
+ }
3119
+ if (lower.includes("nullable")) {
3120
+ const inner = pickSchemaChild(def);
3121
+ if (!inner)
3122
+ return {};
3123
+ return {
3124
+ anyOf: [buildIntrospectedFallbackJSONSchema(inner, seen), { type: "null" }]
3125
+ };
3126
+ }
3127
+ if (lower.includes("lazy")) {
3128
+ const getter = def.getter;
3129
+ if (typeof getter !== "function")
3130
+ return {};
3131
+ try {
3132
+ return buildIntrospectedFallbackJSONSchema(getter(), seen);
3133
+ } catch {
3134
+ return {};
3135
+ }
3136
+ }
3137
+ const child = pickSchemaChild(def);
3138
+ if (child)
3139
+ return buildIntrospectedFallbackJSONSchema(child, seen);
3140
+ return {};
3141
+ }
3142
+ function buildFallbackJSONSchema(schema) {
3143
+ const def = getValidatorSchemaDef(schema);
3144
+ if (!def)
3145
+ return {};
3146
+ return buildIntrospectedFallbackJSONSchema(schema);
3147
+ }
2656
3148
  function addStructuredInputToOperation(operation, inputJSONSchema) {
2657
3149
  if (!isRecord(inputJSONSchema))
2658
3150
  return;
@@ -2803,38 +3295,155 @@ function generateOpenAPIDocument(routes, options) {
2803
3295
 
2804
3296
  // src/core/server.ts
2805
3297
  var OPENAPI_TAILWIND_ASSET_PATH = "/_vector/openapi/tailwindcdn.js";
3298
+ var OPENAPI_LOGO_DARK_ASSET_PATH = "/_vector/openapi/logo_dark.svg";
3299
+ var OPENAPI_LOGO_WHITE_ASSET_PATH = "/_vector/openapi/logo_white.svg";
3300
+ var OPENAPI_APPLE_TOUCH_ICON_ASSET_PATH = "/_vector/openapi/favicon/apple-touch-icon.png";
3301
+ var OPENAPI_FAVICON_32_ASSET_PATH = "/_vector/openapi/favicon/favicon-32x32.png";
3302
+ var OPENAPI_FAVICON_16_ASSET_PATH = "/_vector/openapi/favicon/favicon-16x16.png";
3303
+ var OPENAPI_FAVICON_ICO_ASSET_PATH = "/_vector/openapi/favicon/favicon.ico";
3304
+ var OPENAPI_WEBMANIFEST_ASSET_PATH = "/_vector/openapi/favicon/site.webmanifest";
3305
+ var OPENAPI_ANDROID_192_ASSET_PATH = "/_vector/openapi/favicon/android-chrome-192x192.png";
3306
+ var OPENAPI_ANDROID_512_ASSET_PATH = "/_vector/openapi/favicon/android-chrome-512x512.png";
2806
3307
  var OPENAPI_TAILWIND_ASSET_RELATIVE_CANDIDATES = [
2807
3308
  "../openapi/assets/tailwindcdn.js",
2808
3309
  "../src/openapi/assets/tailwindcdn.js",
2809
3310
  "../../src/openapi/assets/tailwindcdn.js"
2810
3311
  ];
3312
+ var OPENAPI_LOGO_DARK_ASSET_RELATIVE_CANDIDATES = [
3313
+ "../openapi/assets/logo_dark.svg",
3314
+ "../src/openapi/assets/logo_dark.svg",
3315
+ "../../src/openapi/assets/logo_dark.svg"
3316
+ ];
3317
+ var OPENAPI_LOGO_WHITE_ASSET_RELATIVE_CANDIDATES = [
3318
+ "../openapi/assets/logo_white.svg",
3319
+ "../src/openapi/assets/logo_white.svg",
3320
+ "../../src/openapi/assets/logo_white.svg"
3321
+ ];
2811
3322
  var OPENAPI_TAILWIND_ASSET_CWD_CANDIDATES = [
2812
3323
  "src/openapi/assets/tailwindcdn.js",
2813
3324
  "openapi/assets/tailwindcdn.js",
2814
3325
  "dist/openapi/assets/tailwindcdn.js"
2815
3326
  ];
3327
+ var OPENAPI_LOGO_DARK_ASSET_CWD_CANDIDATES = [
3328
+ "src/openapi/assets/logo_dark.svg",
3329
+ "openapi/assets/logo_dark.svg",
3330
+ "dist/openapi/assets/logo_dark.svg"
3331
+ ];
3332
+ var OPENAPI_LOGO_WHITE_ASSET_CWD_CANDIDATES = [
3333
+ "src/openapi/assets/logo_white.svg",
3334
+ "openapi/assets/logo_white.svg",
3335
+ "dist/openapi/assets/logo_white.svg"
3336
+ ];
3337
+ var OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES = [
3338
+ "../openapi/assets/favicon",
3339
+ "../src/openapi/assets/favicon",
3340
+ "../../src/openapi/assets/favicon"
3341
+ ];
3342
+ var OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES = [
3343
+ "src/openapi/assets/favicon",
3344
+ "openapi/assets/favicon",
3345
+ "dist/openapi/assets/favicon"
3346
+ ];
2816
3347
  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) {
3348
+ function buildOpenAPIAssetCandidatePaths(bases, filename) {
3349
+ return bases.map((base) => `${base}/${filename}`);
3350
+ }
3351
+ function resolveOpenAPIAssetFile(relativeCandidates, cwdCandidates) {
3352
+ for (const relativePath of relativeCandidates) {
2819
3353
  try {
2820
3354
  const fileUrl = new URL(relativePath, import.meta.url);
2821
- if (existsSync2(fileUrl)) {
3355
+ if (existsSync3(fileUrl)) {
2822
3356
  return Bun.file(fileUrl);
2823
3357
  }
2824
3358
  } catch {}
2825
3359
  }
2826
3360
  const cwd = process.cwd();
2827
- for (const relativePath of OPENAPI_TAILWIND_ASSET_CWD_CANDIDATES) {
3361
+ for (const relativePath of cwdCandidates) {
2828
3362
  const absolutePath = join2(cwd, relativePath);
2829
- if (existsSync2(absolutePath)) {
3363
+ if (existsSync3(absolutePath)) {
2830
3364
  return Bun.file(absolutePath);
2831
3365
  }
2832
3366
  }
2833
3367
  return null;
2834
3368
  }
2835
- var OPENAPI_TAILWIND_ASSET_FILE = resolveOpenAPITailwindAssetFile();
3369
+ var OPENAPI_TAILWIND_ASSET_FILE = resolveOpenAPIAssetFile(OPENAPI_TAILWIND_ASSET_RELATIVE_CANDIDATES, OPENAPI_TAILWIND_ASSET_CWD_CANDIDATES);
3370
+ var OPENAPI_LOGO_DARK_ASSET_FILE = resolveOpenAPIAssetFile(OPENAPI_LOGO_DARK_ASSET_RELATIVE_CANDIDATES, OPENAPI_LOGO_DARK_ASSET_CWD_CANDIDATES);
3371
+ var OPENAPI_LOGO_WHITE_ASSET_FILE = resolveOpenAPIAssetFile(OPENAPI_LOGO_WHITE_ASSET_RELATIVE_CANDIDATES, OPENAPI_LOGO_WHITE_ASSET_CWD_CANDIDATES);
3372
+ 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"));
3373
+ 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"));
3374
+ 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"));
3375
+ 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"));
3376
+ var OPENAPI_WEBMANIFEST_ASSET_FILE = resolveOpenAPIAssetFile(buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_RELATIVE_BASE_CANDIDATES, "site.webmanifest"), buildOpenAPIAssetCandidatePaths(OPENAPI_FAVICON_ASSET_CWD_BASE_CANDIDATES, "site.webmanifest"));
3377
+ 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"));
3378
+ 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"));
3379
+ var OPENAPI_FAVICON_ASSETS = [
3380
+ {
3381
+ path: OPENAPI_APPLE_TOUCH_ICON_ASSET_PATH,
3382
+ file: OPENAPI_APPLE_TOUCH_ICON_ASSET_FILE,
3383
+ contentType: "image/png",
3384
+ filename: "apple-touch-icon.png"
3385
+ },
3386
+ {
3387
+ path: OPENAPI_FAVICON_32_ASSET_PATH,
3388
+ file: OPENAPI_FAVICON_32_ASSET_FILE,
3389
+ contentType: "image/png",
3390
+ filename: "favicon-32x32.png"
3391
+ },
3392
+ {
3393
+ path: OPENAPI_FAVICON_16_ASSET_PATH,
3394
+ file: OPENAPI_FAVICON_16_ASSET_FILE,
3395
+ contentType: "image/png",
3396
+ filename: "favicon-16x16.png"
3397
+ },
3398
+ {
3399
+ path: OPENAPI_FAVICON_ICO_ASSET_PATH,
3400
+ file: OPENAPI_FAVICON_ICO_ASSET_FILE,
3401
+ contentType: "image/x-icon",
3402
+ filename: "favicon.ico"
3403
+ },
3404
+ {
3405
+ path: OPENAPI_WEBMANIFEST_ASSET_PATH,
3406
+ file: OPENAPI_WEBMANIFEST_ASSET_FILE,
3407
+ contentType: "application/manifest+json; charset=utf-8",
3408
+ filename: "site.webmanifest"
3409
+ },
3410
+ {
3411
+ path: OPENAPI_ANDROID_192_ASSET_PATH,
3412
+ file: OPENAPI_ANDROID_192_ASSET_FILE,
3413
+ contentType: "image/png",
3414
+ filename: "android-chrome-192x192.png"
3415
+ },
3416
+ {
3417
+ path: OPENAPI_ANDROID_512_ASSET_PATH,
3418
+ file: OPENAPI_ANDROID_512_ASSET_FILE,
3419
+ contentType: "image/png",
3420
+ filename: "android-chrome-512x512.png"
3421
+ }
3422
+ ];
2836
3423
  var DOCS_HTML_CACHE_CONTROL = "public, max-age=0, must-revalidate";
2837
3424
  var DOCS_ASSET_CACHE_CONTROL = "public, max-age=31536000, immutable";
3425
+ var DOCS_ASSET_ERROR_CACHE_CONTROL = "no-store";
3426
+ function escapeRegex(value) {
3427
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3428
+ }
3429
+ function wildcardPatternToRegex(pattern) {
3430
+ let regexSource = "^";
3431
+ for (const char of pattern) {
3432
+ if (char === "*") {
3433
+ regexSource += ".*";
3434
+ continue;
3435
+ }
3436
+ regexSource += escapeRegex(char);
3437
+ }
3438
+ regexSource += "$";
3439
+ return new RegExp(regexSource);
3440
+ }
3441
+ function matchesExposePath(path, exposePathPattern) {
3442
+ if (!exposePathPattern.includes("*")) {
3443
+ return path === exposePathPattern;
3444
+ }
3445
+ return wildcardPatternToRegex(exposePathPattern).test(path);
3446
+ }
2838
3447
 
2839
3448
  class VectorServer {
2840
3449
  server = null;
@@ -2845,6 +3454,8 @@ class VectorServer {
2845
3454
  openapiDocsHtmlCache = null;
2846
3455
  openapiWarningsLogged = false;
2847
3456
  openapiTailwindMissingLogged = false;
3457
+ openapiLogoDarkMissingLogged = false;
3458
+ openapiLogoWhiteMissingLogged = false;
2848
3459
  corsHandler = null;
2849
3460
  corsHeadersEntries = null;
2850
3461
  constructor(router, config) {
@@ -2894,9 +3505,10 @@ class VectorServer {
2894
3505
  }
2895
3506
  const openapiObject = openapi || {};
2896
3507
  const docsValue = openapiObject.docs;
2897
- const docs = typeof docsValue === "boolean" ? { enabled: docsValue, path: "/docs" } : {
3508
+ const docs = typeof docsValue === "boolean" ? { enabled: docsValue, path: "/docs", exposePaths: undefined } : {
2898
3509
  enabled: docsValue?.enabled === true,
2899
- path: docsValue?.path || "/docs"
3510
+ path: docsValue?.path || "/docs",
3511
+ exposePaths: Array.isArray(docsValue?.exposePaths) ? docsValue.exposePaths.map((path) => typeof path === "string" ? path.trim() : "").filter((path) => path.length > 0) : undefined
2900
3512
  };
2901
3513
  return {
2902
3514
  enabled: openapiObject.enabled ?? defaultEnabled,
@@ -2927,11 +3539,29 @@ class VectorServer {
2927
3539
  this.openapiDocCache = result.document;
2928
3540
  return this.openapiDocCache;
2929
3541
  }
3542
+ getOpenAPIDocumentForDocs() {
3543
+ const exposePaths = this.openapiConfig.docs.exposePaths;
3544
+ const document = this.getOpenAPIDocument();
3545
+ if (!Array.isArray(exposePaths) || exposePaths.length === 0) {
3546
+ return document;
3547
+ }
3548
+ const existingPaths = document.paths && typeof document.paths === "object" && !Array.isArray(document.paths) ? document.paths : {};
3549
+ const filteredPaths = {};
3550
+ for (const [path, value] of Object.entries(existingPaths)) {
3551
+ if (exposePaths.some((pattern) => matchesExposePath(path, pattern))) {
3552
+ filteredPaths[path] = value;
3553
+ }
3554
+ }
3555
+ return {
3556
+ ...document,
3557
+ paths: filteredPaths
3558
+ };
3559
+ }
2930
3560
  getOpenAPIDocsHtmlCacheEntry() {
2931
3561
  if (this.openapiDocsHtmlCache) {
2932
3562
  return this.openapiDocsHtmlCache;
2933
3563
  }
2934
- const html = renderOpenAPIDocsHtml(this.getOpenAPIDocument(), this.openapiConfig.path, OPENAPI_TAILWIND_ASSET_PATH);
3564
+ 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
3565
  const gzip = Bun.gzipSync(html);
2936
3566
  const etag = `"${Bun.hash(html).toString(16)}"`;
2937
3567
  this.openapiDocsHtmlCache = { html, gzip, etag };
@@ -2949,6 +3579,11 @@ class VectorServer {
2949
3579
  if (this.openapiConfig.docs.enabled) {
2950
3580
  reserved.add(this.openapiConfig.docs.path);
2951
3581
  reserved.add(OPENAPI_TAILWIND_ASSET_PATH);
3582
+ reserved.add(OPENAPI_LOGO_DARK_ASSET_PATH);
3583
+ reserved.add(OPENAPI_LOGO_WHITE_ASSET_PATH);
3584
+ for (const asset of OPENAPI_FAVICON_ASSETS) {
3585
+ reserved.add(asset.path);
3586
+ }
2952
3587
  }
2953
3588
  const methodConflicts = this.router.getRouteDefinitions().filter((route) => reserved.has(route.path)).map((route) => `${route.method} ${route.path}`);
2954
3589
  const staticConflicts = Object.entries(this.router.getRouteTable()).filter(([path, value]) => reserved.has(path) && value instanceof Response).map(([path]) => `STATIC ${path}`);
@@ -3021,6 +3656,71 @@ class VectorServer {
3021
3656
  }
3022
3657
  });
3023
3658
  }
3659
+ if (this.openapiConfig.docs.enabled && pathname === OPENAPI_LOGO_DARK_ASSET_PATH) {
3660
+ if (!OPENAPI_LOGO_DARK_ASSET_FILE) {
3661
+ if (!this.openapiLogoDarkMissingLogged) {
3662
+ this.openapiLogoDarkMissingLogged = true;
3663
+ console.warn('[OpenAPI] Missing docs runtime asset "logo_dark.svg".');
3664
+ }
3665
+ return new Response("OpenAPI docs runtime asset missing: logo_dark.svg", {
3666
+ status: 404,
3667
+ headers: {
3668
+ "content-type": "text/plain; charset=utf-8",
3669
+ "cache-control": DOCS_ASSET_ERROR_CACHE_CONTROL
3670
+ }
3671
+ });
3672
+ }
3673
+ return new Response(OPENAPI_LOGO_DARK_ASSET_FILE, {
3674
+ status: 200,
3675
+ headers: {
3676
+ "content-type": "image/svg+xml; charset=utf-8",
3677
+ "cache-control": DOCS_ASSET_CACHE_CONTROL
3678
+ }
3679
+ });
3680
+ }
3681
+ if (this.openapiConfig.docs.enabled && pathname === OPENAPI_LOGO_WHITE_ASSET_PATH) {
3682
+ if (!OPENAPI_LOGO_WHITE_ASSET_FILE) {
3683
+ if (!this.openapiLogoWhiteMissingLogged) {
3684
+ this.openapiLogoWhiteMissingLogged = true;
3685
+ console.warn('[OpenAPI] Missing docs runtime asset "logo_white.svg".');
3686
+ }
3687
+ return new Response("OpenAPI docs runtime asset missing: logo_white.svg", {
3688
+ status: 404,
3689
+ headers: {
3690
+ "content-type": "text/plain; charset=utf-8",
3691
+ "cache-control": DOCS_ASSET_ERROR_CACHE_CONTROL
3692
+ }
3693
+ });
3694
+ }
3695
+ return new Response(OPENAPI_LOGO_WHITE_ASSET_FILE, {
3696
+ status: 200,
3697
+ headers: {
3698
+ "content-type": "image/svg+xml; charset=utf-8",
3699
+ "cache-control": DOCS_ASSET_CACHE_CONTROL
3700
+ }
3701
+ });
3702
+ }
3703
+ if (this.openapiConfig.docs.enabled) {
3704
+ const faviconAsset = OPENAPI_FAVICON_ASSETS.find((asset) => asset.path === pathname);
3705
+ if (faviconAsset) {
3706
+ if (!faviconAsset.file) {
3707
+ return new Response(`OpenAPI docs runtime asset missing: ${faviconAsset.filename}`, {
3708
+ status: 404,
3709
+ headers: {
3710
+ "content-type": "text/plain; charset=utf-8",
3711
+ "cache-control": DOCS_ASSET_ERROR_CACHE_CONTROL
3712
+ }
3713
+ });
3714
+ }
3715
+ return new Response(faviconAsset.file, {
3716
+ status: 200,
3717
+ headers: {
3718
+ "content-type": faviconAsset.contentType,
3719
+ "cache-control": DOCS_ASSET_CACHE_CONTROL
3720
+ }
3721
+ });
3722
+ }
3723
+ }
3024
3724
  return null;
3025
3725
  }
3026
3726
  normalizeCorsOptions(options) {
@@ -3071,7 +3771,7 @@ class VectorServer {
3071
3771
  reusePort: this.config.reusePort !== false,
3072
3772
  routes: this.router.getRouteTable(),
3073
3773
  fetch: fallbackFetch,
3074
- idleTimeout: this.config.idleTimeout || 60,
3774
+ idleTimeout: this.config.idleTimeout ?? 60,
3075
3775
  error: (error, request) => {
3076
3776
  console.error("[ERROR] Server error:", error);
3077
3777
  return this.applyCors(new Response("Internal Server Error", { status: 500 }), request);
@@ -3134,6 +3834,7 @@ class Vector {
3134
3834
  routeGenerator = null;
3135
3835
  _protectedHandler = null;
3136
3836
  _cacheHandler = null;
3837
+ shutdownPromise = null;
3137
3838
  constructor() {
3138
3839
  this.middlewareManager = new MiddlewareManager;
3139
3840
  this.authManager = new AuthManager;
@@ -3148,14 +3849,22 @@ class Vector {
3148
3849
  }
3149
3850
  setProtectedHandler(handler) {
3150
3851
  this._protectedHandler = handler;
3151
- this.authManager.setProtectedHandler(handler);
3852
+ if (handler) {
3853
+ this.authManager.setProtectedHandler(handler);
3854
+ return;
3855
+ }
3856
+ this.authManager.clearProtectedHandler();
3152
3857
  }
3153
3858
  getProtectedHandler() {
3154
3859
  return this._protectedHandler;
3155
3860
  }
3156
3861
  setCacheHandler(handler) {
3157
3862
  this._cacheHandler = handler;
3158
- this.cacheManager.setCacheHandler(handler);
3863
+ if (handler) {
3864
+ this.cacheManager.setCacheHandler(handler);
3865
+ return;
3866
+ }
3867
+ this.cacheManager.clearCacheHandler();
3159
3868
  }
3160
3869
  getCacheHandler() {
3161
3870
  return this._cacheHandler;
@@ -3178,6 +3887,9 @@ class Vector {
3178
3887
  if (config?.finally) {
3179
3888
  this.middlewareManager.addFinally(...config.finally);
3180
3889
  }
3890
+ if (typeof this.config.startup === "function") {
3891
+ await this.config.startup();
3892
+ }
3181
3893
  if (this.config.autoDiscover !== false) {
3182
3894
  await this.discoverRoutes();
3183
3895
  }
@@ -3260,6 +3972,22 @@ class Vector {
3260
3972
  this.server = null;
3261
3973
  }
3262
3974
  }
3975
+ async shutdown() {
3976
+ if (this.shutdownPromise) {
3977
+ return this.shutdownPromise;
3978
+ }
3979
+ this.shutdownPromise = (async () => {
3980
+ this.stop();
3981
+ if (typeof this.config.shutdown === "function") {
3982
+ await this.config.shutdown();
3983
+ }
3984
+ })();
3985
+ try {
3986
+ await this.shutdownPromise;
3987
+ } finally {
3988
+ this.shutdownPromise = null;
3989
+ }
3990
+ }
3263
3991
  getServer() {
3264
3992
  return this.server;
3265
3993
  }
@@ -3278,111 +4006,40 @@ class Vector {
3278
4006
  }
3279
4007
  var getVectorInstance = Vector.getInstance;
3280
4008
 
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;
4009
+ // src/start-vector.ts
4010
+ async function startVector(options = {}) {
4011
+ const configLoader = new ConfigLoader(options.configPath);
4012
+ const loadedConfig = await configLoader.load();
4013
+ const configSource = configLoader.getConfigSource();
4014
+ let config = { ...loadedConfig };
4015
+ if (options.mutateConfig) {
4016
+ config = await options.mutateConfig(config, { configSource });
4017
+ }
4018
+ if (options.config) {
4019
+ config = { ...config, ...options.config };
4020
+ }
4021
+ if (options.autoDiscover !== undefined) {
4022
+ config.autoDiscover = options.autoDiscover;
4023
+ }
4024
+ const vector = getVectorInstance();
4025
+ const resolvedProtectedHandler = options.protectedHandler !== undefined ? options.protectedHandler : await configLoader.loadAuthHandler();
4026
+ const resolvedCacheHandler = options.cacheHandler !== undefined ? options.cacheHandler : await configLoader.loadCacheHandler();
4027
+ vector.setProtectedHandler(resolvedProtectedHandler ?? null);
4028
+ vector.setCacheHandler(resolvedCacheHandler ?? null);
4029
+ const server = await vector.startServer(config);
4030
+ const effectiveConfig = {
4031
+ ...config,
4032
+ port: server.port ?? config.port ?? DEFAULT_CONFIG.PORT,
4033
+ hostname: server.hostname || config.hostname || DEFAULT_CONFIG.HOSTNAME,
4034
+ reusePort: config.reusePort !== false,
4035
+ idleTimeout: config.idleTimeout ?? 60
4036
+ };
4037
+ return {
4038
+ server,
4039
+ config: effectiveConfig,
4040
+ stop: () => vector.stop(),
4041
+ shutdown: () => vector.shutdown()
4042
+ };
3386
4043
  }
3387
4044
 
3388
4045
  // src/cli/index.ts
@@ -3429,7 +4086,8 @@ var hasPortOption = args.some((arg) => arg === "--port" || arg === "-p" || arg.s
3429
4086
  async function runDev() {
3430
4087
  const isDev = command === "dev";
3431
4088
  let server = null;
3432
- let vector = null;
4089
+ let app = null;
4090
+ let removeShutdownHandlers = null;
3433
4091
  async function startServer() {
3434
4092
  const timeoutPromise = new Promise((_, reject) => {
3435
4093
  setTimeout(() => {
@@ -3438,34 +4096,30 @@ async function runDev() {
3438
4096
  });
3439
4097
  const serverStartPromise = (async () => {
3440
4098
  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);
4099
+ app = await startVector({
4100
+ configPath: explicitConfigPath,
4101
+ mutateConfig: (loadedConfig) => {
4102
+ const config2 = { ...loadedConfig };
4103
+ config2.port = resolvePort(config2.port, hasPortOption, values.port);
4104
+ config2.hostname = resolveHost(config2.hostname, hasHostOption, values.host);
4105
+ config2.routesDir = resolveRoutesDir(config2.routesDir, hasRoutesOption, values.routes);
4106
+ config2.development = config2.development ?? isDev;
4107
+ config2.autoDiscover = true;
4108
+ if (config2.cors === undefined && values.cors) {
4109
+ config2.cors = {
4110
+ origin: "*",
4111
+ credentials: true,
4112
+ allowHeaders: "Content-Type, Authorization",
4113
+ allowMethods: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
4114
+ exposeHeaders: "Authorization",
4115
+ maxAge: 86400
4116
+ };
4117
+ }
4118
+ return config2;
4119
+ }
4120
+ });
4121
+ server = app.server;
4122
+ const config = app.config;
3469
4123
  if (!server || !server.port) {
3470
4124
  throw new Error("Server started but is not responding correctly");
3471
4125
  }
@@ -3474,13 +4128,18 @@ async function runDev() {
3474
4128
  console.log(`
3475
4129
  Listening on ${cyan}http://${config.hostname}:${config.port}${reset}
3476
4130
  `);
3477
- return { server, vector, config };
4131
+ return { server, app, config };
3478
4132
  })();
3479
4133
  return await Promise.race([serverStartPromise, timeoutPromise]);
3480
4134
  }
3481
4135
  try {
3482
4136
  const result = await startServer();
3483
4137
  server = result.server;
4138
+ if (!removeShutdownHandlers) {
4139
+ removeShutdownHandlers = installGracefulShutdownHandlers({
4140
+ getTarget: () => app
4141
+ });
4142
+ }
3484
4143
  if (isDev && values.watch) {
3485
4144
  try {
3486
4145
  let reloadTimeout = null;
@@ -3504,14 +4163,14 @@ Listening on ${cyan}http://${config.hostname}:${config.port}${reset}
3504
4163
  isReloading = true;
3505
4164
  lastReloadTime = Date.now();
3506
4165
  changedFiles.clear();
3507
- if (vector) {
3508
- vector.stop();
4166
+ if (app) {
4167
+ app.stop();
3509
4168
  }
3510
4169
  await new Promise((resolve3) => setTimeout(resolve3, 100));
3511
4170
  try {
3512
4171
  const result2 = await startServer();
3513
4172
  server = result2.server;
3514
- vector = result2.vector;
4173
+ app = result2.app;
3515
4174
  } catch (error) {
3516
4175
  console.error(`
3517
4176
  [Reload Error]`, error.message || error);