@syncular/server-hono 0.0.6-171 → 0.0.6-178
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/dist/console/gateway.d.ts +5 -0
- package/dist/console/gateway.d.ts.map +1 -1
- package/dist/console/gateway.js +1 -16
- package/dist/console/gateway.js.map +1 -1
- package/dist/console/routes.d.ts.map +1 -1
- package/dist/console/routes.js +15 -30
- package/dist/console/routes.js.map +1 -1
- package/dist/console/types.d.ts +1 -1
- package/dist/proxy/routes.d.ts +3 -1
- package/dist/proxy/routes.d.ts.map +1 -1
- package/dist/proxy/routes.js +1 -16
- package/dist/proxy/routes.js.map +1 -1
- package/dist/routes.d.ts +3 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +133 -77
- package/dist/routes.js.map +1 -1
- package/dist/websocket-origin.d.ts +8 -0
- package/dist/websocket-origin.d.ts.map +1 -0
- package/dist/websocket-origin.js +43 -0
- package/dist/websocket-origin.js.map +1 -0
- package/dist/ws.d.ts +24 -8
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js +54 -44
- package/dist/ws.js.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/console-gateway-live-routes.test.ts +13 -0
- package/src/__tests__/create-server.test.ts +187 -2
- package/src/__tests__/pull-chunk-storage.test.ts +114 -1
- package/src/__tests__/websocket-origin.test.ts +55 -0
- package/src/__tests__/ws-connection-manager.test.ts +22 -7
- package/src/console/gateway.ts +6 -18
- package/src/console/routes.ts +22 -39
- package/src/console/types.ts +1 -1
- package/src/proxy/routes.ts +4 -19
- package/src/routes.ts +192 -109
- package/src/websocket-origin.ts +54 -0
- package/src/ws.ts +86 -45
package/src/routes.ts
CHANGED
|
@@ -52,12 +52,7 @@ import { Hono } from 'hono';
|
|
|
52
52
|
|
|
53
53
|
import type { UpgradeWebSocket } from 'hono/ws';
|
|
54
54
|
import { describeRoute, resolver, validator as zValidator } from 'hono-openapi';
|
|
55
|
-
import {
|
|
56
|
-
type Kysely,
|
|
57
|
-
type SelectQueryBuilder,
|
|
58
|
-
type SqlBool,
|
|
59
|
-
sql,
|
|
60
|
-
} from 'kysely';
|
|
55
|
+
import { type Kysely, sql } from 'kysely';
|
|
61
56
|
import { z } from 'zod';
|
|
62
57
|
import { isBenignConsoleSchemaError } from './console/schema-errors';
|
|
63
58
|
import {
|
|
@@ -65,8 +60,10 @@ import {
|
|
|
65
60
|
DEFAULT_SYNC_RATE_LIMITS,
|
|
66
61
|
type SyncRateLimitConfig,
|
|
67
62
|
} from './rate-limit';
|
|
63
|
+
import { isWebSocketOriginAllowed } from './websocket-origin';
|
|
68
64
|
import {
|
|
69
65
|
createWebSocketConnection,
|
|
66
|
+
createWebSocketConnectionOwnerKey,
|
|
70
67
|
type WebSocketConnection,
|
|
71
68
|
WebSocketConnectionManager,
|
|
72
69
|
} from './ws';
|
|
@@ -119,7 +116,9 @@ export interface SyncWebSocketConfig {
|
|
|
119
116
|
messageRateWindowMs?: number;
|
|
120
117
|
/**
|
|
121
118
|
* Optional list of allowed websocket origins.
|
|
122
|
-
*
|
|
119
|
+
* - undefined: allow same-origin browser upgrades and origin-less non-browser clients
|
|
120
|
+
* - '*': allow all origins
|
|
121
|
+
* - string[]: exact origin match (scheme + host + port)
|
|
123
122
|
*/
|
|
124
123
|
allowedOrigins?: string[] | '*';
|
|
125
124
|
}
|
|
@@ -1414,6 +1413,55 @@ export function createSyncRoutes<
|
|
|
1414
1413
|
const clientId = body.clientId;
|
|
1415
1414
|
const requestId = readRequestId(c);
|
|
1416
1415
|
const traceContext = readTraceContext(c);
|
|
1416
|
+
const connectionOwnerKey = createWebSocketConnectionOwnerKey({
|
|
1417
|
+
partitionId,
|
|
1418
|
+
actorId: auth.actorId,
|
|
1419
|
+
clientId,
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
const clientState = await readClientState(
|
|
1423
|
+
options.db,
|
|
1424
|
+
partitionId,
|
|
1425
|
+
clientId
|
|
1426
|
+
);
|
|
1427
|
+
let allowStaleScopeRebind = false;
|
|
1428
|
+
if (
|
|
1429
|
+
body.pull &&
|
|
1430
|
+
!body.push &&
|
|
1431
|
+
clientState.ownerActorId !== null &&
|
|
1432
|
+
clientState.ownerActorId !== auth.actorId
|
|
1433
|
+
) {
|
|
1434
|
+
const resolved = await resolveEffectiveScopesForSubscriptions({
|
|
1435
|
+
db: options.db,
|
|
1436
|
+
auth,
|
|
1437
|
+
subscriptions: body.pull.subscriptions,
|
|
1438
|
+
handlers: handlerRegistry,
|
|
1439
|
+
scopeCache: options.scopeCache,
|
|
1440
|
+
});
|
|
1441
|
+
allowStaleScopeRebind = resolved.every(
|
|
1442
|
+
(subscription) => subscription.status === 'revoked'
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
if (
|
|
1447
|
+
!allowStaleScopeRebind &&
|
|
1448
|
+
(clientState.hasConflict || clientState.ownerActorId !== null)
|
|
1449
|
+
) {
|
|
1450
|
+
if (
|
|
1451
|
+
clientState.ownerActorId !== auth.actorId ||
|
|
1452
|
+
clientState.hasConflict
|
|
1453
|
+
) {
|
|
1454
|
+
return c.json(
|
|
1455
|
+
{
|
|
1456
|
+
error: 'INVALID_CLIENT_ID',
|
|
1457
|
+
message: clientState.hasConflict
|
|
1458
|
+
? 'clientId has conflicting ownership history'
|
|
1459
|
+
: 'clientId is already bound to a different actor',
|
|
1460
|
+
},
|
|
1461
|
+
400
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1417
1465
|
|
|
1418
1466
|
let pushResponse:
|
|
1419
1467
|
| undefined
|
|
@@ -1527,34 +1575,32 @@ export function createSyncRoutes<
|
|
|
1527
1575
|
throw err;
|
|
1528
1576
|
}
|
|
1529
1577
|
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
clientId,
|
|
1552
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1553
|
-
});
|
|
1578
|
+
try {
|
|
1579
|
+
await recordClientCursor(options.db, options.dialect, {
|
|
1580
|
+
partitionId,
|
|
1581
|
+
clientId,
|
|
1582
|
+
actorId: auth.actorId,
|
|
1583
|
+
cursor: pullResult.clientCursor,
|
|
1584
|
+
effectiveScopes: pullResult.effectiveScopes,
|
|
1585
|
+
});
|
|
1586
|
+
emitConsoleLiveEvent(consoleLiveEmitter, 'client_update', () => ({
|
|
1587
|
+
action: 'cursor_recorded',
|
|
1588
|
+
partitionId,
|
|
1589
|
+
actorId: auth.actorId,
|
|
1590
|
+
clientId,
|
|
1591
|
+
cursor: pullResult.clientCursor,
|
|
1592
|
+
}));
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
logAsyncFailureOnce('sync.client_cursor_record_failed', {
|
|
1595
|
+
event: 'sync.client_cursor_record_failed',
|
|
1596
|
+
userId: auth.actorId,
|
|
1597
|
+
clientId,
|
|
1598
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1554
1599
|
});
|
|
1600
|
+
}
|
|
1555
1601
|
|
|
1556
|
-
wsConnectionManager?.
|
|
1557
|
-
|
|
1602
|
+
wsConnectionManager?.updateConnectionScopeKeys(
|
|
1603
|
+
connectionOwnerKey,
|
|
1558
1604
|
applyPartitionToScopeKeys(
|
|
1559
1605
|
partitionId,
|
|
1560
1606
|
scopeValuesToScopeKeys(pullResult.effectiveScopes)
|
|
@@ -1726,39 +1772,47 @@ export function createSyncRoutes<
|
|
|
1726
1772
|
return c.json({ error: 'NOT_FOUND' }, 404);
|
|
1727
1773
|
}
|
|
1728
1774
|
|
|
1729
|
-
if (requestedChunkScopes) {
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
scopes: requestedChunkScopes,
|
|
1739
|
-
cursor: 0,
|
|
1740
|
-
},
|
|
1741
|
-
],
|
|
1742
|
-
handlers: handlerRegistry,
|
|
1743
|
-
scopeCache: options.scopeCache,
|
|
1744
|
-
});
|
|
1745
|
-
const scopeAuth = resolved[0];
|
|
1746
|
-
if (!scopeAuth || scopeAuth.status !== 'active') {
|
|
1747
|
-
return c.json({ error: 'FORBIDDEN' }, 403);
|
|
1748
|
-
}
|
|
1775
|
+
if (!requestedChunkScopes) {
|
|
1776
|
+
return c.json(
|
|
1777
|
+
{
|
|
1778
|
+
error: 'INVALID_REQUEST',
|
|
1779
|
+
message: 'Snapshot chunk scope values are required',
|
|
1780
|
+
},
|
|
1781
|
+
400
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1749
1784
|
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1785
|
+
try {
|
|
1786
|
+
const resolved = await resolveEffectiveScopesForSubscriptions({
|
|
1787
|
+
db: options.db,
|
|
1788
|
+
auth,
|
|
1789
|
+
subscriptions: [
|
|
1790
|
+
{
|
|
1791
|
+
id: 'snapshot-chunk-authz',
|
|
1792
|
+
table: chunk.scope,
|
|
1793
|
+
scopes: requestedChunkScopes,
|
|
1794
|
+
cursor: 0,
|
|
1795
|
+
},
|
|
1796
|
+
],
|
|
1797
|
+
handlers: handlerRegistry,
|
|
1798
|
+
scopeCache: options.scopeCache,
|
|
1799
|
+
});
|
|
1800
|
+
const scopeAuth = resolved[0];
|
|
1801
|
+
if (!scopeAuth || scopeAuth.status !== 'active') {
|
|
1802
|
+
return c.json({ error: 'FORBIDDEN' }, 403);
|
|
1761
1803
|
}
|
|
1804
|
+
|
|
1805
|
+
const scopeKey = `${partitionId}:${await scopesToSnapshotChunkScopeKey(
|
|
1806
|
+
scopeAuth.scopes
|
|
1807
|
+
)}`;
|
|
1808
|
+
if (scopeKey !== chunk.scopeKey) {
|
|
1809
|
+
return c.json({ error: 'FORBIDDEN' }, 403);
|
|
1810
|
+
}
|
|
1811
|
+
} catch (error) {
|
|
1812
|
+
if (error instanceof InvalidSubscriptionScopeError) {
|
|
1813
|
+
return c.json({ error: 'FORBIDDEN' }, 403);
|
|
1814
|
+
}
|
|
1815
|
+
throw error;
|
|
1762
1816
|
}
|
|
1763
1817
|
|
|
1764
1818
|
const etag = `"sha256:${chunk.sha256}"`;
|
|
@@ -1769,7 +1823,7 @@ export function createSyncRoutes<
|
|
|
1769
1823
|
headers: {
|
|
1770
1824
|
ETag: etag,
|
|
1771
1825
|
'Cache-Control': 'private, max-age=0',
|
|
1772
|
-
Vary: 'Authorization',
|
|
1826
|
+
Vary: 'Authorization, X-Syncular-Snapshot-Scopes',
|
|
1773
1827
|
},
|
|
1774
1828
|
});
|
|
1775
1829
|
}
|
|
@@ -1782,7 +1836,7 @@ export function createSyncRoutes<
|
|
|
1782
1836
|
'Content-Length': String(chunk.byteLength),
|
|
1783
1837
|
ETag: etag,
|
|
1784
1838
|
'Cache-Control': 'private, max-age=0',
|
|
1785
|
-
Vary: 'Authorization',
|
|
1839
|
+
Vary: 'Authorization, X-Syncular-Snapshot-Scopes',
|
|
1786
1840
|
'X-Sync-Chunk-Id': chunk.chunkId,
|
|
1787
1841
|
'X-Sync-Chunk-Sha256': chunk.sha256,
|
|
1788
1842
|
'X-Sync-Chunk-Encoding': chunk.encoding,
|
|
@@ -1819,31 +1873,39 @@ export function createSyncRoutes<
|
|
|
1819
1873
|
c,
|
|
1820
1874
|
c.req.query('transportPath')
|
|
1821
1875
|
);
|
|
1876
|
+
const connectionOwnerKey = createWebSocketConnectionOwnerKey({
|
|
1877
|
+
partitionId,
|
|
1878
|
+
actorId: auth.actorId,
|
|
1879
|
+
clientId,
|
|
1880
|
+
});
|
|
1822
1881
|
|
|
1823
1882
|
// Load last-known effective scopes for this client (best-effort).
|
|
1824
1883
|
// Keeps /realtime lightweight and avoids sending large subscription payloads over the URL.
|
|
1825
1884
|
let initialScopeKeys: string[] = [];
|
|
1826
1885
|
try {
|
|
1827
|
-
const
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1886
|
+
const clientState = await readClientState(
|
|
1887
|
+
options.db,
|
|
1888
|
+
partitionId,
|
|
1889
|
+
clientId
|
|
1890
|
+
);
|
|
1891
|
+
if (clientState.hasConflict || clientState.ownerActorId !== null) {
|
|
1892
|
+
if (
|
|
1893
|
+
clientState.ownerActorId !== auth.actorId ||
|
|
1894
|
+
clientState.hasConflict
|
|
1895
|
+
) {
|
|
1896
|
+
return c.json(
|
|
1897
|
+
{
|
|
1898
|
+
error: 'INVALID_CLIENT_ID',
|
|
1899
|
+
message: clientState.hasConflict
|
|
1900
|
+
? 'clientId has conflicting ownership history'
|
|
1901
|
+
: 'clientId is already bound to a different actor',
|
|
1902
|
+
},
|
|
1903
|
+
400
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1844
1906
|
}
|
|
1845
1907
|
|
|
1846
|
-
const raw =
|
|
1908
|
+
const raw = clientState.effectiveScopes;
|
|
1847
1909
|
let parsed: unknown = raw;
|
|
1848
1910
|
if (typeof raw === 'string') {
|
|
1849
1911
|
try {
|
|
@@ -1884,7 +1946,7 @@ export function createSyncRoutes<
|
|
|
1884
1946
|
|
|
1885
1947
|
if (
|
|
1886
1948
|
maxConnectionsPerClient > 0 &&
|
|
1887
|
-
wsConnectionManager.
|
|
1949
|
+
wsConnectionManager.getScopedConnectionCount(connectionOwnerKey) >=
|
|
1888
1950
|
maxConnectionsPerClient
|
|
1889
1951
|
) {
|
|
1890
1952
|
logSyncEvent({
|
|
@@ -1900,7 +1962,7 @@ export function createSyncRoutes<
|
|
|
1900
1962
|
let unregister: (() => void) | null = null;
|
|
1901
1963
|
let connRef: ReturnType<typeof createWebSocketConnection> | null = null;
|
|
1902
1964
|
const connectionCountBeforeUpgrade =
|
|
1903
|
-
wsConnectionManager.
|
|
1965
|
+
wsConnectionManager.getScopedConnectionCount(connectionOwnerKey);
|
|
1904
1966
|
let sessionStartedAtMs: number | null = null;
|
|
1905
1967
|
let sessionEnded = false;
|
|
1906
1968
|
|
|
@@ -1965,6 +2027,7 @@ export function createSyncRoutes<
|
|
|
1965
2027
|
const conn = createWebSocketConnection(ws, {
|
|
1966
2028
|
actorId: auth.actorId,
|
|
1967
2029
|
clientId,
|
|
2030
|
+
ownerKey: connectionOwnerKey,
|
|
1968
2031
|
transportPath: realtimeTransportPath,
|
|
1969
2032
|
});
|
|
1970
2033
|
connRef = conn;
|
|
@@ -2052,7 +2115,7 @@ export function createSyncRoutes<
|
|
|
2052
2115
|
case 'join':
|
|
2053
2116
|
if (
|
|
2054
2117
|
!wsConnectionManager.joinPresence(
|
|
2055
|
-
|
|
2118
|
+
connectionOwnerKey,
|
|
2056
2119
|
scopeKey,
|
|
2057
2120
|
msg.metadata
|
|
2058
2121
|
)
|
|
@@ -2071,17 +2134,17 @@ export function createSyncRoutes<
|
|
|
2071
2134
|
}
|
|
2072
2135
|
break;
|
|
2073
2136
|
case 'leave':
|
|
2074
|
-
wsConnectionManager.leavePresence(
|
|
2137
|
+
wsConnectionManager.leavePresence(connectionOwnerKey, scopeKey);
|
|
2075
2138
|
break;
|
|
2076
2139
|
case 'update':
|
|
2077
2140
|
if (
|
|
2078
2141
|
!wsConnectionManager.updatePresenceMetadata(
|
|
2079
|
-
|
|
2142
|
+
connectionOwnerKey,
|
|
2080
2143
|
scopeKey,
|
|
2081
2144
|
msg.metadata ?? {}
|
|
2082
2145
|
) &&
|
|
2083
|
-
!wsConnectionManager.
|
|
2084
|
-
|
|
2146
|
+
!wsConnectionManager.isConnectionSubscribedToScopeKey(
|
|
2147
|
+
connectionOwnerKey,
|
|
2085
2148
|
scopeKey
|
|
2086
2149
|
)
|
|
2087
2150
|
) {
|
|
@@ -2360,24 +2423,6 @@ function clampInt(value: number, min: number, max: number): number {
|
|
|
2360
2423
|
return Math.max(min, Math.min(max, value));
|
|
2361
2424
|
}
|
|
2362
2425
|
|
|
2363
|
-
function isWebSocketOriginAllowed(
|
|
2364
|
-
c: Context,
|
|
2365
|
-
allowedOrigins?: string[] | '*'
|
|
2366
|
-
): boolean {
|
|
2367
|
-
if (!allowedOrigins) return true;
|
|
2368
|
-
if (allowedOrigins === '*') return true;
|
|
2369
|
-
|
|
2370
|
-
const origin = c.req.header('origin');
|
|
2371
|
-
if (!origin) return false;
|
|
2372
|
-
|
|
2373
|
-
try {
|
|
2374
|
-
const normalizedOrigin = new URL(origin).origin;
|
|
2375
|
-
return allowedOrigins.includes(normalizedOrigin);
|
|
2376
|
-
} catch {
|
|
2377
|
-
return false;
|
|
2378
|
-
}
|
|
2379
|
-
}
|
|
2380
|
-
|
|
2381
2426
|
function measureWebSocketMessageBytes(data: unknown): number {
|
|
2382
2427
|
if (typeof data === 'string') {
|
|
2383
2428
|
return new TextEncoder().encode(data).byteLength;
|
|
@@ -2501,3 +2546,41 @@ async function readCommitScopeKeys<DB extends SyncCoreDb>(
|
|
|
2501
2546
|
|
|
2502
2547
|
return Array.from(scopeKeys);
|
|
2503
2548
|
}
|
|
2549
|
+
|
|
2550
|
+
async function readClientState<DB extends SyncCoreDb>(
|
|
2551
|
+
db: Kysely<DB>,
|
|
2552
|
+
partitionId: string,
|
|
2553
|
+
clientId: string
|
|
2554
|
+
): Promise<{
|
|
2555
|
+
ownerActorId: string | null;
|
|
2556
|
+
effectiveScopes: unknown;
|
|
2557
|
+
hasConflict: boolean;
|
|
2558
|
+
}> {
|
|
2559
|
+
const [cursorResult, latestCommitResult] = await Promise.all([
|
|
2560
|
+
sql<{ actor_id: string | null; effective_scopes: unknown }>`
|
|
2561
|
+
SELECT actor_id, effective_scopes
|
|
2562
|
+
FROM sync_client_cursors
|
|
2563
|
+
WHERE partition_id = ${partitionId} AND client_id = ${clientId}
|
|
2564
|
+
LIMIT 1
|
|
2565
|
+
`.execute(db),
|
|
2566
|
+
sql<{ actor_id: string | null }>`
|
|
2567
|
+
SELECT actor_id
|
|
2568
|
+
FROM sync_commits
|
|
2569
|
+
WHERE partition_id = ${partitionId} AND client_id = ${clientId}
|
|
2570
|
+
ORDER BY commit_seq DESC
|
|
2571
|
+
LIMIT 1
|
|
2572
|
+
`.execute(db),
|
|
2573
|
+
]);
|
|
2574
|
+
const cursorRow = cursorResult.rows[0];
|
|
2575
|
+
const latestCommitRow = latestCommitResult.rows[0];
|
|
2576
|
+
|
|
2577
|
+
// Cursor state reflects the current authenticated owner for a clientId.
|
|
2578
|
+
// Commit history is only used to seed ownership before the first pull.
|
|
2579
|
+
const ownerActorId = cursorRow?.actor_id ?? latestCommitRow?.actor_id ?? null;
|
|
2580
|
+
|
|
2581
|
+
return {
|
|
2582
|
+
ownerActorId,
|
|
2583
|
+
effectiveScopes: cursorRow?.effective_scopes ?? null,
|
|
2584
|
+
hasConflict: false,
|
|
2585
|
+
};
|
|
2586
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
|
|
3
|
+
function normalizeOrigin(origin: string): string | null {
|
|
4
|
+
try {
|
|
5
|
+
return new URL(origin).origin;
|
|
6
|
+
} catch {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isRequestOriginAllowed(args: {
|
|
12
|
+
requestUrl: string;
|
|
13
|
+
originHeader?: string | null;
|
|
14
|
+
allowedOrigins?: string[] | '*';
|
|
15
|
+
}): boolean {
|
|
16
|
+
if (args.allowedOrigins === '*') {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const origin = args.originHeader;
|
|
21
|
+
if (Array.isArray(args.allowedOrigins)) {
|
|
22
|
+
if (!origin) return false;
|
|
23
|
+
const normalizedOrigin = normalizeOrigin(origin);
|
|
24
|
+
return normalizedOrigin
|
|
25
|
+
? args.allowedOrigins.includes(normalizedOrigin)
|
|
26
|
+
: false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!origin) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const normalizedOrigin = normalizeOrigin(origin);
|
|
34
|
+
if (!normalizedOrigin) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
return normalizedOrigin === new URL(args.requestUrl).origin;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function isWebSocketOriginAllowed(
|
|
46
|
+
c: Context,
|
|
47
|
+
allowedOrigins?: string[] | '*'
|
|
48
|
+
): boolean {
|
|
49
|
+
return isRequestOriginAllowed({
|
|
50
|
+
requestUrl: c.req.url,
|
|
51
|
+
originHeader: c.req.header('origin'),
|
|
52
|
+
allowedOrigins,
|
|
53
|
+
});
|
|
54
|
+
}
|