@stacksjs/router 0.70.88 → 0.70.91
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/action-paths.d.ts +7 -0
- package/dist/action-paths.js +0 -0
- package/dist/api-shape.d.ts +24 -0
- package/dist/api-shape.js +11 -0
- package/dist/encrypted-session-store.d.ts +19 -0
- package/dist/encrypted-session-store.js +73 -0
- package/dist/error-handler.d.ts +68 -0
- package/dist/error-handler.js +310 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +37 -0
- package/dist/middleware.d.ts +37 -0
- package/dist/middleware.js +23 -0
- package/dist/path-sanitize.d.ts +64 -0
- package/dist/path-sanitize.js +37 -0
- package/dist/rate-limit.d.ts +35 -0
- package/dist/rate-limit.js +73 -0
- package/dist/request-augmentation.d.ts +71 -0
- package/dist/request-augmentation.js +0 -0
- package/dist/request-context.d.ts +82 -0
- package/dist/request-context.js +78 -0
- package/dist/response.d.ts +31 -0
- package/dist/response.js +1 -0
- package/dist/route-loader.d.ts +5 -0
- package/dist/route-loader.js +72 -0
- package/dist/route-types.d.ts +12 -0
- package/dist/route-types.js +0 -0
- package/dist/security-headers.d.ts +20 -0
- package/dist/security-headers.js +46 -0
- package/dist/session-factory.d.ts +57 -0
- package/dist/session-factory.js +26 -0
- package/dist/signed-url.d.ts +50 -0
- package/dist/signed-url.js +76 -0
- package/dist/stacks-router.d.ts +213 -0
- package/dist/stacks-router.js +1425 -0
- package/package.json +11 -11
|
@@ -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,26 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { createSessionStore } from "@stacksjs/bun-router";
|
|
3
|
+
import { EncryptedSessionStore } from "./encrypted-session-store";
|
|
4
|
+
export function createStacksSessionStore(config) {
|
|
5
|
+
const base = createSessionStore(config);
|
|
6
|
+
if (!resolveEncryptionMode(config.encrypt))
|
|
7
|
+
return base;
|
|
8
|
+
const appKey = config.appKey ?? process.env.APP_KEY;
|
|
9
|
+
if (!appKey || appKey.length < 16)
|
|
10
|
+
throw Error("[session] createStacksSessionStore: encryption requested but APP_KEY is missing or too short " + "(need \u226516 chars). Either set APP_KEY in env, pass `appKey` in config, or set `encrypt: false` to opt out.");
|
|
11
|
+
return new EncryptedSessionStore(base, { appKey });
|
|
12
|
+
}
|
|
13
|
+
function resolveEncryptionMode(mode) {
|
|
14
|
+
if (mode === !0)
|
|
15
|
+
return !0;
|
|
16
|
+
if (mode === !1)
|
|
17
|
+
return !1;
|
|
18
|
+
return (process.env.APP_ENV ?? process.env.NODE_ENV ?? "").toLowerCase() === "production";
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
DatabaseSessionStore,
|
|
22
|
+
FileSessionStore,
|
|
23
|
+
MemorySessionStore,
|
|
24
|
+
RedisSessionStore,
|
|
25
|
+
createSessionStore
|
|
26
|
+
} 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' }
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { url as buildUrl } from "./stacks-router";
|
|
5
|
+
const SIGNATURE_PARAM = "signature", EXPIRES_PARAM = "expires";
|
|
6
|
+
function getSigningSecret() {
|
|
7
|
+
const secret = process.env.APP_KEY || process.env.STACKS_SIGNED_URL_SECRET;
|
|
8
|
+
if (!secret || secret.length < 16)
|
|
9
|
+
throw Error("[router] signed URLs require APP_KEY (\u2265 16 chars) or STACKS_SIGNED_URL_SECRET. " + "Run `./buddy key:generate` or set the env var.");
|
|
10
|
+
return secret;
|
|
11
|
+
}
|
|
12
|
+
function hmacHex(payload, secret) {
|
|
13
|
+
return new Bun.CryptoHasher("sha256", secret).update(payload).digest("hex");
|
|
14
|
+
}
|
|
15
|
+
function safeEqualHex(a, b) {
|
|
16
|
+
const ba = Buffer.from(a.toLowerCase(), "hex"), bb = Buffer.from(b.toLowerCase(), "hex");
|
|
17
|
+
if (ba.length !== bb.length)
|
|
18
|
+
return !1;
|
|
19
|
+
return timingSafeEqual(ba, bb);
|
|
20
|
+
}
|
|
21
|
+
function stripSignatureParam(input) {
|
|
22
|
+
const u = new URL(input.toString());
|
|
23
|
+
u.searchParams.delete(SIGNATURE_PARAM);
|
|
24
|
+
return u;
|
|
25
|
+
}
|
|
26
|
+
function buildSignaturePayload(input) {
|
|
27
|
+
const stripped = stripSignatureParam(input);
|
|
28
|
+
stripped.searchParams.sort();
|
|
29
|
+
return stripped.toString();
|
|
30
|
+
}
|
|
31
|
+
export function signUrl(input, options = {}) {
|
|
32
|
+
const secret = getSigningSecret(), url = input.startsWith("http") ? new URL(input) : new URL(input.startsWith("/") ? input : `/${input}`, process.env.APP_URL || "https://localhost");
|
|
33
|
+
if (options.ttl !== void 0 && options.expiresAt !== void 0)
|
|
34
|
+
warnOnce("dual-expiry", "[router] signUrl: both `ttl` and `expiresAt` provided \u2014 using `ttl`.");
|
|
35
|
+
if (options.ttl !== void 0)
|
|
36
|
+
url.searchParams.set(EXPIRES_PARAM, String(Math.floor(Date.now() / 1000) + options.ttl));
|
|
37
|
+
else if (options.expiresAt !== void 0)
|
|
38
|
+
url.searchParams.set(EXPIRES_PARAM, String(Math.floor(options.expiresAt)));
|
|
39
|
+
const payload = buildSignaturePayload(url);
|
|
40
|
+
url.searchParams.set(SIGNATURE_PARAM, hmacHex(payload, secret));
|
|
41
|
+
return url.toString();
|
|
42
|
+
}
|
|
43
|
+
const _warnedKeys = new Set;
|
|
44
|
+
function warnOnce(key, message) {
|
|
45
|
+
if (_warnedKeys.has(key))
|
|
46
|
+
return;
|
|
47
|
+
_warnedKeys.add(key);
|
|
48
|
+
console.warn(message);
|
|
49
|
+
}
|
|
50
|
+
export function signedUrl(routeName, params = {}, options = {}) {
|
|
51
|
+
return signUrl(buildUrl(routeName, params), options);
|
|
52
|
+
}
|
|
53
|
+
export function verifySignedUrl(input) {
|
|
54
|
+
const url = typeof input === "string" ? new URL(input, process.env.APP_URL || "https://localhost") : input, presented = url.searchParams.get(SIGNATURE_PARAM);
|
|
55
|
+
if (!presented)
|
|
56
|
+
return { valid: !1, reason: "missing-signature" };
|
|
57
|
+
const expiresRaw = url.searchParams.get(EXPIRES_PARAM);
|
|
58
|
+
if (expiresRaw !== null) {
|
|
59
|
+
const expires = Number.parseInt(expiresRaw, 10);
|
|
60
|
+
if (!Number.isFinite(expires) || Date.now() / 1000 > expires)
|
|
61
|
+
return { valid: !1, reason: "expired" };
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const expected = hmacHex(buildSignaturePayload(url), getSigningSecret());
|
|
65
|
+
return safeEqualHex(presented, expected) ? { valid: !0 } : { valid: !1, reason: "invalid-signature" };
|
|
66
|
+
} catch {
|
|
67
|
+
return { valid: !1, reason: "invalid-signature" };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export async function verifySignedUrlMiddleware(req) {
|
|
71
|
+
const result = verifySignedUrl(req.url);
|
|
72
|
+
if (result.valid)
|
|
73
|
+
return;
|
|
74
|
+
const status = result.reason === "expired" ? 410 : 401;
|
|
75
|
+
throw Response.json({ error: result.reason }, { status });
|
|
76
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { Server } from 'bun';
|
|
2
|
+
import './request-augmentation';
|
|
3
|
+
import { Router } from '@stacksjs/bun-router';
|
|
4
|
+
import type { ActionHandler, EnhancedRequest, Route, ServerOptions } from '@stacksjs/bun-router';
|
|
5
|
+
import type { ActionValidations, ValidationResult } from '@stacksjs/actions';
|
|
6
|
+
/**
|
|
7
|
+
* Warn (once per process) when more than one @stacksjs/router module has loaded
|
|
8
|
+
* (stacksjs/stacks#1975 / #1982). Routing still works — the route table and
|
|
9
|
+
* request context are process-global singletons — but a duplicated install is
|
|
10
|
+
* worth surfacing. Called at serve() boot. Returns whether a split was detected
|
|
11
|
+
* so callers/tests can assert on it without capturing logs.
|
|
12
|
+
*/
|
|
13
|
+
export declare function warnOnMultipleRouterInstances(): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Generate a full URL for a named route, like Laravel's route() helper.
|
|
16
|
+
*
|
|
17
|
+
* Validates path parameters at call time so a typo'd argument
|
|
18
|
+
* (`url('user.post', { userId: 1 })` against `/users/{id}`) throws
|
|
19
|
+
* immediately with a list of expected names instead of silently
|
|
20
|
+
* producing a URL with `{id}` left literal in the path.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* // Define a named route
|
|
25
|
+
* route.get('/api/email/unsubscribe', 'Actions/UnsubscribeAction').name('email.unsubscribe')
|
|
26
|
+
*
|
|
27
|
+
* // Generate URL
|
|
28
|
+
* url('email.unsubscribe', { token: 'abc-123' })
|
|
29
|
+
* // → https://stacksjs.com/api/email/unsubscribe?token=abc-123
|
|
30
|
+
*
|
|
31
|
+
* // With path parameters
|
|
32
|
+
* route.get('/users/{id}/posts/{postId}', handler).name('user.post')
|
|
33
|
+
* url('user.post', { id: 42, postId: 7 })
|
|
34
|
+
* // → https://stacksjs.com/users/42/posts/7
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function url(routeName: string, params?: Record<string, string | number>): string;
|
|
38
|
+
/**
|
|
39
|
+
* List the placeholder names a named route expects — handy for
|
|
40
|
+
* codegen/test cases and for detecting typos before runtime.
|
|
41
|
+
*/
|
|
42
|
+
export declare function routeParams(routeName: string): string[];
|
|
43
|
+
/**
|
|
44
|
+
* Snapshot of the registered routes — `{ method, path, name? }` per
|
|
45
|
+
* route. Used by `buddy route:list` and the dev-server startup banner.
|
|
46
|
+
*/
|
|
47
|
+
export declare function listRegisteredRoutes(): Array<{ method: string, path: string, name?: string }>;
|
|
48
|
+
/**
|
|
49
|
+
* Clear the middleware cache (useful for hot-reload in development).
|
|
50
|
+
*
|
|
51
|
+
* `installMiddlewareHotReload()` will wire this up automatically when
|
|
52
|
+
* called from the dev server — production should never invoke it.
|
|
53
|
+
*/
|
|
54
|
+
export declare function clearMiddlewareCache(): void;
|
|
55
|
+
/**
|
|
56
|
+
* Watch `app/Middleware/` and `app/Middleware.ts` and invalidate the
|
|
57
|
+
* cached middleware modules whenever a file changes. Intended for the
|
|
58
|
+
* dev server only — calling this in production is a no-op (the
|
|
59
|
+
* watcher handle is created but never fires anything user code cares
|
|
60
|
+
* about). Returns a `disposer()` to stop watching.
|
|
61
|
+
*
|
|
62
|
+
* Without this hook, editing a middleware file in dev requires a
|
|
63
|
+
* full server restart to see the change — the import map caches the
|
|
64
|
+
* old version forever.
|
|
65
|
+
*/
|
|
66
|
+
export declare function installMiddlewareHotReload(): () => void;
|
|
67
|
+
/**
|
|
68
|
+
* Resolve every middleware alias referenced by a registered route and
|
|
69
|
+
* report the ones that don't load. `csrf` is always checked too — it's
|
|
70
|
+
* auto-injected on unsafe methods even when no route lists it.
|
|
71
|
+
*
|
|
72
|
+
* Resolution is inherently lazy (`.middleware(name)` is a sync chainable
|
|
73
|
+
* that just records a string; the alias map and middleware modules load
|
|
74
|
+
* via async dynamic import), so a throw at literal registration time is
|
|
75
|
+
* impossible. Calling this after all routes are registered — the end of
|
|
76
|
+
* `importRoutes()` and the compiled-binary boot in core/server — IS
|
|
77
|
+
* effectively registration-time validation. See stacksjs/stacks#1957.
|
|
78
|
+
*/
|
|
79
|
+
export declare function findUnresolvableRouteMiddleware(): Promise<Array<{ alias: string, routes: string[] }>>;
|
|
80
|
+
/**
|
|
81
|
+
* Throw when any registered route references a middleware alias that
|
|
82
|
+
* cannot be resolved. Fail-closed boot validation: a typo'd `auth` alias
|
|
83
|
+
* must abort startup loudly, not serve the route unprotected (the
|
|
84
|
+
* request-time guard in createMiddlewareHandler 500s as a backstop).
|
|
85
|
+
*/
|
|
86
|
+
export declare function assertRouteMiddlewareResolvable(): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Run an action's declarative `validations:` against the request.
|
|
89
|
+
*
|
|
90
|
+
* @internal Exported for regression coverage of path-param coercion
|
|
91
|
+
* (stacksjs/stacks#1865). Production callers should rely on the
|
|
92
|
+
* router's action-resolution path, which invokes this for you.
|
|
93
|
+
*/
|
|
94
|
+
export declare function validateActionInput(req: EnhancedRequest, validations: ActionValidations): Promise<ValidationResult>;
|
|
95
|
+
export declare function stream(source: ReadableStream | AsyncIterable<string | Uint8Array>, options?: StreamOptions): Response;
|
|
96
|
+
// Decorate the incoming request with the helpers the framework's middleware
|
|
97
|
+
// and actions assume are always available. Names follow Laravel's convention
|
|
98
|
+
// because that's the API surface Stacks userland expects.
|
|
99
|
+
export declare function enhanceRequest(req: EnhancedRequest): EnhancedRequest;
|
|
100
|
+
/**
|
|
101
|
+
* Create a Stacks-enhanced router
|
|
102
|
+
*/
|
|
103
|
+
export declare function createStacksRouter(config?: StacksRouterConfig): StacksRouterInstance;
|
|
104
|
+
/**
|
|
105
|
+
* Handle a server request through the router
|
|
106
|
+
* This is the main entry point for the Stacks server
|
|
107
|
+
*/
|
|
108
|
+
export declare function serverResponse(request: Request, _body?: string): Promise<Response>;
|
|
109
|
+
// Export serve function that uses the default router
|
|
110
|
+
export declare function serve(options?: ServerOptions): Promise<Server<unknown>>;
|
|
111
|
+
export declare const route: StacksRouterInstance;
|
|
112
|
+
declare interface StacksRouterConfig {
|
|
113
|
+
verbose?: boolean
|
|
114
|
+
apiPrefix?: string
|
|
115
|
+
}
|
|
116
|
+
declare interface GroupOptions {
|
|
117
|
+
prefix?: string
|
|
118
|
+
middleware?: string | string[]
|
|
119
|
+
apiResponse?: boolean
|
|
120
|
+
}
|
|
121
|
+
declare interface ResourceRouteOptions {
|
|
122
|
+
only?: ResourceAction[]
|
|
123
|
+
except?: ResourceAction[]
|
|
124
|
+
middleware?: string | string[]
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Chainable route interface for middleware and naming support
|
|
128
|
+
*/
|
|
129
|
+
declare interface ChainableRoute {
|
|
130
|
+
middleware: (name: string) => ChainableRoute
|
|
131
|
+
name: (routeName: string) => ChainableRoute
|
|
132
|
+
skipCsrf: () => ChainableRoute
|
|
133
|
+
requireCsrf: () => ChainableRoute
|
|
134
|
+
rateLimit: (max: number, window: 'second' | 'minute' | 'hour' | 'day' | number) => ChainableRoute
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Helper for streaming responses — wraps a `ReadableStream` or async
|
|
138
|
+
* generator with the right headers for the chosen content type.
|
|
139
|
+
*
|
|
140
|
+
* Common shapes:
|
|
141
|
+
*
|
|
142
|
+
* ```ts
|
|
143
|
+
* // Server-Sent Events
|
|
144
|
+
* return stream(async function* () {
|
|
145
|
+
* for await (const evt of source) yield `data: ${JSON.stringify(evt)}\n\n`
|
|
146
|
+
* }, { type: 'sse' })
|
|
147
|
+
*
|
|
148
|
+
* // Chunked JSON (NDJSON) — one JSON object per line
|
|
149
|
+
* return stream(async function* () {
|
|
150
|
+
* for await (const row of rows) yield `${JSON.stringify(row)}\n`
|
|
151
|
+
* }, { type: 'ndjson' })
|
|
152
|
+
*
|
|
153
|
+
* // Raw bytes — caller supplies a ReadableStream of Uint8Array chunks
|
|
154
|
+
* return stream(myReadable, { contentType: 'application/octet-stream' })
|
|
155
|
+
* ```
|
|
156
|
+
*
|
|
157
|
+
* The wrapper sets `Cache-Control: no-cache` and `Connection: keep-alive`
|
|
158
|
+
* for SSE — the two headers a sane proxy / browser pair won't ignore — and
|
|
159
|
+
* leaves backpressure / cancellation to the underlying stream.
|
|
160
|
+
*
|
|
161
|
+
* See stacksjs/stacks#1870 R-4.
|
|
162
|
+
*/
|
|
163
|
+
export declare interface StreamOptions {
|
|
164
|
+
type?: 'sse' | 'ndjson'
|
|
165
|
+
contentType?: string
|
|
166
|
+
headers?: HeadersInit
|
|
167
|
+
status?: number
|
|
168
|
+
}
|
|
169
|
+
export declare interface StacksRouterInstance {
|
|
170
|
+
bunRouter: Router
|
|
171
|
+
routes: Route[]
|
|
172
|
+
get: (path: string, handler: StacksHandler) => ChainableRoute
|
|
173
|
+
post: (path: string, handler: StacksHandler) => ChainableRoute
|
|
174
|
+
put: (path: string, handler: StacksHandler) => ChainableRoute
|
|
175
|
+
patch: (path: string, handler: StacksHandler) => ChainableRoute
|
|
176
|
+
delete: (path: string, handler: StacksHandler) => ChainableRoute
|
|
177
|
+
options: (path: string, handler: StacksHandler) => ChainableRoute
|
|
178
|
+
group: (options: GroupOptions, callback: () => void | Promise<void>) => StacksRouterInstance | Promise<StacksRouterInstance>
|
|
179
|
+
resource: (name: string, handler: string, options?: ResourceRouteOptions) => StacksRouterInstance
|
|
180
|
+
match: (methods: string[], path: string, handler: StacksHandler) => ChainableRoute
|
|
181
|
+
health: () => StacksRouterInstance
|
|
182
|
+
use: (middleware: ActionHandler | ((req: EnhancedRequest, next: () => Promise<Response>) => Response | Promise<Response>)) => StacksRouterInstance
|
|
183
|
+
register: (routePath: string, options?: { prefix?: string, middleware?: string | string[] }) => Promise<StacksRouterInstance>
|
|
184
|
+
serve: (options?: ServerOptions) => Promise<Server<unknown>>
|
|
185
|
+
handleRequest: (req: Request) => Promise<Response>
|
|
186
|
+
getAllowedMethods: (pathname: string, domain?: string) => string[]
|
|
187
|
+
importRoutes: () => Promise<void>
|
|
188
|
+
loadDiscoveredRoutes: () => Promise<void>
|
|
189
|
+
}
|
|
190
|
+
declare type RouteHandlerFn = (_req: EnhancedRequest) => Response | Promise<Response>;
|
|
191
|
+
declare type StacksHandler = string | RouteHandlerFn;
|
|
192
|
+
declare type ResourceAction = 'index' | 'store' | 'show' | 'update' | 'destroy';
|
|
193
|
+
/**
|
|
194
|
+
* FIFO-bounded Map. Wraps `Map` with a hard size cap; on overflow,
|
|
195
|
+
* the oldest entry (Map insertion order) is evicted. Used for the
|
|
196
|
+
* router's small framework-internal caches whose size is normally
|
|
197
|
+
* bounded by action count, but which had no upper limit before —
|
|
198
|
+
* tests that instantiate many short-lived routers would leak entries
|
|
199
|
+
* across `createStacksRouter()` calls (stacksjs/stacks#1863 T-8).
|
|
200
|
+
*
|
|
201
|
+
* Insertion-order LRU is appropriate here because the access pattern
|
|
202
|
+
* is "set once at action-load time, then many reads" — refreshing on
|
|
203
|
+
* get would buy nothing since reads dominate.
|
|
204
|
+
*/
|
|
205
|
+
declare class BoundedMap<K, V> {
|
|
206
|
+
constructor(max: number);
|
|
207
|
+
get(key: K): V | undefined;
|
|
208
|
+
has(key: K): boolean;
|
|
209
|
+
set(key: K, value: V): this;
|
|
210
|
+
delete(key: K): boolean;
|
|
211
|
+
clear(): void;
|
|
212
|
+
get size(): number;
|
|
213
|
+
}
|