@syncular/server-hono 0.0.6-221 → 0.0.6-224
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/README.md +19 -4
- package/dist/create-server.d.ts.map +1 -1
- package/dist/create-server.js +3 -2
- package/dist/create-server.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/routes.d.ts +44 -18
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +121 -29
- package/dist/routes.js.map +1 -1
- package/dist/websocket-origin.d.ts +1 -0
- package/dist/websocket-origin.d.ts.map +1 -1
- package/dist/websocket-origin.js +20 -10
- package/dist/websocket-origin.js.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/create-server.test.ts +99 -16
- package/src/create-server.ts +5 -1
- package/src/index.ts +6 -0
- package/src/routes.ts +226 -56
- package/src/websocket-origin.ts +31 -9
package/src/routes.ts
CHANGED
|
@@ -63,7 +63,10 @@ import {
|
|
|
63
63
|
DEFAULT_SYNC_RATE_LIMITS,
|
|
64
64
|
type SyncRateLimitConfig,
|
|
65
65
|
} from './rate-limit';
|
|
66
|
-
import {
|
|
66
|
+
import {
|
|
67
|
+
isWebSocketOriginAllowed,
|
|
68
|
+
resolveAllowedOriginFromPatterns,
|
|
69
|
+
} from './websocket-origin';
|
|
67
70
|
import {
|
|
68
71
|
createWebSocketConnection,
|
|
69
72
|
createWebSocketConnectionOwnerKey,
|
|
@@ -126,33 +129,61 @@ export interface SyncWebSocketConfig {
|
|
|
126
129
|
allowedOrigins?: string[] | '*';
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
export type SyncCorsOriginResolver = (
|
|
133
|
+
origin: string | undefined,
|
|
134
|
+
context: Context
|
|
135
|
+
) =>
|
|
136
|
+
| boolean
|
|
137
|
+
| string
|
|
138
|
+
| null
|
|
139
|
+
| undefined
|
|
140
|
+
| Promise<boolean | string | null | undefined>;
|
|
141
|
+
|
|
142
|
+
export type SyncCorsOrigin = string | string[] | '*' | SyncCorsOriginResolver;
|
|
143
|
+
|
|
144
|
+
export interface SyncCorsOptions {
|
|
145
|
+
/**
|
|
146
|
+
* Hono-style origin config.
|
|
147
|
+
* - string / string[]: exact or wildcard origin patterns
|
|
148
|
+
* - '*': allow all origins
|
|
149
|
+
* - function: dynamic allow/deny decision
|
|
150
|
+
*/
|
|
151
|
+
origin?: SyncCorsOrigin;
|
|
152
|
+
/**
|
|
153
|
+
* Additional request headers to allow. These are appended to the built-in
|
|
154
|
+
* Syncular transport and tracing headers, not used as a replacement.
|
|
155
|
+
*/
|
|
156
|
+
allowHeaders?: string[];
|
|
157
|
+
/**
|
|
158
|
+
* Additional response headers exposed to the browser.
|
|
159
|
+
*/
|
|
160
|
+
exposeHeaders?: string[];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Legacy sync CORS config.
|
|
165
|
+
* @deprecated Prefer `cors: 'https://app.example.com'` or
|
|
166
|
+
* `cors: { origin: ['https://app.example.com'] }`.
|
|
167
|
+
*/
|
|
168
|
+
export interface LegacySyncCorsOptions {
|
|
169
|
+
allowedOrigins?: string[] | '*';
|
|
170
|
+
resolveOrigin?: (
|
|
171
|
+
origin: string | undefined,
|
|
172
|
+
context: Context
|
|
173
|
+
) => string | null | Promise<string | null>;
|
|
174
|
+
allowCredentials?: boolean;
|
|
175
|
+
allowHeaders?: string[];
|
|
176
|
+
allowMethods?: string[];
|
|
177
|
+
maxAgeSeconds?: number;
|
|
178
|
+
}
|
|
179
|
+
|
|
129
180
|
export interface SyncRoutesConfigWithRateLimit {
|
|
130
181
|
/**
|
|
131
182
|
* Optional browser CORS handling for sync routes.
|
|
132
183
|
* When configured, sync route responses and preflights include matching
|
|
133
184
|
* CORS headers directly from the generated sync app.
|
|
134
185
|
*/
|
|
135
|
-
cors?:
|
|
136
|
-
/**
|
|
137
|
-
* Simple exact-match origin allowlist.
|
|
138
|
-
* Use `'*'` to allow all origins.
|
|
139
|
-
* When omitted, provide `resolveOrigin` for custom logic.
|
|
140
|
-
*/
|
|
141
|
-
allowedOrigins?: string[] | '*';
|
|
142
|
-
/**
|
|
143
|
-
* Advanced origin resolver for dynamic CORS decisions.
|
|
144
|
-
* When both `allowedOrigins` and `resolveOrigin` are provided,
|
|
145
|
-
* `resolveOrigin` takes precedence.
|
|
146
|
-
*/
|
|
147
|
-
resolveOrigin?: (
|
|
148
|
-
origin: string | undefined,
|
|
149
|
-
context: Context
|
|
150
|
-
) => string | null | Promise<string | null>;
|
|
151
|
-
allowCredentials?: boolean;
|
|
152
|
-
allowHeaders?: string[];
|
|
153
|
-
allowMethods?: string[];
|
|
154
|
-
maxAgeSeconds?: number;
|
|
155
|
-
};
|
|
186
|
+
cors?: SyncCorsOrigin | SyncCorsOptions | LegacySyncCorsOptions;
|
|
156
187
|
/**
|
|
157
188
|
* Max commits per pull request.
|
|
158
189
|
* Default: 100
|
|
@@ -364,11 +395,27 @@ const DEFAULT_SYNC_CORS_ALLOW_METHODS = [
|
|
|
364
395
|
'OPTIONS',
|
|
365
396
|
];
|
|
366
397
|
|
|
398
|
+
const DEFAULT_SYNC_CORS_EXPOSE_HEADERS: string[] = [];
|
|
399
|
+
|
|
400
|
+
export type NormalizedSyncCorsConfig = {
|
|
401
|
+
resolveOrigin: (
|
|
402
|
+
origin: string | undefined,
|
|
403
|
+
context: Context
|
|
404
|
+
) => Promise<string | null>;
|
|
405
|
+
staticAllowedOrigins?: string[] | '*';
|
|
406
|
+
allowHeaders: string[];
|
|
407
|
+
exposeHeaders: string[];
|
|
408
|
+
allowMethods: string[];
|
|
409
|
+
allowCredentials: boolean;
|
|
410
|
+
maxAgeSeconds: number;
|
|
411
|
+
};
|
|
412
|
+
|
|
367
413
|
function applySyncCorsHeaders(args: {
|
|
368
414
|
headers: Headers;
|
|
369
415
|
allowedOrigin: string;
|
|
370
416
|
allowCredentials: boolean;
|
|
371
417
|
allowHeaders: string[];
|
|
418
|
+
exposeHeaders: string[];
|
|
372
419
|
allowMethods: string[];
|
|
373
420
|
maxAgeSeconds: number;
|
|
374
421
|
}): void {
|
|
@@ -382,6 +429,12 @@ function applySyncCorsHeaders(args: {
|
|
|
382
429
|
args.allowMethods.join(', ')
|
|
383
430
|
);
|
|
384
431
|
args.headers.set('Access-Control-Max-Age', String(args.maxAgeSeconds));
|
|
432
|
+
if (args.exposeHeaders.length > 0) {
|
|
433
|
+
args.headers.set(
|
|
434
|
+
'Access-Control-Expose-Headers',
|
|
435
|
+
args.exposeHeaders.join(', ')
|
|
436
|
+
);
|
|
437
|
+
}
|
|
385
438
|
if (args.allowedOrigin !== '*') {
|
|
386
439
|
args.headers.append('Vary', 'Origin');
|
|
387
440
|
if (args.allowCredentials) {
|
|
@@ -400,25 +453,149 @@ function createSyncCorsOriginDeniedResponse(origin: string): Response {
|
|
|
400
453
|
);
|
|
401
454
|
}
|
|
402
455
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
456
|
+
function mergeUniqueHeaders(...lists: Array<string[] | undefined>): string[] {
|
|
457
|
+
const seen = new Set<string>();
|
|
458
|
+
const merged: string[] = [];
|
|
459
|
+
for (const list of lists) {
|
|
460
|
+
for (const header of list ?? []) {
|
|
461
|
+
const trimmed = header.trim();
|
|
462
|
+
if (trimmed.length === 0) continue;
|
|
463
|
+
const key = trimmed.toLowerCase();
|
|
464
|
+
if (seen.has(key)) continue;
|
|
465
|
+
seen.add(key);
|
|
466
|
+
merged.push(trimmed);
|
|
467
|
+
}
|
|
414
468
|
}
|
|
415
|
-
|
|
416
|
-
|
|
469
|
+
return merged;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function normalizeOriginResolver(
|
|
473
|
+
resolver: SyncCorsOriginResolver
|
|
474
|
+
): NormalizedSyncCorsConfig['resolveOrigin'] {
|
|
475
|
+
return async (origin, context) => {
|
|
476
|
+
const resolved = await resolver(origin, context);
|
|
477
|
+
if (resolved === true) {
|
|
478
|
+
return origin ?? null;
|
|
479
|
+
}
|
|
480
|
+
if (resolved === false || resolved == null) {
|
|
417
481
|
return null;
|
|
418
482
|
}
|
|
419
|
-
return
|
|
483
|
+
return resolved;
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function createStaticOriginResolver(
|
|
488
|
+
allowedOrigins: string[] | '*'
|
|
489
|
+
): NormalizedSyncCorsConfig['resolveOrigin'] {
|
|
490
|
+
return async (origin) => {
|
|
491
|
+
if (allowedOrigins === '*') {
|
|
492
|
+
return '*';
|
|
493
|
+
}
|
|
494
|
+
return resolveAllowedOriginFromPatterns(origin, allowedOrigins);
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function toStaticAllowedOrigins(
|
|
499
|
+
origin: string | string[] | '*'
|
|
500
|
+
): string[] | '*' {
|
|
501
|
+
return origin === '*' ? '*' : typeof origin === 'string' ? [origin] : origin;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function isLegacySyncCorsOptions(
|
|
505
|
+
value: SyncRoutesConfigWithRateLimit['cors']
|
|
506
|
+
): value is LegacySyncCorsOptions {
|
|
507
|
+
return (
|
|
508
|
+
!!value &&
|
|
509
|
+
typeof value === 'object' &&
|
|
510
|
+
!Array.isArray(value) &&
|
|
511
|
+
('allowedOrigins' in value ||
|
|
512
|
+
'resolveOrigin' in value ||
|
|
513
|
+
'allowCredentials' in value ||
|
|
514
|
+
'allowMethods' in value ||
|
|
515
|
+
'maxAgeSeconds' in value)
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function normalizeSyncCorsConfig(
|
|
520
|
+
config: SyncRoutesConfigWithRateLimit['cors']
|
|
521
|
+
): NormalizedSyncCorsConfig | null {
|
|
522
|
+
if (!config) {
|
|
523
|
+
return null;
|
|
420
524
|
}
|
|
421
|
-
|
|
525
|
+
|
|
526
|
+
if (
|
|
527
|
+
typeof config === 'string' ||
|
|
528
|
+
Array.isArray(config) ||
|
|
529
|
+
typeof config === 'function'
|
|
530
|
+
) {
|
|
531
|
+
const originResolver =
|
|
532
|
+
typeof config === 'function'
|
|
533
|
+
? normalizeOriginResolver(config)
|
|
534
|
+
: createStaticOriginResolver(toStaticAllowedOrigins(config));
|
|
535
|
+
const staticAllowedOrigins =
|
|
536
|
+
typeof config === 'function' ? undefined : toStaticAllowedOrigins(config);
|
|
537
|
+
return {
|
|
538
|
+
resolveOrigin: originResolver,
|
|
539
|
+
staticAllowedOrigins,
|
|
540
|
+
allowHeaders: [...DEFAULT_SYNC_CORS_ALLOW_HEADERS],
|
|
541
|
+
exposeHeaders: [...DEFAULT_SYNC_CORS_EXPOSE_HEADERS],
|
|
542
|
+
allowMethods: [...DEFAULT_SYNC_CORS_ALLOW_METHODS],
|
|
543
|
+
allowCredentials: true,
|
|
544
|
+
maxAgeSeconds: 86_400,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (isLegacySyncCorsOptions(config)) {
|
|
549
|
+
const staticAllowedOrigins = config.allowedOrigins;
|
|
550
|
+
const resolveOrigin =
|
|
551
|
+
typeof config.resolveOrigin === 'function'
|
|
552
|
+
? async (origin: string | undefined, context: Context) =>
|
|
553
|
+
(await config.resolveOrigin?.(origin, context)) ?? null
|
|
554
|
+
: config.allowedOrigins
|
|
555
|
+
? createStaticOriginResolver(config.allowedOrigins)
|
|
556
|
+
: async () => null;
|
|
557
|
+
return {
|
|
558
|
+
resolveOrigin,
|
|
559
|
+
staticAllowedOrigins,
|
|
560
|
+
allowHeaders: mergeUniqueHeaders(
|
|
561
|
+
DEFAULT_SYNC_CORS_ALLOW_HEADERS,
|
|
562
|
+
config.allowHeaders
|
|
563
|
+
),
|
|
564
|
+
exposeHeaders: [...DEFAULT_SYNC_CORS_EXPOSE_HEADERS],
|
|
565
|
+
allowMethods: config.allowMethods ?? [...DEFAULT_SYNC_CORS_ALLOW_METHODS],
|
|
566
|
+
allowCredentials: config.allowCredentials ?? true,
|
|
567
|
+
maxAgeSeconds: config.maxAgeSeconds ?? 86_400,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const staticOrigin = config.origin;
|
|
572
|
+
const resolveOrigin =
|
|
573
|
+
typeof staticOrigin === 'function'
|
|
574
|
+
? normalizeOriginResolver(staticOrigin)
|
|
575
|
+
: staticOrigin
|
|
576
|
+
? createStaticOriginResolver(toStaticAllowedOrigins(staticOrigin))
|
|
577
|
+
: async () => null;
|
|
578
|
+
const staticAllowedOrigins =
|
|
579
|
+
typeof staticOrigin === 'function'
|
|
580
|
+
? undefined
|
|
581
|
+
: staticOrigin
|
|
582
|
+
? toStaticAllowedOrigins(staticOrigin)
|
|
583
|
+
: undefined;
|
|
584
|
+
return {
|
|
585
|
+
resolveOrigin,
|
|
586
|
+
staticAllowedOrigins,
|
|
587
|
+
allowHeaders: mergeUniqueHeaders(
|
|
588
|
+
DEFAULT_SYNC_CORS_ALLOW_HEADERS,
|
|
589
|
+
config.allowHeaders
|
|
590
|
+
),
|
|
591
|
+
exposeHeaders: mergeUniqueHeaders(
|
|
592
|
+
DEFAULT_SYNC_CORS_EXPOSE_HEADERS,
|
|
593
|
+
config.exposeHeaders
|
|
594
|
+
),
|
|
595
|
+
allowMethods: [...DEFAULT_SYNC_CORS_ALLOW_METHODS],
|
|
596
|
+
allowCredentials: true,
|
|
597
|
+
maxAgeSeconds: 86_400,
|
|
598
|
+
};
|
|
422
599
|
}
|
|
423
600
|
|
|
424
601
|
function createOpaqueId(prefix: string): string {
|
|
@@ -692,36 +869,28 @@ export function createSyncRoutes<
|
|
|
692
869
|
});
|
|
693
870
|
return c.text('Internal Server Error', 500);
|
|
694
871
|
});
|
|
695
|
-
const corsConfig = config.cors;
|
|
872
|
+
const corsConfig = normalizeSyncCorsConfig(config.cors);
|
|
696
873
|
if (corsConfig) {
|
|
697
874
|
routes.use('*', async (c, next) => {
|
|
698
875
|
const origin = readOriginHeader(c);
|
|
699
|
-
const allowedOrigin = await
|
|
700
|
-
config: corsConfig,
|
|
701
|
-
origin,
|
|
702
|
-
context: c,
|
|
703
|
-
});
|
|
876
|
+
const allowedOrigin = await corsConfig.resolveOrigin(origin, c);
|
|
704
877
|
|
|
705
878
|
if (origin && !allowedOrigin) {
|
|
706
879
|
return createSyncCorsOriginDeniedResponse(origin);
|
|
707
880
|
}
|
|
708
881
|
|
|
709
882
|
const resolvedOrigin = allowedOrigin ?? '*';
|
|
710
|
-
const allowHeaders =
|
|
711
|
-
corsConfig.allowHeaders ?? DEFAULT_SYNC_CORS_ALLOW_HEADERS;
|
|
712
|
-
const allowMethods =
|
|
713
|
-
corsConfig.allowMethods ?? DEFAULT_SYNC_CORS_ALLOW_METHODS;
|
|
714
|
-
const maxAgeSeconds = corsConfig.maxAgeSeconds ?? 86_400;
|
|
715
883
|
|
|
716
884
|
if (c.req.method === 'OPTIONS') {
|
|
717
885
|
const headers = new Headers();
|
|
718
886
|
applySyncCorsHeaders({
|
|
719
887
|
headers,
|
|
720
888
|
allowedOrigin: resolvedOrigin,
|
|
721
|
-
allowCredentials: corsConfig.allowCredentials
|
|
722
|
-
allowHeaders,
|
|
723
|
-
|
|
724
|
-
|
|
889
|
+
allowCredentials: corsConfig.allowCredentials,
|
|
890
|
+
allowHeaders: corsConfig.allowHeaders,
|
|
891
|
+
exposeHeaders: corsConfig.exposeHeaders,
|
|
892
|
+
allowMethods: corsConfig.allowMethods,
|
|
893
|
+
maxAgeSeconds: corsConfig.maxAgeSeconds,
|
|
725
894
|
});
|
|
726
895
|
return new Response(null, { status: 204, headers });
|
|
727
896
|
}
|
|
@@ -730,10 +899,11 @@ export function createSyncRoutes<
|
|
|
730
899
|
applySyncCorsHeaders({
|
|
731
900
|
headers: c.res.headers,
|
|
732
901
|
allowedOrigin: resolvedOrigin,
|
|
733
|
-
allowCredentials: corsConfig.allowCredentials
|
|
734
|
-
allowHeaders,
|
|
735
|
-
|
|
736
|
-
|
|
902
|
+
allowCredentials: corsConfig.allowCredentials,
|
|
903
|
+
allowHeaders: corsConfig.allowHeaders,
|
|
904
|
+
exposeHeaders: corsConfig.exposeHeaders,
|
|
905
|
+
allowMethods: corsConfig.allowMethods,
|
|
906
|
+
maxAgeSeconds: corsConfig.maxAgeSeconds,
|
|
737
907
|
});
|
|
738
908
|
return c.res;
|
|
739
909
|
});
|
package/src/websocket-origin.ts
CHANGED
|
@@ -58,6 +58,35 @@ function isAllowedOriginMatch(origin: URL, pattern: string): boolean {
|
|
|
58
58
|
return matchesWildcardOriginPattern(origin, pattern);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export function resolveAllowedOriginFromPatterns(
|
|
62
|
+
originHeader: string | null | undefined,
|
|
63
|
+
allowedOrigins: string | string[] | '*'
|
|
64
|
+
): string | null {
|
|
65
|
+
if (allowedOrigins === '*') {
|
|
66
|
+
return '*';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const normalizedAllowedOrigins =
|
|
70
|
+
typeof allowedOrigins === 'string' ? [allowedOrigins] : allowedOrigins;
|
|
71
|
+
|
|
72
|
+
if (!originHeader) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let parsedOrigin: URL;
|
|
77
|
+
try {
|
|
78
|
+
parsedOrigin = new URL(originHeader);
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return normalizedAllowedOrigins.some((pattern) =>
|
|
84
|
+
isAllowedOriginMatch(parsedOrigin, pattern)
|
|
85
|
+
)
|
|
86
|
+
? originHeader
|
|
87
|
+
: null;
|
|
88
|
+
}
|
|
89
|
+
|
|
61
90
|
export function isRequestOriginAllowed(args: {
|
|
62
91
|
requestUrl: string;
|
|
63
92
|
originHeader?: string | null;
|
|
@@ -69,15 +98,8 @@ export function isRequestOriginAllowed(args: {
|
|
|
69
98
|
|
|
70
99
|
const origin = args.originHeader;
|
|
71
100
|
if (Array.isArray(args.allowedOrigins)) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
parsedOrigin = new URL(origin);
|
|
76
|
-
} catch {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
return args.allowedOrigins.some((pattern) =>
|
|
80
|
-
isAllowedOriginMatch(parsedOrigin, pattern)
|
|
101
|
+
return (
|
|
102
|
+
resolveAllowedOriginFromPatterns(origin, args.allowedOrigins) !== null
|
|
81
103
|
);
|
|
82
104
|
}
|
|
83
105
|
|