@stacksjs/router 0.75.225 → 0.75.230
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/index.d.ts +4 -1
- package/dist/index.js +1 -1
- package/dist/rate-limit.d.ts +2 -0
- package/dist/rate-limit.js +1 -1
- package/dist/security-settings.d.ts +60 -0
- package/dist/security-settings.js +1 -0
- package/package.json +16 -16
package/dist/index.d.ts
CHANGED
|
@@ -84,7 +84,10 @@ export {
|
|
|
84
84
|
// the same decision the framework makes in formatResult / error-handler.
|
|
85
85
|
export { isApiRequest, JSON_CONTENT_TYPE } from './api-shape';
|
|
86
86
|
// Export action-level rate limiting helpers
|
|
87
|
-
export { rateLimit, rateLimitStatus, clearRateLimit } from './rate-limit';
|
|
87
|
+
export { rateLimit, rateLimitStatus, clearRateLimit, resetRateLimiters } from './rate-limit';
|
|
88
|
+
// `clientAddress` here shadows bun-router's (re-exported above) with one that
|
|
89
|
+
// reads `config.security.trustedProxies`.
|
|
90
|
+
export { cacheRedisConnection, clientAddress, fallbackStorage, rateLimitScale, resetSharedRateLimitStorage, scaledLimit, sharedRateLimitStorage, trustedProxies, useRateLimitStorage } from './security-settings';
|
|
88
91
|
// Export path-param sanitization helper (stacksjs/stacks#1870 R-12).
|
|
89
92
|
// Defense-in-depth for actions that interpolate route params into
|
|
90
93
|
// filesystem paths; the helper enforces no-traversal / no-absolute /
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./request-augmentation";export{readJsonBody,readTextBody,RequestBodyError}from"./body";export*from"@stacksjs/bun-router";export{assertRouteMiddlewareResolvable,configureViewDirectories,clearCsrfModuleCache,clearMiddlewareCache,createStacksRouter,disableViewRouting,enhanceRequest,findUnresolvableRouteMiddleware,installMiddlewareHotReload,loadMiddlewareHandlers,resetBootHooks,route,runBootHooks,serve,serverResponse,url,warnOnMultipleRouterInstances}from"./stacks-router";export{cacheRequestQuery,clearCurrentRequest,getCurrentRequest,getTraceId,request,runWithRequest,setCurrentRequest,withTraceId}from"./request-context";export{defineMiddleware,Middleware}from"./middleware";export{appRouteRegistry,bundleMounts,DEFAULT_ROUTE_BUNDLE_FEATURES,DEFAULT_ROUTE_REGISTRY,listRootMountedAppRoutes,loadRoutes,mountedDefaultRouteBundles}from"./route-loader";export{clearTrackedQueries,createErrorResponse,createMiddlewareErrorResponse,createNotFoundResponse,createValidationErrorResponse,getQueryShapeCounts,trackQuery}from"./error-handler";export{listNamedRoutes,listRegisteredRoutes,routeParams}from"./stacks-router";export{DASHBOARD_ROUTE_GROUPS,dashboardGroupMiddleware,describeUnknownDashboardGroups,resolveDashboardAccess}from"./dashboard-access";export{isRouterAction,wrapAction}from"./stacks-router";export{createTypedRouter}from"./typed-router";export{clearRouteModelBindings,defineRouteModelBinding,resolveRouteModel,routeModelBindings,setRouteModelFallback}from"./route-model-binding";export{isApiRequest,JSON_CONTENT_TYPE}from"./api-shape";export{rateLimit,rateLimitStatus,clearRateLimit}from"./rate-limit";export{PathParamError,safePathParam,sanitizePathParam}from"./path-sanitize";export{checkApplicationHealth,runHealthProbes}from"./health";export{stream}from"./stacks-router";export{signedUrl,signUrl,verifySignedUrl,verifySignedUrlMiddleware}from"./signed-url";export{EncryptedSessionStore}from"./encrypted-session-store";export{createSessionStore,createStacksSessionStore,DatabaseSessionStore,FileSessionStore,MemorySessionStore,RedisSessionStore}from"./session-factory";
|
|
1
|
+
import"./request-augmentation";export{readJsonBody,readTextBody,RequestBodyError}from"./body";export*from"@stacksjs/bun-router";export{assertRouteMiddlewareResolvable,configureViewDirectories,clearCsrfModuleCache,clearMiddlewareCache,createStacksRouter,disableViewRouting,enhanceRequest,findUnresolvableRouteMiddleware,installMiddlewareHotReload,loadMiddlewareHandlers,resetBootHooks,route,runBootHooks,serve,serverResponse,url,warnOnMultipleRouterInstances}from"./stacks-router";export{cacheRequestQuery,clearCurrentRequest,getCurrentRequest,getTraceId,request,runWithRequest,setCurrentRequest,withTraceId}from"./request-context";export{defineMiddleware,Middleware}from"./middleware";export{appRouteRegistry,bundleMounts,DEFAULT_ROUTE_BUNDLE_FEATURES,DEFAULT_ROUTE_REGISTRY,listRootMountedAppRoutes,loadRoutes,mountedDefaultRouteBundles}from"./route-loader";export{clearTrackedQueries,createErrorResponse,createMiddlewareErrorResponse,createNotFoundResponse,createValidationErrorResponse,getQueryShapeCounts,trackQuery}from"./error-handler";export{listNamedRoutes,listRegisteredRoutes,routeParams}from"./stacks-router";export{DASHBOARD_ROUTE_GROUPS,dashboardGroupMiddleware,describeUnknownDashboardGroups,resolveDashboardAccess}from"./dashboard-access";export{isRouterAction,wrapAction}from"./stacks-router";export{createTypedRouter}from"./typed-router";export{clearRouteModelBindings,defineRouteModelBinding,resolveRouteModel,routeModelBindings,setRouteModelFallback}from"./route-model-binding";export{isApiRequest,JSON_CONTENT_TYPE}from"./api-shape";export{rateLimit,rateLimitStatus,clearRateLimit,resetRateLimiters}from"./rate-limit";export{cacheRedisConnection,clientAddress,fallbackStorage,rateLimitScale,resetSharedRateLimitStorage,scaledLimit,sharedRateLimitStorage,trustedProxies,useRateLimitStorage}from"./security-settings";export{PathParamError,safePathParam,sanitizePathParam}from"./path-sanitize";export{checkApplicationHealth,runHealthProbes}from"./health";export{stream}from"./stacks-router";export{signedUrl,signUrl,verifySignedUrl,verifySignedUrlMiddleware}from"./signed-url";export{EncryptedSessionStore}from"./encrypted-session-store";export{createSessionStore,createStacksSessionStore,DatabaseSessionStore,FileSessionStore,MemorySessionStore,RedisSessionStore}from"./session-factory";
|
package/dist/rate-limit.d.ts
CHANGED
package/dist/rate-limit.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{getCurrentRequest}from"./request-context";import{clientAddress,scaledLimit,sharedRateLimitStorage}from"./security-settings";let limiterCache,limiterModule,httpErrorModule,limiterModulePromise,limiterStorage,limiterStoragePromise;function loadLimiterModule(){return limiterModulePromise??=import("ts-rate-limiter").then((module)=>limiterModule=module)}function loadLimiterStorage(){return limiterStoragePromise??=sharedRateLimitStorage().then((storage)=>limiterStorage=storage)}export function resetRateLimiters(){for(const limiter of limiterCache?.values()??[])limiter.dispose?.();limiterCache=void 0;limiterStorage=void 0;limiterStoragePromise=void 0}function getLimiter(requestedMax,windowMs){const max=scaledLimit(requestedMax),cacheKey=`${windowMs}:${max}`;let limiter=limiterCache?.get(cacheKey);if(!limiter){if(!limiterModule||limiterStorage===void 0)return Promise.all([loadLimiterModule(),loadLimiterStorage()]).then(()=>getLimiter(requestedMax,windowMs));const{RateLimiter}=limiterModule;limiter=new RateLimiter({windowMs,maxRequests:max,algorithm:"fixed-window",...limiterStorage&&{storage:limiterStorage},standardHeaders:!1,legacyHeaders:!1});(limiterCache??=new Map).set(cacheKey,limiter)}return limiter}function resolveIdentity(explicit){if(explicit!==void 0)return explicit;const req=getCurrentRequest();if(!req)return"anon";const id=req._authenticatedUser?.id;if(typeof id==="number"&&Number.isFinite(id)||typeof id==="string"&&id!=="")return`user:${id}`;return`ip:${clientAddress(req)??"unknown"}`}export function rateLimit(key,max,options={}){const run=async(windowMs)=>{const id=resolveIdentity(options.identity),pendingLimiter=getLimiter(max,windowMs),limiter=pendingLimiter instanceof Promise?await pendingLimiter:pendingLimiter,bucketKey=`${key}:${id}`;try{await limiter.enforce(bucketKey)}catch(err){const{RateLimitError}=await loadLimiterModule();if(err instanceof RateLimitError){const{HttpError}=httpErrorModule??=await import("@stacksjs/error-handling/http-error");throw Object.assign(new HttpError(429,"Too many requests",{key,max,retryAfter:err.retryAfter}),{headers:err.toHeaders()})}throw err}};return{async per(period){const seconds=period==="second"?1:period==="minute"?60:period==="hour"?3600:period==="day"?86400:void 0;if(!seconds)throw Error(`rateLimit().per: unknown period '${period}'`);await run(seconds*1000)},async over(ttlSeconds){if(!Number.isFinite(ttlSeconds)||ttlSeconds<=0)throw Error(`rateLimit().over: ttl must be a positive number, got ${ttlSeconds}`);await run(ttlSeconds*1000)}}}export async function rateLimitStatus(key,max,windowSeconds,options={}){const identity=resolveIdentity(options.identity),pendingLimiter=getLimiter(max,windowSeconds*1000),[id,limiter]=typeof identity==="string"&&!(pendingLimiter instanceof Promise)?[identity,pendingLimiter]:await Promise.all([identity,pendingLimiter]),bucketKey=`${key}:${id}`,result=await limiter.peek(bucketKey);if(!result)return null;return{count:result.current,limit:result.limit,remaining:Math.max(0,result.limit-result.current)}}export async function clearRateLimit(key,max,windowSeconds,options={}){const identity=resolveIdentity(options.identity),pendingLimiter=getLimiter(max,windowSeconds*1000),[id,limiter]=typeof identity==="string"&&!(pendingLimiter instanceof Promise)?[identity,pendingLimiter]:await Promise.all([identity,pendingLimiter]),bucketKey=`${key}:${id}`;await limiter.reset(bucketKey)}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ClientAddressOptions } from '@stacksjs/bun-router';
|
|
2
|
+
import type { StorageProvider } from 'ts-rate-limiter';
|
|
3
|
+
/**
|
|
4
|
+
* The `config/security.ts` settings the request layer reads on every request:
|
|
5
|
+
* the rate-limit scale, the trusted proxies behind `clientAddress()`, and the
|
|
6
|
+
* shared store rate limits count in.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The multiplier applied to every application rate limit and the login
|
|
10
|
+
* lockout. `STACKS_RATE_LIMIT_SCALE` wins over `config.security.rateLimit.scale`;
|
|
11
|
+
* anything that is not a positive finite number reads as 1.
|
|
12
|
+
*/
|
|
13
|
+
export declare function rateLimitScale(): number;
|
|
14
|
+
/** `max` under the configured scale, never below 1. */
|
|
15
|
+
export declare function scaledLimit(max: number): number;
|
|
16
|
+
/**
|
|
17
|
+
* `config.security.trustedProxies`, or undefined for bun-router's default
|
|
18
|
+
* (loopback, private and link-local networks). A non-array is ignored with
|
|
19
|
+
* the default kept, rather than trusting everything or nothing by accident.
|
|
20
|
+
*/
|
|
21
|
+
export declare function trustedProxies(): readonly string[] | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* The client's address, believing forwarding headers only from the proxies
|
|
24
|
+
* `config.security.trustedProxies` names (default: loopback and private
|
|
25
|
+
* networks). Explicit `options` win. This is what `@stacksjs/router` exports
|
|
26
|
+
* as `clientAddress`, shadowing bun-router's config-unaware export.
|
|
27
|
+
*/
|
|
28
|
+
export declare function clientAddress(req: Request, options?: ClientAddressOptions): string | null;
|
|
29
|
+
/**
|
|
30
|
+
* The Redis URL and key prefix of the configured cache, or null when the
|
|
31
|
+
* cache is not Redis. Shared by every limiter that must count across
|
|
32
|
+
* processes (this package's and `@stacksjs/auth`'s lockout).
|
|
33
|
+
*/
|
|
34
|
+
export declare function cacheRedisConnection(cache?: CacheSettings | undefined): { url: string, prefix: string } | null;
|
|
35
|
+
/**
|
|
36
|
+
* A ts-rate-limiter storage that counts in the shared store and falls back
|
|
37
|
+
* to process memory when the shared store errors, so a Redis outage weakens
|
|
38
|
+
* limits to per-process instead of failing every rate-limited request.
|
|
39
|
+
*/
|
|
40
|
+
export declare function fallbackStorage(shared: StorageProvider, local: StorageProvider): StorageProvider;
|
|
41
|
+
/**
|
|
42
|
+
* The storage route and action rate limits count in: the configured Redis
|
|
43
|
+
* cache when there is one (so a limit holds across every process and
|
|
44
|
+
* instance), otherwise null for ts-rate-limiter's in-memory default.
|
|
45
|
+
*/
|
|
46
|
+
export declare function sharedRateLimitStorage(): Promise<StorageProvider | null>;
|
|
47
|
+
/**
|
|
48
|
+
* Count route and action rate limits in `storage` (any ts-rate-limiter
|
|
49
|
+
* `StorageProvider`: a database-backed one, a test double), or `null` for
|
|
50
|
+
* process memory. Call `resetRateLimiters()` afterwards so limiters already
|
|
51
|
+
* built pick it up.
|
|
52
|
+
*/
|
|
53
|
+
export declare function useRateLimitStorage(storage: StorageProvider | null): void;
|
|
54
|
+
/** Forget the resolved store (tests, or after changing the cache config). */
|
|
55
|
+
export declare function resetSharedRateLimitStorage(): void;
|
|
56
|
+
declare interface CacheSettings {
|
|
57
|
+
driver?: string
|
|
58
|
+
prefix?: string
|
|
59
|
+
drivers?: { redis?: { url?: string, host?: string, port?: number, username?: string, password?: string, database?: number, tls?: boolean } }
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import process from"node:process";import{clientAddress as resolveClientAddress}from"@stacksjs/bun-router";import{config}from"@stacksjs/config";export function rateLimitScale(){const fromEnv=process.env.STACKS_RATE_LIMIT_SCALE,raw=fromEnv!==void 0&&fromEnv!==""?Number(fromEnv):config.security?.rateLimit?.scale,scale=typeof raw==="number"?raw:Number(raw);return Number.isFinite(scale)&&scale>0?scale:1}export function scaledLimit(max){return Math.max(1,Math.floor(max*rateLimitScale()))}export function trustedProxies(){const configured=config.security?.trustedProxies;if(!Array.isArray(configured))return;return configured.filter((entry)=>typeof entry==="string"&&entry.length>0)}export function clientAddress(req,options){if(options)return resolveClientAddress(req,options);const proxies=trustedProxies();return resolveClientAddress(req,proxies?{trustedProxies:proxies}:void 0)}export function cacheRedisConnection(cache=config.cache){const redis=cache?.drivers?.redis;if(cache?.driver!=="redis"||!redis)return null;const prefix=cache.prefix??"stacks";if(redis.url)return{url:redis.url,prefix};const protocol=redis.tls?"rediss":"redis",credentials=redis.username?`${encodeURIComponent(redis.username)}:${encodeURIComponent(redis.password??"")}@`:redis.password?`:${encodeURIComponent(redis.password)}@`:"",database=redis.database==null?"":`/${redis.database}`;return{url:`${protocol}://${credentials}${redis.host??"127.0.0.1"}:${redis.port??6379}${database}`,prefix}}let warnedSharedStore=!1;function warnSharedStoreOnce(error){if(warnedSharedStore)return;warnedSharedStore=!0;console.warn(`[router] rate limits fell back to this process's memory: the configured Redis cache is unreachable (${error instanceof Error?error.message:String(error)}). Limits are per process until it recovers.`)}export function fallbackStorage(shared,local){async function attempt(primary,secondary){try{return await primary()}catch(error){warnSharedStoreOnce(error);return secondary()}}return{increment:(key,windowMs)=>attempt(()=>shared.increment(key,windowMs),()=>local.increment(key,windowMs)),reset:(key)=>attempt(()=>shared.reset(key),()=>local.reset(key)),getCount:(key)=>attempt(()=>shared.getCount?.(key)??Promise.resolve(0),()=>local.getCount?.(key)??Promise.resolve(0)),dispose:()=>{shared.dispose?.();local.dispose?.()}}}let sharedStorage;export function sharedRateLimitStorage(){return sharedStorage??=(async()=>{const connection=cacheRedisConnection();if(!connection)return null;const[{RedisClient},{MemoryStorage,RedisStorage}]=await Promise.all([import("bun"),import("ts-rate-limiter")]),client=new RedisClient(connection.url),shared=new RedisStorage({client,keyPrefix:`${connection.prefix}:ratelimit:`});return fallbackStorage(shared,new MemoryStorage)})()}export function useRateLimitStorage(storage){sharedStorage=Promise.resolve(storage)}export function resetSharedRateLimitStorage(){sharedStorage=void 0;warnedSharedStore=!1}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/router",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.230",
|
|
6
6
|
"description": "The Stacks framework router.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -67,27 +67,27 @@
|
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@stacksjs/bun-router": "^0.1.24",
|
|
70
|
-
"@stacksjs/cache": "0.75.
|
|
71
|
-
"@stacksjs/collections": "0.75.
|
|
72
|
-
"@stacksjs/config": "0.75.
|
|
73
|
-
"@stacksjs/error-handling": "0.75.
|
|
74
|
-
"@stacksjs/logging": "0.75.
|
|
75
|
-
"@stacksjs/pagination": "0.75.
|
|
76
|
-
"@stacksjs/path": "0.75.
|
|
77
|
-
"@stacksjs/security": "0.75.
|
|
78
|
-
"@stacksjs/storage": "0.75.
|
|
70
|
+
"@stacksjs/cache": "0.75.230",
|
|
71
|
+
"@stacksjs/collections": "0.75.230",
|
|
72
|
+
"@stacksjs/config": "0.75.230",
|
|
73
|
+
"@stacksjs/error-handling": "0.75.230",
|
|
74
|
+
"@stacksjs/logging": "0.75.230",
|
|
75
|
+
"@stacksjs/pagination": "0.75.230",
|
|
76
|
+
"@stacksjs/path": "0.75.230",
|
|
77
|
+
"@stacksjs/security": "0.75.230",
|
|
78
|
+
"@stacksjs/storage": "0.75.230",
|
|
79
79
|
"ts-rate-limiter": "^0.4.10"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@stacksjs/actions": "0.75.
|
|
83
|
-
"@stacksjs/types": "0.75.
|
|
82
|
+
"@stacksjs/actions": "0.75.230",
|
|
83
|
+
"@stacksjs/types": "0.75.230",
|
|
84
84
|
"better-dx": "^0.2.24"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
|
-
"@stacksjs/api": "0.75.
|
|
88
|
-
"@stacksjs/auth": "0.75.
|
|
89
|
-
"@stacksjs/database": "0.75.
|
|
90
|
-
"@stacksjs/validation": "0.75.
|
|
87
|
+
"@stacksjs/api": "0.75.230",
|
|
88
|
+
"@stacksjs/auth": "0.75.230",
|
|
89
|
+
"@stacksjs/database": "0.75.230",
|
|
90
|
+
"@stacksjs/validation": "0.75.230"
|
|
91
91
|
},
|
|
92
92
|
"peerDependenciesMeta": {
|
|
93
93
|
"@stacksjs/api": {
|