@stacksjs/router 0.74.20 → 0.74.22
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.
|
@@ -2,6 +2,7 @@ import type { SessionData, SessionStore } from '@stacksjs/bun-router';
|
|
|
2
2
|
export declare interface EncryptedSessionStoreOptions {
|
|
3
3
|
appKey?: string
|
|
4
4
|
}
|
|
5
|
+
declare type SecurityModule = typeof import('@stacksjs/security');
|
|
5
6
|
/**
|
|
6
7
|
* Wrap a bun-router `SessionStore<SessionData>` so all writes are
|
|
7
8
|
* encrypted on the way in and decrypted on the way out. Drop-in
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
let securityModuleLoad;function loadSecurityModule(){securityModuleLoad??=import("@stacksjs/security");return securityModuleLoad}export class EncryptedSessionStore{inner;opts;constructor(inner,opts={}){this.inner=inner;this.opts=opts}async set(sid,session,ttl){const envelope=await this.wrap(sid,session);await this.inner.set(sid,envelope,ttl)}async touch(sid,session,ttl){const envelope=await this.wrap(sid,session);if(this.inner.touch)await this.inner.touch(sid,envelope,ttl);else await this.inner.set(sid,envelope,ttl)}async get(sid){const stored=await this.inner.get(sid);if(!stored)return null;return this.unwrap(stored)}destroy(sid){return this.inner.destroy(sid)}async all(){const wrapped=await this.inner.all?.()??{},out={};for(const[sid,envelope]of Object.entries(wrapped)){const decrypted=await this.unwrap(envelope);if(decrypted)out[sid]=decrypted}return out}async length(){if(this.inner.length)return this.inner.length();return Object.keys(await this.all()).length}async clear(){if(this.inner.clear)return this.inner.clear();const sessions=await this.inner.all?.()??{};await Promise.all(Object.keys(sessions).map((sid)=>this.inner.destroy(sid)))}async wrap(sid,session){const{id,...rest}=session,{encrypt}=await loadSecurityModule(),ciphertext=await encrypt(JSON.stringify(rest),this.opts.appKey);return{_enc:!0,id:id??sid,data:ciphertext}}async unwrap(stored){if(!stored||typeof stored!=="object")return null;const candidate=stored;if(candidate._enc===!0&&typeof candidate.data==="string")try{const{decrypt}=await loadSecurityModule(),decrypted=await decrypt(candidate.data,this.opts.appKey),parsed=JSON.parse(decrypted);if(candidate.id!==void 0)parsed.id=candidate.id;return parsed}catch{return null}return candidate}}
|
package/dist/error-handler.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { EnhancedRequest } from '@stacksjs/bun-router';
|
|
2
1
|
/**
|
|
3
2
|
* Add a query to the recent queries list for error context.
|
|
4
3
|
* Uses a circular buffer for O(1) insert instead of array.shift().
|
|
@@ -9,53 +8,6 @@ import type { EnhancedRequest } from '@stacksjs/bun-router';
|
|
|
9
8
|
* is highly correlated with missing eager loading.
|
|
10
9
|
*/
|
|
11
10
|
export declare function trackQuery(query: string, time?: number, connection?: string): void;
|
|
12
|
-
/**
|
|
13
|
-
* Snapshot of query shape counts for the active request. Useful for
|
|
14
|
-
* tests asserting that an action ran a single query for `posts`
|
|
15
|
-
* instead of one-per-user.
|
|
16
|
-
*/
|
|
17
|
-
export declare function getQueryShapeCounts(): ReadonlyMap<string, number>;
|
|
18
|
-
/**
|
|
19
|
-
* Reset query tracking for the active scope.
|
|
20
|
-
*
|
|
21
|
-
* Inside a request, this clears the per-request tracking object — but
|
|
22
|
-
* the object is also auto-collected when the request goes out of scope,
|
|
23
|
-
* so the explicit call is mainly useful for tests that re-use a single
|
|
24
|
-
* request. Outside a request, this clears the process-wide fallback.
|
|
25
|
-
*/
|
|
26
|
-
export declare function clearTrackedQueries(): void;
|
|
27
|
-
/**
|
|
28
|
-
* Create an Ignition-style error response for development
|
|
29
|
-
*/
|
|
30
|
-
export declare function createErrorResponse(error: Error, request: Request | EnhancedRequest, options?: {
|
|
31
|
-
status?: number
|
|
32
|
-
handlerPath?: string
|
|
33
|
-
routingContext?: {
|
|
34
|
-
controller?: string
|
|
35
|
-
routeName?: string
|
|
36
|
-
middleware?: string[]
|
|
37
|
-
}
|
|
38
|
-
}): Promise<Response>;
|
|
39
|
-
/**
|
|
40
|
-
* Create a middleware error response (401, 403, etc.)
|
|
41
|
-
*
|
|
42
|
-
* Reads `statusCode` OR `status` off the error so both shapes are honored:
|
|
43
|
-
* - middleware that throws `Object.assign(new Error('msg'), { statusCode: 401 })`
|
|
44
|
-
* - framework HttpError instances where the field is named `status`
|
|
45
|
-
*
|
|
46
|
-
* Without the `status` fallback, every `HttpError(401, …)` throw from auth or
|
|
47
|
-
* validation middleware leaks out as a 500 with an Ignition error page —
|
|
48
|
-
* which is what we used to ship for `GET /api/me` without a token.
|
|
49
|
-
*/
|
|
50
|
-
export declare function createMiddlewareErrorResponse(error: Error & { statusCode?: number, status?: number, headers?: Record<string, string> }, request: Request | EnhancedRequest): Promise<Response>;
|
|
51
|
-
/**
|
|
52
|
-
* Create a validation error response
|
|
53
|
-
*/
|
|
54
|
-
export declare function createValidationErrorResponse(errors: Record<string, string[]>, _request: Request | EnhancedRequest): Response;
|
|
55
|
-
/**
|
|
56
|
-
* Create a 404 Not Found response
|
|
57
|
-
*/
|
|
58
|
-
export declare function createNotFoundResponse(path: string, request: Request | EnhancedRequest): Promise<Response>;
|
|
59
11
|
/**
|
|
60
12
|
* Standard error response structure used across all JSON error responses.
|
|
61
13
|
*/
|
package/dist/error-handler.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import process from"node:process";import{log}from"@stacksjs/logging";import{
|
|
1
|
+
import process from"node:process";import{log}from"@stacksjs/logging";import{isApiRequest}from"./api-shape";import{getCurrentRequest}from"./request-context";function buildErrorJson(opts){const body={error:opts.error,message:opts.message,status:opts.status,timestamp:new Date().toISOString()};if(opts.details)body.details=opts.details;return JSON.stringify(body)}function isDebugAllowed(){const appEnv=(process.env.APP_ENV??"").toLowerCase();if(appEnv==="development")return!0;if(!appEnv&&process.env.NODE_ENV==="development")return!0;return!1}function getJsonHeaders(){return{"Content-Type":"application/json"}}function getJsonHeadersFull(){return getJsonHeaders()}const MAX_QUERIES=50,N1_THRESHOLD=5;function newQueryTrack(){return{buffer:Array(MAX_QUERIES).fill(null),writeIndex:0,count:0,shapeCounts:new Map,n1Warned:new Set}}const REQUEST_QUERY_TRACK_KEY=Symbol.for("stacks.queryTracking");let fallbackTrack=newQueryTrack();function getQueryTrack(){const req=getCurrentRequest();if(!req)return fallbackTrack;let track=req[REQUEST_QUERY_TRACK_KEY];if(!track){track=newQueryTrack();req[REQUEST_QUERY_TRACK_KEY]=track}return track}function normalizeQueryShape(query){return query.replace(/'(?:[^']|'')*'/g,"?").replace(/"(?:[^"]|"")*"/g,"?").replace(/\b\d+(?:\.\d+)?\b/g,"?").replace(/IN\s*\([^)]*\)/gi,"IN (?)").replace(/\s+/g," ").trim().toUpperCase()}export function trackQuery(query,time,connection){const track=getQueryTrack();track.buffer[track.writeIndex]={query,time,connection};track.writeIndex=(track.writeIndex+1)%MAX_QUERIES;if(track.count<MAX_QUERIES)track.count++;if(!isDebugAllowed())return;const shape=normalizeQueryShape(query);if(shape.startsWith("INSERT INTO QUERY_LOGS")||shape.startsWith("EXPLAIN"))return;const next=(track.shapeCounts.get(shape)??0)+1;track.shapeCounts.set(shape,next);if(next===N1_THRESHOLD+1&&!track.n1Warned.has(shape)){track.n1Warned.add(shape);import("@stacksjs/logging").then(({log})=>{log.warn(`[orm] Possible N+1 - query shape ran ${next}\xD7 in this request:
|
|
2
2
|
${shape}
|
|
3
|
-
Hint: load related rows with .with('relation') or eager-load via includes() before iterating.`)}).catch(()=>{})}}function getRecentQueries(){const track=getQueryTrack();if(track.count===0)return[];const result=[],start=track.count<MAX_QUERIES?0:track.writeIndex;for(let i=0;i<track.count;i++){const entry=track.buffer[(start+i)%MAX_QUERIES];if(entry)result.push(entry)}return result}export function getQueryShapeCounts(){return new Map(getQueryTrack().shapeCounts)}export function clearTrackedQueries(){const req=getCurrentRequest();if(req
|
|
3
|
+
Hint: load related rows with .with('relation') or eager-load via includes() before iterating.`)}).catch(()=>{})}}const QUERY_TRACKER_KEY=Symbol.for("stacks.database.queryTracker");globalThis[QUERY_TRACKER_KEY]=trackQuery;function getRecentQueries(){const track=getQueryTrack();if(track.count===0)return[];const result=[],start=track.count<MAX_QUERIES?0:track.writeIndex;for(let i=0;i<track.count;i++){const entry=track.buffer[(start+i)%MAX_QUERIES];if(entry)result.push(entry)}return result}export function getQueryShapeCounts(){return new Map(getQueryTrack().shapeCounts)}export function clearTrackedQueries(){const req=getCurrentRequest();if(req){if(req[REQUEST_QUERY_TRACK_KEY])delete req[REQUEST_QUERY_TRACK_KEY];return}fallbackTrack=newQueryTrack()}function getErrorHandlerConfig(){return{appName:"Stacks",theme:"auto",showEnvironment:!0,showQueries:!0,showRequest:!0,enableCopyMarkdown:!0,snippetLines:8,basePaths:[process.cwd()]}}const SENSITIVE_PATTERNS=["password","secret","token","api_key","apikey","access_key","accesskey","private_key","privatekey","credit_card","creditcard","card_number","cardnumber","cvv","ssn","authorization","credential","aws_secret","aws_access","database_password","db_password","encryption_key","signing_key","bearer","session_id","sessionid","cookie"],MAX_SANITIZE_DEPTH=10,CIRCULAR_PLACEHOLDER="[Circular]";function sanitizeData(data,depth=0,seen=new WeakSet){if(!data||typeof data!=="object"||depth>=MAX_SANITIZE_DEPTH)return data;if(seen.has(data))return CIRCULAR_PLACEHOLDER;seen.add(data);if(Array.isArray(data))return data.map((item)=>sanitizeData(item,depth+1,seen));const sanitized={};for(const[key,value]of Object.entries(data)){const lowerKey=key.toLowerCase();if(SENSITIVE_PATTERNS.some((pattern)=>lowerKey.includes(pattern)))sanitized[key]="********";else if(typeof value==="object"&&value!==null)sanitized[key]=sanitizeData(value,depth+1,seen);else sanitized[key]=value}return sanitized}function getRequestBody(request){const req=request;if(req.jsonBody)return sanitizeData(req.jsonBody);if(req.formBody)return sanitizeData(req.formBody);return}async function getUserContext(request){const authed=request._authenticatedUser;if(authed)return{id:authed.id,email:authed.email,name:authed.name||authed.username};return}export async function createErrorResponse(error,request,options){const status=options?.status||500;log.debug(`[error] ${status} ${error.message}`);if(!isDebugAllowed()){if(isApiRequest(request)){const isClientError=status>=400&&status<500,errDetails=error.details;return new Response(buildErrorJson({error:isClientError?error.name||"Client Error":"Internal Server Error",message:isClientError?error.message:"An unexpected error occurred.",status,details:isClientError&&errDetails&&typeof errDetails==="object"?errDetails:void 0}),{status,headers:getJsonHeaders()})}const{renderProductionErrorPage}=await import("@stacksjs/error-handling/error-page");return new Response(renderProductionErrorPage(status),{status,headers:{"Content-Type":"text/html; charset=utf-8"}})}try{const{createErrorHandler}=await import("@stacksjs/error-handling/error-page"),handler=createErrorHandler(getErrorHandlerConfig());handler.setFramework("Stacks","0.70.0");const requestBody=getRequestBody(request);if(requestBody){const url=new URL(request.url);handler.setRequest({method:request.method,url:request.url,headers:Object.fromEntries(request.headers.entries()),queryParams:Object.fromEntries(url.searchParams.entries()),body:requestBody})}else handler.setRequest(request);const userContext=await getUserContext(request);if(userContext)handler.setUser(userContext);if(options?.routingContext)handler.setRouting(options.routingContext);else if(options?.handlerPath)handler.setRouting({controller:options.handlerPath});for(const query of getRecentQueries())handler.addQuery(query.query,query.time,query.connection);if(isApiRequest(request)){const details={handler:options?.handlerPath};if(isDebugAllowed()){details.stack=error.stack?.split(`
|
|
4
4
|
`).slice(0,10);details.queries=getRecentQueries().slice(-10)}return new Response(buildErrorJson({error:error.name||"Error",message:error.message,status,details}),{status,headers:getJsonHeadersFull()})}const corsOrigin=process.env.APP_URL?process.env.APP_URL.startsWith("http")?process.env.APP_URL:`https://${process.env.APP_URL}`:isDebugAllowed()?"*":request.headers.get("origin")??"null",html=await handler.render(error,status);return new Response(html,{status,headers:{"Content-Type":"text/html; charset=utf-8","Access-Control-Allow-Origin":corsOrigin}})}catch(renderError){console.error("[Error Handler] Failed to render error page:",renderError);const escapeHtml=(s)=>s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");return new Response(`
|
|
5
5
|
<html>
|
|
6
6
|
<head><title>Error</title></head>
|
|
@@ -10,4 +10,4 @@ import process from"node:process";import{log}from"@stacksjs/logging";import{crea
|
|
|
10
10
|
<pre>${escapeHtml(error.stack||"")}</pre>
|
|
11
11
|
</body>
|
|
12
12
|
</html>
|
|
13
|
-
`,{status,headers:{"Content-Type":"text/html; charset=utf-8"}})}}export async function createMiddlewareErrorResponse(error,request){const status=error.statusCode??error.status??500,isDevelopment=isDebugAllowed();if(status>=400&&status<500){const headers=error.headers?{...error.headers,...getJsonHeaders()}:getJsonHeaders();return new Response(buildErrorJson({error:error.name||"ClientError",message:error.message,status}),{status,headers})}if(isDevelopment)return await createErrorResponse(error,request,{status});return new Response(buildErrorJson({error:"Internal Server Error",message:"An unexpected error occurred.",status}),{status,headers:getJsonHeaders()})}export function createValidationErrorResponse(errors,_request){return new Response(buildErrorJson({error:"ValidationError",message:"Validation failed",status:422,details:{errors}}),{status:422,headers:getJsonHeaders()})}export async function createNotFoundResponse(path,request){if(isDebugAllowed()){const error=Error(`Route not found: ${path}`);error.name="NotFoundError";return await createErrorResponse(error,request,{status:404})}if(isApiRequest(request))return new Response(buildErrorJson({error:"NotFound",message:`Route not found: ${path}`,status:404}),{status:404,headers:getJsonHeaders()});return new Response(renderProductionErrorPage(404),{status:404,headers:{"Content-Type":"text/html; charset=utf-8"}})}
|
|
13
|
+
`,{status,headers:{"Content-Type":"text/html; charset=utf-8"}})}}export async function createMiddlewareErrorResponse(error,request){const status=error.statusCode??error.status??500,isDevelopment=isDebugAllowed();if(status>=400&&status<500){const headers=error.headers?{...error.headers,...getJsonHeaders()}:getJsonHeaders();return new Response(buildErrorJson({error:error.name||"ClientError",message:error.message,status}),{status,headers})}if(isDevelopment)return await createErrorResponse(error,request,{status});return new Response(buildErrorJson({error:"Internal Server Error",message:"An unexpected error occurred.",status}),{status,headers:getJsonHeaders()})}export function createValidationErrorResponse(errors,_request){return new Response(buildErrorJson({error:"ValidationError",message:"Validation failed",status:422,details:{errors}}),{status:422,headers:getJsonHeaders()})}export async function createNotFoundResponse(path,request){if(isDebugAllowed()){const error=Error(`Route not found: ${path}`);error.name="NotFoundError";return await createErrorResponse(error,request,{status:404})}if(isApiRequest(request))return new Response(buildErrorJson({error:"NotFound",message:`Route not found: ${path}`,status:404}),{status:404,headers:getJsonHeaders()});const{renderProductionErrorPage}=await import("@stacksjs/error-handling/error-page");return new Response(renderProductionErrorPage(404),{status:404,headers:{"Content-Type":"text/html; charset=utf-8"}})}
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import"./request-augmentation";export*from"@stacksjs/bun-router";export{assertRouteMiddlewareResolvable,configureViewDirectories,clearCsrfModuleCache,clearMiddlewareCache,createStacksRouter,disableViewRouting,findUnresolvableRouteMiddleware,installMiddlewareHotReload,resetBootHooks,route,runBootHooks,serve,serverResponse,url,warnOnMultipleRouterInstances}from"./stacks-router";export{cacheRequestQuery,clearCurrentRequest,getCurrentRequest,getTraceId,request,runWithRequest,setCurrentRequest,withTraceId}from"./request-context";export{defineMiddleware,Middleware}from"./middleware";export{listRootMountedAppRoutes,loadRoutes}from"./route-loader";export{clearTrackedQueries,createErrorResponse,createMiddlewareErrorResponse,createNotFoundResponse,createValidationErrorResponse,getQueryShapeCounts,trackQuery}from"./error-handler";export{listNamedRoutes,listRegisteredRoutes,routeParams}from"./stacks-router";export{isRouterAction,wrapAction}from"./stacks-router";export{createTypedRouter}from"./typed-router";export{clearRouteModelBindings,defineRouteModelBinding,resolveRouteModel,routeModelBindings,setRouteModelFallback}from"./route-model-binding";export{isApiRequest,JSON_CONTENT_TYPE}from"./api-shape";export{rateLimit,rateLimitStatus,clearRateLimit}from"./rate-limit";export{PathParamError,safePathParam,sanitizePathParam}from"./path-sanitize";export{checkApplicationHealth,runHealthProbes}from"./health";export{stream}from"./stacks-router";export{signedUrl,signUrl,verifySignedUrl,verifySignedUrlMiddleware}from"./signed-url";export{EncryptedSessionStore}from"./encrypted-session-store";export{createSessionStore,createStacksSessionStore,DatabaseSessionStore,FileSessionStore,MemorySessionStore,RedisSessionStore}from"./session-factory";
|
package/dist/rate-limit.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{getCurrentRequest}from"./request-context";const PERIOD_SECONDS={second:1,minute:60,hour:3600,day:86400},limiterCache=new Map;let limiterModulePromise;function loadLimiterModule(){return limiterModulePromise??=import("ts-rate-limiter")}async function getLimiter(max,windowMs){const cacheKey=`${windowMs}:${max}`;let limiter=limiterCache.get(cacheKey);if(!limiter){const{RateLimiter}=await loadLimiterModule();limiter=new RateLimiter({windowMs,maxRequests:max,algorithm:"fixed-window",standardHeaders:!1,legacyHeaders:!1});limiterCache.set(cacheKey,limiter)}return limiter}async function resolveIdentity(explicit){if(explicit!==void 0)return explicit;const req=getCurrentRequest(),{defaultIdentity}=await loadLimiterModule();return req?defaultIdentity(req):"anon"}export function rateLimit(key,max,options={}){const run=async(windowMs)=>{const[id,limiter]=await Promise.all([resolveIdentity(options.identity),getLimiter(max,windowMs)]),bucketKey=`${key}:${id}`;try{await limiter.enforce(bucketKey)}catch(err){const{RateLimitError}=await loadLimiterModule();if(err instanceof RateLimitError){const{HttpError}=await import("@stacksjs/error-handling/http");throw Object.assign(new HttpError(429,"Too many requests",{key,max,retryAfter:err.retryAfter}),{headers:err.toHeaders()})}throw err}};return{async per(period){const seconds=PERIOD_SECONDS[period];if(!seconds)throw Error(`rateLimit().per: unknown period '${period}'`);await run(seconds*1000)},async over(ttlSeconds){if(!Number.isFinite(ttlSeconds)||ttlSeconds<=0)throw Error(`rateLimit().over: ttl must be a positive number, got ${ttlSeconds}`);await run(ttlSeconds*1000)}}}export async function rateLimitStatus(key,max,windowSeconds,options={}){const[id,limiter]=await Promise.all([resolveIdentity(options.identity),getLimiter(max,windowSeconds*1000)]),bucketKey=`${key}:${id}`,result=await limiter.peek(bucketKey);if(!result)return null;return{count:result.current,limit:result.limit,remaining:Math.max(0,result.limit-result.current)}}export async function clearRateLimit(key,max,windowSeconds,options={}){const[id,limiter]=await Promise.all([resolveIdentity(options.identity),getLimiter(max,windowSeconds*1000)]),bucketKey=`${key}:${id}`;await limiter.reset(bucketKey)}
|
package/dist/stacks-router.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import{response}from"@stacksjs/bun-router";import{Middleware}from"./middleware";import"./request-augmentation";import process from"node:process";import{Buffer}from"node:buffer";import{existsSync}from"node:fs";import{timingSafeEqual}from"node:crypto";import{collect}from"@stacksjs/collections";import{log,report}from"@stacksjs/logging";import{path as p}from"@stacksjs/path";import{UploadedFile}from"@stacksjs/storage";import{applyRequestEnhancements,Router}from"@stacksjs/bun-router";import{checkApplicationHealth}from"./health";const ROUTER_INSTANCES_KEY=Symbol.for("@stacksjs/router:loaded-instances"),loadedRouterInstances=globalThis[ROUTER_INSTANCES_KEY]??=new Set;loadedRouterInstances.add(import.meta.path);const MULTI_INSTANCE_WARNED_KEY=Symbol.for("@stacksjs/router:multi-instance-warned"),bootHooks=[];let bootHooksRun=!1;export async function runBootHooks(){if(bootHooksRun)return;bootHooksRun=!0;for(const hook of bootHooks)try{await hook.run()}catch(error){log.error(`[router] boot hook "${hook.name}" failed:`,error)}}export function resetBootHooks(){bootHooks.length=0;bootHooksRun=!1}export function warnOnMultipleRouterInstances(){if(loadedRouterInstances.size<=1)return!1;const g=globalThis;if(!g[MULTI_INSTANCE_WARNED_KEY]){g[MULTI_INSTANCE_WARNED_KEY]=!0;const paths=[...loadedRouterInstances].map((p)=>` - ${p}`).join(`
|
|
1
|
+
import{response}from"@stacksjs/bun-router";import{Middleware}from"./middleware";import"./request-augmentation";import process from"node:process";import{Buffer}from"node:buffer";import{existsSync}from"node:fs";import{timingSafeEqual}from"node:crypto";import{collect}from"@stacksjs/collections";import{log,report}from"@stacksjs/logging";import{path as p}from"@stacksjs/path";import{UploadedFile}from"@stacksjs/storage/uploaded-file";import{applyRequestEnhancements,Router}from"@stacksjs/bun-router";import{checkApplicationHealth}from"./health";const ROUTER_INSTANCES_KEY=Symbol.for("@stacksjs/router:loaded-instances"),loadedRouterInstances=globalThis[ROUTER_INSTANCES_KEY]??=new Set;loadedRouterInstances.add(import.meta.path);const MULTI_INSTANCE_WARNED_KEY=Symbol.for("@stacksjs/router:multi-instance-warned"),bootHooks=[];let bootHooksRun=!1;export async function runBootHooks(){if(bootHooksRun)return;bootHooksRun=!0;for(const hook of bootHooks)try{await hook.run()}catch(error){log.error(`[router] boot hook "${hook.name}" failed:`,error)}}export function resetBootHooks(){bootHooks.length=0;bootHooksRun=!1}export function warnOnMultipleRouterInstances(){if(loadedRouterInstances.size<=1)return!1;const g=globalThis;if(!g[MULTI_INSTANCE_WARNED_KEY]){g[MULTI_INSTANCE_WARNED_KEY]=!0;const paths=[...loadedRouterInstances].map((p)=>` - ${p}`).join(`
|
|
2
2
|
`);log.warn(`${loadedRouterInstances.size} distinct @stacksjs/router modules loaded in one process; they now share one route table (stacksjs/stacks#1982) so routing still works, but this is a duplicated install worth fixing. It usually means an app vendors storage/framework/core AND installs the published @stacksjs/* dist, and a tsconfig \`paths\` mapping (\`@stacksjs/* -> ./*/src\`) splits module resolution between core files and root files. See stacksjs/stacks#1975.
|
|
3
3
|
Loaded instances:
|
|
4
|
-
${paths}`)}return!0}let __defaultsPkgRoot;const __defaultsPathCache=new Map;function resolveDefaultsPath(rel){const cached=__defaultsPathCache.get(rel);if(cached!==void 0)return cached;if(rel.startsWith("app/")){const published=p.appPath(rel.slice(4));if(existsSync(published)){__defaultsPathCache.set(rel,published);return published}}const vendored=p.storagePath(`framework/defaults/${rel}`);let resolved;if(existsSync(vendored))resolved=vendored;else{if(__defaultsPkgRoot===void 0)try{const pkgJson=Bun.resolveSync("@stacksjs/defaults/package.json",process.cwd());__defaultsPkgRoot=pkgJson.slice(0,pkgJson.lastIndexOf("/"))}catch{__defaultsPkgRoot=null}resolved=__defaultsPkgRoot?`${__defaultsPkgRoot}/${rel}`:vendored}__defaultsPathCache.set(rel,resolved);return resolved}import{runWithRequest}from"./request-context";import{isApiRequest,JSON_CONTENT_TYPE}from"./api-shape";import{clearTrackedQueries,createErrorResponse,createMiddlewareErrorResponse}from"./error-handler";import{rateLimit as enforceRateLimit}from"./rate-limit";import{applySecurityHeaders}from"./security-headers";import{isCursorPaginator,isPaginator,isSimplePaginator}from"@stacksjs/pagination";const csrfSkipRegistry=new Set,csrfRequireRegistry=new Set,routeRateLimitRegistry=new Map;function rateLimitWindowToSeconds(window){if(typeof window==="number"){if(!Number.isFinite(window)||window<=0)throw Error(`[Router] .rateLimit(): window must be a positive number of seconds, got ${window}`);return Math.floor(window)}switch(window){case"second":return 1;case"minute":return 60;case"hour":return 3600;case"day":return 86400;default:throw Error(`[Router] .rateLimit(): unknown period '${String(window)}'`)}}class BoundedMap{max;map=new Map;constructor(max){this.max=max}get(key){return this.map.get(key)}has(key){return this.map.has(key)}set(key,value){if(this.map.has(key))this.map.delete(key);this.map.set(key,value);if(this.map.size>this.max){const oldest=this.map.keys().next().value;if(oldest!==void 0)this.map.delete(oldest)}return this}delete(key){return this.map.delete(key)}clear(){this.map.clear()}get size(){return this.map.size}}function isExposeRoutesAuthorized(req){const flag=process.env.STACKS_EXPOSE_ROUTES??"";if(!flag)return!((process.env.APP_ENV??"").toLowerCase()==="production"||process.env.NODE_ENV==="production");if(flag==="1")return!((process.env.APP_ENV??"").toLowerCase()==="production"||process.env.NODE_ENV==="production");const url=new URL(req.url),submitted=req.headers.get("x-stacks-routes-token")||req.headers.get("X-Stacks-Routes-Token")||url.searchParams.get("token")||"";if(typeof submitted!=="string"||submitted.length===0||submitted.length!==flag.length)return!1;try{return timingSafeEqual(Buffer.from(submitted),Buffer.from(flag))}catch{return!1}}async function applyCorsIfConfigured(req,response){if(!req._corsConfig||!response)return response;try{const{applyCorsHeaders}=await import(resolveDefaultsPath("app/Middleware/Cors.ts"));return applyCorsHeaders(req,response,req._corsConfig)}catch(err){log.warn("[router] CORS header injection failed",{error:err});return response}}const ACTION_CACHE_MAX=5000,actionSkipsCsrfCache=new BoundedMap(ACTION_CACHE_MAX),routeHandlerKeyRegistry=new BoundedMap(ACTION_CACHE_MAX),routeActionRegistry=new Map,CSRF_PROTECTED_METHODS=new Set(["POST","PUT","PATCH","DELETE"]),namedRouteRegistry=new Map;function compileNamedRoute(path){const paramNames=extractRouteParamNames(path),colonRegex=new Map;for(const name of paramNames)colonRegex.set(name,new RegExp(`(^|/):${name}(?=$|/)`,"g"));return{path,paramNames,colonRegex}}function extractRouteParamNames(routePath){const names=new Set;for(const m of routePath.matchAll(/\{(\w+)\}/g))if(m[1])names.add(m[1]);for(const m of routePath.matchAll(/(?:^|\/):(\w+)(?=$|\/)/g))if(m[1])names.add(m[1]);return[...names]}export function url(routeName,params={}){const named=namedRouteRegistry.get(routeName);if(!named)throw Error(`Route '${routeName}' is not defined. Available routes: ${[...namedRouteRegistry.keys()].join(", ")}`);const missing=named.paramNames.filter((name)=>!(name in params)||params[name]===void 0);if(missing.length>0)throw Error(`url('${routeName}'): missing required path param${missing.length>1?"s":""} [${missing.join(", ")}] for path '${named.path}'. Pass them as the second argument: url('${routeName}', { ${named.paramNames.join(", ")} })`);let appUrl;try{appUrl=process.env.APP_URL||"https://localhost"}catch{appUrl="https://localhost"}appUrl=appUrl.replace(/\/$/,"");if(!appUrl.startsWith("http"))appUrl=`https://${appUrl}`;let resolvedPath=named.path;const queryParams={};for(const[key,value]of Object.entries(params)){const curly=`{${key}}`;if(resolvedPath.includes(curly))resolvedPath=resolvedPath.replaceAll(curly,encodeURIComponent(String(value)));else{const re=named.colonRegex.get(key);if(re&&re.test(resolvedPath)){re.lastIndex=0;resolvedPath=resolvedPath.replace(re,`$1${encodeURIComponent(String(value))}`)}else queryParams[key]=String(value)}}const queryString=Object.keys(queryParams).length>0?`?${new URLSearchParams(queryParams).toString()}`:"";return`${appUrl}${resolvedPath}${queryString}`}export function routeParams(routeName){const named=namedRouteRegistry.get(routeName);return named?[...named.paramNames]:[]}export function listNamedRoutes(){const out={};for(const[name,named]of namedRouteRegistry.entries())out[name]=named.path;return out}export function listRegisteredRoutes(){const out=[],seen=new Set;for(const key of routeMiddlewareRegistry.keys()){if(seen.has(key))continue;seen.add(key);const idx=key.indexOf(":");if(idx===-1)continue;const method=key.slice(0,idx),path=key.slice(idx+1);let routeName;for(const[n,named]of namedRouteRegistry.entries())if(named.path===path){routeName=n;break}out.push({method,path,name:routeName,handler:routeHandlerKeyRegistry.get(key),action:routeActionRegistry.get(key)})}return out.sort((a,b)=>a.path.localeCompare(b.path))}const MIDDLEWARE_TIMEOUT_MS=30000;let _debugLoggingCache;function isDebugLogging(){if(_debugLoggingCache===void 0){const level=(process.env.LOG_LEVEL||"info").toLowerCase();_debugLoggingCache=level!=="info"&&level!=="warn"&&level!=="error"}return _debugLoggingCache}const DEFAULT_MIDDLEWARE_PRIORITY=10,_warnedInvalidPriorities=new Set;function warnInvalidMiddlewarePriority(name,raw){const key=`${name}:${String(raw)}`;if(_warnedInvalidPriorities.has(key))return;_warnedInvalidPriorities.add(key);log.warn(`[Router] Middleware '${name}' declared an invalid priority (${String(raw)}). Priorities must be a finite non-negative number; falling back to default ${DEFAULT_MIDDLEWARE_PRIORITY}.`)}function adaptMiddlewareForBunRouter(middleware){if(middleware instanceof Middleware)return middleware.toRouterHandler();if(middleware&&typeof middleware==="object"&&typeof middleware.handle==="function"&&typeof middleware!=="function"){const handle=middleware.handle.bind(middleware);return async(req,next)=>{try{await handle(req)}catch(thrown){if(thrown instanceof Response)return thrown;throw thrown}return next()}}return middleware}const middlewareCache=new Map;let middlewareAliasesPromise=null;async function getMiddlewareAliases(){if(middlewareAliasesPromise)return middlewareAliasesPromise;middlewareAliasesPromise=(async()=>{const merged={};for(const load of[()=>import(resolveDefaultsPath("app/Middleware.ts")),()=>import(p.appPath("Middleware.ts"))])try{const module=await load();Object.assign(merged,module.default??{})}catch{}return merged})();return middlewareAliasesPromise}const PASCAL_SPLIT_REGEX=/[-_\s]+/,pascalCaseCache=new Map;function toPascalCase(input){if(!input)return input;const cached=pascalCaseCache.get(input);if(cached!==void 0)return cached;const out=input.split(PASCAL_SPLIT_REGEX).filter(Boolean).map((part)=>part.charAt(0).toUpperCase()+part.slice(1)).join("");pascalCaseCache.set(input,out);return out}async function resolveMiddlewareName(name){const resolved=(await getMiddlewareAliases())[name]||toPascalCase(name);log.debug(`[middleware] Resolved: ${name} \u2192 ${resolved}`);return resolved}let middlewareRegistryPromise=null;async function getMiddlewareRegistry(){if(middlewareRegistryPromise)return middlewareRegistryPromise;middlewareRegistryPromise=(async()=>{try{const dir=p.storagePath("framework/auto-imports"),module=await import(`${dir}/middleware.ts`);if(!module.middleware)return null;const{resolve}=await import("node:path");return Object.fromEntries(Object.entries(module.middleware).map(([name,file])=>[name,resolve(dir,file)]))}catch{return null}})();return middlewareRegistryPromise}async function loadMiddleware(name){if(middlewareCache.has(name))return middlewareCache.get(name)??null;const className=await resolveMiddlewareName(name),registered=(await getMiddlewareRegistry())?.[className];if(registered)try{const handler=(await import(registered)).default??null;if(!handler||typeof handler.handle!=="function"){log.error(`[Router] Middleware '${name}' resolved to ${registered}, but the file has no default export with a handle() method`);middlewareCache.set(name,null);return null}middlewareCache.set(name,handler);return handler}catch(err){log.error(`[Router] Failed to load middleware '${name}' from ${registered}:`,err);return null}let userPathError;try{const userPath=p.appPath(`Middleware/${className}.ts`),handler=(await import(userPath)).default??null;if(!handler||typeof handler.handle!=="function"){log.error(`[Router] Middleware '${name}' resolved to ${userPath}, but the file has no default export with a handle() method`);middlewareCache.set(name,null);return null}middlewareCache.set(name,handler);return handler}catch(err){userPathError=err}try{const defaultPath=resolveDefaultsPath(`app/Middleware/${className}.ts`),handler=(await import(defaultPath)).default??null;if(!handler||typeof handler.handle!=="function"){log.error(`[Router] Middleware '${name}' resolved to ${defaultPath}, but the file has no default export with a handle() method`);middlewareCache.set(name,null);return null}middlewareCache.set(name,handler);return handler}catch(err){const userMsg=userPathError instanceof Error?userPathError.message:String(userPathError);log.error(`[Router] Failed to load middleware '${name}' (resolved to '${className}'). app/Middleware: ${userMsg}; defaults:`,err);return null}}export async function middlewareAliases(){return{...await getMiddlewareAliases()}}const negatedMiddlewareCache=new Map;function isShortCircuit(thrown){if(thrown instanceof Response)return!0;return typeof thrown==="object"&&thrown!==null&&(("status"in thrown)||("statusCode"in thrown))}function negateMiddleware(name,inner){const cached=negatedMiddlewareCache.get(name);if(cached)return cached;const negated={priority:inner.priority,async handle(req){try{await inner.handle(req)}catch(thrown){if(isShortCircuit(thrown))return;throw thrown}const{HttpError}=await import("@stacksjs/error-handling");throw new HttpError(403,`Access denied. This route requires "${name}" not to apply.`)}};negatedMiddlewareCache.set(name,negated);return negated}async function loadParsedMiddleware(parsed){const handler=await loadMiddleware(parsed.name);if(!handler||!parsed.negated)return handler;return negateMiddleware(parsed.name,handler)}export function clearMiddlewareCache(){middlewareCache.clear();negatedMiddlewareCache.clear();middlewareAliasesPromise=null;middlewareRegistryPromise=null;actionRegistryPromise=null;actionSkipsCsrfCache.clear();routeHandlerKeyRegistry.clear();routeActionRegistry.clear();clearCsrfModuleCache()}export function installMiddlewareHotReload(){if(process.env.APP_ENV==="production"||process.env.NODE_ENV==="production")return()=>{};let fsWatchers=[];(async()=>{try{const fs=await import("node:fs"),targets=[p.appPath("Middleware"),p.appPath("Middleware.ts")];for(const target of targets)try{if(!fs.existsSync(target))continue;const w=fs.watch(target,{recursive:!0},()=>{log.debug("[middleware] hot-reload: clearing cache");clearMiddlewareCache()});fsWatchers.push(w)}catch{}}catch{}})();return()=>{for(const w of fsWatchers)try{w.close()}catch{}fsWatchers=[]}}const routeMiddlewareRegistry=new Map;export function clearRouteMiddlewareRegistry(){routeMiddlewareRegistry.clear()}const routeApiResponseRegistry=new Set;async function parseMiddlewareEntry(middleware){const negated=middleware.startsWith("!"),bare=negated?middleware.slice(1):middleware,aliases=await getMiddlewareAliases();if(Object.hasOwn(aliases,bare))return{name:bare,negated};const colonIndex=bare.indexOf(":");if(colonIndex===-1)return{name:bare,negated};return{name:bare.substring(0,colonIndex),negated,params:bare.substring(colonIndex+1)}}export async function findUnresolvableRouteMiddleware(){const usage=new Map;for(const[routeKey,entries]of routeMiddlewareRegistry)for(const entry of entries){const parsed=await parseMiddlewareEntry(entry),alias=parsed.negated?`!${parsed.name}`:parsed.name,seen=usage.get(alias)??{parsed,routes:[]};seen.routes.push(routeKey);usage.set(alias,seen)}if(!usage.has("csrf"))usage.set("csrf",{parsed:{name:"csrf",negated:!1},routes:["(auto-injected on POST/PUT/PATCH/DELETE)"]});const unresolvable=[];for(const[alias,{parsed,routes}]of usage){const handler=await loadParsedMiddleware(parsed);if(!handler||typeof handler.handle!=="function")unresolvable.push({alias,routes})}return unresolvable}export async function assertRouteMiddlewareResolvable(){const unresolvable=await findUnresolvableRouteMiddleware();if(unresolvable.length===0)return;const detail=unresolvable.map((u)=>`"${u.alias}" (used by ${u.routes.join(", ")})`).join("; ");throw Error(`[Router] Unresolvable middleware alias(es): ${detail}. Check the alias map in app/Middleware.ts or add app/Middleware/<Class>.ts.`)}function createMiddlewareHandler(routeKey,handler){const wrappedBase=wrapHandler(handler,!0,routeKey),routeMethod=routeKey.slice(0,routeKey.indexOf(":")).toUpperCase(),routeAcceptsCsrf=CSRF_PROTECTED_METHODS.has(routeMethod),forcesJsonByGroup=routeApiResponseRegistry.has(routeKey),handlerKey=typeof handler==="string"?handler:isRouterAction(handler)?routeKey:void 0;let actionPrefetch=null;if(typeof handler==="string"&&routeAcceptsCsrf)actionPrefetch=resolveStringHandler(handler).then(()=>{return}).catch(()=>{return});return async(req)=>{try{await parseRequestBody(req)}catch(err){const error=err instanceof Error?err:Error(String(err));return createMiddlewareErrorResponse(error,req)}const enhancedReq=enhanceRequest(req),renderTokenSeeding=seedCsrfTokenForRender(enhancedReq);if(renderTokenSeeding)await renderTokenSeeding;if(actionPrefetch)await actionPrefetch;if(forcesJsonByGroup)req._forceJson=!0;return runWithRequest(enhancedReq,async()=>{const rl=routeRateLimitRegistry.get(routeKey);if(rl)try{await enforceRateLimit(routeKey,rl.max).over(rl.windowSeconds)}catch(err){return createMiddlewareErrorResponse(err,req)}const userMiddleware=routeMiddlewareRegistry.get(routeKey)||[];let shouldInjectCsrf=!1;if(routeAcceptsCsrf){const alreadyHasCsrf=userMiddleware.some((m)=>m==="csrf"||m.startsWith("csrf:")),routeSkipped=csrfSkipRegistry.has(routeKey),routeRequired=csrfRequireRegistry.has(routeKey),actionSkipped=handlerKey?actionSkipsCsrfCache.get(handlerKey)===!0:!1;shouldInjectCsrf=!alreadyHasCsrf&&(routeRequired||!routeSkipped&&!actionSkipped)}const middlewareEntries=shouldInjectCsrf?["csrf",...userMiddleware]:userMiddleware;if(middlewareEntries.length>0&&isDebugLogging()){const schemeEnd=req.url.indexOf("://"),pathStart=schemeEnd===-1?0:req.url.indexOf("/",schemeEnd+3),q=req.url.indexOf("?",pathStart<0?0:pathStart),urlPath=pathStart<0?"/":req.url.slice(pathStart,q===-1?void 0:q);log.debug(`[middleware] Executing chain: [${middlewareEntries.join(", ")}] for ${routeMethod} ${urlPath}`)}const resolved=[];for(const middlewareEntry of middlewareEntries){const parsed=await parseMiddlewareEntry(middlewareEntry),{name:middlewareName,params}=parsed;if(params){enhancedReq._middlewareParams=enhancedReq._middlewareParams||{};enhancedReq._middlewareParams[middlewareName]=params}const middleware=await loadParsedMiddleware(parsed);if(!middleware||typeof middleware.handle!=="function"){log.error(`[Router] Middleware '${middlewareEntry}' on ${routeKey} could not be resolved - failing closed`);const failClosedError=Error(`Middleware '${middlewareEntry}' could not be resolved`),failClosedResponse=await createErrorResponse(failClosedError,enhancedReq,{status:500});return await applyCorsIfConfigured(enhancedReq,failClosedResponse)}const rawPriority=middleware.priority;let priority=DEFAULT_MIDDLEWARE_PRIORITY;if(typeof rawPriority==="number"&&Number.isFinite(rawPriority)&&rawPriority>=0)priority=rawPriority;else if(rawPriority!==void 0)warnInvalidMiddlewarePriority(middlewareEntry,rawPriority);resolved.push({name:middlewareEntry,handler:middleware,priority})}resolved.sort((a,b)=>a.priority-b.priority);const middlewareTimings=[];let chainTimer,chainBudget,runningMiddleware="";const armChainBudget=()=>{if(!chainBudget){chainBudget=new Promise((_,reject)=>{chainTimer=setTimeout(()=>reject(Error(`Middleware '${runningMiddleware}' exceeded ${MIDDLEWARE_TIMEOUT_MS}ms`)),MIDDLEWARE_TIMEOUT_MS)});chainBudget.catch(()=>{})}return chainBudget};try{for(const{name:middlewareName,handler:middleware}of resolved){const mwStart=process.hrtime.bigint();runningMiddleware=middlewareName;try{const outcome=middleware.handle(enhancedReq);if(outcome&&typeof outcome.then==="function")await Promise.race([outcome,armChainBudget()]);const elapsedMs=Number(process.hrtime.bigint()-mwStart)/1e6;middlewareTimings.push({name:middlewareName,ms:elapsedMs})}catch(error){const elapsedMs=Number(process.hrtime.bigint()-mwStart)/1e6;middlewareTimings.push({name:middlewareName,ms:elapsedMs});log.debug(`[middleware] Blocked by: ${middlewareName}`);if(error instanceof Response){try{const{_requestId:reqId,_startNs:startNs}=enhancedReq,total=startNs!=null?Number(process.hrtime.bigint()-startNs)/1e6:null,parts=total!=null?[`total;dur=${total.toFixed(1)}`]:[];for(const t of middlewareTimings){const safeName=t.name.replace(/[^A-Za-z0-9_-]/g,"_").slice(0,32);parts.push(`mw_${safeName};dur=${t.ms.toFixed(1)}`)}if(parts.length>0)error.headers.set("Server-Timing",parts.join(", "));if(reqId)error.headers.set("X-Request-ID",reqId)}catch{}return await applyCorsIfConfigured(enhancedReq,error)}const err=error instanceof Error?error:Error(String(error)),errorResponse="statusCode"in err||"status"in err?await createMiddlewareErrorResponse(err,enhancedReq):await(()=>{log.error(`[Router] Middleware '${middlewareName}' threw an unexpected error:`,err);return createErrorResponse(err,enhancedReq,{status:500})})();try{const{_requestId:reqId,_startNs:startNs}=enhancedReq,total=startNs!=null?Number(process.hrtime.bigint()-startNs)/1e6:null,parts=total!=null?[`total;dur=${total.toFixed(1)}`]:[];for(const t of middlewareTimings){const safeName=t.name.replace(/[^A-Za-z0-9_-]/g,"_").slice(0,32);parts.push(`mw_${safeName};dur=${t.ms.toFixed(1)}`)}if(parts.length>0)errorResponse.headers.set("Server-Timing",parts.join(", "));if(reqId)errorResponse.headers.set("X-Request-ID",reqId)}catch{}return await applyCorsIfConfigured(enhancedReq,errorResponse)}}}finally{clearTimeout(chainTimer)}let response=await wrappedBase(enhancedReq);clearTrackedQueries();if(response){if(req.method==="GET"||req.method==="HEAD"||req.method==="OPTIONS")try{const mod=loadCsrfModule(),csrf=mod instanceof Promise?await mod:mod;if(csrf)response=csrf.seedCsrfCookieIfMissing(enhancedReq,response,enhancedReq._csrfToken)}catch(err){log.warn("[router] CSRF cookie seeding failed",{error:err})}}if(response)response=await applyCorsIfConfigured(enhancedReq,response);const{_requestId:reqId,_startNs:startNs}=enhancedReq,durMs=startNs!=null?Number(process.hrtime.bigint()-startNs)/1e6:null,setHeaders=(h)=>{const after=enhancedReq._afterResponse;if(Array.isArray(after))for(const callback of after)try{if(typeof callback==="function")callback({status:response?.status??0,durationMs:durMs??0})}catch{}const requested=enhancedReq._responseHeaders;if(requested&&typeof requested==="object"){for(const[name,value]of Object.entries(requested))if(typeof value==="string")h.set(name,value)}if(reqId)h.set("X-Request-ID",reqId);if(durMs!=null){const parts=[`total;dur=${durMs.toFixed(1)}`];for(const t of middlewareTimings){const safeName=t.name.replace(/[^A-Za-z0-9_-]/g,"_").slice(0,32);parts.push(`mw_${safeName};dur=${t.ms.toFixed(1)}`)}h.set("Server-Timing",parts.join(", "))}applySecurityHeaders(h)};if(response&&typeof response.headers?.set==="function"){if(response.status>=400&&(response.headers.get("content-type")||"").includes("json")&&reqId)try{const text=await response.clone().text(),parsed=JSON.parse(text);if(parsed&&typeof parsed==="object"){const newHeaders=new Headers(response.headers);setHeaders(newHeaders);return new Response(JSON.stringify({...parsed,request_id:reqId}),{status:response.status,statusText:response.statusText,headers:newHeaders})}}catch{}try{setHeaders(response.headers)}catch{try{const cloned=response.clone(),newHeaders=new Headers(response.headers);setHeaders(newHeaders);return new Response(cloned.body,{status:response.status,statusText:response.statusText,headers:newHeaders})}catch{}}}if(enhancedReq._compress===!0&&response)try{const{applyCompression}=await import(resolveDefaultsPath("app/Middleware/Compress.ts"));return await applyCompression(enhancedReq,response)}catch(err){log.warn(`[router] Compression failed; sending uncompressed response: ${err instanceof Error?err.message:String(err)}`)}return response})}}function createInertRoute(){const inert={middleware:()=>inert,name:()=>inert,skipCsrf:()=>inert,requireCsrf:()=>inert,rateLimit:()=>inert};return inert}function createChainableRoute(routeKey,shadowed=!1){if(shadowed)return createInertRoute();if(!routeMiddlewareRegistry.has(routeKey))routeMiddlewareRegistry.set(routeKey,[]);const routePath=routeKey.includes(":")?routeKey.substring(routeKey.indexOf(":")+1):routeKey,chain={middleware(name){const middlewareList=routeMiddlewareRegistry.get(routeKey);if(!middlewareList)return chain;for(const entry of Array.isArray(name)?name:[name]){if(typeof entry!=="string")throw TypeError(`[Router] middleware() on ${routeKey} was given a ${typeof entry}; it takes an alias or an array of aliases`);middlewareList.push(entry)}return chain},name(routeName){namedRouteRegistry.set(routeName,compileNamedRoute(routePath));return chain},skipCsrf(){csrfSkipRegistry.add(routeKey);csrfRequireRegistry.delete(routeKey);return chain},requireCsrf(){csrfRequireRegistry.add(routeKey);csrfSkipRegistry.delete(routeKey);return chain},rateLimit(max,window){if(!Number.isFinite(max)||max<=0)throw Error(`[Router] .rateLimit(): max must be a positive number, got ${String(max)}`);const windowSeconds=rateLimitWindowToSeconds(window);routeRateLimitRegistry.set(routeKey,{max:Math.floor(max),windowSeconds});return chain}};return chain}async function fileExists(path){try{return await Bun.file(path).exists()}catch{return!1}}function assertSafeHandlerPath(handlerPath){if(typeof handlerPath!=="string"||handlerPath.length===0)throw Error(`[Router] Refusing to resolve handler '${String(handlerPath)}': empty or non-string`);if(handlerPath.includes("\x00"))throw Error("[Router] Refusing to resolve handler with null byte");if(handlerPath.startsWith("/")||/^[A-Za-z]:[\\/]/.test(handlerPath))throw Error(`[Router] Refusing to resolve absolute handler path '${handlerPath}'`);if(handlerPath.split(/[/\\]/).some((s)=>s===".."))throw Error(`[Router] Refusing to resolve handler path '${handlerPath}' (contains '..' segment)`)}const _moduleImportCache=new Map;function cachedImport(fullPath){let p=_moduleImportCache.get(fullPath);if(!p){p=import(fullPath);_moduleImportCache.set(fullPath,p)}return p}const _resolvedHandlerCache=new Map;function resolveStringHandler(handlerPath){let resolved=_resolvedHandlerCache.get(handlerPath);if(!resolved){resolved=resolveStringHandlerUncached(handlerPath);_resolvedHandlerCache.set(handlerPath,resolved);resolved.catch(()=>_resolvedHandlerCache.delete(handlerPath))}return resolved}function validationFailureResponse(errors){return response.validationError(errors)}export function precognitionRequest(req){const header=req.headers?.get?.("Precognition"),viaHeader=typeof header==="string"&&header.toLowerCase()==="true";let viaQuery=!1;try{viaQuery=new URL(req.url).searchParams.get("_validate")==="1"}catch{viaQuery=!1}if(!viaHeader&&!viaQuery)return null;return{only:(req.headers?.get?.("Precognition-Validate-Only")??"").split(",").map((field)=>field.trim()).filter(Boolean)}}export function precognitionSuccess(){return new Response(null,{status:204,headers:{Precognition:"true","Precognition-Success":"true",Vary:"Precognition, Precognition-Validate-Only"}})}let actionRegistryPromise=null;async function getActionRegistry(){if(actionRegistryPromise)return actionRegistryPromise;actionRegistryPromise=(async()=>{try{const dir=p.storagePath("framework/auto-imports"),module=await import(`${dir}/actions.ts`);if(!module.actions)return null;const{resolve}=await import("node:path");return Object.fromEntries(Object.entries(module.actions).map(([name,file])=>[name,resolve(dir,file)]))}catch{return null}})();return actionRegistryPromise}async function resolveStringHandlerUncached(handlerPath){assertSafeHandlerPath(handlerPath);let modulePath=handlerPath;modulePath=modulePath.endsWith(".ts")?modulePath.slice(0,-3):modulePath;if(modulePath.includes("Controller")){const[controllerPath,methodName="index"]=modulePath.split("@"),userPath=p.appPath(`${controllerPath}.ts`),defaultPath=resolveDefaultsPath(`app/${controllerPath}.ts`),fullPath=await fileExists(userPath)?userPath:defaultPath;try{const controller=await cachedImport(fullPath);if(!controller.default||typeof controller.default!=="function")throw Error(`Controller ${controllerPath} does not export a default class`);const instance=new controller.default;if(typeof instance[methodName]!=="function")throw Error(`Method ${methodName} not found in controller ${controllerPath}`);return async(req)=>{const result=await instance[methodName](req);return formatResult(result,req)}}catch(error){log.error(`[Router] Failed to load controller '${fullPath}':`,error);throw error}}let fullPath;if(modulePath.includes("storage/framework/orm"))fullPath=modulePath;else if(modulePath.includes("OrmAction"))fullPath=p.storagePath(`framework/actions/src/${modulePath}.ts`);else if(modulePath.includes("Actions")){const registered=(await getActionRegistry())?.[modulePath];if(registered)fullPath=registered;else{const userPath=p.projectPath(`app/${modulePath}.ts`),defaultPath=resolveDefaultsPath(`app/${modulePath}.ts`);fullPath=await fileExists(userPath)?userPath:defaultPath}}else{const userPath=p.appPath(`${modulePath}.ts`),defaultPath=resolveDefaultsPath(`app/${modulePath}.ts`);fullPath=await fileExists(userPath)?userPath:defaultPath}try{const action=(await cachedImport(fullPath)).default;if(!action)throw Error(`Action '${handlerPath}' has no default export`);if(typeof action.handle!=="function"){log.error(`[Router] Action '${handlerPath}' structure:`,Object.keys(action));throw Error(`Action '${handlerPath}' has no handle() method. Got: ${typeof action.handle}`)}return wrapAction(action,handlerPath)}catch(importError){log.error(`[Router] Failed to import action '${fullPath}':`,importError);throw importError}}export function isRouterAction(handler){return typeof handler==="object"&&handler!==null&&typeof handler.handle==="function"}export function wrapAction(action,handlerKey){const actionSkipsCsrf=action.skipCsrf===!0||action.csrf===!1;actionSkipsCsrfCache.set(handlerKey,actionSkipsCsrf);const actionForcesJson=action.apiResponse===!0,requestValidationRules=action.validations??modelValidationRules(action.modelDefinition??action.model);return async(req)=>{if(actionSkipsCsrf)req._skipCsrf=!0;if(actionForcesJson)req._forceJson=!0;req._requestValidationRules=requestValidationRules;try{const precognition=precognitionRequest(req);if(precognition){if(!action.validations)return precognitionSuccess();const rules=precognition.only.length>0?Object.fromEntries(Object.entries(action.validations).filter(([field])=>precognition.only.includes(field))):action.validations,precognitionResult=await validateActionInput(req,rules);return precognitionResult.valid?precognitionSuccess():validationFailureResponse(precognitionResult.errors)}if(action.validations){const validationResult=await validateActionInput(req,action.validations);if(!validationResult.valid)return validationFailureResponse(validationResult.errors)}if(typeof action.authorize==="function"){const auth=await action.authorize(req);if(auth instanceof Response)return auth;if(auth===!1)return Response.json({error:"Forbidden"},{status:403})}if(typeof action.before==="function"){const pre=await action.before(req);if(pre instanceof Response)return pre}const result=await action.handle(req);return formatResult(result,req)}catch(handleError){report(handleError,{label:`[Router] action.handle() for '${handlerKey}'`});throw handleError}}}export async function validateActionInput(req,validations){const errors={},input=await getRequestInput(req,validations);for(const[field,validation]of Object.entries(validations)){const value=input[field];let result;try{result=validation.rule.validate(value)}catch{result={valid:!1,errors:[{message:`${field} validation failed`}]}}if(!result.valid){const fieldErrors=[],label=field.replace(/[-_]+/g," ").replace(/([a-z])([A-Z])/g,"$1 $2").replace(/^./,(c)=>c.toUpperCase()),decorate=(msg)=>msg.toLowerCase().startsWith(field.toLowerCase())||msg.includes(label)?msg:`${label} ${msg}`;if(result.errors&&result.errors.length>0)if(validation.message){const firstMessage=result.errors[0]?.message??"";fieldErrors.push(typeof validation.message==="string"?validation.message:validation.message[field]||decorate(firstMessage))}else result.errors.forEach((err)=>fieldErrors.push(decorate(err.message)));else fieldErrors.push(validation.message?typeof validation.message==="string"?validation.message:`${label} is invalid`:`${label} is invalid`);errors[field]=fieldErrors}}const valid=Object.keys(errors).length===0;if(valid){const validated={};for(const field of Object.keys(validations))if(input[field]!==void 0)validated[field]=input[field];req._validatedInput=validated}return{valid,errors}}function modelValidationRules(model){if(!model?.attributes||typeof model.attributes!=="object")return;const rules={};for(const[field,attribute]of Object.entries(model.attributes)){const validation=attribute?.validation;if(validation?.rule)rules[field]=validation}return Object.keys(rules).length>0?rules:void 0}async function getRequestInput(req,validations){const input={},q=req.query;if(q)for(const key in q)input[key]=q[key];else new URL(req.url).searchParams.forEach((value,key)=>{input[key]=value});if(req.params)Object.assign(input,req.params);if(req.jsonBody&&typeof req.jsonBody==="object")Object.assign(input,req.jsonBody);else if(req.formBody&&typeof req.formBody==="object")Object.assign(input,req.formBody);if(typeof req.allFiles==="function")try{const files=req.allFiles();for(const key of Object.keys(files??{}))if(!(key in input))input[key]=files[key]}catch{}if(!validations)return input;for(const[field,validation]of Object.entries(validations)){const value=input[field];if(typeof value!=="string")continue;const validatorName=validation.rule?.name;if(validatorName==="number"){const n=Number(value);if(Number.isFinite(n))input[field]=n}else if(validatorName==="boolean"){if(value==="true"||value==="1")input[field]=!0;else if(value==="false"||value==="0")input[field]=!1}}return input}function formatResult(result,req){if(result instanceof Response)return result;if(result instanceof ReadableStream)return new Response(result,{headers:{"Content-Type":"application/octet-stream"}});const apiShaped=req._forceJson===!0||isApiRequest(req);if(result===null||result===void 0)return apiShaped?new Response(null,{status:204}):new Response("",{status:200});if(typeof result==="object"){const linkHeader=buildPaginatorLinkHeader(result);if(linkHeader)return Response.json(result,{headers:{Link:linkHeader}});return Response.json(result)}if(apiShaped)return Response.json(result);return new Response(String(result),{headers:{"Content-Type":"text/plain; charset=utf-8"}})}function buildPaginatorLinkHeader(value){if(!isPaginator(value)&&!isSimplePaginator(value)&&!isCursorPaginator(value))return null;const v=value,parts=[];if(v.prev_page_url)parts.push(`<${v.prev_page_url}>; rel="prev"`);if(v.next_page_url)parts.push(`<${v.next_page_url}>; rel="next"`);if(v.first_page_url)parts.push(`<${v.first_page_url}>; rel="first"`);if(v.last_page_url)parts.push(`<${v.last_page_url}>; rel="last"`);return parts.length>0?parts.join(", "):null}export function stream(source,options={}){const baseHeaders={};if(options.type==="sse"){baseHeaders["Content-Type"]="text/event-stream; charset=utf-8";baseHeaders["Cache-Control"]="no-cache";baseHeaders.Connection="keep-alive"}else if(options.type==="ndjson")baseHeaders["Content-Type"]="application/x-ndjson; charset=utf-8";else baseHeaders["Content-Type"]=options.contentType??"application/octet-stream";const body=source instanceof ReadableStream?source:new ReadableStream({async start(controller){try{for await(const chunk of source)controller.enqueue(typeof chunk==="string"?new TextEncoder().encode(chunk):chunk);controller.close()}catch(err){controller.error(err)}}}),merged=new Headers(baseHeaders);if(options.headers)new Headers(options.headers).forEach((value,key)=>merged.set(key,value));return new Response(body,{status:options.status??200,headers:merged})}function getAllInputFor(req){const cached=req._allInputCache;if(cached)return cached;const input={},query=req.query;if(query)for(const key in query)input[key]=query[key];if(req.jsonBody&&typeof req.jsonBody==="object")Object.assign(input,req.jsonBody);if(req.formBody&&typeof req.formBody==="object")Object.assign(input,req.formBody);if(req.params&&typeof req.params==="object")Object.assign(input,req.params);req._allInputCache=input;return input}function flashInputFor(req,keys){const input=getAllInputFor(req);req._oldInput=keys?Object.fromEntries(keys.filter((key)=>(key in input)).map((key)=>[key,input[key]])):{...input}}const REQUEST_METHODS={get(key,defaultValue){const value=getAllInputFor(this)[key];return value!==void 0?value:defaultValue},input(key,defaultValue){const value=getAllInputFor(this)[key];return value!==void 0?value:defaultValue},all(){return getAllInputFor(this)},only(keys){const input=getAllInputFor(this),result={};for(const key of keys)if(key in input)result[key]=input[key];return result},except(keys){const result={...getAllInputFor(this)};for(const key of keys)delete result[key];return result},has(key){const input=getAllInputFor(this);if(Array.isArray(key))return key.every((k)=>(k in input)&&input[k]!==void 0);return key in input&&input[key]!==void 0},hasAny(keys){const input=getAllInputFor(this);return keys.some((k)=>(k in input)&&input[k]!==void 0)},filled(key){const input=getAllInputFor(this),isFilled=(k)=>{const value=input[k];return value!==void 0&&value!==null&&value!==""&&!(Array.isArray(value)&&value.length===0)};if(Array.isArray(key))return key.every(isFilled);return isFilled(key)},missing(key){const input=getAllInputFor(this);if(Array.isArray(key))return key.every((k)=>!(k in input)||input[k]===void 0);return!(key in input)||input[key]===void 0},merge(data){Object.assign(getAllInputFor(this),data)},keys(){return Object.keys(getAllInputFor(this))},string(key,defaultValue=""){const value=getAllInputFor(this)[key];return value!==void 0&&value!==null?String(value):defaultValue},integer(key,defaultValue=0){const value=getAllInputFor(this)[key];if(value===void 0||value===null||value==="")return defaultValue;if(typeof value==="number")return Number.isFinite(value)?Math.trunc(value):defaultValue;const str=String(value).trim();if(!/^-?\d+$/.test(str))return defaultValue;const parsed=Number.parseInt(str,10);return Number.isFinite(parsed)?parsed:defaultValue},float(key,defaultValue=0){const value=getAllInputFor(this)[key];if(value===void 0||value===null||value==="")return defaultValue;if(typeof value==="number")return Number.isFinite(value)?value:defaultValue;const str=String(value).trim();if(!/^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(str))return defaultValue;const parsed=Number.parseFloat(str);return Number.isFinite(parsed)?parsed:defaultValue},boolean(key,defaultValue=!1){const value=getAllInputFor(this)[key];if(value===void 0||value===null)return defaultValue;if(typeof value==="boolean")return value;if(value==="true"||value==="1"||value===1)return!0;if(value==="false"||value==="0"||value===0)return!1;return defaultValue},array(key){const value=getAllInputFor(this)[key];if(Array.isArray(value))return value;return value!==void 0&&value!==null?[value]:[]},date(key){const value=getAllInputFor(this)[key];if(value===void 0||value===null||value==="")return null;const parsed=new Date(value);return Number.isNaN(parsed.getTime())?null:parsed},enum(key,enumType){const value=getAllInputFor(this)[key];if(value===void 0||value===null)return null;if(Object.values(enumType).includes(value))return value;const enumKey=String(value);return enumKey in enumType?enumType[enumKey]:null},collect(key){const value=getAllInputFor(this)[key];if(Array.isArray(value))return collect(value);return collect(value===void 0||value===null?[]:[value])},whenHas(key,callback,defaultCallback){const input=getAllInputFor(this);if(key in input&&input[key]!==void 0)callback(input[key]);else defaultCallback?.()},whenFilled(key,callback,defaultCallback){const value=getAllInputFor(this)[key];if(value!==void 0&&value!==null&&value!==""&&!(Array.isArray(value)&&value.length===0))callback(value);else defaultCallback?.()},isValue(key,value){return getAllInputFor(this)[key]===value},async validate(rules,messages={}){const selectedRules=rules??this._requestValidationRules;if(!selectedRules||Object.keys(selectedRules).length===0){const input=getAllInputFor(this);this._validatedInput=input;return input}const normalized={};for(const[field,definition]of Object.entries(selectedRules)){if(typeof definition==="string")throw TypeError(`String validation rules are not supported for "${field}". Use schema validators.`);if(definition&&typeof definition==="object"&&"rule"in definition){const message=messages[field];normalized[field]=message?{...definition,message}:definition}else normalized[field]=definition}const{validate}=await import("@stacksjs/validation"),validated=await validate(this,normalized);this._validatedInput=validated;return validated},getValidated(){return this._validatedInput??{}},safe(){const data=this._validatedInput??{};return{all:()=>({...data}),get:(key,defaultValue)=>(key in data)?data[key]:defaultValue,only:(keys)=>Object.fromEntries(keys.filter((key)=>(key in data)).map((key)=>[key,data[key]])),except:(keys)=>Object.fromEntries(Object.entries(data).filter(([key])=>!keys.includes(key)))}},old(key,defaultValue){return this._oldInput?.[key]??defaultValue},flashInput(keys){flashInputFor(this,keys)},flashInputOnly(keys){flashInputFor(this,keys)},flashInputExcept(keys){const input=getAllInputFor(this);this._oldInput=Object.fromEntries(Object.entries(input).filter(([key])=>!keys.includes(key)))},file(key){const file=(this.files||{})[key];if(!file)return null;const rawFile=Array.isArray(file)?file[0]:file;return rawFile?new UploadedFile(rawFile):null},getFiles(key){const file=(this.files||{})[key];if(!file)return[];return(Array.isArray(file)?file:[file]).map((f)=>new UploadedFile(f))},hasFile(key){const files=this.files||{};return key in files&&files[key]!==void 0},allFiles(){const files=this.files||{},result={};for(const[key,value]of Object.entries(files))if(Array.isArray(value))result[key]=value.map((f)=>new UploadedFile(f));else result[key]=new UploadedFile(value);return result},getParams(){return{...this.params}},isEmpty(){return Object.keys(getAllInputFor(this)).length===0},browser(){return this.headers.get("sec-ch-ua")||this.headers.get("user-agent")},ipForRateLimit(){const ip=this.ip;if(typeof ip==="function")return ip.call(this)||null;return typeof ip==="string"&&ip?ip:null},getMethod(){return this.method.toUpperCase()},async user(){return this._authenticatedUser},async userToken(){return this._currentAccessToken},async tokenCan(ability){if(typeof ability!=="string"||ability.length===0)return!1;const token=this._currentAccessToken;if(!token||typeof token!=="object")return!1;const abilities=token.abilities;if(!Array.isArray(abilities))return!1;if(abilities.includes("*"))return!0;return abilities.includes(ability)},async tokenCant(ability){return!await this.tokenCan(ability)},async can(ability,...args){if(typeof ability!=="string"||ability.length===0)return!1;const{Gate}=await import("@stacksjs/auth"),user=this._authenticatedUser??null;return Gate.allows(ability,user,...args)},async cannot(ability,...args){return!await this.can(ability,...args)},async authorize(ability,...args){const{Gate}=await import("@stacksjs/auth"),user=this._authenticatedUser??null;await Gate.authorize(ability,user,...args)}};let csrfModule,csrfModuleLoad;function loadCsrfModule(){if(csrfModule!==void 0)return csrfModule;csrfModuleLoad??=import(resolveDefaultsPath("app/Middleware/Csrf.ts")).then((mod)=>{csrfModule=mod;return csrfModule}).catch(()=>{csrfModule=null;return null});return csrfModuleLoad}export function clearCsrfModuleCache(){csrfModule=void 0;csrfModuleLoad=void 0}function applyCsrfRenderToken(req,cookieHeader,mod){const token=mod.generateCsrfToken();req._csrfToken=token;try{const merged=cookieHeader?`${cookieHeader}; ${mod.CSRF_COOKIE_NAME}=${token}`:`${mod.CSRF_COOKIE_NAME}=${token}`;req.headers.set("cookie",merged)}catch{}}function seedCsrfTokenForRender(req){const method=req.method?.toUpperCase?.()??"GET";if(method!=="GET"&&method!=="HEAD")return;const cookieHeader=req.headers?.get?.("cookie")??"";if(cookieHeader.includes("X-CSRF-Token=")||cookieHeader.includes("csrf-token="))return;const mod=loadCsrfModule();if(mod===null)return;if(mod instanceof Promise)return mod.then((resolved)=>{if(resolved)applyCsrfRenderToken(req,cookieHeader,resolved)});applyCsrfRenderToken(req,cookieHeader,mod)}export function enhanceRequest(req){const routeParams=req.params??{};req.params=routeParams;applyRequestEnhancements(req,routeParams);if(!req._requestId)req._requestId=incomingRequestId(req)??crypto.randomUUID();Object.assign(req,REQUEST_METHODS);return req}function incomingRequestId(req){try{const supplied=req.headers?.get?.("x-request-id")?.trim();if(supplied&&/^[\w.:-]{8,200}$/.test(supplied))return supplied}catch{}return}function wrapHandler(handler,skipParsing=!1,handlerKey=""){if(isRouterAction(handler))return wrapAction(handler,handlerKey);if(typeof handler==="string"){const handlerPath=handler;return async(req)=>{try{if(!skipParsing){await parseRequestBody(req);req=enhanceRequest(req)}return await(await resolveStringHandler(handlerPath))(req)}catch(error){report(error,{label:`[Router] ${handlerPath}`});const rawStatus=error?.statusCode??error?.status,status=typeof rawStatus==="number"&&Number.isInteger(rawStatus)&&rawStatus>=400&&rawStatus<600?rawStatus:void 0;return await createErrorResponse(error instanceof Error?error:Error(String(error)),req,{handlerPath,status})}}}const fn=handler;return async(req)=>{const result=await fn(req);return formatResult(result,req)}}async function readJsonBodyOnce(req,contentType){const raw=await req.text();req._rawBody=raw;const target=req;target.text=()=>Promise.resolve(raw);target.json=()=>{try{return Promise.resolve(raw.length===0?null:JSON.parse(raw))}catch(err){return Promise.reject(err)}};target.bytes=()=>Promise.resolve(new TextEncoder().encode(raw));target.arrayBuffer=()=>{const bytes=new TextEncoder().encode(raw);return Promise.resolve(bytes.buffer.slice(bytes.byteOffset,bytes.byteOffset+bytes.byteLength))};target.blob=()=>Promise.resolve(new Blob([raw],{type:contentType}));target.clone=()=>new Request(req.url,{method:req.method,headers:req.headers,body:raw});return raw}async function parseRequestBody(req){if(req._bodyParsed)return;req._bodyParsed=!0;const contentType=req.headers.get("content-type")||"";try{if(JSON_CONTENT_TYPE.test(contentType)){const raw=await readJsonBodyOnce(req,contentType);if(raw.length===0)req.jsonBody={};else try{const body=JSON.parse(raw);req.jsonBody=body&&typeof body==="object"?body:{}}catch(parseErr){const message=parseErr instanceof Error?parseErr.message:"Invalid JSON",{HttpError}=await import("@stacksjs/error-handling");throw new HttpError(400,`Invalid JSON body: ${message}`)}}else if(contentType.includes("application/x-www-form-urlencoded")){const text=await req.clone().text(),params=new URLSearchParams(text),formBody={};params.forEach((value,key)=>{formBody[key]=value});req.formBody=formBody}else if(contentType.includes("multipart/form-data")){const formData=await req.clone().formData(),formBody={},files={};formData.forEach((value,key)=>{if(value instanceof File)if(files[key])if(Array.isArray(files[key]))files[key].push(value);else files[key]=[files[key],value];else files[key]=value;else formBody[key]=value});Reflect.set(req,"formBody",formBody);Reflect.set(req,"files",files)}}catch(e){if(typeof(e?.status??e?.statusCode)==="number")throw e;log.debug("[stacks-router] Body parsing failed:",e)}}export function createStacksRouter(config={}){const bunRouter=new Router({verbose:config.verbose??!1});let currentPrefix="",currentGroupMiddleware=[],currentGroupApiResponse=!1;const registeredRouteKeys=new Set;function registerRoute(method,path,_handler){const fullPath=currentPrefix+path,routeKey=`${method}:${fullPath}`;log.debug(`[router] ${method} ${fullPath} \u2192 ${typeof _handler==="string"?_handler:"function"}`);const shadowed=registeredRouteKeys.has(routeKey);if(!shadowed)registeredRouteKeys.add(routeKey);if(!shadowed&¤tGroupMiddleware.length>0)routeMiddlewareRegistry.set(routeKey,[...currentGroupMiddleware]);if(!shadowed&¤tGroupApiResponse)routeApiResponseRegistry.add(routeKey);if(!shadowed&&typeof _handler==="string")routeHandlerKeyRegistry.set(routeKey,_handler);if(!shadowed&&isRouterAction(_handler))routeActionRegistry.set(routeKey,_handler);return{fullPath,routeKey,shadowed}}const stacksRouter={bunRouter,get routes(){return bunRouter.routes},get(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("GET",path,handler);bunRouter.get(fullPath,createMiddlewareHandler(routeKey,handler));return createChainableRoute(routeKey,shadowed)},post(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("POST",path,handler);bunRouter.post(fullPath,createMiddlewareHandler(routeKey,handler));return createChainableRoute(routeKey,shadowed)},put(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("PUT",path,handler);bunRouter.put(fullPath,createMiddlewareHandler(routeKey,handler));return createChainableRoute(routeKey,shadowed)},patch(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("PATCH",path,handler);bunRouter.patch(fullPath,createMiddlewareHandler(routeKey,handler));return createChainableRoute(routeKey,shadowed)},delete(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("DELETE",path,handler);bunRouter.delete(fullPath,createMiddlewareHandler(routeKey,handler));return createChainableRoute(routeKey,shadowed)},options(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("OPTIONS",path,handler);bunRouter.options(fullPath,createMiddlewareHandler(routeKey,handler));return createChainableRoute(routeKey,shadowed)},group(options,callback){const previousPrefix=currentPrefix,previousMiddleware=[...currentGroupMiddleware],previousApiResponse=currentGroupApiResponse;if(options.prefix)currentPrefix=previousPrefix+options.prefix;const middlewareList=options.middleware?Array.isArray(options.middleware)?options.middleware:[options.middleware]:void 0;if(middlewareList)currentGroupMiddleware=[...currentGroupMiddleware,...middlewareList];if(options.apiResponse===!0)currentGroupApiResponse=!0;log.debug(`[router] Entering group: prefix=${options.prefix||"/"} middleware=[${middlewareList?.join(", ")||""}]${currentGroupApiResponse?" apiResponse=true":""}`);const result=callback();if(result instanceof Promise)return result.then(()=>{currentPrefix=previousPrefix;currentGroupMiddleware=previousMiddleware;currentGroupApiResponse=previousApiResponse;return stacksRouter}).catch((err)=>{currentPrefix=previousPrefix;currentGroupMiddleware=previousMiddleware;currentGroupApiResponse=previousApiResponse;throw err});currentPrefix=previousPrefix;currentGroupMiddleware=previousMiddleware;currentGroupApiResponse=previousApiResponse;return stacksRouter},resource(name,handler,options){const actions=["index","store","show","update","destroy"],activeActions=options?.only?actions.filter((a)=>options.only.includes(a)):options?.except?actions.filter((a)=>!options.except.includes(a)):actions,stripped=handler.replace(/Action$/,""),handlerBase=stripped.startsWith("Actions/")?stripped:`Actions/${stripped}`;log.debug(`[router] Resource: /${name} \u2192 ${handlerBase}*Action [${activeActions.join(", ")}]`);const sibling=(suffix)=>`${handlerBase}${suffix}`,registerResourceRoutes=()=>{for(const action of activeActions)switch(action){case"index":stacksRouter.get(`/${name}`,sibling("IndexAction"));break;case"store":stacksRouter.post(`/${name}`,sibling("StoreAction"));break;case"show":stacksRouter.get(`/${name}/:id`,sibling("ShowAction"));break;case"update":stacksRouter.put(`/${name}/:id`,sibling("UpdateAction"));break;case"destroy":stacksRouter.delete(`/${name}/:id`,sibling("DestroyAction"));break}};if(options?.middleware)stacksRouter.group({middleware:options.middleware},registerResourceRoutes);else registerResourceRoutes();return stacksRouter},match(methods,path,handler){log.debug(`[router] Match: [${methods.join(", ")}] ${path} \u2192 ${typeof handler==="string"?handler:"function"}`);let firstShadowed=!1;for(const[index,method]of methods.entries()){const m=method.toUpperCase(),{fullPath,routeKey,shadowed}=registerRoute(m,path,handler);if(index===0)firstShadowed=shadowed;const wrappedHandler=createMiddlewareHandler(routeKey,handler);switch(m){case"GET":bunRouter.get(fullPath,wrappedHandler);break;case"POST":bunRouter.post(fullPath,wrappedHandler);break;case"PUT":bunRouter.put(fullPath,wrappedHandler);break;case"PATCH":bunRouter.patch(fullPath,wrappedHandler);break;case"DELETE":bunRouter.delete(fullPath,wrappedHandler);break;case"OPTIONS":bunRouter.options(fullPath,wrappedHandler);break}}return createChainableRoute(`${methods[0]}:${currentPrefix}${path}`,firstShadowed)},health(){bunRouter.get("/api/health",async()=>{const health=await checkApplicationHealth();return Response.json(health,{status:health.status==="healthy"?200:503})});bunRouter.get("/__routes",(req)=>{if(!isExposeRoutesAuthorized(req))return Response.json({error:"disabled"},{status:404});return Response.json(listRegisteredRoutes())});bunRouter.get("/__storage/:path",async(req)=>{const url=new URL(req.url),token=url.searchParams.get("token"),params=req.params,rawPath=params?.path?decodeURIComponent(params.path):decodeURIComponent(url.pathname.replace(/^\/__storage\//,""));if(!token||typeof rawPath!=="string"||rawPath.length===0)return new Response("Forbidden",{status:403});const{verifySignedStorageToken,Storage}=await import("@stacksjs/storage");if(!verifySignedStorageToken(token,rawPath).valid)return new Response("Forbidden",{status:403});try{const adapter=Storage.disk();if(!await adapter.fileExists(rawPath))return new Response("Not Found",{status:404});const buf=await adapter.readToBuffer(rawPath),mime=await adapter.mimeType(rawPath).catch(()=>"application/octet-stream");return new Response(buf,{status:200,headers:{"Content-Type":mime,"Cache-Control":"private, max-age=60","X-Content-Type-Options":"nosniff"}})}catch(err){log.error("[storage] signed-url fetch failed:",err);return new Response("Internal Error",{status:500})}});bunRouter.get("/__openapi.json",async(req)=>{if(!isExposeRoutesAuthorized(req))return Response.json({error:"disabled"},{status:404});try{const{generateOpenApi}=await import("@stacksjs/api"),spec=await generateOpenApi({write:!1,portable:!1});return Response.json(spec)}catch(err){return Response.json({error:"OpenAPI generation failed",message:err instanceof Error?err.message:String(err)},{status:500})}});return stacksRouter},use(middleware){const adapted=adaptMiddlewareForBunRouter(middleware);bunRouter.globalMiddleware.push(adapted);return stacksRouter},booting(name,run){bootHooks.push({name,run});return stacksRouter},async serve(options={}){warnOnMultipleRouterInstances();configureViewDirectories(bunRouter);wrapHandleRequestForCsrf(bunRouter);await runBootHooks();return bunRouter.serve(options)},async handleRequest(req){return bunRouter.handleRequest(req)},getAllowedMethods(pathname,domain){return bunRouter.getAllowedMethods(pathname,domain)},async register(routePath,options){log.debug(`[router] Register: ${routePath} prefix=${options?.prefix||"none"}`);const callback=async()=>{await import(routePath)};if(options?.prefix||options?.middleware)await stacksRouter.group({prefix:options.prefix,middleware:options.middleware},callback);else await callback();return stacksRouter},async importRoutes(){log.debug("[router] Loading user routes from registry...");try{const{loadRoutes}=await import("./route-loader"),{appPath}=await import("@stacksjs/path"),routeRegistry=(await import(appPath("Routes.ts"))).default;await loadRoutes(routeRegistry)}catch(error){log.error("Failed to load route registry:",error);throw error}log.debug("[router] Loading ORM routes...");const ormRoutesPackage="@stacksjs/orm/routes";let ormRoutesLoaded=!1;try{await import(ormRoutesPackage);ormRoutesLoaded=!0;log.debug(`[router] ORM routes loaded from ${ormRoutesPackage}`)}catch(error){log.debug(`[router] ORM routes not available from the package, trying the vendored copy
|
|
4
|
+
${paths}`)}return!0}let __defaultsPkgRoot;const __defaultsPathCache=new Map;function resolveDefaultsPath(rel){const cached=__defaultsPathCache.get(rel);if(cached!==void 0)return cached;if(rel.startsWith("app/")){const published=p.appPath(rel.slice(4));if(existsSync(published)){__defaultsPathCache.set(rel,published);return published}}const vendored=p.storagePath(`framework/defaults/${rel}`);let resolved;if(existsSync(vendored))resolved=vendored;else{if(__defaultsPkgRoot===void 0)try{const pkgJson=Bun.resolveSync("@stacksjs/defaults/package.json",process.cwd());__defaultsPkgRoot=pkgJson.slice(0,pkgJson.lastIndexOf("/"))}catch{__defaultsPkgRoot=null}resolved=__defaultsPkgRoot?`${__defaultsPkgRoot}/${rel}`:vendored}__defaultsPathCache.set(rel,resolved);return resolved}import{runWithRequest}from"./request-context";import{isApiRequest,JSON_CONTENT_TYPE}from"./api-shape";import{clearTrackedQueries,createErrorResponse,createMiddlewareErrorResponse}from"./error-handler";import{applySecurityHeaders}from"./security-headers";import{isCursorPaginator,isPaginator,isSimplePaginator}from"@stacksjs/pagination";const csrfSkipRegistry=new Set,csrfRequireRegistry=new Set,routeRateLimitRegistry=new Map;function rateLimitWindowToSeconds(window){if(typeof window==="number"){if(!Number.isFinite(window)||window<=0)throw Error(`[Router] .rateLimit(): window must be a positive number of seconds, got ${window}`);return Math.floor(window)}switch(window){case"second":return 1;case"minute":return 60;case"hour":return 3600;case"day":return 86400;default:throw Error(`[Router] .rateLimit(): unknown period '${String(window)}'`)}}class BoundedMap{max;map=new Map;constructor(max){this.max=max}get(key){return this.map.get(key)}has(key){return this.map.has(key)}set(key,value){if(this.map.has(key))this.map.delete(key);this.map.set(key,value);if(this.map.size>this.max){const oldest=this.map.keys().next().value;if(oldest!==void 0)this.map.delete(oldest)}return this}delete(key){return this.map.delete(key)}clear(){this.map.clear()}get size(){return this.map.size}}function isExposeRoutesAuthorized(req){const flag=process.env.STACKS_EXPOSE_ROUTES??"";if(!flag)return!((process.env.APP_ENV??"").toLowerCase()==="production"||process.env.NODE_ENV==="production");if(flag==="1")return!((process.env.APP_ENV??"").toLowerCase()==="production"||process.env.NODE_ENV==="production");const url=new URL(req.url),submitted=req.headers.get("x-stacks-routes-token")||req.headers.get("X-Stacks-Routes-Token")||url.searchParams.get("token")||"";if(typeof submitted!=="string"||submitted.length===0||submitted.length!==flag.length)return!1;try{return timingSafeEqual(Buffer.from(submitted),Buffer.from(flag))}catch{return!1}}async function applyCorsIfConfigured(req,response){if(!req._corsConfig||!response)return response;try{const{applyCorsHeaders}=await import(resolveDefaultsPath("app/Middleware/Cors.ts"));return applyCorsHeaders(req,response,req._corsConfig)}catch(err){log.warn("[router] CORS header injection failed",{error:err});return response}}const ACTION_CACHE_MAX=5000,actionSkipsCsrfCache=new BoundedMap(ACTION_CACHE_MAX),routeHandlerKeyRegistry=new BoundedMap(ACTION_CACHE_MAX),routeActionRegistry=new Map,CSRF_PROTECTED_METHODS=new Set(["POST","PUT","PATCH","DELETE"]),CSRF_SEEDED_BY_HANDLE_REQUEST=Symbol.for("stacks.router.csrfSeededByHandleRequest"),EMPTY_RESOLVED_MIDDLEWARE=[],EMPTY_MIDDLEWARE_TIMINGS=[],namedRouteRegistry=new Map;function compileNamedRoute(path){const paramNames=extractRouteParamNames(path),colonRegex=new Map;for(const name of paramNames)colonRegex.set(name,new RegExp(`(^|/):${name}(?=$|/)`,"g"));return{path,paramNames,colonRegex}}function extractRouteParamNames(routePath){const names=new Set;for(const m of routePath.matchAll(/\{(\w+)\}/g))if(m[1])names.add(m[1]);for(const m of routePath.matchAll(/(?:^|\/):(\w+)(?=$|\/)/g))if(m[1])names.add(m[1]);return[...names]}export function url(routeName,params={}){const named=namedRouteRegistry.get(routeName);if(!named)throw Error(`Route '${routeName}' is not defined. Available routes: ${[...namedRouteRegistry.keys()].join(", ")}`);const missing=named.paramNames.filter((name)=>!(name in params)||params[name]===void 0);if(missing.length>0)throw Error(`url('${routeName}'): missing required path param${missing.length>1?"s":""} [${missing.join(", ")}] for path '${named.path}'. Pass them as the second argument: url('${routeName}', { ${named.paramNames.join(", ")} })`);let appUrl;try{appUrl=process.env.APP_URL||"https://localhost"}catch{appUrl="https://localhost"}appUrl=appUrl.replace(/\/$/,"");if(!appUrl.startsWith("http"))appUrl=`https://${appUrl}`;let resolvedPath=named.path;const queryParams={};for(const[key,value]of Object.entries(params)){const curly=`{${key}}`;if(resolvedPath.includes(curly))resolvedPath=resolvedPath.replaceAll(curly,encodeURIComponent(String(value)));else{const re=named.colonRegex.get(key);if(re&&re.test(resolvedPath)){re.lastIndex=0;resolvedPath=resolvedPath.replace(re,`$1${encodeURIComponent(String(value))}`)}else queryParams[key]=String(value)}}const queryString=Object.keys(queryParams).length>0?`?${new URLSearchParams(queryParams).toString()}`:"";return`${appUrl}${resolvedPath}${queryString}`}export function routeParams(routeName){const named=namedRouteRegistry.get(routeName);return named?[...named.paramNames]:[]}export function listNamedRoutes(){const out={};for(const[name,named]of namedRouteRegistry.entries())out[name]=named.path;return out}export function listRegisteredRoutes(){const out=[],seen=new Set;for(const key of routeMiddlewareRegistry.keys()){if(seen.has(key))continue;seen.add(key);const idx=key.indexOf(":");if(idx===-1)continue;const method=key.slice(0,idx),path=key.slice(idx+1);let routeName;for(const[n,named]of namedRouteRegistry.entries())if(named.path===path){routeName=n;break}out.push({method,path,name:routeName,handler:routeHandlerKeyRegistry.get(key),action:routeActionRegistry.get(key)})}return out.sort((a,b)=>a.path.localeCompare(b.path))}const MIDDLEWARE_TIMEOUT_MS=30000;let _debugLoggingCache;function isDebugLogging(){if(_debugLoggingCache===void 0){const level=(process.env.LOG_LEVEL||"info").toLowerCase();_debugLoggingCache=level!=="info"&&level!=="warn"&&level!=="error"}return _debugLoggingCache}const DEFAULT_MIDDLEWARE_PRIORITY=10,_warnedInvalidPriorities=new Set;function warnInvalidMiddlewarePriority(name,raw){const key=`${name}:${String(raw)}`;if(_warnedInvalidPriorities.has(key))return;_warnedInvalidPriorities.add(key);log.warn(`[Router] Middleware '${name}' declared an invalid priority (${String(raw)}). Priorities must be a finite non-negative number; falling back to default ${DEFAULT_MIDDLEWARE_PRIORITY}.`)}function adaptMiddlewareForBunRouter(middleware){if(middleware instanceof Middleware)return middleware.toRouterHandler();if(middleware&&typeof middleware==="object"&&typeof middleware.handle==="function"&&typeof middleware!=="function"){const handle=middleware.handle.bind(middleware);return async(req,next)=>{try{await handle(req)}catch(thrown){if(thrown instanceof Response)return thrown;throw thrown}return next()}}return middleware}const middlewareCache=new Map;let middlewareAliasesPromise=null;async function getMiddlewareAliases(){if(middlewareAliasesPromise)return middlewareAliasesPromise;middlewareAliasesPromise=(async()=>{const merged={};for(const load of[()=>import(resolveDefaultsPath("app/Middleware.ts")),()=>import(p.appPath("Middleware.ts"))])try{const module=await load();Object.assign(merged,module.default??{})}catch{}return merged})();return middlewareAliasesPromise}const PASCAL_SPLIT_REGEX=/[-_\s]+/,pascalCaseCache=new Map;function toPascalCase(input){if(!input)return input;const cached=pascalCaseCache.get(input);if(cached!==void 0)return cached;const out=input.split(PASCAL_SPLIT_REGEX).filter(Boolean).map((part)=>part.charAt(0).toUpperCase()+part.slice(1)).join("");pascalCaseCache.set(input,out);return out}async function resolveMiddlewareName(name){const resolved=(await getMiddlewareAliases())[name]||toPascalCase(name);log.debug(`[middleware] Resolved: ${name} \u2192 ${resolved}`);return resolved}let middlewareRegistryPromise=null;async function getMiddlewareRegistry(){if(middlewareRegistryPromise)return middlewareRegistryPromise;middlewareRegistryPromise=(async()=>{try{const dir=p.storagePath("framework/auto-imports"),module=await import(`${dir}/middleware.ts`);if(!module.middleware)return null;const{resolve}=await import("node:path");return Object.fromEntries(Object.entries(module.middleware).map(([name,file])=>[name,resolve(dir,file)]))}catch{return null}})();return middlewareRegistryPromise}async function loadMiddleware(name){if(middlewareCache.has(name))return middlewareCache.get(name)??null;const className=await resolveMiddlewareName(name),registered=(await getMiddlewareRegistry())?.[className];if(registered)try{const handler=(await import(registered)).default??null;if(!handler||typeof handler.handle!=="function"){log.error(`[Router] Middleware '${name}' resolved to ${registered}, but the file has no default export with a handle() method`);middlewareCache.set(name,null);return null}middlewareCache.set(name,handler);return handler}catch(err){log.error(`[Router] Failed to load middleware '${name}' from ${registered}:`,err);return null}let userPathError;try{const userPath=p.appPath(`Middleware/${className}.ts`),handler=(await import(userPath)).default??null;if(!handler||typeof handler.handle!=="function"){log.error(`[Router] Middleware '${name}' resolved to ${userPath}, but the file has no default export with a handle() method`);middlewareCache.set(name,null);return null}middlewareCache.set(name,handler);return handler}catch(err){userPathError=err}try{const defaultPath=resolveDefaultsPath(`app/Middleware/${className}.ts`),handler=(await import(defaultPath)).default??null;if(!handler||typeof handler.handle!=="function"){log.error(`[Router] Middleware '${name}' resolved to ${defaultPath}, but the file has no default export with a handle() method`);middlewareCache.set(name,null);return null}middlewareCache.set(name,handler);return handler}catch(err){const userMsg=userPathError instanceof Error?userPathError.message:String(userPathError);log.error(`[Router] Failed to load middleware '${name}' (resolved to '${className}'). app/Middleware: ${userMsg}; defaults:`,err);return null}}export async function middlewareAliases(){return{...await getMiddlewareAliases()}}const negatedMiddlewareCache=new Map;function isShortCircuit(thrown){if(thrown instanceof Response)return!0;return typeof thrown==="object"&&thrown!==null&&(("status"in thrown)||("statusCode"in thrown))}function negateMiddleware(name,inner){const cached=negatedMiddlewareCache.get(name);if(cached)return cached;const negated={priority:inner.priority,async handle(req){try{await inner.handle(req)}catch(thrown){if(isShortCircuit(thrown))return;throw thrown}const{HttpError}=await import("@stacksjs/error-handling");throw new HttpError(403,`Access denied. This route requires "${name}" not to apply.`)}};negatedMiddlewareCache.set(name,negated);return negated}async function loadParsedMiddleware(parsed){const handler=await loadMiddleware(parsed.name);if(!handler||!parsed.negated)return handler;return negateMiddleware(parsed.name,handler)}export function clearMiddlewareCache(){middlewareCache.clear();negatedMiddlewareCache.clear();middlewareAliasesPromise=null;middlewareRegistryPromise=null;actionRegistryPromise=null;actionSkipsCsrfCache.clear();routeHandlerKeyRegistry.clear();routeActionRegistry.clear();clearCsrfModuleCache()}export function installMiddlewareHotReload(){if(process.env.APP_ENV==="production"||process.env.NODE_ENV==="production")return()=>{};let fsWatchers=[];(async()=>{try{const fs=await import("node:fs"),targets=[p.appPath("Middleware"),p.appPath("Middleware.ts")];for(const target of targets)try{if(!fs.existsSync(target))continue;const w=fs.watch(target,{recursive:!0},()=>{log.debug("[middleware] hot-reload: clearing cache");clearMiddlewareCache()});fsWatchers.push(w)}catch{}}catch{}})();return()=>{for(const w of fsWatchers)try{w.close()}catch{}fsWatchers=[]}}const routeMiddlewareRegistry=new Map;export function clearRouteMiddlewareRegistry(){routeMiddlewareRegistry.clear()}const routeApiResponseRegistry=new Set;async function parseMiddlewareEntry(middleware){const negated=middleware.startsWith("!"),bare=negated?middleware.slice(1):middleware,aliases=await getMiddlewareAliases();if(Object.hasOwn(aliases,bare))return{name:bare,negated};const colonIndex=bare.indexOf(":");if(colonIndex===-1)return{name:bare,negated};return{name:bare.substring(0,colonIndex),negated,params:bare.substring(colonIndex+1)}}export async function findUnresolvableRouteMiddleware(){const usage=new Map;for(const[routeKey,entries]of routeMiddlewareRegistry)for(const entry of entries){const parsed=await parseMiddlewareEntry(entry),alias=parsed.negated?`!${parsed.name}`:parsed.name,seen=usage.get(alias)??{parsed,routes:[]};seen.routes.push(routeKey);usage.set(alias,seen)}if(!usage.has("csrf"))usage.set("csrf",{parsed:{name:"csrf",negated:!1},routes:["(auto-injected on POST/PUT/PATCH/DELETE)"]});const unresolvable=[];for(const[alias,{parsed,routes}]of usage){const handler=await loadParsedMiddleware(parsed);if(!handler||typeof handler.handle!=="function")unresolvable.push({alias,routes})}return unresolvable}export async function assertRouteMiddlewareResolvable(){const unresolvable=await findUnresolvableRouteMiddleware();if(unresolvable.length===0)return;const detail=unresolvable.map((u)=>`"${u.alias}" (used by ${u.routes.join(", ")})`).join("; ");throw Error(`[Router] Unresolvable middleware alias(es): ${detail}. Check the alias map in app/Middleware.ts or add app/Middleware/<Class>.ts.`)}function createMiddlewareHandler(routeKey,handler){const wrappedBase=wrapHandler(handler,!0,routeKey),routeMethod=routeKey.slice(0,routeKey.indexOf(":")).toUpperCase(),routeAcceptsCsrf=CSRF_PROTECTED_METHODS.has(routeMethod),routeMayHaveBody=routeMethod!=="GET"&&routeMethod!=="HEAD",routeSeedsCsrf=routeMethod==="GET"||routeMethod==="HEAD"||routeMethod==="OPTIONS",forcesJsonByGroup=routeApiResponseRegistry.has(routeKey),handlerKey=typeof handler==="string"?handler:isRouterAction(handler)?routeKey:void 0;let actionPrefetch=null;if(typeof handler==="string"&&routeAcceptsCsrf)actionPrefetch=resolveStringHandler(handler).then(()=>{return}).catch(()=>{return});return async(req)=>{if(routeMayHaveBody)try{await parseRequestBody(req)}catch(err){const error=err instanceof Error?err:Error(String(err));return createMiddlewareErrorResponse(error,req)}const enhancedReq=enhanceRequest(req),csrfHandledByOuter=enhancedReq[CSRF_SEEDED_BY_HANDLE_REQUEST]===!0;if(!csrfHandledByOuter){const renderTokenSeeding=seedCsrfTokenForRender(enhancedReq);if(renderTokenSeeding)await renderTokenSeeding}if(actionPrefetch)await actionPrefetch;if(forcesJsonByGroup)req._forceJson=!0;return runWithRequest(enhancedReq,async()=>{const rl=routeRateLimitRegistry.get(routeKey);if(rl)try{const{rateLimit:enforceRateLimit}=await import("./rate-limit");await enforceRateLimit(routeKey,rl.max).over(rl.windowSeconds)}catch(err){return createMiddlewareErrorResponse(err,req)}const userMiddleware=routeMiddlewareRegistry.get(routeKey)||[];let shouldInjectCsrf=!1;if(routeAcceptsCsrf){const alreadyHasCsrf=userMiddleware.some((m)=>m==="csrf"||m.startsWith("csrf:")),routeSkipped=csrfSkipRegistry.has(routeKey),routeRequired=csrfRequireRegistry.has(routeKey),actionSkipped=handlerKey?actionSkipsCsrfCache.get(handlerKey)===!0:!1;shouldInjectCsrf=!alreadyHasCsrf&&(routeRequired||!routeSkipped&&!actionSkipped)}const middlewareEntries=shouldInjectCsrf?["csrf",...userMiddleware]:userMiddleware;if(middlewareEntries.length>0&&isDebugLogging()){const schemeEnd=req.url.indexOf("://"),pathStart=schemeEnd===-1?0:req.url.indexOf("/",schemeEnd+3),q=req.url.indexOf("?",pathStart<0?0:pathStart),urlPath=pathStart<0?"/":req.url.slice(pathStart,q===-1?void 0:q);log.debug(`[middleware] Executing chain: [${middlewareEntries.join(", ")}] for ${routeMethod} ${urlPath}`)}const resolved=middlewareEntries.length===0?EMPTY_RESOLVED_MIDDLEWARE:[];for(const middlewareEntry of middlewareEntries){const parsed=await parseMiddlewareEntry(middlewareEntry),{name:middlewareName,params}=parsed;if(params){enhancedReq._middlewareParams=enhancedReq._middlewareParams||{};enhancedReq._middlewareParams[middlewareName]=params}const middleware=await loadParsedMiddleware(parsed);if(!middleware||typeof middleware.handle!=="function"){log.error(`[Router] Middleware '${middlewareEntry}' on ${routeKey} could not be resolved - failing closed`);const failClosedError=Error(`Middleware '${middlewareEntry}' could not be resolved`),failClosedResponse=await createErrorResponse(failClosedError,enhancedReq,{status:500});return await applyCorsIfConfigured(enhancedReq,failClosedResponse)}const rawPriority=middleware.priority;let priority=DEFAULT_MIDDLEWARE_PRIORITY;if(typeof rawPriority==="number"&&Number.isFinite(rawPriority)&&rawPriority>=0)priority=rawPriority;else if(rawPriority!==void 0)warnInvalidMiddlewarePriority(middlewareEntry,rawPriority);resolved.push({name:middlewareEntry,handler:middleware,priority})}if(resolved.length>1)resolved.sort((a,b)=>a.priority-b.priority);const middlewareTimings=resolved.length===0?EMPTY_MIDDLEWARE_TIMINGS:[];let chainTimer,chainBudget,runningMiddleware="",armChainBudget;if(resolved.length>0)armChainBudget=()=>{if(!chainBudget){chainBudget=new Promise((_,reject)=>{chainTimer=setTimeout(()=>reject(Error(`Middleware '${runningMiddleware}' exceeded ${MIDDLEWARE_TIMEOUT_MS}ms`)),MIDDLEWARE_TIMEOUT_MS)});chainBudget.catch(()=>{})}return chainBudget};try{for(const{name:middlewareName,handler:middleware}of resolved){const mwStart=process.hrtime.bigint();runningMiddleware=middlewareName;try{const outcome=middleware.handle(enhancedReq);if(outcome&&typeof outcome.then==="function")await Promise.race([outcome,armChainBudget()]);const elapsedMs=Number(process.hrtime.bigint()-mwStart)/1e6;middlewareTimings.push({name:middlewareName,ms:elapsedMs})}catch(error){const elapsedMs=Number(process.hrtime.bigint()-mwStart)/1e6;middlewareTimings.push({name:middlewareName,ms:elapsedMs});log.debug(`[middleware] Blocked by: ${middlewareName}`);if(error instanceof Response){try{const{_requestId:reqId,_startNs:startNs}=enhancedReq,total=startNs!=null?Number(process.hrtime.bigint()-startNs)/1e6:null,parts=total!=null?[`total;dur=${total.toFixed(1)}`]:[];for(const t of middlewareTimings){const safeName=t.name.replace(/[^A-Za-z0-9_-]/g,"_").slice(0,32);parts.push(`mw_${safeName};dur=${t.ms.toFixed(1)}`)}if(parts.length>0)error.headers.set("Server-Timing",parts.join(", "));if(reqId)error.headers.set("X-Request-ID",reqId)}catch{}return await applyCorsIfConfigured(enhancedReq,error)}const err=error instanceof Error?error:Error(String(error)),errorResponse="statusCode"in err||"status"in err?await createMiddlewareErrorResponse(err,enhancedReq):await(()=>{log.error(`[Router] Middleware '${middlewareName}' threw an unexpected error:`,err);return createErrorResponse(err,enhancedReq,{status:500})})();try{const{_requestId:reqId,_startNs:startNs}=enhancedReq,total=startNs!=null?Number(process.hrtime.bigint()-startNs)/1e6:null,parts=total!=null?[`total;dur=${total.toFixed(1)}`]:[];for(const t of middlewareTimings){const safeName=t.name.replace(/[^A-Za-z0-9_-]/g,"_").slice(0,32);parts.push(`mw_${safeName};dur=${t.ms.toFixed(1)}`)}if(parts.length>0)errorResponse.headers.set("Server-Timing",parts.join(", "));if(reqId)errorResponse.headers.set("X-Request-ID",reqId)}catch{}return await applyCorsIfConfigured(enhancedReq,errorResponse)}}}finally{if(chainTimer)clearTimeout(chainTimer)}const baseResult=wrappedBase(enhancedReq);let response=baseResult instanceof Response?baseResult:await baseResult;clearTrackedQueries();if(response){if(routeSeedsCsrf&&!csrfHandledByOuter)try{const mod=loadCsrfModule(),csrf=mod instanceof Promise?await mod:mod;if(csrf)response=csrf.seedCsrfCookieIfMissing(enhancedReq,response,enhancedReq._csrfToken)}catch(err){log.warn("[router] CSRF cookie seeding failed",{error:err})}}if(response&&enhancedReq._corsConfig)response=await applyCorsIfConfigured(enhancedReq,response);const{_requestId:reqId,_startNs:startNs}=enhancedReq,durMs=startNs!=null?Number(process.hrtime.bigint()-startNs)/1e6:null,setHeaders=(h)=>{const after=enhancedReq._afterResponse;if(Array.isArray(after))for(const callback of after)try{if(typeof callback==="function")callback({status:response?.status??0,durationMs:durMs??0})}catch{}const requested=enhancedReq._responseHeaders;if(requested&&typeof requested==="object"){for(const[name,value]of Object.entries(requested))if(typeof value==="string")h.set(name,value)}if(reqId)h.set("X-Request-ID",reqId);if(durMs!=null){const parts=[`total;dur=${durMs.toFixed(1)}`];for(const t of middlewareTimings){const safeName=t.name.replace(/[^A-Za-z0-9_-]/g,"_").slice(0,32);parts.push(`mw_${safeName};dur=${t.ms.toFixed(1)}`)}h.set("Server-Timing",parts.join(", "))}applySecurityHeaders(h)};if(response&&typeof response.headers?.set==="function"){if(response.status>=400&&(response.headers.get("content-type")||"").includes("json")&&reqId)try{const text=await response.clone().text(),parsed=JSON.parse(text);if(parsed&&typeof parsed==="object"){const newHeaders=new Headers(response.headers);setHeaders(newHeaders);return new Response(JSON.stringify({...parsed,request_id:reqId}),{status:response.status,statusText:response.statusText,headers:newHeaders})}}catch{}try{setHeaders(response.headers)}catch{try{const cloned=response.clone(),newHeaders=new Headers(response.headers);setHeaders(newHeaders);return new Response(cloned.body,{status:response.status,statusText:response.statusText,headers:newHeaders})}catch{}}}if(enhancedReq._compress===!0&&response)try{const{applyCompression}=await import(resolveDefaultsPath("app/Middleware/Compress.ts"));return await applyCompression(enhancedReq,response)}catch(err){log.warn(`[router] Compression failed; sending uncompressed response: ${err instanceof Error?err.message:String(err)}`)}return response})}}function createInertRoute(){const inert={middleware:()=>inert,name:()=>inert,skipCsrf:()=>inert,requireCsrf:()=>inert,rateLimit:()=>inert};return inert}function createChainableRoute(routeKey,shadowed=!1){if(shadowed)return createInertRoute();if(!routeMiddlewareRegistry.has(routeKey))routeMiddlewareRegistry.set(routeKey,[]);const routePath=routeKey.includes(":")?routeKey.substring(routeKey.indexOf(":")+1):routeKey,chain={middleware(name){const middlewareList=routeMiddlewareRegistry.get(routeKey);if(!middlewareList)return chain;for(const entry of Array.isArray(name)?name:[name]){if(typeof entry!=="string")throw TypeError(`[Router] middleware() on ${routeKey} was given a ${typeof entry}; it takes an alias or an array of aliases`);middlewareList.push(entry)}return chain},name(routeName){namedRouteRegistry.set(routeName,compileNamedRoute(routePath));return chain},skipCsrf(){csrfSkipRegistry.add(routeKey);csrfRequireRegistry.delete(routeKey);return chain},requireCsrf(){csrfRequireRegistry.add(routeKey);csrfSkipRegistry.delete(routeKey);return chain},rateLimit(max,window){if(!Number.isFinite(max)||max<=0)throw Error(`[Router] .rateLimit(): max must be a positive number, got ${String(max)}`);const windowSeconds=rateLimitWindowToSeconds(window);routeRateLimitRegistry.set(routeKey,{max:Math.floor(max),windowSeconds});return chain}};return chain}async function fileExists(path){try{return await Bun.file(path).exists()}catch{return!1}}function assertSafeHandlerPath(handlerPath){if(typeof handlerPath!=="string"||handlerPath.length===0)throw Error(`[Router] Refusing to resolve handler '${String(handlerPath)}': empty or non-string`);if(handlerPath.includes("\x00"))throw Error("[Router] Refusing to resolve handler with null byte");if(handlerPath.startsWith("/")||/^[A-Za-z]:[\\/]/.test(handlerPath))throw Error(`[Router] Refusing to resolve absolute handler path '${handlerPath}'`);if(handlerPath.split(/[/\\]/).some((s)=>s===".."))throw Error(`[Router] Refusing to resolve handler path '${handlerPath}' (contains '..' segment)`)}const _moduleImportCache=new Map;function cachedImport(fullPath){let p=_moduleImportCache.get(fullPath);if(!p){p=import(fullPath);_moduleImportCache.set(fullPath,p)}return p}const _resolvedHandlerCache=new Map;function resolveStringHandler(handlerPath){let resolved=_resolvedHandlerCache.get(handlerPath);if(!resolved){resolved=resolveStringHandlerUncached(handlerPath);_resolvedHandlerCache.set(handlerPath,resolved);resolved.catch(()=>_resolvedHandlerCache.delete(handlerPath))}return resolved}function validationFailureResponse(errors){return response.validationError(errors)}export function precognitionRequest(req){const header=req.headers?.get?.("Precognition"),viaHeader=typeof header==="string"&&header.toLowerCase()==="true";let viaQuery=!1;try{viaQuery=new URL(req.url).searchParams.get("_validate")==="1"}catch{viaQuery=!1}if(!viaHeader&&!viaQuery)return null;return{only:(req.headers?.get?.("Precognition-Validate-Only")??"").split(",").map((field)=>field.trim()).filter(Boolean)}}export function precognitionSuccess(){return new Response(null,{status:204,headers:{Precognition:"true","Precognition-Success":"true",Vary:"Precognition, Precognition-Validate-Only"}})}let actionRegistryPromise=null;async function getActionRegistry(){if(actionRegistryPromise)return actionRegistryPromise;actionRegistryPromise=(async()=>{try{const dir=p.storagePath("framework/auto-imports"),module=await import(`${dir}/actions.ts`);if(!module.actions)return null;const{resolve}=await import("node:path");return Object.fromEntries(Object.entries(module.actions).map(([name,file])=>[name,resolve(dir,file)]))}catch{return null}})();return actionRegistryPromise}async function resolveStringHandlerUncached(handlerPath){assertSafeHandlerPath(handlerPath);let modulePath=handlerPath;modulePath=modulePath.endsWith(".ts")?modulePath.slice(0,-3):modulePath;if(modulePath.includes("Controller")){const[controllerPath,methodName="index"]=modulePath.split("@"),userPath=p.appPath(`${controllerPath}.ts`),defaultPath=resolveDefaultsPath(`app/${controllerPath}.ts`),fullPath=await fileExists(userPath)?userPath:defaultPath;try{const controller=await cachedImport(fullPath);if(!controller.default||typeof controller.default!=="function")throw Error(`Controller ${controllerPath} does not export a default class`);const instance=new controller.default;if(typeof instance[methodName]!=="function")throw Error(`Method ${methodName} not found in controller ${controllerPath}`);return async(req)=>{const result=await instance[methodName](req);return formatResult(result,req)}}catch(error){log.error(`[Router] Failed to load controller '${fullPath}':`,error);throw error}}let fullPath;if(modulePath.includes("storage/framework/orm"))fullPath=modulePath;else if(modulePath.includes("OrmAction"))fullPath=p.storagePath(`framework/actions/src/${modulePath}.ts`);else if(modulePath.includes("Actions")){const registered=(await getActionRegistry())?.[modulePath];if(registered)fullPath=registered;else{const userPath=p.projectPath(`app/${modulePath}.ts`),defaultPath=resolveDefaultsPath(`app/${modulePath}.ts`);fullPath=await fileExists(userPath)?userPath:defaultPath}}else{const userPath=p.appPath(`${modulePath}.ts`),defaultPath=resolveDefaultsPath(`app/${modulePath}.ts`);fullPath=await fileExists(userPath)?userPath:defaultPath}try{const action=(await cachedImport(fullPath)).default;if(!action)throw Error(`Action '${handlerPath}' has no default export`);if(typeof action.handle!=="function"){log.error(`[Router] Action '${handlerPath}' structure:`,Object.keys(action));throw Error(`Action '${handlerPath}' has no handle() method. Got: ${typeof action.handle}`)}return wrapAction(action,handlerPath)}catch(importError){log.error(`[Router] Failed to import action '${fullPath}':`,importError);throw importError}}export function isRouterAction(handler){return typeof handler==="object"&&handler!==null&&typeof handler.handle==="function"}export function wrapAction(action,handlerKey){const actionSkipsCsrf=action.skipCsrf===!0||action.csrf===!1;actionSkipsCsrfCache.set(handlerKey,actionSkipsCsrf);const actionForcesJson=action.apiResponse===!0,requestValidationRules=action.validations??modelValidationRules(action.modelDefinition??action.model);return async(req)=>{if(actionSkipsCsrf)req._skipCsrf=!0;if(actionForcesJson)req._forceJson=!0;req._requestValidationRules=requestValidationRules;try{const precognition=precognitionRequest(req);if(precognition){if(!action.validations)return precognitionSuccess();const rules=precognition.only.length>0?Object.fromEntries(Object.entries(action.validations).filter(([field])=>precognition.only.includes(field))):action.validations,precognitionResult=await validateActionInput(req,rules);return precognitionResult.valid?precognitionSuccess():validationFailureResponse(precognitionResult.errors)}if(action.validations){const validationResult=await validateActionInput(req,action.validations);if(!validationResult.valid)return validationFailureResponse(validationResult.errors)}if(typeof action.authorize==="function"){const auth=await action.authorize(req);if(auth instanceof Response)return auth;if(auth===!1)return Response.json({error:"Forbidden"},{status:403})}if(typeof action.before==="function"){const pre=await action.before(req);if(pre instanceof Response)return pre}const result=await action.handle(req);return formatResult(result,req)}catch(handleError){report(handleError,{label:`[Router] action.handle() for '${handlerKey}'`});throw handleError}}}export async function validateActionInput(req,validations){const errors={},input=await getRequestInput(req,validations);for(const[field,validation]of Object.entries(validations)){const value=input[field];let result;try{result=validation.rule.validate(value)}catch{result={valid:!1,errors:[{message:`${field} validation failed`}]}}if(!result.valid){const fieldErrors=[],label=field.replace(/[-_]+/g," ").replace(/([a-z])([A-Z])/g,"$1 $2").replace(/^./,(c)=>c.toUpperCase()),decorate=(msg)=>msg.toLowerCase().startsWith(field.toLowerCase())||msg.includes(label)?msg:`${label} ${msg}`;if(result.errors&&result.errors.length>0)if(validation.message){const firstMessage=result.errors[0]?.message??"";fieldErrors.push(typeof validation.message==="string"?validation.message:validation.message[field]||decorate(firstMessage))}else result.errors.forEach((err)=>fieldErrors.push(decorate(err.message)));else fieldErrors.push(validation.message?typeof validation.message==="string"?validation.message:`${label} is invalid`:`${label} is invalid`);errors[field]=fieldErrors}}const valid=Object.keys(errors).length===0;if(valid){const validated={};for(const field of Object.keys(validations))if(input[field]!==void 0)validated[field]=input[field];req._validatedInput=validated}return{valid,errors}}function modelValidationRules(model){if(!model?.attributes||typeof model.attributes!=="object")return;const rules={};for(const[field,attribute]of Object.entries(model.attributes)){const validation=attribute?.validation;if(validation?.rule)rules[field]=validation}return Object.keys(rules).length>0?rules:void 0}async function getRequestInput(req,validations){const input={},q=req.query;if(q)for(const key in q)input[key]=q[key];else new URL(req.url).searchParams.forEach((value,key)=>{input[key]=value});if(req.params)Object.assign(input,req.params);if(req.jsonBody&&typeof req.jsonBody==="object")Object.assign(input,req.jsonBody);else if(req.formBody&&typeof req.formBody==="object")Object.assign(input,req.formBody);if(typeof req.allFiles==="function")try{const files=req.allFiles();for(const key of Object.keys(files??{}))if(!(key in input))input[key]=files[key]}catch{}if(!validations)return input;for(const[field,validation]of Object.entries(validations)){const value=input[field];if(typeof value!=="string")continue;const validatorName=validation.rule?.name;if(validatorName==="number"){const n=Number(value);if(Number.isFinite(n))input[field]=n}else if(validatorName==="boolean"){if(value==="true"||value==="1")input[field]=!0;else if(value==="false"||value==="0")input[field]=!1}}return input}function formatResult(result,req){if(result instanceof Response)return result;if(result instanceof ReadableStream)return new Response(result,{headers:{"Content-Type":"application/octet-stream"}});const apiShaped=req._forceJson===!0||isApiRequest(req);if(result===null||result===void 0)return apiShaped?new Response(null,{status:204}):new Response("",{status:200});if(typeof result==="object"){const linkHeader=buildPaginatorLinkHeader(result);if(linkHeader)return Response.json(result,{headers:{Link:linkHeader}});return Response.json(result)}if(apiShaped)return Response.json(result);return new Response(String(result),{headers:{"Content-Type":"text/plain; charset=utf-8"}})}function buildPaginatorLinkHeader(value){if(!isPaginator(value)&&!isSimplePaginator(value)&&!isCursorPaginator(value))return null;const v=value,parts=[];if(v.prev_page_url)parts.push(`<${v.prev_page_url}>; rel="prev"`);if(v.next_page_url)parts.push(`<${v.next_page_url}>; rel="next"`);if(v.first_page_url)parts.push(`<${v.first_page_url}>; rel="first"`);if(v.last_page_url)parts.push(`<${v.last_page_url}>; rel="last"`);return parts.length>0?parts.join(", "):null}export function stream(source,options={}){const baseHeaders={};if(options.type==="sse"){baseHeaders["Content-Type"]="text/event-stream; charset=utf-8";baseHeaders["Cache-Control"]="no-cache";baseHeaders.Connection="keep-alive"}else if(options.type==="ndjson")baseHeaders["Content-Type"]="application/x-ndjson; charset=utf-8";else baseHeaders["Content-Type"]=options.contentType??"application/octet-stream";const body=source instanceof ReadableStream?source:new ReadableStream({async start(controller){try{for await(const chunk of source)controller.enqueue(typeof chunk==="string"?new TextEncoder().encode(chunk):chunk);controller.close()}catch(err){controller.error(err)}}}),merged=new Headers(baseHeaders);if(options.headers)new Headers(options.headers).forEach((value,key)=>merged.set(key,value));return new Response(body,{status:options.status??200,headers:merged})}function getAllInputFor(req){const cached=req._allInputCache;if(cached)return cached;const input={},query=req.query;if(query)for(const key in query)input[key]=query[key];if(req.jsonBody&&typeof req.jsonBody==="object")Object.assign(input,req.jsonBody);if(req.formBody&&typeof req.formBody==="object")Object.assign(input,req.formBody);if(req.params&&typeof req.params==="object")Object.assign(input,req.params);req._allInputCache=input;return input}function flashInputFor(req,keys){const input=getAllInputFor(req);req._oldInput=keys?Object.fromEntries(keys.filter((key)=>(key in input)).map((key)=>[key,input[key]])):{...input}}const REQUEST_METHODS={get(key,defaultValue){const value=getAllInputFor(this)[key];return value!==void 0?value:defaultValue},input(key,defaultValue){const value=getAllInputFor(this)[key];return value!==void 0?value:defaultValue},all(){return getAllInputFor(this)},only(keys){const input=getAllInputFor(this),result={};for(const key of keys)if(key in input)result[key]=input[key];return result},except(keys){const result={...getAllInputFor(this)};for(const key of keys)delete result[key];return result},has(key){const input=getAllInputFor(this);if(Array.isArray(key))return key.every((k)=>(k in input)&&input[k]!==void 0);return key in input&&input[key]!==void 0},hasAny(keys){const input=getAllInputFor(this);return keys.some((k)=>(k in input)&&input[k]!==void 0)},filled(key){const input=getAllInputFor(this),isFilled=(k)=>{const value=input[k];return value!==void 0&&value!==null&&value!==""&&!(Array.isArray(value)&&value.length===0)};if(Array.isArray(key))return key.every(isFilled);return isFilled(key)},missing(key){const input=getAllInputFor(this);if(Array.isArray(key))return key.every((k)=>!(k in input)||input[k]===void 0);return!(key in input)||input[key]===void 0},merge(data){Object.assign(getAllInputFor(this),data)},keys(){return Object.keys(getAllInputFor(this))},string(key,defaultValue=""){const value=getAllInputFor(this)[key];return value!==void 0&&value!==null?String(value):defaultValue},integer(key,defaultValue=0){const value=getAllInputFor(this)[key];if(value===void 0||value===null||value==="")return defaultValue;if(typeof value==="number")return Number.isFinite(value)?Math.trunc(value):defaultValue;const str=String(value).trim();if(!/^-?\d+$/.test(str))return defaultValue;const parsed=Number.parseInt(str,10);return Number.isFinite(parsed)?parsed:defaultValue},float(key,defaultValue=0){const value=getAllInputFor(this)[key];if(value===void 0||value===null||value==="")return defaultValue;if(typeof value==="number")return Number.isFinite(value)?value:defaultValue;const str=String(value).trim();if(!/^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(str))return defaultValue;const parsed=Number.parseFloat(str);return Number.isFinite(parsed)?parsed:defaultValue},boolean(key,defaultValue=!1){const value=getAllInputFor(this)[key];if(value===void 0||value===null)return defaultValue;if(typeof value==="boolean")return value;if(value==="true"||value==="1"||value===1)return!0;if(value==="false"||value==="0"||value===0)return!1;return defaultValue},array(key){const value=getAllInputFor(this)[key];if(Array.isArray(value))return value;return value!==void 0&&value!==null?[value]:[]},date(key){const value=getAllInputFor(this)[key];if(value===void 0||value===null||value==="")return null;const parsed=new Date(value);return Number.isNaN(parsed.getTime())?null:parsed},enum(key,enumType){const value=getAllInputFor(this)[key];if(value===void 0||value===null)return null;if(Object.values(enumType).includes(value))return value;const enumKey=String(value);return enumKey in enumType?enumType[enumKey]:null},collect(key){const value=getAllInputFor(this)[key];if(Array.isArray(value))return collect(value);return collect(value===void 0||value===null?[]:[value])},whenHas(key,callback,defaultCallback){const input=getAllInputFor(this);if(key in input&&input[key]!==void 0)callback(input[key]);else defaultCallback?.()},whenFilled(key,callback,defaultCallback){const value=getAllInputFor(this)[key];if(value!==void 0&&value!==null&&value!==""&&!(Array.isArray(value)&&value.length===0))callback(value);else defaultCallback?.()},isValue(key,value){return getAllInputFor(this)[key]===value},async validate(rules,messages={}){const selectedRules=rules??this._requestValidationRules;if(!selectedRules||Object.keys(selectedRules).length===0){const input=getAllInputFor(this);this._validatedInput=input;return input}const normalized={};for(const[field,definition]of Object.entries(selectedRules)){if(typeof definition==="string")throw TypeError(`String validation rules are not supported for "${field}". Use schema validators.`);if(definition&&typeof definition==="object"&&"rule"in definition){const message=messages[field];normalized[field]=message?{...definition,message}:definition}else normalized[field]=definition}const{validate}=await import("@stacksjs/validation"),validated=await validate(this,normalized);this._validatedInput=validated;return validated},getValidated(){return this._validatedInput??{}},safe(){const data=this._validatedInput??{};return{all:()=>({...data}),get:(key,defaultValue)=>(key in data)?data[key]:defaultValue,only:(keys)=>Object.fromEntries(keys.filter((key)=>(key in data)).map((key)=>[key,data[key]])),except:(keys)=>Object.fromEntries(Object.entries(data).filter(([key])=>!keys.includes(key)))}},old(key,defaultValue){return this._oldInput?.[key]??defaultValue},flashInput(keys){flashInputFor(this,keys)},flashInputOnly(keys){flashInputFor(this,keys)},flashInputExcept(keys){const input=getAllInputFor(this);this._oldInput=Object.fromEntries(Object.entries(input).filter(([key])=>!keys.includes(key)))},file(key){const file=(this.files||{})[key];if(!file)return null;const rawFile=Array.isArray(file)?file[0]:file;return rawFile?new UploadedFile(rawFile):null},getFiles(key){const file=(this.files||{})[key];if(!file)return[];return(Array.isArray(file)?file:[file]).map((f)=>new UploadedFile(f))},hasFile(key){const files=this.files||{};return key in files&&files[key]!==void 0},allFiles(){const files=this.files||{},result={};for(const[key,value]of Object.entries(files))if(Array.isArray(value))result[key]=value.map((f)=>new UploadedFile(f));else result[key]=new UploadedFile(value);return result},getParams(){return{...this.params}},isEmpty(){return Object.keys(getAllInputFor(this)).length===0},browser(){return this.headers.get("sec-ch-ua")||this.headers.get("user-agent")},ipForRateLimit(){const ip=this.ip;if(typeof ip==="function")return ip.call(this)||null;return typeof ip==="string"&&ip?ip:null},getMethod(){return this.method.toUpperCase()},async user(){return this._authenticatedUser},async userToken(){return this._currentAccessToken},async tokenCan(ability){if(typeof ability!=="string"||ability.length===0)return!1;const token=this._currentAccessToken;if(!token||typeof token!=="object")return!1;const abilities=token.abilities;if(!Array.isArray(abilities))return!1;if(abilities.includes("*"))return!0;return abilities.includes(ability)},async tokenCant(ability){return!await this.tokenCan(ability)},async can(ability,...args){if(typeof ability!=="string"||ability.length===0)return!1;const{Gate}=await import("@stacksjs/auth"),user=this._authenticatedUser??null;return Gate.allows(ability,user,...args)},async cannot(ability,...args){return!await this.can(ability,...args)},async authorize(ability,...args){const{Gate}=await import("@stacksjs/auth"),user=this._authenticatedUser??null;await Gate.authorize(ability,user,...args)}},stacksRequestPrototypes=new WeakMap;let csrfModule,csrfModuleLoad;function loadCsrfModule(){if(csrfModule!==void 0)return csrfModule;csrfModuleLoad??=import(resolveDefaultsPath("app/Middleware/Csrf.ts")).then((mod)=>{csrfModule=mod;return csrfModule}).catch(()=>{csrfModule=null;return null});return csrfModuleLoad}export function clearCsrfModuleCache(){csrfModule=void 0;csrfModuleLoad=void 0}function applyCsrfRenderToken(req,cookieHeader,mod){const token=mod.generateCsrfToken();req._csrfToken=token;try{const merged=cookieHeader?`${cookieHeader}; ${mod.CSRF_COOKIE_NAME}=${token}`:`${mod.CSRF_COOKIE_NAME}=${token}`;req.headers.set("cookie",merged)}catch{}}function seedCsrfTokenForRender(req){const method=req.method?.toUpperCase?.()??"GET";if(method!=="GET"&&method!=="HEAD")return;const cookieHeader=req.headers?.get?.("cookie")??"";if(cookieHeader.includes("X-CSRF-Token=")||cookieHeader.includes("csrf-token="))return;const mod=loadCsrfModule();if(mod===null)return;if(mod instanceof Promise)return mod.then((resolved)=>{if(resolved)applyCsrfRenderToken(req,cookieHeader,resolved)});applyCsrfRenderToken(req,cookieHeader,mod)}export function enhanceRequest(req){const routeParams=req.params??{};req.params=routeParams;applyRequestEnhancements(req,routeParams);if(!req._requestId)req._requestId=incomingRequestId(req)??crypto.randomUUID();const routerPrototype=Object.getPrototypeOf(req);let stacksPrototype=stacksRequestPrototypes.get(routerPrototype);if(!stacksPrototype){stacksPrototype=Object.assign(Object.create(routerPrototype),REQUEST_METHODS);stacksRequestPrototypes.set(routerPrototype,stacksPrototype)}Object.setPrototypeOf(req,stacksPrototype);return req}function incomingRequestId(req){try{const supplied=req.headers?.get?.("x-request-id")?.trim();if(supplied&&/^[\w.:-]{8,200}$/.test(supplied))return supplied}catch{}return}function wrapHandler(handler,skipParsing=!1,handlerKey=""){if(isRouterAction(handler))return wrapAction(handler,handlerKey);if(typeof handler==="string"){const handlerPath=handler;return async(req)=>{try{if(!skipParsing){await parseRequestBody(req);req=enhanceRequest(req)}return await(await resolveStringHandler(handlerPath))(req)}catch(error){report(error,{label:`[Router] ${handlerPath}`});const rawStatus=error?.statusCode??error?.status,status=typeof rawStatus==="number"&&Number.isInteger(rawStatus)&&rawStatus>=400&&rawStatus<600?rawStatus:void 0;return await createErrorResponse(error instanceof Error?error:Error(String(error)),req,{handlerPath,status})}}}const fn=handler;return(req)=>{const result=fn(req);if(result instanceof Promise)return result.then((value)=>formatResult(value,req));return formatResult(result,req)}}async function readJsonBodyOnce(req,contentType){const raw=await req.text();req._rawBody=raw;const target=req;target.text=()=>Promise.resolve(raw);target.json=()=>{try{return Promise.resolve(raw.length===0?null:JSON.parse(raw))}catch(err){return Promise.reject(err)}};target.bytes=()=>Promise.resolve(new TextEncoder().encode(raw));target.arrayBuffer=()=>{const bytes=new TextEncoder().encode(raw);return Promise.resolve(bytes.buffer.slice(bytes.byteOffset,bytes.byteOffset+bytes.byteLength))};target.blob=()=>Promise.resolve(new Blob([raw],{type:contentType}));target.clone=()=>new Request(req.url,{method:req.method,headers:req.headers,body:raw});return raw}async function parseRequestBody(req){if(req._bodyParsed)return;req._bodyParsed=!0;const contentType=req.headers.get("content-type")||"";try{if(JSON_CONTENT_TYPE.test(contentType)){const raw=await readJsonBodyOnce(req,contentType);if(raw.length===0)req.jsonBody={};else try{const body=JSON.parse(raw);req.jsonBody=body&&typeof body==="object"?body:{}}catch(parseErr){const message=parseErr instanceof Error?parseErr.message:"Invalid JSON",{HttpError}=await import("@stacksjs/error-handling");throw new HttpError(400,`Invalid JSON body: ${message}`)}}else if(contentType.includes("application/x-www-form-urlencoded")){const text=await req.clone().text(),params=new URLSearchParams(text),formBody={};params.forEach((value,key)=>{formBody[key]=value});req.formBody=formBody}else if(contentType.includes("multipart/form-data")){const formData=await req.clone().formData(),formBody={},files={};formData.forEach((value,key)=>{if(value instanceof File)if(files[key])if(Array.isArray(files[key]))files[key].push(value);else files[key]=[files[key],value];else files[key]=value;else formBody[key]=value});Reflect.set(req,"formBody",formBody);Reflect.set(req,"files",files)}}catch(e){if(typeof(e?.status??e?.statusCode)==="number")throw e;log.debug("[stacks-router] Body parsing failed:",e)}}export function createStacksRouter(config={}){const bunRouter=new Router({verbose:config.verbose??!1});let currentPrefix="",currentGroupMiddleware=[],currentGroupApiResponse=!1;const registeredRouteKeys=new Set;function registerRoute(method,path,_handler){const fullPath=currentPrefix+path,routeKey=`${method}:${fullPath}`;log.debug(`[router] ${method} ${fullPath} \u2192 ${typeof _handler==="string"?_handler:"function"}`);const shadowed=registeredRouteKeys.has(routeKey);if(!shadowed)registeredRouteKeys.add(routeKey);if(!shadowed&¤tGroupMiddleware.length>0)routeMiddlewareRegistry.set(routeKey,[...currentGroupMiddleware]);if(!shadowed&¤tGroupApiResponse)routeApiResponseRegistry.add(routeKey);if(!shadowed&&typeof _handler==="string")routeHandlerKeyRegistry.set(routeKey,_handler);if(!shadowed&&isRouterAction(_handler))routeActionRegistry.set(routeKey,_handler);return{fullPath,routeKey,shadowed}}const stacksRouter={bunRouter,get routes(){return bunRouter.routes},get(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("GET",path,handler);bunRouter.get(fullPath,createMiddlewareHandler(routeKey,handler));return createChainableRoute(routeKey,shadowed)},post(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("POST",path,handler);bunRouter.post(fullPath,createMiddlewareHandler(routeKey,handler));return createChainableRoute(routeKey,shadowed)},put(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("PUT",path,handler);bunRouter.put(fullPath,createMiddlewareHandler(routeKey,handler));return createChainableRoute(routeKey,shadowed)},patch(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("PATCH",path,handler);bunRouter.patch(fullPath,createMiddlewareHandler(routeKey,handler));return createChainableRoute(routeKey,shadowed)},delete(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("DELETE",path,handler);bunRouter.delete(fullPath,createMiddlewareHandler(routeKey,handler));return createChainableRoute(routeKey,shadowed)},options(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("OPTIONS",path,handler);bunRouter.options(fullPath,createMiddlewareHandler(routeKey,handler));return createChainableRoute(routeKey,shadowed)},group(options,callback){const previousPrefix=currentPrefix,previousMiddleware=[...currentGroupMiddleware],previousApiResponse=currentGroupApiResponse;if(options.prefix)currentPrefix=previousPrefix+options.prefix;const middlewareList=options.middleware?Array.isArray(options.middleware)?options.middleware:[options.middleware]:void 0;if(middlewareList)currentGroupMiddleware=[...currentGroupMiddleware,...middlewareList];if(options.apiResponse===!0)currentGroupApiResponse=!0;log.debug(`[router] Entering group: prefix=${options.prefix||"/"} middleware=[${middlewareList?.join(", ")||""}]${currentGroupApiResponse?" apiResponse=true":""}`);const result=callback();if(result instanceof Promise)return result.then(()=>{currentPrefix=previousPrefix;currentGroupMiddleware=previousMiddleware;currentGroupApiResponse=previousApiResponse;return stacksRouter}).catch((err)=>{currentPrefix=previousPrefix;currentGroupMiddleware=previousMiddleware;currentGroupApiResponse=previousApiResponse;throw err});currentPrefix=previousPrefix;currentGroupMiddleware=previousMiddleware;currentGroupApiResponse=previousApiResponse;return stacksRouter},resource(name,handler,options){const actions=["index","store","show","update","destroy"],activeActions=options?.only?actions.filter((a)=>options.only.includes(a)):options?.except?actions.filter((a)=>!options.except.includes(a)):actions,stripped=handler.replace(/Action$/,""),handlerBase=stripped.startsWith("Actions/")?stripped:`Actions/${stripped}`;log.debug(`[router] Resource: /${name} \u2192 ${handlerBase}*Action [${activeActions.join(", ")}]`);const sibling=(suffix)=>`${handlerBase}${suffix}`,registerResourceRoutes=()=>{for(const action of activeActions)switch(action){case"index":stacksRouter.get(`/${name}`,sibling("IndexAction"));break;case"store":stacksRouter.post(`/${name}`,sibling("StoreAction"));break;case"show":stacksRouter.get(`/${name}/:id`,sibling("ShowAction"));break;case"update":stacksRouter.put(`/${name}/:id`,sibling("UpdateAction"));break;case"destroy":stacksRouter.delete(`/${name}/:id`,sibling("DestroyAction"));break}};if(options?.middleware)stacksRouter.group({middleware:options.middleware},registerResourceRoutes);else registerResourceRoutes();return stacksRouter},match(methods,path,handler){log.debug(`[router] Match: [${methods.join(", ")}] ${path} \u2192 ${typeof handler==="string"?handler:"function"}`);let firstShadowed=!1;for(const[index,method]of methods.entries()){const m=method.toUpperCase(),{fullPath,routeKey,shadowed}=registerRoute(m,path,handler);if(index===0)firstShadowed=shadowed;const wrappedHandler=createMiddlewareHandler(routeKey,handler);switch(m){case"GET":bunRouter.get(fullPath,wrappedHandler);break;case"POST":bunRouter.post(fullPath,wrappedHandler);break;case"PUT":bunRouter.put(fullPath,wrappedHandler);break;case"PATCH":bunRouter.patch(fullPath,wrappedHandler);break;case"DELETE":bunRouter.delete(fullPath,wrappedHandler);break;case"OPTIONS":bunRouter.options(fullPath,wrappedHandler);break}}return createChainableRoute(`${methods[0]}:${currentPrefix}${path}`,firstShadowed)},health(){bunRouter.get("/api/health",async()=>{const health=await checkApplicationHealth();return Response.json(health,{status:health.status==="healthy"?200:503})});bunRouter.get("/__routes",(req)=>{if(!isExposeRoutesAuthorized(req))return Response.json({error:"disabled"},{status:404});return Response.json(listRegisteredRoutes())});bunRouter.get("/__storage/:path",async(req)=>{const url=new URL(req.url),token=url.searchParams.get("token"),params=req.params,rawPath=params?.path?decodeURIComponent(params.path):decodeURIComponent(url.pathname.replace(/^\/__storage\//,""));if(!token||typeof rawPath!=="string"||rawPath.length===0)return new Response("Forbidden",{status:403});const{verifySignedStorageToken,Storage}=await import("@stacksjs/storage");if(!verifySignedStorageToken(token,rawPath).valid)return new Response("Forbidden",{status:403});try{const adapter=Storage.disk();if(!await adapter.fileExists(rawPath))return new Response("Not Found",{status:404});const buf=await adapter.readToBuffer(rawPath),mime=await adapter.mimeType(rawPath).catch(()=>"application/octet-stream");return new Response(buf,{status:200,headers:{"Content-Type":mime,"Cache-Control":"private, max-age=60","X-Content-Type-Options":"nosniff"}})}catch(err){log.error("[storage] signed-url fetch failed:",err);return new Response("Internal Error",{status:500})}});bunRouter.get("/__openapi.json",async(req)=>{if(!isExposeRoutesAuthorized(req))return Response.json({error:"disabled"},{status:404});try{const{generateOpenApi}=await import("@stacksjs/api"),spec=await generateOpenApi({write:!1,portable:!1});return Response.json(spec)}catch(err){return Response.json({error:"OpenAPI generation failed",message:err instanceof Error?err.message:String(err)},{status:500})}});return stacksRouter},use(middleware){const adapted=adaptMiddlewareForBunRouter(middleware);bunRouter.globalMiddleware.push(adapted);return stacksRouter},booting(name,run){bootHooks.push({name,run});return stacksRouter},async serve(options={}){warnOnMultipleRouterInstances();configureViewDirectories(bunRouter);wrapHandleRequestForCsrf(bunRouter);await runBootHooks();return bunRouter.serve(options)},async handleRequest(req){return bunRouter.handleRequest(req)},getAllowedMethods(pathname,domain){return bunRouter.getAllowedMethods(pathname,domain)},async register(routePath,options){log.debug(`[router] Register: ${routePath} prefix=${options?.prefix||"none"}`);const callback=async()=>{await import(routePath)};if(options?.prefix||options?.middleware)await stacksRouter.group({prefix:options.prefix,middleware:options.middleware},callback);else await callback();return stacksRouter},async importRoutes(){log.debug("[router] Loading user routes from registry...");try{const{loadRoutes}=await import("./route-loader"),{appPath}=await import("@stacksjs/path"),routeRegistry=(await import(appPath("Routes.ts"))).default;await loadRoutes(routeRegistry)}catch(error){log.error("Failed to load route registry:",error);throw error}log.debug("[router] Loading ORM routes...");const ormRoutesPackage="@stacksjs/orm/routes";let ormRoutesLoaded=!1;try{await import(ormRoutesPackage);ormRoutesLoaded=!0;log.debug(`[router] ORM routes loaded from ${ormRoutesPackage}`)}catch(error){log.debug(`[router] ORM routes not available from the package, trying the vendored copy
|
|
5
5
|
`,error)}const ormRoutesCandidates=ormRoutesLoaded?[]:[p.frameworkPath("orm/routes.ts"),p.frameworkPath("core/orm/routes.ts")];for(const candidate of ormRoutesCandidates)try{if(await Bun.file(candidate).exists()){await import(candidate);ormRoutesLoaded=!0;log.info(`[router] ORM routes loaded from the vendored copy at ${candidate}. This file is not refreshed by upgrading @stacksjs/orm - delete it to use the package.`);break}}catch(error){log.warn(`[router] ORM routes candidate failed to load, falling back to next: ${candidate}
|
|
6
|
-
`,error)}if(!ormRoutesLoaded)log.warn("[router] No ORM routes candidate loaded - model useApi endpoints are unavailable.");log.debug("[router] Loading discovered package routes...");try{await stacksRouter.loadDiscoveredRoutes()}catch(error){log.debug("Package route discovery skipped:",error)}await assertRouteMiddlewareResolvable()},async loadDiscoveredRoutes(){try{const manifestPath=p.storagePath("framework/discovered-packages.json"),file=Bun.file(manifestPath);if(!await file.exists())return;const packages=(await file.json())?.packages;if(!packages)return;for(const[pkgName,meta]of Object.entries(packages)){const routes=meta?.routes;if(!routes)continue;const routeList=Array.isArray(routes)?routes:[routes],pkgDir=meta?.root?p.projectPath(meta.root):`${p.projectPath("pantry")}/${pkgName}`;for(const routeFile of routeList){log.debug(`[router] Discovered route: ${pkgName} \u2192 ${routeFile}`);const fullPath=routeFile.startsWith("/")?routeFile:`${pkgDir}/${routeFile}`,prefix=meta?.routePrefix,middleware=meta?.routeMiddleware;try{await stacksRouter.register(fullPath,{prefix,middleware})}catch(err){log.warn(`Failed to load routes from package '${pkgName}': ${err}`)}}}}catch{}}};return stacksRouter}function wrapHandleRequestForCsrf(bunRouter){const router=bunRouter;if(router._csrfSeedingWrapped)return;const original=router.handleRequest.bind(router);router.handleRequest=async(request)=>{const method=request.method?.toUpperCase?.()??"GET",safe=method==="GET"||method==="HEAD"||method==="OPTIONS";if(safe){const seeding=seedCsrfTokenForRender(request);if(seeding)await seeding}const response=await original(request);if(!safe||!response)return response;try{const mod=loadCsrfModule(),csrf=mod instanceof Promise?await mod:mod;if(!csrf)return response;return csrf.seedCsrfCookieIfMissing(request,response,request._csrfToken)}catch(err){log.warn("[router] CSRF cookie seeding failed on the view path",{error:err});return response}};router._csrfSeedingWrapped=!0}export function configureViewDirectories(bunRouter){const router=bunRouter;if(typeof router.views!=="function")return;const existing=router._fileRoutingConfig;if(existing&&Object.keys(existing).length>0)return;if(!existsSync(p.projectPath("resources/views")))return;const layouts=[p.projectPath("resources/views/layouts"),p.projectPath("resources/layouts")].find(existsSync),partials=[p.projectPath("resources/views/partials"),p.projectPath("resources/partials")].find(existsSync),components=[p.projectPath("resources/components"),p.projectPath("resources/views/components")].find(existsSync);router.views({...components?{componentsDir:components}:{},...layouts?{layoutsDir:layouts}:{},...partials?{partialsDir:partials}:{}})}export function disableViewRouting(bunRouter){const router=bunRouter;if(typeof router.disableFileRouting!=="function")return!1;const existing=router._fileRoutingConfig;if(existing&&Object.keys(existing).length>0)return!1;router.disableFileRouting();return!0}const ROUTE_SINGLETON_KEY=Symbol.for("@stacksjs/router:route-singleton");export const route=globalThis[ROUTE_SINGLETON_KEY]??=createStacksRouter();let routesLoadPromise=null,routingContextRunner=null;async function getRoutingContextRunner(){if(!routingContextRunner)try{const{withRoutingContext}=await import("@stacksjs/database");routingContextRunner=typeof withRoutingContext==="function"?withRoutingContext:(fn)=>fn()}catch{routingContextRunner=(fn)=>fn()}return routingContextRunner}export async function serverResponse(request,_body){return(await getRoutingContextRunner())(()=>handleServerRequest(request))}async function handleServerRequest(request){if(!routesLoadPromise){log.debug("[router] Loading routes for first time...");routesLoadPromise=route.importRoutes().catch((err)=>{routesLoadPromise=null;throw err})}await routesLoadPromise;const response=await route.handleRequest(request);if(response.status===404&&response.headers.get("content-type")?.includes("json"))try{const body=await response.clone().json();if(body?.message==="Not Found"||body?.error==="Not Found"){const url=new URL(request.url),enriched={...body,path:url.pathname,method:request.method};return new Response(JSON.stringify(enriched),{status:404,headers:response.headers})}}catch{}return response}export async function serve(options={}){return route.serve(options)}
|
|
6
|
+
`,error)}if(!ormRoutesLoaded)log.warn("[router] No ORM routes candidate loaded - model useApi endpoints are unavailable.");log.debug("[router] Loading discovered package routes...");try{await stacksRouter.loadDiscoveredRoutes()}catch(error){log.debug("Package route discovery skipped:",error)}await assertRouteMiddlewareResolvable()},async loadDiscoveredRoutes(){try{const manifestPath=p.storagePath("framework/discovered-packages.json"),file=Bun.file(manifestPath);if(!await file.exists())return;const packages=(await file.json())?.packages;if(!packages)return;for(const[pkgName,meta]of Object.entries(packages)){const routes=meta?.routes;if(!routes)continue;const routeList=Array.isArray(routes)?routes:[routes],pkgDir=meta?.root?p.projectPath(meta.root):`${p.projectPath("pantry")}/${pkgName}`;for(const routeFile of routeList){log.debug(`[router] Discovered route: ${pkgName} \u2192 ${routeFile}`);const fullPath=routeFile.startsWith("/")?routeFile:`${pkgDir}/${routeFile}`,prefix=meta?.routePrefix,middleware=meta?.routeMiddleware;try{await stacksRouter.register(fullPath,{prefix,middleware})}catch(err){log.warn(`Failed to load routes from package '${pkgName}': ${err}`)}}}}catch{}}};return stacksRouter}function wrapHandleRequestForCsrf(bunRouter){const router=bunRouter;if(router._csrfSeedingWrapped)return;const original=router.handleRequest.bind(router);router.handleRequest=async(request)=>{const method=request.method?.toUpperCase?.()??"GET",safe=method==="GET"||method==="HEAD"||method==="OPTIONS";if(safe){const seeding=seedCsrfTokenForRender(request);if(seeding)await seeding;request[CSRF_SEEDED_BY_HANDLE_REQUEST]=!0}const response=await original(request);if(!safe||!response)return response;try{const mod=loadCsrfModule(),csrf=mod instanceof Promise?await mod:mod;if(!csrf)return response;return csrf.seedCsrfCookieIfMissing(request,response,request._csrfToken)}catch(err){log.warn("[router] CSRF cookie seeding failed on the view path",{error:err});return response}};router._csrfSeedingWrapped=!0}export function configureViewDirectories(bunRouter){const router=bunRouter;if(typeof router.views!=="function")return;const existing=router._fileRoutingConfig;if(existing&&Object.keys(existing).length>0)return;if(!existsSync(p.projectPath("resources/views")))return;const layouts=[p.projectPath("resources/views/layouts"),p.projectPath("resources/layouts")].find(existsSync),partials=[p.projectPath("resources/views/partials"),p.projectPath("resources/partials")].find(existsSync),components=[p.projectPath("resources/components"),p.projectPath("resources/views/components")].find(existsSync);router.views({...components?{componentsDir:components}:{},...layouts?{layoutsDir:layouts}:{},...partials?{partialsDir:partials}:{}})}export function disableViewRouting(bunRouter){const router=bunRouter;if(typeof router.disableFileRouting!=="function")return!1;const existing=router._fileRoutingConfig;if(existing&&Object.keys(existing).length>0)return!1;router.disableFileRouting();return!0}const ROUTE_SINGLETON_KEY=Symbol.for("@stacksjs/router:route-singleton");export const route=globalThis[ROUTE_SINGLETON_KEY]??=createStacksRouter();let routesLoadPromise=null,routingContextRunner=null;async function getRoutingContextRunner(){if(!routingContextRunner)try{const{withRoutingContext}=await import("@stacksjs/database");routingContextRunner=typeof withRoutingContext==="function"?withRoutingContext:(fn)=>fn()}catch{routingContextRunner=(fn)=>fn()}return routingContextRunner}export async function serverResponse(request,_body){return(await getRoutingContextRunner())(()=>handleServerRequest(request))}async function handleServerRequest(request){if(!routesLoadPromise){log.debug("[router] Loading routes for first time...");routesLoadPromise=route.importRoutes().catch((err)=>{routesLoadPromise=null;throw err})}await routesLoadPromise;const response=await route.handleRequest(request);if(response.status===404&&response.headers.get("content-type")?.includes("json"))try{const body=await response.clone().json();if(body?.message==="Not Found"||body?.error==="Not Found"){const url=new URL(request.url),enriched={...body,path:url.pathname,method:request.method};return new Response(JSON.stringify(enriched),{status:404,headers:response.headers})}}catch{}return response}export async function serve(options={}){return route.serve(options)}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/router",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.74.
|
|
5
|
+
"version": "0.74.22",
|
|
6
6
|
"description": "The Stacks framework router.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -60,28 +60,28 @@
|
|
|
60
60
|
"prepublishOnly": "bun run build"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@stacksjs/bun-router": "^0.1.
|
|
64
|
-
"@stacksjs/cache": "0.74.
|
|
65
|
-
"@stacksjs/collections": "0.74.
|
|
66
|
-
"@stacksjs/config": "0.74.
|
|
67
|
-
"@stacksjs/error-handling": "0.74.
|
|
68
|
-
"@stacksjs/logging": "0.74.
|
|
69
|
-
"@stacksjs/pagination": "0.74.
|
|
70
|
-
"@stacksjs/path": "0.74.
|
|
71
|
-
"@stacksjs/security": "0.74.
|
|
72
|
-
"@stacksjs/storage": "0.74.
|
|
63
|
+
"@stacksjs/bun-router": "^0.1.9",
|
|
64
|
+
"@stacksjs/cache": "0.74.22",
|
|
65
|
+
"@stacksjs/collections": "0.74.22",
|
|
66
|
+
"@stacksjs/config": "0.74.22",
|
|
67
|
+
"@stacksjs/error-handling": "0.74.22",
|
|
68
|
+
"@stacksjs/logging": "0.74.22",
|
|
69
|
+
"@stacksjs/pagination": "0.74.22",
|
|
70
|
+
"@stacksjs/path": "0.74.22",
|
|
71
|
+
"@stacksjs/security": "0.74.22",
|
|
72
|
+
"@stacksjs/storage": "0.74.22",
|
|
73
73
|
"ts-rate-limiter": "^0.4.2"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@stacksjs/actions": "0.74.
|
|
77
|
-
"@stacksjs/types": "0.74.
|
|
76
|
+
"@stacksjs/actions": "0.74.22",
|
|
77
|
+
"@stacksjs/types": "0.74.22",
|
|
78
78
|
"better-dx": "^0.2.24"
|
|
79
79
|
},
|
|
80
80
|
"peerDependencies": {
|
|
81
|
-
"@stacksjs/api": "0.74.
|
|
82
|
-
"@stacksjs/auth": "0.74.
|
|
83
|
-
"@stacksjs/database": "0.74.
|
|
84
|
-
"@stacksjs/validation": "0.74.
|
|
81
|
+
"@stacksjs/api": "0.74.22",
|
|
82
|
+
"@stacksjs/auth": "0.74.22",
|
|
83
|
+
"@stacksjs/database": "0.74.22",
|
|
84
|
+
"@stacksjs/validation": "0.74.22"
|
|
85
85
|
},
|
|
86
86
|
"peerDependenciesMeta": {
|
|
87
87
|
"@stacksjs/api": {
|