@syncular/server-hono 0.0.6-85 → 0.0.6-86
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 +2 -0
- package/dist/console/gateway.d.ts.map +1 -1
- package/dist/console/gateway.js +218 -41
- package/dist/console/gateway.js.map +1 -1
- package/dist/console/routes.d.ts.map +1 -1
- package/dist/console/routes.js +165 -37
- package/dist/console/routes.js.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/console-gateway-live-routes.test.ts +54 -3
- package/src/console/gateway.ts +276 -52
- package/src/console/routes.ts +193 -41
|
@@ -10,9 +10,11 @@ export interface ConsoleGatewayInstance {
|
|
|
10
10
|
enabled?: boolean;
|
|
11
11
|
}
|
|
12
12
|
interface ConsoleGatewayDownstreamSocket {
|
|
13
|
+
onopen?: ((event: Event) => void) | null;
|
|
13
14
|
onmessage: ((event: MessageEvent) => void) | null;
|
|
14
15
|
onerror: ((event: Event) => void) | null;
|
|
15
16
|
close: () => void;
|
|
17
|
+
send?: (data: string) => void;
|
|
16
18
|
}
|
|
17
19
|
export interface CreateConsoleGatewayRoutesOptions {
|
|
18
20
|
instances: ConsoleGatewayInstance[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../src/console/gateway.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AA2DhD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,UAAU,8BAA8B;IACtC,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IAClD,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACzC,KAAK,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../src/console/gateway.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AA2DhD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,UAAU,8BAA8B;IACtC,MAAM,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACzC,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IAClD,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACzC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,iCAAiC;IAChD,SAAS,EAAE,sBAAsB,EAAE,CAAC;IACpC,YAAY,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IAChE,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC;IAC7B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,SAAS,CAAC,EAAE;QACV,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;QACpC,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,8BAA8B,CAAC;KACnE,CAAC;CACH;AAo8BD,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,iCAAiC,GACzC,IAAI,CAi/EN"}
|
package/dist/console/gateway.js
CHANGED
|
@@ -471,28 +471,15 @@ function resolveForwardAuthorization(args) {
|
|
|
471
471
|
if (header) {
|
|
472
472
|
return header;
|
|
473
473
|
}
|
|
474
|
-
const queryToken = args.c.req.query('token')?.trim();
|
|
475
|
-
if (queryToken) {
|
|
476
|
-
return `Bearer ${queryToken}`;
|
|
477
|
-
}
|
|
478
474
|
return null;
|
|
479
475
|
}
|
|
480
|
-
function
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const authHeader = args.c.req.header('Authorization')?.trim();
|
|
485
|
-
if (authHeader?.startsWith('Bearer ')) {
|
|
486
|
-
const token = authHeader.slice(7).trim();
|
|
487
|
-
if (token.length > 0) {
|
|
488
|
-
return token;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
const queryToken = args.c.req.query('token')?.trim();
|
|
492
|
-
if (queryToken) {
|
|
493
|
-
return queryToken;
|
|
476
|
+
function parseBearerToken(authHeader) {
|
|
477
|
+
const value = authHeader?.trim();
|
|
478
|
+
if (!value?.startsWith('Bearer ')) {
|
|
479
|
+
return null;
|
|
494
480
|
}
|
|
495
|
-
|
|
481
|
+
const token = value.slice(7).trim();
|
|
482
|
+
return token.length > 0 ? token : null;
|
|
496
483
|
}
|
|
497
484
|
async function fetchDownstreamJson(args) {
|
|
498
485
|
const url = buildConsoleEndpointUrl({
|
|
@@ -2095,7 +2082,7 @@ export function createConsoleGatewayRoutes(options) {
|
|
|
2095
2082
|
((url) => new WebSocket(url));
|
|
2096
2083
|
const liveState = new WeakMap();
|
|
2097
2084
|
routes.get('/events/live', upgradeWebSocket(async (c) => {
|
|
2098
|
-
const
|
|
2085
|
+
const initialAuth = await options.authenticate(c);
|
|
2099
2086
|
const partitionId = c.req.query('partitionId')?.trim() || undefined;
|
|
2100
2087
|
const replaySince = c.req.query('since')?.trim() || undefined;
|
|
2101
2088
|
const replayLimitRaw = c.req.query('replayLimit');
|
|
@@ -2112,11 +2099,38 @@ export function createConsoleGatewayRoutes(options) {
|
|
|
2112
2099
|
instanceIds: c.req.query('instanceIds') ?? undefined,
|
|
2113
2100
|
},
|
|
2114
2101
|
});
|
|
2102
|
+
const authenticateWithBearer = async (token) => {
|
|
2103
|
+
const trimmedToken = token.trim();
|
|
2104
|
+
if (!trimmedToken) {
|
|
2105
|
+
return null;
|
|
2106
|
+
}
|
|
2107
|
+
const authContext = {
|
|
2108
|
+
req: {
|
|
2109
|
+
header: (name) => name === 'Authorization' ? `Bearer ${trimmedToken}` : undefined,
|
|
2110
|
+
query: () => undefined,
|
|
2111
|
+
},
|
|
2112
|
+
};
|
|
2113
|
+
return options.authenticate(authContext);
|
|
2114
|
+
};
|
|
2115
|
+
const closeUnauthenticated = (ws) => {
|
|
2116
|
+
try {
|
|
2117
|
+
ws.send(JSON.stringify({ type: 'error', message: 'UNAUTHENTICATED' }));
|
|
2118
|
+
}
|
|
2119
|
+
catch {
|
|
2120
|
+
// no-op
|
|
2121
|
+
}
|
|
2122
|
+
ws.close(4001, 'Unauthenticated');
|
|
2123
|
+
};
|
|
2115
2124
|
const cleanup = (ws) => {
|
|
2116
2125
|
const state = liveState.get(ws);
|
|
2117
2126
|
if (!state)
|
|
2118
2127
|
return;
|
|
2119
|
-
|
|
2128
|
+
if (state.heartbeatInterval) {
|
|
2129
|
+
clearInterval(state.heartbeatInterval);
|
|
2130
|
+
}
|
|
2131
|
+
if (state.authTimeout) {
|
|
2132
|
+
clearTimeout(state.authTimeout);
|
|
2133
|
+
}
|
|
2120
2134
|
for (const downstream of state.downstreamSockets) {
|
|
2121
2135
|
try {
|
|
2122
2136
|
downstream.close();
|
|
@@ -2129,11 +2143,6 @@ export function createConsoleGatewayRoutes(options) {
|
|
|
2129
2143
|
};
|
|
2130
2144
|
return {
|
|
2131
2145
|
onOpen(_event, ws) {
|
|
2132
|
-
if (!auth) {
|
|
2133
|
-
ws.send(JSON.stringify({ type: 'error', message: 'UNAUTHENTICATED' }));
|
|
2134
|
-
ws.close(4001, 'Unauthenticated');
|
|
2135
|
-
return;
|
|
2136
|
-
}
|
|
2137
2146
|
if (selectedInstances.length === 0) {
|
|
2138
2147
|
ws.send(JSON.stringify({
|
|
2139
2148
|
type: 'error',
|
|
@@ -2142,16 +2151,171 @@ export function createConsoleGatewayRoutes(options) {
|
|
|
2142
2151
|
ws.close(4004, 'No instances selected');
|
|
2143
2152
|
return;
|
|
2144
2153
|
}
|
|
2145
|
-
const
|
|
2154
|
+
const state = {
|
|
2155
|
+
downstreamSockets: [],
|
|
2156
|
+
heartbeatInterval: null,
|
|
2157
|
+
authTimeout: null,
|
|
2158
|
+
isAuthenticated: false,
|
|
2159
|
+
};
|
|
2160
|
+
liveState.set(ws, state);
|
|
2161
|
+
const startAuthenticatedSession = (upstreamBearerToken) => {
|
|
2162
|
+
if (state.isAuthenticated) {
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
state.isAuthenticated = true;
|
|
2166
|
+
if (state.authTimeout) {
|
|
2167
|
+
clearTimeout(state.authTimeout);
|
|
2168
|
+
state.authTimeout = null;
|
|
2169
|
+
}
|
|
2170
|
+
for (const instance of selectedInstances) {
|
|
2171
|
+
const downstreamQuery = new URLSearchParams();
|
|
2172
|
+
if (partitionId) {
|
|
2173
|
+
downstreamQuery.set('partitionId', partitionId);
|
|
2174
|
+
}
|
|
2175
|
+
if (replaySince) {
|
|
2176
|
+
downstreamQuery.set('since', replaySince);
|
|
2177
|
+
}
|
|
2178
|
+
downstreamQuery.set('replayLimit', String(replayLimit));
|
|
2179
|
+
const downstreamUrl = buildConsoleEndpointUrl({
|
|
2180
|
+
instance,
|
|
2181
|
+
requestUrl: c.req.url,
|
|
2182
|
+
path: '/events/live',
|
|
2183
|
+
query: downstreamQuery,
|
|
2184
|
+
});
|
|
2185
|
+
const downstreamSocket = createDownstreamSocket(downstreamUrl);
|
|
2186
|
+
const downstreamToken = instance.token?.trim() ?? upstreamBearerToken?.trim() ?? null;
|
|
2187
|
+
if (downstreamToken && downstreamSocket.send) {
|
|
2188
|
+
downstreamSocket.onopen = () => {
|
|
2189
|
+
try {
|
|
2190
|
+
downstreamSocket.send?.(JSON.stringify({
|
|
2191
|
+
type: 'auth',
|
|
2192
|
+
token: downstreamToken,
|
|
2193
|
+
}));
|
|
2194
|
+
}
|
|
2195
|
+
catch {
|
|
2196
|
+
// no-op
|
|
2197
|
+
}
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
downstreamSocket.onmessage = (message) => {
|
|
2201
|
+
if (typeof message.data !== 'string') {
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
try {
|
|
2205
|
+
const payload = JSON.parse(message.data);
|
|
2206
|
+
if (typeof payload.type === 'string' &&
|
|
2207
|
+
(payload.type === 'connected' ||
|
|
2208
|
+
payload.type === 'heartbeat')) {
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
const payloadData = payload.data &&
|
|
2212
|
+
typeof payload.data === 'object' &&
|
|
2213
|
+
!Array.isArray(payload.data)
|
|
2214
|
+
? { ...payload.data, instanceId: instance.instanceId }
|
|
2215
|
+
: { instanceId: instance.instanceId };
|
|
2216
|
+
const event = {
|
|
2217
|
+
...payload,
|
|
2218
|
+
data: payloadData,
|
|
2219
|
+
instanceId: instance.instanceId,
|
|
2220
|
+
timestamp: typeof payload.timestamp === 'string'
|
|
2221
|
+
? payload.timestamp
|
|
2222
|
+
: new Date().toISOString(),
|
|
2223
|
+
};
|
|
2224
|
+
ws.send(JSON.stringify(event));
|
|
2225
|
+
}
|
|
2226
|
+
catch {
|
|
2227
|
+
// Ignore malformed downstream events
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
downstreamSocket.onerror = () => {
|
|
2231
|
+
try {
|
|
2232
|
+
ws.send(JSON.stringify({
|
|
2233
|
+
type: 'instance_error',
|
|
2234
|
+
instanceId: instance.instanceId,
|
|
2235
|
+
timestamp: new Date().toISOString(),
|
|
2236
|
+
}));
|
|
2237
|
+
}
|
|
2238
|
+
catch {
|
|
2239
|
+
// ignore send errors
|
|
2240
|
+
}
|
|
2241
|
+
};
|
|
2242
|
+
state.downstreamSockets.push(downstreamSocket);
|
|
2243
|
+
}
|
|
2244
|
+
ws.send(JSON.stringify({
|
|
2245
|
+
type: 'connected',
|
|
2246
|
+
timestamp: new Date().toISOString(),
|
|
2247
|
+
instanceCount: selectedInstances.length,
|
|
2248
|
+
}));
|
|
2249
|
+
const heartbeatInterval = setInterval(() => {
|
|
2250
|
+
try {
|
|
2251
|
+
ws.send(JSON.stringify({
|
|
2252
|
+
type: 'heartbeat',
|
|
2253
|
+
timestamp: new Date().toISOString(),
|
|
2254
|
+
}));
|
|
2255
|
+
}
|
|
2256
|
+
catch {
|
|
2257
|
+
clearInterval(heartbeatInterval);
|
|
2258
|
+
}
|
|
2259
|
+
}, heartbeatIntervalMs);
|
|
2260
|
+
state.heartbeatInterval = heartbeatInterval;
|
|
2261
|
+
};
|
|
2262
|
+
if (initialAuth) {
|
|
2263
|
+
startAuthenticatedSession(parseBearerToken(c.req.header('Authorization')));
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
state.authTimeout = setTimeout(() => {
|
|
2267
|
+
const current = liveState.get(ws);
|
|
2268
|
+
if (!current || current.isAuthenticated) {
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
closeUnauthenticated(ws);
|
|
2272
|
+
cleanup(ws);
|
|
2273
|
+
}, 5_000);
|
|
2274
|
+
},
|
|
2275
|
+
async onMessage(event, ws) {
|
|
2276
|
+
const state = liveState.get(ws);
|
|
2277
|
+
if (!state || state.isAuthenticated) {
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
if (typeof event.data !== 'string') {
|
|
2281
|
+
closeUnauthenticated(ws);
|
|
2282
|
+
cleanup(ws);
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
let token = '';
|
|
2286
|
+
try {
|
|
2287
|
+
const parsed = JSON.parse(event.data);
|
|
2288
|
+
if (parsed.type === 'auth' &&
|
|
2289
|
+
typeof parsed.token === 'string' &&
|
|
2290
|
+
parsed.token.trim().length > 0) {
|
|
2291
|
+
token = parsed.token;
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
catch {
|
|
2295
|
+
// Invalid auth message will be handled below.
|
|
2296
|
+
}
|
|
2297
|
+
if (!token) {
|
|
2298
|
+
closeUnauthenticated(ws);
|
|
2299
|
+
cleanup(ws);
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
const auth = await authenticateWithBearer(token);
|
|
2303
|
+
const current = liveState.get(ws);
|
|
2304
|
+
if (!current || current.isAuthenticated) {
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
if (!auth) {
|
|
2308
|
+
closeUnauthenticated(ws);
|
|
2309
|
+
cleanup(ws);
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
current.isAuthenticated = true;
|
|
2313
|
+
if (current.authTimeout) {
|
|
2314
|
+
clearTimeout(current.authTimeout);
|
|
2315
|
+
current.authTimeout = null;
|
|
2316
|
+
}
|
|
2146
2317
|
for (const instance of selectedInstances) {
|
|
2147
2318
|
const downstreamQuery = new URLSearchParams();
|
|
2148
|
-
const downstreamToken = resolveForwardBearerToken({
|
|
2149
|
-
c,
|
|
2150
|
-
instance,
|
|
2151
|
-
});
|
|
2152
|
-
if (downstreamToken) {
|
|
2153
|
-
downstreamQuery.set('token', downstreamToken);
|
|
2154
|
-
}
|
|
2155
2319
|
if (partitionId) {
|
|
2156
2320
|
downstreamQuery.set('partitionId', partitionId);
|
|
2157
2321
|
}
|
|
@@ -2166,6 +2330,22 @@ export function createConsoleGatewayRoutes(options) {
|
|
|
2166
2330
|
query: downstreamQuery,
|
|
2167
2331
|
});
|
|
2168
2332
|
const downstreamSocket = createDownstreamSocket(downstreamUrl);
|
|
2333
|
+
const upstreamToken = token.trim();
|
|
2334
|
+
const downstreamToken = instance.token?.trim() ||
|
|
2335
|
+
(upstreamToken.length > 0 ? upstreamToken : null);
|
|
2336
|
+
if (downstreamToken && downstreamSocket.send) {
|
|
2337
|
+
downstreamSocket.onopen = () => {
|
|
2338
|
+
try {
|
|
2339
|
+
downstreamSocket.send?.(JSON.stringify({
|
|
2340
|
+
type: 'auth',
|
|
2341
|
+
token: downstreamToken,
|
|
2342
|
+
}));
|
|
2343
|
+
}
|
|
2344
|
+
catch {
|
|
2345
|
+
// no-op
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2169
2349
|
downstreamSocket.onmessage = (message) => {
|
|
2170
2350
|
if (typeof message.data !== 'string') {
|
|
2171
2351
|
return;
|
|
@@ -2182,7 +2362,7 @@ export function createConsoleGatewayRoutes(options) {
|
|
|
2182
2362
|
!Array.isArray(payload.data)
|
|
2183
2363
|
? { ...payload.data, instanceId: instance.instanceId }
|
|
2184
2364
|
: { instanceId: instance.instanceId };
|
|
2185
|
-
const
|
|
2365
|
+
const liveEvent = {
|
|
2186
2366
|
...payload,
|
|
2187
2367
|
data: payloadData,
|
|
2188
2368
|
instanceId: instance.instanceId,
|
|
@@ -2190,7 +2370,7 @@ export function createConsoleGatewayRoutes(options) {
|
|
|
2190
2370
|
? payload.timestamp
|
|
2191
2371
|
: new Date().toISOString(),
|
|
2192
2372
|
};
|
|
2193
|
-
ws.send(JSON.stringify(
|
|
2373
|
+
ws.send(JSON.stringify(liveEvent));
|
|
2194
2374
|
}
|
|
2195
2375
|
catch {
|
|
2196
2376
|
// Ignore malformed downstream events
|
|
@@ -2208,7 +2388,7 @@ export function createConsoleGatewayRoutes(options) {
|
|
|
2208
2388
|
// ignore send errors
|
|
2209
2389
|
}
|
|
2210
2390
|
};
|
|
2211
|
-
downstreamSockets.push(downstreamSocket);
|
|
2391
|
+
current.downstreamSockets.push(downstreamSocket);
|
|
2212
2392
|
}
|
|
2213
2393
|
ws.send(JSON.stringify({
|
|
2214
2394
|
type: 'connected',
|
|
@@ -2226,10 +2406,7 @@ export function createConsoleGatewayRoutes(options) {
|
|
|
2226
2406
|
clearInterval(heartbeatInterval);
|
|
2227
2407
|
}
|
|
2228
2408
|
}, heartbeatIntervalMs);
|
|
2229
|
-
|
|
2230
|
-
downstreamSockets,
|
|
2231
|
-
heartbeatInterval,
|
|
2232
|
-
});
|
|
2409
|
+
current.heartbeatInterval = heartbeatInterval;
|
|
2233
2410
|
},
|
|
2234
2411
|
onClose(_event, ws) {
|
|
2235
2412
|
cleanup(ws);
|