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.
- package/README.md +18 -6
- package/dist/auth/protected.d.ts +4 -4
- package/dist/auth/protected.d.ts.map +1 -1
- package/dist/auth/protected.js +10 -7
- package/dist/auth/protected.js.map +1 -1
- package/dist/cache/manager.d.ts +2 -0
- package/dist/cache/manager.d.ts.map +1 -1
- package/dist/cache/manager.js +21 -4
- package/dist/cache/manager.js.map +1 -1
- package/dist/checkpoint/artifacts/compressor.d.ts +5 -0
- package/dist/checkpoint/artifacts/compressor.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/compressor.js +24 -0
- package/dist/checkpoint/artifacts/compressor.js.map +1 -0
- package/dist/checkpoint/artifacts/decompress-worker.d.ts +2 -0
- package/dist/checkpoint/artifacts/decompress-worker.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/decompress-worker.js +31 -0
- package/dist/checkpoint/artifacts/decompress-worker.js.map +1 -0
- package/dist/checkpoint/artifacts/hasher.d.ts +2 -0
- package/dist/checkpoint/artifacts/hasher.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/hasher.js +7 -0
- package/dist/checkpoint/artifacts/hasher.js.map +1 -0
- package/dist/checkpoint/artifacts/manifest.d.ts +6 -0
- package/dist/checkpoint/artifacts/manifest.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/manifest.js +55 -0
- package/dist/checkpoint/artifacts/manifest.js.map +1 -0
- package/dist/checkpoint/artifacts/materializer.d.ts +16 -0
- package/dist/checkpoint/artifacts/materializer.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/materializer.js +168 -0
- package/dist/checkpoint/artifacts/materializer.js.map +1 -0
- package/dist/checkpoint/artifacts/packager.d.ts +12 -0
- package/dist/checkpoint/artifacts/packager.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/packager.js +82 -0
- package/dist/checkpoint/artifacts/packager.js.map +1 -0
- package/dist/checkpoint/artifacts/repository.d.ts +11 -0
- package/dist/checkpoint/artifacts/repository.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/repository.js +29 -0
- package/dist/checkpoint/artifacts/repository.js.map +1 -0
- package/dist/checkpoint/artifacts/store.d.ts +13 -0
- package/dist/checkpoint/artifacts/store.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/store.js +85 -0
- package/dist/checkpoint/artifacts/store.js.map +1 -0
- package/dist/checkpoint/artifacts/types.d.ts +21 -0
- package/dist/checkpoint/artifacts/types.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/types.js +2 -0
- package/dist/checkpoint/artifacts/types.js.map +1 -0
- package/dist/checkpoint/artifacts/worker-decompressor.d.ts +17 -0
- package/dist/checkpoint/artifacts/worker-decompressor.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/worker-decompressor.js +148 -0
- package/dist/checkpoint/artifacts/worker-decompressor.js.map +1 -0
- package/dist/checkpoint/asset-store.d.ts +10 -0
- package/dist/checkpoint/asset-store.d.ts.map +1 -0
- package/dist/checkpoint/asset-store.js +46 -0
- package/dist/checkpoint/asset-store.js.map +1 -0
- package/dist/checkpoint/bundler.d.ts +15 -0
- package/dist/checkpoint/bundler.d.ts.map +1 -0
- package/dist/checkpoint/bundler.js +45 -0
- package/dist/checkpoint/bundler.js.map +1 -0
- package/dist/checkpoint/cli.d.ts +2 -0
- package/dist/checkpoint/cli.d.ts.map +1 -0
- package/dist/checkpoint/cli.js +157 -0
- package/dist/checkpoint/cli.js.map +1 -0
- package/dist/checkpoint/entrypoint-generator.d.ts +17 -0
- package/dist/checkpoint/entrypoint-generator.d.ts.map +1 -0
- package/dist/checkpoint/entrypoint-generator.js +251 -0
- package/dist/checkpoint/entrypoint-generator.js.map +1 -0
- package/dist/checkpoint/forwarder.d.ts +6 -0
- package/dist/checkpoint/forwarder.d.ts.map +1 -0
- package/dist/checkpoint/forwarder.js +74 -0
- package/dist/checkpoint/forwarder.js.map +1 -0
- package/dist/checkpoint/gateway.d.ts +11 -0
- package/dist/checkpoint/gateway.d.ts.map +1 -0
- package/dist/checkpoint/gateway.js +30 -0
- package/dist/checkpoint/gateway.js.map +1 -0
- package/dist/checkpoint/ipc.d.ts +12 -0
- package/dist/checkpoint/ipc.d.ts.map +1 -0
- package/dist/checkpoint/ipc.js +96 -0
- package/dist/checkpoint/ipc.js.map +1 -0
- package/dist/checkpoint/manager.d.ts +20 -0
- package/dist/checkpoint/manager.d.ts.map +1 -0
- package/dist/checkpoint/manager.js +214 -0
- package/dist/checkpoint/manager.js.map +1 -0
- package/dist/checkpoint/process-manager.d.ts +35 -0
- package/dist/checkpoint/process-manager.d.ts.map +1 -0
- package/dist/checkpoint/process-manager.js +203 -0
- package/dist/checkpoint/process-manager.js.map +1 -0
- package/dist/checkpoint/resolver.d.ts +25 -0
- package/dist/checkpoint/resolver.d.ts.map +1 -0
- package/dist/checkpoint/resolver.js +95 -0
- package/dist/checkpoint/resolver.js.map +1 -0
- package/dist/checkpoint/socket-path.d.ts +2 -0
- package/dist/checkpoint/socket-path.d.ts.map +1 -0
- package/dist/checkpoint/socket-path.js +51 -0
- package/dist/checkpoint/socket-path.js.map +1 -0
- package/dist/checkpoint/types.d.ts +54 -0
- package/dist/checkpoint/types.d.ts.map +1 -0
- package/dist/checkpoint/types.js +2 -0
- package/dist/checkpoint/types.js.map +1 -0
- package/dist/cli/index.js +10 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/option-resolution.d.ts +1 -1
- package/dist/cli/option-resolution.d.ts.map +1 -1
- package/dist/cli/option-resolution.js.map +1 -1
- package/dist/cli.js +3709 -328
- package/dist/core/config-loader.d.ts +1 -0
- package/dist/core/config-loader.d.ts.map +1 -1
- package/dist/core/config-loader.js +10 -2
- package/dist/core/config-loader.js.map +1 -1
- package/dist/core/router.d.ts +24 -3
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +398 -249
- package/dist/core/router.js.map +1 -1
- package/dist/core/server.d.ts +2 -0
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +22 -8
- package/dist/core/server.js.map +1 -1
- package/dist/core/vector.d.ts +3 -0
- package/dist/core/vector.d.ts.map +1 -1
- package/dist/core/vector.js +51 -1
- package/dist/core/vector.js.map +1 -1
- package/dist/dev/route-scanner.d.ts.map +1 -1
- package/dist/dev/route-scanner.js +2 -1
- package/dist/dev/route-scanner.js.map +1 -1
- package/dist/http.d.ts +32 -7
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +144 -13
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +1297 -74
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1296 -73
- package/dist/middleware/manager.d.ts +3 -3
- package/dist/middleware/manager.d.ts.map +1 -1
- package/dist/middleware/manager.js +9 -8
- package/dist/middleware/manager.js.map +1 -1
- package/dist/openapi/docs-ui.d.ts.map +1 -1
- package/dist/openapi/docs-ui.js +1097 -61
- package/dist/openapi/docs-ui.js.map +1 -1
- package/dist/openapi/generator.d.ts +2 -1
- package/dist/openapi/generator.d.ts.map +1 -1
- package/dist/openapi/generator.js +240 -7
- package/dist/openapi/generator.js.map +1 -1
- package/dist/types/index.d.ts +71 -28
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +24 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +3 -2
- package/dist/utils/validation.js.map +1 -1
- package/package.json +2 -1
- package/src/auth/protected.ts +11 -8
- package/src/cache/manager.ts +23 -4
- package/src/checkpoint/artifacts/compressor.ts +30 -0
- package/src/checkpoint/artifacts/decompress-worker.ts +49 -0
- package/src/checkpoint/artifacts/hasher.ts +6 -0
- package/src/checkpoint/artifacts/manifest.ts +72 -0
- package/src/checkpoint/artifacts/materializer.ts +211 -0
- package/src/checkpoint/artifacts/packager.ts +100 -0
- package/src/checkpoint/artifacts/repository.ts +36 -0
- package/src/checkpoint/artifacts/store.ts +102 -0
- package/src/checkpoint/artifacts/types.ts +24 -0
- package/src/checkpoint/artifacts/worker-decompressor.ts +192 -0
- package/src/checkpoint/asset-store.ts +61 -0
- package/src/checkpoint/bundler.ts +64 -0
- package/src/checkpoint/cli.ts +177 -0
- package/src/checkpoint/entrypoint-generator.ts +275 -0
- package/src/checkpoint/forwarder.ts +84 -0
- package/src/checkpoint/gateway.ts +40 -0
- package/src/checkpoint/ipc.ts +107 -0
- package/src/checkpoint/manager.ts +254 -0
- package/src/checkpoint/process-manager.ts +250 -0
- package/src/checkpoint/resolver.ts +124 -0
- package/src/checkpoint/socket-path.ts +61 -0
- package/src/checkpoint/types.ts +63 -0
- package/src/cli/index.ts +11 -2
- package/src/cli/option-resolution.ts +5 -1
- package/src/core/config-loader.ts +11 -2
- package/src/core/router.ts +505 -264
- package/src/core/server.ts +36 -9
- package/src/core/vector.ts +60 -1
- package/src/dev/route-scanner.ts +2 -1
- package/src/http.ts +219 -19
- package/src/index.ts +3 -2
- package/src/middleware/manager.ts +10 -10
- package/src/openapi/docs-ui.ts +1097 -61
- package/src/openapi/generator.ts +265 -6
- package/src/types/index.ts +83 -30
- package/src/utils/validation.ts +5 -3
package/src/core/server.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
-
//
|
|
669
|
-
return this.
|
|
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
|
-
|
|
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
|
|
754
|
+
return this.server?.port ?? normalizePort(this.config.port);
|
|
728
755
|
}
|
|
729
756
|
|
|
730
757
|
getHostname(): string {
|
package/src/core/vector.ts
CHANGED
|
@@ -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
|
}
|
package/src/dev/route-scanner.ts
CHANGED
|
@@ -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: (
|
|
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: (
|
|
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(
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
308
|
+
statusText: options.statusText,
|
|
309
|
+
headers,
|
|
186
310
|
});
|
|
187
311
|
}
|
|
188
312
|
|
|
189
313
|
export const protectedRoute = async <TTypes extends VectorTypes = DefaultVectorTypes>(
|
|
190
|
-
|
|
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(
|
|
202
|
-
|
|
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?:
|
|
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: (
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
393
|
+
setContextField(ctx as unknown as Record<string, unknown>, 'content', await req.formData());
|
|
252
394
|
} else {
|
|
253
|
-
|
|
395
|
+
setContextField(ctx as unknown as Record<string, unknown>, 'content', await req.text());
|
|
254
396
|
}
|
|
255
397
|
} catch {
|
|
256
|
-
|
|
398
|
+
setContextField(ctx as unknown as Record<string, unknown>, 'content', null);
|
|
257
399
|
}
|
|
258
400
|
}
|
|
259
401
|
|
|
260
|
-
const result = await fn(
|
|
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
|
-
|
|
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(
|
|
22
|
-
if (this.beforeHandlers.length === 0) return
|
|
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(
|
|
25
|
+
const result = await handler(context);
|
|
28
26
|
|
|
29
27
|
if (result instanceof Response) {
|
|
30
28
|
return result;
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
|
|
31
|
+
if (result !== undefined) {
|
|
32
|
+
throw new TypeError('Before middleware must return void or Response');
|
|
33
|
+
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
return
|
|
36
|
+
return null;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
async executeFinally(response: 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,
|
|
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);
|