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 +235 -121
- package/package.json +1 -1
- package/plugins/cors.ts +10 -16
- package/example.ts +0 -20
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
|
-
|
|
16
|
-
|
|
14
|
+
: T extends StatusResponseSchema
|
|
15
|
+
? { [K in keyof T]: InferZodType<T[K]> }[keyof T]
|
|
16
|
+
: never;
|
|
17
17
|
|
|
18
|
-
// Cookie interface
|
|
19
|
-
interface
|
|
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
|
|
68
|
-
headers
|
|
69
|
-
cookies
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
-
|
|
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 (
|
|
1000
|
-
const cookieHeaders =
|
|
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
|
-
|
|
1012
|
-
|
|
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
|
-
|
|
1095
|
-
|
|
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
|
-
|
|
1111
|
-
|
|
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,
|
|
1549
|
+
export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute, CookieOptions, BXOOptions, Plugin };
|
|
1434
1550
|
|
|
1435
|
-
// Helper function to create
|
|
1436
|
-
export const
|
|
1437
|
-
name: string,
|
|
1551
|
+
// Helper function to create cookie options
|
|
1552
|
+
export const createCookieOptions = (
|
|
1438
1553
|
value: string,
|
|
1439
|
-
options: Omit<
|
|
1440
|
-
):
|
|
1441
|
-
name,
|
|
1554
|
+
options: Omit<CookieOptions, 'value'> = {}
|
|
1555
|
+
): CookieOptions => ({
|
|
1442
1556
|
value,
|
|
1443
1557
|
...options
|
|
1444
1558
|
});
|
package/package.json
CHANGED
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
|
|
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
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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);
|