@coderbuzz/ken 0.1.0
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 +41 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +41 -0
- package/dist/bun-GUK26ACN.js +281 -0
- package/dist/bun-GUK26ACN.js.map +1 -0
- package/dist/chunk-2BOPD5H7.js +34 -0
- package/dist/chunk-2BOPD5H7.js.map +1 -0
- package/dist/chunk-2MK26YDD.js +269 -0
- package/dist/chunk-2MK26YDD.js.map +1 -0
- package/dist/chunk-DPU3PBLP.js +815 -0
- package/dist/chunk-DPU3PBLP.js.map +1 -0
- package/dist/chunk-WTV4URUZ.js +122 -0
- package/dist/chunk-WTV4URUZ.js.map +1 -0
- package/dist/deno-LZU5JBGL.js +250 -0
- package/dist/deno-LZU5JBGL.js.map +1 -0
- package/dist/index.d.ts +2783 -0
- package/dist/index.js +2728 -0
- package/dist/index.js.map +1 -0
- package/dist/node-JLUTIPEN.js +816 -0
- package/dist/node-JLUTIPEN.js.map +1 -0
- package/dist/package.json +13 -0
- package/dist/uws-VNY2LPIZ.js +622 -0
- package/dist/uws-VNY2LPIZ.js.map +1 -0
- package/package.json +35 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2783 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ken Framework - Context Types
|
|
3
|
+
* Shared type definitions for all Context implementations
|
|
4
|
+
*
|
|
5
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Remote information including address and port.
|
|
9
|
+
*/
|
|
10
|
+
interface RemoteInfo {
|
|
11
|
+
address: string;
|
|
12
|
+
port: number;
|
|
13
|
+
}
|
|
14
|
+
type Validator = (val: any, ctx?: any) => any;
|
|
15
|
+
/**
|
|
16
|
+
* Flatten intersection types into a single object type for better IDE display.
|
|
17
|
+
* Converts A & B & C into a single flat object with all properties.
|
|
18
|
+
*/
|
|
19
|
+
type Flatten<T> = T extends infer U ? {
|
|
20
|
+
[K in keyof U]: U[K];
|
|
21
|
+
} : never;
|
|
22
|
+
/**
|
|
23
|
+
* Type-level utilities to extract param names from route path patterns.
|
|
24
|
+
*/
|
|
25
|
+
type ExtractParams<Path extends string> = Path extends `${infer Segment}/${infer Rest}` ? ExtractParams<Segment> & ExtractParams<Rest> : Path extends `:${infer Param}?` ? {
|
|
26
|
+
[K in Param]?: string;
|
|
27
|
+
} : Path extends `:${infer Param}` ? {
|
|
28
|
+
[K in Param]: string;
|
|
29
|
+
} : Path extends `*` ? {
|
|
30
|
+
"*": string;
|
|
31
|
+
} : {};
|
|
32
|
+
type ParamsFromPath<Path extends string> = Flatten<ExtractParams<Path>>;
|
|
33
|
+
/**
|
|
34
|
+
* Error handler function signature.
|
|
35
|
+
* Receives the thrown error and request context, returns a Response.
|
|
36
|
+
* Route-level onError takes priority over app-level.
|
|
37
|
+
*/
|
|
38
|
+
type ErrorHandler = (error: unknown, ctx: Context<any, any>) => Response | Promise<Response>;
|
|
39
|
+
/**
|
|
40
|
+
* Route Schema definition for validation and type inference.
|
|
41
|
+
* Flat architecture for best DX.
|
|
42
|
+
*/
|
|
43
|
+
interface Schema {
|
|
44
|
+
onError?: ErrorHandler;
|
|
45
|
+
state?: StateMiddleware;
|
|
46
|
+
params?: Record<string, Validator>;
|
|
47
|
+
query?: Record<string, Validator>;
|
|
48
|
+
headers?: Record<string, Validator>;
|
|
49
|
+
cookies?: Record<string, Validator>;
|
|
50
|
+
json?: Validator;
|
|
51
|
+
text?: Validator;
|
|
52
|
+
form?: Record<string, Validator>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Middleware handler function signature.
|
|
56
|
+
* Used for side-effect middleware (logger, CORS, etc.) that don't produce state values.
|
|
57
|
+
*/
|
|
58
|
+
type MiddlewareHandler = (ctx: Context<any, any>) => void | Response | Promise<void | Response>;
|
|
59
|
+
/**
|
|
60
|
+
* State Middleware definition and type inference.
|
|
61
|
+
* Middleware functions that can return values (auth, user data) or void (guards, loggers).
|
|
62
|
+
*/
|
|
63
|
+
type StateMiddleware = {
|
|
64
|
+
[key: string]: (ctx: Context<any, any>) => any;
|
|
65
|
+
};
|
|
66
|
+
type ContainsUndefined<T> = [T] extends [Exclude<T, undefined>] ? false : true;
|
|
67
|
+
type InferObject<T, Default> = T extends Record<string, Validator> ? Flatten<{
|
|
68
|
+
[K in keyof T as ContainsUndefined<ReturnType<T[K]>> extends true ? K : never]?: ReturnType<T[K]>;
|
|
69
|
+
} & {
|
|
70
|
+
[K in keyof T as ContainsUndefined<ReturnType<T[K]>> extends true ? never : K]: ReturnType<T[K]>;
|
|
71
|
+
}> : Default;
|
|
72
|
+
type InferValidator<T, Default> = T extends Validator ? ReturnType<T> : Default;
|
|
73
|
+
/**
|
|
74
|
+
* Context Interface - What route handlers see
|
|
75
|
+
* All runtime-specific Context classes must implement this
|
|
76
|
+
*
|
|
77
|
+
* @template S - Schema type for validation
|
|
78
|
+
* @template Path - Route path string for param extraction
|
|
79
|
+
* @template TState - Accumulated state from middleware
|
|
80
|
+
*/
|
|
81
|
+
interface Context<S extends Schema = {}, Path extends string = string, TState = {}> {
|
|
82
|
+
/**
|
|
83
|
+
* Request URL
|
|
84
|
+
*/
|
|
85
|
+
readonly url: string;
|
|
86
|
+
/**
|
|
87
|
+
* HTTP method
|
|
88
|
+
*/
|
|
89
|
+
readonly method: string;
|
|
90
|
+
/**
|
|
91
|
+
* Raw body stream (runtime-specific)
|
|
92
|
+
*/
|
|
93
|
+
readonly body: any;
|
|
94
|
+
/**
|
|
95
|
+
* State from executed middleware. Only includes middleware with non-void returns.
|
|
96
|
+
*/
|
|
97
|
+
state: Flatten<TState & (S extends {
|
|
98
|
+
state: infer M extends StateMiddleware;
|
|
99
|
+
} ? {
|
|
100
|
+
[K in keyof M as Awaited<ReturnType<M[K]>> extends void ? never : K]: Exclude<Awaited<ReturnType<M[K]>>, Response>;
|
|
101
|
+
} : {})>;
|
|
102
|
+
/**
|
|
103
|
+
* Remote information (IP address and port)
|
|
104
|
+
*/
|
|
105
|
+
readonly remoteInfo: RemoteInfo;
|
|
106
|
+
/**
|
|
107
|
+
* Route parameters (lazily parsed and validated)
|
|
108
|
+
*/
|
|
109
|
+
readonly params: InferObject<S['params'], ParamsFromPath<Path>>;
|
|
110
|
+
/**
|
|
111
|
+
* Query parameters (lazily parsed and validated)
|
|
112
|
+
*/
|
|
113
|
+
readonly query: InferObject<S['query'], Record<string, string>>;
|
|
114
|
+
/**
|
|
115
|
+
* Headers (lazily parsed and validated)
|
|
116
|
+
*/
|
|
117
|
+
readonly headers: InferObject<S['headers'], Record<string, string>>;
|
|
118
|
+
/**
|
|
119
|
+
* Cookies (lazily parsed and validated)
|
|
120
|
+
*/
|
|
121
|
+
readonly cookies: InferObject<S['cookies'], Record<string, string>>;
|
|
122
|
+
/**
|
|
123
|
+
* JSON body (lazily parsed and validated)
|
|
124
|
+
*/
|
|
125
|
+
readonly json: Promise<InferValidator<S['json'], any>>;
|
|
126
|
+
/**
|
|
127
|
+
* Text body (lazily parsed and validated)
|
|
128
|
+
*/
|
|
129
|
+
readonly text: Promise<InferValidator<S['text'], string>>;
|
|
130
|
+
/**
|
|
131
|
+
* Form body (lazily parsed and validated)
|
|
132
|
+
*/
|
|
133
|
+
readonly form: Promise<InferObject<S['form'], FormData>>;
|
|
134
|
+
/**
|
|
135
|
+
* Register callback to be called after handler finishes (success or failure).
|
|
136
|
+
* Useful for middleware that needs to perform cleanup or logging after response.
|
|
137
|
+
*/
|
|
138
|
+
onFinish(callback: (resp?: Response) => void): void;
|
|
139
|
+
/**
|
|
140
|
+
* Set a cookie to be sent with the response.
|
|
141
|
+
* Cookies are collected and applied to the response via onFinish callback.
|
|
142
|
+
*
|
|
143
|
+
* @param name - Cookie name
|
|
144
|
+
* @param value - Cookie value
|
|
145
|
+
* @param options - Cookie options (path, domain, maxAge, expires, httpOnly, secure, sameSite)
|
|
146
|
+
*/
|
|
147
|
+
setCookie(name: string, value: string, options?: {
|
|
148
|
+
path?: string;
|
|
149
|
+
domain?: string;
|
|
150
|
+
maxAge?: number;
|
|
151
|
+
expires?: Date;
|
|
152
|
+
httpOnly?: boolean;
|
|
153
|
+
secure?: boolean;
|
|
154
|
+
sameSite?: 'Strict' | 'Lax' | 'None';
|
|
155
|
+
}): void;
|
|
156
|
+
/**
|
|
157
|
+
* Internal: Execute all registered onFinish callbacks.
|
|
158
|
+
* Called by framework after handler completes.
|
|
159
|
+
*/
|
|
160
|
+
_executeFinishCallbacks(resp?: Response): void;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Ken Framework - Core Router
|
|
165
|
+
* High-performance radix tree router with type-safe route matching
|
|
166
|
+
*
|
|
167
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
168
|
+
*/
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Handler function type - receives Context and returns any value
|
|
172
|
+
* The return value is converted to Response by the runtime
|
|
173
|
+
*/
|
|
174
|
+
type Handler = (ctx: any) => any;
|
|
175
|
+
/**
|
|
176
|
+
* Raw route registration entry
|
|
177
|
+
* Stored during registration phase, compiled by runtime
|
|
178
|
+
*/
|
|
179
|
+
interface RouteRegistration {
|
|
180
|
+
method: string;
|
|
181
|
+
path: string;
|
|
182
|
+
schema?: Schema;
|
|
183
|
+
handler?: Handler;
|
|
184
|
+
staticValue?: unknown;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Route info returned by getRoutes()
|
|
188
|
+
*/
|
|
189
|
+
interface RouteInfo {
|
|
190
|
+
method: string;
|
|
191
|
+
path: string;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Match result from router
|
|
195
|
+
*/
|
|
196
|
+
interface MatchResult {
|
|
197
|
+
handler: (...args: any[]) => any;
|
|
198
|
+
schema?: Schema;
|
|
199
|
+
params: Record<string, string>;
|
|
200
|
+
response?: Response;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Router - Core routing implementation
|
|
204
|
+
*
|
|
205
|
+
* Stores raw route registrations during registration phase.
|
|
206
|
+
* Runtimes compile routes with their native executors.
|
|
207
|
+
*/
|
|
208
|
+
declare class Router {
|
|
209
|
+
private staticRoutes;
|
|
210
|
+
private dynamicRoot;
|
|
211
|
+
routes: RouteRegistration[];
|
|
212
|
+
/**
|
|
213
|
+
* Register a compiled route (called by runtime during compilation)
|
|
214
|
+
*/
|
|
215
|
+
registerCompiled(method: string, path: string, handler: (...args: any[]) => any, schema?: Schema, response?: Response): void;
|
|
216
|
+
private insertDynamic;
|
|
217
|
+
/**
|
|
218
|
+
* Create matcher function for route lookup
|
|
219
|
+
*/
|
|
220
|
+
matcher(): (method: string, pathname: string) => MatchResult | undefined;
|
|
221
|
+
/**
|
|
222
|
+
* Dynamic-only matcher (skips static routes)
|
|
223
|
+
* Used when native routing handles static routes
|
|
224
|
+
*/
|
|
225
|
+
dynamicOnlyMatcher(): (method: string, pathname: string) => MatchResult | undefined;
|
|
226
|
+
/**
|
|
227
|
+
* Get all registered routes as an array of { method, path } objects.
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```ts
|
|
231
|
+
* const routes = app.getRoutes();
|
|
232
|
+
* // [{ method: 'GET', path: '/' }, { method: 'POST', path: '/users' }, ...]
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
getRoutes(): RouteInfo[];
|
|
236
|
+
/**
|
|
237
|
+
* Clear all compiled routes (for recompilation)
|
|
238
|
+
*/
|
|
239
|
+
clear(): void;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Ken Framework - WebSocket Types
|
|
244
|
+
* Runtime-agnostic WebSocket abstractions
|
|
245
|
+
*
|
|
246
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
247
|
+
*/
|
|
248
|
+
/**
|
|
249
|
+
* WebSocket message data type
|
|
250
|
+
*/
|
|
251
|
+
type WsMessageData = string | ArrayBuffer | Uint8Array;
|
|
252
|
+
/**
|
|
253
|
+
* WebSocket ready states
|
|
254
|
+
*/
|
|
255
|
+
declare const WsReadyState: {
|
|
256
|
+
readonly CONNECTING: 0;
|
|
257
|
+
readonly OPEN: 1;
|
|
258
|
+
readonly CLOSING: 2;
|
|
259
|
+
readonly CLOSED: 3;
|
|
260
|
+
};
|
|
261
|
+
type WsReadyStateValue = (typeof WsReadyState)[keyof typeof WsReadyState];
|
|
262
|
+
/**
|
|
263
|
+
* WebSocket peer - represents a single connected client.
|
|
264
|
+
*
|
|
265
|
+
* Provides send/close/subscribe/unsubscribe/publish methods.
|
|
266
|
+
* The `data` field carries user-defined per-connection state
|
|
267
|
+
* set via the `upgrade` handler.
|
|
268
|
+
*
|
|
269
|
+
* @template T - User-defined per-connection data type
|
|
270
|
+
*/
|
|
271
|
+
interface WsPeer<T = unknown> {
|
|
272
|
+
/** User-defined per-connection data (set in upgrade handler) */
|
|
273
|
+
readonly data: T;
|
|
274
|
+
/** Remote address of the connected client */
|
|
275
|
+
readonly remoteAddress: string;
|
|
276
|
+
/** Current ready state of the connection */
|
|
277
|
+
readonly readyState: WsReadyStateValue;
|
|
278
|
+
/**
|
|
279
|
+
* Send a message to this peer.
|
|
280
|
+
* @param data - Message to send (string, ArrayBuffer, or Uint8Array)
|
|
281
|
+
* @param compress - Whether to compress this message (default: false)
|
|
282
|
+
* @returns Number of bytes sent, or -1 if buffered/failed
|
|
283
|
+
*/
|
|
284
|
+
send(data: WsMessageData, compress?: boolean): number;
|
|
285
|
+
/**
|
|
286
|
+
* Close the connection.
|
|
287
|
+
* @param code - Close status code (default: 1000)
|
|
288
|
+
* @param reason - Close reason string
|
|
289
|
+
*/
|
|
290
|
+
close(code?: number, reason?: string): void;
|
|
291
|
+
/**
|
|
292
|
+
* Subscribe this peer to a topic for pub/sub messaging.
|
|
293
|
+
* @param topic - Topic name to subscribe to
|
|
294
|
+
*/
|
|
295
|
+
subscribe(topic: string): void;
|
|
296
|
+
/**
|
|
297
|
+
* Unsubscribe this peer from a topic.
|
|
298
|
+
* @param topic - Topic name to unsubscribe from
|
|
299
|
+
*/
|
|
300
|
+
unsubscribe(topic: string): void;
|
|
301
|
+
/**
|
|
302
|
+
* Publish a message to all subscribers of a topic (excluding this peer).
|
|
303
|
+
* @param topic - Topic to publish to
|
|
304
|
+
* @param data - Message data to publish
|
|
305
|
+
* @param compress - Whether to compress this message (default: false)
|
|
306
|
+
*/
|
|
307
|
+
publish(topic: string, data: WsMessageData, compress?: boolean): void;
|
|
308
|
+
/**
|
|
309
|
+
* Check if this peer is subscribed to a topic.
|
|
310
|
+
* @param topic - Topic name to check
|
|
311
|
+
*/
|
|
312
|
+
isSubscribed(topic: string): boolean;
|
|
313
|
+
/**
|
|
314
|
+
* Send a ping frame to this peer.
|
|
315
|
+
* @param data - Optional ping payload
|
|
316
|
+
*/
|
|
317
|
+
ping(data?: WsMessageData): void;
|
|
318
|
+
/**
|
|
319
|
+
* Send a pong frame to this peer.
|
|
320
|
+
* @param data - Optional pong payload
|
|
321
|
+
*/
|
|
322
|
+
pong(data?: WsMessageData): void;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* WebSocket event handlers.
|
|
326
|
+
*
|
|
327
|
+
* @template T - User-defined per-connection data type
|
|
328
|
+
*
|
|
329
|
+
* @example
|
|
330
|
+
* ```ts
|
|
331
|
+
* app.ws<{ userId: string }>('/chat', {
|
|
332
|
+
* upgrade(req) {
|
|
333
|
+
* const token = req.headers.get('authorization');
|
|
334
|
+
* return { userId: verifyToken(token) };
|
|
335
|
+
* },
|
|
336
|
+
* open(peer) {
|
|
337
|
+
* peer.subscribe('chat');
|
|
338
|
+
* peer.publish('chat', `${peer.data.userId} joined`);
|
|
339
|
+
* },
|
|
340
|
+
* message(peer, message) {
|
|
341
|
+
* peer.publish('chat', `${peer.data.userId}: ${message}`);
|
|
342
|
+
* },
|
|
343
|
+
* close(peer, code, reason) {
|
|
344
|
+
* peer.publish('chat', `${peer.data.userId} left`);
|
|
345
|
+
* },
|
|
346
|
+
* });
|
|
347
|
+
* ```
|
|
348
|
+
*/
|
|
349
|
+
interface WsHandler<T = unknown> {
|
|
350
|
+
/**
|
|
351
|
+
* Called during HTTP→WebSocket upgrade.
|
|
352
|
+
* Return per-connection data, or a Response to reject the upgrade.
|
|
353
|
+
* If not provided, upgrades are accepted with `undefined` data.
|
|
354
|
+
*/
|
|
355
|
+
upgrade?: (req: Request) => T | Response | Promise<T | Response>;
|
|
356
|
+
/**
|
|
357
|
+
* Called when a connection is established.
|
|
358
|
+
*/
|
|
359
|
+
open?: (peer: WsPeer<T>) => void | Promise<void>;
|
|
360
|
+
/**
|
|
361
|
+
* Called when a message is received from the client.
|
|
362
|
+
*/
|
|
363
|
+
message: (peer: WsPeer<T>, message: WsMessageData) => void | Promise<void>;
|
|
364
|
+
/**
|
|
365
|
+
* Called when the connection is closed.
|
|
366
|
+
*/
|
|
367
|
+
close?: (peer: WsPeer<T>, code: number, reason: string) => void | Promise<void>;
|
|
368
|
+
/**
|
|
369
|
+
* Called when a ping frame is received.
|
|
370
|
+
*/
|
|
371
|
+
ping?: (peer: WsPeer<T>, data: WsMessageData) => void;
|
|
372
|
+
/**
|
|
373
|
+
* Called when a pong frame is received.
|
|
374
|
+
*/
|
|
375
|
+
pong?: (peer: WsPeer<T>, data: WsMessageData) => void;
|
|
376
|
+
/**
|
|
377
|
+
* Called when an error occurs on the connection.
|
|
378
|
+
*/
|
|
379
|
+
error?: (peer: WsPeer<T>, error: Error) => void;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* WebSocket configuration options.
|
|
383
|
+
*/
|
|
384
|
+
interface WsOptions {
|
|
385
|
+
/**
|
|
386
|
+
* Maximum message size in bytes.
|
|
387
|
+
* Messages exceeding this limit will close the connection.
|
|
388
|
+
* @default 16_777_216 (16 MB)
|
|
389
|
+
*/
|
|
390
|
+
maxPayloadLength?: number;
|
|
391
|
+
/**
|
|
392
|
+
* Maximum number of bytes that can be buffered for sending.
|
|
393
|
+
* @default 16_777_216 (16 MB)
|
|
394
|
+
*/
|
|
395
|
+
backpressureLimit?: number;
|
|
396
|
+
/**
|
|
397
|
+
* Interval in seconds between server-initiated ping frames.
|
|
398
|
+
* Set to 0 to disable automatic pings.
|
|
399
|
+
* @default 30
|
|
400
|
+
*/
|
|
401
|
+
pingInterval?: number;
|
|
402
|
+
/**
|
|
403
|
+
* Timeout in seconds to wait for a pong response before closing.
|
|
404
|
+
* @default 10
|
|
405
|
+
*/
|
|
406
|
+
pongTimeout?: number;
|
|
407
|
+
/**
|
|
408
|
+
* Whether to enable per-message compression.
|
|
409
|
+
* @default false
|
|
410
|
+
*/
|
|
411
|
+
perMessageDeflate?: boolean;
|
|
412
|
+
/**
|
|
413
|
+
* Idle timeout in seconds. Connections idle for this long are closed.
|
|
414
|
+
* Set to 0 to disable.
|
|
415
|
+
* @default 120
|
|
416
|
+
*/
|
|
417
|
+
idleTimeout?: number;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Internal WebSocket route registration
|
|
421
|
+
*/
|
|
422
|
+
interface WsRoute<T = unknown> {
|
|
423
|
+
path: string;
|
|
424
|
+
handler: WsHandler<T>;
|
|
425
|
+
options?: WsOptions;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Ken Framework - App
|
|
430
|
+
* Type-safe routing with schema validation and middleware support
|
|
431
|
+
*
|
|
432
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
433
|
+
*/
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Infer state shape from middleware definitions.
|
|
437
|
+
* Excludes middleware with void return types.
|
|
438
|
+
*/
|
|
439
|
+
type InferState<T extends StateMiddleware> = Flatten<{
|
|
440
|
+
[K in keyof T as Awaited<ReturnType<T[K]>> extends void ? never : K]: Exclude<Awaited<ReturnType<T[K]>>, Response>;
|
|
441
|
+
}>;
|
|
442
|
+
/**
|
|
443
|
+
* Typed handler function
|
|
444
|
+
*/
|
|
445
|
+
type TypedHandler<S extends Schema = {}, P extends string = string, TState = {}> = (ctx: Context<S, P, TState>) => any;
|
|
446
|
+
/**
|
|
447
|
+
* App - Type-safe router with schema validation
|
|
448
|
+
*
|
|
449
|
+
* Provides full type inference for:
|
|
450
|
+
* - Route parameters from path patterns
|
|
451
|
+
* - Query/headers/cookies/body from schema validators
|
|
452
|
+
* - State from middleware definitions
|
|
453
|
+
*
|
|
454
|
+
* @template TState - Accumulated middleware state type
|
|
455
|
+
*/
|
|
456
|
+
declare class App<TState extends StateMiddleware = {}> extends Router {
|
|
457
|
+
private middleware;
|
|
458
|
+
/** App-level error handler */
|
|
459
|
+
private _onError?;
|
|
460
|
+
/** Not-found handler for this app */
|
|
461
|
+
private _notFoundHandler?;
|
|
462
|
+
/** Accumulated not-found entries from child apps (via .use() and .define()) */
|
|
463
|
+
private _notFoundEntries;
|
|
464
|
+
/** WebSocket route registrations */
|
|
465
|
+
wsRoutes: WsRoute<any>[];
|
|
466
|
+
/**
|
|
467
|
+
* Set app-level error handler.
|
|
468
|
+
* Called when route handlers or middleware throw errors.
|
|
469
|
+
* Route-level onError (in schema) takes priority over app-level.
|
|
470
|
+
*
|
|
471
|
+
* @param handler - Error handler function
|
|
472
|
+
*
|
|
473
|
+
* @example
|
|
474
|
+
* ```ts
|
|
475
|
+
* app.onError((error, ctx) => {
|
|
476
|
+
* console.error(ctx.method, ctx.url, error);
|
|
477
|
+
* return Response.json(
|
|
478
|
+
* { status: 500, message: error instanceof Error ? error.message : 'Internal Server Error' },
|
|
479
|
+
* { status: 500 }
|
|
480
|
+
* );
|
|
481
|
+
* });
|
|
482
|
+
* ```
|
|
483
|
+
*/
|
|
484
|
+
onError(handler: ErrorHandler): void;
|
|
485
|
+
/**
|
|
486
|
+
* Set custom 404 Not Found handler.
|
|
487
|
+
* Called when no route matches the request.
|
|
488
|
+
* Supports nested configuration via .use() prefix scoping.
|
|
489
|
+
*
|
|
490
|
+
* @param handler - Handler function that receives Context
|
|
491
|
+
*
|
|
492
|
+
* @example
|
|
493
|
+
* ```ts
|
|
494
|
+
* // App-level
|
|
495
|
+
* app.notFound((ctx) => {
|
|
496
|
+
* return Response.json({ error: 'Not Found', path: ctx.url }, { status: 404 });
|
|
497
|
+
* });
|
|
498
|
+
*
|
|
499
|
+
* // Sub-app with prefix scoping
|
|
500
|
+
* const api = new App();
|
|
501
|
+
* api.notFound((ctx) => Response.json({ error: 'API Not Found' }, { status: 404 }));
|
|
502
|
+
* app.use('/api', api);
|
|
503
|
+
*
|
|
504
|
+
* // Define scope with middleware state
|
|
505
|
+
* app.define({ auth: (ctx) => verifyAuth(ctx) }, (app) => {
|
|
506
|
+
* app.notFound((ctx) => Response.json({ error: 'Protected', user: ctx.state.auth }, { status: 404 }));
|
|
507
|
+
* });
|
|
508
|
+
* ```
|
|
509
|
+
*/
|
|
510
|
+
notFound(handler: TypedHandler<{}, string, TState>): void;
|
|
511
|
+
/**
|
|
512
|
+
* Apply middleware to routes matching pattern.
|
|
513
|
+
* Supports both side-effect middleware (function) and state-producing middleware (object).
|
|
514
|
+
*
|
|
515
|
+
* @param pattern - Route pattern (default "/*" for all routes)
|
|
516
|
+
* @param stateOrHandler - State middleware object or handler function
|
|
517
|
+
*
|
|
518
|
+
* @example
|
|
519
|
+
* ```ts
|
|
520
|
+
* // Side-effect middleware (no state, just side effects)
|
|
521
|
+
* app.apply('/*', (ctx) => { console.log(ctx.method, ctx.url); });
|
|
522
|
+
*
|
|
523
|
+
* // State-producing middleware (adds to ctx.state)
|
|
524
|
+
* app.apply('/*', { auth: (ctx) => verifyAuth(ctx) });
|
|
525
|
+
* ```
|
|
526
|
+
*/
|
|
527
|
+
apply(pattern: string, handler: MiddlewareHandler): void;
|
|
528
|
+
apply(pattern: string, state: StateMiddleware): void;
|
|
529
|
+
/**
|
|
530
|
+
* Define middleware with lexical scoping and automatic type inference.
|
|
531
|
+
* Routes registered in the callback inherit middleware state types.
|
|
532
|
+
*
|
|
533
|
+
* @param state - State middleware object
|
|
534
|
+
* @param callback - Callback that receives scoped app with accumulated state types
|
|
535
|
+
*/
|
|
536
|
+
define<S extends StateMiddleware>(state: S, callback: (app: App<Flatten<TState & InferState<S>>>) => void): void;
|
|
537
|
+
/**
|
|
538
|
+
* Match middleware patterns against route path.
|
|
539
|
+
*/
|
|
540
|
+
matchMiddleware(path: string): StateMiddleware[];
|
|
541
|
+
/**
|
|
542
|
+
* Simple wildcard pattern matching.
|
|
543
|
+
*/
|
|
544
|
+
private matchPattern;
|
|
545
|
+
/**
|
|
546
|
+
* Merge multiple middleware states with route schema.
|
|
547
|
+
*/
|
|
548
|
+
mergeSchemas(middlewareStates: StateMiddleware[], routeSchema?: Schema): Schema | undefined;
|
|
549
|
+
/**
|
|
550
|
+
* Store raw route registration
|
|
551
|
+
*/
|
|
552
|
+
private storeRoute;
|
|
553
|
+
/**
|
|
554
|
+
* Internal route registration logic
|
|
555
|
+
*/
|
|
556
|
+
private register;
|
|
557
|
+
get<P extends string>(path: P, handler: TypedHandler<{}, P, TState>): void;
|
|
558
|
+
get<S extends Schema, P extends string>(path: P, schema: S, handler: TypedHandler<S, P, TState>): void;
|
|
559
|
+
get<P extends string, T = unknown>(path: P, staticValue: T): void;
|
|
560
|
+
get<S extends Schema, P extends string, T = unknown>(path: P, schema: S, staticValue: T): void;
|
|
561
|
+
post<P extends string>(path: P, handler: TypedHandler<{}, P, TState>): void;
|
|
562
|
+
post<S extends Schema, P extends string>(path: P, schema: S, handler: TypedHandler<S, P, TState>): void;
|
|
563
|
+
post<P extends string, T = unknown>(path: P, staticValue: T): void;
|
|
564
|
+
post<S extends Schema, P extends string, T = unknown>(path: P, schema: S, staticValue: T): void;
|
|
565
|
+
put<P extends string>(path: P, handler: TypedHandler<{}, P, TState>): void;
|
|
566
|
+
put<S extends Schema, P extends string>(path: P, schema: S, handler: TypedHandler<S, P, TState>): void;
|
|
567
|
+
put<P extends string, T = unknown>(path: P, staticValue: T): void;
|
|
568
|
+
put<S extends Schema, P extends string, T = unknown>(path: P, schema: S, staticValue: T): void;
|
|
569
|
+
patch<P extends string>(path: P, handler: TypedHandler<{}, P, TState>): void;
|
|
570
|
+
patch<S extends Schema, P extends string>(path: P, schema: S, handler: TypedHandler<S, P, TState>): void;
|
|
571
|
+
patch<P extends string, T = unknown>(path: P, staticValue: T): void;
|
|
572
|
+
patch<S extends Schema, P extends string, T = unknown>(path: P, schema: S, staticValue: T): void;
|
|
573
|
+
delete<P extends string>(path: P, handler: TypedHandler<{}, P, TState>): void;
|
|
574
|
+
delete<S extends Schema, P extends string>(path: P, schema: S, handler: TypedHandler<S, P, TState>): void;
|
|
575
|
+
delete<P extends string, T = unknown>(path: P, staticValue: T): void;
|
|
576
|
+
delete<S extends Schema, P extends string, T = unknown>(path: P, schema: S, staticValue: T): void;
|
|
577
|
+
head<P extends string>(path: P, handler: TypedHandler<{}, P, TState>): void;
|
|
578
|
+
head<S extends Schema, P extends string>(path: P, schema: S, handler: TypedHandler<S, P, TState>): void;
|
|
579
|
+
head<P extends string, T = unknown>(path: P, staticValue: T): void;
|
|
580
|
+
head<S extends Schema, P extends string, T = unknown>(path: P, schema: S, staticValue: T): void;
|
|
581
|
+
options<P extends string>(path: P, handler: TypedHandler<{}, P, TState>): void;
|
|
582
|
+
options<S extends Schema, P extends string>(path: P, schema: S, handler: TypedHandler<S, P, TState>): void;
|
|
583
|
+
options<P extends string, T = unknown>(path: P, staticValue: T): void;
|
|
584
|
+
options<S extends Schema, P extends string, T = unknown>(path: P, schema: S, staticValue: T): void;
|
|
585
|
+
/**
|
|
586
|
+
* Mount another App with optional prefix
|
|
587
|
+
*/
|
|
588
|
+
use(app: App): void;
|
|
589
|
+
use(prefix: string, app: App): void;
|
|
590
|
+
/**
|
|
591
|
+
* Register a WebSocket handler at the given path.
|
|
592
|
+
*
|
|
593
|
+
* The generic type parameter `T` defines per-connection data,
|
|
594
|
+
* which is set in the `upgrade` handler and accessible via `peer.data`.
|
|
595
|
+
*
|
|
596
|
+
* @template T - Per-connection data type
|
|
597
|
+
* @param path - URL path to handle WebSocket upgrades
|
|
598
|
+
* @param handler - WebSocket event handlers
|
|
599
|
+
*
|
|
600
|
+
* @example
|
|
601
|
+
* ```ts
|
|
602
|
+
* // Simple echo server
|
|
603
|
+
* app.ws('/ws', {
|
|
604
|
+
* message(peer, message) {
|
|
605
|
+
* peer.send(message);
|
|
606
|
+
* },
|
|
607
|
+
* });
|
|
608
|
+
*
|
|
609
|
+
* // Chat with authentication and pub/sub
|
|
610
|
+
* app.ws<{ userId: string }>('/chat', {
|
|
611
|
+
* upgrade(req) {
|
|
612
|
+
* const token = req.headers.get('authorization');
|
|
613
|
+
* return { userId: verifyToken(token) };
|
|
614
|
+
* },
|
|
615
|
+
* open(peer) {
|
|
616
|
+
* peer.subscribe('chat');
|
|
617
|
+
* peer.publish('chat', `${peer.data.userId} joined`);
|
|
618
|
+
* },
|
|
619
|
+
* message(peer, message) {
|
|
620
|
+
* peer.publish('chat', `${peer.data.userId}: ${message}`);
|
|
621
|
+
* },
|
|
622
|
+
* close(peer) {
|
|
623
|
+
* peer.publish('chat', `${peer.data.userId} left`);
|
|
624
|
+
* },
|
|
625
|
+
* });
|
|
626
|
+
*
|
|
627
|
+
* // With options
|
|
628
|
+
* app.ws('/live', {
|
|
629
|
+
* message(peer, message) { peer.send(message); },
|
|
630
|
+
* }, {
|
|
631
|
+
* pingInterval: 15,
|
|
632
|
+
* maxPayloadLength: 1024 * 1024,
|
|
633
|
+
* });
|
|
634
|
+
* ```
|
|
635
|
+
*/
|
|
636
|
+
ws<T = unknown>(path: string, handler: WsHandler<T>, options?: WsOptions): void;
|
|
637
|
+
/**
|
|
638
|
+
* Print all registered routes to the console with colored formatting.
|
|
639
|
+
*
|
|
640
|
+
* Each HTTP method is color-coded:
|
|
641
|
+
* - GET (green), POST (blue), PUT (yellow), PATCH (cyan)
|
|
642
|
+
* - DELETE (red), HEAD (magenta), OPTIONS (white)
|
|
643
|
+
*
|
|
644
|
+
* @example
|
|
645
|
+
* ```ts
|
|
646
|
+
* app.get('/', 'Hello!');
|
|
647
|
+
* app.post('/users', handler);
|
|
648
|
+
* app.get('/users/:id', handler);
|
|
649
|
+
* app.ws('/chat', wsHandler);
|
|
650
|
+
* app.printRoutes();
|
|
651
|
+
* // ┌──────────┬────────────────────┐
|
|
652
|
+
* // │ Method │ Path │
|
|
653
|
+
* // ├──────────┼────────────────────┤
|
|
654
|
+
* // │ GET │ / │
|
|
655
|
+
* // │ POST │ /users │
|
|
656
|
+
* // │ GET │ /users/:id │
|
|
657
|
+
* // │ WS │ /chat │
|
|
658
|
+
* // └──────────┴────────────────────┘
|
|
659
|
+
* ```
|
|
660
|
+
*/
|
|
661
|
+
printRoutes(): void;
|
|
662
|
+
/**
|
|
663
|
+
* Combine prefix with path
|
|
664
|
+
*/
|
|
665
|
+
private combinePath;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Ken Framework - AppServer
|
|
670
|
+
* Combines App routing with automatic runtime detection and server lifecycle.
|
|
671
|
+
*
|
|
672
|
+
* Usage:
|
|
673
|
+
* ```ts
|
|
674
|
+
* import { AppServer } from 'ken';
|
|
675
|
+
* const app = new AppServer({ port: 3000 });
|
|
676
|
+
* app.get('/hello', 'Hello, World!');
|
|
677
|
+
* await app.run();
|
|
678
|
+
* ```
|
|
679
|
+
*
|
|
680
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
681
|
+
*/
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* AppServer initialization options.
|
|
685
|
+
*/
|
|
686
|
+
interface AppServerInit {
|
|
687
|
+
port?: number;
|
|
688
|
+
hostname?: string;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* AppServer - App with built-in server lifecycle.
|
|
692
|
+
*
|
|
693
|
+
* Extends App with `run()` and `stop()` methods.
|
|
694
|
+
* Automatically detects the current runtime (Bun, Deno, Node.js)
|
|
695
|
+
* and lazily loads only the needed adapter.
|
|
696
|
+
*
|
|
697
|
+
* @template TState - Accumulated middleware state type
|
|
698
|
+
*/
|
|
699
|
+
declare class AppServer<TState extends StateMiddleware = {}> extends App<TState> {
|
|
700
|
+
/** Configured hostname */
|
|
701
|
+
hostname: string;
|
|
702
|
+
/** Configured port */
|
|
703
|
+
port: number;
|
|
704
|
+
private _server;
|
|
705
|
+
constructor(init?: AppServerInit);
|
|
706
|
+
/**
|
|
707
|
+
* Start the server with automatic runtime detection.
|
|
708
|
+
* Returns the resolved hostname and port.
|
|
709
|
+
*
|
|
710
|
+
* @example
|
|
711
|
+
* ```ts
|
|
712
|
+
* const app = new AppServer({ port: 3000 });
|
|
713
|
+
* app.get('/', 'Hello!');
|
|
714
|
+
* const { hostname, port } = await app.run();
|
|
715
|
+
* console.log(`Listening on ${hostname}:${port}`);
|
|
716
|
+
* ```
|
|
717
|
+
*/
|
|
718
|
+
run(): Promise<{
|
|
719
|
+
hostname: string;
|
|
720
|
+
port: number;
|
|
721
|
+
}>;
|
|
722
|
+
/**
|
|
723
|
+
* Stop the server gracefully.
|
|
724
|
+
*/
|
|
725
|
+
stop(): Promise<void>;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Ken Framework - Runtime Compiler
|
|
730
|
+
* Generic executor factory for all runtimes
|
|
731
|
+
*
|
|
732
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
733
|
+
*/
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Default error handler - returns standard JSON error response.
|
|
737
|
+
* Response errors are passed through directly.
|
|
738
|
+
*
|
|
739
|
+
* Error response format:
|
|
740
|
+
* ```json
|
|
741
|
+
* { "status": 500, "message": "Internal Server Error" }
|
|
742
|
+
* ```
|
|
743
|
+
*/
|
|
744
|
+
declare function defaultErrorHandler(err: unknown): Response;
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Ken Framework - Runtime Adapters
|
|
748
|
+
* Auto-detection with lazy loading for each runtime
|
|
749
|
+
*
|
|
750
|
+
* Detection order:
|
|
751
|
+
* 1. Deno → loads deno runtime
|
|
752
|
+
* 2. Bun → loads bun runtime
|
|
753
|
+
* 3. Node.js → checks UWS env / uWebSockets.js availability
|
|
754
|
+
* - UWS=1|true → use uWebSockets.js (error if not installed)
|
|
755
|
+
* - UWS=0|false → use node:http
|
|
756
|
+
* - Not set → try uWebSockets.js, fallback to node:http
|
|
757
|
+
*
|
|
758
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
759
|
+
*/
|
|
760
|
+
|
|
761
|
+
/** True when running on Deno */
|
|
762
|
+
declare const isDeno: boolean;
|
|
763
|
+
/** True when running on Bun */
|
|
764
|
+
declare const isBun: boolean;
|
|
765
|
+
/** True when running on Node.js (excludes Bun which also exposes process) */
|
|
766
|
+
declare const isNode: boolean;
|
|
767
|
+
/**
|
|
768
|
+
* Server instance returned by all runtime adapters.
|
|
769
|
+
*/
|
|
770
|
+
interface Server {
|
|
771
|
+
/** Start listening. Returns the resolved hostname and port. */
|
|
772
|
+
run(): Promise<{
|
|
773
|
+
hostname: string;
|
|
774
|
+
port: number;
|
|
775
|
+
}>;
|
|
776
|
+
/** Gracefully stop the server. */
|
|
777
|
+
stop(): void | Promise<void>;
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Options accepted by every server factory.
|
|
781
|
+
*/
|
|
782
|
+
interface ServerOptions {
|
|
783
|
+
port?: number;
|
|
784
|
+
hostname?: string;
|
|
785
|
+
router: Router;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Ken Framework - Pathname Utilities
|
|
790
|
+
* Fast pathname extraction from URLs
|
|
791
|
+
*
|
|
792
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
793
|
+
*/
|
|
794
|
+
/**
|
|
795
|
+
* Fast pathname extraction from URL string
|
|
796
|
+
* Optimized for hot path - avoids URL object creation
|
|
797
|
+
*/
|
|
798
|
+
declare function getPathname(url: string): string;
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Ken Framework - Encryption Utilities
|
|
802
|
+
* High-performance AES-GCM encryption with key caching
|
|
803
|
+
*
|
|
804
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
805
|
+
*/
|
|
806
|
+
/**
|
|
807
|
+
* Generate a random 256-bit secret key as a base64 string
|
|
808
|
+
*/
|
|
809
|
+
declare function generateSecretKey(): string;
|
|
810
|
+
/**
|
|
811
|
+
* Encrypt a string using AES-GCM.
|
|
812
|
+
* Returns base64-encoded: IV (12 bytes) + ciphertext.
|
|
813
|
+
* Uses cached CryptoKeys and manual base64 for maximum throughput.
|
|
814
|
+
*/
|
|
815
|
+
declare function encryptString(value: string, password: string): Promise<string>;
|
|
816
|
+
/**
|
|
817
|
+
* Decrypt a base64-encoded encrypted string using AES-GCM.
|
|
818
|
+
* Uses cached CryptoKeys, manual base64 decode, and subarray (zero-copy) for IV/ciphertext split.
|
|
819
|
+
*/
|
|
820
|
+
declare function decryptString(encrypted: string, password: string): Promise<string>;
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Ken Framework - Memoize Utility
|
|
824
|
+
* High-performance function memoization with configurable cache strategy
|
|
825
|
+
*
|
|
826
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
827
|
+
*/
|
|
828
|
+
/**
|
|
829
|
+
* Options for configuring memoize behavior.
|
|
830
|
+
*/
|
|
831
|
+
interface MemoizeOptions<Args extends unknown[]> {
|
|
832
|
+
/**
|
|
833
|
+
* Maximum number of cached entries. When exceeded, the oldest entry is evicted (LRU via insertion order).
|
|
834
|
+
* @default 256
|
|
835
|
+
*/
|
|
836
|
+
maxSize?: number;
|
|
837
|
+
/**
|
|
838
|
+
* Time-to-live in milliseconds. Entries older than this are considered stale.
|
|
839
|
+
* When set to 0 or omitted, entries never expire.
|
|
840
|
+
* @default 0
|
|
841
|
+
*/
|
|
842
|
+
ttl?: number;
|
|
843
|
+
/**
|
|
844
|
+
* Custom key resolver. Receives the same arguments as the memoized function
|
|
845
|
+
* and must return a string or number to use as the cache key.
|
|
846
|
+
* Defaults to using the first argument directly (fast path for single-arg functions).
|
|
847
|
+
*/
|
|
848
|
+
key?: (...args: Args) => string | number;
|
|
849
|
+
}
|
|
850
|
+
interface CacheEntry<T> {
|
|
851
|
+
value: T;
|
|
852
|
+
expiry: number;
|
|
853
|
+
}
|
|
854
|
+
/** Memoized function with exposed cache and clear method */
|
|
855
|
+
type MemoizedSync<Args extends unknown[], R> = ((...args: Args) => R) & {
|
|
856
|
+
cache: Map<string | number, CacheEntry<R>>;
|
|
857
|
+
clear: () => void;
|
|
858
|
+
};
|
|
859
|
+
/** Memoized async function with exposed cache, inflight map, and clear method */
|
|
860
|
+
type MemoizedAsync<Args extends unknown[], R> = ((...args: Args) => Promise<R>) & {
|
|
861
|
+
cache: Map<string | number, CacheEntry<R>>;
|
|
862
|
+
inflight: Map<string | number, Promise<R>>;
|
|
863
|
+
clear: () => void;
|
|
864
|
+
};
|
|
865
|
+
/**
|
|
866
|
+
* Create a memoized version of a function. Auto-detects async functions
|
|
867
|
+
* at creation time and selects the optimal strategy:
|
|
868
|
+
*
|
|
869
|
+
* - **Sync**: Map-based cache with LRU eviction and TTL expiry.
|
|
870
|
+
* - **Async**: Same cache plus in-flight deduplication — concurrent calls
|
|
871
|
+
* for the same key share a single Promise (thundering herd protection).
|
|
872
|
+
*
|
|
873
|
+
* Detection uses `fn.constructor.name === 'AsyncFunction'` (compile-time branch,
|
|
874
|
+
* zero per-call overhead).
|
|
875
|
+
*
|
|
876
|
+
* @example
|
|
877
|
+
* ```ts
|
|
878
|
+
* // Sync — auto-detected
|
|
879
|
+
* const expensive = memoize((id: string) => computeResult(id));
|
|
880
|
+
* expensive('abc'); // computes
|
|
881
|
+
* expensive('abc'); // cached
|
|
882
|
+
*
|
|
883
|
+
* // Async — auto-detected, with TTL and max size
|
|
884
|
+
* const fetchUser = memoize(
|
|
885
|
+
* async (id: string) => db.users.findById(id),
|
|
886
|
+
* { ttl: 30_000, maxSize: 500 }
|
|
887
|
+
* );
|
|
888
|
+
*
|
|
889
|
+
* // Multi-arg with custom key
|
|
890
|
+
* const multi = memoize(
|
|
891
|
+
* (a: number, b: number) => a + b,
|
|
892
|
+
* { key: (a, b) => `${a}:${b}` }
|
|
893
|
+
* );
|
|
894
|
+
* ```
|
|
895
|
+
*/
|
|
896
|
+
declare function memoize<Args extends unknown[], R>(fn: (...args: Args) => Promise<R>, options?: MemoizeOptions<Args>): MemoizedAsync<Args, R>;
|
|
897
|
+
declare function memoize<Args extends unknown[], R>(fn: (...args: Args) => R, options?: MemoizeOptions<Args>): MemoizedSync<Args, R>;
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Ken Framework - Compression Utilities
|
|
901
|
+
* High-performance string compression for cookies and JSON storage.
|
|
902
|
+
*
|
|
903
|
+
* Uses Web Standard CompressionStream/DecompressionStream API
|
|
904
|
+
* for cross-runtime compatibility (Bun, Node.js, Deno, Cloudflare Workers).
|
|
905
|
+
* Output is base64url-encoded (no +, /, or = characters) — safe for
|
|
906
|
+
* cookies, URLs, and JSON values without escaping.
|
|
907
|
+
*
|
|
908
|
+
* Default format is `brotli` — best compression ratio for cookie/session
|
|
909
|
+
* storage where every byte counts.
|
|
910
|
+
*
|
|
911
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
912
|
+
*/
|
|
913
|
+
/** Supported compression formats — extends the Web Standard with brotli (supported by Bun, Node.js, Deno). */
|
|
914
|
+
type SupportedCompressionFormat = CompressionFormat | 'brotli';
|
|
915
|
+
/**
|
|
916
|
+
* Options for {@link compressString}.
|
|
917
|
+
*
|
|
918
|
+
* The `level` range depends on the chosen `encoding`:
|
|
919
|
+
* - **`brotli`**: 0–11 (default: `11` — best compression, slowest)
|
|
920
|
+
* - **`deflate` / `gzip` / `deflate-raw`**: 0–9 (default: runtime default, typically `6`)
|
|
921
|
+
*
|
|
922
|
+
* Lower values = faster encoding + larger output.
|
|
923
|
+
* Higher values = slower encoding + smaller output.
|
|
924
|
+
*/
|
|
925
|
+
interface CompressOptions$1 {
|
|
926
|
+
/** Compression format. Default: `'brotli'`. */
|
|
927
|
+
encoding?: SupportedCompressionFormat;
|
|
928
|
+
/**
|
|
929
|
+
* Compression quality level.
|
|
930
|
+
* - brotli: 0–11 (default 11)
|
|
931
|
+
* - deflate / gzip / deflate-raw: 0–9 (default ~6)
|
|
932
|
+
*/
|
|
933
|
+
level?: number;
|
|
934
|
+
}
|
|
935
|
+
/** Options for {@link decompressString}. */
|
|
936
|
+
interface DecompressOptions {
|
|
937
|
+
/** Compression format used during compression. Default: `'brotli'`. */
|
|
938
|
+
encoding?: SupportedCompressionFormat;
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Compress a string and return a base64url-encoded result.
|
|
942
|
+
* Output is safe for cookies, URLs, and JSON values (no +, /, or = characters).
|
|
943
|
+
*
|
|
944
|
+
* Uses `brotli` by default for best compression ratio — produces smaller
|
|
945
|
+
* output than `deflate` or `gzip`, ideal for cookie/session storage.
|
|
946
|
+
*
|
|
947
|
+
* @param input - The string to compress
|
|
948
|
+
* @param options - Compression options: `encoding` (default `'brotli'`) and optional `level`
|
|
949
|
+
* @returns Base64url-encoded compressed string
|
|
950
|
+
*
|
|
951
|
+
* @example
|
|
952
|
+
* ```ts
|
|
953
|
+
* // Best compression (default brotli)
|
|
954
|
+
* const compressed = await compressString('Hello, World!');
|
|
955
|
+
*
|
|
956
|
+
* // Faster, lighter brotli
|
|
957
|
+
* const fast = await compressString('Hello, World!', { level: 4 });
|
|
958
|
+
*
|
|
959
|
+
* // gzip with max compression
|
|
960
|
+
* const gz = await compressString('Hello, World!', { encoding: 'gzip', level: 9 });
|
|
961
|
+
* ```
|
|
962
|
+
*/
|
|
963
|
+
declare function compressString(input: string, options?: CompressOptions$1): Promise<string>;
|
|
964
|
+
/**
|
|
965
|
+
* Decompress a base64url-encoded compressed string back to the original.
|
|
966
|
+
*
|
|
967
|
+
* @param compressed - Base64url-encoded compressed string from {@link compressString}
|
|
968
|
+
* @param options - Decompression options: `encoding` must match what was used to compress (default `'brotli'`)
|
|
969
|
+
* @returns The original decompressed string
|
|
970
|
+
*
|
|
971
|
+
* @example
|
|
972
|
+
* ```ts
|
|
973
|
+
* const original = await decompressString(compressed);
|
|
974
|
+
* // 'Hello, World!'
|
|
975
|
+
*
|
|
976
|
+
* const original = await decompressString(compressed, { encoding: 'gzip' });
|
|
977
|
+
* ```
|
|
978
|
+
*/
|
|
979
|
+
declare function decompressString(compressed: string, options?: DecompressOptions): Promise<string>;
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Ken Framework - File Utilities
|
|
983
|
+
* Cross-runtime file operations: serve, list, upload, save
|
|
984
|
+
*
|
|
985
|
+
* Optimized paths per runtime:
|
|
986
|
+
* - Bun: Uses Bun.file() for zero-copy sendfile responses
|
|
987
|
+
* - Node.js / Deno: Uses node:fs streams with backpressure-aware ReadableStream
|
|
988
|
+
*
|
|
989
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
990
|
+
*/
|
|
991
|
+
/**
|
|
992
|
+
* Get MIME type from a file path or extension.
|
|
993
|
+
* Falls back to `application/octet-stream` for unknown types.
|
|
994
|
+
*
|
|
995
|
+
* @example
|
|
996
|
+
* ```ts
|
|
997
|
+
* getMimeType('photo.jpg') // 'image/jpeg'
|
|
998
|
+
* getMimeType('.css') // 'text/css; charset=utf-8'
|
|
999
|
+
* ```
|
|
1000
|
+
*/
|
|
1001
|
+
declare function getMimeType(filePath: string): string;
|
|
1002
|
+
/**
|
|
1003
|
+
* Options for {@link sendFile}.
|
|
1004
|
+
*/
|
|
1005
|
+
interface SendFileOptions {
|
|
1006
|
+
/** Override Content-Type (auto-detected from extension by default) */
|
|
1007
|
+
contentType?: string;
|
|
1008
|
+
/** Trigger browser download. `true` uses original filename, string sets a custom filename. */
|
|
1009
|
+
download?: boolean | string;
|
|
1010
|
+
/** Cache-Control header value (e.g., `'public, max-age=3600'`) */
|
|
1011
|
+
cacheControl?: string;
|
|
1012
|
+
/** Additional response headers */
|
|
1013
|
+
headers?: Record<string, string>;
|
|
1014
|
+
/** HTTP status code override for the full-file response (default: 200) */
|
|
1015
|
+
status?: number;
|
|
1016
|
+
/**
|
|
1017
|
+
* Incoming request headers for conditional and range request support.
|
|
1018
|
+
* Pass `ctx.headers` or the raw `Headers` object.
|
|
1019
|
+
* Enables ETag / If-None-Match, Last-Modified / If-Modified-Since,
|
|
1020
|
+
* and Range (206 Partial Content) handling.
|
|
1021
|
+
*/
|
|
1022
|
+
reqHeaders?: Headers | Record<string, string>;
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Options for {@link listDirectory}.
|
|
1026
|
+
*/
|
|
1027
|
+
interface ListDirectoryOptions {
|
|
1028
|
+
/** Recurse into subdirectories (default: `false`) */
|
|
1029
|
+
recursive?: boolean;
|
|
1030
|
+
/** Maximum recursion depth when `recursive` is `true` (default: `10`) */
|
|
1031
|
+
maxDepth?: number;
|
|
1032
|
+
/** Include file stats — size & modifiedAt (default: `true`) */
|
|
1033
|
+
stats?: boolean;
|
|
1034
|
+
/** Filter entries. Return `true` to include, `false` to skip. */
|
|
1035
|
+
filter?: (entry: FileEntry) => boolean;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* A single file or directory entry returned by {@link listDirectory}.
|
|
1039
|
+
*/
|
|
1040
|
+
interface FileEntry {
|
|
1041
|
+
/** Filename (including extension) */
|
|
1042
|
+
name: string;
|
|
1043
|
+
/** Relative path from the listed directory root */
|
|
1044
|
+
path: string;
|
|
1045
|
+
/** `true` if the entry is a directory */
|
|
1046
|
+
isDirectory: boolean;
|
|
1047
|
+
/** Size in bytes (`0` for directories, `0` if stats disabled) */
|
|
1048
|
+
size: number;
|
|
1049
|
+
/** Last modified timestamp (epoch 0 if stats disabled) */
|
|
1050
|
+
modifiedAt: Date;
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Options for {@link receiveFiles}.
|
|
1054
|
+
*/
|
|
1055
|
+
interface ReceiveFileOptions {
|
|
1056
|
+
/** Maximum individual file size in bytes */
|
|
1057
|
+
maxFileSize?: number;
|
|
1058
|
+
/** Maximum number of files to accept */
|
|
1059
|
+
maxFiles?: number;
|
|
1060
|
+
/** Allowed MIME types (e.g., `['image/png', 'image/jpeg']`) */
|
|
1061
|
+
allowedTypes?: string[];
|
|
1062
|
+
/** Only extract files from these form field names */
|
|
1063
|
+
fields?: string[];
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* A file extracted by {@link receiveFiles}.
|
|
1067
|
+
*/
|
|
1068
|
+
interface UploadedFile {
|
|
1069
|
+
/** Form field name the file was submitted under */
|
|
1070
|
+
fieldName: string;
|
|
1071
|
+
/** Original filename from the client */
|
|
1072
|
+
fileName: string;
|
|
1073
|
+
/** MIME type */
|
|
1074
|
+
type: string;
|
|
1075
|
+
/** Size in bytes */
|
|
1076
|
+
size: number;
|
|
1077
|
+
/** Raw file contents */
|
|
1078
|
+
data: ArrayBuffer;
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Serve a file as an HTTP `Response` with correct headers.
|
|
1082
|
+
*
|
|
1083
|
+
* Features:
|
|
1084
|
+
* - Auto-detected Content-Type from file extension
|
|
1085
|
+
* - Range request support (206 Partial Content) for streaming / resumable downloads
|
|
1086
|
+
* - Conditional requests (ETag, Last-Modified → 304 Not Modified)
|
|
1087
|
+
* - Content-Disposition for triggering downloads
|
|
1088
|
+
* - Uses `Bun.file()` on Bun for zero-copy `sendfile` performance
|
|
1089
|
+
*
|
|
1090
|
+
* Returns a **404** `Response` when the file does not exist (no throw).
|
|
1091
|
+
*
|
|
1092
|
+
* @example
|
|
1093
|
+
* ```ts
|
|
1094
|
+
* // Basic usage
|
|
1095
|
+
* app.get('/files/:name', (ctx) =>
|
|
1096
|
+
* sendFile(`./public/${ctx.params.name}`)
|
|
1097
|
+
* );
|
|
1098
|
+
*
|
|
1099
|
+
* // With caching and range support
|
|
1100
|
+
* app.get('/media/:file', (ctx) =>
|
|
1101
|
+
* sendFile(`./media/${ctx.params.file}`, {
|
|
1102
|
+
* reqHeaders: ctx.headers,
|
|
1103
|
+
* cacheControl: 'public, max-age=86400',
|
|
1104
|
+
* })
|
|
1105
|
+
* );
|
|
1106
|
+
*
|
|
1107
|
+
* // Force download
|
|
1108
|
+
* app.get('/download/:file', (ctx) =>
|
|
1109
|
+
* sendFile(`./files/${ctx.params.file}`, { download: true })
|
|
1110
|
+
* );
|
|
1111
|
+
* ```
|
|
1112
|
+
*/
|
|
1113
|
+
declare function sendFile(filePath: string, options?: SendFileOptions): Promise<Response>;
|
|
1114
|
+
/**
|
|
1115
|
+
* List the contents of a directory with optional metadata.
|
|
1116
|
+
*
|
|
1117
|
+
* Stat calls within each directory level are parallelized via `Promise.all`
|
|
1118
|
+
* for maximum throughput. Uses an iterative stack (no recursion overhead).
|
|
1119
|
+
*
|
|
1120
|
+
* @example
|
|
1121
|
+
* ```ts
|
|
1122
|
+
* // Flat listing
|
|
1123
|
+
* app.get('/browse', async () => {
|
|
1124
|
+
* return listDirectory('./data');
|
|
1125
|
+
* });
|
|
1126
|
+
*
|
|
1127
|
+
* // Recursive with filter
|
|
1128
|
+
* app.get('/browse/images', async () => {
|
|
1129
|
+
* return listDirectory('./public', {
|
|
1130
|
+
* recursive: true,
|
|
1131
|
+
* filter: (e) => e.isDirectory || e.name.endsWith('.png'),
|
|
1132
|
+
* });
|
|
1133
|
+
* });
|
|
1134
|
+
* ```
|
|
1135
|
+
*/
|
|
1136
|
+
declare function listDirectory(dirPath: string, options?: ListDirectoryOptions): Promise<FileEntry[]>;
|
|
1137
|
+
/**
|
|
1138
|
+
* Extract uploaded files from `FormData` with validation.
|
|
1139
|
+
*
|
|
1140
|
+
* @example
|
|
1141
|
+
* ```ts
|
|
1142
|
+
* app.post('/upload', async (ctx) => {
|
|
1143
|
+
* const files = await receiveFiles(await ctx.form, {
|
|
1144
|
+
* maxFileSize: 10 * 1024 * 1024, // 10 MB
|
|
1145
|
+
* maxFiles: 5,
|
|
1146
|
+
* allowedTypes: ['image/png', 'image/jpeg'],
|
|
1147
|
+
* });
|
|
1148
|
+
*
|
|
1149
|
+
* for (const file of files) {
|
|
1150
|
+
* await saveFile(`./uploads/${file.fileName}`, file.data);
|
|
1151
|
+
* }
|
|
1152
|
+
* return { uploaded: files.length };
|
|
1153
|
+
* });
|
|
1154
|
+
* ```
|
|
1155
|
+
*/
|
|
1156
|
+
declare function receiveFiles(formData: FormData, options?: ReceiveFileOptions): Promise<UploadedFile[]>;
|
|
1157
|
+
/**
|
|
1158
|
+
* Save data to a file. Auto-creates parent directories when needed.
|
|
1159
|
+
* Works across Bun, Node.js, and Deno.
|
|
1160
|
+
*
|
|
1161
|
+
* @example
|
|
1162
|
+
* ```ts
|
|
1163
|
+
* await saveFile('./uploads/photo.jpg', file.data);
|
|
1164
|
+
* await saveFile('./data/config.json', JSON.stringify(config));
|
|
1165
|
+
* ```
|
|
1166
|
+
*/
|
|
1167
|
+
declare function saveFile(filePath: string, data: Blob | ArrayBuffer | Uint8Array | string): Promise<void>;
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Ken Framework - Basic Authentication Middleware
|
|
1171
|
+
* Validates HTTP Basic Authentication credentials
|
|
1172
|
+
*
|
|
1173
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
1174
|
+
*/
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* Options for the Basic Auth middleware.
|
|
1178
|
+
*/
|
|
1179
|
+
interface BasicAuthOptions {
|
|
1180
|
+
/** Expected username (required if verifyUser is not provided) */
|
|
1181
|
+
username?: string;
|
|
1182
|
+
/** Expected password (required if verifyUser is not provided) */
|
|
1183
|
+
password?: string;
|
|
1184
|
+
/** Realm for WWW-Authenticate header (default: "Secure Area") */
|
|
1185
|
+
realm?: string;
|
|
1186
|
+
/** Custom verification function. Return true to allow access. */
|
|
1187
|
+
verifyUser?: (username: string, password: string, ctx: Context) => boolean | Promise<boolean>;
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Basic Authentication middleware.
|
|
1191
|
+
*
|
|
1192
|
+
* Validates the `Authorization: Basic <base64>` header.
|
|
1193
|
+
* Returns 401 Response if credentials are missing or invalid.
|
|
1194
|
+
*
|
|
1195
|
+
* @example
|
|
1196
|
+
* ```ts
|
|
1197
|
+
* // Static credentials
|
|
1198
|
+
* app.get("/admin", {
|
|
1199
|
+
* state: { auth: basicAuth({ username: 'admin', password: 'secret' }) }
|
|
1200
|
+
* }, (ctx) => Response.json({ user: ctx.state.auth.username }));
|
|
1201
|
+
*
|
|
1202
|
+
* // Custom verification
|
|
1203
|
+
* app.get("/admin", {
|
|
1204
|
+
* state: { auth: basicAuth({ verifyUser: (u, p) => u === 'admin' && p === 'secret' }) }
|
|
1205
|
+
* }, (ctx) => Response.json({ user: ctx.state.auth.username }));
|
|
1206
|
+
* ```
|
|
1207
|
+
*/
|
|
1208
|
+
declare function basicAuth(options: BasicAuthOptions): ((ctx: Context) => Promise<{
|
|
1209
|
+
username: string;
|
|
1210
|
+
} | Response>) | ((ctx: Context) => {
|
|
1211
|
+
username: string;
|
|
1212
|
+
} | Response);
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Ken Framework - Bearer Authentication Middleware
|
|
1216
|
+
* Validates Bearer token in the Authorization header
|
|
1217
|
+
*
|
|
1218
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
1219
|
+
*/
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Options for the Bearer Auth middleware.
|
|
1223
|
+
*/
|
|
1224
|
+
interface BearerAuthOptions {
|
|
1225
|
+
/** Expected token(s). Ignored if verifyToken is provided. */
|
|
1226
|
+
token?: string | string[];
|
|
1227
|
+
/** Custom token verification function. Return true to allow access. */
|
|
1228
|
+
verifyToken?: (token: string, ctx: Context) => boolean | Promise<boolean>;
|
|
1229
|
+
/** Realm for WWW-Authenticate header (default: "") */
|
|
1230
|
+
realm?: string;
|
|
1231
|
+
/** Prefix/scheme for the Authorization header (default: "Bearer") */
|
|
1232
|
+
prefix?: string;
|
|
1233
|
+
/** Header name to read token from (default: "authorization") */
|
|
1234
|
+
headerName?: string;
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Bearer Authentication middleware.
|
|
1238
|
+
*
|
|
1239
|
+
* Validates the `Authorization: Bearer <token>` header.
|
|
1240
|
+
* Returns 401 Response if token is missing or invalid.
|
|
1241
|
+
*
|
|
1242
|
+
* @example
|
|
1243
|
+
* ```ts
|
|
1244
|
+
* app.get("/api/data", {
|
|
1245
|
+
* state: { auth: bearerAuth({ token: 'my-api-token' }) }
|
|
1246
|
+
* }, (ctx) => Response.json({ token: ctx.state.auth.token }));
|
|
1247
|
+
*
|
|
1248
|
+
* // Multiple valid tokens
|
|
1249
|
+
* app.get("/api/data", {
|
|
1250
|
+
* state: { auth: bearerAuth({ token: ['token-a', 'token-b'] }) }
|
|
1251
|
+
* }, (ctx) => Response.json({ token: ctx.state.auth.token }));
|
|
1252
|
+
*
|
|
1253
|
+
* // Custom verification
|
|
1254
|
+
* app.get("/api/data", {
|
|
1255
|
+
* state: { auth: bearerAuth({ verifyToken: async (t) => t === 'dynamic' }) }
|
|
1256
|
+
* }, (ctx) => Response.json({ token: ctx.state.auth.token }));
|
|
1257
|
+
* ```
|
|
1258
|
+
*/
|
|
1259
|
+
declare function bearerAuth(options: BearerAuthOptions): ((ctx: Context) => Promise<{
|
|
1260
|
+
token: string;
|
|
1261
|
+
} | Response>) | ((ctx: Context) => {
|
|
1262
|
+
token: string;
|
|
1263
|
+
} | Response);
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* Ken Framework - Body Limit Middleware
|
|
1267
|
+
* Rejects requests with bodies exceeding the configured size limit
|
|
1268
|
+
*
|
|
1269
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
1270
|
+
*/
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Options for the Body Limit middleware.
|
|
1274
|
+
*/
|
|
1275
|
+
interface BodyLimitOptions {
|
|
1276
|
+
/** Maximum body size in bytes */
|
|
1277
|
+
maxSize: number;
|
|
1278
|
+
/** Custom error handler. By default throws 413 Payload Too Large. */
|
|
1279
|
+
onError?: (ctx: Context) => Response;
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Body Limit middleware.
|
|
1283
|
+
*
|
|
1284
|
+
* Checks the `Content-Length` header and rejects requests exceeding the limit.
|
|
1285
|
+
* Returns 413 Payload Too Large response if the body is too large.
|
|
1286
|
+
*
|
|
1287
|
+
* @example
|
|
1288
|
+
* ```ts
|
|
1289
|
+
* // Limit body to 1MB
|
|
1290
|
+
* app.post("/upload", {
|
|
1291
|
+
* state: { limit: bodyLimit({ maxSize: 1024 * 1024 }) }
|
|
1292
|
+
* }, async (ctx) => {
|
|
1293
|
+
* const body = await ctx.json;
|
|
1294
|
+
* return Response.json({ received: true });
|
|
1295
|
+
* });
|
|
1296
|
+
* ```
|
|
1297
|
+
*/
|
|
1298
|
+
declare function bodyLimit(options: BodyLimitOptions): (ctx: Context) => void | Response;
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Ken Framework - Cache Middleware
|
|
1302
|
+
* Sets Cache-Control and related caching headers on responses
|
|
1303
|
+
*
|
|
1304
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
1305
|
+
*/
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Options for the Cache middleware.
|
|
1309
|
+
*/
|
|
1310
|
+
interface CacheOptions {
|
|
1311
|
+
/** Cache-Control max-age in seconds (default: 0) */
|
|
1312
|
+
maxAge?: number;
|
|
1313
|
+
/** Cache-Control s-maxage in seconds (for shared caches/CDNs) */
|
|
1314
|
+
sMaxAge?: number;
|
|
1315
|
+
/** Cache-Control stale-while-revalidate in seconds */
|
|
1316
|
+
staleWhileRevalidate?: number;
|
|
1317
|
+
/** Cache-Control stale-if-error in seconds */
|
|
1318
|
+
staleIfError?: number;
|
|
1319
|
+
/** Set `public` directive (default: false) */
|
|
1320
|
+
public?: boolean;
|
|
1321
|
+
/** Set `private` directive (default: false) */
|
|
1322
|
+
private?: boolean;
|
|
1323
|
+
/** Set `no-cache` directive (default: false) */
|
|
1324
|
+
noCache?: boolean;
|
|
1325
|
+
/** Set `no-store` directive (default: false) */
|
|
1326
|
+
noStore?: boolean;
|
|
1327
|
+
/** Set `must-revalidate` directive (default: false) */
|
|
1328
|
+
mustRevalidate?: boolean;
|
|
1329
|
+
/** Set `proxy-revalidate` directive (default: false) */
|
|
1330
|
+
proxyRevalidate?: boolean;
|
|
1331
|
+
/** Set `immutable` directive (default: false) */
|
|
1332
|
+
immutable?: boolean;
|
|
1333
|
+
/** Set `no-transform` directive (default: false) */
|
|
1334
|
+
noTransform?: boolean;
|
|
1335
|
+
/** Custom Vary header value */
|
|
1336
|
+
vary?: string;
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Cache middleware.
|
|
1340
|
+
*
|
|
1341
|
+
* Adds `Cache-Control` and optionally `Vary` headers to responses.
|
|
1342
|
+
*
|
|
1343
|
+
* @example
|
|
1344
|
+
* ```ts
|
|
1345
|
+
* // Cache for 1 hour
|
|
1346
|
+
* app.get("/static", {
|
|
1347
|
+
* state: { caching: cache({ maxAge: 3600, public: true }) }
|
|
1348
|
+
* }, () => new Response("static content"));
|
|
1349
|
+
*
|
|
1350
|
+
* // No caching
|
|
1351
|
+
* app.get("/dynamic", {
|
|
1352
|
+
* state: { caching: cache({ noStore: true }) }
|
|
1353
|
+
* }, () => Response.json({ time: Date.now() }));
|
|
1354
|
+
* ```
|
|
1355
|
+
*/
|
|
1356
|
+
declare function cache(options?: CacheOptions): (ctx: Context) => void;
|
|
1357
|
+
|
|
1358
|
+
/**
|
|
1359
|
+
* Ken Framework - Compress Middleware
|
|
1360
|
+
* Detects client-supported compression and provides encoding information
|
|
1361
|
+
*
|
|
1362
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
1363
|
+
*/
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* Supported compression encodings.
|
|
1367
|
+
*/
|
|
1368
|
+
type CompressionEncoding = 'gzip' | 'deflate' | 'br';
|
|
1369
|
+
/**
|
|
1370
|
+
* Options for the Compress middleware.
|
|
1371
|
+
*/
|
|
1372
|
+
interface CompressOptions {
|
|
1373
|
+
/** Preferred encoding order (default: ['br', 'gzip', 'deflate']) */
|
|
1374
|
+
preferred?: CompressionEncoding[];
|
|
1375
|
+
/** Minimum body size in bytes to compress (default: 1024) */
|
|
1376
|
+
threshold?: number;
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Compress middleware.
|
|
1380
|
+
*
|
|
1381
|
+
* Parses the `Accept-Encoding` header and returns the best encoding
|
|
1382
|
+
* in state. Adds `Vary: Accept-Encoding` to responses.
|
|
1383
|
+
*
|
|
1384
|
+
* Note: Ken delegates actual body compression to the runtime layer.
|
|
1385
|
+
* Bun, Deno, and most reverse proxies handle compression natively.
|
|
1386
|
+
* This middleware provides encoding preference in state and sets
|
|
1387
|
+
* appropriate response headers.
|
|
1388
|
+
*
|
|
1389
|
+
* @example
|
|
1390
|
+
* ```ts
|
|
1391
|
+
* app.get("/data", {
|
|
1392
|
+
* state: { encoding: compress() }
|
|
1393
|
+
* }, (ctx) => {
|
|
1394
|
+
* // ctx.state.encoding.encoding is 'gzip' | 'deflate' | 'br' | null
|
|
1395
|
+
* return Response.json({ data: largePayload });
|
|
1396
|
+
* });
|
|
1397
|
+
* ```
|
|
1398
|
+
*/
|
|
1399
|
+
declare function compress(options?: CompressOptions): (ctx: Context) => {
|
|
1400
|
+
encoding: CompressionEncoding | null;
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
/**
|
|
1404
|
+
* Ken Framework - CORS Middleware
|
|
1405
|
+
* Cross-Origin Resource Sharing headers
|
|
1406
|
+
*
|
|
1407
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
1408
|
+
*/
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* Options for the CORS middleware.
|
|
1412
|
+
*/
|
|
1413
|
+
interface CorsOptions {
|
|
1414
|
+
/**
|
|
1415
|
+
* Allowed origin(s).
|
|
1416
|
+
* - `'*'` allows all origins (default)
|
|
1417
|
+
* - A string matches exactly
|
|
1418
|
+
* - An array of strings matches any
|
|
1419
|
+
* - A function receives the origin and returns the allowed origin
|
|
1420
|
+
*/
|
|
1421
|
+
origin?: string | string[] | ((origin: string, ctx: Context) => string);
|
|
1422
|
+
/** Allowed HTTP methods (default: ['GET','HEAD','PUT','POST','DELETE','PATCH']) */
|
|
1423
|
+
allowMethods?: string[];
|
|
1424
|
+
/** Allowed request headers */
|
|
1425
|
+
allowHeaders?: string[];
|
|
1426
|
+
/** Headers exposed to the client */
|
|
1427
|
+
exposeHeaders?: string[];
|
|
1428
|
+
/** Max age for preflight cache in seconds */
|
|
1429
|
+
maxAge?: number;
|
|
1430
|
+
/** Allow credentials (cookies, authorization headers) */
|
|
1431
|
+
credentials?: boolean;
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* CORS middleware.
|
|
1435
|
+
*
|
|
1436
|
+
* Returns an App instance that automatically handles preflight OPTIONS requests
|
|
1437
|
+
* and adds CORS headers to all responses for the specified path pattern.
|
|
1438
|
+
*
|
|
1439
|
+
* @example.
|
|
1440
|
+
*
|
|
1441
|
+
* @example
|
|
1442
|
+
* ```ts
|
|
1443
|
+
* // Use CORS for all routes - mount at root
|
|
1444
|
+
* const corsApp = cors();
|
|
1445
|
+
* corsApp.get("/api/data", () => Response.json({ data: 1 }));
|
|
1446
|
+
* app.use(corsApp);
|
|
1447
|
+
*
|
|
1448
|
+
* // Use CORS for specific path prefix
|
|
1449
|
+
* const apiCors = cors({ origin: 'https://example.com', credentials: true });
|
|
1450
|
+
* apiCors.get("/data", () => Response.json({ data: 1 }));
|
|
1451
|
+
* app.use('/api', apiCors);
|
|
1452
|
+
* ```
|
|
1453
|
+
*/
|
|
1454
|
+
declare function cors(options?: CorsOptions): App;
|
|
1455
|
+
|
|
1456
|
+
/**
|
|
1457
|
+
* Ken Framework - CSRF Protection Middleware
|
|
1458
|
+
* Cross-Site Request Forgery protection
|
|
1459
|
+
*
|
|
1460
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
1461
|
+
*/
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* Options for the CSRF middleware.
|
|
1465
|
+
*/
|
|
1466
|
+
interface CsrfOptions {
|
|
1467
|
+
/**
|
|
1468
|
+
* Allowed origin(s).
|
|
1469
|
+
* - A string matches exactly
|
|
1470
|
+
* - An array of strings matches any
|
|
1471
|
+
* - A function receives the origin and returns true to allow
|
|
1472
|
+
* - If not set, only same-origin requests are allowed
|
|
1473
|
+
*/
|
|
1474
|
+
origin?: string | string[] | ((origin: string, ctx: Context) => boolean);
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* CSRF Protection middleware.
|
|
1478
|
+
*
|
|
1479
|
+
* Validates the `Origin` header on unsafe requests (POST, PUT, PATCH, DELETE)
|
|
1480
|
+
* with form-compatible content types. Returns 403 if the origin is not allowed.
|
|
1481
|
+
* Safe methods (GET, HEAD, OPTIONS) are always allowed.
|
|
1482
|
+
*
|
|
1483
|
+
* @example
|
|
1484
|
+
* ```ts
|
|
1485
|
+
* // Auto-detect same origin
|
|
1486
|
+
* app.define({ protection: csrf() }, (app) => {
|
|
1487
|
+
* app.post("/submit", async (ctx) => {
|
|
1488
|
+
* const body = await ctx.json;
|
|
1489
|
+
* return Response.json({ success: true });
|
|
1490
|
+
* });
|
|
1491
|
+
* });
|
|
1492
|
+
*
|
|
1493
|
+
* // Specific origin
|
|
1494
|
+
* app.post("/submit", {
|
|
1495
|
+
* state: { protection: csrf({ origin: 'https://myapp.example.com' }) }
|
|
1496
|
+
* }, async (ctx) => Response.json({ success: true }));
|
|
1497
|
+
* ```
|
|
1498
|
+
*/
|
|
1499
|
+
declare function csrf(options?: CsrfOptions): (ctx: Context) => void | Response;
|
|
1500
|
+
|
|
1501
|
+
/**
|
|
1502
|
+
* Ken Framework - ETag Middleware
|
|
1503
|
+
* Provides If-None-Match header value for conditional requests
|
|
1504
|
+
*
|
|
1505
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
1506
|
+
*/
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* Options for the ETag middleware.
|
|
1510
|
+
*/
|
|
1511
|
+
interface ETagOptions {
|
|
1512
|
+
/** Use weak ETags (prefixed with W/) (default: false) */
|
|
1513
|
+
weak?: boolean;
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* ETag middleware.
|
|
1517
|
+
*
|
|
1518
|
+
* Reads the `If-None-Match` request header and returns its value in state.
|
|
1519
|
+
* The handler can compare this with its computed ETag to return 304.
|
|
1520
|
+
*
|
|
1521
|
+
* @example
|
|
1522
|
+
* ```ts
|
|
1523
|
+
* app.get("/data", {
|
|
1524
|
+
* state: { etagValue: etag() }
|
|
1525
|
+
* }, (ctx) => {
|
|
1526
|
+
* const tag = '"data-v1"';
|
|
1527
|
+
* if (ctx.state.etagValue === tag) {
|
|
1528
|
+
* return new Response(null, { status: 304 });
|
|
1529
|
+
* }
|
|
1530
|
+
* return new Response("content", { headers: { 'ETag': tag } });
|
|
1531
|
+
* });
|
|
1532
|
+
* ```
|
|
1533
|
+
*/
|
|
1534
|
+
declare function etag(_options?: ETagOptions): (ctx: Context) => string | null;
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* Ken Framework - IP Restriction Middleware
|
|
1538
|
+
* Allow or deny requests based on client IP address
|
|
1539
|
+
*
|
|
1540
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
1541
|
+
*/
|
|
1542
|
+
|
|
1543
|
+
/**
|
|
1544
|
+
* Options for the IP Restriction middleware.
|
|
1545
|
+
*/
|
|
1546
|
+
interface IpRestrictionOptions {
|
|
1547
|
+
/** List of allowed IP addresses. If set, only these IPs are allowed. */
|
|
1548
|
+
allowList?: string[];
|
|
1549
|
+
/** List of denied IP addresses. Checked after allowList. */
|
|
1550
|
+
denyList?: string[];
|
|
1551
|
+
/** Custom error response factory */
|
|
1552
|
+
onError?: (ctx: Context) => Response;
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* IP Restriction middleware.
|
|
1556
|
+
*
|
|
1557
|
+
* Restricts access based on client IP address using the remote info.
|
|
1558
|
+
* Returns 403 if the IP is not in the allow list or is in the deny list.
|
|
1559
|
+
*
|
|
1560
|
+
* Remote IP detection checks proxy headers (CF-Connecting-IP, X-Real-IP,
|
|
1561
|
+
* X-Forwarded-For, True-Client-IP) before falling back to runtime info.
|
|
1562
|
+
*
|
|
1563
|
+
* @example
|
|
1564
|
+
* ```ts
|
|
1565
|
+
* // Allow only specific IPs
|
|
1566
|
+
* app.get("/admin", {
|
|
1567
|
+
* state: { ipCheck: ipRestriction({ allowList: ['127.0.0.1', '::1'] }) }
|
|
1568
|
+
* }, () => Response.json({ admin: true }));
|
|
1569
|
+
*
|
|
1570
|
+
* // Deny specific IPs
|
|
1571
|
+
* app.get("/public", {
|
|
1572
|
+
* state: { ipCheck: ipRestriction({ denyList: ['10.0.0.1'] }) }
|
|
1573
|
+
* }, () => new Response("public content"));
|
|
1574
|
+
* ```
|
|
1575
|
+
*/
|
|
1576
|
+
declare function ipRestriction(options: IpRestrictionOptions): (ctx: Context) => void | Response;
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* Ken Framework - JWT Middleware
|
|
1580
|
+
* JSON Web Token verification using Web Crypto API
|
|
1581
|
+
*
|
|
1582
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
1583
|
+
*/
|
|
1584
|
+
|
|
1585
|
+
/**
|
|
1586
|
+
* JWT payload type
|
|
1587
|
+
*/
|
|
1588
|
+
interface JWTPayload {
|
|
1589
|
+
/** Issuer */
|
|
1590
|
+
iss?: string;
|
|
1591
|
+
/** Subject */
|
|
1592
|
+
sub?: string;
|
|
1593
|
+
/** Audience */
|
|
1594
|
+
aud?: string | string[];
|
|
1595
|
+
/** Expiration time (seconds since epoch) */
|
|
1596
|
+
exp?: number;
|
|
1597
|
+
/** Not before (seconds since epoch) */
|
|
1598
|
+
nbf?: number;
|
|
1599
|
+
/** Issued at (seconds since epoch) */
|
|
1600
|
+
iat?: number;
|
|
1601
|
+
/** JWT ID */
|
|
1602
|
+
jti?: string;
|
|
1603
|
+
/** Custom claims */
|
|
1604
|
+
[key: string]: unknown;
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Supported HMAC algorithms
|
|
1608
|
+
*/
|
|
1609
|
+
type JWTAlgorithm = 'HS256' | 'HS384' | 'HS512';
|
|
1610
|
+
/**
|
|
1611
|
+
* Options for the JWT middleware.
|
|
1612
|
+
*/
|
|
1613
|
+
interface JWTOptions {
|
|
1614
|
+
/** HMAC secret key for signature verification */
|
|
1615
|
+
secret: string;
|
|
1616
|
+
/** Expected algorithm (default: 'HS256') */
|
|
1617
|
+
algorithm?: JWTAlgorithm;
|
|
1618
|
+
/** Expected issuer claim */
|
|
1619
|
+
issuer?: string;
|
|
1620
|
+
/** Expected audience claim */
|
|
1621
|
+
audience?: string;
|
|
1622
|
+
/** Header name (default: 'authorization') */
|
|
1623
|
+
headerName?: string;
|
|
1624
|
+
/** Token prefix (default: 'Bearer') */
|
|
1625
|
+
prefix?: string;
|
|
1626
|
+
/** Clock tolerance in seconds for exp/nbf checks (default: 0) */
|
|
1627
|
+
clockTolerance?: number;
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Verify a JWT token and return the payload.
|
|
1631
|
+
*
|
|
1632
|
+
* @param token - The JWT token string
|
|
1633
|
+
* @param secret - The HMAC secret key
|
|
1634
|
+
* @param options - Verification options
|
|
1635
|
+
* @returns The decoded JWT payload
|
|
1636
|
+
* @throws Error if the token is invalid
|
|
1637
|
+
*/
|
|
1638
|
+
declare function verifyJwt(token: string, secret: string, options?: {
|
|
1639
|
+
algorithm?: JWTAlgorithm;
|
|
1640
|
+
issuer?: string;
|
|
1641
|
+
audience?: string;
|
|
1642
|
+
clockTolerance?: number;
|
|
1643
|
+
}): Promise<JWTPayload>;
|
|
1644
|
+
/**
|
|
1645
|
+
* Sign a JWT payload and return the token string.
|
|
1646
|
+
*
|
|
1647
|
+
* @param payload - The JWT payload
|
|
1648
|
+
* @param secret - The HMAC secret key
|
|
1649
|
+
* @param algorithm - The algorithm to use (default: 'HS256')
|
|
1650
|
+
* @returns The signed JWT token string
|
|
1651
|
+
*/
|
|
1652
|
+
declare function signJwt(payload: JWTPayload, secret: string, algorithm?: JWTAlgorithm): Promise<string>;
|
|
1653
|
+
/**
|
|
1654
|
+
* Decode a JWT token without verification (unsafe - for debugging only).
|
|
1655
|
+
*/
|
|
1656
|
+
declare function decodeJwt(token: string): {
|
|
1657
|
+
header: any;
|
|
1658
|
+
payload: JWTPayload;
|
|
1659
|
+
};
|
|
1660
|
+
/**
|
|
1661
|
+
* JWT middleware.
|
|
1662
|
+
*
|
|
1663
|
+
* Verifies the JWT token from the Authorization header using HMAC.
|
|
1664
|
+
* Returns 401 if the token is missing, invalid, or expired.
|
|
1665
|
+
* Returns the decoded payload.
|
|
1666
|
+
*
|
|
1667
|
+
* @example
|
|
1668
|
+
* ```ts
|
|
1669
|
+
* app.get("/protected", {
|
|
1670
|
+
* state: { auth: jwt({ secret: 'my-secret-key' }) }
|
|
1671
|
+
* }, (ctx) => Response.json({ user: ctx.state.auth.sub }));
|
|
1672
|
+
* ```
|
|
1673
|
+
*/
|
|
1674
|
+
declare function jwt(options: JWTOptions): (ctx: Context) => Promise<JWTPayload | Response>;
|
|
1675
|
+
|
|
1676
|
+
/**
|
|
1677
|
+
* Ken Framework - JWK Middleware
|
|
1678
|
+
* JSON Web Key Set (JWKS) based JWT verification
|
|
1679
|
+
*
|
|
1680
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
1681
|
+
*/
|
|
1682
|
+
|
|
1683
|
+
/**
|
|
1684
|
+
* JWK key object
|
|
1685
|
+
*/
|
|
1686
|
+
interface JWK {
|
|
1687
|
+
kty: string;
|
|
1688
|
+
kid?: string;
|
|
1689
|
+
use?: string;
|
|
1690
|
+
alg?: string;
|
|
1691
|
+
n?: string;
|
|
1692
|
+
e?: string;
|
|
1693
|
+
crv?: string;
|
|
1694
|
+
x?: string;
|
|
1695
|
+
y?: string;
|
|
1696
|
+
[key: string]: unknown;
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* JWKS response
|
|
1700
|
+
*/
|
|
1701
|
+
interface JWKS {
|
|
1702
|
+
keys: JWK[];
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Options for the JWK middleware.
|
|
1706
|
+
*/
|
|
1707
|
+
interface JWKOptions {
|
|
1708
|
+
/** URL of the JWKS endpoint */
|
|
1709
|
+
jwksUrl?: string;
|
|
1710
|
+
/** Pre-loaded JWK keys (alternative to jwksUrl) */
|
|
1711
|
+
keys?: JWK[];
|
|
1712
|
+
/** Expected issuer claim */
|
|
1713
|
+
issuer?: string;
|
|
1714
|
+
/** Expected audience claim */
|
|
1715
|
+
audience?: string;
|
|
1716
|
+
/** Header name (default: 'authorization') */
|
|
1717
|
+
headerName?: string;
|
|
1718
|
+
/** Token prefix (default: 'Bearer') */
|
|
1719
|
+
prefix?: string;
|
|
1720
|
+
/** Clock tolerance in seconds for exp/nbf checks (default: 0) */
|
|
1721
|
+
clockTolerance?: number;
|
|
1722
|
+
/** Cache TTL for JWKS keys in milliseconds (default: 600000 = 10 min) */
|
|
1723
|
+
cacheTtl?: number;
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* JWK middleware.
|
|
1727
|
+
*
|
|
1728
|
+
* Verifies JWT tokens using JSON Web Key Set (JWKS).
|
|
1729
|
+
* Supports RSA (RS256/384/512) and ECDSA (ES256/384/512) algorithms.
|
|
1730
|
+
* Keys are cached and refreshed automatically.
|
|
1731
|
+
*
|
|
1732
|
+
* @example
|
|
1733
|
+
* ```ts
|
|
1734
|
+
* // With JWKS URL (e.g., from Auth0, Firebase, etc.)
|
|
1735
|
+
* app.get("/protected", {
|
|
1736
|
+
* state: { auth: jwk({ jwksUrl: 'https://auth.example.com/.well-known/jwks.json' }) }
|
|
1737
|
+
* }, (ctx) => Response.json({ user: ctx.state.auth.sub }));
|
|
1738
|
+
*
|
|
1739
|
+
* // With pre-loaded keys
|
|
1740
|
+
* app.get("/protected", {
|
|
1741
|
+
* state: { auth: jwk({ keys: [{ kty: 'RSA', n: '...', e: '...' }] }) }
|
|
1742
|
+
* }, (ctx) => Response.json({ user: ctx.state.auth.sub }));
|
|
1743
|
+
* ```
|
|
1744
|
+
*/
|
|
1745
|
+
declare function jwk(options: JWKOptions): (ctx: Context) => Promise<JWTPayload | Response>;
|
|
1746
|
+
|
|
1747
|
+
/**
|
|
1748
|
+
* Ken Framework - Logger Middleware
|
|
1749
|
+
* Logs request/response information
|
|
1750
|
+
*
|
|
1751
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
1752
|
+
*/
|
|
1753
|
+
|
|
1754
|
+
/**
|
|
1755
|
+
* Options for the Logger middleware.
|
|
1756
|
+
*/
|
|
1757
|
+
interface LoggerOptions {
|
|
1758
|
+
/** Custom log function (default: console.log) */
|
|
1759
|
+
logFn?: (message: string) => void;
|
|
1760
|
+
/**
|
|
1761
|
+
* Custom log format function.
|
|
1762
|
+
* Receives request info and returns the log message.
|
|
1763
|
+
*/
|
|
1764
|
+
format?: (info: {
|
|
1765
|
+
method: string;
|
|
1766
|
+
url: string;
|
|
1767
|
+
status: number;
|
|
1768
|
+
duration: number;
|
|
1769
|
+
}) => string;
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Logger middleware.
|
|
1773
|
+
*
|
|
1774
|
+
* Returns an App instance that logs all requests for all HTTP methods.
|
|
1775
|
+
* Captures method, URL, status code, and response time.
|
|
1776
|
+
*
|
|
1777
|
+
* @example
|
|
1778
|
+
* ```ts
|
|
1779
|
+
* // Use logger for all routes (default console.log with colors)
|
|
1780
|
+
* app.use(logger());
|
|
1781
|
+
* app.get("/api/data", () => Response.json({ data: 1 }));
|
|
1782
|
+
*
|
|
1783
|
+
* // Use logger for specific path prefix
|
|
1784
|
+
* app.use('/api', logger());
|
|
1785
|
+
*
|
|
1786
|
+
* // Custom log function
|
|
1787
|
+
* const logs: string[] = [];
|
|
1788
|
+
* app.use(logger({ logFn: (msg) => logs.push(msg) }));
|
|
1789
|
+
* ```
|
|
1790
|
+
*/
|
|
1791
|
+
declare function logger(options?: LoggerOptions): App;
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* Ken Framework - Request ID Middleware
|
|
1795
|
+
* Generates a unique request identifier
|
|
1796
|
+
*
|
|
1797
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
1798
|
+
*/
|
|
1799
|
+
|
|
1800
|
+
/**
|
|
1801
|
+
* Options for the Request ID middleware.
|
|
1802
|
+
*/
|
|
1803
|
+
interface RequestIdOptions {
|
|
1804
|
+
/** Custom ID generator (default: crypto.randomUUID) */
|
|
1805
|
+
generator?: () => string;
|
|
1806
|
+
/** Header name to set on the response (default: 'X-Request-Id') */
|
|
1807
|
+
headerName?: string;
|
|
1808
|
+
/** Header name to read from the incoming request. If present, reuses the ID. */
|
|
1809
|
+
requestHeaderName?: string;
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Request ID middleware.
|
|
1813
|
+
*
|
|
1814
|
+
* Generates a unique ID for each request and returns it in state.
|
|
1815
|
+
* Also adds the ID to the response via the specified header.
|
|
1816
|
+
*
|
|
1817
|
+
* @example
|
|
1818
|
+
* ```ts
|
|
1819
|
+
* app.get("/api/data", {
|
|
1820
|
+
* state: { reqId: requestId() }
|
|
1821
|
+
* }, (ctx) => Response.json({ id: ctx.state.reqId }));
|
|
1822
|
+
* ```
|
|
1823
|
+
*/
|
|
1824
|
+
declare function requestId(options?: RequestIdOptions): (ctx: Context) => string;
|
|
1825
|
+
|
|
1826
|
+
/**
|
|
1827
|
+
* Ken Framework - Secure Headers Middleware
|
|
1828
|
+
* Adds security-related HTTP headers to responses
|
|
1829
|
+
*
|
|
1830
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
1831
|
+
*/
|
|
1832
|
+
|
|
1833
|
+
/**
|
|
1834
|
+
* Options for the Secure Headers middleware.
|
|
1835
|
+
* Set a header to `false` to disable it, or a string to override its value.
|
|
1836
|
+
*/
|
|
1837
|
+
interface SecureHeadersOptions {
|
|
1838
|
+
/** Content-Security-Policy (default: not set) */
|
|
1839
|
+
contentSecurityPolicy?: string | false;
|
|
1840
|
+
/** Cross-Origin-Embedder-Policy (default: not set) */
|
|
1841
|
+
crossOriginEmbedderPolicy?: string | false;
|
|
1842
|
+
/** Cross-Origin-Opener-Policy (default: 'same-origin') */
|
|
1843
|
+
crossOriginOpenerPolicy?: string | false;
|
|
1844
|
+
/** Cross-Origin-Resource-Policy (default: 'same-origin') */
|
|
1845
|
+
crossOriginResourcePolicy?: string | false;
|
|
1846
|
+
/** Origin-Agent-Cluster (default: '?1') */
|
|
1847
|
+
originAgentCluster?: string | false;
|
|
1848
|
+
/** Referrer-Policy (default: 'no-referrer') */
|
|
1849
|
+
referrerPolicy?: string | false;
|
|
1850
|
+
/** Strict-Transport-Security (default: 'max-age=15552000; includeSubDomains') */
|
|
1851
|
+
strictTransportSecurity?: string | false;
|
|
1852
|
+
/** X-Content-Type-Options (default: 'nosniff') */
|
|
1853
|
+
xContentTypeOptions?: string | false;
|
|
1854
|
+
/** X-DNS-Prefetch-Control (default: 'off') */
|
|
1855
|
+
xDnsPrefetchControl?: string | false;
|
|
1856
|
+
/** X-Download-Options (default: 'noopen') */
|
|
1857
|
+
xDownloadOptions?: string | false;
|
|
1858
|
+
/** X-Frame-Options (default: 'SAMEORIGIN') */
|
|
1859
|
+
xFrameOptions?: string | false;
|
|
1860
|
+
/** X-Permitted-Cross-Domain-Policies (default: 'none') */
|
|
1861
|
+
xPermittedCrossDomainPolicies?: string | false;
|
|
1862
|
+
/** X-XSS-Protection (default: '0') */
|
|
1863
|
+
xXssProtection?: string | false;
|
|
1864
|
+
/** Remove X-Powered-By header (default: true) */
|
|
1865
|
+
removePoweredBy?: boolean;
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Secure Headers middleware.
|
|
1869
|
+
*
|
|
1870
|
+
* Adds security-related HTTP headers to responses. Uses sensible
|
|
1871
|
+
* defaults inspired by Helmet. Individual headers can be overridden
|
|
1872
|
+
* or disabled.
|
|
1873
|
+
*
|
|
1874
|
+
* @example
|
|
1875
|
+
* ```ts
|
|
1876
|
+
* // Use sensible defaults
|
|
1877
|
+
* app.define({ sec: secureHeaders() }, (app) => {
|
|
1878
|
+
* app.get("/", () => new Response("secure"));
|
|
1879
|
+
* });
|
|
1880
|
+
*
|
|
1881
|
+
* // Customize
|
|
1882
|
+
* app.define({ sec: secureHeaders({
|
|
1883
|
+
* xFrameOptions: 'DENY',
|
|
1884
|
+
* contentSecurityPolicy: "default-src 'self'",
|
|
1885
|
+
* }) }, (app) => {
|
|
1886
|
+
* app.get("/", () => new Response("secure"));
|
|
1887
|
+
* });
|
|
1888
|
+
* ```
|
|
1889
|
+
*/
|
|
1890
|
+
declare function secureHeaders(options?: SecureHeadersOptions): (ctx: Context) => void;
|
|
1891
|
+
|
|
1892
|
+
/**
|
|
1893
|
+
* Ken Framework - Session Middleware
|
|
1894
|
+
* Flexible session management with customizable validation logic.
|
|
1895
|
+
* Auto-detects sync vs async validation at initialization time.
|
|
1896
|
+
*
|
|
1897
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
1898
|
+
*/
|
|
1899
|
+
|
|
1900
|
+
/**
|
|
1901
|
+
* Options for the Session middleware.
|
|
1902
|
+
*/
|
|
1903
|
+
interface SessionOptions<T = any> {
|
|
1904
|
+
/** Name of the cookie to read session from */
|
|
1905
|
+
cookieName: string;
|
|
1906
|
+
/**
|
|
1907
|
+
* Custom validation function.
|
|
1908
|
+
* Receives the cookie value and context.
|
|
1909
|
+
* Return session data on success, or throw/return a Response to deny access.
|
|
1910
|
+
* Returning null/undefined also triggers the unauthorized handler.
|
|
1911
|
+
*/
|
|
1912
|
+
validate: (cookieValue: string, ctx: Context) => T | Response | Promise<T | Response>;
|
|
1913
|
+
/**
|
|
1914
|
+
* Custom unauthorized response factory.
|
|
1915
|
+
* Called when cookie is missing, validation returns null, or throws a non-Response error.
|
|
1916
|
+
* Defaults to `new Response('Unauthorized', { status: 401 })`.
|
|
1917
|
+
*/
|
|
1918
|
+
onUnauthorized?: (ctx: Context) => Response;
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Session middleware with auto-detection of sync vs async validation.
|
|
1922
|
+
*
|
|
1923
|
+
* Reads a cookie, passes it to a user-supplied `validate` function,
|
|
1924
|
+
* and either returns session data into `ctx.state` or short-circuits
|
|
1925
|
+
* with a `Response`. The sync/async branch is resolved once at
|
|
1926
|
+
* initialization time via `constructor.name` check — zero per-request overhead.
|
|
1927
|
+
*
|
|
1928
|
+
* @example
|
|
1929
|
+
* ```ts
|
|
1930
|
+
* // Sync — in-memory lookup
|
|
1931
|
+
* const auth = session({
|
|
1932
|
+
* cookieName: '_sid',
|
|
1933
|
+
* validate: (sid) => {
|
|
1934
|
+
* const user = USERS.get(sid);
|
|
1935
|
+
* if (!user) throw new Response('Unauthorized', { status: 401 });
|
|
1936
|
+
* return user;
|
|
1937
|
+
* },
|
|
1938
|
+
* });
|
|
1939
|
+
*
|
|
1940
|
+
* // Async — encrypted cookie + DB lookup
|
|
1941
|
+
* const auth = session({
|
|
1942
|
+
* cookieName: '_ak',
|
|
1943
|
+
* validate: async (cookieValue, ctx) => {
|
|
1944
|
+
* const apiKey = await decryptString(cookieValue, secretKey);
|
|
1945
|
+
* if (!MEDIA_OWNERS[apiKey]?.active) {
|
|
1946
|
+
* throw new Response(null, {
|
|
1947
|
+
* status: 302,
|
|
1948
|
+
* headers: { 'Location': '/auth?redirect=' + ctx.url },
|
|
1949
|
+
* });
|
|
1950
|
+
* }
|
|
1951
|
+
* return apiKey;
|
|
1952
|
+
* },
|
|
1953
|
+
* });
|
|
1954
|
+
*
|
|
1955
|
+
* app.get('/', { state: { session: auth } }, (ctx) => {
|
|
1956
|
+
* return Response.json({ key: ctx.state.session });
|
|
1957
|
+
* });
|
|
1958
|
+
* ```
|
|
1959
|
+
*/
|
|
1960
|
+
declare function session<T = any>(options: SessionOptions<T>): ((ctx: Context) => Promise<T | Response>) | ((ctx: Context) => T | Response);
|
|
1961
|
+
|
|
1962
|
+
/**
|
|
1963
|
+
* Ken Framework - Timeout Middleware
|
|
1964
|
+
* Provides request timeout capabilities via AbortSignal
|
|
1965
|
+
*
|
|
1966
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
1967
|
+
*/
|
|
1968
|
+
|
|
1969
|
+
/**
|
|
1970
|
+
* Options for the Timeout middleware.
|
|
1971
|
+
*/
|
|
1972
|
+
interface TimeoutOptions {
|
|
1973
|
+
/** Timeout duration in milliseconds */
|
|
1974
|
+
duration: number;
|
|
1975
|
+
/** Custom timeout response (default: 408 Request Timeout) */
|
|
1976
|
+
onTimeout?: (ctx: Context) => Response;
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Timeout middleware.
|
|
1980
|
+
*
|
|
1981
|
+
* Creates an AbortController with a timeout. Returns the AbortSignal
|
|
1982
|
+
* in state so the handler can check for timeout or pass it to fetch calls.
|
|
1983
|
+
* The timer is automatically cleared when the response finishes.
|
|
1984
|
+
*
|
|
1985
|
+
* @example
|
|
1986
|
+
* ```ts
|
|
1987
|
+
* app.get("/slow-endpoint", {
|
|
1988
|
+
* state: { timeoutSig: timeout({ duration: 5000 }) }
|
|
1989
|
+
* }, async (ctx) => {
|
|
1990
|
+
* // Pass signal to downstream fetch calls
|
|
1991
|
+
* const data = await fetch('https://api.example.com/data', {
|
|
1992
|
+
* signal: ctx.state.timeoutSig.signal,
|
|
1993
|
+
* });
|
|
1994
|
+
* return Response.json(await data.json());
|
|
1995
|
+
* });
|
|
1996
|
+
* ```
|
|
1997
|
+
*/
|
|
1998
|
+
declare function timeout(options: TimeoutOptions): (ctx: Context) => {
|
|
1999
|
+
signal: AbortSignal;
|
|
2000
|
+
} | Response;
|
|
2001
|
+
|
|
2002
|
+
/**
|
|
2003
|
+
* Ken Framework - Timing Middleware
|
|
2004
|
+
* Server-Timing header for performance monitoring
|
|
2005
|
+
*
|
|
2006
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
2007
|
+
*/
|
|
2008
|
+
|
|
2009
|
+
/**
|
|
2010
|
+
* Options for the Timing middleware.
|
|
2011
|
+
*/
|
|
2012
|
+
interface TimingOptions {
|
|
2013
|
+
/** Metric name (default: 'total') */
|
|
2014
|
+
name?: string;
|
|
2015
|
+
/** Description for the Server-Timing entry */
|
|
2016
|
+
description?: string;
|
|
2017
|
+
/** Whether to include the timing header (default: true) */
|
|
2018
|
+
enabled?: boolean;
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* Timing middleware.
|
|
2022
|
+
*
|
|
2023
|
+
* Measures request processing time and adds a `Server-Timing` header
|
|
2024
|
+
* to the response. Useful for performance monitoring.
|
|
2025
|
+
*
|
|
2026
|
+
* @example
|
|
2027
|
+
* ```ts
|
|
2028
|
+
* app.define({ perf: timing() }, (app) => {
|
|
2029
|
+
* app.get("/api/data", () => Response.json({ data: 1 }));
|
|
2030
|
+
* });
|
|
2031
|
+
* // Response header: Server-Timing: total;dur=2.5
|
|
2032
|
+
*
|
|
2033
|
+
* // Custom metric name
|
|
2034
|
+
* app.get("/api/data", {
|
|
2035
|
+
* state: { perf: timing({ name: 'app', description: 'Application processing' }) }
|
|
2036
|
+
* }, () => Response.json({ data: 1 }));
|
|
2037
|
+
* // Response header: Server-Timing: app;desc="Application processing";dur=2.5
|
|
2038
|
+
* ```
|
|
2039
|
+
*/
|
|
2040
|
+
declare function timing(options?: TimingOptions): (ctx: Context) => void;
|
|
2041
|
+
|
|
2042
|
+
/**
|
|
2043
|
+
* Ken Framework - WSClient Protocol Middleware
|
|
2044
|
+
* Transparently handles the Ken Binary WebSocket Protocol (KBWP) for WSClient users.
|
|
2045
|
+
*
|
|
2046
|
+
* Includes:
|
|
2047
|
+
* - wsClientProtocol: registers a WebSocket route with full KBWP support
|
|
2048
|
+
*
|
|
2049
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
2050
|
+
*/
|
|
2051
|
+
|
|
2052
|
+
/**
|
|
2053
|
+
* Combined handler + options definition passed to {@link wsClientProtocol}.
|
|
2054
|
+
*
|
|
2055
|
+
* Merges all {@link WsHandler} callbacks and {@link WsOptions} fields into
|
|
2056
|
+
* a single object so there is only one place to configure a WebSocket route.
|
|
2057
|
+
*
|
|
2058
|
+
* @template T - Per-connection data type
|
|
2059
|
+
*/
|
|
2060
|
+
interface WsDefinition<T = unknown> extends WsHandler<T> {
|
|
2061
|
+
/** @see WsOptions.maxPayloadLength */
|
|
2062
|
+
maxPayloadLength?: number;
|
|
2063
|
+
/** @see WsOptions.backpressureLimit */
|
|
2064
|
+
backpressureLimit?: number;
|
|
2065
|
+
/** @see WsOptions.pingInterval */
|
|
2066
|
+
pingInterval?: number;
|
|
2067
|
+
/** @see WsOptions.pongTimeout */
|
|
2068
|
+
pongTimeout?: number;
|
|
2069
|
+
/** @see WsOptions.perMessageDeflate */
|
|
2070
|
+
perMessageDeflate?: boolean;
|
|
2071
|
+
/** @see WsOptions.idleTimeout */
|
|
2072
|
+
idleTimeout?: number;
|
|
2073
|
+
/**
|
|
2074
|
+
* Authentication handler. When provided, all connections must authenticate
|
|
2075
|
+
* via post-connect AUTH binary frame or URL query parameter (dev/staging).
|
|
2076
|
+
*
|
|
2077
|
+
* Called with the token string. Return user data (`T`) on success,
|
|
2078
|
+
* or `null` to reject the connection.
|
|
2079
|
+
*
|
|
2080
|
+
* When not provided, auth is disabled — all connections pass through.
|
|
2081
|
+
*
|
|
2082
|
+
* @example
|
|
2083
|
+
* ```ts
|
|
2084
|
+
* wsClientProtocol<{ userId: string }>({
|
|
2085
|
+
* authenticate: async (token) => {
|
|
2086
|
+
* const user = await verifyJwt(token);
|
|
2087
|
+
* if (!user) return null;
|
|
2088
|
+
* return { userId: user.id };
|
|
2089
|
+
* },
|
|
2090
|
+
* message(peer, msg) {
|
|
2091
|
+
* // peer.data.userId is available here
|
|
2092
|
+
* },
|
|
2093
|
+
* });
|
|
2094
|
+
* ```
|
|
2095
|
+
*/
|
|
2096
|
+
authenticate?: (token: string) => T | null | Promise<T | null>;
|
|
2097
|
+
/**
|
|
2098
|
+
* URL query parameter name for token-based auth (dev/staging support).
|
|
2099
|
+
* When a connection URL contains this parameter, the token is validated
|
|
2100
|
+
* during upgrade and the connection is pre-authenticated.
|
|
2101
|
+
* Only used when `authenticate` is provided.
|
|
2102
|
+
* @default 'token'
|
|
2103
|
+
*/
|
|
2104
|
+
tokenParam?: string;
|
|
2105
|
+
}
|
|
2106
|
+
/**
|
|
2107
|
+
* WebSocket middleware with full WSClient binary protocol (KBWP) support.
|
|
2108
|
+
*
|
|
2109
|
+
* Registers a WebSocket route at '/' and transparently handles the Ken
|
|
2110
|
+
* Binary WebSocket Protocol — heartbeat (ping/pong), pub/sub, and
|
|
2111
|
+
* request-response — so your `message` handler only receives domain messages.
|
|
2112
|
+
*
|
|
2113
|
+
* Returns an `App` instance so it composes naturally with `app.use()`.
|
|
2114
|
+
* The path is supplied via the `app.use()` call, which prepends it to the
|
|
2115
|
+
* WebSocket route.
|
|
2116
|
+
*
|
|
2117
|
+
* **Binary protocol messages intercepted (client → server):**
|
|
2118
|
+
* - PING (0x01) → responds with PONG (0x02)
|
|
2119
|
+
* - SUBSCRIBE (0x05) → calls `peer.subscribe(topic)`
|
|
2120
|
+
* - UNSUBSCRIBE (0x06) → calls `peer.unsubscribe(topic)`
|
|
2121
|
+
* - PUBLISH (0x07) → calls `peer.publish(topic, jsonEnvelope)`
|
|
2122
|
+
* so all topic subscribers receive a `{"type":"message",...}` envelope
|
|
2123
|
+
* that `WSClient` knows how to route.
|
|
2124
|
+
* - REQUEST (0x03) → calls user `message` handler with decoded payload;
|
|
2125
|
+
* the first `peer.send()` is automatically wrapped in a RESPONSE (0x04)
|
|
2126
|
+
* frame echoing the correlation ID.
|
|
2127
|
+
*
|
|
2128
|
+
* **Non-binary messages** (plain strings) are forwarded directly to the
|
|
2129
|
+
* user `message` handler without any protocol interception.
|
|
2130
|
+
*
|
|
2131
|
+
* All `WsOptions` fields can be co-located in the `config` object.
|
|
2132
|
+
*
|
|
2133
|
+
* @example
|
|
2134
|
+
* ```ts
|
|
2135
|
+
* // Mount at a path (no auth)
|
|
2136
|
+
* app.use('/chat', wsClientProtocol({
|
|
2137
|
+
* pingInterval: 20,
|
|
2138
|
+
* idleTimeout: 60,
|
|
2139
|
+
* upgrade(req) {
|
|
2140
|
+
* const name = new URL(req.url).searchParams.get('name') ?? 'anon';
|
|
2141
|
+
* return { username: name };
|
|
2142
|
+
* },
|
|
2143
|
+
* open(peer) { peer.subscribe('chat'); },
|
|
2144
|
+
* message(peer, msg) { peer.send(`echo: ${msg}`); },
|
|
2145
|
+
* close(peer) { peer.unsubscribe('chat'); },
|
|
2146
|
+
* }));
|
|
2147
|
+
*
|
|
2148
|
+
* // With post-connect binary auth (secure, invisible in logs)
|
|
2149
|
+
* app.use('/ws', wsClientProtocol<{ userId: string }>({
|
|
2150
|
+
* authenticate: async (token) => {
|
|
2151
|
+
* const user = await verifyJwt(token);
|
|
2152
|
+
* return user ? { userId: user.id } : null;
|
|
2153
|
+
* },
|
|
2154
|
+
* // tokenParam: 'token', // also accepts ?token= in URL (for Insomnia/dev)
|
|
2155
|
+
* open(peer) { console.log('authed:', peer.data.userId); },
|
|
2156
|
+
* message(peer, msg) { peer.send(msg); },
|
|
2157
|
+
* }));
|
|
2158
|
+
* ```
|
|
2159
|
+
*
|
|
2160
|
+
* @template T - Per-connection data type
|
|
2161
|
+
* @param config - Handler callbacks and optional WsOptions, co-located in one object
|
|
2162
|
+
* @returns An `App` instance ready to be passed to `app.use()`
|
|
2163
|
+
*/
|
|
2164
|
+
declare function wsClientProtocol<T = unknown>(config: WsDefinition<T>): App;
|
|
2165
|
+
|
|
2166
|
+
/**
|
|
2167
|
+
* Ken Framework - Binary WebSocket Protocol Codec (KBWP)
|
|
2168
|
+
*
|
|
2169
|
+
* Compact binary framing for WebSocket messages optimized for bandwidth-
|
|
2170
|
+
* constrained environments (4G, IoT, mobile). Eliminates JSON protocol
|
|
2171
|
+
* overhead while preserving full pub/sub and request-response semantics.
|
|
2172
|
+
*
|
|
2173
|
+
* Frame layout (all multi-byte integers are big-endian):
|
|
2174
|
+
*
|
|
2175
|
+
* ┌──────────┬──────────────────────────────────────────┐
|
|
2176
|
+
* │ Type (1B)│ Type-specific fields (variable) │
|
|
2177
|
+
* └──────────┴──────────────────────────────────────────┘
|
|
2178
|
+
*
|
|
2179
|
+
* Message types:
|
|
2180
|
+
* 0x01 PING [] 1 byte
|
|
2181
|
+
* 0x02 PONG [] 1 byte
|
|
2182
|
+
* 0x03 REQUEST [corrId:u32] [payload…] 5+ bytes
|
|
2183
|
+
* 0x04 RESPONSE [corrId:u32] [payload…] 5+ bytes
|
|
2184
|
+
* 0x05 SUBSCRIBE [topicLen:u8] [topic…] 2+ bytes
|
|
2185
|
+
* 0x06 UNSUBSCRIBE [topicLen:u8] [topic…] 2+ bytes
|
|
2186
|
+
* 0x07 PUBLISH [topicLen:u8] [topic…] [payload…] 2+ bytes
|
|
2187
|
+
* 0x08 MESSAGE [topicLen:u8] [topic…] [payload…] 2+ bytes
|
|
2188
|
+
* 0x09 AUTH [payload…] 1+ bytes
|
|
2189
|
+
* 0x0A AUTH_OK [payload…] 1+ bytes
|
|
2190
|
+
* 0x0B AUTH_FAIL [payload…] 1+ bytes
|
|
2191
|
+
*
|
|
2192
|
+
* Payload is the remaining bytes after fixed-length fields — no length
|
|
2193
|
+
* prefix needed because WebSocket frames are already length-delimited.
|
|
2194
|
+
*
|
|
2195
|
+
* Bandwidth comparison vs JSON:
|
|
2196
|
+
* Ping: 1 byte vs 15 bytes ({"type":"ping"}) → 93% reduction
|
|
2197
|
+
* Subscribe: ~6 bytes vs ~35 bytes → 83% reduction
|
|
2198
|
+
* Request: 5 + N vs 20 + N (JSON _kid overhead) → significant
|
|
2199
|
+
*
|
|
2200
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
2201
|
+
*/
|
|
2202
|
+
/** Binary protocol message type identifiers */
|
|
2203
|
+
declare const MsgType: {
|
|
2204
|
+
readonly PING: 1;
|
|
2205
|
+
readonly PONG: 2;
|
|
2206
|
+
readonly REQUEST: 3;
|
|
2207
|
+
readonly RESPONSE: 4;
|
|
2208
|
+
readonly SUBSCRIBE: 5;
|
|
2209
|
+
readonly UNSUBSCRIBE: 6;
|
|
2210
|
+
readonly PUBLISH: 7;
|
|
2211
|
+
readonly MESSAGE: 8;
|
|
2212
|
+
readonly AUTH: 9;
|
|
2213
|
+
readonly AUTH_OK: 10;
|
|
2214
|
+
readonly AUTH_FAIL: 11;
|
|
2215
|
+
};
|
|
2216
|
+
type MsgTypeValue = (typeof MsgType)[keyof typeof MsgType];
|
|
2217
|
+
interface DecodedPing {
|
|
2218
|
+
readonly type: typeof MsgType.PING;
|
|
2219
|
+
}
|
|
2220
|
+
interface DecodedPong {
|
|
2221
|
+
readonly type: typeof MsgType.PONG;
|
|
2222
|
+
}
|
|
2223
|
+
interface DecodedRequest {
|
|
2224
|
+
readonly type: typeof MsgType.REQUEST;
|
|
2225
|
+
readonly corrId: number;
|
|
2226
|
+
/** Raw JSON string payload (caller parses if needed) */
|
|
2227
|
+
readonly payload: string;
|
|
2228
|
+
}
|
|
2229
|
+
interface DecodedResponse {
|
|
2230
|
+
readonly type: typeof MsgType.RESPONSE;
|
|
2231
|
+
readonly corrId: number;
|
|
2232
|
+
/** Raw JSON string payload (caller parses if needed) */
|
|
2233
|
+
readonly payload: string;
|
|
2234
|
+
}
|
|
2235
|
+
interface DecodedSubscribe {
|
|
2236
|
+
readonly type: typeof MsgType.SUBSCRIBE;
|
|
2237
|
+
readonly topic: string;
|
|
2238
|
+
}
|
|
2239
|
+
interface DecodedUnsubscribe {
|
|
2240
|
+
readonly type: typeof MsgType.UNSUBSCRIBE;
|
|
2241
|
+
readonly topic: string;
|
|
2242
|
+
}
|
|
2243
|
+
interface DecodedPublish {
|
|
2244
|
+
readonly type: typeof MsgType.PUBLISH;
|
|
2245
|
+
readonly topic: string;
|
|
2246
|
+
/** Raw JSON string payload (caller parses if needed) */
|
|
2247
|
+
readonly payload: string;
|
|
2248
|
+
}
|
|
2249
|
+
interface DecodedMessage {
|
|
2250
|
+
readonly type: typeof MsgType.MESSAGE;
|
|
2251
|
+
readonly topic: string;
|
|
2252
|
+
/** Raw JSON string payload (caller parses if needed) */
|
|
2253
|
+
readonly payload: string;
|
|
2254
|
+
}
|
|
2255
|
+
interface DecodedAuth {
|
|
2256
|
+
readonly type: typeof MsgType.AUTH;
|
|
2257
|
+
/** Auth token */
|
|
2258
|
+
readonly payload: string;
|
|
2259
|
+
}
|
|
2260
|
+
interface DecodedAuthOk {
|
|
2261
|
+
readonly type: typeof MsgType.AUTH_OK;
|
|
2262
|
+
/** Optional data from server (may be empty) */
|
|
2263
|
+
readonly payload: string;
|
|
2264
|
+
}
|
|
2265
|
+
interface DecodedAuthFail {
|
|
2266
|
+
readonly type: typeof MsgType.AUTH_FAIL;
|
|
2267
|
+
/** Reason string */
|
|
2268
|
+
readonly payload: string;
|
|
2269
|
+
}
|
|
2270
|
+
type DecodedFrame = DecodedPing | DecodedPong | DecodedRequest | DecodedResponse | DecodedSubscribe | DecodedUnsubscribe | DecodedPublish | DecodedMessage | DecodedAuth | DecodedAuthOk | DecodedAuthFail;
|
|
2271
|
+
/** Returns the pre-allocated 1-byte PING frame. Do NOT mutate. */
|
|
2272
|
+
declare function encodePing(): Uint8Array;
|
|
2273
|
+
/** Returns the pre-allocated 1-byte PONG frame. Do NOT mutate. */
|
|
2274
|
+
declare function encodePong(): Uint8Array;
|
|
2275
|
+
/**
|
|
2276
|
+
* Encode a REQUEST frame: [0x03][corrId:u32][payload UTF-8…]
|
|
2277
|
+
*
|
|
2278
|
+
* @param corrId - Correlation ID (uint32, 0–4294967295)
|
|
2279
|
+
* @param payload - JSON string payload
|
|
2280
|
+
*/
|
|
2281
|
+
declare function encodeRequest(corrId: number, payload: string): Uint8Array;
|
|
2282
|
+
/**
|
|
2283
|
+
* Encode a RESPONSE frame: [0x04][corrId:u32][payload UTF-8…]
|
|
2284
|
+
*
|
|
2285
|
+
* @param corrId - Correlation ID echoed from the REQUEST
|
|
2286
|
+
* @param payload - Response data (string or binary)
|
|
2287
|
+
*/
|
|
2288
|
+
declare function encodeResponse(corrId: number, payload: string | Uint8Array): Uint8Array;
|
|
2289
|
+
/**
|
|
2290
|
+
* Encode a SUBSCRIBE frame: [0x05][topicLen:u8][topic UTF-8…]
|
|
2291
|
+
*
|
|
2292
|
+
* @param topic - Topic name (max 255 UTF-8 bytes)
|
|
2293
|
+
*/
|
|
2294
|
+
declare function encodeSubscribe(topic: string): Uint8Array;
|
|
2295
|
+
/**
|
|
2296
|
+
* Encode an UNSUBSCRIBE frame: [0x06][topicLen:u8][topic UTF-8…]
|
|
2297
|
+
*
|
|
2298
|
+
* @param topic - Topic name (max 255 UTF-8 bytes)
|
|
2299
|
+
*/
|
|
2300
|
+
declare function encodeUnsubscribe(topic: string): Uint8Array;
|
|
2301
|
+
/**
|
|
2302
|
+
* Encode a PUBLISH frame: [0x07][topicLen:u8][topic UTF-8…][payload UTF-8…]
|
|
2303
|
+
*
|
|
2304
|
+
* @param topic - Topic name (max 255 UTF-8 bytes)
|
|
2305
|
+
* @param payload - JSON string payload
|
|
2306
|
+
*/
|
|
2307
|
+
declare function encodePublish(topic: string, payload: string): Uint8Array;
|
|
2308
|
+
/**
|
|
2309
|
+
* Encode a MESSAGE frame: [0x08][topicLen:u8][topic UTF-8…][payload UTF-8…]
|
|
2310
|
+
*
|
|
2311
|
+
* @param topic - Topic name (max 255 UTF-8 bytes)
|
|
2312
|
+
* @param payload - JSON string payload
|
|
2313
|
+
*/
|
|
2314
|
+
declare function encodeMessage(topic: string, payload: string): Uint8Array;
|
|
2315
|
+
/**
|
|
2316
|
+
* Decode a binary WebSocket frame into a typed message.
|
|
2317
|
+
*
|
|
2318
|
+
* @param data - Raw binary data (ArrayBuffer or Uint8Array)
|
|
2319
|
+
* @returns Decoded frame, or `null` if the type byte is unrecognized or
|
|
2320
|
+
* the frame is too short for the declared type.
|
|
2321
|
+
*/
|
|
2322
|
+
declare function decode(data: ArrayBuffer | Uint8Array): DecodedFrame | null;
|
|
2323
|
+
/**
|
|
2324
|
+
* Check if a binary buffer starts with a known Ken protocol type byte.
|
|
2325
|
+
* Use this as a fast pre-check before calling `decode()`.
|
|
2326
|
+
*
|
|
2327
|
+
* @param data - Binary data (first byte is checked)
|
|
2328
|
+
* @returns true if byte 0 is a recognized MsgType
|
|
2329
|
+
*/
|
|
2330
|
+
declare function isKenBinaryFrame(data: ArrayBuffer | Uint8Array): boolean;
|
|
2331
|
+
|
|
2332
|
+
/**
|
|
2333
|
+
* Ken Framework - WSClient
|
|
2334
|
+
* High-performance, fault-tolerant WebSocket client with binary protocol.
|
|
2335
|
+
* Zero-dependency. Works on Bun, Deno, and Node.js.
|
|
2336
|
+
*
|
|
2337
|
+
* Uses the Ken Binary WebSocket Protocol (KBWP) for minimal bandwidth
|
|
2338
|
+
* overhead — optimized for 24/7 operation on bandwidth-constrained devices.
|
|
2339
|
+
*
|
|
2340
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
2341
|
+
*/
|
|
2342
|
+
|
|
2343
|
+
/** Connection state enum — use numeric values for fast comparison */
|
|
2344
|
+
declare const WsClientState: {
|
|
2345
|
+
readonly CONNECTING: 0;
|
|
2346
|
+
readonly OPEN: 1;
|
|
2347
|
+
readonly CLOSING: 2;
|
|
2348
|
+
readonly CLOSED: 3;
|
|
2349
|
+
readonly RECONNECTING: 4;
|
|
2350
|
+
};
|
|
2351
|
+
type WsClientStateValue = (typeof WsClientState)[keyof typeof WsClientState];
|
|
2352
|
+
/**
|
|
2353
|
+
* WSClient configuration options
|
|
2354
|
+
*/
|
|
2355
|
+
interface WsClientOptions {
|
|
2356
|
+
/**
|
|
2357
|
+
* Auth token. Sent as a binary AUTH frame (post-connect, secure)
|
|
2358
|
+
* or as a `?token=` URL param depending on `authMode`.
|
|
2359
|
+
*/
|
|
2360
|
+
token?: string;
|
|
2361
|
+
/**
|
|
2362
|
+
* How to send the auth token:
|
|
2363
|
+
* - `'message'` — Post-connect binary AUTH frame (secure, invisible in logs). Default.
|
|
2364
|
+
* - `'query'` — URL query parameter (visible in logs, for dev tools like Insomnia).
|
|
2365
|
+
*
|
|
2366
|
+
* Only relevant when `token` is set. Ignored otherwise.
|
|
2367
|
+
* @default 'message'
|
|
2368
|
+
*/
|
|
2369
|
+
authMode?: 'message' | 'query';
|
|
2370
|
+
/**
|
|
2371
|
+
* Milliseconds to wait for AUTH_OK after sending AUTH frame.
|
|
2372
|
+
* Only used when `authMode` is `'message'`.
|
|
2373
|
+
* @default 10_000
|
|
2374
|
+
*/
|
|
2375
|
+
authTimeout?: number;
|
|
2376
|
+
/**
|
|
2377
|
+
* Maximum number of reconnect attempts. Default: Infinity.
|
|
2378
|
+
*/
|
|
2379
|
+
maxRetries?: number;
|
|
2380
|
+
/**
|
|
2381
|
+
* Multiplier applied to the backoff delay after each failed attempt.
|
|
2382
|
+
* Default: 2
|
|
2383
|
+
*/
|
|
2384
|
+
backoffFactor?: number;
|
|
2385
|
+
/**
|
|
2386
|
+
* Maximum backoff delay in ms. Default: 30_000
|
|
2387
|
+
*/
|
|
2388
|
+
backoffMax?: number;
|
|
2389
|
+
/**
|
|
2390
|
+
* Base reconnect delay in ms. Default: 500
|
|
2391
|
+
*/
|
|
2392
|
+
backoffBase?: number;
|
|
2393
|
+
/**
|
|
2394
|
+
* Milliseconds between application-level heartbeat pings.
|
|
2395
|
+
* Set to 0 to disable. Default: 30_000
|
|
2396
|
+
*/
|
|
2397
|
+
heartbeatInterval?: number;
|
|
2398
|
+
/**
|
|
2399
|
+
* Milliseconds before a `sendWait()` call times out. Default: 10_000
|
|
2400
|
+
*/
|
|
2401
|
+
requestTimeout?: number;
|
|
2402
|
+
/**
|
|
2403
|
+
* Event hook — called for every incoming binary protocol frame.
|
|
2404
|
+
* Receives the decoded frame object. Fires after internal routing
|
|
2405
|
+
* (REQUEST/RESPONSE correlation, pub/sub dispatch) has been applied.
|
|
2406
|
+
*/
|
|
2407
|
+
onFrame?: (frame: DecodedFrame) => void;
|
|
2408
|
+
/**
|
|
2409
|
+
* Event hook — called when the connection is established.
|
|
2410
|
+
*/
|
|
2411
|
+
onConnect?: () => void;
|
|
2412
|
+
/**
|
|
2413
|
+
* Event hook — called when the connection is lost.
|
|
2414
|
+
* Receives the WebSocket close code and reason.
|
|
2415
|
+
*/
|
|
2416
|
+
onDisconnect?: (code: number, reason: string) => void;
|
|
2417
|
+
/**
|
|
2418
|
+
* Event hook — called before each reconnect attempt.
|
|
2419
|
+
* Receives the attempt number (1-based) and the delay in ms.
|
|
2420
|
+
*/
|
|
2421
|
+
onReconnectAttempt?: (attempt: number, delay: number) => void;
|
|
2422
|
+
/**
|
|
2423
|
+
* Event hook — called for every incoming message.
|
|
2424
|
+
*/
|
|
2425
|
+
onData?: (data: string | ArrayBuffer) => void;
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Ken WSClient — A fault-tolerant, high-performance WebSocket client
|
|
2429
|
+
* with binary protocol transport.
|
|
2430
|
+
*
|
|
2431
|
+
* Features:
|
|
2432
|
+
* - Ken Binary WebSocket Protocol (KBWP) — 80-93% bandwidth reduction
|
|
2433
|
+
* over JSON for protocol messages (ping/pong, subscribe, publish)
|
|
2434
|
+
* - State machine (CONNECTING / OPEN / CLOSING / CLOSED / RECONNECTING)
|
|
2435
|
+
* - Exponential backoff + jitter reconnection
|
|
2436
|
+
* - 1-byte binary heartbeat (ping/pong)
|
|
2437
|
+
* - Request-response via `sendWait()` with uint32 correlation IDs
|
|
2438
|
+
* (no field collision — corrId lives in binary header, not payload)
|
|
2439
|
+
* - Pub/sub with binary framing (subscribe/unsubscribe/publish/message)
|
|
2440
|
+
* - Full lifecycle hooks: onConnect, onDisconnect, onReconnectAttempt, onData
|
|
2441
|
+
* - Zero memory leaks — all timers and listeners are strictly cleaned up
|
|
2442
|
+
*
|
|
2443
|
+
* @example
|
|
2444
|
+
* ```ts
|
|
2445
|
+
* const client = new WSClient('wss://api.example.com/ws', {
|
|
2446
|
+
* token: 'my-jwt',
|
|
2447
|
+
* onConnect: () => console.log('Connected'),
|
|
2448
|
+
* onDisconnect: (code) => console.log('Disconnected', code),
|
|
2449
|
+
* });
|
|
2450
|
+
*
|
|
2451
|
+
* await client.connect();
|
|
2452
|
+
*
|
|
2453
|
+
* // Fire-and-forget (raw string — not wrapped in binary protocol)
|
|
2454
|
+
* client.send('hello');
|
|
2455
|
+
*
|
|
2456
|
+
* // Request-response (binary REQUEST frame, no _kid collision)
|
|
2457
|
+
* const result = await client.sendWait({ type: 'getData', id: 42 });
|
|
2458
|
+
*
|
|
2459
|
+
* // Pub/sub (binary SUBSCRIBE/PUBLISH frames)
|
|
2460
|
+
* client.subscribe('events', (data) => console.log(data));
|
|
2461
|
+
* client.publish('events', { action: 'click' });
|
|
2462
|
+
*
|
|
2463
|
+
* await client.close();
|
|
2464
|
+
* ```
|
|
2465
|
+
*/
|
|
2466
|
+
declare class WSClient {
|
|
2467
|
+
/** Current state of the connection */
|
|
2468
|
+
state: WsClientStateValue;
|
|
2469
|
+
private readonly _url;
|
|
2470
|
+
private readonly _authTimeout;
|
|
2471
|
+
private readonly _maxRetries;
|
|
2472
|
+
private readonly _backoffFactor;
|
|
2473
|
+
private readonly _backoffMax;
|
|
2474
|
+
private readonly _backoffBase;
|
|
2475
|
+
private readonly _heartbeatInterval;
|
|
2476
|
+
private readonly _requestTimeout;
|
|
2477
|
+
private readonly _onConnect;
|
|
2478
|
+
private readonly _onDisconnect;
|
|
2479
|
+
private readonly _onReconnectAttempt;
|
|
2480
|
+
private readonly _onData;
|
|
2481
|
+
private readonly _onFrame;
|
|
2482
|
+
/** Active WebSocket instance */
|
|
2483
|
+
private _ws;
|
|
2484
|
+
/** Number of consecutive failed reconnect attempts */
|
|
2485
|
+
private _retries;
|
|
2486
|
+
/** Correlation registry: corrId (uint32) → PendingRequest */
|
|
2487
|
+
private _pending;
|
|
2488
|
+
/**
|
|
2489
|
+
* Active pub/sub subscriptions: topic → callback.
|
|
2490
|
+
* Persists across reconnects so topics are re-sent in _onOpen.
|
|
2491
|
+
*/
|
|
2492
|
+
private _subscriptions;
|
|
2493
|
+
/** Heartbeat timer handle */
|
|
2494
|
+
private _heartbeatTimer;
|
|
2495
|
+
/** Reconnect delay timer handle */
|
|
2496
|
+
private _reconnectTimer;
|
|
2497
|
+
/** Whether destroy() / close() was explicitly called */
|
|
2498
|
+
private _destroyed;
|
|
2499
|
+
/** Post-connect auth resolve/reject for the connect() promise */
|
|
2500
|
+
private _authResolve;
|
|
2501
|
+
private _authReject;
|
|
2502
|
+
/** Auth timeout timer handle */
|
|
2503
|
+
private _authTimer;
|
|
2504
|
+
/** Whether auth is pending (post-connect, waiting for AUTH_OK/AUTH_FAIL) */
|
|
2505
|
+
private _authPending;
|
|
2506
|
+
/** Pre-encoded AUTH frame — zero per-reconnect allocation */
|
|
2507
|
+
private readonly _authFrame;
|
|
2508
|
+
/**
|
|
2509
|
+
* Monotonically increasing uint32 counter for correlation IDs.
|
|
2510
|
+
* Wraps at 0xFFFFFFFF to stay within 4-byte range.
|
|
2511
|
+
*/
|
|
2512
|
+
private _idCounter;
|
|
2513
|
+
private readonly _handleOpen;
|
|
2514
|
+
private readonly _handleMessage;
|
|
2515
|
+
private readonly _handleClose;
|
|
2516
|
+
private readonly _handleError;
|
|
2517
|
+
constructor(url: string, options?: WsClientOptions);
|
|
2518
|
+
/**
|
|
2519
|
+
* Connect to the WebSocket server.
|
|
2520
|
+
* Returns a Promise that resolves when the connection is established.
|
|
2521
|
+
* Safe to call multiple times — subsequent calls while already OPEN are no-ops.
|
|
2522
|
+
*/
|
|
2523
|
+
connect(): Promise<void>;
|
|
2524
|
+
/**
|
|
2525
|
+
* Send a raw string or binary message. The connection must be OPEN.
|
|
2526
|
+
* For fire-and-forget use cases.
|
|
2527
|
+
*
|
|
2528
|
+
* @returns true if sent, false if not connected.
|
|
2529
|
+
*/
|
|
2530
|
+
send(data: string | ArrayBuffer | Uint8Array<ArrayBuffer>): boolean;
|
|
2531
|
+
/**
|
|
2532
|
+
* Send a JSON message and wait for a correlated response.
|
|
2533
|
+
*
|
|
2534
|
+
* Uses the binary REQUEST frame format: [0x03][corrId:u32][JSON payload].
|
|
2535
|
+
* The correlation ID is carried in the binary header — NOT mixed into the
|
|
2536
|
+
* user payload — eliminating the previous `_kid` field collision issue.
|
|
2537
|
+
*
|
|
2538
|
+
* The server must respond with a binary RESPONSE frame echoing the same
|
|
2539
|
+
* corrId. When using `defineWsHandler`, this is handled automatically.
|
|
2540
|
+
*
|
|
2541
|
+
* @param data - Object to JSON-serialize and send.
|
|
2542
|
+
* @param timeout - Override the default `requestTimeout` for this request.
|
|
2543
|
+
* @returns Promise resolving with the parsed response payload.
|
|
2544
|
+
* @throws If the connection is not open, the request times out, or the
|
|
2545
|
+
* connection closes before a response arrives.
|
|
2546
|
+
*/
|
|
2547
|
+
sendWait<T = unknown>(data: Record<string, unknown>, timeout?: number): Promise<T>;
|
|
2548
|
+
/**
|
|
2549
|
+
* Subscribe to a server-side topic.
|
|
2550
|
+
* Sends `{"type":"subscribe","topic":"..."}` to the server and registers a
|
|
2551
|
+
* local callback invoked for each `{"type":"message","topic":"...","data":...}`
|
|
2552
|
+
* frame received on that topic.
|
|
2553
|
+
*
|
|
2554
|
+
* Safe to call before `connect()` — the subscription is registered locally
|
|
2555
|
+
* and sent once the connection opens (or re-opens after reconnect).
|
|
2556
|
+
*
|
|
2557
|
+
* Calling with the same topic replaces the existing callback.
|
|
2558
|
+
*
|
|
2559
|
+
* @param topic - Topic name to subscribe to
|
|
2560
|
+
* @param callback - Called with the `data` field from each topic message
|
|
2561
|
+
*/
|
|
2562
|
+
subscribe(topic: string, callback: (data: unknown) => void): void;
|
|
2563
|
+
/**
|
|
2564
|
+
* Unsubscribe from a server-side topic.
|
|
2565
|
+
* Sends `{"type":"unsubscribe","topic":"..."}` if currently connected.
|
|
2566
|
+
* The local callback is removed regardless of connection state.
|
|
2567
|
+
*
|
|
2568
|
+
* @param topic - Topic name to unsubscribe from
|
|
2569
|
+
*/
|
|
2570
|
+
unsubscribe(topic: string): void;
|
|
2571
|
+
/**
|
|
2572
|
+
* Publish a message to a server-side topic.
|
|
2573
|
+
* Sends `{"type":"publish","topic":"...","data":...}` to the server.
|
|
2574
|
+
* The server (when using `defineWsHandler`) re-broadcasts the message to all
|
|
2575
|
+
* topic subscribers as `{"type":"message","topic":"...","data":...}`.
|
|
2576
|
+
*
|
|
2577
|
+
* @param topic - Topic to publish to
|
|
2578
|
+
* @param data - Payload (JSON-serializable)
|
|
2579
|
+
* @returns true if sent, false if not connected
|
|
2580
|
+
*/
|
|
2581
|
+
publish(topic: string, data: unknown): boolean;
|
|
2582
|
+
/**
|
|
2583
|
+
* Gracefully close the connection.
|
|
2584
|
+
* Does NOT reconnect. Cleans up all resources.
|
|
2585
|
+
*/
|
|
2586
|
+
close(code?: number, reason?: string): Promise<void>;
|
|
2587
|
+
private _openConnection;
|
|
2588
|
+
/** Open handler for auth mode — sends pre-encoded AUTH frame */
|
|
2589
|
+
private _onOpenAuth;
|
|
2590
|
+
/** Open handler for no-auth mode — proceeds directly to _finishOpen */
|
|
2591
|
+
private _onOpenNoAuth;
|
|
2592
|
+
/**
|
|
2593
|
+
* Complete the open sequence: re-subscribe, start heartbeat, fire onConnect.
|
|
2594
|
+
* Called directly from _onOpen (no auth) or from _onMessage after AUTH_OK.
|
|
2595
|
+
*/
|
|
2596
|
+
private _finishOpen;
|
|
2597
|
+
private _onMessage;
|
|
2598
|
+
private _onClose;
|
|
2599
|
+
private _onError;
|
|
2600
|
+
private _scheduleReconnect;
|
|
2601
|
+
/**
|
|
2602
|
+
* Exponential backoff with full jitter to avoid thundering-herd reconnects.
|
|
2603
|
+
* delay = random(0, min(backoffMax, backoffBase * backoffFactor^attempt))
|
|
2604
|
+
*/
|
|
2605
|
+
private _computeBackoff;
|
|
2606
|
+
private _clearReconnectTimer;
|
|
2607
|
+
private _startHeartbeat;
|
|
2608
|
+
private _stopHeartbeat;
|
|
2609
|
+
private _detachHandlers;
|
|
2610
|
+
/**
|
|
2611
|
+
* Returns a monotonically increasing uint32 correlation ID.
|
|
2612
|
+
* Wraps at 0xFFFFFFFF back to 1 (zero is reserved).
|
|
2613
|
+
* No random component needed — IDs are scoped to a single connection
|
|
2614
|
+
* and the uint32 space (4B values) far exceeds concurrent in-flight requests.
|
|
2615
|
+
*/
|
|
2616
|
+
private _nextId;
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
/**
|
|
2620
|
+
* Ken Framework - WebSocket Pub/Sub
|
|
2621
|
+
*
|
|
2622
|
+
* Two pub/sub abstractions:
|
|
2623
|
+
*
|
|
2624
|
+
* 1. PubSubHub — low-level exclude-sender pub/sub.
|
|
2625
|
+
* Used by Deno/Node peer adapters for WsPeer.subscribe / WsPeer.publish.
|
|
2626
|
+
* On Bun and uWS, native pub/sub is used directly by the peer adapters.
|
|
2627
|
+
*
|
|
2628
|
+
* 2. WsTopicHub — MQTT/Redis-style per-topic message routing.
|
|
2629
|
+
* Per-topic callbacks, include-all (broadcast) publish, dead peer detection.
|
|
2630
|
+
* Works uniformly across all runtimes.
|
|
2631
|
+
*
|
|
2632
|
+
* MIT License - Copyright (c) 2025 Indra Gunawan
|
|
2633
|
+
*/
|
|
2634
|
+
|
|
2635
|
+
/**
|
|
2636
|
+
* Per-topic message callback.
|
|
2637
|
+
* Invoked when hub.dispatch(peer, message) is called for a subscribed peer.
|
|
2638
|
+
*
|
|
2639
|
+
* @template T - Per-connection data type
|
|
2640
|
+
*/
|
|
2641
|
+
type TopicCallback<T = unknown> = (peer: WsPeer<T>, message: WsMessageData) => void | Promise<void>;
|
|
2642
|
+
/**
|
|
2643
|
+
* WsTopicHub — MQTT/Redis-style topic-based pub/sub for WebSocket servers.
|
|
2644
|
+
*
|
|
2645
|
+
* **Key differences from WsPeer.subscribe/publish:**
|
|
2646
|
+
* - `hub.subscribe(peer, topic, callback)` registers a per-topic message handler
|
|
2647
|
+
* - `hub.publish(topic, data)` broadcasts to ALL subscribers (including sender)
|
|
2648
|
+
* - `hub.dispatch(peer, msg)` routes a message to the peer's topic callbacks
|
|
2649
|
+
* - Dead peer detection via `markAlive()` + `startDeadPeerCheck()`
|
|
2650
|
+
* - Works uniformly across Bun, Node, Deno, and uWS
|
|
2651
|
+
*
|
|
2652
|
+
* Uses native runtime pub/sub (Bun/uWS `peer.subscribe/publish`) for "exclude self"
|
|
2653
|
+
* semantics when callers use `peer.publish()` directly. Hub publish() always uses
|
|
2654
|
+
* an in-memory iterator for include-all broadcast.
|
|
2655
|
+
*
|
|
2656
|
+
* @template T - Per-connection data type
|
|
2657
|
+
*
|
|
2658
|
+
* @example
|
|
2659
|
+
* ```ts
|
|
2660
|
+
* const hub = new WsTopicHub<{ username: string }>();
|
|
2661
|
+
*
|
|
2662
|
+
* app.ws<{ username: string }>('/chat', {
|
|
2663
|
+
* upgrade(req) {
|
|
2664
|
+
* const name = new URL(req.url).searchParams.get('name') ?? 'anon';
|
|
2665
|
+
* return { username: name };
|
|
2666
|
+
* },
|
|
2667
|
+
* open(peer) {
|
|
2668
|
+
* hub.subscribe(peer, 'chat', (peer, msg) => {
|
|
2669
|
+
* hub.publish('chat', `${peer.data.username}: ${msg}`);
|
|
2670
|
+
* });
|
|
2671
|
+
* },
|
|
2672
|
+
* message(peer, msg) {
|
|
2673
|
+
* hub.dispatch(peer, msg); // routes to topic callbacks
|
|
2674
|
+
* },
|
|
2675
|
+
* close(peer) {
|
|
2676
|
+
* hub.leave(peer); // cleanup all subscriptions
|
|
2677
|
+
* },
|
|
2678
|
+
* pong(peer) {
|
|
2679
|
+
* hub.markAlive(peer); // dead peer detection (Bun/Node/uWS)
|
|
2680
|
+
* },
|
|
2681
|
+
* }, { pingInterval: 30 });
|
|
2682
|
+
*
|
|
2683
|
+
* // Server-initiated broadcast (from any route or CRON)
|
|
2684
|
+
* hub.publish('chat', 'System: restarting in 60s');
|
|
2685
|
+
*
|
|
2686
|
+
* // Start automatic dead peer cleanup
|
|
2687
|
+
* hub.startDeadPeerCheck(5_000, 120_000);
|
|
2688
|
+
* ```
|
|
2689
|
+
*/
|
|
2690
|
+
declare class WsTopicHub<T = unknown> {
|
|
2691
|
+
/** topic → Set<peer> — O(1) publish iteration */
|
|
2692
|
+
private _topics;
|
|
2693
|
+
/** peer → Map<topic, callback> — O(1) dispatch and cleanup */
|
|
2694
|
+
private _peerTopics;
|
|
2695
|
+
/** peer → last-pong timestamp (ms) — for dead peer detection */
|
|
2696
|
+
private _lastPong;
|
|
2697
|
+
private _deadPeerTimer;
|
|
2698
|
+
/**
|
|
2699
|
+
* Subscribe a peer to a topic with a per-message callback.
|
|
2700
|
+
*
|
|
2701
|
+
* The callback is invoked whenever `hub.dispatch(peer, msg)` is called while
|
|
2702
|
+
* the peer is subscribed to this topic.
|
|
2703
|
+
*
|
|
2704
|
+
* A peer may be subscribed to multiple topics simultaneously, each with its
|
|
2705
|
+
* own callback. Subsequent calls with the same topic replace the handler.
|
|
2706
|
+
*
|
|
2707
|
+
* @param peer - The connected WebSocket peer
|
|
2708
|
+
* @param topic - Topic name (e.g. 'chat', 'notifications', 'room/42')
|
|
2709
|
+
* @param handler - Called with (peer, message) on each dispatched message
|
|
2710
|
+
*/
|
|
2711
|
+
subscribe(peer: WsPeer<T>, topic: string, handler: TopicCallback<T>): void;
|
|
2712
|
+
/**
|
|
2713
|
+
* Unsubscribe a peer from a specific topic.
|
|
2714
|
+
* The peer remains subscribed to any other topics.
|
|
2715
|
+
*/
|
|
2716
|
+
unsubscribe(peer: WsPeer<T>, topic: string): void;
|
|
2717
|
+
/**
|
|
2718
|
+
* Remove a peer from ALL topics and clean up liveness state.
|
|
2719
|
+
* Call this from `WsHandler.close()` to prevent memory leaks.
|
|
2720
|
+
*/
|
|
2721
|
+
leave(peer: WsPeer<T>): void;
|
|
2722
|
+
/**
|
|
2723
|
+
* Route an incoming message from a peer to all its registered topic handlers.
|
|
2724
|
+
* Call this from `WsHandler.message()`.
|
|
2725
|
+
*
|
|
2726
|
+
* If the peer is subscribed to multiple topics, the message is dispatched
|
|
2727
|
+
* to each topic's callback in insertion order.
|
|
2728
|
+
*/
|
|
2729
|
+
dispatch(peer: WsPeer<T>, msg: WsMessageData): void;
|
|
2730
|
+
/**
|
|
2731
|
+
* Broadcast a message to ALL subscribers of a topic, including the sender.
|
|
2732
|
+
*
|
|
2733
|
+
* For "exclude self" semantics (the sender does not receive their own message),
|
|
2734
|
+
* use `peer.publish(topic, data)` instead — which on Bun and uWS routes through
|
|
2735
|
+
* the native C++ pub/sub engine.
|
|
2736
|
+
*/
|
|
2737
|
+
publish(topic: string, data: WsMessageData, compress?: boolean): void;
|
|
2738
|
+
/**
|
|
2739
|
+
* Mark a peer as alive. Call from `WsHandler.pong()`.
|
|
2740
|
+
*
|
|
2741
|
+
* Works with native runtime protocol ping/pong (Bun, Node, uWS).
|
|
2742
|
+
* On Deno, calling this has no effect as `handler.pong` is never triggered.
|
|
2743
|
+
*/
|
|
2744
|
+
markAlive(peer: WsPeer<T>): void;
|
|
2745
|
+
/**
|
|
2746
|
+
* Returns true if the peer last responded within `timeoutMs` milliseconds.
|
|
2747
|
+
*/
|
|
2748
|
+
isPeerAlive(peer: WsPeer<T>, timeoutMs: number): boolean;
|
|
2749
|
+
/**
|
|
2750
|
+
* Close and remove all peers that have not called `markAlive()` within `timeoutMs`.
|
|
2751
|
+
* Recommended value: `(pingInterval + pongTimeout) * 1000`.
|
|
2752
|
+
*/
|
|
2753
|
+
pruneDeadPeers(timeoutMs: number): void;
|
|
2754
|
+
/**
|
|
2755
|
+
* Start automatic dead peer detection at a fixed interval.
|
|
2756
|
+
*
|
|
2757
|
+
* @param checkIntervalMs - How often to scan for dead peers (ms)
|
|
2758
|
+
* @param timeoutMs - Peers unseen longer than this are closed and removed
|
|
2759
|
+
*
|
|
2760
|
+
* @example
|
|
2761
|
+
* ```ts
|
|
2762
|
+
* // Check every 5 seconds; close peers silent for 2 minutes
|
|
2763
|
+
* hub.startDeadPeerCheck(5_000, 120_000);
|
|
2764
|
+
*
|
|
2765
|
+
* // Integrate with WsOptions:
|
|
2766
|
+
* // { pingInterval: 30, pongTimeout: 10 }
|
|
2767
|
+
* // → hub.startDeadPeerCheck(5_000, (30 + 10) * 1000);
|
|
2768
|
+
* ```
|
|
2769
|
+
*/
|
|
2770
|
+
startDeadPeerCheck(checkIntervalMs: number, timeoutMs: number): void;
|
|
2771
|
+
/**
|
|
2772
|
+
* Stop the automatic dead peer check started by `startDeadPeerCheck()`.
|
|
2773
|
+
*/
|
|
2774
|
+
stopDeadPeerCheck(): void;
|
|
2775
|
+
/** Number of peers currently subscribed to a topic. */
|
|
2776
|
+
subscriberCount(topic: string): number;
|
|
2777
|
+
/** Returns true if a peer is subscribed to the given topic. */
|
|
2778
|
+
isSubscribed(peer: WsPeer<T>, topic: string): boolean;
|
|
2779
|
+
/** Returns an array of all active topic names. */
|
|
2780
|
+
topicNames(): string[];
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
export { App, AppServer, type AppServerInit, type BasicAuthOptions, type BearerAuthOptions, type BodyLimitOptions, type CacheOptions, type CompressOptions$1 as CompressOptions, type CompressionEncoding, type Context, type CorsOptions, type CsrfOptions, type DecodedFrame, type DecompressOptions, type ETagOptions, type ErrorHandler, type FileEntry, type Flatten, type InferObject, type InferState, type InferValidator, type IpRestrictionOptions, type JWK, type JWKOptions, type JWKS, type JWTAlgorithm, type JWTOptions, type JWTPayload, type ListDirectoryOptions, type LoggerOptions, type MemoizeOptions, type MiddlewareHandler, MsgType, type MsgTypeValue, type ParamsFromPath, type ReceiveFileOptions, type RemoteInfo, type RequestIdOptions, type RouteInfo, type Schema, type SecureHeadersOptions, type SendFileOptions, type Server, type ServerOptions, type SessionOptions, type StateMiddleware, type TimeoutOptions, type TimingOptions, type TopicCallback, type TypedHandler, type UploadedFile, type Validator, WSClient, type WsClientOptions, WsClientState, type WsClientStateValue, type WsDefinition, type WsHandler, type WsMessageData, type WsOptions, type WsPeer, WsReadyState, type WsReadyStateValue, WsTopicHub, basicAuth, bearerAuth, bodyLimit, cache, compress, compressString, cors, csrf, decode as decodeFrame, decodeJwt, decompressString, decryptString, defaultErrorHandler, encodeMessage, encodePing, encodePong, encodePublish, encodeRequest, encodeResponse, encodeSubscribe, encodeUnsubscribe, encryptString, etag, generateSecretKey, getMimeType, getPathname, ipRestriction, isBun, isDeno, isKenBinaryFrame, isNode, jwk, jwt, listDirectory, logger, memoize, receiveFiles, requestId, saveFile, secureHeaders, sendFile, session, signJwt, timeout, timing, verifyJwt, wsClientProtocol };
|