bxo 0.0.5-dev.73 → 0.0.5-dev.75

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.
@@ -0,0 +1,151 @@
1
+ import BXO, { z } from "../src";
2
+
3
+ async function main() {
4
+ const app = new BXO({ serve: { port: 3001 } });
5
+
6
+ // Example 1: Simple cookie setting
7
+ app.get("/set-simple-cookie", (ctx) => {
8
+ // Set a simple cookie
9
+ ctx.set.cookie("theme", "dark");
10
+
11
+ return ctx.json({ message: "Simple cookie set!" });
12
+ });
13
+
14
+ // Example 2: Cookie with options
15
+ app.get("/set-secure-cookie", (ctx) => {
16
+ // Set a secure cookie with various options
17
+ ctx.set.cookie("sessionId", "abc123", {
18
+ httpOnly: true,
19
+ secure: true,
20
+ sameSite: "strict",
21
+ maxAge: 3600, // 1 hour
22
+ path: "/"
23
+ });
24
+
25
+ return ctx.json({ message: "Secure cookie set!" });
26
+ });
27
+
28
+ // Example 3: Multiple cookies
29
+ app.get("/set-multiple-cookies", (ctx) => {
30
+ // Set multiple cookies
31
+ ctx.set.cookie("user", "john_doe", {
32
+ httpOnly: true,
33
+ maxAge: 86400 // 1 day
34
+ });
35
+
36
+ ctx.set.cookie("preferences", "dark_mode", {
37
+ maxAge: 604800 // 1 week
38
+ });
39
+
40
+ ctx.set.cookie("lastVisit", new Date().toISOString(), {
41
+ expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days
42
+ });
43
+
44
+ return ctx.json({ message: "Multiple cookies set!" });
45
+ });
46
+
47
+ // Example 4: Reading cookies
48
+ app.get("/read-cookies", (ctx) => {
49
+ return ctx.json({
50
+ message: "Current cookies:",
51
+ cookies: ctx.cookies,
52
+ theme: ctx.cookies.theme || "not set",
53
+ sessionId: ctx.cookies.sessionId || "not set",
54
+ user: ctx.cookies.user || "not set"
55
+ });
56
+ });
57
+
58
+ // Example 5: Login with cookie validation
59
+ app.post("/login", (ctx) => {
60
+ const { username, password } = ctx.body;
61
+
62
+ // Simple validation (in real app, check against database)
63
+ if (username === "admin" && password === "password") {
64
+ // Set session cookie
65
+ ctx.set.cookie("sessionId", `session_${Date.now()}`, {
66
+ httpOnly: true,
67
+ secure: process.env.NODE_ENV === "production",
68
+ sameSite: "strict",
69
+ maxAge: 3600 // 1 hour
70
+ });
71
+
72
+ ctx.set.cookie("username", username, {
73
+ maxAge: 3600
74
+ });
75
+
76
+ return ctx.json({
77
+ message: "Login successful!",
78
+ username
79
+ });
80
+ }
81
+
82
+ return ctx.status(401, { error: "Invalid credentials" });
83
+ }, {
84
+ body: z.object({
85
+ username: z.string(),
86
+ password: z.string()
87
+ })
88
+ });
89
+
90
+ // Example 6: Protected route with cookie validation
91
+ app.get("/profile", (ctx) => {
92
+ const sessionId = ctx.cookies.sessionId;
93
+ const username = ctx.cookies.username;
94
+
95
+ if (!sessionId) {
96
+ return ctx.status(401, { error: "Not authenticated" });
97
+ }
98
+
99
+ return ctx.json({
100
+ message: "Welcome to your profile!",
101
+ username,
102
+ sessionId: sessionId.substring(0, 10) + "..." // Don't expose full session ID
103
+ });
104
+ }, {
105
+ cookies: z.object({
106
+ sessionId: z.string().optional(),
107
+ username: z.string().optional()
108
+ })
109
+ });
110
+
111
+ // Example 7: Logout (clear cookies)
112
+ app.post("/logout", (ctx) => {
113
+ // Clear cookies by setting them with maxAge: 0
114
+ ctx.set.cookie("sessionId", "", {
115
+ maxAge: 0,
116
+ path: "/"
117
+ });
118
+
119
+ ctx.set.cookie("username", "", {
120
+ maxAge: 0,
121
+ path: "/"
122
+ });
123
+
124
+ return ctx.json({ message: "Logged out successfully" });
125
+ });
126
+
127
+ // Example 8: Cookie with domain and path
128
+ app.get("/set-domain-cookie", (ctx) => {
129
+ ctx.set.cookie("globalPref", "enabled", {
130
+ domain: "localhost", // or your domain
131
+ path: "/api",
132
+ maxAge: 86400
133
+ });
134
+
135
+ return ctx.json({ message: "Domain-specific cookie set!" });
136
+ });
137
+
138
+ app.start();
139
+ console.log(`Cookie example server running on http://localhost:${app.server?.port}`);
140
+ console.log("\nTry these endpoints:");
141
+ console.log("GET /set-simple-cookie");
142
+ console.log("GET /set-secure-cookie");
143
+ console.log("GET /set-multiple-cookies");
144
+ console.log("GET /read-cookies");
145
+ console.log("POST /login (with body: {\"username\": \"admin\", \"password\": \"password\"})");
146
+ console.log("GET /profile");
147
+ console.log("POST /logout");
148
+ console.log("GET /set-domain-cookie");
149
+ }
150
+
151
+ main().catch(console.error);
@@ -0,0 +1,115 @@
1
+ import BXO, { z } from "../src";
2
+
3
+ async function main() {
4
+ const app = new BXO({ serve: { port: 3002 } });
5
+
6
+ // Example 1: Header validation with passthrough
7
+ app.get("/api/headers", (ctx) => {
8
+ return ctx.json({
9
+ message: "Headers received",
10
+ // All headers are available, including extra ones not in schema
11
+ allHeaders: ctx.headers,
12
+ // Schema-validated headers are typed
13
+ contentType: ctx.headers["content-type"],
14
+ authorization: ctx.headers["authorization"]
15
+ });
16
+ }, {
17
+ headers: z.object({
18
+ "content-type": z.string().optional(),
19
+ "authorization": z.string().optional()
20
+ }).passthrough() // This allows extra headers to pass through
21
+ });
22
+
23
+ // Example 2: Cookie validation with passthrough
24
+ app.get("/api/cookies", (ctx) => {
25
+ return ctx.json({
26
+ message: "Cookies received",
27
+ // All cookies are available, including extra ones not in schema
28
+ allCookies: ctx.cookies,
29
+ // Schema-validated cookies are typed
30
+ sessionId: ctx.cookies.sessionId,
31
+ theme: ctx.cookies.theme
32
+ });
33
+ }, {
34
+ cookies: z.object({
35
+ sessionId: z.string().optional(),
36
+ theme: z.enum(["light", "dark"]).optional()
37
+ }).passthrough() // This allows extra cookies to pass through
38
+ });
39
+
40
+ // Example 3: Both headers and cookies with passthrough
41
+ app.post("/api/auth", (ctx) => {
42
+ return ctx.json({
43
+ message: "Authentication successful",
44
+ headers: {
45
+ // Only the required headers are typed
46
+ contentType: ctx.headers["content-type"],
47
+ authorization: ctx.headers["authorization"],
48
+ // But all headers are available
49
+ allHeaders: ctx.headers
50
+ },
51
+ cookies: {
52
+ // Only the required cookies are typed
53
+ sessionId: ctx.cookies.sessionId,
54
+ theme: ctx.cookies.theme,
55
+ // But all cookies are available
56
+ allCookies: ctx.cookies
57
+ }
58
+ });
59
+ }, {
60
+ headers: z.object({
61
+ "content-type": z.string(),
62
+ "authorization": z.string()
63
+ }).passthrough(),
64
+ cookies: z.object({
65
+ sessionId: z.string(),
66
+ theme: z.enum(["light", "dark"])
67
+ }).passthrough(),
68
+ body: z.object({
69
+ username: z.string(),
70
+ password: z.string()
71
+ })
72
+ });
73
+
74
+ // Example 4: Without passthrough (strict validation)
75
+ app.get("/api/strict", (ctx) => {
76
+ return ctx.json({
77
+ message: "Strict validation - only schema fields available",
78
+ headers: ctx.headers,
79
+ cookies: ctx.cookies
80
+ });
81
+ }, {
82
+ headers: z.object({
83
+ "content-type": z.string().optional()
84
+ }), // No passthrough - will fail if extra headers are present
85
+ cookies: z.object({
86
+ sessionId: z.string().optional()
87
+ }) // No passthrough - will fail if extra cookies are present
88
+ });
89
+
90
+ // Example 5: Demonstrating the difference
91
+ app.get("/api/demo", (ctx) => {
92
+ // Set some cookies to demonstrate
93
+ ctx.set.cookie("sessionId", "abc123");
94
+ ctx.set.cookie("theme", "dark");
95
+ ctx.set.cookie("extraCookie", "this-will-be-available-with-passthrough");
96
+
97
+ return ctx.json({
98
+ message: "Check the cookies in your browser dev tools",
99
+ note: "extraCookie will be available in /api/cookies but not in /api/strict"
100
+ });
101
+ });
102
+
103
+ app.start();
104
+ console.log(`Passthrough validation example server running on http://localhost:${app.server?.port}`);
105
+ console.log("\nTry these endpoints:");
106
+ console.log("GET /api/headers (with extra headers)");
107
+ console.log("GET /api/cookies (with extra cookies)");
108
+ console.log("POST /api/auth (with both headers and cookies)");
109
+ console.log("GET /api/strict (strict validation - may fail with extra fields)");
110
+ console.log("GET /api/demo (sets some cookies for testing)");
111
+ console.log("\nExample with curl:");
112
+ console.log('curl -H "Content-Type: application/json" -H "Authorization: Bearer token123" -H "X-Custom-Header: value" -b "sessionId=abc123; theme=dark; extraCookie=test" http://localhost:3002/api/headers');
113
+ }
114
+
115
+ main().catch(console.error);
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  ".": "./src/index.ts",
6
6
  "./plugins": "./plugins/index.ts"
7
7
  },
8
- "version": "0.0.5-dev.73",
8
+ "version": "0.0.5-dev.75",
9
9
  "type": "module",
10
10
  "devDependencies": {
11
11
  "@types/bun": "latest"
package/src/index.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { z } from "zod";
2
2
 
3
-
4
3
  type Method =
5
4
  | "GET"
6
5
  | "POST"
@@ -52,6 +51,16 @@ type QueryObject = Record<string, string | string[]>;
52
51
  type CookieObject = Record<string, string>;
53
52
  type HeaderObject = Record<string, string>;
54
53
 
54
+ export type CookieOptions = {
55
+ domain?: string;
56
+ expires?: Date;
57
+ httpOnly?: boolean;
58
+ maxAge?: number;
59
+ path?: string;
60
+ sameSite?: "strict" | "lax" | "none";
61
+ secure?: boolean;
62
+ };
63
+
55
64
  // Lifecycle hook types
56
65
  export type BeforeRequestHook = (req: Request, ctx?: Partial<Context<any, any>>) => Request | Response | Promise<Request | Response | void>;
57
66
  export type AfterRequestHook = (req: Request, res: Response, ctx?: Partial<Context<any, any>>) => Response | Promise<Response | void>;
@@ -65,7 +74,10 @@ export type Context<P extends string = string, S extends RouteSchema | undefined
65
74
  headers: S extends RouteSchema ? InferOr<S["headers"], HeaderObject> : HeaderObject;
66
75
  cookies: S extends RouteSchema ? InferOr<S["cookies"], CookieObject> : CookieObject;
67
76
  body: S extends RouteSchema ? InferOr<S["body"], unknown> : unknown;
68
- set: { headers: Record<string, string> };
77
+ set: {
78
+ headers: Record<string, string | string[]>;
79
+ cookie: (name: string, value: string, options?: CookieOptions) => void;
80
+ };
69
81
  json: <T>(data: T, status?: number) => Response;
70
82
  text: (data: string, status?: number) => Response;
71
83
  status: <T extends number>(status: T, data: InferResponse<S, T>) => Response;
@@ -108,6 +120,42 @@ function parseCookies(cookieHeader: string | null): CookieObject {
108
120
  return out;
109
121
  }
110
122
 
123
+ function serializeCookie(name: string, value: string, options: CookieOptions = {}): string {
124
+ let cookie = `${name}=${encodeURIComponent(value)}`;
125
+
126
+ if (options.domain) {
127
+ cookie += `; Domain=${options.domain}`;
128
+ }
129
+
130
+ if (options.path) {
131
+ cookie += `; Path=${options.path}`;
132
+ } else {
133
+ cookie += `; Path=/`;
134
+ }
135
+
136
+ if (options.expires) {
137
+ cookie += `; Expires=${options.expires.toUTCString()}`;
138
+ }
139
+
140
+ if (options.maxAge !== undefined) {
141
+ cookie += `; Max-Age=${options.maxAge}`;
142
+ }
143
+
144
+ if (options.httpOnly) {
145
+ cookie += `; HttpOnly`;
146
+ }
147
+
148
+ if (options.secure) {
149
+ cookie += `; Secure`;
150
+ }
151
+
152
+ if (options.sameSite) {
153
+ cookie += `; SameSite=${options.sameSite}`;
154
+ }
155
+
156
+ return cookie;
157
+ }
158
+
111
159
  function parseQuery(searchParams: URLSearchParams): QueryObject;
112
160
  function parseQuery<T extends z.ZodTypeAny>(searchParams: URLSearchParams, schema: T): z.infer<T>;
113
161
  function parseQuery(searchParams: URLSearchParams, schema?: z.ZodTypeAny): any {
@@ -161,9 +209,18 @@ function buildMatcher(path: string): { regex: RegExp | null; names: string[] } {
161
209
  return { regex, names };
162
210
  }
163
211
 
164
- function mergeHeaders(base: HeadersInit | undefined, extra: Record<string, string>): Headers {
212
+ function mergeHeaders(base: HeadersInit | undefined, extra: Record<string, string | string[]>): Headers {
165
213
  const h = new Headers(base);
166
- for (const [k, v] of Object.entries(extra)) h.set(k, v);
214
+ for (const [k, v] of Object.entries(extra)) {
215
+ if (Array.isArray(v)) {
216
+ // For arrays (like Set-Cookie), append each value as a separate header
217
+ for (const value of v) {
218
+ h.append(k, value);
219
+ }
220
+ } else {
221
+ h.set(k, v);
222
+ }
223
+ }
167
224
  return h;
168
225
  }
169
226
 
@@ -277,7 +334,9 @@ export default class BXO {
277
334
  this.server = Bun.serve({
278
335
  ...this.serveOptions,
279
336
  routes: nativeRoutes as any,
280
- fetch: (req: Request) => this.dispatchAny(req, nativeRoutes)
337
+ fetch: (req: Request) => {
338
+ return new Response("Not Found", { status: 404 });
339
+ }
281
340
  });
282
341
  }
283
342
 
@@ -347,7 +406,7 @@ export default class BXO {
347
406
  try {
348
407
  queryObj = route.schema?.query ? parseQuery(url.searchParams, route.schema.query as any) : parseQuery(url.searchParams);
349
408
  } catch (err: any) {
350
- const payload = err?.issues ? { error: "Validation Error", issues: err.issues } : { error: "Validation Error" };
409
+ const payload = err?.issues ? { error: "Validation Error", issues: err.issues } : { error: "Validation Error", issues: [], message: err.message };
351
410
  return new Response(JSON.stringify(payload), { status: 400, headers: { "Content-Type": "application/json" } });
352
411
  }
353
412
 
@@ -355,27 +414,27 @@ export default class BXO {
355
414
  try {
356
415
  const contentType = req.headers.get("content-type") || "";
357
416
  if (contentType.includes("application/json")) {
358
- const raw = await req.json();
417
+ const raw = await req.json().catch(() => undefined);
359
418
  bodyObj = route.schema?.body ? (route.schema.body as any).parse(raw) : raw;
360
419
  } else if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
361
- const fd = await req.formData();
362
- const raw = formDataToObject(fd);
420
+ const fd = await req.formData().catch(() => undefined);
421
+ const raw = formDataToObject(fd || new FormData());
363
422
  bodyObj = route.schema?.body ? (route.schema.body as any).parse(raw) : raw;
364
423
  } else if (contentType.includes("text/")) {
365
- const raw = await req.text();
424
+ const raw = await req.text().catch(() => undefined);
366
425
  bodyObj = route.schema?.body ? (route.schema.body as any).parse(raw) : raw;
367
426
  } else if (contentType) {
368
427
  // Unknown content-type: provide ArrayBuffer
369
- const raw = await req.arrayBuffer();
428
+ const raw = await req.arrayBuffer().catch(() => undefined);
370
429
  bodyObj = route.schema?.body ? (route.schema.body as any).parse(raw) : raw;
371
430
  } else {
372
431
  // No content-type: try JSON then text, otherwise undefined
373
432
  try {
374
- const raw = await req.json();
433
+ const raw = await req.json().catch(() => undefined);
375
434
  bodyObj = route.schema?.body ? (route.schema.body as any).parse(raw) : raw;
376
435
  } catch {
377
436
  try {
378
- const raw = await req.text();
437
+ const raw = await req.text().catch(() => undefined);
379
438
  bodyObj = route.schema?.body ? (route.schema.body as any).parse(raw) : raw;
380
439
  } catch {
381
440
  bodyObj = undefined;
@@ -386,7 +445,7 @@ export default class BXO {
386
445
  return new Response(JSON.stringify({ error: "Body is required" }), { status: 400, headers: { "Content-Type": "application/json" } });
387
446
  }
388
447
  } catch (err: any) {
389
- const payload = err?.issues ? { error: "Validation Error", issues: err.issues } : { error: "Validation Error" };
448
+ const payload = err?.issues ? { error: "Validation Error", issues: err.issues } : { error: "Validation Error", issues: [], message: err.message };
390
449
  return new Response(JSON.stringify(payload), { status: 400, headers: { "Content-Type": "application/json" } });
391
450
  }
392
451
 
@@ -400,7 +459,22 @@ export default class BXO {
400
459
  headers: headerObj,
401
460
  cookies: cookieObj,
402
461
  body: bodyObj,
403
- set: { headers: {} },
462
+ set: {
463
+ headers: {},
464
+ cookie: (name: string, value: string, options: CookieOptions = {}) => {
465
+ const cookieString = serializeCookie(name, value, options);
466
+ const existingCookies = ctx.set.headers["Set-Cookie"];
467
+ if (existingCookies) {
468
+ if (Array.isArray(existingCookies)) {
469
+ existingCookies.push(cookieString);
470
+ } else {
471
+ ctx.set.headers["Set-Cookie"] = [existingCookies, cookieString];
472
+ }
473
+ } else {
474
+ ctx.set.headers["Set-Cookie"] = [cookieString];
475
+ }
476
+ }
477
+ },
404
478
  json: (data, status = 200) => {
405
479
  // Response validation if declared
406
480
  if (route.schema?.response?.[status]) {
@@ -449,13 +523,23 @@ export default class BXO {
449
523
  if (route.schema) {
450
524
  try {
451
525
  if (route.schema.headers) {
452
- (route.schema.headers as any).parse(headerObj);
526
+ const headerSchema = route.schema.headers as any;
527
+ if (headerSchema.passthrough) {
528
+ headerSchema.passthrough().parse(headerObj);
529
+ } else {
530
+ headerSchema.parse(headerObj);
531
+ }
453
532
  }
454
533
  if (route.schema.cookies) {
455
- (route.schema.cookies as any).parse(cookieObj);
534
+ const cookieSchema = route.schema.cookies as any;
535
+ if (cookieSchema.passthrough) {
536
+ cookieSchema.passthrough().parse(cookieObj);
537
+ } else {
538
+ cookieSchema.parse(cookieObj);
539
+ }
456
540
  }
457
541
  } catch (err: any) {
458
- const payload = err?.issues ? { error: "Validation Error", issues: err.issues } : { error: "Validation Error" };
542
+ const payload = err?.issues ? { error: "Validation Error", issues: err.issues } : { error: "Validation Error", issues: [], message: err.message };
459
543
  return new Response(JSON.stringify(payload), { status: 400, headers: { "Content-Type": "application/json" } });
460
544
  }
461
545
  }