vector-framework 1.2.2 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/README.md +18 -6
  2. package/dist/auth/protected.d.ts +4 -4
  3. package/dist/auth/protected.d.ts.map +1 -1
  4. package/dist/auth/protected.js +10 -7
  5. package/dist/auth/protected.js.map +1 -1
  6. package/dist/cache/manager.d.ts +2 -0
  7. package/dist/cache/manager.d.ts.map +1 -1
  8. package/dist/cache/manager.js +21 -4
  9. package/dist/cache/manager.js.map +1 -1
  10. package/dist/checkpoint/artifacts/compressor.d.ts +5 -0
  11. package/dist/checkpoint/artifacts/compressor.d.ts.map +1 -0
  12. package/dist/checkpoint/artifacts/compressor.js +24 -0
  13. package/dist/checkpoint/artifacts/compressor.js.map +1 -0
  14. package/dist/checkpoint/artifacts/decompress-worker.d.ts +2 -0
  15. package/dist/checkpoint/artifacts/decompress-worker.d.ts.map +1 -0
  16. package/dist/checkpoint/artifacts/decompress-worker.js +31 -0
  17. package/dist/checkpoint/artifacts/decompress-worker.js.map +1 -0
  18. package/dist/checkpoint/artifacts/hasher.d.ts +2 -0
  19. package/dist/checkpoint/artifacts/hasher.d.ts.map +1 -0
  20. package/dist/checkpoint/artifacts/hasher.js +7 -0
  21. package/dist/checkpoint/artifacts/hasher.js.map +1 -0
  22. package/dist/checkpoint/artifacts/manifest.d.ts +6 -0
  23. package/dist/checkpoint/artifacts/manifest.d.ts.map +1 -0
  24. package/dist/checkpoint/artifacts/manifest.js +55 -0
  25. package/dist/checkpoint/artifacts/manifest.js.map +1 -0
  26. package/dist/checkpoint/artifacts/materializer.d.ts +16 -0
  27. package/dist/checkpoint/artifacts/materializer.d.ts.map +1 -0
  28. package/dist/checkpoint/artifacts/materializer.js +168 -0
  29. package/dist/checkpoint/artifacts/materializer.js.map +1 -0
  30. package/dist/checkpoint/artifacts/packager.d.ts +12 -0
  31. package/dist/checkpoint/artifacts/packager.d.ts.map +1 -0
  32. package/dist/checkpoint/artifacts/packager.js +82 -0
  33. package/dist/checkpoint/artifacts/packager.js.map +1 -0
  34. package/dist/checkpoint/artifacts/repository.d.ts +11 -0
  35. package/dist/checkpoint/artifacts/repository.d.ts.map +1 -0
  36. package/dist/checkpoint/artifacts/repository.js +29 -0
  37. package/dist/checkpoint/artifacts/repository.js.map +1 -0
  38. package/dist/checkpoint/artifacts/store.d.ts +13 -0
  39. package/dist/checkpoint/artifacts/store.d.ts.map +1 -0
  40. package/dist/checkpoint/artifacts/store.js +85 -0
  41. package/dist/checkpoint/artifacts/store.js.map +1 -0
  42. package/dist/checkpoint/artifacts/types.d.ts +21 -0
  43. package/dist/checkpoint/artifacts/types.d.ts.map +1 -0
  44. package/dist/checkpoint/artifacts/types.js +2 -0
  45. package/dist/checkpoint/artifacts/types.js.map +1 -0
  46. package/dist/checkpoint/artifacts/worker-decompressor.d.ts +17 -0
  47. package/dist/checkpoint/artifacts/worker-decompressor.d.ts.map +1 -0
  48. package/dist/checkpoint/artifacts/worker-decompressor.js +148 -0
  49. package/dist/checkpoint/artifacts/worker-decompressor.js.map +1 -0
  50. package/dist/checkpoint/asset-store.d.ts +10 -0
  51. package/dist/checkpoint/asset-store.d.ts.map +1 -0
  52. package/dist/checkpoint/asset-store.js +46 -0
  53. package/dist/checkpoint/asset-store.js.map +1 -0
  54. package/dist/checkpoint/bundler.d.ts +15 -0
  55. package/dist/checkpoint/bundler.d.ts.map +1 -0
  56. package/dist/checkpoint/bundler.js +45 -0
  57. package/dist/checkpoint/bundler.js.map +1 -0
  58. package/dist/checkpoint/cli.d.ts +2 -0
  59. package/dist/checkpoint/cli.d.ts.map +1 -0
  60. package/dist/checkpoint/cli.js +157 -0
  61. package/dist/checkpoint/cli.js.map +1 -0
  62. package/dist/checkpoint/entrypoint-generator.d.ts +17 -0
  63. package/dist/checkpoint/entrypoint-generator.d.ts.map +1 -0
  64. package/dist/checkpoint/entrypoint-generator.js +251 -0
  65. package/dist/checkpoint/entrypoint-generator.js.map +1 -0
  66. package/dist/checkpoint/forwarder.d.ts +6 -0
  67. package/dist/checkpoint/forwarder.d.ts.map +1 -0
  68. package/dist/checkpoint/forwarder.js +74 -0
  69. package/dist/checkpoint/forwarder.js.map +1 -0
  70. package/dist/checkpoint/gateway.d.ts +11 -0
  71. package/dist/checkpoint/gateway.d.ts.map +1 -0
  72. package/dist/checkpoint/gateway.js +30 -0
  73. package/dist/checkpoint/gateway.js.map +1 -0
  74. package/dist/checkpoint/ipc.d.ts +12 -0
  75. package/dist/checkpoint/ipc.d.ts.map +1 -0
  76. package/dist/checkpoint/ipc.js +96 -0
  77. package/dist/checkpoint/ipc.js.map +1 -0
  78. package/dist/checkpoint/manager.d.ts +20 -0
  79. package/dist/checkpoint/manager.d.ts.map +1 -0
  80. package/dist/checkpoint/manager.js +214 -0
  81. package/dist/checkpoint/manager.js.map +1 -0
  82. package/dist/checkpoint/process-manager.d.ts +35 -0
  83. package/dist/checkpoint/process-manager.d.ts.map +1 -0
  84. package/dist/checkpoint/process-manager.js +203 -0
  85. package/dist/checkpoint/process-manager.js.map +1 -0
  86. package/dist/checkpoint/resolver.d.ts +25 -0
  87. package/dist/checkpoint/resolver.d.ts.map +1 -0
  88. package/dist/checkpoint/resolver.js +95 -0
  89. package/dist/checkpoint/resolver.js.map +1 -0
  90. package/dist/checkpoint/socket-path.d.ts +2 -0
  91. package/dist/checkpoint/socket-path.d.ts.map +1 -0
  92. package/dist/checkpoint/socket-path.js +51 -0
  93. package/dist/checkpoint/socket-path.js.map +1 -0
  94. package/dist/checkpoint/types.d.ts +54 -0
  95. package/dist/checkpoint/types.d.ts.map +1 -0
  96. package/dist/checkpoint/types.js +2 -0
  97. package/dist/checkpoint/types.js.map +1 -0
  98. package/dist/cli/index.js +10 -2
  99. package/dist/cli/index.js.map +1 -1
  100. package/dist/cli/option-resolution.d.ts +1 -1
  101. package/dist/cli/option-resolution.d.ts.map +1 -1
  102. package/dist/cli/option-resolution.js.map +1 -1
  103. package/dist/cli.js +3709 -328
  104. package/dist/core/config-loader.d.ts +1 -0
  105. package/dist/core/config-loader.d.ts.map +1 -1
  106. package/dist/core/config-loader.js +10 -2
  107. package/dist/core/config-loader.js.map +1 -1
  108. package/dist/core/router.d.ts +24 -3
  109. package/dist/core/router.d.ts.map +1 -1
  110. package/dist/core/router.js +398 -249
  111. package/dist/core/router.js.map +1 -1
  112. package/dist/core/server.d.ts +2 -0
  113. package/dist/core/server.d.ts.map +1 -1
  114. package/dist/core/server.js +22 -8
  115. package/dist/core/server.js.map +1 -1
  116. package/dist/core/vector.d.ts +3 -0
  117. package/dist/core/vector.d.ts.map +1 -1
  118. package/dist/core/vector.js +51 -1
  119. package/dist/core/vector.js.map +1 -1
  120. package/dist/dev/route-scanner.d.ts.map +1 -1
  121. package/dist/dev/route-scanner.js +2 -1
  122. package/dist/dev/route-scanner.js.map +1 -1
  123. package/dist/http.d.ts +32 -7
  124. package/dist/http.d.ts.map +1 -1
  125. package/dist/http.js +144 -13
  126. package/dist/http.js.map +1 -1
  127. package/dist/index.cjs +1297 -74
  128. package/dist/index.d.ts +3 -2
  129. package/dist/index.d.ts.map +1 -1
  130. package/dist/index.js +2 -2
  131. package/dist/index.js.map +1 -1
  132. package/dist/index.mjs +1296 -73
  133. package/dist/middleware/manager.d.ts +3 -3
  134. package/dist/middleware/manager.d.ts.map +1 -1
  135. package/dist/middleware/manager.js +9 -8
  136. package/dist/middleware/manager.js.map +1 -1
  137. package/dist/openapi/docs-ui.d.ts.map +1 -1
  138. package/dist/openapi/docs-ui.js +1097 -61
  139. package/dist/openapi/docs-ui.js.map +1 -1
  140. package/dist/openapi/generator.d.ts +2 -1
  141. package/dist/openapi/generator.d.ts.map +1 -1
  142. package/dist/openapi/generator.js +240 -7
  143. package/dist/openapi/generator.js.map +1 -1
  144. package/dist/types/index.d.ts +71 -28
  145. package/dist/types/index.d.ts.map +1 -1
  146. package/dist/types/index.js +24 -1
  147. package/dist/types/index.js.map +1 -1
  148. package/dist/utils/validation.d.ts.map +1 -1
  149. package/dist/utils/validation.js +3 -2
  150. package/dist/utils/validation.js.map +1 -1
  151. package/package.json +2 -1
  152. package/src/auth/protected.ts +11 -8
  153. package/src/cache/manager.ts +23 -4
  154. package/src/checkpoint/artifacts/compressor.ts +30 -0
  155. package/src/checkpoint/artifacts/decompress-worker.ts +49 -0
  156. package/src/checkpoint/artifacts/hasher.ts +6 -0
  157. package/src/checkpoint/artifacts/manifest.ts +72 -0
  158. package/src/checkpoint/artifacts/materializer.ts +211 -0
  159. package/src/checkpoint/artifacts/packager.ts +100 -0
  160. package/src/checkpoint/artifacts/repository.ts +36 -0
  161. package/src/checkpoint/artifacts/store.ts +102 -0
  162. package/src/checkpoint/artifacts/types.ts +24 -0
  163. package/src/checkpoint/artifacts/worker-decompressor.ts +192 -0
  164. package/src/checkpoint/asset-store.ts +61 -0
  165. package/src/checkpoint/bundler.ts +64 -0
  166. package/src/checkpoint/cli.ts +177 -0
  167. package/src/checkpoint/entrypoint-generator.ts +275 -0
  168. package/src/checkpoint/forwarder.ts +84 -0
  169. package/src/checkpoint/gateway.ts +40 -0
  170. package/src/checkpoint/ipc.ts +107 -0
  171. package/src/checkpoint/manager.ts +254 -0
  172. package/src/checkpoint/process-manager.ts +250 -0
  173. package/src/checkpoint/resolver.ts +124 -0
  174. package/src/checkpoint/socket-path.ts +61 -0
  175. package/src/checkpoint/types.ts +63 -0
  176. package/src/cli/index.ts +11 -2
  177. package/src/cli/option-resolution.ts +5 -1
  178. package/src/core/config-loader.ts +11 -2
  179. package/src/core/router.ts +505 -264
  180. package/src/core/server.ts +36 -9
  181. package/src/core/vector.ts +60 -1
  182. package/src/dev/route-scanner.ts +2 -1
  183. package/src/http.ts +219 -19
  184. package/src/index.ts +3 -2
  185. package/src/middleware/manager.ts +10 -10
  186. package/src/openapi/docs-ui.ts +1097 -61
  187. package/src/openapi/generator.ts +265 -6
  188. package/src/types/index.ts +83 -30
  189. package/src/utils/validation.ts +5 -3
@@ -1,17 +1,25 @@
1
1
  import type { Server } from 'bun';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { STATIC_RESPONSES } from '../constants';
5
4
  import { cors } from '../utils/cors';
6
5
  import { renderOpenAPIDocsHtml } from '../openapi/docs-ui';
7
6
  import { generateOpenAPIDocument } from '../openapi/generator';
8
- import type { CorsOptions, DefaultVectorTypes, OpenAPIOptions, VectorConfig, VectorTypes } from '../types';
7
+ import type {
8
+ CorsOptions,
9
+ DefaultVectorTypes,
10
+ OpenAPIAuthOptions,
11
+ OpenAPIOptions,
12
+ VectorConfig,
13
+ VectorTypes,
14
+ } from '../types';
15
+ import type { CheckpointGateway } from '../checkpoint/gateway';
9
16
  import type { VectorRouter } from './router';
10
17
 
11
18
  interface NormalizedOpenAPIConfig {
12
19
  enabled: boolean;
13
20
  path: string;
14
21
  target: string;
22
+ auth?: OpenAPIAuthOptions;
15
23
  docs: {
16
24
  enabled: boolean;
17
25
  path: string;
@@ -202,6 +210,7 @@ const OPENAPI_FAVICON_ASSETS = [
202
210
  const DOCS_HTML_CACHE_CONTROL = 'public, max-age=0, must-revalidate';
203
211
  const DOCS_ASSET_CACHE_CONTROL = 'public, max-age=31536000, immutable';
204
212
  const DOCS_ASSET_ERROR_CACHE_CONTROL = 'no-store';
213
+ const DEFAULT_PORT = 3000;
205
214
 
206
215
  interface OpenAPIDocsHtmlCacheEntry {
207
216
  html: string;
@@ -209,6 +218,19 @@ interface OpenAPIDocsHtmlCacheEntry {
209
218
  etag: string;
210
219
  }
211
220
 
221
+ function normalizePort(port: number | string | undefined): number {
222
+ if (port === undefined) {
223
+ return DEFAULT_PORT;
224
+ }
225
+
226
+ const normalized = Number(port);
227
+ if (!Number.isInteger(normalized) || normalized < 0 || normalized > 65535) {
228
+ throw new Error(`Invalid port: ${String(port)}. Port must be an integer between 0 and 65535.`);
229
+ }
230
+
231
+ return normalized;
232
+ }
233
+
212
234
  function escapeRegex(value: string): string {
213
235
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
214
236
  }
@@ -283,6 +305,10 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
283
305
  }
284
306
  }
285
307
 
308
+ setCheckpointGateway(gateway: CheckpointGateway): void {
309
+ this.router.setCheckpointGateway(gateway);
310
+ }
311
+
286
312
  private normalizeOpenAPIConfig(
287
313
  openapi: OpenAPIOptions | boolean | undefined,
288
314
  development: boolean | undefined
@@ -329,6 +355,7 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
329
355
  target: openapiObject.target || 'openapi-3.0',
330
356
  docs,
331
357
  info: openapiObject.info,
358
+ auth: openapiObject.auth,
332
359
  };
333
360
  }
334
361
 
@@ -359,6 +386,7 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
359
386
  const result = generateOpenAPIDocument(routes as any, {
360
387
  target: this.openapiConfig.target,
361
388
  info: this.openapiConfig.info,
389
+ auth: this.openapiConfig.auth,
362
390
  });
363
391
 
364
392
  if (!this.openapiWarningsLogged && result.warnings.length > 0) {
@@ -647,12 +675,12 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
647
675
  }
648
676
 
649
677
  async start(): Promise<Server> {
650
- const port = this.config.port ?? 3000;
678
+ const port = normalizePort(this.config.port);
651
679
  const hostname = this.config.hostname || 'localhost';
652
680
 
653
681
  this.validateReservedOpenAPIPaths();
654
682
 
655
- const fallbackFetch = async (request: Request): Promise<Response> => {
683
+ const appFetch = async (request: Request): Promise<Response> => {
656
684
  try {
657
685
  // Handle CORS preflight for any path
658
686
  if (this.corsHandler && request.method === 'OPTIONS') {
@@ -665,8 +693,8 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
665
693
  return this.applyCors(openapiResponse, request);
666
694
  }
667
695
 
668
- // No route matched return 404
669
- return this.applyCors(STATIC_RESPONSES.NOT_FOUND.clone() as unknown as Response, request);
696
+ // Route through Vector router (includes middleware/auth/validation + CORS).
697
+ return await this.router.handle(request);
670
698
  } catch (error) {
671
699
  console.error('Server error:', error);
672
700
  return this.applyCors(new Response('Internal Server Error', { status: 500 }), request);
@@ -678,8 +706,7 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
678
706
  port,
679
707
  hostname,
680
708
  reusePort: this.config.reusePort !== false,
681
- routes: this.router.getRouteTable(),
682
- fetch: fallbackFetch,
709
+ fetch: appFetch,
683
710
  idleTimeout: this.config.idleTimeout ?? 60,
684
711
  error: (error, request?: Request) => {
685
712
  console.error('[ERROR] Server error:', error);
@@ -724,7 +751,7 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
724
751
  }
725
752
 
726
753
  getPort(): number {
727
- return this.server?.port ?? this.config.port ?? 3000;
754
+ return this.server?.port ?? normalizePort(this.config.port);
728
755
  }
729
756
 
730
757
  getHostname(): string {
@@ -5,6 +5,7 @@ import { RouteGenerator } from '../dev/route-generator';
5
5
  import { RouteScanner } from '../dev/route-scanner';
6
6
  import { MiddlewareManager } from '../middleware/manager';
7
7
  import { toFileUrl } from '../utils/path';
8
+ import type { CheckpointConfig } from '../checkpoint/types';
8
9
  import type {
9
10
  CacheHandler,
10
11
  DefaultVectorTypes,
@@ -40,6 +41,7 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
40
41
  private _protectedHandler: ProtectedHandler<TTypes> | null = null;
41
42
  private _cacheHandler: CacheHandler | null = null;
42
43
  private shutdownPromise: Promise<void> | null = null;
44
+ private checkpointProcessManager: { stopAll: () => Promise<void> } | null = null;
43
45
 
44
46
  private constructor() {
45
47
  this.middlewareManager = new MiddlewareManager<TTypes>();
@@ -95,6 +97,11 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
95
97
 
96
98
  // Internal method to start server - only called by CLI
97
99
  async startServer(config?: VectorConfig<TTypes>): Promise<Server> {
100
+ if (this.checkpointProcessManager) {
101
+ await this.checkpointProcessManager.stopAll();
102
+ this.checkpointProcessManager = null;
103
+ }
104
+
98
105
  this.config = { ...this.config, ...config };
99
106
  const routeDefaults = { ...this.config.defaults?.route };
100
107
  this.router.setRouteBooleanDefaults(routeDefaults);
@@ -126,6 +133,9 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
126
133
 
127
134
  this.server = new VectorServer<TTypes>(this.router, this.config);
128
135
  const bunServer = await this.server.start();
136
+ if (this.config.checkpoint && this.config.checkpoint.enabled !== false) {
137
+ await this.enableCheckpointGateway(this.config.checkpoint);
138
+ }
129
139
 
130
140
  return bunServer;
131
141
  }
@@ -154,7 +164,7 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
154
164
  try {
155
165
  const importPath = toFileUrl(route.path);
156
166
 
157
- const module = await import(importPath);
167
+ const module = await import(`${importPath}?t=${Date.now()}`);
158
168
  const exported = route.name === 'default' ? module.default : module[route.name];
159
169
 
160
170
  if (exported) {
@@ -232,6 +242,49 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
232
242
  // Silent - no logging
233
243
  }
234
244
 
245
+ async enableCheckpointGateway(checkpointConfig?: CheckpointConfig): Promise<void> {
246
+ if (!this.server) {
247
+ throw new Error('Cannot enable checkpoint gateway before server is started. Call startServer() first.');
248
+ }
249
+
250
+ const { CheckpointManager } = await import('../checkpoint/manager');
251
+ const { CheckpointProcessManager } = await import('../checkpoint/process-manager');
252
+ const { CheckpointResolver } = await import('../checkpoint/resolver');
253
+ const { CheckpointForwarder } = await import('../checkpoint/forwarder');
254
+ const { CheckpointGateway } = await import('../checkpoint/gateway');
255
+
256
+ if (this.checkpointProcessManager) {
257
+ await this.checkpointProcessManager.stopAll();
258
+ this.checkpointProcessManager = null;
259
+ }
260
+
261
+ const manager = new CheckpointManager(checkpointConfig);
262
+ const processManager = new CheckpointProcessManager({
263
+ idleTimeoutMs: checkpointConfig?.idleTimeoutMs,
264
+ });
265
+ this.checkpointProcessManager = processManager;
266
+
267
+ const resolver = new CheckpointResolver(manager, processManager, {
268
+ versionHeader: checkpointConfig?.versionHeader,
269
+ cacheKeyOverride: checkpointConfig?.cacheKeyOverride,
270
+ });
271
+ const forwarder = new CheckpointForwarder();
272
+ const gateway = new CheckpointGateway(resolver, forwarder);
273
+
274
+ this.server.setCheckpointGateway(gateway);
275
+
276
+ // Auto-start the active checkpoint if one exists
277
+ const active = await manager.getActive();
278
+ if (active) {
279
+ try {
280
+ const manifest = await manager.readManifest(active.version);
281
+ await processManager.spawn(manifest, manager.getStorageDir());
282
+ } catch (err) {
283
+ console.error(`[Checkpoint] Failed to start active checkpoint ${active.version}:`, err);
284
+ }
285
+ }
286
+ }
287
+
235
288
  stop(): void {
236
289
  if (this.server) {
237
290
  this.server.stop();
@@ -250,6 +303,12 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
250
303
  // Stop accepting new traffic first.
251
304
  this.stop();
252
305
 
306
+ // Stop all checkpoint child processes
307
+ if (this.checkpointProcessManager) {
308
+ await this.checkpointProcessManager.stopAll();
309
+ this.checkpointProcessManager = null;
310
+ }
311
+
253
312
  if (typeof this.config.shutdown === 'function') {
254
313
  await this.config.shutdown();
255
314
  }
@@ -95,9 +95,10 @@ export class RouteScanner {
95
95
 
96
96
  try {
97
97
  // Convert Windows paths to URLs for import
98
+ // Append cache-busting query to force re-import on dev reload
98
99
  const importPath = process.platform === 'win32' ? `file:///${fullPath.replace(/\\/g, '/')}` : fullPath;
99
100
 
100
- const module = await import(importPath);
101
+ const module = await import(`${importPath}?t=${Date.now()}`);
101
102
 
102
103
  if (module.default && typeof module.default === 'function') {
103
104
  routes.push({
package/src/http.ts CHANGED
@@ -4,7 +4,9 @@ import type {
4
4
  DefaultVectorTypes,
5
5
  GetAuthType,
6
6
  InferRouteInputFromSchemaDefinition,
7
+ RouteAuthOption,
7
8
  RouteSchemaDefinition,
9
+ VectorContext,
8
10
  VectorRequest,
9
11
  VectorTypes,
10
12
  } from './types';
@@ -23,7 +25,7 @@ export interface RouteDefinition<
23
25
  > {
24
26
  entry: { method: string; path: string };
25
27
  options: ExtendedApiOptions<TSchemaDef>;
26
- handler: (req: VectorRequest<TTypes, TValidatedInput>) => Promise<unknown> | unknown;
28
+ handler: (ctx: VectorContext<TTypes, TValidatedInput>) => Promise<unknown> | unknown;
27
29
  }
28
30
 
29
31
  export function route<
@@ -31,7 +33,7 @@ export function route<
31
33
  TSchemaDef extends RouteSchemaDefinition | undefined = RouteSchemaDefinition | undefined,
32
34
  >(
33
35
  options: ExtendedApiOptions<TSchemaDef>,
34
- fn: (req: VectorRequest<TTypes, InferRouteInputFromSchemaDefinition<TSchemaDef>>) => Promise<unknown> | unknown
36
+ fn: (ctx: VectorContext<TTypes, InferRouteInputFromSchemaDefinition<TSchemaDef>>) => Promise<unknown> | unknown
35
37
  ): RouteDefinition<TTypes, InferRouteInputFromSchemaDefinition<TSchemaDef>, TSchemaDef> {
36
38
  return {
37
39
  entry: {
@@ -43,6 +45,22 @@ export function route<
43
45
  };
44
46
  }
45
47
 
48
+ export function depRoute<
49
+ TTypes extends VectorTypes = DefaultVectorTypes,
50
+ TSchemaDef extends RouteSchemaDefinition | undefined = RouteSchemaDefinition | undefined,
51
+ >(
52
+ options: ExtendedApiOptions<TSchemaDef>,
53
+ fn: (ctx: VectorContext<TTypes, InferRouteInputFromSchemaDefinition<TSchemaDef>>) => Promise<unknown> | unknown
54
+ ): RouteDefinition<TTypes, InferRouteInputFromSchemaDefinition<TSchemaDef>, TSchemaDef> {
55
+ return route<TTypes, TSchemaDef>(
56
+ {
57
+ ...options,
58
+ deprecated: true,
59
+ },
60
+ fn
61
+ );
62
+ }
63
+
46
64
  function stringifyData(data: unknown): string {
47
65
  const val = data ?? null;
48
66
  try {
@@ -55,6 +73,93 @@ function stringifyData(data: unknown): string {
55
73
  }
56
74
  }
57
75
 
76
+ export type ResponseCookieSameSite = 'Strict' | 'Lax' | 'None';
77
+ export type ResponseCookiePriority = 'Low' | 'Medium' | 'High';
78
+
79
+ export interface ResponseCookie {
80
+ name: string;
81
+ value: string;
82
+ domain?: string;
83
+ path?: string;
84
+ maxAge?: number;
85
+ expires?: Date | string;
86
+ httpOnly?: boolean;
87
+ secure?: boolean;
88
+ sameSite?: ResponseCookieSameSite;
89
+ partitioned?: boolean;
90
+ priority?: ResponseCookiePriority;
91
+ }
92
+
93
+ export type ResponseCookieInput = ResponseCookie | string;
94
+ export type ResponseHeadersInit = Headers | Array<[string, string]> | Record<string, string>;
95
+
96
+ export interface CreateResponseOptions {
97
+ contentType?: string;
98
+ headers?: ResponseHeadersInit;
99
+ cookies?: ResponseCookieInput[];
100
+ statusText?: string;
101
+ }
102
+
103
+ function isJsonContentType(contentType: string): boolean {
104
+ const mimeType = contentType.split(';', 1)[0] ?? contentType;
105
+ return mimeType.trim().toLowerCase() === CONTENT_TYPES.JSON;
106
+ }
107
+
108
+ function serializeCookie(cookie: ResponseCookie): string {
109
+ const segments = [`${cookie.name}=${cookie.value}`];
110
+
111
+ if (cookie.maxAge !== undefined && Number.isFinite(cookie.maxAge)) {
112
+ segments.push(`Max-Age=${Math.trunc(cookie.maxAge)}`);
113
+ }
114
+
115
+ if (cookie.domain) {
116
+ segments.push(`Domain=${cookie.domain}`);
117
+ }
118
+
119
+ if (cookie.path) {
120
+ segments.push(`Path=${cookie.path}`);
121
+ }
122
+
123
+ if (cookie.expires !== undefined) {
124
+ const expiresAt = cookie.expires instanceof Date ? cookie.expires : new Date(cookie.expires);
125
+ if (!Number.isNaN(expiresAt.getTime())) {
126
+ segments.push(`Expires=${expiresAt.toUTCString()}`);
127
+ }
128
+ }
129
+
130
+ if (cookie.httpOnly) {
131
+ segments.push('HttpOnly');
132
+ }
133
+
134
+ if (cookie.secure) {
135
+ segments.push('Secure');
136
+ }
137
+
138
+ if (cookie.sameSite) {
139
+ segments.push(`SameSite=${cookie.sameSite}`);
140
+ }
141
+
142
+ if (cookie.partitioned) {
143
+ segments.push('Partitioned');
144
+ }
145
+
146
+ if (cookie.priority) {
147
+ segments.push(`Priority=${cookie.priority}`);
148
+ }
149
+
150
+ return segments.join('; ');
151
+ }
152
+
153
+ function appendSetCookieHeaders(headers: Headers, cookies?: ResponseCookieInput[]): void {
154
+ if (!cookies || cookies.length === 0) {
155
+ return;
156
+ }
157
+
158
+ for (const cookie of cookies) {
159
+ headers.append('set-cookie', typeof cookie === 'string' ? cookie : serializeCookie(cookie));
160
+ }
161
+ }
162
+
58
163
  const ApiResponse = {
59
164
  success: <T>(data: T, contentType?: string) => createResponse(HTTP_STATUS.OK, data, contentType),
60
165
  created: <T>(data: T, contentType?: string) => createResponse(HTTP_STATUS.CREATED, data, contentType),
@@ -177,17 +282,36 @@ export const APIError = {
177
282
  custom: (statusCode: number, msg: string, contentType?: string) => createErrorResponse(statusCode, msg, contentType),
178
283
  };
179
284
 
180
- export function createResponse(statusCode: number, data?: unknown, contentType: string = CONTENT_TYPES.JSON): Response {
181
- const body = contentType === CONTENT_TYPES.JSON ? stringifyData(data) : data;
285
+ export function createResponse(
286
+ statusCode: number,
287
+ data?: unknown,
288
+ optionsOrContentType: string | CreateResponseOptions = CONTENT_TYPES.JSON
289
+ ): Response {
290
+ const options =
291
+ typeof optionsOrContentType === 'string'
292
+ ? ({ contentType: optionsOrContentType } as CreateResponseOptions)
293
+ : (optionsOrContentType ?? {});
294
+
295
+ const headers = new Headers(options.headers);
296
+ const contentType = options.contentType ?? headers.get('content-type') ?? CONTENT_TYPES.JSON;
297
+
298
+ if (options.contentType || !headers.has('content-type')) {
299
+ headers.set('content-type', contentType);
300
+ }
182
301
 
183
- return new Response(body as string, {
302
+ appendSetCookieHeaders(headers, options.cookies);
303
+
304
+ const body = isJsonContentType(contentType) ? stringifyData(data) : data;
305
+
306
+ return new Response(body as any, {
184
307
  status: statusCode,
185
- headers: { 'content-type': contentType },
308
+ statusText: options.statusText,
309
+ headers,
186
310
  });
187
311
  }
188
312
 
189
313
  export const protectedRoute = async <TTypes extends VectorTypes = DefaultVectorTypes>(
190
- request: VectorRequest<TTypes>,
314
+ context: VectorContext<TTypes>,
191
315
  responseContentType?: string
192
316
  ) => {
193
317
  const vector = getVectorInstance();
@@ -198,16 +322,17 @@ export const protectedRoute = async <TTypes extends VectorTypes = DefaultVectorT
198
322
  }
199
323
 
200
324
  try {
201
- const authUser = await protectedHandler(request as any);
202
- request.authUser = authUser as GetAuthType<TTypes>;
325
+ const authUser = await protectedHandler(context as any);
326
+ context.authUser = authUser as GetAuthType<TTypes>;
203
327
  } catch (error) {
204
328
  throw APIError.unauthorized(error instanceof Error ? error.message : 'Authentication failed', responseContentType);
205
329
  }
206
330
  };
207
331
 
208
332
  export interface ApiOptions<TSchemaDef extends RouteSchemaDefinition | undefined = RouteSchemaDefinition | undefined> {
209
- auth?: boolean;
333
+ auth?: RouteAuthOption;
210
334
  expose?: boolean;
335
+ deprecated?: boolean;
211
336
  rawRequest?: boolean;
212
337
  validate?: boolean;
213
338
  rawResponse?: boolean;
@@ -218,7 +343,7 @@ export interface ApiOptions<TSchemaDef extends RouteSchemaDefinition | undefined
218
343
 
219
344
  export function api<TTypes extends VectorTypes = DefaultVectorTypes, TValidatedInput = undefined>(
220
345
  options: ApiOptions,
221
- fn: (request: VectorRequest<TTypes, TValidatedInput>) => Promise<unknown> | unknown
346
+ fn: (context: VectorContext<TTypes, TValidatedInput>) => Promise<unknown> | unknown
222
347
  ) {
223
348
  const {
224
349
  auth = false,
@@ -229,7 +354,20 @@ export function api<TTypes extends VectorTypes = DefaultVectorTypes, TValidatedI
229
354
  } = options;
230
355
 
231
356
  return async (request: Request) => {
232
- const req = request as unknown as VectorRequest<TTypes>;
357
+ const req = request as unknown as VectorRequest<TTypes, TValidatedInput>;
358
+ let query: Record<string, string | string[]> = {};
359
+ try {
360
+ query = parseQuery(new URL(request.url));
361
+ } catch {
362
+ query = {};
363
+ }
364
+ const ctx = {
365
+ request: req,
366
+ metadata: {} as any,
367
+ params: {} as any,
368
+ query: query as any,
369
+ cookies: parseCookies(request.headers.get('cookie')) as any,
370
+ } as VectorContext<TTypes, TValidatedInput>;
233
371
 
234
372
  if (!expose) {
235
373
  return APIError.forbidden('Forbidden');
@@ -237,27 +375,31 @@ export function api<TTypes extends VectorTypes = DefaultVectorTypes, TValidatedI
237
375
 
238
376
  try {
239
377
  if (auth) {
240
- await protectedRoute(req, responseContentType);
378
+ await protectedRoute(ctx as unknown as VectorContext<TTypes>, responseContentType);
241
379
  }
242
380
 
243
381
  if (!rawRequest && req.method !== 'GET' && req.method !== 'HEAD') {
244
382
  try {
245
383
  const contentType = req.headers.get('content-type');
246
384
  if (contentType?.startsWith('application/json')) {
247
- req.content = await req.json();
385
+ setContextField(ctx as unknown as Record<string, unknown>, 'content', await req.json());
248
386
  } else if (contentType?.startsWith('application/x-www-form-urlencoded')) {
249
- req.content = Object.fromEntries(await req.formData());
387
+ setContextField(
388
+ ctx as unknown as Record<string, unknown>,
389
+ 'content',
390
+ Object.fromEntries(await req.formData())
391
+ );
250
392
  } else if (contentType?.startsWith('multipart/form-data')) {
251
- req.content = await req.formData();
393
+ setContextField(ctx as unknown as Record<string, unknown>, 'content', await req.formData());
252
394
  } else {
253
- req.content = await req.text();
395
+ setContextField(ctx as unknown as Record<string, unknown>, 'content', await req.text());
254
396
  }
255
397
  } catch {
256
- req.content = null;
398
+ setContextField(ctx as unknown as Record<string, unknown>, 'content', null);
257
399
  }
258
400
  }
259
401
 
260
- const result = await fn(req as unknown as VectorRequest<TTypes, TValidatedInput>);
402
+ const result = await fn(ctx);
261
403
 
262
404
  return rawResponse ? result : ApiResponse.success(result, responseContentType);
263
405
  } catch (err: unknown) {
@@ -269,4 +411,62 @@ export function api<TTypes extends VectorTypes = DefaultVectorTypes, TValidatedI
269
411
  };
270
412
  }
271
413
 
414
+ function setContextField(target: Record<string, unknown>, key: string, value: unknown): void {
415
+ const ownDescriptor = Object.getOwnPropertyDescriptor(target as object, key);
416
+ if (ownDescriptor && ownDescriptor.writable === false && typeof ownDescriptor.set !== 'function') {
417
+ return;
418
+ }
419
+
420
+ try {
421
+ target[key] = value;
422
+ return;
423
+ } catch {
424
+ // Fall back to defining an own property when inherited Request fields are readonly accessors.
425
+ }
426
+
427
+ try {
428
+ Object.defineProperty(target, key, {
429
+ value,
430
+ writable: true,
431
+ configurable: true,
432
+ enumerable: true,
433
+ });
434
+ } catch {
435
+ // Ignore when target is non-extensible.
436
+ }
437
+ }
438
+
272
439
  export default ApiResponse;
440
+
441
+ function parseQuery(url: URL): Record<string, string | string[]> {
442
+ const query: Record<string, string | string[]> = {};
443
+ for (const [key, value] of url.searchParams) {
444
+ if (key in query) {
445
+ const existing = query[key];
446
+ if (Array.isArray(existing)) {
447
+ existing.push(value);
448
+ } else {
449
+ query[key] = [existing as string, value];
450
+ }
451
+ } else {
452
+ query[key] = value;
453
+ }
454
+ }
455
+ return query;
456
+ }
457
+
458
+ function parseCookies(cookieHeader: string | null): Record<string, string> {
459
+ const cookies: Record<string, string> = {};
460
+ if (!cookieHeader) {
461
+ return cookies;
462
+ }
463
+
464
+ for (const pair of cookieHeader.split(';')) {
465
+ const idx = pair.indexOf('=');
466
+ if (idx > 0) {
467
+ cookies[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
468
+ }
469
+ }
470
+
471
+ return cookies;
472
+ }
package/src/index.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  // Public exports for route definitions and Vector startup API
2
- import { route } from './http';
2
+ import { depRoute, route } from './http';
3
3
  import { startVector } from './start-vector';
4
4
 
5
5
  // Export route function for defining routes
6
- export { route };
6
+ export { depRoute, route };
7
7
  export { startVector };
8
8
 
9
9
  // Export utilities for route handlers
10
10
  export { APIError, createResponse } from './http';
11
+ export type { CreateResponseOptions, ResponseCookie, ResponseCookieInput } from './http';
11
12
 
12
13
  // Export types for TypeScript users
13
14
  export * from './types';
@@ -2,7 +2,7 @@ import type {
2
2
  AfterMiddlewareHandler,
3
3
  BeforeMiddlewareHandler,
4
4
  DefaultVectorTypes,
5
- VectorRequest,
5
+ VectorContext,
6
6
  VectorTypes,
7
7
  } from '../types';
8
8
 
@@ -18,32 +18,32 @@ export class MiddlewareManager<TTypes extends VectorTypes = DefaultVectorTypes>
18
18
  this.finallyHandlers.push(...handlers);
19
19
  }
20
20
 
21
- async executeBefore(request: VectorRequest<TTypes>): Promise<VectorRequest<TTypes> | Response> {
22
- if (this.beforeHandlers.length === 0) return request;
23
-
24
- let currentRequest = request;
21
+ async executeBefore(context: VectorContext<TTypes>): Promise<Response | null> {
22
+ if (this.beforeHandlers.length === 0) return null;
25
23
 
26
24
  for (const handler of this.beforeHandlers) {
27
- const result = await handler(currentRequest);
25
+ const result = await handler(context);
28
26
 
29
27
  if (result instanceof Response) {
30
28
  return result;
31
29
  }
32
30
 
33
- currentRequest = result;
31
+ if (result !== undefined) {
32
+ throw new TypeError('Before middleware must return void or Response');
33
+ }
34
34
  }
35
35
 
36
- return currentRequest;
36
+ return null;
37
37
  }
38
38
 
39
- async executeFinally(response: Response, request: VectorRequest<TTypes>): Promise<Response> {
39
+ async executeFinally(response: Response, context: VectorContext<TTypes>): Promise<Response> {
40
40
  if (this.finallyHandlers.length === 0) return response;
41
41
 
42
42
  let currentResponse = response;
43
43
 
44
44
  for (const handler of this.finallyHandlers) {
45
45
  try {
46
- currentResponse = await handler(currentResponse, request);
46
+ currentResponse = await handler(currentResponse, context);
47
47
  } catch (error) {
48
48
  // Log but don't throw - we don't want to break the response chain
49
49
  console.error('After middleware error:', error);