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
@@ -3,7 +3,9 @@ import type { CacheManager } from '../cache/manager';
3
3
  import { APIError, createResponse } from '../http';
4
4
  import type { MiddlewareManager } from '../middleware/manager';
5
5
  import { STATIC_RESPONSES } from '../constants';
6
+ import { AuthKind } from '../types';
6
7
  import { buildRouteRegex } from '../utils/path';
8
+ import type { CheckpointGateway } from '../checkpoint/gateway';
7
9
  import type {
8
10
  BunMethodMap,
9
11
  BunRouteTable,
@@ -14,6 +16,7 @@ import type {
14
16
  RouteHandler,
15
17
  RouteOptions,
16
18
  RouteSchemaDefinition,
19
+ VectorContext,
17
20
  VectorRequest,
18
21
  VectorTypes,
19
22
  } from '../types';
@@ -25,6 +28,12 @@ import {
25
28
  runStandardValidation,
26
29
  } from '../utils/schema-validation';
27
30
 
31
+ const AUTH_KIND_VALUES = new Set<string>(Object.values(AuthKind));
32
+
33
+ function isAuthKindValue(value: unknown): value is AuthKind {
34
+ return typeof value === 'string' && AUTH_KIND_VALUES.has(value);
35
+ }
36
+
28
37
  export interface RegisteredRouteDefinition<TTypes extends VectorTypes = DefaultVectorTypes> {
29
38
  method: string;
30
39
  path: string;
@@ -37,6 +46,11 @@ interface RouteMatcher {
37
46
  specificity: number;
38
47
  }
39
48
 
49
+ interface InputValidationResult {
50
+ response: Response | null;
51
+ requiresBody: boolean;
52
+ }
53
+
40
54
  export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
41
55
  private middlewareManager: MiddlewareManager<TTypes>;
42
56
  private authManager: AuthManager<TTypes>;
@@ -48,6 +62,7 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
48
62
  private routeMatchers: RouteMatcher[] = [];
49
63
  private corsHeadersEntries: [string, string][] | null = null;
50
64
  private corsHandler: ((response: Response, request: Request) => Response) | null = null;
65
+ private checkpointGateway: CheckpointGateway | null = null;
51
66
 
52
67
  constructor(
53
68
  middlewareManager: MiddlewareManager<TTypes>,
@@ -67,6 +82,10 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
67
82
  this.corsHandler = handler;
68
83
  }
69
84
 
85
+ setCheckpointGateway(gateway: CheckpointGateway | null): void {
86
+ this.checkpointGateway = gateway;
87
+ }
88
+
70
89
  setRouteBooleanDefaults(defaults?: RouteBooleanDefaults): void {
71
90
  this.routeBooleanDefaults = { ...defaults };
72
91
  }
@@ -87,6 +106,12 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
87
106
  }
88
107
  }
89
108
 
109
+ // If a route explicitly sets auth:true and the global default auth is an AuthKind,
110
+ // promote the route to that kind so OpenAPI docs and runtime defaults stay aligned.
111
+ if (resolved.auth === true && isAuthKindValue(defaults.auth)) {
112
+ resolved.auth = defaults.auth;
113
+ }
114
+
90
115
  return resolved;
91
116
  }
92
117
 
@@ -181,157 +206,249 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
181
206
  } catch {
182
207
  return APIError.badRequest('Malformed request URL');
183
208
  }
184
- (request as any)._parsedUrl = url;
185
209
  const pathname = url.pathname;
210
+ // Fast path: exact route lookup avoids scanning regex matchers for common static/method routes.
211
+ const exactPathRoute = this.routeTable[pathname];
212
+
213
+ if (exactPathRoute) {
214
+ if (exactPathRoute instanceof Response) {
215
+ // Route table stores a shared Response instance for static routes; clone per request.
216
+ return this.applyCorsResponse(exactPathRoute.clone() as unknown as Response, request);
217
+ }
218
+
219
+ const exactPathMethodMap = exactPathRoute as BunMethodMap;
220
+ const handler =
221
+ exactPathMethodMap[request.method] ?? (request.method === 'HEAD' ? exactPathMethodMap['GET'] : undefined);
222
+
223
+ if (handler) {
224
+ const response = await handler(request);
225
+ if (response) {
226
+ return response;
227
+ }
228
+ }
229
+ }
186
230
 
187
231
  for (const matcher of this.routeMatchers) {
188
232
  const path = matcher.path;
189
- const value = this.routeTable[path];
190
- if (!value) continue;
191
- if (value instanceof Response) continue;
192
- const methodMap = value as BunMethodMap;
193
- if (request.method === 'OPTIONS' || request.method in methodMap) {
194
- const match = pathname.match(matcher.regex);
195
- if (match) {
196
- try {
197
- (request as any).params = match.groups ?? {};
198
- } catch {
199
- // Request.params can be readonly on Bun-native requests.
200
- }
201
- const handler = methodMap[request.method] ?? methodMap['GET'];
202
- if (handler) {
203
- const response = await handler(request);
204
- if (response) return response;
205
- }
233
+ const routeEntry = this.routeTable[path];
234
+ if (!routeEntry) continue;
235
+ if (routeEntry instanceof Response) {
236
+ if (pathname === path) {
237
+ // Same reason as exact-path static route handling above.
238
+ return this.applyCorsResponse(routeEntry.clone() as unknown as Response, request);
206
239
  }
240
+ continue;
241
+ }
242
+ const methodMap = routeEntry as BunMethodMap;
243
+ const handler = methodMap[request.method] ?? (request.method === 'HEAD' ? methodMap['GET'] : undefined);
244
+ if (!handler) {
245
+ continue;
207
246
  }
247
+
248
+ const match = pathname.match(matcher.regex);
249
+ if (!match) {
250
+ continue;
251
+ }
252
+
253
+ const response = await handler(request);
254
+ if (response) return response;
208
255
  }
209
256
 
210
- return STATIC_RESPONSES.NOT_FOUND.clone() as unknown as Response;
257
+ // STATIC_RESPONSES are shared singletons; clone before per-request header mutation.
258
+ return this.applyCorsResponse(STATIC_RESPONSES.NOT_FOUND.clone() as unknown as Response, request);
211
259
  }
212
260
 
213
- private prepareRequest(
261
+ private cloneMetadata<T>(value: T): T {
262
+ if (Array.isArray(value)) {
263
+ return [...value] as unknown as T;
264
+ }
265
+
266
+ if (value && typeof value === 'object') {
267
+ return { ...(value as Record<string, unknown>) } as unknown as T;
268
+ }
269
+
270
+ return value;
271
+ }
272
+
273
+ private createContext(
214
274
  request: VectorRequest<TTypes>,
215
275
  options?: {
216
- params?: Record<string, string>;
217
- route?: string;
218
276
  metadata?: any;
277
+ params?: Record<string, string>;
278
+ query?: Record<string, string | string[]>;
279
+ cookies?: Record<string, string>;
280
+ }
281
+ ): VectorContext<TTypes> {
282
+ const context = {
283
+ request,
284
+ } as VectorContext<TTypes>;
285
+
286
+ this.setContextField(
287
+ context,
288
+ 'metadata',
289
+ options?.metadata !== undefined ? this.cloneMetadata(options.metadata) : ({} as any)
290
+ );
291
+ this.setContextField(context, 'params', options?.params ?? {});
292
+ this.setContextField(context, 'query', options?.query ?? {});
293
+ this.setContextField(context, 'cookies', options?.cookies ?? {});
294
+
295
+ return context;
296
+ }
297
+
298
+ private setContextField(context: VectorContext<TTypes>, key: string, value: unknown): void {
299
+ (context as Record<string, unknown>)[key] = value;
300
+ }
301
+
302
+ private hasOwnContextField(context: VectorContext<TTypes>, key: string): boolean {
303
+ return Object.prototype.hasOwnProperty.call(context, key);
304
+ }
305
+
306
+ private buildCheckpointContextPayload(context: VectorContext<TTypes>): Record<string, unknown> {
307
+ const payload: Record<string, unknown> = {};
308
+ const allowedKeys = ['metadata', 'content', 'validatedInput', 'authUser'] as const;
309
+ for (const key of allowedKeys) {
310
+ if (!this.hasOwnContextField(context, key)) {
311
+ continue;
312
+ }
313
+
314
+ const value = (context as Record<string, unknown>)[key];
315
+ if (typeof value === 'function' || typeof value === 'symbol' || value === undefined) {
316
+ continue;
317
+ }
318
+ payload[key] = value;
219
319
  }
220
- ): void {
221
- if (!request.context) {
222
- request.context = {} as any;
320
+ return payload;
321
+ }
322
+
323
+ private resolveFallbackParams(pathname: string, routeMatcher: RegExp | null): Record<string, string> | undefined {
324
+ if (!routeMatcher) {
325
+ return undefined;
223
326
  }
224
327
 
225
- const hasEmptyParamsObject =
226
- !!request.params &&
227
- typeof request.params === 'object' &&
228
- !Array.isArray(request.params) &&
229
- Object.keys(request.params as Record<string, unknown>).length === 0;
328
+ const matched = pathname.match(routeMatcher);
329
+ if (!matched?.groups) {
330
+ return undefined;
331
+ }
230
332
 
231
- if (options?.params !== undefined && (request.params === undefined || hasEmptyParamsObject)) {
232
- try {
233
- request.params = options.params;
234
- } catch {
235
- // params is readonly (set by Bun natively) — use as-is
236
- }
333
+ return matched.groups as Record<string, string>;
334
+ }
335
+
336
+ private getRequestedCheckpointVersion(request: Request): string | null {
337
+ if (!this.checkpointGateway) {
338
+ return null;
237
339
  }
238
- if (options?.route !== undefined) {
239
- request.route = options.route;
340
+
341
+ const gateway = this.checkpointGateway as unknown as {
342
+ getRequestedVersion?: (request: Request) => string | null;
343
+ } | null;
344
+ if (gateway?.getRequestedVersion) {
345
+ return gateway.getRequestedVersion(request);
240
346
  }
241
- if (options?.metadata !== undefined) {
242
- request.metadata = options.metadata;
347
+
348
+ const primary = request.headers.get('x-vector-checkpoint-version');
349
+ if (primary && primary.trim().length > 0) {
350
+ return primary.trim();
243
351
  }
244
352
 
245
- if (request.query == null && request.url) {
246
- try {
247
- Object.defineProperty(request, 'query', {
248
- get() {
249
- const url = (this as any)._parsedUrl ?? new URL(this.url);
250
- const query = VectorRouter.parseQuery(url);
251
- Object.defineProperty(this, 'query', {
252
- value: query,
253
- writable: true,
254
- configurable: true,
255
- enumerable: true,
256
- });
257
- return query;
258
- },
259
- set(value) {
260
- Object.defineProperty(this, 'query', {
261
- value,
262
- writable: true,
263
- configurable: true,
264
- enumerable: true,
265
- });
266
- },
267
- configurable: true,
268
- enumerable: true,
269
- });
270
- } catch {
271
- const url = (request as any)._parsedUrl ?? new URL(request.url);
272
- try {
273
- request.query = VectorRouter.parseQuery(url);
274
- } catch {
275
- // Leave query as-is when request shape is non-extensible.
276
- }
277
- }
353
+ const fallback = request.headers.get('x-vector-checkpoint');
354
+ if (fallback && fallback.trim().length > 0) {
355
+ return fallback.trim();
278
356
  }
279
357
 
280
- if (!Object.getOwnPropertyDescriptor(request, 'cookies')) {
281
- Object.defineProperty(request, 'cookies', {
282
- get() {
283
- const cookieHeader = this.headers.get('cookie') ?? '';
284
- const cookies: Record<string, string> = {};
285
- if (cookieHeader) {
286
- for (const pair of cookieHeader.split(';')) {
287
- const idx = pair.indexOf('=');
288
- if (idx > 0) {
289
- cookies[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
290
- }
291
- }
292
- }
293
- Object.defineProperty(this, 'cookies', {
294
- value: cookies,
295
- writable: true,
296
- configurable: true,
297
- enumerable: true,
298
- });
299
- return cookies;
300
- },
301
- configurable: true,
302
- enumerable: true,
303
- });
358
+ return null;
359
+ }
360
+
361
+ private getCheckpointCacheKeyOverrideValue(request: Request): string | null {
362
+ if (!this.checkpointGateway) {
363
+ return null;
364
+ }
365
+
366
+ const gateway = this.checkpointGateway as unknown as {
367
+ getCacheKeyOverrideValue?: (request: Request) => string | null;
368
+ } | null;
369
+ if (gateway?.getCacheKeyOverrideValue) {
370
+ return gateway.getCacheKeyOverrideValue(request);
371
+ }
372
+
373
+ const primary = request.headers.get('x-vector-checkpoint-version');
374
+ if (primary && primary.trim().length > 0) {
375
+ return `x-vector-checkpoint-version:${primary.trim()}`;
376
+ }
377
+
378
+ const fallback = request.headers.get('x-vector-checkpoint');
379
+ if (fallback && fallback.trim().length > 0) {
380
+ return `x-vector-checkpoint:${fallback.trim()}`;
304
381
  }
382
+
383
+ return null;
305
384
  }
306
385
 
307
- private resolveFallbackParams(request: Request, routeMatcher: RegExp | null): Record<string, string> | undefined {
308
- if (!routeMatcher) {
309
- return undefined;
386
+ private applyCheckpointCacheNamespace(cacheKey: string, request: Request): string {
387
+ const checkpointVersion = this.getRequestedCheckpointVersion(request);
388
+ if (!checkpointVersion) {
389
+ return cacheKey;
310
390
  }
311
391
 
312
- const currentParams = (request as any).params;
313
- if (
314
- currentParams &&
315
- typeof currentParams === 'object' &&
316
- !Array.isArray(currentParams) &&
317
- Object.keys(currentParams as Record<string, unknown>).length > 0
318
- ) {
319
- return undefined;
392
+ return `${cacheKey}:checkpoint=${checkpointVersion}`;
393
+ }
394
+
395
+ private applyCheckpointRouteKeyOverride(cacheKey: string, request: Request): string {
396
+ const override = this.getCheckpointCacheKeyOverrideValue(request);
397
+ if (!override) {
398
+ return cacheKey;
320
399
  }
321
400
 
322
- let pathname: string;
401
+ return override;
402
+ }
403
+
404
+ private async parseRequestBodyForContext(
405
+ context: VectorContext<TTypes>,
406
+ request: Request,
407
+ checkpointRequested: boolean
408
+ ): Promise<void> {
409
+ let parsedContent: unknown = null;
323
410
  try {
324
- pathname = ((request as any)._parsedUrl ?? new URL(request.url)).pathname;
411
+ // For checkpoint requests we may forward the original stream later, so parse from a clone.
412
+ const bodyReadRequest = checkpointRequested ? request.clone() : request;
413
+ const contentType = bodyReadRequest.headers.get('content-type');
414
+ if (contentType?.startsWith('application/json')) {
415
+ parsedContent = await bodyReadRequest.json();
416
+ } else if (contentType?.startsWith('application/x-www-form-urlencoded')) {
417
+ parsedContent = Object.fromEntries(await bodyReadRequest.formData());
418
+ } else if (contentType?.startsWith('multipart/form-data')) {
419
+ parsedContent = await bodyReadRequest.formData();
420
+ } else {
421
+ parsedContent = await bodyReadRequest.text();
422
+ }
325
423
  } catch {
326
- return undefined;
424
+ parsedContent = null;
327
425
  }
328
426
 
329
- const matched = pathname.match(routeMatcher);
330
- if (!matched?.groups) {
331
- return undefined;
427
+ this.setContextField(context, 'content', parsedContent);
428
+ }
429
+
430
+ private isLikelyStreamingBodyRequest(request: Request): boolean {
431
+ if (request.method === 'GET' || request.method === 'HEAD') {
432
+ return false;
332
433
  }
333
434
 
334
- return matched.groups as Record<string, string>;
435
+ if (!request.body) {
436
+ return false;
437
+ }
438
+
439
+ if ((request as { duplex?: unknown }).duplex === 'half') {
440
+ return true;
441
+ }
442
+
443
+ const transferEncoding = request.headers.get('transfer-encoding');
444
+ if (transferEncoding) {
445
+ const hasChunked = transferEncoding.split(',').some((value) => value.trim().toLowerCase() === 'chunked');
446
+ if (hasChunked) {
447
+ return true;
448
+ }
449
+ }
450
+
451
+ return false;
335
452
  }
336
453
 
337
454
  private wrapHandler(options: RouteOptions<TTypes>, handler: RouteHandler<TTypes>) {
@@ -340,12 +457,18 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
340
457
 
341
458
  return async (request: Request) => {
342
459
  const vectorRequest = request as unknown as VectorRequest<TTypes>;
343
- const fallbackParams = this.resolveFallbackParams(request, routeMatcher);
344
-
345
- this.prepareRequest(vectorRequest, {
346
- params: fallbackParams,
347
- route: routePath,
460
+ let pathname = '';
461
+ try {
462
+ pathname = new URL(request.url).pathname;
463
+ } catch {
464
+ // Ignore malformed URLs here; router.handle() already guards route matching.
465
+ }
466
+ const fallbackParams = this.resolveFallbackParams(pathname, routeMatcher);
467
+ const context = this.createContext(vectorRequest, {
348
468
  metadata: options.metadata,
469
+ params: this.getRequestParams(request, fallbackParams),
470
+ query: this.getRequestQuery(request),
471
+ cookies: this.getRequestCookies(request),
349
472
  });
350
473
 
351
474
  try {
@@ -353,15 +476,14 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
353
476
  return APIError.forbidden('Forbidden');
354
477
  }
355
478
 
356
- const beforeResult = await this.middlewareManager.executeBefore(vectorRequest);
357
- if (beforeResult instanceof Response) {
358
- return beforeResult;
479
+ const beforeResponse = await this.middlewareManager.executeBefore(context);
480
+ if (beforeResponse instanceof Response) {
481
+ return beforeResponse;
359
482
  }
360
- const req = beforeResult as VectorRequest<TTypes>;
361
483
 
362
484
  if (options.auth) {
363
485
  try {
364
- await this.authManager.authenticate(req);
486
+ await this.authManager.authenticate(context);
365
487
  } catch (error) {
366
488
  return APIError.unauthorized(
367
489
  error instanceof Error ? error.message : 'Authentication failed',
@@ -370,84 +492,93 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
370
492
  }
371
493
  }
372
494
 
373
- if (!options.rawRequest && req.method !== 'GET' && req.method !== 'HEAD') {
374
- let parsedContent: unknown = null;
375
- try {
376
- const contentType = req.headers.get('content-type');
377
- if (contentType?.startsWith('application/json')) {
378
- parsedContent = await req.json();
379
- } else if (contentType?.startsWith('application/x-www-form-urlencoded')) {
380
- parsedContent = Object.fromEntries(await req.formData());
381
- } else if (contentType?.startsWith('multipart/form-data')) {
382
- parsedContent = await req.formData();
383
- } else {
384
- parsedContent = await req.text();
495
+ const executeRoute = async (): Promise<unknown> => {
496
+ const req = context.request;
497
+ const requestForRoute = req as unknown as Request;
498
+ const checkpointRequested = this.getRequestedCheckpointVersion(requestForRoute) !== null;
499
+ // Library-wide behavior: applies to any streaming request with input schema validation enabled,
500
+ // regardless of whether checkpoint routing is in play.
501
+ const shouldDeferStreamingValidation =
502
+ this.isLikelyStreamingBodyRequest(requestForRoute) &&
503
+ options.schema?.input !== undefined &&
504
+ options.validate !== false;
505
+
506
+ if (!options.rawRequest && req.method !== 'GET' && req.method !== 'HEAD' && !shouldDeferStreamingValidation) {
507
+ await this.parseRequestBodyForContext(context, requestForRoute, checkpointRequested);
508
+ }
509
+
510
+ if (shouldDeferStreamingValidation) {
511
+ const validationWithoutBody = await this.validateInputSchema(context, options, fallbackParams, {
512
+ includeBody: false,
513
+ allowBodyDeferral: true,
514
+ });
515
+ if (validationWithoutBody.response) {
516
+ return validationWithoutBody.response;
517
+ }
518
+
519
+ if (validationWithoutBody.requiresBody) {
520
+ if (!options.rawRequest && req.method !== 'GET' && req.method !== 'HEAD') {
521
+ await this.parseRequestBodyForContext(context, requestForRoute, checkpointRequested);
522
+ }
523
+
524
+ const fullValidation = await this.validateInputSchema(context, options, fallbackParams);
525
+ if (fullValidation.response) {
526
+ return fullValidation.response;
527
+ }
528
+ }
529
+ } else {
530
+ const inputValidation = await this.validateInputSchema(context, options, fallbackParams);
531
+ if (inputValidation.response) {
532
+ return inputValidation.response;
385
533
  }
386
- } catch {
387
- parsedContent = null;
388
534
  }
389
- this.setContentAndBodyAlias(req, parsedContent);
390
- }
391
535
 
392
- const inputValidationResponse = await this.validateInputSchema(req, options);
393
- if (inputValidationResponse) {
394
- return inputValidationResponse;
395
- }
536
+ if (this.checkpointGateway) {
537
+ const checkpointResponse = await this.checkpointGateway.handle(
538
+ req as unknown as Request,
539
+ this.buildCheckpointContextPayload(context)
540
+ );
541
+ if (checkpointResponse) {
542
+ return checkpointResponse;
543
+ }
544
+ }
545
+
546
+ return await handler(context as any);
547
+ };
396
548
 
397
- let result;
549
+ let result: any;
398
550
  const cacheOptions = options.cache;
399
551
 
400
552
  if (cacheOptions && typeof cacheOptions === 'number' && cacheOptions > 0) {
401
- const cacheKey = this.cacheManager.generateKey(req as any, {
402
- authUser: req.authUser,
403
- });
404
- result = await this.cacheManager.get(
405
- cacheKey,
406
- async () => {
407
- const res = await handler(req);
408
- if (res instanceof Response) {
409
- return {
410
- _isResponse: true,
411
- body: await res.text(),
412
- status: res.status,
413
- headers: Object.fromEntries(res.headers.entries()),
414
- };
415
- }
416
- return res;
417
- },
418
- cacheOptions
553
+ const cacheKey = this.applyCheckpointCacheNamespace(
554
+ this.cacheManager.generateKey(context.request as any, {
555
+ authUser: context.authUser,
556
+ }),
557
+ context.request as unknown as Request
419
558
  );
559
+ result = await this.cacheManager.get(cacheKey, async () => await executeRoute(), cacheOptions);
420
560
  } else if (cacheOptions && typeof cacheOptions === 'object' && cacheOptions.ttl) {
421
- const cacheKey =
422
- cacheOptions.key ||
423
- this.cacheManager.generateKey(req as any, {
424
- authUser: req.authUser,
561
+ const hasRouteCacheKey = typeof cacheOptions.key === 'string' && cacheOptions.key.length > 0;
562
+ let cacheKey: string;
563
+ if (hasRouteCacheKey) {
564
+ cacheKey = this.applyCheckpointRouteKeyOverride(
565
+ cacheOptions.key as string,
566
+ context.request as unknown as Request
567
+ );
568
+ } else {
569
+ const generatedKey = this.cacheManager.generateKey(context.request as any, {
570
+ authUser: context.authUser,
425
571
  });
426
- result = await this.cacheManager.get(
427
- cacheKey,
428
- async () => {
429
- const res = await handler(req);
430
- if (res instanceof Response) {
431
- return {
432
- _isResponse: true,
433
- body: await res.text(),
434
- status: res.status,
435
- headers: Object.fromEntries(res.headers.entries()),
436
- };
437
- }
438
- return res;
439
- },
440
- cacheOptions.ttl
441
- );
572
+ cacheKey = this.applyCheckpointCacheNamespace(generatedKey, context.request as unknown as Request);
573
+ }
574
+ result = await this.cacheManager.get(cacheKey, async () => await executeRoute(), cacheOptions.ttl);
442
575
  } else {
443
- result = await handler(req);
576
+ result = await executeRoute();
444
577
  }
445
578
 
446
- if (result && typeof result === 'object' && result._isResponse === true) {
447
- result = new Response(result.body, {
448
- status: result.status,
449
- headers: result.headers,
450
- });
579
+ if (result instanceof Response && !!cacheOptions) {
580
+ // Cache layers can return shared Response instances; clone before per-request mutations.
581
+ result = result.clone();
451
582
  }
452
583
 
453
584
  let response: Response;
@@ -457,22 +588,9 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
457
588
  response = createResponse(200, result, options.responseContentType);
458
589
  }
459
590
 
460
- response = await this.middlewareManager.executeFinally(response, req);
461
-
462
- // Apply pre-built CORS headers if configured
463
- const entries = this.corsHeadersEntries;
464
- if (entries) {
465
- for (const [k, v] of entries) {
466
- response.headers.set(k, v);
467
- }
468
- } else {
469
- const dynamicCors = this.corsHandler;
470
- if (dynamicCors) {
471
- response = dynamicCors(response, req as unknown as Request);
472
- }
473
- }
591
+ response = await this.middlewareManager.executeFinally(response, context);
474
592
 
475
- return response;
593
+ return this.applyCorsResponse(response, context.request as unknown as Request);
476
594
  } catch (error) {
477
595
  if (error instanceof Response) {
478
596
  return error;
@@ -497,13 +615,18 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
497
615
  }
498
616
 
499
617
  private async buildInputValidationPayload(
500
- request: VectorRequest<TTypes>,
501
- options: RouteOptions<TTypes>
618
+ context: VectorContext<TTypes>,
619
+ options: RouteOptions<TTypes>,
620
+ fallbackParams?: Record<string, string>,
621
+ validationOptions?: { includeBody?: boolean }
502
622
  ): Promise<Record<string, unknown>> {
503
- let body = request.content;
623
+ const request = context.request;
624
+ const includeBody = validationOptions?.includeBody !== false;
625
+ let body = includeBody && this.hasOwnContextField(context, 'content') ? context.content : undefined;
504
626
 
505
- if (options.rawRequest && request.method !== 'GET' && request.method !== 'HEAD') {
627
+ if (includeBody && options.rawRequest && request.method !== 'GET' && request.method !== 'HEAD') {
506
628
  try {
629
+ // Read raw body from a clone so handlers/checkpoint forwarding can still consume the original stream.
507
630
  body = await (request as unknown as Request).clone().text();
508
631
  } catch {
509
632
  body = null;
@@ -511,109 +634,194 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
511
634
  }
512
635
 
513
636
  return {
514
- params: request.params ?? {},
515
- query: request.query ?? {},
637
+ params: this.getRequestParams(request as unknown as Request, fallbackParams),
638
+ query: this.getRequestQuery(request as unknown as Request),
516
639
  headers: Object.fromEntries(request.headers.entries()),
517
- cookies: request.cookies ?? {},
640
+ cookies: this.getRequestCookies(request as unknown as Request),
518
641
  body,
519
642
  };
520
643
  }
521
644
 
522
- private applyValidatedInput(request: VectorRequest<TTypes>, validatedValue: unknown): void {
523
- request.validatedInput = validatedValue;
645
+ private getRequestParams(request: Request, fallbackParams?: Record<string, string>): Record<string, string> {
646
+ const nativeParams = this.readRequestObjectField(request, 'params');
647
+ if (nativeParams && Object.keys(nativeParams).length > 0) {
648
+ return nativeParams as Record<string, string>;
649
+ }
650
+ return fallbackParams ?? {};
651
+ }
524
652
 
525
- if (!validatedValue || typeof validatedValue !== 'object') {
526
- return;
653
+ private getRequestQuery(request: Request): Record<string, string | string[]> {
654
+ const nativeQuery = this.readRequestObjectField(request, 'query');
655
+ if (nativeQuery) {
656
+ return nativeQuery as Record<string, string | string[]>;
527
657
  }
528
658
 
529
- const validated = validatedValue as Record<string, unknown>;
659
+ try {
660
+ return VectorRouter.parseQuery(new URL(request.url));
661
+ } catch {
662
+ return {};
663
+ }
664
+ }
530
665
 
531
- if ('params' in validated) {
532
- try {
533
- request.params = validated.params as any;
534
- } catch {
535
- // Request.params can be readonly on Bun-native requests.
536
- }
666
+ private getRequestCookies(request: Request): Record<string, string> {
667
+ const nativeCookies = this.readRequestObjectField(request, 'cookies');
668
+ if (nativeCookies) {
669
+ return nativeCookies as Record<string, string>;
537
670
  }
538
- if ('query' in validated) {
539
- try {
540
- request.query = validated.query as any;
541
- } catch {
542
- // Request.query can be readonly/non-configurable on some request objects.
543
- }
671
+
672
+ return VectorRouter.parseCookies(request.headers.get('cookie'));
673
+ }
674
+
675
+ private readRequestObjectField(request: Request, key: string): Record<string, unknown> | undefined {
676
+ const value = (request as any)[key];
677
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
678
+ return undefined;
544
679
  }
545
- if ('cookies' in validated) {
546
- try {
547
- request.cookies = validated.cookies as any;
548
- } catch {
549
- // Request.cookies can be readonly/non-configurable on some request objects.
550
- }
680
+ return value as Record<string, unknown>;
681
+ }
682
+
683
+ private applyValidatedInput(context: VectorContext<TTypes>, validatedValue: unknown): void {
684
+ this.setContextField(context, 'validatedInput', validatedValue as any);
685
+ }
686
+
687
+ private issueHasBodyPath(issue: unknown): boolean {
688
+ if (!issue || typeof issue !== 'object' || !('path' in (issue as Record<string, unknown>))) {
689
+ return false;
551
690
  }
552
- if ('body' in validated) {
553
- this.setContentAndBodyAlias(request, validated.body);
691
+
692
+ const path = (issue as { path?: unknown }).path;
693
+ if (!Array.isArray(path) || path.length === 0) {
694
+ return false;
695
+ }
696
+
697
+ const segment = path[0];
698
+ if (segment && typeof segment === 'object' && 'key' in (segment as Record<string, unknown>)) {
699
+ return (segment as { key?: unknown }).key === 'body';
554
700
  }
701
+ return segment === 'body';
555
702
  }
556
703
 
557
- private setContentAndBodyAlias(request: VectorRequest<TTypes>, value: unknown): void {
558
- try {
559
- request.content = value;
560
- } catch {
561
- // Request.content can be readonly/non-configurable on some request objects.
562
- return;
704
+ private issueHasExplicitNonBodyPath(issue: unknown): boolean {
705
+ if (!issue || typeof issue !== 'object' || !('path' in (issue as Record<string, unknown>))) {
706
+ return false;
563
707
  }
564
708
 
565
- this.setBodyAlias(request, value);
709
+ const path = (issue as { path?: unknown }).path;
710
+ if (!Array.isArray(path) || path.length === 0) {
711
+ return false;
712
+ }
713
+
714
+ const segment = path[0];
715
+ if (segment && typeof segment === 'object' && 'key' in (segment as Record<string, unknown>)) {
716
+ return (segment as { key?: unknown }).key !== 'body';
717
+ }
718
+ return segment !== 'body';
566
719
  }
567
720
 
568
- private setBodyAlias(request: VectorRequest<TTypes>, value: unknown): void {
569
- try {
570
- request.body = value as any;
571
- } catch {
572
- // Keep request.content as source of truth when body alias is readonly.
721
+ private issueHasUnknownPath(issue: unknown): boolean {
722
+ if (!issue || typeof issue !== 'object' || !('path' in (issue as Record<string, unknown>))) {
723
+ return true;
724
+ }
725
+
726
+ const path = (issue as { path?: unknown }).path;
727
+ if (!Array.isArray(path)) {
728
+ return true;
729
+ }
730
+
731
+ return path.length === 0;
732
+ }
733
+
734
+ private shouldDeferBodyValidation(
735
+ issues: readonly unknown[],
736
+ context: VectorContext<TTypes>,
737
+ validationOptions?: { includeBody?: boolean; allowBodyDeferral?: boolean }
738
+ ): boolean {
739
+ if (!(validationOptions?.allowBodyDeferral === true && validationOptions?.includeBody === false)) {
740
+ return false;
741
+ }
742
+
743
+ const request = context.request as unknown as Request;
744
+ const mayHaveRequestBody = request.method !== 'GET' && request.method !== 'HEAD' && request.body !== null;
745
+ if (!mayHaveRequestBody || issues.length === 0) {
746
+ return false;
747
+ }
748
+
749
+ if (issues.some((issue) => this.issueHasBodyPath(issue))) {
750
+ return true;
573
751
  }
752
+
753
+ // Conservative fallback: if issues do not identify a non-body target and at least one issue
754
+ // has unknown/empty path, retry once with body included.
755
+ const hasExplicitNonBodyPath = issues.some((issue) => this.issueHasExplicitNonBodyPath(issue));
756
+ const hasUnknownPath = issues.some((issue) => this.issueHasUnknownPath(issue));
757
+ return !hasExplicitNonBodyPath && hasUnknownPath;
574
758
  }
575
759
 
576
760
  private async validateInputSchema(
577
- request: VectorRequest<TTypes>,
578
- options: RouteOptions<TTypes>
579
- ): Promise<Response | null> {
761
+ context: VectorContext<TTypes>,
762
+ options: RouteOptions<TTypes>,
763
+ fallbackParams?: Record<string, string>,
764
+ validationOptions?: { includeBody?: boolean; allowBodyDeferral?: boolean }
765
+ ): Promise<InputValidationResult> {
580
766
  const inputSchema = options.schema?.input;
581
767
 
582
768
  if (!inputSchema) {
583
- return null;
769
+ return { response: null, requiresBody: false };
584
770
  }
585
771
 
586
772
  if (options.validate === false) {
587
- return null;
773
+ return { response: null, requiresBody: false };
588
774
  }
589
775
 
590
776
  if (!isStandardRouteSchema(inputSchema)) {
591
- return APIError.internalServerError('Invalid route schema configuration', options.responseContentType);
777
+ return {
778
+ response: APIError.internalServerError('Invalid route schema configuration', options.responseContentType),
779
+ requiresBody: false,
780
+ };
592
781
  }
593
782
 
594
783
  const includeRawIssues = this.isDevelopmentMode();
595
- const payload = await this.buildInputValidationPayload(request, options);
784
+ const payload = await this.buildInputValidationPayload(context, options, fallbackParams, {
785
+ includeBody: validationOptions?.includeBody,
786
+ });
596
787
 
597
788
  try {
598
789
  const validation = await runStandardValidation(inputSchema, payload);
599
790
  if (validation.success === false) {
791
+ if (this.shouldDeferBodyValidation(validation.issues, context, validationOptions)) {
792
+ return { response: null, requiresBody: true };
793
+ }
794
+
600
795
  const issues = normalizeValidationIssues(validation.issues, includeRawIssues);
601
- return createResponse(422, createValidationErrorPayload('input', issues), options.responseContentType);
796
+ return {
797
+ response: createResponse(422, createValidationErrorPayload('input', issues), options.responseContentType),
798
+ requiresBody: false,
799
+ };
602
800
  }
603
801
 
604
- this.applyValidatedInput(request, validation.value);
605
- return null;
802
+ this.applyValidatedInput(context, validation.value);
803
+ return { response: null, requiresBody: false };
606
804
  } catch (error) {
607
805
  const thrownIssues = extractThrownIssues(error);
608
806
  if (thrownIssues) {
807
+ if (this.shouldDeferBodyValidation(thrownIssues, context, validationOptions)) {
808
+ return { response: null, requiresBody: true };
809
+ }
810
+
609
811
  const issues = normalizeValidationIssues(thrownIssues, includeRawIssues);
610
- return createResponse(422, createValidationErrorPayload('input', issues), options.responseContentType);
812
+ return {
813
+ response: createResponse(422, createValidationErrorPayload('input', issues), options.responseContentType),
814
+ requiresBody: false,
815
+ };
611
816
  }
612
817
 
613
- return APIError.internalServerError(
614
- error instanceof Error ? error.message : 'Validation failed',
615
- options.responseContentType
616
- );
818
+ return {
819
+ response: APIError.internalServerError(
820
+ error instanceof Error ? error.message : 'Validation failed',
821
+ options.responseContentType
822
+ ),
823
+ requiresBody: false,
824
+ };
617
825
  }
618
826
  }
619
827
 
@@ -672,6 +880,22 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
672
880
  return query;
673
881
  }
674
882
 
883
+ private static parseCookies(cookieHeader: string | null): Record<string, string> {
884
+ const cookies: Record<string, string> = {};
885
+ if (!cookieHeader) {
886
+ return cookies;
887
+ }
888
+
889
+ for (const pair of cookieHeader.split(';')) {
890
+ const idx = pair.indexOf('=');
891
+ if (idx > 0) {
892
+ cookies[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
893
+ }
894
+ }
895
+
896
+ return cookies;
897
+ }
898
+
675
899
  private routeSpecificityScore(path: string): number {
676
900
  const STATIC_SEGMENT_WEIGHT = 1000;
677
901
  const PARAM_SEGMENT_WEIGHT = 10;
@@ -698,4 +922,21 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
698
922
 
699
923
  return score;
700
924
  }
925
+
926
+ private applyCorsResponse(response: Response, request: Request): Response {
927
+ const entries = this.corsHeadersEntries;
928
+ if (entries) {
929
+ for (const [k, v] of entries) {
930
+ response.headers.set(k, v);
931
+ }
932
+ return response;
933
+ }
934
+
935
+ const dynamicCors = this.corsHandler;
936
+ if (dynamicCors) {
937
+ return dynamicCors(response, request);
938
+ }
939
+
940
+ return response;
941
+ }
701
942
  }