@syncular/server-hono 0.0.6-126 → 0.0.6-135

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.
@@ -2,6 +2,7 @@ import { Hono } from 'hono';
2
2
  import { cors } from 'hono/cors';
3
3
  import { describeRoute, resolver, validator as zValidator } from 'hono-openapi';
4
4
  import { z } from 'zod';
5
+ import { closeUnauthenticatedSocket, parseBearerToken, parseWebSocketAuthToken, } from './live-auth.js';
5
6
  import { ApiKeyTypeSchema, ConsoleApiKeyBulkRevokeRequestSchema, ConsoleApiKeyBulkRevokeResponseSchema, ConsoleApiKeyCreateRequestSchema, ConsoleApiKeyCreateResponseSchema, ConsoleApiKeyRevokeResponseSchema, ConsoleApiKeySchema, ConsoleClearEventsResultSchema, ConsoleClientSchema, ConsoleCommitDetailSchema, ConsoleCommitListItemSchema, ConsoleCompactResultSchema, ConsoleEvictResultSchema, ConsoleHandlerSchema, ConsoleOperationEventSchema, ConsoleOperationsQuerySchema, ConsolePaginatedResponseSchema, ConsolePaginationQuerySchema, ConsolePartitionedPaginationQuerySchema, ConsolePartitionQuerySchema, ConsolePruneEventsResultSchema, ConsolePrunePreviewSchema, ConsolePruneResultSchema, ConsoleRequestEventSchema, ConsoleRequestPayloadSchema, ConsoleTimelineItemSchema, ConsoleTimelineQuerySchema, LatencyQuerySchema, LatencyStatsResponseSchema, SyncStatsSchema, TimeseriesQuerySchema, TimeseriesStatsResponseSchema, } from './schemas.js';
6
7
  const GatewayFailureSchema = z.object({
7
8
  instanceId: z.string(),
@@ -230,65 +231,35 @@ function parseLocalNumericId(value) {
230
231
  return null;
231
232
  return parsed;
232
233
  }
233
- function resolveEventTarget(args) {
234
- const federated = parseFederatedNumericId(args.id);
235
- if (federated) {
236
- const instance = findInstanceById({
237
- instances: args.instances,
238
- instanceId: federated.instanceId,
239
- });
240
- if (!instance) {
241
- return {
242
- ok: false,
243
- status: 404,
244
- error: 'NOT_FOUND',
245
- message: 'Instance not found',
246
- };
247
- }
248
- return { ok: true, instance, localEventId: federated.localId };
249
- }
250
- const localEventId = parseLocalNumericId(args.id);
251
- if (localEventId === null) {
252
- return {
253
- ok: false,
254
- status: 400,
255
- error: 'INVALID_FEDERATED_ID',
256
- message: 'Expected either "<instanceId>:<eventId>" or "<eventId>" with an explicit instance filter.',
257
- };
258
- }
259
- const selectedInstances = selectInstances({
260
- instances: args.instances,
261
- query: args.query,
262
- });
234
+ function noInstancesSelectedResponse() {
235
+ return {
236
+ ok: false,
237
+ status: 400,
238
+ error: 'NO_INSTANCES_SELECTED',
239
+ message: 'No enabled instances matched the provided instance filter.',
240
+ };
241
+ }
242
+ function resolveSingleSelectedInstance(args) {
243
+ const selectedInstances = selectInstances(args);
263
244
  if (selectedInstances.length === 0) {
264
- return {
265
- ok: false,
266
- status: 400,
267
- error: 'NO_INSTANCES_SELECTED',
268
- message: 'No enabled instances matched the provided instance filter.',
269
- };
245
+ return noInstancesSelectedResponse();
270
246
  }
271
247
  if (selectedInstances.length > 1) {
272
248
  return {
273
249
  ok: false,
274
250
  status: 400,
275
- error: 'AMBIGUOUS_EVENT_ID',
276
- message: 'Local event IDs are ambiguous across multiple instances. Use "<instanceId>:<eventId>" or select one instance.',
251
+ error: args.onMultiple.error,
252
+ message: args.onMultiple.message,
277
253
  };
278
254
  }
279
255
  const instance = selectedInstances[0];
280
256
  if (!instance) {
281
- return {
282
- ok: false,
283
- status: 400,
284
- error: 'NO_INSTANCES_SELECTED',
285
- message: 'No enabled instances matched the provided instance filter.',
286
- };
257
+ return noInstancesSelectedResponse();
287
258
  }
288
- return { ok: true, instance, localEventId };
259
+ return { ok: true, instance };
289
260
  }
290
- function resolveCommitTarget(args) {
291
- const federated = parseFederatedNumericId(args.seq);
261
+ function resolveFederatedOrLocalNumericTarget(args) {
262
+ const federated = parseFederatedNumericId(args.id);
292
263
  if (federated) {
293
264
  const instance = findInstanceById({
294
265
  instances: args.instances,
@@ -302,76 +273,71 @@ function resolveCommitTarget(args) {
302
273
  message: 'Instance not found',
303
274
  };
304
275
  }
305
- return { ok: true, instance, localCommitSeq: federated.localId };
276
+ return { ok: true, instance, localId: federated.localId };
306
277
  }
307
- const localCommitSeq = parseLocalNumericId(args.seq);
308
- if (localCommitSeq === null) {
278
+ const localId = parseLocalNumericId(args.id);
279
+ if (localId === null) {
309
280
  return {
310
281
  ok: false,
311
282
  status: 400,
312
283
  error: 'INVALID_FEDERATED_ID',
313
- message: 'Expected either "<instanceId>:<commitSeq>" or "<commitSeq>" with an explicit instance filter.',
284
+ message: args.invalidMessage,
314
285
  };
315
286
  }
316
- const selectedInstances = selectInstances({
287
+ const selection = resolveSingleSelectedInstance({
317
288
  instances: args.instances,
318
289
  query: args.query,
290
+ onMultiple: {
291
+ error: args.ambiguousError,
292
+ message: args.ambiguousMessage,
293
+ },
319
294
  });
320
- if (selectedInstances.length === 0) {
321
- return {
322
- ok: false,
323
- status: 400,
324
- error: 'NO_INSTANCES_SELECTED',
325
- message: 'No enabled instances matched the provided instance filter.',
326
- };
327
- }
328
- if (selectedInstances.length > 1) {
329
- return {
330
- ok: false,
331
- status: 400,
332
- error: 'AMBIGUOUS_COMMIT_ID',
333
- message: 'Local commit IDs are ambiguous across multiple instances. Use "<instanceId>:<commitSeq>" or select one instance.',
334
- };
335
- }
336
- const instance = selectedInstances[0];
337
- if (!instance) {
338
- return {
339
- ok: false,
340
- status: 400,
341
- error: 'NO_INSTANCES_SELECTED',
342
- message: 'No enabled instances matched the provided instance filter.',
343
- };
344
- }
345
- return { ok: true, instance, localCommitSeq };
295
+ if (!selection.ok)
296
+ return selection;
297
+ return { ok: true, instance: selection.instance, localId };
298
+ }
299
+ function resolveEventTarget(args) {
300
+ const resolved = resolveFederatedOrLocalNumericTarget({
301
+ id: args.id,
302
+ instances: args.instances,
303
+ query: args.query,
304
+ invalidMessage: 'Expected either "<instanceId>:<eventId>" or "<eventId>" with an explicit instance filter.',
305
+ ambiguousError: 'AMBIGUOUS_EVENT_ID',
306
+ ambiguousMessage: 'Local event IDs are ambiguous across multiple instances. Use "<instanceId>:<eventId>" or select one instance.',
307
+ });
308
+ if (!resolved.ok)
309
+ return resolved;
310
+ return {
311
+ ok: true,
312
+ instance: resolved.instance,
313
+ localEventId: resolved.localId,
314
+ };
315
+ }
316
+ function resolveCommitTarget(args) {
317
+ const resolved = resolveFederatedOrLocalNumericTarget({
318
+ id: args.seq,
319
+ instances: args.instances,
320
+ query: args.query,
321
+ invalidMessage: 'Expected either "<instanceId>:<commitSeq>" or "<commitSeq>" with an explicit instance filter.',
322
+ ambiguousError: 'AMBIGUOUS_COMMIT_ID',
323
+ ambiguousMessage: 'Local commit IDs are ambiguous across multiple instances. Use "<instanceId>:<commitSeq>" or select one instance.',
324
+ });
325
+ if (!resolved.ok)
326
+ return resolved;
327
+ return {
328
+ ok: true,
329
+ instance: resolved.instance,
330
+ localCommitSeq: resolved.localId,
331
+ };
346
332
  }
347
333
  function resolveSingleInstanceTarget(args) {
348
- const selectedInstances = selectInstances(args);
349
- if (selectedInstances.length === 0) {
350
- return {
351
- ok: false,
352
- status: 400,
353
- error: 'NO_INSTANCES_SELECTED',
354
- message: 'No enabled instances matched the provided instance filter.',
355
- };
356
- }
357
- if (selectedInstances.length > 1) {
358
- return {
359
- ok: false,
360
- status: 400,
334
+ return resolveSingleSelectedInstance({
335
+ ...args,
336
+ onMultiple: {
361
337
  error: 'INSTANCE_REQUIRED',
362
338
  message: 'This endpoint requires exactly one target instance. Provide `instanceId` or a single-value `instanceIds` filter.',
363
- };
364
- }
365
- const instance = selectedInstances[0];
366
- if (!instance) {
367
- return {
368
- ok: false,
369
- status: 400,
370
- error: 'NO_INSTANCES_SELECTED',
371
- message: 'No enabled instances matched the provided instance filter.',
372
- };
373
- }
374
- return { ok: true, instance };
339
+ },
340
+ });
375
341
  }
376
342
  function minNullable(values) {
377
343
  const filtered = values.filter((value) => value !== null);
@@ -473,14 +439,6 @@ function resolveForwardAuthorization(args) {
473
439
  }
474
440
  return null;
475
441
  }
476
- function parseBearerToken(authHeader) {
477
- const value = authHeader?.trim();
478
- if (!value?.startsWith('Bearer ')) {
479
- return null;
480
- }
481
- const token = value.slice(7).trim();
482
- return token.length > 0 ? token : null;
483
- }
484
442
  async function fetchDownstreamJson(args) {
485
443
  const url = buildConsoleEndpointUrl({
486
444
  instance: args.instance,
@@ -741,6 +699,124 @@ export function createConsoleGatewayRoutes(options) {
741
699
  ],
742
700
  credentials: true,
743
701
  }));
702
+ const withGatewayAuth = async (c, callback) => {
703
+ const auth = await options.authenticate(c);
704
+ if (!auth) {
705
+ return unauthorizedResponse(c);
706
+ }
707
+ return callback();
708
+ };
709
+ const proxySingleInstanceJsonRequest = async (args) => {
710
+ const target = resolveSingleInstanceTarget({
711
+ instances,
712
+ query: args.query,
713
+ });
714
+ if (!target.ok) {
715
+ return args.c.json({
716
+ error: target.error,
717
+ message: target.message,
718
+ }, target.status);
719
+ }
720
+ const forwardQuery = sanitizeForwardQueryParams(new URL(args.c.req.url).searchParams);
721
+ const result = await forwardDownstreamJsonRequest({
722
+ c: args.c,
723
+ instance: target.instance,
724
+ method: args.method,
725
+ path: args.path,
726
+ query: forwardQuery,
727
+ ...(args.body === undefined ? {} : { body: args.body }),
728
+ responseSchema: args.responseSchema,
729
+ fetchImpl,
730
+ });
731
+ if (!result.ok) {
732
+ return jsonResponse(result.body, result.status);
733
+ }
734
+ return jsonResponse(result.data, result.status);
735
+ };
736
+ const selectTargetInstances = (c, query) => {
737
+ const selectedInstances = selectInstances({ instances, query });
738
+ if (selectedInstances.length > 0) {
739
+ return { ok: true, selectedInstances };
740
+ }
741
+ const noInstanceError = noInstancesSelectedResponse();
742
+ return {
743
+ ok: false,
744
+ response: c.json({
745
+ error: noInstanceError.error,
746
+ message: noInstanceError.message,
747
+ }, noInstanceError.status),
748
+ };
749
+ };
750
+ const fetchFromSelectedInstances = async (args) => {
751
+ const results = await Promise.all(args.selectedInstances.map((instance) => fetchDownstreamJson({
752
+ c: args.c,
753
+ instance,
754
+ path: args.path,
755
+ query: args.query,
756
+ schema: args.schema,
757
+ fetchImpl,
758
+ })));
759
+ const failedInstances = results
760
+ .filter((result) => !result.ok)
761
+ .map((result) => result.failure);
762
+ const successfulResults = results
763
+ .map((result, index) => ({
764
+ result,
765
+ instance: args.selectedInstances[index],
766
+ }))
767
+ .filter((entry) => Boolean(entry.instance) && entry.result.ok)
768
+ .map((entry) => ({
769
+ instance: entry.instance,
770
+ data: entry.result.data,
771
+ }));
772
+ if (successfulResults.length === 0) {
773
+ return {
774
+ ok: false,
775
+ response: allInstancesFailedResponse(args.c, failedInstances),
776
+ };
777
+ }
778
+ return {
779
+ ok: true,
780
+ successfulResults,
781
+ failedInstances,
782
+ };
783
+ };
784
+ const fetchPagedFromSelectedInstances = async (args) => {
785
+ const results = await Promise.all(args.selectedInstances.map((instance) => fetchDownstreamPaged({
786
+ c: args.c,
787
+ instance,
788
+ path: args.path,
789
+ query: args.query,
790
+ targetCount: args.targetCount,
791
+ schema: args.schema,
792
+ fetchImpl,
793
+ })));
794
+ const failedInstances = results
795
+ .filter((result) => !result.ok)
796
+ .map((result) => result.failure);
797
+ const successfulResults = results
798
+ .map((result, index) => ({
799
+ result,
800
+ instance: args.selectedInstances[index],
801
+ }))
802
+ .filter((entry) => Boolean(entry.instance) && entry.result.ok)
803
+ .map((entry) => ({
804
+ instance: entry.instance,
805
+ items: entry.result.items,
806
+ total: entry.result.total,
807
+ }));
808
+ if (successfulResults.length === 0) {
809
+ return {
810
+ ok: false,
811
+ response: allInstancesFailedResponse(args.c, failedInstances),
812
+ };
813
+ }
814
+ return {
815
+ ok: true,
816
+ successfulResults,
817
+ failedInstances,
818
+ };
819
+ };
744
820
  routes.get('/instances', describeRoute({
745
821
  tags: ['console-gateway'],
746
822
  summary: 'List configured downstream console instances',
@@ -763,17 +839,15 @@ export function createConsoleGatewayRoutes(options) {
763
839
  },
764
840
  },
765
841
  }), async (c) => {
766
- const auth = await options.authenticate(c);
767
- if (!auth) {
768
- return unauthorizedResponse(c);
769
- }
770
- return c.json({
771
- items: instances.map((instance) => ({
772
- instanceId: instance.instanceId,
773
- label: instance.label ?? instance.instanceId,
774
- baseUrl: instance.baseUrl,
775
- enabled: instance.enabled ?? true,
776
- })),
842
+ return withGatewayAuth(c, async () => {
843
+ return c.json({
844
+ items: instances.map((instance) => ({
845
+ instanceId: instance.instanceId,
846
+ label: instance.label ?? instance.instanceId,
847
+ baseUrl: instance.baseUrl,
848
+ enabled: instance.enabled ?? true,
849
+ })),
850
+ });
777
851
  });
778
852
  });
779
853
  routes.get('/instances/health', describeRoute({
@@ -798,34 +872,29 @@ export function createConsoleGatewayRoutes(options) {
798
872
  },
799
873
  },
800
874
  }), zValidator('query', GatewayInstanceFilterSchema), async (c) => {
801
- const auth = await options.authenticate(c);
802
- if (!auth) {
803
- return unauthorizedResponse(c);
804
- }
805
- const query = c.req.valid('query');
806
- const selectedInstances = selectInstances({ instances, query });
807
- if (selectedInstances.length === 0) {
875
+ return withGatewayAuth(c, async () => {
876
+ const query = c.req.valid('query');
877
+ const selection = selectTargetInstances(c, query);
878
+ if (!selection.ok) {
879
+ return selection.response;
880
+ }
881
+ const items = await Promise.all(selection.selectedInstances.map((instance) => checkDownstreamInstanceHealth({
882
+ c,
883
+ instance,
884
+ fetchImpl,
885
+ })));
886
+ const failedInstances = items
887
+ .filter((item) => !item.healthy)
888
+ .map((item) => ({
889
+ instanceId: item.instanceId,
890
+ reason: item.reason ?? 'Health probe failed',
891
+ ...(item.status !== undefined ? { status: item.status } : {}),
892
+ }));
808
893
  return c.json({
809
- error: 'NO_INSTANCES_SELECTED',
810
- message: 'No enabled instances matched the provided instance filter.',
811
- }, 400);
812
- }
813
- const items = await Promise.all(selectedInstances.map((instance) => checkDownstreamInstanceHealth({
814
- c,
815
- instance,
816
- fetchImpl,
817
- })));
818
- const failedInstances = items
819
- .filter((item) => !item.healthy)
820
- .map((item) => ({
821
- instanceId: item.instanceId,
822
- reason: item.reason ?? 'Health probe failed',
823
- ...(item.status !== undefined ? { status: item.status } : {}),
824
- }));
825
- return c.json({
826
- items,
827
- partial: failedInstances.length > 0,
828
- failedInstances,
894
+ items,
895
+ partial: failedInstances.length > 0,
896
+ failedInstances,
897
+ });
829
898
  });
830
899
  });
831
900
  routes.get('/handlers', describeRoute({
@@ -842,32 +911,16 @@ export function createConsoleGatewayRoutes(options) {
842
911
  },
843
912
  },
844
913
  }), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
845
- const auth = await options.authenticate(c);
846
- if (!auth) {
847
- return unauthorizedResponse(c);
848
- }
849
- const query = c.req.valid('query');
850
- const target = resolveSingleInstanceTarget({ instances, query });
851
- if (!target.ok) {
852
- return c.json({
853
- error: target.error,
854
- message: target.message,
855
- }, target.status);
856
- }
857
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
858
- const result = await forwardDownstreamJsonRequest({
859
- c,
860
- instance: target.instance,
861
- method: 'GET',
862
- path: '/handlers',
863
- query: forwardQuery,
864
- responseSchema: GatewayHandlersResponseSchema,
865
- fetchImpl,
914
+ return withGatewayAuth(c, async () => {
915
+ const query = c.req.valid('query');
916
+ return proxySingleInstanceJsonRequest({
917
+ c,
918
+ query,
919
+ method: 'GET',
920
+ path: '/handlers',
921
+ responseSchema: GatewayHandlersResponseSchema,
922
+ });
866
923
  });
867
- if (!result.ok) {
868
- return jsonResponse(result.body, result.status);
869
- }
870
- return jsonResponse(result.data, result.status);
871
924
  });
872
925
  routes.post('/prune/preview', describeRoute({
873
926
  tags: ['console-gateway'],
@@ -883,32 +936,16 @@ export function createConsoleGatewayRoutes(options) {
883
936
  },
884
937
  },
885
938
  }), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
886
- const auth = await options.authenticate(c);
887
- if (!auth) {
888
- return unauthorizedResponse(c);
889
- }
890
- const query = c.req.valid('query');
891
- const target = resolveSingleInstanceTarget({ instances, query });
892
- if (!target.ok) {
893
- return c.json({
894
- error: target.error,
895
- message: target.message,
896
- }, target.status);
897
- }
898
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
899
- const result = await forwardDownstreamJsonRequest({
900
- c,
901
- instance: target.instance,
902
- method: 'POST',
903
- path: '/prune/preview',
904
- query: forwardQuery,
905
- responseSchema: ConsolePrunePreviewSchema,
906
- fetchImpl,
939
+ return withGatewayAuth(c, async () => {
940
+ const query = c.req.valid('query');
941
+ return proxySingleInstanceJsonRequest({
942
+ c,
943
+ query,
944
+ method: 'POST',
945
+ path: '/prune/preview',
946
+ responseSchema: ConsolePrunePreviewSchema,
947
+ });
907
948
  });
908
- if (!result.ok) {
909
- return jsonResponse(result.body, result.status);
910
- }
911
- return jsonResponse(result.data, result.status);
912
949
  });
913
950
  routes.post('/prune', describeRoute({
914
951
  tags: ['console-gateway'],
@@ -924,32 +961,16 @@ export function createConsoleGatewayRoutes(options) {
924
961
  },
925
962
  },
926
963
  }), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
927
- const auth = await options.authenticate(c);
928
- if (!auth) {
929
- return unauthorizedResponse(c);
930
- }
931
- const query = c.req.valid('query');
932
- const target = resolveSingleInstanceTarget({ instances, query });
933
- if (!target.ok) {
934
- return c.json({
935
- error: target.error,
936
- message: target.message,
937
- }, target.status);
938
- }
939
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
940
- const result = await forwardDownstreamJsonRequest({
941
- c,
942
- instance: target.instance,
943
- method: 'POST',
944
- path: '/prune',
945
- query: forwardQuery,
946
- responseSchema: ConsolePruneResultSchema,
947
- fetchImpl,
964
+ return withGatewayAuth(c, async () => {
965
+ const query = c.req.valid('query');
966
+ return proxySingleInstanceJsonRequest({
967
+ c,
968
+ query,
969
+ method: 'POST',
970
+ path: '/prune',
971
+ responseSchema: ConsolePruneResultSchema,
972
+ });
948
973
  });
949
- if (!result.ok) {
950
- return jsonResponse(result.body, result.status);
951
- }
952
- return jsonResponse(result.data, result.status);
953
974
  });
954
975
  routes.post('/compact', describeRoute({
955
976
  tags: ['console-gateway'],
@@ -965,32 +986,16 @@ export function createConsoleGatewayRoutes(options) {
965
986
  },
966
987
  },
967
988
  }), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
968
- const auth = await options.authenticate(c);
969
- if (!auth) {
970
- return unauthorizedResponse(c);
971
- }
972
- const query = c.req.valid('query');
973
- const target = resolveSingleInstanceTarget({ instances, query });
974
- if (!target.ok) {
975
- return c.json({
976
- error: target.error,
977
- message: target.message,
978
- }, target.status);
979
- }
980
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
981
- const result = await forwardDownstreamJsonRequest({
982
- c,
983
- instance: target.instance,
984
- method: 'POST',
985
- path: '/compact',
986
- query: forwardQuery,
987
- responseSchema: ConsoleCompactResultSchema,
988
- fetchImpl,
989
+ return withGatewayAuth(c, async () => {
990
+ const query = c.req.valid('query');
991
+ return proxySingleInstanceJsonRequest({
992
+ c,
993
+ query,
994
+ method: 'POST',
995
+ path: '/compact',
996
+ responseSchema: ConsoleCompactResultSchema,
997
+ });
989
998
  });
990
- if (!result.ok) {
991
- return jsonResponse(result.body, result.status);
992
- }
993
- return jsonResponse(result.data, result.status);
994
999
  });
995
1000
  routes.post('/notify-data-change', describeRoute({
996
1001
  tags: ['console-gateway'],
@@ -1006,34 +1011,18 @@ export function createConsoleGatewayRoutes(options) {
1006
1011
  },
1007
1012
  },
1008
1013
  }), zValidator('query', GatewaySingleInstanceQuerySchema), zValidator('json', GatewayNotifyDataChangeRequestSchema), async (c) => {
1009
- const auth = await options.authenticate(c);
1010
- if (!auth) {
1011
- return unauthorizedResponse(c);
1012
- }
1013
- const query = c.req.valid('query');
1014
- const body = c.req.valid('json');
1015
- const target = resolveSingleInstanceTarget({ instances, query });
1016
- if (!target.ok) {
1017
- return c.json({
1018
- error: target.error,
1019
- message: target.message,
1020
- }, target.status);
1021
- }
1022
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1023
- const result = await forwardDownstreamJsonRequest({
1024
- c,
1025
- instance: target.instance,
1026
- method: 'POST',
1027
- path: '/notify-data-change',
1028
- query: forwardQuery,
1029
- body,
1030
- responseSchema: GatewayNotifyDataChangeResponseSchema,
1031
- fetchImpl,
1014
+ return withGatewayAuth(c, async () => {
1015
+ const query = c.req.valid('query');
1016
+ const body = c.req.valid('json');
1017
+ return proxySingleInstanceJsonRequest({
1018
+ c,
1019
+ query,
1020
+ method: 'POST',
1021
+ path: '/notify-data-change',
1022
+ body,
1023
+ responseSchema: GatewayNotifyDataChangeResponseSchema,
1024
+ });
1032
1025
  });
1033
- if (!result.ok) {
1034
- return jsonResponse(result.body, result.status);
1035
- }
1036
- return jsonResponse(result.data, result.status);
1037
1026
  });
1038
1027
  routes.delete('/clients/:id', describeRoute({
1039
1028
  tags: ['console-gateway'],
@@ -1049,33 +1038,17 @@ export function createConsoleGatewayRoutes(options) {
1049
1038
  },
1050
1039
  },
1051
1040
  }), zValidator('param', GatewayClientPathParamSchema), zValidator('query', GatewaySingleInstancePartitionQuerySchema), async (c) => {
1052
- const auth = await options.authenticate(c);
1053
- if (!auth) {
1054
- return unauthorizedResponse(c);
1055
- }
1056
- const { id } = c.req.valid('param');
1057
- const query = c.req.valid('query');
1058
- const target = resolveSingleInstanceTarget({ instances, query });
1059
- if (!target.ok) {
1060
- return c.json({
1061
- error: target.error,
1062
- message: target.message,
1063
- }, target.status);
1064
- }
1065
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1066
- const result = await forwardDownstreamJsonRequest({
1067
- c,
1068
- instance: target.instance,
1069
- method: 'DELETE',
1070
- path: `/clients/${encodeURIComponent(id)}`,
1071
- query: forwardQuery,
1072
- responseSchema: ConsoleEvictResultSchema,
1073
- fetchImpl,
1041
+ return withGatewayAuth(c, async () => {
1042
+ const { id } = c.req.valid('param');
1043
+ const query = c.req.valid('query');
1044
+ return proxySingleInstanceJsonRequest({
1045
+ c,
1046
+ query,
1047
+ method: 'DELETE',
1048
+ path: `/clients/${encodeURIComponent(id)}`,
1049
+ responseSchema: ConsoleEvictResultSchema,
1050
+ });
1074
1051
  });
1075
- if (!result.ok) {
1076
- return jsonResponse(result.body, result.status);
1077
- }
1078
- return jsonResponse(result.data, result.status);
1079
1052
  });
1080
1053
  routes.delete('/events', describeRoute({
1081
1054
  tags: ['console-gateway'],
@@ -1091,32 +1064,16 @@ export function createConsoleGatewayRoutes(options) {
1091
1064
  },
1092
1065
  },
1093
1066
  }), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
1094
- const auth = await options.authenticate(c);
1095
- if (!auth) {
1096
- return unauthorizedResponse(c);
1097
- }
1098
- const query = c.req.valid('query');
1099
- const target = resolveSingleInstanceTarget({ instances, query });
1100
- if (!target.ok) {
1101
- return c.json({
1102
- error: target.error,
1103
- message: target.message,
1104
- }, target.status);
1105
- }
1106
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1107
- const result = await forwardDownstreamJsonRequest({
1108
- c,
1109
- instance: target.instance,
1110
- method: 'DELETE',
1111
- path: '/events',
1112
- query: forwardQuery,
1113
- responseSchema: ConsoleClearEventsResultSchema,
1114
- fetchImpl,
1067
+ return withGatewayAuth(c, async () => {
1068
+ const query = c.req.valid('query');
1069
+ return proxySingleInstanceJsonRequest({
1070
+ c,
1071
+ query,
1072
+ method: 'DELETE',
1073
+ path: '/events',
1074
+ responseSchema: ConsoleClearEventsResultSchema,
1075
+ });
1115
1076
  });
1116
- if (!result.ok) {
1117
- return jsonResponse(result.body, result.status);
1118
- }
1119
- return jsonResponse(result.data, result.status);
1120
1077
  });
1121
1078
  routes.post('/events/prune', describeRoute({
1122
1079
  tags: ['console-gateway'],
@@ -1132,32 +1089,16 @@ export function createConsoleGatewayRoutes(options) {
1132
1089
  },
1133
1090
  },
1134
1091
  }), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
1135
- const auth = await options.authenticate(c);
1136
- if (!auth) {
1137
- return unauthorizedResponse(c);
1138
- }
1139
- const query = c.req.valid('query');
1140
- const target = resolveSingleInstanceTarget({ instances, query });
1141
- if (!target.ok) {
1142
- return c.json({
1143
- error: target.error,
1144
- message: target.message,
1145
- }, target.status);
1146
- }
1147
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1148
- const result = await forwardDownstreamJsonRequest({
1149
- c,
1150
- instance: target.instance,
1151
- method: 'POST',
1152
- path: '/events/prune',
1153
- query: forwardQuery,
1154
- responseSchema: ConsolePruneEventsResultSchema,
1155
- fetchImpl,
1092
+ return withGatewayAuth(c, async () => {
1093
+ const query = c.req.valid('query');
1094
+ return proxySingleInstanceJsonRequest({
1095
+ c,
1096
+ query,
1097
+ method: 'POST',
1098
+ path: '/events/prune',
1099
+ responseSchema: ConsolePruneEventsResultSchema,
1100
+ });
1156
1101
  });
1157
- if (!result.ok) {
1158
- return jsonResponse(result.body, result.status);
1159
- }
1160
- return jsonResponse(result.data, result.status);
1161
1102
  });
1162
1103
  routes.get('/api-keys', describeRoute({
1163
1104
  tags: ['console-gateway'],
@@ -1173,32 +1114,16 @@ export function createConsoleGatewayRoutes(options) {
1173
1114
  },
1174
1115
  },
1175
1116
  }), zValidator('query', GatewayApiKeysQuerySchema), async (c) => {
1176
- const auth = await options.authenticate(c);
1177
- if (!auth) {
1178
- return unauthorizedResponse(c);
1179
- }
1180
- const query = c.req.valid('query');
1181
- const target = resolveSingleInstanceTarget({ instances, query });
1182
- if (!target.ok) {
1183
- return c.json({
1184
- error: target.error,
1185
- message: target.message,
1186
- }, target.status);
1187
- }
1188
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1189
- const result = await forwardDownstreamJsonRequest({
1190
- c,
1191
- instance: target.instance,
1192
- method: 'GET',
1193
- path: '/api-keys',
1194
- query: forwardQuery,
1195
- responseSchema: ConsolePaginatedResponseSchema(ConsoleApiKeySchema),
1196
- fetchImpl,
1117
+ return withGatewayAuth(c, async () => {
1118
+ const query = c.req.valid('query');
1119
+ return proxySingleInstanceJsonRequest({
1120
+ c,
1121
+ query,
1122
+ method: 'GET',
1123
+ path: '/api-keys',
1124
+ responseSchema: ConsolePaginatedResponseSchema(ConsoleApiKeySchema),
1125
+ });
1197
1126
  });
1198
- if (!result.ok) {
1199
- return jsonResponse(result.body, result.status);
1200
- }
1201
- return jsonResponse(result.data, result.status);
1202
1127
  });
1203
1128
  routes.post('/api-keys', describeRoute({
1204
1129
  tags: ['console-gateway'],
@@ -1214,34 +1139,18 @@ export function createConsoleGatewayRoutes(options) {
1214
1139
  },
1215
1140
  },
1216
1141
  }), zValidator('query', GatewaySingleInstanceQuerySchema), zValidator('json', ConsoleApiKeyCreateRequestSchema), async (c) => {
1217
- const auth = await options.authenticate(c);
1218
- if (!auth) {
1219
- return unauthorizedResponse(c);
1220
- }
1221
- const query = c.req.valid('query');
1222
- const body = c.req.valid('json');
1223
- const target = resolveSingleInstanceTarget({ instances, query });
1224
- if (!target.ok) {
1225
- return c.json({
1226
- error: target.error,
1227
- message: target.message,
1228
- }, target.status);
1229
- }
1230
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1231
- const result = await forwardDownstreamJsonRequest({
1232
- c,
1233
- instance: target.instance,
1234
- method: 'POST',
1235
- path: '/api-keys',
1236
- query: forwardQuery,
1237
- body,
1238
- responseSchema: ConsoleApiKeyCreateResponseSchema,
1239
- fetchImpl,
1142
+ return withGatewayAuth(c, async () => {
1143
+ const query = c.req.valid('query');
1144
+ const body = c.req.valid('json');
1145
+ return proxySingleInstanceJsonRequest({
1146
+ c,
1147
+ query,
1148
+ method: 'POST',
1149
+ path: '/api-keys',
1150
+ body,
1151
+ responseSchema: ConsoleApiKeyCreateResponseSchema,
1152
+ });
1240
1153
  });
1241
- if (!result.ok) {
1242
- return jsonResponse(result.body, result.status);
1243
- }
1244
- return jsonResponse(result.data, result.status);
1245
1154
  });
1246
1155
  routes.get('/api-keys/:id', describeRoute({
1247
1156
  tags: ['console-gateway'],
@@ -1257,33 +1166,17 @@ export function createConsoleGatewayRoutes(options) {
1257
1166
  },
1258
1167
  },
1259
1168
  }), zValidator('param', GatewayApiKeyPathParamSchema), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
1260
- const auth = await options.authenticate(c);
1261
- if (!auth) {
1262
- return unauthorizedResponse(c);
1263
- }
1264
- const { id } = c.req.valid('param');
1265
- const query = c.req.valid('query');
1266
- const target = resolveSingleInstanceTarget({ instances, query });
1267
- if (!target.ok) {
1268
- return c.json({
1269
- error: target.error,
1270
- message: target.message,
1271
- }, target.status);
1272
- }
1273
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1274
- const result = await forwardDownstreamJsonRequest({
1275
- c,
1276
- instance: target.instance,
1277
- method: 'GET',
1278
- path: `/api-keys/${encodeURIComponent(id)}`,
1279
- query: forwardQuery,
1280
- responseSchema: ConsoleApiKeySchema,
1281
- fetchImpl,
1169
+ return withGatewayAuth(c, async () => {
1170
+ const { id } = c.req.valid('param');
1171
+ const query = c.req.valid('query');
1172
+ return proxySingleInstanceJsonRequest({
1173
+ c,
1174
+ query,
1175
+ method: 'GET',
1176
+ path: `/api-keys/${encodeURIComponent(id)}`,
1177
+ responseSchema: ConsoleApiKeySchema,
1178
+ });
1282
1179
  });
1283
- if (!result.ok) {
1284
- return jsonResponse(result.body, result.status);
1285
- }
1286
- return jsonResponse(result.data, result.status);
1287
1180
  });
1288
1181
  routes.delete('/api-keys/:id', describeRoute({
1289
1182
  tags: ['console-gateway'],
@@ -1299,33 +1192,17 @@ export function createConsoleGatewayRoutes(options) {
1299
1192
  },
1300
1193
  },
1301
1194
  }), zValidator('param', GatewayApiKeyPathParamSchema), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
1302
- const auth = await options.authenticate(c);
1303
- if (!auth) {
1304
- return unauthorizedResponse(c);
1305
- }
1306
- const { id } = c.req.valid('param');
1307
- const query = c.req.valid('query');
1308
- const target = resolveSingleInstanceTarget({ instances, query });
1309
- if (!target.ok) {
1310
- return c.json({
1311
- error: target.error,
1312
- message: target.message,
1313
- }, target.status);
1314
- }
1315
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1316
- const result = await forwardDownstreamJsonRequest({
1317
- c,
1318
- instance: target.instance,
1319
- method: 'DELETE',
1320
- path: `/api-keys/${encodeURIComponent(id)}`,
1321
- query: forwardQuery,
1322
- responseSchema: ConsoleApiKeyRevokeResponseSchema,
1323
- fetchImpl,
1195
+ return withGatewayAuth(c, async () => {
1196
+ const { id } = c.req.valid('param');
1197
+ const query = c.req.valid('query');
1198
+ return proxySingleInstanceJsonRequest({
1199
+ c,
1200
+ query,
1201
+ method: 'DELETE',
1202
+ path: `/api-keys/${encodeURIComponent(id)}`,
1203
+ responseSchema: ConsoleApiKeyRevokeResponseSchema,
1204
+ });
1324
1205
  });
1325
- if (!result.ok) {
1326
- return jsonResponse(result.body, result.status);
1327
- }
1328
- return jsonResponse(result.data, result.status);
1329
1206
  });
1330
1207
  routes.post('/api-keys/bulk-revoke', describeRoute({
1331
1208
  tags: ['console-gateway'],
@@ -1341,34 +1218,18 @@ export function createConsoleGatewayRoutes(options) {
1341
1218
  },
1342
1219
  },
1343
1220
  }), zValidator('query', GatewaySingleInstanceQuerySchema), zValidator('json', ConsoleApiKeyBulkRevokeRequestSchema), async (c) => {
1344
- const auth = await options.authenticate(c);
1345
- if (!auth) {
1346
- return unauthorizedResponse(c);
1347
- }
1348
- const query = c.req.valid('query');
1349
- const body = c.req.valid('json');
1350
- const target = resolveSingleInstanceTarget({ instances, query });
1351
- if (!target.ok) {
1352
- return c.json({
1353
- error: target.error,
1354
- message: target.message,
1355
- }, target.status);
1356
- }
1357
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1358
- const result = await forwardDownstreamJsonRequest({
1359
- c,
1360
- instance: target.instance,
1361
- method: 'POST',
1362
- path: '/api-keys/bulk-revoke',
1363
- query: forwardQuery,
1364
- body,
1365
- responseSchema: ConsoleApiKeyBulkRevokeResponseSchema,
1366
- fetchImpl,
1221
+ return withGatewayAuth(c, async () => {
1222
+ const query = c.req.valid('query');
1223
+ const body = c.req.valid('json');
1224
+ return proxySingleInstanceJsonRequest({
1225
+ c,
1226
+ query,
1227
+ method: 'POST',
1228
+ path: '/api-keys/bulk-revoke',
1229
+ body,
1230
+ responseSchema: ConsoleApiKeyBulkRevokeResponseSchema,
1231
+ });
1367
1232
  });
1368
- if (!result.ok) {
1369
- return jsonResponse(result.body, result.status);
1370
- }
1371
- return jsonResponse(result.data, result.status);
1372
1233
  });
1373
1234
  routes.post('/api-keys/:id/rotate/stage', describeRoute({
1374
1235
  tags: ['console-gateway'],
@@ -1384,33 +1245,17 @@ export function createConsoleGatewayRoutes(options) {
1384
1245
  },
1385
1246
  },
1386
1247
  }), zValidator('param', GatewayApiKeyPathParamSchema), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
1387
- const auth = await options.authenticate(c);
1388
- if (!auth) {
1389
- return unauthorizedResponse(c);
1390
- }
1391
- const { id } = c.req.valid('param');
1392
- const query = c.req.valid('query');
1393
- const target = resolveSingleInstanceTarget({ instances, query });
1394
- if (!target.ok) {
1395
- return c.json({
1396
- error: target.error,
1397
- message: target.message,
1398
- }, target.status);
1399
- }
1400
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1401
- const result = await forwardDownstreamJsonRequest({
1402
- c,
1403
- instance: target.instance,
1404
- method: 'POST',
1405
- path: `/api-keys/${encodeURIComponent(id)}/rotate/stage`,
1406
- query: forwardQuery,
1407
- responseSchema: ConsoleApiKeyCreateResponseSchema,
1408
- fetchImpl,
1248
+ return withGatewayAuth(c, async () => {
1249
+ const { id } = c.req.valid('param');
1250
+ const query = c.req.valid('query');
1251
+ return proxySingleInstanceJsonRequest({
1252
+ c,
1253
+ query,
1254
+ method: 'POST',
1255
+ path: `/api-keys/${encodeURIComponent(id)}/rotate/stage`,
1256
+ responseSchema: ConsoleApiKeyCreateResponseSchema,
1257
+ });
1409
1258
  });
1410
- if (!result.ok) {
1411
- return jsonResponse(result.body, result.status);
1412
- }
1413
- return jsonResponse(result.data, result.status);
1414
1259
  });
1415
1260
  routes.post('/api-keys/:id/rotate', describeRoute({
1416
1261
  tags: ['console-gateway'],
@@ -1426,33 +1271,17 @@ export function createConsoleGatewayRoutes(options) {
1426
1271
  },
1427
1272
  },
1428
1273
  }), zValidator('param', GatewayApiKeyPathParamSchema), zValidator('query', GatewaySingleInstanceQuerySchema), async (c) => {
1429
- const auth = await options.authenticate(c);
1430
- if (!auth) {
1431
- return unauthorizedResponse(c);
1432
- }
1433
- const { id } = c.req.valid('param');
1434
- const query = c.req.valid('query');
1435
- const target = resolveSingleInstanceTarget({ instances, query });
1436
- if (!target.ok) {
1437
- return c.json({
1438
- error: target.error,
1439
- message: target.message,
1440
- }, target.status);
1441
- }
1442
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1443
- const result = await forwardDownstreamJsonRequest({
1444
- c,
1445
- instance: target.instance,
1446
- method: 'POST',
1447
- path: `/api-keys/${encodeURIComponent(id)}/rotate`,
1448
- query: forwardQuery,
1449
- responseSchema: ConsoleApiKeyCreateResponseSchema,
1450
- fetchImpl,
1274
+ return withGatewayAuth(c, async () => {
1275
+ const { id } = c.req.valid('param');
1276
+ const query = c.req.valid('query');
1277
+ return proxySingleInstanceJsonRequest({
1278
+ c,
1279
+ query,
1280
+ method: 'POST',
1281
+ path: `/api-keys/${encodeURIComponent(id)}/rotate`,
1282
+ responseSchema: ConsoleApiKeyCreateResponseSchema,
1283
+ });
1451
1284
  });
1452
- if (!result.ok) {
1453
- return jsonResponse(result.body, result.status);
1454
- }
1455
- return jsonResponse(result.data, result.status);
1456
1285
  });
1457
1286
  routes.get('/stats', describeRoute({
1458
1287
  tags: ['console-gateway'],
@@ -1468,65 +1297,49 @@ export function createConsoleGatewayRoutes(options) {
1468
1297
  },
1469
1298
  },
1470
1299
  }), zValidator('query', GatewayStatsQuerySchema), async (c) => {
1471
- const auth = await options.authenticate(c);
1472
- if (!auth) {
1473
- return unauthorizedResponse(c);
1474
- }
1475
- const query = c.req.valid('query');
1476
- const selectedInstances = selectInstances({ instances, query });
1477
- if (selectedInstances.length === 0) {
1300
+ return withGatewayAuth(c, async () => {
1301
+ const query = c.req.valid('query');
1302
+ const selection = selectTargetInstances(c, query);
1303
+ if (!selection.ok) {
1304
+ return selection.response;
1305
+ }
1306
+ const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1307
+ const fetched = await fetchFromSelectedInstances({
1308
+ c,
1309
+ selectedInstances: selection.selectedInstances,
1310
+ path: '/stats',
1311
+ query: forwardQuery,
1312
+ schema: SyncStatsSchema,
1313
+ });
1314
+ if (!fetched.ok) {
1315
+ return fetched.response;
1316
+ }
1317
+ const statsByInstance = new Map();
1318
+ for (const result of fetched.successfulResults) {
1319
+ statsByInstance.set(result.instance.instanceId, result.data);
1320
+ }
1321
+ const statsValues = Array.from(statsByInstance.values());
1322
+ const sum = (selector) => statsValues.reduce((acc, stats) => acc + selector(stats), 0);
1323
+ const minCommitSeqByInstance = {};
1324
+ const maxCommitSeqByInstance = {};
1325
+ for (const [instanceId, stats] of statsByInstance.entries()) {
1326
+ minCommitSeqByInstance[instanceId] = stats.minCommitSeq;
1327
+ maxCommitSeqByInstance[instanceId] = stats.maxCommitSeq;
1328
+ }
1478
1329
  return c.json({
1479
- error: 'NO_INSTANCES_SELECTED',
1480
- message: 'No enabled instances matched the provided instance filter.',
1481
- }, 400);
1482
- }
1483
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1484
- const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamJson({
1485
- c,
1486
- instance,
1487
- path: '/stats',
1488
- query: forwardQuery,
1489
- schema: SyncStatsSchema,
1490
- fetchImpl,
1491
- })));
1492
- const failedInstances = results
1493
- .filter((result) => !result.ok)
1494
- .map((result) => result.failure);
1495
- const successfulResults = results.filter((result) => result.ok);
1496
- if (successfulResults.length === 0) {
1497
- return allInstancesFailedResponse(c, failedInstances);
1498
- }
1499
- const statsByInstance = new Map();
1500
- for (let i = 0; i < selectedInstances.length; i++) {
1501
- const result = results[i];
1502
- if (!result || !result.ok)
1503
- continue;
1504
- const instance = selectedInstances[i];
1505
- if (!instance)
1506
- continue;
1507
- statsByInstance.set(instance.instanceId, result.data);
1508
- }
1509
- const statsValues = Array.from(statsByInstance.values());
1510
- const sum = (selector) => statsValues.reduce((acc, stats) => acc + selector(stats), 0);
1511
- const minCommitSeqByInstance = {};
1512
- const maxCommitSeqByInstance = {};
1513
- for (const [instanceId, stats] of statsByInstance.entries()) {
1514
- minCommitSeqByInstance[instanceId] = stats.minCommitSeq;
1515
- maxCommitSeqByInstance[instanceId] = stats.maxCommitSeq;
1516
- }
1517
- return c.json({
1518
- commitCount: sum((stats) => stats.commitCount),
1519
- changeCount: sum((stats) => stats.changeCount),
1520
- minCommitSeq: Math.min(...statsValues.map((stats) => stats.minCommitSeq)),
1521
- maxCommitSeq: Math.max(...statsValues.map((stats) => stats.maxCommitSeq)),
1522
- clientCount: sum((stats) => stats.clientCount),
1523
- activeClientCount: sum((stats) => stats.activeClientCount),
1524
- minActiveClientCursor: minNullable(statsValues.map((stats) => stats.minActiveClientCursor)),
1525
- maxActiveClientCursor: maxNullable(statsValues.map((stats) => stats.maxActiveClientCursor)),
1526
- minCommitSeqByInstance,
1527
- maxCommitSeqByInstance,
1528
- partial: failedInstances.length > 0,
1529
- failedInstances,
1330
+ commitCount: sum((stats) => stats.commitCount),
1331
+ changeCount: sum((stats) => stats.changeCount),
1332
+ minCommitSeq: Math.min(...statsValues.map((stats) => stats.minCommitSeq)),
1333
+ maxCommitSeq: Math.max(...statsValues.map((stats) => stats.maxCommitSeq)),
1334
+ clientCount: sum((stats) => stats.clientCount),
1335
+ activeClientCount: sum((stats) => stats.activeClientCount),
1336
+ minActiveClientCursor: minNullable(statsValues.map((stats) => stats.minActiveClientCursor)),
1337
+ maxActiveClientCursor: maxNullable(statsValues.map((stats) => stats.maxActiveClientCursor)),
1338
+ minCommitSeqByInstance,
1339
+ maxCommitSeqByInstance,
1340
+ partial: fetched.failedInstances.length > 0,
1341
+ failedInstances: fetched.failedInstances,
1342
+ });
1530
1343
  });
1531
1344
  });
1532
1345
  routes.get('/stats/timeseries', describeRoute({
@@ -1543,40 +1356,30 @@ export function createConsoleGatewayRoutes(options) {
1543
1356
  },
1544
1357
  },
1545
1358
  }), zValidator('query', GatewayTimeseriesQuerySchema), async (c) => {
1546
- const auth = await options.authenticate(c);
1547
- if (!auth) {
1548
- return unauthorizedResponse(c);
1549
- }
1550
- const query = c.req.valid('query');
1551
- const selectedInstances = selectInstances({ instances, query });
1552
- if (selectedInstances.length === 0) {
1359
+ return withGatewayAuth(c, async () => {
1360
+ const query = c.req.valid('query');
1361
+ const selection = selectTargetInstances(c, query);
1362
+ if (!selection.ok) {
1363
+ return selection.response;
1364
+ }
1365
+ const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1366
+ const fetched = await fetchFromSelectedInstances({
1367
+ c,
1368
+ selectedInstances: selection.selectedInstances,
1369
+ path: '/stats/timeseries',
1370
+ query: forwardQuery,
1371
+ schema: TimeseriesStatsResponseSchema,
1372
+ });
1373
+ if (!fetched.ok) {
1374
+ return fetched.response;
1375
+ }
1553
1376
  return c.json({
1554
- error: 'NO_INSTANCES_SELECTED',
1555
- message: 'No enabled instances matched the provided instance filter.',
1556
- }, 400);
1557
- }
1558
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1559
- const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamJson({
1560
- c,
1561
- instance,
1562
- path: '/stats/timeseries',
1563
- query: forwardQuery,
1564
- schema: TimeseriesStatsResponseSchema,
1565
- fetchImpl,
1566
- })));
1567
- const failedInstances = results
1568
- .filter((result) => !result.ok)
1569
- .map((result) => result.failure);
1570
- const successfulResults = results.filter((result) => result.ok);
1571
- if (successfulResults.length === 0) {
1572
- return allInstancesFailedResponse(c, failedInstances);
1573
- }
1574
- return c.json({
1575
- buckets: mergeTimeseriesBuckets(successfulResults.map((result) => result.data)),
1576
- interval: query.interval,
1577
- range: query.range,
1578
- partial: failedInstances.length > 0,
1579
- failedInstances,
1377
+ buckets: mergeTimeseriesBuckets(fetched.successfulResults.map((result) => result.data)),
1378
+ interval: query.interval,
1379
+ range: query.range,
1380
+ partial: fetched.failedInstances.length > 0,
1381
+ failedInstances: fetched.failedInstances,
1382
+ });
1580
1383
  });
1581
1384
  });
1582
1385
  routes.get('/stats/latency', describeRoute({
@@ -1593,40 +1396,30 @@ export function createConsoleGatewayRoutes(options) {
1593
1396
  },
1594
1397
  },
1595
1398
  }), zValidator('query', GatewayLatencyQuerySchema), async (c) => {
1596
- const auth = await options.authenticate(c);
1597
- if (!auth) {
1598
- return unauthorizedResponse(c);
1599
- }
1600
- const query = c.req.valid('query');
1601
- const selectedInstances = selectInstances({ instances, query });
1602
- if (selectedInstances.length === 0) {
1399
+ return withGatewayAuth(c, async () => {
1400
+ const query = c.req.valid('query');
1401
+ const selection = selectTargetInstances(c, query);
1402
+ if (!selection.ok) {
1403
+ return selection.response;
1404
+ }
1405
+ const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1406
+ const fetched = await fetchFromSelectedInstances({
1407
+ c,
1408
+ selectedInstances: selection.selectedInstances,
1409
+ path: '/stats/latency',
1410
+ query: forwardQuery,
1411
+ schema: LatencyStatsResponseSchema,
1412
+ });
1413
+ if (!fetched.ok) {
1414
+ return fetched.response;
1415
+ }
1603
1416
  return c.json({
1604
- error: 'NO_INSTANCES_SELECTED',
1605
- message: 'No enabled instances matched the provided instance filter.',
1606
- }, 400);
1607
- }
1608
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1609
- const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamJson({
1610
- c,
1611
- instance,
1612
- path: '/stats/latency',
1613
- query: forwardQuery,
1614
- schema: LatencyStatsResponseSchema,
1615
- fetchImpl,
1616
- })));
1617
- const failedInstances = results
1618
- .filter((result) => !result.ok)
1619
- .map((result) => result.failure);
1620
- const successfulResults = results.filter((result) => result.ok);
1621
- if (successfulResults.length === 0) {
1622
- return allInstancesFailedResponse(c, failedInstances);
1623
- }
1624
- return c.json({
1625
- push: averagePercentiles(successfulResults.map((result) => result.data.push)),
1626
- pull: averagePercentiles(successfulResults.map((result) => result.data.pull)),
1627
- range: query.range,
1628
- partial: failedInstances.length > 0,
1629
- failedInstances,
1417
+ push: averagePercentiles(fetched.successfulResults.map((result) => result.data.push)),
1418
+ pull: averagePercentiles(fetched.successfulResults.map((result) => result.data.pull)),
1419
+ range: query.range,
1420
+ partial: fetched.failedInstances.length > 0,
1421
+ failedInstances: fetched.failedInstances,
1422
+ });
1630
1423
  });
1631
1424
  });
1632
1425
  routes.get('/commits', describeRoute({
@@ -1643,66 +1436,51 @@ export function createConsoleGatewayRoutes(options) {
1643
1436
  },
1644
1437
  },
1645
1438
  }), zValidator('query', GatewayPaginatedQuerySchema), async (c) => {
1646
- const auth = await options.authenticate(c);
1647
- if (!auth) {
1648
- return unauthorizedResponse(c);
1649
- }
1650
- const query = c.req.valid('query');
1651
- const selectedInstances = selectInstances({ instances, query });
1652
- if (selectedInstances.length === 0) {
1439
+ return withGatewayAuth(c, async () => {
1440
+ const query = c.req.valid('query');
1441
+ const selection = selectTargetInstances(c, query);
1442
+ if (!selection.ok) {
1443
+ return selection.response;
1444
+ }
1445
+ const targetCount = query.offset + query.limit;
1446
+ const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1447
+ forwardQuery.delete('limit');
1448
+ forwardQuery.delete('offset');
1449
+ const pageSchema = ConsolePaginatedResponseSchema(ConsoleCommitListItemSchema);
1450
+ const fetched = await fetchPagedFromSelectedInstances({
1451
+ c,
1452
+ selectedInstances: selection.selectedInstances,
1453
+ path: '/commits',
1454
+ query: forwardQuery,
1455
+ targetCount,
1456
+ schema: pageSchema,
1457
+ });
1458
+ if (!fetched.ok) {
1459
+ return fetched.response;
1460
+ }
1461
+ const merged = fetched.successfulResults
1462
+ .flatMap(({ items, instance }) => items.map((commit) => ({
1463
+ ...commit,
1464
+ instanceId: instance.instanceId,
1465
+ federatedCommitId: `${instance.instanceId}:${commit.commitSeq}`,
1466
+ })))
1467
+ .sort((a, b) => {
1468
+ const byTime = compareIsoDesc(a.createdAt, b.createdAt);
1469
+ if (byTime !== 0)
1470
+ return byTime;
1471
+ const byInstance = a.instanceId.localeCompare(b.instanceId);
1472
+ if (byInstance !== 0)
1473
+ return byInstance;
1474
+ return b.commitSeq - a.commitSeq;
1475
+ });
1653
1476
  return c.json({
1654
- error: 'NO_INSTANCES_SELECTED',
1655
- message: 'No enabled instances matched the provided instance filter.',
1656
- }, 400);
1657
- }
1658
- const targetCount = query.offset + query.limit;
1659
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1660
- forwardQuery.delete('limit');
1661
- forwardQuery.delete('offset');
1662
- const pageSchema = ConsolePaginatedResponseSchema(ConsoleCommitListItemSchema);
1663
- const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamPaged({
1664
- c,
1665
- instance,
1666
- path: '/commits',
1667
- query: forwardQuery,
1668
- targetCount,
1669
- schema: pageSchema,
1670
- fetchImpl,
1671
- })));
1672
- const failedInstances = results
1673
- .filter((result) => !result.ok)
1674
- .map((result) => result.failure);
1675
- const successful = results
1676
- .map((result, index) => ({
1677
- result,
1678
- instance: selectedInstances[index],
1679
- }))
1680
- .filter((entry) => Boolean(entry.instance) && entry.result.ok);
1681
- if (successful.length === 0) {
1682
- return allInstancesFailedResponse(c, failedInstances);
1683
- }
1684
- const merged = successful
1685
- .flatMap(({ result, instance }) => result.items.map((commit) => ({
1686
- ...commit,
1687
- instanceId: instance.instanceId,
1688
- federatedCommitId: `${instance.instanceId}:${commit.commitSeq}`,
1689
- })))
1690
- .sort((a, b) => {
1691
- const byTime = compareIsoDesc(a.createdAt, b.createdAt);
1692
- if (byTime !== 0)
1693
- return byTime;
1694
- const byInstance = a.instanceId.localeCompare(b.instanceId);
1695
- if (byInstance !== 0)
1696
- return byInstance;
1697
- return b.commitSeq - a.commitSeq;
1698
- });
1699
- return c.json({
1700
- items: merged.slice(query.offset, query.offset + query.limit),
1701
- total: successful.reduce((acc, entry) => acc + entry.result.total, 0),
1702
- offset: query.offset,
1703
- limit: query.limit,
1704
- partial: failedInstances.length > 0,
1705
- failedInstances,
1477
+ items: merged.slice(query.offset, query.offset + query.limit),
1478
+ total: fetched.successfulResults.reduce((acc, entry) => acc + entry.total, 0),
1479
+ offset: query.offset,
1480
+ limit: query.limit,
1481
+ partial: fetched.failedInstances.length > 0,
1482
+ failedInstances: fetched.failedInstances,
1483
+ });
1706
1484
  });
1707
1485
  });
1708
1486
  routes.get('/commits/:seq', describeRoute({
@@ -1719,42 +1497,40 @@ export function createConsoleGatewayRoutes(options) {
1719
1497
  },
1720
1498
  },
1721
1499
  }), zValidator('param', GatewayCommitPathParamSchema), zValidator('query', ConsolePartitionQuerySchema.extend(GatewayInstanceFilterSchema.shape)), async (c) => {
1722
- const auth = await options.authenticate(c);
1723
- if (!auth) {
1724
- return unauthorizedResponse(c);
1725
- }
1726
- const { seq } = c.req.valid('param');
1727
- const query = c.req.valid('query');
1728
- const target = resolveCommitTarget({ seq, instances, query });
1729
- if (!target.ok) {
1730
- return c.json({
1731
- error: target.error,
1732
- ...(target.message ? { message: target.message } : {}),
1733
- }, target.status);
1734
- }
1735
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1736
- const result = await fetchDownstreamJson({
1737
- c,
1738
- instance: target.instance,
1739
- path: `/commits/${target.localCommitSeq}`,
1740
- query: forwardQuery,
1741
- schema: ConsoleCommitDetailSchema,
1742
- fetchImpl,
1743
- });
1744
- if (!result.ok) {
1745
- if (result.failure.status === 404) {
1746
- return c.json({ error: 'NOT_FOUND' }, 404);
1500
+ return withGatewayAuth(c, async () => {
1501
+ const { seq } = c.req.valid('param');
1502
+ const query = c.req.valid('query');
1503
+ const target = resolveCommitTarget({ seq, instances, query });
1504
+ if (!target.ok) {
1505
+ return c.json({
1506
+ error: target.error,
1507
+ ...(target.message ? { message: target.message } : {}),
1508
+ }, target.status);
1509
+ }
1510
+ const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1511
+ const result = await fetchDownstreamJson({
1512
+ c,
1513
+ instance: target.instance,
1514
+ path: `/commits/${target.localCommitSeq}`,
1515
+ query: forwardQuery,
1516
+ schema: ConsoleCommitDetailSchema,
1517
+ fetchImpl,
1518
+ });
1519
+ if (!result.ok) {
1520
+ if (result.failure.status === 404) {
1521
+ return c.json({ error: 'NOT_FOUND' }, 404);
1522
+ }
1523
+ return c.json({
1524
+ error: 'DOWNSTREAM_UNAVAILABLE',
1525
+ failedInstances: [result.failure],
1526
+ }, 502);
1747
1527
  }
1748
1528
  return c.json({
1749
- error: 'DOWNSTREAM_UNAVAILABLE',
1750
- failedInstances: [result.failure],
1751
- }, 502);
1752
- }
1753
- return c.json({
1754
- ...result.data,
1755
- instanceId: target.instance.instanceId,
1756
- federatedCommitId: `${target.instance.instanceId}:${result.data.commitSeq}`,
1757
- localCommitSeq: result.data.commitSeq,
1529
+ ...result.data,
1530
+ instanceId: target.instance.instanceId,
1531
+ federatedCommitId: `${target.instance.instanceId}:${result.data.commitSeq}`,
1532
+ localCommitSeq: result.data.commitSeq,
1533
+ });
1758
1534
  });
1759
1535
  });
1760
1536
  routes.get('/clients', describeRoute({
@@ -1771,66 +1547,51 @@ export function createConsoleGatewayRoutes(options) {
1771
1547
  },
1772
1548
  },
1773
1549
  }), zValidator('query', GatewayPaginatedQuerySchema), async (c) => {
1774
- const auth = await options.authenticate(c);
1775
- if (!auth) {
1776
- return unauthorizedResponse(c);
1777
- }
1778
- const query = c.req.valid('query');
1779
- const selectedInstances = selectInstances({ instances, query });
1780
- if (selectedInstances.length === 0) {
1550
+ return withGatewayAuth(c, async () => {
1551
+ const query = c.req.valid('query');
1552
+ const selection = selectTargetInstances(c, query);
1553
+ if (!selection.ok) {
1554
+ return selection.response;
1555
+ }
1556
+ const targetCount = query.offset + query.limit;
1557
+ const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1558
+ forwardQuery.delete('limit');
1559
+ forwardQuery.delete('offset');
1560
+ const pageSchema = ConsolePaginatedResponseSchema(ConsoleClientSchema);
1561
+ const fetched = await fetchPagedFromSelectedInstances({
1562
+ c,
1563
+ selectedInstances: selection.selectedInstances,
1564
+ path: '/clients',
1565
+ query: forwardQuery,
1566
+ targetCount,
1567
+ schema: pageSchema,
1568
+ });
1569
+ if (!fetched.ok) {
1570
+ return fetched.response;
1571
+ }
1572
+ const merged = fetched.successfulResults
1573
+ .flatMap(({ items, instance }) => items.map((client) => ({
1574
+ ...client,
1575
+ instanceId: instance.instanceId,
1576
+ federatedClientId: `${instance.instanceId}:${client.clientId}`,
1577
+ })))
1578
+ .sort((a, b) => {
1579
+ const byTime = compareIsoDesc(a.updatedAt, b.updatedAt);
1580
+ if (byTime !== 0)
1581
+ return byTime;
1582
+ const byInstance = a.instanceId.localeCompare(b.instanceId);
1583
+ if (byInstance !== 0)
1584
+ return byInstance;
1585
+ return a.clientId.localeCompare(b.clientId);
1586
+ });
1781
1587
  return c.json({
1782
- error: 'NO_INSTANCES_SELECTED',
1783
- message: 'No enabled instances matched the provided instance filter.',
1784
- }, 400);
1785
- }
1786
- const targetCount = query.offset + query.limit;
1787
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1788
- forwardQuery.delete('limit');
1789
- forwardQuery.delete('offset');
1790
- const pageSchema = ConsolePaginatedResponseSchema(ConsoleClientSchema);
1791
- const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamPaged({
1792
- c,
1793
- instance,
1794
- path: '/clients',
1795
- query: forwardQuery,
1796
- targetCount,
1797
- schema: pageSchema,
1798
- fetchImpl,
1799
- })));
1800
- const failedInstances = results
1801
- .filter((result) => !result.ok)
1802
- .map((result) => result.failure);
1803
- const successful = results
1804
- .map((result, index) => ({
1805
- result,
1806
- instance: selectedInstances[index],
1807
- }))
1808
- .filter((entry) => Boolean(entry.instance) && entry.result.ok);
1809
- if (successful.length === 0) {
1810
- return allInstancesFailedResponse(c, failedInstances);
1811
- }
1812
- const merged = successful
1813
- .flatMap(({ result, instance }) => result.items.map((client) => ({
1814
- ...client,
1815
- instanceId: instance.instanceId,
1816
- federatedClientId: `${instance.instanceId}:${client.clientId}`,
1817
- })))
1818
- .sort((a, b) => {
1819
- const byTime = compareIsoDesc(a.updatedAt, b.updatedAt);
1820
- if (byTime !== 0)
1821
- return byTime;
1822
- const byInstance = a.instanceId.localeCompare(b.instanceId);
1823
- if (byInstance !== 0)
1824
- return byInstance;
1825
- return a.clientId.localeCompare(b.clientId);
1826
- });
1827
- return c.json({
1828
- items: merged.slice(query.offset, query.offset + query.limit),
1829
- total: successful.reduce((acc, entry) => acc + entry.result.total, 0),
1830
- offset: query.offset,
1831
- limit: query.limit,
1832
- partial: failedInstances.length > 0,
1833
- failedInstances,
1588
+ items: merged.slice(query.offset, query.offset + query.limit),
1589
+ total: fetched.successfulResults.reduce((acc, entry) => acc + entry.total, 0),
1590
+ offset: query.offset,
1591
+ limit: query.limit,
1592
+ partial: fetched.failedInstances.length > 0,
1593
+ failedInstances: fetched.failedInstances,
1594
+ });
1834
1595
  });
1835
1596
  });
1836
1597
  routes.get('/timeline', describeRoute({
@@ -1847,77 +1608,64 @@ export function createConsoleGatewayRoutes(options) {
1847
1608
  },
1848
1609
  },
1849
1610
  }), zValidator('query', GatewayTimelineQuerySchema), async (c) => {
1850
- const auth = await options.authenticate(c);
1851
- if (!auth) {
1852
- return unauthorizedResponse(c);
1853
- }
1854
- const query = c.req.valid('query');
1855
- const selectedInstances = selectInstances({ instances, query });
1856
- if (selectedInstances.length === 0) {
1611
+ return withGatewayAuth(c, async () => {
1612
+ const query = c.req.valid('query');
1613
+ const selection = selectTargetInstances(c, query);
1614
+ if (!selection.ok) {
1615
+ return selection.response;
1616
+ }
1617
+ const targetCount = query.offset + query.limit;
1618
+ const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1619
+ forwardQuery.delete('limit');
1620
+ forwardQuery.delete('offset');
1621
+ const pageSchema = ConsolePaginatedResponseSchema(ConsoleTimelineItemSchema);
1622
+ const fetched = await fetchPagedFromSelectedInstances({
1623
+ c,
1624
+ selectedInstances: selection.selectedInstances,
1625
+ path: '/timeline',
1626
+ query: forwardQuery,
1627
+ targetCount,
1628
+ schema: pageSchema,
1629
+ });
1630
+ if (!fetched.ok) {
1631
+ return fetched.response;
1632
+ }
1633
+ const merged = fetched.successfulResults
1634
+ .flatMap(({ items, instance }) => items.map((item) => {
1635
+ const localCommitSeq = item.type === 'commit'
1636
+ ? (item.commit?.commitSeq ?? null)
1637
+ : null;
1638
+ const localEventId = item.type === 'event' ? (item.event?.eventId ?? null) : null;
1639
+ const localIdSegment = item.type === 'commit'
1640
+ ? String(localCommitSeq ?? 'unknown')
1641
+ : String(localEventId ?? 'unknown');
1642
+ return {
1643
+ ...item,
1644
+ instanceId: instance.instanceId,
1645
+ federatedTimelineId: `${instance.instanceId}:${item.type}:${localIdSegment}`,
1646
+ localCommitSeq,
1647
+ localEventId,
1648
+ };
1649
+ }))
1650
+ .sort((a, b) => {
1651
+ const byTime = compareIsoDesc(a.timestamp, b.timestamp);
1652
+ if (byTime !== 0)
1653
+ return byTime;
1654
+ const byInstance = a.instanceId.localeCompare(b.instanceId);
1655
+ if (byInstance !== 0)
1656
+ return byInstance;
1657
+ const aLocalId = a.localCommitSeq ?? a.localEventId ?? 0;
1658
+ const bLocalId = b.localCommitSeq ?? b.localEventId ?? 0;
1659
+ return bLocalId - aLocalId;
1660
+ });
1857
1661
  return c.json({
1858
- error: 'NO_INSTANCES_SELECTED',
1859
- message: 'No enabled instances matched the provided instance filter.',
1860
- }, 400);
1861
- }
1862
- const targetCount = query.offset + query.limit;
1863
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1864
- forwardQuery.delete('limit');
1865
- forwardQuery.delete('offset');
1866
- const pageSchema = ConsolePaginatedResponseSchema(ConsoleTimelineItemSchema);
1867
- const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamPaged({
1868
- c,
1869
- instance,
1870
- path: '/timeline',
1871
- query: forwardQuery,
1872
- targetCount,
1873
- schema: pageSchema,
1874
- fetchImpl,
1875
- })));
1876
- const failedInstances = results
1877
- .filter((result) => !result.ok)
1878
- .map((result) => result.failure);
1879
- const successful = results
1880
- .map((result, index) => ({
1881
- result,
1882
- instance: selectedInstances[index],
1883
- }))
1884
- .filter((entry) => Boolean(entry.instance) && entry.result.ok);
1885
- if (successful.length === 0) {
1886
- return allInstancesFailedResponse(c, failedInstances);
1887
- }
1888
- const merged = successful
1889
- .flatMap(({ result, instance }) => result.items.map((item) => {
1890
- const localCommitSeq = item.type === 'commit' ? (item.commit?.commitSeq ?? null) : null;
1891
- const localEventId = item.type === 'event' ? (item.event?.eventId ?? null) : null;
1892
- const localIdSegment = item.type === 'commit'
1893
- ? String(localCommitSeq ?? 'unknown')
1894
- : String(localEventId ?? 'unknown');
1895
- return {
1896
- ...item,
1897
- instanceId: instance.instanceId,
1898
- federatedTimelineId: `${instance.instanceId}:${item.type}:${localIdSegment}`,
1899
- localCommitSeq,
1900
- localEventId,
1901
- };
1902
- }))
1903
- .sort((a, b) => {
1904
- const byTime = compareIsoDesc(a.timestamp, b.timestamp);
1905
- if (byTime !== 0)
1906
- return byTime;
1907
- const byInstance = a.instanceId.localeCompare(b.instanceId);
1908
- if (byInstance !== 0)
1909
- return byInstance;
1910
- const aLocalId = a.localCommitSeq ?? a.localEventId ?? 0;
1911
- const bLocalId = b.localCommitSeq ?? b.localEventId ?? 0;
1912
- return bLocalId - aLocalId;
1913
- });
1914
- return c.json({
1915
- items: merged.slice(query.offset, query.offset + query.limit),
1916
- total: successful.reduce((acc, entry) => acc + entry.result.total, 0),
1917
- offset: query.offset,
1918
- limit: query.limit,
1919
- partial: failedInstances.length > 0,
1920
- failedInstances,
1662
+ items: merged.slice(query.offset, query.offset + query.limit),
1663
+ total: fetched.successfulResults.reduce((acc, entry) => acc + entry.total, 0),
1664
+ offset: query.offset,
1665
+ limit: query.limit,
1666
+ partial: fetched.failedInstances.length > 0,
1667
+ failedInstances: fetched.failedInstances,
1668
+ });
1921
1669
  });
1922
1670
  });
1923
1671
  routes.get('/operations', describeRoute({
@@ -1934,67 +1682,52 @@ export function createConsoleGatewayRoutes(options) {
1934
1682
  },
1935
1683
  },
1936
1684
  }), zValidator('query', GatewayOperationsQuerySchema), async (c) => {
1937
- const auth = await options.authenticate(c);
1938
- if (!auth) {
1939
- return unauthorizedResponse(c);
1940
- }
1941
- const query = c.req.valid('query');
1942
- const selectedInstances = selectInstances({ instances, query });
1943
- if (selectedInstances.length === 0) {
1685
+ return withGatewayAuth(c, async () => {
1686
+ const query = c.req.valid('query');
1687
+ const selection = selectTargetInstances(c, query);
1688
+ if (!selection.ok) {
1689
+ return selection.response;
1690
+ }
1691
+ const targetCount = query.offset + query.limit;
1692
+ const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1693
+ forwardQuery.delete('limit');
1694
+ forwardQuery.delete('offset');
1695
+ const pageSchema = ConsolePaginatedResponseSchema(ConsoleOperationEventSchema);
1696
+ const fetched = await fetchPagedFromSelectedInstances({
1697
+ c,
1698
+ selectedInstances: selection.selectedInstances,
1699
+ path: '/operations',
1700
+ query: forwardQuery,
1701
+ targetCount,
1702
+ schema: pageSchema,
1703
+ });
1704
+ if (!fetched.ok) {
1705
+ return fetched.response;
1706
+ }
1707
+ const merged = fetched.successfulResults
1708
+ .flatMap(({ items, instance }) => items.map((operation) => ({
1709
+ ...operation,
1710
+ instanceId: instance.instanceId,
1711
+ federatedOperationId: `${instance.instanceId}:${operation.operationId}`,
1712
+ localOperationId: operation.operationId,
1713
+ })))
1714
+ .sort((a, b) => {
1715
+ const byTime = compareIsoDesc(a.createdAt, b.createdAt);
1716
+ if (byTime !== 0)
1717
+ return byTime;
1718
+ const byInstance = a.instanceId.localeCompare(b.instanceId);
1719
+ if (byInstance !== 0)
1720
+ return byInstance;
1721
+ return b.localOperationId - a.localOperationId;
1722
+ });
1944
1723
  return c.json({
1945
- error: 'NO_INSTANCES_SELECTED',
1946
- message: 'No enabled instances matched the provided instance filter.',
1947
- }, 400);
1948
- }
1949
- const targetCount = query.offset + query.limit;
1950
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1951
- forwardQuery.delete('limit');
1952
- forwardQuery.delete('offset');
1953
- const pageSchema = ConsolePaginatedResponseSchema(ConsoleOperationEventSchema);
1954
- const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamPaged({
1955
- c,
1956
- instance,
1957
- path: '/operations',
1958
- query: forwardQuery,
1959
- targetCount,
1960
- schema: pageSchema,
1961
- fetchImpl,
1962
- })));
1963
- const failedInstances = results
1964
- .filter((result) => !result.ok)
1965
- .map((result) => result.failure);
1966
- const successful = results
1967
- .map((result, index) => ({
1968
- result,
1969
- instance: selectedInstances[index],
1970
- }))
1971
- .filter((entry) => Boolean(entry.instance) && entry.result.ok);
1972
- if (successful.length === 0) {
1973
- return allInstancesFailedResponse(c, failedInstances);
1974
- }
1975
- const merged = successful
1976
- .flatMap(({ result, instance }) => result.items.map((operation) => ({
1977
- ...operation,
1978
- instanceId: instance.instanceId,
1979
- federatedOperationId: `${instance.instanceId}:${operation.operationId}`,
1980
- localOperationId: operation.operationId,
1981
- })))
1982
- .sort((a, b) => {
1983
- const byTime = compareIsoDesc(a.createdAt, b.createdAt);
1984
- if (byTime !== 0)
1985
- return byTime;
1986
- const byInstance = a.instanceId.localeCompare(b.instanceId);
1987
- if (byInstance !== 0)
1988
- return byInstance;
1989
- return b.localOperationId - a.localOperationId;
1990
- });
1991
- return c.json({
1992
- items: merged.slice(query.offset, query.offset + query.limit),
1993
- total: successful.reduce((acc, entry) => acc + entry.result.total, 0),
1994
- offset: query.offset,
1995
- limit: query.limit,
1996
- partial: failedInstances.length > 0,
1997
- failedInstances,
1724
+ items: merged.slice(query.offset, query.offset + query.limit),
1725
+ total: fetched.successfulResults.reduce((acc, entry) => acc + entry.total, 0),
1726
+ offset: query.offset,
1727
+ limit: query.limit,
1728
+ partial: fetched.failedInstances.length > 0,
1729
+ failedInstances: fetched.failedInstances,
1730
+ });
1998
1731
  });
1999
1732
  });
2000
1733
  routes.get('/events', describeRoute({
@@ -2011,67 +1744,52 @@ export function createConsoleGatewayRoutes(options) {
2011
1744
  },
2012
1745
  },
2013
1746
  }), zValidator('query', GatewayEventsQuerySchema), async (c) => {
2014
- const auth = await options.authenticate(c);
2015
- if (!auth) {
2016
- return unauthorizedResponse(c);
2017
- }
2018
- const query = c.req.valid('query');
2019
- const selectedInstances = selectInstances({ instances, query });
2020
- if (selectedInstances.length === 0) {
1747
+ return withGatewayAuth(c, async () => {
1748
+ const query = c.req.valid('query');
1749
+ const selection = selectTargetInstances(c, query);
1750
+ if (!selection.ok) {
1751
+ return selection.response;
1752
+ }
1753
+ const targetCount = query.offset + query.limit;
1754
+ const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
1755
+ forwardQuery.delete('limit');
1756
+ forwardQuery.delete('offset');
1757
+ const pageSchema = ConsolePaginatedResponseSchema(ConsoleRequestEventSchema);
1758
+ const fetched = await fetchPagedFromSelectedInstances({
1759
+ c,
1760
+ selectedInstances: selection.selectedInstances,
1761
+ path: '/events',
1762
+ query: forwardQuery,
1763
+ targetCount,
1764
+ schema: pageSchema,
1765
+ });
1766
+ if (!fetched.ok) {
1767
+ return fetched.response;
1768
+ }
1769
+ const merged = fetched.successfulResults
1770
+ .flatMap(({ items, instance }) => items.map((event) => ({
1771
+ ...event,
1772
+ instanceId: instance.instanceId,
1773
+ federatedEventId: `${instance.instanceId}:${event.eventId}`,
1774
+ localEventId: event.eventId,
1775
+ })))
1776
+ .sort((a, b) => {
1777
+ const byTime = compareIsoDesc(a.createdAt, b.createdAt);
1778
+ if (byTime !== 0)
1779
+ return byTime;
1780
+ const byInstance = a.instanceId.localeCompare(b.instanceId);
1781
+ if (byInstance !== 0)
1782
+ return byInstance;
1783
+ return b.localEventId - a.localEventId;
1784
+ });
2021
1785
  return c.json({
2022
- error: 'NO_INSTANCES_SELECTED',
2023
- message: 'No enabled instances matched the provided instance filter.',
2024
- }, 400);
2025
- }
2026
- const targetCount = query.offset + query.limit;
2027
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
2028
- forwardQuery.delete('limit');
2029
- forwardQuery.delete('offset');
2030
- const pageSchema = ConsolePaginatedResponseSchema(ConsoleRequestEventSchema);
2031
- const results = await Promise.all(selectedInstances.map((instance) => fetchDownstreamPaged({
2032
- c,
2033
- instance,
2034
- path: '/events',
2035
- query: forwardQuery,
2036
- targetCount,
2037
- schema: pageSchema,
2038
- fetchImpl,
2039
- })));
2040
- const failedInstances = results
2041
- .filter((result) => !result.ok)
2042
- .map((result) => result.failure);
2043
- const successful = results
2044
- .map((result, index) => ({
2045
- result,
2046
- instance: selectedInstances[index],
2047
- }))
2048
- .filter((entry) => Boolean(entry.instance) && entry.result.ok);
2049
- if (successful.length === 0) {
2050
- return allInstancesFailedResponse(c, failedInstances);
2051
- }
2052
- const merged = successful
2053
- .flatMap(({ result, instance }) => result.items.map((event) => ({
2054
- ...event,
2055
- instanceId: instance.instanceId,
2056
- federatedEventId: `${instance.instanceId}:${event.eventId}`,
2057
- localEventId: event.eventId,
2058
- })))
2059
- .sort((a, b) => {
2060
- const byTime = compareIsoDesc(a.createdAt, b.createdAt);
2061
- if (byTime !== 0)
2062
- return byTime;
2063
- const byInstance = a.instanceId.localeCompare(b.instanceId);
2064
- if (byInstance !== 0)
2065
- return byInstance;
2066
- return b.localEventId - a.localEventId;
2067
- });
2068
- return c.json({
2069
- items: merged.slice(query.offset, query.offset + query.limit),
2070
- total: successful.reduce((acc, entry) => acc + entry.result.total, 0),
2071
- offset: query.offset,
2072
- limit: query.limit,
2073
- partial: failedInstances.length > 0,
2074
- failedInstances,
1786
+ items: merged.slice(query.offset, query.offset + query.limit),
1787
+ total: fetched.successfulResults.reduce((acc, entry) => acc + entry.total, 0),
1788
+ offset: query.offset,
1789
+ limit: query.limit,
1790
+ partial: fetched.failedInstances.length > 0,
1791
+ failedInstances: fetched.failedInstances,
1792
+ });
2075
1793
  });
2076
1794
  });
2077
1795
  if (options.websocket?.enabled &&
@@ -2112,15 +1830,6 @@ export function createConsoleGatewayRoutes(options) {
2112
1830
  };
2113
1831
  return options.authenticate(authContext);
2114
1832
  };
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
- };
2124
1833
  const cleanup = (ws) => {
2125
1834
  const state = liveState.get(ws);
2126
1835
  if (!state)
@@ -2156,6 +1865,7 @@ export function createConsoleGatewayRoutes(options) {
2156
1865
  heartbeatInterval: null,
2157
1866
  authTimeout: null,
2158
1867
  isAuthenticated: false,
1868
+ startAuthenticatedSession: null,
2159
1869
  };
2160
1870
  liveState.set(ws, state);
2161
1871
  const startAuthenticatedSession = (upstreamBearerToken) => {
@@ -2259,6 +1969,7 @@ export function createConsoleGatewayRoutes(options) {
2259
1969
  }, heartbeatIntervalMs);
2260
1970
  state.heartbeatInterval = heartbeatInterval;
2261
1971
  };
1972
+ state.startAuthenticatedSession = startAuthenticatedSession;
2262
1973
  if (initialAuth) {
2263
1974
  startAuthenticatedSession(parseBearerToken(c.req.header('Authorization')));
2264
1975
  return;
@@ -2268,7 +1979,7 @@ export function createConsoleGatewayRoutes(options) {
2268
1979
  if (!current || current.isAuthenticated) {
2269
1980
  return;
2270
1981
  }
2271
- closeUnauthenticated(ws);
1982
+ closeUnauthenticatedSocket(ws);
2272
1983
  cleanup(ws);
2273
1984
  }, 5_000);
2274
1985
  },
@@ -2278,24 +1989,13 @@ export function createConsoleGatewayRoutes(options) {
2278
1989
  return;
2279
1990
  }
2280
1991
  if (typeof event.data !== 'string') {
2281
- closeUnauthenticated(ws);
1992
+ closeUnauthenticatedSocket(ws);
2282
1993
  cleanup(ws);
2283
1994
  return;
2284
1995
  }
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
- }
1996
+ const token = parseWebSocketAuthToken(event.data);
2297
1997
  if (!token) {
2298
- closeUnauthenticated(ws);
1998
+ closeUnauthenticatedSocket(ws);
2299
1999
  cleanup(ws);
2300
2000
  return;
2301
2001
  }
@@ -2305,108 +2005,11 @@ export function createConsoleGatewayRoutes(options) {
2305
2005
  return;
2306
2006
  }
2307
2007
  if (!auth) {
2308
- closeUnauthenticated(ws);
2008
+ closeUnauthenticatedSocket(ws);
2309
2009
  cleanup(ws);
2310
2010
  return;
2311
2011
  }
2312
- current.isAuthenticated = true;
2313
- if (current.authTimeout) {
2314
- clearTimeout(current.authTimeout);
2315
- current.authTimeout = null;
2316
- }
2317
- for (const instance of selectedInstances) {
2318
- const downstreamQuery = new URLSearchParams();
2319
- if (partitionId) {
2320
- downstreamQuery.set('partitionId', partitionId);
2321
- }
2322
- if (replaySince) {
2323
- downstreamQuery.set('since', replaySince);
2324
- }
2325
- downstreamQuery.set('replayLimit', String(replayLimit));
2326
- const downstreamUrl = buildConsoleEndpointUrl({
2327
- instance,
2328
- requestUrl: c.req.url,
2329
- path: '/events/live',
2330
- query: downstreamQuery,
2331
- });
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
- }
2349
- downstreamSocket.onmessage = (message) => {
2350
- if (typeof message.data !== 'string') {
2351
- return;
2352
- }
2353
- try {
2354
- const payload = JSON.parse(message.data);
2355
- if (typeof payload.type === 'string' &&
2356
- (payload.type === 'connected' ||
2357
- payload.type === 'heartbeat')) {
2358
- return;
2359
- }
2360
- const payloadData = payload.data &&
2361
- typeof payload.data === 'object' &&
2362
- !Array.isArray(payload.data)
2363
- ? { ...payload.data, instanceId: instance.instanceId }
2364
- : { instanceId: instance.instanceId };
2365
- const liveEvent = {
2366
- ...payload,
2367
- data: payloadData,
2368
- instanceId: instance.instanceId,
2369
- timestamp: typeof payload.timestamp === 'string'
2370
- ? payload.timestamp
2371
- : new Date().toISOString(),
2372
- };
2373
- ws.send(JSON.stringify(liveEvent));
2374
- }
2375
- catch {
2376
- // Ignore malformed downstream events
2377
- }
2378
- };
2379
- downstreamSocket.onerror = () => {
2380
- try {
2381
- ws.send(JSON.stringify({
2382
- type: 'instance_error',
2383
- instanceId: instance.instanceId,
2384
- timestamp: new Date().toISOString(),
2385
- }));
2386
- }
2387
- catch {
2388
- // ignore send errors
2389
- }
2390
- };
2391
- current.downstreamSockets.push(downstreamSocket);
2392
- }
2393
- ws.send(JSON.stringify({
2394
- type: 'connected',
2395
- timestamp: new Date().toISOString(),
2396
- instanceCount: selectedInstances.length,
2397
- }));
2398
- const heartbeatInterval = setInterval(() => {
2399
- try {
2400
- ws.send(JSON.stringify({
2401
- type: 'heartbeat',
2402
- timestamp: new Date().toISOString(),
2403
- }));
2404
- }
2405
- catch {
2406
- clearInterval(heartbeatInterval);
2407
- }
2408
- }, heartbeatIntervalMs);
2409
- current.heartbeatInterval = heartbeatInterval;
2012
+ current.startAuthenticatedSession?.(token);
2410
2013
  },
2411
2014
  onClose(_event, ws) {
2412
2015
  cleanup(ws);
@@ -2431,46 +2034,44 @@ export function createConsoleGatewayRoutes(options) {
2431
2034
  },
2432
2035
  },
2433
2036
  }), zValidator('param', GatewayEventPathParamSchema), zValidator('query', ConsolePartitionQuerySchema.extend(GatewayInstanceFilterSchema.shape)), async (c) => {
2434
- const auth = await options.authenticate(c);
2435
- if (!auth) {
2436
- return unauthorizedResponse(c);
2437
- }
2438
- const { id } = c.req.valid('param');
2439
- const query = c.req.valid('query');
2440
- const target = resolveEventTarget({
2441
- id,
2442
- instances,
2443
- query,
2444
- });
2445
- if (!target.ok) {
2446
- return c.json({
2447
- error: target.error,
2448
- ...(target.message ? { message: target.message } : {}),
2449
- }, target.status);
2450
- }
2451
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
2452
- const result = await fetchDownstreamJson({
2453
- c,
2454
- instance: target.instance,
2455
- path: `/events/${target.localEventId}`,
2456
- query: forwardQuery,
2457
- schema: ConsoleRequestEventSchema,
2458
- fetchImpl,
2459
- });
2460
- if (!result.ok) {
2461
- if (result.failure.status === 404) {
2462
- return c.json({ error: 'NOT_FOUND' }, 404);
2037
+ return withGatewayAuth(c, async () => {
2038
+ const { id } = c.req.valid('param');
2039
+ const query = c.req.valid('query');
2040
+ const target = resolveEventTarget({
2041
+ id,
2042
+ instances,
2043
+ query,
2044
+ });
2045
+ if (!target.ok) {
2046
+ return c.json({
2047
+ error: target.error,
2048
+ ...(target.message ? { message: target.message } : {}),
2049
+ }, target.status);
2050
+ }
2051
+ const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
2052
+ const result = await fetchDownstreamJson({
2053
+ c,
2054
+ instance: target.instance,
2055
+ path: `/events/${target.localEventId}`,
2056
+ query: forwardQuery,
2057
+ schema: ConsoleRequestEventSchema,
2058
+ fetchImpl,
2059
+ });
2060
+ if (!result.ok) {
2061
+ if (result.failure.status === 404) {
2062
+ return c.json({ error: 'NOT_FOUND' }, 404);
2063
+ }
2064
+ return c.json({
2065
+ error: 'DOWNSTREAM_UNAVAILABLE',
2066
+ failedInstances: [result.failure],
2067
+ }, 502);
2463
2068
  }
2464
2069
  return c.json({
2465
- error: 'DOWNSTREAM_UNAVAILABLE',
2466
- failedInstances: [result.failure],
2467
- }, 502);
2468
- }
2469
- return c.json({
2470
- ...result.data,
2471
- instanceId: target.instance.instanceId,
2472
- federatedEventId: `${target.instance.instanceId}:${result.data.eventId}`,
2473
- localEventId: result.data.eventId,
2070
+ ...result.data,
2071
+ instanceId: target.instance.instanceId,
2072
+ federatedEventId: `${target.instance.instanceId}:${result.data.eventId}`,
2073
+ localEventId: result.data.eventId,
2074
+ });
2474
2075
  });
2475
2076
  });
2476
2077
  routes.get('/events/:id/payload', describeRoute({
@@ -2487,46 +2088,44 @@ export function createConsoleGatewayRoutes(options) {
2487
2088
  },
2488
2089
  },
2489
2090
  }), zValidator('param', GatewayEventPathParamSchema), zValidator('query', ConsolePartitionQuerySchema.extend(GatewayInstanceFilterSchema.shape)), async (c) => {
2490
- const auth = await options.authenticate(c);
2491
- if (!auth) {
2492
- return unauthorizedResponse(c);
2493
- }
2494
- const { id } = c.req.valid('param');
2495
- const query = c.req.valid('query');
2496
- const target = resolveEventTarget({
2497
- id,
2498
- instances,
2499
- query,
2500
- });
2501
- if (!target.ok) {
2502
- return c.json({
2503
- error: target.error,
2504
- ...(target.message ? { message: target.message } : {}),
2505
- }, target.status);
2506
- }
2507
- const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
2508
- const result = await fetchDownstreamJson({
2509
- c,
2510
- instance: target.instance,
2511
- path: `/events/${target.localEventId}/payload`,
2512
- query: forwardQuery,
2513
- schema: ConsoleRequestPayloadSchema,
2514
- fetchImpl,
2515
- });
2516
- if (!result.ok) {
2517
- if (result.failure.status === 404) {
2518
- return c.json({ error: 'NOT_FOUND' }, 404);
2091
+ return withGatewayAuth(c, async () => {
2092
+ const { id } = c.req.valid('param');
2093
+ const query = c.req.valid('query');
2094
+ const target = resolveEventTarget({
2095
+ id,
2096
+ instances,
2097
+ query,
2098
+ });
2099
+ if (!target.ok) {
2100
+ return c.json({
2101
+ error: target.error,
2102
+ ...(target.message ? { message: target.message } : {}),
2103
+ }, target.status);
2104
+ }
2105
+ const forwardQuery = sanitizeForwardQueryParams(new URL(c.req.url).searchParams);
2106
+ const result = await fetchDownstreamJson({
2107
+ c,
2108
+ instance: target.instance,
2109
+ path: `/events/${target.localEventId}/payload`,
2110
+ query: forwardQuery,
2111
+ schema: ConsoleRequestPayloadSchema,
2112
+ fetchImpl,
2113
+ });
2114
+ if (!result.ok) {
2115
+ if (result.failure.status === 404) {
2116
+ return c.json({ error: 'NOT_FOUND' }, 404);
2117
+ }
2118
+ return c.json({
2119
+ error: 'DOWNSTREAM_UNAVAILABLE',
2120
+ failedInstances: [result.failure],
2121
+ }, 502);
2519
2122
  }
2520
2123
  return c.json({
2521
- error: 'DOWNSTREAM_UNAVAILABLE',
2522
- failedInstances: [result.failure],
2523
- }, 502);
2524
- }
2525
- return c.json({
2526
- ...result.data,
2527
- instanceId: target.instance.instanceId,
2528
- federatedEventId: `${target.instance.instanceId}:${target.localEventId}`,
2529
- localEventId: target.localEventId,
2124
+ ...result.data,
2125
+ instanceId: target.instance.instanceId,
2126
+ federatedEventId: `${target.instance.instanceId}:${target.localEventId}`,
2127
+ localEventId: target.localEventId,
2128
+ });
2530
2129
  });
2531
2130
  });
2532
2131
  return routes;