@stacksjs/server 0.75.66 → 0.75.68

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.
@@ -1,4 +1,4 @@
1
- import{log}from"@stacksjs/logging/runtime";import*as p from"@stacksjs/path";import{existsSync}from"node:fs";const DEFAULT_MAINTENANCE_PAYLOAD={mode:"maintenance",status:503,message:"We are currently performing maintenance. Please check back soon."},DEFAULT_COMING_SOON_PAYLOAD={mode:"coming-soon",status:200,message:"We are getting ready to launch. Check back soon.",redirect:"/coming-soon"};function defaultsForMode(mode){return mode==="coming-soon"?DEFAULT_COMING_SOON_PAYLOAD:DEFAULT_MAINTENANCE_PAYLOAD}let siteModePaths;function currentSiteModePaths(){const cwd=process.cwd();if(siteModePaths?.cwd!==cwd)siteModePaths={cwd};return siteModePaths}export function maintenanceFilePath(){return currentSiteModePaths().maintenance??=p.storagePath("framework/down")}export function comingSoonFilePath(){return currentSiteModePaths().comingSoon??=p.storagePath("framework/coming-soon")}export function siteModeFilePath(mode){return mode==="coming-soon"?comingSoonFilePath():maintenanceFilePath()}export async function isDownForMaintenance(){try{return existsSync(maintenanceFilePath())}catch{return!1}}export async function isComingSoon(){try{return existsSync(comingSoonFilePath())}catch{return!1}}export async function maintenancePayload(){return siteModePayload("maintenance")}export async function comingSoonPayload(){return siteModePayload("coming-soon")}function readSiteModePayload(mode){try{const filePath=siteModeFilePath(mode);if(!existsSync(filePath))return null;return Bun.file(filePath).text().then((content)=>({...defaultsForMode(mode),...JSON.parse(content),mode})).catch(()=>null)}catch{return null}}export async function siteModePayload(mode){return readSiteModePayload(mode)}function comingSoonPayloadResult(){const payload=readSiteModePayload("coming-soon");return payload?payload.then((value)=>value??envSiteModePayload()):envSiteModePayload()}function activeSiteModePayloadResult(){const payload=readSiteModePayload("maintenance");return payload?payload.then((value)=>value??comingSoonPayloadResult()):comingSoonPayloadResult()}export async function activeSiteModePayload(){return activeSiteModePayloadResult()}function envSiteModePayload(){if(isTruthy(process.env.APP_MAINTENANCE))return{...DEFAULT_MAINTENANCE_PAYLOAD,mode:"maintenance",time:Date.now(),secret:process.env.APP_MAINTENANCE_SECRET||void 0};if(isTruthy(process.env.APP_COMING_SOON))return{...DEFAULT_COMING_SOON_PAYLOAD,mode:"coming-soon",time:Date.now(),secret:process.env.APP_COMING_SOON_SECRET||void 0};return null}function isTruthy(value){const normalized=value?.toLowerCase();return normalized==="1"||normalized==="true"||normalized==="yes"||normalized==="on"}export async function down(options={}){const payload={...DEFAULT_MAINTENANCE_PAYLOAD,...options,mode:"maintenance",time:Date.now()},frameworkDir=p.storagePath("framework"),{mkdirSync,existsSync}=await import("@stacksjs/storage");if(!existsSync(frameworkDir))mkdirSync(frameworkDir,{recursive:!0});await Bun.write(maintenanceFilePath(),JSON.stringify(payload,null,2));log.info("Application is now in maintenance mode.");if(payload.secret)log.info("Maintenance bypass secret has been configured")}export async function comingSoon(options={}){const payload={...DEFAULT_COMING_SOON_PAYLOAD,...options,mode:"coming-soon",time:Date.now()},frameworkDir=p.storagePath("framework"),{mkdirSync,existsSync}=await import("@stacksjs/storage");if(!existsSync(frameworkDir))mkdirSync(frameworkDir,{recursive:!0});await Bun.write(comingSoonFilePath(),JSON.stringify(payload,null,2));log.info("Application is now in coming soon mode.");if(payload.secret)log.info("Coming soon bypass secret has been configured")}export async function up(){const{unlinkSync,existsSync}=await import("node:fs"),filePath=maintenanceFilePath();if(existsSync(filePath)){unlinkSync(filePath);log.info("Application is now live.")}else log.info("Application is already live.")}export async function launch(){const{unlinkSync,existsSync}=await import("node:fs"),filePath=comingSoonFilePath();if(existsSync(filePath)){unlinkSync(filePath);log.info("Application is out of coming soon mode.")}else log.info("Application is not in coming soon mode.")}export function isAllowedIp(ip,allowed=[],trustLocalhost=!0){if(trustLocalhost&&["127.0.0.1","::1","localhost"].includes(ip))return!0;if(allowed.length===0)return!1;return allowed.includes(ip)}export function bypassCookieName(mode="maintenance"){return mode==="coming-soon"?"stacks_coming_soon_bypass":"stacks_maintenance_bypass"}export function hasValidBypassCookie(cookies,secret,mode="maintenance"){return cookies[bypassCookieName(mode)]===secret}export function isSecretPath(path,secret){return path===`/${secret}`||path.startsWith(`/${secret}/`)}export function maintenanceHtml(payload){const mode=payload.mode??"maintenance",defaults=defaultsForMode(mode),message=escapeHtml(payload.message||defaults.message||""),title=escapeHtml(payload.title||(mode==="coming-soon"?"Coming soon":"Down for maintenance")),appName=escapeHtml((process.env.APP_NAME||"").trim());return`<!DOCTYPE html>
1
+ import{clientAddress}from"@stacksjs/bun-router";import{log}from"@stacksjs/logging/runtime";import*as p from"@stacksjs/path";import{existsSync}from"node:fs";const DEFAULT_MAINTENANCE_PAYLOAD={mode:"maintenance",status:503,message:"We are currently performing maintenance. Please check back soon."},DEFAULT_COMING_SOON_PAYLOAD={mode:"coming-soon",status:200,message:"We are getting ready to launch. Check back soon.",redirect:"/coming-soon"};function defaultsForMode(mode){return mode==="coming-soon"?DEFAULT_COMING_SOON_PAYLOAD:DEFAULT_MAINTENANCE_PAYLOAD}let siteModePaths;function currentSiteModePaths(){const cwd=process.cwd();if(siteModePaths?.cwd!==cwd)siteModePaths={cwd};return siteModePaths}export function maintenanceFilePath(){return currentSiteModePaths().maintenance??=p.storagePath("framework/down")}export function comingSoonFilePath(){return currentSiteModePaths().comingSoon??=p.storagePath("framework/coming-soon")}export function siteModeFilePath(mode){return mode==="coming-soon"?comingSoonFilePath():maintenanceFilePath()}export async function isDownForMaintenance(){try{return existsSync(maintenanceFilePath())}catch{return!1}}export async function isComingSoon(){try{return existsSync(comingSoonFilePath())}catch{return!1}}export async function maintenancePayload(){return siteModePayload("maintenance")}export async function comingSoonPayload(){return siteModePayload("coming-soon")}function readSiteModePayload(mode){try{const filePath=siteModeFilePath(mode);if(!existsSync(filePath))return null;return Bun.file(filePath).text().then((content)=>({...defaultsForMode(mode),...JSON.parse(content),mode})).catch(()=>null)}catch{return null}}export async function siteModePayload(mode){return readSiteModePayload(mode)}function comingSoonPayloadResult(){const payload=readSiteModePayload("coming-soon");return payload?payload.then((value)=>value??envSiteModePayload()):envSiteModePayload()}function activeSiteModePayloadResult(){const payload=readSiteModePayload("maintenance");return payload?payload.then((value)=>value??comingSoonPayloadResult()):comingSoonPayloadResult()}export async function activeSiteModePayload(){return activeSiteModePayloadResult()}function envSiteModePayload(){if(isTruthy(process.env.APP_MAINTENANCE))return{...DEFAULT_MAINTENANCE_PAYLOAD,mode:"maintenance",time:Date.now(),secret:process.env.APP_MAINTENANCE_SECRET||void 0};if(isTruthy(process.env.APP_COMING_SOON))return{...DEFAULT_COMING_SOON_PAYLOAD,mode:"coming-soon",time:Date.now(),secret:process.env.APP_COMING_SOON_SECRET||void 0};return null}function isTruthy(value){const normalized=value?.toLowerCase();return normalized==="1"||normalized==="true"||normalized==="yes"||normalized==="on"}export async function down(options={}){const payload={...DEFAULT_MAINTENANCE_PAYLOAD,...options,mode:"maintenance",time:Date.now()},frameworkDir=p.storagePath("framework"),{mkdirSync,existsSync}=await import("@stacksjs/storage");if(!existsSync(frameworkDir))mkdirSync(frameworkDir,{recursive:!0});await Bun.write(maintenanceFilePath(),JSON.stringify(payload,null,2));log.info("Application is now in maintenance mode.");if(payload.secret)log.info("Maintenance bypass secret has been configured")}export async function comingSoon(options={}){const payload={...DEFAULT_COMING_SOON_PAYLOAD,...options,mode:"coming-soon",time:Date.now()},frameworkDir=p.storagePath("framework"),{mkdirSync,existsSync}=await import("@stacksjs/storage");if(!existsSync(frameworkDir))mkdirSync(frameworkDir,{recursive:!0});await Bun.write(comingSoonFilePath(),JSON.stringify(payload,null,2));log.info("Application is now in coming soon mode.");if(payload.secret)log.info("Coming soon bypass secret has been configured")}export async function up(){const{unlinkSync,existsSync}=await import("node:fs"),filePath=maintenanceFilePath();if(existsSync(filePath)){unlinkSync(filePath);log.info("Application is now live.")}else log.info("Application is already live.")}export async function launch(){const{unlinkSync,existsSync}=await import("node:fs"),filePath=comingSoonFilePath();if(existsSync(filePath)){unlinkSync(filePath);log.info("Application is out of coming soon mode.")}else log.info("Application is not in coming soon mode.")}export function isAllowedIp(ip,allowed=[],trustLocalhost=!0){if(trustLocalhost&&["127.0.0.1","::1","localhost"].includes(ip))return!0;if(allowed.length===0)return!1;return allowed.includes(ip)}export function bypassCookieName(mode="maintenance"){return mode==="coming-soon"?"stacks_coming_soon_bypass":"stacks_maintenance_bypass"}export function hasValidBypassCookie(cookies,secret,mode="maintenance"){return cookies[bypassCookieName(mode)]===secret}export function isSecretPath(path,secret){return path===`/${secret}`||path.startsWith(`/${secret}/`)}export function maintenanceHtml(payload){const mode=payload.mode??"maintenance",defaults=defaultsForMode(mode),message=escapeHtml(payload.message||defaults.message||""),title=escapeHtml(payload.title||(mode==="coming-soon"?"Coming soon":"Down for maintenance")),appName=escapeHtml((process.env.APP_NAME||"").trim());return`<!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
@@ -74,4 +74,4 @@ import{log}from"@stacksjs/logging/runtime";import*as p from"@stacksjs/path";impo
74
74
  ${payload.retry?`<p class="retry">Expected back in about ${Math.ceil(payload.retry/60)} minutes.</p>`:""}
75
75
  </main>
76
76
  </body>
77
- </html>`}function escapeHtml(value){return value.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#039;")}export function maintenanceResponse(payload){return siteModeResponse(payload)}export function siteModeResponse(payload){const headers={"Content-Type":"text/html; charset=utf-8"};if(payload.retry)headers["Retry-After"]=String(payload.retry);if(payload.redirect)return new Response(null,{status:302,headers:{Location:payload.redirect}});return new Response(maintenanceHtml(payload),{status:payload.status||(payload.mode==="coming-soon"?200:503),headers})}export function bypassCookieValue(secret,mode="maintenance"){return`${bypassCookieName(mode)}=${secret}; Path=/; HttpOnly; SameSite=Lax`}const ALWAYS_ALLOWED_PATHS=new Set(["/coming-soon","/api/email/subscribe","/favicon.ico"]),ALWAYS_ALLOWED_PREFIXES=["/css/","/js/","/images/","/fonts/","/assets/","/_stx/","/_modules/","/@vite/","/@fs/","/__deps/"],ALWAYS_ALLOWED_EXTENSIONS=[".css",".js",".mjs",".map",".woff",".woff2",".ttf",".otf",".eot",".svg",".png",".jpg",".jpeg",".gif",".webp",".avif",".ico"];function configuredAllowList(){return(process.env.APP_MAINTENANCE_ALLOW??process.env.APP_COMING_SOON_ALLOW??"").split(",").map((entry)=>entry.trim()).filter(Boolean)}function isConfiguredAllowed(path){for(const entry of configuredAllowList()){if(entry.endsWith("*")){if(path.startsWith(entry.slice(0,-1)))return!0;continue}if(entry.endsWith("/")){if(path.startsWith(entry))return!0;continue}if(path===entry)return!0}return!1}export function isAlwaysAllowed(path){if(ALWAYS_ALLOWED_PATHS.has(path))return!0;if(isConfiguredAllowed(path))return!0;if(ALWAYS_ALLOWED_PREFIXES.some((p)=>path.startsWith(p)))return!0;const lower=path.toLowerCase();return ALWAYS_ALLOWED_EXTENSIONS.some((ext)=>lower.endsWith(ext))}function parseCookieHeader(header){const out={};if(!header)return out;for(const part of header.split(";")){const trimmed=part.trim(),eq=trimmed.indexOf("=");if(eq===-1)continue;const k=trimmed.slice(0,eq).trim(),v=trimmed.slice(eq+1).trim();if(k)out[k]=v}return out}function clientIp(req){const fwd=req.headers.get("x-forwarded-for");if(fwd)return fwd.split(",")[0]?.trim()??"127.0.0.1";const real=req.headers.get("x-real-ip");if(real)return real;return"127.0.0.1"}export function maintenanceGate(req){const payloadResult=activeSiteModePayloadResult();return payloadResult instanceof Promise?payloadResult.then((payload)=>applyMaintenanceGate(req,payload)):applyMaintenanceGate(req,payloadResult)}function applyMaintenanceGate(req,payload){if(!payload)return null;const mode=payload.mode??"maintenance",path=new URL(req.url).pathname;if(isAlwaysAllowed(path))return null;if(payload.secret&&isSecretPath(path,payload.secret)){const location=mode==="coming-soon"?`/?preview=${encodeURIComponent(payload.secret)}`:"/";return new Response(null,{status:302,headers:{Location:location,"Set-Cookie":bypassCookieValue(payload.secret,mode)}})}const cookies=parseCookieHeader(req.headers.get("cookie")),hasCookie=!!payload.secret&&hasValidBypassCookie(cookies,payload.secret,mode),appEnv=(process.env.APP_ENV||"development").toLowerCase(),trustLocalhost=!["production","staging"].includes(appEnv),ipAllowed=isAllowedIp(clientIp(req),payload.allowed,trustLocalhost);if(hasCookie||ipAllowed)return null;return siteModeResponse(payload)}
77
+ </html>`}function escapeHtml(value){return value.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#039;")}export function maintenanceResponse(payload){return siteModeResponse(payload)}export function siteModeResponse(payload){const headers={"Content-Type":"text/html; charset=utf-8"};if(payload.retry)headers["Retry-After"]=String(payload.retry);if(payload.redirect)return new Response(null,{status:302,headers:{Location:payload.redirect}});return new Response(maintenanceHtml(payload),{status:payload.status||(payload.mode==="coming-soon"?200:503),headers})}export function bypassCookieValue(secret,mode="maintenance"){return`${bypassCookieName(mode)}=${secret}; Path=/; HttpOnly; SameSite=Lax`}const ALWAYS_ALLOWED_PATHS=new Set(["/coming-soon","/api/email/subscribe","/favicon.ico"]),ALWAYS_ALLOWED_PREFIXES=["/css/","/js/","/images/","/fonts/","/assets/","/_stx/","/_modules/","/@vite/","/@fs/","/__deps/"],ALWAYS_ALLOWED_EXTENSIONS=[".css",".js",".mjs",".map",".woff",".woff2",".ttf",".otf",".eot",".svg",".png",".jpg",".jpeg",".gif",".webp",".avif",".ico"];function configuredAllowList(){return(process.env.APP_MAINTENANCE_ALLOW??process.env.APP_COMING_SOON_ALLOW??"").split(",").map((entry)=>entry.trim()).filter(Boolean)}function isConfiguredAllowed(path){for(const entry of configuredAllowList()){if(entry.endsWith("*")){if(path.startsWith(entry.slice(0,-1)))return!0;continue}if(entry.endsWith("/")){if(path.startsWith(entry))return!0;continue}if(path===entry)return!0}return!1}export function isAlwaysAllowed(path){if(ALWAYS_ALLOWED_PATHS.has(path))return!0;if(isConfiguredAllowed(path))return!0;if(ALWAYS_ALLOWED_PREFIXES.some((p)=>path.startsWith(p)))return!0;const lower=path.toLowerCase();return ALWAYS_ALLOWED_EXTENSIONS.some((ext)=>lower.endsWith(ext))}function parseCookieHeader(header){const out={};if(!header)return out;for(const part of header.split(";")){const trimmed=part.trim(),eq=trimmed.indexOf("=");if(eq===-1)continue;const k=trimmed.slice(0,eq).trim(),v=trimmed.slice(eq+1).trim();if(k)out[k]=v}return out}function clientIp(req){return clientAddress(req)??"127.0.0.1"}export function maintenanceGate(req){const payloadResult=activeSiteModePayloadResult();return payloadResult instanceof Promise?payloadResult.then((payload)=>applyMaintenanceGate(req,payload)):applyMaintenanceGate(req,payloadResult)}function applyMaintenanceGate(req,payload){if(!payload)return null;const mode=payload.mode??"maintenance",path=new URL(req.url).pathname;if(isAlwaysAllowed(path))return null;if(payload.secret&&isSecretPath(path,payload.secret)){const location=mode==="coming-soon"?`/?preview=${encodeURIComponent(payload.secret)}`:"/";return new Response(null,{status:302,headers:{Location:location,"Set-Cookie":bypassCookieValue(payload.secret,mode)}})}const cookies=parseCookieHeader(req.headers.get("cookie")),hasCookie=!!payload.secret&&hasValidBypassCookie(cookies,payload.secret,mode),appEnv=(process.env.APP_ENV||"development").toLowerCase(),trustLocalhost=!["production","staging"].includes(appEnv),ipAllowed=isAllowedIp(clientIp(req),payload.allowed,trustLocalhost);if(hasCookie||ipAllowed)return null;return siteModeResponse(payload)}
package/dist/proxy.d.ts CHANGED
@@ -53,7 +53,12 @@ export declare function unforwardableRoutes<T extends RoutePathLike>(routes: rea
53
53
  * would.
54
54
  */
55
55
  export declare function describeUnforwardableRoutes(routes: readonly (RoutePathLike & { file?: string })[]): string;
56
- export declare function proxyToBackend(req: Request, backendBase: string, stripPrefix?: string): Promise<Response>;
56
+ /**
57
+ * Forward `req` to `backendBase`. `peerOf` is the request whose socket peer is
58
+ * appended to `X-Forwarded-For`: `req` itself, or - when `req` is a rewritten
59
+ * copy, which has no socket - the request that arrived.
60
+ */
61
+ export declare function proxyToBackend(req: Request, backendBase: string, stripPrefix?: string, peerOf?: Request): Promise<Response>;
57
62
  /**` traffic goes, or null when there is no safe answer.
58
63
  *
59
64
  * Returning null is the important part. On a shared box `127.0.0.1:<default>`
@@ -73,9 +78,6 @@ export declare function proxyToBackend(req: Request, backendBase: string, stripP
73
78
  * drift.
74
79
  */
75
80
  export declare function resolveApiBase(configuredPort?: number, env?: NodeJS.ProcessEnv): string | null;
76
- /**` traffic behaves identically
77
- * in both topologies (stacksjs/stacks#1950).
78
- */
79
81
  /** The prefix that is always API-bound, with or without configuration. */
80
82
  export declare const DEFAULT_API_PREFIX: '/api/';
81
83
  /**
package/dist/proxy.js CHANGED
@@ -1,2 +1,2 @@
1
- export const DEFAULT_API_PREFIX="/api/",DEFAULT_API_METHODS=["POST","PUT","PATCH","DELETE"];export function resolveApiProxyRules(input={}){const prefixes=[DEFAULT_API_PREFIX];for(const raw of input.prefixes??[]){const prefix=String(raw).trim();if(!prefix||!prefix.startsWith("/"))continue;const normalized=prefix.endsWith("/")?prefix:`${prefix}/`;if(!prefixes.includes(normalized))prefixes.push(normalized)}const paths=[];for(const raw of input.paths??[]){const path=String(raw).trim();if(!path||!path.startsWith("/"))continue;const normalized=path.length>1&&path.endsWith("/")?path.slice(0,-1):path;if(!paths.includes(normalized))paths.push(normalized)}const methods=new Set((input.methods??DEFAULT_API_METHODS).map((method)=>String(method).toUpperCase()));return{prefixes,paths,methods}}function matchesPrefix(pathname,prefix){return pathname.startsWith(prefix)||pathname===prefix.slice(0,-1)}export function isApiBoundRequest(req,pathname,rules){return isApiBoundPath(req.method,pathname,rules)}export function isApiBoundPath(method,pathname,rules){const verb=method.toUpperCase();if(!rules)return pathname.startsWith(DEFAULT_API_PREFIX)||DEFAULT_API_METHODS.includes(verb);if(rules.methods.has(verb))return!0;if(rules.paths.includes(pathname))return!0;return rules.prefixes.some((prefix)=>matchesPrefix(pathname,prefix))}export function describeApiProxyRules(rules){const parts=[`prefixes ${rules.prefixes.join(" ")}`,`methods ${[...rules.methods].sort().join("/")}`];if(rules.paths.length>0)parts.push(`paths ${rules.paths.join(" ")}`);return parts.join(", ")}export function unforwardableRoutes(routes,rules){return routes.filter((route)=>!isApiBoundPath(route.method,route.path,rules))}export function describeUnforwardableRoutes(routes){const lines=routes.map((route)=>{const origin=route.file?` (${route.file})`:"";return` ${route.method} ${route.path}${origin}`});return[`${routes.length} root-mounted route${routes.length===1?"":"s"} the views server will not forward, so ${routes.length===1?"it":"they"} will answer the view 404 rather than reaching the handler:`,...lines,"Add them to `proxy.paths` in config/server.ts (or their prefix to `proxy.prefixes`) so the views server forwards them to the API."].join(`
2
- `)}export async function proxyToBackend(req,backendBase,stripPrefix){const incoming=new URL(req.url);let pathname=incoming.pathname;if(stripPrefix&&(pathname===stripPrefix||pathname.startsWith(`${stripPrefix}/`)))pathname=pathname.slice(stripPrefix.length)||"/";const target=`${backendBase}${pathname}${incoming.search}`,fwd=new Headers(req.headers);fwd.delete("host");fwd.delete("content-length");fwd.set("x-forwarded-host",incoming.host);fwd.set("x-forwarded-proto",incoming.protocol.replace(":",""));const body=req.method==="GET"||req.method==="HEAD"?void 0:await req.arrayBuffer(),upstream=await fetch(target,{method:req.method,headers:fwd,body,redirect:"manual"}),out=new Headers(upstream.headers);out.delete("content-length");out.delete("content-encoding");return new Response(upstream.body,{status:upstream.status,statusText:upstream.statusText,headers:out})}export function resolveApiBase(configuredPort,env=process.env){if(env.API_URL)return env.API_URL;const explicitPort=Number(env.PORT_API);if(explicitPort)return`http://127.0.0.1:${explicitPort}`;if(["production","staging","development"].includes((env.APP_ENV||"").toLowerCase()))return null;return`http://127.0.0.1:${configuredPort||3008}`}
1
+ import{peerAddress}from"@stacksjs/bun-router";export const DEFAULT_API_PREFIX="/api/",DEFAULT_API_METHODS=["POST","PUT","PATCH","DELETE"];export function resolveApiProxyRules(input={}){const prefixes=[DEFAULT_API_PREFIX];for(const raw of input.prefixes??[]){const prefix=String(raw).trim();if(!prefix||!prefix.startsWith("/"))continue;const normalized=prefix.endsWith("/")?prefix:`${prefix}/`;if(!prefixes.includes(normalized))prefixes.push(normalized)}const paths=[];for(const raw of input.paths??[]){const path=String(raw).trim();if(!path||!path.startsWith("/"))continue;const normalized=path.length>1&&path.endsWith("/")?path.slice(0,-1):path;if(!paths.includes(normalized))paths.push(normalized)}const methods=new Set((input.methods??DEFAULT_API_METHODS).map((method)=>String(method).toUpperCase()));return{prefixes,paths,methods}}function matchesPrefix(pathname,prefix){return pathname.startsWith(prefix)||pathname===prefix.slice(0,-1)}export function isApiBoundRequest(req,pathname,rules){return isApiBoundPath(req.method,pathname,rules)}export function isApiBoundPath(method,pathname,rules){const verb=method.toUpperCase();if(!rules)return pathname.startsWith(DEFAULT_API_PREFIX)||DEFAULT_API_METHODS.includes(verb);if(rules.methods.has(verb))return!0;if(rules.paths.includes(pathname))return!0;return rules.prefixes.some((prefix)=>matchesPrefix(pathname,prefix))}export function describeApiProxyRules(rules){const parts=[`prefixes ${rules.prefixes.join(" ")}`,`methods ${[...rules.methods].sort().join("/")}`];if(rules.paths.length>0)parts.push(`paths ${rules.paths.join(" ")}`);return parts.join(", ")}export function unforwardableRoutes(routes,rules){return routes.filter((route)=>!isApiBoundPath(route.method,route.path,rules))}export function describeUnforwardableRoutes(routes){const lines=routes.map((route)=>{const origin=route.file?` (${route.file})`:"";return` ${route.method} ${route.path}${origin}`});return[`${routes.length} root-mounted route${routes.length===1?"":"s"} the views server will not forward, so ${routes.length===1?"it":"they"} will answer the view 404 rather than reaching the handler:`,...lines,"Add them to `proxy.paths` in config/server.ts (or their prefix to `proxy.prefixes`) so the views server forwards them to the API."].join(`
2
+ `)}export async function proxyToBackend(req,backendBase,stripPrefix,peerOf=req){const incoming=new URL(req.url);let pathname=incoming.pathname;if(stripPrefix&&(pathname===stripPrefix||pathname.startsWith(`${stripPrefix}/`)))pathname=pathname.slice(stripPrefix.length)||"/";const target=`${backendBase}${pathname}${incoming.search}`,fwd=new Headers(req.headers);fwd.delete("host");fwd.delete("content-length");const peer=peerAddress(peerOf);if(peer){const forwardedFor=fwd.get("x-forwarded-for");fwd.set("x-forwarded-for",forwardedFor?`${forwardedFor}, ${peer}`:peer)}fwd.set("x-forwarded-host",incoming.host);fwd.set("x-forwarded-proto",incoming.protocol.replace(":",""));const body=req.method==="GET"||req.method==="HEAD"?void 0:await req.arrayBuffer(),upstream=await fetch(target,{method:req.method,headers:fwd,body,redirect:"manual"}),out=new Headers(upstream.headers);out.delete("content-length");out.delete("content-encoding");return new Response(upstream.body,{status:upstream.status,statusText:upstream.statusText,headers:out})}export function resolveApiBase(configuredPort,env=process.env){if(env.API_URL)return env.API_URL;const explicitPort=Number(env.PORT_API);if(explicitPort)return`http://127.0.0.1:${explicitPort}`;if(["production","staging","development"].includes((env.APP_ENV||"").toLowerCase()))return null;return`http://127.0.0.1:${configuredPort||3008}`}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/server",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.66",
5
+ "version": "0.75.68",
6
6
  "description": "Local development and production-ready.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -58,19 +58,20 @@
58
58
  "prepublishOnly": "bun run build"
59
59
  },
60
60
  "dependencies": {
61
- "@stacksjs/auth": "0.75.66",
62
- "@stacksjs/config": "0.75.66",
63
- "@stacksjs/events": "0.75.66",
64
- "@stacksjs/i18n": "0.75.66",
65
- "@stacksjs/logging": "0.75.66",
66
- "@stacksjs/orm": "0.75.66",
67
- "@stacksjs/path": "0.75.66",
68
- "@stacksjs/router": "0.75.66",
69
- "@stacksjs/storage": "0.75.66",
61
+ "@stacksjs/auth": "0.75.68",
62
+ "@stacksjs/bun-router": "^0.1.22",
63
+ "@stacksjs/config": "0.75.68",
64
+ "@stacksjs/events": "0.75.68",
65
+ "@stacksjs/i18n": "0.75.68",
66
+ "@stacksjs/logging": "0.75.68",
67
+ "@stacksjs/orm": "0.75.68",
68
+ "@stacksjs/path": "0.75.68",
69
+ "@stacksjs/router": "0.75.68",
70
+ "@stacksjs/storage": "0.75.68",
70
71
  "bun-plugin-auto-imports": "^0.4.3"
71
72
  },
72
73
  "devDependencies": {
73
- "@stacksjs/validation": "0.75.66",
74
+ "@stacksjs/validation": "0.75.68",
74
75
  "better-dx": "^0.2.24"
75
76
  }
76
77
  }