@syncular/server-hono 0.0.6-158 → 0.0.6-165
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/blobs.d.ts +10 -4
- package/dist/blobs.d.ts.map +1 -1
- package/dist/blobs.js +260 -26
- package/dist/blobs.js.map +1 -1
- package/dist/console/gateway.d.ts +4 -0
- package/dist/console/gateway.d.ts.map +1 -1
- package/dist/console/gateway.js +97 -60
- package/dist/console/gateway.js.map +1 -1
- package/dist/console/route-descriptor.d.ts +6 -0
- package/dist/console/route-descriptor.d.ts.map +1 -0
- package/dist/console/route-descriptor.js +16 -0
- package/dist/console/route-descriptor.js.map +1 -0
- package/dist/console/routes.d.ts.map +1 -1
- package/dist/console/routes.js +153 -108
- package/dist/console/routes.js.map +1 -1
- package/dist/console/schema-errors.d.ts +2 -0
- package/dist/console/schema-errors.d.ts.map +1 -0
- package/dist/console/schema-errors.js +17 -0
- package/dist/console/schema-errors.js.map +1 -0
- package/dist/console/schemas.js +1 -1
- package/dist/console/schemas.js.map +1 -1
- package/dist/console/types.d.ts +32 -0
- package/dist/console/types.d.ts.map +1 -1
- package/dist/create-server.d.ts.map +1 -1
- package/dist/create-server.js +13 -10
- package/dist/create-server.js.map +1 -1
- package/dist/proxy/routes.d.ts +10 -0
- package/dist/proxy/routes.d.ts.map +1 -1
- package/dist/proxy/routes.js +57 -6
- package/dist/proxy/routes.js.map +1 -1
- package/dist/routes.d.ts +21 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +338 -352
- package/dist/routes.js.map +1 -1
- package/package.json +7 -6
- package/src/__tests__/blob-routes.test.ts +286 -18
- package/src/__tests__/console-gateway-live-routes.test.ts +61 -1
- package/src/__tests__/console-routes.test.ts +30 -1
- package/src/__tests__/create-server.test.ts +237 -1
- package/src/__tests__/pull-chunk-storage.test.ts +98 -0
- package/src/blobs.ts +360 -34
- package/src/console/gateway.ts +335 -288
- package/src/console/route-descriptor.ts +22 -0
- package/src/console/routes.ts +327 -248
- package/src/console/schema-errors.ts +23 -0
- package/src/console/schemas.ts +1 -1
- package/src/console/types.ts +32 -0
- package/src/create-server.ts +13 -10
- package/src/proxy/routes.ts +73 -9
- package/src/routes.ts +449 -396
package/src/console/gateway.ts
CHANGED
|
@@ -2,13 +2,14 @@ import type { Context } from 'hono';
|
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
3
|
import { cors } from 'hono/cors';
|
|
4
4
|
import type { UpgradeWebSocket } from 'hono/ws';
|
|
5
|
-
import {
|
|
5
|
+
import { resolver, validator as zValidator } from 'hono-openapi';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import {
|
|
8
8
|
closeUnauthenticatedSocket,
|
|
9
9
|
parseBearerToken,
|
|
10
10
|
parseWebSocketAuthToken,
|
|
11
11
|
} from './live-auth';
|
|
12
|
+
import { describeConsoleGatewayRoute } from './route-descriptor';
|
|
12
13
|
import type {
|
|
13
14
|
ConsoleApiKey,
|
|
14
15
|
ConsoleApiKeyBulkRevokeResponse,
|
|
@@ -87,6 +88,10 @@ export interface CreateConsoleGatewayRoutesOptions {
|
|
|
87
88
|
upgradeWebSocket?: UpgradeWebSocket;
|
|
88
89
|
heartbeatIntervalMs?: number;
|
|
89
90
|
createWebSocket?: (url: string) => ConsoleGatewayDownstreamSocket;
|
|
91
|
+
maxMessageBytes?: number;
|
|
92
|
+
maxMessagesPerWindow?: number;
|
|
93
|
+
messageRateWindowMs?: number;
|
|
94
|
+
allowedOrigins?: string[] | '*';
|
|
90
95
|
};
|
|
91
96
|
}
|
|
92
97
|
|
|
@@ -1268,8 +1273,7 @@ export function createConsoleGatewayRoutes(
|
|
|
1268
1273
|
|
|
1269
1274
|
routes.get(
|
|
1270
1275
|
'/instances',
|
|
1271
|
-
|
|
1272
|
-
tags: ['console-gateway'],
|
|
1276
|
+
describeConsoleGatewayRoute({
|
|
1273
1277
|
summary: 'List configured downstream console instances',
|
|
1274
1278
|
responses: {
|
|
1275
1279
|
200: {
|
|
@@ -1306,8 +1310,7 @@ export function createConsoleGatewayRoutes(
|
|
|
1306
1310
|
|
|
1307
1311
|
routes.get(
|
|
1308
1312
|
'/instances/health',
|
|
1309
|
-
|
|
1310
|
-
tags: ['console-gateway'],
|
|
1313
|
+
describeConsoleGatewayRoute({
|
|
1311
1314
|
summary: 'Probe downstream console health by instance',
|
|
1312
1315
|
responses: {
|
|
1313
1316
|
200: {
|
|
@@ -1366,8 +1369,7 @@ export function createConsoleGatewayRoutes(
|
|
|
1366
1369
|
|
|
1367
1370
|
routes.get(
|
|
1368
1371
|
'/handlers',
|
|
1369
|
-
|
|
1370
|
-
tags: ['console-gateway'],
|
|
1372
|
+
describeConsoleGatewayRoute({
|
|
1371
1373
|
summary:
|
|
1372
1374
|
'List handlers for a single target instance (requires instance selection)',
|
|
1373
1375
|
responses: {
|
|
@@ -1398,8 +1400,7 @@ export function createConsoleGatewayRoutes(
|
|
|
1398
1400
|
|
|
1399
1401
|
routes.post(
|
|
1400
1402
|
'/prune/preview',
|
|
1401
|
-
|
|
1402
|
-
tags: ['console-gateway'],
|
|
1403
|
+
describeConsoleGatewayRoute({
|
|
1403
1404
|
summary:
|
|
1404
1405
|
'Preview prune on a single target instance (requires instance selection)',
|
|
1405
1406
|
responses: {
|
|
@@ -1430,8 +1431,7 @@ export function createConsoleGatewayRoutes(
|
|
|
1430
1431
|
|
|
1431
1432
|
routes.post(
|
|
1432
1433
|
'/prune',
|
|
1433
|
-
|
|
1434
|
-
tags: ['console-gateway'],
|
|
1434
|
+
describeConsoleGatewayRoute({
|
|
1435
1435
|
summary:
|
|
1436
1436
|
'Trigger prune on a single target instance (requires instance selection)',
|
|
1437
1437
|
responses: {
|
|
@@ -1462,8 +1462,7 @@ export function createConsoleGatewayRoutes(
|
|
|
1462
1462
|
|
|
1463
1463
|
routes.post(
|
|
1464
1464
|
'/compact',
|
|
1465
|
-
|
|
1466
|
-
tags: ['console-gateway'],
|
|
1465
|
+
describeConsoleGatewayRoute({
|
|
1467
1466
|
summary:
|
|
1468
1467
|
'Trigger compaction on a single target instance (requires instance selection)',
|
|
1469
1468
|
responses: {
|
|
@@ -1494,8 +1493,7 @@ export function createConsoleGatewayRoutes(
|
|
|
1494
1493
|
|
|
1495
1494
|
routes.post(
|
|
1496
1495
|
'/notify-data-change',
|
|
1497
|
-
|
|
1498
|
-
tags: ['console-gateway'],
|
|
1496
|
+
describeConsoleGatewayRoute({
|
|
1499
1497
|
summary:
|
|
1500
1498
|
'Notify data change on a single target instance (requires instance selection)',
|
|
1501
1499
|
responses: {
|
|
@@ -1529,8 +1527,7 @@ export function createConsoleGatewayRoutes(
|
|
|
1529
1527
|
|
|
1530
1528
|
routes.delete(
|
|
1531
1529
|
'/clients/:id',
|
|
1532
|
-
|
|
1533
|
-
tags: ['console-gateway'],
|
|
1530
|
+
describeConsoleGatewayRoute({
|
|
1534
1531
|
summary:
|
|
1535
1532
|
'Evict client on a single target instance (requires instance selection)',
|
|
1536
1533
|
responses: {
|
|
@@ -1563,8 +1560,7 @@ export function createConsoleGatewayRoutes(
|
|
|
1563
1560
|
|
|
1564
1561
|
routes.delete(
|
|
1565
1562
|
'/events',
|
|
1566
|
-
|
|
1567
|
-
tags: ['console-gateway'],
|
|
1563
|
+
describeConsoleGatewayRoute({
|
|
1568
1564
|
summary:
|
|
1569
1565
|
'Clear request events on a single target instance (requires instance selection)',
|
|
1570
1566
|
responses: {
|
|
@@ -1595,8 +1591,7 @@ export function createConsoleGatewayRoutes(
|
|
|
1595
1591
|
|
|
1596
1592
|
routes.post(
|
|
1597
1593
|
'/events/prune',
|
|
1598
|
-
|
|
1599
|
-
tags: ['console-gateway'],
|
|
1594
|
+
describeConsoleGatewayRoute({
|
|
1600
1595
|
summary:
|
|
1601
1596
|
'Prune request events on a single target instance (requires instance selection)',
|
|
1602
1597
|
responses: {
|
|
@@ -1627,8 +1622,7 @@ export function createConsoleGatewayRoutes(
|
|
|
1627
1622
|
|
|
1628
1623
|
routes.get(
|
|
1629
1624
|
'/api-keys',
|
|
1630
|
-
|
|
1631
|
-
tags: ['console-gateway'],
|
|
1625
|
+
describeConsoleGatewayRoute({
|
|
1632
1626
|
summary:
|
|
1633
1627
|
'List API keys for a single target instance (requires instance selection)',
|
|
1634
1628
|
responses: {
|
|
@@ -1663,8 +1657,7 @@ export function createConsoleGatewayRoutes(
|
|
|
1663
1657
|
|
|
1664
1658
|
routes.post(
|
|
1665
1659
|
'/api-keys',
|
|
1666
|
-
|
|
1667
|
-
tags: ['console-gateway'],
|
|
1660
|
+
describeConsoleGatewayRoute({
|
|
1668
1661
|
summary:
|
|
1669
1662
|
'Create API key on a single target instance (requires instance selection)',
|
|
1670
1663
|
responses: {
|
|
@@ -1698,8 +1691,7 @@ export function createConsoleGatewayRoutes(
|
|
|
1698
1691
|
|
|
1699
1692
|
routes.get(
|
|
1700
1693
|
'/api-keys/:id',
|
|
1701
|
-
|
|
1702
|
-
tags: ['console-gateway'],
|
|
1694
|
+
describeConsoleGatewayRoute({
|
|
1703
1695
|
summary:
|
|
1704
1696
|
'Get API key from a single target instance (requires instance selection)',
|
|
1705
1697
|
responses: {
|
|
@@ -1732,8 +1724,7 @@ export function createConsoleGatewayRoutes(
|
|
|
1732
1724
|
|
|
1733
1725
|
routes.delete(
|
|
1734
1726
|
'/api-keys/:id',
|
|
1735
|
-
|
|
1736
|
-
tags: ['console-gateway'],
|
|
1727
|
+
describeConsoleGatewayRoute({
|
|
1737
1728
|
summary:
|
|
1738
1729
|
'Revoke API key on a single target instance (requires instance selection)',
|
|
1739
1730
|
responses: {
|
|
@@ -1766,8 +1757,7 @@ export function createConsoleGatewayRoutes(
|
|
|
1766
1757
|
|
|
1767
1758
|
routes.post(
|
|
1768
1759
|
'/api-keys/bulk-revoke',
|
|
1769
|
-
|
|
1770
|
-
tags: ['console-gateway'],
|
|
1760
|
+
describeConsoleGatewayRoute({
|
|
1771
1761
|
summary:
|
|
1772
1762
|
'Bulk revoke API keys on a single target instance (requires instance selection)',
|
|
1773
1763
|
responses: {
|
|
@@ -1801,8 +1791,7 @@ export function createConsoleGatewayRoutes(
|
|
|
1801
1791
|
|
|
1802
1792
|
routes.post(
|
|
1803
1793
|
'/api-keys/:id/rotate/stage',
|
|
1804
|
-
|
|
1805
|
-
tags: ['console-gateway'],
|
|
1794
|
+
describeConsoleGatewayRoute({
|
|
1806
1795
|
summary:
|
|
1807
1796
|
'Stage-rotate API key on a single target instance (requires instance selection)',
|
|
1808
1797
|
responses: {
|
|
@@ -1835,8 +1824,7 @@ export function createConsoleGatewayRoutes(
|
|
|
1835
1824
|
|
|
1836
1825
|
routes.post(
|
|
1837
1826
|
'/api-keys/:id/rotate',
|
|
1838
|
-
|
|
1839
|
-
tags: ['console-gateway'],
|
|
1827
|
+
describeConsoleGatewayRoute({
|
|
1840
1828
|
summary:
|
|
1841
1829
|
'Rotate API key on a single target instance (requires instance selection)',
|
|
1842
1830
|
responses: {
|
|
@@ -1869,8 +1857,7 @@ export function createConsoleGatewayRoutes(
|
|
|
1869
1857
|
|
|
1870
1858
|
routes.get(
|
|
1871
1859
|
'/stats',
|
|
1872
|
-
|
|
1873
|
-
tags: ['console-gateway'],
|
|
1860
|
+
describeConsoleGatewayRoute({
|
|
1874
1861
|
summary: 'Get merged sync stats across instances',
|
|
1875
1862
|
responses: {
|
|
1876
1863
|
200: {
|
|
@@ -1950,8 +1937,7 @@ export function createConsoleGatewayRoutes(
|
|
|
1950
1937
|
|
|
1951
1938
|
routes.get(
|
|
1952
1939
|
'/stats/timeseries',
|
|
1953
|
-
|
|
1954
|
-
tags: ['console-gateway'],
|
|
1940
|
+
describeConsoleGatewayRoute({
|
|
1955
1941
|
summary: 'Get merged time-series stats across instances',
|
|
1956
1942
|
responses: {
|
|
1957
1943
|
200: {
|
|
@@ -2003,8 +1989,7 @@ export function createConsoleGatewayRoutes(
|
|
|
2003
1989
|
|
|
2004
1990
|
routes.get(
|
|
2005
1991
|
'/stats/latency',
|
|
2006
|
-
|
|
2007
|
-
tags: ['console-gateway'],
|
|
1992
|
+
describeConsoleGatewayRoute({
|
|
2008
1993
|
summary: 'Get merged latency stats across instances',
|
|
2009
1994
|
responses: {
|
|
2010
1995
|
200: {
|
|
@@ -2057,8 +2042,7 @@ export function createConsoleGatewayRoutes(
|
|
|
2057
2042
|
|
|
2058
2043
|
routes.get(
|
|
2059
2044
|
'/commits',
|
|
2060
|
-
|
|
2061
|
-
tags: ['console-gateway'],
|
|
2045
|
+
describeConsoleGatewayRoute({
|
|
2062
2046
|
summary: 'List merged commits across instances',
|
|
2063
2047
|
responses: {
|
|
2064
2048
|
200: {
|
|
@@ -2137,8 +2121,7 @@ export function createConsoleGatewayRoutes(
|
|
|
2137
2121
|
|
|
2138
2122
|
routes.get(
|
|
2139
2123
|
'/commits/:seq',
|
|
2140
|
-
|
|
2141
|
-
tags: ['console-gateway'],
|
|
2124
|
+
describeConsoleGatewayRoute({
|
|
2142
2125
|
summary: 'Get merged commit detail by federated id',
|
|
2143
2126
|
responses: {
|
|
2144
2127
|
200: {
|
|
@@ -2208,8 +2191,7 @@ export function createConsoleGatewayRoutes(
|
|
|
2208
2191
|
|
|
2209
2192
|
routes.get(
|
|
2210
2193
|
'/clients',
|
|
2211
|
-
|
|
2212
|
-
tags: ['console-gateway'],
|
|
2194
|
+
describeConsoleGatewayRoute({
|
|
2213
2195
|
summary: 'List merged clients across instances',
|
|
2214
2196
|
responses: {
|
|
2215
2197
|
200: {
|
|
@@ -2285,8 +2267,7 @@ export function createConsoleGatewayRoutes(
|
|
|
2285
2267
|
|
|
2286
2268
|
routes.get(
|
|
2287
2269
|
'/timeline',
|
|
2288
|
-
|
|
2289
|
-
tags: ['console-gateway'],
|
|
2270
|
+
describeConsoleGatewayRoute({
|
|
2290
2271
|
summary: 'List merged timeline items across instances',
|
|
2291
2272
|
responses: {
|
|
2292
2273
|
200: {
|
|
@@ -2382,8 +2363,7 @@ export function createConsoleGatewayRoutes(
|
|
|
2382
2363
|
|
|
2383
2364
|
routes.get(
|
|
2384
2365
|
'/operations',
|
|
2385
|
-
|
|
2386
|
-
tags: ['console-gateway'],
|
|
2366
|
+
describeConsoleGatewayRoute({
|
|
2387
2367
|
summary: 'List merged operation events across instances',
|
|
2388
2368
|
responses: {
|
|
2389
2369
|
200: {
|
|
@@ -2463,8 +2443,7 @@ export function createConsoleGatewayRoutes(
|
|
|
2463
2443
|
|
|
2464
2444
|
routes.get(
|
|
2465
2445
|
'/events',
|
|
2466
|
-
|
|
2467
|
-
tags: ['console-gateway'],
|
|
2446
|
+
describeConsoleGatewayRoute({
|
|
2468
2447
|
summary: 'List merged request events across instances',
|
|
2469
2448
|
responses: {
|
|
2470
2449
|
200: {
|
|
@@ -2548,6 +2527,9 @@ export function createConsoleGatewayRoutes(
|
|
|
2548
2527
|
) {
|
|
2549
2528
|
const upgradeWebSocket = options.websocket.upgradeWebSocket;
|
|
2550
2529
|
const heartbeatIntervalMs = options.websocket.heartbeatIntervalMs ?? 30000;
|
|
2530
|
+
const maxMessageBytes = options.websocket.maxMessageBytes ?? 1024 * 1024;
|
|
2531
|
+
const maxMessagesPerWindow = options.websocket.maxMessagesPerWindow ?? 120;
|
|
2532
|
+
const messageRateWindowMs = options.websocket.messageRateWindowMs ?? 10000;
|
|
2551
2533
|
const createDownstreamSocket =
|
|
2552
2534
|
options.websocket.createWebSocket ??
|
|
2553
2535
|
((url: string): ConsoleGatewayDownstreamSocket => new WebSocket(url));
|
|
@@ -2565,288 +2547,320 @@ export function createConsoleGatewayRoutes(
|
|
|
2565
2547
|
authTimeout: ReturnType<typeof setTimeout> | null;
|
|
2566
2548
|
isAuthenticated: boolean;
|
|
2567
2549
|
startAuthenticatedSession: ((token: string | null) => void) | null;
|
|
2550
|
+
messageRateWindowStart: number;
|
|
2551
|
+
messageRateWindowCount: number;
|
|
2568
2552
|
}
|
|
2569
2553
|
>();
|
|
2570
2554
|
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2555
|
+
const liveEventsWebSocketRoute = upgradeWebSocket(async (c) => {
|
|
2556
|
+
const initialAuth = await options.authenticate(c);
|
|
2557
|
+
const partitionId = c.req.query('partitionId')?.trim() || undefined;
|
|
2558
|
+
const replaySince = c.req.query('since')?.trim() || undefined;
|
|
2559
|
+
const replayLimitRaw = c.req.query('replayLimit');
|
|
2560
|
+
const replayLimitNumber = replayLimitRaw
|
|
2561
|
+
? Number.parseInt(replayLimitRaw, 10)
|
|
2562
|
+
: Number.NaN;
|
|
2563
|
+
const replayLimit = Number.isFinite(replayLimitNumber)
|
|
2564
|
+
? Math.max(1, Math.min(500, replayLimitNumber))
|
|
2565
|
+
: 100;
|
|
2566
|
+
|
|
2567
|
+
const selectedInstances = selectInstances({
|
|
2568
|
+
instances,
|
|
2569
|
+
query: {
|
|
2570
|
+
instanceId: c.req.query('instanceId') ?? undefined,
|
|
2571
|
+
instanceIds: c.req.query('instanceIds') ?? undefined,
|
|
2572
|
+
},
|
|
2573
|
+
});
|
|
2574
|
+
|
|
2575
|
+
const authenticateWithBearer = async (
|
|
2576
|
+
token: string
|
|
2577
|
+
): Promise<ConsoleAuthResult | null> => {
|
|
2578
|
+
const trimmedToken = token.trim();
|
|
2579
|
+
if (!trimmedToken) {
|
|
2580
|
+
return null;
|
|
2581
|
+
}
|
|
2582
|
+
const authContext = {
|
|
2583
|
+
req: {
|
|
2584
|
+
header: (name: string) =>
|
|
2585
|
+
name === 'Authorization' ? `Bearer ${trimmedToken}` : undefined,
|
|
2586
|
+
query: () => undefined,
|
|
2590
2587
|
},
|
|
2591
|
-
}
|
|
2588
|
+
} as unknown as Context;
|
|
2589
|
+
return options.authenticate(authContext);
|
|
2590
|
+
};
|
|
2592
2591
|
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
)
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2592
|
+
const cleanup = (ws: WebSocketLike) => {
|
|
2593
|
+
const state = liveState.get(ws);
|
|
2594
|
+
if (!state) return;
|
|
2595
|
+
if (state.heartbeatInterval) {
|
|
2596
|
+
clearInterval(state.heartbeatInterval);
|
|
2597
|
+
}
|
|
2598
|
+
if (state.authTimeout) {
|
|
2599
|
+
clearTimeout(state.authTimeout);
|
|
2600
|
+
}
|
|
2601
|
+
for (const downstream of state.downstreamSockets) {
|
|
2602
|
+
try {
|
|
2603
|
+
downstream.close();
|
|
2604
|
+
} catch {
|
|
2605
|
+
// no-op
|
|
2599
2606
|
}
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
name === 'Authorization' ? `Bearer ${trimmedToken}` : undefined,
|
|
2604
|
-
query: () => undefined,
|
|
2605
|
-
},
|
|
2606
|
-
} as unknown as Context;
|
|
2607
|
-
return options.authenticate(authContext);
|
|
2608
|
-
};
|
|
2607
|
+
}
|
|
2608
|
+
liveState.delete(ws);
|
|
2609
|
+
};
|
|
2609
2610
|
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
if (
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
} catch {
|
|
2623
|
-
// no-op
|
|
2624
|
-
}
|
|
2611
|
+
return {
|
|
2612
|
+
onOpen(_event, ws) {
|
|
2613
|
+
if (selectedInstances.length === 0) {
|
|
2614
|
+
ws.send(
|
|
2615
|
+
JSON.stringify({
|
|
2616
|
+
type: 'error',
|
|
2617
|
+
message:
|
|
2618
|
+
'No enabled instances matched the provided instance filter.',
|
|
2619
|
+
})
|
|
2620
|
+
);
|
|
2621
|
+
ws.close(4004, 'No instances selected');
|
|
2622
|
+
return;
|
|
2625
2623
|
}
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2624
|
+
|
|
2625
|
+
const state: {
|
|
2626
|
+
downstreamSockets: ConsoleGatewayDownstreamSocket[];
|
|
2627
|
+
heartbeatInterval: ReturnType<typeof setInterval> | null;
|
|
2628
|
+
authTimeout: ReturnType<typeof setTimeout> | null;
|
|
2629
|
+
isAuthenticated: boolean;
|
|
2630
|
+
startAuthenticatedSession: ((token: string | null) => void) | null;
|
|
2631
|
+
messageRateWindowStart: number;
|
|
2632
|
+
messageRateWindowCount: number;
|
|
2633
|
+
} = {
|
|
2634
|
+
downstreamSockets: [],
|
|
2635
|
+
heartbeatInterval: null,
|
|
2636
|
+
authTimeout: null,
|
|
2637
|
+
isAuthenticated: false,
|
|
2638
|
+
startAuthenticatedSession: null,
|
|
2639
|
+
messageRateWindowStart: Date.now(),
|
|
2640
|
+
messageRateWindowCount: 0,
|
|
2641
|
+
};
|
|
2642
|
+
liveState.set(ws, state);
|
|
2643
|
+
|
|
2644
|
+
const startAuthenticatedSession = (
|
|
2645
|
+
upstreamBearerToken: string | null
|
|
2646
|
+
) => {
|
|
2647
|
+
if (state.isAuthenticated) {
|
|
2640
2648
|
return;
|
|
2641
2649
|
}
|
|
2650
|
+
state.isAuthenticated = true;
|
|
2651
|
+
if (state.authTimeout) {
|
|
2652
|
+
clearTimeout(state.authTimeout);
|
|
2653
|
+
state.authTimeout = null;
|
|
2654
|
+
}
|
|
2642
2655
|
|
|
2643
|
-
const
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
isAuthenticated: boolean;
|
|
2648
|
-
startAuthenticatedSession:
|
|
2649
|
-
| ((token: string | null) => void)
|
|
2650
|
-
| null;
|
|
2651
|
-
} = {
|
|
2652
|
-
downstreamSockets: [],
|
|
2653
|
-
heartbeatInterval: null,
|
|
2654
|
-
authTimeout: null,
|
|
2655
|
-
isAuthenticated: false,
|
|
2656
|
-
startAuthenticatedSession: null,
|
|
2657
|
-
};
|
|
2658
|
-
liveState.set(ws, state);
|
|
2659
|
-
|
|
2660
|
-
const startAuthenticatedSession = (
|
|
2661
|
-
upstreamBearerToken: string | null
|
|
2662
|
-
) => {
|
|
2663
|
-
if (state.isAuthenticated) {
|
|
2664
|
-
return;
|
|
2656
|
+
for (const instance of selectedInstances) {
|
|
2657
|
+
const downstreamQuery = new URLSearchParams();
|
|
2658
|
+
if (partitionId) {
|
|
2659
|
+
downstreamQuery.set('partitionId', partitionId);
|
|
2665
2660
|
}
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
clearTimeout(state.authTimeout);
|
|
2669
|
-
state.authTimeout = null;
|
|
2661
|
+
if (replaySince) {
|
|
2662
|
+
downstreamQuery.set('since', replaySince);
|
|
2670
2663
|
}
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
path: '/events/live',
|
|
2686
|
-
query: downstreamQuery,
|
|
2687
|
-
});
|
|
2688
|
-
|
|
2689
|
-
const downstreamSocket = createDownstreamSocket(downstreamUrl);
|
|
2690
|
-
const downstreamToken =
|
|
2691
|
-
instance.token?.trim() ?? upstreamBearerToken?.trim() ?? null;
|
|
2692
|
-
if (downstreamToken && downstreamSocket.send) {
|
|
2693
|
-
downstreamSocket.onopen = () => {
|
|
2694
|
-
try {
|
|
2695
|
-
downstreamSocket.send?.(
|
|
2696
|
-
JSON.stringify({
|
|
2697
|
-
type: 'auth',
|
|
2698
|
-
token: downstreamToken,
|
|
2699
|
-
})
|
|
2700
|
-
);
|
|
2701
|
-
} catch {
|
|
2702
|
-
// no-op
|
|
2703
|
-
}
|
|
2704
|
-
};
|
|
2705
|
-
}
|
|
2706
|
-
|
|
2707
|
-
downstreamSocket.onmessage = (message: MessageEvent) => {
|
|
2708
|
-
if (typeof message.data !== 'string') {
|
|
2709
|
-
return;
|
|
2710
|
-
}
|
|
2664
|
+
downstreamQuery.set('replayLimit', String(replayLimit));
|
|
2665
|
+
|
|
2666
|
+
const downstreamUrl = buildConsoleEndpointUrl({
|
|
2667
|
+
instance,
|
|
2668
|
+
requestUrl: c.req.url,
|
|
2669
|
+
path: '/events/live',
|
|
2670
|
+
query: downstreamQuery,
|
|
2671
|
+
});
|
|
2672
|
+
|
|
2673
|
+
const downstreamSocket = createDownstreamSocket(downstreamUrl);
|
|
2674
|
+
const downstreamToken =
|
|
2675
|
+
instance.token?.trim() ?? upstreamBearerToken?.trim() ?? null;
|
|
2676
|
+
if (downstreamToken && downstreamSocket.send) {
|
|
2677
|
+
downstreamSocket.onopen = () => {
|
|
2711
2678
|
try {
|
|
2712
|
-
|
|
2713
|
-
string,
|
|
2714
|
-
unknown
|
|
2715
|
-
>;
|
|
2716
|
-
if (
|
|
2717
|
-
typeof payload.type === 'string' &&
|
|
2718
|
-
(payload.type === 'connected' ||
|
|
2719
|
-
payload.type === 'heartbeat')
|
|
2720
|
-
) {
|
|
2721
|
-
return;
|
|
2722
|
-
}
|
|
2723
|
-
|
|
2724
|
-
const payloadData =
|
|
2725
|
-
payload.data &&
|
|
2726
|
-
typeof payload.data === 'object' &&
|
|
2727
|
-
!Array.isArray(payload.data)
|
|
2728
|
-
? { ...payload.data, instanceId: instance.instanceId }
|
|
2729
|
-
: { instanceId: instance.instanceId };
|
|
2730
|
-
|
|
2731
|
-
const event = {
|
|
2732
|
-
...payload,
|
|
2733
|
-
data: payloadData,
|
|
2734
|
-
instanceId: instance.instanceId,
|
|
2735
|
-
timestamp:
|
|
2736
|
-
typeof payload.timestamp === 'string'
|
|
2737
|
-
? payload.timestamp
|
|
2738
|
-
: new Date().toISOString(),
|
|
2739
|
-
};
|
|
2740
|
-
ws.send(JSON.stringify(event));
|
|
2741
|
-
} catch {
|
|
2742
|
-
// Ignore malformed downstream events
|
|
2743
|
-
}
|
|
2744
|
-
};
|
|
2745
|
-
|
|
2746
|
-
downstreamSocket.onerror = () => {
|
|
2747
|
-
try {
|
|
2748
|
-
ws.send(
|
|
2679
|
+
downstreamSocket.send?.(
|
|
2749
2680
|
JSON.stringify({
|
|
2750
|
-
type: '
|
|
2751
|
-
|
|
2752
|
-
timestamp: new Date().toISOString(),
|
|
2681
|
+
type: 'auth',
|
|
2682
|
+
token: downstreamToken,
|
|
2753
2683
|
})
|
|
2754
2684
|
);
|
|
2755
2685
|
} catch {
|
|
2756
|
-
//
|
|
2686
|
+
// no-op
|
|
2757
2687
|
}
|
|
2758
2688
|
};
|
|
2759
|
-
|
|
2760
|
-
state.downstreamSockets.push(downstreamSocket);
|
|
2761
2689
|
}
|
|
2762
2690
|
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2691
|
+
downstreamSocket.onmessage = (message: MessageEvent) => {
|
|
2692
|
+
if (typeof message.data !== 'string') {
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2695
|
+
try {
|
|
2696
|
+
const payload = JSON.parse(message.data) as Record<
|
|
2697
|
+
string,
|
|
2698
|
+
unknown
|
|
2699
|
+
>;
|
|
2700
|
+
if (
|
|
2701
|
+
typeof payload.type === 'string' &&
|
|
2702
|
+
(payload.type === 'connected' ||
|
|
2703
|
+
payload.type === 'heartbeat')
|
|
2704
|
+
) {
|
|
2705
|
+
return;
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
const payloadData =
|
|
2709
|
+
payload.data &&
|
|
2710
|
+
typeof payload.data === 'object' &&
|
|
2711
|
+
!Array.isArray(payload.data)
|
|
2712
|
+
? { ...payload.data, instanceId: instance.instanceId }
|
|
2713
|
+
: { instanceId: instance.instanceId };
|
|
2714
|
+
|
|
2715
|
+
const event = {
|
|
2716
|
+
...payload,
|
|
2717
|
+
data: payloadData,
|
|
2718
|
+
instanceId: instance.instanceId,
|
|
2719
|
+
timestamp:
|
|
2720
|
+
typeof payload.timestamp === 'string'
|
|
2721
|
+
? payload.timestamp
|
|
2722
|
+
: new Date().toISOString(),
|
|
2723
|
+
};
|
|
2724
|
+
ws.send(JSON.stringify(event));
|
|
2725
|
+
} catch {
|
|
2726
|
+
// Ignore malformed downstream events
|
|
2727
|
+
}
|
|
2728
|
+
};
|
|
2770
2729
|
|
|
2771
|
-
|
|
2730
|
+
downstreamSocket.onerror = () => {
|
|
2772
2731
|
try {
|
|
2773
2732
|
ws.send(
|
|
2774
2733
|
JSON.stringify({
|
|
2775
|
-
type: '
|
|
2734
|
+
type: 'instance_error',
|
|
2735
|
+
instanceId: instance.instanceId,
|
|
2776
2736
|
timestamp: new Date().toISOString(),
|
|
2777
2737
|
})
|
|
2778
2738
|
);
|
|
2779
2739
|
} catch {
|
|
2780
|
-
|
|
2740
|
+
// ignore send errors
|
|
2781
2741
|
}
|
|
2782
|
-
}
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
state.startAuthenticatedSession = startAuthenticatedSession;
|
|
2786
|
-
|
|
2787
|
-
if (initialAuth) {
|
|
2788
|
-
startAuthenticatedSession(
|
|
2789
|
-
parseBearerToken(c.req.header('Authorization'))
|
|
2790
|
-
);
|
|
2791
|
-
return;
|
|
2742
|
+
};
|
|
2743
|
+
|
|
2744
|
+
state.downstreamSockets.push(downstreamSocket);
|
|
2792
2745
|
}
|
|
2793
2746
|
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2747
|
+
ws.send(
|
|
2748
|
+
JSON.stringify({
|
|
2749
|
+
type: 'connected',
|
|
2750
|
+
timestamp: new Date().toISOString(),
|
|
2751
|
+
instanceCount: selectedInstances.length,
|
|
2752
|
+
})
|
|
2753
|
+
);
|
|
2754
|
+
|
|
2755
|
+
const heartbeatInterval = setInterval(() => {
|
|
2756
|
+
try {
|
|
2757
|
+
ws.send(
|
|
2758
|
+
JSON.stringify({
|
|
2759
|
+
type: 'heartbeat',
|
|
2760
|
+
timestamp: new Date().toISOString(),
|
|
2761
|
+
})
|
|
2762
|
+
);
|
|
2763
|
+
} catch {
|
|
2764
|
+
clearInterval(heartbeatInterval);
|
|
2798
2765
|
}
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2766
|
+
}, heartbeatIntervalMs);
|
|
2767
|
+
state.heartbeatInterval = heartbeatInterval;
|
|
2768
|
+
};
|
|
2769
|
+
state.startAuthenticatedSession = startAuthenticatedSession;
|
|
2770
|
+
|
|
2771
|
+
if (initialAuth) {
|
|
2772
|
+
startAuthenticatedSession(
|
|
2773
|
+
parseBearerToken(c.req.header('Authorization'))
|
|
2774
|
+
);
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2808
2777
|
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2778
|
+
state.authTimeout = setTimeout(() => {
|
|
2779
|
+
const current = liveState.get(ws);
|
|
2780
|
+
if (!current || current.isAuthenticated) {
|
|
2812
2781
|
return;
|
|
2813
2782
|
}
|
|
2783
|
+
closeUnauthenticatedSocket(ws);
|
|
2784
|
+
cleanup(ws);
|
|
2785
|
+
}, 5_000);
|
|
2786
|
+
},
|
|
2787
|
+
async onMessage(event, ws) {
|
|
2788
|
+
const state = liveState.get(ws);
|
|
2789
|
+
if (!state) {
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2814
2792
|
|
|
2815
|
-
|
|
2793
|
+
const messageBytes = measureWebSocketMessageBytes(event.data);
|
|
2794
|
+
if (messageBytes > maxMessageBytes) {
|
|
2795
|
+
ws.close(1009, 'message too large');
|
|
2796
|
+
cleanup(ws);
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2816
2799
|
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2800
|
+
if (maxMessagesPerWindow > 0 && messageRateWindowMs > 0) {
|
|
2801
|
+
const nowMs = Date.now();
|
|
2802
|
+
if (nowMs - state.messageRateWindowStart >= messageRateWindowMs) {
|
|
2803
|
+
state.messageRateWindowStart = nowMs;
|
|
2804
|
+
state.messageRateWindowCount = 0;
|
|
2821
2805
|
}
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
if (!current || current.isAuthenticated) {
|
|
2826
|
-
return;
|
|
2827
|
-
}
|
|
2828
|
-
if (!auth) {
|
|
2829
|
-
closeUnauthenticatedSocket(ws);
|
|
2806
|
+
state.messageRateWindowCount += 1;
|
|
2807
|
+
if (state.messageRateWindowCount > maxMessagesPerWindow) {
|
|
2808
|
+
ws.close(1008, 'message rate exceeded');
|
|
2830
2809
|
cleanup(ws);
|
|
2831
2810
|
return;
|
|
2832
2811
|
}
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
if (state.isAuthenticated) {
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
if (typeof event.data !== 'string') {
|
|
2819
|
+
closeUnauthenticatedSocket(ws);
|
|
2836
2820
|
cleanup(ws);
|
|
2837
|
-
|
|
2838
|
-
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
const token = parseWebSocketAuthToken(event.data);
|
|
2825
|
+
|
|
2826
|
+
if (!token) {
|
|
2827
|
+
closeUnauthenticatedSocket(ws);
|
|
2839
2828
|
cleanup(ws);
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
const auth = await authenticateWithBearer(token);
|
|
2833
|
+
const current = liveState.get(ws);
|
|
2834
|
+
if (!current || current.isAuthenticated) {
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
if (!auth) {
|
|
2838
|
+
closeUnauthenticatedSocket(ws);
|
|
2839
|
+
cleanup(ws);
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
current.startAuthenticatedSession?.(token);
|
|
2843
|
+
},
|
|
2844
|
+
onClose(_event, ws) {
|
|
2845
|
+
cleanup(ws);
|
|
2846
|
+
},
|
|
2847
|
+
onError(_event, ws) {
|
|
2848
|
+
cleanup(ws);
|
|
2849
|
+
},
|
|
2850
|
+
};
|
|
2851
|
+
});
|
|
2852
|
+
|
|
2853
|
+
routes.get('/events/live', async (c, next) => {
|
|
2854
|
+
if (!isWebSocketOriginAllowed(c, options.websocket?.allowedOrigins)) {
|
|
2855
|
+
return c.json({ error: 'FORBIDDEN_ORIGIN' }, 403);
|
|
2856
|
+
}
|
|
2857
|
+
return liveEventsWebSocketRoute(c, next);
|
|
2858
|
+
});
|
|
2844
2859
|
}
|
|
2845
2860
|
|
|
2846
2861
|
routes.get(
|
|
2847
2862
|
'/events/:id',
|
|
2848
|
-
|
|
2849
|
-
tags: ['console-gateway'],
|
|
2863
|
+
describeConsoleGatewayRoute({
|
|
2850
2864
|
summary: 'Get merged event detail by federated id',
|
|
2851
2865
|
responses: {
|
|
2852
2866
|
200: {
|
|
@@ -2920,8 +2934,7 @@ export function createConsoleGatewayRoutes(
|
|
|
2920
2934
|
|
|
2921
2935
|
routes.get(
|
|
2922
2936
|
'/events/:id/payload',
|
|
2923
|
-
|
|
2924
|
-
tags: ['console-gateway'],
|
|
2937
|
+
describeConsoleGatewayRoute({
|
|
2925
2938
|
summary: 'Get merged event payload by federated id',
|
|
2926
2939
|
responses: {
|
|
2927
2940
|
200: {
|
|
@@ -2995,3 +3008,37 @@ export function createConsoleGatewayRoutes(
|
|
|
2995
3008
|
|
|
2996
3009
|
return routes;
|
|
2997
3010
|
}
|
|
3011
|
+
|
|
3012
|
+
function isWebSocketOriginAllowed(
|
|
3013
|
+
c: Context,
|
|
3014
|
+
allowedOrigins?: string[] | '*'
|
|
3015
|
+
): boolean {
|
|
3016
|
+
if (!allowedOrigins) return true;
|
|
3017
|
+
if (allowedOrigins === '*') return true;
|
|
3018
|
+
|
|
3019
|
+
const origin = c.req.header('origin');
|
|
3020
|
+
if (!origin) return false;
|
|
3021
|
+
|
|
3022
|
+
try {
|
|
3023
|
+
const normalizedOrigin = new URL(origin).origin;
|
|
3024
|
+
return allowedOrigins.includes(normalizedOrigin);
|
|
3025
|
+
} catch {
|
|
3026
|
+
return false;
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
function measureWebSocketMessageBytes(data: unknown): number {
|
|
3031
|
+
if (typeof data === 'string') {
|
|
3032
|
+
return new TextEncoder().encode(data).byteLength;
|
|
3033
|
+
}
|
|
3034
|
+
if (data instanceof ArrayBuffer) {
|
|
3035
|
+
return data.byteLength;
|
|
3036
|
+
}
|
|
3037
|
+
if (ArrayBuffer.isView(data)) {
|
|
3038
|
+
return data.byteLength;
|
|
3039
|
+
}
|
|
3040
|
+
if (typeof Blob !== 'undefined' && data instanceof Blob) {
|
|
3041
|
+
return data.size;
|
|
3042
|
+
}
|
|
3043
|
+
return new TextEncoder().encode(String(data)).byteLength;
|
|
3044
|
+
}
|