New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

bunshine

Package Overview
Dependencies
Maintainers
0
Versions
43
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

bunshine - npm Package Compare versions

Comparing version 3.0.0-rc.1 to 3.0.0-rc.2

src/middleware/applyHandlerIf/applyHandlerIf.ts

21

dist/index.d.ts

@@ -104,6 +104,4 @@ // Generated by dts-bundle-generator v9.5.1

export type SingleHandler<ParamsShape extends Record<string, string> = Record<string, string>> = (context: Context<ParamsShape>, next: NextFunction) => Response | void | Promise<Response | void>;
export type SingleErrorHandler<ParamsShape extends Record<string, string> = Record<string, string>> = (context: ContextWithError<ParamsShape>, next: NextFunction) => Response | void | Promise<Response | void>;
export type Middleware<ParamsShape extends Record<string, string> = Record<string, string>> = SingleHandler<ParamsShape>;
export type Handler<ParamsShape extends Record<string, string> = Record<string, string>> = SingleHandler<ParamsShape> | Handler<ParamsShape>[];
export type ErrorHandler<ParamsShape extends Record<string, string> = Record<string, string>> = SingleErrorHandler<ParamsShape> | ErrorHandler<ParamsShape>[];
export type ListenOptions = Omit<ServeOptions, "fetch" | "websocket"> | number;

@@ -125,4 +123,6 @@ export type HttpMethods = "ALL" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS" | "TRACE";

_wsRouter?: SocketRouter;
private _onErrors;
private _on404s;
onNotFound: typeof HttpRouter.on404;
onError: typeof HttpRouter.on500;
private _on404Handlers;
private _on500Handlers;
constructor(options?: HttpRouterOptions);

@@ -145,4 +145,4 @@ listen(portOrOptions?: ListenOptions): Server;

use(...handlers: Handler<{}>[]): this;
onError(...handlers: ErrorHandler<Record<string, string>>[]): this;
on404(...handlers: Handler<Record<string, string>>[]): this;
on404(...handlers: SingleHandler<Record<string, string>>[]): this;
on500(...handlers: SingleHandler<Record<string, string>>[]): this;
fetch: (request: Request, server: Server) => Promise<Response>;

@@ -172,5 +172,2 @@ }

}): Promise<Response>;
export type ContextWithError<ParamsShape extends Record<string, string> = Record<string, string>> = Context<ParamsShape> & {
error: Error;
};
export declare class Context<ParamsShape extends Record<string, string> = Record<string, string>> {

@@ -223,2 +220,8 @@ /** The raw request object */

}
export type ApplyHandlerIfArgs = {
requestCondition?: (c: Context) => Promise<boolean> | boolean;
responseCondition?: (c: Context, resp: Response) => Promise<boolean> | boolean;
handler: SingleHandler;
};
export declare function applyHandlerIf(conditions: ApplyHandlerIfArgs): Middleware;
export type CompressionOptions = {

@@ -225,0 +228,0 @@ prefer: "br" | "gzip" | "none";

@@ -0,9 +1,5 @@

export { default as Context } from './src/Context/Context';
export {
default as Context,
type ContextWithError,
} from './src/Context/Context';
export {
default as HttpRouter,
type EmitUrlOptions,
type ErrorHandler,
type Handler,

@@ -15,3 +11,2 @@ type HttpMethods,

type NextFunction,
type SingleErrorHandler,
type SingleHandler,

@@ -33,2 +28,6 @@ } from './src/HttpRouter/HttpRouter';

export {
applyHandlerIf,
type ApplyHandlerIfArgs,
} from './src/middleware/applyHandlerIf/applyHandlerIf';
export {
compression,

@@ -35,0 +34,0 @@ compressionDefaults,

{
"name": "bunshine",
"version": "3.0.0-rc.1",
"version": "3.0.0-rc.2",
"module": "server/server.ts",

@@ -5,0 +5,0 @@ "type": "module",

@@ -5,11 +5,10 @@ # Bunshine

<img alt="Bunshine Logo" src="https://github.com/kensnyder/bunshine/raw/main/assets/bunshine-logo.png?v=3.0.0-rc.1" width="200" height="187" />
<img alt="Bunshine Logo" src="https://github.com/kensnyder/bunshine/raw/main/assets/bunshine-logo.png?v=3.0.0-rc.2" width="200" height="187" />
[![NPM Link](https://img.shields.io/npm/v/bunshine?v=3.0.0-rc.1)](https://npmjs.com/package/bunshine)
[![Language](https://badgen.net/static/language/TS?v=3.0.0-rc.1)](https://github.com/search?q=repo:kensnyder/bunshine++language:TypeScript&type=code)
![Test Coverage: 96%](https://badgen.net/static/test%20coverage/92%25/green?v=3.0.0-rc.1)
[![Gzipped Size](https://badgen.net/bundlephobia/minzip/bunshine?label=minzipped&v=3.0.0-rc.1)](https://bundlephobia.com/package/bunshine@3.0.0-rc.1)
[![Dependency details](https://badgen.net/bundlephobia/dependency-count/bunshine?v=3.0.0-rc.1)](https://www.npmjs.com/package/bunshine?activeTab=dependencies)
[![Tree shakeable](https://badgen.net/bundlephobia/tree-shaking/bunshine?v=3.0.0-rc.1)](https://www.npmjs.com/package/bunshine)
[![ISC License](https://badgen.net/github/license/kensnyder/bunshine?v=3.0.0-rc.1)](https://opensource.org/licenses/ISC)
[![NPM Link](https://img.shields.io/npm/v/bunshine?v=3.0.0-rc.2)](https://npmjs.com/package/bunshine)
[![Language: TypeScript](https://badgen.net/static/language/TS?v=3.0.0-rc.2)](https://github.com/search?q=repo:kensnyder/bunshine++language:TypeScript&type=code)
![Test Coverage: 96%](https://badgen.net/static/test%20coverage/96%25/green?v=3.0.0-rc.2)
[![Dependencies: 1](https://badgen.net/static/dependencies/1/green?v=3.0.0-rc.2)](https://www.npmjs.com/package/bunshine?activeTab=dependencies)
![Tree shakeable](https://badgen.net/static/tree%20shakeable/yes/green?v=3.0.0-rc.2)
[![ISC License](https://badgen.net/github/license/kensnyder/bunshine?v=3.0.0-rc.2)](https://opensource.org/licenses/ISC)

@@ -28,12 +27,13 @@ ## Installation

1. Use bare `Request` and `Response` objects
2. Support for routing `WebSocket` requests
3. Support for Server Sent Events
4. Support ranged file downloads (e.g. for video streaming)
5. Be very lightweight
6. Treat every handler like middleware
7. Support async handlers
8. Provide common middleware out of the box (cors, prodLogger, headers, compression, etags)
9. Support traditional routing syntax
10. Make specifically for Bun
11. Comprehensive unit tests
2. Integrated support for routing `WebSocket` requests
3. Integrated support for _Server Sent Events_
4. Support _ranged file downloads_ (e.g. for video streaming)
5. Be very _lightweight_
6. _Elegantly_ treat every handler like middleware
7. Support _async handlers_
8. Provide _common middleware_ out of the box (cors, prodLogger, headers,
compression, etags)
9. Support _traditional routing_ syntax
10. Make specifically for _Bun_
11. Comprehensive _unit tests_
12. Support for `X-HTTP-Method-Override` header

@@ -61,13 +61,16 @@

- [etags](#etags)
- [Recommended Middleware](#recommended-middleware)
11. [TypeScript pro-tips](#typescript-pro-tips)
12. [Roadmap](#roadmap)
13. [License](./LICENSE.md)
12. [Design Decisions](#design-decisions)
13. [Roadmap](#roadmap)
14. [ISC License](./LICENSE.md)
## Upgrading from 1.x to 2.x
RegExp symbols are not allowed in route definitions to avoid ReDoS vulnerabilities.
`RegExp` symbols are not allowed in route definitions to avoid ReDoS
vulnerabilities.
## Upgrading from 2.x to 3.x
- The `securityHeaders` middleware has been dropped. Use a library such as
- The `securityHeaders` middleware has been discontinued. Use a library such as
[@side/fortifyjs](https://www.npmjs.com/package/@side/fortifyjs) instead.

@@ -113,3 +116,4 @@ - The `serveFiles` middleware no longer accepts options for `etags` or `gzip`.

app.on404(c => {
app.onNotFound(c => {
// alias: on404
// called when no handlers match the requested path

@@ -119,3 +123,4 @@ return c.text('Page Not found', { status: 404 });

app.on500(c => {
app.onError(c => {
// alias: on500
// called when a handler throws an error

@@ -174,3 +179,3 @@ console.error('500', c.error);

c.locals; // A place to persist data between handlers for the duration of the request
c.error; // An error object available to handlers registered with app.on500()
c.error; // An error object available to handlers registered with app.onError()
c.ip; // The IP address of the client or load balancer (not necessarily the end user)

@@ -212,3 +217,3 @@ c.date; // The date of the request

Also note you can serve files with bunshine anywhere with `bunx bunshine serve`.
Also note you can serve files with Bunshine anywhere with `bunx bunshine-serve`.
It currently uses the default `serveFiles()` options.

@@ -230,5 +235,4 @@

app.use(c => {
if (!isAllowed(c.request.headers.get('Authorization'))) {
// redirect instead of running other middleware or handlers
return c.redirect('/login', { status: 403 });
if (isBot(c.request.headers.get('User-Agent'))) {
return c.text('Bots are forbidden', { status: 403 });
}

@@ -238,3 +242,3 @@ // continue to other handlers

// Run after each request
// Run code after each request
app.use(async (c, next) => {

@@ -251,3 +255,3 @@ // wait for response from other handlers

// Run before AND after each request
// Run code before AND after each request
app.use(async (c, next) => {

@@ -261,7 +265,8 @@ logRequest(c.request);

// Middleware at a certain path
app.get('/admin', c => {
const requireAdmin: Middleware = c => {
if (!isAdmin(c.request.headers.get('Authorization'))) {
return c.redirect('/login', { status: 403 });
}
});
};
app.get('/admin', requireAdmin);

@@ -286,2 +291,18 @@ // Middleware before a given handler (as args)

// define a handler function to be used in multiple places
const ensureSafeData = async (_, next) => {
const unsafeResponse = await next();
const text = await unsafeResponse.text();
const scrubbed = scrubSensitiveData(text);
return new Response(scrubbed, {
headers: unsafeResponse.headers,
status: unsafeResponse.status,
statusText: unsafeResponse.statusText,
});
};
// all routes that start with /api will get ensureSafeData applied
app.get('/api/*', ensureSafeData);
app.get('/api/v1/users/:id', getUser);
app.listen({ port: 3100, reusePort: true });

@@ -296,3 +317,3 @@ ```

app.get('/users/me', handler1);
app.get('/users/:id', handler2); // runs only if id is not "me" or handler1 doesn't respond
app.get('/users/:id', handler2); // runs only if handler1 doesn't respond
app.get('*', http404Handler);

@@ -320,2 +341,6 @@ ```

});
app.get('/', async (c, next) => {
console.log('never');
return c.text('Hello2');
});
// logs 1, 2, 3, 4, then 5

@@ -362,8 +387,9 @@

// ❌ Incorrect use of next
// ❌ Incorrect use of next()
app.get('/hello', (c: Context, next: NextFunction) => {
const resp = next();
// oops! resp is a Promise
});
// ✅ Correct use of next
// ✅ Correct use of next()
app.get('/hello', async (c: Context, next: NextFunction) => {

@@ -376,4 +402,4 @@ // wait for other handlers to return a response

And finally, it means that `.use()` is just a convenience function for
registering middleware. Consider the following:
And it means that `.use()` is just a convenience function for registering
middleware. Consider the following:

@@ -398,3 +424,3 @@ ```ts

// middleware can be inserted with parameters
// middleware can be inserted as parameters to app.get()
app.get('/admin', getAuthMiddleware('admin'), middleware2, handler);

@@ -437,2 +463,6 @@

Throwing a `Response` effectively skips the currently running handler/middleware
and passes control to the next handler. That way thrown responses will be passed
to subsequent middleware such as loggers.
## WebSockets

@@ -593,3 +623,3 @@

Creating an `EventSource` object will open a connection to the server, and if
the server closes the connection, the browser will automatically reconnect.
the server closes the connection, a browser will automatically reconnect.

@@ -612,2 +642,3 @@ So if you want to tell the browser you are done sending events, send a

const onComplete = () => {
// Browser code will close connection when percent is 100
send('progress', { percent: 100 });

@@ -664,7 +695,7 @@ };

Due to a discovered
[RegExp Denial of Service vulnerability](https://security.snyk.io/vuln/SNYK-JS-PATHTOREGEXP-7925106),
Bunshine v2+ no longer uses
[path-to-regexp docs](https://www.npmjs.com/package/path-to-regexp).
[RegExp Denial of Service (ReDoS) vulnerability](https://security.snyk.io/vuln/SNYK-JS-PATHTOREGEXP-7925106)
Bunshine v2+ no longer uses `path-to-regexp` because the new, safer version
imposes disruptive limitations such as no support for unnamed wildcards.
### Support
### What is supported

@@ -675,10 +706,10 @@ Bunshine supports the following route matching features:

- End wildcards using stars (e.g. `/assets/*`)
- Middle non-slash wildcards using stars (e.g. `/assets/*/*.css`)
- Middle (non-slash) wildcards using stars (e.g. `/assets/*/*.css`)
- Static paths (e.g. `/posts`)
Support for other behaviors can lead to a Regular Expression Denial of service
vulnerability where an attacker can request long URLs and tie up your server
CPU with backtracking regular expression searches.
Support for RegExp symbols such as `\d+` can lead to a Regular Expression Denial
of Service (ReDoS) vulnerability where an attacker can request long URLs and
tie up your server CPU with backtracking regular-expression searches.
### Path examples
### Supported path examples

@@ -696,14 +727,16 @@ | Path | URL | params |

### Special Characters
### Special characters are not supported
Note that all regular-expression special characters including
`\ ^ $ * + ? . ( ) | { } [ ]` will be escaped. If you need any of these
behaviors, you'll need to pass in a `RegExp`.
behaviors, you'll need to pass in a `RegExp`. But be sure to check your
`RegExp` with a ReDoS such as [Devina](https://devina.io/redos-checker) or
[redos-checker on npm](https://www.npmjs.com/package/redos-detector).
For example, the dot in `/assets/*.js` will not match all characters--only dots.
### Not supported
### Examples of unsupported routes
Support for regex-like syntax has been dropped in v2 due to the aforementioned
[RegExp Denial of Service vulnerability](https://security.snyk.io/vuln/SNYK-JS-PATHTOREGEXP-7925106).
Support for regex-like syntax has been dropped in Bunshine v2 due to the
aforementioned RegExp Denial of Service (ReDoS) vulnerability.
For cases where you need to limit by character or specify optional segments,

@@ -714,10 +747,11 @@ you'll need to pass in a `RegExp`. Be sure to check your `RegExp` with a ReDoS

| Example | Explaination | Equivalent RegExp |
| ------------------- | ----------------------------------------- | ------------------------ |
| `/users/([a-z-]+)/` | Character classes are not supported | `^\/users\/([a-z-]+)$` |
| `/users/(\\d+)` | Character class escapes are not supported | `^/\/users\/(\d+)$` |
| `/(users\|u)/:id` | Pipes are not supported | `^\/(users\|u)/([^/]+)$` |
| `/:a/:b?` | Optional params are not supported | `^\/([^/]*)\/(.*)$` |
| Example | Explaination | Equivalent Safe RegExp |
| ---------------------- | ----------------------------------------- | ------------------------ |
| `/users/([a-z-]+)/` ❌ | Character classes are not supported | `^\/users\/([a-z-]+)$` |
| `/users/(\\d+)` ❌ | Character class escapes are not supported | `^/\/users\/(\d+)$` |
| `/(users\|u)/:id` ❌ | Pipes are not supported | `^\/(users\|u)/([^/]+)$` |
| `/:a/:b?` ❌ | Optional params are not supported | `^\/([^/]*)\/(.*)$` |
If you want to double check all your routes, you can use code like the following:
If you want to double-check all your routes, you can install `redos-detector`
and use Bunshine's `detectPotentialDos` function:

@@ -768,5 +802,5 @@ ```ts

Serve static files from a directory. As shown above, serving static files is
easy with the `serveFiles` middleware. Note that ranged requests are
supported, so you can use it for video streaming or partial downloads.
Serve static files from a directory. Note that ranged requests are
supported, so you can use it to serve video streams and support partial
downloads.

@@ -790,5 +824,5 @@ ```ts

app.headGet('/public/*', serveFiles(`${import.meta.dir}/public`));
// or
app.on(['HEAD', 'GET'], '/public/*', serveFiles(`${import.meta.dir}/public`));
// or
app.headGet('/public/*', serveFiles(`${import.meta.dir}/public`));

@@ -798,20 +832,2 @@ app.listen({ port: 3100, reusePort: true });

How to alter the response provided by another handler:
```ts
import { HttpRouter, serveFiles } from 'bunshine';
const app = new HttpRouter();
const addFooHeader = async (_, next) => {
const response = await next();
response.headers.set('x-foo', 'bar');
return response;
};
app.get('/public/*', addFooHeader, serveFiles(`${import.meta.dir}/public`));
app.listen({ port: 3100, reusePort: true });
```
serveFiles accepts an optional second parameter for options:

@@ -841,3 +857,3 @@

| dotfiles | `"ignore"` | How to handle dotfiles; allow=>serve normally, deny=>return 403, ignore=>run next handler |
| extensions | `[]` | If given, a list of file extensions to allow |
| extensions | `[]` | If given, a whitelist of file extensions to allow |
| fallthrough | `true` | If false, issue a 404 when a file is not found, otherwise proceed to next handler |

@@ -847,3 +863,3 @@ | maxAge | `undefined` | If given, add a Cache-Control header with max-age† |

| index | `[]` | If given, a list of filenames (e.g. index.html) to look for when path is a folder |
| lastModified | `true` | If true, set the Last-Modified header |
| lastModified | `true` | If true, set the Last-Modified header based on the filesystem's last modified date |

@@ -855,3 +871,3 @@ † _A number in milliseconds or expression such as '30min', '14 days', '1y'._

Simple caching can be accomplished with the `responseCache()` middleware. It
saves responses to a cache you supply, based on URL. This can be useful for
saves responses to a cache you supply, with URLs as keys. This can be useful for
builds, where your assets aren't changing. In the example below, `lru-cache` is

@@ -862,4 +878,4 @@ used to store assets in memory. Any cache that implements `has(url: string)`,

Keep in mind that your `set()` function will receive a `Response` object and
your `get()` function should be an object with a `clone()` method that returns
a `Response` object.
your `get()` function should return an object with a `clone()` method that
returns a `Response` object.

@@ -878,2 +894,6 @@ ```ts

Note that caching in memory can potentially consume a lot of memory. Using a
service such as CloudFlare removes the need for caching assets and takes a load
off your application.
### compression

@@ -888,2 +908,6 @@

// compress all payloads
app.use(compression());
// compress only certain payloads
app.get('/public/*', compression(), serveFiles(`${import.meta.dir}/public`));

@@ -948,3 +972,4 @@

_origin_: A string, regex, array of strings/regexes, or a function that returns the desired origin header
_origin_: A string, regex, array of strings/regexes, or a function that returns
the desired origin header
_allowMethods_: an array of HTTP verbs to allow clients to make

@@ -954,3 +979,4 @@ _allowHeaders_: an array of HTTP headers to allow clients to send

_maxAge_: the number of seconds clients should cache the CORS headers
_credentials_: whether to allow clients to send credentials (e.g. cookies or auth headers)
_credentials_: whether to allow clients to send credentials (e.g. cookies or
auth headers)

@@ -983,2 +1009,24 @@ ### headers

You can also use pass a function as a second parameter to `headers`, to only
apply
the given headers under certain conditions.
```ts
import { HttpRouter, headers } from '../index';
const app = new HttpRouter();
const htmlSecurityHeaders = headers(
{
'Content-Security-Policy': `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;`,
'Referrer-Policy': 'strict-origin',
'Permissions-Policy':
'accelerometer=(), ambient-light-sensor=(), autoplay=(*), battery=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), local-fonts=(), magnetometer=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-create=(), publickey-credentials-get=(), screen-wake-lock=(), serial=(), usb=(), web-share=(self)',
},
(c, response) => response.headers.get('content-type')?.includes('text/html')
);
app.use(htmlSecurityHeaders);
```
### devLogger & prodLogger

@@ -1009,5 +1057,5 @@

"runtime": "Bun v1.1.33",
"poweredBy": "Bunshine v3.0.0-rc.1",
"poweredBy": "Bunshine v3.0.0-rc.2",
"machine": "server1",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0-rc.1.0 Safari/537.36",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0-rc.2.0 Safari/537.36",
"pid": 123

@@ -1029,5 +1077,5 @@ }

"runtime": "Bun v1.1.3",
"poweredBy": "Bunshine v3.0.0-rc.1",
"poweredBy": "Bunshine v3.0.0-rc.2",
"machine": "server1",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0-rc.1.0 Safari/537.36",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0-rc.2.0 Safari/537.36",
"pid": 123,

@@ -1048,5 +1096,7 @@ "took": 5

const logger = process.env.NODE_ENV === 'development' ? devLogger : prodLogger;
// attach very first to log all requests
app.use(logger());
// or at a specific path
app.use('/api/*', logger());
// OR attach only to a specific path
app.get('/api/*', logger());
api.get('/api/users/:id', getUser);

@@ -1088,2 +1138,22 @@ app.listen({ port: 3100, reusePort: true });

### Recommended Middleware
Most applications will want a full-featured set of middleware. Below is the
recommended middleware in order.
```ts
import { HttpRouter, compression, etags, performanceHeader } from 'bunshine';
const app = new HttpRouter();
// add total execution time in milliseconds
app.use(performanceHeader);
// log all requests
app.use(process.env.NODE_ENV === 'development' ? devLogger() : prodLogger());
// use etag headers
app.use(etags());
// compress all payloads
app.use(compression());
```
## TypeScript pro-tips

@@ -1096,3 +1166,3 @@

You can type URL params by passing a type to any of the route methods:
You can specify URL param types by passing a type to any of the route methods:

@@ -1162,5 +1232,7 @@ ```ts

```ts
import { HttpRouter, type Middleware } from 'bunshine';
function myMiddleware(options: Options): Middleware {
return (c, next) => {
// TypeScript infers c and next because of Middleware
// TypeScript infers c and next because of Middleware type
};

@@ -1173,4 +1245,3 @@ }

```ts
import { HttpRouter } from 'bunshine';
import { Middleware } from './HttpRouter';
import { HttpRouter, type Middleware } from 'bunshine';

@@ -1230,2 +1301,3 @@ const app = new HttpRouter();

const authValue = c.request.headers.get('Authorization');
// subsequent handler will have access to this auth information
c.locals.auth = {

@@ -1248,2 +1320,3 @@ identity: await getUser(authValue),

}
app.post('/api/users', castSchema(userCreateSchema), createUser);

@@ -1254,16 +1327,16 @@

// do stuff with url and request
return text('my json response');
return json({ message: 'my json response' });
});
```
## Decisions
## Design Decisions
The following decisions are based on scripts in /benchmarks:
- bound-functions.ts - The Context object created for each request has its
- `bound-functions.ts` - The Context object created for each request has its
methods automatically bound to the instance. It is convenient for developers
and adds only a tiny overhead.
- inner-functions.ts - The Context is a class, not a set of functions in an
enclosure which saves about 3% of time.
- compression.ts - gzip is the default preferred format for the compression
- `inner-functions.ts` - Context is a class, not a set of functions in a
closure, which saves about 3% of time.
- `compression.ts` - gzip is the default preferred format for the compression
middleware. Deflate provides no advantage, and Brotli provides 2-8% additional

@@ -1273,15 +1346,15 @@ size savings at the cost of 7-10x as much CPU time as gzip. Brotli takes on

for gzip.
- etags - etag calculation is very fast. On the order of tens of microseconds
for 100kb of html.
- lru-matcher.ts - The default LRU cache size used for the router is 4000.
- `etags.ts` - etag calculation is very fast. On the order of tens of
microseconds for 100kb of html.
- `lru-matcher.ts` - The default LRU cache size used for the router is 4000.
Cache sizes of 4000+ are all about 1.4x faster than no cache.
- response-reencoding.ts - Both the etags middleware and compression middleware
convert the response body to an ArrayBuffer, process it, then create a new
Response object. The decode/reencode process takes only 10s of microseconds.
- TextEncoder-reuse.ts - The Context object's response factories (c.json(),
c.html(), etc.) reuse a single TextEncoder object. That gains about 18% which
turns out to be only on the order of 10s of nanoseconds.
- timer-resolution.ts - performance.now() is faster than Date.now() even though
it provides additional precision. The performanceHeader uses performance.now()
when it sets the X-Took header, which is rounded to 3 decimal places.
- `response-reencoding.ts` - Both the etags middleware and compression
middleware convert the response body to an ArrayBuffer, process it, then create a new Response object. The decode/reencode process takes only 10s of microseconds.
- `TextEncoder-reuse.ts` - The Context object's response factories (c.json(),
c.html(), etc.) reuse a single `TextEncoder` object. That gains about 18%
which turns out to be only on the order of 10s of nanoseconds.
- `timer-resolution.ts` - `performance.now()` is faster than `Date.now()` even
though it provides additional precision. The `performanceHeader()` middleware
uses `performance.now()` when it sets the `X-Took` header with the number of
milliseconds rounded to 3 decimal places.

@@ -1293,4 +1366,15 @@ Some additional design decisions:

multiple handlers and running each one in order of registration. Bunshine v1
did use `path-to-regexp`, but that recently stopped supporting `*` in route
registration.
did use `path-to-regexp`, but that recently stopped supporting many common
route definitions.
- Handlers must use `Request` and `Response` objects. We all work with these
modern APIs every time we use `fetch`. Cloudflare Workers, Deno, Bun, and
other runtimes decided to use bare `Request` and `Response` objects for their
standard libraries. Someday Node may add support too. Frameworks like Express
and Hono are nice, but take some learning to understand their proprietary APIs
for creating responses, sending responses, and setting headers.
- Handlers receive a raw `URL` object. `URL` objects are a fast and standardized
way to parse incoming URLs. The router only needs to know the path, and so
does no other processing for query parameters, ports and so on. There are many
approaches to parse query parameters; Bunshine is agnostic. Use
`c.url.searchParams` however you like.

@@ -1297,0 +1381,0 @@ ## Roadmap

@@ -19,6 +19,2 @@ import type { BunFile, Server } from 'bun';

export type ContextWithError<
ParamsShape extends Record<string, string> = Record<string, string>,
> = Context<ParamsShape> & { error: Error };
export default class Context<

@@ -25,0 +21,0 @@ ParamsShape extends Record<string, string> = Record<string, string>,

@@ -1,4 +0,4 @@

import type { ContextWithError } from '../Context/Context';
import type Context from '../Context/Context';
export const fallback500 = (context: ContextWithError) => {
export const fallback500 = (context: Context) => {
const error = context.error;

@@ -8,4 +8,4 @@ const headers = new Headers();

if (Bun.env.NODE_ENV === 'development') {
const message = error.message || String(error);
const stack = error.stack || 'N/A';
const message = error ? error.message || String(error) : 'Unknown Error';
const stack = error?.stack || 'N/A';
headers.append('Reason', 'Error was not handled');

@@ -12,0 +12,0 @@ headers.append('Error-Text', JSON.stringify(message));

import type { ServeOptions, Server } from 'bun';
import os from 'node:os';
import bunshine from '../../package.json' assert { type: 'json' };
import Context, { type ContextWithError } from '../Context/Context';
import bunshinePkg from '../../package.json' assert { type: 'json' };
import Context from '../Context/Context';
import MatcherWithCache from '../MatcherWithCache/MatcherWithCache';

@@ -19,9 +19,2 @@ import SocketRouter from '../SocketRouter/SocketRouter';

export type SingleErrorHandler<
ParamsShape extends Record<string, string> = Record<string, string>,
> = (
context: ContextWithError<ParamsShape>,
next: NextFunction
) => Response | void | Promise<Response | void>;
export type Middleware<

@@ -35,6 +28,2 @@ ParamsShape extends Record<string, string> = Record<string, string>,

export type ErrorHandler<
ParamsShape extends Record<string, string> = Record<string, string>,
> = SingleErrorHandler<ParamsShape> | ErrorHandler<ParamsShape>[];
export type ListenOptions = Omit<ServeOptions, 'fetch' | 'websocket'> | number;

@@ -64,3 +53,3 @@

export default class HttpRouter {
version: string = bunshine.version;
version: string = bunshinePkg.version;
locals: Record<string, any> = {};

@@ -70,4 +59,6 @@ server: Server | undefined;

_wsRouter?: SocketRouter;
private _onErrors: any[] = [];
private _on404s: any[] = [];
onNotFound: typeof this.on404;
onError: typeof this.on500;
private _on404Handlers: SingleHandler[] = [];
private _on500Handlers: SingleHandler[] = [];
constructor(options: HttpRouterOptions = {}) {

@@ -77,2 +68,4 @@ this.routeMatcher = new MatcherWithCache<SingleHandler>(

);
this.onNotFound = this.on404;
this.onError = this.on500;
}

@@ -106,3 +99,3 @@ listen(portOrOptions: ListenOptions = {}) {

: `Node v${process.versions.node}`;
message = `☀️ Bunshine v${bunshine.version} on ${runtime} serving at ${servingAt} on "${server}" in ${mode} (${took}ms)`;
message = `☀️ Bunshine v${bunshinePkg.version} on ${runtime} serving at ${servingAt} on "${server}" in ${mode} (${took}ms)`;
} else {

@@ -213,8 +206,8 @@ message = `☀️ Serving ${servingAt}`;

}
onError(...handlers: ErrorHandler<Record<string, string>>[]) {
this._onErrors.push(...handlers.flat(9));
on404(...handlers: SingleHandler<Record<string, string>>[]) {
this._on404Handlers.push(...handlers.flat(9));
return this;
}
on404(...handlers: Handler<Record<string, string>>[]) {
this._on404s.push(...handlers.flat(9));
on500(...handlers: SingleHandler<Record<string, string>>[]) {
this._on500Handlers.push(...handlers.flat(9));
return this;

@@ -228,3 +221,7 @@ }

).toUpperCase();
const matched = this.routeMatcher.match(method, pathname, this._on404s);
const matched = this.routeMatcher.match(
method,
pathname,
this._on404Handlers
);
let i = 0;

@@ -252,4 +249,3 @@ const next: NextFunction = async () => {

} catch (e) {
// @ts-expect-error
return errorHandler(e);
return errorHandler(e as Error);
}

@@ -265,5 +261,5 @@ };

const nextError: NextFunction = async () => {
const handler = this._onErrors[idx++];
const handler = this._on500Handlers[idx++];
if (!handler) {
return fallback500(context as ContextWithError);
return fallback500(context);
}

@@ -270,0 +266,0 @@ try {

@@ -21,5 +21,7 @@ import type Context from '../../Context/Context';

type OriginResolver = (incoming: string, c: Context) => string | null;
export const corsDefaults = {
origin: '*',
allowMethods: ['GET', 'HEAD', 'PUT', 'POST', 'PATCH', 'DELETE'],
allowMethods: ['GET', 'HEAD', 'PUT', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],
allowHeaders: [],

@@ -34,48 +36,65 @@ exposeHeaders: [],

};
const findAllowOrigin = (optsOrigin => {
if (typeof optsOrigin === 'string') {
return () => optsOrigin;
} else if (optsOrigin instanceof RegExp) {
return (incoming: string) =>
optsOrigin.test(incoming) ? incoming : null;
} else if (Array.isArray(optsOrigin)) {
return (incoming: string) => {
for (const origin of optsOrigin) {
if (
(typeof origin === 'string' && origin === incoming) ||
(origin instanceof RegExp && origin.test(incoming))
) {
return incoming;
}
const originResolver = getOriginResolver(opts.origin);
const optionsRequestHandler = getOptionsRequestHandler(opts, originResolver);
const maybeAddAccessHeaders = getAccessHeaderHandler(opts, originResolver);
return async (c, next) => {
if (c.request.method === 'OPTIONS') {
return optionsRequestHandler(c);
}
const response = await next();
maybeAddAccessHeaders(c, response);
return response;
};
}
function getOriginResolver(optsOrigin: CorsOptions['origin']): OriginResolver {
if (typeof optsOrigin === 'string') {
return () => optsOrigin;
} else if (optsOrigin instanceof RegExp) {
return (incoming: string) => (optsOrigin.test(incoming) ? incoming : null);
} else if (Array.isArray(optsOrigin)) {
return (incoming: string) => {
for (const origin of optsOrigin) {
if (
(typeof origin === 'string' && origin === incoming) ||
(origin instanceof RegExp && origin.test(incoming))
) {
return incoming;
}
}
return null;
};
} else if (optsOrigin === true) {
return (incoming: string) => incoming;
} else if (optsOrigin === false) {
return () => null;
} else if (typeof optsOrigin === 'function') {
return (incoming: string, c: Context) => {
const origins = optsOrigin(incoming, c);
if (origins === true) {
return incoming;
} else if (origins === false) {
return null;
};
} else if (optsOrigin === true) {
return (incoming: string) => incoming;
} else if (optsOrigin === false) {
return () => null;
} else if (typeof optsOrigin === 'function') {
return (incoming: string, c: Context) => {
const origins = optsOrigin(incoming, c);
if (origins === true) {
return incoming;
} else if (origins === false) {
return null;
} else if (Array.isArray(origins)) {
return origins.includes(incoming) ? incoming : null;
} else if (typeof origins === 'string') {
return origins;
} else {
return null;
}
};
} else {
throw new Error('Invalid cors origin option');
}
})(opts.origin);
function handleOptionsRequest(c: Context) {
} else if (Array.isArray(origins)) {
return origins.includes(incoming) ? incoming : null;
} else if (typeof origins === 'string') {
return origins;
} else {
return null;
}
};
} else {
throw new Error('Invalid cors origin option');
}
}
function getOptionsRequestHandler(
opts: CorsOptions,
originResolver: OriginResolver
) {
return function handleOptionsRequest(c: Context) {
const respHeaders = new Headers();
const incomingOrigin = c.request.headers.get('origin');
const allowOrigin = incomingOrigin
? findAllowOrigin(incomingOrigin, c)
? originResolver(incomingOrigin, c)
: null;

@@ -85,3 +104,3 @@ if (allowOrigin) {

} else {
// TODO: find out if we should send a 4xx instead?
return new Response(null, { status: 403 });
}

@@ -110,14 +129,17 @@ if (opts.maxAge != null) {

}
// TODO: is this deletion necessary?
respHeaders.delete('Content-Length');
respHeaders.delete('Content-Type');
return new Response(null, {
headers: respHeaders,
status: 204,
status: 200,
});
}
function maybeAddAccessHeaders(c: Context, response: Response) {
};
}
function getAccessHeaderHandler(
opts: CorsOptions,
originResolver: OriginResolver
) {
return function maybeAddAccessHeaders(c: Context, response: Response) {
const incomingOrigin = c.request.headers.get('origin');
const allowOrigin = incomingOrigin
? findAllowOrigin(incomingOrigin, c)
? originResolver(incomingOrigin, c)
: null;

@@ -139,11 +161,3 @@ if (allowOrigin) {

}
}
return async (c, next) => {
if (c.request.method === 'OPTIONS') {
return handleOptionsRequest(c);
}
const response = await next();
maybeAddAccessHeaders(c, response);
return response;
};
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc