@stacksjs/router 0.70.45 → 0.70.53
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/LICENSE.md +21 -0
- package/dist/api-shape.d.ts +24 -0
- package/dist/encrypted-session-store.d.ts +19 -0
- package/dist/error-handler.d.ts +10 -4
- package/dist/index.d.ts +57 -11
- package/dist/index.js +33 -32
- package/dist/middleware.d.ts +1 -0
- package/dist/path-sanitize.d.ts +64 -0
- package/dist/request-augmentation.d.ts +67 -0
- package/dist/request-context.d.ts +32 -9
- package/dist/route-loader.d.ts +1 -1
- package/dist/route-types.d.ts +12 -0
- package/dist/security-headers.d.ts +20 -0
- package/dist/session-factory.d.ts +57 -0
- package/dist/signed-url.d.ts +50 -0
- package/dist/stacks-router.d.ts +93 -0
- package/package.json +11 -11
package/dist/middleware.d.ts
CHANGED
|
@@ -33,4 +33,5 @@ export declare class Middleware {
|
|
|
33
33
|
readonly priority: number;
|
|
34
34
|
readonly handle: (request: EnhancedRequest) => void | Promise<void>;
|
|
35
35
|
constructor(config: MiddlewareConfig);
|
|
36
|
+
toRouterHandler(): (req: EnhancedRequest, next: () => Promise<Response>) => Promise<Response>;
|
|
36
37
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate and return a path parameter, throwing if it's unsafe to use
|
|
3
|
+
* in filesystem interpolation.
|
|
4
|
+
*
|
|
5
|
+
* The default contract is single-segment: no `/`, no `\`, no `..`, no
|
|
6
|
+
* absolute path, no null bytes, no control characters, length ≤ 255.
|
|
7
|
+
* Pass `allowSlashes: true` for a multi-segment path (still rejects
|
|
8
|
+
* the rest).
|
|
9
|
+
*
|
|
10
|
+
* @throws {PathParamError} when the value fails any check.
|
|
11
|
+
*/
|
|
12
|
+
export declare function sanitizePathParam(value: unknown, options?: SanitizePathParamOptions): string;
|
|
13
|
+
/**
|
|
14
|
+
* Non-throwing variant. Returns the sanitized value or `null` if any
|
|
15
|
+
* check failed. Use when you want a fast yes/no in a conditional
|
|
16
|
+
* without a try/catch around the throw site.
|
|
17
|
+
*/
|
|
18
|
+
export declare function safePathParam(value: unknown, options?: SanitizePathParamOptions): string | null;
|
|
19
|
+
export declare interface SanitizePathParamOptions {
|
|
20
|
+
context?: string
|
|
21
|
+
maxLength?: number
|
|
22
|
+
allowSlashes?: boolean
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Path-parameter sanitization helpers.
|
|
26
|
+
*
|
|
27
|
+
* Route params arrive from the URL as untyped strings and are merged
|
|
28
|
+
* directly into `req.params`. Actions that interpolate those values
|
|
29
|
+
* into filesystem paths or shell commands without first scrubbing
|
|
30
|
+
* them are vulnerable to `..`-traversal, absolute-path takeovers, and
|
|
31
|
+
* null-byte truncation attacks.
|
|
32
|
+
*
|
|
33
|
+
* The router itself can't auto-sanitize every param (some are
|
|
34
|
+
* deliberately path-shaped — file servers, asset proxies, etc.). What
|
|
35
|
+
* we ship instead is a single canonical helper that callers reach for
|
|
36
|
+
* at the boundary where the param meets the filesystem.
|
|
37
|
+
*
|
|
38
|
+
* See stacksjs/stacks#1870 R-12.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* import { sanitizePathParam } from '@stacksjs/router'
|
|
43
|
+
*
|
|
44
|
+
* const filename = sanitizePathParam(req.params.filename, {
|
|
45
|
+
* context: 'avatar download',
|
|
46
|
+
* })
|
|
47
|
+
* return new Response(Bun.file(path.appPath(`avatars/${filename}`)))
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* Reasons {@link sanitizePathParam} rejects a value. Surfaced via the
|
|
52
|
+
* thrown error so callers can log or branch.
|
|
53
|
+
*/
|
|
54
|
+
export type PathParamRejection = | 'empty'
|
|
55
|
+
| 'not-string'
|
|
56
|
+
| 'absolute-path'
|
|
57
|
+
| 'traversal'
|
|
58
|
+
| 'null-byte'
|
|
59
|
+
| 'control-char'
|
|
60
|
+
| 'too-long';
|
|
61
|
+
export declare class PathParamError extends Error {
|
|
62
|
+
readonly reason: PathParamRejection;
|
|
63
|
+
constructor(reason: PathParamRejection, value: unknown, context?: string);
|
|
64
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { FileInfo } from '@stacksjs/bun-router';
|
|
2
|
+
/**
|
|
3
|
+
* Stacks-specific marker fields attached to the request by the
|
|
4
|
+
* router itself and the framework's default middleware.
|
|
5
|
+
*
|
|
6
|
+
* Markers are deliberately prefixed with `_` so they can't collide
|
|
7
|
+
* with userland keys on the request, and their lifetimes are bounded
|
|
8
|
+
* by the request's lifetime — they're never persisted.
|
|
9
|
+
*/
|
|
10
|
+
export declare interface StacksRequestMarkers {
|
|
11
|
+
_corsConfig?: unknown
|
|
12
|
+
_forceJson?: boolean
|
|
13
|
+
_skipCsrf?: boolean
|
|
14
|
+
_compress?: boolean
|
|
15
|
+
_middlewareParams?: Record<string, string>
|
|
16
|
+
_requestId?: string
|
|
17
|
+
_startNs?: bigint
|
|
18
|
+
_authenticatedUser?: unknown
|
|
19
|
+
_currentAccessToken?: unknown
|
|
20
|
+
_bodyParsed?: boolean
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Laravel-style request-input macros that Stacks attaches in
|
|
24
|
+
* `enhanceRequest` (router/src/stacks-router.ts). These shadow some
|
|
25
|
+
* of bun-router's `RequestMacroMethods` with Stacks-specific
|
|
26
|
+
* implementations (more permissive `T = any` generics so action
|
|
27
|
+
* callers don't have to specify the return type for every read).
|
|
28
|
+
*
|
|
29
|
+
* Listed here as part of the augmentation so call sites like
|
|
30
|
+
* `request.input(key)` type-check without `as any`.
|
|
31
|
+
*/
|
|
32
|
+
export declare interface StacksRequestMacros {
|
|
33
|
+
input?: <T = unknown>(key: string, defaultValue?: T) => T
|
|
34
|
+
get?: <T = unknown>(key: string, defaultValue?: T) => T
|
|
35
|
+
all?: () => Record<string, unknown>
|
|
36
|
+
only?: <T extends Record<string, unknown>>(keys: string[]) => T
|
|
37
|
+
except?: <T extends Record<string, unknown>>(keys: string[]) => T
|
|
38
|
+
has?: (key: string | string[]) => boolean
|
|
39
|
+
hasAny?: (keys: string[]) => boolean
|
|
40
|
+
missing?: (key: string) => boolean
|
|
41
|
+
filled?: (key: string) => boolean
|
|
42
|
+
integer?: (key: string, defaultValue?: number) => number
|
|
43
|
+
float?: (key: string, defaultValue?: number) => number
|
|
44
|
+
boolean?: (key: string, defaultValue?: boolean) => boolean
|
|
45
|
+
string?: (key: string, defaultValue?: string) => string
|
|
46
|
+
array?: <T = unknown>(key: string, defaultValue?: T[]) => T[]
|
|
47
|
+
file?: (key: string) => FileInfo | null
|
|
48
|
+
files?: (key: string) => FileInfo[]
|
|
49
|
+
hasFile?: (key: string) => boolean
|
|
50
|
+
allFiles?: () => Record<string, FileInfo | FileInfo[]>
|
|
51
|
+
getFiles?: () => Record<string, FileInfo | FileInfo[]>
|
|
52
|
+
user?: () => Promise<unknown>
|
|
53
|
+
userToken?: () => Promise<unknown>
|
|
54
|
+
tokenCan?: (ability: string) => Promise<boolean>
|
|
55
|
+
tokenCant?: (ability: string) => Promise<boolean>
|
|
56
|
+
can?: (ability: string, ...args: unknown[]) => Promise<boolean>
|
|
57
|
+
cannot?: (ability: string, ...args: unknown[]) => Promise<boolean>
|
|
58
|
+
authorize?: (ability: string, ...args: unknown[]) => Promise<void>
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Union of Stacks markers + macros — useful as a single type alias for
|
|
62
|
+
* places that previously cast to `any`.
|
|
63
|
+
*/
|
|
64
|
+
export type StacksRequestExtensions = StacksRequestMarkers & StacksRequestMacros;
|
|
65
|
+
declare module '@stacksjs/bun-router' {
|
|
66
|
+
interface EnhancedRequestextends StacksRequestExtensions {}
|
|
67
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EnhancedRequest } from '@stacksjs/bun-router';
|
|
2
|
+
import type { RequestInstance } from '@stacksjs/types';
|
|
2
3
|
/**
|
|
3
4
|
* Read the active trace id, or `undefined` outside any traced scope.
|
|
4
5
|
*
|
|
@@ -36,6 +37,16 @@ export declare function cacheRequestQuery<T>(key: string, fetcher: () => T | Pro
|
|
|
36
37
|
* Called by middleware/router when handling a request
|
|
37
38
|
*/
|
|
38
39
|
export declare function setCurrentRequest(req: EnhancedRequest): void;
|
|
40
|
+
/**
|
|
41
|
+
* Clear the current request context.
|
|
42
|
+
*
|
|
43
|
+
* `setCurrentRequest` uses `AsyncLocalStorage.enterWith`, which mutates the
|
|
44
|
+
* caller's async scope and never restores it. Call this in test teardown
|
|
45
|
+
* (`afterEach`) whenever a test body calls `setCurrentRequest`, so the leaked
|
|
46
|
+
* frame doesn't poison subsequently-collected test files (bun's runner
|
|
47
|
+
* mis-registers tests when collected on a foreign async frame).
|
|
48
|
+
*/
|
|
49
|
+
export declare function clearCurrentRequest(): void;
|
|
39
50
|
/**
|
|
40
51
|
* Run a function with a request context
|
|
41
52
|
* All code executed within the callback will have access to the request
|
|
@@ -47,13 +58,25 @@ export declare function runWithRequest<T>(req: EnhancedRequest, fn: () => T): T;
|
|
|
47
58
|
export declare function getCurrentRequest(): EnhancedRequest | undefined;
|
|
48
59
|
/**
|
|
49
60
|
* Request proxy that provides access to the current request
|
|
50
|
-
*
|
|
61
|
+
* (Laravel's `request()` helper, but typed).
|
|
51
62
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
* The proxy is statically typed as {@link RequestInstance} —
|
|
64
|
+
* the canonical Stacks-side action-request surface
|
|
65
|
+
* (stacksjs/stacks#1851 Phase 1). All the macros action handlers
|
|
66
|
+
* reach for (`all`, `get`, `input`, `cookies`, `param`, `validate`,
|
|
67
|
+
* `user`, `bearerToken`, …) resolve to their declared types instead
|
|
68
|
+
* of `any`, eliminating most `(request as any)` casts in action code.
|
|
69
|
+
*
|
|
70
|
+
* Runtime is unchanged — the proxy still delegates to whichever
|
|
71
|
+
* `EnhancedRequest` is in the AsyncLocalStorage slot. The type swap
|
|
72
|
+
* is API-compatible: every method action code uses on `request`
|
|
73
|
+
* existed on either type already, but only `RequestInstance` carries
|
|
74
|
+
* the model-aware / path-aware narrowing.
|
|
75
|
+
*
|
|
76
|
+
* Methods worth knowing about:
|
|
77
|
+
* - `bearerToken()` — Authorization header
|
|
78
|
+
* - `user()` — authenticated user (async)
|
|
79
|
+
* - `userToken()` — current access token (async)
|
|
80
|
+
* - `tokenCan(ability)` / `tokenCant(ability)` — async ability checks
|
|
81
|
+
*/
|
|
82
|
+
export declare const request: RequestInstance;
|
package/dist/route-loader.d.ts
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route registry types — owned by `@stacksjs/router` because the router
|
|
3
|
+
* consumes them. `app/Routes.ts` (the project-level route map) imports
|
|
4
|
+
* these via the public package name rather than a relative reach into
|
|
5
|
+
* the framework defaults tree (stacksjs/stacks#1863, T-10).
|
|
6
|
+
*/
|
|
7
|
+
export declare interface RouteDefinition {
|
|
8
|
+
path: string
|
|
9
|
+
prefix?: string
|
|
10
|
+
middleware?: string | string[]
|
|
11
|
+
}
|
|
12
|
+
export type RouteRegistry = Record<string, string | RouteDefinition>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply the default security headers to a Headers instance in-place.
|
|
3
|
+
*
|
|
4
|
+
* Headers applied unconditionally (cheap, no compat risk):
|
|
5
|
+
* - `X-Content-Type-Options: nosniff` — blocks MIME-sniff XSS
|
|
6
|
+
* - `X-Frame-Options: SAMEORIGIN` — clickjacking protection (CSP
|
|
7
|
+
* `frame-ancestors` is the modern equivalent but XFO still ships)
|
|
8
|
+
* - `Referrer-Policy: strict-origin-when-cross-origin` — modern default
|
|
9
|
+
*
|
|
10
|
+
* Production-only:
|
|
11
|
+
* - `Strict-Transport-Security: max-age=31536000; includeSubDomains` —
|
|
12
|
+
* tells browsers to commit to HTTPS for a year. Omits `preload` since
|
|
13
|
+
* that's an irreversible commitment to the browser preload list.
|
|
14
|
+
*
|
|
15
|
+
* Skips overwriting any header that's already set — explicit userland
|
|
16
|
+
* config wins. Skips entirely when `STACKS_SECURITY_HEADERS_DISABLE=true`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function applySecurityHeaders(headers: Headers): void;
|
|
19
|
+
/** Test helper — reset the cached env-derived flags. */
|
|
20
|
+
export declare function __resetSecurityHeadersCache(): void;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createSessionStore } from '@stacksjs/bun-router';
|
|
2
|
+
import type { RedisClient, SessionConfig, SessionData, SessionStore } from '@stacksjs/bun-router';
|
|
3
|
+
// Re-export bun-router's session types so app code only has to
|
|
4
|
+
// import from `@stacksjs/router` (one less package boundary to
|
|
5
|
+
// learn). Drivers stay accessible by name for callers that want
|
|
6
|
+
// to assemble a custom store manually.
|
|
7
|
+
export type {
|
|
8
|
+
RedisClient,
|
|
9
|
+
SessionConfig,
|
|
10
|
+
SessionData,
|
|
11
|
+
SessionStore,
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Build a session store from the Stacks config. Mirrors what
|
|
15
|
+
* `mail` / `Jobs` do for their driver registries — call once at
|
|
16
|
+
* boot, pass the result into the session middleware.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* // config/session.ts (typical)
|
|
21
|
+
* export const session = {
|
|
22
|
+
* driver: 'redis',
|
|
23
|
+
* ttl: 60 * 60 * 24, // 24h
|
|
24
|
+
* cookie: { name: 'sid', httpOnly: true, sameSite: 'lax' },
|
|
25
|
+
* redis: { client: useRedis() },
|
|
26
|
+
* } satisfies StacksSessionConfig
|
|
27
|
+
*
|
|
28
|
+
* // Then at boot:
|
|
29
|
+
* const store = createStacksSessionStore(session)
|
|
30
|
+
* app.use(sessionMiddleware({ store }))
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function createStacksSessionStore(config: StacksSessionConfig): SessionStore<SessionData>;
|
|
34
|
+
/**
|
|
35
|
+
* Stacks-level session configuration. Extends bun-router's
|
|
36
|
+
* {@link SessionConfig} with:
|
|
37
|
+
*
|
|
38
|
+
* - `encrypt`: opt-in/out of {@link EncryptedSessionStore}
|
|
39
|
+
* wrapping (default: `'auto'` — on in production, off in
|
|
40
|
+
* dev/test where readability matters more than encryption-
|
|
41
|
+
* at-rest)
|
|
42
|
+
* - `appKey`: override key for the encryption (defaults to
|
|
43
|
+
* `process.env.APP_KEY`)
|
|
44
|
+
*
|
|
45
|
+
* All other fields pass through to bun-router unchanged.
|
|
46
|
+
*/
|
|
47
|
+
export declare interface StacksSessionConfig extends SessionConfig {
|
|
48
|
+
encrypt?: boolean | 'auto'
|
|
49
|
+
appKey?: string
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
DatabaseSessionStore,
|
|
53
|
+
FileSessionStore,
|
|
54
|
+
MemorySessionStore,
|
|
55
|
+
RedisSessionStore,
|
|
56
|
+
createSessionStore,
|
|
57
|
+
} from '@stacksjs/bun-router';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { EnhancedRequest } from '@stacksjs/bun-router';
|
|
2
|
+
/**
|
|
3
|
+
* Sign an existing URL (full or path-only). Returns a new URL string
|
|
4
|
+
* with `expires` (optional) and `signature` query params appended.
|
|
5
|
+
*
|
|
6
|
+
* Path-only inputs (`/api/email/verify?user=42`) inherit `APP_URL` as
|
|
7
|
+
* the origin — same convention as {@link buildUrl}.
|
|
8
|
+
*/
|
|
9
|
+
export declare function signUrl(input: string, options?: SignedUrlOptions): string;
|
|
10
|
+
/**
|
|
11
|
+
* Convenience wrapper that resolves a named route to a URL via {@link buildUrl}
|
|
12
|
+
* and then signs it. Mirrors Laravel's `URL::signedRoute()`.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* route.get('/api/email/verify', VerifyEmailAction).name('email.verify')
|
|
17
|
+
* const link = signedUrl('email.verify', { user: 42 }, { ttl: 60 * 60 * 24 })
|
|
18
|
+
* // → https://app.example.com/api/email/verify?user=42&expires=1716470400&signature=…
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function signedUrl(routeName: string, params?: Record<string, string | number>, options?: SignedUrlOptions): string;
|
|
22
|
+
/**
|
|
23
|
+
* Verify the `signature` (and optional `expires`) on an incoming URL.
|
|
24
|
+
* Returns a discriminated result so callers can pick their own status
|
|
25
|
+
* code per failure mode.
|
|
26
|
+
*/
|
|
27
|
+
export declare function verifySignedUrl(input: string | URL): SignedUrlVerifyResult;
|
|
28
|
+
/**
|
|
29
|
+
* Middleware shape for `route.middleware('signed')`. Verifies the
|
|
30
|
+
* incoming URL's signature and throws a `Response` (the router's
|
|
31
|
+
* short-circuit contract) when it fails. Drop in as a route-level
|
|
32
|
+
* middleware on any URL minted by {@link signedUrl}.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* route.get('/email/verify', 'Actions/VerifyEmail').middleware('signed')
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function verifySignedUrlMiddleware(req: EnhancedRequest): Promise<void>;
|
|
40
|
+
export declare interface SignedUrlOptions {
|
|
41
|
+
expiresAt?: number
|
|
42
|
+
ttl?: number
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Result of {@link verifySignedUrl}. The `reason` only fires when `valid`
|
|
46
|
+
* is `false` — gives callers (and middleware) enough to decide whether
|
|
47
|
+
* to log, return 401, or return 410 (expired vs. tampered).
|
|
48
|
+
*/
|
|
49
|
+
export type SignedUrlVerifyResult = | { valid: true }
|
|
50
|
+
| { valid: false, reason: 'missing-signature' | 'expired' | 'invalid-signature' }
|
package/dist/stacks-router.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Server } from 'bun';
|
|
2
|
+
import './request-augmentation';
|
|
2
3
|
import { Router } from '@stacksjs/bun-router';
|
|
3
4
|
import type { ActionHandler, EnhancedRequest, Route, ServerOptions } from '@stacksjs/bun-router';
|
|
5
|
+
import type { ActionValidations, ValidationResult } from '@stacksjs/actions';
|
|
4
6
|
/**
|
|
5
7
|
* Generate a full URL for a named route, like Laravel's route() helper.
|
|
6
8
|
*
|
|
@@ -54,6 +56,39 @@ export declare function clearMiddlewareCache(): void;
|
|
|
54
56
|
* old version forever.
|
|
55
57
|
*/
|
|
56
58
|
export declare function installMiddlewareHotReload(): () => void;
|
|
59
|
+
/**
|
|
60
|
+
* Resolve every middleware alias referenced by a registered route and
|
|
61
|
+
* report the ones that don't load. `csrf` is always checked too — it's
|
|
62
|
+
* auto-injected on unsafe methods even when no route lists it.
|
|
63
|
+
*
|
|
64
|
+
* Resolution is inherently lazy (`.middleware(name)` is a sync chainable
|
|
65
|
+
* that just records a string; the alias map and middleware modules load
|
|
66
|
+
* via async dynamic import), so a throw at literal registration time is
|
|
67
|
+
* impossible. Calling this after all routes are registered — the end of
|
|
68
|
+
* `importRoutes()` and the compiled-binary boot in core/server — IS
|
|
69
|
+
* effectively registration-time validation. See stacksjs/stacks#1957.
|
|
70
|
+
*/
|
|
71
|
+
export declare function findUnresolvableRouteMiddleware(): Promise<Array<{ alias: string, routes: string[] }>>;
|
|
72
|
+
/**
|
|
73
|
+
* Throw when any registered route references a middleware alias that
|
|
74
|
+
* cannot be resolved. Fail-closed boot validation: a typo'd `auth` alias
|
|
75
|
+
* must abort startup loudly, not serve the route unprotected (the
|
|
76
|
+
* request-time guard in createMiddlewareHandler 500s as a backstop).
|
|
77
|
+
*/
|
|
78
|
+
export declare function assertRouteMiddlewareResolvable(): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Run an action's declarative `validations:` against the request.
|
|
81
|
+
*
|
|
82
|
+
* @internal Exported for regression coverage of path-param coercion
|
|
83
|
+
* (stacksjs/stacks#1865). Production callers should rely on the
|
|
84
|
+
* router's action-resolution path, which invokes this for you.
|
|
85
|
+
*/
|
|
86
|
+
export declare function validateActionInput(req: EnhancedRequest, validations: ActionValidations): Promise<ValidationResult>;
|
|
87
|
+
export declare function stream(source: ReadableStream | AsyncIterable<string | Uint8Array>, options?: StreamOptions): Response;
|
|
88
|
+
// Decorate the incoming request with the helpers the framework's middleware
|
|
89
|
+
// and actions assume are always available. Names follow Laravel's convention
|
|
90
|
+
// because that's the API surface Stacks userland expects.
|
|
91
|
+
export declare function enhanceRequest(req: EnhancedRequest): EnhancedRequest;
|
|
57
92
|
/**
|
|
58
93
|
* Create a Stacks-enhanced router
|
|
59
94
|
*/
|
|
@@ -74,6 +109,7 @@ declare interface StacksRouterConfig {
|
|
|
74
109
|
declare interface GroupOptions {
|
|
75
110
|
prefix?: string
|
|
76
111
|
middleware?: string | string[]
|
|
112
|
+
apiResponse?: boolean
|
|
77
113
|
}
|
|
78
114
|
declare interface ResourceRouteOptions {
|
|
79
115
|
only?: ResourceAction[]
|
|
@@ -87,6 +123,41 @@ declare interface ChainableRoute {
|
|
|
87
123
|
middleware: (name: string) => ChainableRoute
|
|
88
124
|
name: (routeName: string) => ChainableRoute
|
|
89
125
|
skipCsrf: () => ChainableRoute
|
|
126
|
+
requireCsrf: () => ChainableRoute
|
|
127
|
+
rateLimit: (max: number, window: 'second' | 'minute' | 'hour' | 'day' | number) => ChainableRoute
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Helper for streaming responses — wraps a `ReadableStream` or async
|
|
131
|
+
* generator with the right headers for the chosen content type.
|
|
132
|
+
*
|
|
133
|
+
* Common shapes:
|
|
134
|
+
*
|
|
135
|
+
* ```ts
|
|
136
|
+
* // Server-Sent Events
|
|
137
|
+
* return stream(async function* () {
|
|
138
|
+
* for await (const evt of source) yield `data: ${JSON.stringify(evt)}\n\n`
|
|
139
|
+
* }, { type: 'sse' })
|
|
140
|
+
*
|
|
141
|
+
* // Chunked JSON (NDJSON) — one JSON object per line
|
|
142
|
+
* return stream(async function* () {
|
|
143
|
+
* for await (const row of rows) yield `${JSON.stringify(row)}\n`
|
|
144
|
+
* }, { type: 'ndjson' })
|
|
145
|
+
*
|
|
146
|
+
* // Raw bytes — caller supplies a ReadableStream of Uint8Array chunks
|
|
147
|
+
* return stream(myReadable, { contentType: 'application/octet-stream' })
|
|
148
|
+
* ```
|
|
149
|
+
*
|
|
150
|
+
* The wrapper sets `Cache-Control: no-cache` and `Connection: keep-alive`
|
|
151
|
+
* for SSE — the two headers a sane proxy / browser pair won't ignore — and
|
|
152
|
+
* leaves backpressure / cancellation to the underlying stream.
|
|
153
|
+
*
|
|
154
|
+
* See stacksjs/stacks#1870 R-4.
|
|
155
|
+
*/
|
|
156
|
+
export declare interface StreamOptions {
|
|
157
|
+
type?: 'sse' | 'ndjson'
|
|
158
|
+
contentType?: string
|
|
159
|
+
headers?: HeadersInit
|
|
160
|
+
status?: number
|
|
90
161
|
}
|
|
91
162
|
export declare interface StacksRouterInstance {
|
|
92
163
|
bunRouter: Router
|
|
@@ -105,9 +176,31 @@ export declare interface StacksRouterInstance {
|
|
|
105
176
|
register: (routePath: string, options?: { prefix?: string, middleware?: string | string[] }) => Promise<StacksRouterInstance>
|
|
106
177
|
serve: (options?: ServerOptions) => Promise<Server<unknown>>
|
|
107
178
|
handleRequest: (req: Request) => Promise<Response>
|
|
179
|
+
getAllowedMethods: (pathname: string, domain?: string) => string[]
|
|
108
180
|
importRoutes: () => Promise<void>
|
|
109
181
|
loadDiscoveredRoutes: () => Promise<void>
|
|
110
182
|
}
|
|
111
183
|
declare type RouteHandlerFn = (_req: EnhancedRequest) => Response | Promise<Response>;
|
|
112
184
|
declare type StacksHandler = string | RouteHandlerFn;
|
|
113
185
|
declare type ResourceAction = 'index' | 'store' | 'show' | 'update' | 'destroy';
|
|
186
|
+
/**
|
|
187
|
+
* FIFO-bounded Map. Wraps `Map` with a hard size cap; on overflow,
|
|
188
|
+
* the oldest entry (Map insertion order) is evicted. Used for the
|
|
189
|
+
* router's small framework-internal caches whose size is normally
|
|
190
|
+
* bounded by action count, but which had no upper limit before —
|
|
191
|
+
* tests that instantiate many short-lived routers would leak entries
|
|
192
|
+
* across `createStacksRouter()` calls (stacksjs/stacks#1863 T-8).
|
|
193
|
+
*
|
|
194
|
+
* Insertion-order LRU is appropriate here because the access pattern
|
|
195
|
+
* is "set once at action-load time, then many reads" — refreshing on
|
|
196
|
+
* get would buy nothing since reads dominate.
|
|
197
|
+
*/
|
|
198
|
+
declare class BoundedMap<K, V> {
|
|
199
|
+
constructor(max: number);
|
|
200
|
+
get(key: K): V | undefined;
|
|
201
|
+
has(key: K): boolean;
|
|
202
|
+
set(key: K, value: V): this;
|
|
203
|
+
delete(key: K): boolean;
|
|
204
|
+
clear(): void;
|
|
205
|
+
get size(): number;
|
|
206
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/router",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.70.
|
|
4
|
+
"version": "0.70.53",
|
|
5
5
|
"description": "The Stacks framework router.",
|
|
6
6
|
"author": "Chris Breuer",
|
|
7
7
|
"contributors": [
|
|
@@ -49,18 +49,18 @@
|
|
|
49
49
|
"prepublishOnly": "bun run build"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@stacksjs/bun-router": "
|
|
52
|
+
"@stacksjs/bun-router": "0.0.17"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@stacksjs/actions": "
|
|
56
|
-
"@stacksjs/config": "
|
|
55
|
+
"@stacksjs/actions": "0.70.53",
|
|
56
|
+
"@stacksjs/config": "0.70.53",
|
|
57
57
|
"better-dx": "^0.2.12",
|
|
58
|
-
"@stacksjs/error-handling": "
|
|
59
|
-
"@stacksjs/logging": "
|
|
60
|
-
"@stacksjs/orm": "
|
|
61
|
-
"@stacksjs/path": "
|
|
62
|
-
"@stacksjs/storage": "
|
|
63
|
-
"@stacksjs/types": "
|
|
64
|
-
"@stacksjs/validation": "
|
|
58
|
+
"@stacksjs/error-handling": "0.70.53",
|
|
59
|
+
"@stacksjs/logging": "0.70.53",
|
|
60
|
+
"@stacksjs/orm": "0.70.53",
|
|
61
|
+
"@stacksjs/path": "0.70.53",
|
|
62
|
+
"@stacksjs/storage": "0.70.53",
|
|
63
|
+
"@stacksjs/types": "0.70.53",
|
|
64
|
+
"@stacksjs/validation": "0.70.53"
|
|
65
65
|
}
|
|
66
66
|
}
|