@vistagenic/vista 0.3.2 → 0.3.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 (98) hide show
  1. package/bin/vista.js +79 -109
  2. package/dist/ai/embeddings.d.ts +11 -11
  3. package/dist/ai/embeddings.js +71 -71
  4. package/dist/ai/index.d.ts +10 -10
  5. package/dist/ai/index.js +26 -26
  6. package/dist/ai/providers/base.js +85 -85
  7. package/dist/ai/react/react-server.d.ts +1 -1
  8. package/dist/ai/react/react-server.js +17 -17
  9. package/dist/ai/react/use-agent.js +138 -138
  10. package/dist/auth/core.d.ts +140 -140
  11. package/dist/auth/core.js +165 -165
  12. package/dist/auth/index.d.ts +20 -20
  13. package/dist/auth/index.js +369 -369
  14. package/dist/auth/react-server.d.ts +1 -1
  15. package/dist/auth/react-server.js +10 -10
  16. package/dist/auth/react.d.ts +18 -18
  17. package/dist/auth/react.js +71 -71
  18. package/dist/bin/build-rsc-flashpack.js +6 -0
  19. package/dist/bin/build-rsc.js +537 -492
  20. package/dist/bin/build.js +355 -353
  21. package/dist/bin/generate.js +513 -510
  22. package/dist/bin/webpack.config.js +4 -3
  23. package/dist/build/manifest.d.ts +169 -169
  24. package/dist/build/manifest.js +423 -423
  25. package/dist/build/rsc/client-manifest.d.ts +62 -62
  26. package/dist/build/rsc/client-manifest.js +295 -295
  27. package/dist/build/rsc/compiler.js +5 -4
  28. package/dist/build/rsc/native-scanner.d.ts +135 -135
  29. package/dist/build/rsc/native-scanner.js +203 -203
  30. package/dist/build/rsc/react-client-reference-manifest.d.ts +28 -28
  31. package/dist/build/rsc/react-client-reference-manifest.js +240 -240
  32. package/dist/build/standalone.js +260 -254
  33. package/dist/client/dynamic.react-server.d.ts +2 -2
  34. package/dist/client/dynamic.react-server.js +23 -23
  35. package/dist/client/link.react-server.d.ts +2 -2
  36. package/dist/client/link.react-server.js +11 -11
  37. package/dist/client/navigation.react-server.d.ts +1 -1
  38. package/dist/client/navigation.react-server.js +17 -17
  39. package/dist/client/router.react-server.d.ts +1 -1
  40. package/dist/client/router.react-server.js +17 -17
  41. package/dist/client/rsc-router.react-server.d.ts +1 -1
  42. package/dist/client/rsc-router.react-server.js +17 -17
  43. package/dist/client/script.react-server.d.ts +2 -2
  44. package/dist/client/script.react-server.js +23 -23
  45. package/dist/config.d.ts +1 -1
  46. package/dist/config.js +1 -7
  47. package/dist/deploy/adapters/cloudflare.js +43 -19
  48. package/dist/deploy/adapters/docker.d.ts +1 -0
  49. package/dist/deploy/adapters/docker.js +5 -4
  50. package/dist/deploy/adapters/netlify.js +62 -17
  51. package/dist/deploy/adapters/vercel.js +80 -50
  52. package/dist/deploy/preflight.js +4 -3
  53. package/dist/deploy/runtime-pack.d.ts +11 -0
  54. package/dist/deploy/runtime-pack.js +300 -0
  55. package/dist/deploy/utils.d.ts +1 -1
  56. package/dist/deploy/utils.js +31 -2
  57. package/dist/flashpack/command.js +1 -0
  58. package/dist/flashpack/runtime.js +2 -1
  59. package/dist/index.d.ts +23 -23
  60. package/dist/index.js +61 -61
  61. package/dist/server/app-dir.d.ts +22 -0
  62. package/dist/server/app-dir.js +77 -0
  63. package/dist/server/cookie-parse.d.ts +4 -4
  64. package/dist/server/cookie-parse.js +14 -14
  65. package/dist/server/engine.js +5 -5
  66. package/dist/server/image-optimizer.js +2 -1
  67. package/dist/server/index.d.ts +109 -109
  68. package/dist/server/index.js +315 -315
  69. package/dist/server/middleware-runner.d.ts +125 -125
  70. package/dist/server/middleware-runner.js +607 -615
  71. package/dist/server/middleware-security.d.ts +36 -36
  72. package/dist/server/middleware-security.js +183 -183
  73. package/dist/server/module-compile-hook.js +695 -695
  74. package/dist/server/root-resolver.js +6 -5
  75. package/dist/server/rsc-engine-flashpack.d.ts +3 -1
  76. package/dist/server/rsc-engine-flashpack.js +12 -1
  77. package/dist/server/rsc-engine.d.ts +6 -1
  78. package/dist/server/rsc-engine.js +203 -219
  79. package/dist/server/rsc-upstream.js +893 -892
  80. package/dist/server/static-generator.js +4 -3
  81. package/dist/server/structure-validator.js +2 -1
  82. package/dist/server/structure-watch.js +2 -0
  83. package/dist/server/typed-api-runtime.js +639 -635
  84. package/dist/server/vista-import-map.js +134 -134
  85. package/dist/stack/index.d.ts +34 -34
  86. package/dist/stack/index.js +49 -49
  87. package/dist/stack/server/caller.d.ts +13 -13
  88. package/dist/stack/server/caller.js +42 -42
  89. package/dist/stack/server/executor.d.ts +40 -40
  90. package/dist/stack/server/executor.js +222 -222
  91. package/dist/stack/server/index.d.ts +11 -11
  92. package/dist/stack/server/index.js +24 -24
  93. package/dist/stack/server/procedure.d.ts +20 -20
  94. package/dist/stack/server/procedure.js +66 -66
  95. package/dist/stack/server/types.d.ts +113 -113
  96. package/dist/theme/react-server.d.ts +10 -10
  97. package/dist/theme/react-server.js +16 -16
  98. package/package.json +1 -1
@@ -1,615 +1,607 @@
1
- "use strict";
2
- /**
3
- * Vista Middleware Runner
4
- *
5
- * Built-in middleware system for Vista.js.
6
- * Supports:
7
- * 1. Global middleware (top-level middleware.ts / middleware.js at cwd or src/)
8
- * 2. Route-specific middleware (co-located in app/ directory segments, e.g. app/api/auth/middleware.ts)
9
- * 3. Exported middleware from route files (e.g. export const middleware = ... in page.tsx or route.ts)
10
- * 4. Middleware signature: middleware({ request, next }) matching Issue #7 Part 5
11
- * 5. Middleware chaining and clearly defined execution order:
12
- * Global -> Segment middlewares (shallowest to deepest) -> Route-level middleware -> Handler
13
- * 6. Request modification (headers, URL rewrites) and Response modification / short-circuiting
14
- */
15
- var __importDefault = (this && this.__importDefault) || function (mod) {
16
- return (mod && mod.__esModule) ? mod : { "default": mod };
17
- };
18
- Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.clearMiddlewareCaches = clearMiddlewareCaches;
20
- exports.discoverGlobalMiddleware = discoverGlobalMiddleware;
21
- exports.discoverRouteMiddlewares = discoverRouteMiddlewares;
22
- exports.patternToRegExp = patternToRegExp;
23
- exports.shouldRunMiddleware = shouldRunMiddleware;
24
- exports.buildNextRequest = buildNextRequest;
25
- exports.runMiddleware = runMiddleware;
26
- exports.applyMiddlewareResult = applyMiddlewareResult;
27
- const path_1 = __importDefault(require("path"));
28
- const fs_1 = __importDefault(require("fs"));
29
- const cookie_parse_1 = require("./cookie-parse");
30
- const middleware_security_1 = require("./middleware-security");
31
- // ---------------------------------------------------------------------------
32
- // Discovery Caches
33
- // ---------------------------------------------------------------------------
34
- const globalDiscoveryCache = new Map();
35
- const routeDiscoveryCache = new Map();
36
- const MIDDLEWARE_FILE_EXTS = ['.ts', '.tsx', '.js', '.jsx'];
37
- const ROUTE_FILE_STEMS = ['page', 'route', 'index', 'layout'];
38
- function clearMiddlewareCaches() {
39
- globalDiscoveryCache.clear();
40
- routeDiscoveryCache.clear();
41
- }
42
- // ---------------------------------------------------------------------------
43
- // Helpers
44
- // ---------------------------------------------------------------------------
45
- function getAppDir(cwd) {
46
- const candidates = [path_1.default.join(cwd, 'app'), path_1.default.join(cwd, 'src', 'app')];
47
- for (const candidate of candidates) {
48
- if (fs_1.default.existsSync(candidate) && fs_1.default.statSync(candidate).isDirectory()) {
49
- return candidate;
50
- }
51
- }
52
- return null;
53
- }
54
- /**
55
- * Discover top-level global middleware.
56
- * Checks `<cwd>/middleware.*` then `<cwd>/src/middleware.*`.
57
- */
58
- function discoverGlobalMiddleware(cwd, bustCache) {
59
- if (!bustCache && globalDiscoveryCache.has(cwd)) {
60
- return globalDiscoveryCache.get(cwd);
61
- }
62
- const searchDirs = [cwd, path_1.default.join(cwd, 'src')];
63
- for (const dir of searchDirs) {
64
- for (const ext of MIDDLEWARE_FILE_EXTS) {
65
- const candidate = path_1.default.join(dir, `middleware${ext}`);
66
- if (fs_1.default.existsSync(candidate) && fs_1.default.statSync(candidate).isFile()) {
67
- globalDiscoveryCache.set(cwd, candidate);
68
- return candidate;
69
- }
70
- }
71
- }
72
- globalDiscoveryCache.set(cwd, null);
73
- return null;
74
- }
75
- /**
76
- * Discover route-specific middleware files along the path hierarchy.
77
- * Returns file paths from shallowest segment to deepest segment.
78
- */
79
- function discoverRouteMiddlewares(cwd, pathname, bustCache) {
80
- const cacheKey = `${cwd}:${pathname}`;
81
- if (!bustCache && routeDiscoveryCache.has(cacheKey)) {
82
- return routeDiscoveryCache.get(cacheKey);
83
- }
84
- const appDir = getAppDir(cwd);
85
- if (!appDir) {
86
- routeDiscoveryCache.set(cacheKey, []);
87
- return [];
88
- }
89
- const globalMiddleware = discoverGlobalMiddleware(cwd, bustCache);
90
- const results = [];
91
- // 1. Check app/middleware.* (if different from global middleware)
92
- for (const ext of MIDDLEWARE_FILE_EXTS) {
93
- const appLevel = path_1.default.join(appDir, `middleware${ext}`);
94
- if (fs_1.default.existsSync(appLevel) && fs_1.default.statSync(appLevel).isFile() && appLevel !== globalMiddleware) {
95
- results.push(appLevel);
96
- break;
97
- }
98
- }
99
- // 2. Traverse route segments from root to leaf
100
- const segments = pathname.split('/').filter(Boolean);
101
- let currentDir = appDir;
102
- for (let i = 0; i < segments.length; i++) {
103
- const seg = segments[i];
104
- let nextDir = null;
105
- // Direct segment match (e.g. app/api)
106
- const exactDir = path_1.default.join(currentDir, seg);
107
- if (fs_1.default.existsSync(exactDir) && fs_1.default.statSync(exactDir).isDirectory()) {
108
- nextDir = exactDir;
109
- }
110
- else {
111
- // Dynamic route segment match (e.g. [id], [...slug])
112
- try {
113
- const entries = fs_1.default.readdirSync(currentDir, { withFileTypes: true });
114
- const dynamicEntry = entries.find((e) => e.isDirectory() &&
115
- e.name.startsWith('[') &&
116
- e.name.endsWith(']') &&
117
- e.name !== '[not-found]');
118
- if (dynamicEntry) {
119
- nextDir = path_1.default.join(currentDir, dynamicEntry.name);
120
- }
121
- }
122
- catch {
123
- // Ignore read errors
124
- }
125
- }
126
- if (!nextDir) {
127
- break;
128
- }
129
- currentDir = nextDir;
130
- // Check for co-located middleware in this segment directory
131
- for (const ext of MIDDLEWARE_FILE_EXTS) {
132
- const segmentMiddleware = path_1.default.join(currentDir, `middleware${ext}`);
133
- if (fs_1.default.existsSync(segmentMiddleware) &&
134
- fs_1.default.statSync(segmentMiddleware).isFile() &&
135
- segmentMiddleware !== globalMiddleware &&
136
- !results.includes(segmentMiddleware)) {
137
- results.push(segmentMiddleware);
138
- break;
139
- }
140
- }
141
- }
142
- // 3. At leaf directory, check for exported middleware in route files (page.tsx, route.ts, etc.)
143
- for (const stem of ROUTE_FILE_STEMS) {
144
- for (const ext of MIDDLEWARE_FILE_EXTS) {
145
- const candidateRouteFile = path_1.default.join(currentDir, `${stem}${ext}`);
146
- if (fs_1.default.existsSync(candidateRouteFile) &&
147
- fs_1.default.statSync(candidateRouteFile).isFile() &&
148
- !results.includes(candidateRouteFile)) {
149
- // Record candidate route file to inspect for exported middleware
150
- results.push(candidateRouteFile);
151
- }
152
- }
153
- }
154
- routeDiscoveryCache.set(cacheKey, results);
155
- return results;
156
- }
157
- // ---------------------------------------------------------------------------
158
- // Matcher Support
159
- // ---------------------------------------------------------------------------
160
- function patternToRegExp(pattern) {
161
- // Convert Next.js-style / path pattern to RegExp:
162
- // /foo/:path* → /foo(?:/(.*))?
163
- // /foo/:bar → /foo/[^/]+
164
- // /foo/* → /foo(?:/(.*))?
165
- let re = pattern
166
- .replace(/\/:[^/]+\*/g, '(?:/(.*))?') // /:path* (0 or more sub-paths)
167
- .replace(/:[^/]+\*/g, '(.*)') // bare :path*
168
- .replace(/:[^/]+/g, '[^/]+') // :param (single segment)
169
- .replace(/\/\*/g, '(?:/(.*))?') // /*
170
- .replace(/\*/g, '(.*)'); // bare *
171
- return new RegExp(`^${re}/?$`);
172
- }
173
- function shouldRunMiddleware(middlewareModule, pathname, request) {
174
- const config = middlewareModule.config;
175
- if (!config?.matcher)
176
- return true;
177
- const matchers = Array.isArray(config.matcher)
178
- ? config.matcher
179
- : [config.matcher];
180
- return matchers.some((matcher) => {
181
- try {
182
- const source = typeof matcher === 'string' ? matcher : matcher.source;
183
- const re = patternToRegExp(source);
184
- if (!re.test(pathname)) {
185
- return false;
186
- }
187
- if (typeof matcher === 'string' || !request) {
188
- return true;
189
- }
190
- if (matcher.has && !matcher.has.every((rule) => matchRequestRule(request, rule))) {
191
- return false;
192
- }
193
- if (matcher.missing && !matcher.missing.every((rule) => !matchRequestRule(request, rule))) {
194
- return false;
195
- }
196
- return true;
197
- }
198
- catch {
199
- // Fail closed: a broken matcher must not skip auth middleware.
200
- return true;
201
- }
202
- });
203
- }
204
- function matchRequestRule(request, rule) {
205
- let actual;
206
- if (rule.type === 'header') {
207
- actual = request.headers.get(rule.key) || undefined;
208
- }
209
- else if (rule.type === 'cookie') {
210
- actual = request.cookies.get(rule.key)?.value;
211
- }
212
- else {
213
- actual = request.nextUrl.searchParams.get(rule.key) || undefined;
214
- }
215
- if (rule.value === undefined) {
216
- return Boolean(actual);
217
- }
218
- return actual === rule.value;
219
- }
220
- // ---------------------------------------------------------------------------
221
- // Build NextRequest-like Object
222
- // ---------------------------------------------------------------------------
223
- function buildNextRequest(req) {
224
- const protocol = req.protocol || 'http';
225
- const host = (req.get && req.get('host')) || req.headers?.host || 'localhost';
226
- const rawPath = req.path || (req.url ? req.url.split('?')[0] : '/');
227
- const fullUrl = `${protocol}://${host}${req.originalUrl || req.url || rawPath}`;
228
- const headers = new Headers();
229
- if (req.headers) {
230
- for (const [key, val] of Object.entries(req.headers)) {
231
- if (val !== undefined) {
232
- if (Array.isArray(val)) {
233
- for (const item of val) {
234
- headers.append(key, item);
235
- }
236
- }
237
- else {
238
- headers.set(key, String(val));
239
- }
240
- }
241
- }
242
- }
243
- const queryParams = new URLSearchParams(typeof req.query === 'object' && req.query !== null
244
- ? req.query
245
- : {});
246
- // Cookie helpers
247
- const rawCookies = req.cookies || {};
248
- if (!Object.keys(rawCookies).length && req.headers?.cookie) {
249
- for (const pair of String(req.headers.cookie).split(';')) {
250
- const [k, ...v] = pair.split('=');
251
- const name = k?.trim();
252
- if (name) {
253
- rawCookies[name] = (0, cookie_parse_1.safeDecodeURIComponent)(v.join('=').trim());
254
- }
255
- }
256
- }
257
- return {
258
- url: fullUrl,
259
- method: req.method || 'GET',
260
- path: rawPath,
261
- headers,
262
- nextUrl: {
263
- pathname: rawPath,
264
- searchParams: queryParams,
265
- href: fullUrl,
266
- origin: `${protocol}://${host}`,
267
- },
268
- cookies: {
269
- get: (name) => rawCookies[name] !== undefined ? { name, value: String(rawCookies[name]) } : undefined,
270
- getAll: () => Object.entries(rawCookies).map(([n, v]) => ({
271
- name: n,
272
- value: v,
273
- })),
274
- has: (name) => rawCookies[name] !== undefined,
275
- },
276
- };
277
- }
278
- function cloneRequestWithHeaders(req, newHeaders) {
279
- const mergedHeaders = new Headers(req.headers);
280
- if (newHeaders instanceof Headers) {
281
- newHeaders.forEach((val, key) => mergedHeaders.set(key, val));
282
- }
283
- else {
284
- for (const [k, v] of Object.entries(newHeaders)) {
285
- mergedHeaders.set(k, String(v));
286
- }
287
- }
288
- return {
289
- ...req,
290
- headers: mergedHeaders,
291
- };
292
- }
293
- // ---------------------------------------------------------------------------
294
- // Execution Pipeline
295
- // ---------------------------------------------------------------------------
296
- function loadModule(filePath, isDev) {
297
- try {
298
- if (isDev) {
299
- try {
300
- delete require.cache[require.resolve(filePath)];
301
- }
302
- catch { }
303
- }
304
- return require(filePath);
305
- }
306
- catch (err) {
307
- console.warn(`[vista:middleware] Failed to load middleware module ${filePath}:`, err?.message);
308
- return null;
309
- }
310
- }
311
- /**
312
- * Run user-defined middleware chain (global + route-specific) for the given request.
313
- *
314
- * Execution Order:
315
- * 1. Global middleware (cwd/middleware.ts or cwd/src/middleware.ts)
316
- * 2. Route segment middlewares (from shallowest to deepest in app/)
317
- * 3. Route file middleware (exported `middleware` from page.tsx/route.ts)
318
- * 4. Downstream handler (Page component or API route handler)
319
- *
320
- * Returns a `MiddlewareResult` indicating whether the request was redirected,
321
- * short-circuited with a response, rewritten, or continued to the next handler.
322
- */
323
- async function runMiddleware(req, cwd, isDev = false) {
324
- const pathname = req.path || (req.url ? req.url.split('?')[0] : '/');
325
- // 1. Discover Global Middleware
326
- const globalFile = discoverGlobalMiddleware(cwd, isDev);
327
- const entries = [];
328
- if (globalFile) {
329
- entries.push({
330
- source: 'global',
331
- filePath: globalFile,
332
- depth: 0,
333
- });
334
- }
335
- // 2. Discover Route-Specific Middlewares
336
- const routeFiles = discoverRouteMiddlewares(cwd, pathname, isDev);
337
- for (let i = 0; i < routeFiles.length; i++) {
338
- const file = routeFiles[i];
339
- const isMiddlewareFile = path_1.default.basename(file).startsWith('middleware.');
340
- entries.push({
341
- source: isMiddlewareFile ? 'segment' : 'route',
342
- filePath: file,
343
- depth: i + 1,
344
- });
345
- }
346
- // If no middleware discovered, skip directly
347
- if (entries.length === 0) {
348
- return { kind: 'skip' };
349
- }
350
- const activeChain = [];
351
- for (const entry of entries) {
352
- const mod = loadModule(entry.filePath, isDev);
353
- if (!mod) {
354
- // Dedicated middleware files must fail closed. Skipping them would
355
- // silently bypass auth. Route files may fail for unrelated reasons.
356
- if (entry.source !== 'route') {
357
- return {
358
- kind: 'short-circuit',
359
- status: 500,
360
- body: 'Middleware Error',
361
- };
362
- }
363
- continue;
364
- }
365
- // In a route file (e.g. page.tsx, route.ts), we ONLY accept an explicit `export const middleware`
366
- // or `export function middleware` (not `export default`, which is the page component)
367
- let fn;
368
- if (entry.source === 'route') {
369
- if (typeof mod.middleware === 'function') {
370
- fn = mod.middleware;
371
- }
372
- }
373
- else {
374
- // In a dedicated middleware.* file, accept either `export default` or `export function middleware`
375
- if (typeof mod.middleware === 'function') {
376
- fn = mod.middleware;
377
- }
378
- else if (typeof mod.default === 'function') {
379
- fn = mod.default;
380
- }
381
- }
382
- if (!fn) {
383
- if (entry.source !== 'route') {
384
- return {
385
- kind: 'short-circuit',
386
- status: 500,
387
- body: 'Middleware Error',
388
- };
389
- }
390
- continue;
391
- }
392
- // Check optional matcher config
393
- if (!shouldRunMiddleware(mod, pathname, buildNextRequest(req))) {
394
- continue;
395
- }
396
- activeChain.push({
397
- filePath: entry.filePath,
398
- source: entry.source,
399
- fn,
400
- allowedRedirectHosts: mod.config?.allowedRedirectHosts,
401
- });
402
- }
403
- if (activeChain.length === 0) {
404
- return { kind: 'skip' };
405
- }
406
- // 4. Execute the chain
407
- let currentRequest = buildNextRequest(req);
408
- const aggregatedResponseHeaders = new Map();
409
- const modifiedRequestHeaders = new Map();
410
- async function dispatch(index, requestObj) {
411
- if (index >= activeChain.length) {
412
- // Terminal node: reached end of middleware chain
413
- return new Response(null, {
414
- status: 200,
415
- headers: {
416
- 'x-middleware-next': '1',
417
- },
418
- });
419
- }
420
- const currentItem = activeChain[index];
421
- let nextCalled = false;
422
- let nextRequestObj = requestObj;
423
- const nextFn = async (options) => {
424
- nextCalled = true;
425
- if (options?.request?.headers) {
426
- const sanitized = (0, middleware_security_1.sanitizeRequestHeaderMap)(options.request.headers);
427
- const headerRecord = {};
428
- sanitized.forEach((value, key) => {
429
- headerRecord[key] = value;
430
- modifiedRequestHeaders.set(key, value);
431
- });
432
- nextRequestObj = cloneRequestWithHeaders(requestObj, headerRecord);
433
- }
434
- return dispatch(index + 1, nextRequestObj);
435
- };
436
- const context = {
437
- request: requestObj,
438
- next: nextFn,
439
- url: requestObj.url,
440
- method: requestObj.method,
441
- headers: requestObj.headers,
442
- nextUrl: requestObj.nextUrl,
443
- cookies: requestObj.cookies,
444
- };
445
- try {
446
- // Invoke middleware. Supports:
447
- // - middleware({ request, next }) [Issue #7 Part 5 signature]
448
- // - middleware(request, next)
449
- // - middleware(request)
450
- const output = await currentItem.fn(context, nextFn);
451
- // Handle direct Response return
452
- if (output instanceof Response) {
453
- return output;
454
- }
455
- // If next() was explicitly called and returned nothing, propagate standard continuation
456
- if (nextCalled) {
457
- return new Response(null, {
458
- status: 200,
459
- headers: {
460
- 'x-middleware-next': '1',
461
- },
462
- });
463
- }
464
- // If middleware returned void without calling next(), continue to next in chain
465
- return dispatch(index + 1, requestObj);
466
- }
467
- catch (err) {
468
- console.error(`[vista:middleware] Error in middleware at ${currentItem.filePath}:`, err?.message ?? String(err));
469
- if (process.env.NODE_ENV === 'production') {
470
- return new Response('Middleware Error', { status: 500 });
471
- }
472
- return dispatch(index + 1, requestObj);
473
- }
474
- }
475
- const finalResponse = await dispatch(0, currentRequest);
476
- // 5. Convert final Response to MiddlewareResult
477
- // Collect all response headers
478
- if (finalResponse.headers && typeof finalResponse.headers.forEach === 'function') {
479
- finalResponse.headers.forEach((val, key) => {
480
- const lower = key.toLowerCase();
481
- if (lower.startsWith('x-middleware-request-')) {
482
- const name = lower.slice('x-middleware-request-'.length);
483
- if (!name || name === 'host' || name === 'cookie' || name.startsWith('x-forwarded-') || name.startsWith('x-middleware-')) {
484
- return;
485
- }
486
- modifiedRequestHeaders.set(name, val);
487
- return;
488
- }
489
- aggregatedResponseHeaders.set(key, val);
490
- });
491
- }
492
- if (process.env.NODE_ENV === 'production') {
493
- (0, middleware_security_1.securityHeaders)().forEach((value, key) => {
494
- if (!aggregatedResponseHeaders.has(key) && !aggregatedResponseHeaders.has(key.toLowerCase())) {
495
- aggregatedResponseHeaders.set(key, value);
496
- }
497
- });
498
- }
499
- const allowedRedirectHosts = activeChain.flatMap((item) => item.allowedRedirectHosts || []);
500
- const requestUrl = currentRequest.url;
501
- // 5a. Redirect
502
- const location = finalResponse.headers?.get?.('Location') || finalResponse.headers?.get?.('location');
503
- if (location) {
504
- if (!(0, middleware_security_1.isSafeRedirectLocation)(location, requestUrl, allowedRedirectHosts)) {
505
- return {
506
- kind: 'short-circuit',
507
- status: 400,
508
- responseHeaders: aggregatedResponseHeaders,
509
- requestHeaders: modifiedRequestHeaders,
510
- body: 'Invalid redirect',
511
- };
512
- }
513
- return {
514
- kind: 'redirect',
515
- status: finalResponse.status || 307,
516
- location,
517
- responseHeaders: aggregatedResponseHeaders,
518
- requestHeaders: modifiedRequestHeaders,
519
- };
520
- }
521
- // 5b. Rewrite
522
- const rewrite = finalResponse.headers?.get?.('x-middleware-rewrite');
523
- if (rewrite) {
524
- if (!(0, middleware_security_1.isSafeRewriteLocation)(rewrite)) {
525
- return {
526
- kind: 'short-circuit',
527
- status: 400,
528
- responseHeaders: aggregatedResponseHeaders,
529
- requestHeaders: modifiedRequestHeaders,
530
- body: 'Invalid rewrite',
531
- };
532
- }
533
- return {
534
- kind: 'rewrite',
535
- location: rewrite,
536
- responseHeaders: aggregatedResponseHeaders,
537
- requestHeaders: modifiedRequestHeaders,
538
- };
539
- }
540
- // 5c. Continue to page/route handler
541
- const shouldContinue = finalResponse.headers?.get?.('x-middleware-next');
542
- if (shouldContinue) {
543
- return {
544
- kind: 'next',
545
- responseHeaders: aggregatedResponseHeaders,
546
- requestHeaders: modifiedRequestHeaders,
547
- };
548
- }
549
- // 5d. Short-circuit with response body and custom status
550
- let bodyContent = null;
551
- try {
552
- bodyContent = await finalResponse.text();
553
- }
554
- catch { }
555
- return {
556
- kind: 'short-circuit',
557
- status: finalResponse.status || 200,
558
- responseHeaders: aggregatedResponseHeaders,
559
- requestHeaders: modifiedRequestHeaders,
560
- body: bodyContent,
561
- };
562
- }
563
- // ---------------------------------------------------------------------------
564
- // Apply Middleware Result
565
- // ---------------------------------------------------------------------------
566
- /**
567
- * Apply a MiddlewareResult to the Express request/response.
568
- * Returns `true` if the response was finalized (caller should `return`),
569
- * `false` if the request should continue to the next handler.
570
- */
571
- function applyMiddlewareResult(result, req, res) {
572
- // 1. Forward any modified request headers to req.headers
573
- if (result.requestHeaders && req.headers) {
574
- const sanitized = (0, middleware_security_1.sanitizeRequestHeaderMap)(result.requestHeaders);
575
- sanitized.forEach((value, key) => {
576
- req.headers[key] = value;
577
- });
578
- }
579
- // 2. Forward any response headers the middleware set
580
- if (result.responseHeaders) {
581
- result.responseHeaders.forEach((value, key) => {
582
- const lower = key.toLowerCase();
583
- // Skip internal transport headers
584
- if (lower === 'x-middleware-next' ||
585
- lower === 'x-middleware-rewrite' ||
586
- lower === 'location' ||
587
- lower.startsWith('x-middleware-request-')) {
588
- return;
589
- }
590
- res.setHeader(key, value);
591
- });
592
- }
593
- switch (result.kind) {
594
- case 'redirect':
595
- res.redirect(result.status || 307, result.location);
596
- return true;
597
- case 'rewrite':
598
- req.url = result.location;
599
- return false; // continue with rewritten URL
600
- case 'short-circuit': {
601
- const status = result.status || 403;
602
- if (result.body !== undefined && result.body !== null && result.body !== '') {
603
- res.status(status).send(result.body);
604
- }
605
- else {
606
- res.status(status).end();
607
- }
608
- return true;
609
- }
610
- case 'next':
611
- case 'skip':
612
- default:
613
- return false; // continue
614
- }
615
- }
1
+ "use strict";
2
+ /**
3
+ * Vista Middleware Runner
4
+ *
5
+ * Built-in middleware system for Vista.js.
6
+ * Supports:
7
+ * 1. Global middleware (top-level middleware.ts / middleware.js at cwd or src/)
8
+ * 2. Route-specific middleware (co-located in app/ directory segments, e.g. app/api/auth/middleware.ts)
9
+ * 3. Exported middleware from route files (e.g. export const middleware = ... in page.tsx or route.ts)
10
+ * 4. Middleware signature: middleware({ request, next }) matching Issue #7 Part 5
11
+ * 5. Middleware chaining and clearly defined execution order:
12
+ * Global -> Segment middlewares (shallowest to deepest) -> Route-level middleware -> Handler
13
+ * 6. Request modification (headers, URL rewrites) and Response modification / short-circuiting
14
+ */
15
+ var __importDefault = (this && this.__importDefault) || function (mod) {
16
+ return (mod && mod.__esModule) ? mod : { "default": mod };
17
+ };
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.clearMiddlewareCaches = clearMiddlewareCaches;
20
+ exports.discoverGlobalMiddleware = discoverGlobalMiddleware;
21
+ exports.discoverRouteMiddlewares = discoverRouteMiddlewares;
22
+ exports.patternToRegExp = patternToRegExp;
23
+ exports.shouldRunMiddleware = shouldRunMiddleware;
24
+ exports.buildNextRequest = buildNextRequest;
25
+ exports.runMiddleware = runMiddleware;
26
+ exports.applyMiddlewareResult = applyMiddlewareResult;
27
+ const path_1 = __importDefault(require("path"));
28
+ const fs_1 = __importDefault(require("fs"));
29
+ const cookie_parse_1 = require("./cookie-parse");
30
+ const middleware_security_1 = require("./middleware-security");
31
+ const app_dir_1 = require("./app-dir");
32
+ // ---------------------------------------------------------------------------
33
+ // Discovery Caches
34
+ // ---------------------------------------------------------------------------
35
+ const globalDiscoveryCache = new Map();
36
+ const routeDiscoveryCache = new Map();
37
+ const MIDDLEWARE_FILE_EXTS = ['.ts', '.tsx', '.js', '.jsx'];
38
+ const ROUTE_FILE_STEMS = ['page', 'route', 'index', 'layout'];
39
+ function clearMiddlewareCaches() {
40
+ globalDiscoveryCache.clear();
41
+ routeDiscoveryCache.clear();
42
+ }
43
+ // ---------------------------------------------------------------------------
44
+ // Helpers
45
+ // ---------------------------------------------------------------------------
46
+ /**
47
+ * Discover top-level global middleware.
48
+ * Checks `<cwd>/middleware.*` then `<cwd>/src/middleware.*`.
49
+ */
50
+ function discoverGlobalMiddleware(cwd, bustCache) {
51
+ if (!bustCache && globalDiscoveryCache.has(cwd)) {
52
+ return globalDiscoveryCache.get(cwd);
53
+ }
54
+ const searchDirs = [cwd, path_1.default.join(cwd, 'src')];
55
+ for (const dir of searchDirs) {
56
+ for (const ext of MIDDLEWARE_FILE_EXTS) {
57
+ const candidate = path_1.default.join(dir, `middleware${ext}`);
58
+ if (fs_1.default.existsSync(candidate) && fs_1.default.statSync(candidate).isFile()) {
59
+ globalDiscoveryCache.set(cwd, candidate);
60
+ return candidate;
61
+ }
62
+ }
63
+ }
64
+ globalDiscoveryCache.set(cwd, null);
65
+ return null;
66
+ }
67
+ /**
68
+ * Discover route-specific middleware files along the path hierarchy.
69
+ * Returns file paths from shallowest segment to deepest segment.
70
+ */
71
+ function discoverRouteMiddlewares(cwd, pathname, bustCache) {
72
+ const cacheKey = `${cwd}:${pathname}`;
73
+ if (!bustCache && routeDiscoveryCache.has(cacheKey)) {
74
+ return routeDiscoveryCache.get(cacheKey);
75
+ }
76
+ const appDir = (0, app_dir_1.resolveAppDirOrNull)(cwd);
77
+ if (!appDir) {
78
+ routeDiscoveryCache.set(cacheKey, []);
79
+ return [];
80
+ }
81
+ const globalMiddleware = discoverGlobalMiddleware(cwd, bustCache);
82
+ const results = [];
83
+ // 1. Check app/middleware.* (if different from global middleware)
84
+ for (const ext of MIDDLEWARE_FILE_EXTS) {
85
+ const appLevel = path_1.default.join(appDir, `middleware${ext}`);
86
+ if (fs_1.default.existsSync(appLevel) && fs_1.default.statSync(appLevel).isFile() && appLevel !== globalMiddleware) {
87
+ results.push(appLevel);
88
+ break;
89
+ }
90
+ }
91
+ // 2. Traverse route segments from root to leaf
92
+ const segments = pathname.split('/').filter(Boolean);
93
+ let currentDir = appDir;
94
+ for (let i = 0; i < segments.length; i++) {
95
+ const seg = segments[i];
96
+ let nextDir = null;
97
+ // Direct segment match (e.g. app/api)
98
+ const exactDir = path_1.default.join(currentDir, seg);
99
+ if (fs_1.default.existsSync(exactDir) && fs_1.default.statSync(exactDir).isDirectory()) {
100
+ nextDir = exactDir;
101
+ }
102
+ else {
103
+ // Dynamic route segment match (e.g. [id], [...slug])
104
+ try {
105
+ const entries = fs_1.default.readdirSync(currentDir, { withFileTypes: true });
106
+ const dynamicEntry = entries.find((e) => e.isDirectory() &&
107
+ e.name.startsWith('[') &&
108
+ e.name.endsWith(']') &&
109
+ e.name !== '[not-found]');
110
+ if (dynamicEntry) {
111
+ nextDir = path_1.default.join(currentDir, dynamicEntry.name);
112
+ }
113
+ }
114
+ catch {
115
+ // Ignore read errors
116
+ }
117
+ }
118
+ if (!nextDir) {
119
+ break;
120
+ }
121
+ currentDir = nextDir;
122
+ // Check for co-located middleware in this segment directory
123
+ for (const ext of MIDDLEWARE_FILE_EXTS) {
124
+ const segmentMiddleware = path_1.default.join(currentDir, `middleware${ext}`);
125
+ if (fs_1.default.existsSync(segmentMiddleware) &&
126
+ fs_1.default.statSync(segmentMiddleware).isFile() &&
127
+ segmentMiddleware !== globalMiddleware &&
128
+ !results.includes(segmentMiddleware)) {
129
+ results.push(segmentMiddleware);
130
+ break;
131
+ }
132
+ }
133
+ }
134
+ // 3. At leaf directory, check for exported middleware in route files (page.tsx, route.ts, etc.)
135
+ for (const stem of ROUTE_FILE_STEMS) {
136
+ for (const ext of MIDDLEWARE_FILE_EXTS) {
137
+ const candidateRouteFile = path_1.default.join(currentDir, `${stem}${ext}`);
138
+ if (fs_1.default.existsSync(candidateRouteFile) &&
139
+ fs_1.default.statSync(candidateRouteFile).isFile() &&
140
+ !results.includes(candidateRouteFile)) {
141
+ // Record candidate route file to inspect for exported middleware
142
+ results.push(candidateRouteFile);
143
+ }
144
+ }
145
+ }
146
+ routeDiscoveryCache.set(cacheKey, results);
147
+ return results;
148
+ }
149
+ // ---------------------------------------------------------------------------
150
+ // Matcher Support
151
+ // ---------------------------------------------------------------------------
152
+ function patternToRegExp(pattern) {
153
+ // Convert Next.js-style / path pattern to RegExp:
154
+ // /foo/:path* → /foo(?:/(.*))?
155
+ // /foo/:bar → /foo/[^/]+
156
+ // /foo/* → /foo(?:/(.*))?
157
+ let re = pattern
158
+ .replace(/\/:[^/]+\*/g, '(?:/(.*))?') // /:path* (0 or more sub-paths)
159
+ .replace(/:[^/]+\*/g, '(.*)') // bare :path*
160
+ .replace(/:[^/]+/g, '[^/]+') // :param (single segment)
161
+ .replace(/\/\*/g, '(?:/(.*))?') // /*
162
+ .replace(/\*/g, '(.*)'); // bare *
163
+ return new RegExp(`^${re}/?$`);
164
+ }
165
+ function shouldRunMiddleware(middlewareModule, pathname, request) {
166
+ const config = middlewareModule.config;
167
+ if (!config?.matcher)
168
+ return true;
169
+ const matchers = Array.isArray(config.matcher)
170
+ ? config.matcher
171
+ : [config.matcher];
172
+ return matchers.some((matcher) => {
173
+ try {
174
+ const source = typeof matcher === 'string' ? matcher : matcher.source;
175
+ const re = patternToRegExp(source);
176
+ if (!re.test(pathname)) {
177
+ return false;
178
+ }
179
+ if (typeof matcher === 'string' || !request) {
180
+ return true;
181
+ }
182
+ if (matcher.has && !matcher.has.every((rule) => matchRequestRule(request, rule))) {
183
+ return false;
184
+ }
185
+ if (matcher.missing && !matcher.missing.every((rule) => !matchRequestRule(request, rule))) {
186
+ return false;
187
+ }
188
+ return true;
189
+ }
190
+ catch {
191
+ // Fail closed: a broken matcher must not skip auth middleware.
192
+ return true;
193
+ }
194
+ });
195
+ }
196
+ function matchRequestRule(request, rule) {
197
+ let actual;
198
+ if (rule.type === 'header') {
199
+ actual = request.headers.get(rule.key) || undefined;
200
+ }
201
+ else if (rule.type === 'cookie') {
202
+ actual = request.cookies.get(rule.key)?.value;
203
+ }
204
+ else {
205
+ actual = request.nextUrl.searchParams.get(rule.key) || undefined;
206
+ }
207
+ if (rule.value === undefined) {
208
+ return Boolean(actual);
209
+ }
210
+ return actual === rule.value;
211
+ }
212
+ // ---------------------------------------------------------------------------
213
+ // Build NextRequest-like Object
214
+ // ---------------------------------------------------------------------------
215
+ function buildNextRequest(req) {
216
+ const protocol = req.protocol || 'http';
217
+ const host = (req.get && req.get('host')) || req.headers?.host || 'localhost';
218
+ const rawPath = req.path || (req.url ? req.url.split('?')[0] : '/');
219
+ const fullUrl = `${protocol}://${host}${req.originalUrl || req.url || rawPath}`;
220
+ const headers = new Headers();
221
+ if (req.headers) {
222
+ for (const [key, val] of Object.entries(req.headers)) {
223
+ if (val !== undefined) {
224
+ if (Array.isArray(val)) {
225
+ for (const item of val) {
226
+ headers.append(key, item);
227
+ }
228
+ }
229
+ else {
230
+ headers.set(key, String(val));
231
+ }
232
+ }
233
+ }
234
+ }
235
+ const queryParams = new URLSearchParams(typeof req.query === 'object' && req.query !== null
236
+ ? req.query
237
+ : {});
238
+ // Cookie helpers
239
+ const rawCookies = req.cookies || {};
240
+ if (!Object.keys(rawCookies).length && req.headers?.cookie) {
241
+ for (const pair of String(req.headers.cookie).split(';')) {
242
+ const [k, ...v] = pair.split('=');
243
+ const name = k?.trim();
244
+ if (name) {
245
+ rawCookies[name] = (0, cookie_parse_1.safeDecodeURIComponent)(v.join('=').trim());
246
+ }
247
+ }
248
+ }
249
+ return {
250
+ url: fullUrl,
251
+ method: req.method || 'GET',
252
+ path: rawPath,
253
+ headers,
254
+ nextUrl: {
255
+ pathname: rawPath,
256
+ searchParams: queryParams,
257
+ href: fullUrl,
258
+ origin: `${protocol}://${host}`,
259
+ },
260
+ cookies: {
261
+ get: (name) => rawCookies[name] !== undefined ? { name, value: String(rawCookies[name]) } : undefined,
262
+ getAll: () => Object.entries(rawCookies).map(([n, v]) => ({
263
+ name: n,
264
+ value: v,
265
+ })),
266
+ has: (name) => rawCookies[name] !== undefined,
267
+ },
268
+ };
269
+ }
270
+ function cloneRequestWithHeaders(req, newHeaders) {
271
+ const mergedHeaders = new Headers(req.headers);
272
+ if (newHeaders instanceof Headers) {
273
+ newHeaders.forEach((val, key) => mergedHeaders.set(key, val));
274
+ }
275
+ else {
276
+ for (const [k, v] of Object.entries(newHeaders)) {
277
+ mergedHeaders.set(k, String(v));
278
+ }
279
+ }
280
+ return {
281
+ ...req,
282
+ headers: mergedHeaders,
283
+ };
284
+ }
285
+ // ---------------------------------------------------------------------------
286
+ // Execution Pipeline
287
+ // ---------------------------------------------------------------------------
288
+ function loadModule(filePath, isDev) {
289
+ try {
290
+ if (isDev) {
291
+ try {
292
+ delete require.cache[require.resolve(filePath)];
293
+ }
294
+ catch { }
295
+ }
296
+ return require(filePath);
297
+ }
298
+ catch (err) {
299
+ console.warn(`[vista:middleware] Failed to load middleware module ${filePath}:`, err?.message);
300
+ return null;
301
+ }
302
+ }
303
+ /**
304
+ * Run user-defined middleware chain (global + route-specific) for the given request.
305
+ *
306
+ * Execution Order:
307
+ * 1. Global middleware (cwd/middleware.ts or cwd/src/middleware.ts)
308
+ * 2. Route segment middlewares (from shallowest to deepest in app/)
309
+ * 3. Route file middleware (exported `middleware` from page.tsx/route.ts)
310
+ * 4. Downstream handler (Page component or API route handler)
311
+ *
312
+ * Returns a `MiddlewareResult` indicating whether the request was redirected,
313
+ * short-circuited with a response, rewritten, or continued to the next handler.
314
+ */
315
+ async function runMiddleware(req, cwd, isDev = false) {
316
+ const pathname = req.path || (req.url ? req.url.split('?')[0] : '/');
317
+ // 1. Discover Global Middleware
318
+ const globalFile = discoverGlobalMiddleware(cwd, isDev);
319
+ const entries = [];
320
+ if (globalFile) {
321
+ entries.push({
322
+ source: 'global',
323
+ filePath: globalFile,
324
+ depth: 0,
325
+ });
326
+ }
327
+ // 2. Discover Route-Specific Middlewares
328
+ const routeFiles = discoverRouteMiddlewares(cwd, pathname, isDev);
329
+ for (let i = 0; i < routeFiles.length; i++) {
330
+ const file = routeFiles[i];
331
+ const isMiddlewareFile = path_1.default.basename(file).startsWith('middleware.');
332
+ entries.push({
333
+ source: isMiddlewareFile ? 'segment' : 'route',
334
+ filePath: file,
335
+ depth: i + 1,
336
+ });
337
+ }
338
+ // If no middleware discovered, skip directly
339
+ if (entries.length === 0) {
340
+ return { kind: 'skip' };
341
+ }
342
+ const activeChain = [];
343
+ for (const entry of entries) {
344
+ const mod = loadModule(entry.filePath, isDev);
345
+ if (!mod) {
346
+ // Dedicated middleware files must fail closed. Skipping them would
347
+ // silently bypass auth. Route files may fail for unrelated reasons.
348
+ if (entry.source !== 'route') {
349
+ return {
350
+ kind: 'short-circuit',
351
+ status: 500,
352
+ body: 'Middleware Error',
353
+ };
354
+ }
355
+ continue;
356
+ }
357
+ // In a route file (e.g. page.tsx, route.ts), we ONLY accept an explicit `export const middleware`
358
+ // or `export function middleware` (not `export default`, which is the page component)
359
+ let fn;
360
+ if (entry.source === 'route') {
361
+ if (typeof mod.middleware === 'function') {
362
+ fn = mod.middleware;
363
+ }
364
+ }
365
+ else {
366
+ // In a dedicated middleware.* file, accept either `export default` or `export function middleware`
367
+ if (typeof mod.middleware === 'function') {
368
+ fn = mod.middleware;
369
+ }
370
+ else if (typeof mod.default === 'function') {
371
+ fn = mod.default;
372
+ }
373
+ }
374
+ if (!fn) {
375
+ if (entry.source !== 'route') {
376
+ return {
377
+ kind: 'short-circuit',
378
+ status: 500,
379
+ body: 'Middleware Error',
380
+ };
381
+ }
382
+ continue;
383
+ }
384
+ // Check optional matcher config
385
+ if (!shouldRunMiddleware(mod, pathname, buildNextRequest(req))) {
386
+ continue;
387
+ }
388
+ activeChain.push({
389
+ filePath: entry.filePath,
390
+ source: entry.source,
391
+ fn,
392
+ allowedRedirectHosts: mod.config?.allowedRedirectHosts,
393
+ });
394
+ }
395
+ if (activeChain.length === 0) {
396
+ return { kind: 'skip' };
397
+ }
398
+ // 4. Execute the chain
399
+ let currentRequest = buildNextRequest(req);
400
+ const aggregatedResponseHeaders = new Map();
401
+ const modifiedRequestHeaders = new Map();
402
+ async function dispatch(index, requestObj) {
403
+ if (index >= activeChain.length) {
404
+ // Terminal node: reached end of middleware chain
405
+ return new Response(null, {
406
+ status: 200,
407
+ headers: {
408
+ 'x-middleware-next': '1',
409
+ },
410
+ });
411
+ }
412
+ const currentItem = activeChain[index];
413
+ let nextCalled = false;
414
+ let nextRequestObj = requestObj;
415
+ const nextFn = async (options) => {
416
+ nextCalled = true;
417
+ if (options?.request?.headers) {
418
+ const sanitized = (0, middleware_security_1.sanitizeRequestHeaderMap)(options.request.headers);
419
+ const headerRecord = {};
420
+ sanitized.forEach((value, key) => {
421
+ headerRecord[key] = value;
422
+ modifiedRequestHeaders.set(key, value);
423
+ });
424
+ nextRequestObj = cloneRequestWithHeaders(requestObj, headerRecord);
425
+ }
426
+ return dispatch(index + 1, nextRequestObj);
427
+ };
428
+ const context = {
429
+ request: requestObj,
430
+ next: nextFn,
431
+ url: requestObj.url,
432
+ method: requestObj.method,
433
+ headers: requestObj.headers,
434
+ nextUrl: requestObj.nextUrl,
435
+ cookies: requestObj.cookies,
436
+ };
437
+ try {
438
+ // Invoke middleware. Supports:
439
+ // - middleware({ request, next }) [Issue #7 Part 5 signature]
440
+ // - middleware(request, next)
441
+ // - middleware(request)
442
+ const output = await currentItem.fn(context, nextFn);
443
+ // Handle direct Response return
444
+ if (output instanceof Response) {
445
+ return output;
446
+ }
447
+ // If next() was explicitly called and returned nothing, propagate standard continuation
448
+ if (nextCalled) {
449
+ return new Response(null, {
450
+ status: 200,
451
+ headers: {
452
+ 'x-middleware-next': '1',
453
+ },
454
+ });
455
+ }
456
+ // If middleware returned void without calling next(), continue to next in chain
457
+ return dispatch(index + 1, requestObj);
458
+ }
459
+ catch (err) {
460
+ console.error(`[vista:middleware] Error in middleware at ${currentItem.filePath}:`, err?.message ?? String(err));
461
+ if (process.env.NODE_ENV === 'production') {
462
+ return new Response('Middleware Error', { status: 500 });
463
+ }
464
+ return dispatch(index + 1, requestObj);
465
+ }
466
+ }
467
+ const finalResponse = await dispatch(0, currentRequest);
468
+ // 5. Convert final Response to MiddlewareResult
469
+ // Collect all response headers
470
+ if (finalResponse.headers && typeof finalResponse.headers.forEach === 'function') {
471
+ finalResponse.headers.forEach((val, key) => {
472
+ const lower = key.toLowerCase();
473
+ if (lower.startsWith('x-middleware-request-')) {
474
+ const name = lower.slice('x-middleware-request-'.length);
475
+ if (!name || name === 'host' || name === 'cookie' || name.startsWith('x-forwarded-') || name.startsWith('x-middleware-')) {
476
+ return;
477
+ }
478
+ modifiedRequestHeaders.set(name, val);
479
+ return;
480
+ }
481
+ aggregatedResponseHeaders.set(key, val);
482
+ });
483
+ }
484
+ if (process.env.NODE_ENV === 'production') {
485
+ (0, middleware_security_1.securityHeaders)().forEach((value, key) => {
486
+ if (!aggregatedResponseHeaders.has(key) && !aggregatedResponseHeaders.has(key.toLowerCase())) {
487
+ aggregatedResponseHeaders.set(key, value);
488
+ }
489
+ });
490
+ }
491
+ const allowedRedirectHosts = activeChain.flatMap((item) => item.allowedRedirectHosts || []);
492
+ const requestUrl = currentRequest.url;
493
+ // 5a. Redirect
494
+ const location = finalResponse.headers?.get?.('Location') || finalResponse.headers?.get?.('location');
495
+ if (location) {
496
+ if (!(0, middleware_security_1.isSafeRedirectLocation)(location, requestUrl, allowedRedirectHosts)) {
497
+ return {
498
+ kind: 'short-circuit',
499
+ status: 400,
500
+ responseHeaders: aggregatedResponseHeaders,
501
+ requestHeaders: modifiedRequestHeaders,
502
+ body: 'Invalid redirect',
503
+ };
504
+ }
505
+ return {
506
+ kind: 'redirect',
507
+ status: finalResponse.status || 307,
508
+ location,
509
+ responseHeaders: aggregatedResponseHeaders,
510
+ requestHeaders: modifiedRequestHeaders,
511
+ };
512
+ }
513
+ // 5b. Rewrite
514
+ const rewrite = finalResponse.headers?.get?.('x-middleware-rewrite');
515
+ if (rewrite) {
516
+ if (!(0, middleware_security_1.isSafeRewriteLocation)(rewrite)) {
517
+ return {
518
+ kind: 'short-circuit',
519
+ status: 400,
520
+ responseHeaders: aggregatedResponseHeaders,
521
+ requestHeaders: modifiedRequestHeaders,
522
+ body: 'Invalid rewrite',
523
+ };
524
+ }
525
+ return {
526
+ kind: 'rewrite',
527
+ location: rewrite,
528
+ responseHeaders: aggregatedResponseHeaders,
529
+ requestHeaders: modifiedRequestHeaders,
530
+ };
531
+ }
532
+ // 5c. Continue to page/route handler
533
+ const shouldContinue = finalResponse.headers?.get?.('x-middleware-next');
534
+ if (shouldContinue) {
535
+ return {
536
+ kind: 'next',
537
+ responseHeaders: aggregatedResponseHeaders,
538
+ requestHeaders: modifiedRequestHeaders,
539
+ };
540
+ }
541
+ // 5d. Short-circuit with response body and custom status
542
+ let bodyContent = null;
543
+ try {
544
+ bodyContent = await finalResponse.text();
545
+ }
546
+ catch { }
547
+ return {
548
+ kind: 'short-circuit',
549
+ status: finalResponse.status || 200,
550
+ responseHeaders: aggregatedResponseHeaders,
551
+ requestHeaders: modifiedRequestHeaders,
552
+ body: bodyContent,
553
+ };
554
+ }
555
+ // ---------------------------------------------------------------------------
556
+ // Apply Middleware Result
557
+ // ---------------------------------------------------------------------------
558
+ /**
559
+ * Apply a MiddlewareResult to the Express request/response.
560
+ * Returns `true` if the response was finalized (caller should `return`),
561
+ * `false` if the request should continue to the next handler.
562
+ */
563
+ function applyMiddlewareResult(result, req, res) {
564
+ // 1. Forward any modified request headers to req.headers
565
+ if (result.requestHeaders && req.headers) {
566
+ const sanitized = (0, middleware_security_1.sanitizeRequestHeaderMap)(result.requestHeaders);
567
+ sanitized.forEach((value, key) => {
568
+ req.headers[key] = value;
569
+ });
570
+ }
571
+ // 2. Forward any response headers the middleware set
572
+ if (result.responseHeaders) {
573
+ result.responseHeaders.forEach((value, key) => {
574
+ const lower = key.toLowerCase();
575
+ // Skip internal transport headers
576
+ if (lower === 'x-middleware-next' ||
577
+ lower === 'x-middleware-rewrite' ||
578
+ lower === 'location' ||
579
+ lower.startsWith('x-middleware-request-')) {
580
+ return;
581
+ }
582
+ res.setHeader(key, value);
583
+ });
584
+ }
585
+ switch (result.kind) {
586
+ case 'redirect':
587
+ res.redirect(result.status || 307, result.location);
588
+ return true;
589
+ case 'rewrite':
590
+ req.url = result.location;
591
+ return false; // continue with rewritten URL
592
+ case 'short-circuit': {
593
+ const status = result.status || 403;
594
+ if (result.body !== undefined && result.body !== null && result.body !== '') {
595
+ res.status(status).send(result.body);
596
+ }
597
+ else {
598
+ res.status(status).end();
599
+ }
600
+ return true;
601
+ }
602
+ case 'next':
603
+ case 'skip':
604
+ default:
605
+ return false; // continue
606
+ }
607
+ }