@stacksjs/router 0.70.87 → 0.70.90

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.
File without changes
@@ -0,0 +1,11 @@
1
+ export const JSON_CONTENT_TYPE = /^application\/(?:json|.+\+json)(?:;|$)/i;
2
+ export function isApiRequest(req) {
3
+ const headers = req.headers, contentType = headers.get("content-type") || "";
4
+ if (JSON_CONTENT_TYPE.test(contentType))
5
+ return !0;
6
+ if (headers.get("sec-fetch-dest") === "document")
7
+ return !1;
8
+ if ((headers.get("accept") || "").includes("text/html"))
9
+ return !1;
10
+ return !0;
11
+ }
@@ -0,0 +1,73 @@
1
+ import { decrypt, encrypt } from "@stacksjs/security";
2
+
3
+ export class EncryptedSessionStore {
4
+ inner;
5
+ opts;
6
+ constructor(inner, opts = {}) {
7
+ this.inner = inner;
8
+ this.opts = opts;
9
+ }
10
+ async set(sid, session, ttl) {
11
+ const envelope = await this.wrap(sid, session);
12
+ await this.inner.set(sid, envelope, ttl);
13
+ }
14
+ async touch(sid, session, ttl) {
15
+ const envelope = await this.wrap(sid, session);
16
+ if (this.inner.touch)
17
+ await this.inner.touch(sid, envelope, ttl);
18
+ else
19
+ await this.inner.set(sid, envelope, ttl);
20
+ }
21
+ async get(sid) {
22
+ const stored = await this.inner.get(sid);
23
+ if (!stored)
24
+ return null;
25
+ return this.unwrap(stored);
26
+ }
27
+ destroy(sid) {
28
+ return this.inner.destroy(sid);
29
+ }
30
+ async all() {
31
+ const wrapped = await this.inner.all?.() ?? {}, out = {};
32
+ for (const [sid, envelope] of Object.entries(wrapped)) {
33
+ const decrypted = await this.unwrap(envelope);
34
+ if (decrypted)
35
+ out[sid] = decrypted;
36
+ }
37
+ return out;
38
+ }
39
+ async length() {
40
+ if (this.inner.length)
41
+ return this.inner.length();
42
+ return Object.keys(await this.all()).length;
43
+ }
44
+ async clear() {
45
+ if (this.inner.clear)
46
+ return this.inner.clear();
47
+ const sessions = await this.inner.all?.() ?? {};
48
+ await Promise.all(Object.keys(sessions).map((sid) => this.inner.destroy(sid)));
49
+ }
50
+ async wrap(sid, session) {
51
+ const { id, ...rest } = session, ciphertext = await encrypt(JSON.stringify(rest), this.opts.appKey);
52
+ return {
53
+ _enc: !0,
54
+ id: id ?? sid,
55
+ data: ciphertext
56
+ };
57
+ }
58
+ async unwrap(stored) {
59
+ if (!stored || typeof stored !== "object")
60
+ return null;
61
+ const candidate = stored;
62
+ if (candidate._enc === !0 && typeof candidate.data === "string")
63
+ try {
64
+ const decrypted = await decrypt(candidate.data, this.opts.appKey), parsed = JSON.parse(decrypted);
65
+ if (candidate.id !== void 0)
66
+ parsed.id = candidate.id;
67
+ return parsed;
68
+ } catch {
69
+ return null;
70
+ }
71
+ return candidate;
72
+ }
73
+ }
@@ -0,0 +1,310 @@
1
+ import process from "node:process";
2
+ import { log } from "@stacksjs/logging";
3
+ import {
4
+ createErrorHandler,
5
+ renderProductionErrorPage
6
+ } from "@stacksjs/error-handling";
7
+ import { isApiRequest } from "./api-shape";
8
+ import { getCurrentRequest } from "./request-context";
9
+ function buildErrorJson(opts) {
10
+ const body = {
11
+ error: opts.error,
12
+ message: opts.message,
13
+ status: opts.status,
14
+ timestamp: new Date().toISOString()
15
+ };
16
+ if (opts.details)
17
+ body.details = opts.details;
18
+ return JSON.stringify(body);
19
+ }
20
+ function isDebugAllowed() {
21
+ const appEnv = (process.env.APP_ENV ?? "").toLowerCase();
22
+ if (appEnv === "development")
23
+ return !0;
24
+ if (!appEnv && process.env.NODE_ENV === "development")
25
+ return !0;
26
+ return !1;
27
+ }
28
+ function getJsonHeaders() {
29
+ return { "Content-Type": "application/json" };
30
+ }
31
+ function getJsonHeadersFull() {
32
+ return getJsonHeaders();
33
+ }
34
+ const MAX_QUERIES = 50, N1_THRESHOLD = 5;
35
+ function newQueryTrack() {
36
+ return {
37
+ buffer: Array(MAX_QUERIES).fill(null),
38
+ writeIndex: 0,
39
+ count: 0,
40
+ shapeCounts: new Map,
41
+ n1Warned: new Set
42
+ };
43
+ }
44
+ const REQUEST_QUERY_TRACK_KEY = Symbol.for("stacks.queryTracking");
45
+ let fallbackTrack = newQueryTrack();
46
+ function getQueryTrack() {
47
+ const req = getCurrentRequest();
48
+ if (!req)
49
+ return fallbackTrack;
50
+ let track = req[REQUEST_QUERY_TRACK_KEY];
51
+ if (!track) {
52
+ track = newQueryTrack();
53
+ req[REQUEST_QUERY_TRACK_KEY] = track;
54
+ }
55
+ return track;
56
+ }
57
+ function normalizeQueryShape(query) {
58
+ return query.replace(/'(?:[^']|'')*'/g, "?").replace(/"(?:[^"]|"")*"/g, "?").replace(/\b\d+(?:\.\d+)?\b/g, "?").replace(/IN\s*\([^)]*\)/gi, "IN (?)").replace(/\s+/g, " ").trim().toUpperCase();
59
+ }
60
+ export function trackQuery(query, time, connection) {
61
+ const track = getQueryTrack();
62
+ track.buffer[track.writeIndex] = { query, time, connection };
63
+ track.writeIndex = (track.writeIndex + 1) % MAX_QUERIES;
64
+ if (track.count < MAX_QUERIES)
65
+ track.count++;
66
+ if (!isDebugAllowed())
67
+ return;
68
+ const shape = normalizeQueryShape(query);
69
+ if (shape.startsWith("INSERT INTO QUERY_LOGS") || shape.startsWith("EXPLAIN"))
70
+ return;
71
+ const next = (track.shapeCounts.get(shape) ?? 0) + 1;
72
+ track.shapeCounts.set(shape, next);
73
+ if (next === N1_THRESHOLD + 1 && !track.n1Warned.has(shape)) {
74
+ track.n1Warned.add(shape);
75
+ import("@stacksjs/logging").then(({ log }) => {
76
+ log.warn(`[orm] Possible N+1 \u2014 query shape ran ${next}\xD7 in this request:
77
+ ${shape}
78
+ Hint: load related rows with .with('relation') or eager-load via includes() before iterating.`);
79
+ }).catch(() => {});
80
+ }
81
+ }
82
+ function getRecentQueries() {
83
+ const track = getQueryTrack();
84
+ if (track.count === 0)
85
+ return [];
86
+ const result = [], start = track.count < MAX_QUERIES ? 0 : track.writeIndex;
87
+ for (let i = 0;i < track.count; i++) {
88
+ const entry = track.buffer[(start + i) % MAX_QUERIES];
89
+ if (entry)
90
+ result.push(entry);
91
+ }
92
+ return result;
93
+ }
94
+ export function getQueryShapeCounts() {
95
+ return new Map(getQueryTrack().shapeCounts);
96
+ }
97
+ export function clearTrackedQueries() {
98
+ const req = getCurrentRequest();
99
+ if (req && req[REQUEST_QUERY_TRACK_KEY]) {
100
+ req[REQUEST_QUERY_TRACK_KEY] = newQueryTrack();
101
+ return;
102
+ }
103
+ fallbackTrack = newQueryTrack();
104
+ }
105
+ function getErrorHandlerConfig() {
106
+ return {
107
+ appName: "Stacks",
108
+ theme: "auto",
109
+ showEnvironment: !0,
110
+ showQueries: !0,
111
+ showRequest: !0,
112
+ enableCopyMarkdown: !0,
113
+ snippetLines: 8,
114
+ basePaths: [process.cwd()]
115
+ };
116
+ }
117
+ const SENSITIVE_PATTERNS = [
118
+ "password",
119
+ "secret",
120
+ "token",
121
+ "api_key",
122
+ "apikey",
123
+ "access_key",
124
+ "accesskey",
125
+ "private_key",
126
+ "privatekey",
127
+ "credit_card",
128
+ "creditcard",
129
+ "card_number",
130
+ "cardnumber",
131
+ "cvv",
132
+ "ssn",
133
+ "authorization",
134
+ "credential",
135
+ "aws_secret",
136
+ "aws_access",
137
+ "database_password",
138
+ "db_password",
139
+ "encryption_key",
140
+ "signing_key",
141
+ "bearer",
142
+ "session_id",
143
+ "sessionid",
144
+ "cookie"
145
+ ], MAX_SANITIZE_DEPTH = 10, CIRCULAR_PLACEHOLDER = "[Circular]";
146
+ function sanitizeData(data, depth = 0, seen = new WeakSet) {
147
+ if (!data || typeof data !== "object" || depth >= MAX_SANITIZE_DEPTH)
148
+ return data;
149
+ if (seen.has(data))
150
+ return CIRCULAR_PLACEHOLDER;
151
+ seen.add(data);
152
+ if (Array.isArray(data))
153
+ return data.map((item) => sanitizeData(item, depth + 1, seen));
154
+ const sanitized = {};
155
+ for (const [key, value] of Object.entries(data)) {
156
+ const lowerKey = key.toLowerCase();
157
+ if (SENSITIVE_PATTERNS.some((pattern) => lowerKey.includes(pattern)))
158
+ sanitized[key] = "********";
159
+ else if (typeof value === "object" && value !== null)
160
+ sanitized[key] = sanitizeData(value, depth + 1, seen);
161
+ else
162
+ sanitized[key] = value;
163
+ }
164
+ return sanitized;
165
+ }
166
+ function getRequestBody(request) {
167
+ const req = request;
168
+ if (req.jsonBody)
169
+ return sanitizeData(req.jsonBody);
170
+ if (req.formBody)
171
+ return sanitizeData(req.formBody);
172
+ return;
173
+ }
174
+ async function getUserContext(request) {
175
+ const authed = request._authenticatedUser;
176
+ if (authed)
177
+ return {
178
+ id: authed.id,
179
+ email: authed.email,
180
+ name: authed.name || authed.username
181
+ };
182
+ return;
183
+ }
184
+ export async function createErrorResponse(error, request, options) {
185
+ const status = options?.status || 500;
186
+ log.debug(`[error] ${status} ${error.message}`);
187
+ if (!isDebugAllowed()) {
188
+ if (isApiRequest(request)) {
189
+ const isClientError = status >= 400 && status < 500, errDetails = error.details;
190
+ return new Response(buildErrorJson({
191
+ error: isClientError ? error.name || "Client Error" : "Internal Server Error",
192
+ message: isClientError ? error.message : "An unexpected error occurred.",
193
+ status,
194
+ details: isClientError && errDetails && typeof errDetails === "object" ? errDetails : void 0
195
+ }), { status, headers: getJsonHeaders() });
196
+ }
197
+ return new Response(renderProductionErrorPage(status), {
198
+ status,
199
+ headers: { "Content-Type": "text/html; charset=utf-8" }
200
+ });
201
+ }
202
+ try {
203
+ const handler = createErrorHandler(getErrorHandlerConfig());
204
+ handler.setFramework("Stacks", "0.70.0");
205
+ const requestBody = getRequestBody(request);
206
+ if (requestBody) {
207
+ const url = new URL(request.url);
208
+ handler.setRequest({
209
+ method: request.method,
210
+ url: request.url,
211
+ headers: Object.fromEntries(request.headers.entries()),
212
+ queryParams: Object.fromEntries(url.searchParams.entries()),
213
+ body: requestBody
214
+ });
215
+ } else
216
+ handler.setRequest(request);
217
+ const userContext = await getUserContext(request);
218
+ if (userContext)
219
+ handler.setUser(userContext);
220
+ if (options?.routingContext)
221
+ handler.setRouting(options.routingContext);
222
+ else if (options?.handlerPath)
223
+ handler.setRouting({
224
+ controller: options.handlerPath
225
+ });
226
+ for (const query of getRecentQueries())
227
+ handler.addQuery(query.query, query.time, query.connection);
228
+ if (isApiRequest(request)) {
229
+ const details = { handler: options?.handlerPath };
230
+ if (isDebugAllowed()) {
231
+ details.stack = error.stack?.split(`
232
+ `).slice(0, 10);
233
+ details.queries = getRecentQueries().slice(-10);
234
+ }
235
+ return new Response(buildErrorJson({
236
+ error: error.name || "Error",
237
+ message: error.message,
238
+ status,
239
+ details
240
+ }), { status, headers: getJsonHeadersFull() });
241
+ }
242
+ 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);
243
+ return new Response(html, {
244
+ status,
245
+ headers: {
246
+ "Content-Type": "text/html; charset=utf-8",
247
+ "Access-Control-Allow-Origin": corsOrigin
248
+ }
249
+ });
250
+ } catch (renderError) {
251
+ console.error("[Error Handler] Failed to render error page:", renderError);
252
+ const escapeHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
253
+ return new Response(`
254
+ <html>
255
+ <head><title>Error</title></head>
256
+ <body>
257
+ <h1>Error</h1>
258
+ <p>${escapeHtml(error.message)}</p>
259
+ <pre>${escapeHtml(error.stack || "")}</pre>
260
+ </body>
261
+ </html>
262
+ `, {
263
+ status,
264
+ headers: { "Content-Type": "text/html; charset=utf-8" }
265
+ });
266
+ }
267
+ }
268
+ export async function createMiddlewareErrorResponse(error, request) {
269
+ const status = error.statusCode ?? error.status ?? 500, isDevelopment = isDebugAllowed();
270
+ if (status >= 400 && status < 500) {
271
+ const headers = error.headers ? { ...error.headers, ...getJsonHeaders() } : getJsonHeaders();
272
+ return new Response(buildErrorJson({
273
+ error: error.name || "ClientError",
274
+ message: error.message,
275
+ status
276
+ }), { status, headers });
277
+ }
278
+ if (isDevelopment)
279
+ return await createErrorResponse(error, request, { status });
280
+ return new Response(buildErrorJson({
281
+ error: "Internal Server Error",
282
+ message: "An unexpected error occurred.",
283
+ status
284
+ }), { status, headers: getJsonHeaders() });
285
+ }
286
+ export function createValidationErrorResponse(errors, _request) {
287
+ return new Response(buildErrorJson({
288
+ error: "ValidationError",
289
+ message: "Validation failed",
290
+ status: 422,
291
+ details: { errors }
292
+ }), { status: 422, headers: getJsonHeaders() });
293
+ }
294
+ export async function createNotFoundResponse(path, request) {
295
+ if (isDebugAllowed()) {
296
+ const error = Error(`Route not found: ${path}`);
297
+ error.name = "NotFoundError";
298
+ return await createErrorResponse(error, request, { status: 404 });
299
+ }
300
+ if (isApiRequest(request))
301
+ return new Response(buildErrorJson({
302
+ error: "NotFound",
303
+ message: `Route not found: ${path}`,
304
+ status: 404
305
+ }), { status: 404, headers: getJsonHeaders() });
306
+ return new Response(renderProductionErrorPage(404), {
307
+ status: 404,
308
+ headers: { "Content-Type": "text/html; charset=utf-8" }
309
+ });
310
+ }
package/dist/index.js CHANGED
@@ -1,14 +1,37 @@
1
- // @bun
2
- var{defineProperty:K,getOwnPropertyNames:R,getOwnPropertyDescriptor:k}=Object,g=Object.prototype.hasOwnProperty;function h($){return this[$]}var E=($)=>{var F=(Y??=new WeakMap).get($),W;if(F)return F;if(F=K({},"__esModule",{value:!0}),$&&typeof $==="object"||typeof $==="function"){for(var V of R($))if(!g.call(F,V))K(F,V,{get:h.bind($,V),enumerable:!(W=k($,V))||W.enumerable})}return Y.set($,F),F},Y;var l=($)=>$;function q($,F){this[$]=l.bind(null,F)}var d=($,F)=>{for(var W in F)K($,W,{get:F[W],enumerable:!0,configurable:!0,set:q.bind(F,W)})};var f=($,F)=>()=>($&&(F=$($=0)),F);var D=import.meta.require;function z($){let F=$.headers,W=F.get("content-type")||"";if(i.test(W))return!0;if(F.get("sec-fetch-dest")==="document")return!1;if((F.get("accept")||"").includes("text/html"))return!1;return!0}var i;var b=f(()=>{i=/^application\/(?:json|.+\+json)(?:;|$)/i});import c from"process";import{AsyncLocalStorage as U}from"async_hooks";import{log as v0}from"@stacksjs/logging";function I(){return n.getStore()}var p,n,u,w0,x0;var H=f(()=>{p=Symbol.for("stacks.router.requestStorage"),n=globalThis[p]??=new U,u=Symbol.for("stacks.router.traceStorage"),w0=globalThis[u]??=new U;x0=new Proxy({},{get($,F){let W=I();if(!W){if(c.env.NODE_ENV!=="production")console.warn(`[RequestContext] Accessing request.${String(F)} outside of request context`);if(F==="bearerToken")return()=>null;if(F==="user"||F==="userToken")return async()=>{return};if(F==="tokenCan"||F==="tokenCant")return async()=>!1;if(F==="headers")return new Headers;if(F==="url")return"";if(F==="method")return"GET";return}let V=W[F];if(typeof V==="function")return V.bind(W);return V}})});var Q={};d(Q,{trackQuery:()=>e,getQueryShapeCounts:()=>$0,createValidationErrorResponse:()=>O0,createNotFoundResponse:()=>L0,createMiddlewareErrorResponse:()=>N0,createErrorResponse:()=>J,clearTrackedQueries:()=>F0});import N from"process";import{log as o}from"@stacksjs/logging";import{createErrorHandler as a,renderProductionErrorPage as A}from"@stacksjs/error-handling";function P($){let F={error:$.error,message:$.message,status:$.status,timestamp:new Date().toISOString()};if($.details)F.details=$.details;return JSON.stringify(F)}function M(){let $=(N.env.APP_ENV??"").toLowerCase();if($==="development")return!0;if(!$&&N.env.NODE_ENV==="development")return!0;return!1}function O(){return{"Content-Type":"application/json"}}function s(){return O()}function v(){return{buffer:Array(X).fill(null),writeIndex:0,count:0,shapeCounts:new Map,n1Warned:new Set}}function m(){let $=I();if(!$)return S;let F=$[w];if(!F)F=v(),$[w]=F;return F}function r($){return $.replace(/'(?:[^']|'')*'/g,"?").replace(/"(?:[^"]|"")*"/g,"?").replace(/\b\d+(?:\.\d+)?\b/g,"?").replace(/IN\s*\([^)]*\)/gi,"IN (?)").replace(/\s+/g," ").trim().toUpperCase()}function e($,F,W){let V=m();if(V.buffer[V.writeIndex]={query:$,time:F,connection:W},V.writeIndex=(V.writeIndex+1)%X,V.count<X)V.count++;if(!M())return;let G=r($);if(G.startsWith("INSERT INTO QUERY_LOGS")||G.startsWith("EXPLAIN"))return;let Z=(V.shapeCounts.get(G)??0)+1;if(V.shapeCounts.set(G,Z),Z===t+1&&!V.n1Warned.has(G))V.n1Warned.add(G),import("@stacksjs/logging").then(({log:B})=>{B.warn(`[orm] Possible N+1 \u2014 query shape ran ${Z}\xD7 in this request:
3
- ${G}
4
- Hint: load related rows with .with('relation') or eager-load via includes() before iterating.`)}).catch(()=>{})}function C(){let $=m();if($.count===0)return[];let F=[],W=$.count<X?0:$.writeIndex;for(let V=0;V<$.count;V++){let G=$.buffer[(W+V)%X];if(G)F.push(G)}return F}function $0(){return new Map(m().shapeCounts)}function F0(){let $=I();if($&&$[w]){$[w]=v();return}S=v()}function V0(){return{appName:"Stacks",theme:"auto",showEnvironment:!0,showQueries:!0,showRequest:!0,enableCopyMarkdown:!0,snippetLines:8,basePaths:[N.cwd()]}}function x($,F=0,W=new WeakSet){if(!$||typeof $!=="object"||F>=Z0)return $;if(W.has($))return G0;if(W.add($),Array.isArray($))return $.map((G)=>x(G,F+1,W));let V={};for(let[G,Z]of Object.entries($)){let B=G.toLowerCase();if(W0.some((L)=>B.includes(L)))V[G]="********";else if(typeof Z==="object"&&Z!==null)V[G]=x(Z,F+1,W);else V[G]=Z}return V}function B0($){let F=$;if(F.jsonBody)return x(F.jsonBody);if(F.formBody)return x(F.formBody);return}async function j0($){let W=$._authenticatedUser;if(W)return{id:W.id,email:W.email,name:W.name||W.username};return}async function J($,F,W){let V=W?.status||500;if(o.debug(`[error] ${V} ${$.message}`),!M()){if(z(F)){let Z=V>=400&&V<500,B=$.details;return new Response(P({error:Z?$.name||"Client Error":"Internal Server Error",message:Z?$.message:"An unexpected error occurred.",status:V,details:Z&&B&&typeof B==="object"?B:void 0}),{status:V,headers:O()})}return new Response(A(V),{status:V,headers:{"Content-Type":"text/html; charset=utf-8"}})}try{let Z=a(V0());Z.setFramework("Stacks","0.70.0");let B=B0(F);if(B){let j=new URL(F.url);Z.setRequest({method:F.method,url:F.url,headers:Object.fromEntries(F.headers.entries()),queryParams:Object.fromEntries(j.searchParams.entries()),body:B})}else Z.setRequest(F);let L=await j0(F);if(L)Z.setUser(L);if(W?.routingContext)Z.setRouting(W.routingContext);else if(W?.handlerPath)Z.setRouting({controller:W.handlerPath});for(let j of C())Z.addQuery(j.query,j.time,j.connection);if(z(F)){let j={handler:W?.handlerPath};if(M())j.stack=$.stack?.split(`
5
- `).slice(0,10),j.queries=C().slice(-10);return new Response(P({error:$.name||"Error",message:$.message,status:V,details:j}),{status:V,headers:s()})}let T=N.env.APP_URL?N.env.APP_URL.startsWith("http")?N.env.APP_URL:`https://${N.env.APP_URL}`:M()?"*":F.headers.get("origin")??"null",y=await Z.render($,V);return new Response(y,{status:V,headers:{"Content-Type":"text/html; charset=utf-8","Access-Control-Allow-Origin":T}})}catch(Z){console.error("[Error Handler] Failed to render error page:",Z);let B=(L)=>L.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");return new Response(`
6
- <html>
7
- <head><title>Error</title></head>
8
- <body>
9
- <h1>Error</h1>
10
- <p>${B($.message)}</p>
11
- <pre>${B($.stack||"")}</pre>
12
- </body>
13
- </html>
14
- `,{status:V,headers:{"Content-Type":"text/html; charset=utf-8"}})}}async function N0($,F){let W=$.statusCode??$.status??500,V=M();if(W>=400&&W<500){let G=$.headers?{...$.headers,...O()}:O();return new Response(P({error:$.name||"ClientError",message:$.message,status:W}),{status:W,headers:G})}if(V)return await J($,F,{status:W});return new Response(P({error:"Internal Server Error",message:"An unexpected error occurred.",status:W}),{status:W,headers:O()})}function O0($,F){return new Response(P({error:"ValidationError",message:"Validation failed",status:422,details:{errors:$}}),{status:422,headers:O()})}async function L0($,F){if(M()){let V=Error(`Route not found: ${$}`);return V.name="NotFoundError",await J(V,F,{status:404})}if(z(F))return new Response(P({error:"NotFound",message:`Route not found: ${$}`,status:404}),{status:404,headers:O()});return new Response(A(404),{status:404,headers:{"Content-Type":"text/html; charset=utf-8"}})}var X=50,t=5,w,S,W0,Z0=10,G0="[Circular]";var _=f(()=>{b();H();w=Symbol.for("stacks.queryTracking"),S=v();W0=["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"]});export*from"@stacksjs/bun-router";import("@stacksjs/database").then(({setQueryTracker:$})=>{if(typeof $==="function"){let{trackQuery:F}=(_(),E(Q));$(F)}}).catch(()=>{});export{p0 as withTraceId,g0 as warnOnMultipleRouterInstances,D1 as verifySignedUrlMiddleware,Y1 as verifySignedUrl,k0 as url,W1 as trackQuery,K1 as stream,m1 as signedUrl,J1 as signUrl,c0 as setCurrentRequest,R0 as serverResponse,y0 as serve,w1 as sanitizePathParam,v1 as safePathParam,i0 as runWithRequest,B1 as routeParams,T0 as route,d0 as request,P1 as rateLimitStatus,M1 as rateLimit,a0 as loadRoutes,G1 as listRegisteredRoutes,N1 as isApiRequest,_0 as installMiddlewareHotReload,q0 as getTraceId,V1 as getQueryShapeCounts,l0 as getCurrentRequest,Q0 as findUnresolvableRouteMiddleware,F1 as createValidationErrorResponse,A1 as createStacksSessionStore,S0 as createStacksRouter,C1 as createSessionStore,$1 as createNotFoundResponse,e0 as createMiddlewareErrorResponse,r0 as createErrorResponse,t0 as clearTrackedQueries,X1 as clearRateLimit,A0 as clearMiddlewareCache,E0 as cacheRequestQuery,C0 as assertRouteMiddlewareResolvable,T1 as RedisSessionStore,I1 as PathParamError,u0 as Middleware,_1 as MemorySessionStore,O1 as JSON_CONTENT_TYPE,Q1 as FileSessionStore,U1 as EncryptedSessionStore,S1 as DatabaseSessionStore};
1
+ var {require}=import.meta;import"./request-augmentation";
2
+
3
+ export * from "@stacksjs/bun-router";
4
+ export { assertRouteMiddlewareResolvable, clearMiddlewareCache, createStacksRouter, findUnresolvableRouteMiddleware, installMiddlewareHotReload, route, serve, serverResponse, url, warnOnMultipleRouterInstances } from "./stacks-router";
5
+ export { cacheRequestQuery, getCurrentRequest, getTraceId, request, runWithRequest, setCurrentRequest, withTraceId } from "./request-context";
6
+ export { Middleware } from "./middleware";
7
+ export { loadRoutes } from "./route-loader";
8
+ export {
9
+ clearTrackedQueries,
10
+ createErrorResponse,
11
+ createMiddlewareErrorResponse,
12
+ createNotFoundResponse,
13
+ createValidationErrorResponse,
14
+ getQueryShapeCounts,
15
+ trackQuery
16
+ } from "./error-handler";
17
+ export { listRegisteredRoutes, routeParams } from "./stacks-router";
18
+ export { isApiRequest, JSON_CONTENT_TYPE } from "./api-shape";
19
+ export { rateLimit, rateLimitStatus, clearRateLimit } from "./rate-limit";
20
+ export { PathParamError, safePathParam, sanitizePathParam } from "./path-sanitize";
21
+ export { stream } from "./stacks-router";
22
+ export { signedUrl, signUrl, verifySignedUrl, verifySignedUrlMiddleware } from "./signed-url";
23
+ export { EncryptedSessionStore } from "./encrypted-session-store";
24
+ export {
25
+ createSessionStore,
26
+ createStacksSessionStore,
27
+ DatabaseSessionStore,
28
+ FileSessionStore,
29
+ MemorySessionStore,
30
+ RedisSessionStore
31
+ } from "./session-factory";
32
+ import("@stacksjs/database").then(({ setQueryTracker }) => {
33
+ if (typeof setQueryTracker === "function") {
34
+ const { trackQuery } = require("./error-handler");
35
+ setQueryTracker(trackQuery);
36
+ }
37
+ }).catch(() => {});
@@ -0,0 +1,23 @@
1
+ export class Middleware {
2
+ name;
3
+ priority;
4
+ handle;
5
+ constructor(config) {
6
+ this.name = config.name;
7
+ this.priority = config.priority ?? 10;
8
+ this.handle = config.handle;
9
+ }
10
+ toRouterHandler() {
11
+ const handle = this.handle.bind(this);
12
+ return async (req, next) => {
13
+ try {
14
+ await handle(req);
15
+ } catch (thrown) {
16
+ if (thrown instanceof Response)
17
+ return thrown;
18
+ throw thrown;
19
+ }
20
+ return next();
21
+ };
22
+ }
23
+ }
@@ -0,0 +1,37 @@
1
+ export class PathParamError extends Error {
2
+ reason;
3
+ constructor(reason, value, context) {
4
+ const ctx = context ? ` in ${context}` : "";
5
+ super(`[router] Refusing to use ${JSON.stringify(value)} as a path parameter${ctx} \u2014 ${reason}`);
6
+ this.name = "PathParamError";
7
+ this.reason = reason;
8
+ }
9
+ }
10
+ const CONTROL_CHARS = /[\u0000-\u001F\u007F]/;
11
+ export function sanitizePathParam(value, options = {}) {
12
+ if (typeof value !== "string")
13
+ throw new PathParamError("not-string", value, options.context);
14
+ if (value.length === 0)
15
+ throw new PathParamError("empty", value, options.context);
16
+ const maxLength = options.maxLength ?? 255;
17
+ if (value.length > maxLength)
18
+ throw new PathParamError("too-long", value, options.context);
19
+ if (value.includes("\x00"))
20
+ throw new PathParamError("null-byte", value, options.context);
21
+ if (CONTROL_CHARS.test(value))
22
+ throw new PathParamError("control-char", value, options.context);
23
+ if (value.startsWith("/") || /^[A-Z]:[\\/]/i.test(value))
24
+ throw new PathParamError("absolute-path", value, options.context);
25
+ if (/(^|[\\/])\.\.([\\/]|$)/.test(value))
26
+ throw new PathParamError("traversal", value, options.context);
27
+ if (!options.allowSlashes && /[\\/]/.test(value))
28
+ throw new PathParamError("traversal", value, options.context);
29
+ return value;
30
+ }
31
+ export function safePathParam(value, options = {}) {
32
+ try {
33
+ return sanitizePathParam(value, options);
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
@@ -0,0 +1,73 @@
1
+ import { HttpError } from "@stacksjs/error-handling";
2
+ import { RateLimitError, RateLimiter, defaultIdentity } from "ts-rate-limiter";
3
+ import { getCurrentRequest } from "./request-context";
4
+ const PERIOD_SECONDS = {
5
+ second: 1,
6
+ minute: 60,
7
+ hour: 3600,
8
+ day: 86400
9
+ }, limiterCache = new Map;
10
+ function getLimiter(max, windowMs) {
11
+ const cacheKey = `${windowMs}:${max}`;
12
+ let limiter = limiterCache.get(cacheKey);
13
+ if (!limiter) {
14
+ limiter = new RateLimiter({
15
+ windowMs,
16
+ maxRequests: max,
17
+ algorithm: "fixed-window",
18
+ standardHeaders: !1,
19
+ legacyHeaders: !1
20
+ });
21
+ limiterCache.set(cacheKey, limiter);
22
+ }
23
+ return limiter;
24
+ }
25
+ function resolveIdentity(explicit) {
26
+ if (explicit !== void 0)
27
+ return explicit;
28
+ const req = getCurrentRequest();
29
+ return req ? defaultIdentity(req) : "anon";
30
+ }
31
+ export function rateLimit(key, max, options = {}) {
32
+ const id = resolveIdentity(options.identity), bucketKey = `${key}:${id}`, run = async (windowMs) => {
33
+ const limiter = getLimiter(max, windowMs);
34
+ try {
35
+ await limiter.enforce(bucketKey);
36
+ } catch (err) {
37
+ if (err instanceof RateLimitError)
38
+ throw Object.assign(new HttpError(429, "Too many requests", {
39
+ key,
40
+ max,
41
+ retryAfter: err.retryAfter
42
+ }), { headers: err.toHeaders() });
43
+ throw err;
44
+ }
45
+ };
46
+ return {
47
+ async per(period) {
48
+ const seconds = PERIOD_SECONDS[period];
49
+ if (!seconds)
50
+ throw Error(`rateLimit().per: unknown period '${period}'`);
51
+ await run(seconds * 1000);
52
+ },
53
+ async over(ttlSeconds) {
54
+ if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0)
55
+ throw Error(`rateLimit().over: ttl must be a positive number, got ${ttlSeconds}`);
56
+ await run(ttlSeconds * 1000);
57
+ }
58
+ };
59
+ }
60
+ export async function rateLimitStatus(key, max, windowSeconds, options = {}) {
61
+ const id = resolveIdentity(options.identity), bucketKey = `${key}:${id}`, result = await getLimiter(max, windowSeconds * 1000).peek(bucketKey);
62
+ if (!result)
63
+ return null;
64
+ return {
65
+ count: result.current,
66
+ limit: result.limit,
67
+ remaining: Math.max(0, result.limit - result.current)
68
+ };
69
+ }
70
+ export async function clearRateLimit(key, max, windowSeconds, options = {}) {
71
+ const id = resolveIdentity(options.identity), bucketKey = `${key}:${id}`;
72
+ await getLimiter(max, windowSeconds * 1000).reset(bucketKey);
73
+ }
File without changes
@@ -0,0 +1,78 @@
1
+ import process from "node:process";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { log } from "@stacksjs/logging";
4
+ const REQUEST_STORAGE_KEY = Symbol.for("stacks.router.requestStorage"), requestStorage = globalThis[REQUEST_STORAGE_KEY] ??= new AsyncLocalStorage, TRACE_STORAGE_KEY = Symbol.for("stacks.router.traceStorage"), traceStorage = globalThis[TRACE_STORAGE_KEY] ??= new AsyncLocalStorage;
5
+ export function getTraceId() {
6
+ const explicit = traceStorage.getStore();
7
+ if (explicit)
8
+ return explicit;
9
+ return requestStorage.getStore()?._requestId;
10
+ }
11
+ export function withTraceId(id, fn) {
12
+ return traceStorage.run(id, fn);
13
+ }
14
+ const REQUEST_QUERY_CACHE_KEY = Symbol.for("stacks.requestQueryCache");
15
+ function getRequestCache() {
16
+ const req = requestStorage.getStore();
17
+ if (!req)
18
+ return;
19
+ let cache = req[REQUEST_QUERY_CACHE_KEY];
20
+ if (!cache) {
21
+ cache = { map: new Map };
22
+ req[REQUEST_QUERY_CACHE_KEY] = cache;
23
+ }
24
+ return cache;
25
+ }
26
+ export async function cacheRequestQuery(key, fetcher) {
27
+ const cache = getRequestCache();
28
+ if (!cache)
29
+ return fetcher();
30
+ const existing = cache.map.get(key);
31
+ if (existing)
32
+ return existing;
33
+ const promise = Promise.resolve().then(() => fetcher());
34
+ cache.map.set(key, promise);
35
+ promise.catch(() => cache.map.delete(key));
36
+ return promise;
37
+ }
38
+ export function setCurrentRequest(req) {
39
+ log.debug(`[request] ${req.method} ${new URL(req.url).pathname}`);
40
+ requestStorage.enterWith(req);
41
+ }
42
+ export function clearCurrentRequest() {
43
+ requestStorage.disable();
44
+ }
45
+ export function runWithRequest(req, fn) {
46
+ return requestStorage.run(req, fn);
47
+ }
48
+ export function getCurrentRequest() {
49
+ return requestStorage.getStore();
50
+ }
51
+ export const request = new Proxy({}, {
52
+ get(_target, prop) {
53
+ const currentRequest = getCurrentRequest();
54
+ if (!currentRequest) {
55
+ if (process.env.NODE_ENV !== "production")
56
+ console.warn(`[RequestContext] Accessing request.${String(prop)} outside of request context`);
57
+ if (prop === "bearerToken")
58
+ return () => null;
59
+ if (prop === "user" || prop === "userToken")
60
+ return async () => {
61
+ return;
62
+ };
63
+ if (prop === "tokenCan" || prop === "tokenCant")
64
+ return async () => !1;
65
+ if (prop === "headers")
66
+ return new Headers;
67
+ if (prop === "url")
68
+ return "";
69
+ if (prop === "method")
70
+ return "GET";
71
+ return;
72
+ }
73
+ const value = currentRequest[prop];
74
+ if (typeof value === "function")
75
+ return value.bind(currentRequest);
76
+ return value;
77
+ }
78
+ });