bxo 0.0.5-dev.47 → 0.0.5-dev.49

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/index.ts CHANGED
@@ -9,15 +9,14 @@ type StatusResponseSchema = Record<number, ResponseSchema>;
9
9
  type ResponseConfig = ResponseSchema | StatusResponseSchema;
10
10
 
11
11
  // Type utility to extract response type from response config
12
- type InferResponseType<T> = T extends ResponseSchema
12
+ type InferResponseType<T> = T extends ResponseSchema
13
13
  ? InferZodType<T>
14
- : T extends StatusResponseSchema
15
- ? { [K in keyof T]: InferZodType<T[K]> }[keyof T]
16
- : never;
14
+ : T extends StatusResponseSchema
15
+ ? { [K in keyof T]: InferZodType<T[K]> }[keyof T]
16
+ : never;
17
17
 
18
- // Cookie interface
19
- interface Cookie {
20
- name: string;
18
+ // Cookie options interface for setting cookies
19
+ interface CookieOptions {
21
20
  value: string;
22
21
  domain?: string;
23
22
  path?: string;
@@ -64,34 +63,47 @@ export type Context<TConfig extends RouteConfig = {}> = {
64
63
  path: string;
65
64
  request: Request;
66
65
  set: {
67
- status?: number;
68
- headers?: Record<string, string>;
69
- cookies?: Cookie[];
66
+ status: number;
67
+ headers: Record<string, string>;
68
+ cookies: (name: string, options: CookieOptions) => void;
70
69
  redirect?: { location: string; status?: number };
71
70
  };
72
71
  status: <T extends number>(
73
- code: TConfig['response'] extends StatusResponseSchema
72
+ code: TConfig['response'] extends StatusResponseSchema
74
73
  ? StatusCodes<TConfig['response']> | number
75
74
  : T,
76
- data?: TConfig['response'] extends StatusResponseSchema
77
- ? T extends keyof TConfig['response']
78
- ? InferZodType<TConfig['response'][T]>
79
- : any
80
- : TConfig['response'] extends ResponseSchema
81
- ? InferZodType<TConfig['response']>
82
- : any
83
- ) => TConfig['response'] extends StatusResponseSchema
84
- ? T extends keyof TConfig['response']
75
+ data?: TConfig['response'] extends StatusResponseSchema
76
+ ? T extends keyof TConfig['response']
85
77
  ? InferZodType<TConfig['response'][T]>
86
78
  : any
87
- : TConfig['response'] extends ResponseSchema
79
+ : TConfig['response'] extends ResponseSchema
88
80
  ? InferZodType<TConfig['response']>
89
- : any;
81
+ : any
82
+ ) => TConfig['response'] extends StatusResponseSchema
83
+ ? T extends keyof TConfig['response']
84
+ ? InferZodType<TConfig['response'][T]>
85
+ : any
86
+ : TConfig['response'] extends ResponseSchema
87
+ ? InferZodType<TConfig['response']>
88
+ : any;
90
89
  redirect: (location: string, status?: number) => Response;
91
90
  clearRedirect: () => void;
92
91
  [key: string]: any;
93
92
  };
94
93
 
94
+ // Internal cookie storage interface
95
+ interface InternalCookie {
96
+ name: string;
97
+ value: string;
98
+ domain?: string;
99
+ path?: string;
100
+ expires?: Date;
101
+ maxAge?: number;
102
+ secure?: boolean;
103
+ httpOnly?: boolean;
104
+ sameSite?: 'Strict' | 'Lax' | 'None';
105
+ }
106
+
95
107
  // Handler function type with proper response typing
96
108
  type Handler<TConfig extends RouteConfig = {}, EC = {}> = (ctx: Context<TConfig> & EC) => Promise<InferResponseType<TConfig['response']> | any> | InferResponseType<TConfig['response']> | any;
97
109
 
@@ -346,10 +358,10 @@ export default class BXO {
346
358
 
347
359
  // Check for double wildcard (**) in the route
348
360
  const hasDoubleWildcard = routeSegments.some(segment => segment === '**');
349
-
361
+
350
362
  // Handle double wildcard at the end (catch-all with slashes)
351
363
  const hasDoubleWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '**';
352
-
364
+
353
365
  // Handle single wildcard at the end (catch-all)
354
366
  const hasWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '*';
355
367
 
@@ -392,31 +404,31 @@ export default class BXO {
392
404
  if (routeSegment === '**') {
393
405
  // Find the next non-wildcard segment to match against
394
406
  let nextNonWildcardIndex = i + 1;
395
- while (nextNonWildcardIndex < routeSegments.length &&
396
- (routeSegments[nextNonWildcardIndex] === '*' || routeSegments[nextNonWildcardIndex] === '**')) {
407
+ while (nextNonWildcardIndex < routeSegments.length &&
408
+ (routeSegments[nextNonWildcardIndex] === '*' || routeSegments[nextNonWildcardIndex] === '**')) {
397
409
  nextNonWildcardIndex++;
398
410
  }
399
-
411
+
400
412
  if (nextNonWildcardIndex >= routeSegments.length) {
401
413
  // Double wildcard is at the end or followed by other wildcards
402
414
  const remainingPath = pathSegments.slice(i).join('/');
403
415
  params['**'] = remainingPath;
404
416
  break;
405
417
  }
406
-
418
+
407
419
  // Find the next matching segment in the path
408
420
  const nextRouteSegment = routeSegments[nextNonWildcardIndex];
409
421
  if (!nextRouteSegment) {
410
422
  isMatch = false;
411
423
  break;
412
424
  }
413
-
425
+
414
426
  let foundMatch = false;
415
427
  let matchedPath = '';
416
-
428
+
417
429
  for (let j = i; j < pathSegments.length; j++) {
418
430
  const currentPathSegment = pathSegments[j];
419
-
431
+
420
432
  // Check if this path segment matches the next route segment
421
433
  if (nextRouteSegment.startsWith(':')) {
422
434
  // Param segment - always matches
@@ -441,12 +453,12 @@ export default class BXO {
441
453
  break;
442
454
  }
443
455
  }
444
-
456
+
445
457
  if (!foundMatch) {
446
458
  isMatch = false;
447
459
  break;
448
460
  }
449
-
461
+
450
462
  continue;
451
463
  }
452
464
 
@@ -488,10 +500,10 @@ export default class BXO {
488
500
 
489
501
  // Check for double wildcard (**) in the route
490
502
  const hasDoubleWildcard = routeSegments.some(segment => segment === '**');
491
-
503
+
492
504
  // Handle double wildcard at the end (catch-all with slashes)
493
505
  const hasDoubleWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '**';
494
-
506
+
495
507
  // Handle single wildcard at the end (catch-all)
496
508
  const hasWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '*';
497
509
 
@@ -534,31 +546,31 @@ export default class BXO {
534
546
  if (routeSegment === '**') {
535
547
  // Find the next non-wildcard segment to match against
536
548
  let nextNonWildcardIndex = i + 1;
537
- while (nextNonWildcardIndex < routeSegments.length &&
538
- (routeSegments[nextNonWildcardIndex] === '*' || routeSegments[nextNonWildcardIndex] === '**')) {
549
+ while (nextNonWildcardIndex < routeSegments.length &&
550
+ (routeSegments[nextNonWildcardIndex] === '*' || routeSegments[nextNonWildcardIndex] === '**')) {
539
551
  nextNonWildcardIndex++;
540
552
  }
541
-
553
+
542
554
  if (nextNonWildcardIndex >= routeSegments.length) {
543
555
  // Double wildcard is at the end or followed by other wildcards
544
556
  const remainingPath = pathSegments.slice(i).join('/');
545
557
  params['**'] = remainingPath;
546
558
  break;
547
559
  }
548
-
560
+
549
561
  // Find the next matching segment in the path
550
562
  const nextRouteSegment = routeSegments[nextNonWildcardIndex];
551
563
  if (!nextRouteSegment) {
552
564
  isMatch = false;
553
565
  break;
554
566
  }
555
-
567
+
556
568
  let foundMatch = false;
557
569
  let matchedPath = '';
558
-
570
+
559
571
  for (let j = i; j < pathSegments.length; j++) {
560
572
  const currentPathSegment = pathSegments[j];
561
-
573
+
562
574
  // Check if this path segment matches the next route segment
563
575
  if (nextRouteSegment.startsWith(':')) {
564
576
  // Param segment - always matches
@@ -583,12 +595,12 @@ export default class BXO {
583
595
  break;
584
596
  }
585
597
  }
586
-
598
+
587
599
  if (!foundMatch) {
588
600
  isMatch = false;
589
601
  break;
590
602
  }
591
-
603
+
592
604
  continue;
593
605
  }
594
606
 
@@ -638,9 +650,9 @@ export default class BXO {
638
650
  // Parse cookies from Cookie header
639
651
  private parseCookies(cookieHeader: string | null): Record<string, string> {
640
652
  const cookies: Record<string, string> = {};
641
-
653
+
642
654
  if (!cookieHeader) return cookies;
643
-
655
+
644
656
  const cookiePairs = cookieHeader.split(';');
645
657
  for (const pair of cookiePairs) {
646
658
  const [name, value] = pair.trim().split('=');
@@ -648,7 +660,7 @@ export default class BXO {
648
660
  cookies[decodeURIComponent(name)] = decodeURIComponent(value);
649
661
  }
650
662
  }
651
-
663
+
652
664
  return cookies;
653
665
  }
654
666
 
@@ -661,19 +673,19 @@ export default class BXO {
661
673
  // Validate response against response config (supports both simple and status-based schemas)
662
674
  private validateResponse(responseConfig: ResponseConfig | undefined, data: any, status: number = 200): any {
663
675
  if (!responseConfig || !this.enableValidation) return data;
664
-
676
+
665
677
  // If it's a simple schema (not status-based)
666
678
  if ('parse' in responseConfig && typeof responseConfig.parse === 'function') {
667
679
  return responseConfig.parse(data);
668
680
  }
669
-
681
+
670
682
  // If it's a status-based schema
671
683
  if (typeof responseConfig === 'object' && !('parse' in responseConfig)) {
672
684
  const statusSchema = responseConfig[status];
673
685
  if (statusSchema) {
674
686
  return statusSchema.parse(data);
675
687
  }
676
-
688
+
677
689
  // If no specific status schema found, try to find a fallback
678
690
  // Common fallback statuses: 200, 201, 400, 500
679
691
  const fallbackStatuses = [200, 201, 400, 500];
@@ -682,11 +694,11 @@ export default class BXO {
682
694
  return responseConfig[fallbackStatus]?.parse(data);
683
695
  }
684
696
  }
685
-
697
+
686
698
  // If no schema found for the status, return data as-is
687
699
  return data;
688
700
  }
689
-
701
+
690
702
  return data;
691
703
  }
692
704
 
@@ -732,7 +744,17 @@ export default class BXO {
732
744
  cookies: {},
733
745
  path: pathname,
734
746
  request,
735
- set: {},
747
+ set: {
748
+ status: 200,
749
+ headers: {},
750
+ cookies: (name: string, options?: CookieOptions) => {
751
+ // This is a placeholder for setting cookies.
752
+ // In a real Bun.serve context, you'd use Bun.serve's cookie handling.
753
+ // For now, we'll just log it or throw an error if not Bun.serve.
754
+ console.warn(`Setting cookie '${name}' via ctx.set.cookies is not directly supported by Bun.serve. Use Bun.serve's cookie handling.`);
755
+ },
756
+ redirect: undefined
757
+ },
736
758
  status: ((code: number, data?: any) => {
737
759
  ctx.set.status = code;
738
760
  return data;
@@ -758,7 +780,7 @@ export default class BXO {
758
780
  }
759
781
  }
760
782
  if (typeof ctx.set.status === 'number' && ctx.set.status >= 300 && ctx.set.status < 400) {
761
- delete ctx.set.status;
783
+ ctx.set.status = 200;
762
784
  }
763
785
  }) as any
764
786
  };
@@ -837,6 +859,9 @@ export default class BXO {
837
859
  }
838
860
  }
839
861
 
862
+ // Create internal cookie storage
863
+ const internalCookies: InternalCookie[] = [];
864
+
840
865
  // Create context with validation
841
866
  let ctx: Context;
842
867
  try {
@@ -846,7 +871,7 @@ export default class BXO {
846
871
  const validatedBody = this.enableValidation && route.config?.body ? this.validateData(route.config.body, body) : body;
847
872
  const validatedHeaders = this.enableValidation && route.config?.headers ? this.validateData(route.config.headers, headers) : headers;
848
873
  const validatedCookies = this.enableValidation && route.config?.cookies ? this.validateData(route.config.cookies, cookies) : cookies;
849
-
874
+
850
875
  ctx = {
851
876
  params: validatedParams,
852
877
  query: validatedQuery,
@@ -855,44 +880,66 @@ export default class BXO {
855
880
  cookies: validatedCookies,
856
881
  path: pathname,
857
882
  request,
858
- set: {},
883
+ set: {
884
+ status: 200,
885
+ headers: {},
886
+ cookies: (name: string, options: CookieOptions) => {
887
+ internalCookies.push({
888
+ name,
889
+ value: options.value,
890
+ domain: options.domain,
891
+ path: options.path,
892
+ expires: options.expires,
893
+ maxAge: options.maxAge,
894
+ secure: options.secure,
895
+ httpOnly: options.httpOnly,
896
+ sameSite: options.sameSite
897
+ });
898
+ }
899
+ },
859
900
  status: ((code: number, data?: any) => {
860
901
  ctx.set.status = code;
861
902
  return data;
862
903
  }) as any,
863
- redirect: ((location: string, status: number = 302) => {
864
- // Record redirect intent only; avoid mutating generic status/headers so it can be canceled later
865
- ctx.set.redirect = { location, status };
866
-
867
- // Prepare headers for immediate Response return without persisting to ctx.set.headers
868
- const responseHeaders: Record<string, string> = {
869
- Location: location,
870
- ...(ctx.set.headers || {})
871
- };
872
-
873
- // Handle cookies if any are set on context
874
- if (ctx.set.cookies && ctx.set.cookies.length > 0) {
875
- const cookieHeaders = ctx.set.cookies.map(cookie => {
876
- let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
877
- if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
878
- if (cookie.path) cookieString += `; Path=${cookie.path}`;
879
- if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
880
- if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
881
- if (cookie.secure) cookieString += `; Secure`;
882
- if (cookie.httpOnly) cookieString += `; HttpOnly`;
883
- if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
884
- return cookieString;
885
- });
886
- cookieHeaders.forEach((cookieHeader, index) => {
887
- responseHeaders[index === 0 ? 'Set-Cookie' : `Set-Cookie-${index}`] = cookieHeader;
888
- });
889
- }
890
-
891
- return new Response(null, {
892
- status,
893
- headers: responseHeaders
894
- });
895
- }) as any,
904
+ redirect: ((location: string, status: number = 302) => {
905
+ // Record redirect intent only; avoid mutating generic status/headers so it can be canceled later
906
+ ctx.set.redirect = { location, status };
907
+
908
+ // Prepare headers for immediate Response return without persisting to ctx.set.headers
909
+ const responseHeaders = new Headers();
910
+ responseHeaders.set('Location', location);
911
+
912
+ // Add any additional headers from ctx.set.headers
913
+ if (ctx.set.headers) {
914
+ Object.entries(ctx.set.headers).forEach(([key, value]) => {
915
+ responseHeaders.set(key, value);
916
+ });
917
+ }
918
+
919
+ // Handle cookies if any are set on context
920
+ if (internalCookies.length > 0) {
921
+ const cookieHeaders = internalCookies.map(cookie => {
922
+ let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
923
+ if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
924
+ if (cookie.path) cookieString += `; Path=${cookie.path}`;
925
+ if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
926
+ if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
927
+ if (cookie.secure) cookieString += `; Secure`;
928
+ if (cookie.httpOnly) cookieString += `; HttpOnly`;
929
+ if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
930
+ return cookieString;
931
+ });
932
+ // Set multiple Set-Cookie headers properly
933
+ cookieHeaders.forEach(cookieHeader => {
934
+ responseHeaders.append('Set-Cookie', cookieHeader);
935
+ });
936
+ }
937
+
938
+ return new Response(null, {
939
+ status,
940
+ headers: responseHeaders
941
+ });
942
+ }) as any,
896
943
  clearRedirect: (() => {
897
944
  // Clear explicit redirect intent
898
945
  delete ctx.set.redirect;
@@ -906,13 +953,13 @@ export default class BXO {
906
953
  }
907
954
  // Reset status if it is a redirect
908
955
  if (typeof ctx.set.status === 'number' && ctx.set.status >= 300 && ctx.set.status < 400) {
909
- delete ctx.set.status;
956
+ ctx.set.status = 200;
910
957
  }
911
958
  }) as any
912
959
  };
913
960
  } catch (validationError) {
914
961
  // Validation failed - return error response
915
-
962
+
916
963
  // Extract detailed validation errors from Zod
917
964
  let validationDetails = undefined;
918
965
  if (validationError instanceof Error) {
@@ -922,13 +969,13 @@ export default class BXO {
922
969
  validationDetails = validationError.issues;
923
970
  }
924
971
  }
925
-
972
+
926
973
  // Create a clean error message
927
- const errorMessage = validationDetails && validationDetails.length > 0
974
+ const errorMessage = validationDetails && validationDetails.length > 0
928
975
  ? `Validation failed for ${validationDetails.length} field(s)`
929
976
  : 'Validation failed';
930
-
931
- return new Response(JSON.stringify({
977
+
978
+ return new Response(JSON.stringify({
932
979
  error: errorMessage,
933
980
  details: validationDetails
934
981
  }), {
@@ -981,7 +1028,7 @@ export default class BXO {
981
1028
 
982
1029
  // If the handler did not return a response, but a redirect was configured via ctx.set,
983
1030
  // automatically create a redirect Response so users can call ctx.redirect(...) or set headers without returning.
984
- const hasImplicitRedirectIntent = !!ctx.set.redirect
1031
+ const hasImplicitRedirectIntent = !!ctx.set.redirect
985
1032
  || (typeof ctx.set.status === 'number' && ctx.set.status >= 300 && ctx.set.status < 400);
986
1033
  if ((response === undefined || response === null) && hasImplicitRedirectIntent) {
987
1034
  const locationFromHeaders = ctx.set.headers && Object.entries(ctx.set.headers).find(([k]) => k.toLowerCase() === 'location')?.[1];
@@ -996,8 +1043,8 @@ export default class BXO {
996
1043
  const status = ctx.set.redirect?.status ?? ctx.set.status ?? 302;
997
1044
 
998
1045
  // Handle cookies if any are set
999
- if (ctx.set.cookies && ctx.set.cookies.length > 0) {
1000
- const cookieHeaders = ctx.set.cookies.map(cookie => {
1046
+ if (internalCookies.length > 0) {
1047
+ const cookieHeaders = internalCookies.map(cookie => {
1001
1048
  let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
1002
1049
  if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
1003
1050
  if (cookie.path) cookieString += `; Path=${cookie.path}`;
@@ -1008,9 +1055,20 @@ export default class BXO {
1008
1055
  if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
1009
1056
  return cookieString;
1010
1057
  });
1011
- cookieHeaders.forEach((cookieHeader, index) => {
1012
- responseHeaders[index === 0 ? 'Set-Cookie' : `Set-Cookie-${index}`] = cookieHeader;
1058
+ // Convert responseHeaders to Headers object for proper multiple Set-Cookie handling
1059
+ const headers = new Headers();
1060
+ Object.entries(responseHeaders).forEach(([key, value]) => {
1061
+ headers.set(key, value);
1062
+ });
1063
+ cookieHeaders.forEach(cookieHeader => {
1064
+ headers.append('Set-Cookie', cookieHeader);
1065
+ });
1066
+ // Convert back to plain object for Response constructor
1067
+ const finalHeaders: Record<string, string> = {};
1068
+ headers.forEach((value, key) => {
1069
+ finalHeaders[key] = value;
1013
1070
  });
1071
+ responseHeaders = finalHeaders;
1014
1072
  }
1015
1073
 
1016
1074
  return new Response(null, {
@@ -1027,7 +1085,7 @@ export default class BXO {
1027
1085
  response = this.validateResponse(route.config.response, response, status);
1028
1086
  } catch (validationError) {
1029
1087
  // Response validation failed
1030
-
1088
+
1031
1089
  // Extract detailed validation errors from Zod
1032
1090
  let validationDetails = undefined;
1033
1091
  if (validationError instanceof Error) {
@@ -1037,13 +1095,13 @@ export default class BXO {
1037
1095
  validationDetails = validationError.issues;
1038
1096
  }
1039
1097
  }
1040
-
1098
+
1041
1099
  // Create a clean error message
1042
- const errorMessage = validationDetails && validationDetails.length > 0
1100
+ const errorMessage = validationDetails && validationDetails.length > 0
1043
1101
  ? `Response validation failed for ${validationDetails.length} field(s)`
1044
1102
  : 'Response validation failed';
1045
-
1046
- return new Response(JSON.stringify({
1103
+
1104
+ return new Response(JSON.stringify({
1047
1105
  error: errorMessage,
1048
1106
  details: validationDetails
1049
1107
  }), {
@@ -1055,6 +1113,43 @@ export default class BXO {
1055
1113
 
1056
1114
  // Convert response to Response object
1057
1115
  if (response instanceof Response) {
1116
+ // If there are headers set via ctx.set.headers, merge them with the Response headers
1117
+ if (ctx.set.headers && Object.keys(ctx.set.headers).length > 0) {
1118
+ const newHeaders = new Headers(response.headers);
1119
+
1120
+ // Add headers from ctx.set.headers
1121
+ Object.entries(ctx.set.headers).forEach(([key, value]) => {
1122
+ newHeaders.set(key, value);
1123
+ });
1124
+
1125
+ // Handle cookies if any are set
1126
+ if (internalCookies.length > 0) {
1127
+ const cookieHeaders = internalCookies.map(cookie => {
1128
+ let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
1129
+ if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
1130
+ if (cookie.path) cookieString += `; Path=${cookie.path}`;
1131
+ if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
1132
+ if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
1133
+ if (cookie.secure) cookieString += `; Secure`;
1134
+ if (cookie.httpOnly) cookieString += `; HttpOnly`;
1135
+ if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
1136
+ return cookieString;
1137
+ });
1138
+
1139
+ // Add Set-Cookie headers
1140
+ cookieHeaders.forEach(cookieHeader => {
1141
+ newHeaders.append('Set-Cookie', cookieHeader);
1142
+ });
1143
+ }
1144
+
1145
+ // Create new Response with merged headers
1146
+ return new Response(response.body, {
1147
+ status: ctx.set.status || response.status,
1148
+ statusText: response.statusText,
1149
+ headers: newHeaders
1150
+ });
1151
+ }
1152
+
1058
1153
  return response;
1059
1154
  }
1060
1155
 
@@ -1089,12 +1184,13 @@ export default class BXO {
1089
1184
 
1090
1185
  // Prepare headers with cookies
1091
1186
  let responseHeaders = ctx.set.headers ? { ...ctx.set.headers } : {};
1092
-
1187
+
1093
1188
  // Handle cookies if any are set
1094
- if (ctx.set.cookies && ctx.set.cookies.length > 0) {
1095
- const cookieHeaders = ctx.set.cookies.map(cookie => {
1189
+ console.log('Checking cookies:', internalCookies.length, internalCookies);
1190
+ if (internalCookies.length > 0) {
1191
+ const cookieHeaders = internalCookies.map(cookie => {
1096
1192
  let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
1097
-
1193
+
1098
1194
  if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
1099
1195
  if (cookie.path) cookieString += `; Path=${cookie.path}`;
1100
1196
  if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
@@ -1102,16 +1198,36 @@ export default class BXO {
1102
1198
  if (cookie.secure) cookieString += `; Secure`;
1103
1199
  if (cookie.httpOnly) cookieString += `; HttpOnly`;
1104
1200
  if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
1105
-
1201
+
1106
1202
  return cookieString;
1107
1203
  });
1108
-
1204
+
1109
1205
  // Add Set-Cookie headers
1110
- cookieHeaders.forEach((cookieHeader, index) => {
1111
- responseHeaders[index === 0 ? 'Set-Cookie' : `Set-Cookie-${index}`] = cookieHeader;
1206
+ // Use Headers object directly for Response constructor to handle multiple Set-Cookie headers properly
1207
+ const headers = new Headers();
1208
+ Object.entries(responseHeaders).forEach(([key, value]) => {
1209
+ headers.set(key, value);
1210
+ });
1211
+ cookieHeaders.forEach(cookieHeader => {
1212
+ headers.append('Set-Cookie', cookieHeader);
1213
+ });
1214
+
1215
+ const responseInit: ResponseInit = {
1216
+ status: ctx.set.status || 200,
1217
+ headers: headers
1218
+ };
1219
+
1220
+ if (typeof response === 'string') {
1221
+ return new Response(response, responseInit);
1222
+ }
1223
+
1224
+ return new Response(JSON.stringify(response), {
1225
+ status: responseInit.status,
1226
+ headers: headers
1112
1227
  });
1113
1228
  }
1114
1229
 
1230
+ // If no cookies, use the original responseHeaders
1115
1231
  const responseInit: ResponseInit = {
1116
1232
  status: ctx.set.status || 200,
1117
1233
  headers: responseHeaders
@@ -1262,7 +1378,7 @@ export default class BXO {
1262
1378
  } catch (stopError) {
1263
1379
  console.error('❌ Error calling server.stop():', stopError);
1264
1380
  }
1265
-
1381
+
1266
1382
  // Clear the server reference
1267
1383
  this.server = undefined;
1268
1384
  }
@@ -1430,15 +1546,13 @@ const redirect = (location: string, status: number = 302) => {
1430
1546
  export { z, error, file, redirect };
1431
1547
 
1432
1548
  // Export types for external use
1433
- export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute, Cookie, BXOOptions, Plugin };
1549
+ export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute, CookieOptions, BXOOptions, Plugin };
1434
1550
 
1435
- // Helper function to create a cookie
1436
- export const createCookie = (
1437
- name: string,
1551
+ // Helper function to create cookie options
1552
+ export const createCookieOptions = (
1438
1553
  value: string,
1439
- options: Omit<Cookie, 'name' | 'value'> = {}
1440
- ): Cookie => ({
1441
- name,
1554
+ options: Omit<CookieOptions, 'value'> = {}
1555
+ ): CookieOptions => ({
1442
1556
  value,
1443
1557
  ...options
1444
1558
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bxo",
3
3
  "module": "index.ts",
4
- "version": "0.0.5-dev.47",
4
+ "version": "0.0.5-dev.49",
5
5
  "description": "A simple and lightweight web framework for Bun",
6
6
  "type": "module",
7
7
  "exports": {
package/plugins/cors.ts CHANGED
@@ -85,29 +85,23 @@ export function cors(options: CORSOptions = {}): Plugin {
85
85
  });
86
86
  }
87
87
  },
88
- onResponse: async (ctx, response) => {
88
+ onResponse: async (ctx) => {
89
89
  // Handle CORS headers for actual requests
90
90
  const requestOrigin = getRequestOrigin(ctx.request);
91
91
  const allowedOrigin = validateOrigin(requestOrigin, origin);
92
92
 
93
- // Clone the response to modify headers
94
- const newResponse = new Response(response.body, {
95
- status: response.status,
96
- statusText: response.statusText,
97
- headers: new Headers(response.headers)
98
- });
99
-
100
- // Set CORS headers
101
- newResponse.headers.set('Access-Control-Allow-Origin', allowedOrigin || '*');
102
- newResponse.headers.set('Access-Control-Allow-Methods', methods.join(', '));
103
- newResponse.headers.set('Access-Control-Allow-Headers', allowedHeaders.join(', '));
104
- newResponse.headers.set('Access-Control-Max-Age', maxAge.toString());
93
+ // Set CORS headers for all responses
94
+ ctx.set.headers = {
95
+ ...ctx.set.headers,
96
+ 'Access-Control-Allow-Origin': allowedOrigin || '*',
97
+ 'Access-Control-Allow-Methods': methods.join(', '),
98
+ 'Access-Control-Allow-Headers': allowedHeaders.join(', '),
99
+ 'Access-Control-Max-Age': maxAge.toString()
100
+ };
105
101
 
106
102
  if (credentials) {
107
- newResponse.headers.set('Access-Control-Allow-Credentials', 'true');
103
+ ctx.set.headers['Access-Control-Allow-Credentials'] = 'true';
108
104
  }
109
-
110
- return newResponse;
111
105
  }
112
106
  };
113
107
  }
package/example.ts DELETED
@@ -1,20 +0,0 @@
1
- import BXO from ".";
2
- import { cors } from "./plugins";
3
-
4
- const app = new BXO();
5
-
6
- app.use(cors());
7
-
8
- app.get('/', (ctx) => {
9
- return { message: 'Hello, world!' };
10
- });
11
-
12
- app.get("/api/actions/nodula.auth.login", (ctx) => {
13
- return { message: 'Hello, world!' };
14
- });
15
-
16
- app.post("/api/actions/nodula.auth.login", (ctx) => {
17
- return { message: 'Hello, world!' };
18
- });
19
-
20
- app.listen(3000);