@stacksjs/buddy 0.74.39 → 0.74.40

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.
@@ -40,17 +40,36 @@ export declare function startProductionServer(options?: { port?: string | number
40
40
  * {@link applyDocumentCacheControl} is what keeps that safe.
41
41
  */
42
42
  export declare function buildDocumentCacheControl(documents?: { maxAge?: number, staleWhileRevalidate?: number }): string | undefined;
43
+ /**
44
+ * Does this request belong to a signed-in visitor?
45
+ *
46
+ * A page rendered for someone in particular must never be handed to a shared
47
+ * cache, and the response alone cannot always say so — an app that keeps its
48
+ * session in `localStorage` sends no cookie back, and one that reuses an
49
+ * existing session sets none either. So the REQUEST is what decides.
50
+ *
51
+ * The CSRF cookie is excluded deliberately: it is a double-submit token every
52
+ * visitor gets, signed in or not, and treating it as a session would make
53
+ * every page uncacheable for everybody — which is exactly the state this is
54
+ * here to fix.
55
+ */
56
+ export declare function isAuthenticatedRequest(request: Request): boolean;
43
57
  /**
44
58
  * Mark a document cacheable, unless it is about one visitor.
45
59
  *
46
- * The cookie check is the whole safety of the feature and is not the caller's
47
- * to remember: a response carrying `Set-Cookie` is per-visitor by definition,
48
- * and a shared cache told it may reuse that document will hand one person's
49
- * page session cookie included to the next person who asks. An app that
50
- * wants its pages cached should stop setting the cookie, not have the header
51
- * applied over the top of it.
60
+ * Two guards, and both are the framework's to enforce rather than each app's
61
+ * to remember:
62
+ *
63
+ * - the response sets a cookie — it is per-visitor by definition, and a
64
+ * shared cache told to reuse it serves that cookie to whoever asks next;
65
+ * - the request was authenticated — the page may hold someone's data even
66
+ * when the response sets nothing.
52
67
  *
53
- * Anything already carrying its own `Cache-Control` is left alone; that is a
54
- * decision something closer to the response already made.
68
+ * Past those, the declared header REPLACES what is already there. That is the
69
+ * point: the page pipeline marks renders `no-store` by default, so a version
70
+ * that only filled in a missing header could never do anything, and an app
71
+ * that declared `documents` would get silence instead of caching. Declaring it
72
+ * is the app asserting these pages are shareable; the guards above are what
73
+ * keep that assertion from being taken on faith.
55
74
  */
56
- export declare function applyDocumentCacheControl(response: Response, cacheControl?: string): Response;
75
+ export declare function applyDocumentCacheControl(request: Request, response: Response, cacheControl?: string): Response;
@@ -1 +1 @@
1
- import{existsSync}from"node:fs";import{join}from"node:path";import process from"node:process";import{installRequestContext,parseCookieHeader}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{siteConfigPath}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{resolveStxSource}from"./stx-source";function productionRequestSnapshot(){const{__stxServeContext:snapshot,__stxServeCookies:legacyCookies,__stxServeSearch:legacySearch,__stxServeSite:site}=globalThis;if(!snapshot&&!legacyCookies&&legacySearch===void 0)return;return{...snapshot,cookies:snapshot?.cookies??legacyCookies??{},url:snapshot?.url||snapshot?.search||legacySearch||"",search:snapshot?.search??legacySearch??"",site:snapshot?.site??site??null}}installRequestContext(productionRequestSnapshot);function parseCookies(req){return parseCookieHeader(req.headers.get("cookie"))}export function resolveUserPartialsPath(cwd=process.cwd(),configuredDir){if(configuredDir){const configured=[join(cwd,"resources",configuredDir),join(cwd,configuredDir)].find((candidate)=>existsSync(candidate));if(configured)return configured}const existing=["resources/partials","resources/views/partials","partials","resources/components"].filter((candidate)=>existsSync(join(cwd,candidate)));if(existing.length===0)return;const populated=existing.find((candidate)=>containsTemplates(join(cwd,candidate)));return join(cwd,populated??existing[0])}function containsTemplates(dir){try{return[...new Bun.Glob("**/*.stx").scanSync({cwd:dir,onlyFiles:!0})].length>0}catch{return!1}}export async function loadStxPartialsDir(cwd=process.cwd()){for(const name of["stx","ui"]){const configPath=join(cwd,`config/${name}.ts`);if(!existsSync(configPath))continue;try{const dir=(await import(configPath)).default?.partialsDir;if(typeof dir==="string"&&dir.length>0)return dir}catch{}}return}function resolveCsrfMiddlewarePath(){const rel="app/Middleware/Csrf.ts",vendored=join(process.cwd(),"storage/framework/defaults",rel);if(existsSync(vendored))return vendored;try{const pkgJson=Bun.resolveSync("@stacksjs/defaults/package.json",process.cwd());return`${pkgJson.slice(0,pkgJson.lastIndexOf("/"))}/${rel}`}catch{return vendored}}export async function startProductionServer(options){if(options?.port)process.env.PORT=String(options.port);process.env.APP_ENV=process.env.APP_ENV||"production";const port=Number(process.env.PORT)||3000,{config,overridesReady,resolveViewPatterns}=await import("@stacksjs/config");await overridesReady;const{ensureDiscoveredPackages}=await import("@stacksjs/actions");await ensureDiscoveredPackages();const{applyViewSecurityHeaders,describeApiProxyRules,describeRedirectRules,describeRewriteRules,injectGlobalAutoImports,resolveApiBase,resolveApiProxyRules,resolveEmbeddableRules,resolveRedirectRules,resolveRewriteRules}=await import("@stacksjs/server"),{resolveDefaultsResources}=await import("@stacksjs/actions/dev/defaults-resources"),{stxPageAuthMiddleware}=await import("@stacksjs/auth"),{enhanceRequest,loadMiddlewareHandlers}=await import("@stacksjs/router"),pageMiddleware=await loadMiddlewareHandlers();try{const{autoImportsAreStale,generateAutoImportFiles}=await import("@stacksjs/server");if(autoImportsAreStale()){log.info("[server] Installed packages moved since the auto-import barrel was written; rebuilding it.");await generateAutoImportFiles({declarations:!1})}}catch(error){log.warn(`[server] Could not refresh the auto-import barrel: ${error instanceof Error?error.message:String(error)}`)}await injectGlobalAutoImports();let stxServe;const serveSource=resolveStxSource({value:process.env.BUN_PLUGIN_STX_SRC});if(serveSource.kind==="missing")await log.exit(`BUN_PLUGIN_STX_SRC points at ${serveSource.path}, which does not exist. Unset it to use the installed bun-plugin-stx.`,ExitCode.FatalError);if(serveSource.kind==="override"){({serve:stxServe}=await import(serveSource.path));log.warn(`Serving views through ${serveSource.path} instead of the installed bun-plugin-stx.`)}else({serve:stxServe}=await import("bun-plugin-stx/serve"));log.debug(`stx serve implementation: ${serveSource.kind==="override"?serveSource.path:"bun-plugin-stx/serve"}`);const stxModule=await resolveVendoredStxModule(),{site:siteConfig,i18n:i18nConfig}=await loadStxSiteConfig(),userViewsPath="resources/views",defaultsResources=resolveDefaultsResources(),defaultViewsPath=join(defaultsResources,"views"),userLayoutsPath=existsSync("resources/views/layouts")?"resources/views/layouts":"resources/layouts",userComponentsPath=existsSync("resources/views/components")?"resources/views/components":"resources/components",userPartialsPath=resolveUserPartialsPath(process.cwd(),await loadStxPartialsDir()),apiBase=resolveApiBase(config.ports?.api),viewPatterns=resolveViewPatterns(userViewsPath,defaultViewsPath,config?.ui?.defaultViews);for(const name of viewPatterns.missing)log.warn(`ui.defaultViews lists "${name}", which does not exist under ${defaultViewsPath} - ignoring.`);const apiProxyRules=resolveApiProxyRules(config.server?.proxy);if(apiProxyRules.paths.length>0||apiProxyRules.prefixes.length>1)log.info(`API proxy: ${describeApiProxyRules(apiProxyRules)}`);const redirectRules=resolveRedirectRules(config.server?.redirects);if(redirectRules.size>0)log.info(`Redirects: ${describeRedirectRules(redirectRules)}`);const rewriteRules=resolveRewriteRules(config.server?.rewrites);if(rewriteRules.size>0)log.info(`Rewrites: ${describeRewriteRules(rewriteRules)}`);const cacheConfig=config.server?.cache,documentCacheControl=buildDocumentCacheControl(cacheConfig?.documents);if(cacheConfig?.renders)log.info(`Render cache: on, keyed by ${cacheConfig.renderVary??"request"}${cacheConfig.prewarm?", prewarmed":""}`);if(documentCacheControl)log.info(`Document cache: ${documentCacheControl}`);const embeddableRules=resolveEmbeddableRules(config.server?.security?.embeddable);if(embeddableRules.paths.length>0||embeddableRules.prefixes.length>0)log.info(`Frameable by other origins: ${[...embeddableRules.paths,...embeddableRules.prefixes].join(" ")}`);log.info(`Starting production server on port ${port}...`);await stxServe({patterns:viewPatterns.patterns,port,autoIncrementPort:!1,reusePort:["production","staging","development"].includes((process.env.APP_ENV||"").toLowerCase()),componentsDir:userComponentsPath,fallbackComponentsDir:join(defaultsResources,"components"),layoutsDir:userLayoutsPath,...userPartialsPath&&{partialsDir:userPartialsPath},fallbackLayoutsDir:join(defaultsResources,"layouts"),fallbackPartialsDir:defaultViewsPath,quiet:options?.verbose!==!0,...cacheConfig?.renders&&{renderCache:!0,renderCacheVary:cacheConfig.renderVary??"request",...cacheConfig.prewarm&&{prewarmRenderCache:!0}},...stxModule&&{stxModule},...i18nConfig&&{i18n:i18nConfig},...siteConfig?.url&&{site:siteConfig},middleware:{...pageMiddleware,...stxPageAuthMiddleware()},prepareMiddlewareRequest:(request)=>enhanceRequest(request),onRequest:async(req)=>{const{maintenanceGate,isApiBoundRequest:isApiBound,proxyToBackend,resolveRedirect,resolveRewrite}=await import("@stacksjs/server"),gated=await maintenanceGate(req);if(gated)return gated;const url=new URL(req.url),redirected=resolveRedirect(url,redirectRules);if(redirected)return redirected;const rewritten=resolveRewrite(url.pathname,rewriteRules);if(rewritten){if(!apiBase){log.error(`No API target configured for ${url.pathname} (rewritten to ${rewritten}); refusing to proxy.`);return new Response("Bad Gateway",{status:502})}try{const target=new URL(`${rewritten}${url.search}`,url.origin);return await proxyToBackend(new Request(target,req),apiBase)}catch(error){log.error(`Rewrite of ${url.pathname} to ${rewritten} failed: ${error.message}`);return new Response("Bad Gateway",{status:502})}}if(isApiBound(req,url.pathname,apiProxyRules)){if(!apiBase){log.error(`No API target configured for ${url.pathname}. This app shares its host with other deployments, so there is no safe default port to guess - refusing to proxy. Set PORT_API (or API_URL) for this site, and deploy an \`api\` site on its own port.`);return new Response("Bad Gateway",{status:502})}try{return await proxyToBackend(req,apiBase)}catch(error){log.error(`API proxy to ${apiBase} failed: ${error.message}`);return new Response("Bad Gateway",{status:502})}}if(existsSync(join(process.cwd(),"resources/views/blog.stx"))){const{renderBlogFeed}=await import("@stacksjs/actions/blog"),feed=await renderBlogFeed(req);if(feed)return feed}globalThis.__stxServeSearch=url.search;globalThis.__stxServeCookies=parseCookies(req);if(config.analytics?.capturePageviews)import("@stacksjs/analytics").then(({recordPageview})=>recordPageview(req)).catch(()=>{});if(config.sites?.enabled){const sites=await import("@stacksjs/sites"),resolved=await sites.resolveSiteByHost(sites.requestHost(req.headers,sites.sitesOptions()));sites.setCurrentSite(resolved);globalThis.__stxServeSite=sites.toSiteSnapshot(resolved)}else globalThis.__stxServeSite=null;return},onResponse:async(req,response)=>{const secured=applyViewSecurityHeaders(req,response,embeddableRules),method=req.method.toUpperCase();if(method!=="GET"&&method!=="HEAD")return secured;const baseline=secured??response;let current=baseline;if(current.status===404&&config.sites?.enabled)try{const{cmsNotFoundFallback}=await import("@stacksjs/cms"),cmsResponse=await cmsNotFoundFallback(req);if(cmsResponse)current=cmsResponse}catch(error){log.debug(`CMS fallback skipped: ${error.message}`)}let finished;try{const{seedCsrfCookieIfMissing}=await import(resolveCsrfMiddlewarePath());finished=await seedCsrfCookieIfMissing(req,current)??(current===baseline?secured:current)}catch(error){log.debug(`CSRF cookie seeding skipped: ${error.message}`);finished=current===baseline?secured:current}if(!documentCacheControl)return finished;return applyDocumentCacheControl(finished??response,documentCacheControl)}});log.success(`Production server listening on http://0.0.0.0:${port}`)}export function buildDocumentCacheControl(documents){if(!documents)return;const maxAge=Number(documents.maxAge??0);if(!Number.isFinite(maxAge)||maxAge<=0)return;const stale=Number(documents.staleWhileRevalidate??0),parts=["public",`max-age=${Math.floor(maxAge)}`];if(Number.isFinite(stale)&&stale>0)parts.push(`stale-while-revalidate=${Math.floor(stale)}`);return parts.join(", ")}export function applyDocumentCacheControl(response,cacheControl){if(!cacheControl)return response;const contentType=response.headers.get("content-type")||"";if(response.status!==200||!contentType.startsWith("text/html"))return response;if(response.headers.has("cache-control"))return response;if(response.headers.getSetCookie().length>0)return response;const headers=new Headers(response.headers);headers.set("cache-control",cacheControl);return new Response(response.body,{status:response.status,statusText:response.statusText,headers})}async function resolveVendoredStxModule(){const source=resolveStxSource({value:process.env.STACKS_STX_SRC});if(source.kind==="missing")await log.exit(`STACKS_STX_SRC points at ${source.path}, which does not exist. Unset it to use the installed @stacksjs/stx.`,ExitCode.FatalError);if(source.kind==="override"){log.warn(`Rendering through ${source.path} instead of the installed @stacksjs/stx.`);return await import(source.path)}try{return await import("@stacksjs/stx")}catch{}return}function fallbackI18nFromSite(site){const locales=site.i18n.locales,defaultLocale=site.i18n.defaultLocale??locales[0];return{locales,defaultLocale,labels:site.i18n.labels??Object.fromEntries(locales.map((c)=>[c,c.toUpperCase()])),translations:{},pickerSelector:site.i18n.pickerSelector??"#lang-picker"}}async function resolveSiteI18n(site){try{const resolved=await resolveVendoredStxModule();if(typeof resolved?.resolveI18n==="function"){const i18n=resolved.resolveI18n(site,process.cwd());if(i18n)return i18n}}catch{}return fallbackI18nFromSite(site)}async function loadStxSiteConfig(){const sitePath=siteConfigPath();if(!existsSync(sitePath))return{};try{const mod=await import(sitePath),site=mod.default??mod.site??mod.config;if(!site)return{};if(!site.i18n)return{site};const i18n=await resolveSiteI18n(site);return{site,i18n}}catch{}return{}}
1
+ import{existsSync}from"node:fs";import{join}from"node:path";import process from"node:process";import{installRequestContext,parseCookieHeader}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{siteConfigPath}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{resolveStxSource}from"./stx-source";function productionRequestSnapshot(){const{__stxServeContext:snapshot,__stxServeCookies:legacyCookies,__stxServeSearch:legacySearch,__stxServeSite:site}=globalThis;if(!snapshot&&!legacyCookies&&legacySearch===void 0)return;return{...snapshot,cookies:snapshot?.cookies??legacyCookies??{},url:snapshot?.url||snapshot?.search||legacySearch||"",search:snapshot?.search??legacySearch??"",site:snapshot?.site??site??null}}installRequestContext(productionRequestSnapshot);function parseCookies(req){return parseCookieHeader(req.headers.get("cookie"))}export function resolveUserPartialsPath(cwd=process.cwd(),configuredDir){if(configuredDir){const configured=[join(cwd,"resources",configuredDir),join(cwd,configuredDir)].find((candidate)=>existsSync(candidate));if(configured)return configured}const existing=["resources/partials","resources/views/partials","partials","resources/components"].filter((candidate)=>existsSync(join(cwd,candidate)));if(existing.length===0)return;const populated=existing.find((candidate)=>containsTemplates(join(cwd,candidate)));return join(cwd,populated??existing[0])}function containsTemplates(dir){try{return[...new Bun.Glob("**/*.stx").scanSync({cwd:dir,onlyFiles:!0})].length>0}catch{return!1}}export async function loadStxPartialsDir(cwd=process.cwd()){for(const name of["stx","ui"]){const configPath=join(cwd,`config/${name}.ts`);if(!existsSync(configPath))continue;try{const dir=(await import(configPath)).default?.partialsDir;if(typeof dir==="string"&&dir.length>0)return dir}catch{}}return}function resolveCsrfMiddlewarePath(){const rel="app/Middleware/Csrf.ts",vendored=join(process.cwd(),"storage/framework/defaults",rel);if(existsSync(vendored))return vendored;try{const pkgJson=Bun.resolveSync("@stacksjs/defaults/package.json",process.cwd());return`${pkgJson.slice(0,pkgJson.lastIndexOf("/"))}/${rel}`}catch{return vendored}}export async function startProductionServer(options){if(options?.port)process.env.PORT=String(options.port);process.env.APP_ENV=process.env.APP_ENV||"production";const port=Number(process.env.PORT)||3000,{config,overridesReady,resolveViewPatterns}=await import("@stacksjs/config");await overridesReady;const{ensureDiscoveredPackages}=await import("@stacksjs/actions");await ensureDiscoveredPackages();const{applyViewSecurityHeaders,describeApiProxyRules,describeRedirectRules,describeRewriteRules,injectGlobalAutoImports,resolveApiBase,resolveApiProxyRules,resolveEmbeddableRules,resolveRedirectRules,resolveRewriteRules}=await import("@stacksjs/server"),{resolveDefaultsResources}=await import("@stacksjs/actions/dev/defaults-resources"),{stxPageAuthMiddleware}=await import("@stacksjs/auth"),{enhanceRequest,loadMiddlewareHandlers}=await import("@stacksjs/router"),pageMiddleware=await loadMiddlewareHandlers();try{const{autoImportsAreStale,generateAutoImportFiles}=await import("@stacksjs/server");if(autoImportsAreStale()){log.info("[server] Installed packages moved since the auto-import barrel was written; rebuilding it.");await generateAutoImportFiles({declarations:!1})}}catch(error){log.warn(`[server] Could not refresh the auto-import barrel: ${error instanceof Error?error.message:String(error)}`)}await injectGlobalAutoImports();let stxServe;const serveSource=resolveStxSource({value:process.env.BUN_PLUGIN_STX_SRC});if(serveSource.kind==="missing")await log.exit(`BUN_PLUGIN_STX_SRC points at ${serveSource.path}, which does not exist. Unset it to use the installed bun-plugin-stx.`,ExitCode.FatalError);if(serveSource.kind==="override"){({serve:stxServe}=await import(serveSource.path));log.warn(`Serving views through ${serveSource.path} instead of the installed bun-plugin-stx.`)}else({serve:stxServe}=await import("bun-plugin-stx/serve"));log.debug(`stx serve implementation: ${serveSource.kind==="override"?serveSource.path:"bun-plugin-stx/serve"}`);const stxModule=await resolveVendoredStxModule(),{site:siteConfig,i18n:i18nConfig}=await loadStxSiteConfig(),userViewsPath="resources/views",defaultsResources=resolveDefaultsResources(),defaultViewsPath=join(defaultsResources,"views"),userLayoutsPath=existsSync("resources/views/layouts")?"resources/views/layouts":"resources/layouts",userComponentsPath=existsSync("resources/views/components")?"resources/views/components":"resources/components",userPartialsPath=resolveUserPartialsPath(process.cwd(),await loadStxPartialsDir()),apiBase=resolveApiBase(config.ports?.api),viewPatterns=resolveViewPatterns(userViewsPath,defaultViewsPath,config?.ui?.defaultViews);for(const name of viewPatterns.missing)log.warn(`ui.defaultViews lists "${name}", which does not exist under ${defaultViewsPath} - ignoring.`);const apiProxyRules=resolveApiProxyRules(config.server?.proxy);if(apiProxyRules.paths.length>0||apiProxyRules.prefixes.length>1)log.info(`API proxy: ${describeApiProxyRules(apiProxyRules)}`);const redirectRules=resolveRedirectRules(config.server?.redirects);if(redirectRules.size>0)log.info(`Redirects: ${describeRedirectRules(redirectRules)}`);const rewriteRules=resolveRewriteRules(config.server?.rewrites);if(rewriteRules.size>0)log.info(`Rewrites: ${describeRewriteRules(rewriteRules)}`);const cacheConfig=config.server?.cache,documentCacheControl=buildDocumentCacheControl(cacheConfig?.documents);if(cacheConfig?.renders)log.info(`Render cache: on, keyed by ${cacheConfig.renderVary??"request"}${cacheConfig.prewarm?", prewarmed":""}`);if(documentCacheControl)log.info(`Document cache: ${documentCacheControl}`);const embeddableRules=resolveEmbeddableRules(config.server?.security?.embeddable);if(embeddableRules.paths.length>0||embeddableRules.prefixes.length>0)log.info(`Frameable by other origins: ${[...embeddableRules.paths,...embeddableRules.prefixes].join(" ")}`);log.info(`Starting production server on port ${port}...`);await stxServe({patterns:viewPatterns.patterns,port,autoIncrementPort:!1,reusePort:["production","staging","development"].includes((process.env.APP_ENV||"").toLowerCase()),componentsDir:userComponentsPath,fallbackComponentsDir:join(defaultsResources,"components"),layoutsDir:userLayoutsPath,...userPartialsPath&&{partialsDir:userPartialsPath},fallbackLayoutsDir:join(defaultsResources,"layouts"),fallbackPartialsDir:defaultViewsPath,quiet:options?.verbose!==!0,...cacheConfig?.renders&&{renderCache:!0,renderCacheVary:cacheConfig.renderVary??"request",...cacheConfig.prewarm&&{prewarmRenderCache:!0}},...stxModule&&{stxModule},...i18nConfig&&{i18n:i18nConfig},...siteConfig?.url&&{site:siteConfig},middleware:{...pageMiddleware,...stxPageAuthMiddleware()},prepareMiddlewareRequest:(request)=>enhanceRequest(request),onRequest:async(req)=>{const{maintenanceGate,isApiBoundRequest:isApiBound,proxyToBackend,resolveRedirect,resolveRewrite}=await import("@stacksjs/server"),gated=await maintenanceGate(req);if(gated)return gated;const url=new URL(req.url),redirected=resolveRedirect(url,redirectRules);if(redirected)return redirected;const rewritten=resolveRewrite(url.pathname,rewriteRules);if(rewritten){if(!apiBase){log.error(`No API target configured for ${url.pathname} (rewritten to ${rewritten}); refusing to proxy.`);return new Response("Bad Gateway",{status:502})}try{const target=new URL(`${rewritten}${url.search}`,url.origin);return await proxyToBackend(new Request(target,req),apiBase)}catch(error){log.error(`Rewrite of ${url.pathname} to ${rewritten} failed: ${error.message}`);return new Response("Bad Gateway",{status:502})}}if(isApiBound(req,url.pathname,apiProxyRules)){if(!apiBase){log.error(`No API target configured for ${url.pathname}. This app shares its host with other deployments, so there is no safe default port to guess - refusing to proxy. Set PORT_API (or API_URL) for this site, and deploy an \`api\` site on its own port.`);return new Response("Bad Gateway",{status:502})}try{return await proxyToBackend(req,apiBase)}catch(error){log.error(`API proxy to ${apiBase} failed: ${error.message}`);return new Response("Bad Gateway",{status:502})}}if(existsSync(join(process.cwd(),"resources/views/blog.stx"))){const{renderBlogFeed}=await import("@stacksjs/actions/blog"),feed=await renderBlogFeed(req);if(feed)return feed}globalThis.__stxServeSearch=url.search;globalThis.__stxServeCookies=parseCookies(req);if(config.analytics?.capturePageviews)import("@stacksjs/analytics").then(({recordPageview})=>recordPageview(req)).catch(()=>{});if(config.sites?.enabled){const sites=await import("@stacksjs/sites"),resolved=await sites.resolveSiteByHost(sites.requestHost(req.headers,sites.sitesOptions()));sites.setCurrentSite(resolved);globalThis.__stxServeSite=sites.toSiteSnapshot(resolved)}else globalThis.__stxServeSite=null;return},onResponse:async(req,response)=>{const secured=applyViewSecurityHeaders(req,response,embeddableRules),method=req.method.toUpperCase();if(method!=="GET"&&method!=="HEAD")return secured;const baseline=secured??response;let current=baseline;if(current.status===404&&config.sites?.enabled)try{const{cmsNotFoundFallback}=await import("@stacksjs/cms"),cmsResponse=await cmsNotFoundFallback(req);if(cmsResponse)current=cmsResponse}catch(error){log.debug(`CMS fallback skipped: ${error.message}`)}let finished;try{const{seedCsrfCookieIfMissing}=await import(resolveCsrfMiddlewarePath());finished=await seedCsrfCookieIfMissing(req,current)??(current===baseline?secured:current)}catch(error){log.debug(`CSRF cookie seeding skipped: ${error.message}`);finished=current===baseline?secured:current}if(!documentCacheControl)return finished;return applyDocumentCacheControl(req,finished??response,documentCacheControl)}});log.success(`Production server listening on http://0.0.0.0:${port}`)}export function buildDocumentCacheControl(documents){if(!documents)return;const maxAge=Number(documents.maxAge??0);if(!Number.isFinite(maxAge)||maxAge<=0)return;const stale=Number(documents.staleWhileRevalidate??0),parts=["public",`max-age=${Math.floor(maxAge)}`];if(Number.isFinite(stale)&&stale>0)parts.push(`stale-while-revalidate=${Math.floor(stale)}`);return parts.join(", ")}export function isAuthenticatedRequest(request){if(request.headers.get("authorization"))return!0;const cookie=request.headers.get("cookie")||"";if(!cookie)return!1;return cookie.split(";").map((part)=>part.trim().split("=")[0]?.toLowerCase()??"").some((name)=>name!==""&&!name.includes("csrf")&&/session|auth|token|remember/.test(name))}export function applyDocumentCacheControl(request,response,cacheControl){if(!cacheControl)return response;const contentType=response.headers.get("content-type")||"";if(response.status!==200||!contentType.startsWith("text/html"))return response;if(response.headers.getSetCookie().length>0)return response;if(isAuthenticatedRequest(request))return response;const headers=new Headers(response.headers);headers.set("cache-control",cacheControl);return new Response(response.body,{status:response.status,statusText:response.statusText,headers})}async function resolveVendoredStxModule(){const source=resolveStxSource({value:process.env.STACKS_STX_SRC});if(source.kind==="missing")await log.exit(`STACKS_STX_SRC points at ${source.path}, which does not exist. Unset it to use the installed @stacksjs/stx.`,ExitCode.FatalError);if(source.kind==="override"){log.warn(`Rendering through ${source.path} instead of the installed @stacksjs/stx.`);return await import(source.path)}try{return await import("@stacksjs/stx")}catch{}return}function fallbackI18nFromSite(site){const locales=site.i18n.locales,defaultLocale=site.i18n.defaultLocale??locales[0];return{locales,defaultLocale,labels:site.i18n.labels??Object.fromEntries(locales.map((c)=>[c,c.toUpperCase()])),translations:{},pickerSelector:site.i18n.pickerSelector??"#lang-picker"}}async function resolveSiteI18n(site){try{const resolved=await resolveVendoredStxModule();if(typeof resolved?.resolveI18n==="function"){const i18n=resolved.resolveI18n(site,process.cwd());if(i18n)return i18n}}catch{}return fallbackI18nFromSite(site)}async function loadStxSiteConfig(){const sitePath=siteConfigPath();if(!existsSync(sitePath))return{};try{const mod=await import(sitePath),site=mod.default??mod.site??mod.config;if(!site)return{};if(!site.i18n)return{site};const i18n=await resolveSiteI18n(site);return{site,i18n}}catch{}return{}}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/buddy",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.74.39",
5
+ "version": "0.74.40",
6
6
  "description": "Meet Buddy. The Stacks runtime.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -95,66 +95,66 @@
95
95
  "prepublishOnly": "bun run build"
96
96
  },
97
97
  "dependencies": {
98
- "@stacksjs/actions": "^0.74.39",
99
- "@stacksjs/ai": "^0.74.39",
100
- "@stacksjs/alias": "^0.74.39",
101
- "@stacksjs/analytics": "^0.74.39",
102
- "@stacksjs/api": "^0.74.39",
103
- "@stacksjs/arrays": "^0.74.39",
104
- "@stacksjs/auth": "^0.74.39",
105
- "@stacksjs/browser-extension": "^0.74.39",
106
- "@stacksjs/build": "^0.74.39",
107
- "@stacksjs/cache": "^0.74.39",
108
- "@stacksjs/chat": "^0.74.39",
98
+ "@stacksjs/actions": "^0.74.40",
99
+ "@stacksjs/ai": "^0.74.40",
100
+ "@stacksjs/alias": "^0.74.40",
101
+ "@stacksjs/analytics": "^0.74.40",
102
+ "@stacksjs/api": "^0.74.40",
103
+ "@stacksjs/arrays": "^0.74.40",
104
+ "@stacksjs/auth": "^0.74.40",
105
+ "@stacksjs/browser-extension": "^0.74.40",
106
+ "@stacksjs/build": "^0.74.40",
107
+ "@stacksjs/cache": "^0.74.40",
108
+ "@stacksjs/chat": "^0.74.40",
109
109
  "@stacksjs/clapp": "^0.2.12",
110
- "@stacksjs/cli": "^0.74.39",
111
- "@stacksjs/cloud": "^0.74.39",
112
- "@stacksjs/cms": "^0.74.39",
113
- "@stacksjs/collections": "^0.74.39",
114
- "@stacksjs/config": "^0.74.39",
115
- "@stacksjs/database": "^0.74.39",
116
- "@stacksjs/desktop-build": "^0.74.39",
117
- "@stacksjs/dns": "^0.74.39",
110
+ "@stacksjs/cli": "^0.74.40",
111
+ "@stacksjs/cloud": "^0.74.40",
112
+ "@stacksjs/cms": "^0.74.40",
113
+ "@stacksjs/collections": "^0.74.40",
114
+ "@stacksjs/config": "^0.74.40",
115
+ "@stacksjs/database": "^0.74.40",
116
+ "@stacksjs/desktop-build": "^0.74.40",
117
+ "@stacksjs/dns": "^0.74.40",
118
118
  "@stacksjs/dnsx": "^0.2.3",
119
- "@stacksjs/email": "^0.74.39",
120
- "@stacksjs/enums": "^0.74.39",
121
- "@stacksjs/env": "^0.74.39",
122
- "@stacksjs/error-handling": "^0.74.39",
123
- "@stacksjs/events": "^0.74.39",
124
- "@stacksjs/features": "^0.74.39",
125
- "@stacksjs/git": "^0.74.39",
119
+ "@stacksjs/email": "^0.74.40",
120
+ "@stacksjs/enums": "^0.74.40",
121
+ "@stacksjs/env": "^0.74.40",
122
+ "@stacksjs/error-handling": "^0.74.40",
123
+ "@stacksjs/events": "^0.74.40",
124
+ "@stacksjs/features": "^0.74.40",
125
+ "@stacksjs/git": "^0.74.40",
126
126
  "@stacksjs/gitit": "^0.2.5",
127
- "@stacksjs/health": "^0.74.39",
127
+ "@stacksjs/health": "^0.74.40",
128
128
  "@stacksjs/httx": "^0.1.10",
129
- "@stacksjs/image": "^0.74.39",
130
- "@stacksjs/lint": "^0.74.39",
131
- "@stacksjs/logging": "^0.74.39",
132
- "@stacksjs/notifications": "^0.74.39",
133
- "@stacksjs/objects": "^0.74.39",
134
- "@stacksjs/orm": "^0.74.39",
135
- "@stacksjs/path": "^0.74.39",
136
- "@stacksjs/payments": "^0.74.39",
137
- "@stacksjs/realtime": "^0.74.39",
138
- "@stacksjs/router": "^0.74.39",
129
+ "@stacksjs/image": "^0.74.40",
130
+ "@stacksjs/lint": "^0.74.40",
131
+ "@stacksjs/logging": "^0.74.40",
132
+ "@stacksjs/notifications": "^0.74.40",
133
+ "@stacksjs/objects": "^0.74.40",
134
+ "@stacksjs/orm": "^0.74.40",
135
+ "@stacksjs/path": "^0.74.40",
136
+ "@stacksjs/payments": "^0.74.40",
137
+ "@stacksjs/realtime": "^0.74.40",
138
+ "@stacksjs/router": "^0.74.40",
139
139
  "@stacksjs/rpx": "^0.11.53",
140
- "@stacksjs/scheduler": "^0.74.39",
141
- "@stacksjs/search-engine": "^0.74.39",
142
- "@stacksjs/security": "^0.74.39",
143
- "@stacksjs/server": "^0.74.39",
144
- "@stacksjs/sites": "^0.74.39",
145
- "@stacksjs/skills": "^0.74.39",
146
- "@stacksjs/storage": "^0.74.39",
147
- "@stacksjs/strings": "^0.74.39",
140
+ "@stacksjs/scheduler": "^0.74.40",
141
+ "@stacksjs/search-engine": "^0.74.40",
142
+ "@stacksjs/security": "^0.74.40",
143
+ "@stacksjs/server": "^0.74.40",
144
+ "@stacksjs/sites": "^0.74.40",
145
+ "@stacksjs/skills": "^0.74.40",
146
+ "@stacksjs/storage": "^0.74.40",
147
+ "@stacksjs/strings": "^0.74.40",
148
148
  "@stacksjs/stx": "^0.2.274",
149
- "@stacksjs/testing": "^0.74.39",
150
- "@stacksjs/tinker": "^0.74.39",
149
+ "@stacksjs/testing": "^0.74.40",
150
+ "@stacksjs/tinker": "^0.74.40",
151
151
  "@stacksjs/tlsx": "^0.13.19",
152
152
  "@stacksjs/ts-cloud": "^0.16.0",
153
- "@stacksjs/tunnel": "^0.74.39",
154
- "@stacksjs/types": "^0.74.39",
155
- "@stacksjs/ui": "^0.74.39",
156
- "@stacksjs/utils": "^0.74.39",
157
- "@stacksjs/validation": "^0.74.39",
153
+ "@stacksjs/tunnel": "^0.74.40",
154
+ "@stacksjs/types": "^0.74.40",
155
+ "@stacksjs/ui": "^0.74.40",
156
+ "@stacksjs/utils": "^0.74.40",
157
+ "@stacksjs/validation": "^0.74.40",
158
158
  "ajv": "^8.20.0",
159
159
  "ajv-formats": "^3.0.1",
160
160
  "bun-plugin-stx": "^0.2.279",