@stacksjs/router 0.74.28 → 0.74.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/error-handler.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/rate-limit.d.ts +1 -7
- package/dist/rate-limit.js +1 -1
- package/dist/route-loader.js +1 -1
- package/dist/route-model-binding.js +1 -1
- package/dist/security-headers.js +1 -1
- package/dist/signed-url.js +1 -1
- package/dist/stacks-router.d.ts +9 -427
- package/dist/stacks-router.js +4 -4
- package/package.json +16 -16
package/dist/error-handler.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
|
|
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;function getQueryTrack(){const req=getCurrentRequest();if(!req)return fallbackTrack??=newQueryTrack();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(()=>{})}}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=
|
|
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=void 0}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>
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import './request-augmentation';
|
|
|
2
2
|
// Re-export the augmentation types so userland can refer to the marker
|
|
3
3
|
// surface explicitly when needed.
|
|
4
4
|
export type { StacksRequestExtensions, StacksRequestMacros, StacksRequestMarkers } from './request-augmentation';
|
|
5
|
-
export type { BootHook } from './stacks-router';
|
|
5
|
+
export type { BootHook, StacksRouterConfig } from './stacks-router';
|
|
6
6
|
export type { MiddlewareAliases, MiddlewareClasses, MiddlewareClassName, MiddlewareConfig, Request } from './middleware';
|
|
7
7
|
// Export route loader
|
|
8
8
|
export type { RootMountedAppRoute } from './route-loader';
|
package/dist/rate-limit.d.ts
CHANGED
|
@@ -26,10 +26,4 @@ export declare function rateLimitStatus(key: string, max: number, windowSeconds:
|
|
|
26
26
|
* the failed-attempt counter should reset).
|
|
27
27
|
*/
|
|
28
28
|
export declare function clearRateLimit(key: string, max: number, windowSeconds: number, options?: { identity?: string }): Promise<void>;
|
|
29
|
-
declare
|
|
30
|
-
second: 1;
|
|
31
|
-
minute: 60;
|
|
32
|
-
hour: 3600;
|
|
33
|
-
day: unknown
|
|
34
|
-
};
|
|
35
|
-
declare type Period = keyof typeof PERIOD_SECONDS;
|
|
29
|
+
declare type Period = 'second' | 'minute' | 'hour' | 'day';
|
package/dist/rate-limit.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getCurrentRequest}from"./request-context";
|
|
1
|
+
import{getCurrentRequest}from"./request-context";let limiterCache,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??=new Map).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==="second"?1:period==="minute"?60:period==="hour"?3600:period==="day"?86400:void 0;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/route-loader.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{log}from"@stacksjs/logging";import{route}from"./stacks-router";
|
|
1
|
+
import{log}from"@stacksjs/logging";import{route}from"./stacks-router";let rootMountedAppRoutes;export function listRootMountedAppRoutes(){return rootMountedAppRoutes??=[]}export async function loadRoutes(registry){for(const[key,definition]of Object.entries(registry)){const config=normalizeDefinition(definition),prefix=config.prefix!==void 0?config.prefix?config.prefix.startsWith("/")?config.prefix:`/${config.prefix}`:void 0:key==="web"?void 0:`/${key}`,middleware=normalizeMiddleware(config.middleware);log.debug(`[route-loader] Loading: ${config.path} prefix=${prefix||"/"} middleware=[${middleware.join(", ")}]`);try{if(prefix||middleware.length>0)await route.group({prefix,middleware:middleware.length>0?middleware:void 0},async()=>{await importRouteFile(config.path)});else{const before=registeredRouteKeys();await importRouteFile(config.path);for(const entry of currentRouteTable()){const method=String(entry.method??"").toUpperCase(),path=String(entry.path??"");if(!method||!path||before.has(`${method} ${path}`))continue;(rootMountedAppRoutes??=[]).push({method,path,file:`routes/${config.path}.ts`})}}}catch(error){const message=error instanceof Error?error.message:String(error);console.error(`[Routes] Failed to load route file '${config.path}': ${message}`);throw Error(`Route loading failed for '${config.path}': ${message}`)}}await loadFrameworkRoutes()}export const DEFAULT_ROUTE_BUNDLES=["auth","dashboard","delivery","email","forms"],OPT_IN_ROUTE_BUNDLES=["social"],DEFAULT_ROUTE_BUNDLES_KEY="__stacksDefaultRouteBundles";export function resolveDefaultRouteBundles(env=process.env){return resolveDefaultRouteBundlesWithDiagnostics(env).bundles}export function resolveDefaultRouteBundlesWithDiagnostics(env=process.env){const all=()=>new Set(DEFAULT_ROUTE_BUNDLES),raw=env.STACKS_DEFAULT_ROUTES;if(raw!==void 0){const names=raw.split(",").map((name)=>name.trim().toLowerCase()).filter(Boolean);if(names.length===0||names.includes("none"))return{bundles:new Set,unknown:[],explicit:!0};if(names.includes("all"))return{bundles:all(),unknown:[],explicit:!0};const bundles=new Set,unknown=[];for(const name of names)if(DEFAULT_ROUTE_BUNDLES.includes(name)||OPT_IN_ROUTE_BUNDLES.includes(name))bundles.add(name);else unknown.push(name);return{bundles,unknown,explicit:!0}}if(env.STACKS_SKIP_DEFAULT_ROUTES==="1")return{bundles:new Set,unknown:[],explicit:!0};return{bundles:all(),unknown:[],explicit:!1}}async function loadFrameworkRoutes(){const{bundles,unknown,explicit}=resolveDefaultRouteBundlesWithDiagnostics();for(const name of unknown)log.warn(`[Routes] STACKS_DEFAULT_ROUTES lists unknown bundle "${name}" - known bundles are ${DEFAULT_ROUTE_BUNDLES.join(", ")}.`);if(bundles.size===0)return;globalThis[DEFAULT_ROUTE_BUNDLES_KEY]={bundles,explicit};try{try{const{overridesReady}=await import("@stacksjs/config");await overridesReady}catch{}const{frameworkPath}=await import("@stacksjs/path"),bootstrapPath=frameworkPath("defaults/bootstrap.ts");if(await Bun.file(bootstrapPath).exists())await import(bootstrapPath)}catch(error){const message=error instanceof Error?error.message:String(error);if(!message.includes("Cannot find module")&&!message.includes("MODULE_NOT_FOUND"))console.error(`[Routes] Failed to load framework bootstrap: ${message}`)}}function assertSafeRouteName(routeName){if(typeof routeName!=="string"||routeName.length===0)throw Error("[route-loader] Invalid route path: empty or non-string");if(routeName.includes("\x00"))throw Error("[route-loader] Invalid route path: null byte");let decoded;try{decoded=decodeURIComponent(routeName)}catch{throw Error("[route-loader] Invalid route path: malformed URL encoding")}const cleanPath=decoded.replace(/\.ts$/,"");if(cleanPath.startsWith("/")||/^[A-Za-z]:[\\/]/.test(cleanPath))throw Error(`[route-loader] Invalid route path: absolute paths not allowed (${cleanPath})`);if(cleanPath.split(/[/\\]/).some((s)=>s===".."))throw Error(`[route-loader] Invalid route path: '..' segment not allowed (${cleanPath})`);return cleanPath}async function importRouteFile(routeName){const cleanPath=assertSafeRouteName(routeName),{projectPath}=await import("@stacksjs/path");await import(projectPath(`routes/${cleanPath}`))}function normalizeDefinition(def){if(typeof def==="string")return{path:def};return def}function normalizeMiddleware(middleware){if(!middleware)return[];if(typeof middleware==="string")return[middleware];return middleware}function currentRouteTable(){return route.routes??[]}function registeredRouteKeys(){const keys=new Set;for(const entry of currentRouteTable())keys.add(`${String(entry.method??"").toUpperCase()} ${String(entry.path??"")}`);return keys}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
let bindings,fallbackResolver=null;export function defineRouteModelBinding(param,resolver){(bindings??=new Map).set(param,resolver)}export function setRouteModelFallback(resolver){fallbackResolver=resolver}export function routeModelBindings(){return bindings?[...bindings.keys()].sort():[]}export function clearRouteModelBindings(){bindings=void 0;fallbackResolver=null}export async function resolveRouteModel(param,value,request){const resolver=bindings?.get(param)??fallbackResolver;if(!resolver)return{bound:!1};try{const model=await resolver(value,{param,request});if(model===void 0)return{bound:!1};return{bound:!0,model:model??void 0}}catch{return{bound:!0,model:void 0}}}
|
package/dist/security-headers.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import process from"node:process";let _isProductionCache,_isDisabledCache,_cspCache,_headerTemplateCache,_jsonHeaderTemplateCache,_jsonLengthHeaderTemplateCache;const JSON_LENGTH_HEADER_CACHE_LIMIT=64;function isProduction(){if(_isProductionCache!==void 0)return _isProductionCache;_isProductionCache=(process.env.APP_ENV??process.env.NODE_ENV??"").toLowerCase()==="production";return _isProductionCache}function isDisabled(){if(_isDisabledCache!==void 0)return _isDisabledCache;_isDisabledCache=process.env.STACKS_SECURITY_HEADERS_DISABLE==="true";return _isDisabledCache}function resolveCsp(){if(_cspCache!==void 0)return _cspCache;const enforce=process.env.STACKS_CSP,report=process.env.STACKS_CSP_REPORT_ONLY;if(enforce)_cspCache={header:"Content-Security-Policy",value:enforce};else if(report)_cspCache={header:"Content-Security-Policy-Report-Only",value:report};else _cspCache=null;return _cspCache}export function applySecurityHeaders(headers,knownMissing=!1){if(isDisabled())return;if(knownMissing||headers.get("X-Content-Type-Options")===null)headers.set("X-Content-Type-Options","nosniff");if(knownMissing||headers.get("X-Frame-Options")===null)headers.set("X-Frame-Options","SAMEORIGIN");if(knownMissing||headers.get("Referrer-Policy")===null)headers.set("Referrer-Policy","strict-origin-when-cross-origin");if(isProduction()&&(knownMissing||headers.get("Strict-Transport-Security")===null))headers.set("Strict-Transport-Security","max-age=31536000; includeSubDomains");const csp=resolveCsp();if(csp&&(knownMissing||headers.get(csp.header)===null))headers.set(csp.header,csp.value)}export function applySecurityHeadersToRecord(headers){if(isDisabled())return;headers["X-Content-Type-Options"]="nosniff";headers["X-Frame-Options"]="SAMEORIGIN";headers["Referrer-Policy"]="strict-origin-when-cross-origin";if(isProduction())headers["Strict-Transport-Security"]="max-age=31536000; includeSubDomains";const csp=resolveCsp();if(csp)headers[csp.header]=csp.value}function securityHeadersTemplate(){if(!_headerTemplateCache){_headerTemplateCache=new Headers;applySecurityHeaders(_headerTemplateCache,!0)}return _headerTemplateCache}export function createSecurityHeaders(){return new Headers(securityHeadersTemplate())}function jsonSecurityHeadersTemplate(){if(!_jsonHeaderTemplateCache){_jsonHeaderTemplateCache=new Headers(securityHeadersTemplate());_jsonHeaderTemplateCache.set("Content-Type","application/json;charset=utf-8")
|
|
1
|
+
import process from"node:process";let _isProductionCache,_isDisabledCache,_cspCache,_headerTemplateCache,_jsonHeaderTemplateCache,_jsonMetadataHeaderTemplates,_jsonLengthHeaderTemplateCache;const JSON_LENGTH_HEADER_CACHE_LIMIT=64;function isProduction(){if(_isProductionCache!==void 0)return _isProductionCache;_isProductionCache=(process.env.APP_ENV??process.env.NODE_ENV??"").toLowerCase()==="production";return _isProductionCache}function isDisabled(){if(_isDisabledCache!==void 0)return _isDisabledCache;_isDisabledCache=process.env.STACKS_SECURITY_HEADERS_DISABLE==="true";return _isDisabledCache}function resolveCsp(){if(_cspCache!==void 0)return _cspCache;const enforce=process.env.STACKS_CSP,report=process.env.STACKS_CSP_REPORT_ONLY;if(enforce)_cspCache={header:"Content-Security-Policy",value:enforce};else if(report)_cspCache={header:"Content-Security-Policy-Report-Only",value:report};else _cspCache=null;return _cspCache}export function applySecurityHeaders(headers,knownMissing=!1){if(isDisabled())return;if(knownMissing||headers.get("X-Content-Type-Options")===null)headers.set("X-Content-Type-Options","nosniff");if(knownMissing||headers.get("X-Frame-Options")===null)headers.set("X-Frame-Options","SAMEORIGIN");if(knownMissing||headers.get("Referrer-Policy")===null)headers.set("Referrer-Policy","strict-origin-when-cross-origin");if(isProduction()&&(knownMissing||headers.get("Strict-Transport-Security")===null))headers.set("Strict-Transport-Security","max-age=31536000; includeSubDomains");const csp=resolveCsp();if(csp&&(knownMissing||headers.get(csp.header)===null))headers.set(csp.header,csp.value)}export function applySecurityHeadersToRecord(headers){if(isDisabled())return;headers["X-Content-Type-Options"]="nosniff";headers["X-Frame-Options"]="SAMEORIGIN";headers["Referrer-Policy"]="strict-origin-when-cross-origin";if(isProduction())headers["Strict-Transport-Security"]="max-age=31536000; includeSubDomains";const csp=resolveCsp();if(csp)headers[csp.header]=csp.value}function securityHeadersTemplate(){if(!_headerTemplateCache){_headerTemplateCache=new Headers;applySecurityHeaders(_headerTemplateCache,!0)}return _headerTemplateCache}export function createSecurityHeaders(){return new Headers(securityHeadersTemplate())}function jsonSecurityHeadersTemplate(){if(!_jsonHeaderTemplateCache){_jsonHeaderTemplateCache=new Headers(securityHeadersTemplate());_jsonHeaderTemplateCache.set("Content-Type","application/json;charset=utf-8")}return _jsonHeaderTemplateCache}export function createJsonSecurityHeaders(){return new Headers(jsonSecurityHeadersTemplate())}export function secureSerializedJsonResponse(body,bodyLength,requestId,csrfCookie){const baseHeaders=jsonSecurityHeadersTemplate();if(bodyLength===void 0){const templates=_jsonMetadataHeaderTemplates??={};if(!requestId&&!csrfCookie)return new Response(body,templates.plain??={headers:baseHeaders});if(csrfCookie){const responseInit=requestId?templates.withCsrfAndRequestId??={headers:new Headers(baseHeaders)}:templates.withCsrf??={headers:new Headers(baseHeaders)};responseInit.headers.set("Set-Cookie",csrfCookie);if(requestId)responseInit.headers.set("X-Request-ID",requestId);return new Response(body,responseInit)}const responseInit=templates.withRequestId??={headers:new Headers(baseHeaders)};if(requestId)responseInit.headers.set("X-Request-ID",requestId);return new Response(body,responseInit)}const templates=_jsonLengthHeaderTemplateCache??=new Map;let lengthTemplates=templates.get(bodyLength);if(!lengthTemplates&&templates.size<JSON_LENGTH_HEADER_CACHE_LIMIT){const headers=new Headers(baseHeaders);headers.set("Content-Length",String(bodyLength));lengthTemplates={plain:{headers}};templates.set(bodyLength,lengthTemplates)}if(lengthTemplates){if(csrfCookie){const responseInit=requestId?lengthTemplates.withCsrfAndRequestId??={headers:new Headers(lengthTemplates.plain.headers)}:lengthTemplates.withCsrf??={headers:new Headers(lengthTemplates.plain.headers)};responseInit.headers.set("Set-Cookie",csrfCookie);if(requestId)responseInit.headers.set("X-Request-ID",requestId);return new Response(body,responseInit)}if(!requestId)return new Response(body,lengthTemplates.plain);const responseInit=lengthTemplates.withRequestId??={headers:new Headers(lengthTemplates.plain.headers)};responseInit.headers.set("X-Request-ID",requestId);return new Response(body,responseInit)}const response=new Response(body,{headers:baseHeaders});response.headers.set("Content-Length",String(bodyLength));if(requestId)response.headers.set("X-Request-ID",requestId);if(csrfCookie)response.headers.append("Set-Cookie",csrfCookie);return response}export function __resetSecurityHeadersCache(){_isProductionCache=void 0;_isDisabledCache=void 0;_cspCache=void 0;_headerTemplateCache=void 0;_jsonHeaderTemplateCache=void 0;_jsonMetadataHeaderTemplates=void 0;_jsonLengthHeaderTemplateCache=void 0}
|
package/dist/signed-url.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{timingSafeEqual}from"node:crypto";import{Buffer}from"node:buffer";import process from"node:process";import{url as buildUrl}from"./stacks-router";const SIGNATURE_PARAM="signature",EXPIRES_PARAM="expires";function getSigningSecret(){const secret=process.env.APP_KEY||process.env.STACKS_SIGNED_URL_SECRET;if(!secret||secret.length<16)throw Error("[router] signed URLs require APP_KEY (\u2265 16 chars) or STACKS_SIGNED_URL_SECRET. "+"Run `./buddy key:generate` or set the env var.");return secret}function hmacHex(payload,secret){return new Bun.CryptoHasher("sha256",secret).update(payload).digest("hex")}function safeEqualHex(a,b){const ba=Buffer.from(a.toLowerCase(),"hex"),bb=Buffer.from(b.toLowerCase(),"hex");if(ba.length!==bb.length)return!1;return timingSafeEqual(ba,bb)}function stripSignatureParam(input){const u=new URL(input.toString());u.searchParams.delete(SIGNATURE_PARAM);return u}function buildSignaturePayload(input){const stripped=stripSignatureParam(input);stripped.searchParams.sort();return stripped.toString()}export function signUrl(input,options={}){const secret=getSigningSecret(),url=input.startsWith("http")?new URL(input):new URL(input.startsWith("/")?input:`/${input}`,process.env.APP_URL||"https://localhost");if(options.ttl!==void 0&&options.expiresAt!==void 0)warnOnce("dual-expiry","[router] signUrl: both `ttl` and `expiresAt` provided - using `ttl`.");if(options.ttl!==void 0)url.searchParams.set(EXPIRES_PARAM,String(Math.floor(Date.now()/1000)+options.ttl));else if(options.expiresAt!==void 0)url.searchParams.set(EXPIRES_PARAM,String(Math.floor(options.expiresAt)));const payload=buildSignaturePayload(url);url.searchParams.set(SIGNATURE_PARAM,hmacHex(payload,secret));return url.toString()}
|
|
1
|
+
import{timingSafeEqual}from"node:crypto";import{Buffer}from"node:buffer";import process from"node:process";import{url as buildUrl}from"./stacks-router";const SIGNATURE_PARAM="signature",EXPIRES_PARAM="expires";function getSigningSecret(){const secret=process.env.APP_KEY||process.env.STACKS_SIGNED_URL_SECRET;if(!secret||secret.length<16)throw Error("[router] signed URLs require APP_KEY (\u2265 16 chars) or STACKS_SIGNED_URL_SECRET. "+"Run `./buddy key:generate` or set the env var.");return secret}function hmacHex(payload,secret){return new Bun.CryptoHasher("sha256",secret).update(payload).digest("hex")}function safeEqualHex(a,b){const ba=Buffer.from(a.toLowerCase(),"hex"),bb=Buffer.from(b.toLowerCase(),"hex");if(ba.length!==bb.length)return!1;return timingSafeEqual(ba,bb)}function stripSignatureParam(input){const u=new URL(input.toString());u.searchParams.delete(SIGNATURE_PARAM);return u}function buildSignaturePayload(input){const stripped=stripSignatureParam(input);stripped.searchParams.sort();return stripped.toString()}export function signUrl(input,options={}){const secret=getSigningSecret(),url=input.startsWith("http")?new URL(input):new URL(input.startsWith("/")?input:`/${input}`,process.env.APP_URL||"https://localhost");if(options.ttl!==void 0&&options.expiresAt!==void 0)warnOnce("dual-expiry","[router] signUrl: both `ttl` and `expiresAt` provided - using `ttl`.");if(options.ttl!==void 0)url.searchParams.set(EXPIRES_PARAM,String(Math.floor(Date.now()/1000)+options.ttl));else if(options.expiresAt!==void 0)url.searchParams.set(EXPIRES_PARAM,String(Math.floor(options.expiresAt)));const payload=buildSignaturePayload(url);url.searchParams.set(SIGNATURE_PARAM,hmacHex(payload,secret));return url.toString()}let _warnedKeys;function warnOnce(key,message){if(_warnedKeys?.has(key))return;(_warnedKeys??=new Set).add(key);console.warn(message)}export function signedUrl(routeName,params={},options={}){return signUrl(buildUrl(routeName,params),options)}export function verifySignedUrl(input){const url=typeof input==="string"?new URL(input,process.env.APP_URL||"https://localhost"):input,presented=url.searchParams.get(SIGNATURE_PARAM);if(!presented)return{valid:!1,reason:"missing-signature"};const expiresRaw=url.searchParams.get(EXPIRES_PARAM);if(expiresRaw!==null){const expires=Number.parseInt(expiresRaw,10);if(!Number.isFinite(expires)||Date.now()/1000>expires)return{valid:!1,reason:"expired"}}try{const expected=hmacHex(buildSignaturePayload(url),getSigningSecret());return safeEqualHex(presented,expected)?{valid:!0}:{valid:!1,reason:"invalid-signature"}}catch{return{valid:!1,reason:"invalid-signature"}}}export async function verifySignedUrlMiddleware(req){const result=verifySignedUrl(req.url);if(result.valid)return;const status=result.reason==="expired"?410:401;throw Response.json({error:result.reason},{status})}
|
package/dist/stacks-router.d.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import type { Server } from 'bun';
|
|
2
1
|
import './request-augmentation';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import type { ActionHandler, ActionPath, EnhancedRequest, ExtractRouteParams, KnownRouteName, MiddlewareHandler as BunMiddlewareHandler, MiddlewareReference, PathForRouteName, RequestFor, Route, ServerOptions } from '@stacksjs/bun-router';
|
|
6
|
-
import type { ActionResult, ActionValidations, ValidationResult } from '@stacksjs/actions';
|
|
2
|
+
import type { ActionPath, EnhancedRequest, MiddlewareReference, RequestFor, Route } from '@stacksjs/bun-router';
|
|
3
|
+
import type { ActionResult } from '@stacksjs/actions';
|
|
7
4
|
/**
|
|
8
5
|
* Run every registered boot hook, once per process.
|
|
9
6
|
*
|
|
@@ -24,218 +21,9 @@ export declare function warnOnMultipleRouterInstances(): boolean;
|
|
|
24
21
|
* overlapping constrained, domain-scoped, or mixed-parameter route expected.
|
|
25
22
|
*/
|
|
26
23
|
export declare function shouldUseNativeRoutesByDefault(routes: readonly Route[]): boolean;
|
|
27
|
-
// False positive: this is an overload signature, which has no body for its
|
|
28
|
-
// parameters to be used in. The implementation below uses them.
|
|
29
|
-
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
30
|
-
export declare function url<TName extends KnownRouteName>(routeName: TName, ...params: RequiresUrlParams<TName> extends true ? [params: UrlParams<TName>] : [params?: UrlParams<TName>]): string;
|
|
31
|
-
/**
|
|
32
|
-
* List the placeholder names a named route expects — handy for
|
|
33
|
-
* codegen/test cases and for detecting typos before runtime.
|
|
34
|
-
*/
|
|
35
|
-
export declare function routeParams(routeName: string): string[];
|
|
36
|
-
/**
|
|
37
|
-
* Every named route, as `name → path`.
|
|
38
|
-
*
|
|
39
|
-
* `listRegisteredRoutes()` reports a name by searching the named registry for a
|
|
40
|
-
* matching path, which answers "what is this route called" and cannot answer
|
|
41
|
-
* "what routes are there names for" - two routes on one path give the first
|
|
42
|
-
* name found, and a name whose route was never registered is invisible.
|
|
43
|
-
*
|
|
44
|
-
* The type generator needs the second question: it writes this map into the
|
|
45
|
-
* router's type registry so `url('users.shwo')` stops compiling. Reading the
|
|
46
|
-
* registry directly is the only way to get every name.
|
|
47
|
-
*/
|
|
48
|
-
export declare function listNamedRoutes(): Record<string, string>;
|
|
49
|
-
/**
|
|
50
|
-
* Snapshot of the registered routes — `{ method, path, name?, handler? }` per
|
|
51
|
-
* route. Used by `buddy route:list`, the dev-server startup banner, and the
|
|
52
|
-
* OpenAPI generator.
|
|
53
|
-
*
|
|
54
|
-
* `handler` is the string a route was registered with (`'Actions/…'`), and it
|
|
55
|
-
* is absent for an inline function, which has no file to read anything out of.
|
|
56
|
-
* The OpenAPI generator needs it to find the action's `validations`: it used to
|
|
57
|
-
* guess the path by title-casing the route *name* (`posts.store` →
|
|
58
|
-
* `Actions/PostsStoreAction`), which found a schema for the handful of routes
|
|
59
|
-
* following that convention and silently described every other endpoint as
|
|
60
|
-
* taking no input at all. The registry has known the real answer the whole
|
|
61
|
-
* time; it was simply not being reported.
|
|
62
|
-
*/
|
|
63
|
-
export declare function listRegisteredRoutes(): Array<{ method: string, path: string, name?: string, handler?: string, action?: RouterAction }>;
|
|
64
|
-
/**
|
|
65
|
-
* The merged middleware alias map: the framework defaults, with the
|
|
66
|
-
* application's own map on top.
|
|
67
|
-
*
|
|
68
|
-
* Exported for diagnostics and tests. A copy, so a caller cannot edit the
|
|
69
|
-
* cached map out from under the router.
|
|
70
|
-
*/
|
|
71
|
-
export declare function middlewareAliases(): Promise<Record<string, string>>;
|
|
72
|
-
/*.ts` as the single middleware registry instead of asking
|
|
73
|
-
* the application to define a second set of anonymous page-only functions.
|
|
74
|
-
*/
|
|
75
|
-
export declare function loadMiddlewareHandlers(): Promise<Record<string, MiddlewareHandler>>;
|
|
76
|
-
/**
|
|
77
|
-
* Clear the middleware cache (useful for hot-reload in development).
|
|
78
|
-
*
|
|
79
|
-
* `installMiddlewareHotReload()` will wire this up automatically when
|
|
80
|
-
* called from the dev server — production should never invoke it.
|
|
81
|
-
*/
|
|
82
|
-
export declare function clearMiddlewareCache(): void;
|
|
83
|
-
/**
|
|
84
|
-
* Watch `app/Middleware/` and `app/Middleware.ts` and invalidate the
|
|
85
|
-
* cached middleware modules whenever a file changes. Intended for the
|
|
86
|
-
* dev server only — calling this in production is a no-op (the
|
|
87
|
-
* watcher handle is created but never fires anything user code cares
|
|
88
|
-
* about). Returns a `disposer()` to stop watching.
|
|
89
|
-
*
|
|
90
|
-
* Without this hook, editing a middleware file in dev requires a
|
|
91
|
-
* full server restart to see the change — the import map caches the
|
|
92
|
-
* old version forever.
|
|
93
|
-
*/
|
|
94
|
-
export declare function installMiddlewareHotReload(): () => void;
|
|
95
|
-
/**
|
|
96
|
-
* Drop every registered route → middleware mapping.
|
|
97
|
-
*
|
|
98
|
-
* The registry is module-scoped and lives for the life of the process, which
|
|
99
|
-
* is what `listRegisteredRoutes()` and the boot-time validators read. That is
|
|
100
|
-
* correct at runtime — routes are registered once at boot — but it means a
|
|
101
|
-
* test that registers a route keeps it visible to every later caller in the
|
|
102
|
-
* same process, including `assertRouteMiddlewareResolvable()`. A suite that
|
|
103
|
-
* deliberately registers an unresolvable alias therefore made an unrelated
|
|
104
|
-
* file's `serverResponse()` throw on routes it never declared.
|
|
105
|
-
*
|
|
106
|
-
* `clearMiddlewareCache()` deliberately does NOT do this: it flushes resolved
|
|
107
|
-
* middleware so a hot-reloaded file is re-read, while the routes themselves
|
|
108
|
-
* must survive. Clearing the route table is a separate, coarser action.
|
|
109
|
-
*/
|
|
110
|
-
export declare function clearRouteMiddlewareRegistry(): void;
|
|
111
|
-
/**
|
|
112
|
-
* Resolve every middleware alias referenced by a registered route and
|
|
113
|
-
* report the ones that don't load. `csrf` is always checked too — it's
|
|
114
|
-
* auto-injected on unsafe methods even when no route lists it.
|
|
115
|
-
*
|
|
116
|
-
* Resolution is inherently lazy (`.middleware(name)` is a sync chainable
|
|
117
|
-
* that just records a string; the alias map and middleware modules load
|
|
118
|
-
* via async dynamic import), so a throw at literal registration time is
|
|
119
|
-
* impossible. Calling this after all routes are registered — the end of
|
|
120
|
-
* `importRoutes()` and the compiled-binary boot in core/server — IS
|
|
121
|
-
* effectively registration-time validation. See stacksjs/stacks#1957.
|
|
122
|
-
*/
|
|
123
|
-
export declare function findUnresolvableRouteMiddleware(): Promise<Array<{ alias: string, routes: string[] }>>;
|
|
124
|
-
/**
|
|
125
|
-
* Throw when any registered route references a middleware alias that
|
|
126
|
-
* cannot be resolved. Fail-closed boot validation: a typo'd `auth` alias
|
|
127
|
-
* must abort startup loudly, not serve the route unprotected (the
|
|
128
|
-
* request-time guard in createMiddlewareHandler 500s as a backstop).
|
|
129
|
-
*/
|
|
130
|
-
export declare function assertRouteMiddlewareResolvable(): Promise<void>;
|
|
131
|
-
/**
|
|
132
|
-
* Whether this request is asking "would this be accepted?" rather than
|
|
133
|
-
* "do this" (stacksjs/stacks#2226).
|
|
134
|
-
*
|
|
135
|
-
* An Action's `validations:` block was a server-only artifact: nothing could
|
|
136
|
-
* read it from a template or a client script, so every form in every app
|
|
137
|
-
* retyped the rules in the browser and the two copies drifted. The framework's
|
|
138
|
-
* own defaults demonstrated it — the browser refused a 7-character password
|
|
139
|
-
* that `POST /register` would have accepted.
|
|
140
|
-
*
|
|
141
|
-
* Rather than serialise the rules (a `schema.*` chain is a live object with
|
|
142
|
-
* no faithful JSON projection), the client asks the real endpoint to run the
|
|
143
|
-
* real rules and stops short of the side effect. Same shape as Laravel
|
|
144
|
-
* Precognition, and deliberately header-compatible with it.
|
|
145
|
-
*
|
|
146
|
-
* `?_validate=1` is accepted too: a header cannot be set on a plain form
|
|
147
|
-
* submission or a `<script client>` block using a bare fetch shorthand, and
|
|
148
|
-
* requiring one would have put this out of reach of the simplest caller.
|
|
149
|
-
*/
|
|
150
|
-
export declare function precognitionRequest(req: EnhancedRequest, knownHeader?: string | null): { only: string[] } | null;
|
|
151
|
-
/**
|
|
152
|
-
* The "nothing to report" answer to a precognition request. 204 rather than an
|
|
153
|
-
* empty 200 so a client cannot mistake it for the action's real result.
|
|
154
|
-
*
|
|
155
|
-
* `Vary` because the same URL and method now has two different answers
|
|
156
|
-
* depending on a request header — without it a shared cache is entitled to
|
|
157
|
-
* serve this 204 to a caller that meant to submit.
|
|
158
|
-
*/
|
|
159
|
-
export declare function precognitionSuccess(): Response;
|
|
160
|
-
/** Whether a route handler is an action rather than a plain function. */
|
|
161
|
-
export declare function isRouterAction(handler: unknown): handler is RouterAction;
|
|
162
|
-
/**
|
|
163
|
-
* Turn a resolved action into the function the route actually runs.
|
|
164
|
-
*
|
|
165
|
-
* Split out of the string-resolution path so registering an action by import
|
|
166
|
-
* (`createTypedRouter().get('/x', ShowAction)`) and registering it by name
|
|
167
|
-
* (`route.get('/x', 'Actions/ShowAction')`) share one runtime path. The two
|
|
168
|
-
* forms differ only in when the module is loaded and in what the compiler can
|
|
169
|
-
* see; everything below - validation, precognition, `authorize`, `before`,
|
|
170
|
-
* result formatting, error reporting - is the same code for both.
|
|
171
|
-
*
|
|
172
|
-
* `handlerKey` identifies the action for the CSRF skip cache and for error
|
|
173
|
-
* labels. It is the handler string when there is one, and the route key
|
|
174
|
-
* otherwise.
|
|
175
|
-
*/
|
|
176
|
-
export declare function wrapAction(action: RouterAction, handlerKey: string): RouteHandlerFn;
|
|
177
|
-
export declare function validateActionInput(req: EnhancedRequest, validations: ActionValidations): Promise<ValidationResult>;
|
|
178
|
-
export declare function stream(source: ReadableStream | AsyncIterable<string | Uint8Array>, options?: StreamOptions): Response;
|
|
179
|
-
/** Test / hot-reload seam: forget the resolved module. */
|
|
180
|
-
export declare function clearCsrfModuleCache(): void;
|
|
181
|
-
export declare function enhanceRequest(req: EnhancedRequest, initializeRequestId?: boolean): EnhancedRequest;
|
|
182
|
-
/**
|
|
183
|
-
* Create a Stacks-enhanced router
|
|
184
|
-
*/
|
|
185
|
-
export declare function createStacksRouter(config?: StacksRouterConfig): StacksRouterInstance;
|
|
186
|
-
/**
|
|
187
|
-
* Tell the file-based view renderer where this application keeps its templates.
|
|
188
|
-
*
|
|
189
|
-
* Two different programs render `.stx` in a Stacks app and they were not
|
|
190
|
-
* agreeing. `buddy dev` and the production server go through
|
|
191
|
-
* `bun-plugin-stx`'s own `serve()`, which is handed the components, layouts and
|
|
192
|
-
* partials directories explicitly. `route.serve()` goes through bun-router's
|
|
193
|
-
* file routing, which was handed nothing - so its components directory fell
|
|
194
|
-
* back to `<viewsDir>/components`, and `resources/components`, where every
|
|
195
|
-
* Stacks app actually keeps them, was never looked in.
|
|
196
|
-
*
|
|
197
|
-
* The failure is silent in the direction that hides bugs. The page still
|
|
198
|
-
* answers 200; the component is replaced inline with
|
|
199
|
-
* `[Error loading component: ENOENT ...]`. So a test that boots the router and
|
|
200
|
-
* asserts on rendered HTML cannot see any component at all, and reads as though
|
|
201
|
-
* the feature under test were missing rather than the harness.
|
|
202
|
-
*
|
|
203
|
-
* Only fills in what has not been set: an application that called
|
|
204
|
-
* `route.bunRouter.views(...)` itself has said something more specific, and
|
|
205
|
-
* this must not overwrite it.
|
|
206
|
-
*
|
|
207
|
-
* **It deliberately does not set `viewsPath`.** Naming one switches on
|
|
208
|
-
* file-based route discovery, which walks the whole views tree - so an
|
|
209
|
-
* application that never asked for file routing would start doing it because a
|
|
210
|
-
* directory happened to exist. Where the views are is already decided
|
|
211
|
-
* elsewhere; the only thing missing was where the *components* are.
|
|
212
|
-
*/
|
|
213
|
-
export declare function configureViewDirectories(bunRouter: Router): void;
|
|
214
|
-
/*.ts` works.
|
|
215
|
-
*
|
|
216
|
-
* The two functions are coupled and the coupling is bun-router's, not ours:
|
|
217
|
-
* `serve()` calls {@link configureViewDirectories} after this, and that only
|
|
218
|
-
* backs off because `disableFileRouting()` happens to leave a non-empty
|
|
219
|
-
* `_fileRoutingConfig` behind. Were that flag ever to move to a field of its
|
|
220
|
-
* own, `configureViewDirectories` would overwrite the config and re-mount every
|
|
221
|
-
* template. `api-view-routing.test.ts` drives both, in that order, so the
|
|
222
|
-
* upgrade that changes it fails a test rather than quietly restoring the site
|
|
223
|
-
* to the API port.
|
|
224
|
-
*
|
|
225
|
-
* @returns whether file routing was switched off by this call.
|
|
226
|
-
*/
|
|
227
|
-
export declare function disableViewRouting(bunRouter: Router): boolean;
|
|
228
|
-
/**
|
|
229
|
-
* Handle a server request through the router
|
|
230
|
-
* This is the main entry point for the Stacks server
|
|
231
|
-
*/
|
|
232
|
-
export declare function serverResponse(request: Request, _body?: string): Promise<Response>;
|
|
233
|
-
// Export serve function that uses the default router
|
|
234
|
-
export declare function serve(options?: ServerOptions): Promise<Server<unknown>>;
|
|
235
24
|
declare const CSRF_DEFAULT: 0;
|
|
236
25
|
declare const CSRF_SKIPPED: 1;
|
|
237
26
|
declare const CSRF_REQUIRED: 2;
|
|
238
|
-
export declare const route: StacksRouterInstance;
|
|
239
27
|
/**
|
|
240
28
|
* Warn (once per process) when more than one @stacksjs/router module has loaded
|
|
241
29
|
* (stacksjs/stacks#1975 / #1982). Routing still works — the route table and
|
|
@@ -274,17 +62,13 @@ export declare interface BootHook {
|
|
|
274
62
|
name: string
|
|
275
63
|
run: () => void | Promise<void>
|
|
276
64
|
}
|
|
277
|
-
declare interface StacksRouterConfig {
|
|
65
|
+
export declare interface StacksRouterConfig {
|
|
278
66
|
verbose?: boolean
|
|
279
67
|
apiPrefix?: string
|
|
68
|
+
autoDiscoverRoutes?: boolean
|
|
280
69
|
requestIds?: boolean
|
|
281
70
|
csrf?: boolean
|
|
282
71
|
}
|
|
283
|
-
declare interface GroupOptions {
|
|
284
|
-
prefix?: string
|
|
285
|
-
middleware?: MiddlewareReference | MiddlewareReference[]
|
|
286
|
-
apiResponse?: boolean
|
|
287
|
-
}
|
|
288
72
|
/** The file-name half each CRUD action is composed from. */
|
|
289
73
|
declare interface ResourceKindOf {
|
|
290
74
|
index: 'Index'
|
|
@@ -303,119 +87,12 @@ export declare interface ChainableRoute {
|
|
|
303
87
|
requireCsrf: () => ChainableRoute
|
|
304
88
|
rateLimit: (max: number, window: 'second' | 'minute' | 'hour' | 'day' | number) => ChainableRoute
|
|
305
89
|
}
|
|
306
|
-
/** Represents a middleware module with a handle method */
|
|
307
|
-
declare interface MiddlewareHandler {
|
|
308
|
-
handle: (req: EnhancedRequest) => Promise<void> | void
|
|
309
|
-
priority?: number
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Anything the router is willing to treat as an action.
|
|
313
|
-
*
|
|
314
|
-
* Structural rather than `instanceof Action`, because an action can also be a
|
|
315
|
-
* plain object with a `handle()` - the framework's own defaults include both
|
|
316
|
-
* shapes - and because a duplicated install would make an `instanceof` check
|
|
317
|
-
* reject a perfectly good action from the other copy of the package.
|
|
318
|
-
*/
|
|
319
|
-
export declare interface RouterAction {
|
|
320
|
-
handle: (req: any) => unknown
|
|
321
|
-
validations?: ActionValidations
|
|
322
|
-
authorize?: (req: any) => unknown
|
|
323
|
-
before?: (req: any) => unknown
|
|
324
|
-
skipCsrf?: boolean
|
|
325
|
-
csrf?: boolean
|
|
326
|
-
apiResponse?: boolean
|
|
327
|
-
model?: unknown
|
|
328
|
-
modelDefinition?: unknown
|
|
329
|
-
name?: string
|
|
330
|
-
responses?: unknown
|
|
331
|
-
responseHeaders?: unknown
|
|
332
|
-
requestHeaders?: unknown
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Helper for streaming responses — wraps a `ReadableStream` or async
|
|
336
|
-
* generator with the right headers for the chosen content type.
|
|
337
|
-
*
|
|
338
|
-
* Common shapes:
|
|
339
|
-
*
|
|
340
|
-
* ```ts
|
|
341
|
-
* // Server-Sent Events
|
|
342
|
-
* return stream(async function* () {
|
|
343
|
-
* for await (const evt of source) yield `data: ${JSON.stringify(evt)}\n\n`
|
|
344
|
-
* }, { type: 'sse' })
|
|
345
|
-
*
|
|
346
|
-
* // Chunked JSON (NDJSON) — one JSON object per line
|
|
347
|
-
* return stream(async function* () {
|
|
348
|
-
* for await (const row of rows) yield `${JSON.stringify(row)}\n`
|
|
349
|
-
* }, { type: 'ndjson' })
|
|
350
|
-
*
|
|
351
|
-
* // Raw bytes — caller supplies a ReadableStream of Uint8Array chunks
|
|
352
|
-
* return stream(myReadable, { contentType: 'application/octet-stream' })
|
|
353
|
-
* ```
|
|
354
|
-
*
|
|
355
|
-
* The wrapper sets `Cache-Control: no-cache` and `Connection: keep-alive`
|
|
356
|
-
* for SSE — the two headers a sane proxy / browser pair won't ignore — and
|
|
357
|
-
* leaves backpressure / cancellation to the underlying stream.
|
|
358
|
-
*
|
|
359
|
-
* See stacksjs/stacks#1870 R-4.
|
|
360
|
-
*/
|
|
361
|
-
export declare interface StreamOptions {
|
|
362
|
-
type?: 'sse' | 'ndjson'
|
|
363
|
-
contentType?: string
|
|
364
|
-
headers?: HeadersInit
|
|
365
|
-
status?: number
|
|
366
|
-
}
|
|
367
|
-
export declare interface StacksRouterInstance {
|
|
368
|
-
bunRouter: Router
|
|
369
|
-
routes: Route[]
|
|
370
|
-
get: {
|
|
371
|
-
<TPath extends string>(path: TPath, handler: TypedInlineRouteHandler<TPath>): ChainableRoute
|
|
372
|
-
(path: string, handler: StacksHandler): ChainableRoute
|
|
373
|
-
}
|
|
374
|
-
post: {
|
|
375
|
-
<TPath extends string>(path: TPath, handler: TypedInlineRouteHandler<TPath>): ChainableRoute
|
|
376
|
-
(path: string, handler: StacksHandler): ChainableRoute
|
|
377
|
-
}
|
|
378
|
-
put: {
|
|
379
|
-
<TPath extends string>(path: TPath, handler: TypedInlineRouteHandler<TPath>): ChainableRoute
|
|
380
|
-
(path: string, handler: StacksHandler): ChainableRoute
|
|
381
|
-
}
|
|
382
|
-
patch: {
|
|
383
|
-
<TPath extends string>(path: TPath, handler: TypedInlineRouteHandler<TPath>): ChainableRoute
|
|
384
|
-
(path: string, handler: StacksHandler): ChainableRoute
|
|
385
|
-
}
|
|
386
|
-
delete: {
|
|
387
|
-
<TPath extends string>(path: TPath, handler: TypedInlineRouteHandler<TPath>): ChainableRoute
|
|
388
|
-
(path: string, handler: StacksHandler): ChainableRoute
|
|
389
|
-
}
|
|
390
|
-
options: {
|
|
391
|
-
<TPath extends string>(path: TPath, handler: TypedInlineRouteHandler<TPath>): ChainableRoute
|
|
392
|
-
(path: string, handler: StacksHandler): ChainableRoute
|
|
393
|
-
}
|
|
394
|
-
group: (options: GroupOptions, callback: () => void | Promise<void>) => StacksRouterInstance | Promise<StacksRouterInstance>
|
|
395
|
-
resource: <TBase extends string, TOnly extends ResourceAction = never, TExcept extends ResourceAction = never>(
|
|
396
|
-
name: string,
|
|
397
|
-
handler: TBase & ResourceBaseCheck<TBase, ActiveResourceActions<TOnly, TExcept>>,
|
|
398
|
-
options?: {
|
|
399
|
-
only?: readonly TOnly[]
|
|
400
|
-
except?: readonly TExcept[]
|
|
401
|
-
middleware?: MiddlewareReference | MiddlewareReference[]
|
|
402
|
-
},
|
|
403
|
-
) => StacksRouterInstance
|
|
404
|
-
match: {
|
|
405
|
-
<TPath extends string>(methods: string[], path: TPath, handler: TypedInlineRouteHandler<TPath>): ChainableRoute
|
|
406
|
-
(methods: string[], path: string, handler: StacksHandler): ChainableRoute
|
|
407
|
-
}
|
|
408
|
-
health: () => StacksRouterInstance
|
|
409
|
-
use: (middleware: ActionHandler | BunMiddlewareHandler) => StacksRouterInstance
|
|
410
|
-
register: (routePath: string, options?: { prefix?: string, middleware?: MiddlewareReference | MiddlewareReference[] }) => Promise<StacksRouterInstance>
|
|
411
|
-
booting: (name: string, run: () => void | Promise<void>) => StacksRouterInstance
|
|
412
|
-
serve: (options?: ServerOptions) => Promise<Server<unknown>>
|
|
413
|
-
handleRequest: (req: Request) => Promise<Response>
|
|
414
|
-
getAllowedMethods: (pathname: string, domain?: string) => string[]
|
|
415
|
-
importRoutes: () => Promise<void>
|
|
416
|
-
loadDiscoveredRoutes: () => Promise<void>
|
|
417
|
-
}
|
|
418
90
|
declare type RouteHandlerFn = (_req: EnhancedRequest) => Response | Promise<Response>;
|
|
91
|
+
declare type AsyncRouteHandlerFn = (
|
|
92
|
+
req: EnhancedRequest,
|
|
93
|
+
preparedBaseResult?: Response | Promise<Response>,
|
|
94
|
+
hasPreparedBaseResult?: boolean,
|
|
95
|
+
) => Promise<Response>;
|
|
419
96
|
/**
|
|
420
97
|
* An inline handler, as a route file writes one.
|
|
421
98
|
*
|
|
@@ -477,98 +154,3 @@ declare type ResourceBaseCheck<TBase extends string, TActive extends ResourceAct
|
|
|
477
154
|
? unknown
|
|
478
155
|
: { 'these actions do not exist': MissingResourceActions<TBase, TActive> }
|
|
479
156
|
declare type RouteCsrfMode = typeof CSRF_DEFAULT | typeof CSRF_SKIPPED | typeof CSRF_REQUIRED;
|
|
480
|
-
/**
|
|
481
|
-
* Generate a full URL for a named route, like Laravel's route() helper.
|
|
482
|
-
*
|
|
483
|
-
* Validates path parameters at call time so a typo'd argument
|
|
484
|
-
* (`url('user.post', { userId: 1 })` against `/users/{id}`) throws
|
|
485
|
-
* immediately with a list of expected names instead of silently
|
|
486
|
-
* producing a URL with `{id}` left literal in the path.
|
|
487
|
-
*
|
|
488
|
-
* @example
|
|
489
|
-
* ```typescript
|
|
490
|
-
* // Define a named route
|
|
491
|
-
* route.get('/api/email/unsubscribe', 'Actions/UnsubscribeAction').name('email.unsubscribe')
|
|
492
|
-
*
|
|
493
|
-
* // Generate URL
|
|
494
|
-
* url('email.unsubscribe', { token: 'abc-123' })
|
|
495
|
-
* // → https://stacksjs.com/api/email/unsubscribe?token=abc-123
|
|
496
|
-
*
|
|
497
|
-
* // With path parameters
|
|
498
|
-
* route.get('/users/{id}/posts/{postId}', handler).name('user.post')
|
|
499
|
-
* url('user.post', { id: 42, postId: 7 })
|
|
500
|
-
* // → https://stacksjs.com/users/42/posts/7
|
|
501
|
-
* ```
|
|
502
|
-
*/
|
|
503
|
-
/**
|
|
504
|
-
* The params a named route needs, plus anything else that becomes query string.
|
|
505
|
-
*
|
|
506
|
-
* The required half comes from the path the name resolves to, so
|
|
507
|
-
* `url('user.post', { id: 42 })` against `/users/{id}/posts/{postId}` stops
|
|
508
|
-
* compiling instead of throwing at call time. Extra keys stay allowed: the
|
|
509
|
-
* implementation appends whatever it did not consume as a query string, which
|
|
510
|
-
* is what `url('email.unsubscribe', { token })` relies on.
|
|
511
|
-
*/
|
|
512
|
-
export type UrlParams<TName extends string> = { [K in keyof ExtractRouteParams<PathForRouteName<TName>>]: string | number }
|
|
513
|
-
& Record<string, string | number>;
|
|
514
|
-
/** The keys of `T` that are not optional. */
|
|
515
|
-
declare type RequiredParamKeys<T> = { [K in keyof T]-?: object extends Pick<T, K> ? never : K }[keyof T];
|
|
516
|
-
/**
|
|
517
|
-
* Whether `url()` must be given a second argument.
|
|
518
|
-
*
|
|
519
|
-
* Keyed on REQUIRED params: a path whose only placeholder is optional is
|
|
520
|
-
* reachable with nothing at all, and demanding an empty object for it would be
|
|
521
|
-
* the type getting in the way of the truth.
|
|
522
|
-
*/
|
|
523
|
-
declare type RequiresUrlParams<TName extends string> = [RequiredParamKeys<ExtractRouteParams<PathForRouteName<TName>>>] extends [never] ? false : true;
|
|
524
|
-
/**
|
|
525
|
-
* Run an action's declarative `validations:` against the request.
|
|
526
|
-
*
|
|
527
|
-
* The action path uses this synchronous core because input collection and the
|
|
528
|
-
* validator contract are synchronous. The exported wrapper below stays
|
|
529
|
-
* Promise-based for compatibility with callers that chain or await it.
|
|
530
|
-
*/
|
|
531
|
-
declare type ActionRuleTest = (value: unknown) => boolean;
|
|
532
|
-
declare type ActionValidationEntry = [string, ActionValidations[string], ActionRuleTest[]?];
|
|
533
|
-
/**
|
|
534
|
-
* Per-request read-routing context.
|
|
535
|
-
*
|
|
536
|
-
* The database package tracks writes per async context so that a request
|
|
537
|
-
* which reads back what it just wrote is never served from a read replica
|
|
538
|
-
* (replication is asynchronous, so the row may not be there yet). That
|
|
539
|
-
* tracking only works if something establishes the context at the request
|
|
540
|
-
* boundary — without this, `contextHasWritten()` has no store to consult,
|
|
541
|
-
* always reports false, and every read routes to a replica including the
|
|
542
|
-
* one immediately following a write. The safety rule would be dead code.
|
|
543
|
-
*
|
|
544
|
-
* Loaded lazily rather than imported: `@stacksjs/database` already depends
|
|
545
|
-
* on `@stacksjs/router`, so a static import here would close a package
|
|
546
|
-
* cycle. The dynamic import is resolved once and cached, and the fallback
|
|
547
|
-
* is a plain pass-through so a build without the database package (or an
|
|
548
|
-
* older one) still serves requests.
|
|
549
|
-
*/
|
|
550
|
-
declare type ContextDispatcher = <T, A>(fn: (arg: A) => T, arg: A) => T;
|
|
551
|
-
declare type NativeRouteHandler = (request: Request) => Response | Promise<Response>;
|
|
552
|
-
declare type NativeRouteTable = Record<string, Record<string, NativeRouteHandler>>;
|
|
553
|
-
declare type NativeResponseFinalizer = (response: Response, request: Request) => Response | Promise<Response>;
|
|
554
|
-
/**
|
|
555
|
-
* FIFO-bounded Map. Wraps `Map` with a hard size cap; on overflow,
|
|
556
|
-
* the oldest entry (Map insertion order) is evicted. Used for the
|
|
557
|
-
* router's small framework-internal caches whose size is normally
|
|
558
|
-
* bounded by action count, but which had no upper limit before —
|
|
559
|
-
* tests that instantiate many short-lived routers would leak entries
|
|
560
|
-
* across `createStacksRouter()` calls (stacksjs/stacks#1863 T-8).
|
|
561
|
-
*
|
|
562
|
-
* Insertion-order LRU is appropriate here because the access pattern
|
|
563
|
-
* is "set once at action-load time, then many reads" — refreshing on
|
|
564
|
-
* get would buy nothing since reads dominate.
|
|
565
|
-
*/
|
|
566
|
-
declare class BoundedMap<K, V> {
|
|
567
|
-
constructor(max: number);
|
|
568
|
-
get(key: K): V | undefined;
|
|
569
|
-
has(key: K): boolean;
|
|
570
|
-
set(key: K, value: V): this;
|
|
571
|
-
delete(key: K): boolean;
|
|
572
|
-
clear(): void;
|
|
573
|
-
get size(): number;
|
|
574
|
-
}
|
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{
|
|
1
|
+
var {require}=import.meta;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{log,report}from"@stacksjs/logging";import{appPath,frameworkPath,projectPath,storagePath}from"@stacksjs/path/project";import{applyRequestEnhancements,applyResponseCompression,Router,runWithRequest as runWithBunRouterRequest}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}const NATIVE_ROUTE_METHODS=new Set(["GET","POST","PUT","PATCH","DELETE","OPTIONS","HEAD"]),NATIVE_ROUTE_PARAM=/^\{[A-Z_$][\w$]*\}$/i;function hasNativeCompatiblePath(path){if(path==="*")return!0;const segments=path.split("/");for(let index=0;index<segments.length;index++){const segment=segments[index];if(segment===""||NATIVE_ROUTE_PARAM.test(segment))continue;if(segment==="*"&&index===segments.length-1)continue;if(segment.includes("{")||segment.includes(":")||segment.includes("*"))return!1}return!0}export function shouldUseNativeRoutesByDefault(routes){let hasEligibleRoute=!1;for(const route of routes){if(!NATIVE_ROUTE_METHODS.has(route.method)||route.handler instanceof Response)continue;hasEligibleRoute=!0;if(route.nativeDispatch===!1||route.domain||route.constraints&&Object.keys(route.constraints).length>0||!hasNativeCompatiblePath(route.path))return!1}return hasEligibleRoute}import{runWithRequestArgument,runWithRequestArguments}from"./request-context";import{isApiRequest,JSON_CONTENT_TYPE}from"./api-shape";import{createErrorResponse,createMiddlewareErrorResponse}from"./error-handler";import{applySecurityHeaders,createJsonSecurityHeaders,secureSerializedJsonResponse}from"./security-headers";import{isCursorPaginator,isPaginator,isSimplePaginator}from"@stacksjs/pagination";const stacksRouteHandlers=new WeakSet;function rememberStacksRouteHandler(handler){stacksRouteHandlers.add(handler);return handler}const CSRF_DEFAULT=0,CSRF_SKIPPED=1,CSRF_REQUIRED=2,routeCsrfRegistry=new Map,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"),FRAMEWORK_RESPONSE_METADATA_APPLIED=Symbol("stacks.router.frameworkResponseMetadataApplied"),FRAMEWORK_RESPONSE_BODY_LENGTH=Symbol("stacks.router.frameworkResponseBodyLength"),CSRF_SECURE_TRANSPORT=Symbol.for("@stacksjs/router:csrf-secure-transport"),secureNativeRouteRouters=new WeakSet,csrfEnabledNativeRouteRouters=new WeakSet,EMPTY_MIDDLEWARE_TIMINGS=[],EMPTY_MIDDLEWARE_ENTRIES=[],CSRF_ONLY_MIDDLEWARE=["csrf"];let resolvedMiddlewareEntryCache=new Map,resolvedCsrfOnlyMiddleware;const 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()}}export async function loadMiddlewareHandlers(){const aliases=await getMiddlewareAliases(),generated=await getMiddlewareRegistry(),references=new Set([...Object.keys(aliases),...Object.values(aliases),...Object.keys(generated??{})]),handlers={};await Promise.all([...references].map(async(reference)=>{const handler=await loadMiddleware(reference);if(handler)handlers[reference]=handler}));return handlers}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}function loadParsedMiddleware(parsed){const cached=middlewareCache.get(parsed.name);if(cached!==void 0)return cached&&parsed.negated?negateMiddleware(parsed.name,cached):cached;return loadMiddleware(parsed.name).then((handler)=>handler&&parsed.negated?negateMiddleware(parsed.name,handler):handler)}export function clearMiddlewareCache(){middlewareCache.clear();negatedMiddlewareCache.clear();resolvedMiddlewareEntryCache=new Map;resolvedCsrfOnlyMiddleware=void 0;parsedMiddlewareCache=new Map;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(){for(const middleware of routeMiddlewareRegistry.values())middleware.length=0;routeMiddlewareRegistry.clear()}const routeApiResponseRegistry=new Set;let parsedMiddlewareCache=new Map;function parseMiddlewareEntry(middleware){const cache=parsedMiddlewareCache;let parsed=cache.get(middleware);if(!parsed){parsed=parseMiddlewareEntryUncached(middleware).then((resolved)=>{cache.set(middleware,resolved);return resolved},(error)=>{cache.delete(middleware);throw error});cache.set(middleware,parsed)}return parsed}async function parseMiddlewareEntryUncached(middleware){const timingName=middleware.replace(/[^A-Za-z0-9_-]/g,"_").slice(0,32),negated=middleware.startsWith("!"),bare=negated?middleware.slice(1):middleware,aliases=await getMiddlewareAliases();if(Object.hasOwn(aliases,bare))return{name:bare,negated,timingName};const colonIndex=bare.indexOf(":");if(colonIndex===-1)return{name:bare,negated,timingName};return{name:bare.substring(0,colonIndex),negated,timingName,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,timingName:"csrf"},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,csrfEnabled=!0){const wrappedBase=wrapHandler(handler,!0,routeKey),routeMiddleware=routeMiddlewareRegistry.get(routeKey)??EMPTY_MIDDLEWARE_ENTRIES,routeCsrf=routeCsrfRegistry.get(routeKey),routeRateLimit=routeRateLimitRegistry.get(routeKey),routeMethod=routeKey.slice(0,routeKey.indexOf(":")).toUpperCase(),routeAcceptsCsrf=csrfEnabled&&CSRF_PROTECTED_METHODS.has(routeMethod),routeMayHaveBody=routeMethod!=="GET"&&routeMethod!=="HEAD",routeRendersCsrf=csrfEnabled&&(routeMethod==="GET"||routeMethod==="HEAD"),routeSeedsCsrf=csrfEnabled&&(routeMethod==="GET"||routeMethod==="HEAD"||routeMethod==="OPTIONS"),forcesJsonByGroup=routeApiResponseRegistry.has(routeKey),directActionSkipsCsrf=isRouterAction(handler)&&(handler.skipCsrf===!0||handler.csrf===!1),handlerKey=typeof handler==="string"?handler:void 0,synchronousInlineHandler=typeof handler==="function"&&handler.constructor.name!=="AsyncFunction",asynchronousInlineHandler=typeof handler==="function"&&handler.constructor.name==="AsyncFunction"?handler:void 0;let actionPrefetch=null;if(typeof handler==="string"&&routeAcceptsCsrf){const pending=resolveStringHandler(handler);if(pending instanceof Promise){const settled=()=>{actionPrefetch=null};actionPrefetch=pending.then(settled,settled)}}const finishSynchronousResult=(req,response,csrfHandledByOuter)=>{const frameworkMetadataApplied=response[FRAMEWORK_RESPONSE_METADATA_APPLIED]===!0;if(response.status>=400||req._corsConfig||req._compress===!0||req._startNs!=null||Array.isArray(req._afterResponse)||req._responseHeaders)return;if(frameworkMetadataApplied&&!routeSeedsCsrf)return response;let finalized=response;const csrfSeededDuringFormatting=req[CSRF_SEEDED_BY_HANDLE_REQUEST]===!0;if(routeSeedsCsrf&&!csrfHandledByOuter&&!csrfSeededDuringFormatting){const mod=loadCsrfModule();if(mod instanceof Promise)return;if(mod)finalized=mod.seedCsrfCookieIfMissing(req,finalized,req._csrfToken,frameworkMetadataApplied)}if(!frameworkMetadataApplied)try{const requestId=req._requestId;if(requestId)finalized.headers.set("X-Request-ID",requestId);applySecurityHeaders(finalized.headers)}catch{return}return finalized},handleAsyncInContext=async(req,preparedBaseResult,hasPreparedBaseResult=!1)=>{if(routeMayHaveBody)try{await parseRequestBody(req)}catch(err){const error=err instanceof Error?err:Error(String(err));return createMiddlewareErrorResponse(error,req)}const enhancedReq=req,csrfHandledByOuter=enhancedReq[CSRF_SEEDED_BY_HANDLE_REQUEST]===!0;if(!csrfHandledByOuter&&routeRendersCsrf&&requestMayRenderHtml(enhancedReq)){const renderTokenSeeding=seedCsrfTokenForRender(enhancedReq);if(renderTokenSeeding)await renderTokenSeeding}if(actionPrefetch)await actionPrefetch;if(forcesJsonByGroup)req._forceJson=!0;if(!hasPreparedBaseResult&&synchronousInlineHandler&&!routeAcceptsCsrf&&routeMiddleware.length===0&&!routeRateLimit?.config){preparedBaseResult=runWithRequestArgument(enhancedReq,wrappedBase,enhancedReq);hasPreparedBaseResult=!0;if(preparedBaseResult instanceof Response){const finished=finishSynchronousResult(enhancedReq,preparedBaseResult,csrfHandledByOuter);if(finished)return finished}}const rl=routeRateLimit?.config;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=routeMiddleware;let shouldInjectCsrf=!1;if(routeAcceptsCsrf){const alreadyHasCsrf=userMiddleware.some((m)=>m==="csrf"||m.startsWith("csrf:")),routeCsrfMode=routeCsrf?.mode??CSRF_DEFAULT,routeSkipped=routeCsrfMode===CSRF_SKIPPED,routeRequired=routeCsrfMode===CSRF_REQUIRED,actionSkipped=directActionSkipsCsrf||(handlerKey?actionSkipsCsrfCache.get(handlerKey)===!0:!1);shouldInjectCsrf=!alreadyHasCsrf&&(routeRequired||!routeSkipped&&!actionSkipped)}const middlewareEntries=shouldInjectCsrf?userMiddleware.length===0?CSRF_ONLY_MIDDLEWARE:["csrf",...userMiddleware]:userMiddleware;let middlewareTimings=EMPTY_MIDDLEWARE_TIMINGS;if(middlewareEntries.length>0){if(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 cachedCsrfOnlyMiddleware=middlewareEntries===CSRF_ONLY_MIDDLEWARE?resolvedCsrfOnlyMiddleware:void 0,resolved=cachedCsrfOnlyMiddleware??[],unresolvedEntries=cachedCsrfOnlyMiddleware?EMPTY_MIDDLEWARE_ENTRIES:middlewareEntries;for(const middlewareEntry of unresolvedEntries){let resolvedEntry=resolvedMiddlewareEntryCache.get(middlewareEntry);if(!resolvedEntry){const pending=parseMiddlewareEntry(middlewareEntry),parsed=pending instanceof Promise?await pending:pending,loaded=loadParsedMiddleware(parsed),middleware=loaded instanceof Promise?await loaded:loaded;if(middleware&&typeof middleware.handle==="function"){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);resolvedEntry={name:middlewareEntry,timingName:parsed.timingName,handler:middleware,priority,parameterName:parsed.params?parsed.name:void 0,params:parsed.params};resolvedMiddlewareEntryCache.set(middlewareEntry,resolvedEntry)}}if(!resolvedEntry){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)}if(resolvedEntry.params&&resolvedEntry.parameterName){enhancedReq._middlewareParams=enhancedReq._middlewareParams||{};enhancedReq._middlewareParams[resolvedEntry.parameterName]=resolvedEntry.params}resolved.push(resolvedEntry)}if(middlewareEntries===CSRF_ONLY_MIDDLEWARE&&!cachedCsrfOnlyMiddleware&&resolved.length===1)resolvedCsrfOnlyMiddleware=resolved;if(resolved.length>1)resolved.sort((a,b)=>a.priority-b.priority);const collectsMiddlewareTimings=enhancedReq._startNs!=null;if(collectsMiddlewareTimings)middlewareTimings=[];let chainTimer,chainBudget,runningMiddleware="";try{for(const{name:middlewareName,timingName,handler:middleware}of resolved){const mwStart=collectsMiddlewareTimings?performance.now():0;runningMiddleware=middlewareName;try{const outcome=middleware.handle(enhancedReq);if(outcome&&typeof outcome.then==="function"){if(!chainBudget){chainBudget=new Promise((_,reject)=>{chainTimer=setTimeout(()=>reject(Error(`Middleware '${runningMiddleware}' exceeded ${MIDDLEWARE_TIMEOUT_MS}ms`)),MIDDLEWARE_TIMEOUT_MS)});chainBudget.catch(()=>{})}await Promise.race([outcome,chainBudget])}if(collectsMiddlewareTimings){const elapsedMs=performance.now()-mwStart;middlewareTimings.push({name:timingName,ms:elapsedMs})}}catch(error){if(collectsMiddlewareTimings){const elapsedMs=performance.now()-mwStart;middlewareTimings.push({name:timingName,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)parts.push(`mw_${t.name};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)parts.push(`mw_${t.name};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 formatsResolvedInlineResult=asynchronousInlineHandler!==void 0&&!hasPreparedBaseResult,baseResult=hasPreparedBaseResult?preparedBaseResult:asynchronousInlineHandler?asynchronousInlineHandler(enhancedReq):wrappedBase(enhancedReq),resolvedResult=baseResult instanceof Response?baseResult:await baseResult;let response=formatsResolvedInlineResult?formatResult(resolvedResult,enhancedReq):resolvedResult;if(response){const csrfSeededDuringFormatting=enhancedReq[CSRF_SEEDED_BY_HANDLE_REQUEST]===!0;if(routeSeedsCsrf&&!csrfHandledByOuter&&!csrfSeededDuringFormatting)try{const mod=loadCsrfModule(),csrf=mod instanceof Promise?await mod:mod;if(csrf)response=csrf.seedCsrfCookieIfMissing(enhancedReq,response,enhancedReq._csrfToken,response[FRAMEWORK_RESPONSE_METADATA_APPLIED]===!0)}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,{_afterResponse:after,_responseHeaders:requested}=enhancedReq,frameworkMetadataApplied=response&&response[FRAMEWORK_RESPONSE_METADATA_APPLIED]===!0;if(response&&(response.status>=400||Array.isArray(after)||requested&&typeof requested==="object"||durMs!=null||!frameworkMetadataApplied)&&typeof response.headers?.set==="function"){const setHeaders=(h)=>{if(Array.isArray(after))for(const callback of after)try{if(typeof callback==="function")callback({status:response.status,durationMs:durMs??0})}catch{}if(requested&&typeof requested==="object"){for(const[name,value]of Object.entries(requested))if(typeof value==="string")h.set(name,value)}if(reqId&&!frameworkMetadataApplied)h.set("X-Request-ID",reqId);if(durMs!=null){let timing=`total;dur=${durMs.toFixed(1)}`;for(const t of middlewareTimings)timing+=`, mw_${t.name};dur=${t.ms.toFixed(1)}`;h.set("Server-Timing",timing)}if(!frameworkMetadataApplied)applySecurityHeaders(h)};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},handleAsync=(req,preparedBaseResult,hasPreparedBaseResult=!1)=>runWithRequestArguments(req,handleAsyncInContext,req,preparedBaseResult,hasPreparedBaseResult);if(!synchronousInlineHandler||routeMayHaveBody)return rememberStacksRouteHandler(handleAsync);return rememberStacksRouteHandler((req)=>{if(routeMiddleware.length!==0||routeRateLimit?.config)return handleAsync(req);const csrfHandledByOuter=req[CSRF_SEEDED_BY_HANDLE_REQUEST]===!0;if(routeRendersCsrf&&!csrfHandledByOuter&&requestMayRenderHtml(req)){const renderTokenSeeding=seedCsrfTokenForRender(req);if(renderTokenSeeding)return renderTokenSeeding.then(()=>handleAsync(req))}if(forcesJsonByGroup)req._forceJson=!0;let preparedBaseResult;try{preparedBaseResult=runWithRequestArgument(req,wrappedBase,req)}catch(error){return Promise.reject(error)}if(preparedBaseResult instanceof Response){const finished=finishSynchronousResult(req,preparedBaseResult,csrfHandledByOuter);if(finished)return finished}return handleAsync(req,preparedBaseResult,!0)})}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,routeCsrf=routeCsrfRegistry.get(routeKey),routeRateLimit=routeRateLimitRegistry.get(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(){if(routeCsrf)routeCsrf.mode=CSRF_SKIPPED;return chain},requireCsrf(){if(routeCsrf)routeCsrf.mode=CSRF_REQUIRED;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);if(routeRateLimit)routeRateLimit.config={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).then((handler)=>{_resolvedHandlerCache.set(handlerPath,handler);return handler},(error)=>{_resolvedHandlerCache.delete(handlerPath);throw error});_resolvedHandlerCache.set(handlerPath,resolved)}return resolved}function validationFailureResponse(errors){return response.validationError(errors)}export function precognitionRequest(req,knownHeader){const header=knownHeader===void 0?req.headers?.get?.("Precognition"):knownHeader,viaHeader=typeof header==="string"&&header.toLowerCase()==="true";let viaQuery=!1;try{if(!viaHeader&&req.url.includes("?"))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),actionValidationEntries=action.validations?compileActionValidationEntries(action.validations):void 0,routeSeparator=handlerKey.indexOf(":"),actionMayHaveRouteParams=routeSeparator===-1||handlerKey.indexOf("{",routeSeparator+1)!==-1,prepareRequest=(req)=>{const precognitionHeader=req.headers.get("Precognition"),requestHasQuery=req.url.includes("?"),precognition=precognitionHeader===null&&!requestHasQuery?null:precognitionRequest(req,precognitionHeader);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=validateActionInputSync(req,rules,rules===action.validations?actionValidationEntries:void 0,actionMayHaveRouteParams,!0,requestHasQuery);return precognitionResult.valid?precognitionSuccess():validationFailureResponse(precognitionResult.errors)}if(action.validations){const validationResult=validateActionInputSync(req,action.validations,actionValidationEntries,actionMayHaveRouteParams,!0,requestHasQuery);if(!validationResult.valid)return validationFailureResponse(validationResult.errors)}},rethrowActionError=(handleError)=>{report(handleError,{label:`[Router] action.handle() for '${handlerKey}'`});throw handleError};if(typeof action.authorize!=="function"&&typeof action.before!=="function")return(req)=>{if(actionSkipsCsrf)req._skipCsrf=!0;if(actionForcesJson)req._forceJson=!0;req._requestValidationRules=requestValidationRules;try{const earlyResponse=prepareRequest(req);if(earlyResponse)return earlyResponse;const result=action.handle(req);return result instanceof Promise?result.then((value)=>{try{return formatResult(value,req)}catch(error){return rethrowActionError(error)}},rethrowActionError):formatResult(result,req)}catch(handleError){return rethrowActionError(handleError)}};return async(req)=>{if(actionSkipsCsrf)req._skipCsrf=!0;if(actionForcesJson)req._forceJson=!0;req._requestValidationRules=requestValidationRules;try{const earlyResponse=prepareRequest(req);if(earlyResponse)return earlyResponse;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 pendingResult=action.handle(req),result=pendingResult instanceof Promise?await pendingResult:pendingResult;return formatResult(result,req)}catch(handleError){return rethrowActionError(handleError)}}}const EMPTY_VALIDATION_ERRORS=Object.freeze({}),VALID_ACTION_VALIDATION_RESULT=Object.freeze({valid:!0,errors:EMPTY_VALIDATION_ERRORS});function propertyOwner(value,property){let current=value;while(current){if(Object.hasOwn(current,property))return current;current=Object.getPrototypeOf(current)}}function compileActionValidationEntries(validations){return Object.entries(validations).map(([field,validation])=>{const rule=validation.rule;if(typeof rule.getRules!=="function"||propertyOwner(rule,"validate")!==propertyOwner(rule,"getRules"))return[field,validation];const rules=rule.getRules();if(!Array.isArray(rules)||rules.some((entry)=>typeof entry?.test!=="function"))return[field,validation];return[field,validation,rules.map((entry)=>entry.test)]})}function validateActionInputSync(req,validations,compiledEntries,mayHaveRouteParams=!0,reuseSuccessResult=!1,requestHasQuery=req.url.includes("?")){let errors;const validated={};let valid=!0;const entries=compiledEntries??Object.entries(validations),input=getRequestInput(req,entries,mayHaveRouteParams,requestHasQuery);for(const[field,validation,ruleTests]of entries){const value=input[field];let result;try{const rule=validation.rule;let passedCompiledRules=!1;if(ruleTests){const empty=value===void 0||value===null||value==="";passedCompiledRules=empty&&rule.isRequired!==!0;if(!empty){passedCompiledRules=!0;for(let i=0;i<ruleTests.length;i++)if(!ruleTests[i](value)){passedCompiledRules=!1;break}}}if(!passedCompiledRules)result=rule.validate(value)}catch{result={valid:!1,errors:[{message:`${field} validation failed`}]}}if(result&&!result.valid){valid=!1;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}else if(value!==void 0)validated[field]=value}if(valid)req._validatedInput=validated;if(valid&&reuseSuccessResult)return VALID_ACTION_VALIDATION_RESULT;return{valid,errors:errors??{}}}export async function validateActionInput(req,validations){return validateActionInputSync(req,validations)}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}function getRequestInput(req,validationEntries,mayHaveRouteParams=!0,requestHasQuery=req.url.includes("?")){let input,mayNeedCoercion=!1;if(requestHasQuery){const q=req.query;if(q)for(const key in q){(input??={})[key]=q[key];mayNeedCoercion=!0}else new URL(req.url).searchParams.forEach((value,key)=>{(input??={})[key]=value;mayNeedCoercion=!0})}let hasRouteParams=!1;if(mayHaveRouteParams&&req.params)for(const key in req.params){if(!Object.hasOwn(req.params,key))continue;(input??={})[key]=req.params[key];hasRouteParams=!0;mayNeedCoercion=!0}if(req.jsonBody&&typeof req.jsonBody==="object")input=mayNeedCoercion?Object.assign(input??{},req.jsonBody):req.jsonBody;else if(req.formBody&&typeof req.formBody==="object"){input=mayNeedCoercion?Object.assign(input??{},req.formBody):{...req.formBody};mayNeedCoercion=!0}if(!requestHasQuery&&!mayHaveRouteParams&&req.jsonBody&&typeof req.jsonBody==="object"&&!Array.isArray(req.jsonBody)&&!req.formBody&&!req.files)req._allInputCache=input;if(req.files&&typeof req.allFiles==="function")try{const files=req.allFiles();for(const key of Object.keys(files??{})){const values=input??={};if(!(key in values))values[key]=files[key]}}catch{}if(!validationEntries)return input??{};input??={};let coerced=!1;if(mayNeedCoercion)for(const[field,validation]of validationEntries){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;coerced=!0}}else if(validatorName==="boolean"){if(value==="true"||value==="1"){input[field]=!0;coerced=!0}else if(value==="false"||value==="0"){input[field]=!1;coerced=!0}}}if(!coerced&&!req.formBody&&!req.files&&!hasRouteParams)req._allInputCache=input;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"}});if(result!==null&&typeof result==="object"){const linkHeader="data"in result?buildPaginatorLinkHeader(result):null;return formatJsonResult(result,req,linkHeader)}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(apiShaped){if(typeof result==="string"||typeof result==="number"||typeof result==="boolean")return formatJsonResult(result,req);return Response.json(result)}return new Response(String(result),{headers:{"Content-Type":"text/plain; charset=utf-8"}})}function formatJsonResult(result,req,linkHeader){const canPreapplyMetadata=!req._responseHeaders,body=JSON.stringify(result)??"",bodyLength=Buffer.byteLength(body),requestMarkers=req,csrf=requestMarkers[CSRF_SEEDED_BY_HANDLE_REQUEST]!==!0&&typeof requestMarkers[CSRF_SECURE_TRANSPORT]==="boolean"?loadCsrfModule():null,csrfCookie=canPreapplyMetadata&&csrf&&!(csrf instanceof Promise)&&typeof csrf.createCsrfCookie==="function"?csrf.createCsrfCookie(req,req._csrfToken):void 0,response=canPreapplyMetadata?secureSerializedJsonResponse(body,bodyLength,req._requestId,csrfCookie):new Response(body,{headers:createJsonSecurityHeaders()});if(!canPreapplyMetadata)response.headers.set("Content-Length",String(bodyLength));if(linkHeader)response.headers.set("Link",linkHeader);if(canPreapplyMetadata){const frameworkResponse=response;frameworkResponse[FRAMEWORK_RESPONSE_METADATA_APPLIED]=!0;frameworkResponse[FRAMEWORK_RESPONSE_BODY_LENGTH]=bodyLength}if(csrfCookie)requestMarkers[CSRF_SEEDED_BY_HANDLE_REQUEST]=!0;return response}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 nativeRequestText=Request.prototype.text,nativeRequestJson=Request.prototype.json,nativeRequestBytes=Request.prototype.bytes,nativeRequestArrayBuffer=Request.prototype.arrayBuffer,nativeRequestBlob=Request.prototype.blob,nativeRequestClone=Request.prototype.clone,requestBodyEncoder=new TextEncoder,REQUEST_METHODS={text(){return this._rawBody===void 0?nativeRequestText.call(this):Promise.resolve(this._rawBody)},json(){if(this._rawBody===void 0)return nativeRequestJson.call(this);try{return Promise.resolve(this._rawBody.length===0?null:JSON.parse(this._rawBody))}catch(error){return Promise.reject(error)}},bytes(){return this._rawBody===void 0?nativeRequestBytes.call(this):Promise.resolve(requestBodyEncoder.encode(this._rawBody))},arrayBuffer(){if(this._rawBody===void 0)return nativeRequestArrayBuffer.call(this);const bytes=requestBodyEncoder.encode(this._rawBody);return Promise.resolve(bytes.buffer.slice(bytes.byteOffset,bytes.byteOffset+bytes.byteLength))},blob(){return this._rawBody===void 0?nativeRequestBlob.call(this):Promise.resolve(new Blob([this._rawBody],{type:this._rawBodyContentType}))},clone(){if(this._rawBody===void 0)return nativeRequestClone.call(this);return new Request(this.url,{method:this.method,headers:this.headers,body:this._rawBody})},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,STACKS_REQUEST_ENHANCED=Symbol.for("stacks.router.requestEnhanced");function fuseRequestEnhancements(router,initializeRequestIds=!0){const original=router.enhanceRequest.bind(router),combinedPrototypes=new WeakMap;router.enhanceRequest=(request,params={})=>{const basePrototype=Object.getPrototypeOf(request),combined=combinedPrototypes.get(basePrototype);if(combined){const enhanced=request;if(enhanced.params!==params)if("params"in request)Object.defineProperty(enhanced,"params",{value:params,writable:!0,configurable:!0,enumerable:!1});else enhanced.params=params;Object.setPrototypeOf(enhanced,combined);if(initializeRequestIds&&!enhanced._requestId)enhanced._requestId=incomingRequestId(enhanced)??generateRequestId();return enhanced}const enhanced=enhanceRequest(original(request,params),initializeRequestIds);combinedPrototypes.set(basePrototype,Object.getPrototypeOf(enhanced));return enhanced}}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 requestMayRenderHtml(req){return req.headers.get("accept")?.includes("text/html")===!0||req.headers.get("sec-fetch-dest")==="document"}function seedCsrfTokenForRender(req,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,initializeRequestId=!0){const routeParams=req.params??{};req.params=routeParams;if(initializeRequestId&&!req._requestId)req._requestId=incomingRequestId(req)??generateRequestId();if(req[STACKS_REQUEST_ENHANCED]===!0)return req;applyRequestEnhancements(req,routeParams);const routerPrototype=Object.getPrototypeOf(req);let stacksPrototype=stacksRequestPrototypes.get(routerPrototype);if(!stacksPrototype){stacksPrototype=Object.assign(Object.create(routerPrototype),REQUEST_METHODS);Object.defineProperty(stacksPrototype,STACKS_REQUEST_ENHANCED,{value:!0});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}const requestIdPrefix=crypto.randomUUID().replaceAll("-","").slice(0,16),REQUEST_ID_DIGITS="0123456789abcdefghijklmnopqrstuvwxyz",REQUEST_ID_SUFFIX_COUNT=1296,requestIdSuffixes=Array.from({length:REQUEST_ID_SUFFIX_COUNT},(_,value)=>REQUEST_ID_DIGITS[Math.floor(value/36)]+REQUEST_ID_DIGITS[value%36]);let requestIdBlock=0,requestIdSuffix=0,requestIdBlockPrefix=`${requestIdPrefix}-0`;function generateRequestId(){requestIdSuffix++;if(requestIdSuffix===REQUEST_ID_SUFFIX_COUNT){requestIdSuffix=0;requestIdBlockPrefix=`${requestIdPrefix}-${(++requestIdBlock).toString(36)}`}return requestIdBlockPrefix+requestIdSuffixes[requestIdSuffix]}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)}const pending=resolveStringHandler(handlerPath);return await(pending instanceof Promise?await pending:pending)(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 parseRequestBody(req){if(req._bodyParsed)return;req._bodyParsed=!0;const contentType=req.headers.get("content-type")||"";try{if(contentType==="application/json"||JSON_CONTENT_TYPE.test(contentType)){const raw=await req.text();req._rawBody=raw;req._rawBodyContentType=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}),csrfEnabled=config.csrf!==!1;if(csrfEnabled)csrfEnabledNativeRouteRouters.add(bunRouter);fuseRequestEnhancements(bunRouter,config.requestIds!==!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)routeMiddlewareRegistry.set(routeKey,[...currentGroupMiddleware]);if(!shadowed)routeCsrfRegistry.set(routeKey,{mode:CSRF_DEFAULT});if(!shadowed)routeRateLimitRegistry.set(routeKey,{});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,csrfEnabled));return createChainableRoute(routeKey,shadowed)},post(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("POST",path,handler);bunRouter.post(fullPath,createMiddlewareHandler(routeKey,handler,csrfEnabled));return createChainableRoute(routeKey,shadowed)},put(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("PUT",path,handler);bunRouter.put(fullPath,createMiddlewareHandler(routeKey,handler,csrfEnabled));return createChainableRoute(routeKey,shadowed)},patch(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("PATCH",path,handler);bunRouter.patch(fullPath,createMiddlewareHandler(routeKey,handler,csrfEnabled));return createChainableRoute(routeKey,shadowed)},delete(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("DELETE",path,handler);bunRouter.delete(fullPath,createMiddlewareHandler(routeKey,handler,csrfEnabled));return createChainableRoute(routeKey,shadowed)},options(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("OPTIONS",path,handler);bunRouter.options(fullPath,createMiddlewareHandler(routeKey,handler,csrfEnabled));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,csrfEnabled);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);if(csrfEnabled)wrapHandleRequestForCsrf(bunRouter);await wrapHandleRequestForDatabaseContext(bunRouter);await runBootHooks();const production=(process.env.APP_ENV??"").toLowerCase()==="production"||process.env.NODE_ENV==="production",callerSuppliedRoutes=Object.prototype.hasOwnProperty.call(options,"routes"),nativeRoutes=options.nativeRoutes??(production&&!callerSuppliedRoutes&&shouldUseNativeRoutesByDefault(bunRouter.routes));if(options.tls)secureNativeRouteRouters.add(bunRouter);else secureNativeRouteRouters.delete(bunRouter);return bunRouter.serve(nativeRoutes===options.nativeRoutes?options:{...options,nativeRoutes})},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
|
-
`,error)}const ormRoutesCandidates=ormRoutesLoaded?[]:[
|
|
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),handleUnseededSafeRequest=async(request,rendersCsrf,cookieHeader)=>{if(rendersCsrf&&requestMayRenderHtml(request)){const seeding=seedCsrfTokenForRender(request,cookieHeader);if(seeding)await seeding}request[CSRF_SEEDED_BY_HANDLE_REQUEST]=!0;const response=await original(request);if(!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}},handleSafeRequest=(request,rendersCsrf)=>{const cookieHeader=request.headers.get("cookie")??"";if(cookieHeader.includes("X-CSRF-Token=")||cookieHeader.includes("csrf-token=")){request[CSRF_SEEDED_BY_HANDLE_REQUEST]=!0;return original(request)}return handleUnseededSafeRequest(request,rendersCsrf,cookieHeader)};router.handleRequest=(request)=>{const method=request.method;if(method==="GET"||method==="HEAD")return handleSafeRequest(request,!0);if(method==="OPTIONS")return handleSafeRequest(request,!1);return original(request)};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,routingContextDispatcher=null;const databaseContextWrappedRouters=new WeakSet;function applyDirectNativeCookies(router,response,request){const cookieChanges=request;return cookieChanges._cookiesToSet?.length||cookieChanges._cookiesToDelete?.length?router.applyModifiedCookies(response,request):response}function nativeRouteErrorResponse(error){return new Response(JSON.stringify({success:!1,message:"Internal Server Error",error:error instanceof Error?error.message:String(error)}),{status:500,headers:{"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST, PUT, DELETE, PATCH, OPTIONS","Access-Control-Allow-Headers":"Content-Type, Authorization, X-Requested-With, Accept, Origin"}})}function handleDirectNativeRouteError(router,error){console.error("Error handling request:",error);return router.errorHandler?router.errorHandler(error instanceof Error?error:Error(String(error))):nativeRouteErrorResponse(error)}function directNativePath(path){const segments=path.split("/");for(let index=0;index<segments.length;index++){const segment=segments[index];if(!segment)continue;const parameter=segment.match(/^\{([A-Z_$][\w$]*)\}$/i);if(parameter){segments[index]=`:${parameter[1]}`;continue}if(segment.includes("{")||segment.includes(":")||segment.includes("*"))return}return segments.join("/")}function buildDirectNativeHandlers(router,finalizeResponse){const nativeRouter=router,handlers=new Map;if(nativeRouter.globalMiddleware.length!==0)return handlers;for(const route of nativeRouter.routes){if(!NATIVE_ROUTE_METHODS.has(route.method)||route.nativeDispatch===!1||route.constraints&&Object.keys(route.constraints).length>0||(route.middleware?.length??0)!==0||typeof route.handler!=="function"||!stacksRouteHandlers.has(route.handler))continue;const handler=route.handler,nativePath=directNativePath(route.path);if(!nativePath)continue;const key=`${route.method}:${nativePath}`;if(handlers.has(key))continue;handlers.set(key,(request)=>{const nativeParams=request.params??{},enhancedRequest=nativeRouter.enhanceRequest(request,nativeParams);enhancedRequest.route=route;return runWithBunRouterRequest(enhancedRequest,()=>{try{const response=handler(enhancedRequest);return response instanceof Response?finalizeResponse(applyDirectNativeCookies(nativeRouter,response,enhancedRequest),request):response.then((result)=>finalizeResponse(applyDirectNativeCookies(nativeRouter,result,enhancedRequest),request),(error)=>{const result=handleDirectNativeRouteError(nativeRouter,error);return result instanceof Response?finalizeResponse(result,request):result.then((response)=>finalizeResponse(response,request))})}catch(error){const result=handleDirectNativeRouteError(nativeRouter,error);return result instanceof Response?finalizeResponse(result,request):result.then((response)=>finalizeResponse(response,request))}})})}return handlers}function wrapNativeRoutesForDatabaseContext(router,dispatchInRoutingContext){const nativeRouter=router;if(nativeRouter._nativeDatabaseContextWrapped||typeof nativeRouter._buildNativeRoutes!=="function")return;const buildNativeRoutes=nativeRouter._buildNativeRoutes.bind(nativeRouter);nativeRouter._buildNativeRoutes=()=>{const routes=buildNativeRoutes();if(!routes)return routes;const compression=nativeRouter.config.compression,compressionDisabled=compression?.enabled===!1,compressionThreshold=compression?.threshold??1024,finalizeResponse=(response,request)=>{const bodyLength=response[FRAMEWORK_RESPONSE_BODY_LENGTH];if(compressionDisabled||typeof bodyLength==="number"&&bodyLength<compressionThreshold)return response;if(bodyLength===void 0){const declaredLength=response.headers.get("content-length");if(declaredLength!==null){const length=Number(declaredLength);if(Number.isSafeInteger(length)&&length>=0&&length<compressionThreshold)return response}}return applyResponseCompression(response,request,compression)},directHandlers=buildDirectNativeHandlers(router,finalizeResponse),secureTransport=secureNativeRouteRouters.has(router),csrfEnabled=csrfEnabledNativeRouteRouters.has(router);for(const[path,methods]of Object.entries(routes))for(const[method,handler]of Object.entries(methods)){const safeMethod=method==="GET"||method==="HEAD"||method==="OPTIONS",directDispatch=directHandlers.get(`${method}:${path}`)??(method==="HEAD"?directHandlers.get(`GET:${path}`):void 0),dispatch=directDispatch??handler;methods[method]=(request)=>{if(safeMethod&&csrfEnabled){const cookie=request.headers.get("cookie")??"",markedRequest=request;if(cookie.includes("X-CSRF-Token=")||cookie.includes("csrf-token="))markedRequest[CSRF_SEEDED_BY_HANDLE_REQUEST]=!0;else markedRequest[CSRF_SECURE_TRANSPORT]=secureTransport}const response=dispatchInRoutingContext(dispatch,request);if(directDispatch)return response;return response instanceof Response?finalizeResponse(response,request):response.then((resolved)=>finalizeResponse(resolved,request))}}return routes};nativeRouter._nativeDatabaseContextWrapped=!0}async function wrapHandleRequestForDatabaseContext(router){if(databaseContextWrappedRouters.has(router))return;const dispatchInRoutingContext=await getRoutingContextDispatcher();if(databaseContextWrappedRouters.has(router))return;wrapNativeRoutesForDatabaseContext(router,dispatchInRoutingContext);const original=router.handleRequest.bind(router);router.handleRequest=(request)=>dispatchInRoutingContext(original,request);databaseContextWrappedRouters.add(router)}async function getRoutingContextDispatcher(){if(!routingContextDispatcher)try{const database=await import("@stacksjs/database"),dispatcher=database.runInDatabaseRoutingContext,runner=database.withDatabaseRoutingContext??database.withRoutingContext;routingContextDispatcher=typeof dispatcher==="function"?dispatcher:typeof runner==="function"?(fn,arg)=>runner(()=>fn(arg)):(fn,arg)=>fn(arg)}catch{routingContextDispatcher=(fn,arg)=>fn(arg)}return routingContextDispatcher}export async function serverResponse(request,_body){return(await getRoutingContextDispatcher())(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)}
|
|
4
|
+
${paths}`)}return!0}let __defaultsPkgRoot,__defaultsPathCache;function resolveDefaultsPath(rel){const cached=__defaultsPathCache?.get(rel);if(cached!==void 0)return cached;if(rel.startsWith("app/")){const published=appPath(rel.slice(4));if(existsSync(published)){(__defaultsPathCache??=new Map).set(rel,published);return published}}const vendored=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??=new Map).set(rel,resolved);return resolved}const NATIVE_ROUTE_PARAM=/^\{[A-Z_$][\w$]*\}$/i;function isNativeRouteMethod(method){return method==="GET"||method==="POST"||method==="PUT"||method==="PATCH"||method==="DELETE"||method==="OPTIONS"||method==="HEAD"}function hasNativeCompatiblePath(path){if(path==="*")return!0;const segments=path.split("/");for(let index=0;index<segments.length;index++){const segment=segments[index];if(segment===""||NATIVE_ROUTE_PARAM.test(segment))continue;if(segment==="*"&&index===segments.length-1)continue;if(segment.includes("{")||segment.includes(":")||segment.includes("*"))return!1}return!0}export function shouldUseNativeRoutesByDefault(routes){let hasEligibleRoute=!1;for(const route of routes){if(!isNativeRouteMethod(route.method)||route.handler instanceof Response)continue;hasEligibleRoute=!0;if(route.nativeDispatch===!1||route.domain||route.constraints&&Object.keys(route.constraints).length>0||!hasNativeCompatiblePath(route.path))return!1}return hasEligibleRoute}import{runWithRequestArgument,runWithRequestArguments}from"./request-context";import{isApiRequest,JSON_CONTENT_TYPE}from"./api-shape";import{createErrorResponse,createMiddlewareErrorResponse}from"./error-handler";import{applySecurityHeaders,createJsonSecurityHeaders,secureSerializedJsonResponse}from"./security-headers";import{isCursorPaginator,isPaginator,isSimplePaginator}from"@stacksjs/pagination";function bindAsyncRouteHandler(handler){return(req,preparedBaseResult,hasPreparedBaseResult=!1)=>runWithRequestArguments(req,handler,req,preparedBaseResult,hasPreparedBaseResult)}const stacksRouteHandlers=new WeakSet;function rememberStacksRouteHandler(handler){stacksRouteHandlers.add(handler);return handler}function invokeInlineHandler(req,handler,_unused){const result=handler(req);if(result instanceof Promise)return result.then((value)=>formatResult(value,req));return formatResult(result,req)}const CSRF_DEFAULT=0,CSRF_SKIPPED=1,CSRF_REQUIRED=2,ROUTE_ACCEPTS_CSRF=1,ROUTE_MAY_HAVE_BODY=2,ROUTE_RENDERS_CSRF=4,ROUTE_SEEDS_CSRF=8,ROUTE_FORCES_JSON=16,ROUTE_ACTION_SKIPS_CSRF=32,routeStateRegistries=new Set,clearedRouteKeysByRegistry=new WeakMap;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;constructor(max){this.max=max}get(key){return this.map?.get(key)}has(key){return this.map?.has(key)??!1}set(key,value){const map=this.map??=new Map;if(map.has(key))map.delete(key);map.set(key,value);if(map.size>this.max){const oldest=map.keys().next().value;if(oldest!==void 0)map.delete(oldest)}return this}delete(key){return this.map?.delete(key)??!1}clear(){this.map=void 0}get size(){return this.map?.size??0}}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{const encoder=new TextEncoder;return crypto.timingSafeEqual(encoder.encode(submitted),encoder.encode(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);let routeActionRegistry;const CSRF_SEEDED_BY_HANDLE_REQUEST=Symbol.for("stacks.router.csrfSeededByHandleRequest"),FRAMEWORK_RESPONSE_BODY_SIZE_UPPER_BOUND=Symbol("stacks.router.frameworkResponseBodySizeUpperBound"),CSRF_SECURE_TRANSPORT=Symbol.for("@stacksjs/router:csrf-secure-transport"),secureNativeRouteRouters=new WeakSet,csrfEnabledNativeRouteRouters=new WeakSet,EMPTY_MIDDLEWARE_TIMINGS=[],EMPTY_MIDDLEWARE_ENTRIES=[],CSRF_ONLY_MIDDLEWARE=["csrf"];let resolvedMiddlewareEntryCache,resolvedCsrfOnlyMiddleware,namedRouteRegistry;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){const availableRoutes=namedRouteRegistry?[...namedRouteRegistry.keys()].join(", "):"";throw Error(`Route '${routeName}' is not defined. Available routes: ${availableRoutes}`)}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={};if(namedRouteRegistry)for(const[name,named]of namedRouteRegistry)out[name]=named.path;return out}export function listRegisteredRoutes(){const out=[],seen=new Set;for(const registry of routeStateRegistries)for(const key of registry.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;if(namedRouteRegistry){for(const[n,named]of namedRouteRegistry)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;let _warnedInvalidPriorities;function warnInvalidMiddlewarePriority(name,raw){const key=`${name}:${String(raw)}`;if(_warnedInvalidPriorities?.has(key))return;(_warnedInvalidPriorities??=new Set).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}let middlewareCache;function cacheMiddleware(name,handler){(middlewareCache??=new Map).set(name,handler)}let middlewareAliasesPromise=null;async function getMiddlewareAliases(){if(middlewareAliasesPromise)return middlewareAliasesPromise;middlewareAliasesPromise=(async()=>{const merged={};for(const load of[()=>import(resolveDefaultsPath("app/Middleware.ts")),()=>import(appPath("Middleware.ts"))])try{const module=await load();Object.assign(merged,module.default??{})}catch{}return merged})();return middlewareAliasesPromise}const PASCAL_SPLIT_REGEX=/[-_\s]+/;let pascalCaseCache;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??=new Map).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=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){const cachedMiddleware=middlewareCache;if(cachedMiddleware?.has(name))return cachedMiddleware.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`);cacheMiddleware(name,null);return null}cacheMiddleware(name,handler);return handler}catch(err){log.error(`[Router] Failed to load middleware '${name}' from ${registered}:`,err);return null}let userPathError;try{const userPath=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`);cacheMiddleware(name,null);return null}cacheMiddleware(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`);cacheMiddleware(name,null);return null}cacheMiddleware(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()}}export async function loadMiddlewareHandlers(){const aliases=await getMiddlewareAliases(),generated=await getMiddlewareRegistry(),references=new Set([...Object.keys(aliases),...Object.values(aliases),...Object.keys(generated??{})]),handlers={};await Promise.all([...references].map(async(reference)=>{const handler=await loadMiddleware(reference);if(handler)handlers[reference]=handler}));return handlers}let negatedMiddlewareCache;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??=new Map).set(name,negated);return negated}function loadParsedMiddleware(parsed){const cached=middlewareCache?.get(parsed.name);if(cached!==void 0)return cached&&parsed.negated?negateMiddleware(parsed.name,cached):cached;return loadMiddleware(parsed.name).then((handler)=>handler&&parsed.negated?negateMiddleware(parsed.name,handler):handler)}export function clearMiddlewareCache(){middlewareCache=void 0;negatedMiddlewareCache=void 0;resolvedMiddlewareEntryCache=void 0;resolvedCsrfOnlyMiddleware=void 0;parsedMiddlewareCache=void 0;middlewareAliasesPromise=null;middlewareRegistryPromise=null;actionRegistryPromise=null;actionSkipsCsrfCache.clear();routeHandlerKeyRegistry.clear();routeActionRegistry=void 0;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=[appPath("Middleware"),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=[]}}export function clearRouteMiddlewareRegistry(){for(const registry of routeStateRegistries){const registeredKeys=clearedRouteKeysByRegistry.get(registry)??new Set;for(const routeKey of registry.keys())registeredKeys.add(routeKey);clearedRouteKeysByRegistry.set(registry,registeredKeys);for(const state of registry.values())if(state.middleware)state.middleware.length=0;registry.clear()}routeStateRegistries.clear()}let routeApiResponseRegistry,parsedMiddlewareCache;function parseMiddlewareEntry(middleware){const cache=parsedMiddlewareCache??=new Map;let parsed=cache.get(middleware);if(!parsed){parsed=parseMiddlewareEntryUncached(middleware).then((resolved)=>{cache.set(middleware,resolved);return resolved},(error)=>{cache.delete(middleware);throw error});cache.set(middleware,parsed)}return parsed}async function parseMiddlewareEntryUncached(middleware){const timingName=middleware.replace(/[^A-Za-z0-9_-]/g,"_").slice(0,32),negated=middleware.startsWith("!"),bare=negated?middleware.slice(1):middleware,aliases=await getMiddlewareAliases();if(Object.hasOwn(aliases,bare))return{name:bare,negated,timingName};const colonIndex=bare.indexOf(":");if(colonIndex===-1)return{name:bare,negated,timingName};return{name:bare.substring(0,colonIndex),negated,timingName,params:bare.substring(colonIndex+1)}}export async function findUnresolvableRouteMiddleware(){const usage=new Map;for(const registry of routeStateRegistries)for(const[routeKey,state]of registry){const entries=state.middleware;if(!entries)continue;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,timingName:"csrf"},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 finishSynchronousResult(req,response,csrfHandledByOuter,routeSeedsCsrf){const frameworkMetadataApplied=typeof response[FRAMEWORK_RESPONSE_BODY_SIZE_UPPER_BOUND]==="number";if(response.status>=400||req._corsConfig||req._compress===!0||req._startNs!=null||Array.isArray(req._afterResponse)||req._responseHeaders)return;if(frameworkMetadataApplied&&!routeSeedsCsrf)return response;let finalized=response;if(routeSeedsCsrf&&!csrfHandledByOuter&&req[CSRF_SEEDED_BY_HANDLE_REQUEST]!==!0){const mod=loadCsrfModule();if(mod instanceof Promise)return;if(mod)finalized=mod.seedCsrfCookieIfMissing(req,finalized,req._csrfToken,frameworkMetadataApplied)}if(!frameworkMetadataApplied)try{const requestId=req._requestId;if(requestId)finalized.headers.set("X-Request-ID",requestId);applySecurityHeaders(finalized.headers)}catch{return}return finalized}function routeCapabilityFlags(routeKey,handler,csrfEnabled){const routeMethod=routeKey.slice(0,routeKey.indexOf(":")).toUpperCase(),action=isRouterAction(handler)?handler:void 0,forcesJson=routeApiResponseRegistry?.has(routeKey)===!0||action?.apiResponse===!0;let flags=0;if(csrfEnabled&&(routeMethod==="POST"||routeMethod==="PUT"||routeMethod==="PATCH"||routeMethod==="DELETE"))flags|=ROUTE_ACCEPTS_CSRF;if(routeMethod!=="GET"&&routeMethod!=="HEAD")flags|=ROUTE_MAY_HAVE_BODY;if(csrfEnabled&&!forcesJson&&(routeMethod==="GET"||routeMethod==="HEAD"))flags|=ROUTE_RENDERS_CSRF;if(csrfEnabled&&(routeMethod==="GET"||routeMethod==="HEAD"||routeMethod==="OPTIONS"))flags|=ROUTE_SEEDS_CSRF;if(forcesJson)flags|=ROUTE_FORCES_JSON;if(action&&(action.skipCsrf===!0||action.csrf===!1))flags|=ROUTE_ACTION_SKIPS_CSRF;return flags}function createMiddlewareHandler(routeStates,routeKey,handler,csrfEnabled=!0){const routeState=routeStates.get(routeKey),routeFlags=routeCapabilityFlags(routeKey,handler,csrfEnabled),handlerKey=typeof handler==="string"?handler:void 0,synchronousInlineHandler=typeof handler==="function"&&handler.constructor.name!=="AsyncFunction"?handler:void 0,asynchronousInlineHandler=typeof handler==="function"&&handler.constructor.name==="AsyncFunction"?handler:void 0,wrappedBase=typeof handler==="function"?void 0:wrapHandler(handler,!0,routeKey);let actionPrefetch=null;if(typeof handler==="string"&&(routeFlags&ROUTE_ACCEPTS_CSRF)!==0){const pending=resolveStringHandler(handler);if(pending instanceof Promise){const settled=()=>{actionPrefetch=null};actionPrefetch=pending.then(settled,settled)}}const handleAsyncInContext=async(req,preparedBaseResult,hasPreparedBaseResult=!1)=>{if((routeFlags&ROUTE_MAY_HAVE_BODY)!==0)try{await parseRequestBody(req)}catch(err){const error=err instanceof Error?err:Error(String(err));return createMiddlewareErrorResponse(error,req)}const enhancedReq=req,csrfHandledByOuter=enhancedReq[CSRF_SEEDED_BY_HANDLE_REQUEST]===!0;if(!csrfHandledByOuter&&(routeFlags&ROUTE_RENDERS_CSRF)!==0&&requestMayRenderHtml(enhancedReq)){const renderTokenSeeding=seedCsrfTokenForRender(enhancedReq);if(renderTokenSeeding)await renderTokenSeeding}if(actionPrefetch)await actionPrefetch;if((routeFlags&ROUTE_FORCES_JSON)!==0)req._forceJson=!0;if(!hasPreparedBaseResult&&synchronousInlineHandler&&(routeFlags&ROUTE_ACCEPTS_CSRF)===0&&!routeState?.middleware?.length&&!routeState?.rateLimit){preparedBaseResult=runWithRequestArguments(enhancedReq,invokeInlineHandler,enhancedReq,synchronousInlineHandler,void 0);hasPreparedBaseResult=!0;if(preparedBaseResult instanceof Response){const finished=finishSynchronousResult(enhancedReq,preparedBaseResult,csrfHandledByOuter,(routeFlags&ROUTE_SEEDS_CSRF)!==0);if(finished)return finished}}const rl=routeState?.rateLimit;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=routeState?.middleware??EMPTY_MIDDLEWARE_ENTRIES;let shouldInjectCsrf=!1;if((routeFlags&ROUTE_ACCEPTS_CSRF)!==0){const alreadyHasCsrf=userMiddleware.some((m)=>m==="csrf"||m.startsWith("csrf:")),routeCsrfMode=routeState?.csrfMode??CSRF_DEFAULT,routeSkipped=routeCsrfMode===CSRF_SKIPPED,routeRequired=routeCsrfMode===CSRF_REQUIRED,actionSkipped=(routeFlags&ROUTE_ACTION_SKIPS_CSRF)!==0||(handlerKey?actionSkipsCsrfCache.get(handlerKey)===!0:!1);shouldInjectCsrf=!alreadyHasCsrf&&(routeRequired||!routeSkipped&&!actionSkipped)}const middlewareEntries=shouldInjectCsrf?userMiddleware.length===0?CSRF_ONLY_MIDDLEWARE:["csrf",...userMiddleware]:userMiddleware;let middlewareTimings=EMPTY_MIDDLEWARE_TIMINGS;if(middlewareEntries.length>0){if(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),routeMethod=routeKey.slice(0,routeKey.indexOf(":")).toUpperCase();log.debug(`[middleware] Executing chain: [${middlewareEntries.join(", ")}] for ${routeMethod} ${urlPath}`)}const cachedCsrfOnlyMiddleware=middlewareEntries===CSRF_ONLY_MIDDLEWARE?resolvedCsrfOnlyMiddleware:void 0,resolved=cachedCsrfOnlyMiddleware??[],unresolvedEntries=cachedCsrfOnlyMiddleware?EMPTY_MIDDLEWARE_ENTRIES:middlewareEntries;for(const middlewareEntry of unresolvedEntries){let resolvedEntry=resolvedMiddlewareEntryCache?.get(middlewareEntry);if(!resolvedEntry){const pending=parseMiddlewareEntry(middlewareEntry),parsed=pending instanceof Promise?await pending:pending,loaded=loadParsedMiddleware(parsed),middleware=loaded instanceof Promise?await loaded:loaded;if(middleware&&typeof middleware.handle==="function"){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);resolvedEntry={name:middlewareEntry,timingName:parsed.timingName,handler:middleware,priority,parameterName:parsed.params?parsed.name:void 0,params:parsed.params};(resolvedMiddlewareEntryCache??=new Map).set(middlewareEntry,resolvedEntry)}}if(!resolvedEntry){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)}if(resolvedEntry.params&&resolvedEntry.parameterName){enhancedReq._middlewareParams=enhancedReq._middlewareParams||{};enhancedReq._middlewareParams[resolvedEntry.parameterName]=resolvedEntry.params}resolved.push(resolvedEntry)}if(middlewareEntries===CSRF_ONLY_MIDDLEWARE&&!cachedCsrfOnlyMiddleware&&resolved.length===1)resolvedCsrfOnlyMiddleware=resolved;if(resolved.length>1)resolved.sort((a,b)=>a.priority-b.priority);const collectsMiddlewareTimings=enhancedReq._startNs!=null;if(collectsMiddlewareTimings)middlewareTimings=[];let chainTimer,chainBudget,runningMiddleware="";try{for(const{name:middlewareName,timingName,handler:middleware}of resolved){const mwStart=collectsMiddlewareTimings?performance.now():0;runningMiddleware=middlewareName;try{const outcome=middleware.handle(enhancedReq);if(outcome&&typeof outcome.then==="function"){if(!chainBudget){chainBudget=new Promise((_,reject)=>{chainTimer=setTimeout(()=>reject(Error(`Middleware '${runningMiddleware}' exceeded ${MIDDLEWARE_TIMEOUT_MS}ms`)),MIDDLEWARE_TIMEOUT_MS)});chainBudget.catch(()=>{})}await Promise.race([outcome,chainBudget])}if(collectsMiddlewareTimings){const elapsedMs=performance.now()-mwStart;middlewareTimings.push({name:timingName,ms:elapsedMs})}}catch(error){if(collectsMiddlewareTimings){const elapsedMs=performance.now()-mwStart;middlewareTimings.push({name:timingName,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)parts.push(`mw_${t.name};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)parts.push(`mw_${t.name};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 formatsResolvedInlineResult=asynchronousInlineHandler!==void 0&&!hasPreparedBaseResult,baseResult=hasPreparedBaseResult?preparedBaseResult:asynchronousInlineHandler?asynchronousInlineHandler(enhancedReq):synchronousInlineHandler?invokeInlineHandler(enhancedReq,synchronousInlineHandler,void 0):wrappedBase(enhancedReq),resolvedResult=baseResult instanceof Response?baseResult:await baseResult;let response=formatsResolvedInlineResult?formatResult(resolvedResult,enhancedReq):resolvedResult;if(response){if((routeFlags&ROUTE_SEEDS_CSRF)!==0&&!csrfHandledByOuter&&enhancedReq[CSRF_SEEDED_BY_HANDLE_REQUEST]!==!0)try{const mod=loadCsrfModule(),csrf=mod instanceof Promise?await mod:mod;if(csrf)response=csrf.seedCsrfCookieIfMissing(enhancedReq,response,enhancedReq._csrfToken,typeof response[FRAMEWORK_RESPONSE_BODY_SIZE_UPPER_BOUND]==="number")}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,{_afterResponse:after,_responseHeaders:requested}=enhancedReq,frameworkMetadataApplied=response&&typeof response[FRAMEWORK_RESPONSE_BODY_SIZE_UPPER_BOUND]==="number";if(response&&(response.status>=400||Array.isArray(after)||requested&&typeof requested==="object"||durMs!=null||!frameworkMetadataApplied)&&typeof response.headers?.set==="function"){const setHeaders=(h)=>{if(Array.isArray(after))for(const callback of after)try{if(typeof callback==="function")callback({status:response.status,durationMs:durMs??0})}catch{}if(requested&&typeof requested==="object"){for(const[name,value]of Object.entries(requested))if(typeof value==="string")h.set(name,value)}if(reqId&&!frameworkMetadataApplied)h.set("X-Request-ID",reqId);if(durMs!=null){let timing=`total;dur=${durMs.toFixed(1)}`;for(const t of middlewareTimings)timing+=`, mw_${t.name};dur=${t.ms.toFixed(1)}`;h.set("Server-Timing",timing)}if(!frameworkMetadataApplied)applySecurityHeaders(h)};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};if(!synchronousInlineHandler||(routeFlags&ROUTE_MAY_HAVE_BODY)!==0)return rememberStacksRouteHandler(bindAsyncRouteHandler(handleAsyncInContext));return rememberStacksRouteHandler((req)=>{if(routeState?.middleware?.length||routeState?.rateLimit)return runWithRequestArgument(req,handleAsyncInContext,req);const csrfHandledByOuter=req[CSRF_SEEDED_BY_HANDLE_REQUEST]===!0;if((routeFlags&ROUTE_RENDERS_CSRF)!==0&&!csrfHandledByOuter&&requestMayRenderHtml(req)){const renderTokenSeeding=seedCsrfTokenForRender(req);if(renderTokenSeeding)return renderTokenSeeding.then(()=>runWithRequestArgument(req,handleAsyncInContext,req))}if((routeFlags&ROUTE_FORCES_JSON)!==0)req._forceJson=!0;let preparedBaseResult;try{preparedBaseResult=runWithRequestArguments(req,invokeInlineHandler,req,synchronousInlineHandler,void 0)}catch(error){return Promise.reject(error)}if(preparedBaseResult instanceof Response){const finished=finishSynchronousResult(req,preparedBaseResult,csrfHandledByOuter,(routeFlags&ROUTE_SEEDS_CSRF)!==0);if(finished)return finished}return runWithRequestArguments(req,handleAsyncInContext,req,preparedBaseResult,!0)})}function createInertRoute(){const inert={middleware:()=>inert,name:()=>inert,skipCsrf:()=>inert,requireCsrf:()=>inert,rateLimit:()=>inert};return inert}function createChainableRoute(routeStates,routeKey,shadowed=!1){if(shadowed)return createInertRoute();const routePath=routeKey.includes(":")?routeKey.substring(routeKey.indexOf(":")+1):routeKey,routeState=routeStates.get(routeKey),chain={middleware(name){if(!routeState)return chain;const middlewareList=routeState.middleware??=[];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??=new Map).set(routeName,compileNamedRoute(routePath));return chain},skipCsrf(){if(routeState)routeState.csrfMode=CSRF_SKIPPED;return chain},requireCsrf(){if(routeState)routeState.csrfMode=CSRF_REQUIRED;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);if(routeState)routeState.rateLimit={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)`)}let _moduleImportCache;function cachedImport(fullPath){const cache=_moduleImportCache??=new Map;let p=cache.get(fullPath);if(!p){p=import(fullPath);cache.set(fullPath,p)}return p}let _resolvedHandlerCache;function resolveStringHandler(handlerPath){const cache=_resolvedHandlerCache??=new Map;let resolved=cache.get(handlerPath);if(!resolved){resolved=resolveStringHandlerUncached(handlerPath).then((handler)=>{cache.set(handlerPath,handler);return handler},(error)=>{cache.delete(handlerPath);throw error});cache.set(handlerPath,resolved)}return resolved}function validationFailureResponse(errors){return response.validationError(errors)}export function precognitionRequest(req,knownHeader){const header=knownHeader===void 0?req.headers?.get?.("Precognition"):knownHeader,viaHeader=typeof header==="string"&&header.toLowerCase()==="true";let viaQuery=!1;try{if(!viaHeader&&req.url.includes("?"))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=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=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=storagePath(`framework/actions/src/${modulePath}.ts`);else if(modulePath.includes("Actions")){const registered=(await getActionRegistry())?.[modulePath];if(registered)fullPath=registered;else{const userPath=projectPath(`app/${modulePath}.ts`),defaultPath=resolveDefaultsPath(`app/${modulePath}.ts`);fullPath=await fileExists(userPath)?userPath:defaultPath}}else{const userPath=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),actionValidationEntries=action.validations?compileActionValidationEntries(action.validations):void 0,routeSeparator=handlerKey.indexOf(":"),actionMayHaveRouteParams=routeSeparator===-1||handlerKey.indexOf("{",routeSeparator+1)!==-1,prepareRequest=(req)=>{const precognitionHeader=req.headers.get("Precognition"),requestHasQuery=req.url.includes("?"),precognition=precognitionHeader===null&&!requestHasQuery?null:precognitionRequest(req,precognitionHeader);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=validateActionInputSync(req,rules,rules===action.validations?actionValidationEntries:void 0,actionMayHaveRouteParams,!0,requestHasQuery);return precognitionResult.valid?precognitionSuccess():validationFailureResponse(precognitionResult.errors)}if(action.validations){const validationResult=validateActionInputSync(req,action.validations,actionValidationEntries,actionMayHaveRouteParams,!0,requestHasQuery);if(!validationResult.valid)return validationFailureResponse(validationResult.errors)}},rethrowActionError=(handleError)=>{report(handleError,{label:`[Router] action.handle() for '${handlerKey}'`});throw handleError};if(typeof action.authorize!=="function"&&typeof action.before!=="function")return(req)=>{if(actionSkipsCsrf)req._skipCsrf=!0;if(actionForcesJson)req._forceJson=!0;req._requestValidationRules=requestValidationRules;try{const earlyResponse=prepareRequest(req);if(earlyResponse)return earlyResponse;const result=action.handle(req);return result instanceof Promise?result.then((value)=>{try{return formatResult(value,req)}catch(error){return rethrowActionError(error)}},rethrowActionError):formatResult(result,req)}catch(handleError){return rethrowActionError(handleError)}};return async(req)=>{if(actionSkipsCsrf)req._skipCsrf=!0;if(actionForcesJson)req._forceJson=!0;req._requestValidationRules=requestValidationRules;try{const earlyResponse=prepareRequest(req);if(earlyResponse)return earlyResponse;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 pendingResult=action.handle(req),result=pendingResult instanceof Promise?await pendingResult:pendingResult;return formatResult(result,req)}catch(handleError){return rethrowActionError(handleError)}}}const EMPTY_VALIDATION_ERRORS=Object.freeze({}),VALID_ACTION_VALIDATION_RESULT=Object.freeze({valid:!0,errors:EMPTY_VALIDATION_ERRORS});function propertyOwner(value,property){let current=value;while(current){if(Object.hasOwn(current,property))return current;current=Object.getPrototypeOf(current)}}function compileActionValidationEntries(validations){return Object.entries(validations).map(([field,validation])=>{const rule=validation.rule;if(typeof rule.getRules!=="function"||propertyOwner(rule,"validate")!==propertyOwner(rule,"getRules"))return[field,validation];const rules=rule.getRules();if(!Array.isArray(rules)||rules.some((entry)=>typeof entry?.test!=="function"))return[field,validation];return[field,validation,rules.map((entry)=>entry.test)]})}function validateActionInputSync(req,validations,compiledEntries,mayHaveRouteParams=!0,reuseSuccessResult=!1,requestHasQuery=req.url.includes("?")){let errors;const validated={};let valid=!0;const entries=compiledEntries??Object.entries(validations),input=getRequestInput(req,entries,mayHaveRouteParams,requestHasQuery);for(let entryIndex=0;entryIndex<entries.length;entryIndex++){const entry=entries[entryIndex],field=entry[0],validation=entry[1],ruleTests=entry[2],value=input[field];let result;try{const rule=validation.rule;let passedCompiledRules=!1;if(ruleTests){const empty=value===void 0||value===null||value==="";passedCompiledRules=empty&&rule.isRequired!==!0;if(!empty){passedCompiledRules=!0;for(let i=0;i<ruleTests.length;i++)if(!ruleTests[i](value)){passedCompiledRules=!1;break}}}if(!passedCompiledRules)result=rule.validate(value)}catch{result={valid:!1,errors:[{message:`${field} validation failed`}]}}if(result&&!result.valid){valid=!1;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}else if(value!==void 0)validated[field]=value}if(valid)req._validatedInput=validated;if(valid&&reuseSuccessResult)return VALID_ACTION_VALIDATION_RESULT;return{valid,errors:errors??{}}}export async function validateActionInput(req,validations){return validateActionInputSync(req,validations)}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}function getRequestInput(req,validationEntries,mayHaveRouteParams=!0,requestHasQuery=req.url.includes("?")){let input,mayNeedCoercion=!1;if(requestHasQuery){const q=req.query;if(q)for(const key in q){(input??={})[key]=q[key];mayNeedCoercion=!0}else new URL(req.url).searchParams.forEach((value,key)=>{(input??={})[key]=value;mayNeedCoercion=!0})}let hasRouteParams=!1;if(mayHaveRouteParams&&req.params)for(const key in req.params){if(!Object.hasOwn(req.params,key))continue;(input??={})[key]=req.params[key];hasRouteParams=!0;mayNeedCoercion=!0}if(req.jsonBody&&typeof req.jsonBody==="object")input=mayNeedCoercion?Object.assign(input??{},req.jsonBody):req.jsonBody;else if(req.formBody&&typeof req.formBody==="object"){input=mayNeedCoercion?Object.assign(input??{},req.formBody):{...req.formBody};mayNeedCoercion=!0}if(!requestHasQuery&&!mayHaveRouteParams&&req.jsonBody&&typeof req.jsonBody==="object"&&!Array.isArray(req.jsonBody)&&!req.formBody&&!req.files)req._allInputCache=input;if(req.files&&typeof req.allFiles==="function")try{const files=req.allFiles();for(const key of Object.keys(files??{})){const values=input??={};if(!(key in values))values[key]=files[key]}}catch{}if(!validationEntries)return input??{};input??={};let coerced=!1;if(mayNeedCoercion)for(const[field,validation]of validationEntries){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;coerced=!0}}else if(validatorName==="boolean"){if(value==="true"||value==="1"){input[field]=!0;coerced=!0}else if(value==="false"||value==="0"){input[field]=!1;coerced=!0}}}if(!coerced&&!req.formBody&&!req.files&&!hasRouteParams)req._allInputCache=input;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"}});if(result!==null&&typeof result==="object"){const linkHeader="data"in result?buildPaginatorLinkHeader(result):null;return formatJsonResult(result,req,linkHeader)}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(apiShaped){if(typeof result==="string"||typeof result==="number"||typeof result==="boolean")return formatJsonResult(result,req);return Response.json(result)}return new Response(String(result),{headers:{"Content-Type":"text/plain; charset=utf-8"}})}function formatJsonResult(result,req,linkHeader){const canPreapplyMetadata=!req._responseHeaders,body=JSON.stringify(result)??"",requestMarkers=req,csrf=requestMarkers[CSRF_SEEDED_BY_HANDLE_REQUEST]!==!0&&typeof requestMarkers[CSRF_SECURE_TRANSPORT]==="boolean"?loadCsrfModule():null,csrfCookie=canPreapplyMetadata&&csrf&&!(csrf instanceof Promise)&&typeof csrf.createCsrfCookie==="function"?csrf.createCsrfCookie(req,req._csrfToken):void 0,response=canPreapplyMetadata?secureSerializedJsonResponse(body,void 0,req._requestId,csrfCookie):new Response(body,{headers:createJsonSecurityHeaders()});if(!canPreapplyMetadata)response.headers.set("Content-Length",String(Buffer.byteLength(body)));if(linkHeader)response.headers.set("Link",linkHeader);if(canPreapplyMetadata){const frameworkResponse=response;frameworkResponse[FRAMEWORK_RESPONSE_BODY_SIZE_UPPER_BOUND]=body.length*3}if(csrfCookie)requestMarkers[CSRF_SEEDED_BY_HANDLE_REQUEST]=!0;return response}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 nativeRequestText=Request.prototype.text,nativeRequestJson=Request.prototype.json,nativeRequestBytes=Request.prototype.bytes,nativeRequestArrayBuffer=Request.prototype.arrayBuffer,nativeRequestBlob=Request.prototype.blob,nativeRequestClone=Request.prototype.clone;let requestBodyEncoder,UploadedFileConstructor;function createUploadedFile(file){UploadedFileConstructor??=require("@stacksjs/storage/uploaded-file").UploadedFile;return new UploadedFileConstructor(file)}const REQUEST_METHODS={text(){return this._rawBody===void 0?nativeRequestText.call(this):Promise.resolve(this._rawBody)},json(){if(this._rawBody===void 0)return nativeRequestJson.call(this);try{return Promise.resolve(this._rawBody.length===0?null:JSON.parse(this._rawBody))}catch(error){return Promise.reject(error)}},bytes(){return this._rawBody===void 0?nativeRequestBytes.call(this):Promise.resolve((requestBodyEncoder??=new TextEncoder).encode(this._rawBody))},arrayBuffer(){if(this._rawBody===void 0)return nativeRequestArrayBuffer.call(this);const bytes=(requestBodyEncoder??=new TextEncoder).encode(this._rawBody);return Promise.resolve(bytes.buffer.slice(bytes.byteOffset,bytes.byteOffset+bytes.byteLength))},blob(){return this._rawBody===void 0?nativeRequestBlob.call(this):Promise.resolve(new Blob([this._rawBody],{type:this._rawBodyContentType}))},clone(){if(this._rawBody===void 0)return nativeRequestClone.call(this);return new Request(this.url,{method:this.method,headers:this.headers,body:this._rawBody})},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{collect}=require("@stacksjs/collections"),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?createUploadedFile(rawFile):null},getFiles(key){const file=(this.files||{})[key];if(!file)return[];return(Array.isArray(file)?file:[file]).map(createUploadedFile)},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)=>createUploadedFile(f));else result[key]=createUploadedFile(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,STACKS_REQUEST_ENHANCED=Symbol.for("stacks.router.requestEnhanced");function fuseRequestEnhancements(router,initializeRequestIds=!0){const original=router.enhanceRequest.bind(router),combinedPrototypes=new WeakMap;router.enhanceRequest=(request,params={})=>{const basePrototype=Object.getPrototypeOf(request),combined=combinedPrototypes.get(basePrototype);if(combined){const enhanced=request;if(enhanced.params!==params)if("params"in request)Object.defineProperty(enhanced,"params",{value:params,writable:!0,configurable:!0,enumerable:!1});else enhanced.params=params;Object.setPrototypeOf(enhanced,combined);if(initializeRequestIds&&!enhanced._requestId)enhanced._requestId=incomingRequestId(enhanced)??generateRequestId();return enhanced}const enhanced=enhanceRequest(original(request,params),initializeRequestIds);combinedPrototypes.set(basePrototype,Object.getPrototypeOf(enhanced));return enhanced}}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 requestMayRenderHtml(req){return req.headers.get("accept")?.includes("text/html")===!0||req.headers.get("sec-fetch-dest")==="document"}function hasCsrfCookie(cookieHeader){return cookieHeader.length!==0&&(cookieHeader.startsWith("X-CSRF-Token=")||cookieHeader.includes("X-CSRF-Token=")||cookieHeader.includes("csrf-token="))}function seedCsrfTokenForRender(req,cookieHeader=req.headers?.get?.("cookie")??""){if(hasCsrfCookie(cookieHeader))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,initializeRequestId=!0){const routeParams=req.params??{};req.params=routeParams;if(initializeRequestId&&!req._requestId)req._requestId=incomingRequestId(req)??generateRequestId();if(req[STACKS_REQUEST_ENHANCED]===!0)return req;applyRequestEnhancements(req,routeParams);const routerPrototype=Object.getPrototypeOf(req);let stacksPrototype=stacksRequestPrototypes.get(routerPrototype);if(!stacksPrototype){stacksPrototype=Object.assign(Object.create(routerPrototype),REQUEST_METHODS);Object.defineProperty(stacksPrototype,STACKS_REQUEST_ENHANCED,{value:!0});stacksRequestPrototypes.set(routerPrototype,stacksPrototype)}Object.setPrototypeOf(req,stacksPrototype);return req}const REQUEST_ID_PATTERN=/^[\w.:-]{8,200}$/;function incomingRequestId(req){try{const supplied=req.headers.get("x-request-id")?.trim();if(supplied&&REQUEST_ID_PATTERN.test(supplied))return supplied}catch{}return}const REQUEST_ID_DIGITS="0123456789abcdefghijklmnopqrstuvwxyz",REQUEST_ID_SUFFIX_COUNT=36;function createRequestIdGenerator(){const prefix=crypto.randomUUID().replaceAll("-","").slice(0,16);let block=0,suffix=0,blockPrefix=`${prefix}-0`;return()=>{suffix++;if(suffix===REQUEST_ID_SUFFIX_COUNT){suffix=0;blockPrefix=`${prefix}-${(++block).toString(36)}`}return blockPrefix+REQUEST_ID_DIGITS[suffix]}}let generateRequestId=()=>{generateRequestId=createRequestIdGenerator();return generateRequestId()};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)}const pending=resolveStringHandler(handlerPath);return await(pending instanceof Promise?await pending:pending)(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 parseRequestBody(req){if(req._bodyParsed)return;req._bodyParsed=!0;const contentType=req.headers.get("content-type")||"";try{if(contentType==="application/json"||JSON_CONTENT_TYPE.test(contentType)){const raw=await req.text();req._rawBody=raw;req._rawBodyContentType=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});if(config.autoDiscoverRoutes===!1)bunRouter._apiRoutesInitialized=!0;const csrfEnabled=config.csrf!==!1;if(csrfEnabled)csrfEnabledNativeRouteRouters.add(bunRouter);fuseRequestEnhancements(bunRouter,config.requestIds!==!1);let currentPrefix="",currentGroupMiddleware=[],currentGroupApiResponse=!1;const routeStates=new Map;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=routeStates.has(routeKey)||clearedRouteKeysByRegistry.get(routeStates)?.has(routeKey)===!0;if(!shadowed){const routeState={csrfMode:CSRF_DEFAULT};if(currentGroupMiddleware.length>0)routeState.middleware=[...currentGroupMiddleware];routeStates.set(routeKey,routeState);routeStateRegistries.add(routeStates)}if(!shadowed&¤tGroupApiResponse)(routeApiResponseRegistry??=new Set).add(routeKey);if(!shadowed&&typeof _handler==="string")routeHandlerKeyRegistry.set(routeKey,_handler);if(!shadowed&&isRouterAction(_handler))(routeActionRegistry??=new Map).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(routeStates,routeKey,handler,csrfEnabled));return createChainableRoute(routeStates,routeKey,shadowed)},post(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("POST",path,handler);bunRouter.post(fullPath,createMiddlewareHandler(routeStates,routeKey,handler,csrfEnabled));return createChainableRoute(routeStates,routeKey,shadowed)},put(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("PUT",path,handler);bunRouter.put(fullPath,createMiddlewareHandler(routeStates,routeKey,handler,csrfEnabled));return createChainableRoute(routeStates,routeKey,shadowed)},patch(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("PATCH",path,handler);bunRouter.patch(fullPath,createMiddlewareHandler(routeStates,routeKey,handler,csrfEnabled));return createChainableRoute(routeStates,routeKey,shadowed)},delete(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("DELETE",path,handler);bunRouter.delete(fullPath,createMiddlewareHandler(routeStates,routeKey,handler,csrfEnabled));return createChainableRoute(routeStates,routeKey,shadowed)},options(path,handler){const{fullPath,routeKey,shadowed}=registerRoute("OPTIONS",path,handler);bunRouter.options(fullPath,createMiddlewareHandler(routeStates,routeKey,handler,csrfEnabled));return createChainableRoute(routeStates,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(routeStates,routeKey,handler,csrfEnabled);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(routeStates,`${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);if(csrfEnabled)wrapHandleRequestForCsrf(bunRouter);await wrapHandleRequestForDatabaseContext(bunRouter);await runBootHooks();const production=(process.env.APP_ENV??"").toLowerCase()==="production"||process.env.NODE_ENV==="production",callerSuppliedRoutes=Object.prototype.hasOwnProperty.call(options,"routes"),nativeRoutes=options.nativeRoutes??(production&&!callerSuppliedRoutes&&shouldUseNativeRoutesByDefault(bunRouter.routes));if(options.tls)secureNativeRouteRouters.add(bunRouter);else secureNativeRouteRouters.delete(bunRouter);return bunRouter.serve(nativeRoutes===options.nativeRoutes?options:{...options,nativeRoutes})},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
|
+
`,error)}const ormRoutesCandidates=ormRoutesLoaded?[]:[frameworkPath("orm/routes.ts"),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=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?projectPath(meta.root):`${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),handleUnseededSafeRequest=async(request,rendersCsrf,cookieHeader)=>{if(rendersCsrf&&requestMayRenderHtml(request)){const seeding=seedCsrfTokenForRender(request,cookieHeader);if(seeding)await seeding}request[CSRF_SEEDED_BY_HANDLE_REQUEST]=!0;const response=await original(request);if(!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}},handleSafeRequest=(request,rendersCsrf)=>{const cookieHeader=request.headers.get("cookie")??"";if(hasCsrfCookie(cookieHeader)){request[CSRF_SEEDED_BY_HANDLE_REQUEST]=!0;return original(request)}return handleUnseededSafeRequest(request,rendersCsrf,cookieHeader)};router.handleRequest=(request)=>{const method=request.method;if(method==="GET"||method==="HEAD")return handleSafeRequest(request,!0);if(method==="OPTIONS")return handleSafeRequest(request,!1);return original(request)};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(projectPath("resources/views")))return;const layouts=[projectPath("resources/views/layouts"),projectPath("resources/layouts")].find(existsSync),partials=[projectPath("resources/views/partials"),projectPath("resources/partials")].find(existsSync),components=[projectPath("resources/components"),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,routingContextDispatcher=null;const databaseContextWrappedRouters=new WeakSet;function applyDirectNativeCookies(router,response,request){const cookieChanges=request;return cookieChanges._cookiesToSet?.length||cookieChanges._cookiesToDelete?.length?router.applyModifiedCookies(response,request):response}function nativeRouteErrorResponse(error){return new Response(JSON.stringify({success:!1,message:"Internal Server Error",error:error instanceof Error?error.message:String(error)}),{status:500,headers:{"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST, PUT, DELETE, PATCH, OPTIONS","Access-Control-Allow-Headers":"Content-Type, Authorization, X-Requested-With, Accept, Origin"}})}function handleDirectNativeRouteError(router,error){console.error("Error handling request:",error);return router.errorHandler?router.errorHandler(error instanceof Error?error:Error(String(error))):nativeRouteErrorResponse(error)}function directNativePath(path){const segments=path.split("/");for(let index=0;index<segments.length;index++){const segment=segments[index];if(!segment)continue;const parameter=segment.match(/^\{([A-Z_$][\w$]*)\}$/i);if(parameter){segments[index]=`:${parameter[1]}`;continue}if(segment.includes("{")||segment.includes(":")||segment.includes("*"))return}return segments.join("/")}function buildDirectNativeHandlers(router,finalizeResponse){const nativeRouter=router,handlers=new Map;if(nativeRouter.globalMiddleware.length!==0)return handlers;for(const route of nativeRouter.routes){if(!isNativeRouteMethod(route.method)||route.nativeDispatch===!1||route.constraints&&Object.keys(route.constraints).length>0||(route.middleware?.length??0)!==0||typeof route.handler!=="function"||!stacksRouteHandlers.has(route.handler))continue;const handler=route.handler,nativePath=directNativePath(route.path);if(!nativePath)continue;const key=`${route.method}:${nativePath}`;if(handlers.has(key))continue;handlers.set(key,(request)=>{const nativeParams=request.params??{},enhancedRequest=nativeRouter.enhanceRequest(request,nativeParams);enhancedRequest.route=route;return runWithBunRouterRequest(enhancedRequest,()=>{try{const response=handler(enhancedRequest);return response instanceof Response?finalizeResponse(applyDirectNativeCookies(nativeRouter,response,enhancedRequest),request):response.then((result)=>finalizeResponse(applyDirectNativeCookies(nativeRouter,result,enhancedRequest),request),(error)=>{const result=handleDirectNativeRouteError(nativeRouter,error);return result instanceof Response?finalizeResponse(result,request):result.then((response)=>finalizeResponse(response,request))})}catch(error){const result=handleDirectNativeRouteError(nativeRouter,error);return result instanceof Response?finalizeResponse(result,request):result.then((response)=>finalizeResponse(response,request))}})})}return handlers}function wrapNativeRoutesForDatabaseContext(router,dispatchInRoutingContext){const nativeRouter=router;if(nativeRouter._nativeDatabaseContextWrapped||typeof nativeRouter._buildNativeRoutes!=="function")return;const buildNativeRoutes=nativeRouter._buildNativeRoutes.bind(nativeRouter);nativeRouter._buildNativeRoutes=()=>{const routes=buildNativeRoutes();if(!routes)return routes;const compression=nativeRouter.config.compression,compressionDisabled=compression?.enabled===!1,compressionThreshold=compression?.threshold??1024,finalizeResponse=(response,request)=>{const bodySizeUpperBound=response[FRAMEWORK_RESPONSE_BODY_SIZE_UPPER_BOUND];if(compressionDisabled||typeof bodySizeUpperBound==="number"&&bodySizeUpperBound<compressionThreshold)return response;if(bodySizeUpperBound===void 0){const declaredLength=response.headers.get("content-length");if(declaredLength!==null){const length=Number(declaredLength);if(Number.isSafeInteger(length)&&length>=0&&length<compressionThreshold)return response}}return applyResponseCompression(response,request,compression)},directHandlers=buildDirectNativeHandlers(router,finalizeResponse),secureTransport=secureNativeRouteRouters.has(router),csrfEnabled=csrfEnabledNativeRouteRouters.has(router);for(const[path,methods]of Object.entries(routes))for(const[method,handler]of Object.entries(methods)){const safeMethod=method==="GET"||method==="HEAD"||method==="OPTIONS",directDispatch=directHandlers.get(`${method}:${path}`)??(method==="HEAD"?directHandlers.get(`GET:${path}`):void 0);if(directDispatch){if(safeMethod&&csrfEnabled)methods[method]=(request)=>{const cookie=request.headers.get("cookie")??"",markedRequest=request;if(hasCsrfCookie(cookie))markedRequest[CSRF_SEEDED_BY_HANDLE_REQUEST]=!0;else markedRequest[CSRF_SECURE_TRANSPORT]=secureTransport;return dispatchInRoutingContext(directDispatch,request)};else methods[method]=(request)=>dispatchInRoutingContext(directDispatch,request);continue}methods[method]=(request)=>{if(safeMethod&&csrfEnabled){const cookie=request.headers.get("cookie")??"",markedRequest=request;if(hasCsrfCookie(cookie))markedRequest[CSRF_SEEDED_BY_HANDLE_REQUEST]=!0;else markedRequest[CSRF_SECURE_TRANSPORT]=secureTransport}const response=dispatchInRoutingContext(handler,request);return response instanceof Response?finalizeResponse(response,request):response.then((resolved)=>finalizeResponse(resolved,request))}}return routes};nativeRouter._nativeDatabaseContextWrapped=!0}async function wrapHandleRequestForDatabaseContext(router){if(databaseContextWrappedRouters.has(router))return;const dispatchInRoutingContext=await getRoutingContextDispatcher();if(databaseContextWrappedRouters.has(router))return;wrapNativeRoutesForDatabaseContext(router,dispatchInRoutingContext);const original=router.handleRequest.bind(router);router.handleRequest=(request)=>dispatchInRoutingContext(original,request);databaseContextWrappedRouters.add(router)}async function getRoutingContextDispatcher(){if(!routingContextDispatcher)try{let database=await import("@stacksjs/database/replicas");if(typeof database.runInDatabaseRoutingContext!=="function"&&typeof database.withDatabaseRoutingContext!=="function")database=await import("@stacksjs/database");const dispatcher=database.runInDatabaseRoutingContext,runner=database.withDatabaseRoutingContext??database.withRoutingContext;routingContextDispatcher=typeof dispatcher==="function"?dispatcher:typeof runner==="function"?(fn,arg)=>runner(()=>fn(arg)):(fn,arg)=>fn(arg)}catch{routingContextDispatcher=(fn,arg)=>fn(arg)}return routingContextDispatcher}export async function serverResponse(request,_body){return(await getRoutingContextDispatcher())(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.29",
|
|
6
6
|
"description": "The Stacks framework router.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -61,27 +61,27 @@
|
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@stacksjs/bun-router": "^0.1.11",
|
|
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.
|
|
64
|
+
"@stacksjs/cache": "0.74.29",
|
|
65
|
+
"@stacksjs/collections": "0.74.29",
|
|
66
|
+
"@stacksjs/config": "0.74.29",
|
|
67
|
+
"@stacksjs/error-handling": "0.74.29",
|
|
68
|
+
"@stacksjs/logging": "0.74.29",
|
|
69
|
+
"@stacksjs/pagination": "0.74.29",
|
|
70
|
+
"@stacksjs/path": "0.74.29",
|
|
71
|
+
"@stacksjs/security": "0.74.29",
|
|
72
|
+
"@stacksjs/storage": "0.74.29",
|
|
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.29",
|
|
77
|
+
"@stacksjs/types": "0.74.29",
|
|
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.29",
|
|
82
|
+
"@stacksjs/auth": "0.74.29",
|
|
83
|
+
"@stacksjs/database": "0.74.29",
|
|
84
|
+
"@stacksjs/validation": "0.74.29"
|
|
85
85
|
},
|
|
86
86
|
"peerDependenciesMeta": {
|
|
87
87
|
"@stacksjs/api": {
|