vector-framework 1.2.1 → 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 (190) 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 +3817 -350
  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 +3 -0
  113. package/dist/core/server.d.ts.map +1 -1
  114. package/dist/core/server.js +35 -10
  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/errors/index.cjs +2 -0
  124. package/dist/http.d.ts +32 -7
  125. package/dist/http.d.ts.map +1 -1
  126. package/dist/http.js +144 -13
  127. package/dist/http.js.map +1 -1
  128. package/dist/index.cjs +2657 -0
  129. package/dist/index.d.ts +3 -2
  130. package/dist/index.d.ts.map +1 -1
  131. package/dist/index.js +12 -1433
  132. package/dist/index.js.map +1 -1
  133. package/dist/index.mjs +1301 -77
  134. package/dist/middleware/manager.d.ts +3 -3
  135. package/dist/middleware/manager.d.ts.map +1 -1
  136. package/dist/middleware/manager.js +9 -8
  137. package/dist/middleware/manager.js.map +1 -1
  138. package/dist/openapi/docs-ui.d.ts.map +1 -1
  139. package/dist/openapi/docs-ui.js +1097 -61
  140. package/dist/openapi/docs-ui.js.map +1 -1
  141. package/dist/openapi/generator.d.ts +2 -1
  142. package/dist/openapi/generator.d.ts.map +1 -1
  143. package/dist/openapi/generator.js +332 -16
  144. package/dist/openapi/generator.js.map +1 -1
  145. package/dist/types/index.d.ts +71 -28
  146. package/dist/types/index.d.ts.map +1 -1
  147. package/dist/types/index.js +24 -1
  148. package/dist/types/index.js.map +1 -1
  149. package/dist/utils/validation.d.ts.map +1 -1
  150. package/dist/utils/validation.js +3 -2
  151. package/dist/utils/validation.js.map +1 -1
  152. package/package.json +9 -14
  153. package/src/auth/protected.ts +11 -8
  154. package/src/cache/manager.ts +23 -4
  155. package/src/checkpoint/artifacts/compressor.ts +30 -0
  156. package/src/checkpoint/artifacts/decompress-worker.ts +49 -0
  157. package/src/checkpoint/artifacts/hasher.ts +6 -0
  158. package/src/checkpoint/artifacts/manifest.ts +72 -0
  159. package/src/checkpoint/artifacts/materializer.ts +211 -0
  160. package/src/checkpoint/artifacts/packager.ts +100 -0
  161. package/src/checkpoint/artifacts/repository.ts +36 -0
  162. package/src/checkpoint/artifacts/store.ts +102 -0
  163. package/src/checkpoint/artifacts/types.ts +24 -0
  164. package/src/checkpoint/artifacts/worker-decompressor.ts +192 -0
  165. package/src/checkpoint/asset-store.ts +61 -0
  166. package/src/checkpoint/bundler.ts +64 -0
  167. package/src/checkpoint/cli.ts +177 -0
  168. package/src/checkpoint/entrypoint-generator.ts +275 -0
  169. package/src/checkpoint/forwarder.ts +84 -0
  170. package/src/checkpoint/gateway.ts +40 -0
  171. package/src/checkpoint/ipc.ts +107 -0
  172. package/src/checkpoint/manager.ts +254 -0
  173. package/src/checkpoint/process-manager.ts +250 -0
  174. package/src/checkpoint/resolver.ts +124 -0
  175. package/src/checkpoint/socket-path.ts +61 -0
  176. package/src/checkpoint/types.ts +63 -0
  177. package/src/cli/index.ts +11 -2
  178. package/src/cli/option-resolution.ts +5 -1
  179. package/src/core/config-loader.ts +11 -2
  180. package/src/core/router.ts +505 -264
  181. package/src/core/server.ts +51 -11
  182. package/src/core/vector.ts +60 -1
  183. package/src/dev/route-scanner.ts +2 -1
  184. package/src/http.ts +219 -19
  185. package/src/index.ts +3 -2
  186. package/src/middleware/manager.ts +10 -10
  187. package/src/openapi/docs-ui.ts +1097 -61
  188. package/src/openapi/generator.ts +380 -13
  189. package/src/types/index.ts +83 -30
  190. package/src/utils/validation.ts +5 -3
@@ -1,7 +1,12 @@
1
1
  import { APIError, createResponse } from '../http';
2
2
  import { STATIC_RESPONSES } from '../constants';
3
+ import { AuthKind } from '../types';
3
4
  import { buildRouteRegex } from '../utils/path';
4
5
  import { createValidationErrorPayload, extractThrownIssues, isStandardRouteSchema, normalizeValidationIssues, runStandardValidation, } from '../utils/schema-validation';
6
+ const AUTH_KIND_VALUES = new Set(Object.values(AuthKind));
7
+ function isAuthKindValue(value) {
8
+ return typeof value === 'string' && AUTH_KIND_VALUES.has(value);
9
+ }
5
10
  export class VectorRouter {
6
11
  middlewareManager;
7
12
  authManager;
@@ -13,6 +18,7 @@ export class VectorRouter {
13
18
  routeMatchers = [];
14
19
  corsHeadersEntries = null;
15
20
  corsHandler = null;
21
+ checkpointGateway = null;
16
22
  constructor(middlewareManager, authManager, cacheManager) {
17
23
  this.middlewareManager = middlewareManager;
18
24
  this.authManager = authManager;
@@ -24,6 +30,9 @@ export class VectorRouter {
24
30
  setCorsHandler(handler) {
25
31
  this.corsHandler = handler;
26
32
  }
33
+ setCheckpointGateway(gateway) {
34
+ this.checkpointGateway = gateway;
35
+ }
27
36
  setRouteBooleanDefaults(defaults) {
28
37
  this.routeBooleanDefaults = { ...defaults };
29
38
  }
@@ -39,6 +48,11 @@ export class VectorRouter {
39
48
  resolved[key] = defaults[key];
40
49
  }
41
50
  }
51
+ // If a route explicitly sets auth:true and the global default auth is an AuthKind,
52
+ // promote the route to that kind so OpenAPI docs and runtime defaults stay aligned.
53
+ if (resolved.auth === true && isAuthKindValue(defaults.auth)) {
54
+ resolved.auth = defaults.auth;
55
+ }
42
56
  return resolved;
43
57
  }
44
58
  route(options, handler) {
@@ -119,243 +133,301 @@ export class VectorRouter {
119
133
  catch {
120
134
  return APIError.badRequest('Malformed request URL');
121
135
  }
122
- request._parsedUrl = url;
123
136
  const pathname = url.pathname;
137
+ // Fast path: exact route lookup avoids scanning regex matchers for common static/method routes.
138
+ const exactPathRoute = this.routeTable[pathname];
139
+ if (exactPathRoute) {
140
+ if (exactPathRoute instanceof Response) {
141
+ // Route table stores a shared Response instance for static routes; clone per request.
142
+ return this.applyCorsResponse(exactPathRoute.clone(), request);
143
+ }
144
+ const exactPathMethodMap = exactPathRoute;
145
+ const handler = exactPathMethodMap[request.method] ?? (request.method === 'HEAD' ? exactPathMethodMap['GET'] : undefined);
146
+ if (handler) {
147
+ const response = await handler(request);
148
+ if (response) {
149
+ return response;
150
+ }
151
+ }
152
+ }
124
153
  for (const matcher of this.routeMatchers) {
125
154
  const path = matcher.path;
126
- const value = this.routeTable[path];
127
- if (!value)
155
+ const routeEntry = this.routeTable[path];
156
+ if (!routeEntry)
128
157
  continue;
129
- if (value instanceof Response)
130
- continue;
131
- const methodMap = value;
132
- if (request.method === 'OPTIONS' || request.method in methodMap) {
133
- const match = pathname.match(matcher.regex);
134
- if (match) {
135
- try {
136
- request.params = match.groups ?? {};
137
- }
138
- catch {
139
- // Request.params can be readonly on Bun-native requests.
140
- }
141
- const handler = methodMap[request.method] ?? methodMap['GET'];
142
- if (handler) {
143
- const response = await handler(request);
144
- if (response)
145
- return response;
146
- }
158
+ if (routeEntry instanceof Response) {
159
+ if (pathname === path) {
160
+ // Same reason as exact-path static route handling above.
161
+ return this.applyCorsResponse(routeEntry.clone(), request);
147
162
  }
163
+ continue;
148
164
  }
149
- }
150
- return STATIC_RESPONSES.NOT_FOUND.clone();
151
- }
152
- prepareRequest(request, options) {
153
- if (!request.context) {
154
- request.context = {};
155
- }
156
- const hasEmptyParamsObject = !!request.params &&
157
- typeof request.params === 'object' &&
158
- !Array.isArray(request.params) &&
159
- Object.keys(request.params).length === 0;
160
- if (options?.params !== undefined && (request.params === undefined || hasEmptyParamsObject)) {
161
- try {
162
- request.params = options.params;
165
+ const methodMap = routeEntry;
166
+ const handler = methodMap[request.method] ?? (request.method === 'HEAD' ? methodMap['GET'] : undefined);
167
+ if (!handler) {
168
+ continue;
163
169
  }
164
- catch {
165
- // params is readonly (set by Bun natively) — use as-is
170
+ const match = pathname.match(matcher.regex);
171
+ if (!match) {
172
+ continue;
166
173
  }
174
+ const response = await handler(request);
175
+ if (response)
176
+ return response;
167
177
  }
168
- if (options?.route !== undefined) {
169
- request.route = options.route;
178
+ // STATIC_RESPONSES are shared singletons; clone before per-request header mutation.
179
+ return this.applyCorsResponse(STATIC_RESPONSES.NOT_FOUND.clone(), request);
180
+ }
181
+ cloneMetadata(value) {
182
+ if (Array.isArray(value)) {
183
+ return [...value];
170
184
  }
171
- if (options?.metadata !== undefined) {
172
- request.metadata = options.metadata;
185
+ if (value && typeof value === 'object') {
186
+ return { ...value };
173
187
  }
174
- if (request.query == null && request.url) {
175
- try {
176
- Object.defineProperty(request, 'query', {
177
- get() {
178
- const url = this._parsedUrl ?? new URL(this.url);
179
- const query = VectorRouter.parseQuery(url);
180
- Object.defineProperty(this, 'query', {
181
- value: query,
182
- writable: true,
183
- configurable: true,
184
- enumerable: true,
185
- });
186
- return query;
187
- },
188
- set(value) {
189
- Object.defineProperty(this, 'query', {
190
- value,
191
- writable: true,
192
- configurable: true,
193
- enumerable: true,
194
- });
195
- },
196
- configurable: true,
197
- enumerable: true,
198
- });
188
+ return value;
189
+ }
190
+ createContext(request, options) {
191
+ const context = {
192
+ request,
193
+ };
194
+ this.setContextField(context, 'metadata', options?.metadata !== undefined ? this.cloneMetadata(options.metadata) : {});
195
+ this.setContextField(context, 'params', options?.params ?? {});
196
+ this.setContextField(context, 'query', options?.query ?? {});
197
+ this.setContextField(context, 'cookies', options?.cookies ?? {});
198
+ return context;
199
+ }
200
+ setContextField(context, key, value) {
201
+ context[key] = value;
202
+ }
203
+ hasOwnContextField(context, key) {
204
+ return Object.prototype.hasOwnProperty.call(context, key);
205
+ }
206
+ buildCheckpointContextPayload(context) {
207
+ const payload = {};
208
+ const allowedKeys = ['metadata', 'content', 'validatedInput', 'authUser'];
209
+ for (const key of allowedKeys) {
210
+ if (!this.hasOwnContextField(context, key)) {
211
+ continue;
199
212
  }
200
- catch {
201
- const url = request._parsedUrl ?? new URL(request.url);
202
- try {
203
- request.query = VectorRouter.parseQuery(url);
204
- }
205
- catch {
206
- // Leave query as-is when request shape is non-extensible.
207
- }
213
+ const value = context[key];
214
+ if (typeof value === 'function' || typeof value === 'symbol' || value === undefined) {
215
+ continue;
208
216
  }
217
+ payload[key] = value;
209
218
  }
210
- if (!Object.getOwnPropertyDescriptor(request, 'cookies')) {
211
- Object.defineProperty(request, 'cookies', {
212
- get() {
213
- const cookieHeader = this.headers.get('cookie') ?? '';
214
- const cookies = {};
215
- if (cookieHeader) {
216
- for (const pair of cookieHeader.split(';')) {
217
- const idx = pair.indexOf('=');
218
- if (idx > 0) {
219
- cookies[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
220
- }
221
- }
222
- }
223
- Object.defineProperty(this, 'cookies', {
224
- value: cookies,
225
- writable: true,
226
- configurable: true,
227
- enumerable: true,
228
- });
229
- return cookies;
230
- },
231
- configurable: true,
232
- enumerable: true,
233
- });
234
- }
219
+ return payload;
235
220
  }
236
- resolveFallbackParams(request, routeMatcher) {
221
+ resolveFallbackParams(pathname, routeMatcher) {
237
222
  if (!routeMatcher) {
238
223
  return undefined;
239
224
  }
240
- const currentParams = request.params;
241
- if (currentParams &&
242
- typeof currentParams === 'object' &&
243
- !Array.isArray(currentParams) &&
244
- Object.keys(currentParams).length > 0) {
225
+ const matched = pathname.match(routeMatcher);
226
+ if (!matched?.groups) {
245
227
  return undefined;
246
228
  }
247
- let pathname;
229
+ return matched.groups;
230
+ }
231
+ getRequestedCheckpointVersion(request) {
232
+ if (!this.checkpointGateway) {
233
+ return null;
234
+ }
235
+ const gateway = this.checkpointGateway;
236
+ if (gateway?.getRequestedVersion) {
237
+ return gateway.getRequestedVersion(request);
238
+ }
239
+ const primary = request.headers.get('x-vector-checkpoint-version');
240
+ if (primary && primary.trim().length > 0) {
241
+ return primary.trim();
242
+ }
243
+ const fallback = request.headers.get('x-vector-checkpoint');
244
+ if (fallback && fallback.trim().length > 0) {
245
+ return fallback.trim();
246
+ }
247
+ return null;
248
+ }
249
+ getCheckpointCacheKeyOverrideValue(request) {
250
+ if (!this.checkpointGateway) {
251
+ return null;
252
+ }
253
+ const gateway = this.checkpointGateway;
254
+ if (gateway?.getCacheKeyOverrideValue) {
255
+ return gateway.getCacheKeyOverrideValue(request);
256
+ }
257
+ const primary = request.headers.get('x-vector-checkpoint-version');
258
+ if (primary && primary.trim().length > 0) {
259
+ return `x-vector-checkpoint-version:${primary.trim()}`;
260
+ }
261
+ const fallback = request.headers.get('x-vector-checkpoint');
262
+ if (fallback && fallback.trim().length > 0) {
263
+ return `x-vector-checkpoint:${fallback.trim()}`;
264
+ }
265
+ return null;
266
+ }
267
+ applyCheckpointCacheNamespace(cacheKey, request) {
268
+ const checkpointVersion = this.getRequestedCheckpointVersion(request);
269
+ if (!checkpointVersion) {
270
+ return cacheKey;
271
+ }
272
+ return `${cacheKey}:checkpoint=${checkpointVersion}`;
273
+ }
274
+ applyCheckpointRouteKeyOverride(cacheKey, request) {
275
+ const override = this.getCheckpointCacheKeyOverrideValue(request);
276
+ if (!override) {
277
+ return cacheKey;
278
+ }
279
+ return override;
280
+ }
281
+ async parseRequestBodyForContext(context, request, checkpointRequested) {
282
+ let parsedContent = null;
248
283
  try {
249
- pathname = (request._parsedUrl ?? new URL(request.url)).pathname;
284
+ // For checkpoint requests we may forward the original stream later, so parse from a clone.
285
+ const bodyReadRequest = checkpointRequested ? request.clone() : request;
286
+ const contentType = bodyReadRequest.headers.get('content-type');
287
+ if (contentType?.startsWith('application/json')) {
288
+ parsedContent = await bodyReadRequest.json();
289
+ }
290
+ else if (contentType?.startsWith('application/x-www-form-urlencoded')) {
291
+ parsedContent = Object.fromEntries(await bodyReadRequest.formData());
292
+ }
293
+ else if (contentType?.startsWith('multipart/form-data')) {
294
+ parsedContent = await bodyReadRequest.formData();
295
+ }
296
+ else {
297
+ parsedContent = await bodyReadRequest.text();
298
+ }
250
299
  }
251
300
  catch {
252
- return undefined;
301
+ parsedContent = null;
253
302
  }
254
- const matched = pathname.match(routeMatcher);
255
- if (!matched?.groups) {
256
- return undefined;
303
+ this.setContextField(context, 'content', parsedContent);
304
+ }
305
+ isLikelyStreamingBodyRequest(request) {
306
+ if (request.method === 'GET' || request.method === 'HEAD') {
307
+ return false;
257
308
  }
258
- return matched.groups;
309
+ if (!request.body) {
310
+ return false;
311
+ }
312
+ if (request.duplex === 'half') {
313
+ return true;
314
+ }
315
+ const transferEncoding = request.headers.get('transfer-encoding');
316
+ if (transferEncoding) {
317
+ const hasChunked = transferEncoding.split(',').some((value) => value.trim().toLowerCase() === 'chunked');
318
+ if (hasChunked) {
319
+ return true;
320
+ }
321
+ }
322
+ return false;
259
323
  }
260
324
  wrapHandler(options, handler) {
261
325
  const routePath = options.path;
262
326
  const routeMatcher = routePath.includes(':') ? buildRouteRegex(routePath) : null;
263
327
  return async (request) => {
264
328
  const vectorRequest = request;
265
- const fallbackParams = this.resolveFallbackParams(request, routeMatcher);
266
- this.prepareRequest(vectorRequest, {
267
- params: fallbackParams,
268
- route: routePath,
329
+ let pathname = '';
330
+ try {
331
+ pathname = new URL(request.url).pathname;
332
+ }
333
+ catch {
334
+ // Ignore malformed URLs here; router.handle() already guards route matching.
335
+ }
336
+ const fallbackParams = this.resolveFallbackParams(pathname, routeMatcher);
337
+ const context = this.createContext(vectorRequest, {
269
338
  metadata: options.metadata,
339
+ params: this.getRequestParams(request, fallbackParams),
340
+ query: this.getRequestQuery(request),
341
+ cookies: this.getRequestCookies(request),
270
342
  });
271
343
  try {
272
344
  if (options.expose === false) {
273
345
  return APIError.forbidden('Forbidden');
274
346
  }
275
- const beforeResult = await this.middlewareManager.executeBefore(vectorRequest);
276
- if (beforeResult instanceof Response) {
277
- return beforeResult;
347
+ const beforeResponse = await this.middlewareManager.executeBefore(context);
348
+ if (beforeResponse instanceof Response) {
349
+ return beforeResponse;
278
350
  }
279
- const req = beforeResult;
280
351
  if (options.auth) {
281
352
  try {
282
- await this.authManager.authenticate(req);
353
+ await this.authManager.authenticate(context);
283
354
  }
284
355
  catch (error) {
285
356
  return APIError.unauthorized(error instanceof Error ? error.message : 'Authentication failed', options.responseContentType);
286
357
  }
287
358
  }
288
- if (!options.rawRequest && req.method !== 'GET' && req.method !== 'HEAD') {
289
- let parsedContent = null;
290
- try {
291
- const contentType = req.headers.get('content-type');
292
- if (contentType?.startsWith('application/json')) {
293
- parsedContent = await req.json();
294
- }
295
- else if (contentType?.startsWith('application/x-www-form-urlencoded')) {
296
- parsedContent = Object.fromEntries(await req.formData());
359
+ const executeRoute = async () => {
360
+ const req = context.request;
361
+ const requestForRoute = req;
362
+ const checkpointRequested = this.getRequestedCheckpointVersion(requestForRoute) !== null;
363
+ // Library-wide behavior: applies to any streaming request with input schema validation enabled,
364
+ // regardless of whether checkpoint routing is in play.
365
+ const shouldDeferStreamingValidation = this.isLikelyStreamingBodyRequest(requestForRoute) &&
366
+ options.schema?.input !== undefined &&
367
+ options.validate !== false;
368
+ if (!options.rawRequest && req.method !== 'GET' && req.method !== 'HEAD' && !shouldDeferStreamingValidation) {
369
+ await this.parseRequestBodyForContext(context, requestForRoute, checkpointRequested);
370
+ }
371
+ if (shouldDeferStreamingValidation) {
372
+ const validationWithoutBody = await this.validateInputSchema(context, options, fallbackParams, {
373
+ includeBody: false,
374
+ allowBodyDeferral: true,
375
+ });
376
+ if (validationWithoutBody.response) {
377
+ return validationWithoutBody.response;
297
378
  }
298
- else if (contentType?.startsWith('multipart/form-data')) {
299
- parsedContent = await req.formData();
379
+ if (validationWithoutBody.requiresBody) {
380
+ if (!options.rawRequest && req.method !== 'GET' && req.method !== 'HEAD') {
381
+ await this.parseRequestBodyForContext(context, requestForRoute, checkpointRequested);
382
+ }
383
+ const fullValidation = await this.validateInputSchema(context, options, fallbackParams);
384
+ if (fullValidation.response) {
385
+ return fullValidation.response;
386
+ }
300
387
  }
301
- else {
302
- parsedContent = await req.text();
388
+ }
389
+ else {
390
+ const inputValidation = await this.validateInputSchema(context, options, fallbackParams);
391
+ if (inputValidation.response) {
392
+ return inputValidation.response;
303
393
  }
304
394
  }
305
- catch {
306
- parsedContent = null;
395
+ if (this.checkpointGateway) {
396
+ const checkpointResponse = await this.checkpointGateway.handle(req, this.buildCheckpointContextPayload(context));
397
+ if (checkpointResponse) {
398
+ return checkpointResponse;
399
+ }
307
400
  }
308
- this.setContentAndBodyAlias(req, parsedContent);
309
- }
310
- const inputValidationResponse = await this.validateInputSchema(req, options);
311
- if (inputValidationResponse) {
312
- return inputValidationResponse;
313
- }
401
+ return await handler(context);
402
+ };
314
403
  let result;
315
404
  const cacheOptions = options.cache;
316
405
  if (cacheOptions && typeof cacheOptions === 'number' && cacheOptions > 0) {
317
- const cacheKey = this.cacheManager.generateKey(req, {
318
- authUser: req.authUser,
319
- });
320
- result = await this.cacheManager.get(cacheKey, async () => {
321
- const res = await handler(req);
322
- if (res instanceof Response) {
323
- return {
324
- _isResponse: true,
325
- body: await res.text(),
326
- status: res.status,
327
- headers: Object.fromEntries(res.headers.entries()),
328
- };
329
- }
330
- return res;
331
- }, cacheOptions);
406
+ const cacheKey = this.applyCheckpointCacheNamespace(this.cacheManager.generateKey(context.request, {
407
+ authUser: context.authUser,
408
+ }), context.request);
409
+ result = await this.cacheManager.get(cacheKey, async () => await executeRoute(), cacheOptions);
332
410
  }
333
411
  else if (cacheOptions && typeof cacheOptions === 'object' && cacheOptions.ttl) {
334
- const cacheKey = cacheOptions.key ||
335
- this.cacheManager.generateKey(req, {
336
- authUser: req.authUser,
412
+ const hasRouteCacheKey = typeof cacheOptions.key === 'string' && cacheOptions.key.length > 0;
413
+ let cacheKey;
414
+ if (hasRouteCacheKey) {
415
+ cacheKey = this.applyCheckpointRouteKeyOverride(cacheOptions.key, context.request);
416
+ }
417
+ else {
418
+ const generatedKey = this.cacheManager.generateKey(context.request, {
419
+ authUser: context.authUser,
337
420
  });
338
- result = await this.cacheManager.get(cacheKey, async () => {
339
- const res = await handler(req);
340
- if (res instanceof Response) {
341
- return {
342
- _isResponse: true,
343
- body: await res.text(),
344
- status: res.status,
345
- headers: Object.fromEntries(res.headers.entries()),
346
- };
347
- }
348
- return res;
349
- }, cacheOptions.ttl);
421
+ cacheKey = this.applyCheckpointCacheNamespace(generatedKey, context.request);
422
+ }
423
+ result = await this.cacheManager.get(cacheKey, async () => await executeRoute(), cacheOptions.ttl);
350
424
  }
351
425
  else {
352
- result = await handler(req);
426
+ result = await executeRoute();
353
427
  }
354
- if (result && typeof result === 'object' && result._isResponse === true) {
355
- result = new Response(result.body, {
356
- status: result.status,
357
- headers: result.headers,
358
- });
428
+ if (result instanceof Response && !!cacheOptions) {
429
+ // Cache layers can return shared Response instances; clone before per-request mutations.
430
+ result = result.clone();
359
431
  }
360
432
  let response;
361
433
  if (options.rawResponse || result instanceof Response) {
@@ -364,21 +436,8 @@ export class VectorRouter {
364
436
  else {
365
437
  response = createResponse(200, result, options.responseContentType);
366
438
  }
367
- response = await this.middlewareManager.executeFinally(response, req);
368
- // Apply pre-built CORS headers if configured
369
- const entries = this.corsHeadersEntries;
370
- if (entries) {
371
- for (const [k, v] of entries) {
372
- response.headers.set(k, v);
373
- }
374
- }
375
- else {
376
- const dynamicCors = this.corsHandler;
377
- if (dynamicCors) {
378
- response = dynamicCors(response, req);
379
- }
380
- }
381
- return response;
439
+ response = await this.middlewareManager.executeFinally(response, context);
440
+ return this.applyCorsResponse(response, context.request);
382
441
  }
383
442
  catch (error) {
384
443
  if (error instanceof Response) {
@@ -396,10 +455,13 @@ export class VectorRouter {
396
455
  const nodeEnv = typeof Bun !== 'undefined' ? Bun.env.NODE_ENV : process.env.NODE_ENV;
397
456
  return nodeEnv !== 'production';
398
457
  }
399
- async buildInputValidationPayload(request, options) {
400
- let body = request.content;
401
- if (options.rawRequest && request.method !== 'GET' && request.method !== 'HEAD') {
458
+ async buildInputValidationPayload(context, options, fallbackParams, validationOptions) {
459
+ const request = context.request;
460
+ const includeBody = validationOptions?.includeBody !== false;
461
+ let body = includeBody && this.hasOwnContextField(context, 'content') ? context.content : undefined;
462
+ if (includeBody && options.rawRequest && request.method !== 'GET' && request.method !== 'HEAD') {
402
463
  try {
464
+ // Read raw body from a clone so handlers/checkpoint forwarding can still consume the original stream.
403
465
  body = await request.clone().text();
404
466
  }
405
467
  catch {
@@ -407,94 +469,154 @@ export class VectorRouter {
407
469
  }
408
470
  }
409
471
  return {
410
- params: request.params ?? {},
411
- query: request.query ?? {},
472
+ params: this.getRequestParams(request, fallbackParams),
473
+ query: this.getRequestQuery(request),
412
474
  headers: Object.fromEntries(request.headers.entries()),
413
- cookies: request.cookies ?? {},
475
+ cookies: this.getRequestCookies(request),
414
476
  body,
415
477
  };
416
478
  }
417
- applyValidatedInput(request, validatedValue) {
418
- request.validatedInput = validatedValue;
419
- if (!validatedValue || typeof validatedValue !== 'object') {
420
- return;
479
+ getRequestParams(request, fallbackParams) {
480
+ const nativeParams = this.readRequestObjectField(request, 'params');
481
+ if (nativeParams && Object.keys(nativeParams).length > 0) {
482
+ return nativeParams;
421
483
  }
422
- const validated = validatedValue;
423
- if ('params' in validated) {
424
- try {
425
- request.params = validated.params;
426
- }
427
- catch {
428
- // Request.params can be readonly on Bun-native requests.
429
- }
484
+ return fallbackParams ?? {};
485
+ }
486
+ getRequestQuery(request) {
487
+ const nativeQuery = this.readRequestObjectField(request, 'query');
488
+ if (nativeQuery) {
489
+ return nativeQuery;
430
490
  }
431
- if ('query' in validated) {
432
- try {
433
- request.query = validated.query;
434
- }
435
- catch {
436
- // Request.query can be readonly/non-configurable on some request objects.
437
- }
491
+ try {
492
+ return VectorRouter.parseQuery(new URL(request.url));
438
493
  }
439
- if ('cookies' in validated) {
440
- try {
441
- request.cookies = validated.cookies;
442
- }
443
- catch {
444
- // Request.cookies can be readonly/non-configurable on some request objects.
445
- }
494
+ catch {
495
+ return {};
446
496
  }
447
- if ('body' in validated) {
448
- this.setContentAndBodyAlias(request, validated.body);
497
+ }
498
+ getRequestCookies(request) {
499
+ const nativeCookies = this.readRequestObjectField(request, 'cookies');
500
+ if (nativeCookies) {
501
+ return nativeCookies;
449
502
  }
503
+ return VectorRouter.parseCookies(request.headers.get('cookie'));
450
504
  }
451
- setContentAndBodyAlias(request, value) {
452
- try {
453
- request.content = value;
505
+ readRequestObjectField(request, key) {
506
+ const value = request[key];
507
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
508
+ return undefined;
454
509
  }
455
- catch {
456
- // Request.content can be readonly/non-configurable on some request objects.
457
- return;
510
+ return value;
511
+ }
512
+ applyValidatedInput(context, validatedValue) {
513
+ this.setContextField(context, 'validatedInput', validatedValue);
514
+ }
515
+ issueHasBodyPath(issue) {
516
+ if (!issue || typeof issue !== 'object' || !('path' in issue)) {
517
+ return false;
458
518
  }
459
- this.setBodyAlias(request, value);
519
+ const path = issue.path;
520
+ if (!Array.isArray(path) || path.length === 0) {
521
+ return false;
522
+ }
523
+ const segment = path[0];
524
+ if (segment && typeof segment === 'object' && 'key' in segment) {
525
+ return segment.key === 'body';
526
+ }
527
+ return segment === 'body';
460
528
  }
461
- setBodyAlias(request, value) {
462
- try {
463
- request.body = value;
529
+ issueHasExplicitNonBodyPath(issue) {
530
+ if (!issue || typeof issue !== 'object' || !('path' in issue)) {
531
+ return false;
464
532
  }
465
- catch {
466
- // Keep request.content as source of truth when body alias is readonly.
533
+ const path = issue.path;
534
+ if (!Array.isArray(path) || path.length === 0) {
535
+ return false;
467
536
  }
537
+ const segment = path[0];
538
+ if (segment && typeof segment === 'object' && 'key' in segment) {
539
+ return segment.key !== 'body';
540
+ }
541
+ return segment !== 'body';
542
+ }
543
+ issueHasUnknownPath(issue) {
544
+ if (!issue || typeof issue !== 'object' || !('path' in issue)) {
545
+ return true;
546
+ }
547
+ const path = issue.path;
548
+ if (!Array.isArray(path)) {
549
+ return true;
550
+ }
551
+ return path.length === 0;
552
+ }
553
+ shouldDeferBodyValidation(issues, context, validationOptions) {
554
+ if (!(validationOptions?.allowBodyDeferral === true && validationOptions?.includeBody === false)) {
555
+ return false;
556
+ }
557
+ const request = context.request;
558
+ const mayHaveRequestBody = request.method !== 'GET' && request.method !== 'HEAD' && request.body !== null;
559
+ if (!mayHaveRequestBody || issues.length === 0) {
560
+ return false;
561
+ }
562
+ if (issues.some((issue) => this.issueHasBodyPath(issue))) {
563
+ return true;
564
+ }
565
+ // Conservative fallback: if issues do not identify a non-body target and at least one issue
566
+ // has unknown/empty path, retry once with body included.
567
+ const hasExplicitNonBodyPath = issues.some((issue) => this.issueHasExplicitNonBodyPath(issue));
568
+ const hasUnknownPath = issues.some((issue) => this.issueHasUnknownPath(issue));
569
+ return !hasExplicitNonBodyPath && hasUnknownPath;
468
570
  }
469
- async validateInputSchema(request, options) {
571
+ async validateInputSchema(context, options, fallbackParams, validationOptions) {
470
572
  const inputSchema = options.schema?.input;
471
573
  if (!inputSchema) {
472
- return null;
574
+ return { response: null, requiresBody: false };
473
575
  }
474
576
  if (options.validate === false) {
475
- return null;
577
+ return { response: null, requiresBody: false };
476
578
  }
477
579
  if (!isStandardRouteSchema(inputSchema)) {
478
- return APIError.internalServerError('Invalid route schema configuration', options.responseContentType);
580
+ return {
581
+ response: APIError.internalServerError('Invalid route schema configuration', options.responseContentType),
582
+ requiresBody: false,
583
+ };
479
584
  }
480
585
  const includeRawIssues = this.isDevelopmentMode();
481
- const payload = await this.buildInputValidationPayload(request, options);
586
+ const payload = await this.buildInputValidationPayload(context, options, fallbackParams, {
587
+ includeBody: validationOptions?.includeBody,
588
+ });
482
589
  try {
483
590
  const validation = await runStandardValidation(inputSchema, payload);
484
591
  if (validation.success === false) {
592
+ if (this.shouldDeferBodyValidation(validation.issues, context, validationOptions)) {
593
+ return { response: null, requiresBody: true };
594
+ }
485
595
  const issues = normalizeValidationIssues(validation.issues, includeRawIssues);
486
- return createResponse(422, createValidationErrorPayload('input', issues), options.responseContentType);
596
+ return {
597
+ response: createResponse(422, createValidationErrorPayload('input', issues), options.responseContentType),
598
+ requiresBody: false,
599
+ };
487
600
  }
488
- this.applyValidatedInput(request, validation.value);
489
- return null;
601
+ this.applyValidatedInput(context, validation.value);
602
+ return { response: null, requiresBody: false };
490
603
  }
491
604
  catch (error) {
492
605
  const thrownIssues = extractThrownIssues(error);
493
606
  if (thrownIssues) {
607
+ if (this.shouldDeferBodyValidation(thrownIssues, context, validationOptions)) {
608
+ return { response: null, requiresBody: true };
609
+ }
494
610
  const issues = normalizeValidationIssues(thrownIssues, includeRawIssues);
495
- return createResponse(422, createValidationErrorPayload('input', issues), options.responseContentType);
611
+ return {
612
+ response: createResponse(422, createValidationErrorPayload('input', issues), options.responseContentType),
613
+ requiresBody: false,
614
+ };
496
615
  }
497
- return APIError.internalServerError(error instanceof Error ? error.message : 'Validation failed', options.responseContentType);
616
+ return {
617
+ response: APIError.internalServerError(error instanceof Error ? error.message : 'Validation failed', options.responseContentType),
618
+ requiresBody: false,
619
+ };
498
620
  }
499
621
  }
500
622
  getOrCreateMethodMap(path) {
@@ -547,6 +669,19 @@ export class VectorRouter {
547
669
  }
548
670
  return query;
549
671
  }
672
+ static parseCookies(cookieHeader) {
673
+ const cookies = {};
674
+ if (!cookieHeader) {
675
+ return cookies;
676
+ }
677
+ for (const pair of cookieHeader.split(';')) {
678
+ const idx = pair.indexOf('=');
679
+ if (idx > 0) {
680
+ cookies[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
681
+ }
682
+ }
683
+ return cookies;
684
+ }
550
685
  routeSpecificityScore(path) {
551
686
  const STATIC_SEGMENT_WEIGHT = 1000;
552
687
  const PARAM_SEGMENT_WEIGHT = 10;
@@ -571,5 +706,19 @@ export class VectorRouter {
571
706
  }
572
707
  return score;
573
708
  }
709
+ applyCorsResponse(response, request) {
710
+ const entries = this.corsHeadersEntries;
711
+ if (entries) {
712
+ for (const [k, v] of entries) {
713
+ response.headers.set(k, v);
714
+ }
715
+ return response;
716
+ }
717
+ const dynamicCors = this.corsHandler;
718
+ if (dynamicCors) {
719
+ return dynamicCors(response, request);
720
+ }
721
+ return response;
722
+ }
574
723
  }
575
724
  //# sourceMappingURL=router.js.map