@asenajs/hono-adapter 4.0.0 → 5.0.1
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 +5 -3
- package/dist/lib/HonoAdapter.d.ts +6 -22
- package/dist/lib/HonoAdapter.js +89 -90
- package/dist/lib/HonoAdapter.js.map +1 -1
- package/dist/lib/HonoContextWrapper.d.ts +18 -4
- package/dist/lib/HonoContextWrapper.js +202 -23
- package/dist/lib/HonoContextWrapper.js.map +1 -1
- package/dist/lib/defaults/Context.d.ts +5 -1
- package/dist/lib/defaults/Context.js +2 -1
- package/dist/lib/defaults/Context.js.map +1 -1
- package/dist/lib/defaults/MiddlewareService.d.ts +1 -1
- package/dist/lib/defaults/MiddlewareService.js +1 -0
- package/dist/lib/defaults/MiddlewareService.js.map +1 -1
- package/dist/lib/defaults/index.d.ts +1 -0
- package/dist/lib/defaults/index.js +1 -0
- package/dist/lib/defaults/index.js.map +1 -1
- package/dist/lib/defineContext.d.ts +8 -0
- package/dist/lib/defineContext.js +10 -0
- package/dist/lib/defineContext.js.map +1 -0
- package/dist/lib/middlewares/RateLimiterMiddleware.d.ts +73 -3
- package/dist/lib/middlewares/RateLimiterMiddleware.js +95 -7
- package/dist/lib/middlewares/RateLimiterMiddleware.js.map +1 -1
- package/dist/lib/middlewares/WebSocketOriginMiddleware.d.ts +81 -0
- package/dist/lib/middlewares/WebSocketOriginMiddleware.js +81 -0
- package/dist/lib/middlewares/WebSocketOriginMiddleware.js.map +1 -0
- package/dist/lib/utils/serveStatic.d.ts +3 -0
- package/dist/lib/utils/serveStatic.js +17 -0
- package/dist/lib/utils/serveStatic.js.map +1 -0
- package/package.json +4 -4
|
@@ -57,6 +57,9 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
57
57
|
};
|
|
58
58
|
import { MiddlewareService } from '../defaults';
|
|
59
59
|
import { OnStop } from '@asenajs/asena/decorators/ioc';
|
|
60
|
+
const MAX_FORWARDED_HEADER_LENGTH = 1024;
|
|
61
|
+
const IPV4_MAPPED_PREFIX = /^::ffff:(?=\d{1,3}(?:\.\d{1,3}){3}$)/i;
|
|
62
|
+
const unmapIPv4 = (address) => address.replace(IPV4_MAPPED_PREFIX, '');
|
|
60
63
|
/**
|
|
61
64
|
* Rate Limiter Middleware
|
|
62
65
|
*
|
|
@@ -122,6 +125,14 @@ export class RateLimiterMiddleware extends MiddlewareService {
|
|
|
122
125
|
* Client key generator function
|
|
123
126
|
*/
|
|
124
127
|
keyGenerator;
|
|
128
|
+
/**
|
|
129
|
+
* Trusted reverse proxies whose `X-Forwarded-For` may be believed
|
|
130
|
+
*/
|
|
131
|
+
trustProxy;
|
|
132
|
+
/**
|
|
133
|
+
* Upper bound on distinct client buckets held in memory
|
|
134
|
+
*/
|
|
135
|
+
maxBuckets;
|
|
125
136
|
/**
|
|
126
137
|
* Rate limit exceeded message
|
|
127
138
|
*/
|
|
@@ -146,6 +157,7 @@ export class RateLimiterMiddleware extends MiddlewareService {
|
|
|
146
157
|
* Bucket TTL in milliseconds
|
|
147
158
|
*/
|
|
148
159
|
bucketTTL;
|
|
160
|
+
warnedNoForwardedFor = false;
|
|
149
161
|
/**
|
|
150
162
|
* Creates a new rate limiter middleware instance
|
|
151
163
|
*
|
|
@@ -173,6 +185,11 @@ export class RateLimiterMiddleware extends MiddlewareService {
|
|
|
173
185
|
this.capacity = options.capacity ?? 100;
|
|
174
186
|
this.refillRate = options.refillRate ?? 10;
|
|
175
187
|
this.keyGenerator = options.keyGenerator ?? this.defaultKeyGenerator.bind(this);
|
|
188
|
+
this.trustProxy = options.trustProxy ?? false;
|
|
189
|
+
this.maxBuckets = options.maxBuckets ?? 10_000;
|
|
190
|
+
if (!Number.isInteger(this.maxBuckets) || this.maxBuckets < 1) {
|
|
191
|
+
throw new Error(`RateLimiterMiddleware: maxBuckets must be a positive integer, got ${this.maxBuckets}`);
|
|
192
|
+
}
|
|
176
193
|
this.message = options.message ?? 'Rate limit exceeded. Please try again later.';
|
|
177
194
|
this.statusCode = options.statusCode ?? 429;
|
|
178
195
|
this.cost = options.cost ?? 1;
|
|
@@ -208,13 +225,19 @@ export class RateLimiterMiddleware extends MiddlewareService {
|
|
|
208
225
|
const key = this.keyGenerator(context);
|
|
209
226
|
// Get or create bucket
|
|
210
227
|
let bucket = this.buckets.get(key);
|
|
211
|
-
if (
|
|
228
|
+
if (bucket) {
|
|
229
|
+
this.buckets.delete(key);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
212
232
|
bucket = {
|
|
213
233
|
tokens: this.capacity,
|
|
214
234
|
lastRefill: Date.now(),
|
|
215
235
|
};
|
|
216
|
-
this.buckets.
|
|
236
|
+
if (this.buckets.size >= this.maxBuckets) {
|
|
237
|
+
this.buckets.delete(this.buckets.keys().next().value);
|
|
238
|
+
}
|
|
217
239
|
}
|
|
240
|
+
this.buckets.set(key, bucket);
|
|
218
241
|
// Refill tokens based on elapsed time
|
|
219
242
|
const now = Date.now();
|
|
220
243
|
const elapsedSeconds = (now - bucket.lastRefill) / 1000;
|
|
@@ -286,16 +309,81 @@ export class RateLimiterMiddleware extends MiddlewareService {
|
|
|
286
309
|
this.buckets.clear();
|
|
287
310
|
}
|
|
288
311
|
/**
|
|
289
|
-
* Default key generator -
|
|
312
|
+
* Default key generator - the connecting peer, see `trustProxy`
|
|
290
313
|
*
|
|
291
314
|
* @param context - Hono context
|
|
292
315
|
* @returns Client identifier
|
|
293
316
|
*/
|
|
294
317
|
defaultKeyGenerator(context) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
318
|
+
return this.resolveClientKey(context);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Resolve the client identifier: the connecting peer, or the client hop of `X-Forwarded-For`
|
|
322
|
+
* when the peer is a trusted proxy
|
|
323
|
+
*
|
|
324
|
+
* @param context - Hono context
|
|
325
|
+
* @returns Client identifier
|
|
326
|
+
*/
|
|
327
|
+
resolveClientKey(context) {
|
|
328
|
+
const peer = context.getRequestIp();
|
|
329
|
+
if (!this.trusts(peer)) {
|
|
330
|
+
return peer ?? 'unknown';
|
|
331
|
+
}
|
|
332
|
+
const hops = this.forwardedHops(context);
|
|
333
|
+
if (!hops.length) {
|
|
334
|
+
this.warnNoForwardedFor(peer);
|
|
335
|
+
return peer ?? 'unknown';
|
|
336
|
+
}
|
|
337
|
+
for (let i = hops.length - 1; i >= 0; i--) {
|
|
338
|
+
if (!this.trusts(hops[i])) {
|
|
339
|
+
return hops[i];
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return hops.at(-1);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Warn, once per instance, that a trusted peer forwarded no usable `X-Forwarded-For`
|
|
346
|
+
*
|
|
347
|
+
* @param peer - The trusted peer address
|
|
348
|
+
*/
|
|
349
|
+
warnNoForwardedFor(peer) {
|
|
350
|
+
if (this.warnedNoForwardedFor)
|
|
351
|
+
return;
|
|
352
|
+
this.warnedNoForwardedFor = true;
|
|
353
|
+
console.warn(`RateLimiterMiddleware: trusted proxy ${peer ?? '(no peer address)'} sent no usable X-Forwarded-For; ` +
|
|
354
|
+
'every client behind it shares one bucket. Make the proxy append X-Forwarded-For or narrow trustProxy.');
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Whether an address is a reverse proxy whose forwarded hops may be believed
|
|
358
|
+
*
|
|
359
|
+
* @param address - Peer or forwarded hop address
|
|
360
|
+
* @returns True when the address is trusted
|
|
361
|
+
*/
|
|
362
|
+
trusts(address) {
|
|
363
|
+
if (this.trustProxy === true)
|
|
364
|
+
return true;
|
|
365
|
+
if (!this.trustProxy || address === null)
|
|
366
|
+
return false;
|
|
367
|
+
if (!Array.isArray(this.trustProxy))
|
|
368
|
+
return this.trustProxy(address);
|
|
369
|
+
const normalized = unmapIPv4(address);
|
|
370
|
+
return this.trustProxy.some((entry) => unmapIPv4(entry) === normalized);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Parse `X-Forwarded-For` into its hops, left to right
|
|
374
|
+
*
|
|
375
|
+
* @param context - Hono context
|
|
376
|
+
* @returns The forwarded hops, or an empty list when the header is absent or over-long
|
|
377
|
+
*/
|
|
378
|
+
forwardedHops(context) {
|
|
379
|
+
const header = context.req.header('x-forwarded-for');
|
|
380
|
+
if (!header || header.length > MAX_FORWARDED_HEADER_LENGTH) {
|
|
381
|
+
return [];
|
|
382
|
+
}
|
|
383
|
+
return header
|
|
384
|
+
.split(',')
|
|
385
|
+
.map((hop) => hop.trim())
|
|
386
|
+
.filter(Boolean);
|
|
299
387
|
}
|
|
300
388
|
/**
|
|
301
389
|
* Setup automatic cleanup of inactive buckets
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RateLimiterMiddleware.js","sourceRoot":"","sources":["../../../lib/middlewares/RateLimiterMiddleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;;;;;;;;;;AAEH,OAAO,EAAgB,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"RateLimiterMiddleware.js","sourceRoot":"","sources":["../../../lib/middlewares/RateLimiterMiddleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Origin Middleware for Hono Adapter
|
|
3
|
+
*
|
|
4
|
+
* Closes the cross-site WebSocket hijacking hole by:
|
|
5
|
+
* - Rejecting a handshake whose `Origin` is not allowed, before `server.upgrade()`
|
|
6
|
+
* - Deciding explicitly what happens when there is no `Origin` at all
|
|
7
|
+
* - Uses Hono's native header system
|
|
8
|
+
*
|
|
9
|
+
* @module defaults/WebSocketOriginMiddleware
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { Middleware, WebSocket } from '@asenajs/asena/decorators';
|
|
14
|
+
* import { WebSocketOriginMiddleware } from '@asenajs/hono-adapter';
|
|
15
|
+
*
|
|
16
|
+
* @Middleware()
|
|
17
|
+
* export class ChatOrigin extends WebSocketOriginMiddleware {
|
|
18
|
+
* constructor() {
|
|
19
|
+
* super({ allowedOrigins: ['https://app.example.com'] });
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* @WebSocket({ path: 'chat', middlewares: [ChatOrigin, SessionAuth] })
|
|
24
|
+
* export class ChatSocket { ... }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
import { type Context, MiddlewareService } from '../defaults';
|
|
28
|
+
/**
|
|
29
|
+
* WebSocket origin policy options
|
|
30
|
+
*/
|
|
31
|
+
export interface WebSocketOriginOptions {
|
|
32
|
+
/** Origins allowed to open a WebSocket: exact `Origin` header values, or a predicate. */
|
|
33
|
+
allowedOrigins: string[] | ((origin: string) => boolean);
|
|
34
|
+
/**
|
|
35
|
+
* Accept handshakes that carry no `Origin` header (non-browser clients). Browsers always send one,
|
|
36
|
+
* so the default rejects: a missing header is not evidence of a first-party page.
|
|
37
|
+
*
|
|
38
|
+
* @default false
|
|
39
|
+
*/
|
|
40
|
+
allowMissingOrigin?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* WebSocket Origin Middleware
|
|
44
|
+
*
|
|
45
|
+
* Use it as a `@WebSocket({ middlewares: [...] })` middleware, listed **before** any cookie-based
|
|
46
|
+
* auth middleware: the browser attaches cookies to a cross-site handshake, so a session is already
|
|
47
|
+
* authenticated by the time an auth middleware looks at it.
|
|
48
|
+
*
|
|
49
|
+
* `CorsMiddleware` is not a WebSocket origin policy - it sets response headers a browser applies to
|
|
50
|
+
* `fetch`/XHR, and a disallowed origin still upgrades.
|
|
51
|
+
*
|
|
52
|
+
* The `Origin` header is compared exactly: `'https://app.example.com'`, scheme included, no trailing
|
|
53
|
+
* slash.
|
|
54
|
+
*
|
|
55
|
+
* It is an ordinary middleware, so on an HTTP route it would reject every request without an
|
|
56
|
+
* `Origin` - which is why the name says WebSocket.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* // A predicate covers wildcard subdomains; there is no '*' special value
|
|
61
|
+
* new WebSocketOriginMiddleware({ allowedOrigins: (origin) => origin.endsWith('.example.com') });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare class WebSocketOriginMiddleware extends MiddlewareService {
|
|
65
|
+
private readonly allowedOrigins;
|
|
66
|
+
private readonly allowMissingOrigin;
|
|
67
|
+
/**
|
|
68
|
+
* Creates a new WebSocket origin middleware instance
|
|
69
|
+
*
|
|
70
|
+
* @param options - Origin policy options
|
|
71
|
+
*/
|
|
72
|
+
constructor(options: WebSocketOriginOptions);
|
|
73
|
+
/**
|
|
74
|
+
* Reject the handshake unless its `Origin` is allowed
|
|
75
|
+
*
|
|
76
|
+
* @param context - Hono context wrapper
|
|
77
|
+
* @param next - Function to call next middleware or handler
|
|
78
|
+
* @returns A 403 response, or the result of next() for an allowed origin
|
|
79
|
+
*/
|
|
80
|
+
handle(context: Context, next: () => Promise<void>): Promise<any>;
|
|
81
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Origin Middleware for Hono Adapter
|
|
3
|
+
*
|
|
4
|
+
* Closes the cross-site WebSocket hijacking hole by:
|
|
5
|
+
* - Rejecting a handshake whose `Origin` is not allowed, before `server.upgrade()`
|
|
6
|
+
* - Deciding explicitly what happens when there is no `Origin` at all
|
|
7
|
+
* - Uses Hono's native header system
|
|
8
|
+
*
|
|
9
|
+
* @module defaults/WebSocketOriginMiddleware
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { Middleware, WebSocket } from '@asenajs/asena/decorators';
|
|
14
|
+
* import { WebSocketOriginMiddleware } from '@asenajs/hono-adapter';
|
|
15
|
+
*
|
|
16
|
+
* @Middleware()
|
|
17
|
+
* export class ChatOrigin extends WebSocketOriginMiddleware {
|
|
18
|
+
* constructor() {
|
|
19
|
+
* super({ allowedOrigins: ['https://app.example.com'] });
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* @WebSocket({ path: 'chat', middlewares: [ChatOrigin, SessionAuth] })
|
|
24
|
+
* export class ChatSocket { ... }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
import { MiddlewareService } from '../defaults';
|
|
28
|
+
/**
|
|
29
|
+
* WebSocket Origin Middleware
|
|
30
|
+
*
|
|
31
|
+
* Use it as a `@WebSocket({ middlewares: [...] })` middleware, listed **before** any cookie-based
|
|
32
|
+
* auth middleware: the browser attaches cookies to a cross-site handshake, so a session is already
|
|
33
|
+
* authenticated by the time an auth middleware looks at it.
|
|
34
|
+
*
|
|
35
|
+
* `CorsMiddleware` is not a WebSocket origin policy - it sets response headers a browser applies to
|
|
36
|
+
* `fetch`/XHR, and a disallowed origin still upgrades.
|
|
37
|
+
*
|
|
38
|
+
* The `Origin` header is compared exactly: `'https://app.example.com'`, scheme included, no trailing
|
|
39
|
+
* slash.
|
|
40
|
+
*
|
|
41
|
+
* It is an ordinary middleware, so on an HTTP route it would reject every request without an
|
|
42
|
+
* `Origin` - which is why the name says WebSocket.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* // A predicate covers wildcard subdomains; there is no '*' special value
|
|
47
|
+
* new WebSocketOriginMiddleware({ allowedOrigins: (origin) => origin.endsWith('.example.com') });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export class WebSocketOriginMiddleware extends MiddlewareService {
|
|
51
|
+
allowedOrigins;
|
|
52
|
+
allowMissingOrigin;
|
|
53
|
+
/**
|
|
54
|
+
* Creates a new WebSocket origin middleware instance
|
|
55
|
+
*
|
|
56
|
+
* @param options - Origin policy options
|
|
57
|
+
*/
|
|
58
|
+
constructor(options) {
|
|
59
|
+
super();
|
|
60
|
+
this.allowedOrigins = options.allowedOrigins;
|
|
61
|
+
this.allowMissingOrigin = options.allowMissingOrigin ?? false;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Reject the handshake unless its `Origin` is allowed
|
|
65
|
+
*
|
|
66
|
+
* @param context - Hono context wrapper
|
|
67
|
+
* @param next - Function to call next middleware or handler
|
|
68
|
+
* @returns A 403 response, or the result of next() for an allowed origin
|
|
69
|
+
*/
|
|
70
|
+
async handle(context, next) {
|
|
71
|
+
const origin = context.req.header('Origin');
|
|
72
|
+
if (!origin) {
|
|
73
|
+
return this.allowMissingOrigin ? await next() : new Response('Forbidden', { status: 403 });
|
|
74
|
+
}
|
|
75
|
+
const allowed = Array.isArray(this.allowedOrigins)
|
|
76
|
+
? this.allowedOrigins.includes(origin)
|
|
77
|
+
: this.allowedOrigins(origin);
|
|
78
|
+
return allowed ? await next() : new Response('Forbidden', { status: 403 });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=WebSocketOriginMiddleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WebSocketOriginMiddleware.js","sourceRoot":"","sources":["../../../lib/middlewares/WebSocketOriginMiddleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAgB,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAkB9D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,OAAO,yBAA0B,SAAQ,iBAAiB;IAC7C,cAAc,CAA2C;IAEzD,kBAAkB,CAAU;IAE7C;;;;OAIG;IACH,YAAmB,OAA+B;QAChD,KAAK,EAAE,CAAC;QAER,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;QAC7C,IAAI,CAAC,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,IAAI,KAAK,CAAC;IAChE,CAAC;IAED;;;;;;OAMG;IACI,KAAK,CAAC,MAAM,CAAC,OAAgB,EAAE,IAAyB;QAC7D,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAE5C,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QAC7F,CAAC;QAED,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC;YAChD,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC;YACtC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QAEhC,OAAO,OAAO,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;IAC7E,CAAC;CACF"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { serveStatic as baseServeStatic } from 'hono/serve-static';
|
|
4
|
+
const getContent = (async (path) => {
|
|
5
|
+
const file = Bun.file(path);
|
|
6
|
+
return (await file.exists()) ? file : null;
|
|
7
|
+
});
|
|
8
|
+
const isDir = (path) => {
|
|
9
|
+
try {
|
|
10
|
+
return statSync(path, { throwIfNoEntry: false })?.isDirectory();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
export const serveStatic = (options = {}) => baseServeStatic({ ...options, getContent, join, isDir });
|
|
17
|
+
//# sourceMappingURL=serveStatic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serveStatic.js","sourceRoot":"","sources":["../../../lib/utils/serveStatic.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,WAAW,IAAI,eAAe,EAA2B,MAAM,mBAAmB,CAAC;AAI5F,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,IAAY,EAAE,EAAE;IACzC,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAE5B,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7C,CAAC,CAA6B,CAAC;AAE/B,MAAM,KAAK,GAAG,CAAC,IAAY,EAAuB,EAAE;IAClD,IAAI,CAAC;QACH,OAAO,QAAQ,CAAC,IAAI,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC;IAClE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,UAA8B,EAAE,EAAqB,EAAE,CACjF,eAAe,CAAC,EAAE,GAAG,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"README.md",
|
|
10
10
|
"LICENSE"
|
|
11
11
|
],
|
|
12
|
-
"version": "
|
|
12
|
+
"version": "5.0.1",
|
|
13
13
|
"author": "LibirSoft",
|
|
14
14
|
"repository": {
|
|
15
15
|
"type": "git",
|
|
16
|
-
"url": "https://github.com/AsenaJs/hono-adapter.git"
|
|
16
|
+
"url": "git+https://github.com/AsenaJs/hono-adapter.git"
|
|
17
17
|
},
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"engines": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"release": "bun run build && changeset publish"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@asenajs/asena": "^0.
|
|
44
|
+
"@asenajs/asena": "^0.12.0",
|
|
45
45
|
"@changesets/cli": "^2.31.1",
|
|
46
46
|
"@types/bun": "latest",
|
|
47
47
|
"@typescript-eslint/eslint-plugin": "^8.68.0",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"zod": "^4.4.3"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@asenajs/asena": "^0.
|
|
58
|
+
"@asenajs/asena": "^0.12.0",
|
|
59
59
|
"hono": ">=4.12.9",
|
|
60
60
|
"typescript": "^5.8.2",
|
|
61
61
|
"zod": "^4.3.6"
|