bxo 0.0.5-dev.48 ā 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 +207 -134
- package/package.json +1 -1
- package/example.ts +0 -58
- package/test-cors.ts +0 -0
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);
|
|
1013
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;
|
|
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
|
}), {
|
|
@@ -1058,15 +1116,15 @@ export default class BXO {
|
|
|
1058
1116
|
// If there are headers set via ctx.set.headers, merge them with the Response headers
|
|
1059
1117
|
if (ctx.set.headers && Object.keys(ctx.set.headers).length > 0) {
|
|
1060
1118
|
const newHeaders = new Headers(response.headers);
|
|
1061
|
-
|
|
1119
|
+
|
|
1062
1120
|
// Add headers from ctx.set.headers
|
|
1063
1121
|
Object.entries(ctx.set.headers).forEach(([key, value]) => {
|
|
1064
1122
|
newHeaders.set(key, value);
|
|
1065
1123
|
});
|
|
1066
|
-
|
|
1124
|
+
|
|
1067
1125
|
// Handle cookies if any are set
|
|
1068
|
-
if (
|
|
1069
|
-
const cookieHeaders =
|
|
1126
|
+
if (internalCookies.length > 0) {
|
|
1127
|
+
const cookieHeaders = internalCookies.map(cookie => {
|
|
1070
1128
|
let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
|
|
1071
1129
|
if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
|
|
1072
1130
|
if (cookie.path) cookieString += `; Path=${cookie.path}`;
|
|
@@ -1077,17 +1135,13 @@ export default class BXO {
|
|
|
1077
1135
|
if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
|
|
1078
1136
|
return cookieString;
|
|
1079
1137
|
});
|
|
1080
|
-
|
|
1138
|
+
|
|
1081
1139
|
// Add Set-Cookie headers
|
|
1082
|
-
cookieHeaders.forEach(
|
|
1083
|
-
|
|
1084
|
-
newHeaders.set('Set-Cookie', cookieHeader);
|
|
1085
|
-
} else {
|
|
1086
|
-
newHeaders.set(`Set-Cookie-${index}`, cookieHeader);
|
|
1087
|
-
}
|
|
1140
|
+
cookieHeaders.forEach(cookieHeader => {
|
|
1141
|
+
newHeaders.append('Set-Cookie', cookieHeader);
|
|
1088
1142
|
});
|
|
1089
1143
|
}
|
|
1090
|
-
|
|
1144
|
+
|
|
1091
1145
|
// Create new Response with merged headers
|
|
1092
1146
|
return new Response(response.body, {
|
|
1093
1147
|
status: ctx.set.status || response.status,
|
|
@@ -1095,7 +1149,7 @@ export default class BXO {
|
|
|
1095
1149
|
headers: newHeaders
|
|
1096
1150
|
});
|
|
1097
1151
|
}
|
|
1098
|
-
|
|
1152
|
+
|
|
1099
1153
|
return response;
|
|
1100
1154
|
}
|
|
1101
1155
|
|
|
@@ -1130,12 +1184,13 @@ export default class BXO {
|
|
|
1130
1184
|
|
|
1131
1185
|
// Prepare headers with cookies
|
|
1132
1186
|
let responseHeaders = ctx.set.headers ? { ...ctx.set.headers } : {};
|
|
1133
|
-
|
|
1187
|
+
|
|
1134
1188
|
// Handle cookies if any are set
|
|
1135
|
-
|
|
1136
|
-
|
|
1189
|
+
console.log('Checking cookies:', internalCookies.length, internalCookies);
|
|
1190
|
+
if (internalCookies.length > 0) {
|
|
1191
|
+
const cookieHeaders = internalCookies.map(cookie => {
|
|
1137
1192
|
let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
|
|
1138
|
-
|
|
1193
|
+
|
|
1139
1194
|
if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
|
|
1140
1195
|
if (cookie.path) cookieString += `; Path=${cookie.path}`;
|
|
1141
1196
|
if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
|
|
@@ -1143,16 +1198,36 @@ export default class BXO {
|
|
|
1143
1198
|
if (cookie.secure) cookieString += `; Secure`;
|
|
1144
1199
|
if (cookie.httpOnly) cookieString += `; HttpOnly`;
|
|
1145
1200
|
if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
|
|
1146
|
-
|
|
1201
|
+
|
|
1147
1202
|
return cookieString;
|
|
1148
1203
|
});
|
|
1149
|
-
|
|
1204
|
+
|
|
1150
1205
|
// Add Set-Cookie headers
|
|
1151
|
-
|
|
1152
|
-
|
|
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
|
|
1153
1227
|
});
|
|
1154
1228
|
}
|
|
1155
1229
|
|
|
1230
|
+
// If no cookies, use the original responseHeaders
|
|
1156
1231
|
const responseInit: ResponseInit = {
|
|
1157
1232
|
status: ctx.set.status || 200,
|
|
1158
1233
|
headers: responseHeaders
|
|
@@ -1303,7 +1378,7 @@ export default class BXO {
|
|
|
1303
1378
|
} catch (stopError) {
|
|
1304
1379
|
console.error('ā Error calling server.stop():', stopError);
|
|
1305
1380
|
}
|
|
1306
|
-
|
|
1381
|
+
|
|
1307
1382
|
// Clear the server reference
|
|
1308
1383
|
this.server = undefined;
|
|
1309
1384
|
}
|
|
@@ -1471,15 +1546,13 @@ const redirect = (location: string, status: number = 302) => {
|
|
|
1471
1546
|
export { z, error, file, redirect };
|
|
1472
1547
|
|
|
1473
1548
|
// Export types for external use
|
|
1474
|
-
export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute,
|
|
1549
|
+
export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute, CookieOptions, BXOOptions, Plugin };
|
|
1475
1550
|
|
|
1476
|
-
// Helper function to create
|
|
1477
|
-
export const
|
|
1478
|
-
name: string,
|
|
1551
|
+
// Helper function to create cookie options
|
|
1552
|
+
export const createCookieOptions = (
|
|
1479
1553
|
value: string,
|
|
1480
|
-
options: Omit<
|
|
1481
|
-
):
|
|
1482
|
-
name,
|
|
1554
|
+
options: Omit<CookieOptions, 'value'> = {}
|
|
1555
|
+
): CookieOptions => ({
|
|
1483
1556
|
value,
|
|
1484
1557
|
...options
|
|
1485
1558
|
});
|
package/package.json
CHANGED
package/example.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import BXO from './index';
|
|
2
|
-
import { cors } from './plugins/cors';
|
|
3
|
-
|
|
4
|
-
const app = new BXO();
|
|
5
|
-
|
|
6
|
-
// Use CORS plugin
|
|
7
|
-
app.use(cors({
|
|
8
|
-
origin: ['http://localhost:3000', 'http://localhost:3001'],
|
|
9
|
-
credentials: true,
|
|
10
|
-
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
11
|
-
allowedHeaders: ['Content-Type', 'Authorization', 'X-Custom-Header']
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
// Test route that returns a Response object
|
|
15
|
-
app.get('/test-response', () => {
|
|
16
|
-
return new Response(JSON.stringify({ message: 'Hello from Response object' }), {
|
|
17
|
-
status: 200,
|
|
18
|
-
headers: {
|
|
19
|
-
'Content-Type': 'application/json',
|
|
20
|
-
'X-Custom-Response-Header': 'test-value'
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
// Test route that returns plain data
|
|
26
|
-
app.get('/test-data', () => {
|
|
27
|
-
return { message: 'Hello from plain data' };
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
// Test route that sets custom headers via ctx.set
|
|
31
|
-
app.get('/test-headers', (ctx) => {
|
|
32
|
-
ctx.set.headers = {
|
|
33
|
-
'X-Custom-Header': 'set-via-context',
|
|
34
|
-
'Cache-Control': 'no-cache'
|
|
35
|
-
};
|
|
36
|
-
return { message: 'Headers set via context' };
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
app.get("/api/actions/nodula.auth.login", (ctx) => {
|
|
40
|
-
return { message: 'Hello, world!' };
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
app.post("/api/actions/nodula.auth.login", (ctx) => {
|
|
44
|
-
return { message: 'Hello, world!' };
|
|
45
|
-
})
|
|
46
|
-
// Note: OPTIONS requests are handled automatically by the CORS plugin
|
|
47
|
-
|
|
48
|
-
// Start the server
|
|
49
|
-
app.start(3000, 'localhost').then(() => {
|
|
50
|
-
console.log('š Server started at http://localhost:3000');
|
|
51
|
-
console.log('š Test endpoints:');
|
|
52
|
-
console.log(' GET /test-response - Returns Response object');
|
|
53
|
-
console.log(' GET /test-data - Returns plain data');
|
|
54
|
-
console.log(' GET /test-headers - Sets headers via ctx.set');
|
|
55
|
-
console.log(' OPTIONS /* - Handled automatically by CORS plugin');
|
|
56
|
-
console.log('\nš Check CORS headers in browser dev tools or curl:');
|
|
57
|
-
console.log(' curl -H "Origin: http://localhost:3001" -H "Access-Control-Request-Method: GET" -X OPTIONS http://localhost:3000/test-response');
|
|
58
|
-
});
|
package/test-cors.ts
DELETED
|
File without changes
|