riser-bob 0.0.0-stage → 1.0.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/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ # RiserBob Proprietary License
2
+
3
+ Copyright (c) 2026 RiserBob. All rights reserved.
4
+
5
+ ## 1. Ownership
6
+ RiserBob and its original source code, documentation, and associated materials are protected by applicable intellectual property laws. All rights not expressly granted under this license are reserved by the copyright holder.
7
+
8
+ ## 2. Permission to Use
9
+ Subject to the terms of this license, users are granted permission to use the official, unmodified version of RiserBob in their own applications and projects.
10
+
11
+ ## 3. Modification Restrictions
12
+ No person or entity may modify, adapt, alter, or create derivative versions of the RiserBob framework without prior written permission from the copyright holder.
13
+
14
+ ## 4. Distribution Restrictions
15
+ Redistribution, resale, sublicensing, or publication of RiserBob or any modified version of the framework is prohibited unless expressly authorized in writing by the copyright holder.
16
+
17
+ ## 5. Independent Applications
18
+ Users retain ownership of their independently developed application code, subject to any third-party rights and applicable law. Using RiserBob does not, by itself, transfer ownership of an application's independent code to the copyright holder.
19
+
20
+ ## 6. No Implied Permission
21
+ No permission is granted beyond the rights expressly stated in this license. Any exception must be approved in writing by the copyright holder.
22
+
23
+ ## 7. Disclaimer
24
+ RiserBob is provided "AS IS", without warranties of any kind, to the maximum extent permitted by applicable law.
25
+
26
+ ## 8. Contact and Written Permission
27
+ Requests for permission to modify or redistribute RiserBob must be submitted to the copyright holder and approved in writing before the relevant activity takes place.
package/README.md CHANGED
@@ -1,3 +1,231 @@
1
- # Temporary Holding Version
1
+ # 🧽 RiserBob
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A lightweight, fun, and fully tested HTTP framework for [Bun](https://bun.sh), with a built-in router, middleware pipeline, request validation, and consistent error handling.
4
+
5
+ > RiserBob is distributed under the [RiserBob Proprietary License](#license). Please read it before using the framework.
6
+
7
+ ## Features
8
+
9
+ - **Fast routing** with static and dynamic (`/users/:id`) routes, where static routes win over dynamic ones.
10
+ - **Middleware pipeline** with global and per-route middleware, executed in registration order.
11
+ - **Built-in auth middleware** for Bearer-token authentication with your own token verifier.
12
+ - **Validator** with a simple rule syntax (`required`, `string`, `email`, `min:18`, ...).
13
+ - **Response helpers** for JSON, status codes, headers, and redirects.
14
+ - **Consistent errors** through `HttpError` and a central error handler (`404`, `405` with the `Allow` header, `500`).
15
+ - **Fully tested** with Bun's test runner (unit and end-to-end tests).
16
+
17
+ ## Requirements
18
+
19
+ - [Bun](https://bun.sh) 1.3 or newer
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ bun add riser-bob
25
+ ```
26
+
27
+ > The examples below import from `riser-bob`. Adjust the import path if you install or reference the framework differently.
28
+
29
+ ## Quick Start
30
+
31
+ ```ts
32
+ import { RiserBob } from 'riser-bob'
33
+
34
+ const app = new RiserBob()
35
+
36
+ app.spongGet('/', async (spongReq, spongRes) => {
37
+ return spongRes.json({ message: 'Welcome to Bikini Bottom! 🍍' })
38
+ })
39
+
40
+ app.spongListen(3000, () => {
41
+ console.log('RiserBob is running on http://localhost:3000')
42
+ })
43
+ ```
44
+
45
+ Run it:
46
+
47
+ ```bash
48
+ bun run index.ts
49
+ ```
50
+
51
+ ## Routing
52
+
53
+ Register routes with the `Sponge` methods. Every method returns the app, so calls can be chained.
54
+
55
+ ```ts
56
+ app.spongGet('/users', listUsers)
57
+ .spongPost('/users', createUser)
58
+ .spongPut('/users/:id', replaceUser)
59
+ .spongPatch('/users/:id', updateUser)
60
+ .spongDelete('/users/:id', deleteUser)
61
+ ```
62
+
63
+ Paths must start with `/`, and registering the same method and path twice throws an error.
64
+
65
+ ### Route parameters and query strings
66
+
67
+ ```ts
68
+ app.spongGet('/users/:id', async (spongReq, spongRes) => {
69
+ const { id } = spongReq.params // decoded automatically
70
+ const page = spongReq.query.get('page') // URLSearchParams
71
+
72
+ return spongRes.json({ id, page })
73
+ })
74
+ ```
75
+
76
+ ### Reading the request body
77
+
78
+ ```ts
79
+ app.spongPost('/users', async (spongReq, spongRes) => {
80
+ const body = await spongReq.json<{ name: string }>()
81
+
82
+ return spongRes.json({ created: body.name })
83
+ })
84
+ ```
85
+
86
+ `spongReq` also exposes `method`, `path`, `url`, `headers`, `text()`, and the original `request`.
87
+
88
+ ## Middleware
89
+
90
+ A middleware receives the request and a `next` function. Return `next()` to continue, or return a `Response` to stop the chain.
91
+
92
+ ```ts
93
+ import type { SpongMiddleware, SpongRequest } from 'riser-bob'
94
+
95
+ const logger: SpongMiddleware<SpongRequest> = async (spongReq, next) => {
96
+ console.log(`${spongReq.method} ${spongReq.path}`)
97
+ return next()
98
+ }
99
+
100
+ app.spongUse(logger) // global middleware
101
+
102
+ app.spongGet('/admin', handler, adminOnly) // route-specific middleware
103
+ ```
104
+
105
+ > Global middleware is attached when a route is registered, so call `spongUse` **before** registering the routes it should apply to.
106
+
107
+ ### Authentication
108
+
109
+ ```ts
110
+ import { AuthMiddleware } from 'riser-bob'
111
+
112
+ const auth = new AuthMiddleware(async (token) => {
113
+ // Return the user for a valid token, or null for an invalid one.
114
+ return verifyMyToken(token)
115
+ })
116
+
117
+ app.spongGet(
118
+ '/me',
119
+ async (spongReq, spongRes) => {
120
+ return spongRes.json({ user: spongReq.user })
121
+ },
122
+ auth.middleware
123
+ )
124
+ ```
125
+
126
+ The middleware reads the `Authorization: Bearer <token>` header. On success it sets `spongReq.user` and `spongReq.auth`. Otherwise it responds with `401` (`Unauthorized`, `Token is required`, or `Invalid or expired token`).
127
+
128
+ ## Validation
129
+
130
+ ```ts
131
+ import { Validator } from 'riser-bob'
132
+
133
+ const result = Validator.validate(
134
+ { name: 'Zen', age: 17, email: 'zen@example.com' },
135
+ {
136
+ name: ['required', 'string'],
137
+ age: ['required', 'number', 'min:13'],
138
+ email: ['required', 'email']
139
+ }
140
+ )
141
+
142
+ if (!result.valid) {
143
+ console.log(result.errors) // { age: ['age must be at least 13'], ... }
144
+ }
145
+ ```
146
+
147
+ | Rule | Description |
148
+ | ------------------------------- | ------------------------------------------------------ |
149
+ | `required` | Value must be present and not empty. |
150
+ | `nullable` | `null` is allowed. Cannot be combined with `required`. |
151
+ | `string` / `number` / `boolean` | Type checks. Numbers must be finite. |
152
+ | `email` | Must be a valid email address. |
153
+ | `confirmed` | Must match the `<field>Confirmation` value. |
154
+ | `min:n` / `max:n` | Numeric limit for numbers, length limit for strings. |
155
+ | `minLength:n` / `maxLength:n` | String length limits (integers only). |
156
+
157
+ Fields without `required` are optional: missing values and empty strings are skipped.
158
+
159
+ ## Responses and Errors
160
+
161
+ Handlers must return a `Response`. The `spongRes` helper builds one for you, and you can always return a standard `Response` instead.
162
+
163
+ ```ts
164
+ app.spongGet('/ok', async (spongReq, spongRes) => spongRes.json({ ok: true }))
165
+ app.spongGet('/raw', async () => new Response('plain text'))
166
+ ```
167
+
168
+ To fail with a specific status, throw an `HttpError`:
169
+
170
+ ```ts
171
+ import { HttpError } from 'riser-bob'
172
+
173
+ app.spongGet('/secret', async () => {
174
+ throw new HttpError(403, 'Forbidden')
175
+ })
176
+ ```
177
+
178
+ RiserBob handles the common cases for you:
179
+
180
+ | Situation | Response |
181
+ | ------------------------------ | ------------------------------------------------------- |
182
+ | No matching route | `404 Not Found` |
183
+ | Path exists, method does not | `405 Method Not Allowed` with an `Allow` header |
184
+ | Handler throws an `HttpError` | The error's status and message |
185
+ | Handler throws any other error | `500 Internal Server Error` (details are never exposed) |
186
+ | Handler returns no `Response` | `500` |
187
+
188
+ ## Development
189
+
190
+ ```bash
191
+ # Install dependencies
192
+ bun install
193
+
194
+ # Run all tests
195
+ bun test
196
+ ```
197
+
198
+ ```text
199
+ src/
200
+ application/ RiserBob application class
201
+ errors/ HttpError and ErrorHandler
202
+ http/ Response helpers
203
+ middleware/ Middleware pipeline and built-in middleware
204
+ routing/ Router
205
+ server/ HTTP server
206
+ types/ Shared types
207
+ validation/ Validator
208
+ test/
209
+ unit/ Unit tests
210
+ e2e/ End-to-end tests
211
+ ```
212
+
213
+ ## Contributing
214
+
215
+ Contributions are welcome, but because RiserBob is proprietary, please read [CONTRIBUTING.md](CONTRIBUTING.md) first. It explains the contribution terms. Please also follow our [Code of Conduct](CODE_OF_CONDUCT.md).
216
+
217
+ ## Security
218
+
219
+ Please report vulnerabilities privately. See [SECURITY.md](SECURITY.md) for details.
220
+
221
+ ## License
222
+
223
+ RiserBob is released under the **RiserBob Proprietary License**. In short:
224
+
225
+ - You may use the official, unmodified version of RiserBob in your own applications and projects.
226
+ - You may **not** modify, redistribute, resell, or sublicense the framework without prior written permission.
227
+ - You keep full ownership of the application code you write with RiserBob.
228
+
229
+ See the [LICENSE](LICENSE) file for the full terms.
230
+
231
+ Copyright (c) 2026 RiserBob. All rights reserved.
@@ -0,0 +1,17 @@
1
+ import type { SpongHandler } from '../types/spong-handler';
2
+ import type { SpongRequest } from '../types/spong-request';
3
+ import type { SpongMiddleware } from '../types/spong-middleware';
4
+ export declare class RiserBob {
5
+ private readonly router;
6
+ private readonly middlewares;
7
+ private register;
8
+ spongGet(path: string, handler: SpongHandler, ...middlewares: SpongMiddleware<SpongRequest>[]): this;
9
+ spongPost(path: string, handler: SpongHandler, ...middlewares: SpongMiddleware<SpongRequest>[]): this;
10
+ spongPut(path: string, handler: SpongHandler, ...middlewares: SpongMiddleware<SpongRequest>[]): this;
11
+ spongPatch(path: string, handler: SpongHandler, ...middlewares: SpongMiddleware<SpongRequest>[]): this;
12
+ spongDelete(path: string, handler: SpongHandler, ...middlewares: SpongMiddleware<SpongRequest>[]): this;
13
+ spongHead(path: string, handler: SpongHandler, ...middlewares: SpongMiddleware<SpongRequest>[]): this;
14
+ spongOptions(path: string, handler: SpongHandler, ...middlewares: SpongMiddleware<SpongRequest>[]): this;
15
+ spongUse(middleware: SpongMiddleware<SpongRequest>): this;
16
+ spongListen(port?: number, callback?: () => void): Bun.Server<undefined>;
17
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+ import{mkdir as b,writeFile as f}from"fs/promises";import i from"path";function h(o){return o.split(/[-_\s]+/).filter(Boolean).map((e)=>e[0].toUpperCase()+e.slice(1).toLowerCase()).join("")}function w(o){return o.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase().replace(/[_\s]+/g,"-")}function y(o){return/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(o)}async function p(o){if(!y(o))throw Error('Invalid module name. Use letters, numbers, "-" or "_", starting with a letter.');let e=w(o),r=h(e),l=i.resolve(process.cwd(),"src","modules",e),c={[`${e}.controller.ts`]:`import type { SpongHandler } from "riser-bob";
4
+
5
+ export class ${r}Controller {
6
+ readonly create: SpongHandler = async (spongReq, spongRes) => {
7
+ // TODO: Implement creation logic.
8
+ };
9
+
10
+ readonly findAll: SpongHandler = async (spongReq, spongRes) => {
11
+ // TODO: Implement fetching all records.
12
+ };
13
+
14
+ readonly findOne: SpongHandler = async (spongReq, spongRes) => {
15
+ // TODO: Implement fetching a record by ID.
16
+ };
17
+
18
+ readonly update: SpongHandler = async (spongReq, spongRes) => {
19
+ // TODO: Implement updating a record.
20
+ };
21
+
22
+ readonly delete: SpongHandler = async (spongReq, spongRes) => {
23
+ // TODO: Implement deleting a record.
24
+ };
25
+
26
+ verifyToken(token: string) {
27
+ // TODO: Implement token verification.
28
+ return null;
29
+ }
30
+ }
31
+ `,[`${e}.route.ts`]:`import type { RiserBob } from "riser-bob";
32
+ import { AuthMiddleware } from "riser-bob";
33
+ import { ${r}Controller } from "./${e}.controller";
34
+
35
+ export class ${r}Router {
36
+ constructor(
37
+ private readonly bob: RiserBob,
38
+ private readonly controller: ${r}Controller,
39
+ private readonly authMiddleware: AuthMiddleware,
40
+ ) {}
41
+
42
+ register(): void {
43
+ // Public routes
44
+ this.bob.spongPost("/api/${e}", this.controller.create);
45
+ this.bob.spongGet("/api/${e}", this.controller.findAll);
46
+ this.bob.spongGet("/api/${e}/:id", this.controller.findOne);
47
+
48
+ // Protected routes
49
+ this.bob.spongPut(
50
+ "/api/${e}/:id",
51
+ this.controller.update,
52
+ this.authMiddleware.middleware,
53
+ );
54
+
55
+ this.bob.spongDelete(
56
+ "/api/${e}/:id",
57
+ this.controller.delete,
58
+ this.authMiddleware.middleware,
59
+ );
60
+ }
61
+ }
62
+ `,[`${e}.module.ts`]:`import type { RiserBob } from "riser-bob";
63
+ import { AuthMiddleware } from "riser-bob";
64
+ import { ${r}Controller } from "./${e}.controller";
65
+ import { ${r}Router } from "./${e}.route";
66
+
67
+ export class ${r}Module {
68
+ register(bob: RiserBob): void {
69
+ const controller = new ${r}Controller();
70
+
71
+ const authMiddleware = new AuthMiddleware((token) =>
72
+ controller.verifyToken(token),
73
+ );
74
+
75
+ const router = new ${r}Router(
76
+ bob,
77
+ controller,
78
+ authMiddleware,
79
+ );
80
+
81
+ router.register();
82
+ }
83
+ }
84
+ `};await b(l,{recursive:!0});for(let t of Object.keys(c)){let n=i.join(l,t);if(await Bun.file(n).exists())throw Error(`File "${i.relative(process.cwd(),n)}" already exists.`)}let d=[];try{for(let[t,n]of Object.entries(c)){let a=i.join(l,t);await f(a,n,{flag:"wx"}),d.push(a)}}catch(t){let{unlink:n}=await import("fs/promises");throw await Promise.all(d.map((a)=>n(a).catch(()=>{}))),t}console.log(`\uD83E\uDDFD Module "${e}" generated successfully!`),console.log(`\uD83D\uDCC1 ${i.relative(process.cwd(),l)}`);for(let t of Object.keys(c))console.log(` \u2514\u2500\u2500 ${t}`)}var s=process.argv.slice(2),[m,v,g]=s;function u(){console.log(`
85
+ \uD83E\uDDFD RiserBob CLI
86
+
87
+ Usage:
88
+ riserbob g module <name>
89
+ riserbob generate module <name>
90
+ riserbob --help
91
+
92
+ Commands:
93
+ g, generate Generate a component
94
+ --help Show this help message
95
+ `)}async function R(){if(s.length===0||s.includes("--help")||s.includes("-h")){u();return}if(s.includes("--version")||s.includes("-v")){console.log("RiserBob CLI"),console.log("Version is provided by the installed package.");return}if((m==="g"||m==="generate")&&v==="module"&&g){await p(g);return}console.error("\uD83E\uDDFD Oh, barnacles! Unknown command."),u(),process.exitCode=1}R().catch((o)=>{let e=o instanceof Error?o.message:"Something went wrong.";console.error(`\uD83C\uDF4D RiserBob CLI: ${e}`),process.exitCode=1});
@@ -0,0 +1,3 @@
1
+ export declare class ErrorHandler {
2
+ static handle(error: unknown): Response;
3
+ }
@@ -0,0 +1,4 @@
1
+ export declare class HttpError extends Error {
2
+ readonly status: number;
3
+ constructor(status: number, message: string);
4
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Checks whether a request uses JSON as its body format.
3
+ */
4
+ export declare const isJsonContentType: (contentType: string | null) => boolean;
5
+ /**
6
+ * Throws 415 when the request body is not declared as JSON.
7
+ */
8
+ export declare const requireJsonContentType: (contentType: string | null) => void;
9
+ /**
10
+ * Checks whether an Accept header permits a given response media type.
11
+ *
12
+ * Supports exact types, type wildcards, and q= quality values.
13
+ * A missing Accept header means any response type is acceptable.
14
+ */
15
+ export declare const acceptsMediaType: (acceptHeader: string | null, responseType: string) => boolean;
16
+ /**
17
+ * Throws 406 when the client does not accept the response media type.
18
+ */
19
+ export declare const requireAccepts: (acceptHeader: string | null, responseType: string) => void;
@@ -0,0 +1,2 @@
1
+ export declare const DEFAULT_MAX_BODY_SIZE: number;
2
+ export declare const readRequestBody: (request: Request, maxBodySize?: number) => Promise<Uint8Array>;
@@ -0,0 +1,2 @@
1
+ import type { SpongResponse } from '../../types/spong-response';
2
+ export declare function RiserBobResponse(): SpongResponse;
@@ -0,0 +1,65 @@
1
+ export declare const StatusCode: {
2
+ readonly Continue: 100;
3
+ readonly SwitchingProtocols: 101;
4
+ readonly Processing: 102;
5
+ readonly EarlyHints: 103;
6
+ readonly OK: 200;
7
+ readonly Created: 201;
8
+ readonly Accepted: 202;
9
+ readonly NonAuthoritativeInfo: 203;
10
+ readonly NoContent: 204;
11
+ readonly ResetContent: 205;
12
+ readonly PartialContent: 206;
13
+ readonly MultiStatus: 207;
14
+ readonly AlreadyReported: 208;
15
+ readonly IMUsed: 226;
16
+ readonly MultipleChoices: 300;
17
+ readonly MovedPermanently: 301;
18
+ readonly Found: 302;
19
+ readonly SeeOther: 303;
20
+ readonly NotModified: 304;
21
+ readonly UseProxy: 305;
22
+ readonly TemporaryRedirect: 307;
23
+ readonly PermanentRedirect: 308;
24
+ readonly BadRequest: 400;
25
+ readonly Unauthorized: 401;
26
+ readonly PaymentRequired: 402;
27
+ readonly Forbidden: 403;
28
+ readonly NotFound: 404;
29
+ readonly MethodNotAllowed: 405;
30
+ readonly NotAcceptable: 406;
31
+ readonly ProxyAuthenticationRequired: 407;
32
+ readonly RequestTimeout: 408;
33
+ readonly Conflict: 409;
34
+ readonly Gone: 410;
35
+ readonly LengthRequired: 411;
36
+ readonly PreconditionFailed: 412;
37
+ readonly ContentTooLarge: 413;
38
+ readonly URITooLong: 414;
39
+ readonly UnsupportedMediaType: 415;
40
+ readonly RangeNotSatisfiable: 416;
41
+ readonly ExpectationFailed: 417;
42
+ readonly ImATeapot: 418;
43
+ readonly MisdirectedRequest: 421;
44
+ readonly UnprocessableContent: 422;
45
+ readonly Locked: 423;
46
+ readonly FailedDependency: 424;
47
+ readonly TooEarly: 425;
48
+ readonly UpgradeRequired: 426;
49
+ readonly PreconditionRequired: 428;
50
+ readonly TooManyRequests: 429;
51
+ readonly RequestHeaderFieldsTooLarge: 431;
52
+ readonly UnavailableForLegalReasons: 451;
53
+ readonly InternalServerError: 500;
54
+ readonly NotImplemented: 501;
55
+ readonly BadGateway: 502;
56
+ readonly ServiceUnavailable: 503;
57
+ readonly GatewayTimeout: 504;
58
+ readonly HTTPVersionNotSupported: 505;
59
+ readonly VariantAlsoNegotiates: 506;
60
+ readonly InsufficientStorage: 507;
61
+ readonly LoopDetected: 508;
62
+ readonly NotExtended: 510;
63
+ readonly NetworkAuthenticationRequired: 511;
64
+ };
65
+ export type StatusCode = (typeof StatusCode)[keyof typeof StatusCode];
@@ -0,0 +1,14 @@
1
+ export { RiserBob } from './application/riser-bob';
2
+ export { HttpError } from './errors/http-error';
3
+ export { AuthMiddleware } from './middleware/auth-middleware';
4
+ export { corsMiddleware } from './middleware/cors-middleware';
5
+ export { loggerMiddleware } from './middleware/logger-middleware';
6
+ export { MiddlewarePipeline } from './middleware/middleware-pipeline';
7
+ export type { SpongAuth } from './types/spong-auth';
8
+ export type { SpongHandler } from './types/spong-handler';
9
+ export type { SpongMethod } from './types/spong-method';
10
+ export type { SpongMiddleware } from './types/spong-middleware';
11
+ export type { SpongRequest } from './types/spong-request';
12
+ export type { SpongResponse } from './types/spong-response';
13
+ export type { ValidationSchema } from './validation/validator';
14
+ export { Validator } from './validation/validator';
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ // @bun
2
+ class M{routes=new Map;register(e,n,i){let s=`${e} ${n}`;if(this.routes.has(s))throw Error(`\uD83C\uDF4D Oh, barnacles! Route "${s}" already lives in Bikini Bottom! \uD83E\uDDFD`);return this.routes.set(s,i),this}matchPath(e,n){let i=e.split("/").filter(Boolean),s=n.split("/").filter(Boolean);if(i.length!==s.length)return;let r={};for(let o=0;o<i.length;o++){let d=i[o],t=s[o];if(!d||!t)return;if(d.startsWith(":")){let l=d.slice(1);if(!l)return;try{r[l]=decodeURIComponent(t)}catch{return}}else if(d!==t)return}return r}compareRoutes(e,n){let i=e.split("/").filter(Boolean),s=n.split("/").filter(Boolean),r=Math.min(i.length,s.length);for(let o=0;o<r;o++){let d=i[o].startsWith(":"),t=s[o].startsWith(":");if(d!==t)return Number(d)-Number(t)}return s.length-i.length}match(e,n){let i=[...this.routes.entries()].filter(([s])=>s.startsWith(`${e} `)).sort(([s],[r])=>{let o=s.slice(e.length+1),d=r.slice(e.length+1);return this.compareRoutes(o,d)});for(let[s,r]of i){let o=s.slice(e.length+1),d=this.matchPath(o,n);if(d!==void 0)return{handler:r,params:d}}return}allowedMethods(e){return["GET","POST","PUT","PATCH","DELETE"].filter((i)=>this.routes.has(`${i} ${e}`))}}class x{middlewares=[];use(e){return this.middlewares.push(e),this}async execute(e,n,i){let s=-1,r=async(o)=>{if(o<=s)throw Error("[RiserBob] \uD83E\uDDFD Oh, barnacles! next() called multiple times!");s=o;let d=this.middlewares[o];if(!d)return i();return d(e,n,()=>r(o+1))};return r(0)}}class c extends Error{status;constructor(e,n){super(n);this.status=e;this.name="HttpError"}}class S{static handle(e){if(e instanceof c)return Response.json({error:e.message},{status:e.status});return console.error("[RiserBob] \uD83E\uDDFD An unexpected error occurred!",e),Response.json({error:"Internal Server Error"},{status:500})}}var K=(e)=>{if(!e)return!1;let n=e.split(";",1)[0]?.trim().toLowerCase();return n==="application/json"||n?.startsWith("application/")===!0&&n.endsWith("+json")},L=(e)=>{if(!K(e))throw new c(415,"Unsupported Media Type")};var P=10485760,T=async(e,n=P)=>{let i=e.headers.get("content-length");if(i!==null){let l=Number(i);if(!Number.isSafeInteger(l)||l<0)throw new c(400,"Invalid Content-Length");if(l>n)throw new c(413,"Content Too Large")}if(!e.body)return new Uint8Array;let s=e.body.getReader(),r=[],o=0;try{while(!0){let{done:l,value:g}=await s.read();if(l)break;if(o+=g.byteLength,o>n)throw await s.cancel().catch(()=>{}),new c(413,"Content Too Large");r.push(g)}}catch(l){throw await s.cancel().catch(()=>{}),l}finally{s.releaseLock()}let d=new Uint8Array(o),t=0;for(let l of r)d.set(l,t),t+=l.byteLength;return d};var a={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,OK:200,Created:201,Accepted:202,NonAuthoritativeInfo:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,IMUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,ContentTooLarge:413,URITooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableContent:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HTTPVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511};function q(){let e=a.OK,n=new Headers,i=(t)=>{if(!Number.isInteger(t)||t<200||t>599)throw RangeError(`Invalid Fetch Response status: ${t}`)},s=(t)=>t===204||t===205||t===304,r=(t,l=e)=>{if(i(l),s(l))throw TypeError(`Status ${l} cannot have a response body`);return Response.json(t,{status:l,headers:n})},o=(t)=>(i(t),new Response(null,{status:t,headers:n})),d=(t,l="/")=>{let g=new URL(l,"http://localhost");return new Response(null,{status:t,headers:new Headers([...n.entries(),["Location",g.href]])})};return{send(t){let l=new Headers(n);if(!l.has("Content-Type")&&typeof t==="string")l.set("Content-Type","text/plain; charset=utf-8");return new Response(t,{status:e,headers:l})},json:r,status(t){return e=t,this},header(t,l){return n.set(t,l),this},SpongContinue:()=>o(a.Continue),SpongSwitchingProtocols:()=>o(a.SwitchingProtocols),SpongProcessing:()=>o(a.Processing),SpongEarlyHints:()=>o(a.EarlyHints),SpongOK:(t={message:"OK"})=>r(t,a.OK),SpongCreated:(t={message:"Created"})=>r(t,a.Created),SpongAccepted:(t={message:"Accepted"})=>r(t,a.Accepted),SpongNonAuthoritativeInfo:(t={})=>r(t,a.NonAuthoritativeInfo),SpongNoContent:()=>o(a.NoContent),SpongResetContent:()=>o(a.ResetContent),SpongPartialContent:(t={})=>r(t,a.PartialContent),SpongMultiStatus:(t={})=>r(t,a.MultiStatus),SpongAlreadyReported:(t={})=>r(t,a.AlreadyReported),SpongIMUsed:(t={})=>r(t,a.IMUsed),SpongMultipleChoices:(t={})=>r(t,a.MultipleChoices),SpongMovedPermanently:(t)=>d(a.MovedPermanently,t),SpongFound:(t)=>d(a.Found,t),SpongSeeOther:(t)=>d(a.SeeOther,t),SpongNotModified:()=>o(a.NotModified),SpongUseProxy:(t)=>d(a.UseProxy,t),SpongTemporaryRedirect:(t)=>d(a.TemporaryRedirect,t),SpongPermanentRedirect:(t)=>d(a.PermanentRedirect,t),SpongBadRequest:(t={error:"Bad Request"})=>r(t,a.BadRequest),SpongUnauthorized:(t={error:"Unauthorized"})=>r(t,a.Unauthorized),SpongPaymentRequired:(t={})=>r(t,a.PaymentRequired),SpongForbidden:(t={error:"Forbidden"})=>r(t,a.Forbidden),SpongNotFound:(t={error:"Not Found"})=>r(t,a.NotFound),SpongMethodNotAllowed:(t={})=>r(t,a.MethodNotAllowed),SpongNotAcceptable:(t={})=>r(t,a.NotAcceptable),SpongProxyAuthenticationRequired:(t={})=>r(t,a.ProxyAuthenticationRequired),SpongRequestTimeout:(t={})=>r(t,a.RequestTimeout),SpongConflict:(t={})=>r(t,a.Conflict),SpongGone:(t={})=>r(t,a.Gone),SpongLengthRequired:(t={})=>r(t,a.LengthRequired),SpongPreconditionFailed:(t={})=>r(t,a.PreconditionFailed),SpongContentTooLarge:(t={})=>r(t,a.ContentTooLarge),SpongURITooLong:(t={})=>r(t,a.URITooLong),SpongUnsupportedMediaType:(t={})=>r(t,a.UnsupportedMediaType),SpongRangeNotSatisfiable:(t={})=>r(t,a.RangeNotSatisfiable),SpongExpectationFailed:(t={})=>r(t,a.ExpectationFailed),SpongImATeapot:(t={message:"I'm a teapot"})=>r(t,a.ImATeapot),SpongMisdirectedRequest:(t={})=>r(t,a.MisdirectedRequest),SpongUnprocessableContent:(t={})=>r(t,a.UnprocessableContent),SpongLocked:(t={})=>r(t,a.Locked),SpongFailedDependency:(t={})=>r(t,a.FailedDependency),SpongTooEarly:(t={})=>r(t,a.TooEarly),SpongUpgradeRequired:(t={})=>r(t,a.UpgradeRequired),SpongPreconditionRequired:(t={})=>r(t,a.PreconditionRequired),SpongTooManyRequests:(t={})=>r(t,a.TooManyRequests),SpongRequestHeaderFieldsTooLarge:(t={})=>r(t,a.RequestHeaderFieldsTooLarge),SpongUnavailableForLegalReasons:(t={})=>r(t,a.UnavailableForLegalReasons),SpongInternalServerError:(t={error:"Internal Server Error"})=>r(t,a.InternalServerError),SpongNotImplemented:(t={})=>r(t,a.NotImplemented),SpongBadGateway:(t={})=>r(t,a.BadGateway),SpongServiceUnavailable:(t={})=>r(t,a.ServiceUnavailable),SpongGatewayTimeout:(t={})=>r(t,a.GatewayTimeout),SpongHTTPVersionNotSupported:(t={})=>r(t,a.HTTPVersionNotSupported),SpongVariantAlsoNegotiates:(t={})=>r(t,a.VariantAlsoNegotiates),SpongInsufficientStorage:(t={})=>r(t,a.InsufficientStorage),SpongLoopDetected:(t={})=>r(t,a.LoopDetected),SpongNotExtended:(t={})=>r(t,a.NotExtended),SpongNetworkAuthenticationRequired:(t={})=>r(t,a.NetworkAuthenticationRequired)}}var J=globalThis.Bun;var I=(e,n=3000,i)=>{let s=J.serve({port:n,async fetch(r){try{let o=new URL(r.url),d=r.headers.get("origin");if(r.method==="OPTIONS"){let p=new Headers;if(d)p.set("Access-Control-Allow-Origin",d),p.set("Access-Control-Allow-Credentials","true"),p.set("Access-Control-Allow-Methods","GET, POST, PUT, PATCH, DELETE, OPTIONS"),p.set("Access-Control-Allow-Headers","Content-Type, Authorization"),p.set("Access-Control-Max-Age","86400"),p.set("Vary","Origin");return new Response(null,{status:204,headers:p})}let t=e.match(r.method,o.pathname);if(!t){let p=e.allowedMethods(o.pathname);if(p.length>0)return console.error(`[RiserBob] \uD83D\uDE2D\uD83E\uDDFD Method Not Allowed!
3
+ `+`Requested Method: ${r.method}
4
+ Path: ${o.pathname}
5
+ Allowed Methods: ${p.join(", ")}`),Response.json({error:"Method Not Allowed"},{status:405,headers:{Allow:p.join(", ")}});throw console.warn(`[RiserBob] \uD83E\uDDFD\uD83D\uDE02 Welcome to Bikini Bottom!
6
+ \uD83D\uDD0E Requested Method: ${r.method}
7
+ \uD83D\uDEE3\uFE0F Requested Path: ${o.pathname}
8
+ \uD83E\uDD37 Route not found! Did Patrick register it?
9
+ \uD83C\uDF4D SpongeBob: "I checked my pineapple, but the endpoint isn't there!"
10
+ \uD83E\uDD80 Mr. Krabs: "NO ROUTE, NO MONEY!"
11
+ `),new c(404,"Not Found")}let{handler:l,params:g}=t,m=r.headers.get("content-length");if(m!==null&&Number(m)>P)throw new c(413,"Content Too Large");let w={request:r,method:r.method,url:o,path:o.pathname,params:g,query:o.searchParams,headers:r.headers,formData:async()=>{let p=await T(r),u=r.headers.get("content-type");if(!u)throw new c(400,"Missing Content-Type");let O=new Response(p.buffer.slice(p.byteOffset,p.byteOffset+p.byteLength),{headers:{"Content-Type":u}});try{return await O.formData()}catch{throw new c(400,"Invalid form data")}},json:async()=>{L(r.headers.get("content-type"));let p=await T(r),u=new TextDecoder().decode(p);try{return JSON.parse(u)}catch{throw new c(400,"Invalid JSON body")}},text:async()=>{let p=await T(r);return new TextDecoder().decode(p)}},y=q(),h=await l(w,y);if(h instanceof Response)return h;return console.error(`[RiserBob] \uD83D\uDE2D\uD83E\uDDFD Oh no! RiserBob is sad! The Handler forgot to return a valid Response! \uD83D\uDC94
12
+ Method: ${r.method}
13
+ Path: ${o.pathname}
14
+ Expected: Response
15
+ Received: ${h===void 0?"undefined":typeof h}
16
+ \uD83D\uDE22 RiserBob is crying in Bikini Bottom... \uD83C\uDF4D`),S.handle(Error("Handler did not send a response"))}catch(o){return console.error(`[RiserBob] \uD83D\uDE2D\uD83E\uDDFD Oh no! RiserBob is freaking out!
17
+ Something went wrong while handling this request! \uD83D\uDCA5
18
+ RiserBob: "Who lives in a pineapple under a bug?! \uD83D\uDE31\uD83C\uDF4D"
19
+ Error:`,o),S.handle(o)}}});return i?.(),s};class U{router=new M;middlewares=[];register(e,n,i,...s){if(!n.startsWith("/"))throw Error(`\uD83E\uDDFD Oh, barnacles! "${n}" must start with "/"! \uD83C\uDF4D`);let r=new x;for(let d of this.middlewares)r.use(d);for(let d of s)r.use(d);let o=async(d,t)=>r.execute(d,t,async()=>{let l=await i(d,t);if(l instanceof Response)return l;throw console.error("[RiserBob] \uD83E\uDDFD Oh, barnacles! Handler did not return a Response! \uD83C\uDF4D"),new c(500,"Handler did not send a response")});return this.router.register(e,n,o),this}spongGet(e,n,...i){return this.register("GET",e,n,...i)}spongPost(e,n,...i){return this.register("POST",e,n,...i)}spongPut(e,n,...i){return this.register("PUT",e,n,...i)}spongPatch(e,n,...i){return this.register("PATCH",e,n,...i)}spongDelete(e,n,...i){return this.register("DELETE",e,n,...i)}spongHead(e,n,...i){return this.register("HEAD",e,n,...i)}spongOptions(e,n,...i){return this.register("OPTIONS",e,n,...i)}spongUse(e){return this.middlewares.push(e),this}spongListen(e=4000,n){return I(this.router,e,n)}}import{createHmac as Z,timingSafeEqual as Y}from"crypto";var X=/^[\u0021-\u003A\u003C\u003E-\u007E]+$/,Q=/^[\u0021-\u003A\u003C-\u007E]*$/,ee=/^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i,te=/^[\u0020-\u003A\u003D-\u007E]*$/,re=/^[!#$&'()*+\-.\/0-9:<=>?@A-Z[\]\^_`a-z{|}~]*$/,oe=(()=>{let e=function(){};return e.prototype=Object.create(null),e})();function ne(e,n){let i=new oe,s=e.length;if(s<2)return i;let r=n?.decode||de,o=0;do{let d=ae(e,o,s);if(d===s)break;let t=se(e,o,s);if(d>t){o=e.lastIndexOf(";",d-1)+1;continue}let l=B(e,o,d);if(i[l]===void 0)i[l]=r(B(e,d+1,t));o=t+1}while(o<s);return i}function ie(e,n){let i=n?.encode||le;if(!X.test(e.name))throw TypeError(`argument name is invalid: ${e.name}`);let s=e.value==null?"":i(e.value);if(!Q.test(s))throw TypeError(`argument val is invalid: ${e.value}`);let r=e.name+"="+s;if(e.maxAge!==void 0){if(!Number.isInteger(e.maxAge))throw TypeError(`option maxAge is invalid: ${e.maxAge}`);r+="; Max-Age="+e.maxAge}if(e.domain){if(!ee.test(e.domain))throw TypeError(`option domain is invalid: ${e.domain}`);r+="; Domain="+e.domain}if(e.path){if(!te.test(e.path))throw TypeError(`option path is invalid: ${e.path}`);r+="; Path="+e.path}if(e.expires){if(!Number.isFinite(e.expires.valueOf()))throw TypeError(`option expires is invalid: ${e.expires}`);r+="; Expires="+e.expires.toUTCString()}if(e.httpOnly)r+="; HttpOnly";if(e.secure)r+="; Secure";if(e.partitioned)r+="; Partitioned";if(e.priority)switch(typeof e.priority==="string"?e.priority.toLowerCase():void 0){case"low":r+="; Priority=Low";break;case"medium":r+="; Priority=Medium";break;case"high":r+="; Priority=High";break;default:throw TypeError(`option priority is invalid: ${e.priority}`)}if(e.sameSite)switch(typeof e.sameSite==="string"?e.sameSite.toLowerCase():e.sameSite){case!0:case"strict":r+="; SameSite=Strict";break;case"lax":r+="; SameSite=Lax";break;case"none":r+="; SameSite=None";break;default:throw TypeError(`option sameSite is invalid: ${e.sameSite}`)}return r}function se(e,n,i){let s=e.indexOf(";",n);return s===-1?i:s}function ae(e,n,i){let s=e.indexOf("=",n);return s===-1?i:s}function B(e,n,i){if(n===i)return"";let s=n,r=i;do{let o=e.charCodeAt(s);if(o!==32&&o!==9)break}while(++s<r);while(r>s){let o=e.charCodeAt(r-1);if(o!==32&&o!==9)break;r--}return e.slice(s,r)}function de(e){if(e.indexOf("%")===-1)return e;try{return decodeURIComponent(e)}catch(n){return e}}function le(e){return re.test(e)?e:encodeURIComponent(e)}class f extends Error{code;constructor(e,n,i){super(n,i);this.code=e,this.name="CookieError"}}var pe=Object.freeze({path:"/",httpOnly:!0,secure:!0,sameSite:"lax"}),D=4096,F=34560000,ue=16384,V=32,ce=/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/,ge=/^\/[\x20-\x3A\x3C-\x7E]*$/,_="[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?",me=new RegExp(`^\\.?${_}(?:\\.${_})*$`,"i"),he=new Set(["lax","strict","none"]),fe=new Set(["low","medium","high"]);function N(e){return Object.fromEntries(Object.entries(e).filter(([,n])=>n!==void 0))}function j(e,n,i){return Z("sha256",i).update(`${e}=${n}`).digest("base64url")}class A{cookies=new Map;pending=new Map;secrets;defaults;headerRejected;constructor(e,n={}){let i=n.secrets??[];for(let o of i)if(typeof o!=="string"||o.length<V)throw new f("WEAK_SECRET",`Cookie secrets must be strings of at least ${V} characters.`);this.secrets=[...i],this.defaults=N(n.defaults??{}),this.resolveOptions("probe",{});let s=n.maxHeaderBytes??ue,r=!1;if(typeof e==="string"&&e!=="")if(Buffer.byteLength(e,"utf8")>s)r=!0;else this.load(e);this.headerRejected=r}static fromRequest(e,n){return new A(e.headers.get("cookie"),n)}get(e){return this.cookies.get(e)}has(e){return this.cookies.has(e)}names(){return[...this.cookies.keys()]}getAll(){return Object.fromEntries(this.cookies)}set(e,n,i={}){let s=this.resolveOptions(e,i),r=this.serialize(e,n,s);if(this.record(e,s,r),this.isExpired(s))this.cookies.delete(e);else this.cookies.set(e,n);return this}setSigned(e,n,i={}){let s=this.secrets[0];if(s===void 0)throw new f("SECRETS_REQUIRED","Signed cookies need at least one secret in CookieManagerOptions.secrets.");if(typeof n!=="string")throw new f("INVALID_VALUE","Cookie value must be a string.");return this.set(e,`${n}.${j(e,n,s)}`,i)}getSigned(e){let n=this.cookies.get(e);if(n===void 0)return;let i=n.lastIndexOf(".");if(i===-1)return;let s=n.slice(0,i),r=Buffer.from(n.slice(i+1),"utf8"),o=!1;for(let d of this.secrets){let t=Buffer.from(j(e,s,d),"utf8");if(r.length===t.length&&Y(r,t))o=!0}return o?s:void 0}delete(e,n={}){let i=this.resolveOptions(e,{...N(n),expires:new Date(0),maxAge:0}),s=this.serialize(e,"",i);return this.record(e,i,s),this.cookies.delete(e),this}getSetCookieHeaders(){return[...this.pending.values()]}applyTo(e){let n=this.getSetCookieHeaders();if(n.length===0)return e;try{for(let i of n)e.headers.append("Set-Cookie",i);return e}catch{let i=new Response(e.body,{status:e.status,statusText:e.statusText,headers:new Headers(e.headers)});for(let s of n)i.headers.append("Set-Cookie",s);return i}}load(e){let n;try{n=ne(e)}catch{return}for(let[i,s]of Object.entries(n))if(s!==void 0)this.cookies.set(i,s)}resolveOptions(e,n){if(typeof e!=="string"||!ce.test(e))throw new f("INVALID_NAME",`Invalid cookie name: ${JSON.stringify(e)}.`);let i={...pe,...this.defaults,...N(n)},s=(o)=>{throw new f("INVALID_OPTION",`Cookie "${e}": ${o}`)};if(i.maxAge!==void 0){if(!Number.isSafeInteger(i.maxAge)||i.maxAge<0)s("maxAge must be a non-negative integer (seconds).");i.maxAge=Math.min(i.maxAge,F)}if(i.expires!==void 0){if(!(i.expires instanceof Date)||Number.isNaN(i.expires.getTime()))s("expires must be a valid Date.");let o=Date.now()+F*1000;if(i.expires.getTime()>o)i.expires=new Date(o)}if(i.path!==void 0&&!ge.test(i.path))s('path must start with "/" and not contain ";" or control characters.');if(i.domain!==void 0&&(i.domain.length>253||!me.test(i.domain)))s("domain is not a valid host name.");if(i.priority!==void 0&&!fe.has(i.priority))s('priority must be "low", "medium" or "high".');if(typeof i.sameSite==="string"&&!he.has(i.sameSite))s('sameSite must be "lax", "strict" or "none".');let r=e.startsWith("__Host-");if((r||e.startsWith("__Secure-")||i.sameSite==="none"||i.partitioned===!0)&&i.secure!==!0)s("this cookie requires the Secure attribute (__Host-/__Secure- prefix, SameSite=None or Partitioned).");if(r){if(i.domain!==void 0)s("__Host- cookies cannot set a domain.");if(i.path!=="/")s('__Host- cookies must use path "/".')}return i}serialize(e,n,i){if(typeof n!=="string")throw new f("INVALID_VALUE","Cookie value must be a string.");let s;try{s=ie({name:e,value:n,...i})}catch(r){throw new f("SERIALIZATION_FAILED",`Cookie "${e}" could not be serialized.`,{cause:r})}if(Buffer.byteLength(s,"utf8")>D)throw new f("COOKIE_TOO_LARGE",`Cookie "${e}" is larger than ${D} bytes.`);return s}record(e,n,i){let s=[e,n.domain?.toLowerCase()??"",n.path??"/",n.partitioned===!0?"partitioned":""].join("\x00");this.pending.delete(s),this.pending.set(s,i)}isExpired(e){return e.maxAge===0||e.expires!==void 0&&e.expires.getTime()<=Date.now()}}class z{verifyToken;mode;cookieName;constructor(e,n={}){this.verifyToken=e;this.mode=n.mode??"both",this.cookieName=n.cookieName??"token"}getToken(e){if(this.mode==="bearer"||this.mode==="both"){let n=e.headers.get("authorization");if(n){let i=n.match(/^Bearer\s+(\S+)$/i);if(!i)return null;return i[1]??null}if(this.mode==="bearer")return null}if(this.mode==="cookie"||this.mode==="both")return A.fromRequest(e.request).get(this.cookieName)??null;return null}middleware=async(e,n,i)=>{let s=this.getToken(e);if(!s)return n.SpongUnauthorized({success:!1,message:"Authentication token is missing or invalid."});let r;try{r=await this.verifyToken(s)}catch(o){return console.error("[RiserBob] Token verification failed:",o),S.handle(o)}if(!r)return n.SpongUnauthorized({success:!1,message:"Token is invalid or expired."});return e.user=r,e.auth={token:s},i()}}var ye=["GET","POST","PUT","PATCH","DELETE"],we=["Content-Type","Authorization"],Se=(e)=>{let{origin:n,credentials:i=!1,methods:s=ye,allowedHeaders:r=we,exposedHeaders:o=[],maxAge:d=86400}=e;if(n==="*"&&i)throw TypeError('[RiserBob] CORS cannot use "*" with credentials enabled.');if(!Array.isArray(s)||s.length===0)throw TypeError("[RiserBob] CORS methods must contain at least one method.");if(!Array.isArray(r))throw TypeError("[RiserBob] CORS allowedHeaders must be an array.");if(!Array.isArray(o))throw TypeError("[RiserBob] CORS exposedHeaders must be an array.");if(!Number.isInteger(d)||d<0)throw TypeError("[RiserBob] CORS maxAge must be a non-negative integer.");let t=new Set(Array.isArray(n)?n:[]),l=s.join(", "),g=r.join(", "),m=o.join(", ");return async(w,y,h)=>{let p=w.request.headers.get("origin"),u=new Headers;if(n==="*"?!0:p!==null&&t.has(p)||typeof n==="string"&&n!=="*"&&p===n){if(u.set("Access-Control-Allow-Origin",n==="*"?"*":p),i)u.set("Access-Control-Allow-Credentials","true");if(u.set("Access-Control-Allow-Methods",l),r.length>0)u.set("Access-Control-Allow-Headers",g);if(o.length>0)u.set("Access-Control-Expose-Headers",m);if(u.set("Access-Control-Max-Age",String(d)),n!=="*")u.set("Vary","Origin")}if(w.method==="OPTIONS")return new Response(null,{status:204,headers:u});let R=await h(),b=new Headers(R.headers);for(let[k,W]of u)if(k.toLowerCase()==="vary"){let H=b.get("Vary"),E=H?H.split(",").map((v)=>v.trim()):[];if(!E.some((v)=>v.toLowerCase()==="origin"))E.push("Origin");b.set("Vary",E.join(", "))}else b.set(k,W);return new Response(R.body,{status:R.status,statusText:R.statusText,headers:b})}};var Re=async(e,n,i)=>{let s=performance.now(),r=500;try{let o=await i();return r=o.status,o}catch(o){throw console.error("[RiserBob] \uD83D\uDC80\uD83E\uDDFD Barnacles! Request failed!",o),o}finally{let o=(performance.now()-s).toFixed(2),d=new Date().toISOString();console.log(`[RiserBob] ${d} | ${e.method} ${e.path} | ${r} | ${o}ms`)}};var be=new Set(["required","nullable","string","number","boolean","email","confirmed","min","max","minLength","maxLength"]);class G{static parseSchema(e){let n={};for(let[i,s]of Object.entries(e)){if(!Array.isArray(s))throw TypeError(`Invalid validation rules for "${i}"`);n[i]=s.map((o)=>{if(typeof o!=="string")throw TypeError(`Invalid validation rule for "${i}"`);let d=o.indexOf(":"),t=d===-1?o:o.slice(0,d);if(!be.has(t))throw TypeError(`Unknown validation rule: ${o}`);if(!["min","max","minLength","maxLength"].includes(t)){if(d!==-1)throw TypeError(`Rule "${t}" does not accept a limit`);return{name:t}}if(d===-1)throw TypeError(`Rule "${t}" requires a limit`);let g=o.slice(d+1);if(!/^\d+(?:\.\d+)?$/.test(g))throw TypeError(`Invalid validation limit: ${o}`);let m=Number(g);if(!Number.isFinite(m))throw TypeError(`Invalid validation limit: ${o}`);if(t.endsWith("Length")&&!Number.isInteger(m))throw TypeError(`Rule "${t}" requires an integer limit`);return{name:t,limit:m}});let r=n[i].map((o)=>o.name);if(r.includes("required")&&r.includes("nullable"))throw console.error(`[RiserBob] \uD83E\uDDFD\uD83C\uDF4D Oh, barnacles! Invalid Validation Schema!
20
+ Field: ${i}
21
+ \uD83D\uDCA5 "required" and "nullable" cannot be combined!
22
+ \uD83E\uDD91 Squidward says: "Fix your Schema already!"`),TypeError(`[RiserBob] Invalid rules for field "${i}".`)}return n}static validate(e,n){if(e===null||typeof e!=="object"||Array.isArray(e))throw TypeError("Validation data must be a plain object");let i=this.parseSchema(n),s={},r=(o,d)=>{(s[o]??=[]).push(d)};for(let[o,d]of Object.entries(i)){let t=e[o],l=d.map((h)=>h.name),g=l.includes("required"),m=l.includes("nullable"),w=t===void 0||t===null||typeof t==="string"&&t.trim()==="";if(g&&w){r(o,`${o} is required`);continue}if(t===void 0)continue;if(t===null&&m)continue;if(typeof t==="string"&&t.trim()===""&&!g)continue;let y=!1;if(l.includes("string")&&typeof t!=="string")r(o,`${o} must be a string`),y=!0;if(l.includes("number")&&(typeof t!=="number"||!Number.isFinite(t)))r(o,`${o} must be a number`),y=!0;if(l.includes("boolean")&&typeof t!=="boolean")r(o,`${o} must be a boolean`),y=!0;if(y)continue;if(l.includes("email")&&(typeof t!=="string"||!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)))r(o,`${o} must be a valid email`);if(l.includes("confirmed")&&t!==e[`${o}Confirmation`])r(o,`${o} confirmation does not match`);for(let h of d){let{name:p,limit:u}=h;if(u===void 0)continue;if(p==="min"||p==="max")if(typeof t==="number"){if(p==="min"&&t<u)r(o,`${o} must be at least ${u}`);if(p==="max"&&t>u)r(o,`${o} may not be greater than ${u}`)}else if(typeof t==="string"){if(p==="min"&&t.length<u)r(o,`${o} must be at least ${u} characters`);if(p==="max"&&t.length>u)r(o,`${o} may not exceed ${u} characters`)}else r(o,`${o} must be a string or number for ${p}`);if(p==="minLength"){if(typeof t!=="string")r(o,`${o} must be a string`);else if(t.length<u)r(o,`${o} must be at least ${u} characters`)}if(p==="maxLength"){if(typeof t!=="string")r(o,`${o} must be a string`);else if(t.length>u)r(o,`${o} may not exceed ${u} characters`)}}}if(Object.keys(s).length>0)return{valid:!1,errors:s};return{valid:!0,data:e,errors:{}}}}export{z as AuthMiddleware,c as HttpError,x as MiddlewarePipeline,U as RiserBob,G as Validator,Se as corsMiddleware,Re as loggerMiddleware};
@@ -0,0 +1,21 @@
1
+ import type { SpongMiddleware } from '../types/spong-middleware';
2
+ import type { SpongRequest } from '../types/spong-request';
3
+ interface VerifiedUser {
4
+ id: string;
5
+ [key: string]: unknown;
6
+ }
7
+ type TokenVerifier = (token: string) => Promise<VerifiedUser | null> | VerifiedUser | null;
8
+ export type AuthMode = 'cookie' | 'bearer' | 'both';
9
+ export interface AuthMiddlewareOptions {
10
+ mode?: AuthMode;
11
+ cookieName?: string;
12
+ }
13
+ export declare class AuthMiddleware {
14
+ private readonly verifyToken;
15
+ private readonly mode;
16
+ private readonly cookieName;
17
+ constructor(verifyToken: TokenVerifier, options?: AuthMiddlewareOptions);
18
+ private getToken;
19
+ middleware: SpongMiddleware<SpongRequest>;
20
+ }
21
+ export {};
@@ -0,0 +1,15 @@
1
+ import type { SpongMiddleware } from '../types/spong-middleware';
2
+ import type { SpongRequest } from '../types/spong-request';
3
+ export interface CorsOptions {
4
+ /**
5
+ * Allowed origins.
6
+ * Use '*' to allow any origin only when credentials are disabled.
7
+ */
8
+ origin: string | string[] | '*';
9
+ credentials?: boolean;
10
+ methods?: string[];
11
+ allowedHeaders?: string[];
12
+ exposedHeaders?: string[];
13
+ maxAge?: number;
14
+ }
15
+ export declare const corsMiddleware: (options: CorsOptions) => SpongMiddleware<SpongRequest>;
@@ -0,0 +1,5 @@
1
+ import type { SpongRequest } from '../types/spong-request';
2
+ import type { SpongResponse } from '../types/spong-response';
3
+ type SpongNext = () => Promise<Response>;
4
+ export declare const loggerMiddleware: (spongReq: SpongRequest, spongRes: SpongResponse, spongNext: SpongNext) => Promise<Response>;
5
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { SpongMiddleware } from '../types/spong-middleware';
2
+ import type { SpongResponse } from '../types/spong-response';
3
+ export type SpongNext = () => Promise<Response>;
4
+ export declare class MiddlewarePipeline<Context = unknown> {
5
+ private readonly middlewares;
6
+ use(middleware: SpongMiddleware<Context>): this;
7
+ execute(context: Context, spongRes: SpongResponse, finalHandler: SpongNext): Promise<Response>;
8
+ }
@@ -0,0 +1,13 @@
1
+ import type { SpongHandler } from '../types/spong-handler';
2
+ import { type HttpMethod } from '../types/spong-method';
3
+ export declare class Router {
4
+ private routes;
5
+ register(method: HttpMethod, path: string, handler: SpongHandler): this;
6
+ private matchPath;
7
+ private compareRoutes;
8
+ match(method: string, path: string): {
9
+ handler: SpongHandler;
10
+ params: Record<string, string>;
11
+ } | undefined;
12
+ allowedMethods(path: string): HttpMethod[];
13
+ }
@@ -0,0 +1,3 @@
1
+ import type { Router } from '../routing/router';
2
+ import Bun from 'bun';
3
+ export declare const HttpServer: (router: Router, port?: number, callback?: () => void) => Bun.Server<undefined>;
@@ -0,0 +1,3 @@
1
+ export interface SpongAuth {
2
+ token: string;
3
+ }
@@ -0,0 +1,4 @@
1
+ import type { SpongRequest } from './spong-request';
2
+ import type { SpongResponse } from './spong-response';
3
+ export type SpongHandler = (spongReq: SpongRequest, spongRes: SpongResponse) => Response | void | Promise<Response | void>;
4
+ export type RouteHandler = SpongHandler;
@@ -0,0 +1,10 @@
1
+ export declare enum SpongMethod {
2
+ GET = "GET",
3
+ POST = "POST",
4
+ PUT = "PUT",
5
+ PATCH = "PATCH",
6
+ DELETE = "DELETE",
7
+ HEAD = "HEAD",
8
+ OPTIONS = "OPTIONS"
9
+ }
10
+ export type HttpMethod = SpongMethod;
@@ -0,0 +1,3 @@
1
+ import type { SpongResponse } from "./spong-response";
2
+ export type SpongNext = () => Promise<Response>;
3
+ export type SpongMiddleware<Context = unknown> = (context: Context, spongRes: SpongResponse, spongNext: SpongNext) => Response | Promise<Response>;
@@ -0,0 +1,20 @@
1
+ import type { SpongAuth } from './spong-auth';
2
+ interface AuthenticationUser {
3
+ id: string;
4
+ [key: string]: unknown;
5
+ }
6
+ export interface SpongRequest {
7
+ readonly request: Request;
8
+ readonly method: string;
9
+ readonly url: URL;
10
+ readonly path: string;
11
+ readonly params: Record<string, string>;
12
+ readonly query: URLSearchParams;
13
+ readonly headers: Headers;
14
+ user?: AuthenticationUser;
15
+ auth?: SpongAuth;
16
+ json<T = unknown>(): Promise<T>;
17
+ formData(): ReturnType<Response['formData']>;
18
+ text(): Promise<string>;
19
+ }
20
+ export {};
@@ -0,0 +1,68 @@
1
+ export interface SpongResponse {
2
+ send(body: string): Response;
3
+ json(data: unknown, status?: number): Response;
4
+ status(code: number): SpongResponse;
5
+ header(name: string, value: string): SpongResponse;
6
+ SpongContinue(): Response;
7
+ SpongSwitchingProtocols(): Response;
8
+ SpongProcessing(): Response;
9
+ SpongEarlyHints(): Response;
10
+ SpongOK(data?: unknown): Response;
11
+ SpongCreated(data?: unknown): Response;
12
+ SpongAccepted(data?: unknown): Response;
13
+ SpongNonAuthoritativeInfo(data?: unknown): Response;
14
+ SpongNoContent(): Response;
15
+ SpongResetContent(): Response;
16
+ SpongPartialContent(data?: unknown): Response;
17
+ SpongMultiStatus(data?: unknown): Response;
18
+ SpongAlreadyReported(data?: unknown): Response;
19
+ SpongIMUsed(data?: unknown): Response;
20
+ SpongMultipleChoices(data?: unknown): Response;
21
+ SpongMovedPermanently(url?: string): Response;
22
+ SpongFound(url?: string): Response;
23
+ SpongSeeOther(url?: string): Response;
24
+ SpongNotModified(): Response;
25
+ SpongUseProxy(url?: string): Response;
26
+ SpongTemporaryRedirect(url?: string): Response;
27
+ SpongPermanentRedirect(url?: string): Response;
28
+ SpongBadRequest(data?: unknown): Response;
29
+ SpongUnauthorized(data?: unknown): Response;
30
+ SpongPaymentRequired(data?: unknown): Response;
31
+ SpongForbidden(data?: unknown): Response;
32
+ SpongNotFound(data?: unknown): Response;
33
+ SpongMethodNotAllowed(data?: unknown): Response;
34
+ SpongNotAcceptable(data?: unknown): Response;
35
+ SpongProxyAuthenticationRequired(data?: unknown): Response;
36
+ SpongRequestTimeout(data?: unknown): Response;
37
+ SpongConflict(data?: unknown): Response;
38
+ SpongGone(data?: unknown): Response;
39
+ SpongLengthRequired(data?: unknown): Response;
40
+ SpongPreconditionFailed(data?: unknown): Response;
41
+ SpongContentTooLarge(data?: unknown): Response;
42
+ SpongURITooLong(data?: unknown): Response;
43
+ SpongUnsupportedMediaType(data?: unknown): Response;
44
+ SpongRangeNotSatisfiable(data?: unknown): Response;
45
+ SpongExpectationFailed(data?: unknown): Response;
46
+ SpongImATeapot(data?: unknown): Response;
47
+ SpongMisdirectedRequest(data?: unknown): Response;
48
+ SpongUnprocessableContent(data?: unknown): Response;
49
+ SpongLocked(data?: unknown): Response;
50
+ SpongFailedDependency(data?: unknown): Response;
51
+ SpongTooEarly(data?: unknown): Response;
52
+ SpongUpgradeRequired(data?: unknown): Response;
53
+ SpongPreconditionRequired(data?: unknown): Response;
54
+ SpongTooManyRequests(data?: unknown): Response;
55
+ SpongRequestHeaderFieldsTooLarge(data?: unknown): Response;
56
+ SpongUnavailableForLegalReasons(data?: unknown): Response;
57
+ SpongInternalServerError(data?: unknown): Response;
58
+ SpongNotImplemented(data?: unknown): Response;
59
+ SpongBadGateway(data?: unknown): Response;
60
+ SpongServiceUnavailable(data?: unknown): Response;
61
+ SpongGatewayTimeout(data?: unknown): Response;
62
+ SpongHTTPVersionNotSupported(data?: unknown): Response;
63
+ SpongVariantAlsoNegotiates(data?: unknown): Response;
64
+ SpongInsufficientStorage(data?: unknown): Response;
65
+ SpongLoopDetected(data?: unknown): Response;
66
+ SpongNotExtended(data?: unknown): Response;
67
+ SpongNetworkAuthenticationRequired(data?: unknown): Response;
68
+ }
@@ -0,0 +1,15 @@
1
+ export type ValidationRule = 'required' | 'nullable' | 'string' | 'number' | 'boolean' | 'email' | 'confirmed' | `min:${number}` | `max:${number}` | `minLength:${number}` | `maxLength:${number}`;
2
+ export type ValidationSchema = Record<string, ValidationRule[]>;
3
+ export type ValidationErrors = Record<string, string[]>;
4
+ export type ValidationResult<T> = {
5
+ valid: true;
6
+ data: T;
7
+ errors: ValidationErrors;
8
+ } | {
9
+ valid: false;
10
+ errors: ValidationErrors;
11
+ };
12
+ export declare class Validator {
13
+ private static parseSchema;
14
+ static validate<T>(data: Record<string, unknown>, schema: ValidationSchema): ValidationResult<T>;
15
+ }
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
- "name": "riser-bob",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "riser-bob",
3
+ "version": "1.0.0",
4
+ "description": "A TypeScript backend framework built for Bun",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "bin": {
21
+ "riser-bob": "./dist/cli.js"
22
+ },
23
+ "scripts": {
24
+ "dev": "bun run --watch src/index.ts",
25
+ "build": "bun run build:framework && bun run build:types && bun run build:cli",
26
+ "build:framework": "bun build src/index.ts --outdir dist --target bun --minify",
27
+ "build:cli": "bun build cli/index.ts --outfile dist/cli.js --target bun --minify",
28
+ "build:types": "tsc -p tsconfig.build.json",
29
+ "start": "bun run src/index.ts",
30
+ "typecheck": "tsc --noEmit",
31
+ "lint": "eslint test/",
32
+ "lint:fix": "eslint test/ --fix"
33
+ },
34
+ "keywords": [
35
+ "riser-bob",
36
+ "bun",
37
+ "typescript",
38
+ "backend",
39
+ "framework",
40
+ "cli"
41
+ ],
42
+ "license": "MIT",
43
+ "devDependencies": {
44
+ "@eslint/js": "^10.0.1",
45
+ "@types/bun": "^1.4.3",
46
+ "eslint": "^10.13.0",
47
+ "globals": "^17.13.0",
48
+ "jiti": "^2.7.0",
49
+ "typescript": "^5.9.3",
50
+ "typescript-eslint": "^8.71.2"
51
+ },
52
+ "peerDependencies": {
53
+ "typescript": "^5"
54
+ },
55
+ "dependencies": {
56
+ "@riser-bob/cookie": "0.1.1"
57
+ }
58
+ }