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 1.0.1 to 2.0.0

src/RouteMatcher/RouteMatcher.ts

36

index.d.ts

@@ -5,23 +5,25 @@ // Generated by dts-bundle-generator v9.5.1

import { LRUCache } from 'lru-cache';
import { match } from 'path-to-regexp';
import { RequireAtLeastOne } from 'type-fest';
export type Registration<T> = {
matcher: ReturnType<typeof match<Record<string, string>>>;
matcher: (subject: string) => null | Record<string, string>;
pattern: string;
regex: RegExp;
methodFilter: null | ((subject: string) => boolean);
target: T;
};
declare class PathMatcher<Target extends any> {
export type Result<T> = Array<[
T,
Record<string, string>
]>;
declare class RouteMatcher<Target extends any> {
registered: Registration<Target>[];
add(path: string | RegExp, target: Target): void;
match(path: string, filter?: (target: Target) => boolean, fallbacks?: Function[]): {
target: any;
params: Record<string, string>;
}[];
match(method: string, subject: string, fallbacks?: Target[]): Result<Target>;
add(method: string, pattern: string | RegExp, target: Target): this;
detectPotentialDos(detector: any, config?: any): void;
}
declare class MatcherWithCache<Target = any> {
matcher: PathMatcher<Target>;
declare class MatcherWithCache<Target = any> extends RouteMatcher<Target> {
cache: LRUCache<string, any>;
constructor(matcher: PathMatcher<Target>, size?: number);
add(path: string | RegExp, target: Target): void;
match(path: string, filter?: (target: Target) => boolean, fallbacks?: Function[]): any;
constructor(size?: number);
match(method: string, subject: string, fallbacks?: Target[]): any;
}

@@ -93,3 +95,3 @@ declare class SocketContext<UpgradeShape = any, ParamsShape = Record<string, any>> {

httpRouter: HttpRouter;
pathMatcher: PathMatcher<BunshineHandlers<any>>;
routeMatcher: RouteMatcher<BunshineHandlers<any>>;
handlers: BunHandlers;

@@ -107,6 +109,2 @@ constructor(router: HttpRouter);

export type ErrorHandler<ParamsShape extends Record<string, string> = Record<string, string>> = SingleErrorHandler<ParamsShape> | ErrorHandler<ParamsShape>[];
export type RouteInfo = {
verb: string;
handler: Handler<any>;
};
export type ListenOptions = Omit<ServeOptions, "fetch" | "websocket"> | number;

@@ -126,3 +124,3 @@ export type HttpMethods = "ALL" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS" | "TRACE";

server: Server | undefined;
pathMatcher: MatcherWithCache<RouteInfo>;
routeMatcher: MatcherWithCache<SingleHandler>;
_wsRouter?: SocketRouter;

@@ -129,0 +127,0 @@ private _onErrors;

{
"name": "bunshine",
"version": "1.0.1",
"version": "2.0.0",
"module": "server/server.ts",

@@ -15,2 +15,3 @@ "type": "module",

},
"sideEffects": false,
"repository": {

@@ -48,4 +49,3 @@ "type": "git",

"dependencies": {
"lru-cache": "10.2.2",
"path-to-regexp": "6.2.2"
"lru-cache": "11.0.1"
},

@@ -55,11 +55,12 @@ "devDependencies": {

"@types/ms": "0.7.34",
"bun-types": "1.1.7",
"bun-types": "1.1.29",
"eventsource": "2.0.2",
"globby": "14.0.1",
"prettier": "3.2.5",
"prettier-plugin-organize-imports": "3.2.4",
"tinybench": "2.8.0",
"type-fest": "4.18.2",
"typescript": "5.4.5"
"globby": "14.0.2",
"prettier": "3.3.3",
"prettier-plugin-organize-imports": "4.1.0",
"redos-detector": "5.1.0",
"tinybench": "2.9.0",
"type-fest": "4.26.1",
"typescript": "5.6.2"
}
}

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

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

@@ -46,3 +50,3 @@ ## Installation

8. [Server Sent Events](#server-sent-events)
9. [Routing examples](#routing-examples)
9. [Route Matching](#route-matching)
10. [Included middleware](#included-middleware)

@@ -577,25 +581,63 @@ - [serveFiles](#servefiles)

## Routing examples
## Route Matching
Bunshine uses the `path-to-regexp` package for processing path routes. For more
info, checkout the [path-to-regexp docs](https://www.npmjs.com/package/path-to-regexp).
Bunshine v1 used the `path-to-regexp` package for processing path routes.
Due to a discovered
[RegExp Denial of Service vulnerability](https://security.snyk.io/vuln/SNYK-JS-PATHTOREGEXP-7925106),
Bunshine no longer uses
[path-to-regexp docs](https://www.npmjs.com/package/path-to-regexp).
### Support
Bunshine supports the following route matching features:
- Named placeholders using colons (e.g. `/posts/:id`)
- End wildcards using stars (e.g. `/assets/*`)
- Middle non-slash wildcards using stars (e.g. `/assets/*/*.css`)
- Static paths (e.g. `/posts`)
- Custom Regular Expression (e.g. `/^\/author\/([a-z]+)$/i`)
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.
### Path examples
| Path | URL | params |
| ---------------------- | --------------------- | ------------------------ |
| `'/path'` | `'/path'` | `{}` |
| `'/users/:id'` | `'/users/123'` | `{ id: '123' }` |
| `'/users/:id/groups'` | `'/users/123/groups'` | `{ id: '123' }` |
| `'/u/:id/groups/:gid'` | `'/u/1/groups/a'` | `{ id: '1', gid: 'a' }` |
| `'/star/*'` | `'/star/man'` | `{ 0: 'man' }` |
| `'/star/*/can'` | `'/star/man/can'` | `{ 0: 'man' }` |
| `'/users/(\\d+)'` | `'/users/123'` | `{ 0: '123' }` |
| `/users/(\d+)/` | `'/users/123'` | `{ 0: '123' }` |
| `/users/([a-z-]+)/` | `'/users/abc-def'` | `{ 0: 'abc-def' }` |
| `'/(users\|u)/:id'` | `'/users/123'` | `{ id: '123' }` |
| `'/(users\|u)/:id'` | `'/u/123'` | `{ id: '123' }` |
| `'/:a/:b?'` | `'/123'` | `{ a: '123' }` |
| `'/:a/:b?'` | `'/123/abc'` | `{ a: '123', b: 'abc' }` |
| Path | URL | params |
| -------------------- | ------------------- | ----------------------- |
| `/path` | `/path` | `{}` |
| `/users/:id` | `/users/123` | `{ id: '123' }` |
| `/users/:id/groups` | `/users/123/groups` | `{ id: '123' }` |
| `/u/:id/groups/:gid` | `/u/1/groups/a` | `{ id: '1', gid: 'a' }` |
| `/star/*` | `/star/man` | `{ 0: 'man' }` |
| `/star/*` | `/star/man/can` | `{ 0: 'man/can' }` |
| `/star/*/can` | `/star/man/can` | `{ 0: 'man' }` |
| `/star/*/can/*` | `/star/man/can/go` | `{ 0: 'man', 1: 'go' }` |
### Special Characters
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`.
For example, the dot in `/assets/*.js` will not match all characters--only dots.™™
### Not supported
Support for regex-like syntax has been dropped in v2 due to a
[RegExp Denial of Service vulnerability](https://security.snyk.io/vuln/SNYK-JS-PATHTOREGEXP-7925106).
For cases where you need to limit by character or specify optional segments,
you'll need to pass in a `RegExp`. Be sure to check your `RegExp` with a ReDoS
checker such as [Devina](https://devina.io/redos-checker) or
[redos-checker on npm](https://www.npmjs.com/package/redos-detector).
| 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 | `^\/([^/]*)\/(.*)$` |
### Caching
### HTTP methods

@@ -623,2 +665,5 @@

// regular expression matchers are supported
app.get(/^\/author\/([a-z]+)$/i, getPost);
app.listen({ port: 3100 });

@@ -785,3 +830,3 @@ ```

"runtime": "Bun v1.1.4",
"poweredBy": "Bunshine v1.0.1",
"poweredBy": "Bunshine v2.0.0",
"machine": "server1",

@@ -805,3 +850,3 @@ "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",

"runtime": "Bun v1.1.4",
"poweredBy": "Bunshine v1.0.1",
"poweredBy": "Bunshine v2.0.0",
"machine": "server1",

@@ -808,0 +853,0 @@ "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",

@@ -14,4 +14,4 @@ import type { BunFile, ZlibCompressionOptions } from 'bun';

export function gzipString(text: string, zlibOptions?: ZlibCompressionOptions) {
const buffer = Buffer.from(textEncoder.encode(text));
const buffer = new Uint8Array(textEncoder.encode(text));
return Bun.gzipSync(buffer, zlibOptions);
}

@@ -6,3 +6,2 @@ import type { ServeOptions, Server } from 'bun';

import MatcherWithCache from '../MatcherWithCache/MatcherWithCache.ts';
import PathMatcher from '../PathMatcher/PathMatcher';
import SocketRouter from '../SocketRouter/SocketRouter.ts';

@@ -40,7 +39,2 @@ import { fallback404 } from './fallback404';

type RouteInfo = {
verb: string;
handler: Handler<any>;
};
export type ListenOptions = Omit<ServeOptions, 'fetch' | 'websocket'> | number;

@@ -59,18 +53,2 @@

const getPathMatchFilter = (verb: string) => (target: RouteInfo) => {
return target.verb === verb || target.verb === 'ALL';
};
const filters = {
ALL: () => true,
GET: getPathMatchFilter('GET'),
POST: getPathMatchFilter('POST'),
PUT: getPathMatchFilter('PUT'),
PATCH: getPathMatchFilter('PATCH'),
DELETE: getPathMatchFilter('DELETE'),
HEAD: getPathMatchFilter('HEAD'),
OPTIONS: getPathMatchFilter('OPTIONS'),
TRACE: getPathMatchFilter('TRACE'),
};
export type HttpRouterOptions = {

@@ -90,3 +68,3 @@ cacheSize?: number;

server: Server | undefined;
pathMatcher: MatcherWithCache<RouteInfo>;
routeMatcher: MatcherWithCache<SingleHandler>;
_wsRouter?: SocketRouter;

@@ -96,4 +74,3 @@ private _onErrors: any[] = [];

constructor(options: HttpRouterOptions = {}) {
this.pathMatcher = new MatcherWithCache<RouteInfo>(
new PathMatcher(),
this.routeMatcher = new MatcherWithCache<SingleHandler>(
options.cacheSize || 4000

@@ -168,6 +145,3 @@ );

for (const handler of handlers.flat(9)) {
this.pathMatcher.add(path, {
verb: verbOrVerbs as string,
handler: handler as SingleHandler<ParamsShape>,
});
this.routeMatcher.add(verbOrVerbs, path, handler as SingleHandler);
}

@@ -253,4 +227,3 @@ return this;

).toUpperCase();
const filter = filters[method] || getPathMatchFilter(method);
const matched = this.pathMatcher.match(pathname, filter, this._on404s);
const matched = this.routeMatcher.match(method, pathname, this._on404s);
let i = 0;

@@ -262,4 +235,4 @@ const next: NextFunction = async () => {

}
context.params = match.params;
const handler = match.target.handler as SingleHandler;
const handler = match[0] as SingleHandler;
context.params = match[1];

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

import { LRUCache } from 'lru-cache';
import type PathMatcher from '../PathMatcher/PathMatcher.ts';
import RouteMatcher from '../RouteMatcher/RouteMatcher.ts';
export default class MatcherWithCache<Target = any> {
matcher: PathMatcher<Target>;
export default class MatcherWithCache<
Target = any,
> extends RouteMatcher<Target> {
cache: LRUCache<string, any>;
constructor(matcher: PathMatcher<Target>, size: number = 5000) {
this.matcher = matcher;
constructor(size: number = 5000) {
super();
this.cache = new LRUCache<string, any>({ max: size });
}
add(path: string | RegExp, target: Target) {
this.matcher.add(path, target);
}
match(
path: string,
filter?: (target: Target) => boolean,
fallbacks?: Function[]
) {
if (this.cache.has(path)) {
return this.cache.get(path);
match(method: string, subject: string, fallbacks?: Target[]) {
const key = `${method}:${subject}`;
if (this.cache.has(key)) {
return this.cache.get(key);
}
const result = this.matcher.match(path, filter, fallbacks);
this.cache.set(path, result);
const result = super.match(method, subject, fallbacks);
this.cache.set(key, result);
return result;
}
}

@@ -53,3 +53,3 @@ import { Server, ServerWebSocket, ServerWebSocketSendStatus } from 'bun';

} else if (message instanceof Buffer) {
return this.ws!.sendBinary(message, compress);
return this.ws!.sendBinary(new Uint8Array(message.buffer), compress);
} else if (isBufferSource(message)) {

@@ -99,7 +99,3 @@ return this.ws!.send(message, compress);

ping(data?: string | Bun.BufferSource): ServerWebSocketSendStatus {
if (
typeof data === 'string' ||
data instanceof Buffer ||
isBufferSource(data)
) {
if (typeof data === 'string' || isBufferSource(data)) {
return this.ws!.ping(data);

@@ -111,7 +107,3 @@ } else {

pong(data?: string | Bun.BufferSource): ServerWebSocketSendStatus {
if (
typeof data === 'string' ||
data instanceof Buffer ||
isBufferSource(data)
) {
if (typeof data === 'string' || isBufferSource(data)) {
return this.ws!.pong(data);

@@ -118,0 +110,0 @@ } else {

@@ -5,3 +5,3 @@ import type { ServerWebSocket } from 'bun';

import HttpRouter, { NextFunction } from '../HttpRouter/HttpRouter';
import PathMatcher from '../PathMatcher/PathMatcher';
import RouteMatcher from '../RouteMatcher/RouteMatcher';
import SocketContext, { SocketMessage } from './SocketContext.ts';

@@ -77,3 +77,3 @@

httpRouter: HttpRouter;
pathMatcher: PathMatcher<BunshineHandlers<any>>;
routeMatcher: RouteMatcher<BunshineHandlers<any>>;
handlers: BunHandlers;

@@ -83,3 +83,3 @@ constructor(router: HttpRouter) {

this.httpRouter._wsRouter = this;
this.pathMatcher = new PathMatcher<BunshineHandlers<any>>();
this.routeMatcher = new RouteMatcher<BunshineHandlers<any>>();
this.handlers = {

@@ -104,4 +104,4 @@ open: this._createHandler('open'),

// capture the matcher details
// @ts-expect-error
this.pathMatcher.add(path, handlers);
// @ts-expect-error Handlers are more specific than any
this.routeMatcher.add('ALL', path, handlers);
// console.log('ws handlers registered!', path);

@@ -126,3 +126,2 @@ // create a router path that upgrades to a socket

const error = e as Error;
console.error('WebSocket upgrade error', error);
return c.text('Internal server error', {

@@ -132,5 +131,2 @@ status: 500,

}
console.error(
'WebSocket upgrade failed: Client does not support WebSocket'
);
return c.text('Client does not support WebSocket', {

@@ -154,4 +150,4 @@ status: 426, // 426 Upgrade Required

const pathname = sc.url.pathname;
const matched = this.pathMatcher.match(pathname);
const rest: any = [];
const matched = this.routeMatcher.match('', pathname);
const rest: any[] = [];
if (['message', 'ping', 'pong'].includes(eventName)) {

@@ -162,3 +158,3 @@ rest.push(new SocketMessage(eventName, args[0]));

}
for (const { target } of matched) {
for (const [target] of matched) {
if (!target[eventName]) {

@@ -168,3 +164,3 @@ continue;

try {
target[eventName](sc, ...rest);
target[eventName](sc, rest[0], rest[1]);
} catch (e) {

@@ -171,0 +167,0 @@ const handlerError = e as Error;

{
"include": ["index.ts", "package.json", "src/**/*", "examples", "benchmarks", "bin/**/*"],
"exclude": [],
"exclude": [
"**/*.spec.ts"
],
"compilerOptions": {

@@ -5,0 +7,0 @@ "lib": ["ESNext", "dom", "dom.iterable"],

Sorry, the diff of this file is not supported yet

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