@stacksjs/server 0.72.19 → 0.72.21
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/proxy.d.ts +19 -0
- package/dist/proxy.js +1 -1
- package/dist/redirects.d.ts +4 -7
- package/dist/redirects.js +1 -1
- package/package.json +5 -5
package/dist/proxy.d.ts
CHANGED
|
@@ -25,6 +25,25 @@ export declare function isApiBoundRequest(req: Request, pathname: string, rules?
|
|
|
25
25
|
*/
|
|
26
26
|
export declare function describeApiProxyRules(rules: ApiProxyRules): string;
|
|
27
27
|
export declare function proxyToBackend(req: Request, backendBase: string, stripPrefix?: string): Promise<Response>;
|
|
28
|
+
/**` traffic goes, or null when there is no safe answer.
|
|
29
|
+
*
|
|
30
|
+
* Returning null is the important part. On a shared box `127.0.0.1:<default>`
|
|
31
|
+
* is not "my API" — it is whichever tenant bound that port first. Several SSR
|
|
32
|
+
* sites legitimately share one instance, so falling back to the framework-wide
|
|
33
|
+
* default silently forwards this app's session cookies, login POSTs and form
|
|
34
|
+
* bodies into a DIFFERENT tenant's process. A 502 with an actionable log beats
|
|
35
|
+
* misdelivering a visitor's credentials to a stranger.
|
|
36
|
+
*
|
|
37
|
+
* So an explicit target wins, and in a deployed environment there is no
|
|
38
|
+
* fallback at all. Only a local dev machine, where nothing else is listening,
|
|
39
|
+
* gets to guess.
|
|
40
|
+
*
|
|
41
|
+
* Lives here rather than beside the production server because more than one
|
|
42
|
+
* server needs it: the public site and the dashboard both proxy to the same
|
|
43
|
+
* API process, and a second copy of this rule is a second place for it to
|
|
44
|
+
* drift.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveApiBase(configuredPort?: number, env?: NodeJS.ProcessEnv): string | null;
|
|
28
47
|
/**` traffic behaves identically
|
|
29
48
|
* in both topologies (stacksjs/stacks#1950).
|
|
30
49
|
*/
|
package/dist/proxy.js
CHANGED
|
@@ -1 +1 @@
|
|
|
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){if(!rules)return pathname.startsWith(DEFAULT_API_PREFIX)||DEFAULT_API_METHODS.includes(req.method);if(rules.methods.has(req.method))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 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})}
|
|
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){if(!rules)return pathname.startsWith(DEFAULT_API_PREFIX)||DEFAULT_API_METHODS.includes(req.method);if(rules.methods.has(req.method))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 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}`}
|
package/dist/redirects.d.ts
CHANGED
|
@@ -11,13 +11,9 @@
|
|
|
11
11
|
* here rather than in production.
|
|
12
12
|
*/
|
|
13
13
|
export declare function resolveRedirectRules(input?: RedirectConfig): RedirectRules;
|
|
14
|
-
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* Matching is exact on the normalised path. Prefix and pattern rules are
|
|
18
|
-
* deliberately not supported: a redirect table is read by whoever is debugging
|
|
19
|
-
* a URL at two in the morning, and every wildcard in it is a thing they have
|
|
20
|
-
* to simulate in their head to know what a given path does.
|
|
14
|
+
/*': 'https://dash.example.com'` lands on
|
|
15
|
+
* `https://dash.example.com/events/42`. No regular expressions, no captures,
|
|
16
|
+
* no ordering to reason about.
|
|
21
17
|
*/
|
|
22
18
|
export declare function resolveRedirect(url: URL, rules?: RedirectRules): Response | undefined;
|
|
23
19
|
/**
|
|
@@ -39,6 +35,7 @@ export declare interface RedirectRule {
|
|
|
39
35
|
to: string
|
|
40
36
|
status: number
|
|
41
37
|
preserveQuery: boolean
|
|
38
|
+
subtree: boolean
|
|
42
39
|
}
|
|
43
40
|
/** What an app writes in `config/server.ts` under `redirects`. */
|
|
44
41
|
export type RedirectConfig = Record<string, string | RedirectTarget>;
|
package/dist/redirects.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const PROTECTED_PREFIX="/api/";export const DEFAULT_REDIRECT_STATUS=301;function normalizePath(value){const path=String(value).trim();if(!path.startsWith("/"))return"";return path.length>1&&path.endsWith("/")?path.replace(/\/+$/,""):path}export function resolveRedirectRules(input={}){const rules=new Map;for(const[rawFrom,rawTarget]of Object.entries(input??{})){const from=normalizePath(rawFrom);if(!from)continue;if(from===PROTECTED_PREFIX.slice(0,-1)||from.startsWith(PROTECTED_PREFIX))continue;const target=typeof rawTarget==="string"?{to:rawTarget}:rawTarget,to=String(target?.to??"").trim();if(!to)continue;if(!/^https?:\/\//i.test(to)&&normalizePath(to)===from)continue;rules.set(from,{from,to,status:Number(target?.status)||DEFAULT_REDIRECT_STATUS,preserveQuery:target?.preserveQuery!==!1})}return rules}export function resolveRedirect(url,rules){if(!rules?.size)return;const rule=rules.get(
|
|
1
|
+
const PROTECTED_PREFIX="/api/";export const DEFAULT_REDIRECT_STATUS=301;function normalizePath(value){const path=String(value).trim();if(!path.startsWith("/"))return"";return path.length>1&&path.endsWith("/")?path.replace(/\/+$/,""):path}export function resolveRedirectRules(input={}){const rules=new Map;for(const[rawFrom,rawTarget]of Object.entries(input??{})){const subtree=rawFrom.endsWith("/*"),from=normalizePath(subtree?rawFrom.slice(0,-2)||"/":rawFrom);if(!from)continue;if(from===PROTECTED_PREFIX.slice(0,-1)||from.startsWith(PROTECTED_PREFIX))continue;const target=typeof rawTarget==="string"?{to:rawTarget}:rawTarget,to=String(target?.to??"").trim();if(!to)continue;if(!/^https?:\/\//i.test(to)&&normalizePath(to)===from)continue;rules.set(subtree?`${from}/*`:from,{from,to,status:Number(target?.status)||DEFAULT_REDIRECT_STATUS,preserveQuery:target?.preserveQuery!==!1,subtree})}return rules}export function resolveRedirect(url,rules){if(!rules?.size)return;const pathname=normalizePath(url.pathname),rule=rules.get(pathname)??matchSubtree(pathname,rules);if(!rule)return;let location=rule.to;if(rule.subtree){const remainder=pathname.slice(rule.from.length);if(remainder)location=location.replace(/\/$/,"")+remainder}if(rule.preserveQuery&&url.search)location+=location.includes("?")?`&${url.search.slice(1)}`:url.search;return new Response(null,{status:rule.status,headers:{Location:location,"Cache-Control":rule.status===301||rule.status===308?"public, max-age=3600":"no-store"}})}function matchSubtree(pathname,rules){let best;for(const rule of rules.values()){if(!rule.subtree)continue;if(pathname!==rule.from&&!pathname.startsWith(`${rule.from}/`))continue;if(!best||rule.from.length>best.from.length)best=rule}return best}export function describeRedirectRules(rules){if(!rules.size)return"none";const shown=[...rules.values()].slice(0,3).map((rule)=>`${rule.from} \u2192 ${rule.to}`),rest=rules.size-shown.length;return rest>0?`${shown.join(", ")} (+${rest} more)`:shown.join(", ")}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/server",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.72.
|
|
5
|
+
"version": "0.72.21",
|
|
6
6
|
"description": "Local development and production-ready.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -58,11 +58,11 @@
|
|
|
58
58
|
"prepublishOnly": "bun run build"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@stacksjs/config": "0.72.
|
|
61
|
+
"@stacksjs/config": "0.72.21",
|
|
62
62
|
"better-dx": "^0.2.23",
|
|
63
|
-
"@stacksjs/path": "0.72.
|
|
64
|
-
"@stacksjs/router": "0.72.
|
|
65
|
-
"@stacksjs/validation": "0.72.
|
|
63
|
+
"@stacksjs/path": "0.72.21",
|
|
64
|
+
"@stacksjs/router": "0.72.21",
|
|
65
|
+
"@stacksjs/validation": "0.72.21"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"bun-plugin-auto-imports": "^0.4.0"
|