@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.
@@ -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;CACnB;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;AAq9BD,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,iCAAiC,GACzC,IAAI,CAkwEN"}
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"}
@@ -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 resolveForwardBearerToken(args) {
481
- if (args.instance.token) {
482
- return args.instance.token;
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
- return null;
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 auth = await options.authenticate(c);
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
- clearInterval(state.heartbeatInterval);
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 downstreamSockets = [];
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 event = {
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(event));
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
- liveState.set(ws, {
2230
- downstreamSockets,
2231
- heartbeatInterval,
2232
- });
2409
+ current.heartbeatInterval = heartbeatInterval;
2233
2410
  },
2234
2411
  onClose(_event, ws) {
2235
2412
  cleanup(ws);