@stacksjs/router 0.75.233 → 0.75.236
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/README.md +22 -0
- package/dist/body.d.ts +2 -1
- package/dist/body.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/json.d.ts +5 -0
- package/dist/json.js +1 -0
- package/package.json +16 -16
package/README.md
CHANGED
|
@@ -76,6 +76,28 @@ For casual chit-chat with others using this package:
|
|
|
76
76
|
|
|
77
77
|
[Join the Stacks Discord Server](https://stacksjs.com/discord)
|
|
78
78
|
|
|
79
|
+
## Bounded JSON protocols
|
|
80
|
+
|
|
81
|
+
Routes that own their wire body can opt into rejecting duplicate object keys:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { readJsonBody } from '@stacksjs/router/body'
|
|
85
|
+
|
|
86
|
+
const input = await readJsonBody(request, 8192, { rejectDuplicateKeys: true })
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Keep automatic body parsing disabled for this route. The reader checks the media
|
|
90
|
+
type, counts streamed bytes and validates UTF-8 before parsing. Duplicate keys at
|
|
91
|
+
any depth, including equivalent escaped spellings, produce `RequestBodyError`
|
|
92
|
+
with status400. The default retains normal `JSON.parse` behavior for existing
|
|
93
|
+
callers. Identical names in different objects remain valid, and Unicode keys are
|
|
94
|
+
compared after escape decoding without Unicode normalization.
|
|
95
|
+
|
|
96
|
+
`parseJsonText(text, options)` is also exported from `@stacksjs/router` and
|
|
97
|
+
`@stacksjs/router/json` for already-decoded JSON. It throws `SyntaxError` for
|
|
98
|
+
invalid JSON or a duplicate key in strict mode. This helper does not read a
|
|
99
|
+
request, limit bytes, authenticate a caller or validate an application schema.
|
|
100
|
+
|
|
79
101
|
## 📄 License
|
|
80
102
|
|
|
81
103
|
The MIT License (MIT). Please see [LICENSE](https://github.com/stacksjs/stacks/tree/main/LICENSE.md) for more information.
|
package/dist/body.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import type { JsonParseOptions } from './json';
|
|
2
|
+
export declare function readJsonBody(request: Request, maxBytes?: unknown, options?: JsonParseOptions): Promise<unknown>;
|
|
2
3
|
/** Bounded UTF-8 for form and text protocols, counted from actual streamed bytes. */
|
|
3
4
|
export declare function readTextBody(request: Request, maxBytes?: unknown): Promise<string>;
|
|
4
5
|
/** Bounded body reads for protocol handlers that need the original wire body. */
|
package/dist/body.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export class RequestBodyError extends Error{status;constructor(message,status){super(message);this.status=status;this.name="RequestBodyError"}}export async function readJsonBody(request,maxBytes=1048576){if(!Number.isSafeInteger(maxBytes)||maxBytes<1)throw RangeError("maxBytes must be a positive safe integer");if(request.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase()!=="application/json")throw new RequestBodyError("Content-Type must be application/json",415);const text=await readTextBody(request,maxBytes);try{return
|
|
1
|
+
import{parseJsonText}from"./json";export class RequestBodyError extends Error{status;constructor(message,status){super(message);this.status=status;this.name="RequestBodyError"}}export async function readJsonBody(request,maxBytes=1048576,options={}){if(!Number.isSafeInteger(maxBytes)||maxBytes<1)throw RangeError("maxBytes must be a positive safe integer");if(request.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase()!=="application/json")throw new RequestBodyError("Content-Type must be application/json",415);const text=await readTextBody(request,maxBytes);try{return parseJsonText(text,options)}catch{throw new RequestBodyError("Send valid UTF-8 JSON",400)}}export async function readTextBody(request,maxBytes=1048576){if(!Number.isSafeInteger(maxBytes)||maxBytes<1)throw RangeError("maxBytes must be a positive safe integer");const declared=request.headers.get("content-length");if(declared&&(!/^\d+$/.test(declared)||Number(declared)>maxBytes))throw new RequestBodyError("Request body is too large",413);if(!request.body)throw new RequestBodyError("Send a request body",400);const reader=request.body.getReader();let bytes=new Uint8Array(Math.min(maxBytes,8192)),size=0;try{while(!0){const{done,value}=await reader.read();if(done)break;const offset=size;size+=value.byteLength;if(size>maxBytes){reader.cancel().catch(()=>{});throw new RequestBodyError("Request body is too large",413)}if(size>bytes.length){const grown=new Uint8Array(Math.min(maxBytes,Math.max(size,bytes.length*2)));grown.set(bytes);bytes=grown}bytes.set(value,offset)}}finally{reader.releaseLock()}try{return new TextDecoder("utf-8",{fatal:!0}).decode(bytes.subarray(0,size))}catch{throw new RequestBodyError("Send valid UTF-8",400)}}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import './request-augmentation';
|
|
2
|
+
export type { JsonParseOptions } from './json';
|
|
2
3
|
// Re-export the augmentation types so userland can refer to the marker
|
|
3
4
|
// surface explicitly when needed.
|
|
4
5
|
export type { StacksRequestExtensions, StacksRequestMacros, StacksRequestMarkers } from './request-augmentation';
|
|
@@ -27,6 +28,7 @@ export type {
|
|
|
27
28
|
StacksSessionConfig,
|
|
28
29
|
} from './session-factory';
|
|
29
30
|
export { readJsonBody, readTextBody, RequestBodyError } from './body';
|
|
31
|
+
export { parseJsonText } from './json';
|
|
30
32
|
// Re-export everything from bun-router (includes response factory)
|
|
31
33
|
export * from '@stacksjs/bun-router';
|
|
32
34
|
// Export Stacks-specific action resolver and URL helper
|
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,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";
|
|
1
|
+
import"./request-augmentation";export{readJsonBody,readTextBody,RequestBodyError}from"./body";export{parseJsonText}from"./json";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/json.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Parse already-decoded JSON. Callers reading a wire body must enforce its byte/UTF-8 limits first. */
|
|
2
|
+
export declare function parseJsonText(text: string, options?: JsonParseOptions): unknown;
|
|
3
|
+
export declare interface JsonParseOptions {
|
|
4
|
+
rejectDuplicateKeys?: boolean
|
|
5
|
+
}
|
package/dist/json.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function parseJsonText(text,options={}){if(typeof text!=="string")throw TypeError("JSON text must be a string");const result=JSON.parse(text);if(!options.rejectDuplicateKeys)return result;const containers=[];for(let position=0;position<text.length;position++){const character=text[position];if(character==="{")containers.push(new Set);else if(character==="[")containers.push(null);else if(character==="}"||character==="]")containers.pop();else if(character==='"'){const start=position;while(++position<text.length)if(text[position]==="\\")position++;else if(text[position]==='"')break;let next=position+1;while(next<text.length&&/\s/.test(text[next]))next++;if(text[next]!==":")continue;const keys=containers.at(-1);if(!keys)throw SyntaxError("Invalid JSON object key");const key=JSON.parse(text.slice(start,position+1));if(keys.has(key))throw SyntaxError("Duplicate JSON object key");keys.add(key)}}return result}
|
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.236",
|
|
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.236",
|
|
71
|
+
"@stacksjs/collections": "0.75.236",
|
|
72
|
+
"@stacksjs/config": "0.75.236",
|
|
73
|
+
"@stacksjs/error-handling": "0.75.236",
|
|
74
|
+
"@stacksjs/logging": "0.75.236",
|
|
75
|
+
"@stacksjs/pagination": "0.75.236",
|
|
76
|
+
"@stacksjs/path": "0.75.236",
|
|
77
|
+
"@stacksjs/security": "0.75.236",
|
|
78
|
+
"@stacksjs/storage": "0.75.236",
|
|
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.236",
|
|
83
|
+
"@stacksjs/types": "0.75.236",
|
|
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.236",
|
|
88
|
+
"@stacksjs/auth": "0.75.236",
|
|
89
|
+
"@stacksjs/database": "0.75.236",
|
|
90
|
+
"@stacksjs/validation": "0.75.236"
|
|
91
91
|
},
|
|
92
92
|
"peerDependenciesMeta": {
|
|
93
93
|
"@stacksjs/api": {
|